Compare commits
64 Commits
Author | SHA1 | Date | |
---|---|---|---|
947efe8d63 | |||
64189bf8fe | |||
400c630418 | |||
402360b436 | |||
9f001f1521 | |||
368e3a32c5 | |||
002e3d4b3d | |||
f94f9b51c9 | |||
055b15bd46 | |||
0e70b0b0de | |||
8faf9a3eed | |||
075d9f9de5 | |||
840079be32 | |||
50ae962f09 | |||
61aa1f9896 | |||
d88e0e4dd5 | |||
18c36eb4de | |||
80aeabad51 | |||
419e1f473a | |||
80988125e8 | |||
271ad2da71 | |||
b2732f2595 | |||
53820e1e34 | |||
09dd45e437 | |||
1f654a7820 | |||
0690f40bad | |||
2285883149 | |||
af87e41bb8 | |||
9ba884483d | |||
f5a300953a | |||
ab9a962f58 | |||
484adb607f | |||
e1f38d4196 | |||
5de629acf2 | |||
8b4b24a036 | |||
75ab130249 | |||
981ca7e9a4 | |||
acb4e260a7 | |||
ff20b0a844 | |||
1b77c69a01 | |||
158275f5c2 | |||
a085c8093e | |||
cb358bd745 | |||
e788c46601 | |||
d551b4bffb | |||
c168c7b156 | |||
7a46115042 | |||
249a7bde89 | |||
813740a002 | |||
7840c2a6f5 | |||
8f6c0d36d9 | |||
12690b892b | |||
d01b4b71c9 | |||
c29e600786 | |||
6309b7c45d | |||
7e7996e40c | |||
deaeab0f61 | |||
6bd5451230 | |||
fb2d651a6f | |||
4845d7c32d | |||
c33c315120 | |||
99b8f1e789 | |||
6af13e1405 | |||
f59fa4238c |
@ -327,6 +327,24 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "EternalSide",
|
||||
"name": "Lesha",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/118743608?v=4",
|
||||
"profile": "http://t.me/AAT_L",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "bacongobbler",
|
||||
"name": "Matthew Fisher",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1360539?v=4",
|
||||
"profile": "https://blog.bacongobbler.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
@ -168,6 +168,10 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zvn2060"><img src="https://avatars.githubusercontent.com/u/45450852?v=4?s=100" width="100px;" alt="HI_OuO"/><br /><sub><b>HI_OuO</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=zvn2060" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tripheo0412"><img src="https://avatars.githubusercontent.com/u/25382052?v=4?s=100" width="100px;" alt="Tri Hoang"/><br /><sub><b>Tri Hoang</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=tripheo0412" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://t.me/AAT_L"><img src="https://avatars.githubusercontent.com/u/118743608?v=4?s=100" width="100px;" alt="Lesha"/><br /><sub><b>Lesha</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=EternalSide" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.bacongobbler.com"><img src="https://avatars.githubusercontent.com/u/1360539?v=4?s=100" width="100px;" alt="Matthew Fisher"/><br /><sub><b>Matthew Fisher</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=bacongobbler" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "11.7.2",
|
||||
"version": "11.8.22",
|
||||
"description": "Framework to create custom Keycloak UIs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -280,6 +280,24 @@ export async function downloadKeycloakDefaultTheme(params: {
|
||||
"fonts",
|
||||
"OpenSans-Semibold-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-SemiboldItalic-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-SemiboldItalic-webfont.woff"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-SemiboldItalic-webfont.ttf"
|
||||
),
|
||||
pathJoin("patternfly", "dist", "img", "bg-login.jpg"),
|
||||
pathJoin("jquery", "dist", "jquery.min.js"),
|
||||
pathJoin("rfc4648", "lib", "rfc4648.js")
|
||||
|
@ -20,7 +20,7 @@ import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_dele
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
commandName: "add-story",
|
||||
buildContext
|
||||
});
|
||||
@ -74,7 +74,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
|
||||
if (themeType === "admin") {
|
||||
console.log(
|
||||
`${chalk.red("✗")} Sorry, there is no Storybook support for the Account UI.`
|
||||
`${chalk.red("✗")} Sorry, there is no Storybook support for the Admin UI.`
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
|
@ -11,7 +11,7 @@ import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
commandName: "copy-keycloak-resources-to-public",
|
||||
buildContext
|
||||
});
|
||||
|
@ -22,7 +22,7 @@ import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
commandName: "eject-page",
|
||||
buildContext
|
||||
});
|
||||
@ -92,12 +92,14 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
const templateValue = "Template.tsx (Layout common to every page)";
|
||||
const userProfileFormFieldsValue =
|
||||
"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<
|
||||
| LoginThemePageId
|
||||
| AccountThemePageId
|
||||
| typeof templateValue
|
||||
| typeof userProfileFormFieldsValue
|
||||
| typeof otherPageValue
|
||||
>({
|
||||
values: (() => {
|
||||
switch (themeType) {
|
||||
@ -105,10 +107,11 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
return [
|
||||
templateValue,
|
||||
userProfileFormFieldsValue,
|
||||
...LOGIN_THEME_PAGE_IDS
|
||||
...LOGIN_THEME_PAGE_IDS,
|
||||
otherPageValue
|
||||
];
|
||||
case "account":
|
||||
return [templateValue, ...ACCOUNT_THEME_PAGE_IDS];
|
||||
return [templateValue, ...ACCOUNT_THEME_PAGE_IDS, otherPageValue];
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
})()
|
||||
@ -116,6 +119,17 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
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}`);
|
||||
|
||||
const componentBasename = (() => {
|
||||
|
@ -12,7 +12,7 @@ import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath"
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
commandName: "initialize-account-theme",
|
||||
buildContext
|
||||
});
|
||||
@ -23,22 +23,6 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
|
||||
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({
|
||||
projectDirPath: buildContext.projectDirPath
|
||||
});
|
||||
@ -51,17 +35,35 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
|
||||
switch (accountThemeType) {
|
||||
case "Multi-Page":
|
||||
fs.cpSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
"bin",
|
||||
"initialize-account-theme",
|
||||
"multi-page-boilerplate"
|
||||
),
|
||||
accountThemeSrcDirPath,
|
||||
{ recursive: true }
|
||||
);
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
fs.cpSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
"bin",
|
||||
"initialize-account-theme",
|
||||
"multi-page-boilerplate"
|
||||
),
|
||||
accountThemeSrcDirPath,
|
||||
{ recursive: true }
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "Single-Page":
|
||||
{
|
||||
|
@ -1,8 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { i18nBuilder } from "keycloakify/account";
|
||||
import type { ThemeName } from "../kc.gen";
|
||||
|
||||
/** @see: https://docs.keycloakify.dev/i18n */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
/** @see: https://docs.keycloakify.dev/features/i18n */
|
||||
const { useI18n, ofTypeI18n } = i18nBuilder.withThemeName<ThemeName>().build();
|
||||
|
||||
type I18n = typeof ofTypeI18n;
|
||||
|
@ -7,7 +7,7 @@ import { command as updateKcGenCommand } from "./update-kc-gen";
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
commandName: "initialize-admin-theme",
|
||||
buildContext
|
||||
});
|
||||
|
@ -1,16 +1,23 @@
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { transformCodebase } from "./tools/transformCodebase";
|
||||
import type { BuildContext } from "./shared/buildContext";
|
||||
import * as fs from "fs";
|
||||
import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive";
|
||||
import cliSelect from "cli-select";
|
||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { getSupportedDockerImageTags } from "./start-keycloak/getSupportedDockerImageTags";
|
||||
import { exitIfUncommittedChanges } from "./shared/exitIfUncommittedChanges";
|
||||
|
||||
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 }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
commandName: "initialize-email-theme",
|
||||
buildContext
|
||||
});
|
||||
@ -19,6 +26,10 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
return;
|
||||
}
|
||||
|
||||
exitIfUncommittedChanges({
|
||||
projectDirPath: buildContext.projectDirPath
|
||||
});
|
||||
|
||||
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
|
||||
|
||||
if (
|
||||
@ -26,71 +37,120 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
fs.readdirSync(emailThemeSrcDirPath).length > 0
|
||||
) {
|
||||
console.warn(
|
||||
`There is already a non empty ${pathRelative(
|
||||
process.cwd(),
|
||||
emailThemeSrcDirPath
|
||||
)} directory in your project. Aborting.`
|
||||
chalk.red(
|
||||
`There is already a ${pathRelative(
|
||||
process.cwd(),
|
||||
emailThemeSrcDirPath
|
||||
)} directory in your project. Aborting.`
|
||||
)
|
||||
);
|
||||
|
||||
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,
|
||||
"Another email templating solution" as const
|
||||
]
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||
url: await (async () => {
|
||||
const { latestMajorTags } = await getSupportedDockerImageTags({
|
||||
buildContext
|
||||
if (emailThemeType === "Another email templating solution") {
|
||||
console.log(
|
||||
[
|
||||
"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`;
|
||||
})(),
|
||||
cacheDirPath: buildContext.cacheDirPath,
|
||||
fetchOptions: buildContext.fetchOptions,
|
||||
uniqueIdOfOnArchiveFile: "extractOnlyEmailTheme",
|
||||
onArchiveFile: async ({ fileRelativePath, writeFile }) => {
|
||||
const fileRelativePath_target = pathRelative(
|
||||
pathJoin("theme", "base", "email"),
|
||||
fileRelativePath
|
||||
);
|
||||
zParsedPackageJson.parse(parsedPackageJson);
|
||||
|
||||
if (fileRelativePath_target.startsWith("..")) {
|
||||
return;
|
||||
}
|
||||
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||
|
||||
await writeFile({ fileRelativePath: fileRelativePath_target });
|
||||
return parsedPackageJson;
|
||||
})();
|
||||
|
||||
addSyncExtensionsToPostinstallScript({
|
||||
parsedPackageJson,
|
||||
buildContext
|
||||
});
|
||||
|
||||
const moduleName = `@keycloakify/email-native`;
|
||||
|
||||
const [version] = ((): string[] => {
|
||||
const cmdOutput = child_process
|
||||
.execSync(`npm show ${moduleName} versions --json`)
|
||||
.toString("utf8")
|
||||
.trim();
|
||||
|
||||
const versions = JSON.parse(cmdOutput) as string | string[];
|
||||
|
||||
// NOTE: Bug in some older npm versions
|
||||
if (typeof versions === "string") {
|
||||
return [versions];
|
||||
}
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: extractedDirPath,
|
||||
destDirPath: emailThemeSrcDirPath
|
||||
});
|
||||
return versions;
|
||||
})()
|
||||
.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(
|
||||
themePropertyFilePath,
|
||||
Buffer.from(
|
||||
[
|
||||
`parent=base`,
|
||||
fs.readFileSync(themePropertyFilePath).toString("utf8")
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
)
|
||||
buildContext.packageJsonFilePath,
|
||||
Buffer.from(sourceCode, "utf8")
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`The \`${pathJoin(
|
||||
".",
|
||||
pathRelative(process.cwd(), emailThemeSrcDirPath)
|
||||
)}\` directory have been created.`
|
||||
);
|
||||
console.log("You can delete any file you don't modify.");
|
||||
await npmInstall({
|
||||
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
|
||||
});
|
||||
|
||||
console.log(chalk.green("Email theme initialized."));
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ import { readFileSync } from "fs";
|
||||
import { isInside } from "../../tools/isInside";
|
||||
import child_process from "child_process";
|
||||
import { rmSync } from "../../tools/fs.rmSync";
|
||||
import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
|
||||
import { existsAsync } from "../../tools/fs.existsAsync";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_generatePom & {
|
||||
@ -106,29 +105,55 @@ export async function buildJar(params: {
|
||||
}
|
||||
});
|
||||
|
||||
remove_account_v1_in_meta_inf: {
|
||||
if (!doesImplementAccountV1Theme) {
|
||||
// NOTE: We do not have account v1 anyway
|
||||
break remove_account_v1_in_meta_inf;
|
||||
}
|
||||
{
|
||||
const filePath = pathJoin(
|
||||
tmpResourcesDirPath,
|
||||
"META-INF",
|
||||
"keycloak-themes.json"
|
||||
);
|
||||
|
||||
if (keycloakAccountV1Version !== null) {
|
||||
// NOTE: No, we need to keep account-v1 in meta-inf
|
||||
break remove_account_v1_in_meta_inf;
|
||||
}
|
||||
await fs.mkdir(pathDirname(filePath));
|
||||
|
||||
writeMetaInfKeycloakThemes({
|
||||
resourcesDirPath: tmpResourcesDirPath,
|
||||
getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => {
|
||||
assert(metaInfKeycloakTheme !== undefined);
|
||||
await fs.writeFile(
|
||||
filePath,
|
||||
Buffer.from(
|
||||
JSON.stringify(
|
||||
{
|
||||
themes: await (async () => {
|
||||
const dirPath = pathJoin(tmpResourcesDirPath, "theme");
|
||||
|
||||
metaInfKeycloakTheme.themes = metaInfKeycloakTheme.themes.filter(
|
||||
({ name }) => name !== "account-v1"
|
||||
);
|
||||
const themeNames = (await fs.readdir(dirPath)).sort(
|
||||
(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: {
|
||||
@ -195,31 +220,39 @@ export async function buildJar(params: {
|
||||
);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
child_process.exec(
|
||||
`mvn clean install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`,
|
||||
{ cwd: keycloakifyBuildCacheDirPath },
|
||||
error => {
|
||||
if (error !== null) {
|
||||
console.error(
|
||||
`Build jar failed: ${JSON.stringify(
|
||||
{
|
||||
jarFileBasename,
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
{
|
||||
const mvnBuildCmd = `mvn clean install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`;
|
||||
|
||||
reject(error);
|
||||
return;
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
child_process.exec(
|
||||
mvnBuildCmd,
|
||||
{ cwd: keycloakifyBuildCacheDirPath },
|
||||
error => {
|
||||
if (error !== null) {
|
||||
console.error(
|
||||
[
|
||||
`Build jar failed: ${JSON.stringify(
|
||||
{
|
||||
jarFileBasename,
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}`,
|
||||
"Try running the following command to debug the issue (you are probably under a restricted network and you need to configure your proxy):",
|
||||
`cd ${keycloakifyBuildCacheDirPath} && ${mvnBuildCmd}`
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
)
|
||||
);
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await fs.rename(
|
||||
pathJoin(
|
||||
|
@ -190,7 +190,7 @@ function decodeHtmlEntities(htmlStr){
|
||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
|
||||
key == "loginAction" &&
|
||||
areSamePath(path, ["url"]) &&
|
||||
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(xKeycloakify.pageId) &&
|
||||
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl", "frontchannel-logout.ftl"]?seq_contains(xKeycloakify.pageId) &&
|
||||
!(auth?has_content && auth.showTryAnotherWayLink())
|
||||
) || (
|
||||
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
|
||||
|
@ -6,8 +6,7 @@ import {
|
||||
join as pathJoin,
|
||||
relative as pathRelative,
|
||||
dirname as pathDirname,
|
||||
extname as pathExtname,
|
||||
sep as pathSep
|
||||
basename as pathBasename
|
||||
} from "path";
|
||||
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
||||
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
||||
@ -31,16 +30,13 @@ import {
|
||||
type BuildContextLike as BuildContextLike_generateMessageProperties
|
||||
} from "./generateMessageProperties";
|
||||
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
|
||||
import {
|
||||
writeMetaInfKeycloakThemes,
|
||||
type MetaInfKeycloakTheme
|
||||
} from "../../shared/metaInfKeycloakThemes";
|
||||
import { objectEntries } from "tsafe/objectEntries";
|
||||
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
import propertiesParser from "properties-parser";
|
||||
import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed";
|
||||
import { listInstalledModules } from "../../tools/listInstalledModules";
|
||||
import { isInside } from "../../tools/isInside";
|
||||
import { id } from "tsafe/id";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
||||
BuildContextLike_generateMessageProperties & {
|
||||
@ -61,6 +57,8 @@ export async function generateResources(params: {
|
||||
buildContext: BuildContextLike;
|
||||
resourcesDirPath: string;
|
||||
}): Promise<void> {
|
||||
const start = Date.now();
|
||||
|
||||
const { resourcesDirPath, buildContext } = params;
|
||||
|
||||
const [themeName] = buildContext.themeNames;
|
||||
@ -78,12 +76,23 @@ export async function generateResources(params: {
|
||||
};
|
||||
|
||||
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) {
|
||||
if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
|
||||
continue;
|
||||
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
||||
let isNative: boolean;
|
||||
|
||||
{
|
||||
const v = buildContext.implementedThemeTypes[themeType];
|
||||
|
||||
if (!v.isImplemented && !v.isImplemented_native) {
|
||||
continue;
|
||||
}
|
||||
|
||||
isNative = !v.isImplemented && v.isImplemented_native;
|
||||
}
|
||||
|
||||
const getAccountThemeType = () => {
|
||||
@ -102,12 +111,18 @@ export async function generateResources(params: {
|
||||
return getAccountThemeType() === "Single-Page";
|
||||
case "admin":
|
||||
return true;
|
||||
case "email":
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
|
||||
|
||||
apply_replacers_and_move_to_theme_resources: {
|
||||
if (isNative) {
|
||||
break apply_replacers_and_move_to_theme_resources;
|
||||
}
|
||||
|
||||
const destDirPath = pathJoin(
|
||||
themeTypeDirPath,
|
||||
"resources",
|
||||
@ -191,59 +206,93 @@ export async function generateResources(params: {
|
||||
});
|
||||
}
|
||||
|
||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||
themeName,
|
||||
indexHtmlCode: fs
|
||||
.readFileSync(pathJoin(buildContext.projectBuildDirPath, "index.html"))
|
||||
.toString("utf8"),
|
||||
buildContext,
|
||||
keycloakifyVersion: readThisNpmPackageVersion(),
|
||||
themeType,
|
||||
fieldNames: isSpa
|
||||
? []
|
||||
: (assert(themeType !== "admin"),
|
||||
readFieldNameUsage({
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath,
|
||||
themeType
|
||||
}))
|
||||
});
|
||||
generate_ftl_files: {
|
||||
if (isNative) {
|
||||
break generate_ftl_files;
|
||||
}
|
||||
|
||||
[
|
||||
...(() => {
|
||||
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"];
|
||||
assert(themeType !== "email");
|
||||
|
||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||
themeName,
|
||||
indexHtmlCode: fs
|
||||
.readFileSync(
|
||||
pathJoin(buildContext.projectBuildDirPath, "index.html")
|
||||
)
|
||||
.toString("utf8"),
|
||||
buildContext,
|
||||
keycloakifyVersion: readThisNpmPackageVersion(),
|
||||
themeType,
|
||||
fieldNames: isSpa
|
||||
? []
|
||||
: (assert(themeType !== "admin"),
|
||||
readFieldNameUsage({
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath,
|
||||
themeType
|
||||
}))
|
||||
});
|
||||
|
||||
[
|
||||
...(() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return LOGIN_THEME_PAGE_IDS;
|
||||
case "account":
|
||||
return getAccountThemeType() === "Single-Page"
|
||||
? ["index.ftl"]
|
||||
: ACCOUNT_THEME_PAGE_IDS;
|
||||
case "admin":
|
||||
return ["index.ftl"];
|
||||
}
|
||||
})(),
|
||||
...(isSpa
|
||||
? []
|
||||
: readExtraPagesNames({
|
||||
themeType,
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath
|
||||
}))
|
||||
].forEach(pageId => {
|
||||
const { ftlCode } = generateFtlFilesCode({ pageId });
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(themeTypeDirPath, pageId),
|
||||
Buffer.from(ftlCode, "utf8")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
copy_native_theme: {
|
||||
if (!isNative) {
|
||||
break copy_native_theme;
|
||||
}
|
||||
|
||||
const dirPath = pathJoin(buildContext.themeSrcDirPath, themeType);
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: dirPath,
|
||||
destDirPath: getThemeTypeDirPath({ themeName, themeType }),
|
||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||
if (isInside({ dirPath: "messages", filePath: fileRelativePath })) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
})(),
|
||||
...(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;
|
||||
|
||||
i18n_multi_page: {
|
||||
if (isNative) {
|
||||
break i18n_multi_page;
|
||||
}
|
||||
|
||||
if (isSpa) {
|
||||
break i18n_multi_page;
|
||||
}
|
||||
|
||||
assert(themeType !== "admin");
|
||||
assert(themeType !== "admin" && themeType !== "email");
|
||||
|
||||
const wrap = generateMessageProperties({
|
||||
buildContext,
|
||||
@ -364,27 +413,24 @@ export async function generateResources(params: {
|
||||
);
|
||||
}
|
||||
|
||||
i18n_single_page: {
|
||||
if (!isSpa) {
|
||||
break i18n_single_page;
|
||||
i18n_for_spas_and_native: {
|
||||
if (!isSpa && !isNative) {
|
||||
break i18n_for_spas_and_native;
|
||||
}
|
||||
|
||||
if (isLegacyAccountSpa) {
|
||||
break i18n_single_page;
|
||||
break i18n_for_spas_and_native;
|
||||
}
|
||||
|
||||
assert(themeType === "account" || themeType === "admin");
|
||||
|
||||
const messagesDirPath_theme = pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
themeType,
|
||||
"i18n"
|
||||
isNative ? "messages" : "i18n"
|
||||
);
|
||||
|
||||
assert(
|
||||
fs.existsSync(messagesDirPath_theme),
|
||||
`${messagesDirPath_theme} is supposed to exist`
|
||||
);
|
||||
if (!fs.existsSync(messagesDirPath_theme)) {
|
||||
break i18n_for_spas_and_native;
|
||||
}
|
||||
|
||||
const propertiesByLang: Record<
|
||||
string,
|
||||
@ -524,6 +570,10 @@ export async function generateResources(params: {
|
||||
}
|
||||
|
||||
keycloak_static_resources: {
|
||||
if (isNative) {
|
||||
break keycloak_static_resources;
|
||||
}
|
||||
|
||||
if (isSpa) {
|
||||
break keycloak_static_resources;
|
||||
}
|
||||
@ -540,183 +590,167 @@ export async function generateResources(params: {
|
||||
});
|
||||
}
|
||||
|
||||
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>>(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 };
|
||||
bring_in_account_v1: {
|
||||
if (isNative) {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const themeName of buildContext.themeNames) {
|
||||
for (const [themeType, writeMessagePropertiesFiles] of objectEntries(
|
||||
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;
|
||||
if (themeType !== "account") {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
writeMessagePropertiesFiles({
|
||||
messageDirPath: pathJoin(
|
||||
getThemeTypeDirPath({ themeName, themeType }),
|
||||
"messages"
|
||||
),
|
||||
themeName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
modify_email_theme_per_variant: {
|
||||
if (!buildContext.implementedThemeTypes.email.isImplemented) {
|
||||
break modify_email_theme_per_variant;
|
||||
}
|
||||
assert(buildContext.implementedThemeTypes.account.isImplemented);
|
||||
|
||||
for (const themeName of buildContext.themeNames) {
|
||||
const emailThemeDirPath = getThemeTypeDirPath({
|
||||
themeName,
|
||||
themeType: "email"
|
||||
});
|
||||
if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: emailThemeDirPath,
|
||||
destDirPath: emailThemeDirPath,
|
||||
transformSourceCode: ({ filePath, sourceCode }) => {
|
||||
if (!filePath.endsWith(".ftl")) {
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
|
||||
return {
|
||||
modifiedSourceCode: Buffer.from(
|
||||
sourceCode
|
||||
.toString("utf8")
|
||||
.replace(/xKeycloakify\.themeName/g, `"${themeName}"`),
|
||||
"utf8"
|
||||
)
|
||||
};
|
||||
}
|
||||
srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "res", "account-v1"),
|
||||
destDirPath: getThemeTypeDirPath({
|
||||
themeName: "account-v1",
|
||||
themeType: "account"
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { dirname as pathDirname, relative as pathRelative, sep as pathSep } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildContext } from "../buildContext";
|
||||
import type { BuildContext } from "./buildContext";
|
||||
|
||||
export type BuildContextLike = {
|
||||
projectDirPath: string;
|
@ -45,12 +45,16 @@ export type BuildContext = {
|
||||
environmentVariables: { name: string; default: string }[];
|
||||
themeSrcDirPath: string;
|
||||
implementedThemeTypes: {
|
||||
login: { isImplemented: boolean };
|
||||
email: { isImplemented: boolean };
|
||||
login:
|
||||
| { isImplemented: true }
|
||||
| { isImplemented: false; isImplemented_native: boolean };
|
||||
email: { isImplemented: false; isImplemented_native: boolean };
|
||||
account:
|
||||
| { isImplemented: false }
|
||||
| { isImplemented: false; isImplemented_native: boolean }
|
||||
| { isImplemented: true; type: "Single-Page" | "Multi-Page" };
|
||||
admin: { isImplemented: boolean };
|
||||
admin:
|
||||
| { isImplemented: true }
|
||||
| { isImplemented: false; isImplemented_native: boolean };
|
||||
};
|
||||
packageJsonFilePath: string;
|
||||
bundler: "vite" | "webpack";
|
||||
@ -434,27 +438,68 @@ export function getBuildContext(params: {
|
||||
assert<Equals<typeof bundler, never>>(false);
|
||||
})();
|
||||
|
||||
const implementedThemeTypes: BuildContext["implementedThemeTypes"] = {
|
||||
login: {
|
||||
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "login"))
|
||||
},
|
||||
email: {
|
||||
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "email"))
|
||||
},
|
||||
account: (() => {
|
||||
if (buildOptions.accountThemeImplementation === "none") {
|
||||
return { isImplemented: false };
|
||||
}
|
||||
const implementedThemeTypes: BuildContext["implementedThemeTypes"] = (() => {
|
||||
const getIsNative = (dirPath: string) =>
|
||||
fs.existsSync(pathJoin(dirPath, "theme.properties"));
|
||||
|
||||
return {
|
||||
isImplemented: true,
|
||||
type: buildOptions.accountThemeImplementation
|
||||
};
|
||||
})(),
|
||||
admin: {
|
||||
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "admin"))
|
||||
}
|
||||
};
|
||||
return {
|
||||
login: (() => {
|
||||
const dirPath = pathJoin(themeSrcDirPath, "login");
|
||||
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
return { isImplemented: false, isImplemented_native: false };
|
||||
}
|
||||
|
||||
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 (
|
||||
implementedThemeTypes.account.isImplemented &&
|
||||
|
@ -13,13 +13,15 @@ import * as fs from "fs";
|
||||
|
||||
assert<Equals<ApiVersion, "v1">>();
|
||||
|
||||
export function maybeDelegateCommandToCustomHandler(params: {
|
||||
export async function maybeDelegateCommandToCustomHandler(params: {
|
||||
commandName: CommandName;
|
||||
buildContext: BuildContext;
|
||||
}): { hasBeenHandled: boolean } {
|
||||
}): Promise<{ hasBeenHandled: boolean }> {
|
||||
const { commandName, buildContext } = params;
|
||||
|
||||
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
|
||||
const nodeModulesBinDirPath = await getNodeModulesBinDirPath({
|
||||
packageJsonFilePath: buildContext.packageJsonFilePath
|
||||
});
|
||||
|
||||
if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) {
|
||||
return { hasBeenHandled: false };
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { assert, is, type Equals } from "tsafe/assert";
|
||||
import { id } from "tsafe/id";
|
||||
@ -7,8 +7,8 @@ import {
|
||||
addSyncExtensionsToPostinstallScript,
|
||||
type BuildContextLike as BuildContextLike_addSyncExtensionsToPostinstallScript
|
||||
} from "./addSyncExtensionsToPostinstallScript";
|
||||
import { getIsPrettierAvailable, runPrettier } from "../../tools/runPrettier";
|
||||
import { npmInstall } from "../../tools/npmInstall";
|
||||
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";
|
||||
@ -105,14 +105,21 @@ export async function initializeSpa(params: {
|
||||
|
||||
const moduleName = `@keycloakify/keycloak-${themeType}-ui`;
|
||||
|
||||
const version = (
|
||||
JSON.parse(
|
||||
child_process
|
||||
.execSync(`npm show ${moduleName} versions --json`)
|
||||
.toString("utf8")
|
||||
.trim()
|
||||
) as string[]
|
||||
)
|
||||
const version = ((): string[] => {
|
||||
const cmdOutput = child_process
|
||||
.execSync(`npm show ${moduleName} versions --json`)
|
||||
.toString("utf8")
|
||||
.trim();
|
||||
|
||||
const versions = JSON.parse(cmdOutput) as string | string[];
|
||||
|
||||
// NOTE: Bug in some older npm versions
|
||||
if (typeof versions === "string") {
|
||||
return [versions];
|
||||
}
|
||||
|
||||
return versions;
|
||||
})()
|
||||
.reverse()
|
||||
.filter(version => !version.includes("-"))
|
||||
.find(version =>
|
@ -1 +0,0 @@
|
||||
export * from "./initializeSpa";
|
@ -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")
|
||||
);
|
||||
}
|
@ -1,28 +1,20 @@
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { ParsedRealmJson } from "./ParsedRealmJson";
|
||||
import { getDefaultConfig } from "./defaultConfig";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import { objectKeys } from "tsafe/objectKeys";
|
||||
import { TEST_APP_URL } from "../../shared/constants";
|
||||
import { TEST_APP_URL, type ThemeType, THEME_TYPES } from "../../shared/constants";
|
||||
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: {
|
||||
parsedRealmJson: ParsedRealmJson;
|
||||
keycloakMajorVersionNumber: number;
|
||||
buildContext: BuildContextLike;
|
||||
parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
|
||||
}): {
|
||||
realmName: string;
|
||||
clientName: string;
|
||||
username: string;
|
||||
} {
|
||||
const { parsedRealmJson, keycloakMajorVersionNumber, buildContext } = params;
|
||||
const { parsedRealmJson, keycloakMajorVersionNumber, parsedKeycloakThemesJsonEntry } =
|
||||
params;
|
||||
|
||||
const { username } = addOrEditTestUser({
|
||||
parsedRealmJson,
|
||||
@ -38,8 +30,7 @@ export function prepareRealmConfig(params: {
|
||||
|
||||
enableCustomThemes({
|
||||
parsedRealmJson,
|
||||
themeName: buildContext.themeNames[0],
|
||||
implementedThemeTypes: buildContext.implementedThemeTypes
|
||||
parsedKeycloakThemesJsonEntry
|
||||
});
|
||||
|
||||
enable_custom_events_listeners: {
|
||||
@ -63,17 +54,15 @@ export function prepareRealmConfig(params: {
|
||||
|
||||
function enableCustomThemes(params: {
|
||||
parsedRealmJson: ParsedRealmJson;
|
||||
themeName: string;
|
||||
implementedThemeTypes: BuildContextLike["implementedThemeTypes"];
|
||||
parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
|
||||
}) {
|
||||
const { parsedRealmJson, themeName, implementedThemeTypes } = params;
|
||||
const { parsedRealmJson, parsedKeycloakThemesJsonEntry } = params;
|
||||
|
||||
for (const themeType of objectKeys(implementedThemeTypes)) {
|
||||
if (!implementedThemeTypes[themeType].isImplemented) {
|
||||
continue;
|
||||
}
|
||||
|
||||
parsedRealmJson[`${themeType}Theme` as const] = themeName;
|
||||
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
||||
parsedRealmJson[`${themeType}Theme` as const] =
|
||||
!parsedKeycloakThemesJsonEntry.types.includes(themeType)
|
||||
? ""
|
||||
: parsedKeycloakThemesJsonEntry.name;
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +101,6 @@ function addOrEditTestUser(params: {
|
||||
);
|
||||
|
||||
newUser.username = defaultUser_default.username;
|
||||
newUser.email = defaultUser_default.email;
|
||||
|
||||
delete_existing_password_credential_if_any: {
|
||||
const i = newUser.credentials.findIndex(
|
||||
|
@ -1,10 +1,7 @@
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { getDefaultConfig } from "./defaultConfig";
|
||||
import {
|
||||
prepareRealmConfig,
|
||||
type BuildContextLike as BuildContextLike_prepareRealmConfig
|
||||
} from "./prepareRealmConfig";
|
||||
import { prepareRealmConfig } from "./prepareRealmConfig";
|
||||
import * as fs from "fs";
|
||||
import {
|
||||
join as pathJoin,
|
||||
@ -24,18 +21,19 @@ import {
|
||||
} from "./dumpContainerConfig";
|
||||
import * as runExclusive from "run-exclusive";
|
||||
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
|
||||
import type { ThemeType } from "../../shared/constants";
|
||||
import chalk from "chalk";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_dumpContainerConfig &
|
||||
BuildContextLike_prepareRealmConfig & {
|
||||
projectDirPath: string;
|
||||
};
|
||||
export type BuildContextLike = BuildContextLike_dumpContainerConfig & {
|
||||
projectDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>;
|
||||
|
||||
export async function getRealmConfig(params: {
|
||||
keycloakMajorVersionNumber: number;
|
||||
realmJsonFilePath_userProvided: string | undefined;
|
||||
parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<{
|
||||
realmJsonFilePath: string;
|
||||
@ -44,8 +42,12 @@ export async function getRealmConfig(params: {
|
||||
username: string;
|
||||
onRealmConfigChange: () => Promise<void>;
|
||||
}> {
|
||||
const { keycloakMajorVersionNumber, realmJsonFilePath_userProvided, buildContext } =
|
||||
params;
|
||||
const {
|
||||
keycloakMajorVersionNumber,
|
||||
realmJsonFilePath_userProvided,
|
||||
parsedKeycloakThemesJsonEntry,
|
||||
buildContext
|
||||
} = params;
|
||||
|
||||
const realmJsonFilePath = pathJoin(
|
||||
buildContext.projectDirPath,
|
||||
@ -71,8 +73,8 @@ export async function getRealmConfig(params: {
|
||||
|
||||
const { clientName, realmName, username } = prepareRealmConfig({
|
||||
parsedRealmJson,
|
||||
buildContext,
|
||||
keycloakMajorVersionNumber
|
||||
keycloakMajorVersionNumber,
|
||||
parsedKeycloakThemesJsonEntry
|
||||
});
|
||||
|
||||
{
|
||||
|
@ -4,7 +4,8 @@ import {
|
||||
CONTAINER_NAME,
|
||||
KEYCLOAKIFY_SPA_DEV_SERVER_PORT,
|
||||
KEYCLOAKIFY_LOGIN_JAR_BASENAME,
|
||||
TEST_APP_URL
|
||||
TEST_APP_URL,
|
||||
ThemeType
|
||||
} from "../shared/constants";
|
||||
import { SemVer } from "../tools/SemVer";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
@ -34,6 +35,7 @@ import { startViteDevServer } from "./startViteDevServer";
|
||||
import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig";
|
||||
import { getSupportedDockerImageTags } from "./getSupportedDockerImageTags";
|
||||
import { getRealmConfig } from "./realmConfig";
|
||||
import { id } from "tsafe/id";
|
||||
|
||||
export async function command(params: {
|
||||
buildContext: BuildContext;
|
||||
@ -51,11 +53,17 @@ export async function command(params: {
|
||||
.execSync("docker --version", {
|
||||
stdio: ["ignore", "pipe", "ignore"]
|
||||
})
|
||||
?.toString("utf8");
|
||||
} catch {}
|
||||
.toString("utf8");
|
||||
} catch {
|
||||
commandOutput = "";
|
||||
}
|
||||
|
||||
if (commandOutput?.includes("Docker") || commandOutput?.includes("podman")) {
|
||||
break exit_if_docker_not_installed;
|
||||
commandOutput = commandOutput.trim().toLowerCase();
|
||||
|
||||
for (const term of ["docker", "podman"]) {
|
||||
if (commandOutput.includes(term)) {
|
||||
break exit_if_docker_not_installed;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
@ -270,32 +278,6 @@ export async function command(params: {
|
||||
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({
|
||||
buildContext
|
||||
@ -376,10 +358,24 @@ export async function command(params: {
|
||||
))
|
||||
];
|
||||
|
||||
let parsedKeycloakThemesJson = id<
|
||||
{ themes: { name: string; types: (ThemeType | "email")[] }[] } | undefined
|
||||
>(undefined);
|
||||
|
||||
async function extractThemeResourcesFromJar() {
|
||||
await extractArchive({
|
||||
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 })) {
|
||||
await writeFile({
|
||||
filePath: pathJoin(
|
||||
@ -401,6 +397,43 @@ export async function command(params: {
|
||||
|
||||
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(
|
||||
buildContext.cacheDirPath,
|
||||
pathBasename(jarFilePath)
|
||||
@ -754,6 +787,40 @@ export async function command(params: {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ignore_patternfly: {
|
||||
if (
|
||||
!isInside({
|
||||
dirPath: pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
"shared",
|
||||
"@patternfly"
|
||||
),
|
||||
filePath
|
||||
})
|
||||
) {
|
||||
break ignore_patternfly;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ignore_keycloak_ui_shared: {
|
||||
if (
|
||||
!isInside({
|
||||
dirPath: pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
"shared",
|
||||
"keycloak-ui-shared"
|
||||
),
|
||||
filePath
|
||||
})
|
||||
) {
|
||||
break ignore_keycloak_ui_shared;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Detected changes in ${filePath}`);
|
||||
|
@ -45,12 +45,12 @@ export async function getExtensionModuleFileSourceCodeReadyToBeCopied(params: {
|
||||
`This file has been claimed for ownership from ${extensionModuleName} version ${extensionModuleVersion}.`,
|
||||
`To relinquish ownership and restore this file to its original content, run the following command:`,
|
||||
``,
|
||||
`$ npx keycloakify own --path '${path}' --revert`
|
||||
`$ npx keycloakify own --path "${path}" --revert`
|
||||
]
|
||||
: [
|
||||
`WARNING: Before modifying this file, run the following command:`,
|
||||
``,
|
||||
`$ npx keycloakify own --path '${path}'`,
|
||||
`$ npx keycloakify own --path "${path}"`,
|
||||
``,
|
||||
`This file is provided by ${extensionModuleName} version ${extensionModuleVersion}.`,
|
||||
`It was copied into your repository by the postinstall script: \`keycloakify sync-extensions\`.`
|
||||
@ -99,6 +99,22 @@ function addCommentToSourceCode(params: {
|
||||
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")) {
|
||||
const comment = [
|
||||
`<!--`,
|
||||
|
@ -14,6 +14,8 @@ export function getAbsoluteAndInOsFormatPath(params: {
|
||||
|
||||
let pathOut = pathIsh;
|
||||
|
||||
pathOut = pathOut.replace(/^['"]/, "").replace(/['"]$/, "");
|
||||
|
||||
pathOut = pathOut.replace(/\//g, pathSep);
|
||||
|
||||
if (pathOut.startsWith("~")) {
|
||||
|
@ -1,10 +1,29 @@
|
||||
import { sep as pathSep } from "path";
|
||||
import { sep as pathSep, dirname as pathDirname, join as pathJoin } from "path";
|
||||
import { getThisCodebaseRootDirPath } from "./getThisCodebaseRootDirPath";
|
||||
import { getInstalledModuleDirPath } from "./getInstalledModuleDirPath";
|
||||
import { existsAsync } from "./fs.existsAsync";
|
||||
import { z } from "zod";
|
||||
import * as fs from "fs/promises";
|
||||
import { assert, is, type Equals } from "tsafe/assert";
|
||||
import { id } from "tsafe/id";
|
||||
|
||||
let cache: string | undefined = undefined;
|
||||
let cache_bestEffort: string | undefined = undefined;
|
||||
|
||||
export function getNodeModulesBinDirPath() {
|
||||
if (cache !== undefined) {
|
||||
return cache;
|
||||
/** NOTE: Careful, this function can fail when the binary
|
||||
* Used is not in the node_modules directory of the project
|
||||
* (for example when running tests with vscode extension we'll get
|
||||
* '/Users/dylan/.vscode/extensions/vitest.explorer-1.16.0/dist/worker.js'
|
||||
*
|
||||
* instead of
|
||||
* '/Users/joseph/.nvm/versions/node/v22.12.0/bin/node'
|
||||
* or
|
||||
* '/Users/joseph/github/keycloakify-starter/node_modules/.bin/vite'
|
||||
*
|
||||
* as the value of process.argv[1]
|
||||
*/
|
||||
function getNodeModulesBinDirPath_bestEffort() {
|
||||
if (cache_bestEffort !== undefined) {
|
||||
return cache_bestEffort;
|
||||
}
|
||||
|
||||
const binPath = process.argv[1];
|
||||
@ -30,9 +49,122 @@ export function getNodeModulesBinDirPath() {
|
||||
segments.unshift(segment);
|
||||
}
|
||||
|
||||
if (!foundNodeModules) {
|
||||
throw new Error(`Could not find node_modules in path ${binPath}`);
|
||||
}
|
||||
|
||||
const nodeModulesBinDirPath = segments.join(pathSep);
|
||||
|
||||
cache = nodeModulesBinDirPath;
|
||||
cache_bestEffort = nodeModulesBinDirPath;
|
||||
|
||||
return nodeModulesBinDirPath;
|
||||
}
|
||||
|
||||
let cache_withPackageJsonFileDirPath:
|
||||
| { packageJsonFilePath: string; nodeModulesBinDirPath: string }
|
||||
| undefined = undefined;
|
||||
|
||||
async function getNodeModulesBinDirPath_withPackageJsonFileDirPath(params: {
|
||||
packageJsonFilePath: string;
|
||||
}): Promise<string> {
|
||||
const { packageJsonFilePath } = params;
|
||||
|
||||
use_cache: {
|
||||
if (cache_withPackageJsonFileDirPath === undefined) {
|
||||
break use_cache;
|
||||
}
|
||||
|
||||
if (
|
||||
cache_withPackageJsonFileDirPath.packageJsonFilePath !== packageJsonFilePath
|
||||
) {
|
||||
cache_withPackageJsonFileDirPath = undefined;
|
||||
break use_cache;
|
||||
}
|
||||
|
||||
return cache_withPackageJsonFileDirPath.nodeModulesBinDirPath;
|
||||
}
|
||||
|
||||
// [...]node_modules/keycloakify
|
||||
const installedModuleDirPath = await getInstalledModuleDirPath({
|
||||
// Here it will always be "keycloakify" but since we are in tools/ we make something generic
|
||||
moduleName: await (async () => {
|
||||
type ParsedPackageJson = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
const zParsedPackageJson = (() => {
|
||||
type TargetType = ParsedPackageJson;
|
||||
|
||||
const zTargetType = z.object({
|
||||
name: z.string()
|
||||
});
|
||||
|
||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>;
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
|
||||
const parsedPackageJson = JSON.parse(
|
||||
(
|
||||
await fs.readFile(
|
||||
pathJoin(getThisCodebaseRootDirPath(), "package.json")
|
||||
)
|
||||
).toString("utf8")
|
||||
);
|
||||
|
||||
zParsedPackageJson.parse(parsedPackageJson);
|
||||
|
||||
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||
|
||||
return parsedPackageJson.name;
|
||||
})(),
|
||||
packageJsonDirPath: pathDirname(packageJsonFilePath)
|
||||
});
|
||||
|
||||
const segments = installedModuleDirPath.split(pathSep);
|
||||
|
||||
while (true) {
|
||||
const segment = segments.pop();
|
||||
|
||||
if (segment === undefined) {
|
||||
throw new Error(
|
||||
`Could not find .bin directory relative to ${packageJsonFilePath}`
|
||||
);
|
||||
}
|
||||
|
||||
if (segment !== "node_modules") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidate = pathJoin(segments.join(pathSep), segment, ".bin");
|
||||
|
||||
if (!(await existsAsync(candidate))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cache_withPackageJsonFileDirPath = {
|
||||
packageJsonFilePath,
|
||||
nodeModulesBinDirPath: candidate
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return cache_withPackageJsonFileDirPath.nodeModulesBinDirPath;
|
||||
}
|
||||
|
||||
export function getNodeModulesBinDirPath(params: {
|
||||
packageJsonFilePath: string;
|
||||
}): Promise<string>;
|
||||
export function getNodeModulesBinDirPath(params: {
|
||||
packageJsonFilePath: undefined;
|
||||
}): string;
|
||||
export function getNodeModulesBinDirPath(params: {
|
||||
packageJsonFilePath: string | undefined;
|
||||
}): string | Promise<string> {
|
||||
const { packageJsonFilePath } = params ?? {};
|
||||
|
||||
return packageJsonFilePath === undefined
|
||||
? getNodeModulesBinDirPath_bestEffort()
|
||||
: getNodeModulesBinDirPath_withPackageJsonFileDirPath({ packageJsonFilePath });
|
||||
}
|
||||
|
@ -15,7 +15,9 @@ export async function getIsPrettierAvailable(): Promise<boolean> {
|
||||
return getIsPrettierAvailable.cache;
|
||||
}
|
||||
|
||||
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
|
||||
const nodeModulesBinDirPath = getNodeModulesBinDirPath({
|
||||
packageJsonFilePath: undefined
|
||||
});
|
||||
|
||||
const prettierBinPath = pathJoin(nodeModulesBinDirPath, "prettier");
|
||||
|
||||
@ -51,7 +53,7 @@ export async function getPrettier(): Promise<PrettierAndConfigHash> {
|
||||
// We make sure to only do that when linking, otherwise we import properly.
|
||||
if (readThisNpmPackageVersion().startsWith("0.0.0")) {
|
||||
eval(
|
||||
`${symToStr({ prettier })} = require("${pathResolve(pathJoin(getNodeModulesBinDirPath(), "..", "prettier"))}")`
|
||||
`${symToStr({ prettier })} = require("${pathResolve(pathJoin(getNodeModulesBinDirPath({ packageJsonFilePath: undefined }), "..", "prettier"))}")`
|
||||
);
|
||||
|
||||
assert(!is<undefined>(prettier));
|
||||
@ -64,7 +66,7 @@ export async function getPrettier(): Promise<PrettierAndConfigHash> {
|
||||
|
||||
const configHash = await (async () => {
|
||||
const configFilePath = await prettier.resolveConfigFile(
|
||||
pathJoin(getNodeModulesBinDirPath(), "..")
|
||||
pathJoin(getNodeModulesBinDirPath({ packageJsonFilePath: undefined }), "..")
|
||||
);
|
||||
|
||||
if (configFilePath === null) {
|
||||
|
@ -19,7 +19,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
await command({ buildContext });
|
||||
}
|
||||
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
commandName: "update-kc-gen",
|
||||
buildContext
|
||||
});
|
||||
|
@ -769,6 +769,8 @@ export declare namespace Validators {
|
||||
export type PasswordPolicies = {
|
||||
/** The minimum length of the password */
|
||||
length?: number;
|
||||
/** The maximum length of the password */
|
||||
maxLength?: number;
|
||||
/** The minimum number of digits required in the password */
|
||||
digits?: number;
|
||||
/** The minimum number of lowercase characters required in the password */
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { JSX } from "keycloakify/tools/JSX";
|
||||
import { useEffect, useReducer, Fragment } from "react";
|
||||
import { useEffect, Fragment } from "react";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||
import type { KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import {
|
||||
useUserProfileForm,
|
||||
@ -89,7 +90,6 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps<
|
||||
{advancedMsg(attribute.annotations.inputHelperTextAfter)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{AfterField !== undefined && (
|
||||
<AfterField
|
||||
attribute={attribute}
|
||||
@ -106,6 +106,10 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps<
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
{/* See: https://github.com/keycloak/keycloak/issues/38029 */}
|
||||
{kcContext.locale !== undefined && formFieldStates.find(x => x.attribute.name === "locale") === undefined && (
|
||||
<input type="hidden" name="locale" value={i18n.currentLanguage.languageTag} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -249,15 +253,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
||||
|
||||
const { msgStr } = i18n;
|
||||
|
||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
||||
|
||||
useEffect(() => {
|
||||
const passwordInputElement = document.getElementById(passwordInputId);
|
||||
|
||||
assert(passwordInputElement instanceof HTMLInputElement);
|
||||
|
||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
||||
}, [isPasswordRevealed]);
|
||||
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||
|
||||
return (
|
||||
<div className={kcClsx("kcInputGroup")}>
|
||||
|
@ -509,6 +509,8 @@ function formStateSelector(params: { state: internal.State }): FormState {
|
||||
switch (error.source.name) {
|
||||
case "length":
|
||||
return hasLostFocusAtLeastOnce;
|
||||
case "maxLength":
|
||||
return hasLostFocusAtLeastOnce;
|
||||
case "digits":
|
||||
return hasLostFocusAtLeastOnce;
|
||||
case "lowerCase":
|
||||
@ -967,6 +969,34 @@ function createGetErrors(params: { kcContext: KcContextLike_useGetErrors }) {
|
||||
});
|
||||
}
|
||||
|
||||
check_password_policy_x: {
|
||||
const policyName = "maxLength";
|
||||
|
||||
const policy = passwordPolicies[policyName];
|
||||
|
||||
if (!policy) {
|
||||
break check_password_policy_x;
|
||||
}
|
||||
|
||||
const maxLength = policy;
|
||||
|
||||
if (value.length <= maxLength) {
|
||||
break check_password_policy_x;
|
||||
}
|
||||
|
||||
errors.push({
|
||||
advancedMsgArgs: [
|
||||
"invalidPasswordMaxLengthMessage" satisfies MessageKey_defaultSet,
|
||||
`${maxLength}`
|
||||
] as const,
|
||||
fieldIndex: undefined,
|
||||
source: {
|
||||
type: "passwordPolicy",
|
||||
name: policyName
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
check_password_policy_x: {
|
||||
const policyName = "digits";
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { JSX } from "keycloakify/tools/JSX";
|
||||
import { useState, useEffect, useReducer } from "react";
|
||||
import { useState } from "react";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
@ -200,15 +200,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
||||
|
||||
const { msgStr } = i18n;
|
||||
|
||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
||||
|
||||
useEffect(() => {
|
||||
const passwordInputElement = document.getElementById(passwordInputId);
|
||||
|
||||
assert(passwordInputElement instanceof HTMLInputElement);
|
||||
|
||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
||||
}, [isPasswordRevealed]);
|
||||
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||
|
||||
return (
|
||||
<div className={kcClsx("kcInputGroup")}>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import type { JSX } from "keycloakify/tools/JSX";
|
||||
import { useState, useEffect, useReducer } from "react";
|
||||
import { useState } from "react";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
@ -107,15 +107,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
||||
|
||||
const { msgStr } = i18n;
|
||||
|
||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
||||
|
||||
useEffect(() => {
|
||||
const passwordInputElement = document.getElementById(passwordInputId);
|
||||
|
||||
assert(passwordInputElement instanceof HTMLInputElement);
|
||||
|
||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
||||
}, [isPasswordRevealed]);
|
||||
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||
|
||||
return (
|
||||
<div className={kcClsx("kcInputGroup")}>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { JSX } from "keycloakify/tools/JSX";
|
||||
import { useEffect, useReducer } from "react";
|
||||
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
@ -146,15 +145,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
||||
|
||||
const { msgStr } = i18n;
|
||||
|
||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
||||
|
||||
useEffect(() => {
|
||||
const passwordInputElement = document.getElementById(passwordInputId);
|
||||
|
||||
assert(passwordInputElement instanceof HTMLInputElement);
|
||||
|
||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
||||
}, [isPasswordRevealed]);
|
||||
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||
|
||||
return (
|
||||
<div className={kcClsx("kcInputGroup")}>
|
||||
|
@ -98,7 +98,7 @@ export default function LoginUsername(props: PageProps<Extract<KcContext, { page
|
||||
defaultValue={login.username ?? ""}
|
||||
type="text"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
autoComplete="username"
|
||||
aria-invalid={messagesPerField.existsError("username")}
|
||||
/>
|
||||
{messagesPerField.existsError("username") && (
|
||||
|
45
src/tools/useIsPasswordRevealed.ts
Normal file
45
src/tools/useIsPasswordRevealed.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { useEffect, useReducer } from "react";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
|
||||
/**
|
||||
* Initially false, state that enables to dynamically control if
|
||||
* the type of a password input is "password" (false) or "text" (true).
|
||||
*/
|
||||
export function useIsPasswordRevealed(params: { passwordInputId: string }) {
|
||||
const { passwordInputId } = params;
|
||||
|
||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer(
|
||||
(isPasswordRevealed: boolean) => !isPasswordRevealed,
|
||||
false
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const passwordInputElement = document.getElementById(passwordInputId);
|
||||
|
||||
assert(passwordInputElement instanceof HTMLInputElement);
|
||||
|
||||
const type = isPasswordRevealed ? "text" : "password";
|
||||
|
||||
passwordInputElement.type = type;
|
||||
|
||||
const observer = new MutationObserver(mutations => {
|
||||
mutations.forEach(mutation => {
|
||||
if (mutation.attributeName !== "type") {
|
||||
return;
|
||||
}
|
||||
if (passwordInputElement.type === type) {
|
||||
return;
|
||||
}
|
||||
passwordInputElement.type = type;
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(passwordInputElement, { attributes: true });
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [isPasswordRevealed]);
|
||||
|
||||
return { isPasswordRevealed, toggleIsPasswordRevealed };
|
||||
}
|
Reference in New Issue
Block a user