2024-06-09 09:15:16 +02:00
|
|
|
import type { BuildContext } from "../../shared/buildContext";
|
2024-06-10 07:57:12 +02:00
|
|
|
import fs from "fs";
|
|
|
|
import { rmSync } from "../../tools/fs.rmSync";
|
2024-09-27 23:05:51 +02:00
|
|
|
import { transformCodebase } from "../../tools/transformCodebase";
|
|
|
|
import {
|
|
|
|
join as pathJoin,
|
|
|
|
relative as pathRelative,
|
|
|
|
dirname as pathDirname,
|
|
|
|
extname as pathExtname,
|
|
|
|
sep as pathSep
|
|
|
|
} from "path";
|
|
|
|
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
|
|
|
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
|
|
|
import {
|
|
|
|
generateFtlFilesCodeFactory,
|
|
|
|
type BuildContextLike as BuildContextLike_kcContextExclusionsFtlCode
|
|
|
|
} from "../generateFtl";
|
|
|
|
import {
|
|
|
|
type ThemeType,
|
|
|
|
LOGIN_THEME_PAGE_IDS,
|
|
|
|
ACCOUNT_THEME_PAGE_IDS,
|
2024-10-26 21:23:18 +02:00
|
|
|
WELL_KNOWN_DIRECTORY_BASE_NAME,
|
2024-11-30 16:58:48 +01:00
|
|
|
THEME_TYPES,
|
|
|
|
KEYCLOAKIFY_SPA_DEV_SERVER_PORT
|
2024-09-27 23:05:51 +02:00
|
|
|
} from "../../shared/constants";
|
|
|
|
import { assert, type Equals } from "tsafe/assert";
|
|
|
|
import { readFieldNameUsage } from "./readFieldNameUsage";
|
|
|
|
import { readExtraPagesNames } from "./readExtraPageNames";
|
|
|
|
import {
|
|
|
|
generateMessageProperties,
|
|
|
|
type BuildContextLike as BuildContextLike_generateMessageProperties
|
|
|
|
} from "./generateMessageProperties";
|
|
|
|
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
|
|
|
|
import {
|
|
|
|
writeMetaInfKeycloakThemes,
|
|
|
|
type MetaInfKeycloakTheme
|
|
|
|
} from "../../shared/metaInfKeycloakThemes";
|
|
|
|
import { objectEntries } from "tsafe/objectEntries";
|
|
|
|
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
|
|
|
|
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
|
|
|
import propertiesParser from "properties-parser";
|
2024-12-21 12:08:43 +01:00
|
|
|
import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed";
|
2024-12-27 01:36:29 +01:00
|
|
|
import { listInstalledModules } from "../../tools/listInstalledModules";
|
2025-01-04 23:24:44 +01:00
|
|
|
import { isInside } from "../../tools/isInside";
|
2022-08-16 14:41:06 +07:00
|
|
|
|
2024-09-27 23:05:51 +02:00
|
|
|
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
|
|
|
BuildContextLike_generateMessageProperties & {
|
|
|
|
themeNames: string[];
|
|
|
|
extraThemeProperties: string[] | undefined;
|
|
|
|
projectDirPath: string;
|
|
|
|
projectBuildDirPath: string;
|
|
|
|
environmentVariables: { name: string; default: string }[];
|
|
|
|
implementedThemeTypes: BuildContext["implementedThemeTypes"];
|
|
|
|
themeSrcDirPath: string;
|
|
|
|
bundler: "vite" | "webpack";
|
|
|
|
packageJsonFilePath: string;
|
|
|
|
};
|
2022-08-16 14:41:06 +07:00
|
|
|
|
2024-06-09 09:15:16 +02:00
|
|
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
2021-03-22 20:54:28 +01:00
|
|
|
|
2024-06-12 14:48:08 +02:00
|
|
|
export async function generateResources(params: {
|
2024-06-09 09:15:16 +02:00
|
|
|
buildContext: BuildContextLike;
|
2024-06-10 07:57:12 +02:00
|
|
|
resourcesDirPath: string;
|
2024-05-20 15:48:51 +02:00
|
|
|
}): Promise<void> {
|
2024-06-10 07:57:12 +02:00
|
|
|
const { resourcesDirPath, buildContext } = params;
|
2023-03-16 22:13:46 +01:00
|
|
|
|
2024-09-27 23:37:23 +02:00
|
|
|
const [themeName] = buildContext.themeNames;
|
2021-02-21 18:55:06 +01:00
|
|
|
|
2024-06-10 07:57:12 +02:00
|
|
|
if (fs.existsSync(resourcesDirPath)) {
|
|
|
|
rmSync(resourcesDirPath, { recursive: true });
|
|
|
|
}
|
|
|
|
|
2024-09-28 00:05:19 +02:00
|
|
|
const getThemeTypeDirPath = (params: {
|
|
|
|
themeType: ThemeType | "email";
|
|
|
|
themeName: string;
|
|
|
|
}) => {
|
|
|
|
const { themeType, themeName } = params;
|
2024-09-27 23:05:51 +02:00
|
|
|
return pathJoin(resourcesDirPath, "theme", themeName, themeType);
|
|
|
|
};
|
|
|
|
|
|
|
|
const writeMessagePropertiesFilesByThemeType: Partial<
|
2025-01-04 23:24:44 +01:00
|
|
|
Record<
|
|
|
|
ThemeType | "email",
|
|
|
|
(params: { messageDirPath: string; themeName: string }) => void
|
|
|
|
>
|
2024-09-27 23:05:51 +02:00
|
|
|
> = {};
|
|
|
|
|
2025-01-04 23:24:44 +01:00
|
|
|
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
|
|
|
let isNative: boolean;
|
|
|
|
|
|
|
|
{
|
|
|
|
const v = buildContext.implementedThemeTypes[themeType];
|
|
|
|
|
|
|
|
if (!v.isImplemented && !v.isImplemented_native) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
isNative = !v.isImplemented && v.isImplemented_native;
|
2024-09-27 23:05:51 +02:00
|
|
|
}
|
|
|
|
|
2024-10-26 21:23:18 +02:00
|
|
|
const getAccountThemeType = () => {
|
|
|
|
assert(themeType === "account");
|
|
|
|
|
|
|
|
assert(buildContext.implementedThemeTypes.account.isImplemented);
|
|
|
|
|
|
|
|
return buildContext.implementedThemeTypes.account.type;
|
|
|
|
};
|
|
|
|
|
|
|
|
const isSpa = (() => {
|
|
|
|
switch (themeType) {
|
|
|
|
case "login":
|
|
|
|
return false;
|
|
|
|
case "account":
|
|
|
|
return getAccountThemeType() === "Single-Page";
|
|
|
|
case "admin":
|
|
|
|
return true;
|
2025-01-04 23:24:44 +01:00
|
|
|
case "email":
|
|
|
|
return false;
|
2024-10-26 21:23:18 +02:00
|
|
|
}
|
|
|
|
})();
|
2024-09-27 23:05:51 +02:00
|
|
|
|
2024-09-28 00:05:19 +02:00
|
|
|
const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
|
2024-09-27 23:05:51 +02:00
|
|
|
|
|
|
|
apply_replacers_and_move_to_theme_resources: {
|
2025-01-04 23:24:44 +01:00
|
|
|
if (isNative) {
|
|
|
|
break apply_replacers_and_move_to_theme_resources;
|
|
|
|
}
|
|
|
|
|
2024-09-27 23:05:51 +02:00
|
|
|
const destDirPath = pathJoin(
|
|
|
|
themeTypeDirPath,
|
|
|
|
"resources",
|
|
|
|
WELL_KNOWN_DIRECTORY_BASE_NAME.DIST
|
|
|
|
);
|
|
|
|
|
|
|
|
// NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
|
|
|
|
rmSync(destDirPath, { recursive: true, force: true });
|
|
|
|
|
|
|
|
if (
|
2024-10-26 21:23:18 +02:00
|
|
|
themeType !== "login" &&
|
2024-09-27 23:05:51 +02:00
|
|
|
buildContext.implementedThemeTypes.login.isImplemented
|
|
|
|
) {
|
|
|
|
// NOTE: We prevent doing it twice, it has been done for the login theme.
|
|
|
|
|
|
|
|
transformCodebase({
|
|
|
|
srcDirPath: pathJoin(
|
|
|
|
getThemeTypeDirPath({
|
2024-09-28 00:05:19 +02:00
|
|
|
themeName,
|
2024-09-27 23:05:51 +02:00
|
|
|
themeType: "login"
|
|
|
|
}),
|
|
|
|
"resources",
|
|
|
|
WELL_KNOWN_DIRECTORY_BASE_NAME.DIST
|
|
|
|
),
|
|
|
|
destDirPath
|
|
|
|
});
|
|
|
|
|
|
|
|
break apply_replacers_and_move_to_theme_resources;
|
|
|
|
}
|
|
|
|
|
|
|
|
{
|
|
|
|
const dirPath = pathJoin(
|
|
|
|
buildContext.projectBuildDirPath,
|
|
|
|
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
|
|
|
|
);
|
|
|
|
|
|
|
|
if (fs.existsSync(dirPath)) {
|
|
|
|
assert(buildContext.bundler === "webpack");
|
|
|
|
|
|
|
|
throw new Error(
|
|
|
|
[
|
|
|
|
`Keycloakify build error: The ${WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES} directory shouldn't exist in your build directory.`,
|
|
|
|
`(${pathRelative(process.cwd(), dirPath)}).\n`,
|
|
|
|
`Theses assets are only required for local development with Storybook.",
|
|
|
|
"Please remove this directory as an additional step of your command.\n`,
|
|
|
|
`For example: \`"build": "... && rimraf ${pathRelative(buildContext.projectDirPath, dirPath)}"\``
|
|
|
|
].join(" ")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
transformCodebase({
|
|
|
|
srcDirPath: buildContext.projectBuildDirPath,
|
|
|
|
destDirPath,
|
|
|
|
transformSourceCode: ({ filePath, fileRelativePath, sourceCode }) => {
|
|
|
|
if (filePath.endsWith(".css")) {
|
|
|
|
const { fixedCssCode } = replaceImportsInCssCode({
|
|
|
|
cssCode: sourceCode.toString("utf8"),
|
|
|
|
cssFileRelativeDirPath: pathDirname(fileRelativePath),
|
|
|
|
buildContext
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
modifiedSourceCode: Buffer.from(fixedCssCode, "utf8")
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (filePath.endsWith(".js")) {
|
|
|
|
const { fixedJsCode } = replaceImportsInJsCode({
|
|
|
|
jsCode: sourceCode.toString("utf8"),
|
|
|
|
buildContext
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
modifiedSourceCode: Buffer.from(fixedJsCode, "utf8")
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return { modifiedSourceCode: sourceCode };
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-01-04 23:24:44 +01:00
|
|
|
generate_ftl_files: {
|
|
|
|
if (isNative) {
|
|
|
|
break generate_ftl_files;
|
|
|
|
}
|
2022-08-16 14:41:06 +07:00
|
|
|
|
2025-01-04 23:24:44 +01:00
|
|
|
assert(themeType !== "email");
|
2024-09-27 23:05:51 +02:00
|
|
|
|
2025-01-04 23:24:44 +01:00
|
|
|
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
|
|
|
themeName,
|
|
|
|
indexHtmlCode: fs
|
|
|
|
.readFileSync(
|
|
|
|
pathJoin(buildContext.projectBuildDirPath, "index.html")
|
|
|
|
)
|
|
|
|
.toString("utf8"),
|
|
|
|
buildContext,
|
|
|
|
keycloakifyVersion: readThisNpmPackageVersion(),
|
|
|
|
themeType,
|
|
|
|
fieldNames: isSpa
|
|
|
|
? []
|
|
|
|
: (assert(themeType !== "admin"),
|
|
|
|
readFieldNameUsage({
|
|
|
|
themeSrcDirPath: buildContext.themeSrcDirPath,
|
|
|
|
themeType
|
|
|
|
}))
|
|
|
|
});
|
|
|
|
|
|
|
|
[
|
|
|
|
...(() => {
|
|
|
|
switch (themeType) {
|
|
|
|
case "login":
|
|
|
|
return LOGIN_THEME_PAGE_IDS;
|
|
|
|
case "account":
|
|
|
|
return getAccountThemeType() === "Single-Page"
|
|
|
|
? ["index.ftl"]
|
|
|
|
: ACCOUNT_THEME_PAGE_IDS;
|
|
|
|
case "admin":
|
|
|
|
return ["index.ftl"];
|
|
|
|
}
|
|
|
|
})(),
|
|
|
|
...(isSpa
|
|
|
|
? []
|
|
|
|
: readExtraPagesNames({
|
|
|
|
themeType,
|
|
|
|
themeSrcDirPath: buildContext.themeSrcDirPath
|
|
|
|
}))
|
|
|
|
].forEach(pageId => {
|
|
|
|
const { ftlCode } = generateFtlFilesCode({ pageId });
|
|
|
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
pathJoin(themeTypeDirPath, pageId),
|
|
|
|
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 };
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2024-09-27 23:05:51 +02:00
|
|
|
|
|
|
|
let languageTags: string[] | undefined = undefined;
|
|
|
|
|
2024-12-27 01:36:29 +01:00
|
|
|
i18n_multi_page: {
|
2025-01-04 23:24:44 +01:00
|
|
|
if (isNative) {
|
|
|
|
break i18n_multi_page;
|
|
|
|
}
|
|
|
|
|
2024-10-26 21:23:18 +02:00
|
|
|
if (isSpa) {
|
2024-12-27 01:36:29 +01:00
|
|
|
break i18n_multi_page;
|
2024-09-27 23:05:51 +02:00
|
|
|
}
|
|
|
|
|
2025-01-04 23:24:44 +01:00
|
|
|
assert(themeType !== "admin" && themeType !== "email");
|
2024-10-26 21:23:18 +02:00
|
|
|
|
2024-09-27 23:05:51 +02:00
|
|
|
const wrap = generateMessageProperties({
|
|
|
|
buildContext,
|
|
|
|
themeType
|
|
|
|
});
|
|
|
|
|
|
|
|
languageTags = wrap.languageTags;
|
|
|
|
const { writeMessagePropertiesFiles } = wrap;
|
|
|
|
|
|
|
|
writeMessagePropertiesFilesByThemeType[themeType] =
|
|
|
|
writeMessagePropertiesFiles;
|
|
|
|
}
|
|
|
|
|
2024-12-27 01:36:29 +01:00
|
|
|
let isLegacyAccountSpa = false;
|
|
|
|
|
|
|
|
// NOTE: Eventually remove this block.
|
|
|
|
i18n_single_page_account_legacy: {
|
2024-10-26 21:23:18 +02:00
|
|
|
if (!isSpa) {
|
2024-12-27 01:36:29 +01:00
|
|
|
break i18n_single_page_account_legacy;
|
2024-09-27 23:05:51 +02:00
|
|
|
}
|
|
|
|
|
2024-12-21 12:08:43 +01:00
|
|
|
if (themeType !== "account") {
|
2024-12-27 01:36:29 +01:00
|
|
|
break i18n_single_page_account_legacy;
|
2024-12-21 12:08:43 +01:00
|
|
|
}
|
2024-10-26 21:23:18 +02:00
|
|
|
|
2024-12-27 01:36:29 +01:00
|
|
|
const [moduleMeta] = await listInstalledModules({
|
|
|
|
packageJsonFilePath: buildContext.packageJsonFilePath,
|
|
|
|
filter: ({ moduleName }) =>
|
|
|
|
moduleName === "@keycloakify/keycloak-account-ui"
|
|
|
|
});
|
|
|
|
|
|
|
|
assert(
|
|
|
|
moduleMeta !== undefined,
|
|
|
|
`@keycloakify/keycloak-account-ui is supposed to be installed`
|
|
|
|
);
|
|
|
|
|
|
|
|
{
|
|
|
|
const [majorStr] = moduleMeta.version.split(".");
|
|
|
|
|
|
|
|
if (majorStr.length === 6) {
|
|
|
|
// NOTE: Now we use the format MMmmpp (Major, minor, patch) for example for
|
|
|
|
// 26.0.7 it would be 260007.
|
|
|
|
break i18n_single_page_account_legacy;
|
|
|
|
} else {
|
|
|
|
// 25.0.4-rc.5 or later
|
|
|
|
isLegacyAccountSpa = true;
|
|
|
|
}
|
|
|
|
}
|
2024-09-27 23:05:51 +02:00
|
|
|
|
2024-12-27 01:36:29 +01:00
|
|
|
const messageDirPath_defaults = pathJoin(moduleMeta.dirPath, "messages");
|
2024-09-27 23:05:51 +02:00
|
|
|
|
|
|
|
if (!fs.existsSync(messageDirPath_defaults)) {
|
|
|
|
throw new Error(
|
|
|
|
`Please update @keycloakify/keycloak-account-ui to 25.0.4-rc.5 or later.`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-12-27 01:36:29 +01:00
|
|
|
isLegacyAccountSpa = true;
|
|
|
|
|
2024-09-27 23:05:51 +02:00
|
|
|
const messagesDirPath_dest = pathJoin(
|
2024-12-21 12:08:43 +01:00
|
|
|
getThemeTypeDirPath({ themeName, themeType: "account" }),
|
2024-09-27 23:05:51 +02:00
|
|
|
"messages"
|
|
|
|
);
|
|
|
|
|
|
|
|
transformCodebase({
|
|
|
|
srcDirPath: messageDirPath_defaults,
|
|
|
|
destDirPath: messagesDirPath_dest
|
|
|
|
});
|
|
|
|
|
|
|
|
apply_theme_changes: {
|
|
|
|
const messagesDirPath_theme = pathJoin(
|
|
|
|
buildContext.themeSrcDirPath,
|
2024-12-21 12:08:43 +01:00
|
|
|
"account",
|
2024-09-27 23:05:51 +02:00
|
|
|
"messages"
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!fs.existsSync(messagesDirPath_theme)) {
|
|
|
|
break apply_theme_changes;
|
|
|
|
}
|
|
|
|
|
|
|
|
fs.readdirSync(messagesDirPath_theme).forEach(basename => {
|
|
|
|
const filePath_src = pathJoin(messagesDirPath_theme, basename);
|
|
|
|
const filePath_dest = pathJoin(messagesDirPath_dest, basename);
|
|
|
|
|
|
|
|
if (!fs.existsSync(filePath_dest)) {
|
|
|
|
fs.cpSync(filePath_src, filePath_dest);
|
|
|
|
}
|
|
|
|
|
|
|
|
const messages_src = propertiesParser.parse(
|
|
|
|
fs.readFileSync(filePath_src).toString("utf8")
|
|
|
|
);
|
|
|
|
const messages_dest = propertiesParser.parse(
|
|
|
|
fs.readFileSync(filePath_dest).toString("utf8")
|
|
|
|
);
|
|
|
|
|
|
|
|
const messages = {
|
|
|
|
...messages_dest,
|
|
|
|
...messages_src
|
|
|
|
};
|
|
|
|
|
|
|
|
const editor = propertiesParser.createEditor();
|
|
|
|
|
|
|
|
Object.entries(messages).forEach(([key, value]) => {
|
|
|
|
editor.set(key, value);
|
|
|
|
});
|
|
|
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
filePath_dest,
|
|
|
|
Buffer.from(editor.toString(), "utf8")
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
languageTags = fs
|
|
|
|
.readdirSync(messagesDirPath_dest)
|
|
|
|
.map(basename =>
|
|
|
|
basename.replace(/^messages_/, "").replace(/\.properties$/, "")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-01-04 23:24:44 +01:00
|
|
|
i18n_for_spas_and_native: {
|
|
|
|
if (!isSpa && !isNative) {
|
|
|
|
break i18n_for_spas_and_native;
|
2024-12-27 01:36:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (isLegacyAccountSpa) {
|
2025-01-04 23:24:44 +01:00
|
|
|
break i18n_for_spas_and_native;
|
2024-12-21 12:08:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const messagesDirPath_theme = pathJoin(
|
|
|
|
buildContext.themeSrcDirPath,
|
2024-12-27 01:36:29 +01:00
|
|
|
themeType,
|
2025-01-04 23:24:44 +01:00
|
|
|
isNative ? "messages" : "i18n"
|
2024-12-21 12:08:43 +01:00
|
|
|
);
|
|
|
|
|
2025-01-04 23:24:44 +01:00
|
|
|
if (!fs.existsSync(messagesDirPath_theme)) {
|
|
|
|
break i18n_for_spas_and_native;
|
|
|
|
}
|
2024-12-21 12:08:43 +01:00
|
|
|
|
|
|
|
const propertiesByLang: Record<
|
|
|
|
string,
|
|
|
|
{
|
|
|
|
base: Buffer;
|
|
|
|
override: Buffer | undefined;
|
|
|
|
overrideByThemeName: Record<string, Buffer>;
|
|
|
|
}
|
|
|
|
> = {};
|
|
|
|
|
|
|
|
fs.readdirSync(messagesDirPath_theme).forEach(basename => {
|
|
|
|
type ParsedBasename = { lang: string } & (
|
|
|
|
| {
|
|
|
|
isOverride: false;
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
isOverride: true;
|
|
|
|
themeName: string | undefined;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
const parsedBasename = ((): ParsedBasename | undefined => {
|
|
|
|
const match = basename.match(/^messages_([^.]+)\.properties$/);
|
|
|
|
|
|
|
|
if (match === null) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const discriminator = match[1];
|
|
|
|
|
|
|
|
const split = discriminator.split("_override");
|
|
|
|
|
|
|
|
if (split.length === 1) {
|
|
|
|
return {
|
|
|
|
lang: discriminator,
|
|
|
|
isOverride: false
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
assert(split.length === 2);
|
|
|
|
|
|
|
|
if (split[1] === "") {
|
|
|
|
return {
|
|
|
|
lang: split[0],
|
|
|
|
isOverride: true,
|
|
|
|
themeName: undefined
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const match2 = split[1].match(/^_(.+)$/);
|
|
|
|
|
|
|
|
assert(match2 !== null);
|
|
|
|
|
|
|
|
return {
|
|
|
|
lang: split[0],
|
|
|
|
isOverride: true,
|
|
|
|
themeName: match2[1]
|
|
|
|
};
|
|
|
|
})();
|
|
|
|
|
|
|
|
if (parsedBasename === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
propertiesByLang[parsedBasename.lang] ??= {
|
|
|
|
base: createObjectThatThrowsIfAccessed<Buffer>({
|
2024-12-27 01:36:29 +01:00
|
|
|
debugMessage: `No base ${parsedBasename.lang} translation for ${themeType} theme`
|
2024-12-21 12:08:43 +01:00
|
|
|
}),
|
|
|
|
override: undefined,
|
|
|
|
overrideByThemeName: {}
|
|
|
|
};
|
|
|
|
|
|
|
|
const buffer = fs.readFileSync(pathJoin(messagesDirPath_theme, basename));
|
|
|
|
|
|
|
|
if (parsedBasename.isOverride === false) {
|
|
|
|
propertiesByLang[parsedBasename.lang].base = buffer;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (parsedBasename.themeName === undefined) {
|
|
|
|
propertiesByLang[parsedBasename.lang].override = buffer;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
propertiesByLang[parsedBasename.lang].overrideByThemeName[
|
|
|
|
parsedBasename.themeName
|
|
|
|
] = buffer;
|
|
|
|
});
|
|
|
|
|
2024-12-27 01:36:29 +01:00
|
|
|
languageTags = Object.keys(propertiesByLang);
|
|
|
|
|
|
|
|
writeMessagePropertiesFilesByThemeType[themeType] = ({
|
2024-12-21 12:08:43 +01:00
|
|
|
messageDirPath,
|
|
|
|
themeName
|
|
|
|
}) => {
|
|
|
|
if (!fs.existsSync(messageDirPath)) {
|
|
|
|
fs.mkdirSync(messageDirPath, { recursive: true });
|
|
|
|
}
|
|
|
|
|
|
|
|
Object.entries(propertiesByLang).forEach(
|
|
|
|
([lang, { base, override, overrideByThemeName }]) => {
|
|
|
|
const messages = propertiesParser.parse(base.toString("utf8"));
|
|
|
|
|
|
|
|
if (override !== undefined) {
|
|
|
|
const overrideMessages = propertiesParser.parse(
|
|
|
|
override.toString("utf8")
|
|
|
|
);
|
|
|
|
|
|
|
|
Object.entries(overrideMessages).forEach(
|
|
|
|
([key, value]) => (messages[key] = value)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (themeName in overrideByThemeName) {
|
|
|
|
const overrideMessages = propertiesParser.parse(
|
|
|
|
overrideByThemeName[themeName].toString("utf8")
|
|
|
|
);
|
|
|
|
|
|
|
|
Object.entries(overrideMessages).forEach(
|
|
|
|
([key, value]) => (messages[key] = value)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const editor = propertiesParser.createEditor();
|
|
|
|
|
|
|
|
Object.entries(messages).forEach(([key, value]) => {
|
|
|
|
editor.set(key, value);
|
|
|
|
});
|
|
|
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
pathJoin(messageDirPath, `messages_${lang}.properties`),
|
|
|
|
Buffer.from(editor.toString(), "utf8")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-09-27 23:05:51 +02:00
|
|
|
keycloak_static_resources: {
|
2025-01-04 23:24:44 +01:00
|
|
|
if (isNative) {
|
|
|
|
break keycloak_static_resources;
|
|
|
|
}
|
|
|
|
|
2024-10-26 21:23:18 +02:00
|
|
|
if (isSpa) {
|
2024-09-27 23:05:51 +02:00
|
|
|
break keycloak_static_resources;
|
|
|
|
}
|
|
|
|
|
|
|
|
transformCodebase({
|
|
|
|
srcDirPath: pathJoin(
|
|
|
|
getThisCodebaseRootDirPath(),
|
|
|
|
"res",
|
|
|
|
"public",
|
|
|
|
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES,
|
|
|
|
themeType
|
|
|
|
),
|
|
|
|
destDirPath: pathJoin(themeTypeDirPath, "resources")
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-01-04 23:24:44 +01:00
|
|
|
bring_in_account_v1: {
|
|
|
|
if (isNative) {
|
|
|
|
break bring_in_account_v1;
|
|
|
|
}
|
2024-09-27 23:05:51 +02:00
|
|
|
|
2025-01-04 23:24:44 +01:00
|
|
|
if (themeType !== "account") {
|
|
|
|
break bring_in_account_v1;
|
|
|
|
}
|
2024-09-27 23:05:51 +02:00
|
|
|
|
2025-01-04 23:24:44 +01:00
|
|
|
assert(buildContext.implementedThemeTypes.account.isImplemented);
|
2024-09-27 23:05:51 +02:00
|
|
|
|
2025-01-04 23:24:44 +01:00
|
|
|
if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
|
|
|
|
break bring_in_account_v1;
|
|
|
|
}
|
2024-09-27 23:05:51 +02:00
|
|
|
|
2025-01-04 23:24:44 +01:00
|
|
|
transformCodebase({
|
|
|
|
srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "res", "account-v1"),
|
|
|
|
destDirPath: getThemeTypeDirPath({
|
|
|
|
themeName: "account-v1",
|
|
|
|
themeType: "account"
|
|
|
|
})
|
|
|
|
});
|
2024-09-27 23:05:51 +02:00
|
|
|
}
|
|
|
|
|
2025-01-04 23:24:44 +01:00
|
|
|
generate_theme_properties: {
|
|
|
|
if (isNative) {
|
|
|
|
break generate_theme_properties;
|
|
|
|
}
|
|
|
|
|
|
|
|
assert(themeType !== "email");
|
|
|
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
pathJoin(themeTypeDirPath, "theme.properties"),
|
|
|
|
Buffer.from(
|
|
|
|
[
|
|
|
|
`parent=${(() => {
|
|
|
|
switch (themeType) {
|
|
|
|
case "account":
|
|
|
|
switch (getAccountThemeType()) {
|
|
|
|
case "Multi-Page":
|
|
|
|
return "account-v1";
|
|
|
|
case "Single-Page":
|
|
|
|
return "base";
|
|
|
|
}
|
|
|
|
case "login":
|
|
|
|
return "keycloak";
|
|
|
|
case "admin":
|
|
|
|
return "base";
|
|
|
|
}
|
|
|
|
assert<Equals<typeof themeType, never>>;
|
|
|
|
})()}`,
|
|
|
|
...(themeType === "account" &&
|
|
|
|
getAccountThemeType() === "Single-Page"
|
|
|
|
? ["deprecatedMode=false"]
|
|
|
|
: []),
|
|
|
|
...(buildContext.extraThemeProperties ?? []),
|
|
|
|
...[
|
|
|
|
...buildContext.environmentVariables,
|
|
|
|
{ name: KEYCLOAKIFY_SPA_DEV_SERVER_PORT, default: "" }
|
|
|
|
].map(
|
|
|
|
({ name, default: defaultValue }) =>
|
|
|
|
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
|
|
|
|
),
|
|
|
|
...(languageTags === undefined
|
|
|
|
? []
|
|
|
|
: [`locales=${languageTags.join(",")}`])
|
|
|
|
].join("\n\n"),
|
|
|
|
"utf8"
|
|
|
|
)
|
|
|
|
);
|
2024-09-27 23:05:51 +02:00
|
|
|
}
|
2025-01-04 23:24:44 +01:00
|
|
|
}
|
2024-09-27 23:05:51 +02:00
|
|
|
|
2025-01-04 23:24:44 +01:00
|
|
|
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) {
|
|
|
|
break copy_main_theme_to_theme_variant_theme;
|
|
|
|
}
|
|
|
|
|
|
|
|
transformCodebase({
|
|
|
|
srcDirPath: pathJoin(resourcesDirPath, "theme", themeName),
|
|
|
|
destDirPath: pathJoin(resourcesDirPath, "theme", themeVariantName),
|
|
|
|
transformSourceCode: isNative
|
|
|
|
? undefined
|
|
|
|
: ({ fileRelativePath, sourceCode }) => {
|
|
|
|
if (
|
|
|
|
pathExtname(fileRelativePath) === ".ftl" &&
|
|
|
|
fileRelativePath.split(pathSep).length === 2
|
|
|
|
) {
|
|
|
|
const modifiedSourceCode = Buffer.from(
|
|
|
|
Buffer.from(sourceCode)
|
|
|
|
.toString("utf-8")
|
|
|
|
.replace(
|
|
|
|
`"themeName": "${themeName}"`,
|
|
|
|
`"themeName": "${themeVariantName}"`
|
|
|
|
),
|
|
|
|
"utf8"
|
|
|
|
);
|
|
|
|
|
|
|
|
assert(
|
|
|
|
Buffer.compare(modifiedSourceCode, sourceCode) !== 0
|
|
|
|
);
|
|
|
|
|
|
|
|
return { modifiedSourceCode };
|
|
|
|
}
|
|
|
|
|
|
|
|
return { modifiedSourceCode: sourceCode };
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
run_writeMessagePropertiesFiles: {
|
|
|
|
const writeMessagePropertiesFiles =
|
|
|
|
writeMessagePropertiesFilesByThemeType[themeType];
|
|
|
|
|
|
|
|
if (writeMessagePropertiesFiles === undefined) {
|
|
|
|
break run_writeMessagePropertiesFiles;
|
|
|
|
}
|
|
|
|
|
|
|
|
writeMessagePropertiesFiles({
|
|
|
|
messageDirPath: pathJoin(
|
|
|
|
getThemeTypeDirPath({ themeName: themeVariantName, themeType }),
|
|
|
|
"messages"
|
|
|
|
),
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const emailThemeDirPath = getThemeTypeDirPath({
|
|
|
|
themeName,
|
|
|
|
themeType
|
|
|
|
});
|
|
|
|
|
|
|
|
transformCodebase({
|
|
|
|
srcDirPath: emailThemeDirPath,
|
|
|
|
destDirPath: emailThemeDirPath,
|
|
|
|
transformSourceCode: ({ filePath, sourceCode }) => {
|
|
|
|
if (!filePath.endsWith(".ftl")) {
|
|
|
|
return { modifiedSourceCode: sourceCode };
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
modifiedSourceCode: Buffer.from(
|
|
|
|
sourceCode
|
|
|
|
.toString("utf8")
|
|
|
|
.replace(
|
|
|
|
/xKeycloakify\.themeName/g,
|
|
|
|
`"${themeName}"`
|
|
|
|
),
|
|
|
|
"utf8"
|
|
|
|
)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2024-09-27 23:05:51 +02:00
|
|
|
}
|
|
|
|
|
2025-01-04 23:24:44 +01:00
|
|
|
// Generate meta-inf/keycloak-themes.json
|
2024-09-27 23:05:51 +02:00
|
|
|
{
|
|
|
|
const metaInfKeycloakThemes: MetaInfKeycloakTheme = { themes: [] };
|
|
|
|
|
2024-09-27 23:45:14 +02:00
|
|
|
for (const themeName of buildContext.themeNames) {
|
|
|
|
metaInfKeycloakThemes.themes.push({
|
|
|
|
name: themeName,
|
|
|
|
types: objectEntries(buildContext.implementedThemeTypes)
|
2025-01-04 23:24:44 +01:00
|
|
|
.filter(([, v]) => v.isImplemented || v.isImplemented_native)
|
2024-09-27 23:45:14 +02:00
|
|
|
.map(([themeType]) => themeType)
|
|
|
|
});
|
|
|
|
}
|
2024-09-27 23:05:51 +02:00
|
|
|
|
2025-01-04 23:24:44 +01:00
|
|
|
if (
|
|
|
|
buildContext.implementedThemeTypes.account.isImplemented &&
|
|
|
|
buildContext.implementedThemeTypes.account.type === "Multi-Page"
|
|
|
|
) {
|
2024-09-27 23:05:51 +02:00
|
|
|
metaInfKeycloakThemes.themes.push({
|
|
|
|
name: "account-v1",
|
|
|
|
types: ["account"]
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
writeMetaInfKeycloakThemes({
|
|
|
|
resourcesDirPath,
|
|
|
|
getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
|
|
|
|
});
|
|
|
|
}
|
2021-02-21 18:55:06 +01:00
|
|
|
}
|