Fundation

This commit is contained in:
Joseph Garrone 2024-01-30 00:06:17 +01:00
parent b6d2f9f691
commit 5b350274bd
22 changed files with 882 additions and 135 deletions

View File

@ -1,8 +1,11 @@
export const nameOfTheGlobal = "kcContext";
export const keycloak_resources = "keycloak-resources";
export const resources_common = "resources-common";
export const lastKeycloakVersionWithAccountV1 = "21.1.2";
export const keycloakifyViteConfigJsonBasename = ".keycloakifyViteConfig.json";
export const basenameOfTheKeycloakifyResourcesDir = "build";
export const themeTypes = ["login", "account"] as const;
export const accountV1 = "account-v1";
export const accountV1ThemeName = "account-v1";
export type ThemeType = (typeof themeTypes)[number];

View File

@ -24,6 +24,8 @@ export type BuildOptions = {
/** If your app is hosted under a subpath, it's the case in CRA if you have "homepage": "https://example.com/my-app" in your package.json
* In this case the urlPathname will be "/my-app/" */
urlPathname: string | undefined;
assetsDirPath: string;
bundler: "vite" | "webpack";
};
export function readBuildOptions(params: { reactAppRootDirPath: string; processArgv: string[] }): BuildOptions {

View File

@ -1 +0,0 @@
export const ftlValuesGlobalName = "kcContext";

View File

@ -5,10 +5,9 @@ import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInli
import * as fs from "fs";
import { join as pathJoin } from "path";
import { objectKeys } from "tsafe/objectKeys";
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import type { ThemeType } from "../../constants";
import { type ThemeType, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../constants";
export type BuildOptionsLike = {
themeVersion: string;
@ -20,7 +19,6 @@ assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function generateFtlFilesCodeFactory(params: {
themeName: string;
indexHtmlCode: string;
//NOTE: Expected to be an empty object if external assets mode is enabled.
cssGlobalsToDefine: Record<string, string>;
buildOptions: BuildOptionsLike;
keycloakifyVersion: string;
@ -70,7 +68,10 @@ export function generateFtlFilesCodeFactory(params: {
$(element).attr(
attrName,
href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
href.replace(
new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`),
`\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/`
)
);
})
);
@ -114,7 +115,7 @@ export function generateFtlFilesCodeFactory(params: {
$("head").prepend(
[
"<script>",
` window.${ftlValuesGlobalName}= ${objectKeys(replaceValueBySearchValue)[0]};`,
` window.${nameOfTheGlobal}= ${objectKeys(replaceValueBySearchValue)[0]};`,
"</script>",
"",
objectKeys(replaceValueBySearchValue)[1]

View File

@ -3,7 +3,7 @@ import { join as pathJoin, dirname as pathDirname } from "path";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "../BuildOptions";
import { resources_common, lastKeycloakVersionWithAccountV1, accountV1 } from "../../constants";
import { resources_common, lastKeycloakVersionWithAccountV1, accountV1ThemeName } from "../../constants";
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
import { transformCodebase } from "../../tools/transformCodebase";
@ -29,7 +29,7 @@ export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike
buildOptions
});
const accountV1DirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", accountV1, "account");
const accountV1DirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", accountV1ThemeName, "account");
transformCodebase({
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "account"),

View File

@ -3,7 +3,7 @@ import { join as pathJoin, dirname as pathDirname } from "path";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "../BuildOptions";
import { type ThemeType, accountV1 } from "../../constants";
import { type ThemeType, accountV1ThemeName } from "../../constants";
import { bringInAccountV1 } from "./bringInAccountV1";
export type BuildOptionsLike = {
@ -102,7 +102,7 @@ export async function generateJavaStackFiles(params: {
? []
: [
{
"name": accountV1,
"name": accountV1ThemeName,
"types": ["account"]
}
]),

View File

@ -4,7 +4,14 @@ import { join as pathJoin, resolve as pathResolve } from "path";
import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds } from "../generateFtl";
import { themeTypes, type ThemeType, lastKeycloakVersionWithAccountV1, keycloak_resources, accountV1 } from "../../constants";
import {
themeTypes,
type ThemeType,
lastKeycloakVersionWithAccountV1,
keycloak_resources,
accountV1ThemeName,
basenameOfTheKeycloakifyResourcesDir
} from "../../constants";
import { isInside } from "../../tools/isInside";
import type { BuildOptions } from "../BuildOptions";
import { assert, type Equals } from "tsafe/assert";
@ -18,7 +25,6 @@ export type BuildOptionsLike = {
extraThemeProperties: string[] | undefined;
themeVersion: string;
loginThemeResourcesFromKeycloakVersion: string;
urlPathname: string | undefined;
keycloakifyBuildDirPath: string;
reactAppBuildDirPath: string;
cacheDirPath: string;
@ -59,7 +65,7 @@ export async function generateTheme(params: {
}
transformCodebase({
"destDirPath": pathJoin(themeTypeDirPath, "resources", "build"),
"destDirPath": pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir),
"srcDirPath": buildOptions.reactAppBuildDirPath,
"transformSourceCode": ({ filePath, sourceCode }) => {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
@ -182,7 +188,7 @@ export async function generateTheme(params: {
`parent=${(() => {
switch (themeType) {
case "account":
return accountV1;
return accountV1ThemeName;
case "login":
return "keycloak";
}

View File

@ -0,0 +1,79 @@
import * as fs from "fs";
import { assert } from "tsafe";
import type { Equals } from "tsafe";
import { z } from "zod";
import { pathJoin } from "../tools/pathJoin";
import { keycloakifyViteConfigJsonBasename } from "../constants";
import type { OptionalIfCanBeUndefined } from "../tools/OptionalIfCanBeUndefined";
export type ParsedKeycloakifyViteConfig = {
reactAppRootDirPath: string;
publicDirPath: string;
assetsDirPath: string;
reactAppBuildDirPath: string;
urlPathname: string | undefined;
};
export const zParsedKeycloakifyViteConfig = z.object({
"reactAppRootDirPath": z.string(),
"publicDirPath": z.string(),
"assetsDirPath": z.string(),
"reactAppBuildDirPath": z.string(),
"urlPathname": z.string().optional()
});
{
type Got = ReturnType<(typeof zParsedKeycloakifyViteConfig)["parse"]>;
type Expected = OptionalIfCanBeUndefined<ParsedKeycloakifyViteConfig>;
assert<Equals<Got, Expected>>();
}
let cache: { parsedKeycloakifyViteConfig: ParsedKeycloakifyViteConfig | undefined } | undefined = undefined;
export function getParsedKeycloakifyViteConfig(params: { keycloakifyBuildDirPath: string }): ParsedKeycloakifyViteConfig | undefined {
const { keycloakifyBuildDirPath } = params;
if (cache !== undefined) {
return cache.parsedKeycloakifyViteConfig;
}
const parsedKeycloakifyViteConfig = (() => {
const keycloakifyViteConfigJsonFilePath = pathJoin(keycloakifyBuildDirPath, keycloakifyViteConfigJsonBasename);
if (!fs.existsSync(keycloakifyViteConfigJsonFilePath)) {
return undefined;
}
let out: ParsedKeycloakifyViteConfig;
try {
out = JSON.parse(fs.readFileSync(keycloakifyViteConfigJsonFilePath).toString("utf8"));
} catch {
throw new Error("The output of the Keycloakify Vite plugin is not a valid JSON.");
}
try {
const zodParseReturn = zParsedKeycloakifyViteConfig.parse(out);
// So that objectKeys from tsafe return the expected result no matter what.
Object.keys(zodParseReturn)
.filter(key => !(key in out))
.forEach(key => {
delete (out as any)[key];
});
} catch {
throw new Error("The output of the Keycloakify Vite plugin do not match the expected schema.");
}
return out;
})();
if (parsedKeycloakifyViteConfig === undefined && fs.existsSync(pathJoin(keycloakifyBuildDirPath, "vite.config.ts"))) {
throw new Error("Make sure you have enabled the Keycloakiy plugin in your vite.config.ts");
}
cache = { parsedKeycloakifyViteConfig };
return parsedKeycloakifyViteConfig;
}

View File

@ -10,7 +10,6 @@ export type ParsedPackageJson = {
homepage?: string;
keycloakify?: {
extraThemeProperties?: string[];
areAppAndKeycloakServerSharingSameDomain?: boolean;
artifactId?: string;
groupId?: string;
doCreateJar?: boolean;
@ -18,7 +17,6 @@ export type ParsedPackageJson = {
reactAppBuildDirPath?: string;
keycloakifyBuildDirPath?: string;
themeName?: string | string[];
doBuildRetrocompatAccountTheme?: boolean;
};
};
@ -29,22 +27,20 @@ export const zParsedPackageJson = z.object({
"keycloakify": z
.object({
"extraThemeProperties": z.array(z.string()).optional(),
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(),
"artifactId": z.string().optional(),
"groupId": z.string().optional(),
"doCreateJar": z.boolean().optional(),
"loginThemeResourcesFromKeycloakVersion": z.string().optional(),
"reactAppBuildDirPath": z.string().optional(),
"keycloakifyBuildDirPath": z.string().optional(),
"themeName": z.union([z.string(), z.array(z.string())]).optional(),
"doBuildRetrocompatAccountTheme": z.boolean().optional()
"themeName": z.union([z.string(), z.array(z.string())]).optional()
})
.optional()
});
assert<Equals<ReturnType<(typeof zParsedPackageJson)["parse"]>, ParsedPackageJson>>();
let parsedPackageJson: undefined | ReturnType<(typeof zParsedPackageJson)["parse"]>;
let parsedPackageJson: undefined | ParsedPackageJson;
export function getParsedPackageJson(params: { reactAppRootDirPath: string }) {
const { reactAppRootDirPath } = params;
if (parsedPackageJson) {

View File

@ -1,65 +0,0 @@
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
export function replaceImportsFromStaticInJsCode(params: { jsCode: string; bundler: "vite" | "webpack" }): { fixedJsCode: string } {
const { jsCode } = params;
const { fixedJsCode } = (() => {
switch (params.bundler) {
case "vite":
return replaceImportsFromStaticInJsCode_vite({ jsCode });
case "webpack":
return replaceImportsFromStaticInJsCode_webpack({ jsCode });
}
})();
return { fixedJsCode };
}
export function replaceImportsFromStaticInJsCode_vite(params: { jsCode: string }): { fixedJsCode: string } {
const { jsCode } = params;
const fixedJsCode = jsCode.replace(
/\.viteFileDeps = \[(.*)\]/g,
(...args) => `.viteFileDeps = [${args[1]}].map(viteFileDep => window.kcContext.url.resourcesPath.substring(1) + "/build/" + viteFileDep)`
);
return { fixedJsCode };
}
export function replaceImportsFromStaticInJsCode_webpack(params: { jsCode: string }): { fixedJsCode: string } {
const { jsCode } = params;
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=(function\\(([a-z]+)\\){return|([a-z]+)=>)"static\\/${language}\\/"`, "g"),
(...[, n, u, matchedFunction, eForFunction]) => {
const isArrowFunction = matchedFunction.includes("=>");
const e = isArrowFunction ? matchedFunction.replace("=>", "").trim() : eForFunction;
return `
${n}[(function(){
var pd = Object.getOwnPropertyDescriptor(${n}, "p");
if( pd === undefined || pd.configurable ){
Object.defineProperty(${n}, "p", {
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
set: function() {}
});
}
return "${u}";
})()] = ${isArrowFunction ? `${e} =>` : `function(${e}) { return `} "/build/static/${language}/"`
.replace(/\s+/g, " ")
.trim();
}
];
const fixedJsCode = jsCode
.replace(...getReplaceArgs("js"))
.replace(...getReplaceArgs("css"))
.replace(/[a-zA-Z]+\.[a-zA-Z]+\+"static\//g, `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`)
//TODO: Write a test case for this
.replace(
/".chunk.css",([a-zA-Z])+=[a-zA-Z]+\.[a-zA-Z]+\+([a-zA-Z]+),/,
(...[, group1, group2]) => `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group2},`
);
return { fixedJsCode };
}

View File

@ -1,6 +1,7 @@
import * as crypto from "crypto";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { basenameOfTheKeycloakifyResourcesDir } from "../../constants";
export type BuildOptionsLike = {
urlPathname: string | undefined;
@ -45,7 +46,7 @@ export function generateCssCodeToDefineGlobals(params: { cssGlobalsToDefine: Rec
`--${cssVariableName}:`,
cssGlobalsToDefine[cssVariableName].replace(
new RegExp(`url\\(${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`, "g"),
"url(${url.resourcesPath}/build/"
`url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/`
)
].join(" ")
)

View File

@ -1,5 +1,6 @@
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { basenameOfTheKeycloakifyResourcesDir } from "../../constants";
export type BuildOptionsLike = {
urlPathname: string | undefined;
@ -16,7 +17,7 @@ export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOp
buildOptions.urlPathname === undefined
? /url\(["']?\/([^/][^)"']+)["']?\)/g
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
(...[, group]) => `url(\${url.resourcesPath}/build/${group})`
(...[, group]) => `url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/${group})`
);
return { fixedCssCode };

View File

@ -0,0 +1 @@
export * from "./replaceImportsInJsCode";

View File

@ -0,0 +1,85 @@
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../../constants";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "../../BuildOptions";
import * as nodePath from "path";
import { replaceAll } from "../../../tools/String.prototype.replaceAll";
export type BuildOptionsLike = {
reactAppBuildDirPath: string;
assetsDirPath: string;
urlPathname: string | undefined;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function replaceImportsInJsCode_vite(params: {
jsCode: string;
buildOptions: BuildOptionsLike;
basenameOfAssetsFiles: string[];
systemType?: "posix" | "win32";
}): {
fixedJsCode: string;
} {
const { jsCode, buildOptions, basenameOfAssetsFiles, systemType = nodePath.sep === "/" ? "posix" : "win32" } = params;
const { relative: pathRelative, sep: pathSep } = nodePath[systemType];
let fixedJsCode = jsCode;
replace_base_javacript_import: {
if (buildOptions.urlPathname === undefined) {
break replace_base_javacript_import;
}
// Optimization
if (!jsCode.includes(buildOptions.urlPathname)) {
break replace_base_javacript_import;
}
// Replace `Hv=function(e){return"/abcde12345/"+e}` by `Hv=function(e){return"/"+e}`
fixedJsCode = fixedJsCode.replace(
new RegExp(
`([\\w\\$][\\w\\d\\$]*)=function\\(([\\w\\$][\\w\\d\\$]*)\\)\\{return"${replaceAll(buildOptions.urlPathname, "/", "\\/")}"\\+\\2\\}`,
"g"
),
(...[, funcName, paramName]) => `${funcName}=function(${paramName}){return"/"+${paramName}}`
);
}
replace_javascript_relatives_import_paths: {
// Example: "assets/ or "foo/bar/"
const staticDir = (() => {
let out = pathRelative(buildOptions.reactAppBuildDirPath, buildOptions.assetsDirPath);
out = replaceAll(out, pathSep, "/") + "/";
if (out === "/") {
throw new Error(`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`);
}
return out;
})();
// Optimization
if (!jsCode.includes(staticDir)) {
break replace_javascript_relatives_import_paths;
}
basenameOfAssetsFiles
.map(basenameOfAssetsFile => `${staticDir}${basenameOfAssetsFile}`)
.forEach(relativePathOfAssetFile => {
fixedJsCode = replaceAll(
fixedJsCode,
`"${relativePathOfAssetFile}"`,
`(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")`
);
fixedJsCode = replaceAll(
fixedJsCode,
`"${buildOptions.urlPathname ?? "/"}${relativePathOfAssetFile}"`,
`(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")`
);
});
}
return { fixedJsCode };
}

View File

@ -0,0 +1,94 @@
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../../constants";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "../../BuildOptions";
import { relative as pathRelative, sep as pathSep } from "path";
import { replaceAll } from "../../../tools/String.prototype.replaceAll";
export type BuildOptionsLike = {
reactAppBuildDirPath: string;
assetsDirPath: string;
urlPathname: string | undefined;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function replaceImportsInJsCode_webpack(params: { jsCode: string; buildOptions: BuildOptionsLike }): { fixedJsCode: string } {
const { jsCode, buildOptions } = params;
let fixedJsCode = jsCode;
// "__esModule",{value:!0})},n.p="/",function(){if("undefined" -> n.p="/abcde12345/"
// d={NODE_ENV:"production",PUBLIC_URL:"/abcde12345",WDS_SOCKET_HOST
// d={NODE_ENV:"production",PUBLIC_URL:"",WDS_SOCKET_HOST
// ->
// PUBLIC_URL:"${window.${nameOfTheGlobal}.url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}"`
if (buildOptions.urlPathname !== undefined) {
fixedJsCode = fixedJsCode.replace(
new RegExp(`,([a-zA-Z]\\.[a-zA-Z])="${replaceAll(buildOptions.urlPathname, "/", "\\/")}",`, "g"),
(...[, assignTo]) => `,${assignTo}="/",`
);
}
fixedJsCode = fixedJsCode.replace(
new RegExp(
`NODE_ENV:"production",PUBLIC_URL:"${
buildOptions.urlPathname !== undefined ? replaceAll(buildOptions.urlPathname.slice(0, -1), "/", "\\/") : ""
}",`,
"g"
),
`NODE_ENV:"production",PUBLIC_URL: window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}",`
);
// Example: "static/ or "foo/bar/"
const staticDir = (() => {
let out = pathRelative(buildOptions.reactAppBuildDirPath, buildOptions.assetsDirPath);
out = replaceAll(out, pathSep, "/") + "/";
if (out === "/") {
throw new Error(`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`);
}
return out;
})();
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=(function\\(([a-z]+)\\){return|([a-z]+)=>)"${staticDir.replace(/\//g, "\\/")}${language}\\/"`, "g"),
(...[, n, u, matchedFunction, eForFunction]) => {
const isArrowFunction = matchedFunction.includes("=>");
const e = isArrowFunction ? matchedFunction.replace("=>", "").trim() : eForFunction;
return `
${n}[(function(){
var pd = Object.getOwnPropertyDescriptor(${n}, "p");
if( pd === undefined || pd.configurable ){
Object.defineProperty(${n}, "p", {
get: function() { return window.${nameOfTheGlobal}.url.resourcesPath; },
set: function() {}
});
}
return "${u}";
})()] = ${isArrowFunction ? `${e} =>` : `function(${e}) { return `} "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}${language}/"`
.replace(/\s+/g, " ")
.trim();
}
];
fixedJsCode = fixedJsCode
.replace(...getReplaceArgs("js"))
.replace(...getReplaceArgs("css"))
.replace(
new RegExp(`[a-zA-Z]+\\.[a-zA-Z]+\\+"${staticDir.replace(/\//g, "\\/")}"`, "g"),
`window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}`
)
//TODO: Write a test case for this
.replace(
/".chunk.css",([a-zA-Z])+=[a-zA-Z]+\.[a-zA-Z]+\+([a-zA-Z]+),/,
(...[, group1, group2]) =>
`".chunk.css",${group1} = window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/" + ${group2},`
);
return { fixedJsCode };
}

View File

@ -0,0 +1,12 @@
type PropertiesThatCanBeUndefined<T extends Record<string, unknown>> = {
[Key in keyof T]: undefined extends T[Key] ? Key : never;
}[keyof T];
/**
* OptionalIfCanBeUndefined<{ p1: string | undefined; p2: string; }>
* is
* { p1?: string | undefined; p2: string }
*/
export type OptionalIfCanBeUndefined<T extends Record<string, unknown>> = {
[K in PropertiesThatCanBeUndefined<T>]?: T[K];
} & { [K in Exclude<keyof T, PropertiesThatCanBeUndefined<T>>]: T[K] };

View File

@ -0,0 +1,30 @@
export function replaceAll(string: string, searchValue: string | RegExp, replaceValue: string): string {
if ((string as any).replaceAll !== undefined) {
return (string as any).replaceAll(searchValue, replaceValue);
}
// If the searchValue is a string
if (typeof searchValue === "string") {
// Escape special characters in the string to be used in a regex
var escapedSearchValue = searchValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
var regex = new RegExp(escapedSearchValue, "g");
return string.replace(regex, replaceValue);
}
// If the searchValue is a global RegExp, use it directly
if (searchValue instanceof RegExp && searchValue.global) {
return string.replace(searchValue, replaceValue);
}
// If the searchValue is a non-global RegExp, throw an error
if (searchValue instanceof RegExp) {
throw new TypeError("replaceAll must be called with a global RegExp");
}
// Convert searchValue to string if it's not a string or RegExp
var searchString = String(searchValue);
var regexFromString = new RegExp(searchString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
return string.replace(regexFromString, replaceValue);
}

232
src/vite-plugin/config.json Normal file
View File

@ -0,0 +1,232 @@
{
"plugins": [
{
"name": "vite:build-metadata"
},
{
"name": "vite:watch-package-data"
},
{
"name": "vite:pre-alias"
},
{
"name": "alias"
},
{
"name": "vite:react-babel",
"enforce": "pre"
},
{
"name": "vite:react-refresh",
"enforce": "pre"
},
{
"name": "vite:modulepreload-polyfill"
},
{
"name": "vite:resolve"
},
{
"name": "vite:html-inline-proxy"
},
{
"name": "vite:css"
},
{
"name": "vite:esbuild"
},
{
"name": "vite:json"
},
{
"name": "vite:wasm-helper"
},
{
"name": "vite:worker"
},
{
"name": "vite:asset"
},
{
"name": "vite-plugin-commonjs"
},
{
"name": "keycloakify"
},
{
"name": "vite:wasm-fallback"
},
{
"name": "vite:define"
},
{
"name": "vite:css-post"
},
{
"name": "vite:build-html"
},
{
"name": "vite:worker-import-meta-url"
},
{
"name": "vite:asset-import-meta-url"
},
{
"name": "vite:force-systemjs-wrap-complete"
},
{
"name": "commonjs",
"version": "25.0.7"
},
{
"name": "vite:data-uri"
},
{
"name": "vite:dynamic-import-vars"
},
{
"name": "vite:import-glob"
},
{
"name": "vite:build-import-analysis"
},
{
"name": "vite:esbuild-transpile"
},
{
"name": "vite:terser"
},
{
"name": "vite:reporter"
},
{
"name": "vite:load-fallback"
}
],
"optimizeDeps": {
"disabled": "build",
"esbuildOptions": {
"preserveSymlinks": false,
"jsx": "automatic",
"plugins": [
{
"name": "vite-plugin-commonjs:pre-bundle"
}
]
},
"include": ["react", "react/jsx-dev-runtime", "react/jsx-runtime"]
},
"build": {
"target": ["es2020", "edge88", "firefox78", "chrome87", "safari14"],
"cssTarget": ["es2020", "edge88", "firefox78", "chrome87", "safari14"],
"outDir": "dist",
"assetsDir": "assets",
"assetsInlineLimit": 4096,
"cssCodeSplit": true,
"sourcemap": false,
"rollupOptions": {},
"minify": "esbuild",
"terserOptions": {},
"write": true,
"emptyOutDir": null,
"copyPublicDir": true,
"manifest": false,
"lib": false,
"ssr": false,
"ssrManifest": false,
"ssrEmitAssets": false,
"reportCompressedSize": true,
"chunkSizeWarningLimit": 500,
"watch": null,
"commonjsOptions": {
"include": [{}],
"extensions": [".js", ".cjs"]
},
"dynamicImportVarsOptions": {
"warnOnError": true,
"exclude": [{}]
},
"modulePreload": {
"polyfill": true
},
"cssMinify": true
},
"esbuild": {
"jsxDev": false,
"jsx": "automatic"
},
"resolve": {
"mainFields": ["browser", "module", "jsnext:main", "jsnext"],
"conditions": [],
"extensions": [".mjs", ".js", ".mts", ".ts", ".jsx", ".tsx", ".json"],
"dedupe": ["react", "react-dom"],
"preserveSymlinks": false,
"alias": [
{
"find": {},
"replacement": "/@fs/Users/joseph/github/keycloakify-starter/node_modules/vite/dist/client/env.mjs"
},
{
"find": {},
"replacement": "/@fs/Users/joseph/github/keycloakify-starter/node_modules/vite/dist/client/client.mjs"
}
]
},
"configFile": "/Users/joseph/github/keycloakify-starter/vite.config.ts",
"configFileDependencies": ["/Users/joseph/github/keycloakify-starter/vite.config.ts"],
"inlineConfig": {
"optimizeDeps": {},
"build": {}
},
"root": "/Users/joseph/github/keycloakify-starter",
"base": "/",
"rawBase": "/",
"publicDir": "/Users/joseph/github/keycloakify-starter/public",
"cacheDir": "/Users/joseph/github/keycloakify-starter/node_modules/.vite",
"command": "build",
"mode": "production",
"ssr": {
"target": "node",
"optimizeDeps": {
"disabled": true,
"esbuildOptions": {
"preserveSymlinks": false
}
}
},
"isWorker": false,
"mainConfig": null,
"isProduction": true,
"css": {},
"server": {
"preTransformRequests": true,
"middlewareMode": false,
"fs": {
"strict": true,
"allow": ["/Users/joseph/github/keycloakify-starter"],
"deny": [".env", ".env.*", "*.{crt,pem}"],
"cachedChecks": false
}
},
"preview": {},
"envDir": "/Users/joseph/github/keycloakify-starter",
"env": {
"BASE_URL": "/",
"MODE": "production",
"DEV": false,
"PROD": true
},
"logger": {
"hasWarned": false
},
"packageCache": {},
"worker": {
"format": "iife",
"rollupOptions": {}
},
"appType": "spa",
"experimental": {
"importGlobRestoreExtension": false,
"hmrPartialAccept": false
}
}

View File

@ -2,11 +2,16 @@
"extends": "../../tsproject.json",
"compilerOptions": {
"module": "CommonJS",
"target": "ES5",
"target": "ES2019",
"esModuleInterop": true,
"lib": ["es2015", "ES2019.Object"],
"lib": ["es2019", "es2020.bigint", "es2020.string", "es2020.symbol.wellknown"],
"outDir": "../../dist/vite-plugin",
"rootDir": ".",
"skipLibCheck": true
}
},
"references": [
{
"path": "../bin"
}
]
}

View File

@ -1,31 +1,127 @@
// index.ts
import type { Plugin, ResolvedConfig } from "vite";
import { join as pathJoin, sep as pathSep } from "path";
import { getParsedPackageJson } from "../bin/keycloakify/parsedPackageJson";
import type { Plugin } from "vite";
import { assert } from "tsafe/assert";
import { getAbsoluteAndInOsFormatPath } from "../bin/tools/getAbsoluteAndInOsFormatPath";
import * as fs from "fs";
console.log("Hello world!");
import { keycloakifyViteConfigJsonBasename, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../bin/constants";
import type { ParsedKeycloakifyViteConfig } from "../bin/keycloakify/parsedKeycloakifyViteConfig";
import { replaceAll } from "../bin/tools/String.prototype.replaceAll";
export function keycloakify(): Plugin {
let config: ResolvedConfig;
let keycloakifyViteConfig: ParsedKeycloakifyViteConfig | undefined = undefined;
return {
"name": "keycloakify",
"configResolved": resolvedConfig => {
// Store the resolved config
config = resolvedConfig;
const reactAppRootDirPath = resolvedConfig.root;
const reactAppBuildDirPath = pathJoin(reactAppRootDirPath, resolvedConfig.build.outDir);
console.log("========> configResolved", config);
keycloakifyViteConfig = {
reactAppRootDirPath,
"publicDirPath": resolvedConfig.publicDir,
"assetsDirPath": pathJoin(reactAppBuildDirPath, resolvedConfig.build.assetsDir),
reactAppBuildDirPath,
"urlPathname": (() => {
let out = resolvedConfig.env.BASE_URL;
fs.writeFileSync("/Users/joseph/github/keycloakify-starter/log.txt", Buffer.from("Hello World", "utf8"));
if (out === undefined) {
return undefined;
}
if (!out.startsWith("/")) {
out = "/" + out;
}
if (!out.endsWith("/")) {
out += "/";
}
return out;
})()
};
const parsedPackageJson = getParsedPackageJson({ reactAppRootDirPath });
if (parsedPackageJson.keycloakify?.reactAppBuildDirPath !== undefined) {
throw new Error(
[
"Please do not use the keycloakify.reactAppBuildDirPath option in your package.json.",
"In Vite setups it's inferred automatically from the vite config."
].join(" ")
);
}
const keycloakifyBuildDirPath = (() => {
const { keycloakifyBuildDirPath } = parsedPackageJson.keycloakify ?? {};
if (keycloakifyBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": keycloakifyBuildDirPath,
"cwd": reactAppRootDirPath
});
}
return pathJoin(reactAppRootDirPath, "build_keycloak");
})();
if (!fs.existsSync(keycloakifyBuildDirPath)) {
fs.mkdirSync(keycloakifyBuildDirPath);
}
fs.writeFileSync(
pathJoin(keycloakifyBuildDirPath, keycloakifyViteConfigJsonBasename),
Buffer.from(JSON.stringify(keycloakifyViteConfig, null, 2), "utf8")
);
},
"transform": (code, id) => {
assert(keycloakifyViteConfig !== undefined);
"buildStart": () => {
console.log("Public Directory:", config.publicDir); // Path to the public directory
console.log("Dist Directory:", config.build.outDir); // Path to the dist directory
console.log("Assets Directory:", config.build.assetsDir); // Path to the assets directory within outDir
let transformedCode: string | undefined = undefined;
replace_import_meta_env_base_url_in_source_code: {
{
const isWithinSourceDirectory = id.startsWith(pathJoin(keycloakifyViteConfig.publicDirPath, "src") + pathSep);
if (!isWithinSourceDirectory) {
break replace_import_meta_env_base_url_in_source_code;
}
}
const isJavascriptFile = id.endsWith(".js") || id.endsWith(".jsx");
{
const isTypeScriptFile = id.endsWith(".ts") || id.endsWith(".tsx");
if (!isTypeScriptFile && !isJavascriptFile) {
break replace_import_meta_env_base_url_in_source_code;
}
}
const windowToken = isJavascriptFile ? "window" : "(window as any)";
if (transformedCode === undefined) {
transformedCode = code;
}
transformedCode = replaceAll(
transformedCode,
"import.meta.env.BASE_URL",
[
`(`,
`(${windowToken}.${nameOfTheGlobal} === undefined || import.meta.env.MODE === "development") ?`,
` "${keycloakifyViteConfig.urlPathname ?? "/"}" :`,
` \`\${${windowToken}.${nameOfTheGlobal}.url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/\``,
`)`
].join("")
);
}
if (transformedCode !== undefined) {
return {
"code": transformedCode
};
}
}
// ... other hooks
};
}

View File

@ -1,15 +1,56 @@
import { replaceImportsFromStaticInJsCode } from "keycloakify/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode";
import { replaceImportsInJsCode_vite } from "keycloakify/bin/keycloakify/replacers/replaceImportsInJsCode/vite";
import { generateCssCodeToDefineGlobals, replaceImportsInCssCode } from "keycloakify/bin/keycloakify/replacers/replaceImportsInCssCode";
import { replaceImportsInInlineCssCode } from "keycloakify/bin/keycloakify/replacers/replaceImportsInInlineCssCode";
import { same } from "evt/tools/inDepth/same";
import { expect, it, describe } from "vitest";
import { isSameCode } from "../tools/isSameCode";
import { basenameOfTheKeycloakifyResourcesDir, nameOfTheGlobal } from "keycloakify/bin/constants";
describe("bin/js-transforms", () => {
// Vite
{
describe("bin/js-transforms - vite", () => {
it("replaceImportsInJsCode_vite - 1", () => {
const before = `Uv="modulepreload",`;
const after = `,Wc={},`;
const jsCodeUntransformed = `${before}Hv=function(e){return"/foo-bar-baz/"+e}${after}`;
const { fixedJsCode } = replaceImportsInJsCode_vite({
"jsCode": jsCodeUntransformed,
"basenameOfAssetsFiles": [],
"buildOptions": {
"reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/dist/",
"assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/assets/",
"urlPathname": "/foo-bar-baz/"
}
});
const fixedJsCodeExpected = `${before}Hv=function(e){return"/"+e}${after}`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
});
it("replaceImportsInJsCode_vite - 2", () => {
const before = `Uv="modulepreload",`;
const after = `,Wc={},`;
const jsCodeUntransformed = `${before}Hv=function(e){return"/foo/bar/baz/"+e}${after}`;
const { fixedJsCode } = replaceImportsInJsCode_vite({
"jsCode": jsCodeUntransformed,
"basenameOfAssetsFiles": [],
"buildOptions": {
"reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/dist/",
"assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/assets/",
"urlPathname": "/foo/bar/baz/"
}
});
const fixedJsCodeExpected = `${before}Hv=function(e){return"/"+e}${after}`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
});
it("replaceImportsInJsCode_vite - 3", () => {
const jsCodeUntransformed = `
S="/assets/keycloakify-logo-mqjydaoZ.png",H=(()=>{
function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = ["assets/Login-dJpPRzM4.js", "assets/index-XwzrZ5Gu.js"]
@ -17,28 +58,157 @@ describe("bin/js-transforms", () => {
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
}
`;
it("Correctly replace import path in Vite dist/static/xxx.js files", () => {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
for (const { reactAppBuildDirPath, assetsDirPath, systemType } of [
{
"systemType": "posix",
"reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/dist",
"assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/assets"
},
{
"systemType": "win32",
"reactAppBuildDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist",
"assetsDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist\\assets"
}
] as const) {
const { fixedJsCode } = replaceImportsInJsCode_vite({
"jsCode": jsCodeUntransformed,
"bundler": "vite"
"basenameOfAssetsFiles": ["Login-dJpPRzM4.js", "index-XwzrZ5Gu.js"],
"buildOptions": {
reactAppBuildDirPath,
assetsDirPath,
"urlPathname": undefined
},
systemType
});
const fixedJsCodeExpected = `
function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = ["assets/Login-dJpPRzM4.js", "assets/index-XwzrZ5Gu.js"].map(viteFileDep => window.kcContext.url.resourcesPath.substring(1) + "/build/" + viteFileDep)
S=(window.${nameOfTheGlobal}.url + "/${basenameOfTheKeycloakifyResourcesDir}/assets/keycloakify-logo-mqjydaoZ.png"),H=(()=>{
function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = [
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/Login-dJpPRzM4.js)",
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/index-XwzrZ5Gu.js)"
]
}
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
}
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
}
`;
`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
});
}
}
});
// Webpack
{
it("replaceImportsInJsCode_vite - 4", () => {
const jsCodeUntransformed = `
S="/assets/keycloakify-logo-mqjydaoZ.png",H=(()=>{
function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = ["assets/Login-dJpPRzM4.js", "assets/index-XwzrZ5Gu.js"]
}
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
}
`;
for (const { reactAppBuildDirPath, assetsDirPath, systemType } of [
{
"systemType": "posix",
"reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/dist",
"assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/foo/bar"
},
{
"systemType": "win32",
"reactAppBuildDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist",
"assetsDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist\\foo\\bar"
}
] as const) {
const { fixedJsCode } = replaceImportsInJsCode_vite({
"jsCode": jsCodeUntransformed,
"basenameOfAssetsFiles": ["Login-dJpPRzM4.js", "index-XwzrZ5Gu.js"],
"buildOptions": {
reactAppBuildDirPath,
assetsDirPath,
"urlPathname": undefined
},
systemType
});
const fixedJsCodeExpected = `
S=(window.${nameOfTheGlobal}.url + "/${basenameOfTheKeycloakifyResourcesDir}/foo/bar/keycloakify-logo-mqjydaoZ.png"),H=(()=>{
function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = [
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/foo/bar/Login-dJpPRzM4.js)",
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/foo/bar/index-XwzrZ5Gu.js)"
]
}
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
}
`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
}
});
it("replaceImportsInJsCode_vite - 5", () => {
const jsCodeUntransformed = `
S="/foo-bar-baz/assets/keycloakify-logo-mqjydaoZ.png",H=(()=>{
function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = ["assets/Login-dJpPRzM4.js", "assets/index-XwzrZ5Gu.js"]
}
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
}
`;
for (const { reactAppBuildDirPath, assetsDirPath, systemType } of [
{
"systemType": "posix",
"reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/dist",
"assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/assets"
},
{
"systemType": "win32",
"reactAppBuildDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist",
"assetsDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist\\assets"
}
] as const) {
const { fixedJsCode } = replaceImportsInJsCode_vite({
"jsCode": jsCodeUntransformed,
"basenameOfAssetsFiles": ["Login-dJpPRzM4.js", "index-XwzrZ5Gu.js"],
"buildOptions": {
reactAppBuildDirPath,
assetsDirPath,
"urlPathname": "/foo-bar-baz/"
},
systemType
});
const fixedJsCodeExpected = `
S=(window.${nameOfTheGlobal}.url + "/${basenameOfTheKeycloakifyResourcesDir}/assets/keycloakify-logo-mqjydaoZ.png"),H=(()=>{
function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = [
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/Login-dJpPRzM4.js)",
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/index-XwzrZ5Gu.js)"
]
}
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
}
`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
}
});
});
describe("bin/js-transforms - webpack", () => {
const jsCodeUntransformed = `
function f() {
return a.p+"static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
@ -68,13 +238,13 @@ describe("bin/js-transforms", () => {
t.miniCssF=e=>"static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css"
`;
it("Correctly replace import path in Webpack build/static/js/xxx.js files", () => {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": jsCodeUntransformed,
"bundler": "webpack"
});
it("Correctly replace import path in Webpack build/static/js/xxx.js files", () => {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": jsCodeUntransformed,
"bundler": "webpack"
});
const fixedJsCodeExpected = `
const fixedJsCodeExpected = `
function f() {
return window.kcContext.url.resourcesPath + "/build/static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
@ -143,9 +313,8 @@ describe("bin/js-transforms", () => {
})()] = e => "/build/static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css"
`;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
});
}
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
});
});
describe("bin/css-transforms", () => {