Support incorporating theme native theme, with theme variant support #733

This commit is contained in:
Joseph Garrone 2025-01-04 23:24:44 +01:00
parent c33c315120
commit 4845d7c32d
2 changed files with 361 additions and 236 deletions

View File

@ -41,6 +41,7 @@ import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPa
import propertiesParser from "properties-parser"; import propertiesParser from "properties-parser";
import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed"; import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed";
import { listInstalledModules } from "../../tools/listInstalledModules"; import { listInstalledModules } from "../../tools/listInstalledModules";
import { isInside } from "../../tools/isInside";
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode & export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
BuildContextLike_generateMessageProperties & { BuildContextLike_generateMessageProperties & {
@ -78,14 +79,25 @@ export async function generateResources(params: {
}; };
const writeMessagePropertiesFilesByThemeType: Partial< const writeMessagePropertiesFilesByThemeType: Partial<
Record<ThemeType, (params: { messageDirPath: string; themeName: string }) => void> Record<
ThemeType | "email",
(params: { messageDirPath: string; themeName: string }) => void
>
> = {}; > = {};
for (const themeType of THEME_TYPES) { for (const themeType of [...THEME_TYPES, "email"] as const) {
if (!buildContext.implementedThemeTypes[themeType].isImplemented) { let isNative: boolean;
{
const v = buildContext.implementedThemeTypes[themeType];
if (!v.isImplemented && !v.isImplemented_native) {
continue; continue;
} }
isNative = !v.isImplemented && v.isImplemented_native;
}
const getAccountThemeType = () => { const getAccountThemeType = () => {
assert(themeType === "account"); assert(themeType === "account");
@ -102,12 +114,18 @@ export async function generateResources(params: {
return getAccountThemeType() === "Single-Page"; return getAccountThemeType() === "Single-Page";
case "admin": case "admin":
return true; return true;
case "email":
return false;
} }
})(); })();
const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType }); const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
apply_replacers_and_move_to_theme_resources: { apply_replacers_and_move_to_theme_resources: {
if (isNative) {
break apply_replacers_and_move_to_theme_resources;
}
const destDirPath = pathJoin( const destDirPath = pathJoin(
themeTypeDirPath, themeTypeDirPath,
"resources", "resources",
@ -191,10 +209,19 @@ export async function generateResources(params: {
}); });
} }
generate_ftl_files: {
if (isNative) {
break generate_ftl_files;
}
assert(themeType !== "email");
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({ const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
themeName, themeName,
indexHtmlCode: fs indexHtmlCode: fs
.readFileSync(pathJoin(buildContext.projectBuildDirPath, "index.html")) .readFileSync(
pathJoin(buildContext.projectBuildDirPath, "index.html")
)
.toString("utf8"), .toString("utf8"),
buildContext, buildContext,
keycloakifyVersion: readThisNpmPackageVersion(), keycloakifyVersion: readThisNpmPackageVersion(),
@ -235,15 +262,40 @@ export async function generateResources(params: {
Buffer.from(ftlCode, "utf8") Buffer.from(ftlCode, "utf8")
); );
}); });
}
copy_native_theme: {
if (!isNative) {
break copy_native_theme;
}
const dirPath = pathJoin(buildContext.themeSrcDirPath, themeType);
transformCodebase({
srcDirPath: dirPath,
destDirPath: getThemeTypeDirPath({ themeName, themeType }),
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
if (isInside({ dirPath: "messages", filePath: fileRelativePath })) {
return undefined;
}
return { modifiedSourceCode: sourceCode };
}
});
}
let languageTags: string[] | undefined = undefined; let languageTags: string[] | undefined = undefined;
i18n_multi_page: { i18n_multi_page: {
if (isNative) {
break i18n_multi_page;
}
if (isSpa) { if (isSpa) {
break i18n_multi_page; break i18n_multi_page;
} }
assert(themeType !== "admin"); assert(themeType !== "admin" && themeType !== "email");
const wrap = generateMessageProperties({ const wrap = generateMessageProperties({
buildContext, buildContext,
@ -364,27 +416,24 @@ export async function generateResources(params: {
); );
} }
i18n_single_page: { i18n_for_spas_and_native: {
if (!isSpa) { if (!isSpa && !isNative) {
break i18n_single_page; break i18n_for_spas_and_native;
} }
if (isLegacyAccountSpa) { if (isLegacyAccountSpa) {
break i18n_single_page; break i18n_for_spas_and_native;
} }
assert(themeType === "account" || themeType === "admin");
const messagesDirPath_theme = pathJoin( const messagesDirPath_theme = pathJoin(
buildContext.themeSrcDirPath, buildContext.themeSrcDirPath,
themeType, themeType,
"i18n" isNative ? "messages" : "i18n"
); );
assert( if (!fs.existsSync(messagesDirPath_theme)) {
fs.existsSync(messagesDirPath_theme), break i18n_for_spas_and_native;
`${messagesDirPath_theme} is supposed to exist` }
);
const propertiesByLang: Record< const propertiesByLang: Record<
string, string,
@ -524,6 +573,10 @@ export async function generateResources(params: {
} }
keycloak_static_resources: { keycloak_static_resources: {
if (isNative) {
break keycloak_static_resources;
}
if (isSpa) { if (isSpa) {
break keycloak_static_resources; break keycloak_static_resources;
} }
@ -540,6 +593,37 @@ export async function generateResources(params: {
}); });
} }
bring_in_account_v1: {
if (isNative) {
break bring_in_account_v1;
}
if (themeType !== "account") {
break bring_in_account_v1;
}
assert(buildContext.implementedThemeTypes.account.isImplemented);
if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
break bring_in_account_v1;
}
transformCodebase({
srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "res", "account-v1"),
destDirPath: getThemeTypeDirPath({
themeName: "account-v1",
themeType: "account"
})
});
}
generate_theme_properties: {
if (isNative) {
break generate_theme_properties;
}
assert(themeType !== "email");
fs.writeFileSync( fs.writeFileSync(
pathJoin(themeTypeDirPath, "theme.properties"), pathJoin(themeTypeDirPath, "theme.properties"),
Buffer.from( Buffer.from(
@ -558,9 +642,10 @@ export async function generateResources(params: {
case "admin": case "admin":
return "base"; return "base";
} }
assert<Equals<typeof themeType, never>>(false); assert<Equals<typeof themeType, never>>;
})()}`, })()}`,
...(themeType === "account" && getAccountThemeType() === "Single-Page" ...(themeType === "account" &&
getAccountThemeType() === "Single-Page"
? ["deprecatedMode=false"] ? ["deprecatedMode=false"]
: []), : []),
...(buildContext.extraThemeProperties ?? []), ...(buildContext.extraThemeProperties ?? []),
@ -579,72 +664,33 @@ export async function generateResources(params: {
) )
); );
} }
email: {
if (!buildContext.implementedThemeTypes.email.isImplemented) {
break email;
}
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
transformCodebase({
srcDirPath: emailThemeSrcDirPath,
destDirPath: getThemeTypeDirPath({ themeName, themeType: "email" })
});
}
bring_in_account_v1: {
if (!buildContext.implementedThemeTypes.account.isImplemented) {
break bring_in_account_v1;
}
if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
break bring_in_account_v1;
}
transformCodebase({
srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "res", "account-v1"),
destDirPath: getThemeTypeDirPath({
themeName: "account-v1",
themeType: "account"
})
});
}
{
const metaInfKeycloakThemes: MetaInfKeycloakTheme = { themes: [] };
for (const themeName of buildContext.themeNames) {
metaInfKeycloakThemes.themes.push({
name: themeName,
types: objectEntries(buildContext.implementedThemeTypes)
.filter(([, { isImplemented }]) => isImplemented)
.map(([themeType]) => themeType)
});
}
if (buildContext.implementedThemeTypes.account.isImplemented) {
metaInfKeycloakThemes.themes.push({
name: "account-v1",
types: ["account"]
});
}
writeMetaInfKeycloakThemes({
resourcesDirPath,
getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
});
} }
for (const themeVariantName of buildContext.themeNames) { for (const themeVariantName of buildContext.themeNames) {
for (const themeType of [...THEME_TYPES, "email"] as const) {
copy_main_theme_to_theme_variant_theme: {
let isNative: boolean;
{
const v = buildContext.implementedThemeTypes[themeType];
if (!v.isImplemented && !v.isImplemented_native) {
break copy_main_theme_to_theme_variant_theme;
}
isNative = !v.isImplemented && v.isImplemented_native;
}
if (themeVariantName === themeName) { if (themeVariantName === themeName) {
continue; break copy_main_theme_to_theme_variant_theme;
} }
transformCodebase({ transformCodebase({
srcDirPath: pathJoin(resourcesDirPath, "theme", themeName), srcDirPath: pathJoin(resourcesDirPath, "theme", themeName),
destDirPath: pathJoin(resourcesDirPath, "theme", themeVariantName), destDirPath: pathJoin(resourcesDirPath, "theme", themeVariantName),
transformSourceCode: ({ fileRelativePath, sourceCode }) => { transformSourceCode: isNative
? undefined
: ({ fileRelativePath, sourceCode }) => {
if ( if (
pathExtname(fileRelativePath) === ".ftl" && pathExtname(fileRelativePath) === ".ftl" &&
fileRelativePath.split(pathSep).length === 2 fileRelativePath.split(pathSep).length === 2
@ -659,7 +705,9 @@ export async function generateResources(params: {
"utf8" "utf8"
); );
assert(Buffer.compare(modifiedSourceCode, sourceCode) !== 0); assert(
Buffer.compare(modifiedSourceCode, sourceCode) !== 0
);
return { modifiedSourceCode }; return { modifiedSourceCode };
} }
@ -668,35 +716,34 @@ export async function generateResources(params: {
} }
}); });
} }
run_writeMessagePropertiesFiles: {
const writeMessagePropertiesFiles =
writeMessagePropertiesFilesByThemeType[themeType];
for (const themeName of buildContext.themeNames) {
for (const [themeType, writeMessagePropertiesFiles] of objectEntries(
writeMessagePropertiesFilesByThemeType
)) {
// NOTE: This is just a quirk of the type system: We can't really differentiate in a record
// between the case where the key isn't present and the case where the value is `undefined`.
if (writeMessagePropertiesFiles === undefined) { if (writeMessagePropertiesFiles === undefined) {
return; break run_writeMessagePropertiesFiles;
} }
writeMessagePropertiesFiles({ writeMessagePropertiesFiles({
messageDirPath: pathJoin( messageDirPath: pathJoin(
getThemeTypeDirPath({ themeName, themeType }), getThemeTypeDirPath({ themeName: themeVariantName, themeType }),
"messages" "messages"
), ),
themeName themeName: themeVariantName
}); });
} }
replace_xKeycloakify_themeName_in_native_ftl_files: {
{
const v = buildContext.implementedThemeTypes[themeType];
if (v.isImplemented || !v.isImplemented_native) {
break replace_xKeycloakify_themeName_in_native_ftl_files;
}
} }
modify_email_theme_per_variant: {
if (!buildContext.implementedThemeTypes.email.isImplemented) {
break modify_email_theme_per_variant;
}
for (const themeName of buildContext.themeNames) {
const emailThemeDirPath = getThemeTypeDirPath({ const emailThemeDirPath = getThemeTypeDirPath({
themeName, themeName,
themeType: "email" themeType
}); });
transformCodebase({ transformCodebase({
@ -711,7 +758,10 @@ export async function generateResources(params: {
modifiedSourceCode: Buffer.from( modifiedSourceCode: Buffer.from(
sourceCode sourceCode
.toString("utf8") .toString("utf8")
.replace(/xKeycloakify\.themeName/g, `"${themeName}"`), .replace(
/xKeycloakify\.themeName/g,
`"${themeName}"`
),
"utf8" "utf8"
) )
}; };
@ -720,3 +770,33 @@ export async function generateResources(params: {
} }
} }
} }
// Generate meta-inf/keycloak-themes.json
{
const metaInfKeycloakThemes: MetaInfKeycloakTheme = { themes: [] };
for (const themeName of buildContext.themeNames) {
metaInfKeycloakThemes.themes.push({
name: themeName,
types: objectEntries(buildContext.implementedThemeTypes)
.filter(([, v]) => v.isImplemented || v.isImplemented_native)
.map(([themeType]) => themeType)
});
}
if (
buildContext.implementedThemeTypes.account.isImplemented &&
buildContext.implementedThemeTypes.account.type === "Multi-Page"
) {
metaInfKeycloakThemes.themes.push({
name: "account-v1",
types: ["account"]
});
}
writeMetaInfKeycloakThemes({
resourcesDirPath,
getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
});
}
}

View File

@ -45,12 +45,16 @@ export type BuildContext = {
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string; themeSrcDirPath: string;
implementedThemeTypes: { implementedThemeTypes: {
login: { isImplemented: boolean }; login:
email: { isImplemented: boolean }; | { isImplemented: true }
| { isImplemented: false; isImplemented_native: boolean };
email: { isImplemented: false; isImplemented_native: boolean };
account: account:
| { isImplemented: false } | { isImplemented: false; isImplemented_native: boolean }
| { isImplemented: true; type: "Single-Page" | "Multi-Page" }; | { isImplemented: true; type: "Single-Page" | "Multi-Page" };
admin: { isImplemented: boolean }; admin:
| { isImplemented: true }
| { isImplemented: false; isImplemented_native: boolean };
}; };
packageJsonFilePath: string; packageJsonFilePath: string;
bundler: "vite" | "webpack"; bundler: "vite" | "webpack";
@ -434,16 +438,46 @@ export function getBuildContext(params: {
assert<Equals<typeof bundler, never>>(false); assert<Equals<typeof bundler, never>>(false);
})(); })();
const implementedThemeTypes: BuildContext["implementedThemeTypes"] = { const implementedThemeTypes: BuildContext["implementedThemeTypes"] = (() => {
login: { const getIsNative = (dirPath: string) =>
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "login")) fs.existsSync(pathJoin(dirPath, "theme.properties"));
},
email: { return {
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "email")) login: (() => {
}, const dirPath = pathJoin(themeSrcDirPath, "login");
if (!fs.existsSync(dirPath)) {
return { isImplemented: false, isImplemented_native: false };
}
if (getIsNative(dirPath)) {
return { isImplemented: false, isImplemented_native: true };
}
return { isImplemented: true };
})(),
email: (() => {
const dirPath = pathJoin(themeSrcDirPath, "email");
if (!fs.existsSync(dirPath) || !getIsNative(dirPath)) {
return { isImplemented: false, isImplemented_native: false };
}
return { isImplemented: false, isImplemented_native: true };
})(),
account: (() => { account: (() => {
const dirPath = pathJoin(themeSrcDirPath, "account");
if (!fs.existsSync(dirPath)) {
return { isImplemented: false, isImplemented_native: false };
}
if (getIsNative(dirPath)) {
return { isImplemented: false, isImplemented_native: true };
}
if (buildOptions.accountThemeImplementation === "none") { if (buildOptions.accountThemeImplementation === "none") {
return { isImplemented: false }; return { isImplemented: false, isImplemented_native: false };
} }
return { return {
@ -451,10 +485,21 @@ export function getBuildContext(params: {
type: buildOptions.accountThemeImplementation type: buildOptions.accountThemeImplementation
}; };
})(), })(),
admin: { admin: (() => {
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "admin")) const dirPath = pathJoin(themeSrcDirPath, "admin");
if (!fs.existsSync(dirPath)) {
return { isImplemented: false, isImplemented_native: false };
} }
if (getIsNative(dirPath)) {
return { isImplemented: false, isImplemented_native: true };
}
return { isImplemented: true };
})()
}; };
})();
if ( if (
implementedThemeTypes.account.isImplemented && implementedThemeTypes.account.isImplemented &&