From 5b350274bd4780e8e2f45578ee7f68ff170a3388 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Tue, 30 Jan 2024 00:06:17 +0100 Subject: [PATCH] Fundation --- src/bin/constants.ts | 5 +- src/bin/keycloakify/BuildOptions.ts | 2 + src/bin/keycloakify/ftlValuesGlobalName.ts | 1 - .../keycloakify/generateFtl/generateFtl.ts | 11 +- .../bringInAccountV1.ts | 4 +- .../generateJavaStackFiles.ts | 4 +- .../generateTheme/generateTheme.ts | 14 +- .../parsedKeycloakifyViteConfig.ts | 79 ++++++ src/bin/keycloakify/parsedPackageJson.ts | 8 +- .../replaceImportsFromStaticInJsCode.ts | 65 ----- .../replacers/replaceImportsInCssCode.ts | 3 +- .../replaceImportsInInlineCssCode.ts | 3 +- .../replacers/replaceImportsInJsCode/index.ts | 1 + .../replaceImportsInJsCode.ts | 0 .../replacers/replaceImportsInJsCode/vite.ts | 85 +++++++ .../replaceImportsInJsCode/webpack.ts | 94 +++++++ src/bin/tools/OptionalIfCanBeUndefined.ts | 12 + src/bin/tools/String.prototype.replaceAll.ts | 30 +++ src/vite-plugin/config.json | 232 ++++++++++++++++++ src/vite-plugin/tsconfig.json | 11 +- src/vite-plugin/vite-plugin.ts | 130 ++++++++-- test/bin/replaceImportFromStatic.spec.ts | 223 +++++++++++++++-- 22 files changed, 882 insertions(+), 135 deletions(-) create mode 100644 src/bin/keycloakify/parsedKeycloakifyViteConfig.ts delete mode 100644 src/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode.ts create mode 100644 src/bin/keycloakify/replacers/replaceImportsInJsCode/index.ts create mode 100644 src/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.ts create mode 100644 src/bin/keycloakify/replacers/replaceImportsInJsCode/vite.ts create mode 100644 src/bin/keycloakify/replacers/replaceImportsInJsCode/webpack.ts create mode 100644 src/bin/tools/OptionalIfCanBeUndefined.ts create mode 100644 src/bin/tools/String.prototype.replaceAll.ts create mode 100644 src/vite-plugin/config.json diff --git a/src/bin/constants.ts b/src/bin/constants.ts index 6cb76d97..91694c3f 100644 --- a/src/bin/constants.ts +++ b/src/bin/constants.ts @@ -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]; diff --git a/src/bin/keycloakify/BuildOptions.ts b/src/bin/keycloakify/BuildOptions.ts index 48962f12..e5f76d8f 100644 --- a/src/bin/keycloakify/BuildOptions.ts +++ b/src/bin/keycloakify/BuildOptions.ts @@ -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 { diff --git a/src/bin/keycloakify/ftlValuesGlobalName.ts b/src/bin/keycloakify/ftlValuesGlobalName.ts index eb63e562..e69de29b 100644 --- a/src/bin/keycloakify/ftlValuesGlobalName.ts +++ b/src/bin/keycloakify/ftlValuesGlobalName.ts @@ -1 +0,0 @@ -export const ftlValuesGlobalName = "kcContext"; diff --git a/src/bin/keycloakify/generateFtl/generateFtl.ts b/src/bin/keycloakify/generateFtl/generateFtl.ts index a20e0bc9..04392ed6 100644 --- a/src/bin/keycloakify/generateFtl/generateFtl.ts +++ b/src/bin/keycloakify/generateFtl/generateFtl.ts @@ -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(); export function generateFtlFilesCodeFactory(params: { themeName: string; indexHtmlCode: string; - //NOTE: Expected to be an empty object if external assets mode is enabled. cssGlobalsToDefine: Record; 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( [ "", "", objectKeys(replaceValueBySearchValue)[1] diff --git a/src/bin/keycloakify/generateJavaStackFiles/bringInAccountV1.ts b/src/bin/keycloakify/generateJavaStackFiles/bringInAccountV1.ts index 3ca94b27..4b8c26ea 100644 --- a/src/bin/keycloakify/generateJavaStackFiles/bringInAccountV1.ts +++ b/src/bin/keycloakify/generateJavaStackFiles/bringInAccountV1.ts @@ -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"), diff --git a/src/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.ts b/src/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.ts index 732c2007..a8d781f7 100644 --- a/src/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.ts +++ b/src/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.ts @@ -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"] } ]), diff --git a/src/bin/keycloakify/generateTheme/generateTheme.ts b/src/bin/keycloakify/generateTheme/generateTheme.ts index eacf1c8b..761d8aad 100644 --- a/src/bin/keycloakify/generateTheme/generateTheme.ts +++ b/src/bin/keycloakify/generateTheme/generateTheme.ts @@ -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"; } diff --git a/src/bin/keycloakify/parsedKeycloakifyViteConfig.ts b/src/bin/keycloakify/parsedKeycloakifyViteConfig.ts new file mode 100644 index 00000000..504fb92e --- /dev/null +++ b/src/bin/keycloakify/parsedKeycloakifyViteConfig.ts @@ -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; + + assert>(); +} + +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; +} diff --git a/src/bin/keycloakify/parsedPackageJson.ts b/src/bin/keycloakify/parsedPackageJson.ts index 43478f14..d649adc5 100644 --- a/src/bin/keycloakify/parsedPackageJson.ts +++ b/src/bin/keycloakify/parsedPackageJson.ts @@ -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, ParsedPackageJson>>(); -let parsedPackageJson: undefined | ReturnType<(typeof zParsedPackageJson)["parse"]>; +let parsedPackageJson: undefined | ParsedPackageJson; export function getParsedPackageJson(params: { reactAppRootDirPath: string }) { const { reactAppRootDirPath } = params; if (parsedPackageJson) { diff --git a/src/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode.ts b/src/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode.ts deleted file mode 100644 index 150ffff0..00000000 --- a/src/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode.ts +++ /dev/null @@ -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 => [ - 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 }; -} diff --git a/src/bin/keycloakify/replacers/replaceImportsInCssCode.ts b/src/bin/keycloakify/replacers/replaceImportsInCssCode.ts index de53e6f9..3cf192a4 100644 --- a/src/bin/keycloakify/replacers/replaceImportsInCssCode.ts +++ b/src/bin/keycloakify/replacers/replaceImportsInCssCode.ts @@ -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(" ") ) diff --git a/src/bin/keycloakify/replacers/replaceImportsInInlineCssCode.ts b/src/bin/keycloakify/replacers/replaceImportsInInlineCssCode.ts index 88b3e0e8..e8e980bd 100644 --- a/src/bin/keycloakify/replacers/replaceImportsInInlineCssCode.ts +++ b/src/bin/keycloakify/replacers/replaceImportsInInlineCssCode.ts @@ -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 }; diff --git a/src/bin/keycloakify/replacers/replaceImportsInJsCode/index.ts b/src/bin/keycloakify/replacers/replaceImportsInJsCode/index.ts new file mode 100644 index 00000000..93784124 --- /dev/null +++ b/src/bin/keycloakify/replacers/replaceImportsInJsCode/index.ts @@ -0,0 +1 @@ +export * from "./replaceImportsInJsCode"; diff --git a/src/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.ts b/src/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/bin/keycloakify/replacers/replaceImportsInJsCode/vite.ts b/src/bin/keycloakify/replacers/replaceImportsInJsCode/vite.ts new file mode 100644 index 00000000..4d53fc65 --- /dev/null +++ b/src/bin/keycloakify/replacers/replaceImportsInJsCode/vite.ts @@ -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(); + +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 }; +} diff --git a/src/bin/keycloakify/replacers/replaceImportsInJsCode/webpack.ts b/src/bin/keycloakify/replacers/replaceImportsInJsCode/webpack.ts new file mode 100644 index 00000000..5648cab7 --- /dev/null +++ b/src/bin/keycloakify/replacers/replaceImportsInJsCode/webpack.ts @@ -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(); + +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 => [ + 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 }; +} diff --git a/src/bin/tools/OptionalIfCanBeUndefined.ts b/src/bin/tools/OptionalIfCanBeUndefined.ts new file mode 100644 index 00000000..eef4d10a --- /dev/null +++ b/src/bin/tools/OptionalIfCanBeUndefined.ts @@ -0,0 +1,12 @@ +type PropertiesThatCanBeUndefined> = { + [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> = { + [K in PropertiesThatCanBeUndefined]?: T[K]; +} & { [K in Exclude>]: T[K] }; diff --git a/src/bin/tools/String.prototype.replaceAll.ts b/src/bin/tools/String.prototype.replaceAll.ts new file mode 100644 index 00000000..7fc1ebb8 --- /dev/null +++ b/src/bin/tools/String.prototype.replaceAll.ts @@ -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); +} diff --git a/src/vite-plugin/config.json b/src/vite-plugin/config.json new file mode 100644 index 00000000..3dad4b82 --- /dev/null +++ b/src/vite-plugin/config.json @@ -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 + } +} diff --git a/src/vite-plugin/tsconfig.json b/src/vite-plugin/tsconfig.json index 8023c2a4..cb7242ca 100644 --- a/src/vite-plugin/tsconfig.json +++ b/src/vite-plugin/tsconfig.json @@ -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" + } + ] } diff --git a/src/vite-plugin/vite-plugin.ts b/src/vite-plugin/vite-plugin.ts index 1d60b6a4..3ac1c990 100644 --- a/src/vite-plugin/vite-plugin.ts +++ b/src/vite-plugin/vite-plugin.ts @@ -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 }; } diff --git a/test/bin/replaceImportFromStatic.spec.ts b/test/bin/replaceImportFromStatic.spec.ts index 34d912a1..bfe9cc22 100644 --- a/test/bin/replaceImportFromStatic.spec.ts +++ b/test/bin/replaceImportFromStatic.spec.ts @@ -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", () => {