Update prettier configuration

This commit is contained in:
Joseph Garrone 2024-05-20 15:48:51 +02:00
parent 7a89888d11
commit 22e7ff1424
138 changed files with 3681 additions and 2007 deletions

View File

@ -1,11 +1,24 @@
{
"printWidth": 150,
"printWidth": 90,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "preserve",
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid"
"arrowParens": "avoid",
"overrides": [
{
"files": "*.tsx",
"options": {
"printWidth": 150
}
},
{
"files": "useUserProfileForm.tsx",
"options": {
"printWidth": 150
}
}
]
}

View File

@ -107,7 +107,7 @@
"make-fetch-happen": "^11.0.3",
"patch-package": "^8.0.0",
"powerhooks": "^1.0.10",
"prettier": "^2.3.0",
"prettier": "^3.2.5",
"properties-parser": "^0.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",

View File

@ -10,7 +10,10 @@ console.log(chalk.cyan("Building Keycloakify..."));
const startTime = Date.now();
if (fs.existsSync(join("dist", "bin", "main.original.js"))) {
fs.renameSync(join("dist", "bin", "main.original.js"), join("dist", "bin", "main.js"));
fs.renameSync(
join("dist", "bin", "main.original.js"),
join("dist", "bin", "main.js")
);
fs.readdirSync(join("dist", "bin")).forEach(fileBasename => {
if (/[0-9]\.index.js/.test(fileBasename)) {
@ -28,55 +31,74 @@ if (!fs.readFileSync(join("dist", "bin", "main.js")).toString("utf8").includes("
run(`npx ncc build ${join("dist", "bin", "main.js")} -o ${join("dist", "ncc_out")}`);
transformCodebase({
"srcDirPath": join("dist", "ncc_out"),
"destDirPath": join("dist", "bin"),
"transformSourceCode": ({ fileRelativePath, sourceCode }) => {
srcDirPath: join("dist", "ncc_out"),
destDirPath: join("dist", "bin"),
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
if (fileRelativePath === "index.js") {
return {
"newFileName": "main.js",
"modifiedSourceCode": sourceCode
newFileName: "main.js",
modifiedSourceCode: sourceCode
};
}
return { "modifiedSourceCode": sourceCode };
return { modifiedSourceCode: sourceCode };
}
});
fs.rmSync(join("dist", "ncc_out"), { "recursive": true });
fs.rmSync(join("dist", "ncc_out"), { recursive: true });
patchDeprecatedBufferApiUsage(join("dist", "bin", "main.js"));
fs.chmodSync(
join("dist", "bin", "main.js"),
fs.statSync(join("dist", "bin", "main.js")).mode | fs.constants.S_IXUSR | fs.constants.S_IXGRP | fs.constants.S_IXOTH
fs.statSync(join("dist", "bin", "main.js")).mode |
fs.constants.S_IXUSR |
fs.constants.S_IXGRP |
fs.constants.S_IXOTH
);
run(`npx tsc -p ${join("src", "tsconfig.json")}`);
run(`npx tsc-alias -p ${join("src", "tsconfig.json")}`);
if (fs.existsSync(join("dist", "vite-plugin", "index.original.js"))) {
fs.renameSync(join("dist", "vite-plugin", "index.original.js"), join("dist", "vite-plugin", "index.js"));
fs.renameSync(
join("dist", "vite-plugin", "index.original.js"),
join("dist", "vite-plugin", "index.js")
);
}
run(`npx tsc -p ${join("src", "vite-plugin", "tsconfig.json")}`);
if (!fs.readFileSync(join("dist", "vite-plugin", "index.js")).toString("utf8").includes("ncc")) {
fs.cpSync(join("dist", "vite-plugin", "index.js"), join("dist", "vite-plugin", "index.original.js"));
if (
!fs
.readFileSync(join("dist", "vite-plugin", "index.js"))
.toString("utf8")
.includes("ncc")
) {
fs.cpSync(
join("dist", "vite-plugin", "index.js"),
join("dist", "vite-plugin", "index.original.js")
);
}
run(`npx ncc build ${join("dist", "vite-plugin", "index.js")} -o ${join("dist", "ncc_out")}`);
run(
`npx ncc build ${join("dist", "vite-plugin", "index.js")} -o ${join(
"dist",
"ncc_out"
)}`
);
transformCodebase({
"srcDirPath": join("dist", "ncc_out"),
"destDirPath": join("dist", "vite-plugin"),
"transformSourceCode": ({ fileRelativePath, sourceCode }) => {
srcDirPath: join("dist", "ncc_out"),
destDirPath: join("dist", "vite-plugin"),
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
assert(fileRelativePath === "index.js");
return { "modifiedSourceCode": sourceCode };
return { modifiedSourceCode: sourceCode };
}
});
fs.rmSync(join("dist", "ncc_out"), { "recursive": true });
fs.rmSync(join("dist", "ncc_out"), { recursive: true });
patchDeprecatedBufferApiUsage(join("dist", "vite-plugin", "index.js"));
@ -85,7 +107,7 @@ console.log(chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixe
function run(command: string) {
console.log(chalk.grey(`$ ${command}`));
child_process.execSync(command, { "stdio": "inherit" });
child_process.execSync(command, { stdio: "inherit" });
}
function patchDeprecatedBufferApiUsage(filePath: string) {

View File

@ -4,15 +4,34 @@ import { SemVer } from "../src/bin/tools/SemVer";
import { join as pathJoin, relative as pathRelative } from "path";
import chalk from "chalk";
run([`docker exec -it ${containerName}`, `/opt/keycloak/bin/kc.sh export`, `--dir /tmp`, `--realm myrealm`, `--users realm_file`].join(" "));
run(
[
`docker exec -it ${containerName}`,
`/opt/keycloak/bin/kc.sh export`,
`--dir /tmp`,
`--realm myrealm`,
`--users realm_file`
].join(" ")
);
const keycloakMajorVersionNumber = SemVer.parse(
child_process.execSync(`docker inspect --format '{{.Config.Image}}' ${containerName}`).toString("utf8").trim().split(":")[1]
child_process
.execSync(`docker inspect --format '{{.Config.Image}}' ${containerName}`)
.toString("utf8")
.trim()
.split(":")[1]
).major;
const targetFilePath = pathRelative(
process.cwd(),
pathJoin(__dirname, "..", "src", "bin", "start-keycloak", `myrealm-realm-${keycloakMajorVersionNumber}.json`)
pathJoin(
__dirname,
"..",
"src",
"bin",
"start-keycloak",
`myrealm-realm-${keycloakMajorVersionNumber}.json`
)
);
run(`docker cp ${containerName}:/tmp/myrealm-realm.json ${targetFilePath}`);
@ -22,5 +41,5 @@ console.log(`${chalk.green(`✓ Exported realm to`)} ${chalk.bold(targetFilePath
function run(command: string) {
console.log(chalk.grey(`$ ${command}`));
return child_process.execSync(command, { "stdio": "inherit" });
return child_process.execSync(command, { stdio: "inherit" });
}

View File

@ -1,6 +1,11 @@
import "minimal-polyfills/Object.fromEntries";
import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname, sep as pathSep } from "path";
import {
join as pathJoin,
relative as pathRelative,
dirname as pathDirname,
sep as pathSep
} from "path";
import { crawl } from "../src/bin/tools/crawl";
import { downloadKeycloakDefaultTheme } from "../src/bin/shared/downloadKeycloakDefaultTheme";
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
@ -19,18 +24,26 @@ async function main() {
const tmpDirPath = pathJoin(thisCodebaseRootDirPath, "tmp_xImOef9dOd44");
rmSync(tmpDirPath, { "recursive": true, "force": true });
rmSync(tmpDirPath, { recursive: true, force: true });
fs.mkdirSync(tmpDirPath);
fs.writeFileSync(pathJoin(tmpDirPath, ".gitignore"), Buffer.from("/*\n!.gitignore\n", "utf8"));
fs.writeFileSync(
pathJoin(tmpDirPath, ".gitignore"),
Buffer.from("/*\n!.gitignore\n", "utf8")
);
await downloadKeycloakDefaultTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
"buildOptions": {
"cacheDirPath": pathJoin(thisCodebaseRootDirPath, "node_modules", ".cache", "keycloakify"),
"npmWorkspaceRootDirPath": thisCodebaseRootDirPath
destDirPath: tmpDirPath,
buildOptions: {
cacheDirPath: pathJoin(
thisCodebaseRootDirPath,
"node_modules",
".cache",
"keycloakify"
),
npmWorkspaceRootDirPath: thisCodebaseRootDirPath
}
});
@ -40,11 +53,13 @@ async function main() {
{
const baseThemeDirPath = pathJoin(tmpDirPath, "base");
const re = new RegExp(`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`);
const re = new RegExp(
`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`
);
crawl({
"dirPath": baseThemeDirPath,
"returnedPathsType": "relative to dirPath"
dirPath: baseThemeDirPath,
returnedPathsType: "relative to dirPath"
}).forEach(filePath => {
const match = filePath.match(re);
@ -55,14 +70,21 @@ async function main() {
const [, typeOfPage, language] = match;
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries(
Object.entries(propertiesParser.parse(fs.readFileSync(pathJoin(baseThemeDirPath, filePath)).toString("utf8"))).map(
([key, value]: any) => [key === "locale_pt_BR" ? "locale_pt-BR" : key, value.replace(/''/g, "'")]
)
Object.entries(
propertiesParser.parse(
fs
.readFileSync(pathJoin(baseThemeDirPath, filePath))
.toString("utf8")
)
).map(([key, value]: any) => [
key === "locale_pt_BR" ? "locale_pt-BR" : key,
value.replace(/''/g, "'")
])
);
});
}
rmSync(tmpDirPath, { "recursive": true });
rmSync(tmpDirPath, { recursive: true });
Object.keys(record).forEach(themeType => {
const recordForPageType = record[themeType];
@ -71,19 +93,28 @@ async function main() {
return;
}
const baseMessagesDirPath = pathJoin(thisCodebaseRootDirPath, "src", themeType, "i18n", "baseMessages");
const baseMessagesDirPath = pathJoin(
thisCodebaseRootDirPath,
"src",
themeType,
"i18n",
"baseMessages"
);
const languages = Object.keys(recordForPageType);
const generatedFileHeader = [
`//This code was automatically generated by running ${pathRelative(thisCodebaseRootDirPath, __filename)}`,
`//This code was automatically generated by running ${pathRelative(
thisCodebaseRootDirPath,
__filename
)}`,
"//PLEASE DO NOT EDIT MANUALLY"
].join("\n");
languages.forEach(language => {
const filePath = pathJoin(baseMessagesDirPath, `${language}.ts`);
fs.mkdirSync(pathDirname(filePath), { "recursive": true });
fs.mkdirSync(pathDirname(filePath), { recursive: true });
fs.writeFileSync(
filePath,
@ -92,7 +123,11 @@ async function main() {
generatedFileHeader,
"",
"/* spell-checker: disable */",
`const messages= ${JSON.stringify(recordForPageType[language], null, 2)};`,
`const messages= ${JSON.stringify(
recordForPageType[language],
null,
2
)};`,
"",
"export default messages;",
"/* spell-checker: enable */"
@ -117,7 +152,10 @@ async function main() {
` case "en": return en;`,
...languages
.filter(language => language !== "en")
.map(language => ` case "${language}": return import("./${language}");`),
.map(
language =>
` case "${language}": return import("./${language}");`
),
' default: return { "default": {} };',
" }",
" })();",

View File

@ -10,7 +10,8 @@ import { chmod, stat } from "fs/promises";
const promises = Object.values<string>(bin).map(async scriptPath => {
const fullPath = pathJoin(thisCodebaseRootDirPath, scriptPath);
const oldMode = (await stat(fullPath)).mode;
const newMode = oldMode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH;
const newMode =
oldMode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH;
await chmod(fullPath, newMode);
});

View File

@ -13,20 +13,26 @@ fs.writeFileSync(
Buffer.from(
JSON.stringify(
(() => {
const packageJsonParsed = JSON.parse(fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8"));
const packageJsonParsed = JSON.parse(
fs
.readFileSync(pathJoin(rootDirPath, "package.json"))
.toString("utf8")
);
return {
...packageJsonParsed,
"main": packageJsonParsed["main"]?.replace(/^dist\//, ""),
"types": packageJsonParsed["types"]?.replace(/^dist\//, ""),
"module": packageJsonParsed["module"]?.replace(/^dist\//, ""),
"exports": !("exports" in packageJsonParsed)
main: packageJsonParsed["main"]?.replace(/^dist\//, ""),
types: packageJsonParsed["types"]?.replace(/^dist\//, ""),
module: packageJsonParsed["module"]?.replace(/^dist\//, ""),
exports: !("exports" in packageJsonParsed)
? undefined
: Object.fromEntries(
Object.entries(packageJsonParsed["exports"]).map(([key, value]) => [
key,
(value as string).replace(/^\.\/dist\//, "./")
])
Object.entries(packageJsonParsed["exports"]).map(
([key, value]) => [
key,
(value as string).replace(/^\.\/dist\//, "./")
]
)
)
};
})(),
@ -39,9 +45,9 @@ fs.writeFileSync(
const destSrcDirPath = pathJoin(rootDirPath, "dist", "src");
fs.rmSync(destSrcDirPath, { "recursive": true, "force": true });
fs.rmSync(destSrcDirPath, { recursive: true, force: true });
fs.cpSync(pathJoin(rootDirPath, "src"), destSrcDirPath, { "recursive": true });
fs.cpSync(pathJoin(rootDirPath, "src"), destSrcDirPath, { recursive: true });
const commonThirdPartyDeps = (() => {
// For example [ "@emotion" ] it's more convenient than
@ -53,7 +59,9 @@ const commonThirdPartyDeps = (() => {
...namespaceSingletonDependencies
.map(namespaceModuleName =>
fs
.readdirSync(pathJoin(rootDirPath, "node_modules", namespaceModuleName))
.readdirSync(
pathJoin(rootDirPath, "node_modules", namespaceModuleName)
)
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
)
.reduce((prev, curr) => [...prev, ...curr], []),
@ -63,21 +71,25 @@ const commonThirdPartyDeps = (() => {
const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home");
fs.rmSync(yarnGlobalDirPath, { "recursive": true, "force": true });
fs.rmSync(yarnGlobalDirPath, { recursive: true, force: true });
fs.mkdirSync(yarnGlobalDirPath);
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
const { targetModuleName, cwd } = params;
const cmd = ["yarn", "link", ...(targetModuleName !== undefined ? [targetModuleName] : ["--no-bin-links"])].join(" ");
const cmd = [
"yarn",
"link",
...(targetModuleName !== undefined ? [targetModuleName] : ["--no-bin-links"])
].join(" ");
console.log(`$ cd ${pathRelative(rootDirPath, cwd) || "."} && ${cmd}`);
execSync(cmd, {
cwd,
"env": {
env: {
...process.env,
"HOME": yarnGlobalDirPath
HOME: yarnGlobalDirPath
}
});
};
@ -93,7 +105,9 @@ const testAppPaths = (() => {
return testAppPath;
}
console.warn(`Skipping ${testAppName} since it cant be found here: ${testAppPath}`);
console.warn(
`Skipping ${testAppName} since it cant be found here: ${testAppPath}`
);
return undefined;
})
@ -105,7 +119,7 @@ if (testAppPaths.length === 0) {
process.exit(-1);
}
testAppPaths.forEach(testAppPath => execSync("yarn install", { "cwd": testAppPath }));
testAppPaths.forEach(testAppPath => execSync("yarn install", { cwd: testAppPath }));
console.log("=== Linking common dependencies ===");
@ -118,29 +132,37 @@ commonThirdPartyDeps.forEach(commonThirdPartyDep => {
console.log(`${current}/${total} ${commonThirdPartyDep}`);
const localInstallPath = pathJoin(
...[rootDirPath, "node_modules", ...(commonThirdPartyDep.startsWith("@") ? commonThirdPartyDep.split("/") : [commonThirdPartyDep])]
...[
rootDirPath,
"node_modules",
...(commonThirdPartyDep.startsWith("@")
? commonThirdPartyDep.split("/")
: [commonThirdPartyDep])
]
);
execYarnLink({ "cwd": localInstallPath });
execYarnLink({ cwd: localInstallPath });
});
commonThirdPartyDeps.forEach(commonThirdPartyDep =>
testAppPaths.forEach(testAppPath =>
execYarnLink({
"cwd": testAppPath,
"targetModuleName": commonThirdPartyDep
cwd: testAppPath,
targetModuleName: commonThirdPartyDep
})
)
);
console.log("=== Linking in house dependencies ===");
execYarnLink({ "cwd": pathJoin(rootDirPath, "dist") });
execYarnLink({ cwd: pathJoin(rootDirPath, "dist") });
testAppPaths.forEach(testAppPath =>
execYarnLink({
"cwd": testAppPath,
"targetModuleName": JSON.parse(fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8"))["name"]
cwd: testAppPath,
targetModuleName: JSON.parse(
fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8")
)["name"]
})
);

View File

@ -2,16 +2,19 @@ import * as child_process from "child_process";
import * as fs from "fs";
import { join } from "path";
fs.rmSync("node_modules", { "recursive": true, "force": true });
fs.rmSync("dist", { "recursive": true, "force": true });
fs.rmSync(".yarn_home", { "recursive": true, "force": true });
fs.rmSync("node_modules", { recursive: true, force: true });
fs.rmSync("dist", { recursive: true, force: true });
fs.rmSync(".yarn_home", { recursive: true, force: true });
run("yarn install");
run("yarn build");
fs.rmSync(join("..", "keycloakify-starter", "node_modules"), { "recursive": true, "force": true });
fs.rmSync(join("..", "keycloakify-starter", "node_modules"), {
recursive: true,
force: true
});
run("yarn install", { "cwd": join("..", "keycloakify-starter") });
run("yarn install", { cwd: join("..", "keycloakify-starter") });
run(`npx ts-node --skipProject ${join("scripts", "link-in-app.ts")} keycloakify-starter`);
@ -20,5 +23,5 @@ run(`npx chokidar '${join("src", "**", "*")}' -c 'yarn build'`);
function run(command: string, options?: { cwd: string }) {
console.log(`$ ${command}`);
child_process.execSync(command, { "stdio": "inherit", ...options });
child_process.execSync(command, { stdio: "inherit", ...options });
}

View File

@ -1,4 +1,7 @@
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "keycloakify/bin/shared/constants";
import {
nameOfTheGlobal,
basenameOfTheKeycloakifyResourcesDir
} from "keycloakify/bin/shared/constants";
import { assert } from "tsafe/assert";
/**

View File

@ -24,13 +24,13 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
}, []);
useSetClassName({
"qualifiedName": "html",
"className": getClassName("kcHtmlClass")
qualifiedName: "html",
className: getClassName("kcHtmlClass")
});
useSetClassName({
"qualifiedName": "body",
"className": clsx("admin-console", "user", getClassName("kcBodyClass"))
qualifiedName: "body",
className: clsx("admin-console", "user", getClassName("kcBodyClass"))
});
useEffect(() => {
@ -46,7 +46,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
}, []);
const { areAllStyleSheetsLoaded } = useInsertLinkTags({
"hrefs": !doUseDefaultCss
hrefs: !doUseDefaultCss
? []
: [
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,

View File

@ -2,7 +2,10 @@ import type { ReactNode } from "react";
import type { KcContext } from "./kcContext";
import type { I18n } from "./i18n";
export type TemplateProps<KcContext extends KcContext.Common, I18nExtended extends I18n> = {
export type TemplateProps<
KcContext extends KcContext.Common,
I18nExtended extends I18n
> = {
kcContext: KcContext;
i18n: I18nExtended;
doUseDefaultCss: boolean;

View File

@ -91,19 +91,19 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
setI18n({
...createI18nTranslationFunctions({
"fallbackMessages": {
fallbackMessages: {
...fallbackMessages,
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
...(extraMessages[fallbackLanguageTag] ?? {})
} as any,
"messages": {
messages: {
...(await getMessages(currentLanguageTag)),
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
...(extraMessages[currentLanguageTag] ?? {})
} as any
}),
currentLanguageTag,
"getChangeLocalUrl": newLanguageTag => {
getChangeLocalUrl: newLanguageTag => {
const { locale } = kcContext;
assert(locale !== undefined, "Internationalization not enabled");
@ -114,7 +114,7 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
return targetSupportedLocale.url;
},
"labelBySupportedLanguageTag": Object.fromEntries(
labelBySupportedLanguageTag: Object.fromEntries(
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
)
});
@ -170,7 +170,7 @@ function createI18nTranslationFunctions<MessageKey extends string>(params: {
})();
return doRenderMarkdown ? (
<Markdown allowDangerousHtml renderers={{ "paragraph": "span" }}>
<Markdown allowDangerousHtml renderers={{ paragraph: "span" }}>
{messageWithArgsInjectedIfAny}
</Markdown>
) : (
@ -186,7 +186,7 @@ function createI18nTranslationFunctions<MessageKey extends string>(params: {
const keyUnwrappedFromCurlyBraces = match === null ? key : match[1];
const out = resolveMsg({
"key": keyUnwrappedFromCurlyBraces,
key: keyUnwrappedFromCurlyBraces,
args,
doRenderMarkdown
});
@ -195,36 +195,46 @@ function createI18nTranslationFunctions<MessageKey extends string>(params: {
}
return {
"msgStr": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": false }) as string,
"msg": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": true }) as JSX.Element,
"advancedMsg": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": true }) as JSX.Element,
"advancedMsgStr": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": false }) as string
msgStr: (key, ...args) => resolveMsg({ key, args, doRenderMarkdown: false }) as string,
msg: (key, ...args) => resolveMsg({ key, args, doRenderMarkdown: true }) as JSX.Element,
advancedMsg: (key, ...args) =>
resolveMsgAdvanced({
key,
args,
doRenderMarkdown: true
}) as JSX.Element,
advancedMsgStr: (key, ...args) =>
resolveMsgAdvanced({
key,
args,
doRenderMarkdown: false
}) as string
};
}
const keycloakifyExtraMessages = {
"en": {
"shouldBeEqual": "{0} should be equal to {1}",
"shouldBeDifferent": "{0} should be different to {1}",
"shouldMatchPattern": "Pattern should match: `/{0}/`",
"mustBeAnInteger": "Must be an integer",
"notAValidOption": "Not a valid option",
"newPasswordSameAsOld": "New password must be different from the old one",
"passwordConfirmNotMatch": "Password confirmation does not match"
en: {
shouldBeEqual: "{0} should be equal to {1}",
shouldBeDifferent: "{0} should be different to {1}",
shouldMatchPattern: "Pattern should match: `/{0}/`",
mustBeAnInteger: "Must be an integer",
notAValidOption: "Not a valid option",
newPasswordSameAsOld: "New password must be different from the old one",
passwordConfirmNotMatch: "Password confirmation does not match"
},
"fr": {
fr: {
/* spell-checker: disable */
"shouldBeEqual": "{0} doit être égal à {1}",
"shouldBeDifferent": "{0} doit être différent de {1}",
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
"mustBeAnInteger": "Doit être un nombre entier",
"notAValidOption": "N'est pas une option valide",
shouldBeEqual: "{0} doit être égal à {1}",
shouldBeDifferent: "{0} doit être différent de {1}",
shouldMatchPattern: "Dois respecter le schéma: `/{0}/`",
mustBeAnInteger: "Doit être un nombre entier",
notAValidOption: "N'est pas une option valide",
"logoutConfirmTitle": "Déconnexion",
"logoutConfirmHeader": "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
"doLogout": "Se déconnecter",
"newPasswordSameAsOld": "Le nouveau mot de passe doit être différent de l'ancien",
"passwordConfirmNotMatch": "La confirmation du mot de passe ne correspond pas"
logoutConfirmTitle: "Déconnexion",
logoutConfirmHeader: "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
doLogout: "Se déconnecter",
newPasswordSameAsOld: "Le nouveau mot de passe doit être différent de l'ancien",
passwordConfirmNotMatch: "La confirmation du mot de passe ne correspond pas"
/* spell-checker: enable */
}
};

View File

@ -68,7 +68,10 @@ export declare namespace KcContext {
* @param text to return
* @return text if message exists for given field, else undefined
*/
printIfExists: <T extends string>(fieldName: string, text: T) => T | undefined;
printIfExists: <T extends string>(
fieldName: string,
text: T
) => T | undefined;
/**
* Check if exists error message for given fields
*

View File

@ -4,17 +4,28 @@ import { isStorybook } from "keycloakify/lib/isStorybook";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { symToStr } from "tsafe/symToStr";
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
import {
kcContextMocks,
kcContextCommonMock
} from "keycloakify/account/kcContext/kcContextMocks";
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
export function createGetKcContext<
KcContextExtension extends { pageId: string } = never
>(params?: {
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
mockProperties?: Record<string, string>;
}) {
const { mockData, mockProperties } = params ?? {};
function getKcContext<PageId extends ExtendKcContext<KcContextExtension>["pageId"] | undefined = undefined>(params?: {
function getKcContext<
PageId extends
| ExtendKcContext<KcContextExtension>["pageId"]
| undefined = undefined
>(params?: {
mockPageId?: PageId;
storyPartialKcContext?: DeepPartial<Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>>;
storyPartialKcContext?: DeepPartial<
Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>
>;
}): {
kcContext: PageId extends undefined
? ExtendKcContext<KcContextExtension> | undefined
@ -32,34 +43,46 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
break warn_that_mock_is_enbaled;
}
console.log(`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`, "background: red; color: yellow; font-size: medium");
console.log(
`%cKeycloakify: ${symToStr({
mockPageId
})} set to ${mockPageId}.`,
"background: red; color: yellow; font-size: medium"
);
}
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
const kcContextDefaultMock = kcContextMocks.find(
({ pageId }) => pageId === mockPageId
);
const partialKcContextCustomMock = (() => {
const out: DeepPartial<ExtendKcContext<KcContextExtension>> = {};
const mockDataPick = mockData?.find(({ pageId }) => pageId === mockPageId);
const mockDataPick = mockData?.find(
({ pageId }) => pageId === mockPageId
);
if (mockDataPick !== undefined) {
deepAssign({
"target": out,
"source": mockDataPick
target: out,
source: mockDataPick
});
}
if (storyPartialKcContext !== undefined) {
deepAssign({
"target": out,
"source": storyPartialKcContext
target: out,
source: storyPartialKcContext
});
}
return Object.keys(out).length === 0 ? undefined : out;
})();
if (kcContextDefaultMock === undefined && partialKcContextCustomMock === undefined) {
if (
kcContextDefaultMock === undefined &&
partialKcContextCustomMock === undefined
) {
console.warn(
[
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
@ -72,21 +95,24 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
const kcContext: any = {};
deepAssign({
"target": kcContext,
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
target: kcContext,
source:
kcContextDefaultMock !== undefined
? kcContextDefaultMock
: { pageId: mockPageId, ...kcContextCommonMock }
});
if (partialKcContextCustomMock !== undefined) {
deepAssign({
"target": kcContext,
"source": partialKcContextCustomMock
target: kcContext,
source: partialKcContextCustomMock
});
}
if (mockProperties !== undefined) {
deepAssign({
"target": kcContext.properties,
"source": mockProperties
target: kcContext.properties,
source: mockProperties
});
}
@ -94,14 +120,14 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
}
if (realKcContext === undefined) {
return { "kcContext": undefined as any };
return { kcContext: undefined as any };
}
if (realKcContext.themeType !== "account") {
return { "kcContext": undefined as any };
return { kcContext: undefined as any };
}
return { "kcContext": realKcContext as any };
return { kcContext: realKcContext as any };
}
return { getKcContext };

View File

@ -5,7 +5,9 @@ import { createGetKcContext } from "./createGetKcContext";
/** NOTE: We now recommend using createGetKcContext instead of this function to make storybook integration easier
* See: https://github.com/keycloakify/keycloakify-starter/blob/main/src/keycloak-theme/account/kcContext.ts
*/
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
export function getKcContext<
KcContextExtension extends { pageId: string } = never
>(params?: {
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {

View File

@ -2,10 +2,14 @@ import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminati
import { nameOfTheGlobal } from "keycloakify/bin/shared/constants";
import type { KcContext } from "./KcContext";
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [KcContextExtension] extends [never]
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [
KcContextExtension
] extends [never]
? KcContext
: AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>;
export function getKcContextFromWindow<KcContextExtension extends { pageId: string } = never>(): ExtendKcContext<KcContextExtension> | undefined {
export function getKcContextFromWindow<
KcContextExtension extends { pageId: string } = never
>(): ExtendKcContext<KcContextExtension> | undefined {
return typeof window === "undefined" ? undefined : (window as any)[nameOfTheGlobal];
}

View File

@ -7,254 +7,258 @@ import { BASE_URL } from "keycloakify/lib/BASE_URL";
const resourcesPath = `${BASE_URL}${keycloak_resources}/account/resources`;
export const kcContextCommonMock: KcContext.Common = {
"themeVersion": "0.0.0",
"keycloakifyVersion": "0.0.0",
"themeType": "account",
"themeName": "my-theme-name",
"url": {
themeVersion: "0.0.0",
keycloakifyVersion: "0.0.0",
themeType: "account",
themeName: "my-theme-name",
url: {
resourcesPath,
"resourcesCommonPath": `${resourcesPath}/${resources_common}`,
"resourceUrl": "#",
"accountUrl": "#",
"applicationsUrl": "#",
"logoutUrl": "#",
"getLogoutUrl": () => "#",
"logUrl": "#",
"passwordUrl": "#",
"sessionsUrl": "#",
"socialUrl": "#",
"totpUrl": "#"
resourcesCommonPath: `${resourcesPath}/${resources_common}`,
resourceUrl: "#",
accountUrl: "#",
applicationsUrl: "#",
logoutUrl: "#",
getLogoutUrl: () => "#",
logUrl: "#",
passwordUrl: "#",
sessionsUrl: "#",
socialUrl: "#",
totpUrl: "#"
},
"realm": {
"internationalizationEnabled": true,
"userManagedAccessAllowed": true
realm: {
internationalizationEnabled: true,
userManagedAccessAllowed: true
},
"messagesPerField": {
"printIfExists": () => {
messagesPerField: {
printIfExists: () => {
return undefined;
},
"existsError": () => false,
"get": key => `Fake error for ${key}`,
"exists": () => false
existsError: () => false,
get: key => `Fake error for ${key}`,
exists: () => false
},
"locale": {
"supported": [
locale: {
supported: [
/* spell-checker: disable */
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
"label": "Deutsch",
"languageTag": "de"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
label: "Deutsch",
languageTag: "de"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
"label": "Norsk",
"languageTag": "no"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
label: "Norsk",
languageTag: "no"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
"label": "Русский",
"languageTag": "ru"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
label: "Русский",
languageTag: "ru"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
"label": "Svenska",
"languageTag": "sv"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
label: "Svenska",
languageTag: "sv"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
"label": "Português (Brasil)",
"languageTag": "pt-BR"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
label: "Português (Brasil)",
languageTag: "pt-BR"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
"label": "Lietuvių",
"languageTag": "lt"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
label: "Lietuvių",
languageTag: "lt"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
"label": "English",
"languageTag": "en"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
label: "English",
languageTag: "en"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
"label": "Italiano",
"languageTag": "it"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
label: "Italiano",
languageTag: "it"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
"label": "Français",
"languageTag": "fr"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
label: "Français",
languageTag: "fr"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
"label": "中文简体",
"languageTag": "zh-CN"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
label: "中文简体",
languageTag: "zh-CN"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
"label": "Español",
"languageTag": "es"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
label: "Español",
languageTag: "es"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
"label": "Čeština",
"languageTag": "cs"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
label: "Čeština",
languageTag: "cs"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
"label": "日本語",
"languageTag": "ja"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
label: "日本語",
languageTag: "ja"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
"label": "Slovenčina",
"languageTag": "sk"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
label: "Slovenčina",
languageTag: "sk"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
"label": "Polski",
"languageTag": "pl"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
label: "Polski",
languageTag: "pl"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
"label": "Català",
"languageTag": "ca"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
label: "Català",
languageTag: "ca"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
"label": "Nederlands",
"languageTag": "nl"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
label: "Nederlands",
languageTag: "nl"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
"label": "Türkçe",
"languageTag": "tr"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
label: "Türkçe",
languageTag: "tr"
}
/* spell-checker: enable */
],
"currentLanguageTag": "en"
currentLanguageTag: "en"
},
"features": {
"authorization": true,
"identityFederation": true,
"log": true,
"passwordUpdateSupported": true
features: {
authorization: true,
identityFederation: true,
log: true,
passwordUpdateSupported: true
},
"referrer": undefined,
"account": {
"firstName": "john",
"lastName": "doe",
"email": "john.doe@code.gouv.fr",
"username": "doe_j"
referrer: undefined,
account: {
firstName: "john",
lastName: "doe",
email: "john.doe@code.gouv.fr",
username: "doe_j"
},
"properties": {
"parent": "account-v1",
"kcButtonLargeClass": "btn-lg",
"locales": "ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
"kcButtonPrimaryClass": "btn-primary",
"accountResourceProvider": "account-v1",
"styles":
"css/account.css img/icon-sidebar-active.png img/logo.png resources-common/node_modules/patternfly/dist/css/patternfly.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css",
"kcButtonClass": "btn",
"kcButtonDefaultClass": "btn-default"
properties: {
parent: "account-v1",
kcButtonLargeClass: "btn-lg",
locales:
"ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
kcButtonPrimaryClass: "btn-primary",
accountResourceProvider: "account-v1",
styles: "css/account.css img/icon-sidebar-active.png img/logo.png resources-common/node_modules/patternfly/dist/css/patternfly.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css",
kcButtonClass: "btn",
kcButtonDefaultClass: "btn-default"
}
};
export const kcContextMocks: KcContext[] = [
id<KcContext.Password>({
...kcContextCommonMock,
"pageId": "password.ftl",
"password": {
"passwordSet": true
pageId: "password.ftl",
password: {
passwordSet: true
},
"stateChecker": "state checker"
stateChecker: "state checker"
}),
id<KcContext.Account>({
...kcContextCommonMock,
"pageId": "account.ftl",
"url": {
pageId: "account.ftl",
url: {
...kcContextCommonMock.url,
"referrerURI": "#",
"accountUrl": "#"
referrerURI: "#",
accountUrl: "#"
},
"realm": {
realm: {
...kcContextCommonMock.realm,
"registrationEmailAsUsername": true,
"editUsernameAllowed": true
registrationEmailAsUsername: true,
editUsernameAllowed: true
},
"stateChecker": ""
stateChecker: ""
}),
id<KcContext.Sessions>({
...kcContextCommonMock,
"pageId": "sessions.ftl",
"sessions": {
"sessions": [
pageId: "sessions.ftl",
sessions: {
sessions: [
{
"ipAddress": "127.0.0.1",
"started": new Date().toString(),
"lastAccess": new Date().toString(),
"expires": new Date().toString(),
"clients": ["Chrome", "Firefox"],
"id": "f8951177-817d-4a70-9c02-86d3c170fe51"
ipAddress: "127.0.0.1",
started: new Date().toString(),
lastAccess: new Date().toString(),
expires: new Date().toString(),
clients: ["Chrome", "Firefox"],
id: "f8951177-817d-4a70-9c02-86d3c170fe51"
}
]
},
"stateChecker": "g6WB1FaYnKotTkiy7ZrlxvFztSqS0U8jvHsOOOb2z4g"
stateChecker: "g6WB1FaYnKotTkiy7ZrlxvFztSqS0U8jvHsOOOb2z4g"
}),
id<KcContext.Totp>({
...kcContextCommonMock,
"pageId": "totp.ftl",
"totp": {
"enabled": true,
"totpSecretEncoded": "KVVF G2BY N4YX S6LB IUYT K2LH IFYE 4SBV",
"qrUrl": "#",
"totpSecretQrCode":
pageId: "totp.ftl",
totp: {
enabled: true,
totpSecretEncoded: "KVVF G2BY N4YX S6LB IUYT K2LH IFYE 4SBV",
qrUrl: "#",
totpSecretQrCode:
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACM0lEQVR4Xu3OIZJgOQwDUDFd2UxiurLAVnnbHw4YGDKtSiWOn4Gxf81//7r/+q8b4HfLGBZDK9d85NmNR+sB42sXvOYrN5P1DcgYYFTGfOlbzE8gzwy3euweGizw7cfdl34/GRhlkxjKNV+5AebPXPORX1JuB9x8ZfbyyD2y1krWAKsbMq1HnqQDaLfa77p4+MqvzEGSqvSAD/2IHW2yHaigR9tX3m8dDIYGcNf3f+gDpVBZbZU77zyJ6Rlcy+qoTMG887KAPD9hsh6a1Sv3gJUHGHUAxSMzj7zqDDe7Phmt2eG+8UsMxjRGm816MAO+8VMl1R1jGHOrZB/5Zo/WXAPgxixm9Mo96vDGrM1eOto8c4Ax4wF437mifOXlpiPzCnN7Y9l95NnEMxgMY9AAGA8fucH14Y1aVb6N/cqrmyh0BVht7k1e+bU8LK0Cg5vmVq9c5vHIjOfqxDIfeTraNVTwewa4wVe+SW5N+uP1qACeudUZbqGOfA6VZV750Noq2Xx3kpveV44ZelSV1V7KFHzkWyVrrlUwG0Pl9pWnoy3vsQoME6vKI69i5osVgwWzHT7zjmJtMcNUSVn1oYMd7ZodbgowZl45VG0uVuLPUr1yc79uaQBag/mqR34xhlWyHm1prplHboCWdZ4TeZjsK8+dI+jbz1C5hl65mcpgB5dhcj8+dGO+0Ko68+lD37JDD83dpDLzzK+TrQyaVwGj6pUboGV+7+AyN8An/pf84/7rv/4/1l4OCc/1BYMAAAAASUVORK5CYII=",
"manualUrl": "#",
"totpSecret": "G4nsI8lQagRMUchH8jEG",
"otpCredentials": [],
"supportedApplications": ["totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName"],
"policy": {
"algorithm": "HmacSHA1",
"digits": 6,
"lookAheadWindow": 1,
"type": "totp",
"period": 30
manualUrl: "#",
totpSecret: "G4nsI8lQagRMUchH8jEG",
otpCredentials: [],
supportedApplications: [
"totpAppFreeOTPName",
"totpAppMicrosoftAuthenticatorName",
"totpAppGoogleName"
],
policy: {
algorithm: "HmacSHA1",
digits: 6,
lookAheadWindow: 1,
type: "totp",
period: 30
}
},
"mode": "qr",
"isAppInitiatedAction": false,
"stateChecker": ""
mode: "qr",
isAppInitiatedAction: false,
stateChecker: ""
}),
id<KcContext.Log>({
...kcContextCommonMock,
"pageId": "log.ftl",
"log": {
"events": [
pageId: "log.ftl",
log: {
events: [
{
"date": "2/21/2024, 1:28:39 PM",
"event": "login",
"ipAddress": "172.17.0.1",
"client": "security-admin-console",
"details": [{ key: "openid-connect", value: "admin" }]
date: "2/21/2024, 1:28:39 PM",
event: "login",
ipAddress: "172.17.0.1",
client: "security-admin-console",
details: [{ key: "openid-connect", value: "admin" }]
}
]
}
}),
id<KcContext.FederatedIdentity>({
...kcContextCommonMock,
"stateChecker": "",
"pageId": "federatedIdentity.ftl",
"federatedIdentity": {
"identities": [
stateChecker: "",
pageId: "federatedIdentity.ftl",
federatedIdentity: {
identities: [
{
"providerId": "keycloak-oidc",
"displayName": "keycloak-oidc",
"userName": "John",
"connected": true
providerId: "keycloak-oidc",
displayName: "keycloak-oidc",
userName: "John",
connected: true
}
],
"removeLinkPossible": true
removeLinkPossible: true
}
})
];

View File

@ -2,19 +2,20 @@ import { createUseClassName } from "keycloakify/lib/useGetClassName";
import type { ClassKey } from "keycloakify/account/TemplateProps";
export const { useGetClassName } = createUseClassName<ClassKey>({
"defaultClasses": {
"kcHtmlClass": undefined,
"kcBodyClass": undefined,
"kcButtonClass": "btn",
"kcContentWrapperClass": "row",
"kcButtonPrimaryClass": "btn-primary",
"kcButtonLargeClass": "btn-lg",
"kcButtonDefaultClass": "btn-default",
"kcFormClass": "form-horizontal",
"kcFormGroupClass": "form-group",
"kcInputWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
"kcLabelClass": "control-label",
"kcInputClass": "form-control",
"kcInputErrorMessageClass": "pf-c-form__helper-text pf-m-error required kc-feedback-text"
defaultClasses: {
kcHtmlClass: undefined,
kcBodyClass: undefined,
kcButtonClass: "btn",
kcContentWrapperClass: "row",
kcButtonPrimaryClass: "btn-primary",
kcButtonLargeClass: "btn-lg",
kcButtonDefaultClass: "btn-default",
kcFormClass: "form-horizontal",
kcFormGroupClass: "form-group",
kcInputWrapperClass: "col-xs-12 col-sm-12 col-md-12 col-lg-12",
kcLabelClass: "control-label",
kcInputClass: "form-control",
kcInputErrorMessageClass:
"pf-c-form__helper-text pf-m-error required kc-feedback-text"
}
});

View File

@ -9,9 +9,9 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
const { getClassName } = useGetClassName({
doUseDefaultCss,
"classes": {
classes: {
...classes,
"kcBodyClass": clsx(classes?.kcBodyClass, "user")
kcBodyClass: clsx(classes?.kcBodyClass, "user")
}
});

View File

@ -10,9 +10,9 @@ export default function Password(props: PageProps<Extract<KcContext, { pageId: "
const { getClassName } = useGetClassName({
doUseDefaultCss,
"classes": {
classes: {
...classes,
"kcBodyClass": clsx(classes?.kcBodyClass, "password")
kcBodyClass: clsx(classes?.kcBodyClass, "password")
}
});
@ -57,18 +57,18 @@ export default function Password(props: PageProps<Extract<KcContext, { pageId: "
{...{
kcContext: {
...kcContext,
"message": (() => {
message: (() => {
if (newPasswordError !== "") {
return {
"type": "error",
"summary": newPasswordError
type: "error",
summary: newPasswordError
};
}
if (newPasswordConfirmError !== "") {
return {
"type": "error",
"summary": newPasswordConfirmError
type: "error",
summary: newPasswordConfirmError
};
}
@ -98,7 +98,7 @@ export default function Password(props: PageProps<Extract<KcContext, { pageId: "
value={account.username ?? ""}
autoComplete="username"
readOnly
style={{ "display": "none" }}
style={{ display: "none" }}
/>
{password.passwordSet && (

View File

@ -18,9 +18,9 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
const { msg, msgStr } = i18n;
const algToKeyUriAlg: Record<(typeof kcContext)["totp"]["policy"]["algorithm"], string> = {
"HmacSHA1": "SHA1",
"HmacSHA256": "SHA256",
"HmacSHA512": "SHA512"
HmacSHA1: "SHA1",
HmacSHA256: "SHA256",
HmacSHA512: "SHA512"
};
return (

View File

@ -8,9 +8,9 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const buildOptions = readBuildOptions({ cliCommandOptions });
await copyKeycloakResourcesToPublic({
"buildOptions": {
buildOptions: {
...buildOptions,
"publicDirPath": buildOptions.reactAppRootDirPath
publicDirPath: buildOptions.reactAppRootDirPath
}
});
}

View File

@ -12,22 +12,39 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
cliCommandOptions
});
console.log(chalk.cyan("Select the Keycloak version from which you want to download the builtins theme:"));
console.log(
chalk.cyan(
"Select the Keycloak version from which you want to download the builtins theme:"
)
);
const { keycloakVersion } = await promptKeycloakVersion({
"startingFromMajor": undefined,
"cacheDirPath": buildOptions.cacheDirPath
startingFromMajor: undefined,
cacheDirPath: buildOptions.cacheDirPath
});
console.log(`${keycloakVersion}`);
const destDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme");
const destDirPath = pathJoin(
buildOptions.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme"
);
console.log(
[
`Downloading builtins theme of Keycloak ${keycloakVersion} here:`,
`- ${chalk.bold(`.${pathSep}${pathJoin(pathRelative(process.cwd(), destDirPath), "base")}`)}`,
`- ${chalk.bold(`.${pathSep}${pathJoin(pathRelative(process.cwd(), destDirPath), "keycloak")}`)}`
`- ${chalk.bold(
`.${pathSep}${pathJoin(pathRelative(process.cwd(), destDirPath), "base")}`
)}`,
`- ${chalk.bold(
`.${pathSep}${pathJoin(
pathRelative(process.cwd(), destDirPath),
"keycloak"
)}`
)}`
].join("\n")
);

View File

@ -30,7 +30,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
console.log(chalk.cyan("Theme type:"));
const { value: themeType } = await cliSelect<ThemeType>({
"values": [...themeTypes]
values: [...themeTypes]
}).catch(() => {
process.exit(-1);
});
@ -40,7 +40,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
console.log(chalk.cyan("Select the page you want to customize:"));
const { value: pageId } = await cliSelect<LoginThemePageId | AccountThemePageId>({
"values": (() => {
values: (() => {
switch (themeType) {
case "login":
return [...loginThemePageIds];
@ -55,14 +55,29 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
console.log(`${pageId}`);
const componentPageBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(/ftl$/, "tsx");
const componentPageBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(
/ftl$/,
"tsx"
);
const { themeSrcDirPath } = getThemeSrcDirPath({ "reactAppRootDirPath": buildOptions.reactAppRootDirPath });
const { themeSrcDirPath } = getThemeSrcDirPath({
reactAppRootDirPath: buildOptions.reactAppRootDirPath
});
const targetFilePath = pathJoin(themeSrcDirPath, themeType, "pages", componentPageBasename);
const targetFilePath = pathJoin(
themeSrcDirPath,
themeType,
"pages",
componentPageBasename
);
if (fs.existsSync(targetFilePath)) {
console.log(`${pageId} is already ejected, ${pathRelative(process.cwd(), targetFilePath)} already exists`);
console.log(
`${pageId} is already ejected, ${pathRelative(
process.cwd(),
targetFilePath
)} already exists`
);
process.exit(-1);
}
@ -71,12 +86,20 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const targetDirPath = pathDirname(targetFilePath);
if (!fs.existsSync(targetDirPath)) {
fs.mkdirSync(targetDirPath, { "recursive": true });
fs.mkdirSync(targetDirPath, { recursive: true });
}
}
const componentPageContent = fs
.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "src", themeType, "pages", componentPageBasename))
.readFileSync(
pathJoin(
getThisCodebaseRootDirPath(),
"src",
themeType,
"pages",
componentPageBasename
)
)
.toString("utf8");
fs.writeFileSync(targetFilePath, Buffer.from(componentPageContent, "utf8"));
@ -92,11 +115,23 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
``,
`You now need to update your page router:`,
``,
`${chalk.bold(pathJoin(".", pathRelative(process.cwd(), themeSrcDirPath), themeType, "KcApp.tsx"))}:`,
`${chalk.bold(
pathJoin(
".",
pathRelative(process.cwd(), themeSrcDirPath),
themeType,
"KcApp.tsx"
)
)}:`,
chalk.grey("```"),
`// ...`,
``,
chalk.green(`+const ${componentPageBasename.replace(/.tsx$/, "")} = lazy(() => import("./pages/${componentPageBasename}"));`),
chalk.green(
`+const ${componentPageBasename.replace(
/.tsx$/,
""
)} = lazy(() => import("./pages/${componentPageBasename}"));`
),
...[
``,
` export default function KcApp(props: { kcContext: KcContext; }) {`,
@ -114,7 +149,9 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
`+ Template={Template}`,
...(!componentPageContent.includes(userProfileFormFieldComponentName)
? []
: [`+ ${userProfileFormFieldComponentName}={${userProfileFormFieldComponentName}}`]),
: [
`+ ${userProfileFormFieldComponentName}={${userProfileFormFieldComponentName}}`
]),
`+ doUseDefaultCss={true}`,
`+ />`,
`+ );`,

View File

@ -14,13 +14,18 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const buildOptions = readBuildOptions({ cliCommandOptions });
const { themeSrcDirPath } = getThemeSrcDirPath({
"reactAppRootDirPath": buildOptions.reactAppRootDirPath
reactAppRootDirPath: buildOptions.reactAppRootDirPath
});
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
if (fs.existsSync(emailThemeSrcDirPath)) {
console.warn(`There is already a ${pathRelative(process.cwd(), emailThemeSrcDirPath)} directory in your project. Aborting.`);
console.warn(
`There is already a ${pathRelative(
process.cwd(),
emailThemeSrcDirPath
)} directory in your project. Aborting.`
);
process.exit(-1);
}
@ -29,33 +34,50 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const { keycloakVersion } = await promptKeycloakVersion({
// NOTE: This is arbitrary
"startingFromMajor": 17,
"cacheDirPath": buildOptions.cacheDirPath
startingFromMajor: 17,
cacheDirPath: buildOptions.cacheDirPath
});
const builtinKeycloakThemeTmpDirPath = pathJoin(buildOptions.cacheDirPath, "initialize-email-theme_tmp");
const builtinKeycloakThemeTmpDirPath = pathJoin(
buildOptions.cacheDirPath,
"initialize-email-theme_tmp"
);
rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
rmSync(builtinKeycloakThemeTmpDirPath, {
recursive: true,
force: true
});
await downloadKeycloakDefaultTheme({
keycloakVersion,
"destDirPath": builtinKeycloakThemeTmpDirPath,
destDirPath: builtinKeycloakThemeTmpDirPath,
buildOptions
});
transformCodebase({
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
"destDirPath": emailThemeSrcDirPath
srcDirPath: pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
destDirPath: emailThemeSrcDirPath
});
{
const themePropertyFilePath = pathJoin(emailThemeSrcDirPath, "theme.properties");
fs.writeFileSync(themePropertyFilePath, Buffer.from(`parent=base\n${fs.readFileSync(themePropertyFilePath).toString("utf8")}`, "utf8"));
fs.writeFileSync(
themePropertyFilePath,
Buffer.from(
`parent=base\n${fs.readFileSync(themePropertyFilePath).toString("utf8")}`,
"utf8"
)
);
}
console.log(`The \`${pathJoin(".", pathRelative(process.cwd(), emailThemeSrcDirPath))}\` directory have been created.`);
console.log(
`The \`${pathJoin(
".",
pathRelative(process.cwd(), emailThemeSrcDirPath)
)}\` directory have been created.`
);
console.log("You can delete any file you don't modify.");
rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true });
rmSync(builtinKeycloakThemeTmpDirPath, { recursive: true });
}

View File

@ -1,11 +1,17 @@
import { assert, type Equals } from "tsafe/assert";
import type { KeycloakAccountV1Version, KeycloakThemeAdditionalInfoExtensionVersion } from "./extensionVersions";
import type {
KeycloakAccountV1Version,
KeycloakThemeAdditionalInfoExtensionVersion
} from "./extensionVersions";
import { join as pathJoin, dirname as pathDirname } from "path";
import { transformCodebase } from "../../tools/transformCodebase";
import type { BuildOptions } from "../../shared/buildOptions";
import * as fs from "fs/promises";
import { accountV1ThemeName } from "../../shared/constants";
import { generatePom, BuildOptionsLike as BuildOptionsLike_generatePom } from "./generatePom";
import {
generatePom,
BuildOptionsLike as BuildOptionsLike_generatePom
} from "./generatePom";
import { readFileSync } from "fs";
import { isInside } from "../../tools/isInside";
import child_process from "child_process";
@ -29,26 +35,53 @@ export async function buildJar(params: {
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
buildOptions: BuildOptionsLike;
}): Promise<void> {
const { jarFileBasename, keycloakAccountV1Version, keycloakThemeAdditionalInfoExtensionVersion, buildOptions } = params;
const {
jarFileBasename,
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion,
buildOptions
} = params;
const keycloakifyBuildTmpDirPath = pathJoin(buildOptions.cacheDirPath, jarFileBasename.replace(".jar", ""));
const keycloakifyBuildTmpDirPath = pathJoin(
buildOptions.cacheDirPath,
jarFileBasename.replace(".jar", "")
);
rmSync(keycloakifyBuildTmpDirPath, { "recursive": true, "force": true });
rmSync(keycloakifyBuildTmpDirPath, { recursive: true, force: true });
{
const metaInfKeycloakThemesJsonRelativePath = getMetaInfKeycloakThemesJsonFilePath({ "keycloakifyBuildDirPath": "" });
const metaInfKeycloakThemesJsonRelativePath =
getMetaInfKeycloakThemesJsonFilePath({
keycloakifyBuildDirPath: ""
});
const { transformCodebase_common } = (() => {
const includingAccountV1ThemeNames = [...buildOptions.themeNames, accountV1ThemeName];
const includingAccountV1ThemeNames = [
...buildOptions.themeNames,
accountV1ThemeName
];
const transformCodebase_common: Param0<typeof transformCodebase>["transformSourceCode"] = ({ fileRelativePath, sourceCode }) => {
const transformCodebase_common: Param0<
typeof transformCodebase
>["transformSourceCode"] = ({ fileRelativePath, sourceCode }) => {
if (metaInfKeycloakThemesJsonRelativePath === fileRelativePath) {
return { "modifiedSourceCode": sourceCode };
return { modifiedSourceCode: sourceCode };
}
for (const themeName of includingAccountV1ThemeNames) {
if (isInside({ "dirPath": pathJoin("src", "main", "resources", "theme", themeName), "filePath": fileRelativePath })) {
return { "modifiedSourceCode": sourceCode };
if (
isInside({
dirPath: pathJoin(
"src",
"main",
"resources",
"theme",
themeName
),
filePath: fileRelativePath
})
) {
return { modifiedSourceCode: sourceCode };
}
}
@ -60,36 +93,70 @@ export async function buildJar(params: {
const { transformCodebase_patchForUsingBuiltinAccountV1 } = (() => {
if (keycloakAccountV1Version !== null) {
return { "transformCodebase_patchForUsingBuiltinAccountV1": undefined };
return {
transformCodebase_patchForUsingBuiltinAccountV1: undefined
};
}
const themePropertiesFileRelativePathSet = new Set(
...buildOptions.themeNames.map(themeName => pathJoin("src", "main", "resources", "theme", themeName, "account", "theme.properties"))
...buildOptions.themeNames.map(themeName =>
pathJoin(
"src",
"main",
"resources",
"theme",
themeName,
"account",
"theme.properties"
)
)
);
const accountV1RelativeDirPath = pathJoin("src", "main", "resources", "theme", accountV1ThemeName);
const accountV1RelativeDirPath = pathJoin(
"src",
"main",
"resources",
"theme",
accountV1ThemeName
);
const transformCodebase_patchForUsingBuiltinAccountV1: Param0<typeof transformCodebase>["transformSourceCode"] = ({
fileRelativePath,
sourceCode
}) => {
if (isInside({ "dirPath": accountV1RelativeDirPath, "filePath": fileRelativePath })) {
const transformCodebase_patchForUsingBuiltinAccountV1: Param0<
typeof transformCodebase
>["transformSourceCode"] = ({ fileRelativePath, sourceCode }) => {
if (
isInside({
dirPath: accountV1RelativeDirPath,
filePath: fileRelativePath
})
) {
return undefined;
}
if (fileRelativePath === metaInfKeycloakThemesJsonRelativePath) {
const keycloakThemesJsonParsed = JSON.parse(sourceCode.toString("utf8")) as {
const keycloakThemesJsonParsed = JSON.parse(
sourceCode.toString("utf8")
) as {
themes: { name: string; types: string[] }[];
};
keycloakThemesJsonParsed.themes = keycloakThemesJsonParsed.themes.filter(({ name }) => name !== accountV1ThemeName);
keycloakThemesJsonParsed.themes =
keycloakThemesJsonParsed.themes.filter(
({ name }) => name !== accountV1ThemeName
);
return { "modifiedSourceCode": Buffer.from(JSON.stringify(keycloakThemesJsonParsed, null, 2), "utf8") };
return {
modifiedSourceCode: Buffer.from(
JSON.stringify(keycloakThemesJsonParsed, null, 2),
"utf8"
)
};
}
if (themePropertiesFileRelativePathSet.has(fileRelativePath)) {
const modifiedSourceCode = Buffer.from(
sourceCode.toString("utf8").replace(`parent=${accountV1ThemeName}`, "parent=keycloak"),
sourceCode
.toString("utf8")
.replace(`parent=${accountV1ThemeName}`, "parent=keycloak"),
"utf8"
);
@ -99,16 +166,16 @@ export async function buildJar(params: {
return { modifiedSourceCode };
}
return { "modifiedSourceCode": sourceCode };
return { modifiedSourceCode: sourceCode };
};
return { transformCodebase_patchForUsingBuiltinAccountV1 };
})();
transformCodebase({
"srcDirPath": buildOptions.keycloakifyBuildDirPath,
"destDirPath": keycloakifyBuildTmpDirPath,
"transformSourceCode": params => {
srcDirPath: buildOptions.keycloakifyBuildDirPath,
destDirPath: keycloakifyBuildTmpDirPath,
transformSourceCode: params => {
const resultCommon = transformCodebase_common(params);
if (resultCommon === undefined) {
@ -125,7 +192,7 @@ export async function buildJar(params: {
return transformCodebase_patchForUsingBuiltinAccountV1?.({
...params,
"sourceCode": modifiedSourceCode
sourceCode: modifiedSourceCode
});
}
});
@ -152,7 +219,16 @@ export async function buildJar(params: {
(["register.ftl", "login-update-profile.ftl"] as const).forEach(pageId =>
buildOptions.themeNames.map(themeName => {
const ftlFilePath = pathJoin(keycloakifyBuildTmpDirPath, "src", "main", "resources", "theme", themeName, "login", pageId);
const ftlFilePath = pathJoin(
keycloakifyBuildTmpDirPath,
"src",
"main",
"resources",
"theme",
themeName,
"login",
pageId
);
const ftlFileContent = readFileSync(ftlFilePath).toString("utf8");
@ -173,7 +249,10 @@ export async function buildJar(params: {
assert(modifiedFtlFileContent !== ftlFileContent);
fs.writeFile(pathJoin(pathDirname(ftlFilePath), realPageId), Buffer.from(modifiedFtlFileContent, "utf8"));
fs.writeFile(
pathJoin(pathDirname(ftlFilePath), realPageId),
Buffer.from(modifiedFtlFileContent, "utf8")
);
})
);
}
@ -185,35 +264,46 @@ export async function buildJar(params: {
keycloakThemeAdditionalInfoExtensionVersion
});
await fs.writeFile(pathJoin(keycloakifyBuildTmpDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
await fs.writeFile(
pathJoin(keycloakifyBuildTmpDirPath, "pom.xml"),
Buffer.from(pomFileCode, "utf8")
);
}
await new Promise<void>((resolve, reject) =>
child_process.exec("mvn clean install", { "cwd": keycloakifyBuildTmpDirPath }, error => {
if (error !== null) {
console.error(
`Build jar failed: ${JSON.stringify(
{
jarFileBasename,
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion
},
null,
2
)}`
);
child_process.exec(
"mvn clean install",
{ cwd: keycloakifyBuildTmpDirPath },
error => {
if (error !== null) {
console.error(
`Build jar failed: ${JSON.stringify(
{
jarFileBasename,
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion
},
null,
2
)}`
);
reject(error);
return;
reject(error);
return;
}
resolve();
}
resolve();
})
)
);
await fs.rename(
pathJoin(keycloakifyBuildTmpDirPath, "target", `${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`),
pathJoin(
keycloakifyBuildTmpDirPath,
"target",
`${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`
),
pathJoin(buildOptions.keycloakifyBuildDirPath, jarFileBasename)
);
rmSync(keycloakifyBuildTmpDirPath, { "recursive": true });
rmSync(keycloakifyBuildTmpDirPath, { recursive: true });
}

View File

@ -1,6 +1,9 @@
import { assert } from "tsafe/assert";
import { exclude } from "tsafe/exclude";
import { keycloakAccountV1Versions, keycloakThemeAdditionalInfoExtensionVersions } from "./extensionVersions";
import {
keycloakAccountV1Versions,
keycloakThemeAdditionalInfoExtensionVersions
} from "./extensionVersions";
import { getKeycloakVersionRangeForJar } from "./getKeycloakVersionRangeForJar";
import { buildJar, BuildOptionsLike as BuildOptionsLike_buildJar } from "./buildJar";
import type { BuildOptions } from "../../shared/buildOptions";
@ -14,11 +17,13 @@ export type BuildOptionsLike = BuildOptionsLike_buildJar & {
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function buildJars(params: { buildOptions: BuildOptionsLike }): Promise<void> {
export async function buildJars(params: {
buildOptions: BuildOptionsLike;
}): Promise<void> {
const { buildOptions } = params;
const doesImplementAccountTheme = readMetaInfKeycloakThemes({
"keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath
keycloakifyBuildDirPath: buildOptions.keycloakifyBuildDirPath
}).themes.some(({ name }) => name === accountV1ThemeName);
await Promise.all(
@ -36,24 +41,38 @@ export async function buildJars(params: { buildOptions: BuildOptionsLike }): Pro
return undefined;
}
return { keycloakThemeAdditionalInfoExtensionVersion, keycloakVersionRange };
})
.filter(exclude(undefined))
.map(({ keycloakThemeAdditionalInfoExtensionVersion, keycloakVersionRange }) => {
const { jarFileBasename } = getJarFileBasename({ keycloakVersionRange });
return {
keycloakThemeAdditionalInfoExtensionVersion,
jarFileBasename
keycloakVersionRange
};
})
.map(({ keycloakThemeAdditionalInfoExtensionVersion, jarFileBasename }) =>
buildJar({
jarFileBasename,
keycloakAccountV1Version,
.filter(exclude(undefined))
.map(
({
keycloakThemeAdditionalInfoExtensionVersion,
buildOptions
})
keycloakVersionRange
}) => {
const { jarFileBasename } = getJarFileBasename({
keycloakVersionRange
});
return {
keycloakThemeAdditionalInfoExtensionVersion,
jarFileBasename
};
}
)
.map(
({
keycloakThemeAdditionalInfoExtensionVersion,
jarFileBasename
}) =>
buildJar({
jarFileBasename,
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion,
buildOptions
})
)
)
.flat()

View File

@ -13,4 +13,5 @@ export const keycloakThemeAdditionalInfoExtensionVersions = [null, "1.1.5"] as c
* https://central.sonatype.com/artifact/dev.jcputney/keycloak-theme-additional-info-extension
* https://github.com/jcputney/keycloak-theme-additional-info-extension
* */
export type KeycloakThemeAdditionalInfoExtensionVersion = (typeof keycloakThemeAdditionalInfoExtensionVersions)[number];
export type KeycloakThemeAdditionalInfoExtensionVersion =
(typeof keycloakThemeAdditionalInfoExtensionVersions)[number];

View File

@ -1,6 +1,9 @@
import { assert } from "tsafe/assert";
import type { BuildOptions } from "../../shared/buildOptions";
import type { KeycloakAccountV1Version, KeycloakThemeAdditionalInfoExtensionVersion } from "./extensionVersions";
import type {
KeycloakAccountV1Version,
KeycloakThemeAdditionalInfoExtensionVersion
} from "./extensionVersions";
export type BuildOptionsLike = {
groupId: string;
@ -15,7 +18,11 @@ export function generatePom(params: {
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
buildOptions: BuildOptionsLike;
}) {
const { keycloakAccountV1Version, keycloakThemeAdditionalInfoExtensionVersion, buildOptions } = params;
const {
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion,
buildOptions
} = params;
const { pomFileCode } = (function generatePomFileCode(): {
pomFileCode: string;
@ -35,7 +42,8 @@ export function generatePom(params: {
` <properties>`,
` <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>`,
` </properties>`,
...(keycloakAccountV1Version !== null && keycloakThemeAdditionalInfoExtensionVersion !== null
...(keycloakAccountV1Version !== null &&
keycloakThemeAdditionalInfoExtensionVersion !== null
? [
` <build>`,
` <plugins>`,

View File

@ -1,5 +1,8 @@
import { assert, type Equals } from "tsafe/assert";
import type { KeycloakAccountV1Version, KeycloakThemeAdditionalInfoExtensionVersion } from "./extensionVersions";
import type {
KeycloakAccountV1Version,
KeycloakThemeAdditionalInfoExtensionVersion
} from "./extensionVersions";
import type { KeycloakVersionRange } from "../../shared/KeycloakVersionRange";
export function getKeycloakVersionRangeForJar(params: {
@ -7,7 +10,11 @@ export function getKeycloakVersionRangeForJar(params: {
keycloakAccountV1Version: KeycloakAccountV1Version;
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
}): KeycloakVersionRange | undefined {
const { keycloakAccountV1Version, keycloakThemeAdditionalInfoExtensionVersion, doesImplementAccountTheme } = params;
const {
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion,
doesImplementAccountTheme
} = params;
if (doesImplementAccountTheme) {
const keycloakVersionRange = (() => {
@ -19,7 +26,9 @@ export function getKeycloakVersionRangeForJar(params: {
case "1.1.5":
return undefined;
}
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
assert<
Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>
>(false);
case "0.3":
switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null:
@ -27,7 +36,9 @@ export function getKeycloakVersionRangeForJar(params: {
case "1.1.5":
return "23" as const;
}
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
assert<
Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>
>(false);
case "0.4":
switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null:
@ -35,11 +46,18 @@ export function getKeycloakVersionRangeForJar(params: {
case "1.1.5":
return "24-and-above" as const;
}
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
assert<
Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>
>(false);
}
})();
assert<Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithAccountTheme | undefined>>();
assert<
Equals<
typeof keycloakVersionRange,
KeycloakVersionRange.WithAccountTheme | undefined
>
>();
return keycloakVersionRange;
} else {
@ -54,10 +72,17 @@ export function getKeycloakVersionRangeForJar(params: {
case "1.1.5":
return "22-and-above";
}
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(
false
);
})();
assert<Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithoutAccountTheme | undefined>>();
assert<
Equals<
typeof keycloakVersionRange,
KeycloakVersionRange.WithoutAccountTheme | undefined
>
>();
return keycloakVersionRange;
}

View File

@ -6,7 +6,12 @@ import * as fs from "fs";
import { join as pathJoin } from "path";
import type { BuildOptions } from "../../shared/buildOptions";
import { assert } from "tsafe/assert";
import { type ThemeType, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir, resources_common } from "../../shared/constants";
import {
type ThemeType,
nameOfTheGlobal,
basenameOfTheKeycloakifyResourcesDir,
resources_common
} from "../../shared/constants";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
export type BuildOptionsLike = {
@ -28,7 +33,15 @@ export function generateFtlFilesCodeFactory(params: {
themeType: ThemeType;
fieldNames: string[];
}) {
const { themeName, cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion, themeType, fieldNames } = params;
const {
themeName,
cssGlobalsToDefine,
indexHtmlCode,
buildOptions,
keycloakifyVersion,
themeType,
fieldNames
} = params;
const $ = cheerio.load(indexHtmlCode);
@ -38,7 +51,10 @@ export function generateFtlFilesCodeFactory(params: {
assert(jsCode !== null);
const { fixedJsCode } = replaceImportsInJsCode({ jsCode, buildOptions });
const { fixedJsCode } = replaceImportsInJsCode({
jsCode,
buildOptions
});
$(element).text(fixedJsCode);
});
@ -72,7 +88,9 @@ export function generateFtlFilesCodeFactory(params: {
$(element).attr(
attrName,
href.replace(
new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`),
new RegExp(
`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`
),
`\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/`
)
);
@ -98,20 +116,33 @@ export function generateFtlFilesCodeFactory(params: {
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
const ftlObjectToJsCodeDeclaringAnObject = fs
.readFileSync(
pathJoin(getThisCodebaseRootDirPath(), "src", "bin", "keycloakify", "generateFtl", "ftl_object_to_js_code_declaring_an_object.ftl")
pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"keycloakify",
"generateFtl",
"ftl_object_to_js_code_declaring_an_object.ftl"
)
)
.toString("utf8")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1]
.replace("FIELD_NAMES_eKsIY4ZsZ4xeM", fieldNames.map(name => `"${name}"`).join(", "))
.replace(
"FIELD_NAMES_eKsIY4ZsZ4xeM",
fieldNames.map(name => `"${name}"`).join(", ")
)
.replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion)
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion)
.replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType)
.replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName)
.replace("RESOURCES_COMMON_cLsLsMrtDkpVv", resources_common);
const ftlObjectToJsCodeDeclaringAnObjectPlaceholder = '{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }';
const ftlObjectToJsCodeDeclaringAnObjectPlaceholder =
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }';
$("head").prepend(`<script>\nwindow.${nameOfTheGlobal}=${ftlObjectToJsCodeDeclaringAnObjectPlaceholder}</script>`);
$("head").prepend(
`<script>\nwindow.${nameOfTheGlobal}=${ftlObjectToJsCodeDeclaringAnObjectPlaceholder}</script>`
);
// Remove part of the document marked as ignored.
{
@ -119,7 +150,9 @@ export function generateFtlFilesCodeFactory(params: {
startTags.each((...[, startTag]) => {
const $startTag = $(startTag);
const $endTag = $startTag.nextAll('meta[name="keycloakify-ignore-end"]').first();
const $endTag = $startTag
.nextAll('meta[name="keycloakify-ignore-end"]')
.first();
if ($endTag.length) {
let currentNode = $startTag.next();
@ -146,9 +179,13 @@ export function generateFtlFilesCodeFactory(params: {
let ftlCode = $.html();
Object.entries({
[ftlObjectToJsCodeDeclaringAnObjectPlaceholder]: ftlObjectToJsCodeDeclaringAnObject,
"PAGE_ID_xIgLsPgGId9D8e": pageId
}).map(([searchValue, replaceValue]) => (ftlCode = ftlCode.replace(searchValue, replaceValue)));
[ftlObjectToJsCodeDeclaringAnObjectPlaceholder]:
ftlObjectToJsCodeDeclaringAnObject,
PAGE_ID_xIgLsPgGId9D8e: pageId
}).map(
([searchValue, replaceValue]) =>
(ftlCode = ftlCode.replace(searchValue, replaceValue))
);
return { ftlCode };
}

View File

@ -2,7 +2,11 @@ import * as fs from "fs";
import { join as pathJoin } from "path";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "../../shared/buildOptions";
import { resources_common, lastKeycloakVersionWithAccountV1, accountV1ThemeName } from "../../shared/constants";
import {
resources_common,
lastKeycloakVersionWithAccountV1,
accountV1ThemeName
} from "../../shared/constants";
import { downloadKeycloakDefaultTheme } from "../../shared/downloadKeycloakDefaultTheme";
import { transformCodebase } from "../../tools/transformCodebase";
import { rmSync } from "../../tools/fs.rmSync";
@ -18,32 +22,53 @@ assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike }) {
const { buildOptions } = params;
const builtinKeycloakThemeTmpDirPath = pathJoin(buildOptions.cacheDirPath, "bringInAccountV1_tmp");
const builtinKeycloakThemeTmpDirPath = pathJoin(
buildOptions.cacheDirPath,
"bringInAccountV1_tmp"
);
await downloadKeycloakDefaultTheme({
"destDirPath": builtinKeycloakThemeTmpDirPath,
"keycloakVersion": lastKeycloakVersionWithAccountV1,
destDirPath: builtinKeycloakThemeTmpDirPath,
keycloakVersion: lastKeycloakVersionWithAccountV1,
buildOptions
});
const accountV1DirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", accountV1ThemeName, "account");
const accountV1DirPath = pathJoin(
buildOptions.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
accountV1ThemeName,
"account"
);
transformCodebase({
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "account"),
"destDirPath": accountV1DirPath
srcDirPath: pathJoin(builtinKeycloakThemeTmpDirPath, "base", "account"),
destDirPath: accountV1DirPath
});
transformCodebase({
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "account", "resources"),
"destDirPath": pathJoin(accountV1DirPath, "resources")
srcDirPath: pathJoin(
builtinKeycloakThemeTmpDirPath,
"keycloak",
"account",
"resources"
),
destDirPath: pathJoin(accountV1DirPath, "resources")
});
transformCodebase({
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(accountV1DirPath, "resources", resources_common)
srcDirPath: pathJoin(
builtinKeycloakThemeTmpDirPath,
"keycloak",
"common",
"resources"
),
destDirPath: pathJoin(accountV1DirPath, "resources", resources_common)
});
rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true });
rmSync(builtinKeycloakThemeTmpDirPath, { recursive: true });
fs.writeFileSync(
pathJoin(accountV1DirPath, "theme.properties"),
@ -58,8 +83,13 @@ export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike
"css/account.css",
"img/icon-sidebar-active.png",
"img/logo.png",
...["patternfly.min.css", "patternfly-additions.min.css", "patternfly-additions.min.css"].map(
fileBasename => `${resources_common}/node_modules/patternfly/dist/css/${fileBasename}`
...[
"patternfly.min.css",
"patternfly-additions.min.css",
"patternfly-additions.min.css"
].map(
fileBasename =>
`${resources_common}/node_modules/patternfly/dist/css/${fileBasename}`
)
].join(" "),
"",

View File

@ -16,8 +16,8 @@ export function generateMessageProperties(params: {
const { themeSrcDirPath, themeType } = params;
let files = crawl({
"dirPath": pathJoin(themeSrcDirPath, themeType),
"returnedPathsType": "absolute"
dirPath: pathJoin(themeSrcDirPath, themeType),
returnedPathsType: "absolute"
});
files = files.filter(file => {
@ -34,7 +34,9 @@ export function generateMessageProperties(params: {
files = files.sort((a, b) => a.length - b.length);
files = files.filter(file => readFileSync(file).toString("utf8").includes("createUseI18n"));
files = files.filter(file =>
readFileSync(file).toString("utf8").includes("createUseI18n")
);
if (files.length === 0) {
return [];
@ -43,18 +45,25 @@ export function generateMessageProperties(params: {
const extraMessages = files
.map(file => {
const root = recast.parse(readFileSync(file).toString("utf8"), {
"parser": {
"parse": (code: string) => babelParser.parse(code, { "sourceType": "module", "plugins": ["typescript"] }),
"generator": babelGenerate,
"types": babelTypes
parser: {
parse: (code: string) =>
babelParser.parse(code, {
sourceType: "module",
plugins: ["typescript"]
}),
generator: babelGenerate,
types: babelTypes
}
});
const codes: string[] = [];
recast.visit(root, {
"visitCallExpression": function (path) {
if (path.node.callee.type === "Identifier" && path.node.callee.name === "createUseI18n") {
visitCallExpression: function (path) {
if (
path.node.callee.type === "Identifier" &&
path.node.callee.name === "createUseI18n"
) {
codes.push(babelGenerate(path.node.arguments[0] as any).code);
}
this.traverse(path);
@ -65,7 +74,9 @@ export function generateMessageProperties(params: {
})
.flat()
.map(code => {
let extraMessages: { [languageTag: string]: Record<string, string> } = {};
let extraMessages: {
[languageTag: string]: Record<string, string>;
} = {};
try {
eval(`${symToStr({ extraMessages })} = ${code}`);
@ -140,7 +151,14 @@ export function generateMessageProperties(params: {
out.push({
languageTag,
"propertiesFileSource": ["# This file was generated by keycloakify", "", "parent=base", "", propertiesFileSource, ""].join("\n")
propertiesFileSource: [
"# This file was generated by keycloakify",
"",
"parent=base",
"",
propertiesFileSource,
""
].join("\n")
});
}
@ -157,7 +175,12 @@ function toUTF16(codePoint: number): string {
codePoint -= 0x10000;
let highSurrogate = (codePoint >> 10) + 0xd800;
let lowSurrogate = (codePoint % 0x400) + 0xdc00;
return "\\u" + highSurrogate.toString(16).padStart(4, "0") + "\\u" + lowSurrogate.toString(16).padStart(4, "0");
return (
"\\u" +
highSurrogate.toString(16).padStart(4, "0") +
"\\u" +
lowSurrogate.toString(16).padStart(4, "0")
);
}
}

View File

@ -12,7 +12,9 @@ export type BuildOptionsLike = BuildOptionsLike_generateSrcMainResourcesForMainT
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function generateSrcMainResources(params: { buildOptions: BuildOptionsLike }): Promise<void> {
export async function generateSrcMainResources(params: {
buildOptions: BuildOptionsLike;
}): Promise<void> {
const { buildOptions } = params;
const [themeName, ...themeVariantNames] = buildOptions.themeNames;

View File

@ -24,7 +24,10 @@ import { bringInAccountV1 } from "./bringInAccountV1";
import { getThemeSrcDirPath } from "../../shared/getThemeSrcDirPath";
import { rmSync } from "../../tools/fs.rmSync";
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
import { writeMetaInfKeycloakThemes, type MetaInfKeycloakTheme } from "../../shared/metaInfKeycloakThemes";
import {
writeMetaInfKeycloakThemes,
type MetaInfKeycloakTheme
} from "../../shared/metaInfKeycloakThemes";
import { objectEntries } from "tsafe/objectEntries";
export type BuildOptionsLike = {
@ -43,22 +46,35 @@ export type BuildOptionsLike = {
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function generateSrcMainResourcesForMainTheme(params: { themeName: string; buildOptions: BuildOptionsLike }): Promise<void> {
export async function generateSrcMainResourcesForMainTheme(params: {
themeName: string;
buildOptions: BuildOptionsLike;
}): Promise<void> {
const { themeName, buildOptions } = params;
const { themeSrcDirPath } = getThemeSrcDirPath({ "reactAppRootDirPath": buildOptions.reactAppRootDirPath });
const { themeSrcDirPath } = getThemeSrcDirPath({
reactAppRootDirPath: buildOptions.reactAppRootDirPath
});
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
const { themeType } = params;
return pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", themeName, themeType);
return pathJoin(
buildOptions.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
themeName,
themeType
);
};
const cssGlobalsToDefine: Record<string, string> = {};
const implementedThemeTypes: Record<ThemeType | "email", boolean> = {
"login": false,
"account": false,
"email": false
login: false,
account: false,
email: false
};
for (const themeType of ["login", "account"] as const) {
@ -71,18 +87,22 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
const themeTypeDirPath = getThemeTypeDirPath({ themeType });
apply_replacers_and_move_to_theme_resources: {
const destDirPath = pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir);
const destDirPath = pathJoin(
themeTypeDirPath,
"resources",
basenameOfTheKeycloakifyResourcesDir
);
// NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
rmSync(destDirPath, { "recursive": true, "force": true });
rmSync(destDirPath, { recursive: true, force: true });
if (themeType === "account" && implementedThemeTypes.login) {
// NOTE: We prevent doing it twice, it has been done for the login theme.
transformCodebase({
"srcDirPath": pathJoin(
srcDirPath: pathJoin(
getThemeTypeDirPath({
"themeType": "login"
themeType: "login"
}),
"resources",
basenameOfTheKeycloakifyResourcesDir
@ -94,14 +114,17 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
}
transformCodebase({
"srcDirPath": buildOptions.reactAppBuildDirPath,
srcDirPath: buildOptions.reactAppBuildDirPath,
destDirPath,
"transformSourceCode": ({ filePath, sourceCode }) => {
transformSourceCode: ({ filePath, sourceCode }) => {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
// This should not happen if users follow the new instruction setup but we keep it for retrocompatibility.
if (
isInside({
"dirPath": pathJoin(buildOptions.reactAppBuildDirPath, keycloak_resources),
dirPath: pathJoin(
buildOptions.reactAppBuildDirPath,
keycloak_resources
),
filePath
})
) {
@ -109,39 +132,50 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
}
if (/\.css?$/i.test(filePath)) {
const { cssGlobalsToDefine: cssGlobalsToDefineForThisFile, fixedCssCode } = replaceImportsInCssCode({
"cssCode": sourceCode.toString("utf8")
const {
cssGlobalsToDefine: cssGlobalsToDefineForThisFile,
fixedCssCode
} = replaceImportsInCssCode({
cssCode: sourceCode.toString("utf8")
});
Object.entries(cssGlobalsToDefineForThisFile).forEach(([key, value]) => {
cssGlobalsToDefine[key] = value;
});
Object.entries(cssGlobalsToDefineForThisFile).forEach(
([key, value]) => {
cssGlobalsToDefine[key] = value;
}
);
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
return {
modifiedSourceCode: Buffer.from(fixedCssCode, "utf8")
};
}
if (/\.js?$/i.test(filePath)) {
const { fixedJsCode } = replaceImportsInJsCode({
"jsCode": sourceCode.toString("utf8"),
jsCode: sourceCode.toString("utf8"),
buildOptions
});
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
return {
modifiedSourceCode: Buffer.from(fixedJsCode, "utf8")
};
}
return { "modifiedSourceCode": sourceCode };
return { modifiedSourceCode: sourceCode };
}
});
}
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
themeName,
"indexHtmlCode": fs.readFileSync(pathJoin(buildOptions.reactAppBuildDirPath, "index.html")).toString("utf8"),
indexHtmlCode: fs
.readFileSync(pathJoin(buildOptions.reactAppBuildDirPath, "index.html"))
.toString("utf8"),
cssGlobalsToDefine,
buildOptions,
"keycloakifyVersion": readThisNpmPackageVersion(),
keycloakifyVersion: readThisNpmPackageVersion(),
themeType,
"fieldNames": readFieldNameUsage({
fieldNames: readFieldNameUsage({
themeSrcDirPath,
themeType
})
@ -163,9 +197,12 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId });
fs.mkdirSync(themeTypeDirPath, { "recursive": true });
fs.mkdirSync(themeTypeDirPath, { recursive: true });
fs.writeFileSync(pathJoin(themeTypeDirPath, pageId), Buffer.from(ftlCode, "utf8"));
fs.writeFileSync(
pathJoin(themeTypeDirPath, pageId),
Buffer.from(ftlCode, "utf8")
);
});
generateMessageProperties({
@ -174,15 +211,23 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
}).forEach(({ languageTag, propertiesFileSource }) => {
const messagesDirPath = pathJoin(themeTypeDirPath, "messages");
fs.mkdirSync(pathJoin(themeTypeDirPath, "messages"), { "recursive": true });
fs.mkdirSync(pathJoin(themeTypeDirPath, "messages"), {
recursive: true
});
const propertiesFilePath = pathJoin(messagesDirPath, `messages_${languageTag}.properties`);
const propertiesFilePath = pathJoin(
messagesDirPath,
`messages_${languageTag}.properties`
);
fs.writeFileSync(propertiesFilePath, Buffer.from(propertiesFileSource, "utf8"));
fs.writeFileSync(
propertiesFilePath,
Buffer.from(propertiesFileSource, "utf8")
);
});
await downloadKeycloakStaticResources({
"keycloakVersion": (() => {
keycloakVersion: (() => {
switch (themeType) {
case "account":
return lastKeycloakVersionWithAccountV1;
@ -190,7 +235,7 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
return buildOptions.loginThemeResourcesFromKeycloakVersion;
}
})(),
"themeDirPath": pathResolve(pathJoin(themeTypeDirPath, "..")),
themeDirPath: pathResolve(pathJoin(themeTypeDirPath, "..")),
themeType,
buildOptions
});
@ -225,8 +270,8 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
implementedThemeTypes.email = true;
transformCodebase({
"srcDirPath": emailThemeSrcDirPath,
"destDirPath": getThemeTypeDirPath({ "themeType": "email" })
srcDirPath: emailThemeSrcDirPath,
destDirPath: getThemeTypeDirPath({ themeType: "email" })
});
}
@ -237,24 +282,24 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
}
{
const metaInfKeycloakThemes: MetaInfKeycloakTheme = { "themes": [] };
const metaInfKeycloakThemes: MetaInfKeycloakTheme = { themes: [] };
metaInfKeycloakThemes.themes.push({
"name": themeName,
"types": objectEntries(implementedThemeTypes)
name: themeName,
types: objectEntries(implementedThemeTypes)
.filter(([, isImplemented]) => isImplemented)
.map(([themeType]) => themeType)
});
if (implementedThemeTypes.account) {
metaInfKeycloakThemes.themes.push({
"name": accountV1ThemeName,
"types": ["account"]
name: accountV1ThemeName,
types: ["account"]
});
}
writeMetaInfKeycloakThemes({
"keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath,
keycloakifyBuildDirPath: buildOptions.keycloakifyBuildDirPath,
metaInfKeycloakThemes
});
}

View File

@ -1,7 +1,10 @@
import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path";
import { transformCodebase } from "../../tools/transformCodebase";
import type { BuildOptions } from "../../shared/buildOptions";
import { readMetaInfKeycloakThemes, writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
import {
readMetaInfKeycloakThemes,
writeMetaInfKeycloakThemes
} from "../../shared/metaInfKeycloakThemes";
import { assert } from "tsafe/assert";
export type BuildOptionsLike = {
@ -10,20 +13,37 @@ export type BuildOptionsLike = {
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function generateSrcMainResourcesForThemeVariant(params: { themeName: string; themeVariantName: string; buildOptions: BuildOptionsLike }) {
export function generateSrcMainResourcesForThemeVariant(params: {
themeName: string;
themeVariantName: string;
buildOptions: BuildOptionsLike;
}) {
const { themeName, themeVariantName, buildOptions } = params;
const mainThemeDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", themeName);
const mainThemeDirPath = pathJoin(
buildOptions.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
themeName
);
transformCodebase({
"srcDirPath": mainThemeDirPath,
"destDirPath": pathJoin(mainThemeDirPath, "..", themeVariantName),
"transformSourceCode": ({ fileRelativePath, sourceCode }) => {
if (pathExtname(fileRelativePath) === ".ftl" && fileRelativePath.split(pathSep).length === 2) {
srcDirPath: mainThemeDirPath,
destDirPath: pathJoin(mainThemeDirPath, "..", themeVariantName),
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
if (
pathExtname(fileRelativePath) === ".ftl" &&
fileRelativePath.split(pathSep).length === 2
) {
const modifiedSourceCode = Buffer.from(
Buffer.from(sourceCode)
.toString("utf-8")
.replace(`out["themeName"] = "${themeName}";`, `out["themeName"] = "${themeVariantName}";`),
.replace(
`out["themeName"] = "${themeName}";`,
`out["themeName"] = "${themeVariantName}";`
),
"utf8"
);
@ -32,25 +52,29 @@ export function generateSrcMainResourcesForThemeVariant(params: { themeName: str
return { modifiedSourceCode };
}
return { "modifiedSourceCode": sourceCode };
return { modifiedSourceCode: sourceCode };
}
});
{
const updatedMetaInfKeycloakThemes = readMetaInfKeycloakThemes({ "keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath });
const updatedMetaInfKeycloakThemes = readMetaInfKeycloakThemes({
keycloakifyBuildDirPath: buildOptions.keycloakifyBuildDirPath
});
updatedMetaInfKeycloakThemes.themes.push({
"name": themeVariantName,
"types": (() => {
const theme = updatedMetaInfKeycloakThemes.themes.find(({ name }) => name === themeName);
name: themeVariantName,
types: (() => {
const theme = updatedMetaInfKeycloakThemes.themes.find(
({ name }) => name === themeName
);
assert(theme !== undefined);
return theme.types;
})()
});
writeMetaInfKeycloakThemes({
"keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath,
"metaInfKeycloakThemes": updatedMetaInfKeycloakThemes
keycloakifyBuildDirPath: buildOptions.keycloakifyBuildDirPath,
metaInfKeycloakThemes: updatedMetaInfKeycloakThemes
});
}
}

View File

@ -3,17 +3,26 @@ import { id } from "tsafe/id";
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
import * as fs from "fs";
import { join as pathJoin } from "path";
import { type ThemeType, accountThemePageIds, loginThemePageIds } from "../../shared/constants";
import {
type ThemeType,
accountThemePageIds,
loginThemePageIds
} from "../../shared/constants";
export function readExtraPagesNames(params: { themeSrcDirPath: string; themeType: ThemeType }): string[] {
export function readExtraPagesNames(params: {
themeSrcDirPath: string;
themeType: ThemeType;
}): string[] {
const { themeSrcDirPath, themeType } = params;
const filePaths = crawl({
"dirPath": pathJoin(themeSrcDirPath, themeType),
"returnedPathsType": "absolute"
dirPath: pathJoin(themeSrcDirPath, themeType),
returnedPathsType: "absolute"
}).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
const candidateFilePaths = filePaths.filter(filePath => /kcContext\.[^.]+$/.test(filePath));
const candidateFilePaths = filePaths.filter(filePath =>
/kcContext\.[^.]+$/.test(filePath)
);
if (candidateFilePaths.length === 0) {
candidateFilePaths.push(...filePaths);
@ -24,7 +33,12 @@ export function readExtraPagesNames(params: { themeSrcDirPath: string; themeType
for (const candidateFilPath of candidateFilePaths) {
const rawSourceFile = fs.readFileSync(candidateFilPath).toString("utf8");
extraPages.push(...Array.from(rawSourceFile.matchAll(/["']?pageId["']?\s*:\s*["']([^.]+.ftl)["']/g), m => m[1]));
extraPages.push(
...Array.from(
rawSourceFile.matchAll(/["']?pageId["']?\s*:\s*["']([^.]+.ftl)["']/g),
m => m[1]
)
);
}
return extraPages.reduce(...removeDuplicates<string>()).filter(pageId => {

View File

@ -5,13 +5,22 @@ import type { ThemeType } from "../../shared/constants";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
/** Assumes the theme type exists */
export function readFieldNameUsage(params: { themeSrcDirPath: string; themeType: ThemeType }): string[] {
export function readFieldNameUsage(params: {
themeSrcDirPath: string;
themeType: ThemeType;
}): string[] {
const { themeSrcDirPath, themeType } = params;
const fieldNames = new Set<string>();
for (const srcDirPath of [pathJoin(getThisCodebaseRootDirPath(), "src", themeType), pathJoin(themeSrcDirPath, themeType)]) {
const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
for (const srcDirPath of [
pathJoin(getThisCodebaseRootDirPath(), "src", themeType),
pathJoin(themeSrcDirPath, themeType)
]) {
const filePaths = crawl({
dirPath: srcDirPath,
returnedPathsType: "absolute"
}).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
for (const filePath of filePaths) {
const rawSourceFile = fs.readFileSync(filePath).toString("utf8");
@ -20,7 +29,13 @@ export function readFieldNameUsage(params: { themeSrcDirPath: string; themeType:
continue;
}
for (const functionName of ["printIfExists", "existsError", "get", "exists", "getFirstError"] as const) {
for (const functionName of [
"printIfExists",
"existsError",
"get",
"exists",
"getFirstError"
] as const) {
if (!rawSourceFile.includes(functionName)) {
continue;
}
@ -40,9 +55,21 @@ export function readFieldNameUsage(params: { themeSrcDirPath: string; themeType:
return part
.split(",")
.map(a => a.trim())
.filter((...[, i]) => (functionName !== "printIfExists" ? true : i === 0))
.filter(a => a.startsWith('"') || a.startsWith("'") || a.startsWith("`"))
.filter(a => a.endsWith('"') || a.endsWith("'") || a.endsWith("`"))
.filter((...[, i]) =>
functionName !== "printIfExists" ? true : i === 0
)
.filter(
a =>
a.startsWith('"') ||
a.startsWith("'") ||
a.startsWith("`")
)
.filter(
a =>
a.endsWith('"') ||
a.endsWith("'") ||
a.endsWith("`")
)
.map(a => a.slice(1).slice(0, -1));
})
.flat()

View File

@ -1,5 +1,9 @@
import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
import {
join as pathJoin,
relative as pathRelative,
basename as pathBasename
} from "path";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "../shared/buildOptions";
import { accountV1ThemeName } from "../shared/constants";
@ -27,7 +31,10 @@ export function generateStartKeycloakTestingContainer(params: {
const themeRelativeDirPath = pathJoin("src", "main", "resources", "theme");
fs.writeFileSync(
pathJoin(buildOptions.keycloakifyBuildDirPath, generateStartKeycloakTestingContainer.basename),
pathJoin(
buildOptions.keycloakifyBuildDirPath,
generateStartKeycloakTestingContainer.basename
),
Buffer.from(
[
"#!/usr/bin/env bash",
@ -45,9 +52,16 @@ export function generateStartKeycloakTestingContainer(params: {
"$(pwd)",
pathRelative(buildOptions.keycloakifyBuildDirPath, jarFilePath)
)}":"/opt/keycloak/providers/${pathBasename(jarFilePath)}" \\`,
[...(doesImplementAccountTheme ? [accountV1ThemeName] : []), ...buildOptions.themeNames].map(
[
...(doesImplementAccountTheme ? [accountV1ThemeName] : []),
...buildOptions.themeNames
].map(
themeName =>
` -v "${pathJoin("$(pwd)", themeRelativeDirPath, themeName).replace(/\\/g, "/")}":"/opt/keycloak/themes/${themeName}":rw \\`
` -v "${pathJoin(
"$(pwd)",
themeRelativeDirPath,
themeName
).replace(/\\/g, "/")}":"/opt/keycloak/themes/${themeName}":rw \\`
),
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
` start-dev`,
@ -55,6 +69,6 @@ export function generateStartKeycloakTestingContainer(params: {
].join("\n"),
"utf8"
),
{ "mode": 0o755 }
{ mode: 0o755 }
);
}

View File

@ -15,7 +15,9 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
let commandOutput: Buffer | undefined = undefined;
try {
commandOutput = child_process.execSync("mvn --version", { "stdio": ["ignore", "pipe", "ignore"] });
commandOutput = child_process.execSync("mvn --version", {
stdio: ["ignore", "pipe", "ignore"]
});
} catch {}
if (commandOutput?.toString("utf8").includes("Apache Maven")) {
@ -34,7 +36,11 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
}
})();
console.log(`${chalk.red("Apache Maven required.")} Install it with \`${chalk.bold(installationCommand)}\` (for example)`);
console.log(
`${chalk.red("Apache Maven required.")} Install it with \`${chalk.bold(
installationCommand
)}\` (for example)`
);
process.exit(1);
}
@ -46,7 +52,12 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
console.log(
[
chalk.cyan(`keycloakify v${readThisNpmPackageVersion()}`),
chalk.green(`Building the keycloak theme in .${pathSep}${pathRelative(process.cwd(), buildOptions.keycloakifyBuildDirPath)} ...`)
chalk.green(
`Building the keycloak theme in .${pathSep}${pathRelative(
process.cwd(),
buildOptions.keycloakifyBuildDirPath
)} ...`
)
].join(" ")
);
@ -54,10 +65,15 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
{
if (!fs.existsSync(buildOptions.keycloakifyBuildDirPath)) {
fs.mkdirSync(buildOptions.keycloakifyBuildDirPath, { "recursive": true });
fs.mkdirSync(buildOptions.keycloakifyBuildDirPath, {
recursive: true
});
}
fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, ".gitignore"), Buffer.from("*", "utf8"));
fs.writeFileSync(
pathJoin(buildOptions.keycloakifyBuildDirPath, ".gitignore"),
Buffer.from("*", "utf8")
);
}
await generateSrcMainResources({ buildOptions });
@ -68,10 +84,11 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
}
child_process.execSync("npx vite", {
"cwd": buildOptions.reactAppRootDirPath,
"env": {
cwd: buildOptions.reactAppRootDirPath,
env: {
...process.env,
[vitePluginSubScriptEnvNames.runPostBuildScript]: JSON.stringify(buildOptions)
[vitePluginSubScriptEnvNames.runPostBuildScript]:
JSON.stringify(buildOptions)
}
});
}
@ -84,5 +101,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
await buildJars({ buildOptions });
}
console.log(chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`));
console.log(
chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`)
);
}

View File

@ -18,7 +18,15 @@ export function replaceImportsInCssCode(params: { cssCode: string }): {
const cssGlobalsToDefine: Record<string, string> = {};
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*?/g) ?? []).forEach(
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match)
match =>
(cssGlobalsToDefine[
"url" +
crypto
.createHash("sha256")
.update(match)
.digest("hex")
.substring(0, 15)
] = match)
);
let fixedCssCode = cssCode;
@ -26,26 +34,37 @@ export function replaceImportsInCssCode(params: { cssCode: string }): {
Object.keys(cssGlobalsToDefine).forEach(
cssVariableName =>
//NOTE: split/join pattern ~ replace all
(fixedCssCode = fixedCssCode.split(cssGlobalsToDefine[cssVariableName]).join(`var(--${cssVariableName})`))
(fixedCssCode = fixedCssCode
.split(cssGlobalsToDefine[cssVariableName])
.join(`var(--${cssVariableName})`))
);
return { fixedCssCode, cssGlobalsToDefine };
}
export function generateCssCodeToDefineGlobals(params: { cssGlobalsToDefine: Record<string, string>; buildOptions: BuildOptionsLike }): {
export function generateCssCodeToDefineGlobals(params: {
cssGlobalsToDefine: Record<string, string>;
buildOptions: BuildOptionsLike;
}): {
cssCodeToPrependInHead: string;
} {
const { cssGlobalsToDefine, buildOptions } = params;
return {
"cssCodeToPrependInHead": [
cssCodeToPrependInHead: [
":root {",
...Object.keys(cssGlobalsToDefine)
.map(cssVariableName =>
[
`--${cssVariableName}:`,
cssGlobalsToDefine[cssVariableName].replace(
new RegExp(`url\\(${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`, "g"),
new RegExp(
`url\\(${(buildOptions.urlPathname ?? "/").replace(
/\//g,
"\\/"
)}`,
"g"
),
`url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/`
)
].join(" ")

View File

@ -8,7 +8,10 @@ export type BuildOptionsLike = {
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOptions: BuildOptionsLike }): {
export function replaceImportsInInlineCssCode(params: {
cssCode: string;
buildOptions: BuildOptionsLike;
}): {
fixedCssCode: string;
} {
const { cssCode, buildOptions } = params;
@ -17,7 +20,8 @@ export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOp
buildOptions.urlPathname === undefined
? /url\(["']?\/([^/][^)"']+)["']?\)/g
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
(...[, group]) => `url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/${group})`
(...[, group]) =>
`url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/${group})`
);
return { fixedCssCode };

View File

@ -13,7 +13,10 @@ export type BuildOptionsLike = {
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function replaceImportsInJsCode(params: { jsCode: string; buildOptions: BuildOptionsLike }) {
export function replaceImportsInJsCode(params: {
jsCode: string;
buildOptions: BuildOptionsLike;
}) {
const { jsCode, buildOptions } = params;
const { fixedJsCode } = (() => {
@ -22,8 +25,8 @@ export function replaceImportsInJsCode(params: { jsCode: string; buildOptions: B
return replaceImportsInJsCode_vite({
jsCode,
buildOptions,
"basenameOfAssetsFiles": readAssetsDirSync({
"assetsDirPath": params.buildOptions.assetsDirPath
basenameOfAssetsFiles: readAssetsDirSync({
assetsDirPath: params.buildOptions.assetsDirPath
})
});
case "webpack":

View File

@ -1,4 +1,7 @@
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../../shared/constants";
import {
nameOfTheGlobal,
basenameOfTheKeycloakifyResourcesDir
} from "../../../shared/constants";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "../../../shared/buildOptions";
import * as nodePath from "path";
@ -20,7 +23,12 @@ export function replaceImportsInJsCode_vite(params: {
}): {
fixedJsCode: string;
} {
const { jsCode, buildOptions, basenameOfAssetsFiles, systemType = nodePath.sep === "/" ? "posix" : "win32" } = params;
const {
jsCode,
buildOptions,
basenameOfAssetsFiles,
systemType = nodePath.sep === "/" ? "posix" : "win32"
} = params;
const { relative: pathRelative, sep: pathSep } = nodePath[systemType];
@ -38,22 +46,32 @@ export function replaceImportsInJsCode_vite(params: {
// Replace `Hv=function(e){return"/abcde12345/"+e}` by `Hv=function(e){return"/"+e}`
fixedJsCode = fixedJsCode.replace(
new RegExp(
`([\\w\\$][\\w\\d\\$]*)=function\\(([\\w\\$][\\w\\d\\$]*)\\)\\{return"${replaceAll(buildOptions.urlPathname, "/", "\\/")}"\\+\\2\\}`,
`([\\w\\$][\\w\\d\\$]*)=function\\(([\\w\\$][\\w\\d\\$]*)\\)\\{return"${replaceAll(
buildOptions.urlPathname,
"/",
"\\/"
)}"\\+\\2\\}`,
"g"
),
(...[, funcName, paramName]) => `${funcName}=function(${paramName}){return"/"+${paramName}}`
(...[, funcName, paramName]) =>
`${funcName}=function(${paramName}){return"/"+${paramName}}`
);
}
replace_javascript_relatives_import_paths: {
// Example: "assets/ or "foo/bar/"
const staticDir = (() => {
let out = pathRelative(buildOptions.reactAppBuildDirPath, buildOptions.assetsDirPath);
let out = pathRelative(
buildOptions.reactAppBuildDirPath,
buildOptions.assetsDirPath
);
out = replaceAll(out, pathSep, "/") + "/";
if (out === "/") {
throw new Error(`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`);
throw new Error(
`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`
);
}
return out;

View File

@ -1,4 +1,7 @@
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../../shared/constants";
import {
nameOfTheGlobal,
basenameOfTheKeycloakifyResourcesDir
} from "../../../shared/constants";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "../../../shared/buildOptions";
import * as nodePath from "path";
@ -12,10 +15,18 @@ export type BuildOptionsLike = {
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function replaceImportsInJsCode_webpack(params: { jsCode: string; buildOptions: BuildOptionsLike; systemType?: "posix" | "win32" }): {
export function replaceImportsInJsCode_webpack(params: {
jsCode: string;
buildOptions: BuildOptionsLike;
systemType?: "posix" | "win32";
}): {
fixedJsCode: string;
} {
const { jsCode, buildOptions, systemType = nodePath.sep === "/" ? "posix" : "win32" } = params;
const {
jsCode,
buildOptions,
systemType = nodePath.sep === "/" ? "posix" : "win32"
} = params;
const { relative: pathRelative, sep: pathSep } = nodePath[systemType];
@ -24,29 +35,51 @@ export function replaceImportsInJsCode_webpack(params: { jsCode: string; buildOp
if (buildOptions.urlPathname !== undefined) {
// "__esModule",{value:!0})},n.p="/foo-bar/",function(){if("undefined" -> ... n.p="/" ...
fixedJsCode = fixedJsCode.replace(
new RegExp(`,([a-zA-Z]\\.[a-zA-Z])="${replaceAll(buildOptions.urlPathname, "/", "\\/")}",`, "g"),
new RegExp(
`,([a-zA-Z]\\.[a-zA-Z])="${replaceAll(
buildOptions.urlPathname,
"/",
"\\/"
)}",`,
"g"
),
(...[, assignTo]) => `,${assignTo}="/",`
);
}
// Example: "static/ or "foo/bar/"
const staticDir = (() => {
let out = pathRelative(buildOptions.reactAppBuildDirPath, buildOptions.assetsDirPath);
let out = pathRelative(
buildOptions.reactAppBuildDirPath,
buildOptions.assetsDirPath
);
out = replaceAll(out, pathSep, "/") + "/";
if (out === "/") {
throw new Error(`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`);
throw new Error(
`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`
);
}
return out;
})();
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=(function\\(([a-z]+)\\){return|([a-z]+)=>)"${staticDir.replace(/\//g, "\\/")}${language}\\/"`, "g"),
const getReplaceArgs = (
language: "js" | "css"
): Parameters<typeof String.prototype.replace> => [
new RegExp(
`([a-zA-Z_]+)\\.([a-zA-Z]+)=(function\\(([a-z]+)\\){return|([a-z]+)=>)"${staticDir.replace(
/\//g,
"\\/"
)}${language}\\/"`,
"g"
),
(...[, n, u, matchedFunction, eForFunction]) => {
const isArrowFunction = matchedFunction.includes("=>");
const e = isArrowFunction ? matchedFunction.replace("=>", "").trim() : eForFunction;
const e = isArrowFunction
? matchedFunction.replace("=>", "").trim()
: eForFunction;
return `
${n}[(function(){
@ -58,7 +91,9 @@ export function replaceImportsInJsCode_webpack(params: { jsCode: string; buildOp
});
}
return "${u}";
})()] = ${isArrowFunction ? `${e} =>` : `function(${e}) { return `} "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}${language}/"`
})()] = ${
isArrowFunction ? `${e} =>` : `function(${e}) { return `
} "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}${language}/"`
.replace(/\s+/g, " ")
.trim();
}
@ -68,7 +103,10 @@ export function replaceImportsInJsCode_webpack(params: { jsCode: string; buildOp
.replace(...getReplaceArgs("js"))
.replace(...getReplaceArgs("css"))
.replace(
new RegExp(`[a-zA-Z]+\\.[a-zA-Z]+\\+"${staticDir.replace(/\//g, "\\/")}`, "g"),
new RegExp(
`[a-zA-Z]+\\.[a-zA-Z]+\\+"${staticDir.replace(/\//g, "\\/")}`,
"g"
),
`window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}`
);

View File

@ -10,12 +10,12 @@ export type CliCommandOptions = {
const program = termost<CliCommandOptions>(
{
"name": "keycloakify",
"description": "Keycloakify CLI",
"version": readThisNpmPackageVersion()
name: "keycloakify",
description: "Keycloakify CLI",
version: readThisNpmPackageVersion()
},
{
"onException": error => {
onException: error => {
console.error(error);
process.exit(1);
}
@ -25,8 +25,8 @@ const program = termost<CliCommandOptions>(
const optionsKeys: string[] = [];
program.option({
"key": "reactAppRootDirPath",
"name": (() => {
key: "reactAppRootDirPath",
name: (() => {
const long = "project";
const short = "p";
@ -34,19 +34,25 @@ program.option({
return { long, short };
})(),
"description": [
description: [
`For monorepos, path to the keycloakify project.`,
"Example: `npx keycloakify build --project packages/keycloak-theme`",
"https://docs.keycloakify.dev/build-options#project-or-p-cli-option"
].join(" "),
"defaultValue": undefined
defaultValue: undefined
});
function skip(_context: any, argv: { options: Record<string, unknown> }) {
const unrecognizedOptionKey = Object.keys(argv.options).find(key => !optionsKeys.includes(key));
const unrecognizedOptionKey = Object.keys(argv.options).find(
key => !optionsKeys.includes(key)
);
if (unrecognizedOptionKey !== undefined) {
console.error(`keycloakify: Unrecognized option: ${unrecognizedOptionKey.length === 1 ? "-" : "--"}${unrecognizedOptionKey}`);
console.error(
`keycloakify: Unrecognized option: ${
unrecognizedOptionKey.length === 1 ? "-" : "--"
}${unrecognizedOptionKey}`
);
process.exit(1);
}
@ -55,12 +61,12 @@ function skip(_context: any, argv: { options: Record<string, unknown> }) {
program
.command({
"name": "build",
"description": "Build the theme (default subcommand)."
name: "build",
description: "Build the theme (default subcommand)."
})
.task({
skip,
"handler": async cliCommandOptions => {
handler: async cliCommandOptions => {
const { command } = await import("./keycloakify");
await command({ cliCommandOptions });
@ -73,48 +79,55 @@ program
keycloakVersion: string | undefined;
realmJsonFilePath: string | undefined;
}>({
"name": "start-keycloak",
"description": "Spin up a pre configured Docker image of Keycloak to test your theme."
name: "start-keycloak",
description:
"Spin up a pre configured Docker image of Keycloak to test your theme."
})
.option({
"key": "port",
"name": (() => {
key: "port",
name: (() => {
const name = "port";
optionsKeys.push(name);
return name;
})(),
"description": ["Keycloak server port.", "Example `--port 8085`"].join(" "),
"defaultValue": 8080
description: ["Keycloak server port.", "Example `--port 8085`"].join(" "),
defaultValue: 8080
})
.option({
"key": "keycloakVersion",
"name": (() => {
key: "keycloakVersion",
name: (() => {
const name = "keycloak-version";
optionsKeys.push(name);
return name;
})(),
"description": ["Use a specific version of Keycloak.", "Example `--keycloak-version 21.1.1`"].join(" "),
"defaultValue": undefined
description: [
"Use a specific version of Keycloak.",
"Example `--keycloak-version 21.1.1`"
].join(" "),
defaultValue: undefined
})
.option({
"key": "realmJsonFilePath",
"name": (() => {
key: "realmJsonFilePath",
name: (() => {
const name = "import";
optionsKeys.push(name);
return name;
})(),
"defaultValue": undefined,
"description": ["Import your own realm configuration file", "Example `--import path/to/myrealm-realm.json`"].join(" ")
defaultValue: undefined,
description: [
"Import your own realm configuration file",
"Example `--import path/to/myrealm-realm.json`"
].join(" ")
})
.task({
skip,
"handler": async cliCommandOptions => {
handler: async cliCommandOptions => {
const { command } = await import("./start-keycloak");
await command({ cliCommandOptions });
@ -123,12 +136,12 @@ program
program
.command({
"name": "download-keycloak-default-theme",
"description": "Download the built-in Keycloak theme."
name: "download-keycloak-default-theme",
description: "Download the built-in Keycloak theme."
})
.task({
skip,
"handler": async cliCommandOptions => {
handler: async cliCommandOptions => {
const { command } = await import("./download-keycloak-default-theme");
await command({ cliCommandOptions });
@ -137,12 +150,12 @@ program
program
.command({
"name": "eject-page",
"description": "Eject a Keycloak page."
name: "eject-page",
description: "Eject a Keycloak page."
})
.task({
skip,
"handler": async cliCommandOptions => {
handler: async cliCommandOptions => {
const { command } = await import("./eject-page");
await command({ cliCommandOptions });
@ -151,12 +164,12 @@ program
program
.command({
"name": "initialize-email-theme",
"description": "Initialize an email theme."
name: "initialize-email-theme",
description: "Initialize an email theme."
})
.task({
skip,
"handler": async cliCommandOptions => {
handler: async cliCommandOptions => {
const { command } = await import("./initialize-email-theme");
await command({ cliCommandOptions });
@ -165,12 +178,13 @@ program
program
.command({
"name": "copy-keycloak-resources-to-public",
"description": "(Webpack/Create-React-App only) Copy Keycloak default theme resources to the public directory."
name: "copy-keycloak-resources-to-public",
description:
"(Webpack/Create-React-App only) Copy Keycloak default theme resources to the public directory."
})
.task({
skip,
"handler": async cliCommandOptions => {
handler: async cliCommandOptions => {
const { command } = await import("./copy-keycloak-resources-to-public");
await command({ cliCommandOptions });
@ -181,10 +195,17 @@ program
{
const [, , ...rest] = process.argv;
if (rest.length === 0 || (rest[0].startsWith("-") && rest[0] !== "--help" && rest[0] !== "-h")) {
const { status } = child_process.spawnSync("npx", ["keycloakify", "build", ...rest], {
"stdio": "inherit"
});
if (
rest.length === 0 ||
(rest[0].startsWith("-") && rest[0] !== "--help" && rest[0] !== "-h")
) {
const { status } = child_process.spawnSync(
"npx",
["keycloakify", "build", ...rest],
{
stdio: "inherit"
}
);
process.exit(status ?? 1);
}

View File

@ -1,4 +1,6 @@
export type KeycloakVersionRange = KeycloakVersionRange.WithAccountTheme | KeycloakVersionRange.WithoutAccountTheme;
export type KeycloakVersionRange =
| KeycloakVersionRange.WithAccountTheme
| KeycloakVersionRange.WithoutAccountTheme;
export namespace KeycloakVersionRange {
export type WithoutAccountTheme = "21-and-below" | "22-and-above";

View File

@ -49,7 +49,9 @@ export type ResolvedViteConfig = {
userProvidedBuildOptions: UserProvidedBuildOptions;
};
export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions }): BuildOptions {
export function readBuildOptions(params: {
cliCommandOptions: CliCommandOptions;
}): BuildOptions {
const { cliCommandOptions } = params;
const reactAppRootDirPath = (() => {
@ -58,29 +60,39 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
}
return getAbsoluteAndInOsFormatPath({
"pathIsh": cliCommandOptions.reactAppRootDirPath,
"cwd": process.cwd()
pathIsh: cliCommandOptions.reactAppRootDirPath,
cwd: process.cwd()
});
})();
const { resolvedViteConfig } = (() => {
if (fs.readdirSync(reactAppRootDirPath).find(fileBasename => fileBasename.startsWith("vite.config")) === undefined) {
return { "resolvedViteConfig": undefined };
if (
fs
.readdirSync(reactAppRootDirPath)
.find(fileBasename => fileBasename.startsWith("vite.config")) ===
undefined
) {
return { resolvedViteConfig: undefined };
}
const output = child_process
.execSync("npx vite", {
"cwd": reactAppRootDirPath,
"env": {
cwd: reactAppRootDirPath,
env: {
...process.env,
[vitePluginSubScriptEnvNames.resolveViteConfig]: "true"
}
})
.toString("utf8");
assert(output.includes(vitePluginSubScriptEnvNames.resolveViteConfig), "Seems like the Keycloakify's Vite plugin is not installed.");
assert(
output.includes(vitePluginSubScriptEnvNames.resolveViteConfig),
"Seems like the Keycloakify's Vite plugin is not installed."
);
const resolvedViteConfigStr = output.split(vitePluginSubScriptEnvNames.resolveViteConfig).reverse()[0];
const resolvedViteConfigStr = output
.split(vitePluginSubScriptEnvNames.resolveViteConfig)
.reverse()[0];
const resolvedViteConfig: ResolvedViteConfig = JSON.parse(resolvedViteConfigStr);
@ -92,22 +104,24 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
name: string;
version?: string;
homepage?: string;
keycloakify?: UserProvidedBuildOptions & { reactAppBuildDirPath?: string };
keycloakify?: UserProvidedBuildOptions & {
reactAppBuildDirPath?: string;
};
};
const zParsedPackageJson = z.object({
"name": z.string(),
"version": z.string().optional(),
"homepage": z.string().optional(),
"keycloakify": z
name: z.string(),
version: z.string().optional(),
homepage: z.string().optional(),
keycloakify: z
.object({
"extraThemeProperties": z.array(z.string()).optional(),
"artifactId": z.string().optional(),
"groupId": z.string().optional(),
"loginThemeResourcesFromKeycloakVersion": z.string().optional(),
"reactAppBuildDirPath": z.string().optional(),
"keycloakifyBuildDirPath": z.string().optional(),
"themeName": z.union([z.string(), z.array(z.string())]).optional()
extraThemeProperties: z.array(z.string()).optional(),
artifactId: z.string().optional(),
groupId: z.string().optional(),
loginThemeResourcesFromKeycloakVersion: z.string().optional(),
reactAppBuildDirPath: z.string().optional(),
keycloakifyBuildDirPath: z.string().optional(),
themeName: z.union([z.string(), z.array(z.string())]).optional()
})
.optional()
});
@ -119,7 +133,13 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
assert<Expected extends Got ? true : false>();
}
return zParsedPackageJson.parse(JSON.parse(fs.readFileSync(pathJoin(reactAppRootDirPath, "package.json")).toString("utf8")));
return zParsedPackageJson.parse(
JSON.parse(
fs
.readFileSync(pathJoin(reactAppRootDirPath, "package.json"))
.toString("utf8")
)
);
})();
const userProvidedBuildOptions: UserProvidedBuildOptions = {
@ -152,8 +172,8 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
if (parsedPackageJson.keycloakify?.reactAppBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": parsedPackageJson.keycloakify.reactAppBuildDirPath,
"cwd": reactAppRootDirPath
pathIsh: parsedPackageJson.keycloakify.reactAppBuildDirPath,
cwd: reactAppRootDirPath
});
}
@ -165,15 +185,16 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({
reactAppRootDirPath,
"dependencyExpected": "keycloakify"
dependencyExpected: "keycloakify"
});
return {
"bundler": resolvedViteConfig !== undefined ? "vite" : "webpack",
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0",
bundler: resolvedViteConfig !== undefined ? "vite" : "webpack",
themeVersion:
process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0",
themeNames,
"extraThemeProperties": userProvidedBuildOptions.extraThemeProperties,
"groupId": (() => {
extraThemeProperties: userProvidedBuildOptions.extraThemeProperties,
groupId: (() => {
const fallbackGroupId = `${themeNames[0]}.keycloak`;
return (
@ -188,24 +209,30 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
.join(".") ?? fallbackGroupId) + ".keycloak"
);
})(),
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? userProvidedBuildOptions.artifactId ?? `${themeNames[0]}-keycloak-theme`,
"loginThemeResourcesFromKeycloakVersion": userProvidedBuildOptions.loginThemeResourcesFromKeycloakVersion ?? "24.0.4",
artifactId:
process.env.KEYCLOAKIFY_ARTIFACT_ID ??
userProvidedBuildOptions.artifactId ??
`${themeNames[0]}-keycloak-theme`,
loginThemeResourcesFromKeycloakVersion:
userProvidedBuildOptions.loginThemeResourcesFromKeycloakVersion ?? "24.0.4",
reactAppRootDirPath,
reactAppBuildDirPath,
"keycloakifyBuildDirPath": (() => {
keycloakifyBuildDirPath: (() => {
if (userProvidedBuildOptions.keycloakifyBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": userProvidedBuildOptions.keycloakifyBuildDirPath,
"cwd": reactAppRootDirPath
pathIsh: userProvidedBuildOptions.keycloakifyBuildDirPath,
cwd: reactAppRootDirPath
});
}
return pathJoin(
reactAppRootDirPath,
resolvedViteConfig?.buildDir === undefined ? "build_keycloak" : `${resolvedViteConfig.buildDir}_keycloak`
resolvedViteConfig?.buildDir === undefined
? "build_keycloak"
: `${resolvedViteConfig.buildDir}_keycloak`
);
})(),
"publicDirPath": (() => {
publicDirPath: (() => {
webpack: {
if (resolvedViteConfig !== undefined) {
break webpack;
@ -213,8 +240,8 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
if (process.env.PUBLIC_DIR_PATH !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": process.env.PUBLIC_DIR_PATH,
"cwd": reactAppRootDirPath
pathIsh: process.env.PUBLIC_DIR_PATH,
cwd: reactAppRootDirPath
});
}
@ -223,13 +250,13 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
return pathJoin(reactAppRootDirPath, resolvedViteConfig.publicDir);
})(),
"cacheDirPath": (() => {
cacheDirPath: (() => {
const cacheDirPath = pathJoin(
(() => {
if (process.env.XDG_CACHE_HOME !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": process.env.XDG_CACHE_HOME,
"cwd": process.cwd()
pathIsh: process.env.XDG_CACHE_HOME,
cwd: process.cwd()
});
}
@ -240,7 +267,7 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
return cacheDirPath;
})(),
"urlPathname": (() => {
urlPathname: (() => {
webpack: {
if (resolvedViteConfig !== undefined) {
break webpack;
@ -264,7 +291,7 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
return resolvedViteConfig.urlPathname;
})(),
"assetsDirPath": (() => {
assetsDirPath: (() => {
webpack: {
if (resolvedViteConfig !== undefined) {
break webpack;

View File

@ -10,8 +10,8 @@ export const accountV1ThemeName = "account-v1";
export type ThemeType = (typeof themeTypes)[number];
export const vitePluginSubScriptEnvNames = {
"runPostBuildScript": "KEYCLOAKIFY_RUN_POST_BUILD_SCRIPT",
"resolveViteConfig": "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
runPostBuildScript: "KEYCLOAKIFY_RUN_POST_BUILD_SCRIPT",
resolveViteConfig: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
} as const;
export const skipBuildJarsEnvName = "KEYCLOAKIFY_SKIP_BUILD_JAR";

View File

@ -3,7 +3,11 @@ import {
type BuildOptionsLike as BuildOptionsLike_downloadKeycloakStaticResources
} from "./downloadKeycloakStaticResources";
import { join as pathJoin, relative as pathRelative } from "path";
import { themeTypes, keycloak_resources, lastKeycloakVersionWithAccountV1 } from "../shared/constants";
import {
themeTypes,
keycloak_resources,
lastKeycloakVersionWithAccountV1
} from "../shared/constants";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import { assert } from "tsafe/assert";
import * as fs from "fs";
@ -17,7 +21,9 @@ export type BuildOptionsLike = BuildOptionsLike_downloadKeycloakStaticResources
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function copyKeycloakResourcesToPublic(params: { buildOptions: BuildOptionsLike }) {
export async function copyKeycloakResourcesToPublic(params: {
buildOptions: BuildOptionsLike;
}) {
const { buildOptions } = params;
const destDirPath = pathJoin(buildOptions.publicDirPath, keycloak_resources);
@ -27,11 +33,14 @@ export async function copyKeycloakResourcesToPublic(params: { buildOptions: Buil
const keycloakifyBuildinfoRaw = JSON.stringify(
{
destDirPath,
"keycloakifyVersion": readThisNpmPackageVersion(),
"buildOptions": {
"loginThemeResourcesFromKeycloakVersion": readThisNpmPackageVersion(),
"cacheDirPath": pathRelative(destDirPath, buildOptions.cacheDirPath),
"npmWorkspaceRootDirPath": pathRelative(destDirPath, buildOptions.npmWorkspaceRootDirPath)
keycloakifyVersion: readThisNpmPackageVersion(),
buildOptions: {
loginThemeResourcesFromKeycloakVersion: readThisNpmPackageVersion(),
cacheDirPath: pathRelative(destDirPath, buildOptions.cacheDirPath),
npmWorkspaceRootDirPath: pathRelative(
destDirPath,
buildOptions.npmWorkspaceRootDirPath
)
}
},
null,
@ -43,7 +52,9 @@ export async function copyKeycloakResourcesToPublic(params: { buildOptions: Buil
break skip_if_already_done;
}
const keycloakifyBuildinfoRaw_previousRun = fs.readFileSync(keycloakifyBuildinfoFilePath).toString("utf8");
const keycloakifyBuildinfoRaw_previousRun = fs
.readFileSync(keycloakifyBuildinfoFilePath)
.toString("utf8");
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
break skip_if_already_done;
@ -52,15 +63,15 @@ export async function copyKeycloakResourcesToPublic(params: { buildOptions: Buil
return;
}
rmSync(destDirPath, { "force": true, "recursive": true });
rmSync(destDirPath, { force: true, recursive: true });
fs.mkdirSync(destDirPath, { "recursive": true });
fs.mkdirSync(destDirPath, { recursive: true });
fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
for (const themeType of themeTypes) {
await downloadKeycloakStaticResources({
"keycloakVersion": (() => {
keycloakVersion: (() => {
switch (themeType) {
case "login":
return buildOptions.loginThemeResourcesFromKeycloakVersion;
@ -69,7 +80,7 @@ export async function copyKeycloakResourcesToPublic(params: { buildOptions: Buil
}
})(),
themeType,
"themeDirPath": destDirPath,
themeDirPath: destDirPath,
buildOptions
});
}
@ -86,5 +97,8 @@ export async function copyKeycloakResourcesToPublic(params: { buildOptions: Buil
)
);
fs.writeFileSync(keycloakifyBuildinfoFilePath, Buffer.from(keycloakifyBuildinfoRaw, "utf8"));
fs.writeFileSync(
keycloakifyBuildinfoFilePath,
Buffer.from(keycloakifyBuildinfoRaw, "utf8")
);
}

View File

@ -28,22 +28,29 @@ export async function downloadAndUnzip(params: {
};
buildOptions: BuildOptionsLike;
}) {
const { url, destDirPath, specificDirsToExtract, preCacheTransform, buildOptions } = params;
const { url, destDirPath, specificDirsToExtract, preCacheTransform, buildOptions } =
params;
const { extractDirPath, zipFilePath } = (() => {
const zipFileBasenameWithoutExt = generateFileNameFromURL({
url,
"preCacheTransform":
preCacheTransform:
preCacheTransform === undefined
? undefined
: {
"actionCacheId": preCacheTransform.actionCacheId,
"actionFootprint": preCacheTransform.action.toString()
actionCacheId: preCacheTransform.actionCacheId,
actionFootprint: preCacheTransform.action.toString()
}
});
const zipFilePath = pathJoin(buildOptions.cacheDirPath, `${zipFileBasenameWithoutExt}.zip`);
const extractDirPath = pathJoin(buildOptions.cacheDirPath, `tmp_unzip_${zipFileBasenameWithoutExt}`);
const zipFilePath = pathJoin(
buildOptions.cacheDirPath,
`${zipFileBasenameWithoutExt}.zip`
);
const extractDirPath = pathJoin(
buildOptions.cacheDirPath,
`tmp_unzip_${zipFileBasenameWithoutExt}`
);
return { zipFilePath, extractDirPath };
})();
@ -55,28 +62,30 @@ export async function downloadAndUnzip(params: {
const { response, isFromRemoteCache } = await (async () => {
const proxyFetchOptions = await getProxyFetchOptions({
"npmWorkspaceRootDirPath": buildOptions.npmWorkspaceRootDirPath
npmWorkspaceRootDirPath: buildOptions.npmWorkspaceRootDirPath
});
const response = await fetch(
`https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/${pathBasename(zipFilePath)}`,
`https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/${pathBasename(
zipFilePath
)}`,
proxyFetchOptions
);
if (response.status === 200) {
return {
response,
"isFromRemoteCache": true
isFromRemoteCache: true
};
}
return {
"response": await fetch(url, proxyFetchOptions),
"isFromRemoteCache": false
response: await fetch(url, proxyFetchOptions),
isFromRemoteCache: false
};
})();
await mkdir(pathDirname(zipFilePath), { "recursive": true });
await mkdir(pathDirname(zipFilePath), { recursive: true });
/**
* The correct way to fix this is to upgrade node-fetch beyond 3.2.5
@ -102,10 +111,13 @@ export async function downloadAndUnzip(params: {
try {
await preCacheTransform?.action({
"destDirPath": extractDirPath
destDirPath: extractDirPath
});
} catch (error) {
await Promise.all([rm(extractDirPath, { "recursive": true }), unlink(zipFilePath)]);
await Promise.all([
rm(extractDirPath, { recursive: true }),
unlink(zipFilePath)
]);
throw error;
}
@ -114,10 +126,11 @@ export async function downloadAndUnzip(params: {
await zip(extractDirPath, zipFilePath);
await rm(extractDirPath, { "recursive": true });
await rm(extractDirPath, { recursive: true });
upload_to_remote_cache_if_admin: {
const githubToken = process.env["KEYCLOAKIFY_ADMIN_GITHUB_PERSONAL_ACCESS_TOKEN"];
const githubToken =
process.env["KEYCLOAKIFY_ADMIN_GITHUB_PERSONAL_ACCESS_TOKEN"];
if (!githubToken) {
break upload_to_remote_cache_if_admin;
@ -145,7 +158,9 @@ export async function downloadAndUnzip(params: {
githubToken
]);
} catch {
console.log("upload failed, asset probably already exists in remote cache");
console.log(
"upload failed, asset probably already exists in remote cache"
);
}
}
}
@ -153,11 +168,11 @@ export async function downloadAndUnzip(params: {
await unzip(zipFilePath, extractDirPath);
transformCodebase({
"srcDirPath": extractDirPath,
"destDirPath": destDirPath
srcDirPath: extractDirPath,
destDirPath: destDirPath
});
await rm(extractDirPath, { "recursive": true });
await rm(extractDirPath, { recursive: true });
}
function generateFileNameFromURL(params: {
@ -194,9 +209,15 @@ function generateFileNameFromURL(params: {
}
// Sanitize actionCacheId the same way as other components
const sanitizedActionCacheId = preCacheTransform.actionCacheId.replace(/[^a-zA-Z0-9-_]/g, "_");
const sanitizedActionCacheId = preCacheTransform.actionCacheId.replace(
/[^a-zA-Z0-9-_]/g,
"_"
);
fileName += `_${sanitizedActionCacheId}_${createHash("sha256").update(preCacheTransform.actionFootprint).digest("hex").substring(0, 5)}`;
fileName += `_${sanitizedActionCacheId}_${createHash("sha256")
.update(preCacheTransform.actionFootprint)
.digest("hex")
.substring(0, 5)}`;
}
return fileName;

View File

@ -15,25 +15,38 @@ export type BuildOptionsLike = {
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function downloadKeycloakDefaultTheme(params: { keycloakVersion: string; destDirPath: string; buildOptions: BuildOptionsLike }) {
export async function downloadKeycloakDefaultTheme(params: {
keycloakVersion: string;
destDirPath: string;
buildOptions: BuildOptionsLike;
}) {
const { keycloakVersion, destDirPath, buildOptions } = params;
await downloadAndUnzip({
destDirPath,
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
"specificDirsToExtract": ["", "-community"].map(ext => `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`),
url: `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
specificDirsToExtract: ["", "-community"].map(
ext => `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`
),
buildOptions,
"preCacheTransform": {
"actionCacheId": "npm install and build",
"action": async ({ destDirPath }) => {
preCacheTransform: {
actionCacheId: "npm install and build",
action: async ({ destDirPath }) => {
install_common_node_modules: {
const commonResourcesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources");
const commonResourcesDirPath = pathJoin(
destDirPath,
"keycloak",
"common",
"resources"
);
if (!fs.existsSync(commonResourcesDirPath)) {
break install_common_node_modules;
}
if (!fs.existsSync(pathJoin(commonResourcesDirPath, "package.json"))) {
if (
!fs.existsSync(pathJoin(commonResourcesDirPath, "package.json"))
) {
break install_common_node_modules;
}
@ -42,36 +55,67 @@ export async function downloadKeycloakDefaultTheme(params: { keycloakVersion: st
}
child_process.execSync("npm install --omit=dev", {
"cwd": commonResourcesDirPath,
"stdio": "ignore"
cwd: commonResourcesDirPath,
stdio: "ignore"
});
}
repatriate_common_resources_from_base_login_theme: {
const baseLoginThemeResourceDir = pathJoin(destDirPath, "base", "login", "resources");
const baseLoginThemeResourceDir = pathJoin(
destDirPath,
"base",
"login",
"resources"
);
if (!fs.existsSync(baseLoginThemeResourceDir)) {
break repatriate_common_resources_from_base_login_theme;
}
transformCodebase({
"srcDirPath": baseLoginThemeResourceDir,
"destDirPath": pathJoin(destDirPath, "keycloak", "login", "resources")
srcDirPath: baseLoginThemeResourceDir,
destDirPath: pathJoin(
destDirPath,
"keycloak",
"login",
"resources"
)
});
}
install_and_move_to_common_resources_generated_in_keycloak_v2: {
if (!fs.readFileSync(pathJoin(destDirPath, "keycloak", "login", "theme.properties")).toString("utf8").includes("web_modules")) {
if (
!fs
.readFileSync(
pathJoin(
destDirPath,
"keycloak",
"login",
"theme.properties"
)
)
.toString("utf8")
.includes("web_modules")
) {
break install_and_move_to_common_resources_generated_in_keycloak_v2;
}
const accountV2DirSrcDirPath = pathJoin(destDirPath, "keycloak.v2", "account", "src");
const accountV2DirSrcDirPath = pathJoin(
destDirPath,
"keycloak.v2",
"account",
"src"
);
if (!fs.existsSync(accountV2DirSrcDirPath)) {
break install_and_move_to_common_resources_generated_in_keycloak_v2;
}
const packageManager = fs.existsSync(pathJoin(accountV2DirSrcDirPath, "pnpm-lock.yaml")) ? "pnpm" : "npm";
const packageManager = fs.existsSync(
pathJoin(accountV2DirSrcDirPath, "pnpm-lock.yaml")
)
? "pnpm"
: "npm";
if (packageManager === "pnpm") {
try {
@ -82,9 +126,15 @@ export async function downloadKeycloakDefaultTheme(params: { keycloakVersion: st
}
}
child_process.execSync(`${packageManager} install`, { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
child_process.execSync(`${packageManager} install`, {
cwd: accountV2DirSrcDirPath,
stdio: "ignore"
});
const packageJsonFilePath = pathJoin(accountV2DirSrcDirPath, "package.json");
const packageJsonFilePath = pathJoin(
accountV2DirSrcDirPath,
"package.json"
);
const packageJsonRaw = fs.readFileSync(packageJsonFilePath);
@ -94,13 +144,21 @@ export async function downloadKeycloakDefaultTheme(params: { keycloakVersion: st
.replace(`${packageManager} run check-types`, "true")
.replace(`${packageManager} run babel`, "true");
fs.writeFileSync(packageJsonFilePath, Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8"));
fs.writeFileSync(
packageJsonFilePath,
Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8")
);
child_process.execSync(`${packageManager} run build`, { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
child_process.execSync(`${packageManager} run build`, {
cwd: accountV2DirSrcDirPath,
stdio: "ignore"
});
fs.writeFileSync(packageJsonFilePath, packageJsonRaw);
fs.rmSync(pathJoin(accountV2DirSrcDirPath, "node_modules"), { "recursive": true });
fs.rmSync(pathJoin(accountV2DirSrcDirPath, "node_modules"), {
recursive: true
});
}
remove_keycloak_v2: {
@ -110,12 +168,18 @@ export async function downloadKeycloakDefaultTheme(params: { keycloakVersion: st
break remove_keycloak_v2;
}
rmSync(keycloakV2DirPath, { "recursive": true });
rmSync(keycloakV2DirPath, { recursive: true });
}
// Note, this is an optimization for reducing the size of the jar
remove_unused_node_modules: {
const nodeModuleDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "node_modules");
const nodeModuleDirPath = pathJoin(
destDirPath,
"keycloak",
"common",
"resources",
"node_modules"
);
if (!fs.existsSync(nodeModuleDirPath)) {
break remove_unused_node_modules;
@ -167,18 +231,26 @@ export async function downloadKeycloakDefaultTheme(params: { keycloakVersion: st
];
transformCodebase({
"srcDirPath": nodeModuleDirPath,
"destDirPath": nodeModuleDirPath,
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
srcDirPath: nodeModuleDirPath,
destDirPath: nodeModuleDirPath,
transformSourceCode: ({ sourceCode, fileRelativePath }) => {
if (fileRelativePath.endsWith(".map")) {
return undefined;
}
if (toDeletePerfixes.find(prefix => fileRelativePath.startsWith(prefix)) !== undefined) {
if (
toDeletePerfixes.find(prefix =>
fileRelativePath.startsWith(prefix)
) !== undefined
) {
return undefined;
}
if (fileRelativePath.startsWith(pathJoin("patternfly", "dist", "fonts"))) {
if (
fileRelativePath.startsWith(
pathJoin("patternfly", "dist", "fonts")
)
) {
if (
!fileRelativePath.endsWith(".woff2") &&
!fileRelativePath.endsWith(".woff") &&
@ -188,34 +260,50 @@ export async function downloadKeycloakDefaultTheme(params: { keycloakVersion: st
}
}
return { "modifiedSourceCode": sourceCode };
return { modifiedSourceCode: sourceCode };
}
});
}
// Just like node_modules
remove_unused_lib: {
const libDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "lib");
const libDirPath = pathJoin(
destDirPath,
"keycloak",
"common",
"resources",
"lib"
);
if (!fs.existsSync(libDirPath)) {
break remove_unused_lib;
}
const toDeletePerfixes = ["ui-ace", "filesaver", "fileupload", "angular", "ui-ace"];
const toDeletePerfixes = [
"ui-ace",
"filesaver",
"fileupload",
"angular",
"ui-ace"
];
transformCodebase({
"srcDirPath": libDirPath,
"destDirPath": libDirPath,
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
srcDirPath: libDirPath,
destDirPath: libDirPath,
transformSourceCode: ({ sourceCode, fileRelativePath }) => {
if (fileRelativePath.endsWith(".map")) {
return undefined;
}
if (toDeletePerfixes.find(prefix => fileRelativePath.startsWith(prefix)) !== undefined) {
if (
toDeletePerfixes.find(prefix =>
fileRelativePath.startsWith(prefix)
) !== undefined
) {
return undefined;
}
return { "modifiedSourceCode": sourceCode };
return { modifiedSourceCode: sourceCode };
}
});
}
@ -226,34 +314,61 @@ export async function downloadKeycloakDefaultTheme(params: { keycloakVersion: st
}
{
const accountCssFilePath = pathJoin(destDirPath, "keycloak", "account", "resources", "css", "account.css");
const accountCssFilePath = pathJoin(
destDirPath,
"keycloak",
"account",
"resources",
"css",
"account.css"
);
fs.writeFileSync(
accountCssFilePath,
Buffer.from(fs.readFileSync(accountCssFilePath).toString("utf8").replace("top: -34px;", "top: -34px !important;"), "utf8")
Buffer.from(
fs
.readFileSync(accountCssFilePath)
.toString("utf8")
.replace("top: -34px;", "top: -34px !important;"),
"utf8"
)
);
}
// Note, this is an optimization for reducing the size of the jar,
// For this version we know exactly which resources are used.
{
const nodeModulesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "node_modules");
const nodeModulesDirPath = pathJoin(
destDirPath,
"keycloak",
"common",
"resources",
"node_modules"
);
const toKeepPrefixes = [
...["patternfly.min.css", "patternfly-additions.min.css", "patternfly-additions.min.css"].map(fileBasename =>
...[
"patternfly.min.css",
"patternfly-additions.min.css",
"patternfly-additions.min.css"
].map(fileBasename =>
pathJoin("patternfly", "dist", "css", fileBasename)
),
pathJoin("patternfly", "dist", "fonts")
];
transformCodebase({
"srcDirPath": nodeModulesDirPath,
"destDirPath": nodeModulesDirPath,
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
if (toKeepPrefixes.find(prefix => fileRelativePath.startsWith(prefix)) === undefined) {
srcDirPath: nodeModulesDirPath,
destDirPath: nodeModulesDirPath,
transformSourceCode: ({ sourceCode, fileRelativePath }) => {
if (
toKeepPrefixes.find(prefix =>
fileRelativePath.startsWith(prefix)
) === undefined
) {
return undefined;
}
return { "modifiedSourceCode": sourceCode };
return { modifiedSourceCode: sourceCode };
}
});
}

View File

@ -24,26 +24,30 @@ export async function downloadKeycloakStaticResources(params: {
const tmpDirPath = pathJoin(
buildOptions.cacheDirPath,
`downloadKeycloakStaticResources_tmp_${crypto.createHash("sha256").update(`${themeType}-${keycloakVersion}`).digest("hex").slice(0, 8)}`
`downloadKeycloakStaticResources_tmp_${crypto
.createHash("sha256")
.update(`${themeType}-${keycloakVersion}`)
.digest("hex")
.slice(0, 8)}`
);
await downloadKeycloakDefaultTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
destDirPath: tmpDirPath,
buildOptions
});
const resourcesPath = pathJoin(themeDirPath, themeType, "resources");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", themeType, "resources"),
"destDirPath": resourcesPath
srcDirPath: pathJoin(tmpDirPath, "keycloak", themeType, "resources"),
destDirPath: resourcesPath
});
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(resourcesPath, resources_common)
srcDirPath: pathJoin(tmpDirPath, "keycloak", "common", "resources"),
destDirPath: pathJoin(resourcesPath, resources_common)
});
rmSync(tmpDirPath, { "recursive": true });
rmSync(tmpDirPath, { recursive: true });
}

View File

@ -1,6 +1,8 @@
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
export function getJarFileBasename(params: { keycloakVersionRange: KeycloakVersionRange }) {
export function getJarFileBasename(params: {
keycloakVersionRange: KeycloakVersionRange;
}) {
const { keycloakVersionRange } = params;
const jarFileBasename = `keycloak-theme-for-kc-${keycloakVersionRange}.jar`;

View File

@ -12,7 +12,10 @@ export function getThemeSrcDirPath(params: { reactAppRootDirPath: string }) {
const srcDirPath = pathJoin(reactAppRootDirPath, "src");
const themeSrcDirPath: string | undefined = crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })
const themeSrcDirPath: string | undefined = crawl({
dirPath: srcDirPath,
returnedPathsType: "relative to dirPath"
})
.map(fileRelativePath => {
for (const themeSrcDirBasename of themeSrcDirBasenames) {
const split = fileRelativePath.split(themeSrcDirBasename);
@ -32,7 +35,7 @@ export function getThemeSrcDirPath(params: { reactAppRootDirPath: string }) {
if (!fs.existsSync(pathJoin(srcDirPath, themeType))) {
continue;
}
return { "themeSrcDirPath": srcDirPath };
return { themeSrcDirPath: srcDirPath };
}
console.error(

View File

@ -6,29 +6,56 @@ export type MetaInfKeycloakTheme = {
themes: { name: string; types: (ThemeType | "email")[] }[];
};
export function getMetaInfKeycloakThemesJsonFilePath(params: { keycloakifyBuildDirPath: string }) {
export function getMetaInfKeycloakThemesJsonFilePath(params: {
keycloakifyBuildDirPath: string;
}) {
const { keycloakifyBuildDirPath } = params;
return pathJoin(keycloakifyBuildDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json");
return pathJoin(
keycloakifyBuildDirPath,
"src",
"main",
"resources",
"META-INF",
"keycloak-themes.json"
);
}
export function readMetaInfKeycloakThemes(params: { keycloakifyBuildDirPath: string }): MetaInfKeycloakTheme {
export function readMetaInfKeycloakThemes(params: {
keycloakifyBuildDirPath: string;
}): MetaInfKeycloakTheme {
const { keycloakifyBuildDirPath } = params;
return JSON.parse(fs.readFileSync(getMetaInfKeycloakThemesJsonFilePath({ keycloakifyBuildDirPath })).toString("utf8")) as MetaInfKeycloakTheme;
return JSON.parse(
fs
.readFileSync(
getMetaInfKeycloakThemesJsonFilePath({
keycloakifyBuildDirPath
})
)
.toString("utf8")
) as MetaInfKeycloakTheme;
}
export function writeMetaInfKeycloakThemes(params: { keycloakifyBuildDirPath: string; metaInfKeycloakThemes: MetaInfKeycloakTheme }) {
export function writeMetaInfKeycloakThemes(params: {
keycloakifyBuildDirPath: string;
metaInfKeycloakThemes: MetaInfKeycloakTheme;
}) {
const { keycloakifyBuildDirPath, metaInfKeycloakThemes } = params;
const metaInfKeycloakThemesJsonPath = getMetaInfKeycloakThemesJsonFilePath({ keycloakifyBuildDirPath });
const metaInfKeycloakThemesJsonPath = getMetaInfKeycloakThemesJsonFilePath({
keycloakifyBuildDirPath
});
{
const dirPath = pathDirname(metaInfKeycloakThemesJsonPath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { "recursive": true });
fs.mkdirSync(dirPath, { recursive: true });
}
}
fs.writeFileSync(metaInfKeycloakThemesJsonPath, Buffer.from(JSON.stringify(metaInfKeycloakThemes, null, 2), "utf8"));
fs.writeFileSync(
metaInfKeycloakThemesJsonPath,
Buffer.from(JSON.stringify(metaInfKeycloakThemes, null, 2), "utf8")
);
}

View File

@ -7,19 +7,26 @@ import * as fs from "fs";
import type { ReturnType } from "tsafe";
import { id } from "tsafe/id";
export async function promptKeycloakVersion(params: { startingFromMajor: number | undefined; cacheDirPath: string }) {
export async function promptKeycloakVersion(params: {
startingFromMajor: number | undefined;
cacheDirPath: string;
}) {
const { startingFromMajor, cacheDirPath } = params;
const { getLatestsSemVersionedTag } = (() => {
const { octokit } = (() => {
const githubToken = process.env.GITHUB_TOKEN;
const octokit = new Octokit(githubToken === undefined ? undefined : { "auth": githubToken });
const octokit = new Octokit(
githubToken === undefined ? undefined : { auth: githubToken }
);
return { octokit };
})();
const { getLatestsSemVersionedTag } = getLatestsSemVersionedTagFactory({ octokit });
const { getLatestsSemVersionedTag } = getLatestsSemVersionedTagFactory({
octokit
});
return { getLatestsSemVersionedTag };
})();
@ -39,7 +46,9 @@ export async function promptKeycloakVersion(params: { startingFromMajor: number
break use_cache;
}
const cache: Cache = JSON.parse(fs.readFileSync(cacheFilePath).toString("utf8"));
const cache: Cache = JSON.parse(
fs.readFileSync(cacheFilePath).toString("utf8")
);
if (Date.now() - cache.time > 3_600_000) {
fs.unlinkSync(cacheFilePath);
@ -50,16 +59,16 @@ export async function promptKeycloakVersion(params: { startingFromMajor: number
}
const semVersionedTags = await getLatestsSemVersionedTag({
"count": 50,
"owner": "keycloak",
"repo": "keycloak"
count: 50,
owner: "keycloak",
repo: "keycloak"
});
{
const dirPath = pathDirname(cacheFilePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { "recursive": true });
fs.mkdirSync(dirPath, { recursive: true });
}
}
@ -67,7 +76,7 @@ export async function promptKeycloakVersion(params: { startingFromMajor: number
cacheFilePath,
JSON.stringify(
id<Cache>({
"time": Date.now(),
time: Date.now(),
semVersionedTags
}),
null,
@ -79,23 +88,33 @@ export async function promptKeycloakVersion(params: { startingFromMajor: number
})();
semVersionedTags.forEach(semVersionedTag => {
if (startingFromMajor !== undefined && semVersionedTag.version.major < startingFromMajor) {
if (
startingFromMajor !== undefined &&
semVersionedTag.version.major < startingFromMajor
) {
return;
}
const currentSemVersionedTag = semVersionedTagByMajor.get(semVersionedTag.version.major);
const currentSemVersionedTag = semVersionedTagByMajor.get(
semVersionedTag.version.major
);
if (currentSemVersionedTag !== undefined && SemVer.compare(semVersionedTag.version, currentSemVersionedTag.version) === -1) {
if (
currentSemVersionedTag !== undefined &&
SemVer.compare(semVersionedTag.version, currentSemVersionedTag.version) === -1
) {
return;
}
semVersionedTagByMajor.set(semVersionedTag.version.major, semVersionedTag);
});
const lastMajorVersions = Array.from(semVersionedTagByMajor.values()).map(({ tag }) => tag);
const lastMajorVersions = Array.from(semVersionedTagByMajor.values()).map(
({ tag }) => tag
);
const { value } = await cliSelect<string>({
"values": lastMajorVersions
values: lastMajorVersions
}).catch(() => {
process.exit(-1);
});

View File

@ -407,7 +407,11 @@
"otpPolicyLookAheadWindow": 1,
"otpPolicyPeriod": 30,
"otpPolicyCodeReusable": false,
"otpSupportedApplications": ["totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName"],
"otpSupportedApplications": [
"totpAppFreeOTPName",
"totpAppGoogleName",
"totpAppMicrosoftAuthenticatorName"
],
"localizationTexts": {},
"webAuthnPolicyRpEntityName": "keycloak",
"webAuthnPolicySignatureAlgorithms": ["ES256"],
@ -520,7 +524,12 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"],
"optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"]
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
]
},
{
"id": "bb874a19-9a92-4504-a7c0-d32ef6f3d266",
@ -562,7 +571,12 @@
}
],
"defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"],
"optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"]
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
]
},
{
"id": "e19ed660-ca30-4266-be25-7ac226994f0f",
@ -589,7 +603,12 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"],
"optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"]
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
]
},
{
"id": "2c000747-ea5b-4226-82a3-1fc32bc8a079",
@ -616,7 +635,12 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"],
"optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"]
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
]
},
{
"id": "5dfb0d1a-2ab1-480f-8bab-81502f762ac0",
@ -761,7 +785,12 @@
}
],
"defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"],
"optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"]
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
]
},
{
"id": "8efd01a0-40cd-487d-818b-fa2b096170e0",
@ -788,7 +817,12 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"],
"optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"]
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
]
},
{
"id": "199b2e1b-2cdd-4d8b-8816-4a5c79741658",
@ -838,7 +872,12 @@
}
],
"defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"],
"optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"]
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
]
}
],
"clientScopes": [
@ -1379,8 +1418,20 @@
]
}
],
"defaultDefaultClientScopes": ["role_list", "profile", "email", "roles", "web-origins", "acr"],
"defaultOptionalClientScopes": ["offline_access", "address", "phone", "microprofile-jwt"],
"defaultDefaultClientScopes": [
"role_list",
"profile",
"email",
"roles",
"web-origins",
"acr"
],
"defaultOptionalClientScopes": [
"offline_access",
"address",
"phone",
"microprofile-jwt"
],
"browserSecurityHeaders": {
"contentSecurityPolicyReportOnly": "",
"xContentTypeOptions": "nosniff",
@ -1575,7 +1626,19 @@
]
},
"internationalizationEnabled": true,
"supportedLocales": ["ar", "de", "fi", "el", "ja", "en", "fa", "it", "fr", "ca", "es"],
"supportedLocales": [
"ar",
"de",
"fi",
"el",
"ja",
"en",
"fa",
"it",
"fr",
"ca",
"es"
],
"defaultLocale": "en",
"authenticationFlows": [
{

View File

@ -2,13 +2,22 @@ import { readBuildOptions } from "../shared/buildOptions";
import type { CliCommandOptions as CliCommandOptions_common } from "../main";
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
import { readMetaInfKeycloakThemes } from "../shared/metaInfKeycloakThemes";
import { accountV1ThemeName, skipBuildJarsEnvName, containerName } from "../shared/constants";
import {
accountV1ThemeName,
skipBuildJarsEnvName,
containerName
} from "../shared/constants";
import { SemVer } from "../tools/SemVer";
import type { KeycloakVersionRange } from "../shared/KeycloakVersionRange";
import { getJarFileBasename } from "../shared/getJarFileBasename";
import { assert, type Equals } from "tsafe/assert";
import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, sep as pathSep, posix as pathPosix } from "path";
import {
join as pathJoin,
relative as pathRelative,
sep as pathSep,
posix as pathPosix
} from "path";
import * as child_process from "child_process";
import chalk from "chalk";
import chokidar from "chokidar";
@ -30,7 +39,9 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
let commandOutput: Buffer | undefined = undefined;
try {
commandOutput = child_process.execSync("docker --version", { "stdio": ["ignore", "pipe", "ignore"] });
commandOutput = child_process.execSync("docker --version", {
stdio: ["ignore", "pipe", "ignore"]
});
} catch {}
if (commandOutput?.toString("utf8").includes("Docker")) {
@ -40,7 +51,9 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
console.log(
[
`${chalk.red("Docker required.")}`,
`Install it with Docker Desktop: ${chalk.bold.underline("https://www.docker.com/products/docker-desktop/")}`,
`Install it with Docker Desktop: ${chalk.bold.underline(
"https://www.docker.com/products/docker-desktop/"
)}`,
`(or any other way)`
].join(" ")
);
@ -52,7 +65,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
let isDockerRunning: boolean;
try {
child_process.execSync("docker info", { "stdio": "ignore" });
child_process.execSync("docker info", { stdio: "ignore" });
isDockerRunning = true;
} catch {
isDockerRunning = false;
@ -62,7 +75,12 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
break exit_if_docker_not_running;
}
console.log([`${chalk.red("Docker daemon is not running.")}`, `Please start Docker Desktop and try again.`].join(" "));
console.log(
[
`${chalk.red("Docker daemon is not running.")}`,
`Please start Docker Desktop and try again.`
].join(" ")
);
process.exit(1);
}
@ -77,51 +95,60 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
}
console.log(
[`${chalk.red("The theme has not been built.")}`, `Please run ${chalk.bold("npx vite && npx keycloakify build")} first.`].join(" ")
[
`${chalk.red("The theme has not been built.")}`,
`Please run ${chalk.bold("npx vite && npx keycloakify build")} first.`
].join(" ")
);
process.exit(1);
}
const metaInfKeycloakThemes = readMetaInfKeycloakThemes({
"keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath
keycloakifyBuildDirPath: buildOptions.keycloakifyBuildDirPath
});
const doesImplementAccountTheme = metaInfKeycloakThemes.themes.some(({ name }) => name === accountV1ThemeName);
const doesImplementAccountTheme = metaInfKeycloakThemes.themes.some(
({ name }) => name === accountV1ThemeName
);
const { keycloakVersion, keycloakMajorNumber: keycloakMajorVersionNumber } = await (async function getKeycloakMajor(): Promise<{
keycloakVersion: string;
keycloakMajorNumber: number;
}> {
if (cliCommandOptions.keycloakVersion !== undefined) {
return {
"keycloakVersion": cliCommandOptions.keycloakVersion,
"keycloakMajorNumber": SemVer.parse(cliCommandOptions.keycloakVersion).major
};
}
const { keycloakVersion, keycloakMajorNumber: keycloakMajorVersionNumber } =
await (async function getKeycloakMajor(): Promise<{
keycloakVersion: string;
keycloakMajorNumber: number;
}> {
if (cliCommandOptions.keycloakVersion !== undefined) {
return {
keycloakVersion: cliCommandOptions.keycloakVersion,
keycloakMajorNumber: SemVer.parse(cliCommandOptions.keycloakVersion)
.major
};
}
console.log(chalk.cyan("On which version of Keycloak do you want to test your theme?"));
const { keycloakVersion } = await promptKeycloakVersion({
"startingFromMajor": 17,
"cacheDirPath": buildOptions.cacheDirPath
});
console.log(`${keycloakVersion}`);
const keycloakMajorNumber = SemVer.parse(keycloakVersion).major;
if (doesImplementAccountTheme && keycloakMajorNumber === 22) {
console.log(
[
"Unfortunately, Keycloakify themes that implements an account theme do not work on Keycloak 22",
"Please select any other Keycloak version"
].join(" ")
chalk.cyan("On which version of Keycloak do you want to test your theme?")
);
return getKeycloakMajor();
}
return { keycloakVersion, keycloakMajorNumber };
})();
const { keycloakVersion } = await promptKeycloakVersion({
startingFromMajor: 17,
cacheDirPath: buildOptions.cacheDirPath
});
console.log(`${keycloakVersion}`);
const keycloakMajorNumber = SemVer.parse(keycloakVersion).major;
if (doesImplementAccountTheme && keycloakMajorNumber === 22) {
console.log(
[
"Unfortunately, Keycloakify themes that implements an account theme do not work on Keycloak 22",
"Please select any other Keycloak version"
].join(" ")
);
return getKeycloakMajor();
}
return { keycloakVersion, keycloakMajorNumber };
})();
const keycloakVersionRange: KeycloakVersionRange = (() => {
if (doesImplementAccountTheme) {
@ -139,7 +166,9 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
return "24-and-above" as const;
})();
assert<Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithAccountTheme>>();
assert<
Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithAccountTheme>
>();
return keycloakVersionRange;
} else {
@ -151,7 +180,12 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
return "22-and-above" as const;
})();
assert<Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithoutAccountTheme>>();
assert<
Equals<
typeof keycloakVersionRange,
KeycloakVersionRange.WithoutAccountTheme
>
>();
return keycloakVersionRange;
}
@ -163,7 +197,9 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const mountTargets = buildOptions.themeNames
.map(themeName => {
const themeEntry = metaInfKeycloakThemes.themes.find(({ name }) => name === themeName);
const themeEntry = metaInfKeycloakThemes.themes.find(
({ name }) => name === themeName
);
assert(themeEntry !== undefined);
@ -181,10 +217,24 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
return fs
.readdirSync(localPathDirname)
.filter(fileOrDirectoryBasename => !fileOrDirectoryBasename.endsWith(".properties"))
.filter(
fileOrDirectoryBasename =>
!fileOrDirectoryBasename.endsWith(".properties")
)
.map(fileOrDirectoryBasename => ({
"localPath": pathJoin(localPathDirname, fileOrDirectoryBasename),
"containerPath": pathPosix.join("/", "opt", "keycloak", "themes", themeName, themeType, fileOrDirectoryBasename)
localPath: pathJoin(
localPathDirname,
fileOrDirectoryBasename
),
containerPath: pathPosix.join(
"/",
"opt",
"keycloak",
"themes",
themeName,
themeType,
fileOrDirectoryBasename
)
}));
})
.flat();
@ -192,33 +242,56 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
.flat();
try {
child_process.execSync(`docker rm --force ${containerName}`, { "stdio": "ignore" });
child_process.execSync(`docker rm --force ${containerName}`, {
stdio: "ignore"
});
} catch {}
const realmJsonFilePath = await (async () => {
if (cliCommandOptions.realmJsonFilePath !== undefined) {
console.log(chalk.green(`Using realm json file: ${cliCommandOptions.realmJsonFilePath}`));
console.log(
chalk.green(
`Using realm json file: ${cliCommandOptions.realmJsonFilePath}`
)
);
return getAbsoluteAndInOsFormatPath({
"pathIsh": cliCommandOptions.realmJsonFilePath,
"cwd": process.cwd()
pathIsh: cliCommandOptions.realmJsonFilePath,
cwd: process.cwd()
});
}
const dirPath = pathJoin(getThisCodebaseRootDirPath(), "src", "bin", "start-keycloak");
const dirPath = pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"start-keycloak"
);
const filePath = pathJoin(dirPath, `myrealm-realm-${keycloakMajorVersionNumber}.json`);
const filePath = pathJoin(
dirPath,
`myrealm-realm-${keycloakMajorVersionNumber}.json`
);
if (fs.existsSync(filePath)) {
return filePath;
}
console.log(`${chalk.yellow(`Keycloakify do not have a realm configuration for Keycloak ${keycloakMajorVersionNumber} yet.`)}`);
console.log(
`${chalk.yellow(
`Keycloakify do not have a realm configuration for Keycloak ${keycloakMajorVersionNumber} yet.`
)}`
);
console.log(chalk.cyan("Select what configuration to use:"));
const { value } = await cliSelect<string>({
"values": [...fs.readdirSync(dirPath).filter(fileBasename => fileBasename.endsWith(".json")), "none"]
values: [
...fs
.readdirSync(dirPath)
.filter(fileBasename => fileBasename.endsWith(".json")),
"none"
]
}).catch(() => {
process.exit(-1);
});
@ -238,17 +311,37 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
...["--name", containerName],
...["-e", "KEYCLOAK_ADMIN=admin"],
...["-e", "KEYCLOAK_ADMIN_PASSWORD=admin"],
...(realmJsonFilePath === undefined ? [] : ["-v", `${realmJsonFilePath}:/opt/keycloak/data/import/myrealm-realm.json`]),
...["-v", `${pathJoin(buildOptions.keycloakifyBuildDirPath, jarFileBasename)}:/opt/keycloak/providers/keycloak-theme.jar`],
...(keycloakMajorVersionNumber <= 20 ? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"] : []),
...mountTargets.map(({ localPath, containerPath }) => ["-v", `${localPath}:${containerPath}:rw`]).flat(),
...(realmJsonFilePath === undefined
? []
: [
"-v",
`${realmJsonFilePath}:/opt/keycloak/data/import/myrealm-realm.json`
]),
...[
"-v",
`${pathJoin(
buildOptions.keycloakifyBuildDirPath,
jarFileBasename
)}:/opt/keycloak/providers/keycloak-theme.jar`
],
...(keycloakMajorVersionNumber <= 20
? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"]
: []),
...mountTargets
.map(({ localPath, containerPath }) => [
"-v",
`${localPath}:${containerPath}:rw`
])
.flat(),
`quay.io/keycloak/keycloak:${keycloakVersion}`,
"start-dev",
...(21 <= keycloakMajorVersionNumber && keycloakMajorVersionNumber < 24 ? ["--features=declarative-user-profile"] : []),
...(21 <= keycloakMajorVersionNumber && keycloakMajorVersionNumber < 24
? ["--features=declarative-user-profile"]
: []),
...(realmJsonFilePath === undefined ? [] : ["--import-realm"])
],
{
"cwd": buildOptions.keycloakifyBuildDirPath
cwd: buildOptions.keycloakifyBuildDirPath
}
] as const;
@ -278,17 +371,23 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
[
"",
`${chalk.green("Your theme is accessible at:")}`,
`${chalk.green("➜")} ${chalk.cyan.bold("https://my-theme.keycloakify.dev/")}`,
`${chalk.green("➜")} ${chalk.cyan.bold(
"https://my-theme.keycloakify.dev/"
)}`,
"",
"You can login with the following credentials:",
`- username: ${chalk.cyan.bold("testuser")}`,
`- password: ${chalk.cyan.bold("password123")}`,
"",
`Keycloak Admin console: ${chalk.cyan.bold(`http://localhost:${cliCommandOptions.port}`)}`,
`Keycloak Admin console: ${chalk.cyan.bold(
`http://localhost:${cliCommandOptions.port}`
)}`,
`- user: ${chalk.cyan.bold("admin")}`,
`- password: ${chalk.cyan.bold("admin")}`,
"",
`Watching for changes in ${chalk.bold(`.${pathSep}${pathRelative(process.cwd(), srcDirPath)}`)}`
`Watching for changes in ${chalk.bold(
`.${pathSep}${pathRelative(process.cwd(), srcDirPath)}`
)}`
].join("\n")
);
};
@ -297,75 +396,79 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
}
{
const { waitForDebounce } = waitForDebounceFactory({ "delay": 400 });
const { waitForDebounce } = waitForDebounceFactory({ delay: 400 });
chokidar.watch([srcDirPath, getThisCodebaseRootDirPath()], { "ignoreInitial": true }).on("all", async (...[, filePath]) => {
if (
isInside({
"dirPath": pathJoin(getThisCodebaseRootDirPath(), "src", "bin"),
filePath
}) ||
isInside({
"dirPath": pathJoin(getThisCodebaseRootDirPath(), "bin"),
filePath
})
) {
return;
}
chokidar
.watch([srcDirPath, getThisCodebaseRootDirPath()], {
ignoreInitial: true
})
.on("all", async (...[, filePath]) => {
if (
isInside({
dirPath: pathJoin(getThisCodebaseRootDirPath(), "src", "bin"),
filePath
}) ||
isInside({
dirPath: pathJoin(getThisCodebaseRootDirPath(), "bin"),
filePath
})
) {
return;
}
await waitForDebounce();
await waitForDebounce();
console.log(chalk.cyan("Detected changes in the theme. Rebuilding ..."));
console.log(chalk.cyan("Detected changes in the theme. Rebuilding ..."));
const dViteBuildDone = new Deferred<void>();
const dViteBuildDone = new Deferred<void>();
{
const child = child_process.spawn("npx", ["vite", "build"], {
"cwd": buildOptions.reactAppRootDirPath,
"env": process.env
});
{
const child = child_process.spawn("npx", ["vite", "build"], {
cwd: buildOptions.reactAppRootDirPath,
env: process.env
});
child.stdout.on("data", data => {
if (data.toString("utf8").includes("gzip:")) {
return;
}
child.stdout.on("data", data => {
if (data.toString("utf8").includes("gzip:")) {
return;
}
process.stdout.write(data);
});
process.stdout.write(data);
});
child.stderr.on("data", data => process.stderr.write(data));
child.stderr.on("data", data => process.stderr.write(data));
child.on("exit", code => {
if (code === 0) {
dViteBuildDone.resolve();
}
});
}
child.on("exit", code => {
if (code === 0) {
dViteBuildDone.resolve();
}
});
}
await dViteBuildDone.pr;
await dViteBuildDone.pr;
{
const child = child_process.spawn("npx", ["keycloakify", "build"], {
"cwd": buildOptions.reactAppRootDirPath,
"env": {
...process.env,
[skipBuildJarsEnvName]: "true"
}
});
{
const child = child_process.spawn("npx", ["keycloakify", "build"], {
cwd: buildOptions.reactAppRootDirPath,
env: {
...process.env,
[skipBuildJarsEnvName]: "true"
}
});
child.stdout.on("data", data => process.stdout.write(data));
child.stdout.on("data", data => process.stdout.write(data));
child.stderr.on("data", data => process.stderr.write(data));
child.stderr.on("data", data => process.stderr.write(data));
child.on("exit", code => {
if (code !== 0) {
console.log(chalk.yellow("Theme not updated, build failed"));
return;
}
child.on("exit", code => {
if (code !== 0) {
console.log(chalk.yellow("Theme not updated, build failed"));
return;
}
console.log(chalk.green("Rebuild done"));
});
}
});
console.log(chalk.green("Rebuild done"));
});
}
});
}
}

View File

@ -12,35 +12,39 @@ export namespace SemVer {
export type BumpType = (typeof bumpTypes)[number];
export function parse(versionStr: string): SemVer {
const match = versionStr.match(/^v?([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(?:-rc.([0-9]+))?$/);
const match = versionStr.match(
/^v?([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(?:-rc.([0-9]+))?$/
);
if (!match) {
throw new Error(`${versionStr} is not a valid semantic version`);
}
const semVer: Omit<SemVer, "parsedFrom"> = {
"major": parseInt(match[1]),
"minor": parseInt(match[2]),
"patch": (() => {
major: parseInt(match[1]),
minor: parseInt(match[2]),
patch: (() => {
const str = match[3];
return str === undefined ? 0 : parseInt(str);
})(),
...(() => {
const str = match[4];
return str === undefined ? {} : { "rc": parseInt(str) };
return str === undefined ? {} : { rc: parseInt(str) };
})()
};
const initialStr = stringify(semVer);
Object.defineProperty(semVer, "parsedFrom", {
"enumerable": true,
"get": function () {
enumerable: true,
get: function () {
const currentStr = stringify(this);
if (currentStr !== initialStr) {
throw new Error(`SemVer.parsedFrom can't be read anymore, the version have been modified from ${initialStr} to ${currentStr}`);
throw new Error(
`SemVer.parsedFrom can't be read anymore, the version have been modified from ${initialStr} to ${currentStr}`
);
}
return versionStr;
@ -51,7 +55,9 @@ export namespace SemVer {
}
export function stringify(v: Omit<SemVer, "parsedFrom">): string {
return `${v.major}.${v.minor}.${v.patch}${v.rc === undefined ? "" : `-rc.${v.rc}`}`;
return `${v.major}.${v.minor}.${v.patch}${
v.rc === undefined ? "" : `-rc.${v.rc}`
}`;
}
/**
@ -80,12 +86,25 @@ export namespace SemVer {
console.log(compare(parse("3.0.0-rc.3"), parse("4.0.0")) === -1 )
*/
export function bumpType(params: { versionBehind: string | SemVer; versionAhead: string | SemVer }): BumpType | "no bump" {
const versionAhead = typeof params.versionAhead === "string" ? parse(params.versionAhead) : params.versionAhead;
const versionBehind = typeof params.versionBehind === "string" ? parse(params.versionBehind) : params.versionBehind;
export function bumpType(params: {
versionBehind: string | SemVer;
versionAhead: string | SemVer;
}): BumpType | "no bump" {
const versionAhead =
typeof params.versionAhead === "string"
? parse(params.versionAhead)
: params.versionAhead;
const versionBehind =
typeof params.versionBehind === "string"
? parse(params.versionBehind)
: params.versionBehind;
if (compare(versionBehind, versionAhead) === 1) {
throw new Error(`Version regression ${stringify(versionBehind)} -> ${stringify(versionAhead)}`);
throw new Error(
`Version regression ${stringify(versionBehind)} -> ${stringify(
versionAhead
)}`
);
}
for (const level of ["major", "minor", "patch", "rc"] as const) {

View File

@ -1,4 +1,8 @@
export function replaceAll(string: string, searchValue: string | RegExp, replaceValue: string): string {
export function replaceAll(
string: string,
searchValue: string | RegExp,
replaceValue: string
): string {
if ((string as any).replaceAll !== undefined) {
return (string as any).replaceAll(searchValue, replaceValue);
}
@ -24,7 +28,10 @@ export function replaceAll(string: string, searchValue: string | RegExp, replace
// Convert searchValue to string if it's not a string or RegExp
var searchString = String(searchValue);
var regexFromString = new RegExp(searchString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
var regexFromString = new RegExp(
searchString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
"g"
);
return string.replace(regexFromString, replaceValue);
}

View File

@ -16,7 +16,10 @@ const crawlRec = (dirPath: string, filePaths: string[]) => {
};
/** List all files in a given directory return paths relative to the dir_path */
export function crawl(params: { dirPath: string; returnedPathsType: "absolute" | "relative to dirPath" }): string[] {
export function crawl(params: {
dirPath: string;
returnedPathsType: "absolute" | "relative to dirPath";
}): string[] {
const { dirPath, returnedPathsType } = params;
const filePaths: string[] = [];

View File

@ -1,27 +1,42 @@
import { Readable } from "stream";
const crc32tab = [
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535,
0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd,
0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d,
0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4,
0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac,
0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab,
0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f,
0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb,
0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea,
0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce,
0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a,
0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409,
0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739,
0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268,
0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0,
0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8,
0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef,
0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703,
0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7,
0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae,
0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6,
0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d,
0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5,
0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605,
0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
];
@ -33,11 +48,13 @@ const crc32tab = [
export function crc32(input: Readable | String | Buffer): Promise<number> {
if (typeof input === "string") {
let crc = ~0;
for (let i = 0; i < input.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ input.charCodeAt(i)) & 0xff];
for (let i = 0; i < input.length; i++)
crc = (crc >>> 8) ^ crc32tab[(crc ^ input.charCodeAt(i)) & 0xff];
return Promise.resolve((crc ^ -1) >>> 0);
} else if (input instanceof Buffer) {
let crc = ~0;
for (let i = 0; i < input.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ input[i]) & 0xff];
for (let i = 0; i < input.length; i++)
crc = (crc >>> 8) ^ crc32tab[(crc ^ input[i]) & 0xff];
return Promise.resolve((crc ^ -1) >>> 0);
} else if (input instanceof Readable) {
return new Promise<number>((resolve, reject) => {
@ -46,7 +63,8 @@ export function crc32(input: Readable | String | Buffer): Promise<number> {
input.on("end", () => resolve((crc ^ -1) >>> 0));
input.on("error", e => reject(e));
input.on("data", (chunk: Buffer) => {
for (let i = 0; i < chunk.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ chunk[i]) & 0xff];
for (let i = 0; i < chunk.length; i++)
crc = (crc >>> 8) ^ crc32tab[(crc ^ chunk[i]) & 0xff];
});
});
} else {

View File

@ -11,7 +11,10 @@ function ensureSingleOrNone<T>(arg0: T | T[]) {
if (!Array.isArray(arg0)) return arg0;
if (arg0.length === 0) return undefined;
if (arg0.length === 1) return arg0[0];
throw new Error("Illegal configuration, expected a single value but found multiple: " + arg0.map(String).join(", "));
throw new Error(
"Illegal configuration, expected a single value but found multiple: " +
arg0.map(String).join(", ")
);
}
type NPMConfig = Record<string, string | string[]>;
@ -24,10 +27,15 @@ async function getNmpConfig(params: { npmWorkspaceRootDirPath: string }) {
const exec = promisify(execCallback);
const stdout = await exec("npm config get", { "encoding": "utf8", "cwd": npmWorkspaceRootDirPath }).then(({ stdout }) => stdout);
const stdout = await exec("npm config get", {
encoding: "utf8",
cwd: npmWorkspaceRootDirPath
}).then(({ stdout }) => stdout);
const npmConfigReducer = (cfg: NPMConfig, [key, value]: [string, string]) =>
key in cfg ? { ...cfg, [key]: [...ensureArray(cfg[key]), value] } : { ...cfg, [key]: value };
key in cfg
? { ...cfg, [key]: [...ensureArray(cfg[key]), value] }
: { ...cfg, [key]: value };
return stdout
.split("\n")
@ -37,9 +45,14 @@ async function getNmpConfig(params: { npmWorkspaceRootDirPath: string }) {
.reduce(npmConfigReducer, {} as NPMConfig);
}
export type ProxyFetchOptions = Pick<FetchOptions, "proxy" | "noProxy" | "strictSSL" | "cert" | "ca">;
export type ProxyFetchOptions = Pick<
FetchOptions,
"proxy" | "noProxy" | "strictSSL" | "cert" | "ca"
>;
export async function getProxyFetchOptions(params: { npmWorkspaceRootDirPath: string }): Promise<ProxyFetchOptions> {
export async function getProxyFetchOptions(params: {
npmWorkspaceRootDirPath: string;
}): Promise<ProxyFetchOptions> {
const { npmWorkspaceRootDirPath } = params;
const cfg = await getNmpConfig({ npmWorkspaceRootDirPath });
@ -60,14 +73,24 @@ export async function getProxyFetchOptions(params: { npmWorkspaceRootDirPath: st
ca.push(
...(await (async () => {
function chunks<T>(arr: T[], size: number = 2) {
return arr.map((_, i) => i % size == 0 && arr.slice(i, i + size)).filter(Boolean) as T[][];
return arr
.map((_, i) => i % size == 0 && arr.slice(i, i + size))
.filter(Boolean) as T[][];
}
const cafileContent = await readFile(cafile, "utf-8");
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(ca => ca.join("").replace(/^\n/, "").replace(/\n/g, "\\n"));
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(
ca => ca.join("").replace(/^\n/, "").replace(/\n/g, "\\n")
);
})())
);
}
return { proxy, noProxy, strictSSL, cert, "ca": ca.length === 0 ? undefined : ca };
return {
proxy,
noProxy,
strictSSL,
cert,
ca: ca.length === 0 ? undefined : ca
};
}

View File

@ -1,7 +1,15 @@
import { isAbsolute as pathIsAbsolute, sep as pathSep, join as pathJoin, resolve as pathResolve } from "path";
import {
isAbsolute as pathIsAbsolute,
sep as pathSep,
join as pathJoin,
resolve as pathResolve
} from "path";
import * as os from "os";
export function getAbsoluteAndInOsFormatPath(params: { pathIsh: string; cwd: string }): string {
export function getAbsoluteAndInOsFormatPath(params: {
pathIsh: string;
cwd: string;
}): string {
const { pathIsh, cwd } = params;
let pathOut = pathIsh;

View File

@ -3,14 +3,22 @@ import { join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
import { assert } from "tsafe/assert";
import * as fs from "fs";
export function getNpmWorkspaceRootDirPath(params: { reactAppRootDirPath: string; dependencyExpected: string }) {
export function getNpmWorkspaceRootDirPath(params: {
reactAppRootDirPath: string;
dependencyExpected: string;
}) {
const { reactAppRootDirPath, dependencyExpected } = params;
const npmWorkspaceRootDirPath = (function callee(depth: number): string {
const cwd = pathResolve(pathJoin(...[reactAppRootDirPath, ...Array(depth).fill("..")]));
const cwd = pathResolve(
pathJoin(...[reactAppRootDirPath, ...Array(depth).fill("..")])
);
try {
child_process.execSync("npm config get", { cwd, "stdio": "ignore" });
child_process.execSync("npm config get", {
cwd,
stdio: "ignore"
});
} catch (error) {
if (String(error).includes("ENOWORKSPACES")) {
assert(cwd !== pathSep, "NPM workspace not found");
@ -26,11 +34,16 @@ export function getNpmWorkspaceRootDirPath(params: { reactAppRootDirPath: string
assert(fs.existsSync(packageJsonFilePath));
const parsedPackageJson = JSON.parse(fs.readFileSync(packageJsonFilePath).toString("utf8"));
const parsedPackageJson = JSON.parse(
fs.readFileSync(packageJsonFilePath).toString("utf8")
);
let isExpectedDependencyFound = false;
for (const dependenciesOrDevDependencies of ["dependencies", "devDependencies"] as const) {
for (const dependenciesOrDevDependencies of [
"dependencies",
"devDependencies"
] as const) {
const dependencies = parsedPackageJson[dependenciesOrDevDependencies];
if (dependencies === undefined) {

View File

@ -5,7 +5,11 @@ import { SemVer } from "../SemVer";
export function getLatestsSemVersionedTagFactory(params: { octokit: Octokit }) {
const { octokit } = params;
async function getLatestsSemVersionedTag(params: { owner: string; repo: string; count: number }): Promise<
async function getLatestsSemVersionedTag(params: {
owner: string;
repo: string;
count: number;
}): Promise<
{
tag: string;
version: SemVer;
@ -33,7 +37,9 @@ export function getLatestsSemVersionedTagFactory(params: { octokit: Octokit }) {
semVersionedTags.push({ tag, version });
}
return semVersionedTags.sort(({ version: vX }, { version: vY }) => SemVer.compare(vY, vX)).slice(0, count);
return semVersionedTags
.sort(({ version: vX }, { version: vY }) => SemVer.compare(vY, vX))
.slice(0, count);
}
return { getLatestsSemVersionedTag };

View File

@ -5,11 +5,19 @@ const per_page = 99;
export function listTagsFactory(params: { octokit: Octokit }) {
const { octokit } = params;
const octokit_repo_listTags = async (params: { owner: string; repo: string; per_page: number; page: number }) => {
const octokit_repo_listTags = async (params: {
owner: string;
repo: string;
per_page: number;
page: number;
}) => {
return octokit.repos.listTags(params);
};
async function* listTags(params: { owner: string; repo: string }): AsyncGenerator<string> {
async function* listTags(params: {
owner: string;
repo: string;
}): AsyncGenerator<string> {
const { owner, repo } = params;
let page = 1;
@ -19,7 +27,7 @@ export function listTagsFactory(params: { octokit: Octokit }) {
owner,
repo,
per_page,
"page": page++
page: page++
});
for (const branch of resp.data.map(({ name }) => name)) {
@ -33,7 +41,10 @@ export function listTagsFactory(params: { octokit: Octokit }) {
}
/** Returns the same "latest" tag as deno.land/x, not actually the latest though */
async function getLatestTag(params: { owner: string; repo: string }): Promise<string | undefined> {
async function getLatestTag(params: {
owner: string;
repo: string;
}): Promise<string | undefined> {
const { owner, repo } = params;
const itRes = await listTags({ owner, repo }).next();

View File

@ -2,10 +2,19 @@ export type PromiseSettledAndPartitioned<T> = [T[], any[]];
export function partitionPromiseSettledResults<T>() {
return [
([successes, failures]: PromiseSettledAndPartitioned<T>, item: PromiseSettledResult<T>) =>
(
[successes, failures]: PromiseSettledAndPartitioned<T>,
item: PromiseSettledResult<T>
) =>
item.status === "rejected"
? ([successes, [item.reason, ...failures]] as PromiseSettledAndPartitioned<T>)
: ([[item.value, ...successes], failures] as PromiseSettledAndPartitioned<T>),
? ([
successes,
[item.reason, ...failures]
] as PromiseSettledAndPartitioned<T>)
: ([
[item.value, ...successes],
failures
] as PromiseSettledAndPartitioned<T>),
[[], []] as PromiseSettledAndPartitioned<T>
] as const;
}

View File

@ -4,7 +4,11 @@ import * as fs from "fs";
import { join as pathJoin } from "path";
export function readThisNpmPackageVersion(): string {
const version = JSON.parse(fs.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json")).toString("utf8"))["version"];
const version = JSON.parse(
fs
.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json"))
.toString("utf8")
)["version"];
assert(typeof version === "string");

View File

@ -3,7 +3,11 @@ import * as path from "path";
import { crawl } from "./crawl";
import { rmSync } from "../tools/fs.rmSync";
type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string; fileRelativePath: string }) =>
type TransformSourceCode = (params: {
sourceCode: Buffer;
filePath: string;
fileRelativePath: string;
}) =>
| {
modifiedSourceCode: Buffer;
newFileName?: string;
@ -15,18 +19,27 @@ type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string; file
* If source and destination are the same this function can be used to apply the transformation in place
* like filtering out some files or modifying them.
* */
export function transformCodebase(params: { srcDirPath: string; destDirPath: string; transformSourceCode?: TransformSourceCode }) {
export function transformCodebase(params: {
srcDirPath: string;
destDirPath: string;
transformSourceCode?: TransformSourceCode;
}) {
const { srcDirPath, transformSourceCode } = params;
const isTargetSameAsSource = path.relative(srcDirPath, params.destDirPath) === "";
const destDirPath = isTargetSameAsSource ? path.join(srcDirPath, "..", "tmp_xOsPdkPsTdzPs34sOkHs") : params.destDirPath;
const destDirPath = isTargetSameAsSource
? path.join(srcDirPath, "..", "tmp_xOsPdkPsTdzPs34sOkHs")
: params.destDirPath;
fs.mkdirSync(destDirPath, {
"recursive": true
recursive: true
});
for (const fileRelativePath of crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })) {
for (const fileRelativePath of crawl({
dirPath: srcDirPath,
returnedPathsType: "relative to dirPath"
})) {
const filePath = path.join(srcDirPath, fileRelativePath);
const destFilePath = path.join(destDirPath, fileRelativePath);
@ -34,7 +47,7 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
// it using the lower level implementation.
if (transformSourceCode === undefined) {
fs.mkdirSync(path.dirname(destFilePath), {
"recursive": true
recursive: true
});
fs.copyFileSync(filePath, destFilePath);
@ -43,7 +56,7 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
}
const transformSourceCodeResult = transformSourceCode({
"sourceCode": fs.readFileSync(filePath),
sourceCode: fs.readFileSync(filePath),
filePath,
fileRelativePath
});
@ -53,16 +66,22 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
}
fs.mkdirSync(path.dirname(destFilePath), {
"recursive": true
recursive: true
});
const { newFileName, modifiedSourceCode } = transformSourceCodeResult;
fs.writeFileSync(path.join(path.dirname(destFilePath), newFileName ?? path.basename(destFilePath)), modifiedSourceCode);
fs.writeFileSync(
path.join(
path.dirname(destFilePath),
newFileName ?? path.basename(destFilePath)
),
modifiedSourceCode
);
}
if (isTargetSameAsSource) {
rmSync(srcDirPath, { "recursive": true });
rmSync(srcDirPath, { recursive: true });
fs.renameSync(destDirPath, srcDirPath);
}

View File

@ -14,7 +14,10 @@ function populateTemplate(strings: TemplateStringsArray, ...args: unknown[]) {
if (args[i]) {
// if the interpolation value has newlines, indent the interpolation values
// using the last known string indent
const chunk = String(args[i]).replace(/([\r?\n])/g, "$1" + " ".repeat(lastStringLineLength));
const chunk = String(args[i]).replace(
/([\r?\n])/g,
"$1" + " ".repeat(lastStringLineLength)
);
chunks.push(chunk);
}
}

View File

@ -22,7 +22,11 @@ async function pathExists(path: string) {
// Handlings of non posix path is not implemented correctly
// it work by coincidence. Don't have the time to fix but it should be fixed.
export async function unzip(file: string, targetFolder: string, specificDirsToExtract?: string[]) {
export async function unzip(
file: string,
targetFolder: string,
specificDirsToExtract?: string[]
) {
specificDirsToExtract = specificDirsToExtract?.map(dirPath => {
if (!dirPath.endsWith("/") || !dirPath.endsWith("\\")) {
dirPath += "/";
@ -49,7 +53,9 @@ export async function unzip(file: string, targetFolder: string, specificDirsToEx
zipfile.on("entry", async entry => {
if (specificDirsToExtract !== undefined) {
const dirPath = specificDirsToExtract.find(dirPath => entry.fileName.startsWith(dirPath));
const dirPath = specificDirsToExtract.find(dirPath =>
entry.fileName.startsWith(dirPath)
);
// Skip files outside of the unzipSubPath
if (dirPath === undefined) {
@ -85,7 +91,9 @@ export async function unzip(file: string, targetFolder: string, specificDirsToEx
return;
}
await fsp.mkdir(path.dirname(target), { "recursive": true });
await fsp.mkdir(path.dirname(target), {
recursive: true
});
await pipeline(readStream, fs.createWriteStream(target));

View File

@ -1 +1,3 @@
export const isStorybook = typeof window === "object" && Object.keys(window).find(key => key.startsWith("__STORYBOOK")) !== undefined;
export const isStorybook =
typeof window === "object" &&
Object.keys(window).find(key => key.startsWith("__STORYBOOK")) !== undefined;

View File

@ -1,14 +1,23 @@
import { clsx } from "keycloakify/tools/clsx";
import { useConstCallback } from "keycloakify/tools/useConstCallback";
export function createUseClassName<ClassKey extends string>(params: { defaultClasses: Record<ClassKey, string | undefined> }) {
export function createUseClassName<ClassKey extends string>(params: {
defaultClasses: Record<ClassKey, string | undefined>;
}) {
const { defaultClasses } = params;
function useGetClassName(params: { doUseDefaultCss: boolean; classes: Partial<Record<ClassKey, string>> | undefined }) {
function useGetClassName(params: {
doUseDefaultCss: boolean;
classes: Partial<Record<ClassKey, string>> | undefined;
}) {
const { classes, doUseDefaultCss } = params;
const getClassName = useConstCallback((classKey: ClassKey): string => {
return clsx(classKey, doUseDefaultCss ? defaultClasses[classKey] : undefined, classes?.[classKey]);
return clsx(
classKey,
doUseDefaultCss ? defaultClasses[classKey] : undefined,
classes?.[classKey]
);
});
return { getClassName };

View File

@ -41,13 +41,13 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
}, []);
useSetClassName({
"qualifiedName": "html",
"className": getClassName("kcHtmlClass")
qualifiedName: "html",
className: getClassName("kcHtmlClass")
});
useSetClassName({
"qualifiedName": "body",
"className": bodyClassName ?? getClassName("kcBodyClass")
qualifiedName: "body",
className: bodyClassName ?? getClassName("kcBodyClass")
});
useEffect(() => {
@ -63,7 +63,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
}, []);
const { areAllStyleSheetsLoaded } = useInsertLinkTags({
"hrefs": !doUseDefaultCss
hrefs: !doUseDefaultCss
? []
: [
`${url.resourcesCommonPath}/node_modules/@patternfly/patternfly/patternfly.min.css`,
@ -75,17 +75,17 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
});
const { insertScriptTags } = useInsertScriptTags({
"scriptTags": [
scriptTags: [
{
"type": "module",
"src": `${url.resourcesPath}/js/menu-button-links.js`
type: "module",
src: `${url.resourcesPath}/js/menu-button-links.js`
},
...(authenticationSession === undefined
? []
: [
{
"type": "module",
"textContent": [
type: "module",
textContent: [
`import { checkCookiesAndSetTimer } from "${url.resourcesPath}/js/authChecker.js";`,
``,
`checkCookiesAndSetTimer(`,
@ -99,8 +99,8 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
...scripts.map(
script =>
({
"type": "text/javascript",
"src": script
type: "text/javascript",
src: script
} as const)
)
]
@ -237,7 +237,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
<span
className={getClassName("kcAlertTitleClass")}
dangerouslySetInnerHTML={{
"__html": message.summary
__html: message.summary
}}
/>
</div>

View File

@ -2,7 +2,10 @@ import type { ReactNode } from "react";
import type { KcContext } from "./kcContext";
import type { I18n } from "./i18n";
export type TemplateProps<KcContext extends KcContext.Common, I18nExtended extends I18n> = {
export type TemplateProps<
KcContext extends KcContext.Common,
I18nExtended extends I18n
> = {
kcContext: KcContext;
i18n: I18nExtended;
doUseDefaultCss: boolean;

View File

@ -43,7 +43,7 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps)
onIsFormSubmittableValueChange(isFormSubmittable);
}, [isFormSubmittable]);
const groupNameRef = { "current": "" };
const groupNameRef = { current: "" };
return (
<>
@ -62,7 +62,9 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps)
)}
<div
className={getClassName("kcFormGroupClass")}
style={{ "display": attribute.name === "password-confirm" && !doMakeUserConfirmPassword ? "none" : undefined }}
style={{
display: attribute.name === "password-confirm" && !doMakeUserConfirmPassword ? "none" : undefined
}}
>
<div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor={attribute.name} className={getClassName("kcLabelClass")}>
@ -342,9 +344,9 @@ function InputTag(props: InputFiledByTypeProps & { fieldIndex: number | undefine
{...Object.fromEntries(Object.entries(attribute.html5DataAnnotations ?? {}).map(([key, value]) => [`data-${key}`, value]))}
onChange={event =>
formValidationDispatch({
"action": "update",
"name": attribute.name,
"valueOrValues": (() => {
action: "update",
name: attribute.name,
valueOrValues: (() => {
if (fieldIndex !== undefined) {
assert(valueOrValues instanceof Array);
@ -363,9 +365,9 @@ function InputTag(props: InputFiledByTypeProps & { fieldIndex: number | undefine
}
onBlur={() =>
props.formValidationDispatch({
"action": "focus lost",
"name": attribute.name,
"fieldIndex": fieldIndex
action: "focus lost",
name: attribute.name,
fieldIndex: fieldIndex
})
}
/>
@ -484,9 +486,9 @@ function AddRemoveButtonsMultiValuedAttribute(props: {
className="pf-c-button pf-m-inline pf-m-link"
onClick={() =>
dispatchFormAction({
"action": "update",
"name": attribute.name,
"valueOrValues": values.filter((_, i) => i !== fieldIndex)
action: "update",
name: attribute.name,
valueOrValues: values.filter((_, i) => i !== fieldIndex)
})
}
>
@ -501,9 +503,9 @@ function AddRemoveButtonsMultiValuedAttribute(props: {
className="pf-c-button pf-m-inline pf-m-link"
onClick={() =>
dispatchFormAction({
"action": "update",
"name": attribute.name,
"valueOrValues": [...values, ""]
action: "update",
name: attribute.name,
valueOrValues: [...values, ""]
})
}
>
@ -527,17 +529,17 @@ function InputTagSelects(props: InputFiledByTypeProps) {
switch (inputType) {
case "select-radiobuttons":
return {
"inputType": "radio",
"classDiv": getClassName("kcInputClassRadio"),
"classInput": getClassName("kcInputClassRadioInput"),
"classLabel": getClassName("kcInputClassRadioLabel")
inputType: "radio",
classDiv: getClassName("kcInputClassRadio"),
classInput: getClassName("kcInputClassRadioInput"),
classLabel: getClassName("kcInputClassRadioLabel")
};
case "multiselect-checkboxes":
return {
"inputType": "checkbox",
"classDiv": getClassName("kcInputClassCheckbox"),
"classInput": getClassName("kcInputClassCheckboxInput"),
"classLabel": getClassName("kcInputClassCheckboxLabel")
inputType: "checkbox",
classDiv: getClassName("kcInputClassCheckbox"),
classInput: getClassName("kcInputClassCheckboxInput"),
classLabel: getClassName("kcInputClassCheckboxLabel")
};
}
})();
@ -581,9 +583,9 @@ function InputTagSelects(props: InputFiledByTypeProps) {
checked={valueOrValues.includes(option)}
onChange={event =>
formValidationDispatch({
"action": "update",
"name": attribute.name,
"valueOrValues": (() => {
action: "update",
name: attribute.name,
valueOrValues: (() => {
const isChecked = event.target.checked;
if (valueOrValues instanceof Array) {
@ -604,9 +606,9 @@ function InputTagSelects(props: InputFiledByTypeProps) {
}
onBlur={() =>
formValidationDispatch({
"action": "focus lost",
"name": attribute.name,
"fieldIndex": undefined
action: "focus lost",
name: attribute.name,
fieldIndex: undefined
})
}
/>
@ -642,16 +644,16 @@ function TextareaTag(props: InputFiledByTypeProps) {
value={value}
onChange={event =>
formValidationDispatch({
"action": "update",
"name": attribute.name,
"valueOrValues": event.target.value
action: "update",
name: attribute.name,
valueOrValues: event.target.value
})
}
onBlur={() =>
formValidationDispatch({
"action": "focus lost",
"name": attribute.name,
"fieldIndex": undefined
action: "focus lost",
name: attribute.name,
fieldIndex: undefined
})
}
/>
@ -677,9 +679,9 @@ function SelectTag(props: InputFiledByTypeProps) {
value={valueOrValues}
onChange={event =>
formValidationDispatch({
"action": "update",
"name": attribute.name,
"valueOrValues": (() => {
action: "update",
name: attribute.name,
valueOrValues: (() => {
if (isMultiple) {
return Array.from(event.target.selectedOptions).map(option => option.value);
}
@ -690,9 +692,9 @@ function SelectTag(props: InputFiledByTypeProps) {
}
onBlur={() =>
formValidationDispatch({
"action": "focus lost",
"name": attribute.name,
"fieldIndex": undefined
action: "focus lost",
name: attribute.name,
fieldIndex: undefined
})
}
>

View File

@ -91,19 +91,19 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
setI18n({
...createI18nTranslationFunctions({
"fallbackMessages": {
fallbackMessages: {
...fallbackMessages,
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
...(extraMessages[fallbackLanguageTag] ?? {})
} as any,
"messages": {
messages: {
...(await getMessages(currentLanguageTag)),
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
...(extraMessages[currentLanguageTag] ?? {})
} as any
}),
currentLanguageTag,
"getChangeLocalUrl": newLanguageTag => {
getChangeLocalUrl: newLanguageTag => {
const { locale } = kcContext;
assert(locale !== undefined, "Internationalization not enabled");
@ -114,7 +114,7 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
return targetSupportedLocale.url;
},
"labelBySupportedLanguageTag": Object.fromEntries(
labelBySupportedLanguageTag: Object.fromEntries(
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
)
});
@ -170,7 +170,7 @@ function createI18nTranslationFunctions<MessageKey extends string>(params: {
})();
return doRenderMarkdown ? (
<Markdown allowDangerousHtml renderers={{ "paragraph": "span" }}>
<Markdown allowDangerousHtml renderers={{ paragraph: "span" }}>
{messageWithArgsInjectedIfAny}
</Markdown>
) : (
@ -186,7 +186,7 @@ function createI18nTranslationFunctions<MessageKey extends string>(params: {
const keyUnwrappedFromCurlyBraces = match === null ? key : match[1];
const out = resolveMsg({
"key": keyUnwrappedFromCurlyBraces,
key: keyUnwrappedFromCurlyBraces,
args,
doRenderMarkdown
});
@ -195,38 +195,48 @@ function createI18nTranslationFunctions<MessageKey extends string>(params: {
}
return {
"msgStr": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": false }) as string,
"msg": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": true }) as JSX.Element,
"advancedMsg": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": true }) as JSX.Element,
"advancedMsgStr": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": false }) as string
msgStr: (key, ...args) => resolveMsg({ key, args, doRenderMarkdown: false }) as string,
msg: (key, ...args) => resolveMsg({ key, args, doRenderMarkdown: true }) as JSX.Element,
advancedMsg: (key, ...args) =>
resolveMsgAdvanced({
key,
args,
doRenderMarkdown: true
}) as JSX.Element,
advancedMsgStr: (key, ...args) =>
resolveMsgAdvanced({
key,
args,
doRenderMarkdown: false
}) as string
};
}
const keycloakifyExtraMessages = {
"en": {
"shouldBeEqual": "{0} should be equal to {1}",
"shouldBeDifferent": "{0} should be different to {1}",
"shouldMatchPattern": "Pattern should match: `/{0}/`",
"mustBeAnInteger": "Must be an integer",
"notAValidOption": "Not a valid option",
"selectAnOption": "Select an option",
"remove": "Remove",
"addValue": "Add value"
en: {
shouldBeEqual: "{0} should be equal to {1}",
shouldBeDifferent: "{0} should be different to {1}",
shouldMatchPattern: "Pattern should match: `/{0}/`",
mustBeAnInteger: "Must be an integer",
notAValidOption: "Not a valid option",
selectAnOption: "Select an option",
remove: "Remove",
addValue: "Add value"
},
"fr": {
fr: {
/* spell-checker: disable */
"shouldBeEqual": "{0} doit être égal à {1}",
"shouldBeDifferent": "{0} doit être différent de {1}",
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
"mustBeAnInteger": "Doit être un nombre entier",
"notAValidOption": "N'est pas une option valide",
shouldBeEqual: "{0} doit être égal à {1}",
shouldBeDifferent: "{0} doit être différent de {1}",
shouldMatchPattern: "Dois respecter le schéma: `/{0}/`",
mustBeAnInteger: "Doit être un nombre entier",
notAValidOption: "N'est pas une option valide",
"logoutConfirmTitle": "Déconnexion",
"logoutConfirmHeader": "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
"doLogout": "Se déconnecter",
"selectAnOption": "Sélectionner une option",
"remove": "Supprimer",
"addValue": "Ajouter une valeur"
logoutConfirmTitle: "Déconnexion",
logoutConfirmHeader: "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
doLogout: "Se déconnecter",
selectAnOption: "Sélectionner une option",
remove: "Supprimer",
addValue: "Ajouter une valeur"
/* spell-checker: enable */
}
};

View File

@ -3,7 +3,10 @@ import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import type { MessageKey } from "../i18n/i18n";
type ExtractAfterStartingWith<Prefix extends string, StrEnum> = StrEnum extends `${Prefix}${infer U}` ? U : never;
type ExtractAfterStartingWith<
Prefix extends string,
StrEnum
> = StrEnum extends `${Prefix}${infer U}` ? U : never;
/** Take theses type definition with a grain of salt.
* Some values might be undefined on some pages.
@ -103,7 +106,10 @@ export declare namespace KcContext {
* @param text to return
* @return text if message exists for given field, else undefined
*/
printIfExists: <T extends string>(fieldName: string, text: T) => T | undefined;
printIfExists: <T extends string>(
fieldName: string,
text: T
) => T | undefined;
/**
* Check if exists error message for given fields
*

View File

@ -10,15 +10,23 @@ import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { symToStr } from "tsafe/symToStr";
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
export function createGetKcContext<
KcContextExtension extends { pageId: string } = never
>(params?: {
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
mockProperties?: Record<string, string>;
}) {
const { mockData, mockProperties } = params ?? {};
function getKcContext<PageId extends ExtendKcContext<KcContextExtension>["pageId"] | undefined = undefined>(params?: {
function getKcContext<
PageId extends
| ExtendKcContext<KcContextExtension>["pageId"]
| undefined = undefined
>(params?: {
mockPageId?: PageId;
storyPartialKcContext?: DeepPartial<Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>>;
storyPartialKcContext?: DeepPartial<
Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>
>;
}): {
kcContext: PageId extends undefined
? ExtendKcContext<KcContextExtension> | undefined
@ -36,34 +44,46 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
break warn_that_mock_is_enbaled;
}
console.log(`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`, "background: red; color: yellow; font-size: medium");
console.log(
`%cKeycloakify: ${symToStr({
mockPageId
})} set to ${mockPageId}.`,
"background: red; color: yellow; font-size: medium"
);
}
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
const kcContextDefaultMock = kcContextMocks.find(
({ pageId }) => pageId === mockPageId
);
const partialKcContextCustomMock = (() => {
const out: DeepPartial<ExtendKcContext<KcContextExtension>> = {};
const mockDataPick = mockData?.find(({ pageId }) => pageId === mockPageId);
const mockDataPick = mockData?.find(
({ pageId }) => pageId === mockPageId
);
if (mockDataPick !== undefined) {
deepAssign({
"target": out,
"source": mockDataPick
target: out,
source: mockDataPick
});
}
if (storyPartialKcContext !== undefined) {
deepAssign({
"target": out,
"source": storyPartialKcContext
target: out,
source: storyPartialKcContext
});
}
return Object.keys(out).length === 0 ? undefined : out;
})();
if (kcContextDefaultMock === undefined && partialKcContextCustomMock === undefined) {
if (
kcContextDefaultMock === undefined &&
partialKcContextCustomMock === undefined
) {
console.warn(
[
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
@ -76,68 +96,95 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
const kcContext: any = {};
deepAssign({
"target": kcContext,
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
target: kcContext,
source:
kcContextDefaultMock !== undefined
? kcContextDefaultMock
: { pageId: mockPageId, ...kcContextCommonMock }
});
if (partialKcContextCustomMock !== undefined) {
deepAssign({
"target": kcContext,
"source": partialKcContextCustomMock
target: kcContext,
source: partialKcContextCustomMock
});
if ("profile" in partialKcContextCustomMock) {
assert(kcContextDefaultMock !== undefined && "profile" in kcContextDefaultMock);
assert(
kcContextDefaultMock !== undefined &&
"profile" in kcContextDefaultMock
);
const { attributes } = kcContextDefaultMock.profile;
id<KcContext.Register>(kcContext).profile.attributes = [];
id<KcContext.Register>(kcContext).profile.attributesByName = {};
const partialAttributes = [...((partialKcContextCustomMock as DeepPartial<KcContext.Register>).profile?.attributes ?? [])].filter(
exclude(undefined)
);
const partialAttributes = [
...((
partialKcContextCustomMock as DeepPartial<KcContext.Register>
).profile?.attributes ?? [])
].filter(exclude(undefined));
attributes.forEach(attribute => {
const partialAttribute = partialAttributes.find(({ name }) => name === attribute.name);
const partialAttribute = partialAttributes.find(
({ name }) => name === attribute.name
);
const augmentedAttribute: Attribute = {} as any;
deepAssign({
"target": augmentedAttribute,
"source": attribute
target: augmentedAttribute,
source: attribute
});
if (partialAttribute !== undefined) {
partialAttributes.splice(partialAttributes.indexOf(partialAttribute), 1);
partialAttributes.splice(
partialAttributes.indexOf(partialAttribute),
1
);
deepAssign({
"target": augmentedAttribute,
"source": partialAttribute
target: augmentedAttribute,
source: partialAttribute
});
}
id<KcContext.Register>(kcContext).profile.attributes.push(augmentedAttribute);
id<KcContext.Register>(kcContext).profile.attributesByName[augmentedAttribute.name] = augmentedAttribute;
id<KcContext.Register>(kcContext).profile.attributes.push(
augmentedAttribute
);
id<KcContext.Register>(kcContext).profile.attributesByName[
augmentedAttribute.name
] = augmentedAttribute;
});
partialAttributes
.map(partialAttribute => ({ "validators": {}, ...partialAttribute }))
.map(partialAttribute => ({
validators: {},
...partialAttribute
}))
.forEach(partialAttribute => {
const { name } = partialAttribute;
assert(name !== undefined, "If you define a mock attribute it must have at least a name");
assert(
name !== undefined,
"If you define a mock attribute it must have at least a name"
);
id<KcContext.Register>(kcContext).profile.attributes.push(partialAttribute as any);
id<KcContext.Register>(kcContext).profile.attributesByName[name] = partialAttribute as any;
id<KcContext.Register>(kcContext).profile.attributes.push(
partialAttribute as any
);
id<KcContext.Register>(kcContext).profile.attributesByName[
name
] = partialAttribute as any;
});
}
}
if (mockProperties !== undefined) {
deepAssign({
"target": kcContext.properties,
"source": mockProperties
target: kcContext.properties,
source: mockProperties
});
}
@ -145,14 +192,14 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
}
if (realKcContext === undefined) {
return { "kcContext": undefined as any };
return { kcContext: undefined as any };
}
if (realKcContext.themeType !== "login") {
return { "kcContext": undefined as any };
return { kcContext: undefined as any };
}
return { "kcContext": realKcContext as any };
return { kcContext: realKcContext as any };
}
return { getKcContext };

View File

@ -5,7 +5,9 @@ import { createGetKcContext } from "./createGetKcContext";
/** NOTE: We now recommend using createGetKcContext instead of this function to make storybook integration easier
* See: https://github.com/keycloakify/keycloakify-starter/blob/main/src/keycloak-theme/account/kcContext.ts
*/
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
export function getKcContext<
KcContextExtension extends { pageId: string } = never
>(params?: {
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {

View File

@ -2,10 +2,14 @@ import type { KcContext } from "./KcContext";
import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey";
import { nameOfTheGlobal } from "keycloakify/bin/shared/constants";
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [KcContextExtension] extends [never]
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [
KcContextExtension
] extends [never]
? KcContext
: AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>;
export function getKcContextFromWindow<KcContextExtension extends { pageId: string } = never>(): ExtendKcContext<KcContextExtension> | undefined {
export function getKcContextFromWindow<
KcContextExtension extends { pageId: string } = never
>(): ExtendKcContext<KcContextExtension> | undefined {
return typeof window === "undefined" ? undefined : (window as any)[nameOfTheGlobal];
}

View File

@ -1,324 +1,336 @@
import "minimal-polyfills/Object.fromEntries";
import type { KcContext, Attribute } from "./KcContext";
import { resources_common, keycloak_resources, type LoginThemePageId } from "keycloakify/bin/shared/constants";
import {
resources_common,
keycloak_resources,
type LoginThemePageId
} from "keycloakify/bin/shared/constants";
import { id } from "tsafe/id";
import { assert, type Equals } from "tsafe/assert";
import { BASE_URL } from "keycloakify/lib/BASE_URL";
const attributes: Attribute[] = [
{
"validators": {
"length": {
validators: {
length: {
"ignore.empty.value": true,
"min": "3",
"max": "255"
min: "3",
max: "255"
}
},
"displayName": "${username}",
"annotations": {},
"required": true,
"autocomplete": "username",
"readOnly": false,
"name": "username",
"value": "xxxx"
displayName: "${username}",
annotations: {},
required: true,
autocomplete: "username",
readOnly: false,
name: "username",
value: "xxxx"
},
{
"validators": {
"length": {
"max": "255",
validators: {
length: {
max: "255",
"ignore.empty.value": true
},
"email": {
email: {
"ignore.empty.value": true
},
"pattern": {
pattern: {
"ignore.empty.value": true,
"pattern": "gmail\\.com$"
pattern: "gmail\\.com$"
}
},
"displayName": "${email}",
"annotations": {},
"required": true,
"autocomplete": "email",
"readOnly": false,
"name": "email"
displayName: "${email}",
annotations: {},
required: true,
autocomplete: "email",
readOnly: false,
name: "email"
},
{
"validators": {
"length": {
"max": "255",
validators: {
length: {
max: "255",
"ignore.empty.value": true
}
},
"displayName": "${firstName}",
"annotations": {},
"required": true,
"readOnly": false,
"name": "firstName"
displayName: "${firstName}",
annotations: {},
required: true,
readOnly: false,
name: "firstName"
},
{
"validators": {
"length": {
"max": "255",
validators: {
length: {
max: "255",
"ignore.empty.value": true
}
},
"displayName": "${lastName}",
"annotations": {},
"required": true,
"readOnly": false,
"name": "lastName"
displayName: "${lastName}",
annotations: {},
required: true,
readOnly: false,
name: "lastName"
}
];
const attributesByName = Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any;
const attributesByName = Object.fromEntries(
attributes.map(attribute => [attribute.name, attribute])
) as any;
const resourcesPath = `${BASE_URL}${keycloak_resources}/login/resources`;
export const kcContextCommonMock: KcContext.Common = {
"themeVersion": "0.0.0",
"keycloakifyVersion": "0.0.0",
"themeType": "login",
"themeName": "my-theme-name",
"url": {
"loginAction": "#",
themeVersion: "0.0.0",
keycloakifyVersion: "0.0.0",
themeType: "login",
themeName: "my-theme-name",
url: {
loginAction: "#",
resourcesPath,
"resourcesCommonPath": `${resourcesPath}/${resources_common}`,
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
"ssoLoginInOtherTabsUrl": "/auth/realms/myrealm/login-actions/switch?client_id=account&tab_id=HoAx28ja4xg"
resourcesCommonPath: `${resourcesPath}/${resources_common}`,
loginRestartFlowUrl:
"/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
loginUrl:
"/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
ssoLoginInOtherTabsUrl:
"/auth/realms/myrealm/login-actions/switch?client_id=account&tab_id=HoAx28ja4xg"
},
"realm": {
"name": "myrealm",
"displayName": "myrealm",
"displayNameHtml": "myrealm",
"internationalizationEnabled": true,
"registrationEmailAsUsername": false
realm: {
name: "myrealm",
displayName: "myrealm",
displayNameHtml: "myrealm",
internationalizationEnabled: true,
registrationEmailAsUsername: false
},
"messagesPerField": {
"printIfExists": () => {
messagesPerField: {
printIfExists: () => {
return undefined;
},
"existsError": () => false,
"get": fieldName => `Fake error for ${fieldName}`,
"exists": () => false,
"getFirstError": fieldName => `Fake error for ${fieldName}`
existsError: () => false,
get: fieldName => `Fake error for ${fieldName}`,
exists: () => false,
getFirstError: fieldName => `Fake error for ${fieldName}`
},
"locale": {
"supported": [
locale: {
supported: [
/* spell-checker: disable */
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
"label": "Deutsch",
"languageTag": "de"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
label: "Deutsch",
languageTag: "de"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
"label": "Norsk",
"languageTag": "no"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
label: "Norsk",
languageTag: "no"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
"label": "Русский",
"languageTag": "ru"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
label: "Русский",
languageTag: "ru"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
"label": "Svenska",
"languageTag": "sv"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
label: "Svenska",
languageTag: "sv"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
"label": "Português (Brasil)",
"languageTag": "pt-BR"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
label: "Português (Brasil)",
languageTag: "pt-BR"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
"label": "Lietuvių",
"languageTag": "lt"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
label: "Lietuvių",
languageTag: "lt"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
"label": "English",
"languageTag": "en"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
label: "English",
languageTag: "en"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
"label": "Italiano",
"languageTag": "it"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
label: "Italiano",
languageTag: "it"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
"label": "Français",
"languageTag": "fr"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
label: "Français",
languageTag: "fr"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
"label": "中文简体",
"languageTag": "zh-CN"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
label: "中文简体",
languageTag: "zh-CN"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
"label": "Español",
"languageTag": "es"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
label: "Español",
languageTag: "es"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
"label": "Čeština",
"languageTag": "cs"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
label: "Čeština",
languageTag: "cs"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
"label": "日本語",
"languageTag": "ja"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
label: "日本語",
languageTag: "ja"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
"label": "Slovenčina",
"languageTag": "sk"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
label: "Slovenčina",
languageTag: "sk"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
"label": "Polski",
"languageTag": "pl"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
label: "Polski",
languageTag: "pl"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
"label": "Català",
"languageTag": "ca"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
label: "Català",
languageTag: "ca"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
"label": "Nederlands",
"languageTag": "nl"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
label: "Nederlands",
languageTag: "nl"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
"label": "Türkçe",
"languageTag": "tr"
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
label: "Türkçe",
languageTag: "tr"
}
/* spell-checker: enable */
],
"currentLanguageTag": "en"
currentLanguageTag: "en"
},
"auth": {
"showUsername": false,
"showResetCredentials": false,
"showTryAnotherWayLink": false
auth: {
showUsername: false,
showResetCredentials: false,
showTryAnotherWayLink: false
},
"client": {
"clientId": "myApp",
"attributes": {}
client: {
clientId: "myApp",
attributes: {}
},
"scripts": [],
"isAppInitiatedAction": false,
"properties": {}
scripts: [],
isAppInitiatedAction: false,
properties: {}
};
const loginUrl = {
...kcContextCommonMock.url,
"loginResetCredentialsUrl": "/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg",
"registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg",
"oauth2DeviceVerificationAction": "/auth/realms/myrealm/device",
"oauthAction": "/auth/realms/myrealm/login-actions/consent?client_id=account&tab_id=HoAx28ja4xg"
loginResetCredentialsUrl:
"/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg",
registrationUrl:
"/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg",
oauth2DeviceVerificationAction: "/auth/realms/myrealm/device",
oauthAction:
"/auth/realms/myrealm/login-actions/consent?client_id=account&tab_id=HoAx28ja4xg"
};
export const kcContextMocks = [
id<KcContext.Login>({
...kcContextCommonMock,
"pageId": "login.ftl",
"url": loginUrl,
"realm": {
pageId: "login.ftl",
url: loginUrl,
realm: {
...kcContextCommonMock.realm,
"loginWithEmailAllowed": true,
"rememberMe": true,
"password": true,
"resetPasswordAllowed": true,
"registrationAllowed": true
loginWithEmailAllowed: true,
rememberMe: true,
password: true,
resetPasswordAllowed: true,
registrationAllowed: true
},
"auth": kcContextCommonMock.auth!,
"social": {
"displayInfo": true
auth: kcContextCommonMock.auth!,
social: {
displayInfo: true
},
"usernameHidden": false,
"login": {},
"registrationDisabled": false
usernameHidden: false,
login: {},
registrationDisabled: false
}),
id<KcContext.Register>({
...kcContextCommonMock,
"url": {
url: {
...loginUrl,
"registrationAction":
registrationAction:
"http://localhost:8080/auth/realms/myrealm/login-actions/registration?session_code=gwZdUeO7pbYpFTRxiIxRg_QtzMbtFTKrNu6XW_f8asM&execution=12146ce0-b139-4bbd-b25b-0eccfee6577e&client_id=account&tab_id=uS8lYfebLa0"
},
"isAppInitiatedAction": false,
"passwordRequired": true,
"recaptchaRequired": false,
"pageId": "register.ftl",
"profile": {
isAppInitiatedAction: false,
passwordRequired: true,
recaptchaRequired: false,
pageId: "register.ftl",
profile: {
attributes,
attributesByName
},
"scripts": [
scripts: [
//"https://www.google.com/recaptcha/api.js"
]
}),
id<KcContext.Info>({
...kcContextCommonMock,
"pageId": "info.ftl",
"messageHeader": "<Message header>",
"requiredActions": undefined,
"skipLink": false,
"actionUri": "#",
"client": {
"clientId": "myApp",
"baseUrl": "#",
"attributes": {}
pageId: "info.ftl",
messageHeader: "<Message header>",
requiredActions: undefined,
skipLink: false,
actionUri: "#",
client: {
clientId: "myApp",
baseUrl: "#",
attributes: {}
}
}),
id<KcContext.Error>({
...kcContextCommonMock,
"pageId": "error.ftl",
"client": {
"clientId": "myApp",
"baseUrl": "#",
"attributes": {}
pageId: "error.ftl",
client: {
clientId: "myApp",
baseUrl: "#",
attributes: {}
},
"message": {
"type": "error",
"summary": "This is the error message"
message: {
type: "error",
summary: "This is the error message"
}
}),
id<KcContext.LoginResetPassword>({
...kcContextCommonMock,
"pageId": "login-reset-password.ftl",
"realm": {
pageId: "login-reset-password.ftl",
realm: {
...kcContextCommonMock.realm,
"loginWithEmailAllowed": false,
"duplicateEmailsAllowed": false
loginWithEmailAllowed: false,
duplicateEmailsAllowed: false
},
"url": loginUrl,
"auth": {}
url: loginUrl,
auth: {}
}),
id<KcContext.LoginVerifyEmail>({
...kcContextCommonMock,
"pageId": "login-verify-email.ftl",
"user": {
"email": "john.doe@gmail.com"
pageId: "login-verify-email.ftl",
user: {
email: "john.doe@gmail.com"
}
}),
id<KcContext.Terms>({
...kcContextCommonMock,
"pageId": "terms.ftl"
pageId: "terms.ftl"
}),
id<KcContext.LoginDeviceVerifyUserCode>({
...kcContextCommonMock,
"pageId": "login-oauth2-device-verify-user-code.ftl",
pageId: "login-oauth2-device-verify-user-code.ftl",
url: loginUrl
}),
id<KcContext.LoginOauthGrant>({
...kcContextCommonMock,
"pageId": "login-oauth-grant.ftl",
pageId: "login-oauth-grant.ftl",
oauth: {
code: "5-1N4CIzfi1aprIQjmylI-9e3spLCWW9i5d-GDcs-Sw",
clientScopesRequested: [
@ -332,150 +344,152 @@ export const kcContextMocks = [
}),
id<KcContext.LoginOtp>({
...kcContextCommonMock,
"pageId": "login-otp.ftl",
"otpLogin": {
"userOtpCredentials": [
pageId: "login-otp.ftl",
otpLogin: {
userOtpCredentials: [
{
"id": "id1",
"userLabel": "label1"
id: "id1",
userLabel: "label1"
},
{
"id": "id2",
"userLabel": "label2"
id: "id2",
userLabel: "label2"
}
]
}
}),
id<KcContext.LoginUsername>({
...kcContextCommonMock,
"pageId": "login-username.ftl",
"url": loginUrl,
"realm": {
pageId: "login-username.ftl",
url: loginUrl,
realm: {
...kcContextCommonMock.realm,
"loginWithEmailAllowed": true,
"rememberMe": true,
"password": true,
"resetPasswordAllowed": true,
"registrationAllowed": true
loginWithEmailAllowed: true,
rememberMe: true,
password: true,
resetPasswordAllowed: true,
registrationAllowed: true
},
"social": {
"displayInfo": true
social: {
displayInfo: true
},
"usernameHidden": false,
"login": {},
"registrationDisabled": false
usernameHidden: false,
login: {},
registrationDisabled: false
}),
id<KcContext.LoginPassword>({
...kcContextCommonMock,
"pageId": "login-password.ftl",
"url": loginUrl,
"realm": {
pageId: "login-password.ftl",
url: loginUrl,
realm: {
...kcContextCommonMock.realm,
"resetPasswordAllowed": true
resetPasswordAllowed: true
},
"social": {
"displayInfo": false
social: {
displayInfo: false
}
}),
id<KcContext.WebauthnAuthenticate>({
...kcContextCommonMock,
"pageId": "webauthn-authenticate.ftl",
"url": loginUrl,
"authenticators": {
"authenticators": []
pageId: "webauthn-authenticate.ftl",
url: loginUrl,
authenticators: {
authenticators: []
},
"realm": {
realm: {
...kcContextCommonMock.realm,
"password": true,
"registrationAllowed": true
password: true,
registrationAllowed: true
},
"challenge": "",
"userVerification": "not specified",
"rpId": "",
"createTimeout": "0",
"isUserIdentified": "false",
"shouldDisplayAuthenticators": false,
"social": {
"displayInfo": false
challenge: "",
userVerification: "not specified",
rpId: "",
createTimeout: "0",
isUserIdentified: "false",
shouldDisplayAuthenticators: false,
social: {
displayInfo: false
},
"login": {}
login: {}
}),
id<KcContext.LoginUpdatePassword>({
...kcContextCommonMock,
"pageId": "login-update-password.ftl"
pageId: "login-update-password.ftl"
}),
id<KcContext.LoginUpdateProfile>({
...kcContextCommonMock,
"pageId": "login-update-profile.ftl",
"profile": {
pageId: "login-update-profile.ftl",
profile: {
attributes,
attributesByName
}
}),
id<KcContext.LoginIdpLinkConfirm>({
...kcContextCommonMock,
"pageId": "login-idp-link-confirm.ftl",
"idpAlias": "FranceConnect"
pageId: "login-idp-link-confirm.ftl",
idpAlias: "FranceConnect"
}),
id<KcContext.LoginIdpLinkEmail>({
...kcContextCommonMock,
"pageId": "login-idp-link-email.ftl",
"idpAlias": "FranceConnect",
"brokerContext": {
"username": "anUsername"
pageId: "login-idp-link-email.ftl",
idpAlias: "FranceConnect",
brokerContext: {
username: "anUsername"
}
}),
id<KcContext.LoginConfigTotp>({
...kcContextCommonMock,
"pageId": "login-config-totp.ftl",
"totp": {
"totpSecretEncoded": "KVVF G2BY N4YX S6LB IUYT K2LH IFYE 4SBV",
"qrUrl": "#",
"totpSecretQrCode":
pageId: "login-config-totp.ftl",
totp: {
totpSecretEncoded: "KVVF G2BY N4YX S6LB IUYT K2LH IFYE 4SBV",
qrUrl: "#",
totpSecretQrCode:
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACM0lEQVR4Xu3OIZJgOQwDUDFd2UxiurLAVnnbHw4YGDKtSiWOn4Gxf81//7r/+q8b4HfLGBZDK9d85NmNR+sB42sXvOYrN5P1DcgYYFTGfOlbzE8gzwy3euweGizw7cfdl34/GRhlkxjKNV+5AebPXPORX1JuB9x8ZfbyyD2y1krWAKsbMq1HnqQDaLfa77p4+MqvzEGSqvSAD/2IHW2yHaigR9tX3m8dDIYGcNf3f+gDpVBZbZU77zyJ6Rlcy+qoTMG887KAPD9hsh6a1Sv3gJUHGHUAxSMzj7zqDDe7Phmt2eG+8UsMxjRGm816MAO+8VMl1R1jGHOrZB/5Zo/WXAPgxixm9Mo96vDGrM1eOto8c4Ax4wF437mifOXlpiPzCnN7Y9l95NnEMxgMY9AAGA8fucH14Y1aVb6N/cqrmyh0BVht7k1e+bU8LK0Cg5vmVq9c5vHIjOfqxDIfeTraNVTwewa4wVe+SW5N+uP1qACeudUZbqGOfA6VZV750Noq2Xx3kpveV44ZelSV1V7KFHzkWyVrrlUwG0Pl9pWnoy3vsQoME6vKI69i5osVgwWzHT7zjmJtMcNUSVn1oYMd7ZodbgowZl45VG0uVuLPUr1yc79uaQBag/mqR34xhlWyHm1prplHboCWdZ4TeZjsK8+dI+jbz1C5hl65mcpgB5dhcj8+dGO+0Ko68+lD37JDD83dpDLzzK+TrQyaVwGj6pUboGV+7+AyN8An/pf84/7rv/4/1l4OCc/1BYMAAAAASUVORK5CYII=",
"manualUrl": "#",
"totpSecret": "G4nsI8lQagRMUchH8jEG",
"otpCredentials": [],
"supportedApplications": ["FreeOTP", "Google Authenticator"],
"policy": {
"algorithm": "HmacSHA1",
"digits": 6,
"lookAheadWindow": 1,
"type": "totp",
"period": 30,
"getAlgorithmKey": () => "SHA1"
manualUrl: "#",
totpSecret: "G4nsI8lQagRMUchH8jEG",
otpCredentials: [],
supportedApplications: ["FreeOTP", "Google Authenticator"],
policy: {
algorithm: "HmacSHA1",
digits: 6,
lookAheadWindow: 1,
type: "totp",
period: 30,
getAlgorithmKey: () => "SHA1"
}
}
}),
id<KcContext.LogoutConfirm>({
...kcContextCommonMock,
"pageId": "logout-confirm.ftl",
"url": {
pageId: "logout-confirm.ftl",
url: {
...kcContextCommonMock.url,
"logoutConfirmAction": "Continuer?"
logoutConfirmAction: "Continuer?"
},
"client": {
"clientId": "myApp",
"baseUrl": "#",
"attributes": {}
client: {
clientId: "myApp",
baseUrl: "#",
attributes: {}
},
"logoutConfirm": { "code": "123", skipLink: false }
logoutConfirm: { code: "123", skipLink: false }
}),
id<KcContext.IdpReviewUserProfile>({
...kcContextCommonMock,
"pageId": "idp-review-user-profile.ftl",
"profile": {
pageId: "idp-review-user-profile.ftl",
profile: {
attributes,
attributesByName
}
}),
id<KcContext.UpdateEmail>({
...kcContextCommonMock,
"pageId": "update-email.ftl",
"profile": {
"attributes": attributes.filter(attribute => attribute.name === "email"),
"attributesByName": Object.fromEntries(
attributes.filter(attribute => attribute.name === "email").map(attribute => [attribute.name, attribute])
pageId: "update-email.ftl",
profile: {
attributes: attributes.filter(attribute => attribute.name === "email"),
attributesByName: Object.fromEntries(
attributes
.filter(attribute => attribute.name === "email")
.map(attribute => [attribute.name, attribute])
)
}
}),
@ -502,8 +516,8 @@ export const kcContextMocks = [
id<KcContext.SamlPostForm>({
...kcContextCommonMock,
pageId: "saml-post-form.ftl",
"samlPost": {
"url": ""
samlPost: {
url: ""
}
}),
id<KcContext.LoginPageExpired>({
@ -514,107 +528,107 @@ export const kcContextMocks = [
id<KcContext.FrontchannelLogout>({
...kcContextCommonMock,
pageId: "frontchannel-logout.ftl",
"logout": {
"clients": [
logout: {
clients: [
{
"name": "myApp",
"frontChannelLogoutUrl": "#"
name: "myApp",
frontChannelLogoutUrl: "#"
},
{
"name": "myApp2",
"frontChannelLogoutUrl": "#"
name: "myApp2",
frontChannelLogoutUrl: "#"
}
]
}
}),
id<KcContext.WebauthnRegister>({
"pageId": "webauthn-register.ftl",
pageId: "webauthn-register.ftl",
...kcContextCommonMock,
"challenge": "random-challenge-string",
"userid": "user123",
"username": "johndoe",
"signatureAlgorithms": ["ES256", "RS256"],
"rpEntityName": "Example Corp",
"rpId": "example.com",
"attestationConveyancePreference": "direct",
"authenticatorAttachment": "platform",
"requireResidentKey": "required",
"userVerificationRequirement": "preferred",
"createTimeout": 60000,
"excludeCredentialIds": "credId123,credId456",
"isSetRetry": false,
"isAppInitiatedAction": true
challenge: "random-challenge-string",
userid: "user123",
username: "johndoe",
signatureAlgorithms: ["ES256", "RS256"],
rpEntityName: "Example Corp",
rpId: "example.com",
attestationConveyancePreference: "direct",
authenticatorAttachment: "platform",
requireResidentKey: "required",
userVerificationRequirement: "preferred",
createTimeout: 60000,
excludeCredentialIds: "credId123,credId456",
isSetRetry: false,
isAppInitiatedAction: true
}),
id<KcContext.DeleteCredential>({
"pageId": "delete-credential.ftl",
pageId: "delete-credential.ftl",
...kcContextCommonMock,
"credentialLabel": "myCredential"
credentialLabel: "myCredential"
}),
id<KcContext.Code>({
"pageId": "code.ftl",
pageId: "code.ftl",
...kcContextCommonMock,
"code": {
"success": true,
"code": "123456"
code: {
success: true,
code: "123456"
}
}),
id<KcContext.DeleteAccountConfirm>({
"pageId": "delete-account-confirm.ftl",
pageId: "delete-account-confirm.ftl",
...kcContextCommonMock,
"triggered_from_aia": true
triggered_from_aia: true
}),
id<KcContext.LoginRecoveryAuthnCodeConfig>({
"pageId": "login-recovery-authn-code-config.ftl",
pageId: "login-recovery-authn-code-config.ftl",
...kcContextCommonMock,
"recoveryAuthnCodesConfigBean": {
"generatedRecoveryAuthnCodesList": ["code123", "code456", "code789"],
"generatedRecoveryAuthnCodesAsString": "code123, code456, code789",
"generatedAt": new Date().getTime()
recoveryAuthnCodesConfigBean: {
generatedRecoveryAuthnCodesList: ["code123", "code456", "code789"],
generatedRecoveryAuthnCodesAsString: "code123, code456, code789",
generatedAt: new Date().getTime()
}
}),
id<KcContext.LoginRecoveryAuthnCodeInput>({
"pageId": "login-recovery-authn-code-input.ftl",
pageId: "login-recovery-authn-code-input.ftl",
...kcContextCommonMock,
"recoveryAuthnCodesInputBean": {
"codeNumber": 1234
recoveryAuthnCodesInputBean: {
codeNumber: 1234
}
}),
id<KcContext.LoginResetOtp>({
"pageId": "login-reset-otp.ftl",
pageId: "login-reset-otp.ftl",
...kcContextCommonMock,
"configuredOtpCredentials": {
"userOtpCredentials": [
configuredOtpCredentials: {
userOtpCredentials: [
{
"id": "otpId1",
"userLabel": "OTP Device 1"
id: "otpId1",
userLabel: "OTP Device 1"
},
{
"id": "otpId2",
"userLabel": "OTP Device 2"
id: "otpId2",
userLabel: "OTP Device 2"
},
{
"id": "otpId3",
"userLabel": "Backup OTP"
id: "otpId3",
userLabel: "Backup OTP"
}
],
"selectedCredentialId": "otpId2"
selectedCredentialId: "otpId2"
}
}),
id<KcContext.LoginX509Info>({
"pageId": "login-x509-info.ftl",
pageId: "login-x509-info.ftl",
...kcContextCommonMock,
"x509": {
"formData": {
"subjectDN": "CN=John Doe, O=Example Corp, C=US",
"isUserEnabled": true,
"username": "johndoe"
x509: {
formData: {
subjectDN: "CN=John Doe, O=Example Corp, C=US",
isUserEnabled: true,
username: "johndoe"
}
}
}),
id<KcContext.WebauthnError>({
"pageId": "webauthn-error.ftl",
pageId: "webauthn-error.ftl",
...kcContextCommonMock,
"isAppInitiatedAction": true
isAppInitiatedAction: true
})
];

View File

@ -33,7 +33,9 @@ export function useDownloadTerms(params: {
const downloadTermMarkdownConst = useConstCallback(downloadTermMarkdown);
const downloadTermMarkdownMemoized = useConst(() =>
memoize((currentLanguageTag: string) => downloadTermMarkdownConst({ currentLanguageTag }))
memoize((currentLanguageTag: string) =>
downloadTermMarkdownConst({ currentLanguageTag })
)
);
return { downloadTermMarkdownMemoized };
@ -41,9 +43,9 @@ export function useDownloadTerms(params: {
useEffect(() => {
if (kcContext.pageId === "terms.ftl" || kcContext.termsAcceptanceRequired) {
downloadTermMarkdownMemoized(kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag).then(
thermMarkdown => (evtTermsMarkdown.state = thermMarkdown)
);
downloadTermMarkdownMemoized(
kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag
).then(thermMarkdown => (evtTermsMarkdown.state = thermMarkdown));
}
}, []);
}

View File

@ -2,131 +2,138 @@ import { createUseClassName } from "keycloakify/lib/useGetClassName";
import type { ClassKey } from "keycloakify/login/TemplateProps";
export const { useGetClassName } = createUseClassName<ClassKey>({
"defaultClasses": {
"kcBodyClass": undefined,
"kcHeaderWrapperClass": undefined,
"kcLocaleWrapperClass": undefined,
"kcInfoAreaWrapperClass": undefined,
"kcFormButtonsWrapperClass": undefined,
"kcFormOptionsWrapperClass": undefined,
"kcLocaleDropDownClass": undefined,
"kcLocaleListItemClass": undefined,
"kcContentWrapperClass": undefined,
"kcCheckboxInputClass": undefined,
defaultClasses: {
kcBodyClass: undefined,
kcHeaderWrapperClass: undefined,
kcLocaleWrapperClass: undefined,
kcInfoAreaWrapperClass: undefined,
kcFormButtonsWrapperClass: undefined,
kcFormOptionsWrapperClass: undefined,
kcLocaleDropDownClass: undefined,
kcLocaleListItemClass: undefined,
kcContentWrapperClass: undefined,
kcCheckboxInputClass: undefined,
"kcLogoIdP-facebook": "fa fa-facebook",
"kcAuthenticatorOTPClass": "fa fa-mobile list-view-pf-icon-lg",
kcAuthenticatorOTPClass: "fa fa-mobile list-view-pf-icon-lg",
"kcLogoIdP-bitbucket": "fa fa-bitbucket",
"kcAuthenticatorWebAuthnClass": "fa fa-key list-view-pf-icon-lg",
"kcWebAuthnDefaultIcon": "pficon pficon-key",
kcAuthenticatorWebAuthnClass: "fa fa-key list-view-pf-icon-lg",
kcWebAuthnDefaultIcon: "pficon pficon-key",
"kcLogoIdP-stackoverflow": "fa fa-stack-overflow",
"kcSelectAuthListItemClass": "pf-l-stack__item select-auth-box-parent pf-l-split",
kcSelectAuthListItemClass: "pf-l-stack__item select-auth-box-parent pf-l-split",
"kcLogoIdP-microsoft": "fa fa-windows",
"kcLoginOTPListItemHeaderClass": "pf-c-tile__header",
"kcLocaleItemClass": "pf-c-dropdown__menu-item",
"kcLoginOTPListItemIconBodyClass": "pf-c-tile__icon",
"kcInputHelperTextAfterClass": "pf-c-form__helper-text pf-c-form__helper-text-after",
"kcFormClass": "form-horizontal",
"kcSelectAuthListClass": "pf-l-stack select-auth-container",
"kcInputClassRadioCheckboxLabelDisabled": "pf-m-disabled",
"kcSelectAuthListItemIconClass": "pf-l-split__item select-auth-box-icon",
"kcRecoveryCodesWarning": "kc-recovery-codes-warning",
"kcFormSettingClass": "login-pf-settings",
"kcWebAuthnBLE": "fa fa-bluetooth-b",
"kcInputWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
"kcSelectAuthListItemArrowIconClass": "fa fa-angle-right fa-lg",
"kcFeedbackAreaClass": "col-md-12",
"kcFormPasswordVisibilityButtonClass": "pf-c-button pf-m-control",
kcLoginOTPListItemHeaderClass: "pf-c-tile__header",
kcLocaleItemClass: "pf-c-dropdown__menu-item",
kcLoginOTPListItemIconBodyClass: "pf-c-tile__icon",
kcInputHelperTextAfterClass:
"pf-c-form__helper-text pf-c-form__helper-text-after",
kcFormClass: "form-horizontal",
kcSelectAuthListClass: "pf-l-stack select-auth-container",
kcInputClassRadioCheckboxLabelDisabled: "pf-m-disabled",
kcSelectAuthListItemIconClass: "pf-l-split__item select-auth-box-icon",
kcRecoveryCodesWarning: "kc-recovery-codes-warning",
kcFormSettingClass: "login-pf-settings",
kcWebAuthnBLE: "fa fa-bluetooth-b",
kcInputWrapperClass: "col-xs-12 col-sm-12 col-md-12 col-lg-12",
kcSelectAuthListItemArrowIconClass: "fa fa-angle-right fa-lg",
kcFeedbackAreaClass: "col-md-12",
kcFormPasswordVisibilityButtonClass: "pf-c-button pf-m-control",
"kcLogoIdP-google": "fa fa-google",
"kcCheckLabelClass": "pf-c-check__label",
"kcSelectAuthListItemFillClass": "pf-l-split__item pf-m-fill",
"kcAuthenticatorDefaultClass": "fa fa-list list-view-pf-icon-lg",
kcCheckLabelClass: "pf-c-check__label",
kcSelectAuthListItemFillClass: "pf-l-split__item pf-m-fill",
kcAuthenticatorDefaultClass: "fa fa-list list-view-pf-icon-lg",
"kcLogoIdP-gitlab": "fa fa-gitlab",
"kcFormAreaClass": "col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2 col-lg-8 col-lg-offset-2",
"kcFormButtonsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
"kcInputClassRadioLabel": "pf-c-radio__label",
"kcAuthenticatorWebAuthnPasswordlessClass": "fa fa-key list-view-pf-icon-lg",
"kcSelectAuthListItemHeadingClass": "pf-l-stack__item select-auth-box-headline pf-c-title",
"kcInfoAreaClass": "col-xs-12 col-sm-4 col-md-4 col-lg-5 details",
"kcLogoLink": "http://www.keycloak.org",
"kcContainerClass": "container-fluid",
"kcSelectAuthListItemTitle": "select-auth-box-paragraph",
"kcHtmlClass": "login-pf",
"kcLoginOTPListItemTitleClass": "pf-c-tile__title",
kcFormAreaClass:
"col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2 col-lg-8 col-lg-offset-2",
kcFormButtonsClass: "col-xs-12 col-sm-12 col-md-12 col-lg-12",
kcInputClassRadioLabel: "pf-c-radio__label",
kcAuthenticatorWebAuthnPasswordlessClass: "fa fa-key list-view-pf-icon-lg",
kcSelectAuthListItemHeadingClass:
"pf-l-stack__item select-auth-box-headline pf-c-title",
kcInfoAreaClass: "col-xs-12 col-sm-4 col-md-4 col-lg-5 details",
kcLogoLink: "http://www.keycloak.org",
kcContainerClass: "container-fluid",
kcSelectAuthListItemTitle: "select-auth-box-paragraph",
kcHtmlClass: "login-pf",
kcLoginOTPListItemTitleClass: "pf-c-tile__title",
"kcLogoIdP-openshift-v4": "pf-icon pf-icon-openshift",
"kcWebAuthnUnknownIcon": "pficon pficon-key unknown-transport-class",
"kcFormSocialAccountNameClass": "kc-social-provider-name",
kcWebAuthnUnknownIcon: "pficon pficon-key unknown-transport-class",
kcFormSocialAccountNameClass: "kc-social-provider-name",
"kcLogoIdP-openshift-v3": "pf-icon pf-icon-openshift",
"kcLoginOTPListInputClass": "pf-c-tile__input",
"kcWebAuthnUSB": "fa fa-usb",
"kcInputClassRadio": "pf-c-radio",
"kcWebAuthnKeyIcon": "pficon pficon-key",
"kcFeedbackInfoIcon": "fa fa-fw fa-info-circle",
"kcCommonLogoIdP": "kc-social-provider-logo kc-social-gray",
"kcRecoveryCodesActions": "kc-recovery-codes-actions",
"kcFormGroupHeader": "pf-c-form__group",
"kcFormSocialAccountSectionClass": "kc-social-section kc-social-gray",
kcLoginOTPListInputClass: "pf-c-tile__input",
kcWebAuthnUSB: "fa fa-usb",
kcInputClassRadio: "pf-c-radio",
kcWebAuthnKeyIcon: "pficon pficon-key",
kcFeedbackInfoIcon: "fa fa-fw fa-info-circle",
kcCommonLogoIdP: "kc-social-provider-logo kc-social-gray",
kcRecoveryCodesActions: "kc-recovery-codes-actions",
kcFormGroupHeader: "pf-c-form__group",
kcFormSocialAccountSectionClass: "kc-social-section kc-social-gray",
"kcLogoIdP-instagram": "fa fa-instagram",
"kcAlertClass": "pf-c-alert pf-m-inline",
"kcHeaderClass": "login-pf-page-header",
"kcLabelWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
"kcFormPasswordVisibilityIconShow": "fa fa-eye",
"kcFormSocialAccountLinkClass": "pf-c-login__main-footer-links-item-link",
"kcLocaleMainClass": "pf-c-dropdown",
"kcInputGroup": "pf-c-input-group",
"kcTextareaClass": "form-control",
"kcButtonBlockClass": "pf-m-block",
"kcButtonClass": "pf-c-button",
"kcWebAuthnNFC": "fa fa-wifi",
"kcLocaleClass": "col-xs-12 col-sm-1",
"kcInputClassCheckboxInput": "pf-c-check__input",
"kcFeedbackErrorIcon": "fa fa-fw fa-exclamation-circle",
"kcInputLargeClass": "input-lg",
"kcInputErrorMessageClass": "pf-c-form__helper-text pf-m-error required kc-feedback-text",
"kcRecoveryCodesList": "kc-recovery-codes-list",
"kcFormSocialAccountListClass": "pf-c-login__main-footer-links kc-social-links",
"kcAlertTitleClass": "pf-c-alert__title kc-feedback-text",
"kcAuthenticatorPasswordClass": "fa fa-unlock list-view-pf-icon-lg",
"kcCheckInputClass": "pf-c-check__input",
kcAlertClass: "pf-c-alert pf-m-inline",
kcHeaderClass: "login-pf-page-header",
kcLabelWrapperClass: "col-xs-12 col-sm-12 col-md-12 col-lg-12",
kcFormPasswordVisibilityIconShow: "fa fa-eye",
kcFormSocialAccountLinkClass: "pf-c-login__main-footer-links-item-link",
kcLocaleMainClass: "pf-c-dropdown",
kcInputGroup: "pf-c-input-group",
kcTextareaClass: "form-control",
kcButtonBlockClass: "pf-m-block",
kcButtonClass: "pf-c-button",
kcWebAuthnNFC: "fa fa-wifi",
kcLocaleClass: "col-xs-12 col-sm-1",
kcInputClassCheckboxInput: "pf-c-check__input",
kcFeedbackErrorIcon: "fa fa-fw fa-exclamation-circle",
kcInputLargeClass: "input-lg",
kcInputErrorMessageClass:
"pf-c-form__helper-text pf-m-error required kc-feedback-text",
kcRecoveryCodesList: "kc-recovery-codes-list",
kcFormSocialAccountListClass: "pf-c-login__main-footer-links kc-social-links",
kcAlertTitleClass: "pf-c-alert__title kc-feedback-text",
kcAuthenticatorPasswordClass: "fa fa-unlock list-view-pf-icon-lg",
kcCheckInputClass: "pf-c-check__input",
"kcLogoIdP-linkedin": "fa fa-linkedin",
"kcLogoIdP-twitter": "fa fa-twitter",
"kcFeedbackWarningIcon": "fa fa-fw fa-exclamation-triangle",
"kcResetFlowIcon": "pficon pficon-arrow fa",
"kcSelectAuthListItemIconPropertyClass": "fa-2x select-auth-box-icon-properties",
"kcFeedbackSuccessIcon": "fa fa-fw fa-check-circle",
"kcLoginOTPListClass": "pf-c-tile",
"kcSrOnlyClass": "sr-only",
"kcFormSocialAccountListGridClass": "pf-l-grid kc-social-grid",
"kcButtonDefaultClass": "btn-default",
"kcFormGroupErrorClass": "has-error",
"kcSelectAuthListItemDescriptionClass": "pf-l-stack__item select-auth-box-desc",
"kcSelectAuthListItemBodyClass": "pf-l-split__item pf-l-stack",
"kcWebAuthnInternal": "pficon pficon-key",
"kcSelectAuthListItemArrowClass": "pf-l-split__item select-auth-box-arrow",
"kcCheckClass": "pf-c-check",
"kcContentClass": "col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-6 col-lg-offset-3",
"kcLogoClass": "login-pf-brand",
"kcLoginOTPListItemIconClass": "fa fa-mobile",
"kcLoginClass": "login-pf-page",
"kcSignUpClass": "login-pf-signup",
"kcButtonLargeClass": "btn-lg",
"kcFormCardClass": "card-pf",
"kcLocaleListClass": "pf-c-dropdown__menu pf-m-align-right",
"kcInputClass": "pf-c-form-control",
"kcFormGroupClass": "form-group",
kcFeedbackWarningIcon: "fa fa-fw fa-exclamation-triangle",
kcResetFlowIcon: "pficon pficon-arrow fa",
kcSelectAuthListItemIconPropertyClass: "fa-2x select-auth-box-icon-properties",
kcFeedbackSuccessIcon: "fa fa-fw fa-check-circle",
kcLoginOTPListClass: "pf-c-tile",
kcSrOnlyClass: "sr-only",
kcFormSocialAccountListGridClass: "pf-l-grid kc-social-grid",
kcButtonDefaultClass: "btn-default",
kcFormGroupErrorClass: "has-error",
kcSelectAuthListItemDescriptionClass: "pf-l-stack__item select-auth-box-desc",
kcSelectAuthListItemBodyClass: "pf-l-split__item pf-l-stack",
kcWebAuthnInternal: "pficon pficon-key",
kcSelectAuthListItemArrowClass: "pf-l-split__item select-auth-box-arrow",
kcCheckClass: "pf-c-check",
kcContentClass:
"col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-6 col-lg-offset-3",
kcLogoClass: "login-pf-brand",
kcLoginOTPListItemIconClass: "fa fa-mobile",
kcLoginClass: "login-pf-page",
kcSignUpClass: "login-pf-signup",
kcButtonLargeClass: "btn-lg",
kcFormCardClass: "card-pf",
kcLocaleListClass: "pf-c-dropdown__menu pf-m-align-right",
kcInputClass: "pf-c-form-control",
kcFormGroupClass: "form-group",
"kcLogoIdP-paypal": "fa fa-paypal",
"kcInputClassCheckbox": "pf-c-check",
"kcRecoveryCodesConfirmation": "kc-recovery-codes-confirmation",
"kcFormPasswordVisibilityIconHide": "fa fa-eye-slash",
"kcInputClassRadioInput": "pf-c-radio__input",
"kcFormSocialAccountListButtonClass": "pf-c-button pf-m-control pf-m-block kc-social-item kc-social-gray",
"kcInputClassCheckboxLabel": "pf-c-check__label",
"kcFormOptionsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
"kcFormHeaderClass": "login-pf-header",
"kcFormSocialAccountGridItem": "pf-l-grid__item",
"kcButtonPrimaryClass": "pf-m-primary",
"kcInputHelperTextBeforeClass": "pf-c-form__helper-text pf-c-form__helper-text-before",
kcInputClassCheckbox: "pf-c-check",
kcRecoveryCodesConfirmation: "kc-recovery-codes-confirmation",
kcFormPasswordVisibilityIconHide: "fa fa-eye-slash",
kcInputClassRadioInput: "pf-c-radio__input",
kcFormSocialAccountListButtonClass:
"pf-c-button pf-m-control pf-m-block kc-social-item kc-social-gray",
kcInputClassCheckboxLabel: "pf-c-check__label",
kcFormOptionsClass: "col-xs-12 col-sm-12 col-md-12 col-lg-12",
kcFormHeaderClass: "login-pf-header",
kcFormSocialAccountGridItem: "pf-l-grid__item",
kcButtonPrimaryClass: "pf-m-primary",
kcInputHelperTextBeforeClass:
"pf-c-form__helper-text pf-c-form__helper-text-before",
"kcLogoIdP-github": "fa fa-github",
"kcLabelClass": "pf-c-form__label pf-c-form__label-text"
kcLabelClass: "pf-c-form__label pf-c-form__label-text"
}
});

View File

@ -108,11 +108,11 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
const { kcContext, i18n, doMakeUserConfirmPassword } = params;
const { insertScriptTags } = useInsertScriptTags({
"scriptTags": Object.keys(kcContext.profile?.html5DataAnnotations ?? {})
scriptTags: Object.keys(kcContext.profile?.html5DataAnnotations ?? {})
.filter(key => key !== "kcMultivalued" && key !== "kcNumberFormat") // NOTE: Keycloakify handles it.
.map(key => ({
"type": "module",
"src": `${kcContext.url.resourcesPath}/js/${key}.js`
type: "module",
src: `${kcContext.url.resourcesPath}/js/${key}.js`
}))
});
@ -146,15 +146,15 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
.filter(name => (name !== "username" ? true : !kcContext.realm.registrationEmailAsUsername))
.map(name =>
id<Attribute>({
"name": name,
"displayName": id<`\${${MessageKey}}`>(`\${${name}}`),
"required": true,
"value": (kcContext.register as any).formData[name] ?? "",
"html5DataAnnotations": {},
"readOnly": false,
"validators": {},
"annotations": {},
"autocomplete": (() => {
name: name,
displayName: id<`\${${MessageKey}}`>(`\${${name}}`),
required: true,
value: (kcContext.register as any).formData[name] ?? "",
html5DataAnnotations: {},
readOnly: false,
validators: {},
annotations: {},
autocomplete: (() => {
switch (name) {
case "email":
return "email";
@ -174,15 +174,15 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
.filter(name => (name !== "username" ? true : (kcContext.user as any).editUsernameAllowed))
.map(name =>
id<Attribute>({
"name": name,
"displayName": id<`\${${MessageKey}}`>(`\${${name}}`),
"required": true,
"value": (kcContext as any).user[name] ?? "",
"html5DataAnnotations": {},
"readOnly": false,
"validators": {},
"annotations": {},
"autocomplete": (() => {
name: name,
displayName: id<`\${${MessageKey}}`>(`\${${name}}`),
required: true,
value: (kcContext as any).user[name] ?? "",
html5DataAnnotations: {},
readOnly: false,
validators: {},
annotations: {},
autocomplete: (() => {
switch (name) {
case "email":
return "email";
@ -200,15 +200,15 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
//NOTE: Handle legacy update-email.ftl
return [
id<Attribute>({
"name": "email",
"displayName": id<`\${${MessageKey}}`>(`\${email}`),
"required": true,
"value": (kcContext.email as any).value ?? "",
"html5DataAnnotations": {},
"readOnly": false,
"validators": {},
"annotations": {},
"autocomplete": "email"
name: "email",
displayName: id<`\${${MessageKey}}`>(`\${email}`),
required: true,
value: (kcContext.email as any).value ?? "",
html5DataAnnotations: {},
readOnly: false,
validators: {},
annotations: {},
autocomplete: "email"
})
];
}
@ -228,11 +228,11 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
return id<Attribute>({
...rest,
"group": {
"name": group,
"displayHeader": groupDisplayHeader,
"displayDescription": groupDisplayDescription,
"html5DataAnnotations": {}
group: {
name: group,
displayHeader: groupDisplayHeader,
displayDescription: groupDisplayDescription,
html5DataAnnotations: {}
}
});
}
@ -257,28 +257,28 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
syntheticAttributes.push(
{
"name": "password",
"displayName": id<`\${${MessageKey}}`>("${password}"),
"required": true,
"readOnly": false,
"validators": {},
"annotations": {},
"autocomplete": "new-password",
"html5DataAnnotations": {},
name: "password",
displayName: id<`\${${MessageKey}}`>("${password}"),
required: true,
readOnly: false,
validators: {},
annotations: {},
autocomplete: "new-password",
html5DataAnnotations: {},
// NOTE: Compat with Keycloak version prior to 24
...({ "groupAnnotations": {} } as {})
...({ groupAnnotations: {} } as {})
},
{
"name": "password-confirm",
"displayName": id<`\${${MessageKey}}`>("${passwordConfirm}"),
"required": true,
"readOnly": false,
"validators": {},
"annotations": {},
"html5DataAnnotations": {},
"autocomplete": "new-password",
name: "password-confirm",
displayName: id<`\${${MessageKey}}`>("${passwordConfirm}"),
required: true,
readOnly: false,
validators: {},
annotations: {},
html5DataAnnotations: {},
autocomplete: "new-password",
// NOTE: Compat with Keycloak version prior to 24
...({ "groupAnnotations": {} } as {})
...({ groupAnnotations: {} } as {})
}
);
}
@ -288,7 +288,10 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
})();
const initialFormFieldState = (() => {
const out: { attribute: Attribute; valueOrValues: string | string[] }[] = [];
const out: {
attribute: Attribute;
valueOrValues: string | string[];
}[] = [];
for (const attribute of syntheticAttributes) {
handle_multi_valued_attribute: {
@ -324,7 +327,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
out.push({
attribute,
"valueOrValues": values
valueOrValues: values
});
continue;
@ -332,7 +335,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
out.push({
attribute,
"valueOrValues": attribute.value ?? ""
valueOrValues: attribute.value ?? ""
});
}
@ -340,14 +343,14 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
})();
const initialState: internal.State = {
"formFieldStates": initialFormFieldState.map(({ attribute, valueOrValues }) => ({
formFieldStates: initialFormFieldState.map(({ attribute, valueOrValues }) => ({
attribute,
"errors": getErrors({
"attributeName": attribute.name,
"formFieldStates": initialFormFieldState
errors: getErrors({
attributeName: attribute.name,
formFieldStates: initialFormFieldState
}),
"hasLostFocusAtLeastOnce": valueOrValues instanceof Array ? valueOrValues.map(() => false) : false,
"valueOrValues": valueOrValues
hasLostFocusAtLeastOnce: valueOrValues instanceof Array ? valueOrValues.map(() => false) : false,
valueOrValues: valueOrValues
}))
};
@ -381,8 +384,8 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
}
formFieldState.errors = getErrors({
"attributeName": formAction.name,
"formFieldStates": state.formFieldStates
attributeName: formAction.name,
formFieldStates: state.formFieldStates
});
update_password_confirm: {
@ -395,9 +398,9 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
}
state = reducer(state, {
"action": "update",
"name": "password-confirm",
"valueOrValues": formAction.valueOrValues
action: "update",
name: "password-confirm",
valueOrValues: formAction.valueOrValues
});
}
@ -421,9 +424,9 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
const formState: FormState = useMemo(
() => ({
"formFieldStates": state.formFieldStates.map(
formFieldStates: state.formFieldStates.map(
({ errors, hasLostFocusAtLeastOnce: hasLostFocusAtLeastOnceOrArr, attribute, ...valueOrValuesWrap }) => ({
"displayableErrors": errors.filter(error => {
displayableErrors: errors.filter(error => {
const hasLostFocusAtLeastOnce =
typeof hasLostFocusAtLeastOnceOrArr === "boolean"
? hasLostFocusAtLeastOnceOrArr
@ -482,7 +485,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
...valueOrValuesWrap
})
),
"isFormSubmittable": state.formFieldStates.every(({ errors }) => errors.length === 0)
isFormSubmittable: state.formFieldStates.every(({ errors }) => errors.length === 0)
}),
[state]
);
@ -501,7 +504,13 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
const getErrors = useConstCallback(
(params: { attributeName: string; formFieldStates: { attribute: Attribute; valueOrValues: string | string[] }[] }): FormFieldError[] => {
(params: {
attributeName: string;
formFieldStates: {
attribute: Attribute;
valueOrValues: string | string[];
}[];
}): FormFieldError[] => {
const { attributeName, formFieldStates } = params;
const formFieldState = formFieldStates.find(({ attribute }) => attribute.name === attributeName);
@ -572,10 +581,10 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
return [
{
errorMessageStr,
"errorMessage": <span key={0}>{errorMessageStr}</span>,
"fieldIndex": undefined,
"source": {
"type": "server"
errorMessage: <span key={0}>{errorMessageStr}</span>,
fieldIndex: undefined,
source: {
type: "server"
}
}
];
@ -598,19 +607,19 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
.map((...[, index]) => {
const specificValueErrors = getErrors({
attributeName,
"formFieldStates": formFieldStates.map(formFieldState => {
formFieldStates: formFieldStates.map(formFieldState => {
if (formFieldState.attribute.name === attributeName) {
assert(formFieldState.valueOrValues instanceof Array);
return {
"attribute": {
attribute: {
...attribute,
"annotations": {
annotations: {
...attribute.annotations,
"inputType": undefined
inputType: undefined
},
"multivalued": false
multivalued: false
},
"valueOrValues": formFieldState.valueOrValues[index]
valueOrValues: formFieldState.valueOrValues[index]
};
}
@ -626,7 +635,12 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
return true;
})
.map((error): FormFieldError => ({ ...error, "fieldIndex": index }));
.map(
(error): FormFieldError => ({
...error,
fieldIndex: index
})
);
})
.reduce((acc, errors) => [...acc, ...errors], []);
@ -642,12 +656,12 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const msgArgs = ["error-user-attribute-required"] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "other",
"rule": "requiredField"
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
errorMessageStr: msgStr(...msgArgs),
fieldIndex: undefined,
source: {
type: "other",
rule: "requiredField"
}
});
}
@ -696,12 +710,12 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
return [
{
"errorMessage": <Fragment key={0}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "validator",
"name": validatorName
errorMessage: <Fragment key={0}>{msg(...msgArgs)}</Fragment>,
errorMessageStr: msgStr(...msgArgs),
fieldIndex: undefined,
source: {
type: "validator",
name: validatorName
}
}
];
@ -740,12 +754,12 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const msgArgs = ["invalidPasswordMinLengthMessage", `${minLength}`] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "passwordPolicy",
"name": policyName
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
errorMessageStr: msgStr(...msgArgs),
fieldIndex: undefined,
source: {
type: "passwordPolicy",
name: policyName
}
});
}
@ -768,12 +782,12 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const msgArgs = ["invalidPasswordMinDigitsMessage", `${minNumberOfDigits}`] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "passwordPolicy",
"name": policyName
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
errorMessageStr: msgStr(...msgArgs),
fieldIndex: undefined,
source: {
type: "passwordPolicy",
name: policyName
}
});
}
@ -798,12 +812,12 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const msgArgs = ["invalidPasswordMinLowerCaseCharsMessage", `${minNumberOfLowerCaseChar}`] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "passwordPolicy",
"name": policyName
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
errorMessageStr: msgStr(...msgArgs),
fieldIndex: undefined,
source: {
type: "passwordPolicy",
name: policyName
}
});
}
@ -828,12 +842,12 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const msgArgs = ["invalidPasswordMinUpperCaseCharsMessage", `${minNumberOfUpperCaseChar}`] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "passwordPolicy",
"name": policyName
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
errorMessageStr: msgStr(...msgArgs),
fieldIndex: undefined,
source: {
type: "passwordPolicy",
name: policyName
}
});
}
@ -856,12 +870,12 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const msgArgs = ["invalidPasswordMinSpecialCharsMessage", `${minNumberOfSpecialChar}`] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "passwordPolicy",
"name": policyName
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
errorMessageStr: msgStr(...msgArgs),
fieldIndex: undefined,
source: {
type: "passwordPolicy",
name: policyName
}
});
}
@ -906,12 +920,12 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const msgArgs = ["invalidPasswordNotUsernameMessage"] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "passwordPolicy",
"name": policyName
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
errorMessageStr: msgStr(...msgArgs),
fieldIndex: undefined,
source: {
type: "passwordPolicy",
name: policyName
}
});
}
@ -944,12 +958,12 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const msgArgs = ["invalidPasswordNotEmailMessage"] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "passwordPolicy",
"name": policyName
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
errorMessageStr: msgStr(...msgArgs),
fieldIndex: undefined,
source: {
type: "passwordPolicy",
name: policyName
}
});
}
@ -977,12 +991,12 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const msgArgs = ["invalidPasswordConfirmMessage"] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "other",
"rule": "passwordConfirmMatchesPassword"
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
errorMessageStr: msgStr(...msgArgs),
fieldIndex: undefined,
source: {
type: "other",
rule: "passwordConfirmMatchesPassword"
}
});
}
@ -1001,12 +1015,12 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const msgArgs = ["error-user-attribute-required"] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "other",
"rule": "requiredField"
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
errorMessageStr: msgStr(...msgArgs),
fieldIndex: undefined,
source: {
type: "other",
rule: "requiredField"
}
});
}
@ -1027,17 +1041,17 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
}
const source: FormFieldError.Source = {
"type": "validator",
"name": validatorName
type: "validator",
name: validatorName
};
if (max && value.length > parseInt(`${max}`)) {
const msgArgs = ["error-invalid-length-too-long", `${max}`] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
errorMessageStr: msgStr(...msgArgs),
fieldIndex: undefined,
source
});
}
@ -1046,9 +1060,9 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const msgArgs = ["error-invalid-length-too-short", `${min}`] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
errorMessageStr: msgStr(...msgArgs),
fieldIndex: undefined,
source
});
}
@ -1076,12 +1090,12 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const msgArgs = [errorMessageKey ?? id<MessageKey>("shouldMatchPattern"), pattern] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{advancedMsg(...msgArgs)}</Fragment>,
"errorMessageStr": advancedMsgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "validator",
"name": validatorName
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{advancedMsg(...msgArgs)}</Fragment>,
errorMessageStr: advancedMsgStr(...msgArgs),
fieldIndex: undefined,
source: {
type: "validator",
name: validatorName
}
});
}
@ -1115,12 +1129,12 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const msgArgs = [id<MessageKey>("invalidEmailMessage")] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "validator",
"name": validatorName
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
errorMessageStr: msgStr(...msgArgs),
fieldIndex: undefined,
source: {
type: "validator",
name: validatorName
}
});
}
@ -1143,17 +1157,17 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const intValue = parseInt(value);
const source: FormFieldError.Source = {
"type": "validator",
"name": validatorName
type: "validator",
name: validatorName
};
if (isNaN(intValue)) {
const msgArgs = ["mustBeAnInteger"] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
errorMessageStr: msgStr(...msgArgs),
fieldIndex: undefined,
source
});
@ -1164,9 +1178,9 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const msgArgs = ["error-number-out-of-range-too-big", `${max}`] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
errorMessageStr: msgStr(...msgArgs),
fieldIndex: undefined,
source
});
@ -1177,9 +1191,9 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const msgArgs = ["error-number-out-of-range-too-small", `${min}`] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
errorMessageStr: msgStr(...msgArgs),
fieldIndex: undefined,
source
});
@ -1207,12 +1221,12 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
const msgArgs = [id<MessageKey>("notAValidOption")] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{advancedMsg(...msgArgs)}</Fragment>,
"errorMessageStr": advancedMsgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "validator",
"name": validatorName
errorMessage: <Fragment key={`${attributeName}-${errors.length}`}>{advancedMsg(...msgArgs)}</Fragment>,
errorMessageStr: advancedMsgStr(...msgArgs),
fieldIndex: undefined,
source: {
type: "validator",
name: validatorName
}
});
}

View File

@ -19,12 +19,18 @@ export default function DeleteAccountConfirm(props: PageProps<Extract<KcContext,
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("deleteAccountConfirm")}>
<form action={url.loginAction} className="form-vertical" method="post">
<div className="alert alert-warning" style={{ "marginTop": "0", "marginBottom": "30px" }}>
<div className="alert alert-warning" style={{ marginTop: "0", marginBottom: "30px" }}>
<span className="pficon pficon-warning-triangle-o"></span>
{msg("irreversibleAction")}
</div>
<p>{msg("deletingImplies")}</p>
<ul style={{ "color": "#72767b", "listStyle": "disc", "listStylePosition": "inside" }}>
<ul
style={{
color: "#72767b",
listStyle: "disc",
listStylePosition: "inside"
}}
>
<li>{msg("loggingOutImmediately")}</li>
<li>{msg("errasingData")}</li>
</ul>
@ -38,7 +44,7 @@ export default function DeleteAccountConfirm(props: PageProps<Extract<KcContext,
{triggered_from_aia && (
<button
className={clsx(getClassName("kcButtonClass"), getClassName("kcButtonDefaultClass"), getClassName("kcButtonLargeClass"))}
style={{ "marginLeft": "calc(100% - 220px)" }}
style={{ marginLeft: "calc(100% - 220px)" }}
type="submit"
name="cancel-aia"
value="true"

View File

@ -27,7 +27,7 @@ export default function FrontchannelLogout(props: PageProps<Extract<KcContext, {
{logout.clients.map(client => (
<li key={client.name}>
{client.name}
<iframe src={client.frontChannelLogoutUrl} style={{ "display": "none" }} />
<iframe src={client.frontChannelLogoutUrl} style={{ display: "none" }} />
</li>
))}
</ul>

View File

@ -21,10 +21,10 @@ export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<Kc
const { msg, msgStr } = i18n;
const { insertScriptTags } = useInsertScriptTags({
"scriptTags": [
scriptTags: [
{
"type": "text/javascript",
"textContent": `
type: "text/javascript",
textContent: `
/* copy recovery codes */
function copyRecoveryCodes() {

View File

@ -32,18 +32,18 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
const { msg, msgStr } = i18n;
const { insertScriptTags } = useInsertScriptTags({
"scriptTags": [
scriptTags: [
{
"type": "text/javascript",
"src": `${url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js`
type: "text/javascript",
src: `${url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js`
},
{
"type": "text/javascript",
"src": `${url.resourcesPath}/js/base64url.js`
type: "text/javascript",
src: `${url.resourcesPath}/js/base64url.js`
},
{
"type": "text/javascript",
"textContent": `
type: "text/javascript",
textContent: `
function webAuthnAuthenticate() {
let isUserIdentified = ${isUserIdentified};
@ -206,7 +206,10 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
className={getClassName("kcSelectAuthListItemDescriptionClass")}
>
{authenticator.transports.displayNameProperties
.map((nameProperty, i, arr) => ({ nameProperty, "hasNext": i !== arr.length - 1 }))
.map((nameProperty, i, arr) => ({
nameProperty,
hasNext: i !== arr.length - 1
}))
.map(({ nameProperty, hasNext }) => (
<Fragment key={nameProperty}>
<span>{msg(nameProperty)}</span>

View File

@ -35,18 +35,18 @@ export default function WebauthnRegister(props: PageProps<Extract<KcContext, { p
const { msg, msgStr } = i18n;
const { insertScriptTags } = useInsertScriptTags({
"scriptTags": [
scriptTags: [
{
"type": "text/javascript",
"src": `${url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js`
type: "text/javascript",
src: `${url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js`
},
{
"type": "text/javascript",
"src": `${url.resourcesPath}/js/base64url.js`
type: "text/javascript",
src: `${url.resourcesPath}/js/base64url.js`
},
{
"type": "text/javascript",
"textContent": `
type: "text/javascript",
textContent: `
function registerSecurityKey() {
// Check if WebAuthn is supported by this browser

View File

@ -21,5 +21,9 @@ export declare namespace AndByDiscriminatingKey {
SingletonU1 extends Record<DiscriminatingKey, string>,
U2,
U1 extends Record<DiscriminatingKey, string>
> = U2 extends Pick<SingletonU1, DiscriminatingKey> ? U2 & SingletonU1 : U2 extends Pick<U1, DiscriminatingKey> ? never : U2;
> = U2 extends Pick<SingletonU1, DiscriminatingKey>
? U2 & SingletonU1
: U2 extends Pick<U1, DiscriminatingKey>
? never
: U2;
}

View File

@ -17,7 +17,10 @@ if (!Array.prototype.every) {
var len = O.length >>> 0;
// 4. If IsCallable(callbackfn) is false, throw a TypeError exception.
if (typeof callbackfn !== "function" && Object.prototype.toString.call(callbackfn) !== "[object Function]") {
if (
typeof callbackfn !== "function" &&
Object.prototype.toString.call(callbackfn) !== "[object Function]"
) {
throw new TypeError();
}

View File

@ -1,3 +1,5 @@
import type { LazyExoticComponent, ComponentType } from "react";
export type LazyOrNot<Component extends ComponentType<any>> = LazyExoticComponent<Component> | Component;
export type LazyOrNot<Component extends ComponentType<any>> =
| LazyExoticComponent<Component>
| Component;

View File

@ -1,7 +1,13 @@
import { assert } from "tsafe/assert";
import { typeGuard } from "tsafe/typeGuard";
export type CxArg = undefined | null | string | boolean | Partial<Record<string, boolean | null | undefined>> | readonly CxArg[];
export type CxArg =
| undefined
| null
| string
| boolean
| Partial<Record<string, boolean | null | undefined>>
| readonly CxArg[];
export const clsx = (...args: CxArg[]): string => {
const len = args.length;

View File

@ -3,7 +3,10 @@ import { is } from "tsafe/is";
import { deepClone } from "./deepClone";
//Warning: Be mindful that because of array this is not idempotent.
export function deepAssign(params: { target: Record<string, unknown>; source: Record<string, unknown> }) {
export function deepAssign(params: {
target: Record<string, unknown>;
source: Record<string, unknown>;
}) {
const { target } = params;
const source = deepClone(params.source);
@ -11,12 +14,16 @@ export function deepAssign(params: { target: Record<string, unknown>; source: Re
Object.keys(source).forEach(key => {
var dereferencedSource = source[key];
if (target[key] === undefined || dereferencedSource instanceof Function || !(dereferencedSource instanceof Object)) {
if (
target[key] === undefined ||
dereferencedSource instanceof Function ||
!(dereferencedSource instanceof Object)
) {
Object.defineProperty(target, key, {
"enumerable": true,
"writable": true,
"configurable": true,
"value": dereferencedSource
enumerable: true,
writable: true,
configurable: true,
value: dereferencedSource
});
return;
@ -37,8 +44,8 @@ export function deepAssign(params: { target: Record<string, unknown>; source: Re
assert(is<Record<string, unknown>>(dereferencedSource));
deepAssign({
"target": dereferencedTarget,
"source": dereferencedSource
target: dereferencedTarget,
source: dereferencedSource
});
});
}

View File

@ -13,5 +13,7 @@ export function deepClone<T>(o: T): T {
return o.map(deepClone) as any;
}
return Object.fromEntries(Object.entries(o).map(([key, value]) => [key, deepClone(value)])) as any;
return Object.fromEntries(
Object.entries(o).map(([key, value]) => [key, deepClone(value)])
) as any;
}

View File

@ -11,7 +11,10 @@ export const formatNumber = (input: string, format: string): string => {
}
// calculate the maximum size of the given pattern based on the sum of the expected digits
const maxSize = digitPattern.reduce((total, p) => total + parseInt(p.replace("{", "").replace("}", "")), 0);
const maxSize = digitPattern.reduce(
(total, p) => total + parseInt(p.replace("{", "").replace("}", "")),
0
);
// keep only digits
let rawValue = input.replace(/\D+/g, "");

View File

@ -2,7 +2,9 @@ import { useRef, useState } from "react";
import { Parameters } from "tsafe/Parameters";
/** https://stackoverflow.com/questions/65890278/why-cant-usecallback-always-return-the-same-ref */
export function useConstCallback<T extends ((...args: any[]) => unknown) | undefined | null>(callback: NonNullable<T>): T {
export function useConstCallback<
T extends ((...args: any[]) => unknown) | undefined | null
>(callback: NonNullable<T>): T {
const callbackRef = useRef<typeof callback>(null as any);
callbackRef.current = callback;

Some files were not shown because too many files have changed in this diff Show More