Enable to build only for specific keycloak version

This commit is contained in:
Joseph Garrone 2024-06-16 01:29:15 +02:00
parent 7aaedbe2ce
commit 287edabd90
15 changed files with 506 additions and 351 deletions

View File

@ -13,7 +13,6 @@ import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase"; import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert"; import { assert, Equals } from "tsafe/assert";
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
import type { CliCommandOptions } from "./main"; import type { CliCommandOptions } from "./main";
import { getBuildContext } from "./shared/buildContext"; import { getBuildContext } from "./shared/buildContext";
import chalk from "chalk"; import chalk from "chalk";
@ -53,17 +52,13 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
console.log(`${pageId}`); console.log(`${pageId}`);
const { themeSrcDirPath } = getThemeSrcDirPath({
projectDirPath: buildContext.projectDirPath
});
const componentBasename = capitalize(kebabCaseToCamelCase(pageId)).replace( const componentBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(
/ftl$/, /ftl$/,
"stories.tsx" "stories.tsx"
); );
const targetFilePath = pathJoin( const targetFilePath = pathJoin(
themeSrcDirPath, buildContext.themeSrcDirPath,
themeType, themeType,
"pages", "pages",
componentBasename componentBasename

View File

@ -15,7 +15,6 @@ import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase"; import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert"; import { assert, Equals } from "tsafe/assert";
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
import type { CliCommandOptions } from "./main"; import type { CliCommandOptions } from "./main";
import { getBuildContext } from "./shared/buildContext"; import { getBuildContext } from "./shared/buildContext";
import chalk from "chalk"; import chalk from "chalk";
@ -68,10 +67,6 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
console.log(`${pageIdOrComponent}`); console.log(`${pageIdOrComponent}`);
const { themeSrcDirPath } = getThemeSrcDirPath({
projectDirPath: buildContext.projectDirPath
});
const componentBasename = (() => { const componentBasename = (() => {
if (pageIdOrComponent === templateValue) { if (pageIdOrComponent === templateValue) {
return "Template.tsx"; return "Template.tsx";
@ -96,7 +91,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
})(); })();
const targetFilePath = pathJoin( const targetFilePath = pathJoin(
themeSrcDirPath, buildContext.themeSrcDirPath,
themeType, themeType,
pagesOrDot, pagesOrDot,
componentBasename componentBasename
@ -149,7 +144,11 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
break edit_KcApp; 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"); const kcAppTsxCode = fs.readFileSync(kcAppTsxPath).toString("utf8");
@ -199,7 +198,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
`${chalk.bold( `${chalk.bold(
pathJoin( pathJoin(
".", ".",
pathRelative(process.cwd(), themeSrcDirPath), pathRelative(process.cwd(), buildContext.themeSrcDirPath),
themeType, themeType,
"KcPage.tsx" "KcPage.tsx"
) )

View File

@ -4,7 +4,6 @@ import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion"; import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
import { getBuildContext } from "./shared/buildContext"; import { getBuildContext } from "./shared/buildContext";
import * as fs from "fs"; import * as fs from "fs";
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
import type { CliCommandOptions } from "./main"; import type { CliCommandOptions } from "./main";
export async function command(params: { cliCommandOptions: CliCommandOptions }) { export async function command(params: { cliCommandOptions: CliCommandOptions }) {
@ -12,11 +11,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const buildContext = getBuildContext({ cliCommandOptions }); const buildContext = getBuildContext({ cliCommandOptions });
const { themeSrcDirPath } = getThemeSrcDirPath({ const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
projectDirPath: buildContext.projectDirPath
});
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
if (fs.existsSync(emailThemeSrcDirPath)) { if (fs.existsSync(emailThemeSrcDirPath)) {
console.warn( console.warn(

View File

@ -1,5 +1,4 @@
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { exclude } from "tsafe/exclude";
import { import {
keycloakAccountV1Versions, keycloakAccountV1Versions,
keycloakThemeAdditionalInfoExtensionVersions keycloakThemeAdditionalInfoExtensionVersions
@ -7,32 +6,29 @@ import {
import { getKeycloakVersionRangeForJar } from "./getKeycloakVersionRangeForJar"; import { getKeycloakVersionRangeForJar } from "./getKeycloakVersionRangeForJar";
import { buildJar, BuildContextLike as BuildContextLike_buildJar } from "./buildJar"; import { buildJar, BuildContextLike as BuildContextLike_buildJar } from "./buildJar";
import type { BuildContext } from "../../shared/buildContext"; import type { BuildContext } from "../../shared/buildContext";
import { getJarFileBasename } from "../../shared/getJarFileBasename";
import { getImplementedThemeTypes } from "../../shared/getImplementedThemeTypes";
export type BuildContextLike = BuildContextLike_buildJar & { export type BuildContextLike = BuildContextLike_buildJar & {
projectDirPath: string; projectDirPath: string;
keycloakifyBuildDirPath: string; keycloakifyBuildDirPath: string;
recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"];
jarTargets: BuildContext["jarTargets"];
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export async function buildJars(params: { export async function buildJars(params: {
resourcesDirPath: string; resourcesDirPath: string;
onlyBuildJarFileBasename: string | undefined;
buildContext: BuildContextLike; buildContext: BuildContextLike;
}): Promise<void> { }): Promise<void> {
const { onlyBuildJarFileBasename, resourcesDirPath, buildContext } = params; const { resourcesDirPath, buildContext } = params;
const doesImplementAccountTheme = getImplementedThemeTypes({ const doesImplementAccountTheme = buildContext.recordIsImplementedByThemeType.account;
projectDirPath: buildContext.projectDirPath
}).implementedThemeTypes.account;
await Promise.all( await Promise.all(
keycloakAccountV1Versions keycloakAccountV1Versions
.map(keycloakAccountV1Version => .map(keycloakAccountV1Version =>
keycloakThemeAdditionalInfoExtensionVersions keycloakThemeAdditionalInfoExtensionVersions.map(
.map(keycloakThemeAdditionalInfoExtensionVersion => { keycloakThemeAdditionalInfoExtensionVersion => {
const keycloakVersionRange = getKeycloakVersionRangeForJar({ const keycloakVersionRange = getKeycloakVersionRangeForJar({
doesImplementAccountTheme, doesImplementAccountTheme,
keycloakAccountV1Version, keycloakAccountV1Version,
@ -43,48 +39,26 @@ export async function buildJars(params: {
return undefined; return undefined;
} }
return { const jarTarget = buildContext.jarTargets.find(
keycloakThemeAdditionalInfoExtensionVersion, jarTarget =>
keycloakVersionRange jarTarget.keycloakVersionRange === keycloakVersionRange
}; );
})
.filter(exclude(undefined))
.map(
({
keycloakThemeAdditionalInfoExtensionVersion,
keycloakVersionRange
}) => {
const { jarFileBasename } = getJarFileBasename({
keycloakVersionRange
});
if ( if (jarTarget === undefined) {
onlyBuildJarFileBasename !== undefined && return undefined;
onlyBuildJarFileBasename !== jarFileBasename
) {
return undefined;
}
return {
keycloakThemeAdditionalInfoExtensionVersion,
jarFileBasename
};
} }
)
.filter(exclude(undefined)) const { jarFileBasename } = jarTarget;
.map(
({ return buildJar({
jarFileBasename,
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion, keycloakThemeAdditionalInfoExtensionVersion,
jarFileBasename resourcesDirPath,
}) => buildContext
buildJar({ });
jarFileBasename, }
keycloakAccountV1Version, )
keycloakThemeAdditionalInfoExtensionVersion,
resourcesDirPath,
buildContext
})
)
) )
.flat() .flat()
); );

View File

@ -29,7 +29,6 @@ import {
bringInAccountV1, bringInAccountV1,
type BuildContextLike as BuildContextLike_bringInAccountV1 type BuildContextLike as BuildContextLike_bringInAccountV1
} from "./bringInAccountV1"; } from "./bringInAccountV1";
import { getThemeSrcDirPath } from "../../shared/getThemeSrcDirPath";
import { rmSync } from "../../tools/fs.rmSync"; import { rmSync } from "../../tools/fs.rmSync";
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion"; import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
import { import {
@ -38,7 +37,6 @@ import {
} from "../../shared/metaInfKeycloakThemes"; } from "../../shared/metaInfKeycloakThemes";
import { objectEntries } from "tsafe/objectEntries"; import { objectEntries } from "tsafe/objectEntries";
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile"; import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
import { getImplementedThemeTypes } from "../../shared/getImplementedThemeTypes";
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode & export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
BuildContextLike_downloadKeycloakStaticResources & BuildContextLike_downloadKeycloakStaticResources &
@ -48,6 +46,8 @@ export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
projectDirPath: string; projectDirPath: string;
projectBuildDirPath: string; projectBuildDirPath: string;
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"];
themeSrcDirPath: string;
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -59,14 +59,6 @@ export async function generateResourcesForMainTheme(params: {
}): Promise<void> { }): Promise<void> {
const { themeName, resourcesDirPath, buildContext } = params; const { themeName, resourcesDirPath, buildContext } = params;
const { themeSrcDirPath } = getThemeSrcDirPath({
projectDirPath: buildContext.projectDirPath
});
const { implementedThemeTypes } = getImplementedThemeTypes({
projectDirPath: buildContext.projectDirPath
});
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => { const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
const { themeType } = params; const { themeType } = params;
return pathJoin(resourcesDirPath, "theme", themeName, themeType); return pathJoin(resourcesDirPath, "theme", themeName, themeType);
@ -75,7 +67,7 @@ export async function generateResourcesForMainTheme(params: {
const cssGlobalsToDefine: Record<string, string> = {}; const cssGlobalsToDefine: Record<string, string> = {};
for (const themeType of ["login", "account"] as const) { for (const themeType of ["login", "account"] as const) {
if (!implementedThemeTypes[themeType]) { if (!buildContext.recordIsImplementedByThemeType[themeType]) {
continue; 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. // NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
rmSync(destDirPath, { recursive: true, force: true }); 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. // NOTE: We prevent doing it twice, it has been done for the login theme.
transformCodebase({ transformCodebase({
@ -178,7 +173,7 @@ export async function generateResourcesForMainTheme(params: {
keycloakifyVersion: readThisNpmPackageVersion(), keycloakifyVersion: readThisNpmPackageVersion(),
themeType, themeType,
fieldNames: readFieldNameUsage({ fieldNames: readFieldNameUsage({
themeSrcDirPath, themeSrcDirPath: buildContext.themeSrcDirPath,
themeType themeType
}) })
}); });
@ -194,7 +189,7 @@ export async function generateResourcesForMainTheme(params: {
})(), })(),
...readExtraPagesNames({ ...readExtraPagesNames({
themeType, themeType,
themeSrcDirPath themeSrcDirPath: buildContext.themeSrcDirPath
}) })
].forEach(pageId => { ].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId }); const { ftlCode } = generateFtlFilesCode({ pageId });
@ -206,7 +201,7 @@ export async function generateResourcesForMainTheme(params: {
}); });
generateMessageProperties({ generateMessageProperties({
themeSrcDirPath, themeSrcDirPath: buildContext.themeSrcDirPath,
themeType themeType
}).forEach(({ languageTag, propertiesFileSource }) => { }).forEach(({ languageTag, propertiesFileSource }) => {
const messagesDirPath = pathJoin(themeTypeDirPath, "messages"); const messagesDirPath = pathJoin(themeTypeDirPath, "messages");
@ -265,11 +260,11 @@ export async function generateResourcesForMainTheme(params: {
} }
email: { email: {
if (!implementedThemeTypes.email) { if (!buildContext.recordIsImplementedByThemeType.email) {
break email; break email;
} }
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email"); const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
transformCodebase({ transformCodebase({
srcDirPath: emailThemeSrcDirPath, srcDirPath: emailThemeSrcDirPath,
@ -277,7 +272,7 @@ export async function generateResourcesForMainTheme(params: {
}); });
} }
if (implementedThemeTypes.account) { if (buildContext.recordIsImplementedByThemeType.account) {
await bringInAccountV1({ await bringInAccountV1({
resourcesDirPath, resourcesDirPath,
buildContext buildContext
@ -289,12 +284,12 @@ export async function generateResourcesForMainTheme(params: {
metaInfKeycloakThemes.themes.push({ metaInfKeycloakThemes.themes.push({
name: themeName, name: themeName,
types: objectEntries(implementedThemeTypes) types: objectEntries(buildContext.recordIsImplementedByThemeType)
.filter(([, isImplemented]) => isImplemented) .filter(([, isImplemented]) => isImplemented)
.map(([themeType]) => themeType) .map(([themeType]) => themeType)
}); });
if (implementedThemeTypes.account) { if (buildContext.recordIsImplementedByThemeType.account) {
metaInfKeycloakThemes.themes.push({ metaInfKeycloakThemes.themes.push({
name: accountV1ThemeName, name: accountV1ThemeName,
types: ["account"] types: ["account"]

View File

@ -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 child_process from "child_process";
import * as fs from "fs"; import * as fs from "fs";
import { getBuildContext } from "../shared/buildContext"; import { getBuildContext } from "../shared/buildContext";
import { import { vitePluginSubScriptEnvNames } from "../shared/constants";
vitePluginSubScriptEnvNames,
onlyBuildJarFileBasenameEnvName
} from "../shared/constants";
import { buildJars } from "./buildJars"; import { buildJars } from "./buildJars";
import type { CliCommandOptions } from "../main"; import type { CliCommandOptions } from "../main";
import chalk from "chalk"; import chalk from "chalk";
@ -96,16 +93,17 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
cwd: buildContext.projectDirPath, cwd: buildContext.projectDirPath,
env: { env: {
...process.env, ...process.env,
[vitePluginSubScriptEnvNames.runPostBuildScript]: [vitePluginSubScriptEnvNames.runPostBuildScript]: JSON.stringify({
JSON.stringify(buildContext) resourcesDirPath,
buildContext
})
} }
}); });
} }
await buildJars({ await buildJars({
resourcesDirPath, resourcesDirPath,
buildContext, buildContext
onlyBuildJarFileBasename: process.env[onlyBuildJarFileBasenameEnvName]
}); });
rmSync(resourcesDirPath, { recursive: true }); rmSync(resourcesDirPath, { recursive: true });

View File

@ -5,9 +5,22 @@ import { getNpmWorkspaceRootDirPath } from "../tools/getNpmWorkspaceRootDirPath"
import type { CliCommandOptions } from "../main"; import type { CliCommandOptions } from "../main";
import { z } from "zod"; import { z } from "zod";
import * as fs from "fs"; 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 * 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 = { export type BuildContext = {
bundler: "vite" | "webpack"; bundler: "vite" | "webpack";
@ -30,6 +43,12 @@ export type BuildContext = {
npmWorkspaceRootDirPath: string; npmWorkspaceRootDirPath: string;
kcContextExclusionsFtlCode: string | undefined; kcContextExclusionsFtlCode: string | undefined;
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string;
recordIsImplementedByThemeType: Readonly<Record<ThemeType | "email", boolean>>;
jarTargets: {
keycloakVersionRange: KeycloakVersionRange;
jarFileBasename: string;
}[];
}; };
export type BuildOptions = { export type BuildOptions = {
@ -41,8 +60,21 @@ export type BuildOptions = {
loginThemeResourcesFromKeycloakVersion?: string; loginThemeResourcesFromKeycloakVersion?: string;
keycloakifyBuildDirPath?: string; keycloakifyBuildDirPath?: string;
kcContextExclusionsFtl?: 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 = { export type ResolvedViteConfig = {
buildDir: string; buildDir: string;
publicDir: string; publicDir: string;
@ -102,7 +134,7 @@ export function getBuildContext(params: {
})(); })();
const parsedPackageJson = (() => { const parsedPackageJson = (() => {
type WebpackSpecificBuildOptions = { type BuildOptions_packageJson = BuildOptions & {
projectBuildDirPath?: string; projectBuildDirPath?: string;
}; };
@ -110,49 +142,75 @@ export function getBuildContext(params: {
name: string; name: string;
version?: string; version?: string;
homepage?: string; homepage?: string;
keycloakify?: { keycloakify?: BuildOptions_packageJson;
themeName?: string | string[];
environmentVariables?: { name: string; default: string }[];
extraThemeProperties?: string[];
artifactId?: string;
groupId?: string;
loginThemeResourcesFromKeycloakVersion?: string;
keycloakifyBuildDirPath?: string;
kcContextExclusionsFtl?: string;
projectBuildDirPath?: string;
};
}; };
{
type Got = NonNullable<ParsedPackageJson["keycloakify"]>;
type Expected = BuildOptions & WebpackSpecificBuildOptions;
assert<Equals<Got, Expected>>();
}
const zParsedPackageJson = z.object({ const zParsedPackageJson = z.object({
name: z.string(), name: z.string(),
version: z.string().optional(), version: z.string().optional(),
homepage: z.string().optional(), homepage: z.string().optional(),
keycloakify: z keycloakify: id<z.ZodType<BuildOptions_packageJson>>(
.object({ (() => {
extraThemeProperties: z.array(z.string()).optional(), const zBuildOptions_packageJson = z.object({
artifactId: z.string().optional(), extraThemeProperties: z.array(z.string()).optional(),
groupId: z.string().optional(), artifactId: z.string().optional(),
loginThemeResourcesFromKeycloakVersion: z.string().optional(), groupId: z.string().optional(),
projectBuildDirPath: z.string().optional(), loginThemeResourcesFromKeycloakVersion: z.string().optional(),
keycloakifyBuildDirPath: z.string().optional(), projectBuildDirPath: z.string().optional(),
kcContextExclusionsFtl: z.string().optional(), keycloakifyBuildDirPath: z.string().optional(),
environmentVariables: z kcContextExclusionsFtl: z.string().optional(),
.array( environmentVariables: z
z.object({ .array(
name: z.string(), z.object({
default: z.string() name: z.string(),
}) default: z.string()
) })
.optional(), )
themeName: z.union([z.string(), z.array(z.string())]).optional() .optional(),
}) themeName: z.union([z.string(), z.array(z.string())]).optional(),
.optional() jarTargets: id<z.ZodType<BuildOptions.JarTargets>>(
(() => {
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<typeof zJarTargets>;
type Expected = BuildOptions.JarTargets;
assert<Equals<Got, Expected>>();
}
return zJarTargets;
})()
).optional()
});
{
type Got = z.infer<typeof zBuildOptions_packageJson>;
type Expected = BuildOptions_packageJson;
assert<Equals<Got, Expected>>();
}
return zBuildOptions_packageJson;
})()
).optional()
}); });
{ {
@ -173,6 +231,54 @@ export function getBuildContext(params: {
...resolvedViteConfig?.buildOptions ...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[]] => { const themeNames = ((): [string, ...string[]] => {
if (buildOptions.themeName === undefined) { if (buildOptions.themeName === undefined) {
return [ return [
@ -218,8 +324,10 @@ export function getBuildContext(params: {
dependencyExpected: "keycloakify" dependencyExpected: "keycloakify"
}); });
const bundler = resolvedViteConfig !== undefined ? "vite" : "webpack";
return { return {
bundler: resolvedViteConfig !== undefined ? "vite" : "webpack", bundler,
themeVersion: themeVersion:
process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0", process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0",
themeNames, themeNames,
@ -349,6 +457,257 @@ export function getBuildContext(params: {
return buildOptions.kcContextExclusionsFtl; 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<Equals<typeof bundler, never>>(false);
})();
message += `. Please set it to ${!hasAccountTheme} `;
message +=
"and fill up the relevant keycloak version ranges.\n";
message += "Example:\n";
message += JSON.stringify(
id<Pick<BuildOptions, "jarTargets">>({
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;
})()
}; };
} }

View File

@ -15,7 +15,8 @@ export const vitePluginSubScriptEnvNames = {
resolveViteConfig: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG" resolveViteConfig: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
} as const; } as const;
export const onlyBuildJarFileBasenameEnvName = "KEYCLOAKIFY_ONLY_BUILD_JAR_FILE_BASENAME"; export const buildForKeycloakMajorVersionEnvName =
"KEYCLOAKIFY_BUILD_FOR_KEYCLOAK_MAJOR_VERSION";
export const loginThemePageIds = [ export const loginThemePageIds = [
"login.ftl", "login.ftl",

View File

@ -1,6 +1,5 @@
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { BuildContext } from "./buildContext"; import type { BuildContext } from "./buildContext";
import { getThemeSrcDirPath } from "./getThemeSrcDirPath";
import * as fs from "fs/promises"; import * as fs from "fs/promises";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import { existsAsync } from "../tools/fs.existsAsync"; import { existsAsync } from "../tools/fs.existsAsync";
@ -9,6 +8,7 @@ export type BuildContextLike = {
projectDirPath: string; projectDirPath: string;
themeNames: string[]; themeNames: string[];
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string;
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -18,11 +18,7 @@ export async function generateKcGenTs(params: {
}): Promise<void> { }): Promise<void> {
const { buildContext } = params; const { buildContext } = params;
const { themeSrcDirPath } = getThemeSrcDirPath({ const filePath = pathJoin(buildContext.themeSrcDirPath, "kc.gen.ts");
projectDirPath: buildContext.projectDirPath
});
const filePath = pathJoin(themeSrcDirPath, "kc.gen.ts");
const currentContent = (await existsAsync(filePath)) const currentContent = (await existsAsync(filePath))
? await fs.readFile(filePath) ? await fs.readFile(filePath)

View File

@ -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<Record<ThemeType | "email", boolean>>;
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<Record<ThemeType | "email", boolean>> =
objectFromEntries(
(["login", "account", "email"] as const).map(themeType => [
themeType,
fs.existsSync(pathJoin(themeSrcDirPath, themeType))
])
);
cache = { projectDirPath, implementedThemeTypes };
return { implementedThemeTypes };
}

View File

@ -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 };
}

View File

@ -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"];

View File

@ -1,4 +1,4 @@
import { onlyBuildJarFileBasenameEnvName } from "../shared/constants"; import { buildForKeycloakMajorVersionEnvName } from "../shared/constants";
import * as child_process from "child_process"; import * as child_process from "child_process";
import { Deferred } from "evt/tools/Deferred"; import { Deferred } from "evt/tools/Deferred";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
@ -14,10 +14,10 @@ export type BuildContextLike = {
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export async function keycloakifyBuild(params: { export async function keycloakifyBuild(params: {
onlyBuildJarFileBasename: string; buildForKeycloakMajorVersionNumber: number;
buildContext: BuildContextLike; buildContext: BuildContextLike;
}): Promise<{ isKeycloakifyBuildSuccess: boolean }> { }): Promise<{ isKeycloakifyBuildSuccess: boolean }> {
const { buildContext, onlyBuildJarFileBasename } = params; const { buildForKeycloakMajorVersionNumber, buildContext } = params;
const dResult = new Deferred<{ isSuccess: boolean }>(); const dResult = new Deferred<{ isSuccess: boolean }>();
@ -25,7 +25,7 @@ export async function keycloakifyBuild(params: {
cwd: buildContext.projectDirPath, cwd: buildContext.projectDirPath,
env: { env: {
...process.env, ...process.env,
[onlyBuildJarFileBasenameEnvName]: onlyBuildJarFileBasename [buildForKeycloakMajorVersionEnvName]: `${buildForKeycloakMajorVersionNumber}`
}, },
shell: true shell: true
}); });

View File

@ -2,12 +2,9 @@ import { getBuildContext } from "../shared/buildContext";
import { exclude } from "tsafe/exclude"; import { exclude } from "tsafe/exclude";
import type { CliCommandOptions as CliCommandOptions_common } from "../main"; import type { CliCommandOptions as CliCommandOptions_common } from "../main";
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion"; import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
import { getImplementedThemeTypes } from "../shared/getImplementedThemeTypes";
import { accountV1ThemeName, containerName } from "../shared/constants"; import { accountV1ThemeName, containerName } from "../shared/constants";
import { SemVer } from "../tools/SemVer"; import { SemVer } from "../tools/SemVer";
import type { KeycloakVersionRange } from "../shared/KeycloakVersionRange"; import { assert } from "tsafe/assert";
import { getJarFileBasename } from "../shared/getJarFileBasename";
import { assert, type Equals } from "tsafe/assert";
import * as fs from "fs"; import * as fs from "fs";
import { import {
join as pathJoin, join as pathJoin,
@ -91,83 +88,30 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const buildContext = getBuildContext({ cliCommandOptions }); const buildContext = getBuildContext({ cliCommandOptions });
const { keycloakVersion, keycloakMajorNumber: keycloakMajorVersionNumber } = const { keycloakVersion } = await (async () => {
await (async () => { if (cliCommandOptions.keycloakVersion !== undefined) {
if (cliCommandOptions.keycloakVersion !== undefined) { return {
return { keycloakVersion: cliCommandOptions.keycloakVersion,
keycloakVersion: cliCommandOptions.keycloakVersion, keycloakMajorNumber: SemVer.parse(cliCommandOptions.keycloakVersion).major
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<typeof keycloakVersionRange, KeycloakVersionRange.WithAccountTheme>
>();
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;
} }
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({ const { isAppBuildSuccess } = await appBuild({
@ -177,28 +121,36 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
if (!isAppBuildSuccess) { if (!isAppBuildSuccess) {
console.log( console.log(
chalk.red( 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); process.exit(1);
} }
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({ const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
onlyBuildJarFileBasename: jarFileBasename, buildForKeycloakMajorVersionNumber: keycloakMajorVersionNumber,
buildContext buildContext
}); });
if (!isKeycloakifyBuildSuccess) { if (!isKeycloakifyBuildSuccess) {
console.log( console.log(
chalk.red( 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); 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 () => { const realmJsonFilePath = await (async () => {
if (cliCommandOptions.realmJsonFilePath !== undefined) { if (cliCommandOptions.realmJsonFilePath !== undefined) {
@ -284,8 +236,6 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
return filePath; return filePath;
})(); })();
const jarFilePath = pathJoin(buildContext.keycloakifyBuildDirPath, jarFileBasename);
async function extractThemeResourcesFromJar() { async function extractThemeResourcesFromJar() {
await extractArchive({ await extractArchive({
archiveFilePath: jarFilePath, archiveFilePath: jarFilePath,
@ -311,7 +261,10 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
await extractThemeResourcesFromJar(); await extractThemeResourcesFromJar();
const jarFilePath_cacheDir = pathJoin(buildContext.cacheDirPath, jarFileBasename); const jarFilePath_cacheDir = pathJoin(
buildContext.cacheDirPath,
pathBasename(jarFilePath)
);
fs.copyFileSync(jarFilePath, jarFilePath_cacheDir); fs.copyFileSync(jarFilePath, jarFilePath_cacheDir);
@ -453,7 +406,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
} }
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({ const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
onlyBuildJarFileBasename: jarFileBasename, buildForKeycloakMajorVersionNumber: keycloakMajorVersionNumber,
buildContext buildContext
}); });

View File

@ -44,11 +44,12 @@ export function keycloakify(params?: Params) {
break run_post_build_script_case; 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( process.chdir(resourcesDirPath);
pathJoin(buildContext.keycloakifyBuildDirPath, "resources")
);
await postBuild?.(buildContext); await postBuild?.(buildContext);