Compare commits

..

25 Commits

Author SHA1 Message Date
c168c7b156 Bump version 2025-01-06 02:47:42 +01:00
7a46115042 Enable persisting email change on test user 2025-01-06 02:47:28 +01:00
249a7bde89 Fix adding comment to certain ftl files 2025-01-06 02:31:46 +01:00
813740a002 Bump version 2025-01-05 21:34:34 +01:00
7840c2a6f5 Fix previous release 2025-01-05 21:34:16 +01:00
8f6c0d36d9 Bump version 2025-01-05 21:09:24 +01:00
12690b892b Fix replacing of xKeycloakify.themeName in theme variant 2025-01-05 21:09:06 +01:00
d01b4b71c9 Bump version 2025-01-05 21:00:53 +01:00
c29e600786 Fix generateResource bug 2025-01-05 21:00:34 +01:00
6309b7c45d Bump version 2025-01-05 04:27:11 +01:00
7e7996e40c Fix auto enable themes when using start-keycloak 2025-01-05 04:26:45 +01:00
deaeab0f61 Infer META-INF/keycloak-themes.jar 2025-01-05 03:14:34 +01:00
6bd5451230 Bump version 2025-01-05 02:09:33 +01:00
fb2d651a6f Rely on @keycloakify/email-native for email initialization 2025-01-05 02:08:18 +01:00
4845d7c32d Support incorporating theme native theme, with theme variant support #733 2025-01-05 02:08:13 +01:00
c33c315120 Bump version 2025-01-03 22:58:12 +01:00
99b8f1e789 Update i18n account multi-page boilerplate 2025-01-03 22:57:57 +01:00
6af13e1405 Bump version 2025-01-03 22:39:01 +01:00
f59fa4238c Link to the documentation for implementing non builtin pages 2025-01-03 22:38:45 +01:00
248effc57d Bump version 2025-01-03 02:47:31 +01:00
9e540b2c1f Fix assets import from public in .svelte files 2025-01-03 02:47:31 +01:00
ab7b5ff490 Remove ringerhq 2025-01-03 01:06:29 +01:00
486f944e0f Bump version 2025-01-02 10:25:16 +01:00
6cc3d4c442 Fix conflict in --path shorthand with --project, --path shorthand is now -t (target) 2025-01-02 10:25:16 +01:00
083290c6d4 update broken link 2024-12-30 02:04:09 +01:00
20 changed files with 646 additions and 480 deletions

View File

@ -1,4 +1,3 @@
# These are supported funding model platforms # These are supported funding model platforms
github: [garronej] github: [garronej]
custom: ['https://www.ringerhq.com/experts/garronej']

View File

@ -46,7 +46,7 @@ Keycloakify is fully compatible with Keycloak from version 11 to 26...[and beyon
> 📣 **Keycloakify 26 Released** > 📣 **Keycloakify 26 Released**
> Themes built with Keycloakify versions **prior** to Keycloak 26 are **incompatible** with Keycloak 26. > Themes built with Keycloakify versions **prior** to Keycloak 26 are **incompatible** with Keycloak 26.
> To ensure compatibility, simply upgrade to the latest Keycloakify version for your major release (v10 or v11) and rebuild your theme. > To ensure compatibility, simply upgrade to the latest Keycloakify version for your major release (v10 or v11) and rebuild your theme.
> No breaking changes have been introduced, but the target version ranges have been updated. For more details, see [this guide](https://docs.keycloakify.dev/targeting-specific-keycloak-versions). > No breaking changes have been introduced, but the target version ranges have been updated. For more details, see [this guide](https://docs.keycloakify.dev/features/compiler-options/keycloakversiontargets).
## Sponsors ## Sponsors

View File

@ -1,6 +1,6 @@
{ {
"name": "keycloakify", "name": "keycloakify",
"version": "11.7.0", "version": "11.8.5",
"description": "Framework to create custom Keycloak UIs", "description": "Framework to create custom Keycloak UIs",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -92,12 +92,14 @@ export async function command(params: { buildContext: BuildContext }) {
const templateValue = "Template.tsx (Layout common to every page)"; const templateValue = "Template.tsx (Layout common to every page)";
const userProfileFormFieldsValue = const userProfileFormFieldsValue =
"UserProfileFormFields.tsx (Renders the form of the register.ftl, login-update-profile.ftl, update-email.ftl and idp-review-user-profile.ftl)"; "UserProfileFormFields.tsx (Renders the form of the register.ftl, login-update-profile.ftl, update-email.ftl and idp-review-user-profile.ftl)";
const otherPageValue = "The page you're looking for isn't listed here";
const { value: pageIdOrComponent } = await cliSelect< const { value: pageIdOrComponent } = await cliSelect<
| LoginThemePageId | LoginThemePageId
| AccountThemePageId | AccountThemePageId
| typeof templateValue | typeof templateValue
| typeof userProfileFormFieldsValue | typeof userProfileFormFieldsValue
| typeof otherPageValue
>({ >({
values: (() => { values: (() => {
switch (themeType) { switch (themeType) {
@ -105,10 +107,11 @@ export async function command(params: { buildContext: BuildContext }) {
return [ return [
templateValue, templateValue,
userProfileFormFieldsValue, userProfileFormFieldsValue,
...LOGIN_THEME_PAGE_IDS ...LOGIN_THEME_PAGE_IDS,
otherPageValue
]; ];
case "account": case "account":
return [templateValue, ...ACCOUNT_THEME_PAGE_IDS]; return [templateValue, ...ACCOUNT_THEME_PAGE_IDS, otherPageValue];
} }
assert<Equals<typeof themeType, never>>(false); assert<Equals<typeof themeType, never>>(false);
})() })()
@ -116,6 +119,17 @@ export async function command(params: { buildContext: BuildContext }) {
process.exit(-1); process.exit(-1);
}); });
if (pageIdOrComponent === otherPageValue) {
console.log(
[
"To style a page not included in the base Keycloak, such as one added by a third-party Keycloak extension,",
"refer to the documentation: https://docs.keycloakify.dev/features/styling-a-custom-page-not-included-in-base-keycloak"
].join(" ")
);
process.exit(0);
}
console.log(`${pageIdOrComponent}`); console.log(`${pageIdOrComponent}`);
const componentBasename = (() => { const componentBasename = (() => {

View File

@ -23,22 +23,6 @@ export async function command(params: { buildContext: BuildContext }) {
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account"); const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
if (
fs.existsSync(accountThemeSrcDirPath) &&
fs.readdirSync(accountThemeSrcDirPath).length > 0
) {
console.warn(
chalk.red(
`There is already a ${pathRelative(
process.cwd(),
accountThemeSrcDirPath
)} directory in your project. Aborting.`
)
);
process.exit(-1);
}
exitIfUncommittedChanges({ exitIfUncommittedChanges({
projectDirPath: buildContext.projectDirPath projectDirPath: buildContext.projectDirPath
}); });
@ -51,17 +35,35 @@ export async function command(params: { buildContext: BuildContext }) {
switch (accountThemeType) { switch (accountThemeType) {
case "Multi-Page": case "Multi-Page":
fs.cpSync( {
pathJoin( if (
getThisCodebaseRootDirPath(), fs.existsSync(accountThemeSrcDirPath) &&
"src", fs.readdirSync(accountThemeSrcDirPath).length > 0
"bin", ) {
"initialize-account-theme", console.warn(
"multi-page-boilerplate" chalk.red(
), `There is already a ${pathRelative(
accountThemeSrcDirPath, process.cwd(),
{ recursive: true } accountThemeSrcDirPath
); )} directory in your project. Aborting.`
)
);
process.exit(-1);
}
fs.cpSync(
pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"initialize-account-theme",
"multi-page-boilerplate"
),
accountThemeSrcDirPath,
{ recursive: true }
);
}
break; break;
case "Single-Page": case "Single-Page":
{ {

View File

@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { i18nBuilder } from "keycloakify/account"; import { i18nBuilder } from "keycloakify/account";
import type { ThemeName } from "../kc.gen"; import type { ThemeName } from "../kc.gen";
/** @see: https://docs.keycloakify.dev/i18n */ /** @see: https://docs.keycloakify.dev/features/i18n */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { useI18n, ofTypeI18n } = i18nBuilder.withThemeName<ThemeName>().build(); const { useI18n, ofTypeI18n } = i18nBuilder.withThemeName<ThemeName>().build();
type I18n = typeof ofTypeI18n; type I18n = typeof ofTypeI18n;

View File

@ -1,17 +1,24 @@
import { join as pathJoin, relative as pathRelative } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import type { BuildContext } from "./shared/buildContext"; import type { BuildContext } from "./shared/buildContext";
import * as fs from "fs"; import cliSelect from "cli-select";
import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate"; import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import { assert } from "tsafe/assert"; import { exitIfUncommittedChanges } from "./shared/exitIfUncommittedChanges";
import { getSupportedDockerImageTags } from "./start-keycloak/getSupportedDockerImageTags";
import { dirname as pathDirname, join as pathJoin, relative as pathRelative } from "path";
import * as fs from "fs";
import { assert, is, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import { addSyncExtensionsToPostinstallScript } from "./shared/addSyncExtensionsToPostinstallScript";
import { getIsPrettierAvailable, runPrettier } from "./tools/runPrettier";
import { npmInstall } from "./tools/npmInstall";
import * as child_process from "child_process";
import { z } from "zod";
import chalk from "chalk";
export async function command(params: { buildContext: BuildContext }) { export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params; const { buildContext } = params;
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({ const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "initialize-email-theme", commandName: "initialize-account-theme",
buildContext buildContext
}); });
@ -19,6 +26,10 @@ export async function command(params: { buildContext: BuildContext }) {
return; return;
} }
exitIfUncommittedChanges({
projectDirPath: buildContext.projectDirPath
});
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email"); const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
if ( if (
@ -26,71 +37,110 @@ export async function command(params: { buildContext: BuildContext }) {
fs.readdirSync(emailThemeSrcDirPath).length > 0 fs.readdirSync(emailThemeSrcDirPath).length > 0
) { ) {
console.warn( console.warn(
`There is already a non empty ${pathRelative( chalk.red(
process.cwd(), `There is already a ${pathRelative(
emailThemeSrcDirPath process.cwd(),
)} directory in your project. Aborting.` emailThemeSrcDirPath
)} directory in your project. Aborting.`
)
); );
process.exit(-1); process.exit(-1);
} }
console.log("Initialize with the base email theme from which version of Keycloak?"); const { value: emailThemeType } = await cliSelect({
values: ["native (FreeMarker)" as const, "jsx-email (React)" as const]
}).catch(() => {
process.exit(-1);
});
const { extractedDirPath } = await downloadAndExtractArchive({ if (emailThemeType === "jsx-email (React)") {
url: await (async () => { console.log(
const { latestMajorTags } = await getSupportedDockerImageTags({ [
buildContext "There is currently no automated support for keycloakify-email, it has to be done manually, see documentation:",
"https://docs.keycloakify.dev/theme-types/email-theme"
].join("\n")
);
process.exit(0);
}
const parsedPackageJson = (() => {
type ParsedPackageJson = {
scripts?: Record<string, string | undefined>;
dependencies?: Record<string, string | undefined>;
devDependencies?: Record<string, string | undefined>;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
scripts: z.record(z.union([z.string(), z.undefined()])).optional(),
dependencies: z.record(z.union([z.string(), z.undefined()])).optional(),
devDependencies: z.record(z.union([z.string(), z.undefined()])).optional()
}); });
const keycloakVersion = latestMajorTags[0]; assert<Equals<z.infer<typeof zTargetType>, TargetType>>;
assert(keycloakVersion !== undefined); return id<z.ZodType<TargetType>>(zTargetType);
})();
const parsedPackageJson = JSON.parse(
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
);
return `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`; zParsedPackageJson.parse(parsedPackageJson);
})(),
cacheDirPath: buildContext.cacheDirPath,
fetchOptions: buildContext.fetchOptions,
uniqueIdOfOnArchiveFile: "extractOnlyEmailTheme",
onArchiveFile: async ({ fileRelativePath, writeFile }) => {
const fileRelativePath_target = pathRelative(
pathJoin("theme", "base", "email"),
fileRelativePath
);
if (fileRelativePath_target.startsWith("..")) { assert(is<ParsedPackageJson>(parsedPackageJson));
return;
}
await writeFile({ fileRelativePath: fileRelativePath_target }); return parsedPackageJson;
} })();
addSyncExtensionsToPostinstallScript({
parsedPackageJson,
buildContext
}); });
transformCodebase({ const moduleName = `@keycloakify/email-native`;
srcDirPath: extractedDirPath,
destDirPath: emailThemeSrcDirPath const [version] = (
}); JSON.parse(
child_process
.execSync(`npm show ${moduleName} versions --json`)
.toString("utf8")
.trim()
) as string[]
)
.reverse()
.filter(version => !version.includes("-"));
assert(version !== undefined);
(parsedPackageJson.dependencies ??= {})[moduleName] = `~${version}`;
if (parsedPackageJson.devDependencies !== undefined) {
delete parsedPackageJson.devDependencies[moduleName];
}
{ {
const themePropertyFilePath = pathJoin(emailThemeSrcDirPath, "theme.properties"); let sourceCode = JSON.stringify(parsedPackageJson, undefined, 2);
if (await getIsPrettierAvailable()) {
sourceCode = await runPrettier({
sourceCode,
filePath: buildContext.packageJsonFilePath
});
}
fs.writeFileSync( fs.writeFileSync(
themePropertyFilePath, buildContext.packageJsonFilePath,
Buffer.from( Buffer.from(sourceCode, "utf8")
[
`parent=base`,
fs.readFileSync(themePropertyFilePath).toString("utf8")
].join("\n"),
"utf8"
)
); );
} }
console.log( await npmInstall({
`The \`${pathJoin( packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
".", });
pathRelative(process.cwd(), emailThemeSrcDirPath)
)}\` directory have been created.` console.log(chalk.green("Email theme initialized."));
);
console.log("You can delete any file you don't modify.");
} }

View File

@ -15,7 +15,6 @@ import { readFileSync } from "fs";
import { isInside } from "../../tools/isInside"; import { isInside } from "../../tools/isInside";
import child_process from "child_process"; import child_process from "child_process";
import { rmSync } from "../../tools/fs.rmSync"; import { rmSync } from "../../tools/fs.rmSync";
import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
import { existsAsync } from "../../tools/fs.existsAsync"; import { existsAsync } from "../../tools/fs.existsAsync";
export type BuildContextLike = BuildContextLike_generatePom & { export type BuildContextLike = BuildContextLike_generatePom & {
@ -106,29 +105,55 @@ export async function buildJar(params: {
} }
}); });
remove_account_v1_in_meta_inf: { {
if (!doesImplementAccountV1Theme) { const filePath = pathJoin(
// NOTE: We do not have account v1 anyway tmpResourcesDirPath,
break remove_account_v1_in_meta_inf; "META-INF",
} "keycloak-themes.json"
);
if (keycloakAccountV1Version !== null) { await fs.mkdir(pathDirname(filePath));
// NOTE: No, we need to keep account-v1 in meta-inf
break remove_account_v1_in_meta_inf;
}
writeMetaInfKeycloakThemes({ await fs.writeFile(
resourcesDirPath: tmpResourcesDirPath, filePath,
getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => { Buffer.from(
assert(metaInfKeycloakTheme !== undefined); JSON.stringify(
{
themes: await (async () => {
const dirPath = pathJoin(tmpResourcesDirPath, "theme");
metaInfKeycloakTheme.themes = metaInfKeycloakTheme.themes.filter( const themeNames = (await fs.readdir(dirPath)).sort(
({ name }) => name !== "account-v1" (a, b) => {
); const indexA = buildContext.themeNames.indexOf(a);
const indexB = buildContext.themeNames.indexOf(b);
return metaInfKeycloakTheme; const orderA = indexA === -1 ? Infinity : indexA;
} const orderB = indexB === -1 ? Infinity : indexB;
});
return orderA - orderB;
}
);
return Promise.all(
themeNames.map(async themeName => {
const types = await fs.readdir(
pathJoin(dirPath, themeName)
);
return {
name: themeName,
types
};
})
);
})()
},
null,
2
),
"utf8"
)
);
} }
route_legacy_pages: { route_legacy_pages: {

View File

@ -6,8 +6,7 @@ import {
join as pathJoin, join as pathJoin,
relative as pathRelative, relative as pathRelative,
dirname as pathDirname, dirname as pathDirname,
extname as pathExtname, basename as pathBasename
sep as pathSep
} from "path"; } from "path";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode"; import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode"; import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
@ -31,16 +30,13 @@ import {
type BuildContextLike as BuildContextLike_generateMessageProperties type BuildContextLike as BuildContextLike_generateMessageProperties
} from "./generateMessageProperties"; } from "./generateMessageProperties";
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion"; import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
import {
writeMetaInfKeycloakThemes,
type MetaInfKeycloakTheme
} from "../../shared/metaInfKeycloakThemes";
import { objectEntries } from "tsafe/objectEntries";
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile"; import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath"; import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
import propertiesParser from "properties-parser"; import propertiesParser from "properties-parser";
import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed"; import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed";
import { listInstalledModules } from "../../tools/listInstalledModules"; import { listInstalledModules } from "../../tools/listInstalledModules";
import { isInside } from "../../tools/isInside";
import { id } from "tsafe/id";
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode & export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
BuildContextLike_generateMessageProperties & { BuildContextLike_generateMessageProperties & {
@ -61,6 +57,8 @@ export async function generateResources(params: {
buildContext: BuildContextLike; buildContext: BuildContextLike;
resourcesDirPath: string; resourcesDirPath: string;
}): Promise<void> { }): Promise<void> {
const start = Date.now();
const { resourcesDirPath, buildContext } = params; const { resourcesDirPath, buildContext } = params;
const [themeName] = buildContext.themeNames; const [themeName] = buildContext.themeNames;
@ -78,12 +76,23 @@ export async function generateResources(params: {
}; };
const writeMessagePropertiesFilesByThemeType: Partial< const writeMessagePropertiesFilesByThemeType: Partial<
Record<ThemeType, (params: { messageDirPath: string; themeName: string }) => void> Record<
ThemeType | "email",
(params: { messageDirPath: string; themeName: string }) => void
>
> = {}; > = {};
for (const themeType of THEME_TYPES) { for (const themeType of [...THEME_TYPES, "email"] as const) {
if (!buildContext.implementedThemeTypes[themeType].isImplemented) { let isNative: boolean;
continue;
{
const v = buildContext.implementedThemeTypes[themeType];
if (!v.isImplemented && !v.isImplemented_native) {
continue;
}
isNative = !v.isImplemented && v.isImplemented_native;
} }
const getAccountThemeType = () => { const getAccountThemeType = () => {
@ -102,12 +111,18 @@ export async function generateResources(params: {
return getAccountThemeType() === "Single-Page"; return getAccountThemeType() === "Single-Page";
case "admin": case "admin":
return true; return true;
case "email":
return false;
} }
})(); })();
const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType }); const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
apply_replacers_and_move_to_theme_resources: { apply_replacers_and_move_to_theme_resources: {
if (isNative) {
break apply_replacers_and_move_to_theme_resources;
}
const destDirPath = pathJoin( const destDirPath = pathJoin(
themeTypeDirPath, themeTypeDirPath,
"resources", "resources",
@ -191,59 +206,93 @@ export async function generateResources(params: {
}); });
} }
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({ generate_ftl_files: {
themeName, if (isNative) {
indexHtmlCode: fs break generate_ftl_files;
.readFileSync(pathJoin(buildContext.projectBuildDirPath, "index.html")) }
.toString("utf8"),
buildContext,
keycloakifyVersion: readThisNpmPackageVersion(),
themeType,
fieldNames: isSpa
? []
: (assert(themeType !== "admin"),
readFieldNameUsage({
themeSrcDirPath: buildContext.themeSrcDirPath,
themeType
}))
});
[ assert(themeType !== "email");
...(() => {
switch (themeType) { const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
case "login": themeName,
return LOGIN_THEME_PAGE_IDS; indexHtmlCode: fs
case "account": .readFileSync(
return getAccountThemeType() === "Single-Page" pathJoin(buildContext.projectBuildDirPath, "index.html")
? ["index.ftl"] )
: ACCOUNT_THEME_PAGE_IDS; .toString("utf8"),
case "admin": buildContext,
return ["index.ftl"]; 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 };
} }
})(), });
...(isSpa }
? []
: readExtraPagesNames({
themeType,
themeSrcDirPath: buildContext.themeSrcDirPath
}))
].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId });
fs.writeFileSync(
pathJoin(themeTypeDirPath, pageId),
Buffer.from(ftlCode, "utf8")
);
});
let languageTags: string[] | undefined = undefined; let languageTags: string[] | undefined = undefined;
i18n_multi_page: { i18n_multi_page: {
if (isNative) {
break i18n_multi_page;
}
if (isSpa) { if (isSpa) {
break i18n_multi_page; break i18n_multi_page;
} }
assert(themeType !== "admin"); assert(themeType !== "admin" && themeType !== "email");
const wrap = generateMessageProperties({ const wrap = generateMessageProperties({
buildContext, buildContext,
@ -364,27 +413,24 @@ export async function generateResources(params: {
); );
} }
i18n_single_page: { i18n_for_spas_and_native: {
if (!isSpa) { if (!isSpa && !isNative) {
break i18n_single_page; break i18n_for_spas_and_native;
} }
if (isLegacyAccountSpa) { if (isLegacyAccountSpa) {
break i18n_single_page; break i18n_for_spas_and_native;
} }
assert(themeType === "account" || themeType === "admin");
const messagesDirPath_theme = pathJoin( const messagesDirPath_theme = pathJoin(
buildContext.themeSrcDirPath, buildContext.themeSrcDirPath,
themeType, themeType,
"i18n" isNative ? "messages" : "i18n"
); );
assert( if (!fs.existsSync(messagesDirPath_theme)) {
fs.existsSync(messagesDirPath_theme), break i18n_for_spas_and_native;
`${messagesDirPath_theme} is supposed to exist` }
);
const propertiesByLang: Record< const propertiesByLang: Record<
string, string,
@ -524,6 +570,10 @@ export async function generateResources(params: {
} }
keycloak_static_resources: { keycloak_static_resources: {
if (isNative) {
break keycloak_static_resources;
}
if (isSpa) { if (isSpa) {
break keycloak_static_resources; break keycloak_static_resources;
} }
@ -540,183 +590,167 @@ export async function generateResources(params: {
}); });
} }
fs.writeFileSync( bring_in_account_v1: {
pathJoin(themeTypeDirPath, "theme.properties"), if (isNative) {
Buffer.from( break bring_in_account_v1;
[
`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>>(false);
})()}`,
...(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"
)
);
}
email: {
if (!buildContext.implementedThemeTypes.email.isImplemented) {
break email;
}
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
transformCodebase({
srcDirPath: emailThemeSrcDirPath,
destDirPath: getThemeTypeDirPath({ themeName, themeType: "email" })
});
}
bring_in_account_v1: {
if (!buildContext.implementedThemeTypes.account.isImplemented) {
break bring_in_account_v1;
}
if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
break bring_in_account_v1;
}
transformCodebase({
srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "res", "account-v1"),
destDirPath: getThemeTypeDirPath({
themeName: "account-v1",
themeType: "account"
})
});
}
{
const metaInfKeycloakThemes: MetaInfKeycloakTheme = { themes: [] };
for (const themeName of buildContext.themeNames) {
metaInfKeycloakThemes.themes.push({
name: themeName,
types: objectEntries(buildContext.implementedThemeTypes)
.filter(([, { isImplemented }]) => isImplemented)
.map(([themeType]) => themeType)
});
}
if (buildContext.implementedThemeTypes.account.isImplemented) {
metaInfKeycloakThemes.themes.push({
name: "account-v1",
types: ["account"]
});
}
writeMetaInfKeycloakThemes({
resourcesDirPath,
getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
});
}
for (const themeVariantName of buildContext.themeNames) {
if (themeVariantName === themeName) {
continue;
}
transformCodebase({
srcDirPath: pathJoin(resourcesDirPath, "theme", themeName),
destDirPath: pathJoin(resourcesDirPath, "theme", themeVariantName),
transformSourceCode: ({ 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 };
} }
});
}
for (const themeName of buildContext.themeNames) { if (themeType !== "account") {
for (const [themeType, writeMessagePropertiesFiles] of objectEntries( break bring_in_account_v1;
writeMessagePropertiesFilesByThemeType
)) {
// NOTE: This is just a quirk of the type system: We can't really differentiate in a record
// between the case where the key isn't present and the case where the value is `undefined`.
if (writeMessagePropertiesFiles === undefined) {
return;
} }
writeMessagePropertiesFiles({
messageDirPath: pathJoin(
getThemeTypeDirPath({ themeName, themeType }),
"messages"
),
themeName
});
}
}
modify_email_theme_per_variant: { assert(buildContext.implementedThemeTypes.account.isImplemented);
if (!buildContext.implementedThemeTypes.email.isImplemented) {
break modify_email_theme_per_variant;
}
for (const themeName of buildContext.themeNames) { if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
const emailThemeDirPath = getThemeTypeDirPath({ break bring_in_account_v1;
themeName, }
themeType: "email"
});
transformCodebase({ transformCodebase({
srcDirPath: emailThemeDirPath, srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "res", "account-v1"),
destDirPath: emailThemeDirPath, destDirPath: getThemeTypeDirPath({
transformSourceCode: ({ filePath, sourceCode }) => { themeName: "account-v1",
if (!filePath.endsWith(".ftl")) { themeType: "account"
return { modifiedSourceCode: sourceCode }; })
}
return {
modifiedSourceCode: Buffer.from(
sourceCode
.toString("utf8")
.replace(/xKeycloakify\.themeName/g, `"${themeName}"`),
"utf8"
)
};
}
}); });
} }
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"
)
);
}
} }
for (const themeVariantName of [...buildContext.themeNames].reverse()) {
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 (!isNative && themeVariantName === themeName) {
break copy_main_theme_to_theme_variant_theme;
}
transformCodebase({
srcDirPath: getThemeTypeDirPath({ themeName, themeType }),
destDirPath: getThemeTypeDirPath({
themeName: themeVariantName,
themeType
}),
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
patch_xKeycloakify_themeName: {
if (!fileRelativePath.endsWith(".ftl")) {
break patch_xKeycloakify_themeName;
}
if (
!isNative &&
pathBasename(fileRelativePath) !== fileRelativePath
) {
break patch_xKeycloakify_themeName;
}
const modifiedSourceCode = Buffer.from(
Buffer.from(sourceCode)
.toString("utf-8")
.replace(
...id<[string | RegExp, string]>(
isNative
? [
/xKeycloakify\.themeName/g,
`"${themeVariantName}"`
]
: [
`"themeName": "${themeName}"`,
`"themeName": "${themeVariantName}"`
]
)
),
"utf8"
);
if (!isNative) {
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
});
}
}
}
console.log(`Generated resources in ${Date.now() - start}ms`);
} }

View File

@ -303,7 +303,7 @@ program
key: "path", key: "path",
name: (() => { name: (() => {
const long = "path"; const long = "path";
const short = "p"; const short = "t";
optionsKeys.push(long, short); optionsKeys.push(long, short);
@ -318,11 +318,12 @@ program
.option({ .option({
key: "revert", key: "revert",
name: (() => { name: (() => {
const name = "revert"; const long = "revert";
const short = "r";
optionsKeys.push(name); optionsKeys.push(long, short);
return name; return { long, short };
})(), })(),
description: [ description: [
"Restores a file or directory to its original auto-generated state,", "Restores a file or directory to its original auto-generated state,",

View File

@ -1,6 +1,6 @@
import { dirname as pathDirname, relative as pathRelative, sep as pathSep } from "path"; import { dirname as pathDirname, relative as pathRelative, sep as pathSep } from "path";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { BuildContext } from "../buildContext"; import type { BuildContext } from "./buildContext";
export type BuildContextLike = { export type BuildContextLike = {
projectDirPath: string; projectDirPath: string;

View File

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

View File

@ -1,5 +1,5 @@
import { dirname as pathDirname, join as pathJoin, relative as pathRelative } from "path"; import { dirname as pathDirname, join as pathJoin, relative as pathRelative } from "path";
import type { BuildContext } from "../buildContext"; import type { BuildContext } from "./buildContext";
import * as fs from "fs"; import * as fs from "fs";
import { assert, is, type Equals } from "tsafe/assert"; import { assert, is, type Equals } from "tsafe/assert";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
@ -7,8 +7,8 @@ import {
addSyncExtensionsToPostinstallScript, addSyncExtensionsToPostinstallScript,
type BuildContextLike as BuildContextLike_addSyncExtensionsToPostinstallScript type BuildContextLike as BuildContextLike_addSyncExtensionsToPostinstallScript
} from "./addSyncExtensionsToPostinstallScript"; } from "./addSyncExtensionsToPostinstallScript";
import { getIsPrettierAvailable, runPrettier } from "../../tools/runPrettier"; import { getIsPrettierAvailable, runPrettier } from "../tools/runPrettier";
import { npmInstall } from "../../tools/npmInstall"; import { npmInstall } from "../tools/npmInstall";
import * as child_process from "child_process"; import * as child_process from "child_process";
import { z } from "zod"; import { z } from "zod";
import chalk from "chalk"; import chalk from "chalk";

View File

@ -1 +0,0 @@
export * from "./initializeSpa";

View File

@ -1,40 +0,0 @@
import { join as pathJoin, dirname as pathDirname } from "path";
import type { ThemeType } from "./constants";
import * as fs from "fs";
export type MetaInfKeycloakTheme = {
themes: { name: string; types: (ThemeType | "email")[] }[];
};
export function writeMetaInfKeycloakThemes(params: {
resourcesDirPath: string;
getNewMetaInfKeycloakTheme: (params: {
metaInfKeycloakTheme: MetaInfKeycloakTheme | undefined;
}) => MetaInfKeycloakTheme;
}) {
const { resourcesDirPath, getNewMetaInfKeycloakTheme } = params;
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(filePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
fs.writeFileSync(
filePath,
Buffer.from(JSON.stringify(newMetaInfKeycloakThemes, null, 2), "utf8")
);
}

View File

@ -1,28 +1,20 @@
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { ParsedRealmJson } from "./ParsedRealmJson"; import type { ParsedRealmJson } from "./ParsedRealmJson";
import { getDefaultConfig } from "./defaultConfig"; import { getDefaultConfig } from "./defaultConfig";
import type { BuildContext } from "../../shared/buildContext"; import { TEST_APP_URL, type ThemeType, THEME_TYPES } from "../../shared/constants";
import { objectKeys } from "tsafe/objectKeys";
import { TEST_APP_URL } from "../../shared/constants";
import { sameFactory } from "evt/tools/inDepth/same"; import { sameFactory } from "evt/tools/inDepth/same";
export type BuildContextLike = {
themeNames: BuildContext["themeNames"];
implementedThemeTypes: BuildContext["implementedThemeTypes"];
};
assert<BuildContext extends BuildContextLike ? true : false>;
export function prepareRealmConfig(params: { export function prepareRealmConfig(params: {
parsedRealmJson: ParsedRealmJson; parsedRealmJson: ParsedRealmJson;
keycloakMajorVersionNumber: number; keycloakMajorVersionNumber: number;
buildContext: BuildContextLike; parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
}): { }): {
realmName: string; realmName: string;
clientName: string; clientName: string;
username: string; username: string;
} { } {
const { parsedRealmJson, keycloakMajorVersionNumber, buildContext } = params; const { parsedRealmJson, keycloakMajorVersionNumber, parsedKeycloakThemesJsonEntry } =
params;
const { username } = addOrEditTestUser({ const { username } = addOrEditTestUser({
parsedRealmJson, parsedRealmJson,
@ -38,8 +30,7 @@ export function prepareRealmConfig(params: {
enableCustomThemes({ enableCustomThemes({
parsedRealmJson, parsedRealmJson,
themeName: buildContext.themeNames[0], parsedKeycloakThemesJsonEntry
implementedThemeTypes: buildContext.implementedThemeTypes
}); });
enable_custom_events_listeners: { enable_custom_events_listeners: {
@ -63,17 +54,15 @@ export function prepareRealmConfig(params: {
function enableCustomThemes(params: { function enableCustomThemes(params: {
parsedRealmJson: ParsedRealmJson; parsedRealmJson: ParsedRealmJson;
themeName: string; parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
implementedThemeTypes: BuildContextLike["implementedThemeTypes"];
}) { }) {
const { parsedRealmJson, themeName, implementedThemeTypes } = params; const { parsedRealmJson, parsedKeycloakThemesJsonEntry } = params;
for (const themeType of objectKeys(implementedThemeTypes)) { for (const themeType of [...THEME_TYPES, "email"] as const) {
if (!implementedThemeTypes[themeType].isImplemented) { parsedRealmJson[`${themeType}Theme` as const] =
continue; !parsedKeycloakThemesJsonEntry.types.includes(themeType)
} ? ""
: parsedKeycloakThemesJsonEntry.name;
parsedRealmJson[`${themeType}Theme` as const] = themeName;
} }
} }
@ -112,7 +101,6 @@ function addOrEditTestUser(params: {
); );
newUser.username = defaultUser_default.username; newUser.username = defaultUser_default.username;
newUser.email = defaultUser_default.email;
delete_existing_password_credential_if_any: { delete_existing_password_credential_if_any: {
const i = newUser.credentials.findIndex( const i = newUser.credentials.findIndex(

View File

@ -1,10 +1,7 @@
import type { BuildContext } from "../../shared/buildContext"; import type { BuildContext } from "../../shared/buildContext";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { getDefaultConfig } from "./defaultConfig"; import { getDefaultConfig } from "./defaultConfig";
import { import { prepareRealmConfig } from "./prepareRealmConfig";
prepareRealmConfig,
type BuildContextLike as BuildContextLike_prepareRealmConfig
} from "./prepareRealmConfig";
import * as fs from "fs"; import * as fs from "fs";
import { import {
join as pathJoin, join as pathJoin,
@ -24,18 +21,19 @@ import {
} from "./dumpContainerConfig"; } from "./dumpContainerConfig";
import * as runExclusive from "run-exclusive"; import * as runExclusive from "run-exclusive";
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce"; import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
import type { ThemeType } from "../../shared/constants";
import chalk from "chalk"; import chalk from "chalk";
export type BuildContextLike = BuildContextLike_dumpContainerConfig & export type BuildContextLike = BuildContextLike_dumpContainerConfig & {
BuildContextLike_prepareRealmConfig & { projectDirPath: string;
projectDirPath: string; };
};
assert<BuildContext extends BuildContextLike ? true : false>; assert<BuildContext extends BuildContextLike ? true : false>;
export async function getRealmConfig(params: { export async function getRealmConfig(params: {
keycloakMajorVersionNumber: number; keycloakMajorVersionNumber: number;
realmJsonFilePath_userProvided: string | undefined; realmJsonFilePath_userProvided: string | undefined;
parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
buildContext: BuildContextLike; buildContext: BuildContextLike;
}): Promise<{ }): Promise<{
realmJsonFilePath: string; realmJsonFilePath: string;
@ -44,8 +42,12 @@ export async function getRealmConfig(params: {
username: string; username: string;
onRealmConfigChange: () => Promise<void>; onRealmConfigChange: () => Promise<void>;
}> { }> {
const { keycloakMajorVersionNumber, realmJsonFilePath_userProvided, buildContext } = const {
params; keycloakMajorVersionNumber,
realmJsonFilePath_userProvided,
parsedKeycloakThemesJsonEntry,
buildContext
} = params;
const realmJsonFilePath = pathJoin( const realmJsonFilePath = pathJoin(
buildContext.projectDirPath, buildContext.projectDirPath,
@ -71,8 +73,8 @@ export async function getRealmConfig(params: {
const { clientName, realmName, username } = prepareRealmConfig({ const { clientName, realmName, username } = prepareRealmConfig({
parsedRealmJson, parsedRealmJson,
buildContext, keycloakMajorVersionNumber,
keycloakMajorVersionNumber parsedKeycloakThemesJsonEntry
}); });
{ {

View File

@ -4,7 +4,8 @@ import {
CONTAINER_NAME, CONTAINER_NAME,
KEYCLOAKIFY_SPA_DEV_SERVER_PORT, KEYCLOAKIFY_SPA_DEV_SERVER_PORT,
KEYCLOAKIFY_LOGIN_JAR_BASENAME, KEYCLOAKIFY_LOGIN_JAR_BASENAME,
TEST_APP_URL TEST_APP_URL,
ThemeType
} from "../shared/constants"; } from "../shared/constants";
import { SemVer } from "../tools/SemVer"; import { SemVer } from "../tools/SemVer";
import { assert, type Equals } from "tsafe/assert"; import { assert, type Equals } from "tsafe/assert";
@ -34,6 +35,7 @@ import { startViteDevServer } from "./startViteDevServer";
import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig"; import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig";
import { getSupportedDockerImageTags } from "./getSupportedDockerImageTags"; import { getSupportedDockerImageTags } from "./getSupportedDockerImageTags";
import { getRealmConfig } from "./realmConfig"; import { getRealmConfig } from "./realmConfig";
import { id } from "tsafe/id";
export async function command(params: { export async function command(params: {
buildContext: BuildContext; buildContext: BuildContext;
@ -270,32 +272,6 @@ export async function command(params: {
return wrap.majorVersionNumber; return wrap.majorVersionNumber;
})(); })();
const { clientName, onRealmConfigChange, realmJsonFilePath, realmName, username } =
await getRealmConfig({
keycloakMajorVersionNumber,
realmJsonFilePath_userProvided: await (async () => {
if (cliCommandOptions.realmJsonFilePath !== undefined) {
return getAbsoluteAndInOsFormatPath({
pathIsh: cliCommandOptions.realmJsonFilePath,
cwd: process.cwd()
});
}
if (buildContext.startKeycloakOptions.realmJsonFilePath !== undefined) {
assert(
await existsAsync(
buildContext.startKeycloakOptions.realmJsonFilePath
),
`${pathRelative(process.cwd(), buildContext.startKeycloakOptions.realmJsonFilePath)} does not exist`
);
return buildContext.startKeycloakOptions.realmJsonFilePath;
}
return undefined;
})(),
buildContext
});
{ {
const { isAppBuildSuccess } = await appBuild({ const { isAppBuildSuccess } = await appBuild({
buildContext buildContext
@ -376,10 +352,24 @@ export async function command(params: {
)) ))
]; ];
let parsedKeycloakThemesJson = id<
{ themes: { name: string; types: (ThemeType | "email")[] }[] } | undefined
>(undefined);
async function extractThemeResourcesFromJar() { async function extractThemeResourcesFromJar() {
await extractArchive({ await extractArchive({
archiveFilePath: jarFilePath, archiveFilePath: jarFilePath,
onArchiveFile: async ({ relativeFilePathInArchive, writeFile }) => { onArchiveFile: async ({ relativeFilePathInArchive, writeFile, readFile }) => {
if (
relativeFilePathInArchive ===
pathJoin("META-INF", "keycloak-themes.json") &&
parsedKeycloakThemesJson === undefined
) {
parsedKeycloakThemesJson = JSON.parse(
(await readFile()).toString("utf8")
);
}
if (isInside({ dirPath: "theme", filePath: relativeFilePathInArchive })) { if (isInside({ dirPath: "theme", filePath: relativeFilePathInArchive })) {
await writeFile({ await writeFile({
filePath: pathJoin( filePath: pathJoin(
@ -401,6 +391,43 @@ export async function command(params: {
await extractThemeResourcesFromJar(); await extractThemeResourcesFromJar();
assert(parsedKeycloakThemesJson !== undefined);
const { clientName, onRealmConfigChange, realmJsonFilePath, realmName, username } =
await getRealmConfig({
keycloakMajorVersionNumber,
parsedKeycloakThemesJsonEntry: (() => {
const entry = parsedKeycloakThemesJson.themes.find(
({ name }) => name === buildContext.themeNames[0]
);
assert(entry !== undefined);
return entry;
})(),
realmJsonFilePath_userProvided: await (async () => {
if (cliCommandOptions.realmJsonFilePath !== undefined) {
return getAbsoluteAndInOsFormatPath({
pathIsh: cliCommandOptions.realmJsonFilePath,
cwd: process.cwd()
});
}
if (buildContext.startKeycloakOptions.realmJsonFilePath !== undefined) {
assert(
await existsAsync(
buildContext.startKeycloakOptions.realmJsonFilePath
),
`${pathRelative(process.cwd(), buildContext.startKeycloakOptions.realmJsonFilePath)} does not exist`
);
return buildContext.startKeycloakOptions.realmJsonFilePath;
}
return undefined;
})(),
buildContext
});
const jarFilePath_cacheDir = pathJoin( const jarFilePath_cacheDir = pathJoin(
buildContext.cacheDirPath, buildContext.cacheDirPath,
pathBasename(jarFilePath) pathBasename(jarFilePath)

View File

@ -99,12 +99,31 @@ function addCommentToSourceCode(params: {
return toResult(commentLines.map(line => `# ${line}`).join("\n")); return toResult(commentLines.map(line => `# ${line}`).join("\n"));
} }
if (fileRelativePath.endsWith(".ftl")) {
const comment = [`<#--`, ...commentLines.map(line => ` ${line}`), `-->`].join(
"\n"
);
if (sourceCode.trim().startsWith("<#ftl")) {
const [first, ...rest] = sourceCode.split(">");
const last = rest.join(">");
return [`${first}>`, comment, last].join("\n");
}
return toResult(comment);
}
if (fileRelativePath.endsWith(".html") || fileRelativePath.endsWith(".svg")) { if (fileRelativePath.endsWith(".html") || fileRelativePath.endsWith(".svg")) {
const comment = [ const comment = [
`<!--`, `<!--`,
...commentLines.map( ...commentLines.map(
line => line =>
` ${line.replace("--path", "-p").replace("Before modifying", "Before modifying or replacing")}` ` ${line
.replace("--path", "-t")
.replace("--revert", "-r")
.replace("Before modifying", "Before modifying or replacing")}`
), ),
`-->` `-->`
].join("\n"); ].join("\n");

View File

@ -155,8 +155,9 @@ export function keycloakify(params: keycloakify.Params) {
{ {
const isJavascriptFile = id.endsWith(".js") || id.endsWith(".jsx"); const isJavascriptFile = id.endsWith(".js") || id.endsWith(".jsx");
const isTypeScriptFile = id.endsWith(".ts") || id.endsWith(".tsx"); const isTypeScriptFile = id.endsWith(".ts") || id.endsWith(".tsx");
const isSvelteFile = id.endsWith(".svelte");
if (!isTypeScriptFile && !isJavascriptFile) { if (!isTypeScriptFile && !isJavascriptFile && !isSvelteFile) {
return; return;
} }
} }