Compare commits

...

40 Commits

Author SHA1 Message Date
60bd6621c8 Release candidate 2024-06-24 02:43:03 +02:00
b5f6262763 Support cd in running build script in webpack 2024-06-24 02:42:44 +02:00
2b8c4422de Release candidate 2024-06-23 22:48:01 +02:00
a686432c65 Shell: true for windows 2024-06-23 22:47:45 +02:00
449e625877 Fix storybook build 2024-06-23 22:39:29 +02:00
1ac07dafde Release candidate 2024-06-23 21:23:44 +02:00
3878e28b56 Improve monorepo project support, work if there only a package.json at the root (like NX) 2024-06-23 21:23:06 +02:00
cf6bc8666b Include fsevents.node in npm bundle 2024-06-23 21:10:11 +02:00
f76063eb40 Make it easier to link to another starter 2024-06-23 20:54:08 +02:00
ed52c5824d Give immediate feedback if projectDirPath is wrong 2024-06-23 16:56:24 +02:00
9333400322 Remove unused buildContext prop 2024-06-23 02:07:34 +02:00
3689cfcc0d Consistency 2024-06-23 02:06:45 +02:00
b73eceb535 Release candidate 2024-06-23 00:46:01 +02:00
5dc3453fc9 Enable user profile in default keycloak 23 configuration 2024-06-23 00:45:26 +02:00
cef1139a4b Release candidate 2024-06-23 00:37:26 +02:00
ac96959947 Add missing fieldNames from synthetic user attributes 2024-06-23 00:37:06 +02:00
4d73d877ba move used defined exclusions down 2024-06-23 00:18:03 +02:00
9f1186302e Release candidate 2024-06-22 20:12:22 +02:00
319dcc0d15 Stable i18n messages across Keycloak versions 2024-06-22 20:12:02 +02:00
e99fdb8561 Log what file have changed when linking dynamically in starter 2024-06-22 20:11:34 +02:00
f37a342a63 Release candidate 2024-06-22 17:18:52 +02:00
09a039894d Remove React as peer dpendency so that Keycloakify can be more easily used in Vue and Angular projects 2024-06-22 17:18:08 +02:00
3efbb1a9fd Release candidate 2024-06-22 17:05:37 +02:00
920ee62ee3 Implement fallback to english for messages bundle provided via Keycloakify 2024-06-22 17:05:14 +02:00
1ace44fe31 Rename extraMessages -> messageBundle 2024-06-22 17:03:59 +02:00
a60f05415b Export fallback language tag ("en") as a constant 2024-06-22 17:03:44 +02:00
42c9d39e02 Release candidate 2024-06-22 17:01:48 +02:00
a8186f1ed9 Don't use tsafe directly in ejectable components 2024-06-22 17:01:45 +02:00
c2ff515a17 Enable termsText to be extended via local message bundle 2024-06-22 14:09:11 +02:00
960c3ba558 Release candidate 2024-06-22 02:53:51 +02:00
454a9cd01c Remove useDownloadTerms see: https://docs.keycloakify.dev/terms-and-conditions, remove react-markdown 2024-06-22 02:53:30 +02:00
7d42ce1c87 Release candidate 2024-06-21 22:07:50 +02:00
57f6f980cf Update terms storybook 2024-06-21 22:07:36 +02:00
8cba3aae2c Release candidate 2024-06-21 21:25:41 +02:00
01b32f78ed Allow to override termsText 2024-06-21 21:24:04 +02:00
b6066dfd5f Release candidate 2024-06-21 20:28:32 +02:00
3ad554ed59 #569 2024-06-21 20:28:14 +02:00
6aacc6361b Release candidate 2024-06-21 02:13:48 +02:00
638e4e6410 Set the terms to empty string when building 2024-06-21 02:13:31 +02:00
aa9b7cccc7 Rework Terms 2024-06-21 02:01:55 +02:00
48 changed files with 789 additions and 851 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "keycloakify", "name": "keycloakify",
"version": "10.0.0-rc.77", "version": "10.0.0-rc.93",
"description": "Create Keycloak themes using React", "description": "Create Keycloak themes using React",
"repository": { "repository": {
"type": "git", "type": "git",
@ -41,14 +41,14 @@
"!dist/bin/", "!dist/bin/",
"dist/bin/main.js", "dist/bin/main.js",
"dist/bin/*.index.js", "dist/bin/*.index.js",
"!dist/bin/shared/*.js", "dist/bin/*.node",
"dist/bin/shared/constants.js", "dist/bin/shared/constants.js",
"dist/bin/shared/*.d.ts", "dist/bin/shared/*.d.ts",
"dist/bin/shared/*.js.map", "dist/bin/shared/*.js.map",
"!dist/vite-plugin/", "!dist/vite-plugin/",
"dist/vite-plugin/index.js",
"dist/vite-plugin/index.d.ts", "dist/vite-plugin/index.d.ts",
"dist/vite-plugin/vite-plugin.d.ts", "dist/vite-plugin/vite-plugin.d.ts"
"dist/vite-plugin/index.js"
], ],
"keywords": [ "keywords": [
"keycloak", "keycloak",
@ -62,11 +62,7 @@
"bluehats" "bluehats"
], ],
"homepage": "https://www.keycloakify.dev", "homepage": "https://www.keycloakify.dev",
"peerDependencies": {
"react": "*"
},
"dependencies": { "dependencies": {
"react-markdown": "^5.0.3",
"tsafe": "^1.6.6" "tsafe": "^1.6.6"
}, },
"devDependencies": { "devDependencies": {

View File

@ -16,7 +16,7 @@ if (fs.existsSync(join("dist", "bin", "main.original.js"))) {
); );
fs.readdirSync(join("dist", "bin")).forEach(fileBasename => { fs.readdirSync(join("dist", "bin")).forEach(fileBasename => {
if (/[0-9]\.index.js/.test(fileBasename)) { if (/[0-9]\.index.js/.test(fileBasename) || fileBasename.endsWith(".node")) {
fs.rmSync(join("dist", "bin", fileBasename)); fs.rmSync(join("dist", "bin", fileBasename));
} }
}); });
@ -111,9 +111,10 @@ run(
)}` )}`
); );
fs.readdirSync(join("dist", "ncc_out")).forEach(fileBasename => fs.readdirSync(join("dist", "ncc_out")).forEach(fileBasename => {
assert(!fileBasename.endsWith(".index.js")) assert(!fileBasename.endsWith(".index.js"));
); assert(!fileBasename.endsWith(".node"));
});
transformCodebase({ transformCodebase({
srcDirPath: join("dist", "ncc_out"), srcDirPath: join("dist", "ncc_out"),

View File

@ -65,11 +65,14 @@ async function main() {
fs fs
.readFileSync(pathJoin(baseThemeDirPath, filePath)) .readFileSync(pathJoin(baseThemeDirPath, filePath))
.toString("utf8") .toString("utf8")
) ) as Record<string, string>
).map(([key, value]: any) => [ )
key === "locale_pt_BR" ? "locale_pt-BR" : key, .map(([key, value]) => [key, value.replace(/''/g, "'")])
value.replace(/''/g, "'") .map(([key, value]) => [
]) key === "locale_pt_BR" ? "locale_pt-BR" : key,
value
])
.map(([key, value]) => [key, key === "termsText" ? "" : value])
); );
}); });
} }

View File

@ -10,14 +10,16 @@ fs.rmSync(".yarn_home", { recursive: true, force: true });
run("yarn install"); run("yarn install");
run("yarn build"); run("yarn build");
fs.rmSync(join("..", "keycloakify-starter", "node_modules"), { const starterName = "keycloakify-starter-webpack";
fs.rmSync(join("..", starterName, "node_modules"), {
recursive: true, recursive: true,
force: true force: true
}); });
run("yarn install", { cwd: join("..", "keycloakify-starter") }); run("yarn install", { cwd: join("..", starterName) });
run(`npx tsx ${join("scripts", "link-in-app.ts")} keycloakify-starter`); run(`npx tsx ${join("scripts", "link-in-app.ts")} ${starterName}`);
startRebuildOnSrcChange(); startRebuildOnSrcChange();

View File

@ -28,9 +28,13 @@ export function startRebuildOnSrcChange() {
console.log(chalk.green("Watching for changes in src/")); console.log(chalk.green("Watching for changes in src/"));
chokidar.watch(["src", "stories"], { ignoreInitial: true }).on("all", async () => { chokidar
await waitForDebounce(); .watch(["src", "stories"], { ignoreInitial: true })
.on("all", async (event, path) => {
console.log(chalk.bold(`${event}: ${path}`));
runYarnBuild(); await waitForDebounce();
});
runYarnBuild();
});
} }

View File

@ -13,7 +13,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes }); const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
const { msg, msgStr, getChangeLocalUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n; const { msg, msgStr, getChangeLocaleUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
const { locale, url, features, realm, message, referrer } = kcContext; const { locale, url, features, realm, message, referrer } = kcContext;
@ -79,7 +79,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
<ul> <ul>
{locale.supported.map(({ languageTag }) => ( {locale.supported.map(({ languageTag }) => (
<li key={languageTag} className="kc-dropdown-item"> <li key={languageTag} className="kc-dropdown-item">
<a href={getChangeLocalUrl(languageTag)}>{labelBySupportedLanguageTag[languageTag]}</a> <a href={getChangeLocaleUrl(languageTag)}>{labelBySupportedLanguageTag[languageTag]}</a>
</li> </li>
))} ))}
</ul> </ul>

View File

@ -1,12 +1,9 @@
import "keycloakify/tools/Object.fromEntries"; import "keycloakify/tools/Object.fromEntries";
import { useEffect, useState } from "react";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import messages_fallbackLanguage from "./baseMessages/en"; import messages_fallbackLanguage from "./baseMessages/en";
import { getMessages } from "./baseMessages"; import { getMessages } from "./baseMessages";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { Reflect } from "tsafe/Reflect"; import { fallbackLanguageTag } from "keycloakify/bin/shared/constants";
export const fallbackLanguageTag = "en";
export type KcContextLike = { export type KcContextLike = {
locale?: { locale?: {
@ -30,7 +27,7 @@ export type GenericI18n<MessageKey extends string> = {
* Redirect to this url to change the language. * Redirect to this url to change the language.
* After reload currentLanguageTag === newLanguageTag * After reload currentLanguageTag === newLanguageTag
*/ */
getChangeLocalUrl: (newLanguageTag: string) => string; getChangeLocaleUrl: (newLanguageTag: string) => string;
/** /**
* e.g. "en" => "English", "fr" => "Français", ... * e.g. "en" => "English", "fr" => "Français", ...
* *
@ -88,7 +85,9 @@ export type GenericI18n<MessageKey extends string> = {
isFetchingTranslations: boolean; isFetchingTranslations: boolean;
}; };
function createGetI18n<ExtraMessageKey extends string = never>(extraMessages: { [languageTag: string]: { [key in ExtraMessageKey]: string } }) { export function createGetI18n<ExtraMessageKey extends string = never>(messageBundle: {
[languageTag: string]: { [key in ExtraMessageKey]: string };
}) {
type I18n = GenericI18n<MessageKey | ExtraMessageKey>; type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
type Result = { i18n: I18n; prI18n_currentLanguage: Promise<I18n> | undefined }; type Result = { i18n: I18n; prI18n_currentLanguage: Promise<I18n> | undefined };
@ -108,9 +107,9 @@ function createGetI18n<ExtraMessageKey extends string = never>(extraMessages: {
return cachedResult; return cachedResult;
} }
const partialI18n: Pick<I18n, "currentLanguageTag" | "getChangeLocalUrl" | "labelBySupportedLanguageTag"> = { const partialI18n: Pick<I18n, "currentLanguageTag" | "getChangeLocaleUrl" | "labelBySupportedLanguageTag"> = {
currentLanguageTag: kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag, currentLanguageTag: kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag,
getChangeLocalUrl: newLanguageTag => { getChangeLocaleUrl: newLanguageTag => {
const { locale } = kcContext; const { locale } = kcContext;
assert(locale !== undefined, "Internationalization not enabled"); assert(locale !== undefined, "Internationalization not enabled");
@ -126,8 +125,8 @@ function createGetI18n<ExtraMessageKey extends string = never>(extraMessages: {
const { createI18nTranslationFunctions } = createI18nTranslationFunctionsFactory<MessageKey, ExtraMessageKey>({ const { createI18nTranslationFunctions } = createI18nTranslationFunctionsFactory<MessageKey, ExtraMessageKey>({
messages_fallbackLanguage, messages_fallbackLanguage,
extraMessages_fallbackLanguage: extraMessages[fallbackLanguageTag], messageBundle_fallbackLanguage: messageBundle[fallbackLanguageTag],
extraMessages: extraMessages[partialI18n.currentLanguageTag] messageBundle_currentLanguage: messageBundle[partialI18n.currentLanguageTag]
}); });
const isCurrentLanguageFallbackLanguage = partialI18n.currentLanguageTag === fallbackLanguageTag; const isCurrentLanguageFallbackLanguage = partialI18n.currentLanguageTag === fallbackLanguageTag;
@ -135,17 +134,19 @@ function createGetI18n<ExtraMessageKey extends string = never>(extraMessages: {
const result: Result = { const result: Result = {
i18n: { i18n: {
...partialI18n, ...partialI18n,
...createI18nTranslationFunctions({ messages: undefined }), ...createI18nTranslationFunctions({
messages_currentLanguage: isCurrentLanguageFallbackLanguage ? messages_fallbackLanguage : undefined
}),
isFetchingTranslations: !isCurrentLanguageFallbackLanguage isFetchingTranslations: !isCurrentLanguageFallbackLanguage
}, },
prI18n_currentLanguage: isCurrentLanguageFallbackLanguage prI18n_currentLanguage: isCurrentLanguageFallbackLanguage
? undefined ? undefined
: (async () => { : (async () => {
const messages = await getMessages(partialI18n.currentLanguageTag); const messages_currentLanguage = await getMessages(partialI18n.currentLanguageTag);
const i18n_currentLanguage: I18n = { const i18n_currentLanguage: I18n = {
...partialI18n, ...partialI18n,
...createI18nTranslationFunctions({ messages }), ...createI18nTranslationFunctions({ messages_currentLanguage }),
isFetchingTranslations: false isFetchingTranslations: false
}; };
@ -168,66 +169,30 @@ function createGetI18n<ExtraMessageKey extends string = never>(extraMessages: {
return { getI18n }; return { getI18n };
} }
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
[languageTag: string]: { [key in ExtraMessageKey]: string };
}) {
type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
const { getI18n } = createGetI18n(extraMessages);
function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } {
const { kcContext } = params;
const { i18n, prI18n_currentLanguage } = getI18n({ kcContext });
const [i18n_toReturn, setI18n_toReturn] = useState<I18n>(i18n);
useEffect(() => {
let isActive = true;
prI18n_currentLanguage?.then(i18n => {
if (!isActive) {
return;
}
setI18n_toReturn(i18n);
});
return () => {
isActive = false;
};
}, []);
return { i18n: i18n_toReturn };
}
return { useI18n, ofTypeI18n: Reflect<I18n>() };
}
function createI18nTranslationFunctionsFactory<MessageKey extends string, ExtraMessageKey extends string>(params: { function createI18nTranslationFunctionsFactory<MessageKey extends string, ExtraMessageKey extends string>(params: {
messages_fallbackLanguage: Record<MessageKey, string>; messages_fallbackLanguage: Record<MessageKey, string>;
extraMessages_fallbackLanguage: Record<ExtraMessageKey, string> | undefined; messageBundle_fallbackLanguage: Record<ExtraMessageKey, string> | undefined;
extraMessages: Partial<Record<ExtraMessageKey, string>> | undefined; messageBundle_currentLanguage: Partial<Record<ExtraMessageKey, string>> | undefined;
}) { }) {
const { extraMessages } = params; const { messageBundle_currentLanguage } = params;
const messages_fallbackLanguage = { const messages_fallbackLanguage = {
...params.messages_fallbackLanguage, ...params.messages_fallbackLanguage,
...params.extraMessages_fallbackLanguage ...params.messageBundle_fallbackLanguage
}; };
function createI18nTranslationFunctions(params: { function createI18nTranslationFunctions(params: {
messages: Partial<Record<MessageKey, string>> | undefined; messages_currentLanguage: Partial<Record<MessageKey, string>> | undefined;
}): Pick<GenericI18n<MessageKey | ExtraMessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> { }): Pick<GenericI18n<MessageKey | ExtraMessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
const messages = { const messages_currentLanguage = {
...params.messages, ...params.messages_currentLanguage,
...extraMessages ...messageBundle_currentLanguage
}; };
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): string | JSX.Element | undefined { function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): string | JSX.Element | undefined {
const { key, args, doRenderAsHtml } = props; const { key, args, doRenderAsHtml } = props;
const messageOrUndefined: string | undefined = (messages as any)[key] ?? (messages_fallbackLanguage as any)[key]; const messageOrUndefined: string | undefined = (messages_currentLanguage as any)[key] ?? (messages_fallbackLanguage as any)[key];
if (messageOrUndefined === undefined) { if (messageOrUndefined === undefined) {
return undefined; return undefined;

View File

@ -1,5 +1,4 @@
import type { GenericI18n, MessageKey, KcContextLike } from "./i18n"; import type { GenericI18n, MessageKey, KcContextLike } from "./i18n";
export type { MessageKey, KcContextLike }; export type { MessageKey, KcContextLike };
export type I18n = GenericI18n<MessageKey>; export type I18n = GenericI18n<MessageKey>;
export { createUseI18n } from "./i18n"; export { createUseI18n } from "./useI18n";
export { fallbackLanguageTag } from "./i18n";

View File

@ -0,0 +1,44 @@
import { useEffect, useState } from "react";
import {
createGetI18n,
type GenericI18n,
type MessageKey,
type KcContextLike
} from "./i18n";
import { Reflect } from "tsafe/Reflect";
export function createUseI18n<ExtraMessageKey extends string = never>(messageBundle: {
[languageTag: string]: { [key in ExtraMessageKey]: string };
}) {
type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
const { getI18n } = createGetI18n(messageBundle);
function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } {
const { kcContext } = params;
const { i18n, prI18n_currentLanguage } = getI18n({ kcContext });
const [i18n_toReturn, setI18n_toReturn] = useState<I18n>(i18n);
useEffect(() => {
let isActive = true;
prI18n_currentLanguage?.then(i18n => {
if (!isActive) {
return;
}
setI18n_toReturn(i18n);
});
return () => {
isActive = false;
};
}, []);
return { i18n: i18n_toReturn };
}
return { useI18n, ofTypeI18n: Reflect<I18n>() };
}

View File

@ -1,6 +1,12 @@
import cheerio from "cheerio"; import cheerio from "cheerio";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode"; import {
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode"; replaceImportsInJsCode,
BuildContextLike as BuildContextLike_replaceImportsInJsCode
} from "../replacers/replaceImportsInJsCode";
import {
replaceImportsInCssCode,
BuildContextLike as BuildContextLike_replaceImportsInCssCode
} from "../replacers/replaceImportsInCssCode";
import * as fs from "fs"; import * as fs from "fs";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import type { BuildContext } from "../../shared/buildContext"; import type { BuildContext } from "../../shared/buildContext";
@ -8,19 +14,16 @@ import { assert } from "tsafe/assert";
import { import {
type ThemeType, type ThemeType,
basenameOfTheKeycloakifyResourcesDir, basenameOfTheKeycloakifyResourcesDir,
resources_common, resources_common
nameOfTheLocalizationRealmOverridesUserProfileProperty
} from "../../shared/constants"; } from "../../shared/constants";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath"; import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
export type BuildContextLike = { export type BuildContextLike = BuildContextLike_replaceImportsInJsCode &
bundler: "vite" | "webpack"; BuildContextLike_replaceImportsInCssCode & {
themeVersion: string; urlPathname: string | undefined;
urlPathname: string | undefined; themeVersion: string;
projectBuildDirPath: string; kcContextExclusionsFtlCode: string | undefined;
assetsDirPath: string; };
kcContextExclusionsFtlCode: string | undefined;
};
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -119,10 +122,6 @@ export function generateFtlFilesCodeFactory(params: {
.replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType) .replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType)
.replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName) .replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName)
.replace("RESOURCES_COMMON_cLsLsMrtDkpVv", resources_common) .replace("RESOURCES_COMMON_cLsLsMrtDkpVv", resources_common)
.replace(
"lOCALIZATION_REALM_OVERRIDES_USER_PROFILE_PROPERTY_KEY_aaGLsPgGIdeeX",
nameOfTheLocalizationRealmOverridesUserProfileProperty
)
.replace( .replace(
"USER_DEFINED_EXCLUSIONS_eKsaY4ZsZ4eMr2", "USER_DEFINED_EXCLUSIONS_eKsaY4ZsZ4eMr2",
buildContext.kcContextExclusionsFtlCode ?? "" buildContext.kcContextExclusionsFtlCode ?? ""

View File

@ -33,8 +33,9 @@ kcContext.pageId = "${pageId}";
if( kcContext.url && kcContext.url.resourcesPath ){ if( kcContext.url && kcContext.url.resourcesPath ){
kcContext.url.resourcesCommonPath = kcContext.url.resourcesPath + "/" + "RESOURCES_COMMON_cLsLsMrtDkpVv"; kcContext.url.resourcesCommonPath = kcContext.url.resourcesPath + "/" + "RESOURCES_COMMON_cLsLsMrtDkpVv";
} }
kcContext["x-keycloakify"] = {};
<#if profile?? && profile.attributes??> <#if profile?? && profile.attributes??>
kcContext.lOCALIZATION_REALM_OVERRIDES_USER_PROFILE_PROPERTY_KEY_aaGLsPgGIdeeX = { kcContext["x-keycloakify"].realmMessageBundleUserProfile = {
<#list profile.attributes as attribute> <#list profile.attributes as attribute>
<#if attribute.annotations?? && attribute.displayName??> <#if attribute.annotations?? && attribute.displayName??>
"${attribute.displayName}": decodeHtmlEntities("${advancedMsg(attribute.displayName)?js_string}"), "${attribute.displayName}": decodeHtmlEntities("${advancedMsg(attribute.displayName)?js_string}"),
@ -61,6 +62,9 @@ if( kcContext.url && kcContext.url.resourcesPath ){
</#list> </#list>
}; };
</#if> </#if>
<#if pageId == "terms.ftl" || termsAcceptanceRequired?? && termsAcceptanceRequired>
kcContext["x-keycloakify"].realmMessageBundleTermsText= decodeHtmlEntities("${msg("termsText")?js_string}");
</#if>
attributes_to_attributesByName: { attributes_to_attributesByName: {
if( !kcContext.profile ){ if( !kcContext.profile ){
break attributes_to_attributesByName; break attributes_to_attributesByName;
@ -198,14 +202,15 @@ function decodeHtmlEntities(htmlStr){
) || ( ) || (
key == "execution" && key == "execution" &&
are_same_path(path, []) are_same_path(path, [])
) || (
key == "entity" &&
are_same_path(path, ["user"])
) )
> >
<#-- <#local out_seq += ["/*" + path?join(".") + "." + key + " excluded*/"]> --> <#-- <#local out_seq += ["/*" + path?join(".") + "." + key + " excluded*/"]> -->
<#continue> <#continue>
</#if> </#if>
USER_DEFINED_EXCLUSIONS_eKsaY4ZsZ4eMr2
<#-- https://github.com/keycloakify/keycloakify/discussions/406 --> <#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
<#if ( <#if (
["register.ftl", "register-user-profile.ftl", "terms.ftl", "info.ftl", "login.ftl", "login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) && ["register.ftl", "register-user-profile.ftl", "terms.ftl", "info.ftl", "login.ftl", "login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
@ -221,6 +226,8 @@ function decodeHtmlEntities(htmlStr){
<#local out_seq += ["/*Accessing attemptedUsername throwed an exception */"]> <#local out_seq += ["/*Accessing attemptedUsername throwed an exception */"]>
</#attempt> </#attempt>
</#if> </#if>
USER_DEFINED_EXCLUSIONS_eKsaY4ZsZ4eMr2
<#attempt> <#attempt>
<#if !object[key]??> <#if !object[key]??>

View File

@ -7,13 +7,13 @@ import {
lastKeycloakVersionWithAccountV1, lastKeycloakVersionWithAccountV1,
accountV1ThemeName accountV1ThemeName
} from "../../shared/constants"; } from "../../shared/constants";
import { downloadKeycloakDefaultTheme } from "../../shared/downloadKeycloakDefaultTheme"; import {
downloadKeycloakDefaultTheme,
BuildContextLike as BuildContextLike_downloadKeycloakDefaultTheme
} from "../../shared/downloadKeycloakDefaultTheme";
import { transformCodebase } from "../../tools/transformCodebase"; import { transformCodebase } from "../../tools/transformCodebase";
export type BuildContextLike = { export type BuildContextLike = BuildContextLike_downloadKeycloakDefaultTheme;
cacheDirPath: string;
npmWorkspaceRootDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();

View File

@ -1,14 +1,15 @@
import type { ThemeType } from "../../shared/constants"; import { type ThemeType, fallbackLanguageTag } from "../../shared/constants";
import { crawl } from "../../tools/crawl"; import { crawl } from "../../tools/crawl";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import { readFileSync } from "fs";
import { symToStr } from "tsafe/symToStr"; import { symToStr } from "tsafe/symToStr";
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
import * as recast from "recast"; import * as recast from "recast";
import * as babelParser from "@babel/parser"; import * as babelParser from "@babel/parser";
import babelGenerate from "@babel/generator"; import babelGenerate from "@babel/generator";
import * as babelTypes from "@babel/types"; import * as babelTypes from "@babel/types";
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile"; import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
import * as fs from "fs";
import { assert } from "tsafe/assert";
export function generateMessageProperties(params: { export function generateMessageProperties(params: {
themeSrcDirPath: string; themeSrcDirPath: string;
@ -16,36 +17,92 @@ export function generateMessageProperties(params: {
}): { languageTag: string; propertiesFileSource: string }[] { }): { languageTag: string; propertiesFileSource: string }[] {
const { themeSrcDirPath, themeType } = params; const { themeSrcDirPath, themeType } = params;
let files = crawl({ const baseMessagesDirPath = pathJoin(
dirPath: pathJoin(themeSrcDirPath, themeType), getThisCodebaseRootDirPath(),
returnedPathsType: "absolute" "src",
}); themeType,
"i18n",
files = files.filter(file => { "baseMessages"
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) { const baseMessageBundle: { [languageTag: string]: Record<string, string> } =
return []; Object.fromEntries(
} fs
.readdirSync(baseMessagesDirPath)
.filter(baseName => baseName !== "index.ts")
.map(basename => ({
languageTag: basename.replace(/\.ts$/, ""),
filePath: pathJoin(baseMessagesDirPath, basename)
}))
.map(({ languageTag, filePath }) => {
const lines = fs
.readFileSync(filePath)
.toString("utf8")
.split(/\r?\n/);
const extraMessages = files let messagesJson = "{";
.map(file => {
const root = recast.parse(readFileSync(file).toString("utf8"), { let isInDeclaration = false;
for (const line of lines) {
if (!isInDeclaration) {
if (line.startsWith("const messages")) {
isInDeclaration = true;
}
continue;
}
if (line.startsWith("}")) {
messagesJson += "}";
break;
}
messagesJson += line;
}
const messages = JSON.parse(messagesJson) as Record<string, string>;
return [languageTag, messages];
})
);
const { i18nTsFilePath } = (() => {
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 =>
fs.readFileSync(file).toString("utf8").includes("createUseI18n(")
);
const i18nTsFilePath: string | undefined = files[0];
return { i18nTsFilePath };
})();
const messageBundle: { [languageTag: string]: Record<string, string> } | undefined =
(() => {
if (i18nTsFilePath === undefined) {
return undefined;
}
const root = recast.parse(fs.readFileSync(i18nTsFilePath).toString("utf8"), {
parser: { parser: {
parse: (code: string) => parse: (code: string) =>
babelParser.parse(code, { babelParser.parse(code, {
@ -57,7 +114,7 @@ export function generateMessageProperties(params: {
} }
}); });
const codes: string[] = []; let messageBundleDeclarationTsCode: string | undefined = undefined;
recast.visit(root, { recast.visit(root, {
visitCallExpression: function (path) { visitCallExpression: function (path) {
@ -65,103 +122,71 @@ export function generateMessageProperties(params: {
path.node.callee.type === "Identifier" && path.node.callee.type === "Identifier" &&
path.node.callee.name === "createUseI18n" path.node.callee.name === "createUseI18n"
) { ) {
codes.push(babelGenerate(path.node.arguments[0] as any).code); messageBundleDeclarationTsCode = babelGenerate(
path.node.arguments[0] as any
).code;
return false;
} }
this.traverse(path); this.traverse(path);
} }
}); });
return codes; assert(messageBundleDeclarationTsCode !== undefined);
})
.flat() let messageBundle: {
.map(code => {
let extraMessages: {
[languageTag: string]: Record<string, string>; [languageTag: string]: Record<string, string>;
} = {}; } = {};
try { try {
eval(`${symToStr({ extraMessages })} = ${code}`); eval(
`${symToStr({ messageBundle })} = ${messageBundleDeclarationTsCode}`
);
} catch { } catch {
console.warn( console.warn(
[ [
"WARNING: Make sure that the first argument of createUseI18n can be evaluated in a javascript", "WARNING: Make sure the messageBundle your provided as argument of createUseI18n can be statically evaluated.",
"runtime where only the node globals are available.",
"This is important because we need to put your i18n messages in messages_*.properties files", "This is important because we need to put your i18n messages in messages_*.properties files",
"or they won't be available server side.", "or they won't be available server side.",
"\n", "\n",
"The following code could not be evaluated:", "The following code could not be evaluated:",
"\n", "\n",
code messageBundleDeclarationTsCode
].join(" ") ].join(" ")
); );
} }
return extraMessages; return messageBundle;
}); })();
const languageTags = extraMessages const mergedMessageBundle: { [languageTag: string]: Record<string, string> } =
.map(extraMessage => Object.keys(extraMessage)) Object.fromEntries(
.flat() Object.entries(baseMessageBundle).map(([languageTag, messages]) => [
.reduce(...removeDuplicates<string>()); languageTag,
{
const keyValueMapByLanguageTag: Record<string, Record<string, string>> = {}; ...messages,
...(messageBundle === undefined
for (const languageTag of languageTags) { ? {}
const keyValueMap: Record<string, string> = {}; : messageBundle[languageTag] ??
messageBundle[fallbackLanguageTag] ??
for (const extraMessage of extraMessages) { messageBundle[Object.keys(messageBundle)[0]] ??
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; const messageProperties: { languageTag: string; propertiesFileSource: string }[] =
} Object.entries(mergedMessageBundle).map(([languageTag, messages]) => ({
}
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}=${escapeStringForPropertiesFile(value)}`)
.join("\n");
out.push({
languageTag, languageTag,
propertiesFileSource: [ propertiesFileSource: [
"# This file was generated by keycloakify",
"", "",
"parent=base", ...(themeType !== "account" ? ["parent=base"] : []),
"", ...Object.entries(messages).map(
propertiesFileSource, ([key, value]) => `${key}=${escapeStringForPropertiesFile(value)}`
),
"" ""
].join("\n") ].join("\n")
}); }));
}
return out; return messageProperties;
} }

View File

@ -53,6 +53,7 @@ export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"]; recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"];
themeSrcDirPath: string; themeSrcDirPath: string;
bundler: { type: "vite" } | { type: "webpack" };
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -113,7 +114,7 @@ export async function generateResourcesForMainTheme(params: {
); );
if (fs.existsSync(dirPath)) { if (fs.existsSync(dirPath)) {
assert(buildContext.bundler === "webpack"); assert(buildContext.bundler.type === "webpack");
throw new Error( throw new Error(
[ [

View File

@ -11,7 +11,15 @@ export function readFieldNameUsage(params: {
}): string[] { }): string[] {
const { themeSrcDirPath, themeType } = params; const { themeSrcDirPath, themeType } = params;
const fieldNames = new Set<string>(); // NOTE: We pre-populate with the synthetic user attributes defined in useUserProfileForm (can't be parsed automatically)
const fieldNames = new Set<string>([
"firstName",
"lastName",
"email",
"username",
"password",
"password-confirm"
]);
for (const srcDirPath of [ for (const srcDirPath of [
pathJoin(getThisCodebaseRootDirPath(), "src", themeType), pathJoin(getThisCodebaseRootDirPath(), "src", themeType),

View File

@ -85,7 +85,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
}); });
run_post_build_script: { run_post_build_script: {
if (buildContext.bundler !== "vite") { if (buildContext.bundler.type !== "vite") {
break run_post_build_script; break run_post_build_script;
} }

View File

@ -8,7 +8,7 @@ export type BuildContextLike = {
projectBuildDirPath: string; projectBuildDirPath: string;
assetsDirPath: string; assetsDirPath: string;
urlPathname: string | undefined; urlPathname: string | undefined;
bundler: "vite" | "webpack"; bundler: { type: "vite" } | { type: "webpack" };
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -20,7 +20,7 @@ export function replaceImportsInJsCode(params: {
const { jsCode, buildContext } = params; const { jsCode, buildContext } = params;
const { fixedJsCode } = (() => { const { fixedJsCode } = (() => {
switch (buildContext.bundler) { switch (buildContext.bundler.type) {
case "vite": case "vite":
return replaceImportsInJsCode_vite({ return replaceImportsInJsCode_vite({
jsCode, jsCode,

View File

@ -1,7 +1,12 @@
import { parse as urlParse } from "url"; import { parse as urlParse } from "url";
import { join as pathJoin } from "path"; import {
join as pathJoin,
sep as pathSep,
relative as pathRelative,
resolve as pathResolve,
dirname as pathDirname
} from "path";
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath"; import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
import { getNpmWorkspaceRootDirPath } from "../tools/getNpmWorkspaceRootDirPath";
import type { CliCommandOptions } from "../main"; import type { CliCommandOptions } from "../main";
import { z } from "zod"; import { z } from "zod";
import * as fs from "fs"; import * as fs from "fs";
@ -21,9 +26,9 @@ import { type ThemeType } from "./constants";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
import { symToStr } from "tsafe/symToStr"; import { symToStr } from "tsafe/symToStr";
import chalk from "chalk"; import chalk from "chalk";
import { getProxyFetchOptions, type ProxyFetchOptions } from "../tools/fetchProxyOptions";
export type BuildContext = { export type BuildContext = {
bundler: "vite" | "webpack";
themeVersion: string; themeVersion: string;
themeNames: [string, ...string[]]; themeNames: [string, ...string[]];
extraThemeProperties: string[] | undefined; extraThemeProperties: string[] | undefined;
@ -40,7 +45,7 @@ export type BuildContext = {
* In this case the urlPathname will be "/my-app/" */ * In this case the urlPathname will be "/my-app/" */
urlPathname: string | undefined; urlPathname: string | undefined;
assetsDirPath: string; assetsDirPath: string;
npmWorkspaceRootDirPath: string; fetchOptions: ProxyFetchOptions;
kcContextExclusionsFtlCode: string | undefined; kcContextExclusionsFtlCode: string | undefined;
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string; themeSrcDirPath: string;
@ -49,6 +54,15 @@ export type BuildContext = {
keycloakVersionRange: KeycloakVersionRange; keycloakVersionRange: KeycloakVersionRange;
jarFileBasename: string; jarFileBasename: string;
}[]; }[];
bundler:
| {
type: "vite";
}
| {
type: "webpack";
packageJsonDirPath: string;
packageJsonScripts: Record<string, string>;
};
}; };
export type BuildOptions = { export type BuildOptions = {
@ -90,15 +104,54 @@ export function getBuildContext(params: {
}): BuildContext { }): BuildContext {
const { cliCommandOptions } = params; const { cliCommandOptions } = params;
const projectDirPath = (() => { const projectDirPath =
if (cliCommandOptions.projectDirPath === undefined) { cliCommandOptions.projectDirPath !== undefined
return process.cwd(); ? getAbsoluteAndInOsFormatPath({
pathIsh: cliCommandOptions.projectDirPath,
cwd: process.cwd()
})
: process.cwd();
const { themeSrcDirPath } = (() => {
const srcDirPath = pathJoin(projectDirPath, "src");
const themeSrcDirPath: string | undefined = crawl({
dirPath: srcDirPath,
returnedPathsType: "relative to dirPath"
})
.map(fileRelativePath => {
for (const themeSrcDirBasename of ["keycloak-theme", "keycloak_theme"]) {
const split = fileRelativePath.split(themeSrcDirBasename);
if (split.length === 2) {
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
}
}
return undefined;
})
.filter(exclude(undefined))[0];
if (themeSrcDirPath !== undefined) {
return { themeSrcDirPath };
} }
return getAbsoluteAndInOsFormatPath({ for (const themeType of [...themeTypes, "email"]) {
pathIsh: cliCommandOptions.projectDirPath, if (!fs.existsSync(pathJoin(srcDirPath, themeType))) {
cwd: process.cwd() continue;
}); }
return { themeSrcDirPath: srcDirPath };
}
console.log(
chalk.red(
[
`Can't locate your Keycloak theme source directory in .${pathSep}${pathRelative(process.cwd(), srcDirPath)}`,
`Make sure to either use the Keycloakify CLI in the root of your Keycloakify project or use the --project CLI option`,
`If you are collocating your Keycloak theme with your app you must have a directory named 'keycloak-theme' or 'keycloak_theme' in your 'src' directory`
].join("\n")
)
);
process.exit(1);
})(); })();
const { resolvedViteConfig } = (() => { const { resolvedViteConfig } = (() => {
@ -135,6 +188,42 @@ export function getBuildContext(params: {
return { resolvedViteConfig }; return { resolvedViteConfig };
})(); })();
const packageJsonFilePath = (function getPackageJSonDirPath(upCount: number): string {
const dirPath = pathResolve(
pathJoin(...[projectDirPath, ...Array(upCount).fill("..")])
);
assert(dirPath !== pathSep, "Root package.json not found");
success: {
const packageJsonFilePath = pathJoin(dirPath, "package.json");
if (!fs.existsSync(packageJsonFilePath)) {
break success;
}
const parsedPackageJson = z
.object({
name: z.string().optional(),
dependencies: z.record(z.string()).optional(),
devDependencies: z.record(z.string()).optional()
})
.parse(JSON.parse(fs.readFileSync(packageJsonFilePath).toString("utf8")));
if (
parsedPackageJson.dependencies?.keycloakify === undefined &&
parsedPackageJson.devDependencies?.keycloakify === undefined &&
parsedPackageJson.name !== "keycloakify" // NOTE: For local storybook build
) {
break success;
}
return packageJsonFilePath;
}
return getPackageJSonDirPath(upCount + 1);
})(0);
const parsedPackageJson = (() => { const parsedPackageJson = (() => {
type BuildOptions_packageJson = BuildOptions & { type BuildOptions_packageJson = BuildOptions & {
projectBuildDirPath?: string; projectBuildDirPath?: string;
@ -143,14 +232,14 @@ export function getBuildContext(params: {
}; };
type ParsedPackageJson = { type ParsedPackageJson = {
name: string; name?: string;
version?: string; version?: string;
homepage?: string; homepage?: string;
keycloakify?: BuildOptions_packageJson; keycloakify?: BuildOptions_packageJson;
}; };
const zParsedPackageJson = z.object({ const zParsedPackageJson = z.object({
name: z.string(), name: z.string().optional(),
version: z.string().optional(), version: z.string().optional(),
homepage: z.string().optional(), homepage: z.string().optional(),
keycloakify: id<z.ZodType<BuildOptions_packageJson>>( keycloakify: id<z.ZodType<BuildOptions_packageJson>>(
@ -228,10 +317,16 @@ export function getBuildContext(params: {
assert<Equals<Got, Expected>>(); assert<Equals<Got, Expected>>();
} }
const configurationPackageJsonFilePath = (() => {
const rootPackageJsonFilePath = pathJoin(projectDirPath, "package.json");
return fs.existsSync(rootPackageJsonFilePath)
? rootPackageJsonFilePath
: packageJsonFilePath;
})();
return zParsedPackageJson.parse( return zParsedPackageJson.parse(
JSON.parse( JSON.parse(fs.readFileSync(configurationPackageJsonFilePath).toString("utf8"))
fs.readFileSync(pathJoin(projectDirPath, "package.json")).toString("utf8")
)
); );
})(); })();
@ -240,47 +335,6 @@ export function getBuildContext(params: {
...resolvedViteConfig?.buildOptions ...resolvedViteConfig?.buildOptions
}; };
const { themeSrcDirPath } = (() => {
const srcDirPath = pathJoin(projectDirPath, "src");
const themeSrcDirPath: string | undefined = crawl({
dirPath: srcDirPath,
returnedPathsType: "relative to dirPath"
})
.map(fileRelativePath => {
for (const themeSrcDirBasename of ["keycloak-theme", "keycloak_theme"]) {
const split = fileRelativePath.split(themeSrcDirBasename);
if (split.length === 2) {
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
}
}
return undefined;
})
.filter(exclude(undefined))[0];
if (themeSrcDirPath !== undefined) {
return { themeSrcDirPath };
}
for (const themeType of [...themeTypes, "email"]) {
if (!fs.existsSync(pathJoin(srcDirPath, themeType))) {
continue;
}
return { themeSrcDirPath: srcDirPath };
}
console.log(
chalk.red(
[
"Can't locate your keycloak theme source directory.",
"See: https://docs.keycloakify.dev/v/v10/keycloakify-in-my-app/collocation"
].join("\n")
)
);
process.exit(1);
})();
const recordIsImplementedByThemeType = objectFromEntries( const recordIsImplementedByThemeType = objectFromEntries(
(["login", "account", "email"] as const).map(themeType => [ (["login", "account", "email"] as const).map(themeType => [
themeType, themeType,
@ -290,12 +344,14 @@ export function getBuildContext(params: {
const themeNames = ((): [string, ...string[]] => { const themeNames = ((): [string, ...string[]] => {
if (buildOptions.themeName === undefined) { if (buildOptions.themeName === undefined) {
return [ return parsedPackageJson.name === undefined
parsedPackageJson.name ? ["keycloakify"]
.replace(/^@(.*)/, "$1") : [
.split("/") parsedPackageJson.name
.join("-") .replace(/^@(.*)/, "$1")
]; .split("/")
.join("-")
];
} }
if (typeof buildOptions.themeName === "string") { if (typeof buildOptions.themeName === "string") {
@ -328,15 +384,29 @@ export function getBuildContext(params: {
return pathJoin(projectDirPath, resolvedViteConfig.buildDir); return pathJoin(projectDirPath, resolvedViteConfig.buildDir);
})(); })();
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({
projectDirPath,
dependencyExpected: "keycloakify"
});
const bundler = resolvedViteConfig !== undefined ? "vite" : "webpack"; const bundler = resolvedViteConfig !== undefined ? "vite" : "webpack";
return { return {
bundler, bundler:
resolvedViteConfig !== undefined
? { type: "vite" }
: (() => {
const { scripts } = z
.object({
scripts: z.record(z.string()).optional()
})
.parse(
JSON.parse(
fs.readFileSync(packageJsonFilePath).toString("utf8")
)
);
return {
type: "webpack",
packageJsonDirPath: pathDirname(packageJsonFilePath),
packageJsonScripts: scripts ?? {}
};
})(),
themeVersion: buildOptions.themeVersion ?? parsedPackageJson.version ?? "0.0.0", themeVersion: buildOptions.themeVersion ?? parsedPackageJson.version ?? "0.0.0",
themeNames, themeNames,
extraThemeProperties: buildOptions.extraThemeProperties, extraThemeProperties: buildOptions.extraThemeProperties,
@ -413,7 +483,11 @@ export function getBuildContext(params: {
}); });
} }
return pathJoin(npmWorkspaceRootDirPath, "node_modules", ".cache"); return pathJoin(
pathDirname(packageJsonFilePath),
"node_modules",
".cache"
);
})(), })(),
"keycloakify" "keycloakify"
); );
@ -462,7 +536,6 @@ export function getBuildContext(params: {
return pathJoin(projectBuildDirPath, resolvedViteConfig.assetsDir); return pathJoin(projectBuildDirPath, resolvedViteConfig.assetsDir);
})(), })(),
npmWorkspaceRootDirPath,
kcContextExclusionsFtlCode: (() => { kcContextExclusionsFtlCode: (() => {
if (buildOptions.kcContextExclusionsFtl === undefined) { if (buildOptions.kcContextExclusionsFtl === undefined) {
return undefined; return undefined;
@ -482,6 +555,33 @@ export function getBuildContext(params: {
environmentVariables: buildOptions.environmentVariables ?? [], environmentVariables: buildOptions.environmentVariables ?? [],
recordIsImplementedByThemeType, recordIsImplementedByThemeType,
themeSrcDirPath, themeSrcDirPath,
fetchOptions: getProxyFetchOptions({
npmConfigGetCwd: (function callee(upCount: number): string {
const dirPath = pathResolve(
pathJoin(...[projectDirPath, ...Array(upCount).fill("..")])
);
assert(
dirPath !== pathSep,
"Couldn't find a place to run 'npm config get'"
);
try {
child_process.execSync("npm config get", {
cwd: dirPath,
stdio: "ignore"
});
} catch (error) {
if (String(error).includes("ENOWORKSPACES")) {
return callee(upCount + 1);
}
throw error;
}
return dirPath;
})(0)
}),
jarTargets: (() => { jarTargets: (() => {
const getDefaultJarFileBasename = (range: string) => const getDefaultJarFileBasename = (range: string) =>
`keycloak-theme-for-kc-${range}.jar`; `keycloak-theme-for-kc-${range}.jar`;

View File

@ -1,5 +1,3 @@
export const nameOfTheLocalizationRealmOverridesUserProfileProperty =
"__localizationRealmOverridesUserProfile";
export const keycloak_resources = "keycloak-resources"; export const keycloak_resources = "keycloak-resources";
export const resources_common = "resources-common"; export const resources_common = "resources-common";
export const lastKeycloakVersionWithAccountV1 = "21.1.2"; export const lastKeycloakVersionWithAccountV1 = "21.1.2";
@ -69,3 +67,5 @@ export type LoginThemePageId = (typeof loginThemePageIds)[number];
export type AccountThemePageId = (typeof accountThemePageIds)[number]; export type AccountThemePageId = (typeof accountThemePageIds)[number];
export const containerName = "keycloak-keycloakify"; export const containerName = "keycloak-keycloakify";
export const fallbackLanguageTag = "en";

View File

@ -37,10 +37,7 @@ export async function copyKeycloakResourcesToPublic(params: {
buildContext: { buildContext: {
loginThemeResourcesFromKeycloakVersion: readThisNpmPackageVersion(), loginThemeResourcesFromKeycloakVersion: readThisNpmPackageVersion(),
cacheDirPath: pathRelative(destDirPath, buildContext.cacheDirPath), cacheDirPath: pathRelative(destDirPath, buildContext.cacheDirPath),
npmWorkspaceRootDirPath: pathRelative( fetchOptions: buildContext.fetchOptions
destDirPath,
buildContext.npmWorkspaceRootDirPath
)
} }
}, },
null, null,

View File

@ -6,7 +6,7 @@ import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
export type BuildContextLike = { export type BuildContextLike = {
cacheDirPath: string; cacheDirPath: string;
npmWorkspaceRootDirPath: string; fetchOptions: BuildContext["fetchOptions"];
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -23,7 +23,7 @@ export async function downloadKeycloakDefaultTheme(params: {
const { extractedDirPath } = await downloadAndExtractArchive({ const { extractedDirPath } = await downloadAndExtractArchive({
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`, url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
cacheDirPath: buildContext.cacheDirPath, cacheDirPath: buildContext.cacheDirPath,
npmWorkspaceRootDirPath: buildContext.npmWorkspaceRootDirPath, fetchOptions: buildContext.fetchOptions,
uniqueIdOfOnOnArchiveFile: "downloadKeycloakDefaultTheme", uniqueIdOfOnOnArchiveFile: "downloadKeycloakDefaultTheme",
onArchiveFile: async params => { onArchiveFile: async params => {
const fileRelativePath = pathRelative("theme", params.fileRelativePath); const fileRelativePath = pathRelative("theme", params.fileRelativePath);

View File

@ -1,16 +1,15 @@
import * as child_process from "child_process"; import * as child_process from "child_process";
import { Deferred } from "evt/tools/Deferred"; import { Deferred } from "evt/tools/Deferred";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import type { BuildContext } from "../shared/buildContext"; import type { BuildContext } from "../shared/buildContext";
import * as fs from "fs"; import chalk from "chalk";
import { join as pathJoin } from "path"; import { sep as pathSep, join as pathJoin } from "path";
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
export type BuildContextLike = { export type BuildContextLike = {
projectDirPath: string; projectDirPath: string;
keycloakifyBuildDirPath: string; keycloakifyBuildDirPath: string;
bundler: "vite" | "webpack"; bundler: BuildContext["bundler"];
npmWorkspaceRootDirPath: string;
projectBuildDirPath: string; projectBuildDirPath: string;
}; };
@ -21,95 +20,29 @@ export async function appBuild(params: {
}): Promise<{ isAppBuildSuccess: boolean }> { }): Promise<{ isAppBuildSuccess: boolean }> {
const { buildContext } = params; const { buildContext } = params;
const { bundler } = buildContext; switch (buildContext.bundler.type) {
case "vite":
return appBuild_vite({ buildContext });
case "webpack":
return appBuild_webpack({ buildContext });
}
}
const { command, args, cwd } = (() => { async function appBuild_vite(params: {
switch (bundler) { buildContext: BuildContextLike;
case "vite": }): Promise<{ isAppBuildSuccess: boolean }> {
return { const { buildContext } = params;
command: "npx",
args: ["vite", "build"],
cwd: buildContext.projectDirPath
};
case "webpack": {
for (const dirPath of [
buildContext.projectDirPath,
buildContext.npmWorkspaceRootDirPath
]) {
try {
const parsedPackageJson = JSON.parse(
fs
.readFileSync(pathJoin(dirPath, "package.json"))
.toString("utf8")
);
const [scriptName] = assert(buildContext.bundler.type === "vite");
Object.entries(parsedPackageJson.scripts).find(
([, scriptValue]) => {
assert(is<string>(scriptValue));
if (
scriptValue.includes("webpack") &&
scriptValue.includes("--mode production")
) {
return true;
}
if ( const dIsSuccess = new Deferred<boolean>();
scriptValue.includes("react-scripts") &&
scriptValue.includes("build")
) {
return true;
}
if ( console.log(chalk.blue("Running: 'npx vite build'"));
scriptValue.includes("react-app-rewired") &&
scriptValue.includes("build")
) {
return true;
}
if ( const child = child_process.spawn("npx", ["vite", "build"], {
scriptValue.includes("craco") && cwd: buildContext.projectDirPath,
scriptValue.includes("build") shell: true
) { });
return true;
}
if (
scriptValue.includes("ng") &&
scriptValue.includes("build")
) {
return true;
}
return false;
}
) ?? [];
if (scriptName === undefined) {
continue;
}
return {
command: "npm",
args: ["run", scriptName],
cwd: dirPath
};
} catch {
continue;
}
}
throw new Error(
"Keycloakify was unable to determine which script is responsible for building the app."
);
}
}
})();
const dResult = new Deferred<{ isSuccess: boolean }>();
const child = child_process.spawn(command, args, { cwd, shell: true });
child.stdout.on("data", data => { child.stdout.on("data", data => {
if (data.toString("utf8").includes("gzip:")) { if (data.toString("utf8").includes("gzip:")) {
@ -121,9 +54,127 @@ export async function appBuild(params: {
child.stderr.on("data", data => process.stderr.write(data)); child.stderr.on("data", data => process.stderr.write(data));
child.on("exit", code => dResult.resolve({ isSuccess: code === 0 })); child.on("exit", code => dIsSuccess.resolve(code === 0));
const { isSuccess } = await dResult.pr; const isSuccess = await dIsSuccess.pr;
return { isAppBuildSuccess: isSuccess }; return { isAppBuildSuccess: isSuccess };
} }
async function appBuild_webpack(params: {
buildContext: BuildContextLike;
}): Promise<{ isAppBuildSuccess: boolean }> {
const { buildContext } = params;
assert(buildContext.bundler.type === "webpack");
const entries = Object.entries(buildContext.bundler.packageJsonScripts).filter(
([, scriptCommand]) => scriptCommand.includes("keycloakify build")
);
if (entries.length === 0) {
console.log(
chalk.red(
[
`You should have a script in your package.json at ${buildContext.bundler.packageJsonDirPath}`,
`that includes the 'keycloakify build' command`
].join(" ")
)
);
process.exit(-1);
}
const entry =
entries.length === 1
? entries[0]
: entries.find(([scriptName]) => scriptName === "build-keycloak-theme");
if (entry === undefined) {
console.log(
chalk.red(
"There's multiple candidate script for building your app, name one 'build-keycloak-theme'"
)
);
process.exit(-1);
}
const [scriptName, scriptCommand] = entry;
const { appBuildSubCommands } = (() => {
const appBuildSubCommands: string[] = [];
for (const subCmd of scriptCommand.split("&&").map(s => s.trim())) {
if (subCmd.includes("keycloakify build")) {
break;
}
appBuildSubCommands.push(subCmd);
}
return { appBuildSubCommands };
})();
if (appBuildSubCommands.length === 0) {
console.log(
chalk.red(
`Your ${scriptName} script should look like "... && keycloakify build ..."`
)
);
process.exit(-1);
}
let commandCwd = buildContext.bundler.packageJsonDirPath;
for (const subCommand of appBuildSubCommands) {
const dIsSuccess = new Deferred<boolean>();
const [command, ...args] = subCommand.split(" ");
if (command === "cd") {
const [pathIsh] = args;
commandCwd = getAbsoluteAndInOsFormatPath({
pathIsh,
cwd: commandCwd
});
continue;
}
console.log(chalk.blue(`Running: '${subCommand}'`));
const child = child_process.spawn(command, args, {
cwd: commandCwd,
env: {
...process.env,
PATH: (() => {
const separator = pathSep === "/" ? ":" : ";";
return [
pathJoin(
buildContext.bundler.packageJsonDirPath,
"node_modules",
".bin"
),
...(process.env.PATH ?? "").split(separator)
].join(separator);
})()
},
shell: true
});
child.stdout.on("data", data => process.stdout.write(data));
child.stderr.on("data", data => process.stderr.write(data));
child.on("exit", code => dIsSuccess.resolve(code === 0));
const isSuccess = await dIsSuccess.pr;
if (!isSuccess) {
return { isAppBuildSuccess: false };
}
}
return { isAppBuildSuccess: true };
}

View File

@ -7,8 +7,6 @@ import type { BuildContext } from "../shared/buildContext";
export type BuildContextLike = { export type BuildContextLike = {
projectDirPath: string; projectDirPath: string;
keycloakifyBuildDirPath: string; keycloakifyBuildDirPath: string;
bundler: "vite" | "webpack";
npmWorkspaceRootDirPath: string;
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();

View File

@ -1,6 +1,8 @@
{ {
"id": "34c5f904-d66e-4d8f-8876-8f00d9fa9d6c", "id": "34c5f904-d66e-4d8f-8876-8f00d9fa9d6c",
"realm": "myrealm", "realm": "myrealm",
"displayName": "",
"displayNameHtml": "",
"notBefore": 0, "notBefore": 0,
"defaultSignatureAlgorithm": "RS256", "defaultSignatureAlgorithm": "RS256",
"revokeRefreshToken": false, "revokeRefreshToken": false,
@ -1356,11 +1358,11 @@
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper",
"oidc-address-mapper",
"saml-user-property-mapper", "saml-user-property-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper", "oidc-full-name-mapper",
"oidc-usermodel-attribute-mapper",
"saml-role-list-mapper", "saml-role-list-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-attribute-mapper", "saml-user-attribute-mapper",
"oidc-usermodel-property-mapper" "oidc-usermodel-property-mapper"
] ]
@ -1431,13 +1433,13 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"saml-user-property-mapper", "saml-role-list-mapper",
"oidc-full-name-mapper", "oidc-full-name-mapper",
"oidc-address-mapper",
"saml-user-attribute-mapper", "saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper",
"oidc-address-mapper",
"oidc-usermodel-attribute-mapper", "oidc-usermodel-attribute-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper" "oidc-usermodel-property-mapper"
] ]
} }
@ -2127,17 +2129,20 @@
"dockerAuthenticationFlow": "docker auth", "dockerAuthenticationFlow": "docker auth",
"attributes": { "attributes": {
"cibaBackchannelTokenDeliveryMode": "poll", "cibaBackchannelTokenDeliveryMode": "poll",
"cibaExpiresIn": "120",
"cibaAuthRequestedUserHint": "login_hint", "cibaAuthRequestedUserHint": "login_hint",
"oauth2DeviceCodeLifespan": "600",
"clientOfflineSessionMaxLifespan": "0", "clientOfflineSessionMaxLifespan": "0",
"oauth2DevicePollingInterval": "5", "oauth2DevicePollingInterval": "5",
"clientSessionIdleTimeout": "0", "clientSessionIdleTimeout": "0",
"parRequestUriLifespan": "60", "userProfileEnabled": "true",
"clientSessionMaxLifespan": "0",
"clientOfflineSessionIdleTimeout": "0", "clientOfflineSessionIdleTimeout": "0",
"cibaInterval": "5", "cibaInterval": "5",
"realmReusableOtpCode": "false" "realmReusableOtpCode": "false",
"cibaExpiresIn": "120",
"oauth2DeviceCodeLifespan": "600",
"parRequestUriLifespan": "60",
"clientSessionMaxLifespan": "0",
"frontendUrl": "",
"acr.loa.map": "{}"
}, },
"keycloakVersion": "23.0.7", "keycloakVersion": "23.0.7",
"userManagedAccessAllowed": false, "userManagedAccessAllowed": false,

View File

@ -121,7 +121,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
if (!isAppBuildSuccess) { if (!isAppBuildSuccess) {
console.log( console.log(
chalk.red( chalk.red(
`App build failed, exiting. Try running 'npm run build' and see what's wrong.` `App build failed, exiting. Try building your app (e.g 'npm run build') and see what's wrong.`
) )
); );
process.exit(1); process.exit(1);

View File

@ -1,12 +1,12 @@
import fetch from "make-fetch-happen"; import fetch, { type FetchOptions } from "make-fetch-happen";
import { mkdir, unlink, writeFile, readdir, readFile } from "fs/promises"; import { mkdir, unlink, writeFile, readdir, readFile } from "fs/promises";
import { dirname as pathDirname, join as pathJoin } from "path"; import { dirname as pathDirname, join as pathJoin } from "path";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { extractArchive } from "../extractArchive"; import { extractArchive } from "./extractArchive";
import { existsAsync } from "../fs.existsAsync"; import { existsAsync } from "./fs.existsAsync";
import { getProxyFetchOptions } from "./fetchProxyOptions";
import * as crypto from "crypto"; import * as crypto from "crypto";
import { rm } from "../fs.rm"; import { rm } from "./fs.rm";
export async function downloadAndExtractArchive(params: { export async function downloadAndExtractArchive(params: {
url: string; url: string;
@ -20,15 +20,10 @@ export async function downloadAndExtractArchive(params: {
}) => Promise<void>; }) => Promise<void>;
}) => Promise<void>; }) => Promise<void>;
cacheDirPath: string; cacheDirPath: string;
npmWorkspaceRootDirPath: string; fetchOptions: FetchOptions | undefined;
}): Promise<{ extractedDirPath: string }> { }): Promise<{ extractedDirPath: string }> {
const { const { url, uniqueIdOfOnOnArchiveFile, onArchiveFile, cacheDirPath, fetchOptions } =
url, params;
uniqueIdOfOnOnArchiveFile,
onArchiveFile,
cacheDirPath,
npmWorkspaceRootDirPath
} = params;
const archiveFileBasename = url.split("?")[0].split("/").reverse()[0]; const archiveFileBasename = url.split("?")[0].split("/").reverse()[0];
@ -55,10 +50,7 @@ export async function downloadAndExtractArchive(params: {
await mkdir(pathDirname(archiveFilePath), { recursive: true }); await mkdir(pathDirname(archiveFilePath), { recursive: true });
const response = await fetch( const response = await fetch(url, fetchOptions);
url,
await getProxyFetchOptions({ npmWorkspaceRootDirPath })
);
response.body?.setMaxListeners(Number.MAX_VALUE); response.body?.setMaxListeners(Number.MAX_VALUE);
assert(typeof response.body !== "undefined" && response.body != null); assert(typeof response.body !== "undefined" && response.body != null);

View File

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

View File

@ -1,61 +1,40 @@
import { exec as execCallback } from "child_process";
import { readFile } from "fs/promises";
import { type FetchOptions } from "make-fetch-happen"; import { type FetchOptions } from "make-fetch-happen";
import { promisify } from "util"; import * as child_process from "child_process";
import * as fs from "fs";
function ensureArray<T>(arg0: T | T[]) {
return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0];
}
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(", ")
);
}
type NPMConfig = Record<string, string | string[]>;
/**
* Get npm configuration as map
*/
async function getNmpConfig(params: { npmWorkspaceRootDirPath: string }) {
const { npmWorkspaceRootDirPath } = params;
const exec = promisify(execCallback);
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 };
return stdout
.split("\n")
.filter(line => !line.startsWith(";"))
.map(line => line.trim())
.map(line => line.split("=", 2) as [string, string])
.reduce(npmConfigReducer, {} as NPMConfig);
}
export type ProxyFetchOptions = Pick< export type ProxyFetchOptions = Pick<
FetchOptions, FetchOptions,
"proxy" | "noProxy" | "strictSSL" | "cert" | "ca" "proxy" | "noProxy" | "strictSSL" | "cert" | "ca"
>; >;
export async function getProxyFetchOptions(params: { export function getProxyFetchOptions(params: {
npmWorkspaceRootDirPath: string; npmConfigGetCwd: string;
}): Promise<ProxyFetchOptions> { }): ProxyFetchOptions {
const { npmWorkspaceRootDirPath } = params; const { npmConfigGetCwd } = params;
const cfg = await getNmpConfig({ npmWorkspaceRootDirPath }); const cfg = (() => {
const output = child_process
.execSync("npm config get", {
cwd: npmConfigGetCwd
})
.toString("utf8");
return output
.split("\n")
.filter(line => !line.startsWith(";"))
.map(line => line.trim())
.map(line => line.split("=", 2) as [string, string])
.reduce(
(
cfg: Record<string, string | string[]>,
[key, value]: [string, string]
) =>
key in cfg
? { ...cfg, [key]: [...ensureArray(cfg[key]), value] }
: { ...cfg, [key]: value },
{}
);
})();
const proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]); const proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]);
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"]; const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
@ -71,17 +50,16 @@ export async function getProxyFetchOptions(params: {
if (typeof cafile !== "undefined" && cafile !== "null") { if (typeof cafile !== "undefined" && cafile !== "null") {
ca.push( ca.push(
...(await (async () => { ...(() => {
function chunks<T>(arr: T[], size: number = 2) { const cafileContent = fs.readFileSync(cafile).toString("utf8");
return arr
.map((_, i) => i % size == 0 && arr.slice(i, i + size))
.filter(Boolean) as T[][];
}
const cafileContent = await readFile(cafile, "utf-8");
const newLinePlaceholder = "NEW_LINE_PLACEHOLDER_xIsPsK23svt"; const newLinePlaceholder = "NEW_LINE_PLACEHOLDER_xIsPsK23svt";
const chunks = <T>(arr: T[], size: number = 2) =>
arr
.map((_, i) => i % size == 0 && arr.slice(i, i + size))
.filter(Boolean) as T[][];
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map( return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(
ca => ca =>
ca ca
@ -90,7 +68,7 @@ export async function getProxyFetchOptions(params: {
.replace(new RegExp(`^${newLinePlaceholder}`), "") .replace(new RegExp(`^${newLinePlaceholder}`), "")
.replace(new RegExp(newLinePlaceholder, "g"), "\\n") .replace(new RegExp(newLinePlaceholder, "g"), "\\n")
); );
})()) })()
); );
} }
@ -102,3 +80,17 @@ export async function getProxyFetchOptions(params: {
ca: ca.length === 0 ? undefined : ca ca: ca.length === 0 ? undefined : ca
}; };
} }
function ensureArray<T>(arg0: T | T[]) {
return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0];
}
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(", ")
);
}

View File

@ -1,73 +0,0 @@
import * as child_process from "child_process";
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: {
projectDirPath: string;
dependencyExpected: string;
}) {
const { projectDirPath, dependencyExpected } = params;
const npmWorkspaceRootDirPath = (function callee(depth: number): string {
const cwd = pathResolve(
pathJoin(...[projectDirPath, ...Array(depth).fill("..")])
);
assert(cwd !== pathSep, "NPM workspace not found");
try {
child_process.execSync("npm config get", {
cwd,
stdio: "ignore"
});
} catch (error) {
if (String(error).includes("ENOWORKSPACES")) {
return callee(depth + 1);
}
throw error;
}
const packageJsonFilePath = pathJoin(cwd, "package.json");
if (!fs.existsSync(packageJsonFilePath)) {
return callee(depth + 1);
}
assert(fs.existsSync(packageJsonFilePath));
const parsedPackageJson = JSON.parse(
fs.readFileSync(packageJsonFilePath).toString("utf8")
);
let isExpectedDependencyFound = false;
for (const dependenciesOrDevDependencies of [
"dependencies",
"devDependencies"
] as const) {
const dependencies = parsedPackageJson[dependenciesOrDevDependencies];
if (dependencies === undefined) {
continue;
}
assert(dependencies instanceof Object);
if (dependencies[dependencyExpected] === undefined) {
continue;
}
isExpectedDependencyFound = true;
}
if (!isExpectedDependencyFound && parsedPackageJson.name !== dependencyExpected) {
return callee(depth + 1);
}
return cwd;
})(0);
return { npmWorkspaceRootDirPath };
}

View File

@ -1,8 +1,4 @@
import type { import type { ThemeType, LoginThemePageId } from "keycloakify/bin/shared/constants";
ThemeType,
LoginThemePageId,
nameOfTheLocalizationRealmOverridesUserProfileProperty
} from "keycloakify/bin/shared/constants";
import type { ExtractAfterStartingWith } from "keycloakify/tools/ExtractAfterStartingWith"; import type { ExtractAfterStartingWith } from "keycloakify/tools/ExtractAfterStartingWith";
import type { ValueOf } from "keycloakify/tools/ValueOf"; import type { ValueOf } from "keycloakify/tools/ValueOf";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
@ -158,7 +154,10 @@ export declare namespace KcContext {
ssoLoginInOtherTabsUrl: string; ssoLoginInOtherTabsUrl: string;
}; };
properties: {}; properties: {};
__localizationRealmOverridesUserProfile?: Record<string, string>; "x-keycloakify": {
realmMessageBundleUserProfile: Record<string, string> | undefined;
realmMessageBundleTermsText: string | undefined;
};
}; };
export type SamlPostForm = Common & { export type SamlPostForm = Common & {
@ -276,6 +275,7 @@ export declare namespace KcContext {
lastName?: string; lastName?: string;
markedForEviction?: boolean; markedForEviction?: boolean;
}; };
__localizationRealmOverridesTermsText?: string;
}; };
export type LoginDeviceVerifyUserCode = Common & { export type LoginDeviceVerifyUserCode = Common & {
@ -772,11 +772,3 @@ export type PasswordPolicies = {
/** Whether the password can be the email address */ /** Whether the password can be the email address */
notEmail?: boolean; notEmail?: boolean;
}; };
assert<
KcContext.Common extends Partial<
Record<typeof nameOfTheLocalizationRealmOverridesUserProfileProperty, unknown>
>
? true
: false
>();

View File

@ -161,7 +161,10 @@ export const kcContextCommonMock: KcContext.Common = {
scripts: [], scripts: [],
isAppInitiatedAction: false, isAppInitiatedAction: false,
properties: {}, properties: {},
__localizationRealmOverridesUserProfile: {} "x-keycloakify": {
realmMessageBundleUserProfile: undefined,
realmMessageBundleTermsText: undefined
}
}; };
const loginUrl = { const loginUrl = {

View File

@ -29,7 +29,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes }); const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
const { msg, msgStr, getChangeLocalUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n; const { msg, msgStr, getChangeLocaleUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
const { realm, locale, auth, url, message, isAppInitiatedAction, authenticationSession, scripts } = kcContext; const { realm, locale, auth, url, message, isAppInitiatedAction, authenticationSession, scripts } = kcContext;
@ -153,7 +153,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
role="menuitem" role="menuitem"
id={`language-${i + 1}`} id={`language-${i + 1}`}
className={kcClsx("kcLocaleItemClass")} className={kcClsx("kcLocaleItemClass")}
href={getChangeLocalUrl(languageTag)} href={getChangeLocaleUrl(languageTag)}
> >
{labelBySupportedLanguageTag[languageTag]} {labelBySupportedLanguageTag[languageTag]}
</a> </a>

View File

@ -1,19 +1,19 @@
import "keycloakify/tools/Object.fromEntries"; import "keycloakify/tools/Object.fromEntries";
import { useEffect, useState } from "react";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import messages_fallbackLanguage from "./baseMessages/en"; import messages_fallbackLanguage from "./baseMessages/en";
import { getMessages } from "./baseMessages"; import { getMessages } from "./baseMessages";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { Reflect } from "tsafe/Reflect"; import { fallbackLanguageTag } from "keycloakify/bin/shared/constants";
export const fallbackLanguageTag = "en";
export type KcContextLike = { export type KcContextLike = {
locale?: { locale?: {
currentLanguageTag: string; currentLanguageTag: string;
supported: { languageTag: string; url: string; label: string }[]; supported: { languageTag: string; url: string; label: string }[];
}; };
__localizationRealmOverridesUserProfile?: Record<string, string>; "x-keycloakify": {
realmMessageBundleUserProfile: Record<string, string> | undefined;
realmMessageBundleTermsText: string | undefined;
};
}; };
assert<KcContext extends KcContextLike ? true : false>(); assert<KcContext extends KcContextLike ? true : false>();
@ -31,7 +31,7 @@ export type GenericI18n<MessageKey extends string> = {
* Redirect to this url to change the language. * Redirect to this url to change the language.
* After reload currentLanguageTag === newLanguageTag * After reload currentLanguageTag === newLanguageTag
*/ */
getChangeLocalUrl: (newLanguageTag: string) => string; getChangeLocaleUrl: (newLanguageTag: string) => string;
/** /**
* e.g. "en" => "English", "fr" => "Français", ... * e.g. "en" => "English", "fr" => "Français", ...
* *
@ -89,7 +89,9 @@ export type GenericI18n<MessageKey extends string> = {
isFetchingTranslations: boolean; isFetchingTranslations: boolean;
}; };
function createGetI18n<ExtraMessageKey extends string = never>(extraMessages: { [languageTag: string]: { [key in ExtraMessageKey]: string } }) { export function createGetI18n<ExtraMessageKey extends string = never>(messageBundle: {
[languageTag: string]: { [key in ExtraMessageKey]: string };
}) {
type I18n = GenericI18n<MessageKey | ExtraMessageKey>; type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
type Result = { i18n: I18n; prI18n_currentLanguage: Promise<I18n> | undefined }; type Result = { i18n: I18n; prI18n_currentLanguage: Promise<I18n> | undefined };
@ -109,9 +111,9 @@ function createGetI18n<ExtraMessageKey extends string = never>(extraMessages: {
return cachedResult; return cachedResult;
} }
const partialI18n: Pick<I18n, "currentLanguageTag" | "getChangeLocalUrl" | "labelBySupportedLanguageTag"> = { const partialI18n: Pick<I18n, "currentLanguageTag" | "getChangeLocaleUrl" | "labelBySupportedLanguageTag"> = {
currentLanguageTag: kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag, currentLanguageTag: kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag,
getChangeLocalUrl: newLanguageTag => { getChangeLocaleUrl: newLanguageTag => {
const { locale } = kcContext; const { locale } = kcContext;
assert(locale !== undefined, "Internationalization not enabled"); assert(locale !== undefined, "Internationalization not enabled");
@ -127,9 +129,10 @@ function createGetI18n<ExtraMessageKey extends string = never>(extraMessages: {
const { createI18nTranslationFunctions } = createI18nTranslationFunctionsFactory<MessageKey, ExtraMessageKey>({ const { createI18nTranslationFunctions } = createI18nTranslationFunctionsFactory<MessageKey, ExtraMessageKey>({
messages_fallbackLanguage, messages_fallbackLanguage,
extraMessages_fallbackLanguage: extraMessages[fallbackLanguageTag], messageBundle_fallbackLanguage: messageBundle[fallbackLanguageTag],
extraMessages: extraMessages[partialI18n.currentLanguageTag], messageBundle_currentLanguage: messageBundle[partialI18n.currentLanguageTag],
__localizationRealmOverridesUserProfile: kcContext.__localizationRealmOverridesUserProfile realmMessageBundleUserProfile: kcContext["x-keycloakify"].realmMessageBundleUserProfile,
realmMessageBundleTermsText: kcContext["x-keycloakify"].realmMessageBundleTermsText
}); });
const isCurrentLanguageFallbackLanguage = partialI18n.currentLanguageTag === fallbackLanguageTag; const isCurrentLanguageFallbackLanguage = partialI18n.currentLanguageTag === fallbackLanguageTag;
@ -137,17 +140,19 @@ function createGetI18n<ExtraMessageKey extends string = never>(extraMessages: {
const result: Result = { const result: Result = {
i18n: { i18n: {
...partialI18n, ...partialI18n,
...createI18nTranslationFunctions({ messages: undefined }), ...createI18nTranslationFunctions({
messages_currentLanguage: isCurrentLanguageFallbackLanguage ? messages_fallbackLanguage : undefined
}),
isFetchingTranslations: !isCurrentLanguageFallbackLanguage isFetchingTranslations: !isCurrentLanguageFallbackLanguage
}, },
prI18n_currentLanguage: isCurrentLanguageFallbackLanguage prI18n_currentLanguage: isCurrentLanguageFallbackLanguage
? undefined ? undefined
: (async () => { : (async () => {
const messages = await getMessages(partialI18n.currentLanguageTag); const messages_currentLanguage = await getMessages(partialI18n.currentLanguageTag);
const i18n_currentLanguage: I18n = { const i18n_currentLanguage: I18n = {
...partialI18n, ...partialI18n,
...createI18nTranslationFunctions({ messages }), ...createI18nTranslationFunctions({ messages_currentLanguage }),
isFetchingTranslations: false isFetchingTranslations: false
}; };
@ -170,67 +175,40 @@ function createGetI18n<ExtraMessageKey extends string = never>(extraMessages: {
return { getI18n }; return { getI18n };
} }
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
[languageTag: string]: { [key in ExtraMessageKey]: string };
}) {
type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
const { getI18n } = createGetI18n(extraMessages);
function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } {
const { kcContext } = params;
const { i18n, prI18n_currentLanguage } = getI18n({ kcContext });
const [i18n_toReturn, setI18n_toReturn] = useState<I18n>(i18n);
useEffect(() => {
let isActive = true;
prI18n_currentLanguage?.then(i18n => {
if (!isActive) {
return;
}
setI18n_toReturn(i18n);
});
return () => {
isActive = false;
};
}, []);
return { i18n: i18n_toReturn };
}
return { useI18n, ofTypeI18n: Reflect<I18n>() };
}
function createI18nTranslationFunctionsFactory<MessageKey extends string, ExtraMessageKey extends string>(params: { function createI18nTranslationFunctionsFactory<MessageKey extends string, ExtraMessageKey extends string>(params: {
messages_fallbackLanguage: Record<MessageKey, string>; messages_fallbackLanguage: Record<MessageKey, string>;
extraMessages_fallbackLanguage: Record<ExtraMessageKey, string> | undefined; messageBundle_fallbackLanguage: Record<ExtraMessageKey, string> | undefined;
extraMessages: Partial<Record<ExtraMessageKey, string>> | undefined; messageBundle_currentLanguage: Partial<Record<ExtraMessageKey, string>> | undefined;
__localizationRealmOverridesUserProfile: Record<string, string> | undefined; realmMessageBundleUserProfile: Record<string, string> | undefined;
realmMessageBundleTermsText: string | undefined;
}) { }) {
const { __localizationRealmOverridesUserProfile, extraMessages } = params; const { messageBundle_currentLanguage, realmMessageBundleUserProfile, realmMessageBundleTermsText } = params;
const messages_fallbackLanguage = { const messages_fallbackLanguage = {
...params.messages_fallbackLanguage, ...params.messages_fallbackLanguage,
...params.extraMessages_fallbackLanguage ...params.messageBundle_fallbackLanguage
}; };
function createI18nTranslationFunctions(params: { function createI18nTranslationFunctions(params: {
messages: Partial<Record<MessageKey, string>> | undefined; messages_currentLanguage: Partial<Record<MessageKey, string>> | undefined;
}): Pick<GenericI18n<MessageKey | ExtraMessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> { }): Pick<GenericI18n<MessageKey | ExtraMessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
const messages = { const messages_currentLanguage = {
...params.messages, ...params.messages_currentLanguage,
...extraMessages ...messageBundle_currentLanguage
}; };
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): string | JSX.Element | undefined { function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): string | JSX.Element | undefined {
const { key, args, doRenderAsHtml } = props; const { key, args, doRenderAsHtml } = props;
const messageOrUndefined: string | undefined = (messages as any)[key] ?? (messages_fallbackLanguage as any)[key]; const messageOrUndefined: string | undefined = (() => {
const messageOrUndefined = (messages_currentLanguage as any)[key] ?? (messages_fallbackLanguage as any)[key];
if (key === "termsText" && realmMessageBundleTermsText !== undefined) {
return realmMessageBundleTermsText;
}
return messageOrUndefined;
})();
if (messageOrUndefined === undefined) { if (messageOrUndefined === undefined) {
return undefined; return undefined;
@ -281,8 +259,8 @@ function createI18nTranslationFunctionsFactory<MessageKey extends string, ExtraM
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): JSX.Element | string { function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): JSX.Element | string {
const { key, args, doRenderAsHtml } = props; const { key, args, doRenderAsHtml } = props;
if (__localizationRealmOverridesUserProfile !== undefined && key in __localizationRealmOverridesUserProfile) { if (realmMessageBundleUserProfile !== undefined && key in realmMessageBundleUserProfile) {
const resolvedMessage = __localizationRealmOverridesUserProfile[key]; const resolvedMessage = realmMessageBundleUserProfile[key];
return doRenderAsHtml ? ( return doRenderAsHtml ? (
<span <span

View File

@ -1,5 +1,4 @@
import type { GenericI18n, MessageKey, KcContextLike } from "./i18n"; import type { GenericI18n, MessageKey, KcContextLike } from "./i18n";
export type { MessageKey, KcContextLike }; export type { MessageKey, KcContextLike };
export type I18n = GenericI18n<MessageKey>; export type I18n = GenericI18n<MessageKey>;
export { createUseI18n } from "./i18n"; export { createUseI18n } from "./useI18n";
export { fallbackLanguageTag } from "./i18n";

44
src/login/i18n/useI18n.ts Normal file
View File

@ -0,0 +1,44 @@
import { useEffect, useState } from "react";
import {
createGetI18n,
type GenericI18n,
type MessageKey,
type KcContextLike
} from "./i18n";
import { Reflect } from "tsafe/Reflect";
export function createUseI18n<ExtraMessageKey extends string = never>(messageBundle: {
[languageTag: string]: { [key in ExtraMessageKey]: string };
}) {
type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
const { getI18n } = createGetI18n(messageBundle);
function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } {
const { kcContext } = params;
const { i18n, prI18n_currentLanguage } = getI18n({ kcContext });
const [i18n_toReturn, setI18n_toReturn] = useState<I18n>(i18n);
useEffect(() => {
let isActive = true;
prI18n_currentLanguage?.then(i18n => {
if (!isActive) {
return;
}
setI18n_toReturn(i18n);
});
return () => {
isActive = false;
};
}, []);
return { i18n: i18n_toReturn };
}
return { useI18n, ofTypeI18n: Reflect<I18n>() };
}

View File

@ -1,4 +1,3 @@
export type { ExtendKcContext, Attribute } from "keycloakify/login/KcContext"; export type { ExtendKcContext, Attribute } from "keycloakify/login/KcContext";
export type { ClassKey } from "keycloakify/login/TemplateProps"; export type { ClassKey } from "keycloakify/login/TemplateProps";
export { useDownloadTerms } from "keycloakify/login/lib/useDownloadTerms";
export { createUseI18n } from "keycloakify/login/i18n"; export { createUseI18n } from "keycloakify/login/i18n";

View File

@ -1,57 +0,0 @@
import { fallbackLanguageTag } from "keycloakify/login/i18n";
import { assert } from "tsafe/assert";
import {
createStatefulObservable,
useRerenderOnChange
} from "keycloakify/tools/StatefulObservable";
import { useOnFistMount } from "keycloakify/tools/useOnFirstMount";
import { KcContext } from "../KcContext";
const obs = createStatefulObservable<
| {
termsMarkdown: string;
termsLanguageTag: string | undefined;
}
| undefined
>(() => undefined);
export type KcContextLike = {
pageId: string;
locale?: {
currentLanguageTag: string;
};
termsAcceptanceRequired?: boolean;
};
assert<KcContext extends KcContextLike ? true : false>();
/** Allow to avoid bundling the terms and download it on demand*/
export function useDownloadTerms(params: {
kcContext: KcContextLike;
downloadTermsMarkdown: (params: {
currentLanguageTag: string;
}) => Promise<{ termsMarkdown: string; termsLanguageTag: string | undefined }>;
}) {
const { kcContext, downloadTermsMarkdown } = params;
useOnFistMount(async () => {
if (kcContext.pageId === "terms.ftl" || kcContext.termsAcceptanceRequired) {
obs.current = await downloadTermsMarkdown({
currentLanguageTag:
kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag
});
}
});
}
export function useTermsMarkdown() {
useRerenderOnChange(obs);
if (obs.current === undefined) {
return { isDownloadComplete: false as const };
}
const { termsMarkdown, termsLanguageTag } = obs.current;
return { isDownloadComplete: true, termsMarkdown, termsLanguageTag };
}

View File

@ -1,5 +1,5 @@
import { useState, useEffect, useReducer } from "react"; import { useState, useEffect, useReducer } from "react";
import { assert } from "tsafe/assert"; import { assert } from "keycloakify/tools/assert";
import { clsx } from "keycloakify/tools/clsx"; import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";

View File

@ -1,6 +1,6 @@
import { useState, useEffect, useReducer } from "react"; import { useState, useEffect, useReducer } from "react";
import { clsx } from "keycloakify/tools/clsx"; import { clsx } from "keycloakify/tools/clsx";
import { assert } from "tsafe/assert"; import { assert } from "keycloakify/tools/assert";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";

View File

@ -1,5 +1,5 @@
import { useEffect, useReducer } from "react"; import { useEffect, useReducer } from "react";
import { assert } from "tsafe/assert"; import { assert } from "keycloakify/tools/assert";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";

View File

@ -1,7 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { Markdown } from "keycloakify/tools/Markdown";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot"; import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import { useTermsMarkdown } from "keycloakify/login/lib/useDownloadTerms";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFieldsProps"; import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFieldsProps";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
@ -80,21 +78,12 @@ function TermsAcceptance(props: { i18n: I18n; kcClsx: KcClsx; messagesPerField:
const { msg } = i18n; const { msg } = i18n;
// NOTE: Refer to https://docs.keycloakify.dev/terms-and-conditions to load your terms and conditions.
const { termsMarkdown } = useTermsMarkdown();
if (termsMarkdown === undefined) {
return null;
}
return ( return (
<> <>
<div className="form-group"> <div className="form-group">
<div className={kcClsx("kcInputWrapperClass")}> <div className={kcClsx("kcInputWrapperClass")}>
{msg("termsTitle")} {msg("termsTitle")}
<div id="kc-registration-terms-text"> <div id="kc-registration-terms-text">{msg("termsText")}</div>
<Markdown>{termsMarkdown}</Markdown>
</div>
</div> </div>
</div> </div>
<div className="form-group"> <div className="form-group">

View File

@ -1,6 +1,4 @@
import { Markdown } from "keycloakify/tools/Markdown";
import { getKcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import { useTermsMarkdown } from "keycloakify/login/lib/useDownloadTerms";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
@ -15,13 +13,7 @@ export default function Terms(props: PageProps<Extract<KcContext, { pageId: "ter
const { msg, msgStr } = i18n; const { msg, msgStr } = i18n;
const { locale, url } = kcContext; const { url } = kcContext;
const { isDownloadComplete, termsMarkdown, termsLanguageTag } = useTermsMarkdown();
if (!isDownloadComplete) {
return null;
}
return ( return (
<Template <Template
@ -32,9 +24,7 @@ export default function Terms(props: PageProps<Extract<KcContext, { pageId: "ter
displayMessage={false} displayMessage={false}
headerNode={msg("termsTitle")} headerNode={msg("termsTitle")}
> >
<div id="kc-terms-text" lang={termsLanguageTag !== locale?.currentLanguageTag ? termsLanguageTag : undefined}> <div id="kc-terms-text">{msg("termsText")}</div>
<Markdown>{termsMarkdown}</Markdown>
</div>
<form className="form-actions" action={url.loginAction} method="POST"> <form className="form-actions" action={url.loginAction} method="POST">
<input <input
className={kcClsx("kcButtonClass", "kcButtonClass", "kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")} className={kcClsx("kcButtonClass", "kcButtonClass", "kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}

View File

@ -1,5 +1,5 @@
import { useEffect, Fragment } from "react"; import { useEffect, Fragment } from "react";
import { assert } from "tsafe/assert"; import { assert } from "keycloakify/tools/assert";
import { clsx } from "keycloakify/tools/clsx"; import { clsx } from "keycloakify/tools/clsx";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags"; import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { getKcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx } from "keycloakify/login/lib/kcClsx";

View File

@ -1,5 +1,5 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { assert } from "tsafe/assert"; import { assert } from "keycloakify/tools/assert";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags"; import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";

View File

@ -1,3 +0,0 @@
import Markdown from "react-markdown";
export { Markdown };

View File

@ -2,7 +2,6 @@ import React from "react";
import DefaultPage from "../../dist/login/DefaultPage"; import DefaultPage from "../../dist/login/DefaultPage";
import type { KcContext } from "./KcContext"; import type { KcContext } from "./KcContext";
import { useI18n } from "./i18n"; import { useI18n } from "./i18n";
import { useDownloadTerms } from "../../dist/login/lib/useDownloadTerms";
import Template from "../../dist/login/Template"; import Template from "../../dist/login/Template";
import UserProfileFormFields from "../../dist/login/UserProfileFormFields"; import UserProfileFormFields from "../../dist/login/UserProfileFormFields";
@ -11,31 +10,6 @@ export default function KcPage(props: { kcContext: KcContext }) {
const { i18n } = useI18n({ kcContext }); const { i18n } = useI18n({ kcContext });
useDownloadTerms({
kcContext,
downloadTermsMarkdown: async ({ currentLanguageTag }) => {
let termsLanguageTag = currentLanguageTag;
let termsFileName: string;
switch (currentLanguageTag) {
case "fr":
termsFileName = "fr.md";
break;
case "es":
termsFileName = "es.md";
break;
default:
termsFileName = "en.md";
termsLanguageTag = "en";
break;
}
const termsMarkdown = await fetch(`/terms/${termsFileName}`).then(response => response.text());
return { termsMarkdown, termsLanguageTag };
}
});
return ( return (
<DefaultPage <DefaultPage
kcContext={kcContext} kcContext={kcContext}

View File

@ -14,7 +14,15 @@ export default meta;
type Story = StoryObj<typeof meta>; type Story = StoryObj<typeof meta>;
export const Default: Story = { export const Default: Story = {
render: () => <KcPageStory /> render: () => (
<KcPageStory
kcContext={{
"x-keycloakify": {
realmMessageBundleTermsText: "<p>My terms in <strong>English</strong></p>"
}
}}
/>
)
}; };
export const French: Story = { export const French: Story = {
@ -23,18 +31,11 @@ export const French: Story = {
kcContext={{ kcContext={{
locale: { locale: {
currentLanguageTag: "fr" currentLanguageTag: "fr"
} },
}} "x-keycloakify": {
/> // cSpell: disable
) realmMessageBundleTermsText: "<p>Mes terme en <strong>Français</strong></p>"
}; // cSpell: enable
export const Spanish: Story = {
render: () => (
<KcPageStory
kcContext={{
locale: {
currentLanguageTag: "es"
} }
}} }}
/> />

View File

@ -3331,7 +3331,7 @@
"@types/retry" "*" "@types/retry" "*"
"@types/ssri" "*" "@types/ssri" "*"
"@types/mdast@^3.0.0", "@types/mdast@^3.0.3": "@types/mdast@^3.0.0":
version "3.0.11" version "3.0.11"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.11.tgz#dc130f7e7d9306124286f6d6cee40cf4d14a3dc0" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.11.tgz#dc130f7e7d9306124286f6d6cee40cf4d14a3dc0"
integrity sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw== integrity sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw==
@ -5677,7 +5677,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
dependencies: dependencies:
ms "2.0.0" ms "2.0.0"
debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
version "4.3.4" version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@ -5939,7 +5939,7 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1:
dependencies: dependencies:
domelementtype "^2.2.0" domelementtype "^2.2.0"
domhandler@^5.0, domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3: domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3:
version "5.0.3" version "5.0.3"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
@ -7494,16 +7494,6 @@ html-tags@^3.1.0:
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce"
integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==
html-to-react@^1.3.4:
version "1.5.1"
resolved "https://registry.yarnpkg.com/html-to-react/-/html-to-react-1.5.1.tgz#82ea8e5948ae15778a22888201add49e15bf8888"
integrity sha512-dFLZRBjpMk89Ukwa6Fq7oApinn3TEZD0gGFUkmI9DqNQxTjN7gF9owhyu+t8h+bpEZrX2DMxZLYjEfw0C/iL7A==
dependencies:
domhandler "^5.0"
htmlparser2 "^8.0"
lodash.camelcase "^4.3.0"
react "^18.0"
html-void-elements@^1.0.0: html-void-elements@^1.0.0:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.5.tgz#ce9159494e86d95e45795b166c2021c2cfca4483" resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.5.tgz#ce9159494e86d95e45795b166c2021c2cfca4483"
@ -7545,7 +7535,7 @@ htmlparser2@^6.1.0:
domutils "^2.5.2" domutils "^2.5.2"
entities "^2.0.0" entities "^2.0.0"
htmlparser2@^8.0, htmlparser2@^8.0.1: htmlparser2@^8.0.1:
version "8.0.2" version "8.0.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
@ -8618,11 +8608,6 @@ locate-path@^6.0.0:
dependencies: dependencies:
p-locate "^5.0.0" p-locate "^5.0.0"
lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==
lodash.debounce@^4.0.8: lodash.debounce@^4.0.8:
version "4.0.8" version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
@ -8836,13 +8821,6 @@ md5.js@^1.3.4:
inherits "^2.0.1" inherits "^2.0.1"
safe-buffer "^5.1.2" safe-buffer "^5.1.2"
mdast-add-list-metadata@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mdast-add-list-metadata/-/mdast-add-list-metadata-1.0.1.tgz#95e73640ce2fc1fa2dcb7ec443d09e2bfe7db4cf"
integrity sha512-fB/VP4MJ0LaRsog7hGPxgOrSL3gE/2uEdZyDuSEnKCv/8IkYHiDkIQSbChiJoHyxZZXZ9bzckyRk+vNxFzh8rA==
dependencies:
unist-util-visit-parents "1.1.2"
mdast-squeeze-paragraphs@^4.0.0: mdast-squeeze-paragraphs@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-4.0.0.tgz#7c4c114679c3bee27ef10b58e2e015be79f1ef97" resolved "https://registry.yarnpkg.com/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-4.0.0.tgz#7c4c114679c3bee27ef10b58e2e015be79f1ef97"
@ -8857,17 +8835,6 @@ mdast-util-definitions@^4.0.0:
dependencies: dependencies:
unist-util-visit "^2.0.0" unist-util-visit "^2.0.0"
mdast-util-from-markdown@^0.8.0:
version "0.8.5"
resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz#d1ef2ca42bc377ecb0463a987910dae89bd9a28c"
integrity sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==
dependencies:
"@types/mdast" "^3.0.0"
mdast-util-to-string "^2.0.0"
micromark "~2.11.0"
parse-entities "^2.0.0"
unist-util-stringify-position "^2.0.0"
mdast-util-to-hast@10.0.1: mdast-util-to-hast@10.0.1:
version "10.0.1" version "10.0.1"
resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.0.1.tgz#0cfc82089494c52d46eb0e3edb7a4eb2aea021eb" resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.0.1.tgz#0cfc82089494c52d46eb0e3edb7a4eb2aea021eb"
@ -8887,11 +8854,6 @@ mdast-util-to-string@^1.0.0:
resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz#27055500103f51637bd07d01da01eb1967a43527" resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz#27055500103f51637bd07d01da01eb1967a43527"
integrity sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A== integrity sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==
mdast-util-to-string@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz#b8cfe6a713e1091cb5b728fc48885a4767f8b97b"
integrity sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==
mdurl@^1.0.0: mdurl@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
@ -8995,14 +8957,6 @@ microevent.ts@~0.1.1:
resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0"
integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g== integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==
micromark@~2.11.0:
version "2.11.4"
resolved "https://registry.yarnpkg.com/micromark/-/micromark-2.11.4.tgz#d13436138eea826383e822449c9a5c50ee44665a"
integrity sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==
dependencies:
debug "^4.0.0"
parse-entities "^2.0.0"
micromatch@^3.1.10, micromatch@^3.1.4: micromatch@^3.1.10, micromatch@^3.1.4:
version "3.1.10" version "3.1.10"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
@ -10529,7 +10483,7 @@ react-is@17.0.2, react-is@^17.0.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.6: react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@ -10539,22 +10493,6 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
react-markdown@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-5.0.3.tgz#41040ea7a9324b564b328fb81dd6c04f2a5373ac"
integrity sha512-jDWOc1AvWn0WahpjW6NK64mtx6cwjM4iSsLHJPNBqoAgGOVoIdJMqaKX4++plhOtdd4JksdqzlDibgPx6B/M2w==
dependencies:
"@types/mdast" "^3.0.3"
"@types/unist" "^2.0.3"
html-to-react "^1.3.4"
mdast-add-list-metadata "1.0.1"
prop-types "^15.7.2"
react-is "^16.8.6"
remark-parse "^9.0.0"
unified "^9.0.0"
unist-util-visit "^2.0.0"
xtend "^4.0.1"
react-merge-refs@^1.0.0: react-merge-refs@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/react-merge-refs/-/react-merge-refs-1.1.0.tgz#73d88b892c6c68cbb7a66e0800faa374f4c38b06" resolved "https://registry.yarnpkg.com/react-merge-refs/-/react-merge-refs-1.1.0.tgz#73d88b892c6c68cbb7a66e0800faa374f4c38b06"
@ -10575,7 +10513,7 @@ react-sizeme@^3.0.1:
shallowequal "^1.1.0" shallowequal "^1.1.0"
throttle-debounce "^3.0.1" throttle-debounce "^3.0.1"
react@^18.0, react@^18.2.0: react@^18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
@ -10792,13 +10730,6 @@ remark-parse@8.0.3:
vfile-location "^3.0.0" vfile-location "^3.0.0"
xtend "^4.0.1" xtend "^4.0.1"
remark-parse@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-9.0.0.tgz#4d20a299665880e4f4af5d90b7c7b8a935853640"
integrity sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==
dependencies:
mdast-util-from-markdown "^0.8.0"
remark-slug@^6.0.0: remark-slug@^6.0.0:
version "6.1.0" version "6.1.0"
resolved "https://registry.yarnpkg.com/remark-slug/-/remark-slug-6.1.0.tgz#0503268d5f0c4ecb1f33315c00465ccdd97923ce" resolved "https://registry.yarnpkg.com/remark-slug/-/remark-slug-6.1.0.tgz#0503268d5f0c4ecb1f33315c00465ccdd97923ce"
@ -12334,18 +12265,6 @@ unified@9.2.0:
trough "^1.0.0" trough "^1.0.0"
vfile "^4.0.0" vfile "^4.0.0"
unified@^9.0.0:
version "9.2.2"
resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.2.tgz#67649a1abfc3ab85d2969502902775eb03146975"
integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==
dependencies:
bail "^1.0.0"
extend "^3.0.0"
is-buffer "^2.0.0"
is-plain-obj "^2.0.0"
trough "^1.0.0"
vfile "^4.0.0"
union-value@^1.0.0: union-value@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
@ -12425,11 +12344,6 @@ unist-util-stringify-position@^2.0.0:
dependencies: dependencies:
"@types/unist" "^2.0.2" "@types/unist" "^2.0.2"
unist-util-visit-parents@1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-1.1.2.tgz#f6e3afee8bdbf961c0e6f028ea3c0480028c3d06"
integrity sha512-yvo+MMLjEwdc3RhhPYSximset7rwjMrdt9E41Smmvg25UQIenzrN83cRnF1JMzoMi9zZOQeYXHSDf7p+IQkW3Q==
unist-util-visit-parents@^3.0.0: unist-util-visit-parents@^3.0.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz#65a6ce698f78a6b0f56aa0e88f13801886cdaef6" resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz#65a6ce698f78a6b0f56aa0e88f13801886cdaef6"