diff --git a/src/bin/copy-keycloak-resources-to-public.ts b/src/bin/copy-keycloak-resources-to-public.ts
index 78daf92d..5c8d276c 100644
--- a/src/bin/copy-keycloak-resources-to-public.ts
+++ b/src/bin/copy-keycloak-resources-to-public.ts
@@ -28,7 +28,6 @@ import * as fs from "fs";
})(),
themeType,
"themeDirPath": reservedDirPath,
- "usedResources": undefined,
buildOptions
});
}
@@ -44,7 +43,7 @@ import * as fs from "fs";
)
);
- fs.writeFileSync(pathJoin(buildOptions.publicDirPath, "keycloak-resources", ".gitignore"), Buffer.from("*", "utf8"));
+ fs.writeFileSync(pathJoin(buildOptions.publicDirPath, keycloak_resources, ".gitignore"), Buffer.from("*", "utf8"));
console.log(`${pathRelative(reactAppRootDirPath, reservedDirPath)} directory created.`);
})();
diff --git a/src/bin/download-builtin-keycloak-theme.ts b/src/bin/download-builtin-keycloak-theme.ts
index 073aad9c..8ddd4b68 100644
--- a/src/bin/download-builtin-keycloak-theme.ts
+++ b/src/bin/download-builtin-keycloak-theme.ts
@@ -7,6 +7,9 @@ import { readBuildOptions, type BuildOptions } from "./keycloakify/buildOptions"
import { assert } from "tsafe/assert";
import * as child_process from "child_process";
import * as fs from "fs";
+import { rmSync } from "./tools/fs.rmSync";
+import { lastKeycloakVersionWithAccountV1 } from "./constants";
+import { transformCodebase } from "./tools/transformCodebase";
export type BuildOptionsLike = {
cacheDirPath: string;
@@ -26,51 +29,6 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
"preCacheTransform": {
"actionCacheId": "npm install and build",
"action": async ({ destDirPath }) => {
- fix_account_css: {
- const accountCssFilePath = pathJoin(destDirPath, "keycloak", "account", "resources", "css", "account.css");
-
- if (!fs.existsSync(accountCssFilePath)) {
- break fix_account_css;
- }
-
- fs.writeFileSync(
- accountCssFilePath,
- Buffer.from(fs.readFileSync(accountCssFilePath).toString("utf8").replace("top: -34px;", "top: -34px !important;"), "utf8")
- );
- }
-
- fix_account_topt: {
- const totpFtlFilePath = pathJoin(destDirPath, "base", "account", "totp.ftl");
-
- if (!fs.existsSync(totpFtlFilePath)) {
- break fix_account_topt;
- }
-
- fs.writeFileSync(
- totpFtlFilePath,
- Buffer.from(
- fs
- .readFileSync(totpFtlFilePath)
- .toString("utf8")
- .replace(
- [
- " <#list totp.policy.supportedApplications as app>",
- "
${app}",
- " #list>"
- ].join("\n"),
- [
- " <#if totp.policy.supportedApplications?has_content>",
- " <#list totp.policy.supportedApplications as app>",
- " ${app}",
- " #list>",
- " #if>"
- ].join("\n")
- ),
- "utf8"
- )
- );
- }
-
install_common_node_modules: {
const commonResourcesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources");
@@ -128,7 +86,98 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
fs.writeFileSync(packageJsonFilePath, packageJsonRaw);
- fs.rmSync(pathJoin(accountV2DirSrcDirPath, "node_modules"), { "recursive": true });
+ rmSync(pathJoin(accountV2DirSrcDirPath, "node_modules"), { "recursive": true });
+ }
+
+ last_account_v1_transformations: {
+ if (lastKeycloakVersionWithAccountV1 !== keycloakVersion) {
+ break last_account_v1_transformations;
+ }
+
+ {
+ const accountCssFilePath = pathJoin(destDirPath, "keycloak", "account", "resources", "css", "account.css");
+
+ fs.writeFileSync(
+ accountCssFilePath,
+ Buffer.from(fs.readFileSync(accountCssFilePath).toString("utf8").replace("top: -34px;", "top: -34px !important;"), "utf8")
+ );
+ }
+
+ {
+ const totpFtlFilePath = pathJoin(destDirPath, "base", "account", "totp.ftl");
+
+ fs.writeFileSync(
+ totpFtlFilePath,
+ Buffer.from(
+ fs
+ .readFileSync(totpFtlFilePath)
+ .toString("utf8")
+ .replace(
+ [
+ " <#list totp.policy.supportedApplications as app>",
+ " ${app}",
+ " #list>"
+ ].join("\n"),
+ [
+ " <#if totp.policy.supportedApplications?has_content>",
+ " <#list totp.policy.supportedApplications as app>",
+ " ${app}",
+ " #list>",
+ " #if>"
+ ].join("\n")
+ ),
+ "utf8"
+ )
+ );
+ }
+
+ // Note, this is an optimization for reducing the size of the jar
+ {
+ const defaultThemeCommonResourcesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources");
+
+ const usedCommonResourceRelativeFilePaths = [
+ ...["patternfly.min.css", "patternfly-additions.min.css", "patternfly-additions.min.css"].map(fileBasename =>
+ pathJoin("node_modules", "patternfly", "dist", "css", fileBasename)
+ ),
+ ...[
+ "OpenSans-Light-webfont.woff2",
+ "OpenSans-Regular-webfont.woff2",
+ "OpenSans-Bold-webfont.woff2",
+ "OpenSans-Semibold-webfont.woff2",
+ "OpenSans-Bold-webfont.woff",
+ "OpenSans-Light-webfont.woff",
+ "OpenSans-Regular-webfont.woff",
+ "OpenSans-Semibold-webfont.woff",
+ "OpenSans-Regular-webfont.ttf",
+ "OpenSans-Light-webfont.ttf",
+ "OpenSans-Semibold-webfont.ttf",
+ "OpenSans-Bold-webfont.ttf"
+ ].map(fileBasename => pathJoin("node_modules", "patternfly", "dist", "fonts", fileBasename))
+ ];
+
+ transformCodebase({
+ "srcDirPath": defaultThemeCommonResourcesDirPath,
+ "destDirPath": defaultThemeCommonResourcesDirPath,
+ "transformSourceCode": ({ sourceCode, fileRelativePath }) => {
+ if (!usedCommonResourceRelativeFilePaths.includes(fileRelativePath)) {
+ return undefined;
+ }
+
+ return { "modifiedSourceCode": sourceCode };
+ }
+ });
+ }
+
+ // Other optimization: Remove AngularJS
+ {
+ const nodeModuleDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "node_modules");
+
+ fs.readdirSync(nodeModuleDirPath)
+ .filter(basename => basename.startsWith("angular"))
+ .map(basename => pathJoin(nodeModuleDirPath, basename))
+ .filter(dirPath => fs.statSync(dirPath).isDirectory())
+ .forEach(dirPath => rmSync(dirPath, { "recursive": true }));
+ }
}
}
}
diff --git a/src/bin/initialize-email-theme.ts b/src/bin/initialize-email-theme.ts
index b4afdacc..84dbb93b 100644
--- a/src/bin/initialize-email-theme.ts
+++ b/src/bin/initialize-email-theme.ts
@@ -8,6 +8,7 @@ import { readBuildOptions } from "./keycloakify/buildOptions";
import * as fs from "fs";
import { getLogger } from "./tools/logger";
import { getThemeSrcDirPath } from "./getThemeSrcDirPath";
+import { rmSync } from "./tools/fs.rmSync";
export async function main() {
const reactAppRootDirPath = process.cwd();
@@ -54,7 +55,7 @@ export async function main() {
logger.log(`${pathRelative(process.cwd(), emailThemeSrcDirPath)} ready to be customized, feel free to remove every file you do not customize`);
- fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
+ rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
}
if (require.main === module) {
diff --git a/src/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.ts b/src/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.ts
deleted file mode 100644
index b6d93f29..00000000
--- a/src/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import * as fs from "fs";
-import { join as pathJoin, dirname as pathDirname } from "path";
-import { assert } from "tsafe/assert";
-import { Reflect } from "tsafe/Reflect";
-import type { BuildOptions } from "../buildOptions";
-import { type ThemeType, retrocompatPostfix, accountV1ThemeName } from "../../constants";
-import { bringInAccountV1 } from "./bringInAccountV1";
-
-type BuildOptionsLike = {
- groupId: string;
- artifactId: string;
- themeVersion: string;
- cacheDirPath: string;
- keycloakifyBuildDirPath: string;
- themeNames: string[];
- doBuildRetrocompatAccountTheme: boolean;
-};
-
-{
- const buildOptions = Reflect();
-
- assert();
-}
-
-export async function generateJavaStackFiles(params: {
- implementedThemeTypes: Record;
- buildOptions: BuildOptionsLike;
-}): Promise<{
- jarFilePath: string;
-}> {
- const { implementedThemeTypes, buildOptions } = params;
-
- {
- const { pomFileCode } = (function generatePomFileCode(): {
- pomFileCode: string;
- } {
- const pomFileCode = [
- ``,
- ``,
- ` 4.0.0`,
- ` ${buildOptions.groupId}`,
- ` ${buildOptions.artifactId}`,
- ` ${buildOptions.themeVersion}`,
- ` ${buildOptions.artifactId}`,
- ` `,
- ` jar`,
- ` `,
- ` UTF-8`,
- ` `,
- ` `,
- ` `,
- ` `,
- ` org.apache.maven.plugins`,
- ` maven-shade-plugin`,
- ` 3.5.1`,
- ` `,
- ` `,
- ` package`,
- ` `,
- ` shade`,
- ` `,
- ` `,
- ` `,
- ` `,
- ` `,
- ` `,
- ` `,
- ` `,
- ` io.phasetwo.keycloak`,
- ` keycloak-account-v1`,
- ` 0.1`,
- ` `,
- ` `,
- ``
- ].join("\n");
-
- return { pomFileCode };
- })();
-
- fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
- }
-
- if (implementedThemeTypes.account) {
- await bringInAccountV1({ buildOptions });
- }
-
- {
- const themeManifestFilePath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json");
-
- try {
- fs.mkdirSync(pathDirname(themeManifestFilePath));
- } catch {}
-
- fs.writeFileSync(
- themeManifestFilePath,
- Buffer.from(
- JSON.stringify(
- {
- "themes": [
- ...(!implementedThemeTypes.account
- ? []
- : [
- {
- "name": accountV1ThemeName,
- "types": ["account"]
- }
- ]),
- ...buildOptions.themeNames
- .map(themeName => [
- {
- "name": themeName,
- "types": Object.entries(implementedThemeTypes)
- .filter(([, isImplemented]) => isImplemented)
- .map(([themeType]) => themeType)
- },
- ...(!implementedThemeTypes.account || !buildOptions.doBuildRetrocompatAccountTheme
- ? []
- : [
- {
- "name": `${themeName}${retrocompatPostfix}`,
- "types": ["account"]
- }
- ])
- ])
- .flat()
- ]
- },
- null,
- 2
- ),
- "utf8"
- )
- );
- }
-
- return {
- "jarFilePath": pathJoin(buildOptions.keycloakifyBuildDirPath, "target", `${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`)
- };
-}
diff --git a/src/bin/keycloakify/generateJavaStackFiles/index.ts b/src/bin/keycloakify/generateJavaStackFiles/index.ts
deleted file mode 100644
index ea372c91..00000000
--- a/src/bin/keycloakify/generateJavaStackFiles/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./generateJavaStackFiles";
diff --git a/src/bin/keycloakify/generatePom.ts b/src/bin/keycloakify/generatePom.ts
new file mode 100644
index 00000000..555382ab
--- /dev/null
+++ b/src/bin/keycloakify/generatePom.ts
@@ -0,0 +1,70 @@
+import { assert } from "tsafe/assert";
+import { Reflect } from "tsafe/Reflect";
+import type { BuildOptions } from "./buildOptions";
+
+type BuildOptionsLike = {
+ groupId: string;
+ artifactId: string;
+ themeVersion: string;
+ keycloakifyBuildDirPath: string;
+};
+
+{
+ const buildOptions = Reflect();
+
+ assert();
+}
+
+export function generatePom(params: { buildOptions: BuildOptionsLike }) {
+ const { buildOptions } = params;
+
+ const { pomFileCode } = (function generatePomFileCode(): {
+ pomFileCode: string;
+ } {
+ const pomFileCode = [
+ ``,
+ ``,
+ ` 4.0.0`,
+ ` ${buildOptions.groupId}`,
+ ` ${buildOptions.artifactId}`,
+ ` ${buildOptions.themeVersion}`,
+ ` ${buildOptions.artifactId}`,
+ ` `,
+ ` jar`,
+ ` `,
+ ` UTF-8`,
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ ` org.apache.maven.plugins`,
+ ` maven-shade-plugin`,
+ ` 3.5.1`,
+ ` `,
+ ` `,
+ ` package`,
+ ` `,
+ ` shade`,
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ ` io.phasetwo.keycloak`,
+ ` keycloak-account-v1`,
+ ` 0.1`,
+ ` `,
+ ` `,
+ ``
+ ].join("\n");
+
+ return { pomFileCode };
+ })();
+
+ return { pomFileCode };
+}
diff --git a/src/bin/keycloakify/generateJavaStackFiles/bringInAccountV1.ts b/src/bin/keycloakify/generateTheme/bringInAccountV1.ts
similarity index 52%
rename from src/bin/keycloakify/generateJavaStackFiles/bringInAccountV1.ts
rename to src/bin/keycloakify/generateTheme/bringInAccountV1.ts
index 91aea909..6caaeabc 100644
--- a/src/bin/keycloakify/generateJavaStackFiles/bringInAccountV1.ts
+++ b/src/bin/keycloakify/generateTheme/bringInAccountV1.ts
@@ -1,11 +1,12 @@
import * as fs from "fs";
-import { join as pathJoin, dirname as pathDirname } from "path";
+import { join as pathJoin } from "path";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "../buildOptions";
import { resources_common, lastKeycloakVersionWithAccountV1, accountV1ThemeName } from "../../constants";
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
import { transformCodebase } from "../../tools/transformCodebase";
+import { rmSync } from "../../tools/fs.rmSync";
type BuildOptionsLike = {
keycloakifyBuildDirPath: string;
@@ -36,45 +37,17 @@ export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike
"destDirPath": accountV1DirPath
});
- const commonResourceFilePaths = [
- "node_modules/patternfly/dist/css/patternfly.min.css",
- "node_modules/patternfly/dist/css/patternfly-additions.min.css",
- "node_modules/patternfly/dist/css/patternfly-additions.min.css",
- ...[
- "OpenSans-Light-webfont.woff2",
- "OpenSans-Regular-webfont.woff2",
- "OpenSans-Bold-webfont.woff2",
- "OpenSans-Semibold-webfont.woff2",
- "OpenSans-Bold-webfont.woff",
- "OpenSans-Light-webfont.woff",
- "OpenSans-Regular-webfont.woff",
- "OpenSans-Semibold-webfont.woff",
- "OpenSans-Regular-webfont.ttf",
- "OpenSans-Light-webfont.ttf",
- "OpenSans-Semibold-webfont.ttf",
- "OpenSans-Bold-webfont.ttf"
- ].map(path => `node_modules/patternfly/dist/fonts/${path}`)
- ];
+ transformCodebase({
+ "srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "account", "resources"),
+ "destDirPath": pathJoin(accountV1DirPath, "resources")
+ });
- for (const relativeFilePath of commonResourceFilePaths.map(path => pathJoin(...path.split("/")))) {
- const destFilePath = pathJoin(accountV1DirPath, "resources", resources_common, relativeFilePath);
+ transformCodebase({
+ "srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "common", "resources"),
+ "destDirPath": pathJoin(accountV1DirPath, "resources", resources_common)
+ });
- fs.mkdirSync(pathDirname(destFilePath), { "recursive": true });
-
- fs.cpSync(pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "common", "resources", relativeFilePath), destFilePath);
- }
-
- const resourceFilePaths = ["css/account.css", "img/icon-sidebar-active.png", "img/logo.png"];
-
- for (const relativeFilePath of resourceFilePaths.map(path => pathJoin(...path.split("/")))) {
- const destFilePath = pathJoin(accountV1DirPath, "resources", relativeFilePath);
-
- fs.mkdirSync(pathDirname(destFilePath), { "recursive": true });
-
- fs.cpSync(pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "account", "resources", relativeFilePath), destFilePath);
- }
-
- fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true });
+ rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true });
fs.writeFileSync(
pathJoin(accountV1DirPath, "theme.properties"),
@@ -84,7 +57,15 @@ export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike
"",
"locales=ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
"",
- "styles=" + [...resourceFilePaths, ...commonResourceFilePaths.map(path => `resources-common/${path}`)].join(" "),
+ "styles=" +
+ [
+ "css/account.css",
+ "img/icon-sidebar-active.png",
+ "img/logo.png",
+ ...["patternfly.min.css", "patternfly-additions.min.css", "patternfly-additions.min.css"].map(
+ fileBasename => `${resources_common}/node_modules/patternfly/dist/css/${fileBasename}`
+ )
+ ].join(" "),
"",
"##### css classes for form buttons",
"# main class used for all buttons",
diff --git a/src/bin/keycloakify/generateTheme/downloadKeycloakStaticResources.ts b/src/bin/keycloakify/generateTheme/downloadKeycloakStaticResources.ts
index 3e0bd985..8cc0c3f0 100644
--- a/src/bin/keycloakify/generateTheme/downloadKeycloakStaticResources.ts
+++ b/src/bin/keycloakify/generateTheme/downloadKeycloakStaticResources.ts
@@ -1,11 +1,11 @@
import { transformCodebase } from "../../tools/transformCodebase";
-import * as fs from "fs";
-import { join as pathJoin, dirname as pathDirname } from "path";
+import { join as pathJoin } from "path";
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
import { resources_common, type ThemeType } from "../../constants";
import { BuildOptions } from "../buildOptions";
import { assert } from "tsafe/assert";
import * as crypto from "crypto";
+import { rmSync } from "../../tools/fs.rmSync";
export type BuildOptionsLike = {
cacheDirPath: string;
@@ -13,45 +13,14 @@ export type BuildOptionsLike = {
assert();
-export async function downloadKeycloakStaticResources(
- // prettier-ignore
- params: {
- themeType: ThemeType;
- themeDirPath: string;
- keycloakVersion: string;
- usedResources: {
- resourcesCommonFilePaths: string[];
- } | undefined;
- buildOptions: BuildOptionsLike;
- }
-) {
+export async function downloadKeycloakStaticResources(params: {
+ themeType: ThemeType;
+ themeDirPath: string;
+ keycloakVersion: string;
+ buildOptions: BuildOptionsLike;
+}) {
const { themeType, themeDirPath, keycloakVersion, buildOptions } = params;
- // NOTE: Hack for 427
- const usedResources = (() => {
- const { usedResources } = params;
-
- if (usedResources === undefined) {
- return undefined;
- }
-
- assert(usedResources !== undefined);
-
- return {
- "resourcesCommonDirPaths": usedResources.resourcesCommonFilePaths.map(filePath => {
- {
- const splitArg = "/dist/";
-
- if (filePath.includes(splitArg)) {
- return filePath.split(splitArg)[0] + splitArg;
- }
- }
-
- return pathDirname(filePath);
- })
- };
- })();
-
const tmpDirPath = pathJoin(
themeDirPath,
`tmp_suLeKsxId_${crypto.createHash("sha256").update(`${themeType}-${keycloakVersion}`).digest("hex").slice(0, 8)}`
@@ -72,18 +41,8 @@ export async function downloadKeycloakStaticResources(
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
- "destDirPath": pathJoin(resourcesPath, resources_common),
- "transformSourceCode":
- usedResources === undefined
- ? undefined
- : ({ fileRelativePath, sourceCode }) => {
- if (usedResources.resourcesCommonDirPaths.find(dirPath => fileRelativePath.startsWith(dirPath)) === undefined) {
- return undefined;
- }
-
- return { "modifiedSourceCode": sourceCode };
- }
+ "destDirPath": pathJoin(resourcesPath, resources_common)
});
- fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
+ rmSync(tmpDirPath, { "recursive": true, "force": true });
}
diff --git a/src/bin/keycloakify/generateTheme/generateTheme.ts b/src/bin/keycloakify/generateTheme/generateTheme.ts
index b9b95a74..3ff03e79 100644
--- a/src/bin/keycloakify/generateTheme/generateTheme.ts
+++ b/src/bin/keycloakify/generateTheme/generateTheme.ts
@@ -1,11 +1,10 @@
import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs";
-import { join as pathJoin, basename as pathBasename, resolve as pathResolve } from "path";
+import { join as pathJoin, basename as pathBasename, resolve as pathResolve, dirname as pathDirname } from "path";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds } from "../generateFtl";
import {
- themeTypes,
type ThemeType,
lastKeycloakVersionWithAccountV1,
keycloak_resources,
@@ -20,7 +19,7 @@ import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResourc
import { readFieldNameUsage } from "./readFieldNameUsage";
import { readExtraPagesNames } from "./readExtraPageNames";
import { generateMessageProperties } from "./generateMessageProperties";
-import { readStaticResourcesUsage } from "./readStaticResourcesUsage";
+import { bringInAccountV1 } from "./bringInAccountV1";
export type BuildOptionsLike = {
bundler: "vite" | "webpack";
@@ -33,6 +32,7 @@ export type BuildOptionsLike = {
assetsDirPath: string;
urlPathname: string | undefined;
doBuildRetrocompatAccountTheme: boolean;
+ themeNames: string[];
};
assert();
@@ -59,27 +59,47 @@ export async function generateTheme(params: {
);
};
- let allCssGlobalsToDefine: Record = {};
+ const cssGlobalsToDefine: Record = {};
- for (const themeType of themeTypes) {
+ const implementedThemeTypes: Record = {
+ "login": false,
+ "account": false,
+ "email": false
+ };
+
+ for (const themeType of ["login", "account"] as const) {
if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) {
continue;
}
+ implementedThemeTypes[themeType] = true;
+
const themeTypeDirPath = getThemeTypeDirPath({ themeType });
- copy_app_resources_to_theme_path: {
- const isFirstPass = themeType.indexOf(themeType) === 0;
+ apply_replacers_and_move_to_theme_resources: {
+ if (themeType === "account" && implementedThemeTypes.login) {
+ // NOTE: We prevend doing it twice, it has been done for the login theme.
- if (!isFirstPass) {
- break copy_app_resources_to_theme_path;
+ transformCodebase({
+ "srcDirPath": pathJoin(
+ getThemeTypeDirPath({
+ "themeType": "login"
+ }),
+ "resources",
+ basenameOfTheKeycloakifyResourcesDir
+ ),
+ "destDirPath": pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir)
+ });
+
+ break apply_replacers_and_move_to_theme_resources;
}
transformCodebase({
- "destDirPath": pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir),
"srcDirPath": buildOptions.reactAppBuildDirPath,
+ "destDirPath": pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir),
"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(buildOptions.reactAppBuildDirPath, keycloak_resources),
@@ -90,20 +110,13 @@ export async function generateTheme(params: {
}
if (/\.css?$/i.test(filePath)) {
- const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
+ const { cssGlobalsToDefine: cssGlobalsToDefineForThisFile, fixedCssCode } = replaceImportsInCssCode({
"cssCode": sourceCode.toString("utf8")
});
- register_css_variables: {
- if (!isFirstPass) {
- break register_css_variables;
- }
-
- allCssGlobalsToDefine = {
- ...allCssGlobalsToDefine,
- ...cssGlobalsToDefine
- };
- }
+ Object.entries(cssGlobalsToDefineForThisFile).forEach(([key, value]) => {
+ cssGlobalsToDefine[key] = value;
+ });
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
}
@@ -125,7 +138,7 @@ export async function generateTheme(params: {
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
themeName,
"indexHtmlCode": fs.readFileSync(pathJoin(buildOptions.reactAppBuildDirPath, "index.html")).toString("utf8"),
- "cssGlobalsToDefine": allCssGlobalsToDefine,
+ cssGlobalsToDefine,
buildOptions,
keycloakifyVersion,
themeType,
@@ -181,11 +194,6 @@ export async function generateTheme(params: {
})(),
"themeDirPath": pathResolve(pathJoin(themeTypeDirPath, "..")),
themeType,
- "usedResources": readStaticResourcesUsage({
- keycloakifySrcDirPath,
- themeSrcDirPath,
- themeType
- }),
buildOptions
});
@@ -235,9 +243,82 @@ export async function generateTheme(params: {
break email;
}
+ implementedThemeTypes.email = true;
+
transformCodebase({
"srcDirPath": emailThemeSrcDirPath,
"destDirPath": getThemeTypeDirPath({ "themeType": "email" })
});
}
+
+ const parsedKeycloakThemeJson: { themes: { name: string; types: string[] }[] } = { "themes": [] };
+
+ buildOptions.themeNames.forEach(themeName =>
+ parsedKeycloakThemeJson.themes.push({
+ "name": themeName,
+ "types": Object.entries(implementedThemeTypes)
+ .filter(([, isImplemented]) => isImplemented)
+ .map(([themeType]) => themeType)
+ })
+ );
+
+ account_specific_extra_work: {
+ if (!implementedThemeTypes.account) {
+ break account_specific_extra_work;
+ }
+
+ await bringInAccountV1({ buildOptions });
+
+ parsedKeycloakThemeJson.themes.push({
+ "name": accountV1ThemeName,
+ "types": ["account"]
+ });
+
+ add_retrocompat_account_theme: {
+ if (!buildOptions.doBuildRetrocompatAccountTheme) {
+ break add_retrocompat_account_theme;
+ }
+
+ transformCodebase({
+ "srcDirPath": getThemeTypeDirPath({ "themeType": "account" }),
+ "destDirPath": getThemeTypeDirPath({ "themeType": "account", "isRetrocompat": true }),
+ "transformSourceCode": ({ filePath, sourceCode }) => {
+ if (pathBasename(filePath) === "theme.properties") {
+ return {
+ "modifiedSourceCode": Buffer.from(
+ sourceCode.toString("utf8").replace(`parent=${accountV1ThemeName}`, "parent=keycloak"),
+ "utf8"
+ )
+ };
+ }
+
+ return { "modifiedSourceCode": sourceCode };
+ }
+ });
+
+ buildOptions.themeNames.forEach(themeName =>
+ parsedKeycloakThemeJson.themes.push({
+ "name": `${themeName}${retrocompatPostfix}`,
+ "types": ["account"]
+ })
+ );
+ }
+ }
+
+ {
+ const keycloakThemeJsonFilePath = pathJoin(
+ buildOptions.keycloakifyBuildDirPath,
+ "src",
+ "main",
+ "resources",
+ "META-INF",
+ "keycloak-themes.json"
+ );
+
+ try {
+ fs.mkdirSync(pathDirname(keycloakThemeJsonFilePath));
+ } catch {}
+
+ fs.writeFileSync(keycloakThemeJsonFilePath, Buffer.from(JSON.stringify(parsedKeycloakThemeJson, null, 2), "utf8"));
+ }
}
diff --git a/src/bin/keycloakify/generateTheme/readStaticResourcesUsage.ts b/src/bin/keycloakify/generateTheme/readStaticResourcesUsage.ts
deleted file mode 100644
index ea62bff6..00000000
--- a/src/bin/keycloakify/generateTheme/readStaticResourcesUsage.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { crawl } from "../../tools/crawl";
-import { join as pathJoin, sep as pathSep } from "path";
-import * as fs from "fs";
-import type { ThemeType } from "../../constants";
-
-/** Assumes the theme type exists */
-export function readStaticResourcesUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): {
- resourcesCommonFilePaths: string[];
-} {
- const { keycloakifySrcDirPath, themeSrcDirPath, themeType } = params;
-
- const resourcesCommonFilePaths = new Set();
-
- for (const srcDirPath of [pathJoin(keycloakifySrcDirPath, themeType), pathJoin(themeSrcDirPath, themeType)]) {
- const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
-
- for (const filePath of filePaths) {
- const rawSourceFile = fs.readFileSync(filePath).toString("utf8");
-
- if (!rawSourceFile.includes("resourcesCommonPath") && !rawSourceFile.includes("resourcesPath")) {
- continue;
- }
-
- const wrap = readPaths({ rawSourceFile });
-
- wrap.resourcesCommonFilePaths.forEach(filePath => resourcesCommonFilePaths.add(filePath));
- }
- }
-
- return {
- "resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths)
- };
-}
-
-/** Exported for testing purpose */
-export function readPaths(params: { rawSourceFile: string }): {
- resourcesCommonFilePaths: string[];
-} {
- const { rawSourceFile } = params;
-
- const resourcesCommonFilePaths = new Set();
-
- {
- const regexp = new RegExp(`resourcesCommonPath\\s*}([^\`]+)\``, "g");
-
- const matches = [...rawSourceFile.matchAll(regexp)];
-
- for (const match of matches) {
- const filePath = match[1];
-
- resourcesCommonFilePaths.add(filePath);
- }
- }
-
- {
- const regexp = new RegExp(`resourcesCommonPath\\s*[+,]\\s*["']([^"'\`]+)["'\`]`, "g");
-
- const matches = [...rawSourceFile.matchAll(regexp)];
-
- for (const match of matches) {
- const filePath = match[1];
-
- resourcesCommonFilePaths.add(filePath);
- }
- }
-
- const normalizePath = (filePath: string) => {
- filePath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
- filePath = filePath.replace(/\//g, pathSep);
- return filePath;
- };
-
- return {
- "resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths).map(normalizePath)
- };
-}
diff --git a/src/bin/keycloakify/keycloakify.ts b/src/bin/keycloakify/keycloakify.ts
index 5e8f48ea..d8c2ffda 100644
--- a/src/bin/keycloakify/keycloakify.ts
+++ b/src/bin/keycloakify/keycloakify.ts
@@ -1,5 +1,5 @@
import { generateTheme } from "./generateTheme";
-import { generateJavaStackFiles } from "./generateJavaStackFiles";
+import { generatePom } from "./generatePom";
import { join as pathJoin, relative as pathRelative, basename as pathBasename, dirname as pathDirname, sep as pathSep } from "path";
import * as child_process from "child_process";
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
@@ -9,7 +9,6 @@ import { getLogger } from "../tools/logger";
import { assert } from "tsafe/assert";
import { getThemeSrcDirPath } from "../getThemeSrcDirPath";
import { getProjectRoot } from "../tools/getProjectRoot";
-import { objectKeys } from "tsafe/objectKeys";
export async function main() {
const reactAppRootDirPath = process.cwd();
@@ -42,25 +41,13 @@ export async function main() {
});
}
- const { jarFilePath } = await generateJavaStackFiles({
- "implementedThemeTypes": (() => {
- const implementedThemeTypes = {
- "login": false,
- "account": false,
- "email": false
- };
+ {
+ const { pomFileCode } = generatePom({ buildOptions });
- for (const themeType of objectKeys(implementedThemeTypes)) {
- if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) {
- continue;
- }
- implementedThemeTypes[themeType] = true;
- }
+ fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
+ }
- return implementedThemeTypes;
- })(),
- buildOptions
- });
+ const jarFilePath = pathJoin(buildOptions.keycloakifyBuildDirPath, "target", `${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`);
if (buildOptions.doCreateJar) {
child_process.execSync("mvn clean install", { "cwd": buildOptions.keycloakifyBuildDirPath });
@@ -96,48 +83,16 @@ export async function main() {
"",
...(!buildOptions.doCreateJar
? []
- : [
- `✅ Your keycloak theme has been generated and bundled into .${pathSep}${pathRelative(reactAppRootDirPath, jarFilePath)} 🚀`,
- `It is to be placed in "/opt/keycloak/providers" in the container running a quay.io/keycloak/keycloak Docker image.`,
- ""
- ]),
+ : [`✅ Your keycloak theme has been generated and bundled into .${pathSep}${pathRelative(reactAppRootDirPath, jarFilePath)} 🚀`]),
//TODO: Restore when we find a good Helm chart for Keycloak.
//"Using Helm (https://github.com/codecentric/helm-charts), edit to reflect:",
"",
- "value.yaml: ",
- " extraInitContainers: |",
- " - name: realm-ext-provider",
- " image: curlimages/curl",
- " imagePullPolicy: IfNotPresent",
- " command:",
- " - sh",
- " args:",
- " - -c",
- ` - curl -L -f -S -o /extensions/${pathBasename(jarFilePath)} https://AN.URL.FOR/${pathBasename(jarFilePath)}`,
- " volumeMounts:",
- " - name: extensions",
- " mountPath: /extensions",
- " ",
- " extraVolumeMounts: |",
- " - name: extensions",
- " mountPath: /opt/keycloak/providers",
- " extraEnv: |",
- " - name: KEYCLOAK_USER",
- " value: admin",
- " - name: KEYCLOAK_PASSWORD",
- " value: xxxxxxxxx",
- " - name: JAVA_OPTS",
- " value: -Dkeycloak.profile=preview",
- "",
- "",
`To test your theme locally you can spin up a Keycloak ${containerKeycloakVersion} container image with the theme pre loaded by running:`,
"",
`👉 $ .${pathSep}${pathRelative(
reactAppRootDirPath,
pathJoin(buildOptions.keycloakifyBuildDirPath, generateStartKeycloakTestingContainer.basename)
)} 👈`,
- "",
- `Test with different Keycloak versions by editing the .sh file. see available versions here: https://quay.io/repository/keycloak/keycloak?tab=tags`,
``,
`Once your container is up and running: `,
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
diff --git a/src/bin/tools/downloadAndUnzip.ts b/src/bin/tools/downloadAndUnzip.ts
index 4bff442b..d5c82015 100644
--- a/src/bin/tools/downloadAndUnzip.ts
+++ b/src/bin/tools/downloadAndUnzip.ts
@@ -1,12 +1,13 @@
import { exec as execCallback } from "child_process";
import { createHash } from "crypto";
-import { mkdir, readFile, stat, writeFile, unlink, rm } from "fs/promises";
+import { mkdir, readFile, stat, writeFile, unlink } from "fs/promises";
import fetch, { type FetchOptions } from "make-fetch-happen";
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
import { assert } from "tsafe/assert";
import { promisify } from "util";
import { transformCodebase } from "./transformCodebase";
import { unzip, zip } from "./unzip";
+import { rm } from "../tools/fs.rm";
const exec = promisify(execCallback);
diff --git a/src/bin/tools/fs.rm.ts b/src/bin/tools/fs.rm.ts
new file mode 100644
index 00000000..d7d41f50
--- /dev/null
+++ b/src/bin/tools/fs.rm.ts
@@ -0,0 +1,43 @@
+import * as fs from "fs/promises";
+import { join as pathJoin } from "path";
+import { NpmModuleVersion } from "./NpmModuleVersion";
+
+/**
+ * Polyfill of fs.rm(dirPath, { "recursive": true })
+ * For older version of Node
+ */
+export async function rm(dirPath: string, options: { recursive: true; force?: true }) {
+ if (NpmModuleVersion.compare(NpmModuleVersion.parse(process.version), NpmModuleVersion.parse("14.14.0")) > 0) {
+ return fs.rm(dirPath, options);
+ }
+
+ const { force = true } = options;
+
+ if (force && !(await checkDirExists(dirPath))) {
+ return;
+ }
+
+ const removeDir_rec = async (dirPath: string) =>
+ Promise.all(
+ (await fs.readdir(dirPath)).map(async basename => {
+ const fileOrDirpath = pathJoin(dirPath, basename);
+
+ if ((await fs.lstat(fileOrDirpath)).isDirectory()) {
+ await removeDir_rec(fileOrDirpath);
+ } else {
+ await fs.unlink(fileOrDirpath);
+ }
+ })
+ );
+
+ await removeDir_rec(dirPath);
+}
+
+async function checkDirExists(dirPath: string) {
+ try {
+ await fs.access(dirPath, fs.constants.F_OK);
+ return true;
+ } catch {
+ return false;
+ }
+}
diff --git a/src/bin/tools/fs.rmSync.ts b/src/bin/tools/fs.rmSync.ts
new file mode 100644
index 00000000..ff7f5ff8
--- /dev/null
+++ b/src/bin/tools/fs.rmSync.ts
@@ -0,0 +1,33 @@
+import * as fs from "fs";
+import { join as pathJoin } from "path";
+import { NpmModuleVersion } from "./NpmModuleVersion";
+
+/**
+ * Polyfill of fs.rmSync(dirPath, { "recursive": true })
+ * For older version of Node
+ */
+export function rmSync(dirPath: string, options: { recursive: true; force?: true }) {
+ if (NpmModuleVersion.compare(NpmModuleVersion.parse(process.version), NpmModuleVersion.parse("14.14.0")) > 0) {
+ fs.rmSync(dirPath, options);
+ }
+
+ const { force = true } = options;
+
+ if (force && !fs.existsSync(dirPath)) {
+ return;
+ }
+
+ const removeDir_rec = (dirPath: string) =>
+ fs.readdirSync(dirPath).forEach(basename => {
+ const fileOrDirpath = pathJoin(dirPath, basename);
+
+ if (fs.lstatSync(fileOrDirpath).isDirectory()) {
+ removeDir_rec(fileOrDirpath);
+ return;
+ } else {
+ fs.unlinkSync(fileOrDirpath);
+ }
+ });
+
+ removeDir_rec(dirPath);
+}
diff --git a/src/bin/tools/transformCodebase.ts b/src/bin/tools/transformCodebase.ts
index 2064fe7d..5b59978e 100644
--- a/src/bin/tools/transformCodebase.ts
+++ b/src/bin/tools/transformCodebase.ts
@@ -2,6 +2,7 @@ import * as fs from "fs";
import * as path from "path";
import { crawl } from "./crawl";
import { id } from "tsafe/id";
+import { rmSync } from "../tools/fs.rmSync";
type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string; fileRelativePath: string }) =>
| {
@@ -10,15 +11,25 @@ type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string; file
}
| undefined;
-/** Apply a transformation function to every file of directory */
+/**
+ * Apply a transformation function to every file of directory
+ * If source and destination are the same this function can be used to apply the transformation in place
+ * like filtering out some files or modifying them.
+ * */
export function transformCodebase(params: { srcDirPath: string; destDirPath: string; transformSourceCode?: TransformSourceCode }) {
const {
srcDirPath,
- destDirPath,
transformSourceCode = id(({ sourceCode }) => ({
"modifiedSourceCode": sourceCode
}))
} = params;
+ let { destDirPath } = params;
+
+ const isTargetSameAsSource = path.relative(srcDirPath, destDirPath) === "";
+
+ if (isTargetSameAsSource) {
+ destDirPath = path.join(srcDirPath, "..", "tmp_xOsPdkPsTdzPs34sOkHs");
+ }
for (const fileRelativePath of crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })) {
const filePath = path.join(srcDirPath, fileRelativePath);
@@ -44,4 +55,10 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
modifiedSourceCode
);
}
+
+ if (isTargetSameAsSource) {
+ rmSync(srcDirPath, { "recursive": true });
+
+ fs.renameSync(destDirPath, srcDirPath);
+ }
}