diff --git a/src/bin/keycloakify/buildJars/generatePom.ts b/src/bin/keycloakify/buildJars/generatePom.ts index 555382ab..9ee1df71 100644 --- a/src/bin/keycloakify/buildJars/generatePom.ts +++ b/src/bin/keycloakify/buildJars/generatePom.ts @@ -1,6 +1,6 @@ import { assert } from "tsafe/assert"; import { Reflect } from "tsafe/Reflect"; -import type { BuildOptions } from "./buildOptions"; +import type { BuildOptions } from "../buildOptions"; type BuildOptionsLike = { groupId: string; diff --git a/src/bin/keycloakify/generateTheme/bringInAccountV1.ts b/src/bin/keycloakify/generateTheme/bringInAccountV1.ts index 5fba86d8..d8cfafc2 100644 --- a/src/bin/keycloakify/generateTheme/bringInAccountV1.ts +++ b/src/bin/keycloakify/generateTheme/bringInAccountV1.ts @@ -9,7 +9,6 @@ import { transformCodebase } from "../../tools/transformCodebase"; import { rmSync } from "../../tools/fs.rmSync"; type BuildOptionsLike = { - keycloakifyBuildDirPath: string; cacheDirPath: string; npmWorkspaceRootDirPath: string; }; @@ -20,10 +19,10 @@ type BuildOptionsLike = { assert(); } -export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike }) { - const { buildOptions } = params; +export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike; srcMainResourcesDirPath: string }) { + const { buildOptions, srcMainResourcesDirPath } = params; - const builtinKeycloakThemeTmpDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "..", "tmp_yxdE2_builtin_keycloak_theme"); + const builtinKeycloakThemeTmpDirPath = pathJoin(srcMainResourcesDirPath, "..", "tmp_yxdE2_builtin_keycloak_theme"); await downloadBuiltinKeycloakTheme({ "destDirPath": builtinKeycloakThemeTmpDirPath, @@ -31,7 +30,7 @@ export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike buildOptions }); - const accountV1DirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", accountV1ThemeName, "account"); + const accountV1DirPath = pathJoin(srcMainResourcesDirPath, "theme", accountV1ThemeName, "account"); transformCodebase({ "srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "account"), diff --git a/src/bin/keycloakify/generateTheme/generateSrcMainResources.ts b/src/bin/keycloakify/generateTheme/generateSrcMainResources.ts new file mode 100644 index 00000000..62eb244b --- /dev/null +++ b/src/bin/keycloakify/generateTheme/generateSrcMainResources.ts @@ -0,0 +1,267 @@ +import { transformCodebase } from "../../tools/transformCodebase"; +import * as fs from "fs"; +import { join as pathJoin, resolve as pathResolve, dirname as pathDirname } from "path"; +import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode"; +import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode"; +import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds } from "../generateFtl"; +import { + 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"; +import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResources"; +import { readFieldNameUsage } from "./readFieldNameUsage"; +import { readExtraPagesNames } from "./readExtraPageNames"; +import { generateMessageProperties } from "./generateMessageProperties"; +import { bringInAccountV1 } from "./bringInAccountV1"; +import { rmSync } from "../../tools/fs.rmSync"; + +export type BuildOptionsLike = { + bundler: "vite" | "webpack"; + extraThemeProperties: string[] | undefined; + themeVersion: string; + loginThemeResourcesFromKeycloakVersion: string; + reactAppBuildDirPath: string; + cacheDirPath: string; + assetsDirPath: string; + urlPathname: string | undefined; + npmWorkspaceRootDirPath: string; +}; + +assert(); + +export async function generateSrcMainResources(params: { + themeName: string; + themeSrcDirPath: string; + keycloakifySrcDirPath: string; + buildOptions: BuildOptionsLike; + keycloakifyVersion: string; + srcMainResourcesDirPath: string; +}): Promise<{ implementedThemeTypes: Record }> { + const { themeName, themeSrcDirPath, keycloakifySrcDirPath, buildOptions, keycloakifyVersion, srcMainResourcesDirPath } = params; + + const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => { + const { themeType } = params; + return pathJoin(srcMainResourcesDirPath, "theme", themeName, themeType); + }; + + const cssGlobalsToDefine: Record = {}; + + const implementedThemeTypes: Record = { + "login": false, + "account": false, + "email": false + }; + + for (const themeType of ["login", "account"] as const) { + if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) { + continue; + } + + implementedThemeTypes[themeType] = true; + + const themeTypeDirPath = getThemeTypeDirPath({ themeType }); + + apply_replacers_and_move_to_theme_resources: { + const destDirPath = pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir); + + // NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up. + rmSync(destDirPath, { "recursive": true, "force": true }); + + if (themeType === "account" && implementedThemeTypes.login) { + // NOTE: We prevend doing it twice, it has been done for the login theme. + + transformCodebase({ + "srcDirPath": pathJoin( + getThemeTypeDirPath({ + "themeType": "login" + }), + "resources", + basenameOfTheKeycloakifyResourcesDir + ), + destDirPath + }); + + break apply_replacers_and_move_to_theme_resources; + } + + transformCodebase({ + "srcDirPath": buildOptions.reactAppBuildDirPath, + destDirPath, + "transformSourceCode": ({ filePath, sourceCode }) => { + //NOTE: Prevent cycles, excludes the folder we generated for debug in public/ + // This should not happen if users follow the new instruction setup but we keep it for retrocompatibility. + if ( + isInside({ + "dirPath": pathJoin(buildOptions.reactAppBuildDirPath, keycloak_resources), + filePath + }) + ) { + return undefined; + } + + if (/\.css?$/i.test(filePath)) { + const { cssGlobalsToDefine: cssGlobalsToDefineForThisFile, fixedCssCode } = replaceImportsInCssCode({ + "cssCode": sourceCode.toString("utf8") + }); + + Object.entries(cssGlobalsToDefineForThisFile).forEach(([key, value]) => { + cssGlobalsToDefine[key] = value; + }); + + return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") }; + } + + if (/\.js?$/i.test(filePath)) { + const { fixedJsCode } = replaceImportsInJsCode({ + "jsCode": sourceCode.toString("utf8"), + buildOptions + }); + + return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") }; + } + + return { "modifiedSourceCode": sourceCode }; + } + }); + } + + const { generateFtlFilesCode } = generateFtlFilesCodeFactory({ + themeName, + "indexHtmlCode": fs.readFileSync(pathJoin(buildOptions.reactAppBuildDirPath, "index.html")).toString("utf8"), + cssGlobalsToDefine, + buildOptions, + keycloakifyVersion, + themeType, + "fieldNames": readFieldNameUsage({ + keycloakifySrcDirPath, + themeSrcDirPath, + themeType + }) + }); + + [ + ...(() => { + switch (themeType) { + case "login": + return loginThemePageIds; + case "account": + return accountThemePageIds; + } + })(), + ...readExtraPagesNames({ + themeType, + themeSrcDirPath + }) + ].forEach(pageId => { + const { ftlCode } = generateFtlFilesCode({ pageId }); + + fs.mkdirSync(themeTypeDirPath, { "recursive": true }); + + fs.writeFileSync(pathJoin(themeTypeDirPath, pageId), Buffer.from(ftlCode, "utf8")); + }); + + generateMessageProperties({ + themeSrcDirPath, + themeType + }).forEach(({ languageTag, propertiesFileSource }) => { + const messagesDirPath = pathJoin(themeTypeDirPath, "messages"); + + fs.mkdirSync(pathJoin(themeTypeDirPath, "messages"), { "recursive": true }); + + const propertiesFilePath = pathJoin(messagesDirPath, `messages_${languageTag}.properties`); + + fs.writeFileSync(propertiesFilePath, Buffer.from(propertiesFileSource, "utf8")); + }); + + await downloadKeycloakStaticResources({ + "keycloakVersion": (() => { + switch (themeType) { + case "account": + return lastKeycloakVersionWithAccountV1; + case "login": + return buildOptions.loginThemeResourcesFromKeycloakVersion; + } + })(), + "themeDirPath": pathResolve(pathJoin(themeTypeDirPath, "..")), + themeType, + buildOptions + }); + + fs.writeFileSync( + pathJoin(themeTypeDirPath, "theme.properties"), + Buffer.from( + [ + `parent=${(() => { + switch (themeType) { + case "account": + return accountV1ThemeName; + case "login": + return "keycloak"; + } + assert>(false); + })()}`, + ...(buildOptions.extraThemeProperties ?? []) + ].join("\n\n"), + "utf8" + ) + ); + } + + email: { + const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email"); + + if (!fs.existsSync(emailThemeSrcDirPath)) { + break email; + } + + implementedThemeTypes.email = true; + + transformCodebase({ + "srcDirPath": emailThemeSrcDirPath, + "destDirPath": getThemeTypeDirPath({ "themeType": "email" }) + }); + } + + const parsedKeycloakThemeJson: { themes: { name: string; types: string[] }[] } = { "themes": [] }; + + parsedKeycloakThemeJson.themes.push({ + "name": themeName, + "types": Object.entries(implementedThemeTypes) + .filter(([, isImplemented]) => isImplemented) + .map(([themeType]) => themeType) + }); + + account_specific_extra_work: { + if (!implementedThemeTypes.account) { + break account_specific_extra_work; + } + + await bringInAccountV1({ + srcMainResourcesDirPath, + buildOptions + }); + + parsedKeycloakThemeJson.themes.push({ + "name": accountV1ThemeName, + "types": ["account"] + }); + } + + { + const keycloakThemeJsonFilePath = pathJoin(srcMainResourcesDirPath, "META-INF", "keycloak-themes.json"); + + try { + fs.mkdirSync(pathDirname(keycloakThemeJsonFilePath)); + } catch {} + + fs.writeFileSync(keycloakThemeJsonFilePath, Buffer.from(JSON.stringify(parsedKeycloakThemeJson, null, 2), "utf8")); + } + + return { implementedThemeTypes }; +} diff --git a/src/bin/keycloakify/generateTheme/generateTheme.ts b/src/bin/keycloakify/generateTheme/generateTheme.ts index 9ad5ff35..ccbadda2 100644 --- a/src/bin/keycloakify/generateTheme/generateTheme.ts +++ b/src/bin/keycloakify/generateTheme/generateTheme.ts @@ -1,28 +1,13 @@ -import { transformCodebase } from "../../tools/transformCodebase"; -import * as fs from "fs"; -import { join as pathJoin, resolve as pathResolve, dirname as pathDirname } from "path"; -import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode"; -import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode"; -import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds } from "../generateFtl"; -import { - type ThemeType, - lastKeycloakVersionWithAccountV1, - keycloak_resources, - accountV1ThemeName, - basenameOfTheKeycloakifyResourcesDir -} from "../../constants"; -import { isInside } from "../../tools/isInside"; +import { type ThemeType } from "../../constants"; +import { join as pathJoin } from "path"; import type { BuildOptions } from "../buildOptions"; -import { assert, type Equals } from "tsafe/assert"; -import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResources"; -import { readFieldNameUsage } from "./readFieldNameUsage"; -import { readExtraPagesNames } from "./readExtraPageNames"; -import { generateMessageProperties } from "./generateMessageProperties"; -import { bringInAccountV1 } from "./bringInAccountV1"; -import { rmSync } from "../../tools/fs.rmSync"; +import { assert } from "tsafe/assert"; +import { generateSrcMainResources } from "./generateSrcMainResources"; +import { generateThemeVariations } from "./generateThemeVariants"; export type BuildOptionsLike = { bundler: "vite" | "webpack"; + themeNames: string[]; extraThemeProperties: string[] | undefined; themeVersion: string; loginThemeResourcesFromKeycloakVersion: string; @@ -37,235 +22,32 @@ export type BuildOptionsLike = { assert(); export async function generateTheme(params: { - themeName: string; themeSrcDirPath: string; keycloakifySrcDirPath: string; buildOptions: BuildOptionsLike; keycloakifyVersion: string; }): Promise<{ implementedThemeTypes: Record }> { - const { themeName, themeSrcDirPath, keycloakifySrcDirPath, buildOptions, keycloakifyVersion } = params; + const { themeSrcDirPath, keycloakifySrcDirPath, buildOptions, keycloakifyVersion } = params; - const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => { - const { themeType } = params; - return pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", themeName, themeType); - }; + const [themeName, ...themeVariantNames] = buildOptions.themeNames; - const cssGlobalsToDefine: Record = {}; - - const implementedThemeTypes: Record = { - "login": false, - "account": false, - "email": false - }; - - for (const themeType of ["login", "account"] as const) { - if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) { - continue; - } - - implementedThemeTypes[themeType] = true; - - const themeTypeDirPath = getThemeTypeDirPath({ themeType }); - - apply_replacers_and_move_to_theme_resources: { - const destDirPath = pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir); - - // NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up. - rmSync(destDirPath, { "recursive": true, "force": true }); - - if (themeType === "account" && implementedThemeTypes.login) { - // NOTE: We prevend doing it twice, it has been done for the login theme. - - transformCodebase({ - "srcDirPath": pathJoin( - getThemeTypeDirPath({ - "themeType": "login" - }), - "resources", - basenameOfTheKeycloakifyResourcesDir - ), - destDirPath - }); - - break apply_replacers_and_move_to_theme_resources; - } - - transformCodebase({ - "srcDirPath": buildOptions.reactAppBuildDirPath, - destDirPath, - "transformSourceCode": ({ filePath, sourceCode }) => { - //NOTE: Prevent cycles, excludes the folder we generated for debug in public/ - // This should not happen if users follow the new instruction setup but we keep it for retrocompatibility. - if ( - isInside({ - "dirPath": pathJoin(buildOptions.reactAppBuildDirPath, keycloak_resources), - filePath - }) - ) { - return undefined; - } - - if (/\.css?$/i.test(filePath)) { - const { cssGlobalsToDefine: cssGlobalsToDefineForThisFile, fixedCssCode } = replaceImportsInCssCode({ - "cssCode": sourceCode.toString("utf8") - }); - - Object.entries(cssGlobalsToDefineForThisFile).forEach(([key, value]) => { - cssGlobalsToDefine[key] = value; - }); - - return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") }; - } - - if (/\.js?$/i.test(filePath)) { - const { fixedJsCode } = replaceImportsInJsCode({ - "jsCode": sourceCode.toString("utf8"), - buildOptions - }); - - return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") }; - } - - return { "modifiedSourceCode": sourceCode }; - } - }); - } - - const { generateFtlFilesCode } = generateFtlFilesCodeFactory({ - themeName, - "indexHtmlCode": fs.readFileSync(pathJoin(buildOptions.reactAppBuildDirPath, "index.html")).toString("utf8"), - cssGlobalsToDefine, - buildOptions, - keycloakifyVersion, - themeType, - "fieldNames": readFieldNameUsage({ - keycloakifySrcDirPath, - themeSrcDirPath, - themeType - }) - }); - - [ - ...(() => { - switch (themeType) { - case "login": - return loginThemePageIds; - case "account": - return accountThemePageIds; - } - })(), - ...readExtraPagesNames({ - themeType, - themeSrcDirPath - }) - ].forEach(pageId => { - const { ftlCode } = generateFtlFilesCode({ pageId }); - - fs.mkdirSync(themeTypeDirPath, { "recursive": true }); - - fs.writeFileSync(pathJoin(themeTypeDirPath, pageId), Buffer.from(ftlCode, "utf8")); - }); - - generateMessageProperties({ - themeSrcDirPath, - themeType - }).forEach(({ languageTag, propertiesFileSource }) => { - const messagesDirPath = pathJoin(themeTypeDirPath, "messages"); - - fs.mkdirSync(pathJoin(themeTypeDirPath, "messages"), { "recursive": true }); - - const propertiesFilePath = pathJoin(messagesDirPath, `messages_${languageTag}.properties`); - - fs.writeFileSync(propertiesFilePath, Buffer.from(propertiesFileSource, "utf8")); - }); - - await downloadKeycloakStaticResources({ - "keycloakVersion": (() => { - switch (themeType) { - case "account": - return lastKeycloakVersionWithAccountV1; - case "login": - return buildOptions.loginThemeResourcesFromKeycloakVersion; - } - })(), - "themeDirPath": pathResolve(pathJoin(themeTypeDirPath, "..")), - themeType, - buildOptions - }); - - fs.writeFileSync( - pathJoin(themeTypeDirPath, "theme.properties"), - Buffer.from( - [ - `parent=${(() => { - switch (themeType) { - case "account": - return accountV1ThemeName; - case "login": - return "keycloak"; - } - assert>(false); - })()}`, - ...(buildOptions.extraThemeProperties ?? []) - ].join("\n\n"), - "utf8" - ) - ); - } - - email: { - const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email"); - - if (!fs.existsSync(emailThemeSrcDirPath)) { - break email; - } - - implementedThemeTypes.email = true; - - transformCodebase({ - "srcDirPath": emailThemeSrcDirPath, - "destDirPath": getThemeTypeDirPath({ "themeType": "email" }) - }); - } - - const parsedKeycloakThemeJson: { themes: { name: string; types: string[] }[] } = { "themes": [] }; - - parsedKeycloakThemeJson.themes.push({ - "name": themeName, - "types": Object.entries(implementedThemeTypes) - .filter(([, isImplemented]) => isImplemented) - .map(([themeType]) => themeType) + const { implementedThemeTypes } = await generateSrcMainResources({ + themeName, + "srcMainResourcesDirPath": pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources"), + themeSrcDirPath, + keycloakifySrcDirPath, + keycloakifyVersion, + buildOptions }); - account_specific_extra_work: { - if (!implementedThemeTypes.account) { - break account_specific_extra_work; - } - - await bringInAccountV1({ buildOptions }); - - parsedKeycloakThemeJson.themes.push({ - "name": accountV1ThemeName, - "types": ["account"] + for (const themeVariantName of themeVariantNames) { + generateThemeVariations({ + themeName, + themeVariantName, + implementedThemeTypes, + buildOptions }); } - { - const keycloakThemeJsonFilePath = pathJoin( - buildOptions.keycloakifyBuildDirPath, - "src", - "main", - "resources", - "META-INF", - "keycloak-themes.json" - ); - - try { - fs.mkdirSync(pathDirname(keycloakThemeJsonFilePath)); - } catch {} - - fs.writeFileSync(keycloakThemeJsonFilePath, Buffer.from(JSON.stringify(parsedKeycloakThemeJson, null, 2), "utf8")); - } - return { implementedThemeTypes }; } diff --git a/src/bin/keycloakify/generateThemeVariants.ts b/src/bin/keycloakify/generateTheme/generateThemeVariants.ts similarity index 84% rename from src/bin/keycloakify/generateThemeVariants.ts rename to src/bin/keycloakify/generateTheme/generateThemeVariants.ts index 6ec2b86f..63c6e714 100644 --- a/src/bin/keycloakify/generateThemeVariants.ts +++ b/src/bin/keycloakify/generateTheme/generateThemeVariants.ts @@ -1,4 +1,4 @@ -import type { ThemeType } from "../constants"; +import type { ThemeType } from "../../constants"; export function generateThemeVariations(params: { themeName: string; diff --git a/src/bin/keycloakify/keycloakify.ts b/src/bin/keycloakify/keycloakify.ts index f068d9e0..aafb47f2 100644 --- a/src/bin/keycloakify/keycloakify.ts +++ b/src/bin/keycloakify/keycloakify.ts @@ -10,7 +10,6 @@ import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath" import { readThisNpmProjectVersion } from "../tools/readThisNpmProjectVersion"; import { keycloakifyBuildOptionsForPostPostBuildScriptEnvName } from "../constants"; import { buildJars } from "./buildJars"; -import { generateThemeVariations } from "./generateThemeVariants"; export async function main() { const buildOptions = readBuildOptions({ @@ -30,25 +29,13 @@ export async function main() { fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, ".gitignore"), Buffer.from("*", "utf8")); } - const [themeName, ...themeVariantNames] = buildOptions.themeNames; - const { implementedThemeTypes } = await generateTheme({ - themeName, themeSrcDirPath, "keycloakifySrcDirPath": pathJoin(getThisCodebaseRootDirPath(), "src"), "keycloakifyVersion": readThisNpmProjectVersion(), buildOptions }); - for (const themeVariantName of themeVariantNames) { - generateThemeVariations({ - themeName, - themeVariantName, - implementedThemeTypes, - buildOptions - }); - } - run_post_build_script: { if (buildOptions.bundler !== "vite") { break run_post_build_script;