diff --git a/src/account/kcContext/KcContext.ts b/src/account/kcContext/KcContext.ts index 6e57c97e..6187c77c 100644 --- a/src/account/kcContext/KcContext.ts +++ b/src/account/kcContext/KcContext.ts @@ -8,6 +8,7 @@ export declare namespace KcContext { export type Common = { keycloakifyVersion: string; themeType: "account"; + themeName: string; locale?: { supported: { url: string; diff --git a/src/account/kcContext/kcContextMocks.ts b/src/account/kcContext/kcContextMocks.ts index d6a82495..e423bba7 100644 --- a/src/account/kcContext/kcContextMocks.ts +++ b/src/account/kcContext/kcContextMocks.ts @@ -9,6 +9,7 @@ const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/"; export const kcContextCommonMock: KcContext.Common = { "keycloakifyVersion": "0.0.0", "themeType": "account", + "themeName": "my-theme-name", "url": { "resourcesPath": pathJoin(PUBLIC_URL, resourcesDirPathRelativeToPublicDir), "resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonDirPathRelativeToPublicDir), diff --git a/src/bin/keycloakify/BuildOptions.ts b/src/bin/keycloakify/BuildOptions.ts index d8fa05f5..84859b1c 100644 --- a/src/bin/keycloakify/BuildOptions.ts +++ b/src/bin/keycloakify/BuildOptions.ts @@ -16,6 +16,7 @@ export namespace BuildOptions { isSilent: boolean; themeVersion: string; themeName: string; + extraThemeNames: string[]; extraLoginPages: string[] | undefined; extraAccountPages: string[] | undefined; extraThemeProperties?: string[]; @@ -108,8 +109,17 @@ export function readBuildOptions(params: { projectDirPath: string; processArgv: const common: BuildOptions.Common = (() => { const { name, keycloakify = {}, version, homepage } = parsedPackageJson; - const { extraPages, extraLoginPages, extraAccountPages, extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets } = - keycloakify ?? {}; + const { + extraPages, + extraLoginPages, + extraAccountPages, + extraThemeProperties, + groupId, + artifactId, + bundler, + keycloakVersionDefaultAssets, + extraThemeNames = [] + } = keycloakify ?? {}; const themeName = keycloakify.themeName ?? @@ -120,6 +130,7 @@ export function readBuildOptions(params: { projectDirPath: string; processArgv: return { themeName, + extraThemeNames, "bundler": (() => { const { KEYCLOAKIFY_BUNDLER } = process.env; diff --git a/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl b/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl index c4e822ea..d4c58eae 100644 --- a/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl +++ b/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl @@ -122,6 +122,7 @@ out["keycloakifyVersion"] = "KEYCLOAKIFY_VERSION_xEdKd3xEdr"; out["themeVersion"] = "KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx"; out["themeType"] = "KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr"; + out["themeName"] = "KEYCLOAKIFY_THEME_NAME_cXxKd3xEer"; out["pageId"] = "${pageId}"; return out; diff --git a/src/bin/keycloakify/generateFtl/generateFtl.ts b/src/bin/keycloakify/generateFtl/generateFtl.ts index a227e682..7c635c5b 100644 --- a/src/bin/keycloakify/generateFtl/generateFtl.ts +++ b/src/bin/keycloakify/generateFtl/generateFtl.ts @@ -17,6 +17,7 @@ export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.Ex export namespace BuildOptionsLike { export type Common = { + themeName: string; customUserAttributes: string[]; themeVersion: string; }; @@ -134,7 +135,8 @@ export function generateFtlFilesCodeFactory(params: { ) .replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion) .replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion) - .replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType), + .replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType) + .replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", buildOptions.themeName), "": [ "<#if scripts??>", " <#list scripts as script>", diff --git a/src/bin/keycloakify/generateJavaStackFiles.ts b/src/bin/keycloakify/generateJavaStackFiles.ts index 0ee215df..f553c97c 100644 --- a/src/bin/keycloakify/generateJavaStackFiles.ts +++ b/src/bin/keycloakify/generateJavaStackFiles.ts @@ -7,6 +7,7 @@ import type { BuildOptions } from "./BuildOptions"; export type BuildOptionsLike = { themeName: string; + extraThemeNames: string[]; groupId: string; artifactId?: string; themeVersion: string; @@ -26,7 +27,7 @@ export function generateJavaStackFiles(params: { jarFilePath: string; } { const { - buildOptions: { groupId, themeName, themeVersion, artifactId }, + buildOptions: { groupId, themeName, extraThemeNames, themeVersion, artifactId }, keycloakThemeBuildingDirPath, doBundlesEmailTemplate } = params; @@ -67,12 +68,12 @@ export function generateJavaStackFiles(params: { Buffer.from( JSON.stringify( { - "themes": [ + "themes": [themeName, ...extraThemeNames].map(themeName => [ { "name": themeName, "types": [...themeTypes, ...(doBundlesEmailTemplate ? ["email"] : [])] } - ] + ]) }, null, 2 diff --git a/src/bin/keycloakify/generateStartKeycloakTestingContainer.ts b/src/bin/keycloakify/generateStartKeycloakTestingContainer.ts index 70f08d92..458834ae 100644 --- a/src/bin/keycloakify/generateStartKeycloakTestingContainer.ts +++ b/src/bin/keycloakify/generateStartKeycloakTestingContainer.ts @@ -6,6 +6,7 @@ import type { BuildOptions } from "./BuildOptions"; export type BuildOptionsLike = { themeName: string; + extraThemeNames: string[]; }; { @@ -27,11 +28,9 @@ export function generateStartKeycloakTestingContainer(params: { const { keycloakThemeBuildingDirPath, keycloakVersion, - buildOptions: { themeName } + buildOptions: { themeName, extraThemeNames } } = params; - const keycloakThemePath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName).replace(/\\/g, "/"); - fs.writeFileSync( pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename), @@ -49,7 +48,13 @@ export function generateStartKeycloakTestingContainer(params: { " -e KEYCLOAK_ADMIN=admin \\", " -e KEYCLOAK_ADMIN_PASSWORD=admin \\", " -e JAVA_OPTS=-Dkeycloak.profile=preview \\", - ` -v "${keycloakThemePath}":"/opt/keycloak/themes/${themeName}":rw \\`, + ...[themeName, ...extraThemeNames].map( + themeName => + ` -v "${pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName).replace( + /\\/g, + "/" + )}":"/opt/keycloak/themes/${themeName}":rw \\` + ), ` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`, ` start-dev`, "" diff --git a/src/bin/keycloakify/keycloakify.ts b/src/bin/keycloakify/keycloakify.ts index c92ec13f..49ce53bf 100644 --- a/src/bin/keycloakify/keycloakify.ts +++ b/src/bin/keycloakify/keycloakify.ts @@ -23,27 +23,38 @@ export async function main() { const logger = getLogger({ "isSilent": buildOptions.isSilent }); logger.log("🔏 Building the keycloak theme...⌚"); - const { doBundlesEmailTemplate } = await generateTheme({ - keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath, - "emailThemeSrcDirPath": (() => { - const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath({ projectDirPath }); + let doBundlesEmailTemplate: boolean | undefined; - if (emailThemeSrcDirPath === undefined || !fs.existsSync(emailThemeSrcDirPath)) { - return; - } + for (const themeName of [buildOptions.themeName, ...buildOptions.extraThemeNames]) { + const { doBundlesEmailTemplate: doBundlesEmailTemplate_ } = await generateTheme({ + keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath, + "emailThemeSrcDirPath": (() => { + const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath({ projectDirPath }); - return emailThemeSrcDirPath; - })(), - "reactAppBuildDirPath": buildOptions.reactAppBuildDirPath, - buildOptions, - "keycloakifyVersion": (() => { - const version = JSON.parse(fs.readFileSync(pathJoin(getProjectRoot(), "package.json")).toString("utf8"))["version"]; + if (emailThemeSrcDirPath === undefined || !fs.existsSync(emailThemeSrcDirPath)) { + return; + } - assert(typeof version === "string"); + return emailThemeSrcDirPath; + })(), + "reactAppBuildDirPath": buildOptions.reactAppBuildDirPath, + "buildOptions": { + ...buildOptions, + "themeName": themeName + }, + "keycloakifyVersion": (() => { + const version = JSON.parse(fs.readFileSync(pathJoin(getProjectRoot(), "package.json")).toString("utf8"))["version"]; - return version; - })() - }); + assert(typeof version === "string"); + + return version; + })() + }); + + doBundlesEmailTemplate ??= doBundlesEmailTemplate_; + } + + assert(doBundlesEmailTemplate !== undefined); const { jarFilePath } = generateJavaStackFiles({ keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath, diff --git a/src/bin/keycloakify/parsedPackageJson.ts b/src/bin/keycloakify/parsedPackageJson.ts index 6ee593fd..f2acec1f 100644 --- a/src/bin/keycloakify/parsedPackageJson.ts +++ b/src/bin/keycloakify/parsedPackageJson.ts @@ -25,6 +25,7 @@ export type ParsedPackageJson = { keycloakifyBuildDirPath?: string; customUserAttributes?: string[]; themeName?: string; + extraThemeNames?: string[]; }; }; @@ -46,7 +47,8 @@ export const zParsedPackageJson = z.object({ "reactAppBuildDirPath": z.string().optional(), "keycloakifyBuildDirPath": z.string().optional(), "customUserAttributes": z.array(z.string()).optional(), - "themeName": z.string().optional() + "themeName": z.string().optional(), + "extraThemeNames": z.array(z.string()).optional() }) .optional() }); diff --git a/src/login/kcContext/KcContext.ts b/src/login/kcContext/KcContext.ts index c918711e..075f4e51 100644 --- a/src/login/kcContext/KcContext.ts +++ b/src/login/kcContext/KcContext.ts @@ -39,6 +39,7 @@ export declare namespace KcContext { export type Common = { keycloakifyVersion: string; themeType: "login"; + themeName: string; url: { loginAction: string; resourcesPath: string; diff --git a/src/login/kcContext/kcContextMocks.ts b/src/login/kcContext/kcContextMocks.ts index 915db2e4..62c1be4a 100644 --- a/src/login/kcContext/kcContextMocks.ts +++ b/src/login/kcContext/kcContextMocks.ts @@ -105,6 +105,7 @@ const attributesByName = Object.fromEntries(attributes.map(attribute => [attribu export const kcContextCommonMock: KcContext.Common = { "keycloakifyVersion": "0.0.0", "themeType": "login", + "themeName": "my-theme-name", "url": { "loginAction": "#", "resourcesPath": pathJoin(PUBLIC_URL, resourcesDirPathRelativeToPublicDir),