Refactor how we update META-INF and how we read what theme types are implemented

This commit is contained in:
Joseph Garrone 2024-06-15 14:23:35 +02:00
parent 9e70e5c12e
commit 61fbbb0b09
8 changed files with 173 additions and 223 deletions

View File

@ -16,7 +16,7 @@ import { readFileSync } from "fs";
import { isInside } from "../../tools/isInside";
import child_process from "child_process";
import { rmSync } from "../../tools/fs.rmSync";
import { getMetaInfKeycloakThemesJsonFilePath } from "../../shared/metaInfKeycloakThemes";
import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
export type BuildContextLike = BuildContextLike_generatePom & {
keycloakifyBuildDirPath: string;
@ -50,9 +50,16 @@ export async function buildJar(params: {
rmSync(keycloakifyBuildTmpDirPath, { recursive: true, force: true });
const tmpResourcesDirPath = pathJoin(
keycloakifyBuildTmpDirPath,
"src",
"main",
"resources"
);
transformCodebase({
srcDirPath: resourcesDirPath,
destDirPath: pathJoin(keycloakifyBuildTmpDirPath, "src", "main", "resources"),
destDirPath: tmpResourcesDirPath,
transformSourceCode:
keycloakAccountV1Version !== null
? undefined
@ -71,31 +78,6 @@ export async function buildJar(params: {
return undefined;
}
if (
fileRelativePath ===
getMetaInfKeycloakThemesJsonFilePath({
resourcesDirPath: "."
})
) {
const keycloakThemesJsonParsed = JSON.parse(
sourceCode.toString("utf8")
) as {
themes: { name: string; types: string[] }[];
};
keycloakThemesJsonParsed.themes =
keycloakThemesJsonParsed.themes.filter(
({ name }) => name !== accountV1ThemeName
);
return {
modifiedSourceCode: Buffer.from(
JSON.stringify(keycloakThemesJsonParsed, null, 2),
"utf8"
)
};
}
for (const themeName of buildContext.themeNames) {
if (
fileRelativePath ===
@ -123,6 +105,21 @@ export async function buildJar(params: {
}
});
if (keycloakAccountV1Version === null) {
writeMetaInfKeycloakThemes({
resourcesDirPath: tmpResourcesDirPath,
getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => {
assert(metaInfKeycloakTheme !== undefined);
metaInfKeycloakTheme.themes = metaInfKeycloakTheme.themes.filter(
({ name }) => name !== accountV1ThemeName
);
return metaInfKeycloakTheme;
}
});
}
route_legacy_pages: {
// NOTE: If there's no account theme there is no special target for keycloak 24 and up so we create
// the pages anyway. If there is an account pages, since we know that account-v1 is only support keycloak

View File

@ -8,10 +8,10 @@ import { getKeycloakVersionRangeForJar } from "./getKeycloakVersionRangeForJar";
import { buildJar, BuildContextLike as BuildContextLike_buildJar } from "./buildJar";
import type { BuildContext } from "../../shared/buildContext";
import { getJarFileBasename } from "../../shared/getJarFileBasename";
import { readMetaInfKeycloakThemes_fromResourcesDirPath } from "../../shared/metaInfKeycloakThemes";
import { accountV1ThemeName } from "../../shared/constants";
import { getImplementedThemeTypes } from "../../shared/getImplementedThemeTypes";
export type BuildContextLike = BuildContextLike_buildJar & {
projectDirPath: string;
keycloakifyBuildDirPath: string;
};
@ -24,9 +24,9 @@ export async function buildJars(params: {
}): Promise<void> {
const { onlyBuildJarFileBasename, resourcesDirPath, buildContext } = params;
const doesImplementAccountTheme = readMetaInfKeycloakThemes_fromResourcesDirPath({
resourcesDirPath
}).themes.some(({ name }) => name === accountV1ThemeName);
const doesImplementAccountTheme = getImplementedThemeTypes({
projectDirPath: buildContext.projectDirPath
}).implementedThemeTypes.account;
await Promise.all(
keycloakAccountV1Versions

View File

@ -1,6 +1,6 @@
import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs";
import { join as pathJoin, resolve as pathResolve } from "path";
import { join as pathJoin, resolve as pathResolve, relative as pathRelative } from "path";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import {
@ -16,7 +16,6 @@ import {
loginThemePageIds,
accountThemePageIds
} from "../../shared/constants";
import { isInside } from "../../tools/isInside";
import type { BuildContext } from "../../shared/buildContext";
import { assert, type Equals } from "tsafe/assert";
import {
@ -39,6 +38,7 @@ import {
} from "../../shared/metaInfKeycloakThemes";
import { objectEntries } from "tsafe/objectEntries";
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
import { getImplementedThemeTypes } from "../../shared/getImplementedThemeTypes";
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
BuildContextLike_downloadKeycloakStaticResources &
@ -63,6 +63,10 @@ export async function generateResourcesForMainTheme(params: {
projectDirPath: buildContext.projectDirPath
});
const { implementedThemeTypes } = getImplementedThemeTypes({
projectDirPath: buildContext.projectDirPath
});
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
const { themeType } = params;
return pathJoin(resourcesDirPath, "theme", themeName, themeType);
@ -70,19 +74,11 @@ export async function generateResourcesForMainTheme(params: {
const cssGlobalsToDefine: Record<string, string> = {};
const implementedThemeTypes: Record<ThemeType | "email", boolean> = {
login: false,
account: false,
email: false
};
for (const themeType of ["login", "account"] as const) {
if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) {
if (!implementedThemeTypes[themeType]) {
continue;
}
implementedThemeTypes[themeType] = true;
const themeTypeDirPath = getThemeTypeDirPath({ themeType });
apply_replacers_and_move_to_theme_resources: {
@ -112,25 +108,32 @@ export async function generateResourcesForMainTheme(params: {
break apply_replacers_and_move_to_theme_resources;
}
{
const dirPath = pathJoin(
buildContext.projectBuildDirPath,
keycloak_resources
);
if (fs.existsSync(dirPath)) {
assert(buildContext.bundler === "webpack");
throw new Error(
[
`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(" ")
);
}
}
transformCodebase({
srcDirPath: buildContext.projectBuildDirPath,
destDirPath,
transformSourceCode: ({ filePath, sourceCode }) => {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
// This should not happen if users follow the new instruction setup but we keep it for retrocompatibility.
if (
isInside({
dirPath: pathJoin(
buildContext.projectBuildDirPath,
keycloak_resources
),
filePath
})
) {
return undefined;
}
if (/\.css?$/i.test(filePath)) {
if (filePath.endsWith(".css")) {
const {
cssGlobalsToDefine: cssGlobalsToDefineForThisFile,
fixedCssCode
@ -149,7 +152,7 @@ export async function generateResourcesForMainTheme(params: {
};
}
if (/\.js?$/i.test(filePath)) {
if (filePath.endsWith(".js")) {
const { fixedJsCode } = replaceImportsInJsCode({
jsCode: sourceCode.toString("utf8"),
buildContext
@ -262,13 +265,11 @@ export async function generateResourcesForMainTheme(params: {
}
email: {
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
if (!fs.existsSync(emailThemeSrcDirPath)) {
if (!implementedThemeTypes.email) {
break email;
}
implementedThemeTypes.email = true;
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
transformCodebase({
srcDirPath: emailThemeSrcDirPath,
@ -302,7 +303,7 @@ export async function generateResourcesForMainTheme(params: {
writeMetaInfKeycloakThemes({
resourcesDirPath,
metaInfKeycloakThemes
getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
});
}
}

View File

@ -1,10 +1,7 @@
import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path";
import { transformCodebase } from "../../tools/transformCodebase";
import type { BuildContext } from "../../shared/buildContext";
import {
readMetaInfKeycloakThemes_fromResourcesDirPath,
writeMetaInfKeycloakThemes
} from "../../shared/metaInfKeycloakThemes";
import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
import { assert } from "tsafe/assert";
export type BuildContextLike = {
@ -34,8 +31,8 @@ export function generateResourcesForThemeVariant(params: {
Buffer.from(sourceCode)
.toString("utf-8")
.replace(
`out["themeName"] = "${themeName}";`,
`out["themeName"] = "${themeVariantName}";`
`kcContext.themeName = "${themeName}";`,
`kcContext.themeName = "${themeVariantName}";`
),
"utf8"
);
@ -49,26 +46,25 @@ export function generateResourcesForThemeVariant(params: {
}
});
{
const updatedMetaInfKeycloakThemes =
readMetaInfKeycloakThemes_fromResourcesDirPath({
resourcesDirPath
writeMetaInfKeycloakThemes({
resourcesDirPath,
getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => {
assert(metaInfKeycloakTheme !== undefined);
const newMetaInfKeycloakTheme = metaInfKeycloakTheme;
newMetaInfKeycloakTheme.themes.push({
name: themeVariantName,
types: (() => {
const theme = newMetaInfKeycloakTheme.themes.find(
({ name }) => name === themeName
);
assert(theme !== undefined);
return theme.types;
})()
});
updatedMetaInfKeycloakThemes.themes.push({
name: themeVariantName,
types: (() => {
const theme = updatedMetaInfKeycloakThemes.themes.find(
({ name }) => name === themeName
);
assert(theme !== undefined);
return theme.types;
})()
});
writeMetaInfKeycloakThemes({
resourcesDirPath,
metaInfKeycloakThemes: updatedMetaInfKeycloakThemes
});
}
return newMetaInfKeycloakTheme;
}
});
}

View File

@ -0,0 +1,23 @@
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";
export function getImplementedThemeTypes(params: { projectDirPath: string }) {
const { projectDirPath } = params;
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))
])
);
return { implementedThemeTypes };
}

View File

@ -3,48 +3,60 @@ import { exclude } from "tsafe";
import { crawl } from "../tools/crawl";
import { join as pathJoin } from "path";
import { themeTypes } from "./constants";
import chalk from "chalk";
const themeSrcDirBasenames = ["keycloak-theme", "keycloak_theme"];
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;
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) {
if (cache !== undefined && cache.projectDirPath === projectDirPath) {
const { themeSrcDirPath } = cache;
return { themeSrcDirPath };
}
for (const themeType of [...themeTypes, "email"]) {
if (!fs.existsSync(pathJoin(srcDirPath, themeType))) {
continue;
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 };
}
return { themeSrcDirPath: srcDirPath };
}
console.error(
[
"Can't locate your theme source directory. It should be either: ",
"src/ or src/keycloak-theme or src/keycloak_theme.",
"Example in the starter: https://github.com/keycloakify/keycloakify-starter/tree/main/src/keycloak-theme"
].join("\n")
);
for (const themeType of [...themeTypes, "email"]) {
if (!fs.existsSync(pathJoin(srcDirPath, themeType))) {
continue;
}
return { themeSrcDirPath: srcDirPath };
}
process.exit(-1);
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,84 +1,40 @@
import { join as pathJoin, dirname as pathDirname } from "path";
import type { ThemeType } from "./constants";
import * as fs from "fs";
import { assert } from "tsafe/assert";
import { extractArchive } from "../tools/extractArchive";
export type MetaInfKeycloakTheme = {
themes: { name: string; types: (ThemeType | "email")[] }[];
};
export function getMetaInfKeycloakThemesJsonFilePath(params: {
resourcesDirPath: string;
}) {
const { resourcesDirPath } = params;
return pathJoin(
resourcesDirPath === "." ? "" : resourcesDirPath,
"META-INF",
"keycloak-themes.json"
);
}
export function readMetaInfKeycloakThemes_fromResourcesDirPath(params: {
resourcesDirPath: string;
}) {
const { resourcesDirPath } = params;
return JSON.parse(
fs
.readFileSync(
getMetaInfKeycloakThemesJsonFilePath({
resourcesDirPath
})
)
.toString("utf8")
) as MetaInfKeycloakTheme;
}
export async function readMetaInfKeycloakThemes_fromJar(params: {
jarFilePath: string;
}): Promise<MetaInfKeycloakTheme> {
const { jarFilePath } = params;
let metaInfKeycloakThemes: MetaInfKeycloakTheme | undefined = undefined;
await extractArchive({
archiveFilePath: jarFilePath,
onArchiveFile: async ({ relativeFilePathInArchive, readFile, earlyExit }) => {
if (
relativeFilePathInArchive ===
getMetaInfKeycloakThemesJsonFilePath({ resourcesDirPath: "." })
) {
metaInfKeycloakThemes = JSON.parse((await readFile()).toString("utf8"));
earlyExit();
}
}
});
assert(metaInfKeycloakThemes !== undefined);
return metaInfKeycloakThemes;
}
export function writeMetaInfKeycloakThemes(params: {
resourcesDirPath: string;
metaInfKeycloakThemes: MetaInfKeycloakTheme;
getNewMetaInfKeycloakTheme: (params: {
metaInfKeycloakTheme: MetaInfKeycloakTheme | undefined;
}) => MetaInfKeycloakTheme;
}) {
const { resourcesDirPath, metaInfKeycloakThemes } = params;
const { resourcesDirPath, getNewMetaInfKeycloakTheme } = params;
const metaInfKeycloakThemesJsonPath = getMetaInfKeycloakThemesJsonFilePath({
resourcesDirPath
const filePath = pathJoin(resourcesDirPath, "META-INF", "keycloak-themes.json");
const currentMetaInfKeycloakTheme = !fs.existsSync(filePath)
? undefined
: (JSON.parse(
fs.readFileSync(filePath).toString("utf8")
) as MetaInfKeycloakTheme);
const newMetaInfKeycloakThemes = getNewMetaInfKeycloakTheme({
metaInfKeycloakTheme: currentMetaInfKeycloakTheme
});
{
const dirPath = pathDirname(metaInfKeycloakThemesJsonPath);
const dirPath = pathDirname(filePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
fs.writeFileSync(
metaInfKeycloakThemesJsonPath,
Buffer.from(JSON.stringify(metaInfKeycloakThemes, null, 2), "utf8")
filePath,
Buffer.from(JSON.stringify(newMetaInfKeycloakThemes, null, 2), "utf8")
);
}

View File

@ -2,7 +2,7 @@ import { getBuildContext } from "../shared/buildContext";
import { exclude } from "tsafe/exclude";
import type { CliCommandOptions as CliCommandOptions_common } from "../main";
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
import { readMetaInfKeycloakThemes_fromJar } from "../shared/metaInfKeycloakThemes";
import { getImplementedThemeTypes } from "../shared/getImplementedThemeTypes";
import { accountV1ThemeName, containerName } from "../shared/constants";
import { SemVer } from "../tools/SemVer";
import type { KeycloakVersionRange } from "../shared/KeycloakVersionRange";
@ -120,37 +120,12 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
}
}
const { doesImplementAccountTheme } = await (async () => {
const latestJarFilePath = 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(latestJarFilePath !== undefined);
const metaInfKeycloakThemes = await readMetaInfKeycloakThemes_fromJar({
jarFilePath: latestJarFilePath
});
const mainThemeEntry = metaInfKeycloakThemes.themes.find(
({ name }) => name === buildContext.themeNames[0]
);
assert(mainThemeEntry !== undefined);
const doesImplementAccountTheme = mainThemeEntry.types.includes("account");
return { doesImplementAccountTheme };
})();
const doesImplementAccountTheme = getImplementedThemeTypes({
projectDirPath: buildContext.projectDirPath
}).implementedThemeTypes.account;
const { keycloakVersion, keycloakMajorNumber: keycloakMajorVersionNumber } =
await (async function getKeycloakMajor(): Promise<{
keycloakVersion: string;
keycloakMajorNumber: number;
}> {
await (async () => {
if (cliCommandOptions.keycloakVersion !== undefined) {
return {
keycloakVersion: cliCommandOptions.keycloakVersion,
@ -173,16 +148,6 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const keycloakMajorNumber = SemVer.parse(keycloakVersion).major;
if (doesImplementAccountTheme && keycloakMajorNumber === 22) {
console.log(
[
"Unfortunately, Keycloakify themes that implements an account theme do not work on Keycloak 22",
"Please select any other Keycloak version"
].join(" ")
);
return getKeycloakMajor();
}
return { keycloakVersion, keycloakMajorNumber };
})();