diff --git a/src/bin/add-story.ts b/src/bin/add-story.ts index a4e39aab..23a1df4b 100644 --- a/src/bin/add-story.ts +++ b/src/bin/add-story.ts @@ -13,7 +13,6 @@ import * as fs from "fs"; import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase"; import { assert, Equals } from "tsafe/assert"; -import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath"; import type { CliCommandOptions } from "./main"; import { getBuildContext } from "./shared/buildContext"; import chalk from "chalk"; @@ -53,17 +52,13 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) console.log(`→ ${pageId}`); - const { themeSrcDirPath } = getThemeSrcDirPath({ - projectDirPath: buildContext.projectDirPath - }); - const componentBasename = capitalize(kebabCaseToCamelCase(pageId)).replace( /ftl$/, "stories.tsx" ); const targetFilePath = pathJoin( - themeSrcDirPath, + buildContext.themeSrcDirPath, themeType, "pages", componentBasename diff --git a/src/bin/eject-page.ts b/src/bin/eject-page.ts index 01df60b3..b68336ca 100644 --- a/src/bin/eject-page.ts +++ b/src/bin/eject-page.ts @@ -15,7 +15,6 @@ import * as fs from "fs"; import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase"; import { assert, Equals } from "tsafe/assert"; -import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath"; import type { CliCommandOptions } from "./main"; import { getBuildContext } from "./shared/buildContext"; import chalk from "chalk"; @@ -68,10 +67,6 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) console.log(`→ ${pageIdOrComponent}`); - const { themeSrcDirPath } = getThemeSrcDirPath({ - projectDirPath: buildContext.projectDirPath - }); - const componentBasename = (() => { if (pageIdOrComponent === templateValue) { return "Template.tsx"; @@ -96,7 +91,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) })(); const targetFilePath = pathJoin( - themeSrcDirPath, + buildContext.themeSrcDirPath, themeType, pagesOrDot, componentBasename @@ -149,7 +144,11 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) break edit_KcApp; } - const kcAppTsxPath = pathJoin(themeSrcDirPath, themeType, "KcPage.tsx"); + const kcAppTsxPath = pathJoin( + buildContext.themeSrcDirPath, + themeType, + "KcPage.tsx" + ); const kcAppTsxCode = fs.readFileSync(kcAppTsxPath).toString("utf8"); @@ -199,7 +198,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) `${chalk.bold( pathJoin( ".", - pathRelative(process.cwd(), themeSrcDirPath), + pathRelative(process.cwd(), buildContext.themeSrcDirPath), themeType, "KcPage.tsx" ) diff --git a/src/bin/initialize-email-theme.ts b/src/bin/initialize-email-theme.ts index 29546b1f..97128d22 100644 --- a/src/bin/initialize-email-theme.ts +++ b/src/bin/initialize-email-theme.ts @@ -4,7 +4,6 @@ import { transformCodebase } from "./tools/transformCodebase"; import { promptKeycloakVersion } from "./shared/promptKeycloakVersion"; import { getBuildContext } from "./shared/buildContext"; import * as fs from "fs"; -import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath"; import type { CliCommandOptions } from "./main"; export async function command(params: { cliCommandOptions: CliCommandOptions }) { @@ -12,11 +11,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) const buildContext = getBuildContext({ cliCommandOptions }); - const { themeSrcDirPath } = getThemeSrcDirPath({ - projectDirPath: buildContext.projectDirPath - }); - - const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email"); + const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email"); if (fs.existsSync(emailThemeSrcDirPath)) { console.warn( diff --git a/src/bin/keycloakify/buildJars/buildJars.ts b/src/bin/keycloakify/buildJars/buildJars.ts index 7ac24d0e..4fb2be9a 100644 --- a/src/bin/keycloakify/buildJars/buildJars.ts +++ b/src/bin/keycloakify/buildJars/buildJars.ts @@ -1,5 +1,4 @@ import { assert } from "tsafe/assert"; -import { exclude } from "tsafe/exclude"; import { keycloakAccountV1Versions, keycloakThemeAdditionalInfoExtensionVersions @@ -7,32 +6,29 @@ import { import { getKeycloakVersionRangeForJar } from "./getKeycloakVersionRangeForJar"; import { buildJar, BuildContextLike as BuildContextLike_buildJar } from "./buildJar"; import type { BuildContext } from "../../shared/buildContext"; -import { getJarFileBasename } from "../../shared/getJarFileBasename"; -import { getImplementedThemeTypes } from "../../shared/getImplementedThemeTypes"; export type BuildContextLike = BuildContextLike_buildJar & { projectDirPath: string; keycloakifyBuildDirPath: string; + recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"]; + jarTargets: BuildContext["jarTargets"]; }; assert(); export async function buildJars(params: { resourcesDirPath: string; - onlyBuildJarFileBasename: string | undefined; buildContext: BuildContextLike; }): Promise { - const { onlyBuildJarFileBasename, resourcesDirPath, buildContext } = params; + const { resourcesDirPath, buildContext } = params; - const doesImplementAccountTheme = getImplementedThemeTypes({ - projectDirPath: buildContext.projectDirPath - }).implementedThemeTypes.account; + const doesImplementAccountTheme = buildContext.recordIsImplementedByThemeType.account; await Promise.all( keycloakAccountV1Versions .map(keycloakAccountV1Version => - keycloakThemeAdditionalInfoExtensionVersions - .map(keycloakThemeAdditionalInfoExtensionVersion => { + keycloakThemeAdditionalInfoExtensionVersions.map( + keycloakThemeAdditionalInfoExtensionVersion => { const keycloakVersionRange = getKeycloakVersionRangeForJar({ doesImplementAccountTheme, keycloakAccountV1Version, @@ -43,48 +39,26 @@ export async function buildJars(params: { return undefined; } - return { - keycloakThemeAdditionalInfoExtensionVersion, - keycloakVersionRange - }; - }) - .filter(exclude(undefined)) - .map( - ({ - keycloakThemeAdditionalInfoExtensionVersion, - keycloakVersionRange - }) => { - const { jarFileBasename } = getJarFileBasename({ - keycloakVersionRange - }); + const jarTarget = buildContext.jarTargets.find( + jarTarget => + jarTarget.keycloakVersionRange === keycloakVersionRange + ); - if ( - onlyBuildJarFileBasename !== undefined && - onlyBuildJarFileBasename !== jarFileBasename - ) { - return undefined; - } - - return { - keycloakThemeAdditionalInfoExtensionVersion, - jarFileBasename - }; + if (jarTarget === undefined) { + return undefined; } - ) - .filter(exclude(undefined)) - .map( - ({ + + const { jarFileBasename } = jarTarget; + + return buildJar({ + jarFileBasename, + keycloakAccountV1Version, keycloakThemeAdditionalInfoExtensionVersion, - jarFileBasename - }) => - buildJar({ - jarFileBasename, - keycloakAccountV1Version, - keycloakThemeAdditionalInfoExtensionVersion, - resourcesDirPath, - buildContext - }) - ) + resourcesDirPath, + buildContext + }); + } + ) ) .flat() ); diff --git a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts index 654fe288..15c5eb7c 100644 --- a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts +++ b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts @@ -29,7 +29,6 @@ import { bringInAccountV1, type BuildContextLike as BuildContextLike_bringInAccountV1 } from "./bringInAccountV1"; -import { getThemeSrcDirPath } from "../../shared/getThemeSrcDirPath"; import { rmSync } from "../../tools/fs.rmSync"; import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion"; import { @@ -38,7 +37,6 @@ import { } from "../../shared/metaInfKeycloakThemes"; import { objectEntries } from "tsafe/objectEntries"; import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile"; -import { getImplementedThemeTypes } from "../../shared/getImplementedThemeTypes"; export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode & BuildContextLike_downloadKeycloakStaticResources & @@ -48,6 +46,8 @@ export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode & projectDirPath: string; projectBuildDirPath: string; environmentVariables: { name: string; default: string }[]; + recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"]; + themeSrcDirPath: string; }; assert(); @@ -59,14 +59,6 @@ export async function generateResourcesForMainTheme(params: { }): Promise { const { themeName, resourcesDirPath, buildContext } = params; - const { themeSrcDirPath } = getThemeSrcDirPath({ - projectDirPath: buildContext.projectDirPath - }); - - const { implementedThemeTypes } = getImplementedThemeTypes({ - projectDirPath: buildContext.projectDirPath - }); - const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => { const { themeType } = params; return pathJoin(resourcesDirPath, "theme", themeName, themeType); @@ -75,7 +67,7 @@ export async function generateResourcesForMainTheme(params: { const cssGlobalsToDefine: Record = {}; for (const themeType of ["login", "account"] as const) { - if (!implementedThemeTypes[themeType]) { + if (!buildContext.recordIsImplementedByThemeType[themeType]) { continue; } @@ -91,7 +83,10 @@ export async function generateResourcesForMainTheme(params: { // 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) { + if ( + themeType === "account" && + buildContext.recordIsImplementedByThemeType.login + ) { // NOTE: We prevent doing it twice, it has been done for the login theme. transformCodebase({ @@ -178,7 +173,7 @@ export async function generateResourcesForMainTheme(params: { keycloakifyVersion: readThisNpmPackageVersion(), themeType, fieldNames: readFieldNameUsage({ - themeSrcDirPath, + themeSrcDirPath: buildContext.themeSrcDirPath, themeType }) }); @@ -194,7 +189,7 @@ export async function generateResourcesForMainTheme(params: { })(), ...readExtraPagesNames({ themeType, - themeSrcDirPath + themeSrcDirPath: buildContext.themeSrcDirPath }) ].forEach(pageId => { const { ftlCode } = generateFtlFilesCode({ pageId }); @@ -206,7 +201,7 @@ export async function generateResourcesForMainTheme(params: { }); generateMessageProperties({ - themeSrcDirPath, + themeSrcDirPath: buildContext.themeSrcDirPath, themeType }).forEach(({ languageTag, propertiesFileSource }) => { const messagesDirPath = pathJoin(themeTypeDirPath, "messages"); @@ -265,11 +260,11 @@ export async function generateResourcesForMainTheme(params: { } email: { - if (!implementedThemeTypes.email) { + if (!buildContext.recordIsImplementedByThemeType.email) { break email; } - const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email"); + const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email"); transformCodebase({ srcDirPath: emailThemeSrcDirPath, @@ -277,7 +272,7 @@ export async function generateResourcesForMainTheme(params: { }); } - if (implementedThemeTypes.account) { + if (buildContext.recordIsImplementedByThemeType.account) { await bringInAccountV1({ resourcesDirPath, buildContext @@ -289,12 +284,12 @@ export async function generateResourcesForMainTheme(params: { metaInfKeycloakThemes.themes.push({ name: themeName, - types: objectEntries(implementedThemeTypes) + types: objectEntries(buildContext.recordIsImplementedByThemeType) .filter(([, isImplemented]) => isImplemented) .map(([themeType]) => themeType) }); - if (implementedThemeTypes.account) { + if (buildContext.recordIsImplementedByThemeType.account) { metaInfKeycloakThemes.themes.push({ name: accountV1ThemeName, types: ["account"] diff --git a/src/bin/keycloakify/keycloakify.ts b/src/bin/keycloakify/keycloakify.ts index e5b17673..5de98472 100644 --- a/src/bin/keycloakify/keycloakify.ts +++ b/src/bin/keycloakify/keycloakify.ts @@ -3,10 +3,7 @@ import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path import * as child_process from "child_process"; import * as fs from "fs"; import { getBuildContext } from "../shared/buildContext"; -import { - vitePluginSubScriptEnvNames, - onlyBuildJarFileBasenameEnvName -} from "../shared/constants"; +import { vitePluginSubScriptEnvNames } from "../shared/constants"; import { buildJars } from "./buildJars"; import type { CliCommandOptions } from "../main"; import chalk from "chalk"; @@ -96,16 +93,17 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) cwd: buildContext.projectDirPath, env: { ...process.env, - [vitePluginSubScriptEnvNames.runPostBuildScript]: - JSON.stringify(buildContext) + [vitePluginSubScriptEnvNames.runPostBuildScript]: JSON.stringify({ + resourcesDirPath, + buildContext + }) } }); } await buildJars({ resourcesDirPath, - buildContext, - onlyBuildJarFileBasename: process.env[onlyBuildJarFileBasenameEnvName] + buildContext }); rmSync(resourcesDirPath, { recursive: true }); diff --git a/src/bin/shared/buildContext.ts b/src/bin/shared/buildContext.ts index a0515fb1..0da4186a 100644 --- a/src/bin/shared/buildContext.ts +++ b/src/bin/shared/buildContext.ts @@ -5,9 +5,22 @@ import { getNpmWorkspaceRootDirPath } from "../tools/getNpmWorkspaceRootDirPath" import type { CliCommandOptions } from "../main"; import { z } from "zod"; import * as fs from "fs"; -import { assert, type Equals } from "tsafe"; +import { assert, type Equals } from "tsafe/assert"; import * as child_process from "child_process"; -import { vitePluginSubScriptEnvNames } from "./constants"; +import { + vitePluginSubScriptEnvNames, + buildForKeycloakMajorVersionEnvName +} from "./constants"; +import type { KeycloakVersionRange } from "./KeycloakVersionRange"; +import { exclude } from "tsafe"; +import { crawl } from "../tools/crawl"; +import { themeTypes } from "./constants"; +import { objectFromEntries } from "tsafe/objectFromEntries"; +import { objectEntries } from "tsafe/objectEntries"; +import { type ThemeType } from "./constants"; +import { id } from "tsafe/id"; +import { symToStr } from "tsafe/symToStr"; +import chalk from "chalk"; export type BuildContext = { bundler: "vite" | "webpack"; @@ -30,6 +43,12 @@ export type BuildContext = { npmWorkspaceRootDirPath: string; kcContextExclusionsFtlCode: string | undefined; environmentVariables: { name: string; default: string }[]; + themeSrcDirPath: string; + recordIsImplementedByThemeType: Readonly>; + jarTargets: { + keycloakVersionRange: KeycloakVersionRange; + jarFileBasename: string; + }[]; }; export type BuildOptions = { @@ -41,8 +60,21 @@ export type BuildOptions = { loginThemeResourcesFromKeycloakVersion?: string; keycloakifyBuildDirPath?: string; kcContextExclusionsFtl?: string; + jarTargets?: BuildOptions.JarTargets; }; +export namespace BuildOptions { + export type JarTargets = + | ({ hasAccountTheme: true } & Record< + KeycloakVersionRange.WithAccountTheme, + string | boolean + >) + | ({ hasAccountTheme: false } & Record< + KeycloakVersionRange.WithoutAccountTheme, + string | boolean + >); +} + export type ResolvedViteConfig = { buildDir: string; publicDir: string; @@ -102,7 +134,7 @@ export function getBuildContext(params: { })(); const parsedPackageJson = (() => { - type WebpackSpecificBuildOptions = { + type BuildOptions_packageJson = BuildOptions & { projectBuildDirPath?: string; }; @@ -110,49 +142,75 @@ export function getBuildContext(params: { name: string; version?: string; homepage?: string; - keycloakify?: { - themeName?: string | string[]; - environmentVariables?: { name: string; default: string }[]; - extraThemeProperties?: string[]; - artifactId?: string; - groupId?: string; - loginThemeResourcesFromKeycloakVersion?: string; - keycloakifyBuildDirPath?: string; - kcContextExclusionsFtl?: string; - projectBuildDirPath?: string; - }; + keycloakify?: BuildOptions_packageJson; }; - { - type Got = NonNullable; - type Expected = BuildOptions & WebpackSpecificBuildOptions; - assert>(); - } - const zParsedPackageJson = z.object({ name: z.string(), version: z.string().optional(), homepage: z.string().optional(), - keycloakify: z - .object({ - extraThemeProperties: z.array(z.string()).optional(), - artifactId: z.string().optional(), - groupId: z.string().optional(), - loginThemeResourcesFromKeycloakVersion: z.string().optional(), - projectBuildDirPath: z.string().optional(), - keycloakifyBuildDirPath: z.string().optional(), - kcContextExclusionsFtl: z.string().optional(), - environmentVariables: z - .array( - z.object({ - name: z.string(), - default: z.string() - }) - ) - .optional(), - themeName: z.union([z.string(), z.array(z.string())]).optional() - }) - .optional() + keycloakify: id>( + (() => { + const zBuildOptions_packageJson = z.object({ + extraThemeProperties: z.array(z.string()).optional(), + artifactId: z.string().optional(), + groupId: z.string().optional(), + loginThemeResourcesFromKeycloakVersion: z.string().optional(), + projectBuildDirPath: z.string().optional(), + keycloakifyBuildDirPath: z.string().optional(), + kcContextExclusionsFtl: z.string().optional(), + environmentVariables: z + .array( + z.object({ + name: z.string(), + default: z.string() + }) + ) + .optional(), + themeName: z.union([z.string(), z.array(z.string())]).optional(), + jarTargets: id>( + (() => { + const zJarTargets = z.union([ + z.object({ + hasAccountTheme: z.literal(true), + "21-and-below": z.union([ + z.boolean(), + z.string() + ]), + "23": z.union([z.boolean(), z.string()]), + "24": z.union([z.boolean(), z.string()]), + "25-and-above": z.union([z.boolean(), z.string()]) + }), + z.object({ + hasAccountTheme: z.literal(false), + "21-and-below": z.union([ + z.boolean(), + z.string() + ]), + "22-and-above": z.union([z.boolean(), z.string()]) + }) + ]); + + { + type Got = z.infer; + type Expected = BuildOptions.JarTargets; + assert>(); + } + + return zJarTargets; + })() + ).optional() + }); + + { + type Got = z.infer; + type Expected = BuildOptions_packageJson; + assert>(); + } + + return zBuildOptions_packageJson; + })() + ).optional() }); { @@ -173,6 +231,54 @@ export function getBuildContext(params: { ...resolvedViteConfig?.buildOptions }; + const { themeSrcDirPath } = (() => { + const srcDirPath = pathJoin(projectDirPath, "src"); + + const themeSrcDirPath: string | undefined = crawl({ + dirPath: srcDirPath, + returnedPathsType: "relative to dirPath" + }) + .map(fileRelativePath => { + for (const themeSrcDirBasename of ["keycloak-theme", "keycloak_theme"]) { + const split = fileRelativePath.split(themeSrcDirBasename); + if (split.length === 2) { + return pathJoin(srcDirPath, split[0] + themeSrcDirBasename); + } + } + return undefined; + }) + .filter(exclude(undefined))[0]; + + if (themeSrcDirPath !== undefined) { + return { themeSrcDirPath }; + } + + for (const themeType of [...themeTypes, "email"]) { + if (!fs.existsSync(pathJoin(srcDirPath, themeType))) { + continue; + } + return { themeSrcDirPath: srcDirPath }; + } + + console.log( + chalk.red( + [ + "Can't locate your keycloak theme source directory.", + "See: https://docs.keycloakify.dev/v/v10/keycloakify-in-my-app/collocation" + ].join("\n") + ) + ); + + process.exit(1); + })(); + + const recordIsImplementedByThemeType = objectFromEntries( + (["login", "account", "email"] as const).map(themeType => [ + themeType, + fs.existsSync(pathJoin(themeSrcDirPath, themeType)) + ]) + ); + const themeNames = ((): [string, ...string[]] => { if (buildOptions.themeName === undefined) { return [ @@ -218,8 +324,10 @@ export function getBuildContext(params: { dependencyExpected: "keycloakify" }); + const bundler = resolvedViteConfig !== undefined ? "vite" : "webpack"; + return { - bundler: resolvedViteConfig !== undefined ? "vite" : "webpack", + bundler, themeVersion: process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0", themeNames, @@ -349,6 +457,257 @@ export function getBuildContext(params: { return buildOptions.kcContextExclusionsFtl; })(), - environmentVariables: buildOptions.environmentVariables ?? [] + environmentVariables: buildOptions.environmentVariables ?? [], + recordIsImplementedByThemeType, + themeSrcDirPath, + jarTargets: (() => { + const getJarFileBasename = (range: string) => + `keycloak-theme-for-kc-${range}.jar`; + + build_for_specific_keycloak_major_version: { + const buildForKeycloakMajorVersionNumber = (() => { + const envValue = process.env[buildForKeycloakMajorVersionEnvName]; + + if (envValue === undefined) { + return undefined; + } + + const major = parseInt(envValue); + + assert(!isNaN(major)); + + return major; + })(); + + if (buildForKeycloakMajorVersionNumber === undefined) { + break build_for_specific_keycloak_major_version; + } + + const keycloakVersionRange: KeycloakVersionRange = (() => { + const doesImplementAccountTheme = + recordIsImplementedByThemeType.account; + + if (doesImplementAccountTheme) { + const keycloakVersionRange = (() => { + if (buildForKeycloakMajorVersionNumber <= 21) { + return "21-and-below" as const; + } + + assert(buildForKeycloakMajorVersionNumber !== 22); + + if (buildForKeycloakMajorVersionNumber === 23) { + return "23" as const; + } + + if (buildForKeycloakMajorVersionNumber === 24) { + return "24" as const; + } + + return "25-and-above" as const; + })(); + + assert< + Equals< + typeof keycloakVersionRange, + KeycloakVersionRange.WithAccountTheme + > + >(); + + return keycloakVersionRange; + } else { + const keycloakVersionRange = (() => { + if (buildForKeycloakMajorVersionNumber <= 21) { + return "21-and-below" as const; + } + + return "22-and-above" as const; + })(); + + assert< + Equals< + typeof keycloakVersionRange, + KeycloakVersionRange.WithoutAccountTheme + > + >(); + + return keycloakVersionRange; + } + })(); + + return [ + { + keycloakVersionRange, + jarFileBasename: getJarFileBasename(keycloakVersionRange) + } + ]; + } + + const jarTargets_default = (() => { + const jarTargets: BuildContext["jarTargets"] = []; + + if (recordIsImplementedByThemeType.account) { + for (const keycloakVersionRange of [ + "21-and-below", + "23", + "24", + "25-and-above" + ] as const) { + assert< + Equals< + typeof keycloakVersionRange, + KeycloakVersionRange.WithAccountTheme + > + >(true); + jarTargets.push({ + keycloakVersionRange, + jarFileBasename: getJarFileBasename(keycloakVersionRange) + }); + } + } else { + for (const keycloakVersionRange of [ + "21-and-below", + "22-and-above" + ] as const) { + assert< + Equals< + typeof keycloakVersionRange, + KeycloakVersionRange.WithoutAccountTheme + > + >(true); + jarTargets.push({ + keycloakVersionRange, + jarFileBasename: getJarFileBasename(keycloakVersionRange) + }); + } + } + + return jarTargets; + })(); + + if (buildOptions.jarTargets === undefined) { + return jarTargets_default; + } + + if ( + buildOptions.jarTargets.hasAccountTheme !== + recordIsImplementedByThemeType.account + ) { + console.log( + chalk.red( + (() => { + const { jarTargets } = buildOptions; + + let message = `Bad ${symToStr({ jarTargets })} configuration.\n`; + + if (jarTargets.hasAccountTheme) { + message += + "Your codebase does not seem to implement an account theme "; + } else { + message += "Your codebase implements an account theme "; + } + + const { hasAccountTheme } = jarTargets; + + message += `but you have set ${symToStr({ jarTargets })}.${symToStr({ hasAccountTheme })}`; + message += ` to ${hasAccountTheme} in your `; + message += (() => { + switch (bundler) { + case "vite": + return "vite.config.ts"; + case "webpack": + return "package.json"; + } + assert>(false); + })(); + message += `. Please set it to ${!hasAccountTheme} `; + message += + "and fill up the relevant keycloak version ranges.\n"; + message += "Example:\n"; + message += JSON.stringify( + id>({ + jarTargets: { + hasAccountTheme: + recordIsImplementedByThemeType.account, + ...objectFromEntries( + jarTargets_default.map( + ({ + keycloakVersionRange, + jarFileBasename + }) => [ + keycloakVersionRange, + jarFileBasename + ] + ) + ) + } + }), + null, + 2 + ); + + return message; + })() + ) + ); + + process.exit(1); + } + + const jarTargets: BuildContext["jarTargets"] = []; + + const { hasAccountTheme, ...rest } = buildOptions.jarTargets; + + for (const [keycloakVersionRange, jarNameOrBoolean] of objectEntries(rest)) { + if (jarNameOrBoolean === false) { + continue; + } + + if (jarNameOrBoolean === true) { + jarTargets.push({ + keycloakVersionRange: keycloakVersionRange, + jarFileBasename: getJarFileBasename(keycloakVersionRange) + }); + continue; + } + + const jarFileBasename = jarNameOrBoolean; + + if (!jarFileBasename.endsWith(".jar")) { + console.log( + chalk.red(`Bad ${jarFileBasename} should end with '.jar'\n`) + ); + process.exit(1); + } + + if (jarFileBasename.includes("/") || jarFileBasename.includes("\\")) { + console.log( + chalk.red( + [ + `Invalid ${jarFileBasename}. It's not supposed to be a path,`, + `Only the basename of the jar file is expected.`, + `Example: keycloak-theme.jar` + ].join(" ") + ) + ); + process.exit(1); + } + + jarTargets.push({ + keycloakVersionRange: keycloakVersionRange, + jarFileBasename: jarNameOrBoolean + }); + } + + if (jarTargets.length === 0) { + console.log( + chalk.red( + "All jar targets are disabled. Please enable at least one jar target." + ) + ); + process.exit(1); + } + + return jarTargets; + })() }; } diff --git a/src/bin/shared/constants.ts b/src/bin/shared/constants.ts index ca6d4b5d..0e56e17f 100644 --- a/src/bin/shared/constants.ts +++ b/src/bin/shared/constants.ts @@ -15,7 +15,8 @@ export const vitePluginSubScriptEnvNames = { resolveViteConfig: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG" } as const; -export const onlyBuildJarFileBasenameEnvName = "KEYCLOAKIFY_ONLY_BUILD_JAR_FILE_BASENAME"; +export const buildForKeycloakMajorVersionEnvName = + "KEYCLOAKIFY_BUILD_FOR_KEYCLOAK_MAJOR_VERSION"; export const loginThemePageIds = [ "login.ftl", diff --git a/src/bin/shared/generateKcGenTs.ts b/src/bin/shared/generateKcGenTs.ts index 7a437b97..61042913 100644 --- a/src/bin/shared/generateKcGenTs.ts +++ b/src/bin/shared/generateKcGenTs.ts @@ -1,6 +1,5 @@ import { assert } from "tsafe/assert"; import type { BuildContext } from "./buildContext"; -import { getThemeSrcDirPath } from "./getThemeSrcDirPath"; import * as fs from "fs/promises"; import { join as pathJoin } from "path"; import { existsAsync } from "../tools/fs.existsAsync"; @@ -9,6 +8,7 @@ export type BuildContextLike = { projectDirPath: string; themeNames: string[]; environmentVariables: { name: string; default: string }[]; + themeSrcDirPath: string; }; assert(); @@ -18,11 +18,7 @@ export async function generateKcGenTs(params: { }): Promise { const { buildContext } = params; - const { themeSrcDirPath } = getThemeSrcDirPath({ - projectDirPath: buildContext.projectDirPath - }); - - const filePath = pathJoin(themeSrcDirPath, "kc.gen.ts"); + const filePath = pathJoin(buildContext.themeSrcDirPath, "kc.gen.ts"); const currentContent = (await existsAsync(filePath)) ? await fs.readFile(filePath) diff --git a/src/bin/shared/getImplementedThemeTypes.ts b/src/bin/shared/getImplementedThemeTypes.ts deleted file mode 100644 index 0a8955ab..00000000 --- a/src/bin/shared/getImplementedThemeTypes.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { join as pathJoin } from "path"; -import { objectFromEntries } from "tsafe/objectFromEntries"; -import * as fs from "fs"; -import { type ThemeType } from "./constants"; -import { getThemeSrcDirPath } from "./getThemeSrcDirPath"; - -type ImplementedThemeTypes = Readonly>; - -let cache: - | { projectDirPath: string; implementedThemeTypes: ImplementedThemeTypes } - | undefined; - -export function getImplementedThemeTypes(params: { projectDirPath: string }) { - const { projectDirPath } = params; - - if (cache !== undefined && cache.projectDirPath === projectDirPath) { - const { implementedThemeTypes } = cache; - return { implementedThemeTypes }; - } - - cache = undefined; - - const { themeSrcDirPath } = getThemeSrcDirPath({ - projectDirPath - }); - - const implementedThemeTypes: Readonly> = - objectFromEntries( - (["login", "account", "email"] as const).map(themeType => [ - themeType, - fs.existsSync(pathJoin(themeSrcDirPath, themeType)) - ]) - ); - - cache = { projectDirPath, implementedThemeTypes }; - - return { implementedThemeTypes }; -} diff --git a/src/bin/shared/getJarFileBasename.ts b/src/bin/shared/getJarFileBasename.ts deleted file mode 100644 index 5baf8524..00000000 --- a/src/bin/shared/getJarFileBasename.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { KeycloakVersionRange } from "./KeycloakVersionRange"; - -export function getJarFileBasename(params: { - keycloakVersionRange: KeycloakVersionRange; -}) { - const { keycloakVersionRange } = params; - - const jarFileBasename = `keycloak-theme-for-kc-${keycloakVersionRange}.jar`; - - return { jarFileBasename }; -} diff --git a/src/bin/shared/getThemeSrcDirPath.ts b/src/bin/shared/getThemeSrcDirPath.ts deleted file mode 100644 index 0e415619..00000000 --- a/src/bin/shared/getThemeSrcDirPath.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as fs from "fs"; -import { exclude } from "tsafe"; -import { crawl } from "../tools/crawl"; -import { join as pathJoin } from "path"; -import { themeTypes } from "./constants"; -import chalk from "chalk"; - -let cache: { projectDirPath: string; themeSrcDirPath: string } | undefined = undefined; - -/** Can't catch error, if the directory isn't found, this function will just exit the process with an error message. */ -export function getThemeSrcDirPath(params: { projectDirPath: string }) { - const { projectDirPath } = params; - - if (cache !== undefined && cache.projectDirPath === projectDirPath) { - const { themeSrcDirPath } = cache; - return { themeSrcDirPath }; - } - - cache = undefined; - - const { themeSrcDirPath } = (() => { - const srcDirPath = pathJoin(projectDirPath, "src"); - - const themeSrcDirPath: string | undefined = crawl({ - dirPath: srcDirPath, - returnedPathsType: "relative to dirPath" - }) - .map(fileRelativePath => { - for (const themeSrcDirBasename of themeSrcDirBasenames) { - const split = fileRelativePath.split(themeSrcDirBasename); - if (split.length === 2) { - return pathJoin(srcDirPath, split[0] + themeSrcDirBasename); - } - } - return undefined; - }) - .filter(exclude(undefined))[0]; - - if (themeSrcDirPath !== undefined) { - return { themeSrcDirPath }; - } - - for (const themeType of [...themeTypes, "email"]) { - if (!fs.existsSync(pathJoin(srcDirPath, themeType))) { - continue; - } - return { themeSrcDirPath: srcDirPath }; - } - - console.log( - chalk.red("Can't locate your theme source directory. It should be either: ") - ); - - process.exit(-1); - })(); - - cache = { projectDirPath, themeSrcDirPath }; - - return { themeSrcDirPath }; -} - -const themeSrcDirBasenames = ["keycloak-theme", "keycloak_theme"]; diff --git a/src/bin/start-keycloak/keycloakifyBuild.ts b/src/bin/start-keycloak/keycloakifyBuild.ts index 7716faed..f8ecc7eb 100644 --- a/src/bin/start-keycloak/keycloakifyBuild.ts +++ b/src/bin/start-keycloak/keycloakifyBuild.ts @@ -1,4 +1,4 @@ -import { onlyBuildJarFileBasenameEnvName } from "../shared/constants"; +import { buildForKeycloakMajorVersionEnvName } from "../shared/constants"; import * as child_process from "child_process"; import { Deferred } from "evt/tools/Deferred"; import { assert } from "tsafe/assert"; @@ -14,10 +14,10 @@ export type BuildContextLike = { assert(); export async function keycloakifyBuild(params: { - onlyBuildJarFileBasename: string; + buildForKeycloakMajorVersionNumber: number; buildContext: BuildContextLike; }): Promise<{ isKeycloakifyBuildSuccess: boolean }> { - const { buildContext, onlyBuildJarFileBasename } = params; + const { buildForKeycloakMajorVersionNumber, buildContext } = params; const dResult = new Deferred<{ isSuccess: boolean }>(); @@ -25,7 +25,7 @@ export async function keycloakifyBuild(params: { cwd: buildContext.projectDirPath, env: { ...process.env, - [onlyBuildJarFileBasenameEnvName]: onlyBuildJarFileBasename + [buildForKeycloakMajorVersionEnvName]: `${buildForKeycloakMajorVersionNumber}` }, shell: true }); diff --git a/src/bin/start-keycloak/start-keycloak.ts b/src/bin/start-keycloak/start-keycloak.ts index ef8553f3..0ea8921e 100644 --- a/src/bin/start-keycloak/start-keycloak.ts +++ b/src/bin/start-keycloak/start-keycloak.ts @@ -2,12 +2,9 @@ import { getBuildContext } from "../shared/buildContext"; import { exclude } from "tsafe/exclude"; import type { CliCommandOptions as CliCommandOptions_common } from "../main"; import { promptKeycloakVersion } from "../shared/promptKeycloakVersion"; -import { getImplementedThemeTypes } from "../shared/getImplementedThemeTypes"; import { accountV1ThemeName, containerName } from "../shared/constants"; import { SemVer } from "../tools/SemVer"; -import type { KeycloakVersionRange } from "../shared/KeycloakVersionRange"; -import { getJarFileBasename } from "../shared/getJarFileBasename"; -import { assert, type Equals } from "tsafe/assert"; +import { assert } from "tsafe/assert"; import * as fs from "fs"; import { join as pathJoin, @@ -91,83 +88,30 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) const buildContext = getBuildContext({ cliCommandOptions }); - const { keycloakVersion, keycloakMajorNumber: keycloakMajorVersionNumber } = - await (async () => { - if (cliCommandOptions.keycloakVersion !== undefined) { - return { - keycloakVersion: cliCommandOptions.keycloakVersion, - keycloakMajorNumber: SemVer.parse(cliCommandOptions.keycloakVersion) - .major - }; - } - - console.log( - chalk.cyan("On which version of Keycloak do you want to test your theme?") - ); - - const { keycloakVersion } = await promptKeycloakVersion({ - startingFromMajor: 18, - excludeMajorVersions: [22], - cacheDirPath: buildContext.cacheDirPath - }); - - console.log(`→ ${keycloakVersion}`); - - const keycloakMajorNumber = SemVer.parse(keycloakVersion).major; - - return { keycloakVersion, keycloakMajorNumber }; - })(); - - const keycloakVersionRange: KeycloakVersionRange = (() => { - const doesImplementAccountTheme = getImplementedThemeTypes({ - projectDirPath: buildContext.projectDirPath - }).implementedThemeTypes.account; - - if (doesImplementAccountTheme) { - const keycloakVersionRange = (() => { - if (keycloakMajorVersionNumber <= 21) { - return "21-and-below" as const; - } - - assert(keycloakMajorVersionNumber !== 22); - - if (keycloakMajorVersionNumber === 23) { - return "23" as const; - } - - if (keycloakMajorVersionNumber === 24) { - return "24" as const; - } - - return "25-and-above" as const; - })(); - - assert< - Equals - >(); - - return keycloakVersionRange; - } else { - const keycloakVersionRange = (() => { - if (keycloakMajorVersionNumber <= 21) { - return "21-and-below" as const; - } - - return "22-and-above" as const; - })(); - - assert< - Equals< - typeof keycloakVersionRange, - KeycloakVersionRange.WithoutAccountTheme - > - >(); - - return keycloakVersionRange; + const { keycloakVersion } = await (async () => { + if (cliCommandOptions.keycloakVersion !== undefined) { + return { + keycloakVersion: cliCommandOptions.keycloakVersion, + keycloakMajorNumber: SemVer.parse(cliCommandOptions.keycloakVersion).major + }; } + + console.log( + chalk.cyan("On which version of Keycloak do you want to test your theme?") + ); + + const { keycloakVersion } = await promptKeycloakVersion({ + startingFromMajor: 18, + excludeMajorVersions: [22], + cacheDirPath: buildContext.cacheDirPath + }); + + console.log(`→ ${keycloakVersion}`); + + return { keycloakVersion }; })(); - const { jarFileBasename } = getJarFileBasename({ keycloakVersionRange }); + const keycloakMajorVersionNumber = SemVer.parse(keycloakVersion).major; { const { isAppBuildSuccess } = await appBuild({ @@ -177,28 +121,36 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) if (!isAppBuildSuccess) { console.log( chalk.red( - `App build failed, exiting. Try running 'npm run build-keycloak-theme' and see what's wrong.` + `App build failed, exiting. Try running 'npm run build' and see what's wrong.` ) ); process.exit(1); } const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({ - onlyBuildJarFileBasename: jarFileBasename, + buildForKeycloakMajorVersionNumber: keycloakMajorVersionNumber, buildContext }); if (!isKeycloakifyBuildSuccess) { console.log( chalk.red( - `Keycloakify build failed, exiting. Try running 'npm run build-keycloak-theme' and see what's wrong.` + `Keycloakify build failed, exiting. Try running 'npx keycloakify build' and see what's wrong.` ) ); process.exit(1); } } - console.log(`Using Keycloak ${chalk.bold(jarFileBasename)}`); + const jarFilePath = fs + .readdirSync(buildContext.keycloakifyBuildDirPath) + .filter(fileBasename => fileBasename.endsWith(".jar")) + .map(fileBasename => pathJoin(buildContext.keycloakifyBuildDirPath, fileBasename)) + .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0]; + + assert(jarFilePath !== undefined); + + console.log(`Using ${chalk.bold(pathBasename(jarFilePath))}`); const realmJsonFilePath = await (async () => { if (cliCommandOptions.realmJsonFilePath !== undefined) { @@ -284,8 +236,6 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) return filePath; })(); - const jarFilePath = pathJoin(buildContext.keycloakifyBuildDirPath, jarFileBasename); - async function extractThemeResourcesFromJar() { await extractArchive({ archiveFilePath: jarFilePath, @@ -311,7 +261,10 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) await extractThemeResourcesFromJar(); - const jarFilePath_cacheDir = pathJoin(buildContext.cacheDirPath, jarFileBasename); + const jarFilePath_cacheDir = pathJoin( + buildContext.cacheDirPath, + pathBasename(jarFilePath) + ); fs.copyFileSync(jarFilePath, jarFilePath_cacheDir); @@ -453,7 +406,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) } const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({ - onlyBuildJarFileBasename: jarFileBasename, + buildForKeycloakMajorVersionNumber: keycloakMajorVersionNumber, buildContext }); diff --git a/src/vite-plugin/vite-plugin.ts b/src/vite-plugin/vite-plugin.ts index dc78cef5..df14b01f 100644 --- a/src/vite-plugin/vite-plugin.ts +++ b/src/vite-plugin/vite-plugin.ts @@ -44,11 +44,12 @@ export function keycloakify(params?: Params) { break run_post_build_script_case; } - const buildContext = JSON.parse(envValue) as BuildContext; + const { buildContext, resourcesDirPath } = JSON.parse(envValue) as { + buildContext: BuildContext; + resourcesDirPath: string; + }; - process.chdir( - pathJoin(buildContext.keycloakifyBuildDirPath, "resources") - ); + process.chdir(resourcesDirPath); await postBuild?.(buildContext);