Compare commits

..

60 Commits

Author SHA1 Message Date
5156b2e0cc Bump version 2023-08-07 15:21:43 +02:00
6b81cf4a24 #391 2023-08-07 15:21:22 +02:00
cca3a68fe4 Merge branch 'main' of https://github.com/keycloakify/keycloakify 2023-08-07 15:10:48 +02:00
adb2904872 Bump version 2023-08-07 15:10:43 +02:00
d68b8d03dd #392 2023-08-07 15:10:25 +02:00
e7afb88f22 Update README.md 2023-08-02 12:55:08 +02:00
48cbfc64c0 Add warning about Keycloak 22 2023-08-02 09:15:10 +02:00
0b067858bc Bump version 2023-08-02 09:04:05 +02:00
2d44d98f17 Test with Keycloak 21.1.2 by default, use --features=declarative-user-profile for enabling User Profile features 2023-08-02 08:56:47 +02:00
74ef3096ae Can't support Account theme in Keycloak 22 2023-08-02 08:48:38 +02:00
8f1163fd75 Release candidate 2023-08-01 19:10:25 +02:00
a240d503c5 Hotfix for #388 2023-08-01 19:09:18 +02:00
e331a641b2 Bump version 2023-07-29 03:45:50 +02:00
85db4b8e0a Export the default PageId type 2023-07-29 03:45:33 +02:00
0aa139cf4a Bump version 2023-07-26 19:33:55 +02:00
4140ca6fbd #385 2023-07-26 19:33:38 +02:00
a8ce9da9ee Bump version 2023-07-24 23:22:51 +02:00
476a33c0ab #380 2023-07-24 23:22:16 +02:00
8e868c9fda Bump version 2023-07-24 22:39:16 +02:00
17c8b1a172 When no path PUBLIC_URL is empty string 2023-07-24 22:38:53 +02:00
b374c04d73 Bump version 2023-07-24 20:45:31 +02:00
e750d824ad #47 #384 2023-07-24 20:45:02 +02:00
dd4c50c3eb Bump version 2023-07-24 01:11:40 +02:00
20cc869299 #375 2023-07-24 01:11:34 +02:00
7214dbccdb Bump version 2023-07-07 17:03:03 +02:00
e6cebdd546 #377 2023-07-07 17:02:18 +02:00
0301003ccf Bump version 2023-06-27 17:52:18 +02:00
de2efe0c01 #369 2023-06-27 17:51:59 +02:00
90d765d7f6 #366: fix tests 2023-06-21 18:23:12 +02:00
3e0a1721ce Merge pull request #366 from Gravity-Software-srl/main
prevent crawlRec from crashing when dir_path does not exist
2023-06-21 18:17:11 +02:00
7214fbcd4c Bump version 2023-06-21 18:08:02 +02:00
4b8aecfe91 #364 2023-06-21 18:06:12 +02:00
387c71c0aa prevent crawlRec from crashing when dir_path does not exist 2023-06-21 16:24:58 +02:00
8d5ce21df4 Note about compatibility 2023-06-21 14:19:28 +02:00
f6dfcfbae9 Fix scripts build 2023-06-21 04:01:11 +02:00
69e9595db9 Bump version 2023-06-21 03:56:30 +02:00
de390678fd Deprecate the extraPages options, analyze the code to detect extra pages 2023-06-21 03:54:43 +02:00
cf9a7b8c60 Update json-schema 2023-06-21 02:56:55 +02:00
73e9c16a8d Fix some types approximations 2023-06-21 02:55:44 +02:00
9775623981 Bump version 2023-06-19 03:17:26 +02:00
20b7bb3c99 Fix error with inital select state 2023-06-19 03:17:12 +02:00
3defc16658 Bump version 2023-06-19 02:00:13 +02:00
0dbe592182 Match even if there's a cariage return after pritIfExists... 2023-06-19 02:00:02 +02:00
a7c0e5bdaa Bump version 2023-06-19 01:37:20 +02:00
3b051cbbea Fix build 2023-06-19 01:37:03 +02:00
f7edfd1c29 Add release note 2023-06-19 01:37:03 +02:00
b182c43965 Be more lax on the detection of field name. 2023-06-19 01:37:03 +02:00
4639e7ad2e Better exception message 2023-06-19 01:37:03 +02:00
56241203a0 Merge branch 'main' of https://github.com/keycloakify/keycloakify 2023-06-19 00:09:33 +02:00
8c8540de5d Analyze the code to see what field names are acutally used. Deprecates the customUserAttributes option, it's no longer needed 2023-06-19 00:09:21 +02:00
b45af78322 Bump version 2023-06-18 16:13:36 +02:00
98bcf3bf7e #362: otpCredentials is an array! 2023-06-18 16:13:18 +02:00
e28bcfced3 Bump version 2023-06-18 00:27:36 +02:00
a5bd990245 #362 2023-06-18 00:27:20 +02:00
58301e0844 Bump version 2023-06-17 00:52:35 +02:00
c9213fb6cd Bump version 2023-06-16 23:44:21 +02:00
641819a364 #359: fix: 'Local variable assigned outside a macro.' 2023-06-16 23:44:04 +02:00
3ee3a8b41d #359: Remove comment 2023-06-16 23:39:49 +02:00
5600403088 Bump version 2023-06-16 23:30:58 +02:00
3b00bace23 #359: Wrongely named FTL variable name fix 2023-06-16 23:30:36 +02:00
31 changed files with 925 additions and 465 deletions

View File

@ -45,6 +45,14 @@
> when using React; it's a well-regarded solution that many
> developers appreciate.
> 📣 🛑 Account themes generated by Keycloakify are currently not compatible with Keycloak 22.
> We are working on a solution. [More info](https://github.com/keycloakify/keycloakify/issues/389#issuecomment-1661591906).
> Note that login and email themes are not affected.
Keycloakify is fully compatible with Keycloak, starting from version 11 and is anticipated to maintain compatibility with all future versions.
You can update your Keycloak, your Keycloakify generated theme won't break.
To understand the basis of my confidence in this, you can [visit this discussion thread where I've explained in detail](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791).
## Sponsor 👼
We are exclusively sponsored by [Cloud IAM](https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github), a French company offering Keycloak as a service.
@ -117,6 +125,23 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
# Changelog highlights
## 7.15
- The i18n messages you defines in your theme are now also maid available to Keycloak.
In practice this mean that you can now customize the `kcContext.message.summary` that
display a general alert and the values returned by `kcContext.messagesPerField.get()` that
are used to display specific error on some field of the form.
[See video](https://youtu.be/D6tZcemReTI)
## 7.14
- Deprecate the `extraPages` build option. Keycloakify is now able to analyze your code to detect extra pages.
## 7.13
- Deprecate `customUserAttribute`, Keycloakify now analyze your code to predict field name usage. [See doc](https://docs.keycloakify.dev/build-options#customuserattributes).
It's now mandatory to [adopt the new directory structure](https://docs.keycloakify.dev/migration-guides/v6-greater-than-v7).
## 7.12
- You can now pack multiple themes variant in a single `.jar` bundle. In vanilla Keycloak themes you have the ability to extend a base theme.

View File

@ -30,18 +30,6 @@
"type": "string"
}
},
"extraLoginPages": {
"type": "array",
"items": {
"type": "string"
}
},
"extraAccountPages": {
"type": "array",
"items": {
"type": "string"
}
},
"extraThemeProperties": {
"type": "array",
"items": {
@ -70,12 +58,6 @@
"keycloakifyBuildDirPath": {
"type": "string"
},
"customUserAttributes": {
"type": "array",
"items": {
"type": "string"
}
},
"themeName": {
"type": "string"
}

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "7.12.6-rc.0",
"version": "7.15.8",
"description": "Create Keycloak themes using React",
"repository": {
"type": "git",
@ -10,8 +10,7 @@
"types": "dist/index.d.ts",
"scripts": {
"prepare": "yarn generate-i18n-messages",
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/",
"watch-in-starter": "yarn build && yarn link-in-starter && (concurrently \"tsc -p src -w\" \"tsc-alias -p src/tsconfig.json\" \"tsc -p src/bin -w\")",
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/ && cp -r src dist/",
"generate:json-schema": "ts-node scripts/generate-json-schema.ts",
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
"copy-files": "copyfiles -u 1 src/**/*.ftl",
@ -24,6 +23,7 @@
"generate-i18n-messages": "ts-node --skipProject scripts/generate-i18n-messages.ts",
"link-in-app": "ts-node --skipProject scripts/link-in-app.ts",
"link-in-starter": "yarn link-in-app keycloakify-starter",
"watch-in-starter": "yarn build && yarn link-in-starter && (concurrently \"tsc -p src -w\" \"tsc-alias -p src/tsconfig.json\" \"tsc -p src/bin -w\")",
"copy-keycloak-resources-to-storybook-static": "PUBLIC_DIR_PATH=.storybook/static node dist/bin/copy-keycloak-resources-to-public.js",
"storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && start-storybook -p 6006",
"build-storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && build-storybook"
@ -79,12 +79,14 @@
"@storybook/manager-webpack5": "^6.5.13",
"@storybook/react": "^6.5.13",
"@storybook/testing-library": "^0.0.13",
"@types/babel__generator": "^7.6.4",
"@types/make-fetch-happen": "^10.0.1",
"@types/minimist": "^1.2.2",
"@types/node": "^18.15.3",
"@types/react": "^18.0.35",
"@types/react-dom": "^18.0.11",
"@types/yauzl": "^2.10.0",
"@types/yazl": "^2.4.2",
"concurrently": "^8.0.1",
"copyfiles": "^2.4.1",
"eslint-plugin-storybook": "^0.6.7",
@ -106,8 +108,10 @@
"zod-to-json-schema": "^3.20.4"
},
"dependencies": {
"@babel/generator": "^7.22.9",
"@babel/parser": "^7.22.7",
"@babel/types": "^7.22.5",
"@octokit/rest": "^18.12.0",
"@types/yazl": "^2.4.2",
"cheerio": "^1.0.0-rc.5",
"cli-select": "^1.1.2",
"evt": "^2.4.18",
@ -116,6 +120,7 @@
"minimist": "^1.2.6",
"path-browserify": "^1.0.1",
"react-markdown": "^5.0.3",
"recast": "^0.23.3",
"rfc4648": "^1.5.2",
"tsafe": "^1.6.0",
"yauzl": "^2.10.0",

View File

@ -37,7 +37,10 @@ async function main() {
const baseThemeDirPath = pathJoin(tmpDirPath, "base");
const re = new RegExp(`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`);
crawl(baseThemeDirPath).forEach(filePath => {
crawl({
"dirPath": baseThemeDirPath,
"returnedPathsType": "relative to dirPath"
}).forEach(filePath => {
const match = filePath.match(re);
if (match === null) {

View File

@ -4,6 +4,7 @@ export default Fallback;
export { getKcContext } from "keycloakify/account/kcContext/getKcContext";
export { createGetKcContext } from "keycloakify/account/kcContext/createGetKcContext";
export type { AccountThemePageId as PageId } from "keycloakify/bin/keycloakify/generateFtl";
export { createUseI18n } from "keycloakify/account/i18n/i18n";
export type { PageProps } from "keycloakify/account/pages/PageProps";

View File

@ -4,7 +4,7 @@ import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { id } from "tsafe/id";
import type { KcContext } from "./KcContext";
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/";
export const kcContextCommonMock: KcContext.Common = {
"keycloakifyVersion": "0.0.0",

View File

@ -51,10 +51,6 @@ import { getThemeSrcDirPath } from "./getSrcDirPath";
const { themeSrcDirPath } = getThemeSrcDirPath({ "projectDirPath": process.cwd() });
if (themeSrcDirPath === undefined) {
throw new Error("Couldn't locate your theme sources");
}
const targetFilePath = pathJoin(themeSrcDirPath, themeType, "pages", pageBasename);
if (existsSync(targetFilePath)) {

View File

@ -2,15 +2,17 @@ import * as fs from "fs";
import { exclude } from "tsafe";
import { crawl } from "./tools/crawl";
import { join as pathJoin } from "path";
import { themeTypes } from "./keycloakify/generateFtl";
const themeSrcDirBasename = "keycloak-theme";
/** Can't catch error, if the directory isn't found, this function will just exit the process with an error message. */
export function getThemeSrcDirPath(params: { projectDirPath: string }) {
const { projectDirPath } = params;
const srcDirPath = pathJoin(projectDirPath, "src");
const themeSrcDirPath: string | undefined = crawl(srcDirPath)
const themeSrcDirPath: string | undefined = crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })
.map(fileRelativePath => {
const split = fileRelativePath.split(themeSrcDirBasename);
@ -22,22 +24,24 @@ export function getThemeSrcDirPath(params: { projectDirPath: string }) {
})
.filter(exclude(undefined))[0];
if (themeSrcDirPath === undefined) {
if (fs.existsSync(pathJoin(srcDirPath, "login")) || fs.existsSync(pathJoin(srcDirPath, "account"))) {
return { "themeSrcDirPath": srcDirPath };
}
return { "themeSrcDirPath": undefined };
if (themeSrcDirPath !== undefined) {
return { themeSrcDirPath };
}
return { themeSrcDirPath };
}
export function getEmailThemeSrcDirPath(params: { projectDirPath: string }) {
const { projectDirPath } = params;
const { themeSrcDirPath } = getThemeSrcDirPath({ projectDirPath });
const emailThemeSrcDirPath = themeSrcDirPath === undefined ? undefined : pathJoin(themeSrcDirPath, "email");
return { emailThemeSrcDirPath };
for (const themeType of [...themeTypes, "email"]) {
if (!fs.existsSync(pathJoin(srcDirPath, themeType))) {
continue;
}
return { "themeSrcDirPath": srcDirPath };
}
console.error(
[
"Can't locate your theme source directory. It should be either: ",
"src/ or src/keycloak-theme.",
"Example in the starter: https://github.com/keycloakify/keycloakify-starter/tree/main/src/keycloak-theme"
].join("\n")
);
process.exit(-1);
}

View File

@ -7,7 +7,7 @@ import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { readBuildOptions } from "./keycloakify/BuildOptions";
import * as fs from "fs";
import { getLogger } from "./tools/logger";
import { getEmailThemeSrcDirPath } from "./getSrcDirPath";
import { getThemeSrcDirPath } from "./getSrcDirPath";
export async function main() {
const { isSilent } = readBuildOptions({
@ -17,15 +17,11 @@ export async function main() {
const logger = getLogger({ isSilent });
const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath({
const { themeSrcDirPath } = getThemeSrcDirPath({
"projectDirPath": process.cwd()
});
if (emailThemeSrcDirPath === undefined) {
logger.warn("Couldn't locate your theme source directory");
process.exit(-1);
}
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
if (fs.existsSync(emailThemeSrcDirPath)) {
logger.warn(`There is already a ${pathRelative(process.cwd(), emailThemeSrcDirPath)} directory in your project. Aborting.`);

View File

@ -17,9 +17,7 @@ export namespace BuildOptions {
themeVersion: string;
themeName: string;
extraThemeNames: string[];
extraLoginPages: string[] | undefined;
extraAccountPages: string[] | undefined;
extraThemeProperties?: string[];
extraThemeProperties: string[] | undefined;
groupId: string;
artifactId: string;
bundler: Bundler;
@ -28,7 +26,6 @@ export namespace BuildOptions {
reactAppBuildDirPath: string;
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
keycloakifyBuildDirPath: string;
customUserAttributes: string[];
};
export type Standalone = Common & {
@ -109,17 +106,7 @@ export function readBuildOptions(params: { projectDirPath: string; processArgv:
const common: BuildOptions.Common = (() => {
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
const {
extraPages,
extraLoginPages,
extraAccountPages,
extraThemeProperties,
groupId,
artifactId,
bundler,
keycloakVersionDefaultAssets,
extraThemeNames = []
} = keycloakify ?? {};
const { extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets, extraThemeNames = [] } = keycloakify ?? {};
const themeName =
keycloakify.themeName ??
@ -161,8 +148,6 @@ export function readBuildOptions(params: { projectDirPath: string; processArgv:
);
})(),
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0",
"extraLoginPages": [...(extraPages ?? []), ...(extraLoginPages ?? [])],
extraAccountPages,
extraThemeProperties,
"isSilent": isSilentCliParamProvided,
"keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3",
@ -199,8 +184,7 @@ export function readBuildOptions(params: { projectDirPath: string; processArgv:
}
return keycloakifyBuildDirPath;
})(),
"customUserAttributes": keycloakify.customUserAttributes ?? []
})()
};
})();

View File

@ -8,13 +8,7 @@
out["advancedMsg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
out["messagesPerField"]= {
<#assign fieldNames = [
"global", "userLabel", "username", "email", "firstName", "lastName", "password", "password-confirm",
"totp", "totpSecret", "SAMLRequest", "SAMLResponse", "relayState", "device_user_code", "code",
"password-new", "rememberMe", "login", "authenticationExecution", "cancel-aia", "clientDataJSON",
"authenticatorData", "signature", "credentialId", "userHandle", "error", "authn_use_chk", "authenticationExecution",
"isSetRetry", "try-again", "attestationObject", "publicKeyCredentialId", "authenticatorLabel"CUSTOM_USER_ATTRIBUTES_eKsIY4ZsZ4xeM
]>
<#assign fieldNames = [ FIELD_NAMES_eKsIY4ZsZ4xeM ]>
<#attempt>
<#if profile?? && profile.attributes?? && profile.attributes?is_enumerable>
@ -32,72 +26,42 @@
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.printIfExists in this page");
</#if>
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#assign doExistMessageForUsernameOrPassword = "">
<#local doExistMessageForUsernameOrPassword = "">
<#attempt>
<#local doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#local doExistMessageForUsernameOrPassword = true>
</#attempt>
<#if !doExists>
<#attempt>
<#local doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#local doExistMessageForUsernameOrPassword = true>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
return <#if doExistMessageForUsernameOrPassword>text<#else>undefined</#if>;
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
<#else>
return <#if doExistMessageForUsernameOrPassword>text<#else>undefined</#if>;
<#local doExistMessageForField = "">
<#attempt>
<#local doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#local doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>text<#else>undefined</#if>;
</#if>
<#else>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#local doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#local doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#local doExistErrorOnUsernameOrPassword = true>
</#attempt>
<#if doExistErrorOnUsernameOrPassword>
return text;
<#else>
<#local doExistMessageForField = "">
<#assign doExistMessageForField = "">
<#attempt>
<#local doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#local doExistMessageForField = true>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>text<#else>undefined</#if>;
@ -106,107 +70,139 @@
<#else>
<#local doExistMessageForField = "">
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#attempt>
<#local doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#local doExistMessageForField = true>
</#attempt>
<#assign doExistErrorOnUsernameOrPassword = "">
return <#if doExistMessageForField>text<#else>undefined</#if>;
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
<#if doExistErrorOnUsernameOrPassword>
return text;
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>text<#else>undefined</#if>;
</#if>
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>text<#else>undefined</#if>;
</#if>
</#if>
</#if>
}
</#list>
}
</#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if>
throw new Error("There is no " + fieldName + " field. See: https://docs.keycloakify.dev/build-options#keycloakify.customuserattributes");
},
"existsError": function (fieldName) {
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.printIfExists in this page");
</#if>
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#assign doExistMessageForUsernameOrPassword = "">
<#local doExistMessageForUsernameOrPassword = "">
<#attempt>
<#local doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#local doExistMessageForUsernameOrPassword = true>
</#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#local doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#local doExistMessageForUsernameOrPassword = true>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
return <#if doExistMessageForUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>true<#else>false</#if>;
</#if>
return <#if doExistMessageForUsernameOrPassword>true<#else>false</#if>;
<#else>
<#local doExistMessageForField = "">
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#attempt>
<#local doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#local doExistMessageForField = true>
</#attempt>
<#assign doExistErrorOnUsernameOrPassword = "">
return <#if doExistMessageForField>true<#else>false</#if>;
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistErrorMessageForField = "">
<#attempt>
<#assign doExistErrorMessageForField = messagesPerField.existsError('${fieldName}')>
<#recover>
<#assign doExistErrorMessageForField = true>
</#attempt>
return <#if doExistErrorMessageForField>true<#else>false</#if>;
</#if>
</#if>
<#else>
}
</#list>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
<#local doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#local doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#local doExistErrorOnUsernameOrPassword = true>
</#attempt>
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
<#else>
<#local doExistErrorMessageForField = "">
<#attempt>
<#local doExistErrorMessageForField = messagesPerField.existsError('${fieldName}')>
<#recover>
<#local doExistErrorMessageForField = true>
</#attempt>
return <#if doExistErrorMessageForField>true<#else>false</#if>;
</#if>
</#if>
}
</#list>
throw new Error("There is no " + fieldName + " field. See: https://docs.keycloakify.dev/build-options#keycloakify.customuserattributes");
</#if>
},
"get": function (fieldName) {
@ -214,186 +210,185 @@
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.get in this page");
</#if>
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#local doExistMessageForUsernameOrPassword = "">
<#attempt>
<#local doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#local doExistMessageForUsernameOrPassword = true>
</#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#local doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#recover>
<#local doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
<#if !doExistMessageForUsernameOrPassword>
return "";
<#else>
<#attempt>
return "${kcSanitize(msg('invalidUserMessage'))?no_esc}";
<#recover>
return "Invalid username or password.";
</#attempt>
</#if>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover>
return "invalid field";
</#attempt>
</#if>
<#else>
// CONITNUE HERE!!!!!
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#local doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#local doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#local doExistErrorOnUsernameOrPassword = true>
</#attempt>
<#if doExistErrorOnUsernameOrPassword>
<#assign doExistMessageForUsernameOrPassword = "">
<#attempt>
return "${kcSanitize(msg('invalidUserMessage'))?no_esc}";
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
return "Invalid username or password.";
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
<#if !doExistMessageForUsernameOrPassword>
return "";
<#else>
<#attempt>
return "${kcSanitize(msg('invalidUserMessage'))?no_esc}";
<#recover>
return "Invalid username or password.";
</#attempt>
</#if>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover>
return "";
return "invalid field";
</#attempt>
</#if>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover>
return "invalid field";
</#attempt>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
<#if doExistErrorOnUsernameOrPassword>
<#attempt>
return "${kcSanitize(msg('invalidUserMessage'))?no_esc}";
<#recover>
return "Invalid username or password.";
</#attempt>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover>
return "";
</#attempt>
</#if>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover>
return "invalid field";
</#attempt>
</#if>
</#if>
</#if>
}
</#list>
}
</#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
throw new Error("There is no " + fieldName + " field. See: https://docs.keycloakify.dev/build-options#keycloakify.customuserattributes");
</#if>
},
"exists": function (fieldName) {
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.exists in this page");
</#if>
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#assign doExistMessageForUsernameOrPassword = "">
<#local doExistMessageForUsernameOrPassword = "">
<#attempt>
<#local doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#local doExistMessageForUsernameOrPassword = true>
</#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#local doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#local doExistMessageForUsernameOrPassword = true>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
return <#if doExistMessageForUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>true<#else>false</#if>;
</#if>
return <#if doExistMessageForUsernameOrPassword>true<#else>false</#if>;
<#else>
<#local doExistMessageForField = "">
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#attempt>
<#local doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#local doExistMessageForField = true>
</#attempt>
<#assign doExistErrorOnUsernameOrPassword = "">
return <#if doExistMessageForField>true<#else>false</#if>;
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistErrorMessageForField = "">
<#attempt>
<#assign doExistErrorMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistErrorMessageForField = true>
</#attempt>
return <#if doExistErrorMessageForField>true<#else>false</#if>;
</#if>
</#if>
<#else>
}
</#list>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#local doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#local doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#local doExistErrorOnUsernameOrPassword = true>
</#attempt>
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
<#else>
<#local doExistErrorMessageForField = "">
<#attempt>
<#local doExistErrorMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#local doExistErrorMessageForField = true>
</#attempt>
return <#if doExistErrorMessageForField>true<#else>false</#if>;
</#if>
</#if>
}
</#list>
throw new Error("There is no " + fieldName + " field. See: https://docs.keycloakify.dev/build-options#keycloakify.customuserattributes");
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if>
}
};
@ -462,8 +457,12 @@
<#-- https://github.com/keycloakify/keycloakify/issues/357 -->
key == "loginAction" &&
are_same_path(path, ["url"]) &&
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl"]?seq_contains(pageId) &&
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl"]?seq_contains(pageId) &&
!(auth?has_content && auth.showTryAnotherWayLink())
) || (
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
["secretData", "value"]?seq_contains(key) &&
are_same_path(path, [ "totp", "otpCredentials", "*" ])
) || (
["contextData", "idpConfig", "idp", "authenticationSession"]?seq_contains(key) &&
are_same_path(path, ["brokerContext"]) &&

View File

@ -18,7 +18,6 @@ export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.Ex
export namespace BuildOptionsLike {
export type Common = {
themeName: string;
customUserAttributes: string[];
themeVersion: string;
};
@ -57,8 +56,9 @@ export function generateFtlFilesCodeFactory(params: {
buildOptions: BuildOptionsLike;
keycloakifyVersion: string;
themeType: ThemeType;
fieldNames: string[];
}) {
const { cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion, themeType } = params;
const { cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion, themeType, fieldNames } = params;
const $ = cheerio.load(indexHtmlCode);
@ -129,10 +129,7 @@ export function generateFtlFilesCodeFactory(params: {
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
.toString("utf8")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1]
.replace(
"CUSTOM_USER_ATTRIBUTES_eKsIY4ZsZ4xeM",
buildOptions.customUserAttributes.length === 0 ? "" : ", " + buildOptions.customUserAttributes.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)

View File

@ -1,15 +1,15 @@
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
import { themeTypes } from "./generateFtl/generateFtl";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./BuildOptions";
import type { ThemeType } from "./generateFtl";
export type BuildOptionsLike = {
themeName: string;
extraThemeNames: string[];
groupId: string;
artifactId?: string;
artifactId: string;
themeVersion: string;
};
@ -21,7 +21,7 @@ export type BuildOptionsLike = {
export function generateJavaStackFiles(params: {
keycloakThemeBuildingDirPath: string;
doBundlesEmailTemplate: boolean;
implementedThemeTypes: Record<ThemeType | "email", boolean>;
buildOptions: BuildOptionsLike;
}): {
jarFilePath: string;
@ -29,7 +29,7 @@ export function generateJavaStackFiles(params: {
const {
buildOptions: { groupId, themeName, extraThemeNames, themeVersion, artifactId },
keycloakThemeBuildingDirPath,
doBundlesEmailTemplate
implementedThemeTypes
} = params;
{
@ -70,7 +70,9 @@ export function generateJavaStackFiles(params: {
{
"themes": [themeName, ...extraThemeNames].map(themeName => ({
"name": themeName,
"types": [...themeTypes, ...(doBundlesEmailTemplate ? ["email"] : [])]
"types": Object.entries(implementedThemeTypes)
.filter(([, isImplemented]) => isImplemented)
.map(([themeType]) => themeType)
}))
},
null,

View File

@ -33,7 +33,6 @@ export function generateStartKeycloakTestingContainer(params: {
fs.writeFileSync(
pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename),
Buffer.from(
[
"#!/usr/bin/env bash",
@ -47,7 +46,6 @@ export function generateStartKeycloakTestingContainer(params: {
` --name ${containerName} \\`,
" -e KEYCLOAK_ADMIN=admin \\",
" -e KEYCLOAK_ADMIN_PASSWORD=admin \\",
" -e JAVA_OPTS=-Dkeycloak.profile=preview \\",
...[themeName, ...extraThemeNames].map(
themeName =>
` -v "${pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName).replace(
@ -56,7 +54,7 @@ export function generateStartKeycloakTestingContainer(params: {
)}":"/opt/keycloak/themes/${themeName}":rw \\`
),
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
` start-dev`,
` start-dev --features=declarative-user-profile`,
""
].join("\n"),
"utf8"

View File

@ -0,0 +1,179 @@
import type { ThemeType } from "../generateFtl";
import { crawl } from "../../tools/crawl";
import { join as pathJoin } from "path";
import { readFileSync } from "fs";
import { symToStr } from "tsafe/symToStr";
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
import * as recast from "recast";
import * as babelParser from "@babel/parser";
import babelGenerate from "@babel/generator";
import * as babelTypes from "@babel/types";
export function generateMessageProperties(params: {
themeSrcDirPath: string;
themeType: ThemeType;
}): { languageTag: string; propertiesFileSource: string }[] {
const { themeSrcDirPath, themeType } = params;
let files = crawl({
"dirPath": pathJoin(themeSrcDirPath, themeType),
"returnedPathsType": "absolute"
});
files = files.filter(file => {
const regex = /\.(js|ts|tsx)$/;
return regex.test(file);
});
files = files.sort((a, b) => {
const regex = /\.i18n\.(ts|js|tsx)$/;
const aIsI18nFile = regex.test(a);
const bIsI18nFile = regex.test(b);
return aIsI18nFile === bIsI18nFile ? 0 : aIsI18nFile ? -1 : 1;
});
files = files.sort((a, b) => a.length - b.length);
files = files.filter(file => readFileSync(file).toString("utf8").includes("createUseI18n"));
if (files.length === 0) {
return [];
}
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
}
});
const codes: string[] = [];
recast.visit(root, {
"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);
}
});
return codes;
})
.flat()
.map(code => {
let extraMessages: { [languageTag: string]: Record<string, string> } = {};
try {
eval(`${symToStr({ extraMessages })} = ${code}`);
} catch {
console.warn(
[
"WARNING: Make sure that the first argument of createUseI18n can be evaluated in a javascript",
"runtime where only the node globals are available.",
"This is important because we need to put your i18n messages in messages_*.properties files",
"or they won't be available server side.",
"\n",
"The following code could not be evaluated:",
"\n",
code
].join(" ")
);
}
return extraMessages;
});
const languageTags = extraMessages
.map(extraMessage => Object.keys(extraMessage))
.flat()
.reduce(...removeDuplicates<string>());
const keyValueMapByLanguageTag: Record<string, Record<string, string>> = {};
for (const languageTag of languageTags) {
const keyValueMap: Record<string, string> = {};
for (const extraMessage of extraMessages) {
const keyValueMap_i = extraMessage[languageTag];
if (keyValueMap_i === undefined) {
continue;
}
for (const [key, value] of Object.entries(keyValueMap_i)) {
if (keyValueMap[key] !== undefined) {
console.warn(
[
"WARNING: The following key is defined multiple times:",
"\n",
key,
"\n",
"The following value will be ignored:",
"\n",
value,
"\n",
"The following value was already defined:",
"\n",
keyValueMap[key]
].join(" ")
);
continue;
}
keyValueMap[key] = value;
}
}
keyValueMapByLanguageTag[languageTag] = keyValueMap;
}
const out: { languageTag: string; propertiesFileSource: string }[] = [];
for (const [languageTag, keyValueMap] of Object.entries(keyValueMapByLanguageTag)) {
const propertiesFileSource = Object.entries(keyValueMap)
.map(([key, value]) => `${key}=${escapeString(value)}`)
.join("\n");
out.push({
languageTag,
"propertiesFileSource": ["# This file was generated by keycloakify", "", "parent=base", "", propertiesFileSource, ""].join("\n")
});
}
return out;
}
// Convert a JavaScript string to UTF-16 encoding
function toUTF16(codePoint: number): string {
if (codePoint <= 0xffff) {
// BMP character
return "\\u" + codePoint.toString(16).padStart(4, "0");
} else {
// Non-BMP character
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");
}
}
// Escapes special characters and converts unicode to UTF-16 encoding
function escapeString(str: string): string {
let escapedStr = "";
for (const char of [...str]) {
const codePoint = char.codePointAt(0);
if (!codePoint) continue;
if (char === "'") {
escapedStr += "''"; // double single quotes
} else if (codePoint > 0x7f) {
escapedStr += toUTF16(codePoint); // non-ascii characters
} else {
escapedStr += char;
}
}
return escapedStr;
}

View File

@ -9,17 +9,17 @@ import { isInside } from "../../tools/isInside";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResources";
import { readFieldNameUsage } from "./readFieldNameUsage";
import { readExtraPagesNames } from "./readExtraPageNames";
import { generateMessageProperties } from "./generateMessageProperties";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Common = {
themeName: string;
extraLoginPages?: string[];
extraAccountPages?: string[];
extraThemeProperties?: string[];
extraThemeProperties: string[] | undefined;
isSilent: boolean;
customUserAttributes: string[];
themeVersion: string;
keycloakVersionDefaultAssets: string;
};
@ -53,11 +53,12 @@ assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function generateTheme(params: {
reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string;
emailThemeSrcDirPath: string | undefined;
themeSrcDirPath: string;
keycloakifySrcDirPath: string;
buildOptions: BuildOptionsLike;
keycloakifyVersion: string;
}): Promise<{ doBundlesEmailTemplate: boolean }> {
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, emailThemeSrcDirPath, buildOptions, keycloakifyVersion } = params;
}): Promise<void> {
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, themeSrcDirPath, keycloakifySrcDirPath, buildOptions, keycloakifyVersion } = params;
const getThemeDirPath = (themeType: ThemeType | "email") =>
pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType);
@ -67,6 +68,10 @@ export async function generateTheme(params: {
let generateFtlFilesCode_glob: ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"] | undefined = undefined;
for (const themeType of themeTypes) {
if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) {
continue;
}
const themeDirPath = getThemeDirPath(themeType);
copy_app_resources_to_theme_path: {
@ -132,21 +137,21 @@ export async function generateTheme(params: {
});
}
const generateFtlFilesCode = (() => {
if (generateFtlFilesCode_glob !== undefined) {
return generateFtlFilesCode_glob;
}
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
"cssGlobalsToDefine": allCssGlobalsToDefine,
buildOptions,
keycloakifyVersion,
themeType
});
return generateFtlFilesCode;
})();
const generateFtlFilesCode =
generateFtlFilesCode_glob !== undefined
? generateFtlFilesCode_glob
: generateFtlFilesCodeFactory({
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
"cssGlobalsToDefine": allCssGlobalsToDefine,
buildOptions,
keycloakifyVersion,
themeType,
"fieldNames": readFieldNameUsage({
keycloakifySrcDirPath,
themeSrcDirPath,
themeType
})
}).generateFtlFilesCode;
[
...(() => {
@ -157,14 +162,10 @@ export async function generateTheme(params: {
return accountThemePageIds;
}
})(),
...((() => {
switch (themeType) {
case "login":
return buildOptions.extraLoginPages;
case "account":
return buildOptions.extraAccountPages;
}
})() ?? [])
...readExtraPagesNames({
themeType,
themeSrcDirPath
})
].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId });
@ -173,6 +174,19 @@ export async function generateTheme(params: {
fs.writeFileSync(pathJoin(themeDirPath, pageId), Buffer.from(ftlCode, "utf8"));
});
generateMessageProperties({
themeSrcDirPath,
themeType
}).forEach(({ languageTag, propertiesFileSource }) => {
const messagesDirPath = pathJoin(themeDirPath, "messages");
fs.mkdirSync(pathJoin(themeDirPath, "messages"), { "recursive": true });
const propertiesFilePath = pathJoin(messagesDirPath, `messages_${languageTag}.properties`);
fs.writeFileSync(propertiesFilePath, Buffer.from(propertiesFileSource, "utf8"));
});
//TODO: Remove this block we left it for now only for backward compatibility
// we now have a separate script for this
copy_keycloak_resources_to_public: {
@ -216,25 +230,20 @@ export async function generateTheme(params: {
fs.writeFileSync(
pathJoin(themeDirPath, "theme.properties"),
Buffer.from(["parent=keycloak", ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8")
Buffer.from([`parent=keycloak`, ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8")
);
}
let doBundlesEmailTemplate: boolean;
email: {
if (emailThemeSrcDirPath === undefined) {
doBundlesEmailTemplate = false;
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
if (!fs.existsSync(emailThemeSrcDirPath)) {
break email;
}
doBundlesEmailTemplate = true;
transformCodebase({
"srcDirPath": emailThemeSrcDirPath,
"destDirPath": getThemeDirPath("email")
});
}
return { doBundlesEmailTemplate };
}

View File

@ -0,0 +1,38 @@
import { crawl } from "../../tools/crawl";
import { type ThemeType, accountThemePageIds, loginThemePageIds } from "../generateFtl";
import { id } from "tsafe/id";
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
import * as fs from "fs";
import { join as pathJoin } from "path";
export function readExtraPagesNames(params: { themeSrcDirPath: string; themeType: ThemeType }): string[] {
const { themeSrcDirPath, themeType } = params;
const filePaths = crawl({
"dirPath": pathJoin(themeSrcDirPath, themeType),
"returnedPathsType": "absolute"
}).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
const candidateFilePaths = filePaths.filter(filePath => /kcContext\.[^.]+$/.test(filePath));
if (candidateFilePaths.length === 0) {
candidateFilePaths.push(...filePaths);
}
const extraPages: string[] = [];
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]));
}
return extraPages.reduce(...removeDuplicates<string>()).filter(pageId => {
switch (themeType) {
case "account":
return !id<readonly string[]>(accountThemePageIds).includes(pageId);
case "login":
return !id<readonly string[]>(loginThemePageIds).includes(pageId);
}
});
}

View File

@ -0,0 +1,35 @@
import { crawl } from "../../tools/crawl";
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
import { join as pathJoin } from "path";
import * as fs from "fs";
import type { ThemeType } from "../generateFtl";
import { exclude } from "tsafe/exclude";
/** Assumes the theme type exists */
export function readFieldNameUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): string[] {
const { keycloakifySrcDirPath, themeSrcDirPath, themeType } = params;
const fieldNames: string[] = [];
for (const srcDirPath of ([pathJoin(keycloakifySrcDirPath, themeType), pathJoin(themeSrcDirPath, themeType)] as const).filter(
exclude(undefined)
)) {
const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
for (const filePath of filePaths) {
const rawSourceFile = fs.readFileSync(filePath).toString("utf8");
if (!rawSourceFile.includes("messagesPerField")) {
continue;
}
fieldNames.push(
...Array.from(rawSourceFile.matchAll(/(?:(?:printIfExists)|(?:existsError)|(?:get)|(?:exists))\(\s*["']([^"']+)["']/g), m => m[1])
);
}
}
const out = fieldNames.reduce(...removeDuplicates<string>());
return out;
}

View File

@ -9,8 +9,9 @@ import { getLogger } from "../tools/logger";
import jar from "../tools/jar";
import { assert } from "tsafe/assert";
import { Equals } from "tsafe";
import { getEmailThemeSrcDirPath } from "../getSrcDirPath";
import { getThemeSrcDirPath } from "../getSrcDirPath";
import { getProjectRoot } from "../tools/getProjectRoot";
import { objectKeys } from "tsafe/objectKeys";
export async function main() {
const projectDirPath = process.cwd();
@ -23,42 +24,48 @@ export async function main() {
const logger = getLogger({ "isSilent": buildOptions.isSilent });
logger.log("🔏 Building the keycloak theme...⌚");
let doBundlesEmailTemplate: boolean | undefined;
const keycloakifyDirPath = getProjectRoot();
const { themeSrcDirPath } = getThemeSrcDirPath({ projectDirPath });
for (const themeName of [buildOptions.themeName, ...buildOptions.extraThemeNames]) {
const { doBundlesEmailTemplate: doBundlesEmailTemplate_ } = await generateTheme({
keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath,
"emailThemeSrcDirPath": (() => {
const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath({ projectDirPath });
if (emailThemeSrcDirPath === undefined || !fs.existsSync(emailThemeSrcDirPath)) {
return;
}
return emailThemeSrcDirPath;
})(),
await generateTheme({
"keycloakThemeBuildingDirPath": buildOptions.keycloakifyBuildDirPath,
themeSrcDirPath,
"keycloakifySrcDirPath": pathJoin(keycloakifyDirPath, "src"),
"reactAppBuildDirPath": buildOptions.reactAppBuildDirPath,
"buildOptions": {
...buildOptions,
"themeName": themeName
},
"keycloakifyVersion": (() => {
const version = JSON.parse(fs.readFileSync(pathJoin(getProjectRoot(), "package.json")).toString("utf8"))["version"];
const version = JSON.parse(fs.readFileSync(pathJoin(keycloakifyDirPath, "package.json")).toString("utf8"))["version"];
assert(typeof version === "string");
return version;
})()
});
doBundlesEmailTemplate ??= doBundlesEmailTemplate_;
}
assert(doBundlesEmailTemplate !== undefined);
const { jarFilePath } = generateJavaStackFiles({
keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath,
doBundlesEmailTemplate,
"keycloakThemeBuildingDirPath": buildOptions.keycloakifyBuildDirPath,
"implementedThemeTypes": (() => {
const implementedThemeTypes = {
"login": false,
"account": false,
"email": false
};
for (const themeType of objectKeys(implementedThemeTypes)) {
if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) {
continue;
}
implementedThemeTypes[themeType] = true;
}
return implementedThemeTypes;
})(),
buildOptions
});
@ -85,7 +92,7 @@ export async function main() {
}
// We want, however, to test in a container running the latest Keycloak version
const containerKeycloakVersion = "20.0.1";
const containerKeycloakVersion = "21.1.2";
generateStartKeycloakTestingContainer({
keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath,

View File

@ -11,10 +11,6 @@ export type ParsedPackageJson = {
version?: string;
homepage?: string;
keycloakify?: {
/** @deprecated: use extraLoginPages instead */
extraPages?: string[];
extraLoginPages?: string[];
extraAccountPages?: string[];
extraThemeProperties?: string[];
areAppAndKeycloakServerSharingSameDomain?: boolean;
artifactId?: string;
@ -23,7 +19,6 @@ export type ParsedPackageJson = {
keycloakVersionDefaultAssets?: string;
reactAppBuildDirPath?: string;
keycloakifyBuildDirPath?: string;
customUserAttributes?: string[];
themeName?: string;
extraThemeNames?: string[];
};
@ -35,9 +30,6 @@ export const zParsedPackageJson = z.object({
"homepage": z.string().optional(),
"keycloakify": z
.object({
"extraPages": z.array(z.string()).optional(),
"extraLoginPages": z.array(z.string()).optional(),
"extraAccountPages": z.array(z.string()).optional(),
"extraThemeProperties": z.array(z.string()).optional(),
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(),
"artifactId": z.string().optional(),
@ -46,7 +38,6 @@ export const zParsedPackageJson = z.object({
"keycloakVersionDefaultAssets": z.string().optional(),
"reactAppBuildDirPath": z.string().optional(),
"keycloakifyBuildDirPath": z.string().optional(),
"customUserAttributes": z.array(z.string()).optional(),
"themeName": z.string().optional(),
"extraThemeNames": z.array(z.string()).optional()
})

View File

@ -1,27 +1,32 @@
import * as fs from "fs";
import * as path from "path";
/** List all files in a given directory return paths relative to the dir_path */
export const crawl = (() => {
const crawlRec = (dir_path: string, paths: string[]) => {
for (const file_name of fs.readdirSync(dir_path)) {
const file_path = path.join(dir_path, file_name);
const crawlRec = (dir_path: string, paths: string[]) => {
for (const file_name of fs.readdirSync(dir_path)) {
const file_path = path.join(dir_path, file_name);
if (fs.lstatSync(file_path).isDirectory()) {
crawlRec(file_path, paths);
if (fs.lstatSync(file_path).isDirectory()) {
crawlRec(file_path, paths);
continue;
}
paths.push(file_path);
continue;
}
};
return function crawl(dir_path: string): string[] {
const paths: string[] = [];
paths.push(file_path);
}
};
crawlRec(dir_path, paths);
/** 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[] {
const { dirPath, returnedPathsType } = params;
return paths.map(file_path => path.relative(dir_path, file_path));
};
})();
const filePaths: string[] = [];
crawlRec(dirPath, filePaths);
switch (returnedPathsType) {
case "absolute":
return filePaths;
case "relative to dirPath":
return filePaths.map(filePath => path.relative(dirPath, filePath));
}
}

View File

@ -2,8 +2,8 @@ import { exec as execCallback } from "child_process";
import { createHash } from "crypto";
import { mkdir, readFile, stat, writeFile } from "fs/promises";
import fetch, { type FetchOptions } from "make-fetch-happen";
import { dirname as pathDirname, join as pathJoin } from "path";
import { assert } from "tsafe";
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
import { assert } from "tsafe/assert";
import { promisify } from "util";
import { getProjectRoot } from "./getProjectRoot";
import { transformCodebase } from "./transformCodebase";
@ -48,9 +48,28 @@ async function getNmpConfig() {
return readNpmConfig().then(parseNpmConfig);
}
async function readNpmConfig() {
const { stdout } = await exec("npm config get", { encoding: "utf8" });
return stdout;
function readNpmConfig(): Promise<string> {
return (async function callee(depth: number): Promise<string> {
const cwd = pathResolve(pathJoin(...[process.cwd(), ...Array(depth).fill("..")]));
let stdout: string;
try {
stdout = await exec("npm config get", { "encoding": "utf8", cwd }).then(({ stdout }) => stdout);
} catch (error) {
console.log(String(error), error);
if (String(error).includes("ENOWORKSPACES")) {
assert(cwd !== pathSep);
return callee(depth + 1);
}
throw error;
}
return stdout;
})(0);
}
function parseNpmConfig(stdout: string) {

View File

@ -20,12 +20,12 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
}))
} = params;
for (const file_relative_path of crawl(srcDirPath)) {
for (const file_relative_path of crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })) {
const filePath = path.join(srcDirPath, file_relative_path);
const transformSourceCodeResult = transformSourceCode({
"sourceCode": fs.readFileSync(filePath),
"filePath": path.join(srcDirPath, file_relative_path)
filePath
});
if (transformSourceCodeResult === undefined) {

View File

@ -211,7 +211,8 @@ const keycloakifyExtraMessages = {
"shouldBeDifferent": "{0} should be different to {1}",
"shouldMatchPattern": "Pattern should match: `/{0}/`",
"mustBeAnInteger": "Must be an integer",
"notAValidOption": "Not a valid option"
"notAValidOption": "Not a valid option",
"selectAnOption": "Select an option"
},
"fr": {
/* spell-checker: disable */
@ -223,7 +224,8 @@ const keycloakifyExtraMessages = {
"logoutConfirmTitle": "Déconnexion",
"logoutConfirmHeader": "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
"doLogout": "Se déconnecter"
"doLogout": "Se déconnecter",
"selectAnOption": "Sélectionner une option"
/* spell-checker: enable */
}
};

View File

@ -5,6 +5,7 @@ export default Fallback;
export { useDownloadTerms } from "keycloakify/login/lib/useDownloadTerms";
export { getKcContext } from "keycloakify/login/kcContext/getKcContext";
export { createGetKcContext } from "keycloakify/login/kcContext/createGetKcContext";
export type { LoginThemePageId as PageId } from "keycloakify/bin/keycloakify/generateFtl";
export { createUseI18n } from "keycloakify/login/i18n/i18n";
export type { PageProps } from "keycloakify/login/pages/PageProps";

View File

@ -6,7 +6,7 @@ import { id } from "tsafe/id";
import { assert, type Equals } from "tsafe/assert";
import type { LoginThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/";
const attributes: Attribute[] = [
{

View File

@ -25,16 +25,7 @@ export function useFormValidation(params: {
passwordValidators?: Validators;
i18n: I18n;
}) {
const {
kcContext,
passwordValidators = {
"length": {
"ignore.empty.value": true,
"min": "4"
}
},
i18n
} = params;
const { kcContext, passwordValidators = {}, i18n } = params;
const attributesWithPassword = useMemo(
() =>
@ -211,7 +202,7 @@ function useGetErrors(params: {
const { value: defaultValue, validators } = attributes.find(attribute => attribute.name === name)!;
block: {
if (defaultValue !== value) {
if ((defaultValue ?? "") !== value) {
break block;
}

View File

@ -123,7 +123,7 @@ export default function LoginUpdatePassword(props: PageProps<Extract<KcContext,
getClassName("kcButtonLargeClass")
)}
type="submit"
defaultValue={msgStr("doSubmit")}
value={msgStr("doSubmit")}
/>
)}
</div>

View File

@ -17,7 +17,7 @@ export type UserProfileFormFieldsProps = {
export function UserProfileFormFields(props: UserProfileFormFieldsProps) {
const { kcContext, onIsFormSubmittableValueChange, i18n, getClassName, BeforeField, AfterField } = props;
const { advancedMsg } = i18n;
const { advancedMsg, msg } = i18n;
const {
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
@ -98,11 +98,16 @@ export function UserProfileFormFields(props: UserProfileFormFieldsProps) {
}
value={value}
>
{options.options.map(option => (
<option key={option} value={option}>
{option}
<>
<option value="" selected disabled hidden>
{msg("selectAnOption")}
</option>
))}
{options.options.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</>
</select>
);
}

View File

@ -0,0 +1,68 @@
import path from "path";
import { it, describe, expect, vi, beforeAll, afterAll } from "vitest";
import { crawl } from "keycloakify/bin/tools/crawl";
describe("crawl", () => {
describe("crawRec", () => {
beforeAll(() => {
vi.mock("node:fs", async () => {
const mod = await vi.importActual<typeof import("fs")>("fs");
return {
...mod,
readdirSync: vi.fn().mockImplementation((dir_path: string) => {
switch (dir_path) {
case "root_dir":
return ["sub_1_dir", "file_1", "sub_2_dir", "file_2"];
case path.join("root_dir", "sub_1_dir"):
return ["file_3", "sub_3_dir", "file_4"];
case path.join("root_dir", "sub_1_dir", "sub_3_dir"):
return ["file_5"];
case path.join("root_dir", "sub_2_dir"):
return [];
default: {
const enoent = new Error(`ENOENT: no such file or directory, scandir '${dir_path}'`);
// @ts-ignore
enoent.code = "ENOENT";
// @ts-ignore
enoent.syscall = "open";
// @ts-ignore
enoent.path = dir_path;
throw enoent;
}
}
}),
lstatSync: vi.fn().mockImplementation((file_path: string) => {
return { isDirectory: () => file_path.endsWith("_dir") };
})
};
});
});
afterAll(() => {
vi.resetAllMocks();
});
it("returns files under a given dir_path", async () => {
const paths = crawl({ "dirPath": "root_dir/sub_1_dir/sub_3_dir", "returnedPathsType": "absolute" });
expect(paths).toEqual(["root_dir/sub_1_dir/sub_3_dir/file_5"]);
});
it("returns files recursively under a given dir_path", async () => {
const paths = crawl({ "dirPath": "root_dir", "returnedPathsType": "absolute" });
expect(paths).toEqual([
"root_dir/sub_1_dir/file_3",
"root_dir/sub_1_dir/sub_3_dir/file_5",
"root_dir/sub_1_dir/file_4",
"root_dir/file_1",
"root_dir/file_2"
]);
});
it("throw dir_path does not exist", async () => {
try {
crawl({ "dirPath": "404", "returnedPathsType": "absolute" });
} catch {
expect(true);
return;
}
expect(false);
});
});
});

124
yarn.lock
View File

@ -75,6 +75,16 @@
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
"@babel/generator@^7.22.9":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.9.tgz#572ecfa7a31002fa1de2a9d91621fd895da8493d"
integrity sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==
dependencies:
"@babel/types" "^7.22.5"
"@jridgewell/gen-mapping" "^0.3.2"
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
"@babel/helper-annotate-as-pure@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb"
@ -269,11 +279,21 @@
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63"
integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==
"@babel/helper-string-parser@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f"
integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==
"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1":
version "7.19.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
"@babel/helper-validator-identifier@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193"
integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==
"@babel/helper-validator-option@^7.18.6", "@babel/helper-validator-option@^7.21.0":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180"
@ -312,6 +332,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17"
integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==
"@babel/parser@^7.22.7":
version "7.22.7"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.7.tgz#df8cf085ce92ddbdbf668a7f186ce848c9036cae"
integrity sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2"
@ -1120,6 +1145,15 @@
debug "^4.1.0"
globals "^11.1.0"
"@babel/types@^7.0.0", "@babel/types@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.5.tgz#cd93eeaab025880a3a47ec881f4b096a5b786fbe"
integrity sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==
dependencies:
"@babel/helper-string-parser" "^7.22.5"
"@babel/helper-validator-identifier" "^7.22.5"
to-fast-properties "^2.0.0"
"@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.2.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.4", "@babel/types@^7.4.4":
version "7.21.4"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.4.tgz#2d5d6bb7908699b3b416409ffd3b5daa25b030d4"
@ -2763,6 +2797,13 @@
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc"
integrity sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==
"@types/babel__generator@^7.6.4":
version "7.6.4"
resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7"
integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==
dependencies:
"@babel/types" "^7.0.0"
"@types/chai-subset@^1.3.3":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94"
@ -3816,6 +3857,16 @@ assert@^1.1.1:
object-assign "^4.1.1"
util "0.10.3"
assert@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/assert/-/assert-2.0.0.tgz#95fc1c616d48713510680f2eaf2d10dd22e02d32"
integrity sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==
dependencies:
es6-object-assign "^1.1.0"
is-nan "^1.2.1"
object-is "^1.0.1"
util "^0.12.0"
assertion-error@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
@ -3833,6 +3884,13 @@ ast-types@^0.14.2:
dependencies:
tslib "^2.0.1"
ast-types@^0.16.1:
version "0.16.1"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.16.1.tgz#7a9da1617c9081bc121faafe91711b4c8bb81da2"
integrity sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==
dependencies:
tslib "^2.0.1"
astral-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
@ -5739,6 +5797,11 @@ es6-iterator@^2.0.3:
es5-ext "^0.10.35"
es6-symbol "^3.1.1"
es6-object-assign@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c"
integrity sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==
es6-shim@^0.35.5:
version "0.35.8"
resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.8.tgz#89216f6fbf8bacba3f897c8c0e814d2a41c05fb7"
@ -5853,7 +5916,7 @@ eslint-visitor-keys@^3.3.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz#c7f0f956124ce677047ddbc192a68f999454dedc"
integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==
esprima@^4.0.0, esprima@^4.0.1:
esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
@ -7177,7 +7240,7 @@ is-alphanumerical@^1.0.0:
is-alphabetical "^1.0.0"
is-decimal "^1.0.0"
is-arguments@^1.1.1:
is-arguments@^1.0.4, is-arguments@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==
@ -7351,6 +7414,13 @@ is-function@^1.0.2:
resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.2.tgz#4f097f30abf6efadac9833b17ca5dc03f8144e08"
integrity sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==
is-generator-function@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==
dependencies:
has-tostringtag "^1.0.0"
is-glob@^3.0.0, is-glob@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
@ -7380,6 +7450,14 @@ is-map@^2.0.1, is-map@^2.0.2:
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==
is-nan@^1.2.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==
dependencies:
call-bind "^1.0.0"
define-properties "^1.1.3"
is-negative-zero@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
@ -7496,6 +7574,13 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.9:
gopd "^1.0.1"
has-tostringtag "^1.0.0"
is-typed-array@^1.1.3:
version "1.1.12"
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a"
integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==
dependencies:
which-typed-array "^1.1.11"
is-typedarray@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@ -8769,7 +8854,7 @@ object-inspect@^1.12.3, object-inspect@^1.9.0:
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
object-is@^1.1.5:
object-is@^1.0.1, object-is@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==
@ -9916,6 +10001,17 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
recast@^0.23.3:
version "0.23.3"
resolved "https://registry.yarnpkg.com/recast/-/recast-0.23.3.tgz#f205d1f46b2c6f730de413ab18f96c166263d85f"
integrity sha512-HbCVFh2ANP6a09nzD4lx7XthsxMOJWKX5pIcUwtLrmeEIl3I0DwjCoVXDE0Aobk+7k/mS3H50FK4iuYArpcT6Q==
dependencies:
assert "^2.0.0"
ast-types "^0.16.1"
esprima "~4.0.0"
source-map "~0.6.1"
tslib "^2.0.1"
redent@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
@ -11750,6 +11846,17 @@ util@^0.11.0:
dependencies:
inherits "2.0.3"
util@^0.12.0:
version "0.12.5"
resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc"
integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==
dependencies:
inherits "^2.0.3"
is-arguments "^1.0.4"
is-generator-function "^1.0.7"
is-typed-array "^1.1.3"
which-typed-array "^1.1.2"
utila@~0.4:
version "0.4.0"
resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
@ -12085,6 +12192,17 @@ which-pm-runs@^1.0.0:
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.1.0.tgz#35ccf7b1a0fce87bd8b92a478c9d045785d3bf35"
integrity sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==
which-typed-array@^1.1.11, which-typed-array@^1.1.2:
version "1.1.11"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a"
integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==
dependencies:
available-typed-arrays "^1.0.5"
call-bind "^1.0.2"
for-each "^0.3.3"
gopd "^1.0.1"
has-tostringtag "^1.0.0"
which-typed-array@^1.1.9:
version "1.1.9"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6"