From 73a8ec029574adc0323b59a4ee33243dc9b79f87 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Mon, 5 Feb 2024 08:52:58 +0100 Subject: [PATCH] Building version --- src/bin/copy-keycloak-resources-to-public.ts | 3 +- src/bin/download-builtin-keycloak-theme.ts | 141 ++++++++++++------ src/bin/initialize-email-theme.ts | 3 +- .../generateJavaStackFiles.ts | 141 ------------------ .../generateJavaStackFiles/index.ts | 1 - src/bin/keycloakify/generatePom.ts | 70 +++++++++ .../bringInAccountV1.ts | 59 +++----- .../downloadKeycloakStaticResources.ts | 61 ++------ .../generateTheme/generateTheme.ts | 135 +++++++++++++---- .../generateTheme/readStaticResourcesUsage.ts | 76 ---------- src/bin/keycloakify/keycloakify.ts | 59 +------- src/bin/tools/downloadAndUnzip.ts | 3 +- src/bin/tools/fs.rm.ts | 43 ++++++ src/bin/tools/fs.rmSync.ts | 33 ++++ src/bin/tools/transformCodebase.ts | 21 ++- 15 files changed, 410 insertions(+), 439 deletions(-) delete mode 100644 src/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.ts delete mode 100644 src/bin/keycloakify/generateJavaStackFiles/index.ts create mode 100644 src/bin/keycloakify/generatePom.ts rename src/bin/keycloakify/{generateJavaStackFiles => generateTheme}/bringInAccountV1.ts (52%) delete mode 100644 src/bin/keycloakify/generateTheme/readStaticResourcesUsage.ts create mode 100644 src/bin/tools/fs.rm.ts create mode 100644 src/bin/tools/fs.rmSync.ts diff --git a/src/bin/copy-keycloak-resources-to-public.ts b/src/bin/copy-keycloak-resources-to-public.ts index 78daf92d..5c8d276c 100644 --- a/src/bin/copy-keycloak-resources-to-public.ts +++ b/src/bin/copy-keycloak-resources-to-public.ts @@ -28,7 +28,6 @@ import * as fs from "fs"; })(), themeType, "themeDirPath": reservedDirPath, - "usedResources": undefined, buildOptions }); } @@ -44,7 +43,7 @@ import * as fs from "fs"; ) ); - fs.writeFileSync(pathJoin(buildOptions.publicDirPath, "keycloak-resources", ".gitignore"), Buffer.from("*", "utf8")); + fs.writeFileSync(pathJoin(buildOptions.publicDirPath, keycloak_resources, ".gitignore"), Buffer.from("*", "utf8")); console.log(`${pathRelative(reactAppRootDirPath, reservedDirPath)} directory created.`); })(); diff --git a/src/bin/download-builtin-keycloak-theme.ts b/src/bin/download-builtin-keycloak-theme.ts index 073aad9c..8ddd4b68 100644 --- a/src/bin/download-builtin-keycloak-theme.ts +++ b/src/bin/download-builtin-keycloak-theme.ts @@ -7,6 +7,9 @@ import { readBuildOptions, type BuildOptions } from "./keycloakify/buildOptions" import { assert } from "tsafe/assert"; import * as child_process from "child_process"; import * as fs from "fs"; +import { rmSync } from "./tools/fs.rmSync"; +import { lastKeycloakVersionWithAccountV1 } from "./constants"; +import { transformCodebase } from "./tools/transformCodebase"; export type BuildOptionsLike = { cacheDirPath: string; @@ -26,51 +29,6 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st "preCacheTransform": { "actionCacheId": "npm install and build", "action": async ({ destDirPath }) => { - fix_account_css: { - const accountCssFilePath = pathJoin(destDirPath, "keycloak", "account", "resources", "css", "account.css"); - - if (!fs.existsSync(accountCssFilePath)) { - break fix_account_css; - } - - fs.writeFileSync( - accountCssFilePath, - Buffer.from(fs.readFileSync(accountCssFilePath).toString("utf8").replace("top: -34px;", "top: -34px !important;"), "utf8") - ); - } - - fix_account_topt: { - const totpFtlFilePath = pathJoin(destDirPath, "base", "account", "totp.ftl"); - - if (!fs.existsSync(totpFtlFilePath)) { - break fix_account_topt; - } - - fs.writeFileSync( - totpFtlFilePath, - Buffer.from( - fs - .readFileSync(totpFtlFilePath) - .toString("utf8") - .replace( - [ - " <#list totp.policy.supportedApplications as app>", - "
  • ${app}
  • ", - " " - ].join("\n"), - [ - " <#if totp.policy.supportedApplications?has_content>", - " <#list totp.policy.supportedApplications as app>", - "
  • ${app}
  • ", - " ", - " " - ].join("\n") - ), - "utf8" - ) - ); - } - install_common_node_modules: { const commonResourcesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources"); @@ -128,7 +86,98 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st fs.writeFileSync(packageJsonFilePath, packageJsonRaw); - fs.rmSync(pathJoin(accountV2DirSrcDirPath, "node_modules"), { "recursive": true }); + rmSync(pathJoin(accountV2DirSrcDirPath, "node_modules"), { "recursive": true }); + } + + last_account_v1_transformations: { + if (lastKeycloakVersionWithAccountV1 !== keycloakVersion) { + break last_account_v1_transformations; + } + + { + const accountCssFilePath = pathJoin(destDirPath, "keycloak", "account", "resources", "css", "account.css"); + + fs.writeFileSync( + accountCssFilePath, + Buffer.from(fs.readFileSync(accountCssFilePath).toString("utf8").replace("top: -34px;", "top: -34px !important;"), "utf8") + ); + } + + { + const totpFtlFilePath = pathJoin(destDirPath, "base", "account", "totp.ftl"); + + fs.writeFileSync( + totpFtlFilePath, + Buffer.from( + fs + .readFileSync(totpFtlFilePath) + .toString("utf8") + .replace( + [ + " <#list totp.policy.supportedApplications as app>", + "
  • ${app}
  • ", + " " + ].join("\n"), + [ + " <#if totp.policy.supportedApplications?has_content>", + " <#list totp.policy.supportedApplications as app>", + "
  • ${app}
  • ", + " ", + " " + ].join("\n") + ), + "utf8" + ) + ); + } + + // Note, this is an optimization for reducing the size of the jar + { + const defaultThemeCommonResourcesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources"); + + const usedCommonResourceRelativeFilePaths = [ + ...["patternfly.min.css", "patternfly-additions.min.css", "patternfly-additions.min.css"].map(fileBasename => + pathJoin("node_modules", "patternfly", "dist", "css", fileBasename) + ), + ...[ + "OpenSans-Light-webfont.woff2", + "OpenSans-Regular-webfont.woff2", + "OpenSans-Bold-webfont.woff2", + "OpenSans-Semibold-webfont.woff2", + "OpenSans-Bold-webfont.woff", + "OpenSans-Light-webfont.woff", + "OpenSans-Regular-webfont.woff", + "OpenSans-Semibold-webfont.woff", + "OpenSans-Regular-webfont.ttf", + "OpenSans-Light-webfont.ttf", + "OpenSans-Semibold-webfont.ttf", + "OpenSans-Bold-webfont.ttf" + ].map(fileBasename => pathJoin("node_modules", "patternfly", "dist", "fonts", fileBasename)) + ]; + + transformCodebase({ + "srcDirPath": defaultThemeCommonResourcesDirPath, + "destDirPath": defaultThemeCommonResourcesDirPath, + "transformSourceCode": ({ sourceCode, fileRelativePath }) => { + if (!usedCommonResourceRelativeFilePaths.includes(fileRelativePath)) { + return undefined; + } + + return { "modifiedSourceCode": sourceCode }; + } + }); + } + + // Other optimization: Remove AngularJS + { + const nodeModuleDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "node_modules"); + + fs.readdirSync(nodeModuleDirPath) + .filter(basename => basename.startsWith("angular")) + .map(basename => pathJoin(nodeModuleDirPath, basename)) + .filter(dirPath => fs.statSync(dirPath).isDirectory()) + .forEach(dirPath => rmSync(dirPath, { "recursive": true })); + } } } } diff --git a/src/bin/initialize-email-theme.ts b/src/bin/initialize-email-theme.ts index b4afdacc..84dbb93b 100644 --- a/src/bin/initialize-email-theme.ts +++ b/src/bin/initialize-email-theme.ts @@ -8,6 +8,7 @@ import { readBuildOptions } from "./keycloakify/buildOptions"; import * as fs from "fs"; import { getLogger } from "./tools/logger"; import { getThemeSrcDirPath } from "./getThemeSrcDirPath"; +import { rmSync } from "./tools/fs.rmSync"; export async function main() { const reactAppRootDirPath = process.cwd(); @@ -54,7 +55,7 @@ export async function main() { logger.log(`${pathRelative(process.cwd(), emailThemeSrcDirPath)} ready to be customized, feel free to remove every file you do not customize`); - fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true }); + rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true }); } if (require.main === module) { diff --git a/src/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.ts b/src/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.ts deleted file mode 100644 index b6d93f29..00000000 --- a/src/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.ts +++ /dev/null @@ -1,141 +0,0 @@ -import * as fs from "fs"; -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, retrocompatPostfix, accountV1ThemeName } from "../../constants"; -import { bringInAccountV1 } from "./bringInAccountV1"; - -type BuildOptionsLike = { - groupId: string; - artifactId: string; - themeVersion: string; - cacheDirPath: string; - keycloakifyBuildDirPath: string; - themeNames: string[]; - doBuildRetrocompatAccountTheme: boolean; -}; - -{ - const buildOptions = Reflect(); - - assert(); -} - -export async function generateJavaStackFiles(params: { - implementedThemeTypes: Record; - buildOptions: BuildOptionsLike; -}): Promise<{ - jarFilePath: string; -}> { - const { implementedThemeTypes, buildOptions } = params; - - { - const { pomFileCode } = (function generatePomFileCode(): { - pomFileCode: string; - } { - const pomFileCode = [ - ``, - ``, - ` 4.0.0`, - ` ${buildOptions.groupId}`, - ` ${buildOptions.artifactId}`, - ` ${buildOptions.themeVersion}`, - ` ${buildOptions.artifactId}`, - ` `, - ` jar`, - ` `, - ` UTF-8`, - ` `, - ` `, - ` `, - ` `, - ` org.apache.maven.plugins`, - ` maven-shade-plugin`, - ` 3.5.1`, - ` `, - ` `, - ` package`, - ` `, - ` shade`, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` `, - ` io.phasetwo.keycloak`, - ` keycloak-account-v1`, - ` 0.1`, - ` `, - ` `, - `` - ].join("\n"); - - return { pomFileCode }; - })(); - - fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8")); - } - - if (implementedThemeTypes.account) { - await bringInAccountV1({ buildOptions }); - } - - { - const themeManifestFilePath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json"); - - try { - fs.mkdirSync(pathDirname(themeManifestFilePath)); - } catch {} - - fs.writeFileSync( - themeManifestFilePath, - Buffer.from( - JSON.stringify( - { - "themes": [ - ...(!implementedThemeTypes.account - ? [] - : [ - { - "name": accountV1ThemeName, - "types": ["account"] - } - ]), - ...buildOptions.themeNames - .map(themeName => [ - { - "name": themeName, - "types": Object.entries(implementedThemeTypes) - .filter(([, isImplemented]) => isImplemented) - .map(([themeType]) => themeType) - }, - ...(!implementedThemeTypes.account || !buildOptions.doBuildRetrocompatAccountTheme - ? [] - : [ - { - "name": `${themeName}${retrocompatPostfix}`, - "types": ["account"] - } - ]) - ]) - .flat() - ] - }, - null, - 2 - ), - "utf8" - ) - ); - } - - return { - "jarFilePath": pathJoin(buildOptions.keycloakifyBuildDirPath, "target", `${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`) - }; -} diff --git a/src/bin/keycloakify/generateJavaStackFiles/index.ts b/src/bin/keycloakify/generateJavaStackFiles/index.ts deleted file mode 100644 index ea372c91..00000000 --- a/src/bin/keycloakify/generateJavaStackFiles/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./generateJavaStackFiles"; diff --git a/src/bin/keycloakify/generatePom.ts b/src/bin/keycloakify/generatePom.ts new file mode 100644 index 00000000..555382ab --- /dev/null +++ b/src/bin/keycloakify/generatePom.ts @@ -0,0 +1,70 @@ +import { assert } from "tsafe/assert"; +import { Reflect } from "tsafe/Reflect"; +import type { BuildOptions } from "./buildOptions"; + +type BuildOptionsLike = { + groupId: string; + artifactId: string; + themeVersion: string; + keycloakifyBuildDirPath: string; +}; + +{ + const buildOptions = Reflect(); + + assert(); +} + +export function generatePom(params: { buildOptions: BuildOptionsLike }) { + const { buildOptions } = params; + + const { pomFileCode } = (function generatePomFileCode(): { + pomFileCode: string; + } { + const pomFileCode = [ + ``, + ``, + ` 4.0.0`, + ` ${buildOptions.groupId}`, + ` ${buildOptions.artifactId}`, + ` ${buildOptions.themeVersion}`, + ` ${buildOptions.artifactId}`, + ` `, + ` jar`, + ` `, + ` UTF-8`, + ` `, + ` `, + ` `, + ` `, + ` org.apache.maven.plugins`, + ` maven-shade-plugin`, + ` 3.5.1`, + ` `, + ` `, + ` package`, + ` `, + ` shade`, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` `, + ` io.phasetwo.keycloak`, + ` keycloak-account-v1`, + ` 0.1`, + ` `, + ` `, + `` + ].join("\n"); + + return { pomFileCode }; + })(); + + return { pomFileCode }; +} diff --git a/src/bin/keycloakify/generateJavaStackFiles/bringInAccountV1.ts b/src/bin/keycloakify/generateTheme/bringInAccountV1.ts similarity index 52% rename from src/bin/keycloakify/generateJavaStackFiles/bringInAccountV1.ts rename to src/bin/keycloakify/generateTheme/bringInAccountV1.ts index 91aea909..6caaeabc 100644 --- a/src/bin/keycloakify/generateJavaStackFiles/bringInAccountV1.ts +++ b/src/bin/keycloakify/generateTheme/bringInAccountV1.ts @@ -1,11 +1,12 @@ import * as fs from "fs"; -import { join as pathJoin, dirname as pathDirname } from "path"; +import { join as pathJoin } from "path"; import { assert } from "tsafe/assert"; import { Reflect } from "tsafe/Reflect"; import type { BuildOptions } from "../buildOptions"; import { resources_common, lastKeycloakVersionWithAccountV1, accountV1ThemeName } from "../../constants"; import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme"; import { transformCodebase } from "../../tools/transformCodebase"; +import { rmSync } from "../../tools/fs.rmSync"; type BuildOptionsLike = { keycloakifyBuildDirPath: string; @@ -36,45 +37,17 @@ export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike "destDirPath": accountV1DirPath }); - const commonResourceFilePaths = [ - "node_modules/patternfly/dist/css/patternfly.min.css", - "node_modules/patternfly/dist/css/patternfly-additions.min.css", - "node_modules/patternfly/dist/css/patternfly-additions.min.css", - ...[ - "OpenSans-Light-webfont.woff2", - "OpenSans-Regular-webfont.woff2", - "OpenSans-Bold-webfont.woff2", - "OpenSans-Semibold-webfont.woff2", - "OpenSans-Bold-webfont.woff", - "OpenSans-Light-webfont.woff", - "OpenSans-Regular-webfont.woff", - "OpenSans-Semibold-webfont.woff", - "OpenSans-Regular-webfont.ttf", - "OpenSans-Light-webfont.ttf", - "OpenSans-Semibold-webfont.ttf", - "OpenSans-Bold-webfont.ttf" - ].map(path => `node_modules/patternfly/dist/fonts/${path}`) - ]; + transformCodebase({ + "srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "account", "resources"), + "destDirPath": pathJoin(accountV1DirPath, "resources") + }); - for (const relativeFilePath of commonResourceFilePaths.map(path => pathJoin(...path.split("/")))) { - const destFilePath = pathJoin(accountV1DirPath, "resources", resources_common, relativeFilePath); + transformCodebase({ + "srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "common", "resources"), + "destDirPath": pathJoin(accountV1DirPath, "resources", resources_common) + }); - fs.mkdirSync(pathDirname(destFilePath), { "recursive": true }); - - fs.cpSync(pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "common", "resources", relativeFilePath), destFilePath); - } - - const resourceFilePaths = ["css/account.css", "img/icon-sidebar-active.png", "img/logo.png"]; - - for (const relativeFilePath of resourceFilePaths.map(path => pathJoin(...path.split("/")))) { - const destFilePath = pathJoin(accountV1DirPath, "resources", relativeFilePath); - - fs.mkdirSync(pathDirname(destFilePath), { "recursive": true }); - - fs.cpSync(pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "account", "resources", relativeFilePath), destFilePath); - } - - fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true }); + rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true }); fs.writeFileSync( pathJoin(accountV1DirPath, "theme.properties"), @@ -84,7 +57,15 @@ export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike "", "locales=ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN", "", - "styles=" + [...resourceFilePaths, ...commonResourceFilePaths.map(path => `resources-common/${path}`)].join(" "), + "styles=" + + [ + "css/account.css", + "img/icon-sidebar-active.png", + "img/logo.png", + ...["patternfly.min.css", "patternfly-additions.min.css", "patternfly-additions.min.css"].map( + fileBasename => `${resources_common}/node_modules/patternfly/dist/css/${fileBasename}` + ) + ].join(" "), "", "##### css classes for form buttons", "# main class used for all buttons", diff --git a/src/bin/keycloakify/generateTheme/downloadKeycloakStaticResources.ts b/src/bin/keycloakify/generateTheme/downloadKeycloakStaticResources.ts index 3e0bd985..8cc0c3f0 100644 --- a/src/bin/keycloakify/generateTheme/downloadKeycloakStaticResources.ts +++ b/src/bin/keycloakify/generateTheme/downloadKeycloakStaticResources.ts @@ -1,11 +1,11 @@ import { transformCodebase } from "../../tools/transformCodebase"; -import * as fs from "fs"; -import { join as pathJoin, dirname as pathDirname } from "path"; +import { join as pathJoin } from "path"; import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme"; import { resources_common, type ThemeType } from "../../constants"; import { BuildOptions } from "../buildOptions"; import { assert } from "tsafe/assert"; import * as crypto from "crypto"; +import { rmSync } from "../../tools/fs.rmSync"; export type BuildOptionsLike = { cacheDirPath: string; @@ -13,45 +13,14 @@ export type BuildOptionsLike = { assert(); -export async function downloadKeycloakStaticResources( - // prettier-ignore - params: { - themeType: ThemeType; - themeDirPath: string; - keycloakVersion: string; - usedResources: { - resourcesCommonFilePaths: string[]; - } | undefined; - buildOptions: BuildOptionsLike; - } -) { +export async function downloadKeycloakStaticResources(params: { + themeType: ThemeType; + themeDirPath: string; + keycloakVersion: string; + buildOptions: BuildOptionsLike; +}) { const { themeType, themeDirPath, keycloakVersion, buildOptions } = params; - // NOTE: Hack for 427 - const usedResources = (() => { - const { usedResources } = params; - - if (usedResources === undefined) { - return undefined; - } - - assert(usedResources !== undefined); - - return { - "resourcesCommonDirPaths": usedResources.resourcesCommonFilePaths.map(filePath => { - { - const splitArg = "/dist/"; - - if (filePath.includes(splitArg)) { - return filePath.split(splitArg)[0] + splitArg; - } - } - - return pathDirname(filePath); - }) - }; - })(); - const tmpDirPath = pathJoin( themeDirPath, `tmp_suLeKsxId_${crypto.createHash("sha256").update(`${themeType}-${keycloakVersion}`).digest("hex").slice(0, 8)}` @@ -72,18 +41,8 @@ export async function downloadKeycloakStaticResources( transformCodebase({ "srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"), - "destDirPath": pathJoin(resourcesPath, resources_common), - "transformSourceCode": - usedResources === undefined - ? undefined - : ({ fileRelativePath, sourceCode }) => { - if (usedResources.resourcesCommonDirPaths.find(dirPath => fileRelativePath.startsWith(dirPath)) === undefined) { - return undefined; - } - - return { "modifiedSourceCode": sourceCode }; - } + "destDirPath": pathJoin(resourcesPath, resources_common) }); - fs.rmSync(tmpDirPath, { "recursive": true, "force": true }); + rmSync(tmpDirPath, { "recursive": true, "force": true }); } diff --git a/src/bin/keycloakify/generateTheme/generateTheme.ts b/src/bin/keycloakify/generateTheme/generateTheme.ts index b9b95a74..3ff03e79 100644 --- a/src/bin/keycloakify/generateTheme/generateTheme.ts +++ b/src/bin/keycloakify/generateTheme/generateTheme.ts @@ -1,11 +1,10 @@ import { transformCodebase } from "../../tools/transformCodebase"; import * as fs from "fs"; -import { join as pathJoin, basename as pathBasename, resolve as pathResolve } from "path"; +import { join as pathJoin, basename as pathBasename, 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 { - themeTypes, type ThemeType, lastKeycloakVersionWithAccountV1, keycloak_resources, @@ -20,7 +19,7 @@ import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResourc import { readFieldNameUsage } from "./readFieldNameUsage"; import { readExtraPagesNames } from "./readExtraPageNames"; import { generateMessageProperties } from "./generateMessageProperties"; -import { readStaticResourcesUsage } from "./readStaticResourcesUsage"; +import { bringInAccountV1 } from "./bringInAccountV1"; export type BuildOptionsLike = { bundler: "vite" | "webpack"; @@ -33,6 +32,7 @@ export type BuildOptionsLike = { assetsDirPath: string; urlPathname: string | undefined; doBuildRetrocompatAccountTheme: boolean; + themeNames: string[]; }; assert(); @@ -59,27 +59,47 @@ export async function generateTheme(params: { ); }; - let allCssGlobalsToDefine: Record = {}; + const cssGlobalsToDefine: Record = {}; - for (const themeType of themeTypes) { + 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 }); - copy_app_resources_to_theme_path: { - const isFirstPass = themeType.indexOf(themeType) === 0; + apply_replacers_and_move_to_theme_resources: { + if (themeType === "account" && implementedThemeTypes.login) { + // NOTE: We prevend doing it twice, it has been done for the login theme. - if (!isFirstPass) { - break copy_app_resources_to_theme_path; + transformCodebase({ + "srcDirPath": pathJoin( + getThemeTypeDirPath({ + "themeType": "login" + }), + "resources", + basenameOfTheKeycloakifyResourcesDir + ), + "destDirPath": pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir) + }); + + break apply_replacers_and_move_to_theme_resources; } transformCodebase({ - "destDirPath": pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir), "srcDirPath": buildOptions.reactAppBuildDirPath, + "destDirPath": pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir), "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), @@ -90,20 +110,13 @@ export async function generateTheme(params: { } if (/\.css?$/i.test(filePath)) { - const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({ + const { cssGlobalsToDefine: cssGlobalsToDefineForThisFile, fixedCssCode } = replaceImportsInCssCode({ "cssCode": sourceCode.toString("utf8") }); - register_css_variables: { - if (!isFirstPass) { - break register_css_variables; - } - - allCssGlobalsToDefine = { - ...allCssGlobalsToDefine, - ...cssGlobalsToDefine - }; - } + Object.entries(cssGlobalsToDefineForThisFile).forEach(([key, value]) => { + cssGlobalsToDefine[key] = value; + }); return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") }; } @@ -125,7 +138,7 @@ export async function generateTheme(params: { const { generateFtlFilesCode } = generateFtlFilesCodeFactory({ themeName, "indexHtmlCode": fs.readFileSync(pathJoin(buildOptions.reactAppBuildDirPath, "index.html")).toString("utf8"), - "cssGlobalsToDefine": allCssGlobalsToDefine, + cssGlobalsToDefine, buildOptions, keycloakifyVersion, themeType, @@ -181,11 +194,6 @@ export async function generateTheme(params: { })(), "themeDirPath": pathResolve(pathJoin(themeTypeDirPath, "..")), themeType, - "usedResources": readStaticResourcesUsage({ - keycloakifySrcDirPath, - themeSrcDirPath, - themeType - }), buildOptions }); @@ -235,9 +243,82 @@ export async function generateTheme(params: { break email; } + implementedThemeTypes.email = true; + transformCodebase({ "srcDirPath": emailThemeSrcDirPath, "destDirPath": getThemeTypeDirPath({ "themeType": "email" }) }); } + + const parsedKeycloakThemeJson: { themes: { name: string; types: string[] }[] } = { "themes": [] }; + + buildOptions.themeNames.forEach(themeName => + 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({ buildOptions }); + + parsedKeycloakThemeJson.themes.push({ + "name": accountV1ThemeName, + "types": ["account"] + }); + + add_retrocompat_account_theme: { + if (!buildOptions.doBuildRetrocompatAccountTheme) { + break add_retrocompat_account_theme; + } + + transformCodebase({ + "srcDirPath": getThemeTypeDirPath({ "themeType": "account" }), + "destDirPath": getThemeTypeDirPath({ "themeType": "account", "isRetrocompat": true }), + "transformSourceCode": ({ filePath, sourceCode }) => { + if (pathBasename(filePath) === "theme.properties") { + return { + "modifiedSourceCode": Buffer.from( + sourceCode.toString("utf8").replace(`parent=${accountV1ThemeName}`, "parent=keycloak"), + "utf8" + ) + }; + } + + return { "modifiedSourceCode": sourceCode }; + } + }); + + buildOptions.themeNames.forEach(themeName => + parsedKeycloakThemeJson.themes.push({ + "name": `${themeName}${retrocompatPostfix}`, + "types": ["account"] + }) + ); + } + } + + { + 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")); + } } diff --git a/src/bin/keycloakify/generateTheme/readStaticResourcesUsage.ts b/src/bin/keycloakify/generateTheme/readStaticResourcesUsage.ts deleted file mode 100644 index ea62bff6..00000000 --- a/src/bin/keycloakify/generateTheme/readStaticResourcesUsage.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { crawl } from "../../tools/crawl"; -import { join as pathJoin, sep as pathSep } from "path"; -import * as fs from "fs"; -import type { ThemeType } from "../../constants"; - -/** Assumes the theme type exists */ -export function readStaticResourcesUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): { - resourcesCommonFilePaths: string[]; -} { - const { keycloakifySrcDirPath, themeSrcDirPath, themeType } = params; - - const resourcesCommonFilePaths = new Set(); - - for (const srcDirPath of [pathJoin(keycloakifySrcDirPath, themeType), pathJoin(themeSrcDirPath, themeType)]) { - const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath)); - - for (const filePath of filePaths) { - const rawSourceFile = fs.readFileSync(filePath).toString("utf8"); - - if (!rawSourceFile.includes("resourcesCommonPath") && !rawSourceFile.includes("resourcesPath")) { - continue; - } - - const wrap = readPaths({ rawSourceFile }); - - wrap.resourcesCommonFilePaths.forEach(filePath => resourcesCommonFilePaths.add(filePath)); - } - } - - return { - "resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths) - }; -} - -/** Exported for testing purpose */ -export function readPaths(params: { rawSourceFile: string }): { - resourcesCommonFilePaths: string[]; -} { - const { rawSourceFile } = params; - - const resourcesCommonFilePaths = new Set(); - - { - const regexp = new RegExp(`resourcesCommonPath\\s*}([^\`]+)\``, "g"); - - const matches = [...rawSourceFile.matchAll(regexp)]; - - for (const match of matches) { - const filePath = match[1]; - - resourcesCommonFilePaths.add(filePath); - } - } - - { - const regexp = new RegExp(`resourcesCommonPath\\s*[+,]\\s*["']([^"'\`]+)["'\`]`, "g"); - - const matches = [...rawSourceFile.matchAll(regexp)]; - - for (const match of matches) { - const filePath = match[1]; - - resourcesCommonFilePaths.add(filePath); - } - } - - const normalizePath = (filePath: string) => { - filePath = filePath.startsWith("/") ? filePath.slice(1) : filePath; - filePath = filePath.replace(/\//g, pathSep); - return filePath; - }; - - return { - "resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths).map(normalizePath) - }; -} diff --git a/src/bin/keycloakify/keycloakify.ts b/src/bin/keycloakify/keycloakify.ts index 5e8f48ea..d8c2ffda 100644 --- a/src/bin/keycloakify/keycloakify.ts +++ b/src/bin/keycloakify/keycloakify.ts @@ -1,5 +1,5 @@ import { generateTheme } from "./generateTheme"; -import { generateJavaStackFiles } from "./generateJavaStackFiles"; +import { generatePom } from "./generatePom"; import { join as pathJoin, relative as pathRelative, basename as pathBasename, dirname as pathDirname, sep as pathSep } from "path"; import * as child_process from "child_process"; import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer"; @@ -9,7 +9,6 @@ import { getLogger } from "../tools/logger"; import { assert } from "tsafe/assert"; import { getThemeSrcDirPath } from "../getThemeSrcDirPath"; import { getProjectRoot } from "../tools/getProjectRoot"; -import { objectKeys } from "tsafe/objectKeys"; export async function main() { const reactAppRootDirPath = process.cwd(); @@ -42,25 +41,13 @@ export async function main() { }); } - const { jarFilePath } = await generateJavaStackFiles({ - "implementedThemeTypes": (() => { - const implementedThemeTypes = { - "login": false, - "account": false, - "email": false - }; + { + const { pomFileCode } = generatePom({ buildOptions }); - for (const themeType of objectKeys(implementedThemeTypes)) { - if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) { - continue; - } - implementedThemeTypes[themeType] = true; - } + fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8")); + } - return implementedThemeTypes; - })(), - buildOptions - }); + const jarFilePath = pathJoin(buildOptions.keycloakifyBuildDirPath, "target", `${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`); if (buildOptions.doCreateJar) { child_process.execSync("mvn clean install", { "cwd": buildOptions.keycloakifyBuildDirPath }); @@ -96,48 +83,16 @@ export async function main() { "", ...(!buildOptions.doCreateJar ? [] - : [ - `✅ Your keycloak theme has been generated and bundled into .${pathSep}${pathRelative(reactAppRootDirPath, jarFilePath)} 🚀`, - `It is to be placed in "/opt/keycloak/providers" in the container running a quay.io/keycloak/keycloak Docker image.`, - "" - ]), + : [`✅ Your keycloak theme has been generated and bundled into .${pathSep}${pathRelative(reactAppRootDirPath, jarFilePath)} 🚀`]), //TODO: Restore when we find a good Helm chart for Keycloak. //"Using Helm (https://github.com/codecentric/helm-charts), edit to reflect:", "", - "value.yaml: ", - " extraInitContainers: |", - " - name: realm-ext-provider", - " image: curlimages/curl", - " imagePullPolicy: IfNotPresent", - " command:", - " - sh", - " args:", - " - -c", - ` - curl -L -f -S -o /extensions/${pathBasename(jarFilePath)} https://AN.URL.FOR/${pathBasename(jarFilePath)}`, - " volumeMounts:", - " - name: extensions", - " mountPath: /extensions", - " ", - " extraVolumeMounts: |", - " - name: extensions", - " mountPath: /opt/keycloak/providers", - " extraEnv: |", - " - name: KEYCLOAK_USER", - " value: admin", - " - name: KEYCLOAK_PASSWORD", - " value: xxxxxxxxx", - " - name: JAVA_OPTS", - " value: -Dkeycloak.profile=preview", - "", - "", `To test your theme locally you can spin up a Keycloak ${containerKeycloakVersion} container image with the theme pre loaded by running:`, "", `👉 $ .${pathSep}${pathRelative( reactAppRootDirPath, pathJoin(buildOptions.keycloakifyBuildDirPath, generateStartKeycloakTestingContainer.basename) )} 👈`, - "", - `Test with different Keycloak versions by editing the .sh file. see available versions here: https://quay.io/repository/keycloak/keycloak?tab=tags`, ``, `Once your container is up and running: `, "- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈", diff --git a/src/bin/tools/downloadAndUnzip.ts b/src/bin/tools/downloadAndUnzip.ts index 4bff442b..d5c82015 100644 --- a/src/bin/tools/downloadAndUnzip.ts +++ b/src/bin/tools/downloadAndUnzip.ts @@ -1,12 +1,13 @@ import { exec as execCallback } from "child_process"; import { createHash } from "crypto"; -import { mkdir, readFile, stat, writeFile, unlink, rm } from "fs/promises"; +import { mkdir, readFile, stat, writeFile, unlink } from "fs/promises"; import fetch, { type FetchOptions } from "make-fetch-happen"; import { dirname as pathDirname, join as pathJoin, resolve as pathResolve, sep as pathSep } from "path"; import { assert } from "tsafe/assert"; import { promisify } from "util"; import { transformCodebase } from "./transformCodebase"; import { unzip, zip } from "./unzip"; +import { rm } from "../tools/fs.rm"; const exec = promisify(execCallback); diff --git a/src/bin/tools/fs.rm.ts b/src/bin/tools/fs.rm.ts new file mode 100644 index 00000000..d7d41f50 --- /dev/null +++ b/src/bin/tools/fs.rm.ts @@ -0,0 +1,43 @@ +import * as fs from "fs/promises"; +import { join as pathJoin } from "path"; +import { NpmModuleVersion } from "./NpmModuleVersion"; + +/** + * Polyfill of fs.rm(dirPath, { "recursive": true }) + * For older version of Node + */ +export async function rm(dirPath: string, options: { recursive: true; force?: true }) { + if (NpmModuleVersion.compare(NpmModuleVersion.parse(process.version), NpmModuleVersion.parse("14.14.0")) > 0) { + return fs.rm(dirPath, options); + } + + const { force = true } = options; + + if (force && !(await checkDirExists(dirPath))) { + return; + } + + const removeDir_rec = async (dirPath: string) => + Promise.all( + (await fs.readdir(dirPath)).map(async basename => { + const fileOrDirpath = pathJoin(dirPath, basename); + + if ((await fs.lstat(fileOrDirpath)).isDirectory()) { + await removeDir_rec(fileOrDirpath); + } else { + await fs.unlink(fileOrDirpath); + } + }) + ); + + await removeDir_rec(dirPath); +} + +async function checkDirExists(dirPath: string) { + try { + await fs.access(dirPath, fs.constants.F_OK); + return true; + } catch { + return false; + } +} diff --git a/src/bin/tools/fs.rmSync.ts b/src/bin/tools/fs.rmSync.ts new file mode 100644 index 00000000..ff7f5ff8 --- /dev/null +++ b/src/bin/tools/fs.rmSync.ts @@ -0,0 +1,33 @@ +import * as fs from "fs"; +import { join as pathJoin } from "path"; +import { NpmModuleVersion } from "./NpmModuleVersion"; + +/** + * Polyfill of fs.rmSync(dirPath, { "recursive": true }) + * For older version of Node + */ +export function rmSync(dirPath: string, options: { recursive: true; force?: true }) { + if (NpmModuleVersion.compare(NpmModuleVersion.parse(process.version), NpmModuleVersion.parse("14.14.0")) > 0) { + fs.rmSync(dirPath, options); + } + + const { force = true } = options; + + if (force && !fs.existsSync(dirPath)) { + return; + } + + const removeDir_rec = (dirPath: string) => + fs.readdirSync(dirPath).forEach(basename => { + const fileOrDirpath = pathJoin(dirPath, basename); + + if (fs.lstatSync(fileOrDirpath).isDirectory()) { + removeDir_rec(fileOrDirpath); + return; + } else { + fs.unlinkSync(fileOrDirpath); + } + }); + + removeDir_rec(dirPath); +} diff --git a/src/bin/tools/transformCodebase.ts b/src/bin/tools/transformCodebase.ts index 2064fe7d..5b59978e 100644 --- a/src/bin/tools/transformCodebase.ts +++ b/src/bin/tools/transformCodebase.ts @@ -2,6 +2,7 @@ import * as fs from "fs"; import * as path from "path"; import { crawl } from "./crawl"; import { id } from "tsafe/id"; +import { rmSync } from "../tools/fs.rmSync"; type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string; fileRelativePath: string }) => | { @@ -10,15 +11,25 @@ type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string; file } | undefined; -/** Apply a transformation function to every file of directory */ +/** + * Apply a transformation function to every file of directory + * If source and destination are the same this function can be used to apply the transformation in place + * like filtering out some files or modifying them. + * */ export function transformCodebase(params: { srcDirPath: string; destDirPath: string; transformSourceCode?: TransformSourceCode }) { const { srcDirPath, - destDirPath, transformSourceCode = id(({ sourceCode }) => ({ "modifiedSourceCode": sourceCode })) } = params; + let { destDirPath } = params; + + const isTargetSameAsSource = path.relative(srcDirPath, destDirPath) === ""; + + if (isTargetSameAsSource) { + destDirPath = path.join(srcDirPath, "..", "tmp_xOsPdkPsTdzPs34sOkHs"); + } for (const fileRelativePath of crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })) { const filePath = path.join(srcDirPath, fileRelativePath); @@ -44,4 +55,10 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str modifiedSourceCode ); } + + if (isTargetSameAsSource) { + rmSync(srcDirPath, { "recursive": true }); + + fs.renameSync(destDirPath, srcDirPath); + } }