Compare commits

...

78 Commits

Author SHA1 Message Date
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
41739c8528 Bump version 2024-06-20 04:28:33 +02:00
89b32dc7fc Fix wrong code snippet 2024-06-20 04:28:12 +02:00
44aec23251 Release candidate 2024-06-19 22:41:42 +02:00
12fd6160c5 Fix inline CSS in html 2024-06-19 22:41:25 +02:00
8819abc418 Release candidate 2024-06-19 03:56:13 +02:00
96b627095c https://github.com/adbayb/termost/pull/31 2024-06-19 03:52:57 +02:00
dba004f924 Release candidate 2024-06-19 01:41:45 +02:00
5423a07c47 Patch CSS for Keycloak by using relative paths instead of css variables 2024-06-19 01:41:22 +02:00
aba725372e Release candidate 2024-06-18 22:41:08 +02:00
a61aa9dd5d Add missing fonts from the account theme's default assets 2024-06-18 16:41:09 +02:00
74349b20ce Adding missing font from default theme resources 2024-06-17 13:26:32 +02:00
09ab9a1c8f Fix storybook build 2024-06-17 13:03:39 +02:00
abfe5789a3 Publish new storybook 2024-06-17 12:53:06 +02:00
67ebac496d Release candidate 2024-06-17 00:07:53 +02:00
60a2bf173b Add missing base font face 2024-06-17 00:07:38 +02:00
4e03f07864 Do not includes all shared source, it's bundled already 2024-06-17 00:00:41 +02:00
aef1709d7f Release candidate 2024-06-16 18:27:37 +02:00
2f590f7be2 Add missing file in npm bundle 2024-06-16 18:27:18 +02:00
d5fa6ca89a Fix unit tests 2024-06-16 17:55:06 +02:00
8eaaffb25a Release candidate 2024-06-16 15:19:44 +02:00
28c5e2bab2 Rename use 'dist' instead of 'build' for basenameOfTheKeycloakifyResourcesDir 2024-06-16 15:19:27 +02:00
e212039f2c Release cadidate 2024-06-16 14:59:11 +02:00
99b0b67f77 Add publicDirpath option for webpack 2024-06-16 14:58:51 +02:00
6ec9ba3c01 Add version in build options 2024-06-16 14:53:18 +02:00
d7960a7dcf Release candidate 2024-06-16 14:05:38 +02:00
2a6e9af9c9 Enable to use an other directory than build/assets in webpack 2024-06-16 14:05:23 +02:00
327e4d1f90 Add doc link 2024-06-16 11:48:39 +02:00
fffadd7b9e Release candidate 2024-06-16 11:11:53 +02:00
aaaf0d2e77 Add missing declaration files 2024-06-16 11:11:35 +02:00
9f9a9b8c90 Release candidate 2024-06-16 02:30:09 +02:00
1f6edb3c0c Use the configured jar file basename if any 2024-06-16 02:19:56 +02:00
142efb4f99 Do leave artifact in the build directory when using start-keycloak 2024-06-16 01:41:47 +02:00
532655d2d5 Rename jarTargets -> keycloakVersionTargets 2024-06-16 01:34:06 +02:00
287edabd90 Enable to build only for specific keycloak version 2024-06-16 01:29:15 +02:00
7aaedbe2ce Release candidate 2024-06-15 17:40:51 +02:00
4cae1c673c Use getAlgorithmKey in account 2024-06-15 17:33:27 +02:00
8e01d836a9 Cherrypick what resource from the default theme we keep 2024-06-15 17:32:58 +02:00
f6dc8f0741 Memoize getImplementThemeTypes 2024-06-15 14:45:22 +02:00
3a976d08d2 Release candidate 2024-06-15 14:40:56 +02:00
50e83b1eb5 Only build for specific keycloak version in start-keycloak 2024-06-15 14:30:18 +02:00
61fbbb0b09 Refactor how we update META-INF and how we read what theme types are implemented 2024-06-15 14:23:35 +02:00
9e70e5c12e Suggest 'npm run' instead of 'yarn' to be more generic 2024-06-15 11:27:03 +02:00
69d9b64468 Use tsx instead of ts-node 2024-06-15 11:23:53 +02:00
0620d29880 spawn in shell in local scripts 2024-06-15 01:06:06 +02:00
b52dc74d9b Release candidate 2024-06-14 23:59:16 +02:00
a46aef2e7e Use shell for Window resolution of envs 2024-06-14 23:58:54 +02:00
736806a53d Relase candidate 2024-06-14 22:25:23 +02:00
f1475e5cdf Settle on calling the global 'kcContext' and reduce levels of indirections 2024-06-14 22:24:51 +02:00
d04724c70a fetchProxyOptions compatibility Window OS 2024-06-14 21:53:17 +02:00
bacaadc16d Remove dead file 2024-06-14 21:52:46 +02:00
67 changed files with 1773 additions and 2168 deletions

View File

@ -16,8 +16,8 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
- uses: bahmutov/npm-install@v1 - uses: bahmutov/npm-install@v1
- name: If this step fails run 'yarn format' then commit again. - name: If this step fails run 'npm run format' then commit again.
run: yarn format:check run: npm run format:check
test: test:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: test_lint needs: test_lint
@ -32,13 +32,12 @@ jobs:
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- uses: bahmutov/npm-install@v1 - uses: bahmutov/npm-install@v1
- run: yarn build - run: npm run build
- run: yarn test - run: npm run test
#- run: yarn test:keycloakify-starter
storybook: storybook:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'push' #if: github.event_name == 'push'
needs: test needs: test
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -46,11 +45,11 @@ jobs:
with: with:
node-version: '18' node-version: '18'
- uses: bahmutov/npm-install@v1 - uses: bahmutov/npm-install@v1
- run: yarn build-storybook -o ./build_storybook - run: npm run build-storybook
- run: git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${{github.repository}}.git - run: git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${{github.repository}}.git
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: npx -y -p gh-pages@3.1.0 gh-pages -d ./build_storybook -u "github-actions-bot <actions@github.com>" - run: npx -y -p gh-pages@3.1.0 gh-pages -d ./storybook-static -u "github-actions-bot <actions@github.com>"
check_if_version_upgraded: check_if_version_upgraded:
name: Check if version upgrade name: Check if version upgrade
@ -112,7 +111,7 @@ jobs:
with: with:
registry-url: https://registry.npmjs.org/ registry-url: https://registry.npmjs.org/
- uses: bahmutov/npm-install@v1 - uses: bahmutov/npm-install@v1
- run: yarn build - run: npm run build
- run: npx -y -p denoify@1.6.12 enable_short_npm_import_path - run: npx -y -p denoify@1.6.12 enable_short_npm_import_path
env: env:
DRY_RUN: "0" DRY_RUN: "0"

View File

@ -1,24 +1,24 @@
{ {
"name": "keycloakify", "name": "keycloakify",
"version": "10.0.0-rc.61", "version": "10.0.0-rc.89",
"description": "Create Keycloak themes using React", "description": "Create Keycloak themes using React",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/keycloakify/keycloakify.git" "url": "git://github.com/keycloakify/keycloakify.git"
}, },
"scripts": { "scripts": {
"prepare": "patch-package && ts-node --skipProject scripts/generate-i18n-messages.ts", "prepare": "tsx scripts/generate-i18n-messages.ts",
"build": "ts-node --skipProject scripts/build.ts", "build": "tsx scripts/build.ts",
"storybook": "ts-node --skipProject scripts/start-storybook.ts", "storybook": "tsx scripts/start-storybook.ts",
"link-in-starter": "ts-node --skipProject scripts/link-in-starter.ts", "link-in-starter": "tsx scripts/link-in-starter.ts",
"test": "yarn test:types && vitest run", "test": "yarn test:types && vitest run",
"test:types": "tsc -p test/tsconfig.json --noEmit", "test:types": "tsc -p test/tsconfig.json --noEmit",
"_format": "prettier '**/*.{ts,tsx,json,md}'", "_format": "prettier '**/*.{ts,tsx,json,md}'",
"format": "yarn _format --write", "format": "yarn _format --write",
"format:check": "yarn _format --list-different", "format:check": "yarn _format --list-different",
"link-in-app": "ts-node --skipProject scripts/link-in-app.ts", "link-in-app": "tsx scripts/link-in-app.ts",
"build-storybook": "ts-node --skipProject scripts/build-storybook.ts", "build-storybook": "tsx scripts/build-storybook.ts",
"dump-keycloak-realm": "ts-node --skipProject scripts/dump-keycloak-realm.ts" "dump-keycloak-realm": "tsx scripts/dump-keycloak-realm.ts"
}, },
"bin": { "bin": {
"keycloakify": "dist/bin/main.js" "keycloakify": "dist/bin/main.js"
@ -41,10 +41,10 @@
"!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/shared/constants.js", "dist/bin/shared/constants.js",
"dist/bin/shared/constants.d.ts", "dist/bin/shared/*.d.ts",
"dist/bin/shared/constants.js.map", "dist/bin/shared/*.js.map",
"dist/bin/shared/buildContext.d.ts",
"!dist/vite-plugin/", "!dist/vite-plugin/",
"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",
@ -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": {
@ -102,7 +98,6 @@
"lint-staged": "^11.0.0", "lint-staged": "^11.0.0",
"magic-string": "^0.30.7", "magic-string": "^0.30.7",
"make-fetch-happen": "^11.0.3", "make-fetch-happen": "^11.0.3",
"patch-package": "^8.0.0",
"powerhooks": "^1.0.10", "powerhooks": "^1.0.10",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"properties-parser": "^0.3.1", "properties-parser": "^0.3.1",
@ -111,8 +106,7 @@
"recast": "^0.23.3", "recast": "^0.23.3",
"run-exclusive": "^2.2.19", "run-exclusive": "^2.2.19",
"storybook-dark-mode": "^1.1.2", "storybook-dark-mode": "^1.1.2",
"termost": "^0.12.0", "termost": "^v0.12.1",
"ts-node": "^10.9.2",
"tsc-alias": "^1.8.10", "tsc-alias": "^1.8.10",
"tss-react": "^4.9.10", "tss-react": "^4.9.10",
"typescript": "^4.9.1-beta", "typescript": "^4.9.1-beta",
@ -120,6 +114,7 @@
"vitest": "^1.6.0", "vitest": "^1.6.0",
"yauzl": "^2.10.0", "yauzl": "^2.10.0",
"zod": "^3.17.10", "zod": "^3.17.10",
"evt": "^2.5.7" "evt": "^2.5.7",
"tsx": "^4.15.5"
} }
} }

File diff suppressed because one or more lines are too long

View File

@ -11,13 +11,17 @@ import { is } from "tsafe/is";
{ {
const dCompleted = new Deferred<void>(); const dCompleted = new Deferred<void>();
const child = child_process.spawn("docker", [ const child = child_process.spawn(
...["exec", containerName], "docker",
...["/opt/keycloak/bin/kc.sh", "export"], [
...["--dir", "/tmp"], ...["exec", containerName],
...["--realm", "myrealm"], ...["/opt/keycloak/bin/kc.sh", "export"],
...["--users", "realm_file"] ...["--dir", "/tmp"],
]); ...["--realm", "myrealm"],
...["--users", "realm_file"]
],
{ shell: true }
);
let output = ""; let output = "";

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

@ -6,8 +6,24 @@ import * as os from "os";
const singletonDependencies: string[] = ["react", "@types/react"]; const singletonDependencies: string[] = ["react", "@types/react"];
// For example [ "@emotion" ] it's more convenient than
// having to list every sub emotion packages (@emotion/css @emotion/utils ...)
// in singletonDependencies
const namespaceSingletonDependencies: string[] = [];
const rootDirPath = getThisCodebaseRootDirPath(); const rootDirPath = getThisCodebaseRootDirPath();
const commonThirdPartyDeps = [
...namespaceSingletonDependencies
.map(namespaceModuleName =>
fs
.readdirSync(pathJoin(rootDirPath, "node_modules", namespaceModuleName))
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
)
.reduce((prev, curr) => [...prev, ...curr], []),
...singletonDependencies
];
//NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58 //NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58
{ {
let modifiedPackageJsonContent = fs let modifiedPackageJsonContent = fs
@ -34,26 +50,6 @@ const rootDirPath = getThisCodebaseRootDirPath();
); );
} }
const commonThirdPartyDeps = (() => {
// For example [ "@emotion" ] it's more convenient than
// having to list every sub emotion packages (@emotion/css @emotion/utils ...)
// in singletonDependencies
const namespaceSingletonDependencies: string[] = [];
return [
...namespaceSingletonDependencies
.map(namespaceModuleName =>
fs
.readdirSync(
pathJoin(rootDirPath, "node_modules", namespaceModuleName)
)
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
)
.reduce((prev, curr) => [...prev, ...curr], []),
...singletonDependencies
];
})();
const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home"); const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home");
fs.rmSync(yarnGlobalDirPath, { recursive: true, force: true }); fs.rmSync(yarnGlobalDirPath, { recursive: true, force: true });

View File

@ -17,7 +17,7 @@ fs.rmSync(join("..", "keycloakify-starter", "node_modules"), {
run("yarn install", { cwd: join("..", "keycloakify-starter") }); run("yarn install", { cwd: join("..", "keycloakify-starter") });
run(`npx ts-node --skipProject ${join("scripts", "link-in-app.ts")} keycloakify-starter`); run(`npx tsx ${join("scripts", "link-in-app.ts")} keycloakify-starter`);
startRebuildOnSrcChange(); startRebuildOnSrcChange();

View File

@ -13,7 +13,9 @@ run(`node ${join("dist", "bin", "main.js")} copy-keycloak-resources-to-public`,
}); });
{ {
const child = child_process.spawn("npx", ["start-storybook", "-p", "6006"]); const child = child_process.spawn("npx", ["start-storybook", "-p", "6006"], {
shell: true
});
child.stdout.on("data", data => process.stdout.write(data)); child.stdout.on("data", data => process.stdout.write(data));

View File

@ -13,7 +13,7 @@ export function startRebuildOnSrcChange() {
const dCompleted = new Deferred<void>(); const dCompleted = new Deferred<void>();
const child = child_process.spawn("yarn", ["build"]); const child = child_process.spawn("yarn", ["build"], { shell: true });
child.stdout.on("data", data => process.stdout.write(data)); child.stdout.on("data", data => process.stdout.write(data));
@ -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

@ -1,7 +1,4 @@
import { import { basenameOfTheKeycloakifyResourcesDir } from "keycloakify/bin/shared/constants";
nameOfTheGlobal,
basenameOfTheKeycloakifyResourcesDir
} from "keycloakify/bin/shared/constants";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
/** /**
@ -9,7 +6,7 @@ import { assert } from "tsafe/assert";
* This works both in your main app and in your Keycloak theme. * This works both in your main app and in your Keycloak theme.
*/ */
export const PUBLIC_URL = (() => { export const PUBLIC_URL = (() => {
const kcContext = (window as any)[nameOfTheGlobal]; const kcContext = (window as any).kcContext;
if (kcContext === undefined || process.env.NODE_ENV === "development") { if (kcContext === undefined || process.env.NODE_ENV === "development") {
assert( assert(

View File

@ -166,6 +166,7 @@ export declare namespace KcContext {
algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512"; algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512";
digits: number; digits: number;
lookAheadWindow: number; lookAheadWindow: number;
getAlgorithmKey: () => string;
} & ( } & (
| { | {
type: "totp"; type: "totp";

View File

@ -148,7 +148,8 @@ export const kcContextMocks: KcContext[] = [
digits: 6, digits: 6,
lookAheadWindow: 1, lookAheadWindow: 1,
type: "totp", type: "totp",
period: 30 period: 30,
getAlgorithmKey: () => "SHA1"
} }
}, },
mode: "qr", mode: "qr",

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

@ -16,12 +16,6 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
const { msg, msgStr, advancedMsg } = i18n; const { msg, msgStr, advancedMsg } = i18n;
const algToKeyUriAlg: Record<(typeof kcContext)["totp"]["policy"]["algorithm"], string> = {
HmacSHA1: "SHA1",
HmacSHA256: "SHA256",
HmacSHA512: "SHA512"
};
return ( return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="totp"> <Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="totp">
<> <>
@ -100,7 +94,7 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
{msg("totpType")}: {msg(`totp.${totp.policy.type}`)} {msg("totpType")}: {msg(`totp.${totp.policy.type}`)}
</li> </li>
<li id="kc-totp-algorithm"> <li id="kc-totp-algorithm">
{msg("totpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm} {msg("totpAlgorithm")}: {totp.policy.getAlgorithmKey()}
</li> </li>
<li id="kc-totp-digits"> <li id="kc-totp-digits">
{msg("totpDigits")}: {totp.policy.digits} {msg("totpDigits")}: {totp.policy.digits}

View File

@ -13,7 +13,6 @@ import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase"; import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert"; import { assert, Equals } from "tsafe/assert";
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
import type { CliCommandOptions } from "./main"; import type { CliCommandOptions } from "./main";
import { getBuildContext } from "./shared/buildContext"; import { getBuildContext } from "./shared/buildContext";
import chalk from "chalk"; import chalk from "chalk";
@ -53,17 +52,13 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
console.log(`${pageId}`); console.log(`${pageId}`);
const { themeSrcDirPath } = getThemeSrcDirPath({
projectDirPath: buildContext.projectDirPath
});
const componentBasename = capitalize(kebabCaseToCamelCase(pageId)).replace( const componentBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(
/ftl$/, /ftl$/,
"stories.tsx" "stories.tsx"
); );
const targetFilePath = pathJoin( const targetFilePath = pathJoin(
themeSrcDirPath, buildContext.themeSrcDirPath,
themeType, themeType,
"pages", "pages",
componentBasename componentBasename
@ -103,7 +98,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
`${chalk.green("✓")} ${chalk.bold( `${chalk.green("✓")} ${chalk.bold(
pathJoin(".", pathRelative(process.cwd(), targetFilePath)) pathJoin(".", pathRelative(process.cwd(), targetFilePath))
)} copy pasted from the Keycloakify source code into your project`, )} copy pasted from the Keycloakify source code into your project`,
`You can start storybook with ${chalk.bold("yarn storybook")}` `You can start storybook with ${chalk.bold("npm run storybook")}`
].join("\n") ].join("\n")
); );
} }

View File

@ -15,7 +15,6 @@ import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase"; import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert"; import { assert, Equals } from "tsafe/assert";
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
import type { CliCommandOptions } from "./main"; import type { CliCommandOptions } from "./main";
import { getBuildContext } from "./shared/buildContext"; import { getBuildContext } from "./shared/buildContext";
import chalk from "chalk"; import chalk from "chalk";
@ -68,10 +67,6 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
console.log(`${pageIdOrComponent}`); console.log(`${pageIdOrComponent}`);
const { themeSrcDirPath } = getThemeSrcDirPath({
projectDirPath: buildContext.projectDirPath
});
const componentBasename = (() => { const componentBasename = (() => {
if (pageIdOrComponent === templateValue) { if (pageIdOrComponent === templateValue) {
return "Template.tsx"; return "Template.tsx";
@ -96,7 +91,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
})(); })();
const targetFilePath = pathJoin( const targetFilePath = pathJoin(
themeSrcDirPath, buildContext.themeSrcDirPath,
themeType, themeType,
pagesOrDot, pagesOrDot,
componentBasename componentBasename
@ -149,7 +144,11 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
break edit_KcApp; break edit_KcApp;
} }
const kcAppTsxPath = pathJoin(themeSrcDirPath, themeType, "KcPage.tsx"); const kcAppTsxPath = pathJoin(
buildContext.themeSrcDirPath,
themeType,
"KcPage.tsx"
);
const kcAppTsxCode = fs.readFileSync(kcAppTsxPath).toString("utf8"); const kcAppTsxCode = fs.readFileSync(kcAppTsxPath).toString("utf8");
@ -191,6 +190,8 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const userProfileFormFieldComponentName = "UserProfileFormFields"; const userProfileFormFieldComponentName = "UserProfileFormFields";
const componentName = componentBasename.replace(/.tsx$/, "");
console.log( console.log(
[ [
``, ``,
@ -199,7 +200,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
`${chalk.bold( `${chalk.bold(
pathJoin( pathJoin(
".", ".",
pathRelative(process.cwd(), themeSrcDirPath), pathRelative(process.cwd(), buildContext.themeSrcDirPath),
themeType, themeType,
"KcPage.tsx" "KcPage.tsx"
) )
@ -208,10 +209,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
`// ...`, `// ...`,
``, ``,
chalk.green( chalk.green(
`+const ${componentBasename.replace( `+const ${componentName} = lazy(() => import("./pages/${componentName}"));`
/.tsx$/,
""
)} = lazy(() => import("./pages/${componentBasename}"));`
), ),
...[ ...[
``, ``,
@ -225,7 +223,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
` switch (kcContext.pageId) {`, ` switch (kcContext.pageId) {`,
` // ...`, ` // ...`,
`+ case "${pageIdOrComponent}": return (`, `+ case "${pageIdOrComponent}": return (`,
`+ <${componentBasename}`, `+ <${componentName}`,
`+ {...{ kcContext, i18n, classes }}`, `+ {...{ kcContext, i18n, classes }}`,
`+ Template={Template}`, `+ Template={Template}`,
`+ doUseDefaultCss={true}`, `+ doUseDefaultCss={true}`,

View File

@ -4,7 +4,6 @@ import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion"; import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
import { getBuildContext } from "./shared/buildContext"; import { getBuildContext } from "./shared/buildContext";
import * as fs from "fs"; import * as fs from "fs";
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
import type { CliCommandOptions } from "./main"; import type { CliCommandOptions } from "./main";
export async function command(params: { cliCommandOptions: CliCommandOptions }) { export async function command(params: { cliCommandOptions: CliCommandOptions }) {
@ -12,11 +11,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const buildContext = getBuildContext({ cliCommandOptions }); const buildContext = getBuildContext({ cliCommandOptions });
const { themeSrcDirPath } = getThemeSrcDirPath({ const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
projectDirPath: buildContext.projectDirPath
});
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
if (fs.existsSync(emailThemeSrcDirPath)) { if (fs.existsSync(emailThemeSrcDirPath)) {
console.warn( console.warn(

View File

@ -16,7 +16,7 @@ import { readFileSync } from "fs";
import { isInside } from "../../tools/isInside"; import { isInside } from "../../tools/isInside";
import child_process from "child_process"; import child_process from "child_process";
import { rmSync } from "../../tools/fs.rmSync"; import { rmSync } from "../../tools/fs.rmSync";
import { getMetaInfKeycloakThemesJsonFilePath } from "../../shared/metaInfKeycloakThemes"; import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
export type BuildContextLike = BuildContextLike_generatePom & { export type BuildContextLike = BuildContextLike_generatePom & {
keycloakifyBuildDirPath: string; keycloakifyBuildDirPath: string;
@ -50,9 +50,16 @@ export async function buildJar(params: {
rmSync(keycloakifyBuildTmpDirPath, { recursive: true, force: true }); rmSync(keycloakifyBuildTmpDirPath, { recursive: true, force: true });
const tmpResourcesDirPath = pathJoin(
keycloakifyBuildTmpDirPath,
"src",
"main",
"resources"
);
transformCodebase({ transformCodebase({
srcDirPath: resourcesDirPath, srcDirPath: resourcesDirPath,
destDirPath: pathJoin(keycloakifyBuildTmpDirPath, "src", "main", "resources"), destDirPath: tmpResourcesDirPath,
transformSourceCode: transformSourceCode:
keycloakAccountV1Version !== null keycloakAccountV1Version !== null
? undefined ? undefined
@ -71,31 +78,6 @@ export async function buildJar(params: {
return undefined; return undefined;
} }
if (
fileRelativePath ===
getMetaInfKeycloakThemesJsonFilePath({
resourcesDirPath: "."
})
) {
const keycloakThemesJsonParsed = JSON.parse(
sourceCode.toString("utf8")
) as {
themes: { name: string; types: string[] }[];
};
keycloakThemesJsonParsed.themes =
keycloakThemesJsonParsed.themes.filter(
({ name }) => name !== accountV1ThemeName
);
return {
modifiedSourceCode: Buffer.from(
JSON.stringify(keycloakThemesJsonParsed, null, 2),
"utf8"
)
};
}
for (const themeName of buildContext.themeNames) { for (const themeName of buildContext.themeNames) {
if ( if (
fileRelativePath === fileRelativePath ===
@ -123,6 +105,21 @@ export async function buildJar(params: {
} }
}); });
if (keycloakAccountV1Version === null) {
writeMetaInfKeycloakThemes({
resourcesDirPath: tmpResourcesDirPath,
getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => {
assert(metaInfKeycloakTheme !== undefined);
metaInfKeycloakTheme.themes = metaInfKeycloakTheme.themes.filter(
({ name }) => name !== accountV1ThemeName
);
return metaInfKeycloakTheme;
}
});
}
route_legacy_pages: { route_legacy_pages: {
// NOTE: If there's no account theme there is no special target for keycloak 24 and up so we create // NOTE: If there's no account theme there is no special target for keycloak 24 and up so we create
// the pages anyway. If there is an account pages, since we know that account-v1 is only support keycloak // the pages anyway. If there is an account pages, since we know that account-v1 is only support keycloak
@ -168,8 +165,8 @@ export async function buildJar(params: {
})(); })();
const modifiedFtlFileContent = ftlFileContent.replace( const modifiedFtlFileContent = ftlFileContent.replace(
`out.pageId = "\${pageId}";`, `kcContext.pageId = "\${pageId}";`,
`out.pageId = "${pageId}"; out.realPageId = "${realPageId}";` `kcContext.pageId = "${pageId}"; kcContext.realPageId = "${realPageId}";`
); );
assert(modifiedFtlFileContent !== ftlFileContent); assert(modifiedFtlFileContent !== ftlFileContent);

View File

@ -1,5 +1,4 @@
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { exclude } from "tsafe/exclude";
import { import {
keycloakAccountV1Versions, keycloakAccountV1Versions,
keycloakThemeAdditionalInfoExtensionVersions keycloakThemeAdditionalInfoExtensionVersions
@ -7,32 +6,29 @@ import {
import { getKeycloakVersionRangeForJar } from "./getKeycloakVersionRangeForJar"; import { getKeycloakVersionRangeForJar } from "./getKeycloakVersionRangeForJar";
import { buildJar, BuildContextLike as BuildContextLike_buildJar } from "./buildJar"; import { buildJar, BuildContextLike as BuildContextLike_buildJar } from "./buildJar";
import type { BuildContext } from "../../shared/buildContext"; import type { BuildContext } from "../../shared/buildContext";
import { getJarFileBasename } from "../../shared/getJarFileBasename";
import { readMetaInfKeycloakThemes_fromResourcesDirPath } from "../../shared/metaInfKeycloakThemes";
import { accountV1ThemeName } from "../../shared/constants";
export type BuildContextLike = BuildContextLike_buildJar & { export type BuildContextLike = BuildContextLike_buildJar & {
projectDirPath: string;
keycloakifyBuildDirPath: string; keycloakifyBuildDirPath: string;
recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"];
jarTargets: BuildContext["jarTargets"];
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export async function buildJars(params: { export async function buildJars(params: {
resourcesDirPath: string; resourcesDirPath: string;
onlyBuildJarFileBasename: string | undefined;
buildContext: BuildContextLike; buildContext: BuildContextLike;
}): Promise<void> { }): Promise<void> {
const { onlyBuildJarFileBasename, resourcesDirPath, buildContext } = params; const { resourcesDirPath, buildContext } = params;
const doesImplementAccountTheme = readMetaInfKeycloakThemes_fromResourcesDirPath({ const doesImplementAccountTheme = buildContext.recordIsImplementedByThemeType.account;
resourcesDirPath
}).themes.some(({ name }) => name === accountV1ThemeName);
await Promise.all( await Promise.all(
keycloakAccountV1Versions keycloakAccountV1Versions
.map(keycloakAccountV1Version => .map(keycloakAccountV1Version =>
keycloakThemeAdditionalInfoExtensionVersions keycloakThemeAdditionalInfoExtensionVersions.map(
.map(keycloakThemeAdditionalInfoExtensionVersion => { keycloakThemeAdditionalInfoExtensionVersion => {
const keycloakVersionRange = getKeycloakVersionRangeForJar({ const keycloakVersionRange = getKeycloakVersionRangeForJar({
doesImplementAccountTheme, doesImplementAccountTheme,
keycloakAccountV1Version, keycloakAccountV1Version,
@ -43,48 +39,26 @@ export async function buildJars(params: {
return undefined; return undefined;
} }
return { const jarTarget = buildContext.jarTargets.find(
keycloakThemeAdditionalInfoExtensionVersion, jarTarget =>
keycloakVersionRange jarTarget.keycloakVersionRange === keycloakVersionRange
}; );
})
.filter(exclude(undefined))
.map(
({
keycloakThemeAdditionalInfoExtensionVersion,
keycloakVersionRange
}) => {
const { jarFileBasename } = getJarFileBasename({
keycloakVersionRange
});
if ( if (jarTarget === undefined) {
onlyBuildJarFileBasename !== undefined && return undefined;
onlyBuildJarFileBasename !== jarFileBasename
) {
return undefined;
}
return {
keycloakThemeAdditionalInfoExtensionVersion,
jarFileBasename
};
} }
)
.filter(exclude(undefined)) const { jarFileBasename } = jarTarget;
.map(
({ return buildJar({
jarFileBasename,
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion, keycloakThemeAdditionalInfoExtensionVersion,
jarFileBasename resourcesDirPath,
}) => buildContext
buildJar({ });
jarFileBasename, }
keycloakAccountV1Version, )
keycloakThemeAdditionalInfoExtensionVersion,
resourcesDirPath,
buildContext
})
)
) )
.flat() .flat()
); );

View File

@ -1,17 +1,14 @@
import cheerio from "cheerio"; import cheerio from "cheerio";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode"; import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { generateCssCodeToDefineGlobals } from "../replacers/replaceImportsInCssCode"; import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInlineCssCode";
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";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { import {
type ThemeType, type ThemeType,
nameOfTheGlobal,
basenameOfTheKeycloakifyResourcesDir, basenameOfTheKeycloakifyResourcesDir,
resources_common, resources_common
nameOfTheLocalizationRealmOverridesUserProfileProperty
} from "../../shared/constants"; } from "../../shared/constants";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath"; import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
@ -29,7 +26,6 @@ assert<BuildContext extends BuildContextLike ? true : false>();
export function generateFtlFilesCodeFactory(params: { export function generateFtlFilesCodeFactory(params: {
themeName: string; themeName: string;
indexHtmlCode: string; indexHtmlCode: string;
cssGlobalsToDefine: Record<string, string>;
buildContext: BuildContextLike; buildContext: BuildContextLike;
keycloakifyVersion: string; keycloakifyVersion: string;
themeType: ThemeType; themeType: ThemeType;
@ -37,7 +33,6 @@ export function generateFtlFilesCodeFactory(params: {
}) { }) {
const { const {
themeName, themeName,
cssGlobalsToDefine,
indexHtmlCode, indexHtmlCode,
buildContext, buildContext,
keycloakifyVersion, keycloakifyVersion,
@ -66,8 +61,9 @@ export function generateFtlFilesCodeFactory(params: {
assert(cssCode !== null); assert(cssCode !== null);
const { fixedCssCode } = replaceImportsInInlineCssCode({ const { fixedCssCode } = replaceImportsInCssCode({
cssCode, cssCode,
cssFileRelativeDirPath: undefined,
buildContext buildContext
}); });
@ -98,25 +94,10 @@ export function generateFtlFilesCodeFactory(params: {
); );
}) })
); );
if (Object.keys(cssGlobalsToDefine).length !== 0) {
$("head").prepend(
[
"",
"<style>",
generateCssCodeToDefineGlobals({
cssGlobalsToDefine,
buildContext
}).cssCodeToPrependInHead,
"</style>",
""
].join("\n")
);
}
} }
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later. //FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
const ftlObjectToJsCodeDeclaringAnObject = fs const kcContextDeclarationTemplateFtl = fs
.readFileSync( .readFileSync(
pathJoin( pathJoin(
getThisCodebaseRootDirPath(), getThisCodebaseRootDirPath(),
@ -124,11 +105,10 @@ export function generateFtlFilesCodeFactory(params: {
"bin", "bin",
"keycloakify", "keycloakify",
"generateFtl", "generateFtl",
"ftl_object_to_js_code_declaring_an_object.ftl" "kcContextDeclarationTemplate.ftl"
) )
) )
.toString("utf8") .toString("utf8")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1]
.replace( .replace(
"FIELD_NAMES_eKsIY4ZsZ4xeM", "FIELD_NAMES_eKsIY4ZsZ4xeM",
fieldNames.map(name => `"${name}"`).join(", ") fieldNames.map(name => `"${name}"`).join(", ")
@ -138,10 +118,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 ?? ""
@ -150,7 +126,7 @@ export function generateFtlFilesCodeFactory(params: {
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }'; '{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }';
$("head").prepend( $("head").prepend(
`<script>\nwindow.${nameOfTheGlobal}=${ftlObjectToJsCodeDeclaringAnObjectPlaceholder}</script>` `<script>\n${ftlObjectToJsCodeDeclaringAnObjectPlaceholder}\n</script>`
); );
// Remove part of the document marked as ignored. // Remove part of the document marked as ignored.
@ -189,7 +165,7 @@ export function generateFtlFilesCodeFactory(params: {
Object.entries({ Object.entries({
[ftlObjectToJsCodeDeclaringAnObjectPlaceholder]: [ftlObjectToJsCodeDeclaringAnObjectPlaceholder]:
ftlObjectToJsCodeDeclaringAnObject, kcContextDeclarationTemplateFtl,
PAGE_ID_xIgLsPgGId9D8e: pageId PAGE_ID_xIgLsPgGId9D8e: pageId
}).map( }).map(
([searchValue, replaceValue]) => ([searchValue, replaceValue]) =>

View File

@ -1,10 +1,8 @@
<script>const _=
(()=>{
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e"> <#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
const out = ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc}; const kcContext = ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
if( out.messagesPerField ){ if( kcContext.messagesPerField ){
var existsError_singleFieldName = out.messagesPerField.existsError; var existsError_singleFieldName = kcContext.messagesPerField.existsError;
out.messagesPerField.existsError = function (){ kcContext.messagesPerField.existsError = function (){
for( let i = 0; i < arguments.length; i++ ){ for( let i = 0; i < arguments.length; i++ ){
if( existsError_singleFieldName(arguments[i]) ){ if( existsError_singleFieldName(arguments[i]) ){
return true; return true;
@ -12,31 +10,32 @@ if( out.messagesPerField ){
} }
return false; return false;
}; };
out.messagesPerField.exists = function (fieldName) { kcContext.messagesPerField.exists = function (fieldName) {
return out.messagesPerField.get(fieldName) !== ""; return kcContext.messagesPerField.get(fieldName) !== "";
}; };
out.messagesPerField.printIfExists = function (fieldName, text) { kcContext.messagesPerField.printIfExists = function (fieldName, text) {
return out.messagesPerField.exists(fieldName) ? text : undefined; return kcContext.messagesPerField.exists(fieldName) ? text : undefined;
}; };
out.messagesPerField.getFirstError = function () { kcContext.messagesPerField.getFirstError = function () {
for( let i = 0; i < arguments.length; i++ ){ for( let i = 0; i < arguments.length; i++ ){
const fieldName = arguments[i]; const fieldName = arguments[i];
if( out.messagesPerField.existsError(fieldName) ){ if( kcContext.messagesPerField.existsError(fieldName) ){
return out.messagesPerField.get(fieldName); return kcContext.messagesPerField.get(fieldName);
} }
} }
}; };
} }
out.keycloakifyVersion = "KEYCLOAKIFY_VERSION_xEdKd3xEdr"; kcContext.keycloakifyVersion = "KEYCLOAKIFY_VERSION_xEdKd3xEdr";
out.themeVersion = "KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx"; kcContext.themeVersion = "KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx";
out.themeType = "KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr"; kcContext.themeType = "KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr";
out.themeName = "KEYCLOAKIFY_THEME_NAME_cXxKd3xEer"; kcContext.themeName = "KEYCLOAKIFY_THEME_NAME_cXxKd3xEer";
out.pageId = "${pageId}"; kcContext.pageId = "${pageId}";
if( out.url && out.url.resourcesPath ){ if( kcContext.url && kcContext.url.resourcesPath ){
out.url.resourcesCommonPath = out.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??>
out.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}"),
@ -63,22 +62,24 @@ if( out.url && out.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( !out.profile ){ if( !kcContext.profile ){
break attributes_to_attributesByName; break attributes_to_attributesByName;
} }
if( !out.profile.attributes ){ if( !kcContext.profile.attributes ){
break attributes_to_attributesByName; break attributes_to_attributesByName;
} }
var attributes = out.profile.attributes; var attributes = kcContext.profile.attributes;
delete out.profile.attributes; delete kcContext.profile.attributes;
out.profile.attributesByName = {}; kcContext.profile.attributesByName = {};
attributes.forEach(function(attribute){ attributes.forEach(function(attribute){
out.profile.attributesByName[attribute.name] = attribute; kcContext.profile.attributesByName[attribute.name] = attribute;
}); });
} }
return out; window.kcContext = kcContext;
function decodeHtmlEntities(htmlStr){ function decodeHtmlEntities(htmlStr){
var element = decodeHtmlEntities.element; var element = decodeHtmlEntities.element;
if (!element) { if (!element) {
@ -88,7 +89,7 @@ function decodeHtmlEntities(htmlStr){
element.innerHTML = htmlStr; element.innerHTML = htmlStr;
return element.value; return element.value;
} }
})();
<#function ftl_object_to_js_code_declaring_an_object object path> <#function ftl_object_to_js_code_declaring_an_object object path>
<#local isHash = ""> <#local isHash = "">
@ -201,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) &&
@ -224,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]??>
@ -308,12 +312,14 @@ function decodeHtmlEntities(htmlStr){
</#if> </#if>
<#if are_same_path(path, ["totp", "policy", "getAlgorithmKey"])> <#if are_same_path(path, ["totp", "policy", "getAlgorithmKey"])>
<#local returnValue = ""> <#local returnValue = "error">
<#attempt> <#if mode?? && mode = "manual">
<#local returnValue = totp.policy.getAlgorithmKey()> <#attempt>
<#recover> <#local returnValue = totp.policy.getAlgorithmKey()>
<#return "ABORT: Couldn't evaluate totp.policy.getAlgorithmKey()"> <#recover>
</#attempt> <#return "ABORT: Couldn't evaluate totp.policy.getAlgorithmKey()">
</#attempt>
</#if>
<#return 'function(){ return "' + returnValue + '"; }'> <#return 'function(){ return "' + returnValue + '"; }'>
</#if> </#if>
@ -341,7 +347,7 @@ function decodeHtmlEntities(htmlStr){
<#local jsFunctionCode += "if(fieldName === 'username' || fieldName === 'password' ){ "> <#local jsFunctionCode += "if(fieldName === 'username' || fieldName === 'password' ){ ">
<#if messagesPerField.exists('username') || messagesPerField.exists('password')> <#if messagesPerField.exists('username') || messagesPerField.exists('password')>
<#local jsFunctionCode += "return out.message && out.message.summary ? out.message.summary : 'error'; "> <#local jsFunctionCode += "return kcContext.message && kcContext.message.summary ? kcContext.message.summary : 'error'; ">
<#else> <#else>
<#local jsFunctionCode += "return ''; "> <#local jsFunctionCode += "return ''; ">
</#if> </#if>
@ -551,5 +557,4 @@ function decodeHtmlEntities(htmlStr){
<#function are_same_path path searchedPath> <#function are_same_path path searchedPath>
<#return path?size == searchedPath?size && is_subpath(path, searchedPath)> <#return path?size == searchedPath?size && is_subpath(path, searchedPath)>
</#function> </#function>
</script>

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

@ -1,6 +1,11 @@
import { transformCodebase } from "../../tools/transformCodebase"; import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs"; import * as fs from "fs";
import { join as pathJoin, resolve as pathResolve } from "path"; import {
join as pathJoin,
resolve as pathResolve,
relative as pathRelative,
dirname as pathDirname
} from "path";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode"; import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode"; import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import { import {
@ -16,7 +21,6 @@ import {
loginThemePageIds, loginThemePageIds,
accountThemePageIds accountThemePageIds
} from "../../shared/constants"; } from "../../shared/constants";
import { isInside } from "../../tools/isInside";
import type { BuildContext } from "../../shared/buildContext"; import type { BuildContext } from "../../shared/buildContext";
import { assert, type Equals } from "tsafe/assert"; import { assert, type Equals } from "tsafe/assert";
import { import {
@ -30,7 +34,6 @@ import {
bringInAccountV1, bringInAccountV1,
type BuildContextLike as BuildContextLike_bringInAccountV1 type BuildContextLike as BuildContextLike_bringInAccountV1
} from "./bringInAccountV1"; } from "./bringInAccountV1";
import { getThemeSrcDirPath } from "../../shared/getThemeSrcDirPath";
import { rmSync } from "../../tools/fs.rmSync"; import { rmSync } from "../../tools/fs.rmSync";
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion"; import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
import { import {
@ -48,6 +51,8 @@ export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
projectDirPath: string; projectDirPath: string;
projectBuildDirPath: string; projectBuildDirPath: string;
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"];
themeSrcDirPath: string;
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -59,30 +64,16 @@ export async function generateResourcesForMainTheme(params: {
}): Promise<void> { }): Promise<void> {
const { themeName, resourcesDirPath, buildContext } = params; const { themeName, resourcesDirPath, buildContext } = params;
const { themeSrcDirPath } = getThemeSrcDirPath({
projectDirPath: buildContext.projectDirPath
});
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => { const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
const { themeType } = params; const { themeType } = params;
return pathJoin(resourcesDirPath, "theme", themeName, themeType); return pathJoin(resourcesDirPath, "theme", themeName, themeType);
}; };
const cssGlobalsToDefine: Record<string, string> = {};
const implementedThemeTypes: Record<ThemeType | "email", boolean> = {
login: false,
account: false,
email: false
};
for (const themeType of ["login", "account"] as const) { for (const themeType of ["login", "account"] as const) {
if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) { if (!buildContext.recordIsImplementedByThemeType[themeType]) {
continue; continue;
} }
implementedThemeTypes[themeType] = true;
const themeTypeDirPath = getThemeTypeDirPath({ themeType }); const themeTypeDirPath = getThemeTypeDirPath({ themeType });
apply_replacers_and_move_to_theme_resources: { apply_replacers_and_move_to_theme_resources: {
@ -95,7 +86,10 @@ export async function generateResourcesForMainTheme(params: {
// NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up. // NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
rmSync(destDirPath, { recursive: true, force: true }); rmSync(destDirPath, { recursive: true, force: true });
if (themeType === "account" && implementedThemeTypes.login) { if (
themeType === "account" &&
buildContext.recordIsImplementedByThemeType.login
) {
// NOTE: We prevent doing it twice, it has been done for the login theme. // NOTE: We prevent doing it twice, it has been done for the login theme.
transformCodebase({ transformCodebase({
@ -112,44 +106,44 @@ export async function generateResourcesForMainTheme(params: {
break apply_replacers_and_move_to_theme_resources; break apply_replacers_and_move_to_theme_resources;
} }
{
const dirPath = pathJoin(
buildContext.projectBuildDirPath,
keycloak_resources
);
if (fs.existsSync(dirPath)) {
assert(buildContext.bundler === "webpack");
throw new Error(
[
`Keycloakify build error: The ${keycloak_resources} directory shouldn't exist in your build directory.`,
`(${pathRelative(process.cwd(), dirPath)}).\n`,
`Theses assets are only required for local development with Storybook.",
"Please remove this directory as an additional step of your command.\n`,
`For example: \`"build": "... && rimraf ${pathRelative(buildContext.projectDirPath, dirPath)}"\``
].join(" ")
);
}
}
transformCodebase({ transformCodebase({
srcDirPath: buildContext.projectBuildDirPath, srcDirPath: buildContext.projectBuildDirPath,
destDirPath, destDirPath,
transformSourceCode: ({ filePath, sourceCode }) => { transformSourceCode: ({ filePath, fileRelativePath, sourceCode }) => {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/ if (filePath.endsWith(".css")) {
// This should not happen if users follow the new instruction setup but we keep it for retrocompatibility. const { fixedCssCode } = replaceImportsInCssCode({
if ( cssCode: sourceCode.toString("utf8"),
isInside({ cssFileRelativeDirPath: pathDirname(fileRelativePath),
dirPath: pathJoin( buildContext
buildContext.projectBuildDirPath,
keycloak_resources
),
filePath
})
) {
return undefined;
}
if (/\.css?$/i.test(filePath)) {
const {
cssGlobalsToDefine: cssGlobalsToDefineForThisFile,
fixedCssCode
} = replaceImportsInCssCode({
cssCode: sourceCode.toString("utf8")
}); });
Object.entries(cssGlobalsToDefineForThisFile).forEach(
([key, value]) => {
cssGlobalsToDefine[key] = value;
}
);
return { return {
modifiedSourceCode: Buffer.from(fixedCssCode, "utf8") modifiedSourceCode: Buffer.from(fixedCssCode, "utf8")
}; };
} }
if (/\.js?$/i.test(filePath)) { if (filePath.endsWith(".js")) {
const { fixedJsCode } = replaceImportsInJsCode({ const { fixedJsCode } = replaceImportsInJsCode({
jsCode: sourceCode.toString("utf8"), jsCode: sourceCode.toString("utf8"),
buildContext buildContext
@ -170,12 +164,11 @@ export async function generateResourcesForMainTheme(params: {
indexHtmlCode: fs indexHtmlCode: fs
.readFileSync(pathJoin(buildContext.projectBuildDirPath, "index.html")) .readFileSync(pathJoin(buildContext.projectBuildDirPath, "index.html"))
.toString("utf8"), .toString("utf8"),
cssGlobalsToDefine,
buildContext, buildContext,
keycloakifyVersion: readThisNpmPackageVersion(), keycloakifyVersion: readThisNpmPackageVersion(),
themeType, themeType,
fieldNames: readFieldNameUsage({ fieldNames: readFieldNameUsage({
themeSrcDirPath, themeSrcDirPath: buildContext.themeSrcDirPath,
themeType themeType
}) })
}); });
@ -191,7 +184,7 @@ export async function generateResourcesForMainTheme(params: {
})(), })(),
...readExtraPagesNames({ ...readExtraPagesNames({
themeType, themeType,
themeSrcDirPath themeSrcDirPath: buildContext.themeSrcDirPath
}) })
].forEach(pageId => { ].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId }); const { ftlCode } = generateFtlFilesCode({ pageId });
@ -203,7 +196,7 @@ export async function generateResourcesForMainTheme(params: {
}); });
generateMessageProperties({ generateMessageProperties({
themeSrcDirPath, themeSrcDirPath: buildContext.themeSrcDirPath,
themeType themeType
}).forEach(({ languageTag, propertiesFileSource }) => { }).forEach(({ languageTag, propertiesFileSource }) => {
const messagesDirPath = pathJoin(themeTypeDirPath, "messages"); const messagesDirPath = pathJoin(themeTypeDirPath, "messages");
@ -262,13 +255,11 @@ export async function generateResourcesForMainTheme(params: {
} }
email: { email: {
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email"); if (!buildContext.recordIsImplementedByThemeType.email) {
if (!fs.existsSync(emailThemeSrcDirPath)) {
break email; break email;
} }
implementedThemeTypes.email = true; const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
transformCodebase({ transformCodebase({
srcDirPath: emailThemeSrcDirPath, srcDirPath: emailThemeSrcDirPath,
@ -276,7 +267,7 @@ export async function generateResourcesForMainTheme(params: {
}); });
} }
if (implementedThemeTypes.account) { if (buildContext.recordIsImplementedByThemeType.account) {
await bringInAccountV1({ await bringInAccountV1({
resourcesDirPath, resourcesDirPath,
buildContext buildContext
@ -288,12 +279,12 @@ export async function generateResourcesForMainTheme(params: {
metaInfKeycloakThemes.themes.push({ metaInfKeycloakThemes.themes.push({
name: themeName, name: themeName,
types: objectEntries(implementedThemeTypes) types: objectEntries(buildContext.recordIsImplementedByThemeType)
.filter(([, isImplemented]) => isImplemented) .filter(([, isImplemented]) => isImplemented)
.map(([themeType]) => themeType) .map(([themeType]) => themeType)
}); });
if (implementedThemeTypes.account) { if (buildContext.recordIsImplementedByThemeType.account) {
metaInfKeycloakThemes.themes.push({ metaInfKeycloakThemes.themes.push({
name: accountV1ThemeName, name: accountV1ThemeName,
types: ["account"] types: ["account"]
@ -302,7 +293,7 @@ export async function generateResourcesForMainTheme(params: {
writeMetaInfKeycloakThemes({ writeMetaInfKeycloakThemes({
resourcesDirPath, resourcesDirPath,
metaInfKeycloakThemes getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
}); });
} }
} }

View File

@ -1,10 +1,7 @@
import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path"; import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path";
import { transformCodebase } from "../../tools/transformCodebase"; import { transformCodebase } from "../../tools/transformCodebase";
import type { BuildContext } from "../../shared/buildContext"; import type { BuildContext } from "../../shared/buildContext";
import { import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
readMetaInfKeycloakThemes_fromResourcesDirPath,
writeMetaInfKeycloakThemes
} from "../../shared/metaInfKeycloakThemes";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
export type BuildContextLike = { export type BuildContextLike = {
@ -34,8 +31,8 @@ export function generateResourcesForThemeVariant(params: {
Buffer.from(sourceCode) Buffer.from(sourceCode)
.toString("utf-8") .toString("utf-8")
.replace( .replace(
`out["themeName"] = "${themeName}";`, `kcContext.themeName = "${themeName}";`,
`out["themeName"] = "${themeVariantName}";` `kcContext.themeName = "${themeVariantName}";`
), ),
"utf8" "utf8"
); );
@ -49,26 +46,25 @@ export function generateResourcesForThemeVariant(params: {
} }
}); });
{ writeMetaInfKeycloakThemes({
const updatedMetaInfKeycloakThemes = resourcesDirPath,
readMetaInfKeycloakThemes_fromResourcesDirPath({ getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => {
resourcesDirPath assert(metaInfKeycloakTheme !== undefined);
const newMetaInfKeycloakTheme = metaInfKeycloakTheme;
newMetaInfKeycloakTheme.themes.push({
name: themeVariantName,
types: (() => {
const theme = newMetaInfKeycloakTheme.themes.find(
({ name }) => name === themeName
);
assert(theme !== undefined);
return theme.types;
})()
}); });
updatedMetaInfKeycloakThemes.themes.push({ return newMetaInfKeycloakTheme;
name: themeVariantName, }
types: (() => { });
const theme = updatedMetaInfKeycloakThemes.themes.find(
({ name }) => name === themeName
);
assert(theme !== undefined);
return theme.types;
})()
});
writeMetaInfKeycloakThemes({
resourcesDirPath,
metaInfKeycloakThemes: updatedMetaInfKeycloakThemes
});
}
} }

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

@ -3,10 +3,7 @@ import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path
import * as child_process from "child_process"; import * as child_process from "child_process";
import * as fs from "fs"; import * as fs from "fs";
import { getBuildContext } from "../shared/buildContext"; import { getBuildContext } from "../shared/buildContext";
import { import { vitePluginSubScriptEnvNames } from "../shared/constants";
vitePluginSubScriptEnvNames,
onlyBuildJarFileBasenameEnvName
} from "../shared/constants";
import { buildJars } from "./buildJars"; import { buildJars } from "./buildJars";
import type { CliCommandOptions } from "../main"; import type { CliCommandOptions } from "../main";
import chalk from "chalk"; import chalk from "chalk";
@ -96,16 +93,17 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
cwd: buildContext.projectDirPath, cwd: buildContext.projectDirPath,
env: { env: {
...process.env, ...process.env,
[vitePluginSubScriptEnvNames.runPostBuildScript]: [vitePluginSubScriptEnvNames.runPostBuildScript]: JSON.stringify({
JSON.stringify(buildContext) resourcesDirPath,
buildContext
})
} }
}); });
} }
await buildJars({ await buildJars({
resourcesDirPath, resourcesDirPath,
buildContext, buildContext
onlyBuildJarFileBasename: process.env[onlyBuildJarFileBasenameEnvName]
}); });
rmSync(resourcesDirPath, { recursive: true }); rmSync(resourcesDirPath, { recursive: true });

View File

@ -1,7 +1,7 @@
import * as crypto from "crypto";
import type { BuildContext } from "../../shared/buildContext"; import type { BuildContext } from "../../shared/buildContext";
import { assert } from "tsafe/assert";
import { basenameOfTheKeycloakifyResourcesDir } from "../../shared/constants"; import { basenameOfTheKeycloakifyResourcesDir } from "../../shared/constants";
import { assert } from "tsafe/assert";
import { posix } from "path";
export type BuildContextLike = { export type BuildContextLike = {
urlPathname: string | undefined; urlPathname: string | undefined;
@ -9,68 +9,45 @@ export type BuildContextLike = {
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export function replaceImportsInCssCode(params: { cssCode: string }): { export function replaceImportsInCssCode(params: {
fixedCssCode: string; cssCode: string;
cssGlobalsToDefine: Record<string, string>; cssFileRelativeDirPath: string | undefined;
} {
const { cssCode } = params;
const cssGlobalsToDefine: Record<string, string> = {};
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*?/g) ?? []).forEach(
match =>
(cssGlobalsToDefine[
"url" +
crypto
.createHash("sha256")
.update(match)
.digest("hex")
.substring(0, 15)
] = match)
);
let fixedCssCode = cssCode;
Object.keys(cssGlobalsToDefine).forEach(
cssVariableName =>
//NOTE: split/join pattern ~ replace all
(fixedCssCode = fixedCssCode
.split(cssGlobalsToDefine[cssVariableName])
.join(`var(--${cssVariableName})`))
);
return { fixedCssCode, cssGlobalsToDefine };
}
export function generateCssCodeToDefineGlobals(params: {
cssGlobalsToDefine: Record<string, string>;
buildContext: BuildContextLike; buildContext: BuildContextLike;
}): { }): {
cssCodeToPrependInHead: string; fixedCssCode: string;
} { } {
const { cssGlobalsToDefine, buildContext } = params; const { cssCode, cssFileRelativeDirPath, buildContext } = params;
return { const fixedCssCode = cssCode.replace(
cssCodeToPrependInHead: [ /url\(["']?(\/[^/][^)"']+)["']?\)/g,
":root {", (match, assetFileAbsoluteUrlPathname) => {
...Object.keys(cssGlobalsToDefine) if (buildContext.urlPathname !== undefined) {
.map(cssVariableName => if (!assetFileAbsoluteUrlPathname.startsWith(buildContext.urlPathname)) {
[ // NOTE: Should never happen
`--${cssVariableName}:`, return match;
cssGlobalsToDefine[cssVariableName].replace( }
new RegExp( assetFileAbsoluteUrlPathname = assetFileAbsoluteUrlPathname.replace(
`url\\(${(buildContext.urlPathname ?? "/").replace( buildContext.urlPathname,
/\//g, "/"
"\\/" );
)}`, }
"g"
), inline_style_in_html: {
`url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/` if (cssFileRelativeDirPath !== undefined) {
) break inline_style_in_html;
].join(" ") }
)
.map(line => ` ${line};`), return `url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}${assetFileAbsoluteUrlPathname})`;
"}" }
].join("\n")
}; const assetFileRelativeUrlPathname = posix.relative(
cssFileRelativeDirPath.replace(/\\/g, "/"),
assetFileAbsoluteUrlPathname.replace(/^\//, "")
);
return `url(${assetFileRelativeUrlPathname})`;
}
);
return { fixedCssCode };
} }

View File

@ -1,28 +0,0 @@
import type { BuildContext } from "../../shared/buildContext";
import { assert } from "tsafe/assert";
import { basenameOfTheKeycloakifyResourcesDir } from "../../shared/constants";
export type BuildContextLike = {
urlPathname: string | undefined;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export function replaceImportsInInlineCssCode(params: {
cssCode: string;
buildContext: BuildContextLike;
}): {
fixedCssCode: string;
} {
const { cssCode, buildContext } = params;
const fixedCssCode = cssCode.replace(
buildContext.urlPathname === undefined
? /url\(["']?\/([^/][^)"']+)["']?\)/g
: new RegExp(`url\\(["']?${buildContext.urlPathname}([^)"']+)["']?\\)`, "g"),
(...[, group]) =>
`url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/${group})`
);
return { fixedCssCode };
}

View File

@ -1,7 +1,4 @@
import { import { basenameOfTheKeycloakifyResourcesDir } from "../../../shared/constants";
nameOfTheGlobal,
basenameOfTheKeycloakifyResourcesDir
} from "../../../shared/constants";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { BuildContext } from "../../../shared/buildContext"; import type { BuildContext } from "../../../shared/buildContext";
import * as nodePath from "path"; import * as nodePath from "path";
@ -88,13 +85,13 @@ export function replaceImportsInJsCode_vite(params: {
fixedJsCode = replaceAll( fixedJsCode = replaceAll(
fixedJsCode, fixedJsCode,
`"${relativePathOfAssetFile}"`, `"${relativePathOfAssetFile}"`,
`(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")` `(window.kcContext.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")`
); );
fixedJsCode = replaceAll( fixedJsCode = replaceAll(
fixedJsCode, fixedJsCode,
`"${buildContext.urlPathname ?? "/"}${relativePathOfAssetFile}"`, `"${buildContext.urlPathname ?? "/"}${relativePathOfAssetFile}"`,
`(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")` `(window.kcContext.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")`
); );
}); });
} }

View File

@ -1,7 +1,4 @@
import { import { basenameOfTheKeycloakifyResourcesDir } from "../../../shared/constants";
nameOfTheGlobal,
basenameOfTheKeycloakifyResourcesDir
} from "../../../shared/constants";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { BuildContext } from "../../../shared/buildContext"; import type { BuildContext } from "../../../shared/buildContext";
import * as nodePath from "path"; import * as nodePath from "path";
@ -86,7 +83,7 @@ export function replaceImportsInJsCode_webpack(params: {
var pd = Object.getOwnPropertyDescriptor(${n}, "p"); var pd = Object.getOwnPropertyDescriptor(${n}, "p");
if( pd === undefined || pd.configurable ){ if( pd === undefined || pd.configurable ){
Object.defineProperty(${n}, "p", { Object.defineProperty(${n}, "p", {
get: function() { return window.${nameOfTheGlobal}.url.resourcesPath; }, get: function() { return window.kcContext.url.resourcesPath; },
set: function() {} set: function() {}
}); });
} }
@ -107,7 +104,7 @@ export function replaceImportsInJsCode_webpack(params: {
`[a-zA-Z]+\\.[a-zA-Z]+\\+"${staticDir.replace(/\//g, "\\/")}`, `[a-zA-Z]+\\.[a-zA-Z]+\\+"${staticDir.replace(/\//g, "\\/")}`,
"g" "g"
), ),
`window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}` `window.kcContext.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}`
); );
return { fixedJsCode }; return { fixedJsCode };

View File

@ -5,9 +5,22 @@ 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";
import { assert, type Equals } from "tsafe"; import { assert, type Equals } from "tsafe/assert";
import * as child_process from "child_process"; import * as child_process from "child_process";
import { vitePluginSubScriptEnvNames } from "./constants"; import {
vitePluginSubScriptEnvNames,
buildForKeycloakMajorVersionEnvName
} from "./constants";
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
import { exclude } from "tsafe";
import { crawl } from "../tools/crawl";
import { themeTypes } from "./constants";
import { objectFromEntries } from "tsafe/objectFromEntries";
import { objectEntries } from "tsafe/objectEntries";
import { type ThemeType } from "./constants";
import { id } from "tsafe/id";
import { symToStr } from "tsafe/symToStr";
import chalk from "chalk";
export type BuildContext = { export type BuildContext = {
bundler: "vite" | "webpack"; bundler: "vite" | "webpack";
@ -30,10 +43,17 @@ export type BuildContext = {
npmWorkspaceRootDirPath: string; npmWorkspaceRootDirPath: string;
kcContextExclusionsFtlCode: string | undefined; kcContextExclusionsFtlCode: string | undefined;
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string;
recordIsImplementedByThemeType: Readonly<Record<ThemeType | "email", boolean>>;
jarTargets: {
keycloakVersionRange: KeycloakVersionRange;
jarFileBasename: string;
}[];
}; };
export type BuildOptions = { export type BuildOptions = {
themeName?: string | string[]; themeName?: string | string[];
themeVersion?: string;
environmentVariables?: { name: string; default: string }[]; environmentVariables?: { name: string; default: string }[];
extraThemeProperties?: string[]; extraThemeProperties?: string[];
artifactId?: string; artifactId?: string;
@ -41,8 +61,22 @@ export type BuildOptions = {
loginThemeResourcesFromKeycloakVersion?: string; loginThemeResourcesFromKeycloakVersion?: string;
keycloakifyBuildDirPath?: string; keycloakifyBuildDirPath?: string;
kcContextExclusionsFtl?: string; kcContextExclusionsFtl?: string;
/** https://docs.keycloakify.dev/v/v10/targetting-specific-keycloak-versions */
keycloakVersionTargets?: BuildOptions.KeycloakVersionTargets;
}; };
export namespace BuildOptions {
export type KeycloakVersionTargets =
| ({ hasAccountTheme: true } & Record<
KeycloakVersionRange.WithAccountTheme,
string | boolean
>)
| ({ hasAccountTheme: false } & Record<
KeycloakVersionRange.WithoutAccountTheme,
string | boolean
>);
}
export type ResolvedViteConfig = { export type ResolvedViteConfig = {
buildDir: string; buildDir: string;
publicDir: string; publicDir: string;
@ -102,57 +136,90 @@ export function getBuildContext(params: {
})(); })();
const parsedPackageJson = (() => { const parsedPackageJson = (() => {
type WebpackSpecificBuildOptions = { type BuildOptions_packageJson = BuildOptions & {
projectBuildDirPath?: string; projectBuildDirPath?: string;
staticDirPathInProjectBuildDirPath?: string;
publicDirPath?: string;
}; };
type ParsedPackageJson = { type ParsedPackageJson = {
name: string; name: string;
version?: string; version?: string;
homepage?: string; homepage?: string;
keycloakify?: { keycloakify?: BuildOptions_packageJson;
themeName?: string | string[];
environmentVariables?: { name: string; default: string }[];
extraThemeProperties?: string[];
artifactId?: string;
groupId?: string;
loginThemeResourcesFromKeycloakVersion?: string;
keycloakifyBuildDirPath?: string;
kcContextExclusionsFtl?: string;
projectBuildDirPath?: string;
};
}; };
{
type Got = NonNullable<ParsedPackageJson["keycloakify"]>;
type Expected = BuildOptions & WebpackSpecificBuildOptions;
assert<Equals<Got, Expected>>();
}
const zParsedPackageJson = z.object({ const zParsedPackageJson = z.object({
name: z.string(), name: z.string(),
version: z.string().optional(), version: z.string().optional(),
homepage: z.string().optional(), homepage: z.string().optional(),
keycloakify: z keycloakify: id<z.ZodType<BuildOptions_packageJson>>(
.object({ (() => {
extraThemeProperties: z.array(z.string()).optional(), const zBuildOptions_packageJson = z.object({
artifactId: z.string().optional(), extraThemeProperties: z.array(z.string()).optional(),
groupId: z.string().optional(), artifactId: z.string().optional(),
loginThemeResourcesFromKeycloakVersion: z.string().optional(), groupId: z.string().optional(),
projectBuildDirPath: z.string().optional(), loginThemeResourcesFromKeycloakVersion: z.string().optional(),
keycloakifyBuildDirPath: z.string().optional(), projectBuildDirPath: z.string().optional(),
kcContextExclusionsFtl: z.string().optional(), keycloakifyBuildDirPath: z.string().optional(),
environmentVariables: z kcContextExclusionsFtl: z.string().optional(),
.array( environmentVariables: z
z.object({ .array(
name: z.string(), z.object({
default: z.string() name: z.string(),
}) default: z.string()
) })
.optional(), )
themeName: z.union([z.string(), z.array(z.string())]).optional() .optional(),
}) themeName: z.union([z.string(), z.array(z.string())]).optional(),
.optional() themeVersion: z.string().optional(),
staticDirPathInProjectBuildDirPath: z.string().optional(),
publicDirPath: z.string().optional(),
keycloakVersionTargets: id<
z.ZodType<BuildOptions.KeycloakVersionTargets>
>(
(() => {
const zKeycloakVersionTargets = z.union([
z.object({
hasAccountTheme: z.literal(true),
"21-and-below": z.union([
z.boolean(),
z.string()
]),
"23": z.union([z.boolean(), z.string()]),
"24": z.union([z.boolean(), z.string()]),
"25-and-above": z.union([z.boolean(), z.string()])
}),
z.object({
hasAccountTheme: z.literal(false),
"21-and-below": z.union([
z.boolean(),
z.string()
]),
"22-and-above": z.union([z.boolean(), z.string()])
})
]);
{
type Got = z.infer<typeof zKeycloakVersionTargets>;
type Expected = BuildOptions.KeycloakVersionTargets;
assert<Equals<Got, Expected>>();
}
return zKeycloakVersionTargets;
})()
).optional()
});
{
type Got = z.infer<typeof zBuildOptions_packageJson>;
type Expected = BuildOptions_packageJson;
assert<Equals<Got, Expected>>();
}
return zBuildOptions_packageJson;
})()
).optional()
}); });
{ {
@ -168,11 +235,59 @@ export function getBuildContext(params: {
); );
})(); })();
const buildOptions: BuildOptions = { const buildOptions = {
...parsedPackageJson.keycloakify, ...parsedPackageJson.keycloakify,
...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(
(["login", "account", "email"] as const).map(themeType => [
themeType,
fs.existsSync(pathJoin(themeSrcDirPath, themeType))
])
);
const themeNames = ((): [string, ...string[]] => { const themeNames = ((): [string, ...string[]] => {
if (buildOptions.themeName === undefined) { if (buildOptions.themeName === undefined) {
return [ return [
@ -200,9 +315,9 @@ export function getBuildContext(params: {
break webpack; break webpack;
} }
if (parsedPackageJson.keycloakify?.projectBuildDirPath !== undefined) { if (buildOptions.projectBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({ return getAbsoluteAndInOsFormatPath({
pathIsh: parsedPackageJson.keycloakify.projectBuildDirPath, pathIsh: buildOptions.projectBuildDirPath,
cwd: projectDirPath cwd: projectDirPath
}); });
} }
@ -218,10 +333,11 @@ export function getBuildContext(params: {
dependencyExpected: "keycloakify" dependencyExpected: "keycloakify"
}); });
const bundler = resolvedViteConfig !== undefined ? "vite" : "webpack";
return { return {
bundler: resolvedViteConfig !== undefined ? "vite" : "webpack", bundler,
themeVersion: themeVersion: buildOptions.themeVersion ?? parsedPackageJson.version ?? "0.0.0",
process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0",
themeNames, themeNames,
extraThemeProperties: buildOptions.extraThemeProperties, extraThemeProperties: buildOptions.extraThemeProperties,
groupId: (() => { groupId: (() => {
@ -263,14 +379,21 @@ export function getBuildContext(params: {
); );
})(), })(),
publicDirPath: (() => { publicDirPath: (() => {
if (process.env.PUBLIC_DIR_PATH !== undefined) {
return getAbsoluteAndInOsFormatPath({
pathIsh: process.env.PUBLIC_DIR_PATH,
cwd: projectDirPath
});
}
webpack: { webpack: {
if (resolvedViteConfig !== undefined) { if (resolvedViteConfig !== undefined) {
break webpack; break webpack;
} }
if (process.env.PUBLIC_DIR_PATH !== undefined) { if (buildOptions.publicDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({ return getAbsoluteAndInOsFormatPath({
pathIsh: process.env.PUBLIC_DIR_PATH, pathIsh: buildOptions.publicDirPath,
cwd: projectDirPath cwd: projectDirPath
}); });
} }
@ -327,6 +450,13 @@ export function getBuildContext(params: {
break webpack; break webpack;
} }
if (buildOptions.staticDirPathInProjectBuildDirPath !== undefined) {
getAbsoluteAndInOsFormatPath({
pathIsh: buildOptions.staticDirPathInProjectBuildDirPath,
cwd: projectBuildDirPath
});
}
return pathJoin(projectBuildDirPath, "static"); return pathJoin(projectBuildDirPath, "static");
} }
@ -349,6 +479,290 @@ export function getBuildContext(params: {
return buildOptions.kcContextExclusionsFtl; return buildOptions.kcContextExclusionsFtl;
})(), })(),
environmentVariables: buildOptions.environmentVariables ?? [] environmentVariables: buildOptions.environmentVariables ?? [],
recordIsImplementedByThemeType,
themeSrcDirPath,
jarTargets: (() => {
const getDefaultJarFileBasename = (range: string) =>
`keycloak-theme-for-kc-${range}.jar`;
build_for_specific_keycloak_major_version: {
const buildForKeycloakMajorVersionNumber = (() => {
const envValue = process.env[buildForKeycloakMajorVersionEnvName];
if (envValue === undefined) {
return undefined;
}
const major = parseInt(envValue);
assert(!isNaN(major));
return major;
})();
if (buildForKeycloakMajorVersionNumber === undefined) {
break build_for_specific_keycloak_major_version;
}
const keycloakVersionRange: KeycloakVersionRange = (() => {
const doesImplementAccountTheme =
recordIsImplementedByThemeType.account;
if (doesImplementAccountTheme) {
const keycloakVersionRange = (() => {
if (buildForKeycloakMajorVersionNumber <= 21) {
return "21-and-below" as const;
}
assert(buildForKeycloakMajorVersionNumber !== 22);
if (buildForKeycloakMajorVersionNumber === 23) {
return "23" as const;
}
if (buildForKeycloakMajorVersionNumber === 24) {
return "24" as const;
}
return "25-and-above" as const;
})();
assert<
Equals<
typeof keycloakVersionRange,
KeycloakVersionRange.WithAccountTheme
>
>();
return keycloakVersionRange;
} else {
const keycloakVersionRange = (() => {
if (buildForKeycloakMajorVersionNumber <= 21) {
return "21-and-below" as const;
}
return "22-and-above" as const;
})();
assert<
Equals<
typeof keycloakVersionRange,
KeycloakVersionRange.WithoutAccountTheme
>
>();
return keycloakVersionRange;
}
})();
const jarFileBasename = (() => {
use_custom_jar_basename: {
const { keycloakVersionTargets } = buildOptions;
if (keycloakVersionTargets === undefined) {
break use_custom_jar_basename;
}
const entry = objectEntries(keycloakVersionTargets).find(
([keycloakVersionRange_entry]) =>
keycloakVersionRange_entry === keycloakVersionRange
);
if (entry === undefined) {
break use_custom_jar_basename;
}
const maybeJarFileBasename = entry[1];
if (typeof maybeJarFileBasename !== "string") {
break use_custom_jar_basename;
}
return maybeJarFileBasename;
}
return getDefaultJarFileBasename(keycloakVersionRange);
})();
return [
{
keycloakVersionRange,
jarFileBasename
}
];
}
const jarTargets_default = (() => {
const jarTargets: BuildContext["jarTargets"] = [];
if (recordIsImplementedByThemeType.account) {
for (const keycloakVersionRange of [
"21-and-below",
"23",
"24",
"25-and-above"
] as const) {
assert<
Equals<
typeof keycloakVersionRange,
KeycloakVersionRange.WithAccountTheme
>
>(true);
jarTargets.push({
keycloakVersionRange,
jarFileBasename:
getDefaultJarFileBasename(keycloakVersionRange)
});
}
} else {
for (const keycloakVersionRange of [
"21-and-below",
"22-and-above"
] as const) {
assert<
Equals<
typeof keycloakVersionRange,
KeycloakVersionRange.WithoutAccountTheme
>
>(true);
jarTargets.push({
keycloakVersionRange,
jarFileBasename:
getDefaultJarFileBasename(keycloakVersionRange)
});
}
}
return jarTargets;
})();
if (buildOptions.keycloakVersionTargets === undefined) {
return jarTargets_default;
}
if (
buildOptions.keycloakVersionTargets.hasAccountTheme !==
recordIsImplementedByThemeType.account
) {
console.log(
chalk.red(
(() => {
const { keycloakVersionTargets } = buildOptions;
let message = `Bad ${symToStr({ keycloakVersionTargets })} configuration.\n`;
if (keycloakVersionTargets.hasAccountTheme) {
message +=
"Your codebase does not seem to implement an account theme ";
} else {
message += "Your codebase implements an account theme ";
}
const { hasAccountTheme } = keycloakVersionTargets;
message += `but you have set ${symToStr({ keycloakVersionTargets })}.${symToStr({ hasAccountTheme })}`;
message += ` to ${hasAccountTheme} in your `;
message += (() => {
switch (bundler) {
case "vite":
return "vite.config.ts";
case "webpack":
return "package.json";
}
assert<Equals<typeof bundler, never>>(false);
})();
message += `. Please set it to ${!hasAccountTheme} `;
message +=
"and fill up the relevant keycloak version ranges.\n";
message += "Example:\n";
message += JSON.stringify(
id<Pick<BuildOptions, "keycloakVersionTargets">>({
keycloakVersionTargets: {
hasAccountTheme:
recordIsImplementedByThemeType.account,
...objectFromEntries(
jarTargets_default.map(
({
keycloakVersionRange,
jarFileBasename
}) => [
keycloakVersionRange,
jarFileBasename
]
)
)
}
}),
null,
2
);
message +=
"\nSee: https://docs.keycloakify.dev/v/v10/targetting-specific-keycloak-versions";
return message;
})()
)
);
process.exit(1);
}
const jarTargets: BuildContext["jarTargets"] = [];
const { hasAccountTheme, ...rest } = buildOptions.keycloakVersionTargets;
for (const [keycloakVersionRange, jarNameOrBoolean] of objectEntries(rest)) {
if (jarNameOrBoolean === false) {
continue;
}
if (jarNameOrBoolean === true) {
jarTargets.push({
keycloakVersionRange: keycloakVersionRange,
jarFileBasename: getDefaultJarFileBasename(keycloakVersionRange)
});
continue;
}
const jarFileBasename = jarNameOrBoolean;
if (!jarFileBasename.endsWith(".jar")) {
console.log(
chalk.red(`Bad ${jarFileBasename} should end with '.jar'\n`)
);
process.exit(1);
}
if (jarFileBasename.includes("/") || jarFileBasename.includes("\\")) {
console.log(
chalk.red(
[
`Invalid ${jarFileBasename}. It's not supposed to be a path,`,
`Only the basename of the jar file is expected.`,
`Example: keycloak-theme.jar`
].join(" ")
)
);
process.exit(1);
}
jarTargets.push({
keycloakVersionRange: keycloakVersionRange,
jarFileBasename: jarNameOrBoolean
});
}
if (jarTargets.length === 0) {
console.log(
chalk.red(
"All jar targets are disabled. Please enable at least one jar target."
)
);
process.exit(1);
}
return jarTargets;
})()
}; };
} }

View File

@ -1,10 +1,7 @@
export const nameOfTheGlobal = "kcContext";
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";
export const basenameOfTheKeycloakifyResourcesDir = "build"; export const basenameOfTheKeycloakifyResourcesDir = "dist";
export const themeTypes = ["login", "account"] as const; export const themeTypes = ["login", "account"] as const;
export const accountV1ThemeName = "account-v1"; export const accountV1ThemeName = "account-v1";
@ -16,7 +13,8 @@ export const vitePluginSubScriptEnvNames = {
resolveViteConfig: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG" resolveViteConfig: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
} as const; } as const;
export const onlyBuildJarFileBasenameEnvName = "KEYCLOAKIFY_ONLY_BUILD_JAR_FILE_BASENAME"; export const buildForKeycloakMajorVersionEnvName =
"KEYCLOAKIFY_BUILD_FOR_KEYCLOAK_MAJOR_VERSION";
export const loginThemePageIds = [ export const loginThemePageIds = [
"login.ftl", "login.ftl",
@ -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

@ -3,7 +3,6 @@ import { type BuildContext } from "./buildContext";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { lastKeycloakVersionWithAccountV1 } from "./constants"; import { lastKeycloakVersionWithAccountV1 } from "./constants";
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive"; import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
import { isInside } from "../tools/isInside";
export type BuildContextLike = { export type BuildContextLike = {
cacheDirPath: string; cacheDirPath: string;
@ -18,27 +17,25 @@ export async function downloadKeycloakDefaultTheme(params: {
}): Promise<{ defaultThemeDirPath: string }> { }): Promise<{ defaultThemeDirPath: string }> {
const { keycloakVersion, buildContext } = params; const { keycloakVersion, buildContext } = params;
let kcNodeModulesKeepFilePaths: string[] | undefined = undefined;
let kcNodeModulesKeepFilePaths_lastAccountV1: string[] | undefined = undefined;
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, npmWorkspaceRootDirPath: buildContext.npmWorkspaceRootDirPath,
uniqueIdOfOnOnArchiveFile: "downloadKeycloakDefaultTheme", uniqueIdOfOnOnArchiveFile: "downloadKeycloakDefaultTheme",
onArchiveFile: async params => { onArchiveFile: async params => {
if (!isInside({ dirPath: "theme", filePath: params.fileRelativePath })) { const fileRelativePath = pathRelative("theme", params.fileRelativePath);
if (fileRelativePath.startsWith("..")) {
return; return;
} }
const { readFile, writeFile } = params; const { readFile, writeFile } = params;
const fileRelativePath = pathRelative("theme", params.fileRelativePath);
skip_keycloak_v2: { skip_keycloak_v2: {
if ( if (!fileRelativePath.startsWith(pathJoin("keycloak.v2"))) {
!isInside({
dirPath: pathJoin("keycloak.v2"),
filePath: fileRelativePath
})
) {
break skip_keycloak_v2; break skip_keycloak_v2;
} }
@ -50,6 +47,96 @@ export async function downloadKeycloakDefaultTheme(params: {
break last_account_v1_transformations; break last_account_v1_transformations;
} }
skip_web_modules: {
if (
!fileRelativePath.startsWith(
pathJoin("keycloak", "common", "resources", "web_modules")
)
) {
break skip_web_modules;
}
return;
}
skip_lib: {
if (
!fileRelativePath.startsWith(
pathJoin("keycloak", "common", "resources", "lib")
)
) {
break skip_lib;
}
return;
}
skip_node_modules: {
if (
!fileRelativePath.startsWith(
pathJoin("keycloak", "common", "resources", "node_modules")
)
) {
break skip_node_modules;
}
if (kcNodeModulesKeepFilePaths_lastAccountV1 === undefined) {
kcNodeModulesKeepFilePaths_lastAccountV1 = [
pathJoin("patternfly", "dist", "css", "patternfly.min.css"),
pathJoin(
"patternfly",
"dist",
"css",
"patternfly-additions.min.css"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Regular-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Bold-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Light-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Semibold-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"PatternFlyIcons-webfont.ttf"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"PatternFlyIcons-webfont.woff"
)
];
}
for (const keepPath of kcNodeModulesKeepFilePaths_lastAccountV1) {
if (fileRelativePath.endsWith(keepPath)) {
break skip_node_modules;
}
}
return;
}
patch_account_css: { patch_account_css: {
if ( if (
fileRelativePath !== fileRelativePath !==
@ -70,69 +157,6 @@ export async function downloadKeycloakDefaultTheme(params: {
return; return;
} }
skip_web_modules: {
if (
!isInside({
dirPath: pathJoin(
"keycloak",
"common",
"resources",
"web_modules"
),
filePath: fileRelativePath
})
) {
break skip_web_modules;
}
return;
}
skip_unused_node_modules: {
const nodeModulesDirPath = pathJoin(
"keycloak",
"common",
"resources",
"node_modules"
);
if (
!isInside({
dirPath: nodeModulesDirPath,
filePath: fileRelativePath
})
) {
break skip_unused_node_modules;
}
const toKeepPrefixes = [
...[
"patternfly.min.css",
"patternfly-additions.min.css",
"patternfly-additions.min.css"
].map(fileBasename =>
pathJoin(
nodeModulesDirPath,
"patternfly",
"dist",
"css",
fileBasename
)
),
pathJoin(nodeModulesDirPath, "patternfly", "dist", "fonts")
];
if (
toKeepPrefixes.find(prefix =>
fileRelativePath.startsWith(prefix)
) !== undefined
) {
break skip_unused_node_modules;
}
return;
}
} }
skip_unused_resources: { skip_unused_resources: {
@ -140,61 +164,106 @@ export async function downloadKeycloakDefaultTheme(params: {
break skip_unused_resources; break skip_unused_resources;
} }
for (const dirBasename of [ skip_node_modules: {
"@patternfly-v5",
"@rollup",
"rollup",
"react",
"react-dom",
"shx",
".pnpm"
]) {
if ( if (
isInside({ !fileRelativePath.startsWith(
dirPath: pathJoin( pathJoin("keycloak", "common", "resources", "node_modules")
"keycloak", )
"common",
"resources",
"node_modules",
dirBasename
),
filePath: fileRelativePath
})
) { ) {
return; break skip_node_modules;
} }
if (kcNodeModulesKeepFilePaths === undefined) {
kcNodeModulesKeepFilePaths = [
pathJoin("@patternfly", "patternfly", "patternfly.min.css"),
pathJoin("patternfly", "dist", "css", "patternfly.min.css"),
pathJoin(
"patternfly",
"dist",
"css",
"patternfly-additions.min.css"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Regular-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Light-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Bold-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Bold-webfont.woff"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Bold-webfont.ttf"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"fontawesome-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"PatternFlyIcons-webfont.ttf"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"PatternFlyIcons-webfont.woff"
),
pathJoin("jquery", "dist", "jquery.min.js")
];
}
for (const keepPath of kcNodeModulesKeepFilePaths) {
if (fileRelativePath.endsWith(keepPath)) {
break skip_node_modules;
}
}
return;
} }
for (const dirBasename of ["react", "react-dom"]) { skip_vendor: {
if ( if (
isInside({ !fileRelativePath.startsWith(
dirPath: pathJoin( pathJoin("keycloak", "common", "resources", "vendor")
"keycloak", )
"common",
"resources",
"vendor",
dirBasename
),
filePath: fileRelativePath
})
) { ) {
return; break skip_vendor;
} }
return;
} }
if ( skip_rollup_config: {
isInside({ if (
dirPath: pathJoin( fileRelativePath !==
"keycloak", pathJoin("keycloak", "common", "resources", "rollup.config.js")
"common", ) {
"resources", break skip_rollup_config;
"node_modules", }
"@patternfly",
"react-core"
),
filePath: fileRelativePath
})
) {
return; return;
} }
} }

View File

@ -1,6 +1,5 @@
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { BuildContext } from "./buildContext"; import type { BuildContext } from "./buildContext";
import { getThemeSrcDirPath } from "./getThemeSrcDirPath";
import * as fs from "fs/promises"; import * as fs from "fs/promises";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import { existsAsync } from "../tools/fs.existsAsync"; import { existsAsync } from "../tools/fs.existsAsync";
@ -9,6 +8,7 @@ export type BuildContextLike = {
projectDirPath: string; projectDirPath: string;
themeNames: string[]; themeNames: string[];
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string;
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -18,11 +18,7 @@ export async function generateKcGenTs(params: {
}): Promise<void> { }): Promise<void> {
const { buildContext } = params; const { buildContext } = params;
const { themeSrcDirPath } = getThemeSrcDirPath({ const filePath = pathJoin(buildContext.themeSrcDirPath, "kc.gen.ts");
projectDirPath: buildContext.projectDirPath
});
const filePath = pathJoin(themeSrcDirPath, "kc.gen.ts");
const currentContent = (await existsAsync(filePath)) const currentContent = (await existsAsync(filePath))
? await fs.readFile(filePath) ? await fs.readFile(filePath)

View File

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

View File

@ -1,50 +0,0 @@
import * as fs from "fs";
import { exclude } from "tsafe";
import { crawl } from "../tools/crawl";
import { join as pathJoin } from "path";
import { themeTypes } from "./constants";
const themeSrcDirBasenames = ["keycloak-theme", "keycloak_theme"];
/** Can't catch error, if the directory isn't found, this function will just exit the process with an error message. */
export function getThemeSrcDirPath(params: { projectDirPath: string }) {
const { projectDirPath } = params;
const srcDirPath = pathJoin(projectDirPath, "src");
const themeSrcDirPath: string | undefined = crawl({
dirPath: srcDirPath,
returnedPathsType: "relative to dirPath"
})
.map(fileRelativePath => {
for (const themeSrcDirBasename of themeSrcDirBasenames) {
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.error(
[
"Can't locate your theme source directory. It should be either: ",
"src/ or src/keycloak-theme or src/keycloak_theme.",
"Example in the starter: https://github.com/keycloakify/keycloakify-starter/tree/main/src/keycloak-theme"
].join("\n")
);
process.exit(-1);
}

View File

@ -1,84 +1,40 @@
import { join as pathJoin, dirname as pathDirname } from "path"; import { join as pathJoin, dirname as pathDirname } from "path";
import type { ThemeType } from "./constants"; import type { ThemeType } from "./constants";
import * as fs from "fs"; import * as fs from "fs";
import { assert } from "tsafe/assert";
import { extractArchive } from "../tools/extractArchive";
export type MetaInfKeycloakTheme = { export type MetaInfKeycloakTheme = {
themes: { name: string; types: (ThemeType | "email")[] }[]; themes: { name: string; types: (ThemeType | "email")[] }[];
}; };
export function getMetaInfKeycloakThemesJsonFilePath(params: {
resourcesDirPath: string;
}) {
const { resourcesDirPath } = params;
return pathJoin(
resourcesDirPath === "." ? "" : resourcesDirPath,
"META-INF",
"keycloak-themes.json"
);
}
export function readMetaInfKeycloakThemes_fromResourcesDirPath(params: {
resourcesDirPath: string;
}) {
const { resourcesDirPath } = params;
return JSON.parse(
fs
.readFileSync(
getMetaInfKeycloakThemesJsonFilePath({
resourcesDirPath
})
)
.toString("utf8")
) as MetaInfKeycloakTheme;
}
export async function readMetaInfKeycloakThemes_fromJar(params: {
jarFilePath: string;
}): Promise<MetaInfKeycloakTheme> {
const { jarFilePath } = params;
let metaInfKeycloakThemes: MetaInfKeycloakTheme | undefined = undefined;
await extractArchive({
archiveFilePath: jarFilePath,
onArchiveFile: async ({ relativeFilePathInArchive, readFile, earlyExit }) => {
if (
relativeFilePathInArchive ===
getMetaInfKeycloakThemesJsonFilePath({ resourcesDirPath: "." })
) {
metaInfKeycloakThemes = JSON.parse((await readFile()).toString("utf8"));
earlyExit();
}
}
});
assert(metaInfKeycloakThemes !== undefined);
return metaInfKeycloakThemes;
}
export function writeMetaInfKeycloakThemes(params: { export function writeMetaInfKeycloakThemes(params: {
resourcesDirPath: string; resourcesDirPath: string;
metaInfKeycloakThemes: MetaInfKeycloakTheme; getNewMetaInfKeycloakTheme: (params: {
metaInfKeycloakTheme: MetaInfKeycloakTheme | undefined;
}) => MetaInfKeycloakTheme;
}) { }) {
const { resourcesDirPath, metaInfKeycloakThemes } = params; const { resourcesDirPath, getNewMetaInfKeycloakTheme } = params;
const metaInfKeycloakThemesJsonPath = getMetaInfKeycloakThemesJsonFilePath({ const filePath = pathJoin(resourcesDirPath, "META-INF", "keycloak-themes.json");
resourcesDirPath
const currentMetaInfKeycloakTheme = !fs.existsSync(filePath)
? undefined
: (JSON.parse(
fs.readFileSync(filePath).toString("utf8")
) as MetaInfKeycloakTheme);
const newMetaInfKeycloakThemes = getNewMetaInfKeycloakTheme({
metaInfKeycloakTheme: currentMetaInfKeycloakTheme
}); });
{ {
const dirPath = pathDirname(metaInfKeycloakThemesJsonPath); const dirPath = pathDirname(filePath);
if (!fs.existsSync(dirPath)) { if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true }); fs.mkdirSync(dirPath, { recursive: true });
} }
} }
fs.writeFileSync( fs.writeFileSync(
metaInfKeycloakThemesJsonPath, filePath,
Buffer.from(JSON.stringify(metaInfKeycloakThemes, null, 2), "utf8") Buffer.from(JSON.stringify(newMetaInfKeycloakThemes, null, 2), "utf8")
); );
} }

View File

@ -109,7 +109,7 @@ export async function appBuild(params: {
const dResult = new Deferred<{ isSuccess: boolean }>(); const dResult = new Deferred<{ isSuccess: boolean }>();
const child = child_process.spawn(command, args, { cwd }); 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:")) {

View File

@ -1,4 +1,4 @@
import { onlyBuildJarFileBasenameEnvName } from "../shared/constants"; import { buildForKeycloakMajorVersionEnvName } from "../shared/constants";
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";
@ -14,10 +14,10 @@ export type BuildContextLike = {
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export async function keycloakifyBuild(params: { export async function keycloakifyBuild(params: {
onlyBuildJarFileBasename: string | undefined; buildForKeycloakMajorVersionNumber: number;
buildContext: BuildContextLike; buildContext: BuildContextLike;
}): Promise<{ isKeycloakifyBuildSuccess: boolean }> { }): Promise<{ isKeycloakifyBuildSuccess: boolean }> {
const { buildContext, onlyBuildJarFileBasename } = params; const { buildForKeycloakMajorVersionNumber, buildContext } = params;
const dResult = new Deferred<{ isSuccess: boolean }>(); const dResult = new Deferred<{ isSuccess: boolean }>();
@ -25,8 +25,9 @@ export async function keycloakifyBuild(params: {
cwd: buildContext.projectDirPath, cwd: buildContext.projectDirPath,
env: { env: {
...process.env, ...process.env,
[onlyBuildJarFileBasenameEnvName]: onlyBuildJarFileBasename [buildForKeycloakMajorVersionEnvName]: `${buildForKeycloakMajorVersionNumber}`
} },
shell: true
}); });
child.stdout.on("data", data => process.stdout.write(data)); child.stdout.on("data", data => process.stdout.write(data));

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

@ -2,12 +2,9 @@ import { getBuildContext } from "../shared/buildContext";
import { exclude } from "tsafe/exclude"; import { exclude } from "tsafe/exclude";
import type { CliCommandOptions as CliCommandOptions_common } from "../main"; import type { CliCommandOptions as CliCommandOptions_common } from "../main";
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion"; import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
import { readMetaInfKeycloakThemes_fromJar } from "../shared/metaInfKeycloakThemes";
import { accountV1ThemeName, containerName } from "../shared/constants"; import { accountV1ThemeName, containerName } from "../shared/constants";
import { SemVer } from "../tools/SemVer"; import { SemVer } from "../tools/SemVer";
import type { KeycloakVersionRange } from "../shared/KeycloakVersionRange"; import { assert } from "tsafe/assert";
import { getJarFileBasename } from "../shared/getJarFileBasename";
import { assert, type Equals } from "tsafe/assert";
import * as fs from "fs"; import * as fs from "fs";
import { import {
join as pathJoin, join as pathJoin,
@ -91,6 +88,31 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const buildContext = getBuildContext({ cliCommandOptions }); const buildContext = getBuildContext({ cliCommandOptions });
const { keycloakVersion } = await (async () => {
if (cliCommandOptions.keycloakVersion !== undefined) {
return {
keycloakVersion: cliCommandOptions.keycloakVersion,
keycloakMajorNumber: SemVer.parse(cliCommandOptions.keycloakVersion).major
};
}
console.log(
chalk.cyan("On which version of Keycloak do you want to test your theme?")
);
const { keycloakVersion } = await promptKeycloakVersion({
startingFromMajor: 18,
excludeMajorVersions: [22],
cacheDirPath: buildContext.cacheDirPath
});
console.log(`${keycloakVersion}`);
return { keycloakVersion };
})();
const keycloakMajorVersionNumber = SemVer.parse(keycloakVersion).major;
{ {
const { isAppBuildSuccess } = await appBuild({ const { isAppBuildSuccess } = await appBuild({
buildContext buildContext
@ -99,141 +121,36 @@ 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 'yarn build-keycloak-theme' and see what's wrong.` `App build failed, exiting. Try running 'npm run build' and see what's wrong.`
) )
); );
process.exit(1); process.exit(1);
} }
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({ const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
onlyBuildJarFileBasename: undefined, buildForKeycloakMajorVersionNumber: keycloakMajorVersionNumber,
buildContext buildContext
}); });
if (!isKeycloakifyBuildSuccess) { if (!isKeycloakifyBuildSuccess) {
console.log( console.log(
chalk.red( chalk.red(
`Keycloakify build failed, exiting. Try running 'yarn build-keycloak-theme' and see what's wrong.` `Keycloakify build failed, exiting. Try running 'npx keycloakify build' and see what's wrong.`
) )
); );
process.exit(1); process.exit(1);
} }
} }
const { doesImplementAccountTheme } = await (async () => { const jarFilePath = fs
const latestJarFilePath = fs .readdirSync(buildContext.keycloakifyBuildDirPath)
.readdirSync(buildContext.keycloakifyBuildDirPath) .filter(fileBasename => fileBasename.endsWith(".jar"))
.filter(fileBasename => fileBasename.endsWith(".jar")) .map(fileBasename => pathJoin(buildContext.keycloakifyBuildDirPath, fileBasename))
.map(fileBasename => .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0];
pathJoin(buildContext.keycloakifyBuildDirPath, fileBasename)
)
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0];
assert(latestJarFilePath !== undefined); assert(jarFilePath !== undefined);
const metaInfKeycloakThemes = await readMetaInfKeycloakThemes_fromJar({ console.log(`Using ${chalk.bold(pathBasename(jarFilePath))}`);
jarFilePath: latestJarFilePath
});
const mainThemeEntry = metaInfKeycloakThemes.themes.find(
({ name }) => name === buildContext.themeNames[0]
);
assert(mainThemeEntry !== undefined);
const doesImplementAccountTheme = mainThemeEntry.types.includes("account");
return { doesImplementAccountTheme };
})();
const { keycloakVersion, keycloakMajorNumber: keycloakMajorVersionNumber } =
await (async function getKeycloakMajor(): Promise<{
keycloakVersion: string;
keycloakMajorNumber: number;
}> {
if (cliCommandOptions.keycloakVersion !== undefined) {
return {
keycloakVersion: cliCommandOptions.keycloakVersion,
keycloakMajorNumber: SemVer.parse(cliCommandOptions.keycloakVersion)
.major
};
}
console.log(
chalk.cyan("On which version of Keycloak do you want to test your theme?")
);
const { keycloakVersion } = await promptKeycloakVersion({
startingFromMajor: 18,
excludeMajorVersions: [22],
cacheDirPath: buildContext.cacheDirPath
});
console.log(`${keycloakVersion}`);
const keycloakMajorNumber = SemVer.parse(keycloakVersion).major;
if (doesImplementAccountTheme && keycloakMajorNumber === 22) {
console.log(
[
"Unfortunately, Keycloakify themes that implements an account theme do not work on Keycloak 22",
"Please select any other Keycloak version"
].join(" ")
);
return getKeycloakMajor();
}
return { keycloakVersion, keycloakMajorNumber };
})();
const keycloakVersionRange: KeycloakVersionRange = (() => {
if (doesImplementAccountTheme) {
const keycloakVersionRange = (() => {
if (keycloakMajorVersionNumber <= 21) {
return "21-and-below" as const;
}
assert(keycloakMajorVersionNumber !== 22);
if (keycloakMajorVersionNumber === 23) {
return "23" as const;
}
if (keycloakMajorVersionNumber === 24) {
return "24" as const;
}
return "25-and-above" as const;
})();
assert<
Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithAccountTheme>
>();
return keycloakVersionRange;
} else {
const keycloakVersionRange = (() => {
if (keycloakMajorVersionNumber <= 21) {
return "21-and-below" as const;
}
return "22-and-above" as const;
})();
assert<
Equals<
typeof keycloakVersionRange,
KeycloakVersionRange.WithoutAccountTheme
>
>();
return keycloakVersionRange;
}
})();
const { jarFileBasename } = getJarFileBasename({ keycloakVersionRange });
console.log(`Using Keycloak ${chalk.bold(jarFileBasename)}`);
const realmJsonFilePath = await (async () => { const realmJsonFilePath = await (async () => {
if (cliCommandOptions.realmJsonFilePath !== undefined) { if (cliCommandOptions.realmJsonFilePath !== undefined) {
@ -319,8 +236,6 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
return filePath; return filePath;
})(); })();
const jarFilePath = pathJoin(buildContext.keycloakifyBuildDirPath, jarFileBasename);
async function extractThemeResourcesFromJar() { async function extractThemeResourcesFromJar() {
await extractArchive({ await extractArchive({
archiveFilePath: jarFilePath, archiveFilePath: jarFilePath,
@ -346,7 +261,10 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
await extractThemeResourcesFromJar(); await extractThemeResourcesFromJar();
const jarFilePath_cacheDir = pathJoin(buildContext.cacheDirPath, jarFileBasename); const jarFilePath_cacheDir = pathJoin(
buildContext.cacheDirPath,
pathBasename(jarFilePath)
);
fs.copyFileSync(jarFilePath, jarFilePath_cacheDir); fs.copyFileSync(jarFilePath, jarFilePath_cacheDir);
@ -421,7 +339,8 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
...(realmJsonFilePath === undefined ? [] : ["--import-realm"]) ...(realmJsonFilePath === undefined ? [] : ["--import-realm"])
], ],
{ {
cwd: buildContext.keycloakifyBuildDirPath cwd: buildContext.keycloakifyBuildDirPath,
shell: true
} }
] as const; ] as const;
@ -447,6 +366,10 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
console.log( console.log(
[ [
"",
`The ftl files from ${chalk.bold(
`.${pathSep}${pathRelative(process.cwd(), pathJoin(buildContext.keycloakifyBuildDirPath, "theme"))}`
)} are mounted in the Keycloak container.`,
"", "",
`Keycloak Admin console: ${chalk.cyan.bold( `Keycloak Admin console: ${chalk.cyan.bold(
`http://localhost:${cliCommandOptions.port}` `http://localhost:${cliCommandOptions.port}`
@ -487,7 +410,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
} }
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({ const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
onlyBuildJarFileBasename: jarFileBasename, buildForKeycloakMajorVersionNumber: keycloakMajorVersionNumber,
buildContext buildContext
}); });

View File

@ -79,8 +79,16 @@ export async function getProxyFetchOptions(params: {
} }
const cafileContent = await readFile(cafile, "utf-8"); const cafileContent = await readFile(cafile, "utf-8");
const newLinePlaceholder = "NEW_LINE_PLACEHOLDER_xIsPsK23svt";
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map( return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(
ca => ca.join("").replace(/^\n/, "").replace(/\n/g, "\\n") ca =>
ca
.join("")
.replace(/\r?\n/g, newLinePlaceholder)
.replace(new RegExp(`^${newLinePlaceholder}`), "")
.replace(new RegExp(newLinePlaceholder, "g"), "\\n")
); );
})()) })())
); );

View File

@ -1,96 +0,0 @@
import { exec as execCallback } from "child_process";
import { readFile } from "fs/promises";
import { type FetchOptions } from "make-fetch-happen";
import { promisify } from "util";
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<
FetchOptions,
"proxy" | "noProxy" | "strictSSL" | "cert" | "ca"
>;
export async function getProxyFetchOptions(params: {
npmWorkspaceRootDirPath: string;
}): Promise<ProxyFetchOptions> {
const { npmWorkspaceRootDirPath } = params;
const cfg = await getNmpConfig({ npmWorkspaceRootDirPath });
const proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]);
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
function maybeBoolean(arg0: string | undefined) {
return typeof arg0 === "undefined" ? undefined : Boolean(arg0);
}
const strictSSL = maybeBoolean(ensureSingleOrNone(cfg["strict-ssl"]));
const cert = cfg["cert"];
const ca = ensureArray(cfg["ca"] ?? cfg["ca[]"]);
const cafile = ensureSingleOrNone(cfg["cafile"]);
if (typeof cafile !== "undefined" && cafile !== "null") {
ca.push(
...(await (async () => {
function chunks<T>(arr: T[], size: number = 2) {
return arr
.map((_, i) => i % size == 0 && arr.slice(i, i + size))
.filter(Boolean) as T[][];
}
const cafileContent = await readFile(cafile, "utf-8");
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(
ca => ca.join("").replace(/^\n/, "").replace(/\n/g, "\\n")
);
})())
);
}
return {
proxy,
noProxy,
strictSSL,
cert,
ca: ca.length === 0 ? undefined : ca
};
}

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

@ -1,7 +1,6 @@
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path"; import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
import type { Plugin } from "vite"; import type { Plugin } from "vite";
import { import {
nameOfTheGlobal,
basenameOfTheKeycloakifyResourcesDir, basenameOfTheKeycloakifyResourcesDir,
keycloak_resources, keycloak_resources,
vitePluginSubScriptEnvNames vitePluginSubScriptEnvNames
@ -45,11 +44,12 @@ export function keycloakify(params?: Params) {
break run_post_build_script_case; break run_post_build_script_case;
} }
const buildContext = JSON.parse(envValue) as BuildContext; const { buildContext, resourcesDirPath } = JSON.parse(envValue) as {
buildContext: BuildContext;
resourcesDirPath: string;
};
process.chdir( process.chdir(resourcesDirPath);
pathJoin(buildContext.keycloakifyBuildDirPath, "resources")
);
await postBuild?.(buildContext); await postBuild?.(buildContext);
@ -170,9 +170,9 @@ export function keycloakify(params?: Params) {
/import\.meta\.env(?:(?:\.BASE_URL)|(?:\["BASE_URL"\]))/g, /import\.meta\.env(?:(?:\.BASE_URL)|(?:\["BASE_URL"\]))/g,
[ [
`(`, `(`,
`(window.${nameOfTheGlobal} === undefined || import.meta.env.MODE === "development")?`, `(window.kcContext === undefined || import.meta.env.MODE === "development")?`,
`"${urlPathname ?? "/"}":`, `"${urlPathname ?? "/"}":`,
`(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/")`, `(window.kcContext.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/")`,
`)` `)`
].join("") ].join("")
); );

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

@ -1,16 +1,8 @@
import { replaceImportsInJsCode_vite } from "keycloakify/bin/keycloakify/replacers/replaceImportsInJsCode/vite"; import { replaceImportsInJsCode_vite } from "keycloakify/bin/keycloakify/replacers/replaceImportsInJsCode/vite";
import { replaceImportsInJsCode_webpack } from "keycloakify/bin/keycloakify/replacers/replaceImportsInJsCode/webpack"; import { replaceImportsInJsCode_webpack } from "keycloakify/bin/keycloakify/replacers/replaceImportsInJsCode/webpack";
import { import { replaceImportsInCssCode } from "keycloakify/bin/keycloakify/replacers/replaceImportsInCssCode";
generateCssCodeToDefineGlobals,
replaceImportsInCssCode
} from "keycloakify/bin/keycloakify/replacers/replaceImportsInCssCode";
import { replaceImportsInInlineCssCode } from "keycloakify/bin/keycloakify/replacers/replaceImportsInInlineCssCode";
import { same } from "evt/tools/inDepth/same";
import { expect, it, describe } from "vitest"; import { expect, it, describe } from "vitest";
import { import { basenameOfTheKeycloakifyResourcesDir } from "keycloakify/bin/shared/constants";
basenameOfTheKeycloakifyResourcesDir,
nameOfTheGlobal
} from "keycloakify/bin/shared/constants";
describe("js replacer - vite", () => { describe("js replacer - vite", () => {
it("replaceImportsInJsCode_vite - 1", () => { it("replaceImportsInJsCode_vite - 1", () => {
@ -95,13 +87,13 @@ describe("js replacer - vite", () => {
}); });
const fixedJsCodeExpected = ` const fixedJsCodeExpected = `
S=(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/assets/keycloakify-logo-mqjydaoZ.png"),H=(()=>{ S=(window.kcContext.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/assets/keycloakify-logo-mqjydaoZ.png"),H=(()=>{
function __vite__mapDeps(indexes) { function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) { if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = [ __vite__mapDeps.viteFileDeps = [
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/Login-dJpPRzM4.js"), (window.kcContext.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/Login-dJpPRzM4.js"),
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/index-XwzrZ5Gu.js") (window.kcContext.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/index-XwzrZ5Gu.js")
] ]
} }
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i]) return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
@ -154,13 +146,13 @@ describe("js replacer - vite", () => {
}); });
const fixedJsCodeExpected = ` const fixedJsCodeExpected = `
S=(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/foo/bar/keycloakify-logo-mqjydaoZ.png"),H=(()=>{ S=(window.kcContext.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/foo/bar/keycloakify-logo-mqjydaoZ.png"),H=(()=>{
function __vite__mapDeps(indexes) { function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) { if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = [ __vite__mapDeps.viteFileDeps = [
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/foo/bar/Login-dJpPRzM4.js"), (window.kcContext.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/foo/bar/Login-dJpPRzM4.js"),
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/foo/bar/index-XwzrZ5Gu.js") (window.kcContext.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/foo/bar/index-XwzrZ5Gu.js")
] ]
} }
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i]) return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
@ -213,13 +205,13 @@ describe("js replacer - vite", () => {
}); });
const fixedJsCodeExpected = ` const fixedJsCodeExpected = `
S=(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/assets/keycloakify-logo-mqjydaoZ.png"),H=(()=>{ S=(window.kcContext.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/assets/keycloakify-logo-mqjydaoZ.png"),H=(()=>{
function __vite__mapDeps(indexes) { function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) { if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = [ __vite__mapDeps.viteFileDeps = [
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/Login-dJpPRzM4.js"), (window.kcContext.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/Login-dJpPRzM4.js"),
(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/index-XwzrZ5Gu.js") (window.kcContext.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/index-XwzrZ5Gu.js")
] ]
} }
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i]) return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
@ -275,13 +267,13 @@ describe("js replacer - webpack", () => {
const fixedJsCodeExpected = ` const fixedJsCodeExpected = `
function f() { function f() {
return window.kcContext.url.resourcesPath + "/build/static/js/" + ({}[e] || e) + "." + { return window.kcContext.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0" 3: "0664cdc0"
}[e] + ".chunk.js" }[e] + ".chunk.js"
} }
function sameAsF() { function sameAsF() {
return window.kcContext.url.resourcesPath + "/build/static/js/" + ({}[e] || e) + "." + { return window.kcContext.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0" 3: "0664cdc0"
}[e] + ".chunk.js" }[e] + ".chunk.js"
} }
@ -296,7 +288,7 @@ describe("js replacer - webpack", () => {
} }
return "u"; return "u";
})()] = function(e) { })()] = function(e) {
return "/build/static/js/" + e + "." + { return "/${basenameOfTheKeycloakifyResourcesDir}/static/js/" + e + "." + {
147: "6c5cee76", 147: "6c5cee76",
787: "8da10fcf", 787: "8da10fcf",
922: "be170a73" 922: "be170a73"
@ -313,7 +305,7 @@ describe("js replacer - webpack", () => {
} }
return "miniCssF"; return "miniCssF";
})()] = function(e) { })()] = function(e) {
return "/build/static/css/" + e + "." + { return "/${basenameOfTheKeycloakifyResourcesDir}/static/css/" + e + "." + {
164:"dcfd7749", 164:"dcfd7749",
908:"67c9ed2c" 908:"67c9ed2c"
} [e] + ".chunk.css" } [e] + ".chunk.css"
@ -328,7 +320,7 @@ describe("js replacer - webpack", () => {
}); });
} }
return "u"; return "u";
})()] = e => "/build/static/js/"+e+"."+{69:"4f205f87",128:"49264537",453:"b2fed72e",482:"f0106901"}[e]+".chunk.js" })()] = e => "/${basenameOfTheKeycloakifyResourcesDir}/static/js/"+e+"."+{69:"4f205f87",128:"49264537",453:"b2fed72e",482:"f0106901"}[e]+".chunk.js"
t[(function(){ t[(function(){
var pd = Object.getOwnPropertyDescriptor(t, "p"); var pd = Object.getOwnPropertyDescriptor(t, "p");
@ -339,7 +331,7 @@ describe("js replacer - webpack", () => {
}); });
} }
return "miniCssF"; return "miniCssF";
})()] = e => "/build/static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css" })()] = e => "/${basenameOfTheKeycloakifyResourcesDir}/static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css"
`; `;
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true); expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
@ -388,279 +380,156 @@ describe("js replacer - webpack", () => {
}); });
describe("css replacer", () => { describe("css replacer", () => {
it("transforms absolute urls to css globals properly with no urlPathname", () => { it("replaceImportsInCssCode - 1", () => {
const { fixedCssCode, cssGlobalsToDefine } = replaceImportsInCssCode({ const { fixedCssCode } = replaceImportsInCssCode({
cssCode: ` cssCode: `
.my-div { .my-div {
background: url(/logo192.png) no-repeat center center; background: url(/background.png) no-repeat center center;
} }
.my-div2 { .my-div2 {
background: url(/logo192.png) repeat center center; background: url(/assets/background.png) repeat center center;
} }
.my-div { .my-div3 {
background-image: url(/static/media/something.svg); background-image: url(/assets/media/something.svg);
} }
` `,
}); cssFileRelativeDirPath: "assets/",
const fixedCssCodeExpected = `
.my-div {
background: var(--urla882a969fd39473) no-repeat center center;
}
.my-div2 {
background: var(--urla882a969fd39473) repeat center center;
}
.my-div {
background-image: var(--urldd75cab58377c19);
}
`;
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
const cssGlobalsToDefineExpected = {
urla882a969fd39473: "url(/logo192.png)",
urldd75cab58377c19: "url(/static/media/something.svg)"
};
expect(same(cssGlobalsToDefine, cssGlobalsToDefineExpected)).toBe(true);
const { cssCodeToPrependInHead } = generateCssCodeToDefineGlobals({
cssGlobalsToDefine,
buildContext: { buildContext: {
urlPathname: undefined urlPathname: undefined
} }
}); });
const cssCodeToPrependInHeadExpected = `
:root {
--urla882a969fd39473: url(\${url.resourcesPath}/build/logo192.png);
--urldd75cab58377c19: url(\${url.resourcesPath}/build/static/media/something.svg);
}
`;
expect(isSameCode(cssCodeToPrependInHead, cssCodeToPrependInHeadExpected)).toBe(
true
);
});
it("transforms absolute urls to css globals properly with custom urlPathname", () => {
const { fixedCssCode, cssGlobalsToDefine } = replaceImportsInCssCode({
cssCode: `
.my-div {
background: url(/x/y/z/logo192.png) no-repeat center center;
}
.my-div2 {
background: url(/x/y/z/logo192.png) no-repeat center center;
}
.my-div {
background-image: url(/x/y/z/static/media/something.svg);
}
`
});
const fixedCssCodeExpected = ` const fixedCssCodeExpected = `
.my-div { .my-div {
background: var(--url749a3139386b2c8) no-repeat center center; background: url(../background.png) no-repeat center center;
} }
.my-div2 { .my-div2 {
background: var(--url749a3139386b2c8) no-repeat center center; background: url(background.png) repeat center center;
} }
.my-div { .my-div3 {
background-image: var(--url8bdc0887b97ac9a); background-image: url(media/something.svg);
} }
`; `;
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true); expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
});
const cssGlobalsToDefineExpected = { it("replaceImportsInCssCode - 2", () => {
url749a3139386b2c8: "url(/x/y/z/logo192.png)", const { fixedCssCode } = replaceImportsInCssCode({
url8bdc0887b97ac9a: "url(/x/y/z/static/media/something.svg)" cssCode: `
}; .my-div {
background: url(/a/b/background.png) no-repeat center center;
expect(same(cssGlobalsToDefine, cssGlobalsToDefineExpected)).toBe(true); }
const { cssCodeToPrependInHead } = generateCssCodeToDefineGlobals({ .my-div2 {
cssGlobalsToDefine, background: url(/a/b/assets/background.png) repeat center center;
}
.my-div3 {
background-image: url(/a/b/assets/media/something.svg);
}
`,
cssFileRelativeDirPath: "assets/",
buildContext: { buildContext: {
urlPathname: "/x/y/z/" urlPathname: "/a/b/"
} }
}); });
const cssCodeToPrependInHeadExpected = ` const fixedCssCodeExpected = `
:root { .my-div {
--url749a3139386b2c8: url(\${url.resourcesPath}/build/logo192.png); background: url(../background.png) no-repeat center center;
--url8bdc0887b97ac9a: url(\${url.resourcesPath}/build/static/media/something.svg); }
.my-div2 {
background: url(background.png) repeat center center;
}
.my-div3 {
background-image: url(media/something.svg);
} }
`; `;
expect(isSameCode(cssCodeToPrependInHead, cssCodeToPrependInHeadExpected)).toBe( expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
true
);
}); });
});
describe("inline css replacer", () => { it("replaceImportsInCssCode - 3", () => {
describe("no url pathName", () => { const { fixedCssCode } = replaceImportsInCssCode({
const cssCode = ` cssCode: `
@font-face { .my-div {
font-family: "Work Sans"; background: url(/a/b/background.png) no-repeat center center;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/fonts/WorkSans/worksans-regular-webfont.woff2") format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("/fonts/WorkSans/worksans-medium-webfont.woff2") format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/fonts/WorkSans/worksans-semibold-webfont.woff2") format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("/fonts/WorkSans/worksans-bold-webfont.woff2") format("woff2");
}
`;
it("transforms css for standalone app properly", () => {
const { fixedCssCode } = replaceImportsInInlineCssCode({
cssCode,
buildContext: {
urlPathname: undefined
} }
});
.my-div2 {
background: url(/a/b/assets/background.png) repeat center center;
}
.my-div3 {
background-image: url(/a/b/assets/media/something.svg);
}
`,
cssFileRelativeDirPath: undefined,
buildContext: {
urlPathname: "/a/b/"
}
});
const fixedCssCodeExpected = ` const fixedCssCodeExpected = `
@font-face { .my-div {
font-family: "Work Sans"; background: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/background.png) no-repeat center center;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(\${url.resourcesPath}/build/fonts/WorkSans/worksans-regular-webfont.woff2)
format("woff2");
} }
@font-face {
font-family: "Work Sans"; .my-div2 {
font-style: normal; background: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/assets/background.png) repeat center center;
font-weight: 500;
font-display: swap;
src: url(\${url.resourcesPath}/build/fonts/WorkSans/worksans-medium-webfont.woff2)
format("woff2");
} }
@font-face {
font-family: "Work Sans"; .my-div3 {
font-style: normal; background-image: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/assets/media/something.svg);
font-weight: 600;
font-display: swap;
src: url(\${url.resourcesPath}/build/fonts/WorkSans/worksans-semibold-webfont.woff2)
format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(\${url.resourcesPath}/build/fonts/WorkSans/worksans-bold-webfont.woff2)
format("woff2");
} }
`; `;
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true); expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
});
}); });
describe("with url pathName", () => { it("replaceImportsInCssCode - 4", () => {
const cssCode = ` const { fixedCssCode } = replaceImportsInCssCode({
@font-face { cssCode: `
font-family: "Work Sans"; .my-div {
font-style: normal; background: url(/background.png) no-repeat center center;
font-weight: 400;
font-display: swap;
src: url("/x/y/z/fonts/WorkSans/worksans-regular-webfont.woff2") format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("/x/y/z/fonts/WorkSans/worksans-medium-webfont.woff2") format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/x/y/z/fonts/WorkSans/worksans-semibold-webfont.woff2") format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("/x/y/z/fonts/WorkSans/worksans-bold-webfont.woff2") format("woff2");
}
`;
it("transforms css for standalone app properly", () => {
const { fixedCssCode } = replaceImportsInInlineCssCode({
cssCode,
buildContext: {
urlPathname: "/x/y/z/"
} }
});
.my-div2 {
const fixedCssCodeExpected = ` background: url(/assets/background.png) repeat center center;
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(\${url.resourcesPath}/build/fonts/WorkSans/worksans-regular-webfont.woff2)
format("woff2");
} }
@font-face {
font-family: "Work Sans"; .my-div3 {
font-style: normal; background-image: url(/assets/media/something.svg);
font-weight: 500;
font-display: swap;
src: url(\${url.resourcesPath}/build/fonts/WorkSans/worksans-medium-webfont.woff2)
format("woff2");
} }
@font-face { `,
font-family: "Work Sans"; cssFileRelativeDirPath: undefined,
font-style: normal; buildContext: {
font-weight: 600; urlPathname: undefined
font-display: swap; }
src: url(\${url.resourcesPath}/build/fonts/WorkSans/worksans-semibold-webfont.woff2)
format("woff2");
}
@font-face {
font-family: "Work Sans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(\${url.resourcesPath}/build/fonts/WorkSans/worksans-bold-webfont.woff2)
format("woff2");
}
`;
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
}); });
const fixedCssCodeExpected = `
.my-div {
background: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/background.png) no-repeat center center;
}
.my-div2 {
background: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/assets/background.png) repeat center center;
}
.my-div3 {
background-image: url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/assets/media/something.svg);
}
`;
expect(isSameCode(fixedCssCode, fixedCssCodeExpected)).toBe(true);
}); });
}); });

663
yarn.lock

File diff suppressed because it is too large Load Diff