keycloak_theme/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts

367 lines
13 KiB
TypeScript
Raw Normal View History

2024-05-12 20:47:03 +02:00
import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs";
import {
join as pathJoin,
resolve as pathResolve,
relative as pathRelative,
2024-07-08 14:54:09 +02:00
dirname as pathDirname,
basename as pathBasename
} from "path";
2024-05-12 20:47:03 +02:00
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
2024-06-06 01:31:00 +02:00
import {
generateFtlFilesCodeFactory,
2024-06-09 09:15:16 +02:00
type BuildContextLike as BuildContextLike_kcContextExclusionsFtlCode
2024-06-06 01:31:00 +02:00
} from "../generateFtl";
2024-05-12 20:47:03 +02:00
import {
type ThemeType,
2024-07-13 19:33:59 +02:00
LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1,
KEYCLOAK_RESOURCES,
ACCOUNT_V1_THEME_NAME,
BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR,
LOGIN_THEME_PAGE_IDS,
ACCOUNT_THEME_PAGE_IDS
} from "../../shared/constants";
2024-06-09 09:15:16 +02:00
import type { BuildContext } from "../../shared/buildContext";
2024-05-12 20:47:03 +02:00
import { assert, type Equals } from "tsafe/assert";
2024-06-06 01:31:00 +02:00
import {
downloadKeycloakStaticResources,
2024-06-09 09:15:16 +02:00
type BuildContextLike as BuildContextLike_downloadKeycloakStaticResources
2024-06-06 01:31:00 +02:00
} from "../../shared/downloadKeycloakStaticResources";
2024-05-12 20:47:03 +02:00
import { readFieldNameUsage } from "./readFieldNameUsage";
import { readExtraPagesNames } from "./readExtraPageNames";
import { generateMessageProperties } from "./generateMessageProperties";
2024-06-06 01:31:00 +02:00
import {
bringInAccountV1,
2024-06-09 09:15:16 +02:00
type BuildContextLike as BuildContextLike_bringInAccountV1
2024-06-06 01:31:00 +02:00
} from "./bringInAccountV1";
2024-05-12 20:47:03 +02:00
import { rmSync } from "../../tools/fs.rmSync";
2024-05-18 10:24:55 +02:00
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
2024-05-20 15:48:51 +02:00
import {
writeMetaInfKeycloakThemes,
type MetaInfKeycloakTheme
} from "../../shared/metaInfKeycloakThemes";
import { objectEntries } from "tsafe/objectEntries";
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
2024-07-08 14:54:09 +02:00
import { downloadAndExtractArchive } from "../../tools/downloadAndExtractArchive";
2024-05-12 20:47:03 +02:00
2024-06-09 09:15:16 +02:00
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
BuildContextLike_downloadKeycloakStaticResources &
BuildContextLike_bringInAccountV1 & {
2024-06-06 01:31:00 +02:00
extraThemeProperties: string[] | undefined;
loginThemeResourcesFromKeycloakVersion: string;
projectDirPath: string;
2024-06-10 07:57:12 +02:00
projectBuildDirPath: string;
environmentVariables: { name: string; default: string }[];
implementedThemeTypes: BuildContext["implementedThemeTypes"];
themeSrcDirPath: string;
bundler: "vite" | "webpack";
2024-06-06 01:31:00 +02:00
};
2024-05-12 20:47:03 +02:00
2024-06-09 09:15:16 +02:00
assert<BuildContext extends BuildContextLike ? true : false>();
2024-05-12 20:47:03 +02:00
export async function generateResourcesForMainTheme(params: {
2024-05-20 15:48:51 +02:00
themeName: string;
2024-06-10 07:57:12 +02:00
resourcesDirPath: string;
2024-06-09 09:15:16 +02:00
buildContext: BuildContextLike;
2024-05-20 15:48:51 +02:00
}): Promise<void> {
2024-06-10 07:57:12 +02:00
const { themeName, resourcesDirPath, buildContext } = params;
2024-05-12 20:47:03 +02:00
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
const { themeType } = params;
2024-06-10 07:57:12 +02:00
return pathJoin(resourcesDirPath, "theme", themeName, themeType);
2024-05-12 20:47:03 +02:00
};
for (const themeType of ["login", "account"] as const) {
if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
2024-05-12 20:47:03 +02:00
continue;
}
const isForAccountSpa =
themeType === "account" &&
(assert(buildContext.implementedThemeTypes.account.isImplemented),
buildContext.implementedThemeTypes.account.type === "Single-Page");
2024-05-12 20:47:03 +02:00
const themeTypeDirPath = getThemeTypeDirPath({ themeType });
apply_replacers_and_move_to_theme_resources: {
2024-05-20 15:48:51 +02:00
const destDirPath = pathJoin(
themeTypeDirPath,
"resources",
2024-07-13 19:33:59 +02:00
BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR
2024-05-20 15:48:51 +02:00
);
2024-05-12 20:47:03 +02:00
// NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
2024-05-20 15:48:51 +02:00
rmSync(destDirPath, { recursive: true, force: true });
2024-05-12 20:47:03 +02:00
if (
themeType === "account" &&
buildContext.implementedThemeTypes.login.isImplemented
) {
// NOTE: We prevent doing it twice, it has been done for the login theme.
2024-05-12 20:47:03 +02:00
transformCodebase({
2024-05-20 15:48:51 +02:00
srcDirPath: pathJoin(
2024-05-12 20:47:03 +02:00
getThemeTypeDirPath({
2024-05-20 15:48:51 +02:00
themeType: "login"
2024-05-12 20:47:03 +02:00
}),
"resources",
2024-07-13 19:33:59 +02:00
BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR
2024-05-12 20:47:03 +02:00
),
destDirPath
});
break apply_replacers_and_move_to_theme_resources;
}
{
const dirPath = pathJoin(
buildContext.projectBuildDirPath,
2024-07-13 19:33:59 +02:00
KEYCLOAK_RESOURCES
);
if (fs.existsSync(dirPath)) {
assert(buildContext.bundler === "webpack");
throw new Error(
[
2024-07-13 19:33:59 +02:00
`Keycloakify build error: The ${KEYCLOAK_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(" ")
);
}
}
2024-05-12 20:47:03 +02:00
transformCodebase({
2024-06-09 09:15:16 +02:00
srcDirPath: buildContext.projectBuildDirPath,
2024-05-12 20:47:03 +02:00
destDirPath,
transformSourceCode: ({ filePath, fileRelativePath, sourceCode }) => {
if (filePath.endsWith(".css")) {
const { fixedCssCode } = replaceImportsInCssCode({
cssCode: sourceCode.toString("utf8"),
2024-06-19 22:41:25 +02:00
cssFileRelativeDirPath: pathDirname(fileRelativePath),
2024-07-13 18:17:21 +02:00
buildContext
2024-05-12 20:47:03 +02:00
});
2024-05-20 15:48:51 +02:00
return {
modifiedSourceCode: Buffer.from(fixedCssCode, "utf8")
};
2024-05-12 20:47:03 +02:00
}
if (filePath.endsWith(".js")) {
2024-05-12 20:47:03 +02:00
const { fixedJsCode } = replaceImportsInJsCode({
2024-05-20 15:48:51 +02:00
jsCode: sourceCode.toString("utf8"),
2024-06-09 09:15:16 +02:00
buildContext
2024-05-12 20:47:03 +02:00
});
2024-05-20 15:48:51 +02:00
return {
modifiedSourceCode: Buffer.from(fixedJsCode, "utf8")
};
2024-05-12 20:47:03 +02:00
}
2024-05-20 15:48:51 +02:00
return { modifiedSourceCode: sourceCode };
2024-05-12 20:47:03 +02:00
}
});
}
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
themeName,
2024-05-20 15:48:51 +02:00
indexHtmlCode: fs
2024-06-09 09:15:16 +02:00
.readFileSync(pathJoin(buildContext.projectBuildDirPath, "index.html"))
2024-05-20 15:48:51 +02:00
.toString("utf8"),
2024-06-09 09:15:16 +02:00
buildContext,
2024-05-20 15:48:51 +02:00
keycloakifyVersion: readThisNpmPackageVersion(),
2024-05-12 20:47:03 +02:00
themeType,
2024-05-20 15:48:51 +02:00
fieldNames: readFieldNameUsage({
themeSrcDirPath: buildContext.themeSrcDirPath,
2024-05-12 20:47:03 +02:00
themeType
2024-07-13 18:17:21 +02:00
})
2024-05-12 20:47:03 +02:00
});
[
...(() => {
switch (themeType) {
case "login":
2024-07-13 19:33:59 +02:00
return LOGIN_THEME_PAGE_IDS;
2024-05-12 20:47:03 +02:00
case "account":
return isForAccountSpa ? ["index.ftl"] : ACCOUNT_THEME_PAGE_IDS;
2024-05-12 20:47:03 +02:00
}
})(),
...(isForAccountSpa
2024-07-07 18:45:14 +02:00
? []
: readExtraPagesNames({
themeType,
themeSrcDirPath: buildContext.themeSrcDirPath
}))
2024-05-12 20:47:03 +02:00
].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId });
2024-05-20 15:48:51 +02:00
fs.writeFileSync(
pathJoin(themeTypeDirPath, pageId),
Buffer.from(ftlCode, "utf8")
);
2024-05-12 20:47:03 +02:00
});
2024-07-07 18:45:14 +02:00
i18n_messages_generation: {
if (isForAccountSpa) {
2024-07-07 18:45:14 +02:00
break i18n_messages_generation;
}
2024-05-12 20:47:03 +02:00
2024-07-07 18:45:14 +02:00
generateMessageProperties({
themeSrcDirPath: buildContext.themeSrcDirPath,
themeType
}).forEach(({ languageTag, propertiesFileSource }) => {
const messagesDirPath = pathJoin(themeTypeDirPath, "messages");
2024-05-12 20:47:03 +02:00
2024-07-07 18:45:14 +02:00
fs.mkdirSync(pathJoin(themeTypeDirPath, "messages"), {
recursive: true
});
2024-05-12 20:47:03 +02:00
2024-07-07 18:45:14 +02:00
const propertiesFilePath = pathJoin(
messagesDirPath,
`messages_${languageTag}.properties`
);
2024-05-12 20:47:03 +02:00
2024-07-07 18:45:14 +02:00
fs.writeFileSync(
propertiesFilePath,
Buffer.from(propertiesFileSource, "utf8")
);
});
}
keycloak_static_resources: {
if (isForAccountSpa) {
2024-07-07 18:45:14 +02:00
break keycloak_static_resources;
}
await downloadKeycloakStaticResources({
keycloakVersion: (() => {
switch (themeType) {
case "account":
2024-07-13 19:33:59 +02:00
return LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1;
2024-07-07 18:45:14 +02:00
case "login":
return buildContext.loginThemeResourcesFromKeycloakVersion;
}
})(),
themeDirPath: pathResolve(pathJoin(themeTypeDirPath, "..")),
themeType,
buildContext
});
}
2024-05-12 20:47:03 +02:00
fs.writeFileSync(
pathJoin(themeTypeDirPath, "theme.properties"),
Buffer.from(
[
`parent=${(() => {
switch (themeType) {
case "account":
return isForAccountSpa ? "base" : ACCOUNT_V1_THEME_NAME;
2024-05-12 20:47:03 +02:00
case "login":
return "keycloak";
}
assert<Equals<typeof themeType, never>>(false);
})()}`,
...(isForAccountSpa ? ["deprecatedMode=false"] : []),
2024-06-09 09:15:16 +02:00
...(buildContext.extraThemeProperties ?? []),
...buildContext.environmentVariables.map(
({ name, default: defaultValue }) =>
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
)
2024-05-12 20:47:03 +02:00
].join("\n\n"),
"utf8"
)
);
}
email: {
if (!buildContext.implementedThemeTypes.email.isImplemented) {
2024-05-12 20:47:03 +02:00
break email;
}
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
2024-05-12 20:47:03 +02:00
transformCodebase({
2024-05-20 15:48:51 +02:00
srcDirPath: emailThemeSrcDirPath,
destDirPath: getThemeTypeDirPath({ themeType: "email" })
2024-05-12 20:47:03 +02:00
});
}
2024-07-07 18:45:14 +02:00
bring_in_account_v1: {
if (!buildContext.implementedThemeTypes.account.isImplemented) {
2024-07-07 18:45:14 +02:00
break bring_in_account_v1;
}
if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
2024-07-07 18:45:14 +02:00
break bring_in_account_v1;
}
2024-05-12 20:47:03 +02:00
await bringInAccountV1({
2024-06-10 07:57:12 +02:00
resourcesDirPath,
2024-06-09 09:15:16 +02:00
buildContext
2024-05-12 20:47:03 +02:00
});
}
2024-07-08 14:54:09 +02:00
bring_in_account_v3_i18n_messages: {
if (!buildContext.implementedThemeTypes.account.isImplemented) {
break bring_in_account_v3_i18n_messages;
}
if (buildContext.implementedThemeTypes.account.type !== "Single-Page") {
2024-07-08 14:54:09 +02:00
break bring_in_account_v3_i18n_messages;
}
const { extractedDirPath } = await downloadAndExtractArchive({
url: "https://repo1.maven.org/maven2/org/keycloak/keycloak-account-ui/25.0.1/keycloak-account-ui-25.0.1.jar",
2024-07-08 14:54:09 +02:00
cacheDirPath: buildContext.cacheDirPath,
fetchOptions: buildContext.fetchOptions,
uniqueIdOfOnArchiveFile: "bring_in_account_v3_i18n_messages",
onArchiveFile: async ({ fileRelativePath, writeFile }) => {
if (
!fileRelativePath.startsWith(
pathJoin("theme", "keycloak.v3", "account", "messages")
)
) {
return;
}
await writeFile({
fileRelativePath: pathBasename(fileRelativePath)
});
}
});
transformCodebase({
srcDirPath: extractedDirPath,
destDirPath: pathJoin(
getThemeTypeDirPath({ themeType: "account" }),
"messages"
)
});
}
2024-05-12 20:47:03 +02:00
{
2024-05-20 15:48:51 +02:00
const metaInfKeycloakThemes: MetaInfKeycloakTheme = { themes: [] };
metaInfKeycloakThemes.themes.push({
2024-05-20 15:48:51 +02:00
name: themeName,
types: objectEntries(buildContext.implementedThemeTypes)
.filter(([, { isImplemented }]) => isImplemented)
.map(([themeType]) => themeType)
});
2024-05-12 20:47:03 +02:00
if (buildContext.implementedThemeTypes.account.isImplemented) {
metaInfKeycloakThemes.themes.push({
2024-07-13 19:33:59 +02:00
name: ACCOUNT_V1_THEME_NAME,
2024-05-20 15:48:51 +02:00
types: ["account"]
});
}
2024-05-12 20:47:03 +02:00
writeMetaInfKeycloakThemes({
2024-06-10 07:57:12 +02:00
resourcesDirPath,
getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
});
2024-05-12 20:47:03 +02:00
}
}