diff --git a/.all-contributorsrc b/.all-contributorsrc
index 2302c06a..ba2c18dc 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -231,15 +231,6 @@
"contributions": [
"code"
]
- },
- {
- "login": "giorgoslytos",
- "name": "giorgoslytos",
- "avatar_url": "https://avatars.githubusercontent.com/u/50946162?v=4",
- "profile": "https://github.com/giorgoslytos",
- "contributions": [
- "code"
- ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index 60fd2c18..8415975f 100644
--- a/README.md
+++ b/README.md
@@ -14,9 +14,6 @@
-
-
-
@@ -43,10 +40,6 @@
Keycloakify is fully compatible with Keycloak 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, [~~22~~](https://github.com/keycloakify/keycloakify/issues/389#issuecomment-1822509763), **23** [and up](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)!
-> 📣 I've observed that a few people have unstarred the project recently.
-> I'm concerned that I may have inadvertently introduced some misinformation in the documentation, leading to frustration.
-> If you're having a negative experience, [please let me know so I can resolve the issue](https://github.com/keycloakify/keycloakify/discussions/507).
-
## Sponsor 👼
We are exclusively sponsored by [Cloud IAM](https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github), a French company offering Keycloak as a service.
@@ -130,10 +123,18 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
# Changelog highlights
+## 9.4
+
+**Vite Support! 🎉**
+
+- [The starter is now a Vite project](https://github.com/keycloakify/keycloakify-starter).
+ The Webpack based starter is accessible [here](https://github.com/keycloakify/keycloakify-starter-cra).
+- CRA (Webpack) remains supported for the forseable future.
+- If you have a CRA Keycloakify theme that you wish to migrate to Vite checkout [this migration guide](https://docs.keycloakify.dev/migration-guides/cra-greater-than-vite).
+
## 9.0
-Bring back support for account themes in Keycloak v23 and up! [See issue](https://github.com/keycloakify/keycloakify/issues/389).
-[Read the migration guide](https://docs.keycloakify.dev/migration-guides/v8-greater-than-v9).
+Bring back support for account themes in Keycloak v23 and up! [See issue](https://github.com/keycloakify/keycloakify/issues/389).
### Breaking changes
diff --git a/package.json b/package.json
index 64714813..e611d293 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "9.3.1",
+ "version": "9.4.1",
"description": "Create Keycloak themes using React",
"repository": {
"type": "git",
@@ -10,7 +10,7 @@
"types": "dist/index.d.ts",
"scripts": {
"prepare": "yarn generate-i18n-messages",
- "build": "rimraf dist/ && tsc -p src/bin && tsc -p src && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/ && cp -r src dist/",
+ "build": "rimraf dist/ && tsc -p src/bin && tsc -p src && tsc -p src/vite-plugin && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/ && cp -r src dist/",
"generate:json-schema": "ts-node scripts/generate-json-schema.ts",
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
"copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.java",
@@ -105,7 +105,8 @@
"tss-react": "^4.8.2",
"typescript": "^4.9.1-beta",
"vitest": "^0.29.8",
- "zod-to-json-schema": "^3.20.4"
+ "zod-to-json-schema": "^3.20.4",
+ "vite": "^5.0.12"
},
"dependencies": {
"@babel/generator": "^7.22.9",
@@ -118,7 +119,6 @@
"make-fetch-happen": "^11.0.3",
"minimal-polyfills": "^2.2.2",
"minimist": "^1.2.6",
- "path-browserify": "^1.0.1",
"react-markdown": "^5.0.3",
"recast": "^0.23.3",
"rfc4648": "^1.5.2",
diff --git a/scripts/generate-i18n-messages.ts b/scripts/generate-i18n-messages.ts
index 660551bc..aa899487 100644
--- a/scripts/generate-i18n-messages.ts
+++ b/scripts/generate-i18n-messages.ts
@@ -3,7 +3,7 @@ import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname, sep as pathSep } from "path";
import { crawl } from "../src/bin/tools/crawl";
import { downloadBuiltinKeycloakTheme } from "../src/bin/download-builtin-keycloak-theme";
-import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
+import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import { getLogger } from "../src/bin/tools/logger";
// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
@@ -19,15 +19,22 @@ const logger = getLogger({ isSilent });
async function main() {
const keycloakVersion = "23.0.4";
- const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
+ const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
+
+ const tmpDirPath = pathJoin(thisCodebaseRootDirPath, "tmp_xImOef9dOd44");
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
+ fs.mkdirSync(tmpDirPath);
+
+ fs.writeFileSync(pathJoin(tmpDirPath, ".gitignore"), Buffer.from("/*\n!.gitignore\n", "utf8"));
+
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
"buildOptions": {
- "cacheDirPath": pathJoin(getProjectRoot(), "node_modules", ".cache", "keycloakify")
+ "cacheDirPath": pathJoin(thisCodebaseRootDirPath, "node_modules", ".cache", "keycloakify"),
+ "npmWorkspaceRootDirPath": thisCodebaseRootDirPath
}
});
@@ -68,14 +75,13 @@ async function main() {
return;
}
- const baseMessagesDirPath = pathJoin(getProjectRoot(), "src", themeType, "i18n", "baseMessages");
+ const baseMessagesDirPath = pathJoin(thisCodebaseRootDirPath, "src", themeType, "i18n", "baseMessages");
const languages = Object.keys(recordForPageType);
const generatedFileHeader = [
- `//This code was automatically generated by running ${pathRelative(getProjectRoot(), __filename)}`,
- "//PLEASE DO NOT EDIT MANUALLY",
- ""
+ `//This code was automatically generated by running ${pathRelative(thisCodebaseRootDirPath, __filename)}`,
+ "//PLEASE DO NOT EDIT MANUALLY"
].join("\n");
languages.forEach(language => {
@@ -88,6 +94,7 @@ async function main() {
Buffer.from(
[
generatedFileHeader,
+ "",
"/* spell-checker: disable */",
`const messages= ${JSON.stringify(recordForPageType[language], null, 2)};`,
"",
@@ -106,10 +113,15 @@ async function main() {
Buffer.from(
[
generatedFileHeader,
+ `import * as en from "./en";`,
+ "",
"export async function getMessages(currentLanguageTag: string) {",
" const { default: messages } = await (() => {",
" switch (currentLanguageTag) {",
- ...languages.map(language => ` case "${language}": return import("./${language}");`),
+ ` case "en": return en;`,
+ ...languages
+ .filter(language => language !== "en")
+ .map(language => ` case "${language}": return import("./${language}");`),
' default: return { "default": {} };',
" }",
" })();",
diff --git a/scripts/generate-json-schema.ts b/scripts/generate-json-schema.ts
deleted file mode 100644
index 81c2899d..00000000
--- a/scripts/generate-json-schema.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import fs from "fs";
-import path from "path";
-import zodToJsonSchema from "zod-to-json-schema";
-import { zParsedPackageJson } from "../src/bin/keycloakify/parsedPackageJson";
-
-const jsonSchemaName = "keycloakifyPackageJsonSchema";
-const jsonSchema = zodToJsonSchema(zParsedPackageJson, jsonSchemaName);
-
-const baseProperties = {
- // merges package.json schema with keycloakify properties
- "allOf": [{ "$ref": "https://json.schemastore.org/package.json" }, { "$ref": jsonSchemaName }]
-};
-
-fs.writeFileSync(path.join(process.cwd(), "keycloakify-json-schema.json"), JSON.stringify({ ...baseProperties, ...jsonSchema }, null, 2));
diff --git a/scripts/link-in-app.ts b/scripts/link-in-app.ts
index 76b14650..eacb3214 100644
--- a/scripts/link-in-app.ts
+++ b/scripts/link-in-app.ts
@@ -1,11 +1,11 @@
import { execSync } from "child_process";
import { join as pathJoin, relative as pathRelative } from "path";
-import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
+import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import * as fs from "fs";
const singletonDependencies: string[] = ["react", "@types/react"];
-const rootDirPath = getProjectRoot();
+const rootDirPath = getThisCodebaseRootDirPath();
//NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58
fs.writeFileSync(
diff --git a/src/PUBLIC_URL.ts b/src/PUBLIC_URL.ts
new file mode 100644
index 00000000..af0f0bb3
--- /dev/null
+++ b/src/PUBLIC_URL.ts
@@ -0,0 +1,21 @@
+import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "keycloakify/bin/constants";
+import { assert } from "tsafe/assert";
+
+/**
+ * This is an equivalent of process.env.PUBLIC_URL thay you can use in Webpack projects.
+ * This works both in your main app and in your Keycloak theme.
+ */
+export const PUBLIC_URL = (() => {
+ const kcContext = (window as any)[nameOfTheGlobal];
+
+ if (kcContext === undefined || process.env.NODE_ENV === "development") {
+ assert(
+ process.env.PUBLIC_URL !== undefined,
+ `If you use keycloakify/PUBLIC_URL you should be in Webpack and thus process.env.PUBLIC_URL should be defined`
+ );
+
+ return process.env.PUBLIC_URL;
+ }
+
+ return `${kcContext.url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}`;
+})();
diff --git a/src/account/kcContext/createGetKcContext.ts b/src/account/kcContext/createGetKcContext.ts
index e1e37fbc..f45bf322 100644
--- a/src/account/kcContext/createGetKcContext.ts
+++ b/src/account/kcContext/createGetKcContext.ts
@@ -1,10 +1,9 @@
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import { deepAssign } from "keycloakify/tools/deepAssign";
+import { isStorybook } from "keycloakify/lib/isStorybook";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
-import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { symToStr } from "tsafe/symToStr";
-import { resources_common } from "keycloakify/bin/constants";
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
export function createGetKcContext(params?: {
@@ -27,7 +26,13 @@ export function createGetKcContext pageId === mockPageId);
@@ -88,8 +93,6 @@ export function createGetKcContext = [KcContextExtension] extends [never]
@@ -7,5 +7,5 @@ export type ExtendKcContext = [Kc
: AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>;
export function getKcContextFromWindow(): ExtendKcContext | undefined {
- return typeof window === "undefined" ? undefined : (window as any)[ftlValuesGlobalName];
+ return typeof window === "undefined" ? undefined : (window as any)[nameOfTheGlobal];
}
diff --git a/src/account/kcContext/kcContextMocks.ts b/src/account/kcContext/kcContextMocks.ts
index 57480a16..9b13b89b 100644
--- a/src/account/kcContext/kcContextMocks.ts
+++ b/src/account/kcContext/kcContextMocks.ts
@@ -1,12 +1,10 @@
import "minimal-polyfills/Object.fromEntries";
import { resources_common, keycloak_resources } from "keycloakify/bin/constants";
-import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { id } from "tsafe/id";
import type { KcContext } from "./KcContext";
+import { BASE_URL } from "keycloakify/lib/BASE_URL";
-const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/";
-
-const resourcesPath = pathJoin(PUBLIC_URL, keycloak_resources, "account", "resources");
+const resourcesPath = `${BASE_URL}${keycloak_resources}/account/resources`;
export const kcContextCommonMock: KcContext.Common = {
"themeVersion": "0.0.0",
@@ -15,7 +13,7 @@ export const kcContextCommonMock: KcContext.Common = {
"themeName": "my-theme-name",
"url": {
resourcesPath,
- "resourcesCommonPath": pathJoin(resourcesPath, resources_common),
+ "resourcesCommonPath": `${resourcesPath}/${resources_common}`,
"resourceUrl": "#",
"accountUrl": "#",
"applicationsUrl": "#",
diff --git a/src/account/pages/PageProps.ts b/src/account/pages/PageProps.ts
index 15ec16aa..620b6992 100644
--- a/src/account/pages/PageProps.ts
+++ b/src/account/pages/PageProps.ts
@@ -1,10 +1,11 @@
import type { I18n } from "keycloakify/account/i18n";
import type { TemplateProps, ClassKey } from "keycloakify/account/TemplateProps";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
+import type { KcContext } from "keycloakify/account/kcContext";
-export type PageProps = {
+export type PageProps = {
Template: LazyOrNot<(props: TemplateProps) => JSX.Element | null>;
- kcContext: KcContext;
+ kcContext: NarowedKcContext;
i18n: I18nExtended;
doUseDefaultCss: boolean;
classes?: Partial>;
diff --git a/src/bin/constants.ts b/src/bin/constants.ts
index fa8c2a49..4a8b4023 100644
--- a/src/bin/constants.ts
+++ b/src/bin/constants.ts
@@ -1,9 +1,12 @@
+export const nameOfTheGlobal = "kcContext";
export const keycloak_resources = "keycloak-resources";
export const resources_common = "resources-common";
export const lastKeycloakVersionWithAccountV1 = "21.1.2";
+export const resolvedViteConfigJsonBasename = "vite.json";
+export const basenameOfTheKeycloakifyResourcesDir = "build";
export const themeTypes = ["login", "account"] as const;
export const retrocompatPostfix = "_retrocompat";
-export const accountV1 = "account-v1";
+export const accountV1ThemeName = "account-v1";
export type ThemeType = (typeof themeTypes)[number];
diff --git a/src/bin/copy-keycloak-resources-to-public.ts b/src/bin/copy-keycloak-resources-to-public.ts
index f90e2fd1..f877594f 100644
--- a/src/bin/copy-keycloak-resources-to-public.ts
+++ b/src/bin/copy-keycloak-resources-to-public.ts
@@ -1,20 +1,44 @@
#!/usr/bin/env node
-import { downloadKeycloakStaticResources } from "./keycloakify/generateTheme/downloadKeycloakStaticResources";
+import { downloadKeycloakStaticResources, type BuildOptionsLike } from "./keycloakify/generateTheme/downloadKeycloakStaticResources";
import { join as pathJoin, relative as pathRelative } from "path";
-import { readBuildOptions } from "./keycloakify/BuildOptions";
+import { readBuildOptions } from "./keycloakify/buildOptions";
import { themeTypes, keycloak_resources, lastKeycloakVersionWithAccountV1 } from "./constants";
+import { readThisNpmProjectVersion } from "./tools/readThisNpmProjectVersion";
+import { assert, type Equals } from "tsafe/assert";
import * as fs from "fs";
+import { rmSync } from "./tools/fs.rmSync";
-(async () => {
- const reactAppRootDirPath = process.cwd();
+export async function copyKeycloakResourcesToPublic(params: { processArgv: string[] }) {
+ const { processArgv } = params;
- const buildOptions = readBuildOptions({
- reactAppRootDirPath,
- "processArgv": process.argv.slice(2)
+ const buildOptions = readBuildOptions({ processArgv });
+
+ const destDirPath = pathJoin(buildOptions.publicDirPath, keycloak_resources);
+
+ const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
+
+ const { keycloakifyBuildinfoRaw } = generateKeycloakifyBuildinfoRaw({
+ destDirPath,
+ "keycloakifyVersion": readThisNpmProjectVersion(),
+ buildOptions
});
- const reservedDirPath = pathJoin(buildOptions.publicDirPath, keycloak_resources);
+ skip_if_already_done: {
+ if (!fs.existsSync(keycloakifyBuildinfoFilePath)) {
+ break skip_if_already_done;
+ }
+
+ const keycloakifyBuildinfoRaw_previousRun = fs.readFileSync(keycloakifyBuildinfoFilePath).toString("utf8");
+
+ if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
+ break skip_if_already_done;
+ }
+
+ return;
+ }
+
+ rmSync(destDirPath, { "force": true, "recursive": true });
for (const themeType of themeTypes) {
await downloadKeycloakStaticResources({
@@ -27,14 +51,13 @@ import * as fs from "fs";
}
})(),
themeType,
- "themeDirPath": reservedDirPath,
- "usedResources": undefined,
+ "themeDirPath": destDirPath,
buildOptions
});
}
fs.writeFileSync(
- pathJoin(reservedDirPath, "README.txt"),
+ pathJoin(destDirPath, "README.txt"),
Buffer.from(
// prettier-ignore
[
@@ -44,7 +67,46 @@ import * as fs from "fs";
)
);
- fs.writeFileSync(pathJoin(buildOptions.publicDirPath, "keycloak-resources", ".gitignore"), Buffer.from("*", "utf8"));
+ fs.writeFileSync(pathJoin(buildOptions.publicDirPath, keycloak_resources, ".gitignore"), Buffer.from("*", "utf8"));
- console.log(`${pathRelative(reactAppRootDirPath, reservedDirPath)} directory created.`);
-})();
+ fs.writeFileSync(keycloakifyBuildinfoFilePath, Buffer.from(keycloakifyBuildinfoRaw, "utf8"));
+}
+
+export function generateKeycloakifyBuildinfoRaw(params: {
+ destDirPath: string;
+ keycloakifyVersion: string;
+ buildOptions: BuildOptionsLike & {
+ loginThemeResourcesFromKeycloakVersion: string;
+ };
+}) {
+ const { destDirPath, keycloakifyVersion, buildOptions } = params;
+
+ const { cacheDirPath, npmWorkspaceRootDirPath, loginThemeResourcesFromKeycloakVersion, ...rest } = buildOptions;
+
+ assert>(true);
+
+ const keycloakifyBuildinfoRaw = JSON.stringify(
+ {
+ keycloakifyVersion,
+ "buildOptions": {
+ loginThemeResourcesFromKeycloakVersion,
+ "cacheDirPath": pathRelative(destDirPath, cacheDirPath),
+ "npmWorkspaceRootDirPath": pathRelative(destDirPath, npmWorkspaceRootDirPath)
+ }
+ },
+ null,
+ 2
+ );
+
+ return { keycloakifyBuildinfoRaw };
+}
+
+async function main() {
+ await copyKeycloakResourcesToPublic({
+ "processArgv": process.argv.slice(2)
+ });
+}
+
+if (require.main === module) {
+ main();
+}
diff --git a/src/bin/download-builtin-keycloak-theme.ts b/src/bin/download-builtin-keycloak-theme.ts
index 47801229..3986132f 100644
--- a/src/bin/download-builtin-keycloak-theme.ts
+++ b/src/bin/download-builtin-keycloak-theme.ts
@@ -1,16 +1,19 @@
#!/usr/bin/env node
import { join as pathJoin } from "path";
-import { downloadAndUnzip } from "./tools/downloadAndUnzip";
+import { downloadAndUnzip } from "./downloadAndUnzip";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { getLogger } from "./tools/logger";
-import { readBuildOptions } from "./keycloakify/BuildOptions";
+import { readBuildOptions, type BuildOptions } from "./keycloakify/buildOptions";
import { assert } from "tsafe/assert";
-import type { BuildOptions } from "./keycloakify/BuildOptions";
import * as child_process from "child_process";
import * as fs from "fs";
+import { rmSync } from "./tools/fs.rmSync";
+import { lastKeycloakVersionWithAccountV1 } from "./constants";
+import { transformCodebase } from "./tools/transformCodebase";
export type BuildOptionsLike = {
cacheDirPath: string;
+ npmWorkspaceRootDirPath: string;
};
assert();
@@ -19,59 +22,13 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
const { keycloakVersion, destDirPath, buildOptions } = params;
await downloadAndUnzip({
- "doUseCache": true,
- "cacheDirPath": buildOptions.cacheDirPath,
destDirPath,
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
"specificDirsToExtract": ["", "-community"].map(ext => `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`),
+ buildOptions,
"preCacheTransform": {
"actionCacheId": "npm install and build",
"action": async ({ destDirPath }) => {
- fix_account_css: {
- const accountCssFilePath = pathJoin(destDirPath, "keycloak", "account", "resources", "css", "account.css");
-
- if (!fs.existsSync(accountCssFilePath)) {
- break fix_account_css;
- }
-
- fs.writeFileSync(
- accountCssFilePath,
- Buffer.from(fs.readFileSync(accountCssFilePath).toString("utf8").replace("top: -34px;", "top: -34px !important;"), "utf8")
- );
- }
-
- fix_account_topt: {
- const totpFtlFilePath = pathJoin(destDirPath, "base", "account", "totp.ftl");
-
- if (!fs.existsSync(totpFtlFilePath)) {
- break fix_account_topt;
- }
-
- fs.writeFileSync(
- totpFtlFilePath,
- Buffer.from(
- fs
- .readFileSync(totpFtlFilePath)
- .toString("utf8")
- .replace(
- [
- " <#list totp.policy.supportedApplications as app>",
- " ${app}",
- " #list>"
- ].join("\n"),
- [
- " <#if totp.policy.supportedApplications?has_content>",
- " <#list totp.policy.supportedApplications as app>",
- " ${app}",
- " #list>",
- " #if>"
- ].join("\n")
- ),
- "utf8"
- )
- );
- }
-
install_common_node_modules: {
const commonResourcesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources");
@@ -93,43 +50,188 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
});
}
- install_and_move_to_common_resources_generated_in_keycloak_v2: {
- const accountV2DirSrcDirPath = pathJoin(destDirPath, "keycloak.v2", "account", "src");
+ remove_keycloak_v2: {
+ const keycloakV2DirPath = pathJoin(destDirPath, "keycloak.v2");
- if (!fs.existsSync(accountV2DirSrcDirPath)) {
- break install_and_move_to_common_resources_generated_in_keycloak_v2;
+ if (!fs.existsSync(keycloakV2DirPath)) {
+ break remove_keycloak_v2;
}
- const packageManager = fs.existsSync(pathJoin(accountV2DirSrcDirPath, "pnpm-lock.yaml")) ? "pnpm" : "npm";
+ rmSync(keycloakV2DirPath, { "recursive": true });
+ }
- if (packageManager === "pnpm") {
- try {
- child_process.execSync(`which pnpm`);
- } catch {
- console.log(`Installing pnpm globally`);
- child_process.execSync(`npm install -g pnpm`);
+ // Note, this is an optimization for reducing the size of the jar
+ remove_unused_node_modules: {
+ const nodeModuleDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "node_modules");
+
+ if (!fs.existsSync(nodeModuleDirPath)) {
+ break remove_unused_node_modules;
+ }
+
+ const toDeletePerfixes = [
+ "angular",
+ "bootstrap",
+ "rcue",
+ "font-awesome",
+ "ng-file-upload",
+ pathJoin("patternfly", "dist", "sass"),
+ pathJoin("patternfly", "dist", "less"),
+ pathJoin("patternfly", "dist", "js"),
+ "d3",
+ pathJoin("jquery", "src"),
+ "c3",
+ "core-js",
+ "eonasdan-bootstrap-datetimepicker",
+ "moment",
+ "react",
+ "patternfly-bootstrap-treeview",
+ "popper.js",
+ "tippy.js",
+ "jquery-match-height",
+ "google-code-prettify",
+ "patternfly-bootstrap-combobox",
+ "focus-trap",
+ "tabbable",
+ "scheduler",
+ "@types",
+ "datatables.net",
+ "datatables.net-colreorder",
+ "tslib",
+ "prop-types",
+ "file-selector",
+ "datatables.net-colreorder-bs",
+ "object-assign",
+ "warning",
+ "js-tokens",
+ "loose-envify",
+ "prop-types-extra",
+ "attr-accept",
+ "datatables.net-select",
+ "drmonty-datatables-colvis",
+ "datatables.net-bs",
+ pathJoin("@patternfly", "react"),
+ pathJoin("@patternfly", "patternfly", "docs")
+ ];
+
+ transformCodebase({
+ "srcDirPath": nodeModuleDirPath,
+ "destDirPath": nodeModuleDirPath,
+ "transformSourceCode": ({ sourceCode, fileRelativePath }) => {
+ if (fileRelativePath.endsWith(".map")) {
+ return undefined;
+ }
+
+ if (toDeletePerfixes.find(prefix => fileRelativePath.startsWith(prefix)) !== undefined) {
+ return undefined;
+ }
+
+ if (fileRelativePath.startsWith(pathJoin("patternfly", "dist", "fonts"))) {
+ if (
+ !fileRelativePath.endsWith(".woff2") &&
+ !fileRelativePath.endsWith(".woff") &&
+ !fileRelativePath.endsWith(".ttf")
+ ) {
+ return undefined;
+ }
+ }
+
+ return { "modifiedSourceCode": sourceCode };
}
+ });
+ }
+
+ // Just like node_modules
+ remove_unused_lib: {
+ const libDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "lib");
+
+ if (!fs.existsSync(libDirPath)) {
+ break remove_unused_lib;
}
- child_process.execSync(`${packageManager} install`, { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
+ const toDeletePerfixes = ["ui-ace", "filesaver", "fileupload", "angular", "ui-ace", "pficon"];
- const packageJsonFilePath = pathJoin(accountV2DirSrcDirPath, "package.json");
+ transformCodebase({
+ "srcDirPath": libDirPath,
+ "destDirPath": libDirPath,
+ "transformSourceCode": ({ sourceCode, fileRelativePath }) => {
+ if (fileRelativePath.endsWith(".map")) {
+ return undefined;
+ }
- const packageJsonRaw = fs.readFileSync(packageJsonFilePath);
+ if (toDeletePerfixes.find(prefix => fileRelativePath.startsWith(prefix)) !== undefined) {
+ return undefined;
+ }
- const parsedPackageJson = JSON.parse(packageJsonRaw.toString("utf8"));
+ return { "modifiedSourceCode": sourceCode };
+ }
+ });
+ }
- parsedPackageJson.scripts.build = parsedPackageJson.scripts.build
- .replace(`${packageManager} run check-types`, "true")
- .replace(`${packageManager} run babel`, "true");
+ last_account_v1_transformations: {
+ if (lastKeycloakVersionWithAccountV1 !== keycloakVersion) {
+ break last_account_v1_transformations;
+ }
- fs.writeFileSync(packageJsonFilePath, Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8"));
+ {
+ const accountCssFilePath = pathJoin(destDirPath, "keycloak", "account", "resources", "css", "account.css");
- child_process.execSync(`${packageManager} run build`, { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
+ fs.writeFileSync(
+ accountCssFilePath,
+ Buffer.from(fs.readFileSync(accountCssFilePath).toString("utf8").replace("top: -34px;", "top: -34px !important;"), "utf8")
+ );
+ }
- fs.writeFileSync(packageJsonFilePath, packageJsonRaw);
+ {
+ const totpFtlFilePath = pathJoin(destDirPath, "base", "account", "totp.ftl");
- fs.rmSync(pathJoin(accountV2DirSrcDirPath, "node_modules"), { "recursive": true });
+ fs.writeFileSync(
+ totpFtlFilePath,
+ Buffer.from(
+ fs
+ .readFileSync(totpFtlFilePath)
+ .toString("utf8")
+ .replace(
+ [
+ " <#list totp.policy.supportedApplications as app>",
+ " ${app}",
+ " #list>"
+ ].join("\n"),
+ [
+ " <#if totp.policy.supportedApplications?has_content>",
+ " <#list totp.policy.supportedApplications as app>",
+ " ${app}",
+ " #list>",
+ " #if>"
+ ].join("\n")
+ ),
+ "utf8"
+ )
+ );
+ }
+
+ // Note, this is an optimization for reducing the size of the jar,
+ // For this version we know exactly which resources are used.
+ {
+ const nodeModulesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "node_modules");
+
+ const toKeepPrefixes = [
+ ...["patternfly.min.css", "patternfly-additions.min.css", "patternfly-additions.min.css"].map(fileBasename =>
+ pathJoin("patternfly", "dist", "css", fileBasename)
+ ),
+ pathJoin("patternfly", "dist", "fonts")
+ ];
+
+ transformCodebase({
+ "srcDirPath": nodeModulesDirPath,
+ "destDirPath": nodeModulesDirPath,
+ "transformSourceCode": ({ sourceCode, fileRelativePath }) => {
+ if (toKeepPrefixes.find(prefix => fileRelativePath.startsWith(prefix)) === undefined) {
+ return undefined;
+ }
+ return { "modifiedSourceCode": sourceCode };
+ }
+ });
+ }
}
}
}
@@ -138,7 +240,6 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
async function main() {
const buildOptions = readBuildOptions({
- "reactAppRootDirPath": process.cwd(),
"processArgv": process.argv.slice(2)
});
diff --git a/src/bin/downloadAndUnzip.ts b/src/bin/downloadAndUnzip.ts
new file mode 100644
index 00000000..e11a4c1e
--- /dev/null
+++ b/src/bin/downloadAndUnzip.ts
@@ -0,0 +1,203 @@
+import { createHash } from "crypto";
+import { mkdir, writeFile, unlink } from "fs/promises";
+import fetch from "make-fetch-happen";
+import { dirname as pathDirname, join as pathJoin, basename as pathBasename } from "path";
+import { assert } from "tsafe/assert";
+import { transformCodebase } from "./tools/transformCodebase";
+import { unzip, zip } from "./tools/unzip";
+import { rm } from "./tools/fs.rm";
+import * as child_process from "child_process";
+import { existsAsync } from "./tools/fs.existsAsync";
+import type { BuildOptions } from "./keycloakify/buildOptions";
+import { getProxyFetchOptions } from "./tools/fetchProxyOptions";
+
+export type BuildOptionsLike = {
+ cacheDirPath: string;
+ npmWorkspaceRootDirPath: string;
+};
+
+assert();
+
+export async function downloadAndUnzip(params: {
+ url: string;
+ destDirPath: string;
+ specificDirsToExtract?: string[];
+ preCacheTransform?: {
+ actionCacheId: string;
+ action: (params: { destDirPath: string }) => Promise;
+ };
+ buildOptions: BuildOptionsLike;
+}) {
+ const { url, destDirPath, specificDirsToExtract, preCacheTransform, buildOptions } = params;
+
+ const { extractDirPath, zipFilePath } = (() => {
+ const zipFileBasenameWithoutExt = generateFileNameFromURL({
+ url,
+ "preCacheTransform":
+ preCacheTransform === undefined
+ ? undefined
+ : {
+ "actionCacheId": preCacheTransform.actionCacheId,
+ "actionFootprint": preCacheTransform.action.toString()
+ }
+ });
+
+ const zipFilePath = pathJoin(buildOptions.cacheDirPath, `${zipFileBasenameWithoutExt}.zip`);
+ const extractDirPath = pathJoin(buildOptions.cacheDirPath, `tmp_unzip_${zipFileBasenameWithoutExt}`);
+
+ return { zipFilePath, extractDirPath };
+ })();
+
+ download_zip_and_transform: {
+ if (await existsAsync(zipFilePath)) {
+ break download_zip_and_transform;
+ }
+
+ const { response, isFromRemoteCache } = await (async () => {
+ const proxyFetchOptions = await getProxyFetchOptions({
+ "npmWorkspaceRootDirPath": buildOptions.npmWorkspaceRootDirPath
+ });
+
+ const response = await fetch(
+ `https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/${pathBasename(zipFilePath)}`,
+ proxyFetchOptions
+ );
+
+ if (response.status === 200) {
+ return {
+ response,
+ "isFromRemoteCache": true
+ };
+ }
+
+ return {
+ "response": await fetch(url, proxyFetchOptions),
+ "isFromRemoteCache": false
+ };
+ })();
+
+ await mkdir(pathDirname(zipFilePath), { "recursive": true });
+
+ /**
+ * The correct way to fix this is to upgrade node-fetch beyond 3.2.5
+ * (see https://github.com/node-fetch/node-fetch/issues/1295#issuecomment-1144061991.)
+ * Unfortunately, octokit (a dependency of keycloakify) also uses node-fetch, and
+ * does not support node-fetch 3.x. So we stick around with this band-aid until
+ * octokit upgrades.
+ */
+ response.body?.setMaxListeners(Number.MAX_VALUE);
+ assert(typeof response.body !== "undefined" && response.body != null);
+
+ await writeFile(zipFilePath, response.body);
+
+ if (isFromRemoteCache) {
+ break download_zip_and_transform;
+ }
+
+ if (specificDirsToExtract === undefined && preCacheTransform === undefined) {
+ break download_zip_and_transform;
+ }
+
+ await unzip(zipFilePath, extractDirPath, specificDirsToExtract);
+
+ try {
+ await preCacheTransform?.action({
+ "destDirPath": extractDirPath
+ });
+ } catch (error) {
+ await Promise.all([rm(extractDirPath, { "recursive": true }), unlink(zipFilePath)]);
+
+ throw error;
+ }
+
+ await unlink(zipFilePath);
+
+ await zip(extractDirPath, zipFilePath);
+
+ await rm(extractDirPath, { "recursive": true });
+
+ upload_to_remot_cache_if_admin: {
+ const githubToken = process.env["KEYCLOAKIFY_ADMIN_GITHUB_PERSONAL_ACCESS_TOKEN"];
+
+ if (githubToken === undefined) {
+ break upload_to_remot_cache_if_admin;
+ }
+
+ console.log("uploading to remote cache");
+
+ try {
+ child_process.execSync(`which putasset`);
+ } catch {
+ child_process.execSync(`npm install -g putasset`);
+ }
+
+ try {
+ child_process.execFileSync("putasset", [
+ "--owner",
+ "keycloakify",
+ "--repo",
+ "keycloakify",
+ "--tag",
+ "v0.0.1",
+ "--filename",
+ zipFilePath,
+ "--token",
+ githubToken
+ ]);
+ } catch {
+ console.log("upload failed, asset probably already exists in remote cache");
+ }
+ }
+ }
+
+ await unzip(zipFilePath, extractDirPath);
+
+ transformCodebase({
+ "srcDirPath": extractDirPath,
+ "destDirPath": destDirPath
+ });
+
+ await rm(extractDirPath, { "recursive": true });
+}
+
+function generateFileNameFromURL(params: {
+ url: string;
+ preCacheTransform:
+ | {
+ actionCacheId: string;
+ actionFootprint: string;
+ }
+ | undefined;
+}): string {
+ const { preCacheTransform } = params;
+
+ // Parse the URL
+ const url = new URL(params.url);
+
+ // Extract pathname and remove leading slashes
+ let fileName = url.pathname.replace(/^\//, "").replace(/\//g, "_");
+
+ // Optionally, add query parameters replacing special characters
+ if (url.search) {
+ fileName += url.search.replace(/[&=?]/g, "-");
+ }
+
+ // Replace any characters that are not valid in filenames
+ fileName = fileName.replace(/[^a-zA-Z0-9-_]/g, "");
+
+ // Trim or pad the fileName to a specific length
+ fileName = fileName.substring(0, 50);
+
+ add_pre_cache_transform: {
+ if (preCacheTransform === undefined) {
+ break add_pre_cache_transform;
+ }
+
+ // Sanitize actionCacheId the same way as other components
+ const sanitizedActionCacheId = preCacheTransform.actionCacheId.replace(/[^a-zA-Z0-9-_]/g, "_");
+
+ fileName += `_${sanitizedActionCacheId}_${createHash("sha256").update(preCacheTransform.actionFootprint).digest("hex").substring(0, 5)}`;
+ }
+
+ return fileName;
+}
diff --git a/src/bin/eject-keycloak-page.ts b/src/bin/eject-keycloak-page.ts
index e82ff9c7..f777f281 100644
--- a/src/bin/eject-keycloak-page.ts
+++ b/src/bin/eject-keycloak-page.ts
@@ -1,6 +1,6 @@
#!/usr/bin/env node
-import { getProjectRoot } from "./tools/getProjectRoot";
+import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
import cliSelect from "cli-select";
import { loginThemePageIds, accountThemePageIds, type LoginThemePageId, type AccountThemePageId } from "./keycloakify/generateFtl";
import { capitalize } from "tsafe/capitalize";
@@ -9,13 +9,16 @@ import { existsSync } from "fs";
import { join as pathJoin, relative as pathRelative } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
-import { getThemeSrcDirPath } from "./getSrcDirPath";
+import { getThemeSrcDirPath } from "./getThemeSrcDirPath";
import { themeTypes, type ThemeType } from "./constants";
+import { getReactAppRootDirPath } from "./keycloakify/buildOptions/getReactAppRootDirPath";
(async () => {
console.log("Select a theme type");
- const reactAppRootDirPath = process.cwd();
+ const { reactAppRootDirPath } = getReactAppRootDirPath({
+ "processArgv": process.argv.slice(2)
+ });
const { value: themeType } = await cliSelect({
"values": [...themeTypes]
@@ -55,7 +58,7 @@ import { themeTypes, type ThemeType } from "./constants";
process.exit(-1);
}
- await writeFile(targetFilePath, await readFile(pathJoin(getProjectRoot(), "src", themeType, "pages", pageBasename)));
+ await writeFile(targetFilePath, await readFile(pathJoin(getThisCodebaseRootDirPath(), "src", themeType, "pages", pageBasename)));
console.log(`${pathRelative(process.cwd(), targetFilePath)} created`);
})();
diff --git a/src/bin/getSrcDirPath.ts b/src/bin/getThemeSrcDirPath.ts
similarity index 100%
rename from src/bin/getSrcDirPath.ts
rename to src/bin/getThemeSrcDirPath.ts
diff --git a/src/bin/initialize-email-theme.ts b/src/bin/initialize-email-theme.ts
index 14f44d6e..4e2be98f 100644
--- a/src/bin/initialize-email-theme.ts
+++ b/src/bin/initialize-email-theme.ts
@@ -4,23 +4,21 @@ import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme"
import { join as pathJoin, relative as pathRelative } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
-import { readBuildOptions } from "./keycloakify/BuildOptions";
+import { readBuildOptions } from "./keycloakify/buildOptions";
import * as fs from "fs";
import { getLogger } from "./tools/logger";
-import { getThemeSrcDirPath } from "./getSrcDirPath";
+import { getThemeSrcDirPath } from "./getThemeSrcDirPath";
+import { rmSync } from "./tools/fs.rmSync";
export async function main() {
- const reactAppRootDirPath = process.cwd();
-
const buildOptions = readBuildOptions({
- reactAppRootDirPath,
"processArgv": process.argv.slice(2)
});
const logger = getLogger({ "isSilent": buildOptions.isSilent });
const { themeSrcDirPath } = getThemeSrcDirPath({
- reactAppRootDirPath
+ "reactAppRootDirPath": buildOptions.reactAppRootDirPath
});
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
@@ -54,7 +52,7 @@ export async function main() {
logger.log(`${pathRelative(process.cwd(), emailThemeSrcDirPath)} ready to be customized, feel free to remove every file you do not customize`);
- fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
+ rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
}
if (require.main === module) {
diff --git a/src/bin/keycloakify/BuildOptions.ts b/src/bin/keycloakify/BuildOptions.ts
deleted file mode 100644
index 119dd557..00000000
--- a/src/bin/keycloakify/BuildOptions.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-import { parse as urlParse } from "url";
-import { getParsedPackageJson } from "./parsedPackageJson";
-import { join as pathJoin } from "path";
-import parseArgv from "minimist";
-import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
-
-/** Consolidated build option gathered form CLI arguments and config in package.json */
-export type BuildOptions = {
- isSilent: boolean;
- themeVersion: string;
- themeNames: string[];
- extraThemeProperties: string[] | undefined;
- groupId: string;
- artifactId: string;
- doCreateJar: boolean;
- loginThemeResourcesFromKeycloakVersion: string;
- reactAppRootDirPath: string;
- /** Directory of your built react project. Defaults to {cwd}/build */
- reactAppBuildDirPath: string;
- /** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
- keycloakifyBuildDirPath: string;
- publicDirPath: string;
- cacheDirPath: string;
- /** If your app is hosted under a subpath, it's the case in CRA if you have "homepage": "https://example.com/my-app" in your package.json
- * In this case the urlPathname will be "/my-app/" */
- urlPathname: string | undefined;
- doBuildRetrocompatAccountTheme: boolean;
-};
-
-export function readBuildOptions(params: { reactAppRootDirPath: string; processArgv: string[] }): BuildOptions {
- const { reactAppRootDirPath, processArgv } = params;
-
- const { isSilentCliParamProvided } = (() => {
- const argv = parseArgv(processArgv);
-
- return {
- "isSilentCliParamProvided": typeof argv["silent"] === "boolean" ? argv["silent"] : false
- };
- })();
-
- const parsedPackageJson = getParsedPackageJson({ reactAppRootDirPath });
-
- const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
-
- const { extraThemeProperties, groupId, artifactId, doCreateJar, loginThemeResourcesFromKeycloakVersion } = keycloakify ?? {};
-
- const themeNames = (() => {
- if (keycloakify.themeName === undefined) {
- return [
- name
- .replace(/^@(.*)/, "$1")
- .split("/")
- .join("-")
- ];
- }
-
- if (typeof keycloakify.themeName === "string") {
- return [keycloakify.themeName];
- }
-
- return keycloakify.themeName;
- })();
-
- return {
- reactAppRootDirPath,
- themeNames,
- "doCreateJar": doCreateJar ?? true,
- "artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeNames[0]}-keycloak-theme`,
- "groupId": (() => {
- const fallbackGroupId = `${themeNames[0]}.keycloak`;
-
- return (
- process.env.KEYCLOAKIFY_GROUP_ID ??
- groupId ??
- (!homepage
- ? fallbackGroupId
- : urlParse(homepage)
- .host?.replace(/:[0-9]+$/, "")
- ?.split(".")
- .reverse()
- .join(".") ?? fallbackGroupId) + ".keycloak"
- );
- })(),
- "themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0",
- extraThemeProperties,
- "isSilent": isSilentCliParamProvided,
- "loginThemeResourcesFromKeycloakVersion": loginThemeResourcesFromKeycloakVersion ?? "11.0.3",
- "publicDirPath": (() => {
- let { PUBLIC_DIR_PATH } = process.env;
-
- if (PUBLIC_DIR_PATH !== undefined) {
- return getAbsoluteAndInOsFormatPath({
- "pathIsh": PUBLIC_DIR_PATH,
- "cwd": reactAppRootDirPath
- });
- }
-
- return pathJoin(reactAppRootDirPath, "public");
- })(),
- "reactAppBuildDirPath": (() => {
- const { reactAppBuildDirPath } = parsedPackageJson.keycloakify ?? {};
-
- if (reactAppBuildDirPath !== undefined) {
- return getAbsoluteAndInOsFormatPath({
- "pathIsh": reactAppBuildDirPath,
- "cwd": reactAppRootDirPath
- });
- }
-
- return pathJoin(reactAppRootDirPath, "build");
- })(),
- "keycloakifyBuildDirPath": (() => {
- const { keycloakifyBuildDirPath } = parsedPackageJson.keycloakify ?? {};
-
- if (keycloakifyBuildDirPath !== undefined) {
- return getAbsoluteAndInOsFormatPath({
- "pathIsh": keycloakifyBuildDirPath,
- "cwd": reactAppRootDirPath
- });
- }
-
- return pathJoin(reactAppRootDirPath, "build_keycloak");
- })(),
- "cacheDirPath": pathJoin(
- (() => {
- let { XDG_CACHE_HOME } = process.env;
-
- if (XDG_CACHE_HOME !== undefined) {
- return getAbsoluteAndInOsFormatPath({
- "pathIsh": XDG_CACHE_HOME,
- "cwd": reactAppRootDirPath
- });
- }
-
- return pathJoin(reactAppRootDirPath, "node_modules", ".cache");
- })(),
- "keycloakify"
- ),
- "urlPathname": (() => {
- const { homepage } = parsedPackageJson;
-
- let url: URL | undefined = undefined;
-
- if (homepage !== undefined) {
- url = new URL(homepage);
- }
-
- if (url === undefined) {
- return undefined;
- }
-
- const out = url.pathname.replace(/([^/])$/, "$1/");
- return out === "/" ? undefined : out;
- })(),
- "doBuildRetrocompatAccountTheme": parsedPackageJson.keycloakify?.doBuildRetrocompatAccountTheme ?? true
- };
-}
diff --git a/src/bin/keycloakify/buildOptions/buildOptions.ts b/src/bin/keycloakify/buildOptions/buildOptions.ts
new file mode 100644
index 00000000..a9efb540
--- /dev/null
+++ b/src/bin/keycloakify/buildOptions/buildOptions.ts
@@ -0,0 +1,185 @@
+import { parse as urlParse } from "url";
+import { readParsedPackageJson } from "./parsedPackageJson";
+import { join as pathJoin } from "path";
+import parseArgv from "minimist";
+import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
+import { readResolvedViteConfig } from "./resolvedViteConfig";
+import * as fs from "fs";
+import { getCacheDirPath } from "./getCacheDirPath";
+import { getReactAppRootDirPath } from "./getReactAppRootDirPath";
+import { getNpmWorkspaceRootDirPath } from "./getNpmWorkspaceRootDirPath";
+
+/** Consolidated build option gathered form CLI arguments and config in package.json */
+export type BuildOptions = {
+ bundler: "vite" | "webpack";
+ isSilent: boolean;
+ themeVersion: string;
+ themeNames: string[];
+ extraThemeProperties: string[] | undefined;
+ groupId: string;
+ artifactId: string;
+ doCreateJar: boolean;
+ loginThemeResourcesFromKeycloakVersion: string;
+ reactAppRootDirPath: string;
+ reactAppBuildDirPath: string;
+ /** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
+ keycloakifyBuildDirPath: string;
+ publicDirPath: string;
+ cacheDirPath: string;
+ /** If your app is hosted under a subpath, it's the case in CRA if you have "homepage": "https://example.com/my-app" in your package.json
+ * In this case the urlPathname will be "/my-app/" */
+ urlPathname: string | undefined;
+ assetsDirPath: string;
+ doBuildRetrocompatAccountTheme: boolean;
+ npmWorkspaceRootDirPath: string;
+};
+
+export function readBuildOptions(params: { processArgv: string[] }): BuildOptions {
+ const { processArgv } = params;
+
+ const { reactAppRootDirPath } = getReactAppRootDirPath({ processArgv });
+
+ const { cacheDirPath } = getCacheDirPath({ reactAppRootDirPath });
+
+ const { resolvedViteConfig } = readResolvedViteConfig({ cacheDirPath });
+
+ if (resolvedViteConfig === undefined && fs.existsSync(pathJoin(reactAppRootDirPath, "vite.config.ts"))) {
+ throw new Error("Keycloakify's Vite plugin output not found");
+ }
+
+ const parsedPackageJson = readParsedPackageJson({ reactAppRootDirPath });
+
+ const themeNames = (() => {
+ if (parsedPackageJson.keycloakify?.themeName === undefined) {
+ return [
+ parsedPackageJson.name
+ .replace(/^@(.*)/, "$1")
+ .split("/")
+ .join("-")
+ ];
+ }
+
+ if (typeof parsedPackageJson.keycloakify.themeName === "string") {
+ return [parsedPackageJson.keycloakify.themeName];
+ }
+
+ return parsedPackageJson.keycloakify.themeName;
+ })();
+
+ const reactAppBuildDirPath = (() => {
+ webpack: {
+ if (resolvedViteConfig !== undefined) {
+ break webpack;
+ }
+
+ if (parsedPackageJson.keycloakify?.reactAppBuildDirPath !== undefined) {
+ return getAbsoluteAndInOsFormatPath({
+ "pathIsh": parsedPackageJson.keycloakify?.reactAppBuildDirPath,
+ "cwd": reactAppRootDirPath
+ });
+ }
+
+ return pathJoin(reactAppRootDirPath, "build");
+ }
+
+ return pathJoin(reactAppRootDirPath, resolvedViteConfig.buildDir);
+ })();
+
+ const argv = parseArgv(processArgv);
+
+ const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({ reactAppRootDirPath });
+
+ return {
+ "bundler": resolvedViteConfig !== undefined ? "vite" : "webpack",
+ "isSilent": typeof argv["silent"] === "boolean" ? argv["silent"] : false,
+ "themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0",
+ themeNames,
+ "extraThemeProperties": parsedPackageJson.keycloakify?.extraThemeProperties,
+ "groupId": (() => {
+ const fallbackGroupId = `${themeNames[0]}.keycloak`;
+
+ return (
+ process.env.KEYCLOAKIFY_GROUP_ID ??
+ parsedPackageJson.keycloakify?.groupId ??
+ (parsedPackageJson.homepage === undefined
+ ? fallbackGroupId
+ : urlParse(parsedPackageJson.homepage)
+ .host?.replace(/:[0-9]+$/, "")
+ ?.split(".")
+ .reverse()
+ .join(".") ?? fallbackGroupId) + ".keycloak"
+ );
+ })(),
+ "artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? parsedPackageJson.keycloakify?.artifactId ?? `${themeNames[0]}-keycloak-theme`,
+ "doCreateJar": parsedPackageJson.keycloakify?.doCreateJar ?? true,
+ "loginThemeResourcesFromKeycloakVersion": parsedPackageJson.keycloakify?.loginThemeResourcesFromKeycloakVersion ?? "11.0.3",
+ reactAppRootDirPath,
+ reactAppBuildDirPath,
+ "keycloakifyBuildDirPath": (() => {
+ if (parsedPackageJson.keycloakify?.keycloakifyBuildDirPath !== undefined) {
+ return getAbsoluteAndInOsFormatPath({
+ "pathIsh": parsedPackageJson.keycloakify?.keycloakifyBuildDirPath,
+ "cwd": reactAppRootDirPath
+ });
+ }
+
+ return resolvedViteConfig?.buildDir === undefined ? "build_keycloak" : `${resolvedViteConfig.buildDir}_keycloak`;
+ })(),
+ "publicDirPath": (() => {
+ webpack: {
+ if (resolvedViteConfig !== undefined) {
+ break webpack;
+ }
+
+ if (process.env.PUBLIC_DIR_PATH !== undefined) {
+ return getAbsoluteAndInOsFormatPath({
+ "pathIsh": process.env.PUBLIC_DIR_PATH,
+ "cwd": reactAppRootDirPath
+ });
+ }
+
+ return pathJoin(reactAppRootDirPath, "public");
+ }
+
+ return pathJoin(reactAppRootDirPath, resolvedViteConfig.publicDir);
+ })(),
+ cacheDirPath,
+ "urlPathname": (() => {
+ webpack: {
+ if (resolvedViteConfig !== undefined) {
+ break webpack;
+ }
+
+ const { homepage } = parsedPackageJson;
+
+ let url: URL | undefined = undefined;
+
+ if (homepage !== undefined) {
+ url = new URL(homepage);
+ }
+
+ if (url === undefined) {
+ return undefined;
+ }
+
+ const out = url.pathname.replace(/([^/])$/, "$1/");
+ return out === "/" ? undefined : out;
+ }
+
+ return resolvedViteConfig.urlPathname;
+ })(),
+ "assetsDirPath": (() => {
+ webpack: {
+ if (resolvedViteConfig !== undefined) {
+ break webpack;
+ }
+
+ return pathJoin(reactAppBuildDirPath, "static");
+ }
+
+ return pathJoin(reactAppBuildDirPath, resolvedViteConfig.assetsDir);
+ })(),
+ "doBuildRetrocompatAccountTheme": parsedPackageJson.keycloakify?.doBuildRetrocompatAccountTheme ?? true,
+ npmWorkspaceRootDirPath
+ };
+}
diff --git a/src/bin/keycloakify/buildOptions/getCacheDirPath.ts b/src/bin/keycloakify/buildOptions/getCacheDirPath.ts
new file mode 100644
index 00000000..9089e09a
--- /dev/null
+++ b/src/bin/keycloakify/buildOptions/getCacheDirPath.ts
@@ -0,0 +1,25 @@
+import { join as pathJoin } from "path";
+import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
+import { getNpmWorkspaceRootDirPath } from "./getNpmWorkspaceRootDirPath";
+
+export function getCacheDirPath(params: { reactAppRootDirPath: string }) {
+ const { reactAppRootDirPath } = params;
+
+ const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({ reactAppRootDirPath });
+
+ const cacheDirPath = pathJoin(
+ (() => {
+ if (process.env.XDG_CACHE_HOME !== undefined) {
+ return getAbsoluteAndInOsFormatPath({
+ "pathIsh": process.env.XDG_CACHE_HOME,
+ "cwd": reactAppRootDirPath
+ });
+ }
+
+ return pathJoin(npmWorkspaceRootDirPath, "node_modules", ".cache");
+ })(),
+ "keycloakify"
+ );
+
+ return { cacheDirPath };
+}
diff --git a/src/bin/keycloakify/buildOptions/getNpmWorkspaceRootDirPath.ts b/src/bin/keycloakify/buildOptions/getNpmWorkspaceRootDirPath.ts
new file mode 100644
index 00000000..a51a7e8c
--- /dev/null
+++ b/src/bin/keycloakify/buildOptions/getNpmWorkspaceRootDirPath.ts
@@ -0,0 +1,49 @@
+import * as child_process from "child_process";
+import { join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
+import { assert } from "tsafe/assert";
+
+let cache:
+ | {
+ reactAppRootDirPath: string;
+ npmWorkspaceRootDirPath: string;
+ }
+ | undefined = undefined;
+
+export function getNpmWorkspaceRootDirPath(params: { reactAppRootDirPath: string }) {
+ const { reactAppRootDirPath } = params;
+
+ use_cache: {
+ if (cache === undefined || cache.reactAppRootDirPath !== reactAppRootDirPath) {
+ break use_cache;
+ }
+
+ const { npmWorkspaceRootDirPath } = cache;
+
+ return { npmWorkspaceRootDirPath };
+ }
+
+ const npmWorkspaceRootDirPath = (function callee(depth: number): string {
+ const cwd = pathResolve(pathJoin(...[reactAppRootDirPath, ...Array(depth).fill("..")]));
+
+ try {
+ child_process.execSync("npm config get", { cwd: cwd });
+ } catch (error) {
+ if (String(error).includes("ENOWORKSPACES")) {
+ assert(cwd !== pathSep, "NPM workspace not found");
+
+ return callee(depth + 1);
+ }
+
+ throw error;
+ }
+
+ return cwd;
+ })(0);
+
+ cache = {
+ reactAppRootDirPath,
+ npmWorkspaceRootDirPath
+ };
+
+ return { npmWorkspaceRootDirPath };
+}
diff --git a/src/bin/keycloakify/buildOptions/getReactAppRootDirPath.ts b/src/bin/keycloakify/buildOptions/getReactAppRootDirPath.ts
new file mode 100644
index 00000000..49b9e9d3
--- /dev/null
+++ b/src/bin/keycloakify/buildOptions/getReactAppRootDirPath.ts
@@ -0,0 +1,23 @@
+import parseArgv from "minimist";
+import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
+
+export function getReactAppRootDirPath(params: { processArgv: string[] }) {
+ const { processArgv } = params;
+
+ const argv = parseArgv(processArgv);
+
+ const reactAppRootDirPath = (() => {
+ const arg = argv["project"] ?? argv["p"];
+
+ if (typeof arg !== "string") {
+ return process.cwd();
+ }
+
+ return getAbsoluteAndInOsFormatPath({
+ "pathIsh": arg,
+ "cwd": process.cwd()
+ });
+ })();
+
+ return { reactAppRootDirPath };
+}
diff --git a/src/bin/keycloakify/buildOptions/index.ts b/src/bin/keycloakify/buildOptions/index.ts
new file mode 100644
index 00000000..0d6efd40
--- /dev/null
+++ b/src/bin/keycloakify/buildOptions/index.ts
@@ -0,0 +1 @@
+export * from "./buildOptions";
diff --git a/src/bin/keycloakify/parsedPackageJson.ts b/src/bin/keycloakify/buildOptions/parsedPackageJson.ts
similarity index 81%
rename from src/bin/keycloakify/parsedPackageJson.ts
rename to src/bin/keycloakify/buildOptions/parsedPackageJson.ts
index 43478f14..4b2aafff 100644
--- a/src/bin/keycloakify/parsedPackageJson.ts
+++ b/src/bin/keycloakify/buildOptions/parsedPackageJson.ts
@@ -2,7 +2,7 @@ import * as fs from "fs";
import { assert } from "tsafe";
import type { Equals } from "tsafe";
import { z } from "zod";
-import { pathJoin } from "../tools/pathJoin";
+import { join as pathJoin } from "path";
export type ParsedPackageJson = {
name: string;
@@ -10,7 +10,6 @@ export type ParsedPackageJson = {
homepage?: string;
keycloakify?: {
extraThemeProperties?: string[];
- areAppAndKeycloakServerSharingSameDomain?: boolean;
artifactId?: string;
groupId?: string;
doCreateJar?: boolean;
@@ -22,14 +21,13 @@ export type ParsedPackageJson = {
};
};
-export const zParsedPackageJson = z.object({
+const zParsedPackageJson = z.object({
"name": z.string(),
"version": z.string().optional(),
"homepage": z.string().optional(),
"keycloakify": z
.object({
"extraThemeProperties": z.array(z.string()).optional(),
- "areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(),
"artifactId": z.string().optional(),
"groupId": z.string().optional(),
"doCreateJar": z.boolean().optional(),
@@ -44,8 +42,8 @@ export const zParsedPackageJson = z.object({
assert, ParsedPackageJson>>();
-let parsedPackageJson: undefined | ReturnType<(typeof zParsedPackageJson)["parse"]>;
-export function getParsedPackageJson(params: { reactAppRootDirPath: string }) {
+let parsedPackageJson: undefined | ParsedPackageJson;
+export function readParsedPackageJson(params: { reactAppRootDirPath: string }) {
const { reactAppRootDirPath } = params;
if (parsedPackageJson) {
return parsedPackageJson;
diff --git a/src/bin/keycloakify/buildOptions/resolvedViteConfig.ts b/src/bin/keycloakify/buildOptions/resolvedViteConfig.ts
new file mode 100644
index 00000000..4d4fd3fe
--- /dev/null
+++ b/src/bin/keycloakify/buildOptions/resolvedViteConfig.ts
@@ -0,0 +1,71 @@
+import * as fs from "fs";
+import { assert } from "tsafe";
+import type { Equals } from "tsafe";
+import { z } from "zod";
+import { join as pathJoin } from "path";
+import { resolvedViteConfigJsonBasename } from "../../constants";
+import type { OptionalIfCanBeUndefined } from "../../tools/OptionalIfCanBeUndefined";
+
+export type ResolvedViteConfig = {
+ buildDir: string;
+ publicDir: string;
+ assetsDir: string;
+ urlPathname: string | undefined;
+};
+
+const zResolvedViteConfig = z.object({
+ "buildDir": z.string(),
+ "publicDir": z.string(),
+ "assetsDir": z.string(),
+ "urlPathname": z.string().optional()
+});
+
+{
+ type Got = ReturnType<(typeof zResolvedViteConfig)["parse"]>;
+ type Expected = OptionalIfCanBeUndefined;
+
+ assert>();
+}
+
+export function readResolvedViteConfig(params: { cacheDirPath: string }): {
+ resolvedViteConfig: ResolvedViteConfig | undefined;
+} {
+ const { cacheDirPath } = params;
+
+ const resolvedViteConfigJsonFilePath = pathJoin(cacheDirPath, resolvedViteConfigJsonBasename);
+
+ if (!fs.existsSync(resolvedViteConfigJsonFilePath)) {
+ return { "resolvedViteConfig": undefined };
+ }
+
+ const resolvedViteConfig = (() => {
+ if (!fs.existsSync(resolvedViteConfigJsonFilePath)) {
+ throw new Error("Missing Keycloakify Vite plugin output.");
+ }
+
+ let out: ResolvedViteConfig;
+
+ try {
+ out = JSON.parse(fs.readFileSync(resolvedViteConfigJsonFilePath).toString("utf8"));
+ } catch {
+ throw new Error("The output of the Keycloakify Vite plugin is not a valid JSON.");
+ }
+
+ try {
+ const zodParseReturn = zResolvedViteConfig.parse(out);
+
+ // So that objectKeys from tsafe return the expected result no matter what.
+ Object.keys(zodParseReturn)
+ .filter(key => !(key in out))
+ .forEach(key => {
+ delete (out as any)[key];
+ });
+ } catch {
+ throw new Error("The output of the Keycloakify Vite plugin do not match the expected schema.");
+ }
+
+ return out;
+ })();
+
+ return { resolvedViteConfig };
+}
diff --git a/src/bin/keycloakify/ftlValuesGlobalName.ts b/src/bin/keycloakify/ftlValuesGlobalName.ts
deleted file mode 100644
index eb63e562..00000000
--- a/src/bin/keycloakify/ftlValuesGlobalName.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const ftlValuesGlobalName = "kcContext";
diff --git a/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl b/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl
index 9a666004..42c36967 100644
--- a/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl
+++ b/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl
@@ -408,6 +408,14 @@
out["themeName"] = "KEYCLOAKIFY_THEME_NAME_cXxKd3xEer";
out["pageId"] = "${pageId}";
+ try {
+
+ out["url"]["resourcesCommonPath"] = out["url"]["resourcesPath"] + "/" + "RESOURCES_COMMON_cLsLsMrtDkpVv";
+
+ } catch(error) {
+
+ }
+
return out;
})()
diff --git a/src/bin/keycloakify/generateFtl/generateFtl.ts b/src/bin/keycloakify/generateFtl/generateFtl.ts
index 6953c6fd..00d8050b 100644
--- a/src/bin/keycloakify/generateFtl/generateFtl.ts
+++ b/src/bin/keycloakify/generateFtl/generateFtl.ts
@@ -1,18 +1,20 @@
import cheerio from "cheerio";
-import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
+import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { generateCssCodeToDefineGlobals } from "../replacers/replaceImportsInCssCode";
import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInlineCssCode";
import * as fs from "fs";
import { join as pathJoin } from "path";
import { objectKeys } from "tsafe/objectKeys";
-import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
-import type { BuildOptions } from "../BuildOptions";
+import type { BuildOptions } from "../buildOptions";
import { assert } from "tsafe/assert";
-import type { ThemeType } from "../../constants";
+import { type ThemeType, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir, resources_common } from "../../constants";
export type BuildOptionsLike = {
+ bundler: "vite" | "webpack";
themeVersion: string;
urlPathname: string | undefined;
+ reactAppBuildDirPath: string;
+ assetsDirPath: string;
};
assert();
@@ -20,7 +22,6 @@ assert();
export function generateFtlFilesCodeFactory(params: {
themeName: string;
indexHtmlCode: string;
- //NOTE: Expected to be an empty object if external assets mode is enabled.
cssGlobalsToDefine: Record;
buildOptions: BuildOptionsLike;
keycloakifyVersion: string;
@@ -37,7 +38,7 @@ export function generateFtlFilesCodeFactory(params: {
assert(jsCode !== null);
- const { fixedJsCode } = replaceImportsFromStaticInJsCode({ jsCode });
+ const { fixedJsCode } = replaceImportsInJsCode({ jsCode, buildOptions });
$(element).text(fixedJsCode);
});
@@ -70,7 +71,10 @@ export function generateFtlFilesCodeFactory(params: {
$(element).attr(
attrName,
- href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
+ href.replace(
+ new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`),
+ `\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/`
+ )
);
})
);
@@ -101,7 +105,8 @@ export function generateFtlFilesCodeFactory(params: {
.replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion)
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion)
.replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType)
- .replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName),
+ .replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName)
+ .replace("RESOURCES_COMMON_cLsLsMrtDkpVv", resources_common),
"": [
"<#if scripts??>",
" <#list scripts as script>",
@@ -114,7 +119,7 @@ export function generateFtlFilesCodeFactory(params: {
$("head").prepend(
[
"",
"",
objectKeys(replaceValueBySearchValue)[1]
diff --git a/src/bin/keycloakify/generateJavaStackFiles/bringInAccountV1.ts b/src/bin/keycloakify/generateJavaStackFiles/bringInAccountV1.ts
deleted file mode 100644
index c41c618f..00000000
--- a/src/bin/keycloakify/generateJavaStackFiles/bringInAccountV1.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import * as fs from "fs";
-import { join as pathJoin, dirname as pathDirname } from "path";
-import { assert } from "tsafe/assert";
-import { Reflect } from "tsafe/Reflect";
-import type { BuildOptions } from "../BuildOptions";
-import { resources_common, lastKeycloakVersionWithAccountV1, accountV1 } from "../../constants";
-import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
-import { transformCodebase } from "../../tools/transformCodebase";
-
-export type BuildOptionsLike = {
- keycloakifyBuildDirPath: string;
- cacheDirPath: string;
-};
-
-{
- const buildOptions = Reflect();
-
- assert();
-}
-
-export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike }) {
- const { buildOptions } = params;
-
- const builtinKeycloakThemeTmpDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "..", "tmp_yxdE2_builtin_keycloak_theme");
-
- await downloadBuiltinKeycloakTheme({
- "destDirPath": builtinKeycloakThemeTmpDirPath,
- "keycloakVersion": lastKeycloakVersionWithAccountV1,
- buildOptions
- });
-
- const accountV1DirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", accountV1, "account");
-
- transformCodebase({
- "srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "account"),
- "destDirPath": accountV1DirPath
- });
-
- const commonResourceFilePaths = [
- "node_modules/patternfly/dist/css/patternfly.min.css",
- "node_modules/patternfly/dist/css/patternfly-additions.min.css",
- ...[
- "OpenSans-Light-webfont.woff2",
- "OpenSans-Regular-webfont.woff2",
- "OpenSans-Bold-webfont.woff2",
- "OpenSans-Semibold-webfont.woff2",
- "OpenSans-Bold-webfont.woff",
- "OpenSans-Light-webfont.woff",
- "OpenSans-Regular-webfont.woff",
- "OpenSans-Semibold-webfont.woff",
- "OpenSans-Regular-webfont.ttf",
- "OpenSans-Light-webfont.ttf",
- "OpenSans-Semibold-webfont.ttf",
- "OpenSans-Bold-webfont.ttf"
- ].map(path => `node_modules/patternfly/dist/fonts/${path}`)
- ];
-
- for (const relativeFilePath of commonResourceFilePaths.map(path => pathJoin(...path.split("/")))) {
- const destFilePath = pathJoin(accountV1DirPath, "resources", resources_common, relativeFilePath);
-
- fs.mkdirSync(pathDirname(destFilePath), { "recursive": true });
-
- fs.cpSync(pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "common", "resources", relativeFilePath), destFilePath);
- }
-
- const resourceFilePaths = ["css/account.css", "img/icon-sidebar-active.png", "img/logo.png"];
-
- for (const relativeFilePath of resourceFilePaths.map(path => pathJoin(...path.split("/")))) {
- const destFilePath = pathJoin(accountV1DirPath, "resources", relativeFilePath);
-
- fs.mkdirSync(pathDirname(destFilePath), { "recursive": true });
-
- fs.cpSync(pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "account", "resources", relativeFilePath), destFilePath);
- }
-
- fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true });
-
- fs.writeFileSync(
- pathJoin(accountV1DirPath, "theme.properties"),
- Buffer.from(
- [
- "accountResourceProvider=account-v1",
- "",
- "locales=ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
- "",
- "styles=" + [...resourceFilePaths, ...commonResourceFilePaths.map(path => `resources-common/${path}`)].join(" "),
- "",
- "##### css classes for form buttons",
- "# main class used for all buttons",
- "kcButtonClass=btn",
- "# classes defining priority of the button - primary or default (there is typically only one priority button for the form)",
- "kcButtonPrimaryClass=btn-primary",
- "kcButtonDefaultClass=btn-default",
- "# classes defining size of the button",
- "kcButtonLargeClass=btn-lg",
- ""
- ].join("\n"),
- "utf8"
- )
- );
-}
diff --git a/src/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.ts b/src/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.ts
deleted file mode 100644
index df7b92ac..00000000
--- a/src/bin/keycloakify/generateJavaStackFiles/generateJavaStackFiles.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import * as fs from "fs";
-import { join as pathJoin, dirname as pathDirname } from "path";
-import { assert } from "tsafe/assert";
-import { Reflect } from "tsafe/Reflect";
-import type { BuildOptions } from "../BuildOptions";
-import { type ThemeType, retrocompatPostfix, accountV1 } from "../../constants";
-import { bringInAccountV1 } from "./bringInAccountV1";
-
-export type BuildOptionsLike = {
- groupId: string;
- artifactId: string;
- themeVersion: string;
- cacheDirPath: string;
- keycloakifyBuildDirPath: string;
- themeNames: string[];
- doBuildRetrocompatAccountTheme: boolean;
-};
-
-{
- const buildOptions = Reflect();
-
- assert();
-}
-
-export async function generateJavaStackFiles(params: {
- implementedThemeTypes: Record;
- buildOptions: BuildOptionsLike;
-}): Promise<{
- jarFilePath: string;
-}> {
- const { implementedThemeTypes, buildOptions } = params;
-
- {
- const { pomFileCode } = (function generatePomFileCode(): {
- pomFileCode: string;
- } {
- const pomFileCode = [
- ``,
- ``,
- ` 4.0.0`,
- ` ${buildOptions.groupId}`,
- ` ${buildOptions.artifactId}`,
- ` ${buildOptions.themeVersion}`,
- ` ${buildOptions.artifactId}`,
- ` `,
- ` jar`,
- ` `,
- ` UTF-8`,
- ` `,
- ` `,
- ` `,
- ` `,
- ` org.apache.maven.plugins`,
- ` maven-shade-plugin`,
- ` 3.5.1`,
- ` `,
- ` `,
- ` package`,
- ` `,
- ` shade`,
- ` `,
- ` `,
- ` `,
- ` `,
- ` `,
- ` `,
- ` `,
- ` `,
- ` io.phasetwo.keycloak`,
- ` keycloak-account-v1`,
- ` 0.1`,
- ` `,
- ` `,
- ``
- ].join("\n");
-
- return { pomFileCode };
- })();
-
- fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
- }
-
- if (implementedThemeTypes.account) {
- await bringInAccountV1({ buildOptions });
- }
-
- {
- const themeManifestFilePath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json");
-
- try {
- fs.mkdirSync(pathDirname(themeManifestFilePath));
- } catch {}
-
- fs.writeFileSync(
- themeManifestFilePath,
- Buffer.from(
- JSON.stringify(
- {
- "themes": [
- ...(!implementedThemeTypes.account
- ? []
- : [
- {
- "name": accountV1,
- "types": ["account"]
- }
- ]),
- ...buildOptions.themeNames
- .map(themeName => [
- {
- "name": themeName,
- "types": Object.entries(implementedThemeTypes)
- .filter(([, isImplemented]) => isImplemented)
- .map(([themeType]) => themeType)
- },
- ...(!implementedThemeTypes.account || !buildOptions.doBuildRetrocompatAccountTheme
- ? []
- : [
- {
- "name": `${themeName}${retrocompatPostfix}`,
- "types": ["account"]
- }
- ])
- ])
- .flat()
- ]
- },
- null,
- 2
- ),
- "utf8"
- )
- );
- }
-
- return {
- "jarFilePath": pathJoin(buildOptions.keycloakifyBuildDirPath, "target", `${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`)
- };
-}
diff --git a/src/bin/keycloakify/generateJavaStackFiles/index.ts b/src/bin/keycloakify/generateJavaStackFiles/index.ts
deleted file mode 100644
index ea372c91..00000000
--- a/src/bin/keycloakify/generateJavaStackFiles/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./generateJavaStackFiles";
diff --git a/src/bin/keycloakify/generatePom.ts b/src/bin/keycloakify/generatePom.ts
new file mode 100644
index 00000000..555382ab
--- /dev/null
+++ b/src/bin/keycloakify/generatePom.ts
@@ -0,0 +1,70 @@
+import { assert } from "tsafe/assert";
+import { Reflect } from "tsafe/Reflect";
+import type { BuildOptions } from "./buildOptions";
+
+type BuildOptionsLike = {
+ groupId: string;
+ artifactId: string;
+ themeVersion: string;
+ keycloakifyBuildDirPath: string;
+};
+
+{
+ const buildOptions = Reflect();
+
+ assert();
+}
+
+export function generatePom(params: { buildOptions: BuildOptionsLike }) {
+ const { buildOptions } = params;
+
+ const { pomFileCode } = (function generatePomFileCode(): {
+ pomFileCode: string;
+ } {
+ const pomFileCode = [
+ ``,
+ ``,
+ ` 4.0.0`,
+ ` ${buildOptions.groupId}`,
+ ` ${buildOptions.artifactId}`,
+ ` ${buildOptions.themeVersion}`,
+ ` ${buildOptions.artifactId}`,
+ ` `,
+ ` jar`,
+ ` `,
+ ` UTF-8`,
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ ` org.apache.maven.plugins`,
+ ` maven-shade-plugin`,
+ ` 3.5.1`,
+ ` `,
+ ` `,
+ ` package`,
+ ` `,
+ ` shade`,
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ ` `,
+ ` io.phasetwo.keycloak`,
+ ` keycloak-account-v1`,
+ ` 0.1`,
+ ` `,
+ ` `,
+ ``
+ ].join("\n");
+
+ return { pomFileCode };
+ })();
+
+ return { pomFileCode };
+}
diff --git a/src/bin/keycloakify/generateStartKeycloakTestingContainer.ts b/src/bin/keycloakify/generateStartKeycloakTestingContainer.ts
index a14bfe49..045ce29a 100644
--- a/src/bin/keycloakify/generateStartKeycloakTestingContainer.ts
+++ b/src/bin/keycloakify/generateStartKeycloakTestingContainer.ts
@@ -2,7 +2,7 @@ import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
-import type { BuildOptions } from "./BuildOptions";
+import type { BuildOptions } from "./buildOptions";
export type BuildOptionsLike = {
keycloakifyBuildDirPath: string;
@@ -30,7 +30,6 @@ export function generateStartKeycloakTestingContainer(params: { jarFilePath: str
Buffer.from(
[
"#!/usr/bin/env bash",
- `# If you want to test with Keycloak version prior to 23 use the retrocompat-${pathBasename(jarFilePath)}`,
"",
`docker rm ${containerName} || true`,
"",
diff --git a/src/bin/keycloakify/generateTheme/bringInAccountV1.ts b/src/bin/keycloakify/generateTheme/bringInAccountV1.ts
new file mode 100644
index 00000000..5fba86d8
--- /dev/null
+++ b/src/bin/keycloakify/generateTheme/bringInAccountV1.ts
@@ -0,0 +1,84 @@
+import * as fs from "fs";
+import { join as pathJoin } from "path";
+import { assert } from "tsafe/assert";
+import { Reflect } from "tsafe/Reflect";
+import type { BuildOptions } from "../buildOptions";
+import { resources_common, lastKeycloakVersionWithAccountV1, accountV1ThemeName } from "../../constants";
+import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
+import { transformCodebase } from "../../tools/transformCodebase";
+import { rmSync } from "../../tools/fs.rmSync";
+
+type BuildOptionsLike = {
+ keycloakifyBuildDirPath: string;
+ cacheDirPath: string;
+ npmWorkspaceRootDirPath: string;
+};
+
+{
+ const buildOptions = Reflect();
+
+ assert();
+}
+
+export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike }) {
+ const { buildOptions } = params;
+
+ const builtinKeycloakThemeTmpDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "..", "tmp_yxdE2_builtin_keycloak_theme");
+
+ await downloadBuiltinKeycloakTheme({
+ "destDirPath": builtinKeycloakThemeTmpDirPath,
+ "keycloakVersion": lastKeycloakVersionWithAccountV1,
+ buildOptions
+ });
+
+ const accountV1DirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", accountV1ThemeName, "account");
+
+ transformCodebase({
+ "srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "account"),
+ "destDirPath": accountV1DirPath
+ });
+
+ transformCodebase({
+ "srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "account", "resources"),
+ "destDirPath": pathJoin(accountV1DirPath, "resources")
+ });
+
+ transformCodebase({
+ "srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "common", "resources"),
+ "destDirPath": pathJoin(accountV1DirPath, "resources", resources_common)
+ });
+
+ rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true });
+
+ fs.writeFileSync(
+ pathJoin(accountV1DirPath, "theme.properties"),
+ Buffer.from(
+ [
+ "accountResourceProvider=account-v1",
+ "",
+ "locales=ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
+ "",
+ "styles=" +
+ [
+ "css/account.css",
+ "img/icon-sidebar-active.png",
+ "img/logo.png",
+ ...["patternfly.min.css", "patternfly-additions.min.css", "patternfly-additions.min.css"].map(
+ fileBasename => `${resources_common}/node_modules/patternfly/dist/css/${fileBasename}`
+ )
+ ].join(" "),
+ "",
+ "##### css classes for form buttons",
+ "# main class used for all buttons",
+ "kcButtonClass=btn",
+ "# classes defining priority of the button - primary or default (there is typically only one priority button for the form)",
+ "kcButtonPrimaryClass=btn-primary",
+ "kcButtonDefaultClass=btn-default",
+ "# classes defining size of the button",
+ "kcButtonLargeClass=btn-lg",
+ ""
+ ].join("\n"),
+ "utf8"
+ )
+ );
+}
diff --git a/src/bin/keycloakify/generateTheme/downloadKeycloakStaticResources.ts b/src/bin/keycloakify/generateTheme/downloadKeycloakStaticResources.ts
index 7a4f128e..811df236 100644
--- a/src/bin/keycloakify/generateTheme/downloadKeycloakStaticResources.ts
+++ b/src/bin/keycloakify/generateTheme/downloadKeycloakStaticResources.ts
@@ -1,57 +1,27 @@
import { transformCodebase } from "../../tools/transformCodebase";
-import * as fs from "fs";
-import { join as pathJoin, dirname as pathDirname } from "path";
+import { join as pathJoin } from "path";
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
import { resources_common, type ThemeType } from "../../constants";
-import { BuildOptions } from "../BuildOptions";
+import { BuildOptions } from "../buildOptions";
import { assert } from "tsafe/assert";
import * as crypto from "crypto";
+import { rmSync } from "../../tools/fs.rmSync";
export type BuildOptionsLike = {
cacheDirPath: string;
+ npmWorkspaceRootDirPath: string;
};
assert();
-export async function downloadKeycloakStaticResources(
- // prettier-ignore
- params: {
- themeType: ThemeType;
- themeDirPath: string;
- keycloakVersion: string;
- usedResources: {
- resourcesCommonFilePaths: string[];
- } | undefined;
- buildOptions: BuildOptionsLike;
- }
-) {
+export async function downloadKeycloakStaticResources(params: {
+ themeType: ThemeType;
+ themeDirPath: string;
+ keycloakVersion: string;
+ buildOptions: BuildOptionsLike;
+}) {
const { themeType, themeDirPath, keycloakVersion, buildOptions } = params;
- // NOTE: Hack for 427
- const usedResources = (() => {
- const { usedResources } = params;
-
- if (usedResources === undefined) {
- return undefined;
- }
-
- assert(usedResources !== undefined);
-
- return {
- "resourcesCommonDirPaths": usedResources.resourcesCommonFilePaths.map(filePath => {
- {
- const splitArg = "/dist/";
-
- if (filePath.includes(splitArg)) {
- return filePath.split(splitArg)[0] + splitArg;
- }
- }
-
- return pathDirname(filePath);
- })
- };
- })();
-
const tmpDirPath = pathJoin(
themeDirPath,
`tmp_suLeKsxId_${crypto.createHash("sha256").update(`${themeType}-${keycloakVersion}`).digest("hex").slice(0, 8)}`
@@ -72,18 +42,8 @@ export async function downloadKeycloakStaticResources(
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
- "destDirPath": pathJoin(resourcesPath, resources_common),
- "transformSourceCode":
- usedResources === undefined
- ? undefined
- : ({ fileRelativePath, sourceCode }) => {
- if (usedResources.resourcesCommonDirPaths.find(dirPath => fileRelativePath.startsWith(dirPath)) === undefined) {
- return undefined;
- }
-
- return { "modifiedSourceCode": sourceCode };
- }
+ "destDirPath": pathJoin(resourcesPath, resources_common)
});
- fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
+ rmSync(tmpDirPath, { "recursive": true, "force": true });
}
diff --git a/src/bin/keycloakify/generateTheme/generateTheme.ts b/src/bin/keycloakify/generateTheme/generateTheme.ts
index b0e9ef84..c39ada7c 100644
--- a/src/bin/keycloakify/generateTheme/generateTheme.ts
+++ b/src/bin/keycloakify/generateTheme/generateTheme.ts
@@ -1,28 +1,40 @@
import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs";
-import { join as pathJoin, basename as pathBasename, resolve as pathResolve } from "path";
-import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
+import { join as pathJoin, basename as pathBasename, resolve as pathResolve, dirname as pathDirname } from "path";
+import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds } from "../generateFtl";
-import { themeTypes, type ThemeType, lastKeycloakVersionWithAccountV1, keycloak_resources, retrocompatPostfix, accountV1 } from "../../constants";
+import {
+ type ThemeType,
+ lastKeycloakVersionWithAccountV1,
+ keycloak_resources,
+ retrocompatPostfix,
+ accountV1ThemeName,
+ basenameOfTheKeycloakifyResourcesDir
+} from "../../constants";
import { isInside } from "../../tools/isInside";
-import type { BuildOptions } from "../BuildOptions";
+import type { BuildOptions } from "../buildOptions";
import { assert, type Equals } from "tsafe/assert";
import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResources";
import { readFieldNameUsage } from "./readFieldNameUsage";
import { readExtraPagesNames } from "./readExtraPageNames";
import { generateMessageProperties } from "./generateMessageProperties";
-import { readStaticResourcesUsage } from "./readStaticResourcesUsage";
+import { bringInAccountV1 } from "./bringInAccountV1";
+import { rmSync } from "../../tools/fs.rmSync";
export type BuildOptionsLike = {
+ bundler: "vite" | "webpack";
extraThemeProperties: string[] | undefined;
themeVersion: string;
loginThemeResourcesFromKeycloakVersion: string;
- urlPathname: string | undefined;
keycloakifyBuildDirPath: string;
reactAppBuildDirPath: string;
cacheDirPath: string;
+ assetsDirPath: string;
+ urlPathname: string | undefined;
doBuildRetrocompatAccountTheme: boolean;
+ themeNames: string[];
+ npmWorkspaceRootDirPath: string;
};
assert();
@@ -49,29 +61,52 @@ export async function generateTheme(params: {
);
};
- let allCssGlobalsToDefine: Record = {};
+ const cssGlobalsToDefine: Record = {};
- let generateFtlFilesCode_glob: ReturnType["generateFtlFilesCode"] | undefined = undefined;
+ const implementedThemeTypes: Record = {
+ "login": false,
+ "account": false,
+ "email": false
+ };
- for (const themeType of themeTypes) {
+ for (const themeType of ["login", "account"] as const) {
if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) {
continue;
}
+ implementedThemeTypes[themeType] = true;
+
const themeTypeDirPath = getThemeTypeDirPath({ themeType });
- copy_app_resources_to_theme_path: {
- const isFirstPass = themeType.indexOf(themeType) === 0;
+ apply_replacers_and_move_to_theme_resources: {
+ const destDirPath = pathJoin(themeTypeDirPath, "resources", basenameOfTheKeycloakifyResourcesDir);
- if (!isFirstPass) {
- break copy_app_resources_to_theme_path;
+ // NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
+ rmSync(destDirPath, { "recursive": true, "force": true });
+
+ if (themeType === "account" && implementedThemeTypes.login) {
+ // NOTE: We prevend doing it twice, it has been done for the login theme.
+
+ transformCodebase({
+ "srcDirPath": pathJoin(
+ getThemeTypeDirPath({
+ "themeType": "login"
+ }),
+ "resources",
+ basenameOfTheKeycloakifyResourcesDir
+ ),
+ destDirPath
+ });
+
+ break apply_replacers_and_move_to_theme_resources;
}
transformCodebase({
- "destDirPath": pathJoin(themeTypeDirPath, "resources", "build"),
"srcDirPath": buildOptions.reactAppBuildDirPath,
+ destDirPath,
"transformSourceCode": ({ filePath, sourceCode }) => {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
+ // This should not happen if users follow the new instruction setup but we keep it for retrocompatibility.
if (
isInside({
"dirPath": pathJoin(buildOptions.reactAppBuildDirPath, keycloak_resources),
@@ -82,27 +117,21 @@ export async function generateTheme(params: {
}
if (/\.css?$/i.test(filePath)) {
- const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
+ const { cssGlobalsToDefine: cssGlobalsToDefineForThisFile, fixedCssCode } = replaceImportsInCssCode({
"cssCode": sourceCode.toString("utf8")
});
- register_css_variables: {
- if (!isFirstPass) {
- break register_css_variables;
- }
-
- allCssGlobalsToDefine = {
- ...allCssGlobalsToDefine,
- ...cssGlobalsToDefine
- };
- }
+ Object.entries(cssGlobalsToDefineForThisFile).forEach(([key, value]) => {
+ cssGlobalsToDefine[key] = value;
+ });
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
}
if (/\.js?$/i.test(filePath)) {
- const { fixedJsCode } = replaceImportsFromStaticInJsCode({
- "jsCode": sourceCode.toString("utf8")
+ const { fixedJsCode } = replaceImportsInJsCode({
+ "jsCode": sourceCode.toString("utf8"),
+ buildOptions
});
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
@@ -113,22 +142,19 @@ export async function generateTheme(params: {
});
}
- const generateFtlFilesCode =
- generateFtlFilesCode_glob !== undefined
- ? generateFtlFilesCode_glob
- : generateFtlFilesCodeFactory({
- themeName,
- "indexHtmlCode": fs.readFileSync(pathJoin(buildOptions.reactAppBuildDirPath, "index.html")).toString("utf8"),
- "cssGlobalsToDefine": allCssGlobalsToDefine,
- buildOptions,
- keycloakifyVersion,
- themeType,
- "fieldNames": readFieldNameUsage({
- keycloakifySrcDirPath,
- themeSrcDirPath,
- themeType
- })
- }).generateFtlFilesCode;
+ const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
+ themeName,
+ "indexHtmlCode": fs.readFileSync(pathJoin(buildOptions.reactAppBuildDirPath, "index.html")).toString("utf8"),
+ cssGlobalsToDefine,
+ buildOptions,
+ keycloakifyVersion,
+ themeType,
+ "fieldNames": readFieldNameUsage({
+ keycloakifySrcDirPath,
+ themeSrcDirPath,
+ themeType
+ })
+ });
[
...(() => {
@@ -175,11 +201,6 @@ export async function generateTheme(params: {
})(),
"themeDirPath": pathResolve(pathJoin(themeTypeDirPath, "..")),
themeType,
- "usedResources": readStaticResourcesUsage({
- keycloakifySrcDirPath,
- themeSrcDirPath,
- themeType
- }),
buildOptions
});
@@ -190,7 +211,7 @@ export async function generateTheme(params: {
`parent=${(() => {
switch (themeType) {
case "account":
- return accountV1;
+ return accountV1ThemeName;
case "login":
return "keycloak";
}
@@ -209,7 +230,10 @@ export async function generateTheme(params: {
"transformSourceCode": ({ filePath, sourceCode }) => {
if (pathBasename(filePath) === "theme.properties") {
return {
- "modifiedSourceCode": Buffer.from(sourceCode.toString("utf8").replace(`parent=${accountV1}`, "parent=keycloak"), "utf8")
+ "modifiedSourceCode": Buffer.from(
+ sourceCode.toString("utf8").replace(`parent=${accountV1ThemeName}`, "parent=keycloak"),
+ "utf8"
+ )
};
}
@@ -226,9 +250,82 @@ export async function generateTheme(params: {
break email;
}
+ implementedThemeTypes.email = true;
+
transformCodebase({
"srcDirPath": emailThemeSrcDirPath,
"destDirPath": getThemeTypeDirPath({ "themeType": "email" })
});
}
+
+ const parsedKeycloakThemeJson: { themes: { name: string; types: string[] }[] } = { "themes": [] };
+
+ buildOptions.themeNames.forEach(themeName =>
+ parsedKeycloakThemeJson.themes.push({
+ "name": themeName,
+ "types": Object.entries(implementedThemeTypes)
+ .filter(([, isImplemented]) => isImplemented)
+ .map(([themeType]) => themeType)
+ })
+ );
+
+ account_specific_extra_work: {
+ if (!implementedThemeTypes.account) {
+ break account_specific_extra_work;
+ }
+
+ await bringInAccountV1({ buildOptions });
+
+ parsedKeycloakThemeJson.themes.push({
+ "name": accountV1ThemeName,
+ "types": ["account"]
+ });
+
+ add_retrocompat_account_theme: {
+ if (!buildOptions.doBuildRetrocompatAccountTheme) {
+ break add_retrocompat_account_theme;
+ }
+
+ transformCodebase({
+ "srcDirPath": getThemeTypeDirPath({ "themeType": "account" }),
+ "destDirPath": getThemeTypeDirPath({ "themeType": "account", "isRetrocompat": true }),
+ "transformSourceCode": ({ filePath, sourceCode }) => {
+ if (pathBasename(filePath) === "theme.properties") {
+ return {
+ "modifiedSourceCode": Buffer.from(
+ sourceCode.toString("utf8").replace(`parent=${accountV1ThemeName}`, "parent=keycloak"),
+ "utf8"
+ )
+ };
+ }
+
+ return { "modifiedSourceCode": sourceCode };
+ }
+ });
+
+ buildOptions.themeNames.forEach(themeName =>
+ parsedKeycloakThemeJson.themes.push({
+ "name": `${themeName}${retrocompatPostfix}`,
+ "types": ["account"]
+ })
+ );
+ }
+ }
+
+ {
+ const keycloakThemeJsonFilePath = pathJoin(
+ buildOptions.keycloakifyBuildDirPath,
+ "src",
+ "main",
+ "resources",
+ "META-INF",
+ "keycloak-themes.json"
+ );
+
+ try {
+ fs.mkdirSync(pathDirname(keycloakThemeJsonFilePath));
+ } catch {}
+
+ fs.writeFileSync(keycloakThemeJsonFilePath, Buffer.from(JSON.stringify(parsedKeycloakThemeJson, null, 2), "utf8"));
+ }
}
diff --git a/src/bin/keycloakify/generateTheme/readStaticResourcesUsage.ts b/src/bin/keycloakify/generateTheme/readStaticResourcesUsage.ts
deleted file mode 100644
index ea62bff6..00000000
--- a/src/bin/keycloakify/generateTheme/readStaticResourcesUsage.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { crawl } from "../../tools/crawl";
-import { join as pathJoin, sep as pathSep } from "path";
-import * as fs from "fs";
-import type { ThemeType } from "../../constants";
-
-/** Assumes the theme type exists */
-export function readStaticResourcesUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): {
- resourcesCommonFilePaths: string[];
-} {
- const { keycloakifySrcDirPath, themeSrcDirPath, themeType } = params;
-
- const resourcesCommonFilePaths = new Set();
-
- for (const srcDirPath of [pathJoin(keycloakifySrcDirPath, themeType), pathJoin(themeSrcDirPath, themeType)]) {
- const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
-
- for (const filePath of filePaths) {
- const rawSourceFile = fs.readFileSync(filePath).toString("utf8");
-
- if (!rawSourceFile.includes("resourcesCommonPath") && !rawSourceFile.includes("resourcesPath")) {
- continue;
- }
-
- const wrap = readPaths({ rawSourceFile });
-
- wrap.resourcesCommonFilePaths.forEach(filePath => resourcesCommonFilePaths.add(filePath));
- }
- }
-
- return {
- "resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths)
- };
-}
-
-/** Exported for testing purpose */
-export function readPaths(params: { rawSourceFile: string }): {
- resourcesCommonFilePaths: string[];
-} {
- const { rawSourceFile } = params;
-
- const resourcesCommonFilePaths = new Set();
-
- {
- const regexp = new RegExp(`resourcesCommonPath\\s*}([^\`]+)\``, "g");
-
- const matches = [...rawSourceFile.matchAll(regexp)];
-
- for (const match of matches) {
- const filePath = match[1];
-
- resourcesCommonFilePaths.add(filePath);
- }
- }
-
- {
- const regexp = new RegExp(`resourcesCommonPath\\s*[+,]\\s*["']([^"'\`]+)["'\`]`, "g");
-
- const matches = [...rawSourceFile.matchAll(regexp)];
-
- for (const match of matches) {
- const filePath = match[1];
-
- resourcesCommonFilePaths.add(filePath);
- }
- }
-
- const normalizePath = (filePath: string) => {
- filePath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
- filePath = filePath.replace(/\//g, pathSep);
- return filePath;
- };
-
- return {
- "resourcesCommonFilePaths": Array.from(resourcesCommonFilePaths).map(normalizePath)
- };
-}
diff --git a/src/bin/keycloakify/keycloakify.ts b/src/bin/keycloakify/keycloakify.ts
index 3fd541e0..3022cdf9 100644
--- a/src/bin/keycloakify/keycloakify.ts
+++ b/src/bin/keycloakify/keycloakify.ts
@@ -1,66 +1,42 @@
import { generateTheme } from "./generateTheme";
-import { generateJavaStackFiles } from "./generateJavaStackFiles";
+import { generatePom } from "./generatePom";
import { join as pathJoin, relative as pathRelative, basename as pathBasename, dirname as pathDirname, sep as pathSep } from "path";
import * as child_process from "child_process";
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
import * as fs from "fs";
-import { readBuildOptions } from "./BuildOptions";
+import { readBuildOptions } from "./buildOptions";
import { getLogger } from "../tools/logger";
-import { assert } from "tsafe/assert";
-import { getThemeSrcDirPath } from "../getSrcDirPath";
-import { getProjectRoot } from "../tools/getProjectRoot";
-import { objectKeys } from "tsafe/objectKeys";
+import { getThemeSrcDirPath } from "../getThemeSrcDirPath";
+import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
+import { readThisNpmProjectVersion } from "../tools/readThisNpmProjectVersion";
export async function main() {
- const reactAppRootDirPath = process.cwd();
-
const buildOptions = readBuildOptions({
- reactAppRootDirPath,
"processArgv": process.argv.slice(2)
});
const logger = getLogger({ "isSilent": buildOptions.isSilent });
logger.log("🔏 Building the keycloak theme...⌚");
- const keycloakifyDirPath = getProjectRoot();
-
- const { themeSrcDirPath } = getThemeSrcDirPath({ reactAppRootDirPath });
+ const { themeSrcDirPath } = getThemeSrcDirPath({ "reactAppRootDirPath": buildOptions.reactAppRootDirPath });
for (const themeName of buildOptions.themeNames) {
await generateTheme({
themeName,
themeSrcDirPath,
- "keycloakifySrcDirPath": pathJoin(keycloakifyDirPath, "src"),
- buildOptions,
- "keycloakifyVersion": (() => {
- const version = JSON.parse(fs.readFileSync(pathJoin(keycloakifyDirPath, "package.json")).toString("utf8"))["version"];
-
- assert(typeof version === "string");
-
- return version;
- })()
+ "keycloakifySrcDirPath": pathJoin(getThisCodebaseRootDirPath(), "src"),
+ "keycloakifyVersion": readThisNpmProjectVersion(),
+ buildOptions
});
}
- const { jarFilePath } = await generateJavaStackFiles({
- "implementedThemeTypes": (() => {
- const implementedThemeTypes = {
- "login": false,
- "account": false,
- "email": false
- };
+ {
+ const { pomFileCode } = generatePom({ buildOptions });
- for (const themeType of objectKeys(implementedThemeTypes)) {
- if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) {
- continue;
- }
- implementedThemeTypes[themeType] = true;
- }
+ fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
+ }
- return implementedThemeTypes;
- })(),
- buildOptions
- });
+ const jarFilePath = pathJoin(buildOptions.keycloakifyBuildDirPath, "target", `${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`);
if (buildOptions.doCreateJar) {
child_process.execSync("mvn clean install", { "cwd": buildOptions.keycloakifyBuildDirPath });
@@ -83,7 +59,7 @@ export async function main() {
);
}
- const containerKeycloakVersion = "23.0.0";
+ const containerKeycloakVersion = "23.0.6";
generateStartKeycloakTestingContainer({
"keycloakVersion": containerKeycloakVersion,
@@ -91,53 +67,26 @@ export async function main() {
buildOptions
});
+ fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, ".gitignore"), Buffer.from("*", "utf8"));
+
logger.log(
[
"",
...(!buildOptions.doCreateJar
? []
: [
- `✅ Your keycloak theme has been generated and bundled into .${pathSep}${pathRelative(reactAppRootDirPath, jarFilePath)} 🚀`,
- `It is to be placed in "/opt/keycloak/providers" in the container running a quay.io/keycloak/keycloak Docker image.`,
- ""
+ `✅ Your keycloak theme has been generated and bundled into .${pathSep}${pathRelative(
+ buildOptions.reactAppRootDirPath,
+ jarFilePath
+ )} 🚀`
]),
- //TODO: Restore when we find a good Helm chart for Keycloak.
- //"Using Helm (https://github.com/codecentric/helm-charts), edit to reflect:",
- "",
- "value.yaml: ",
- " extraInitContainers: |",
- " - name: realm-ext-provider",
- " image: curlimages/curl",
- " imagePullPolicy: IfNotPresent",
- " command:",
- " - sh",
- " args:",
- " - -c",
- ` - curl -L -f -S -o /extensions/${pathBasename(jarFilePath)} https://AN.URL.FOR/${pathBasename(jarFilePath)}`,
- " volumeMounts:",
- " - name: extensions",
- " mountPath: /extensions",
- " ",
- " extraVolumeMounts: |",
- " - name: extensions",
- " mountPath: /opt/keycloak/providers",
- " extraEnv: |",
- " - name: KEYCLOAK_USER",
- " value: admin",
- " - name: KEYCLOAK_PASSWORD",
- " value: xxxxxxxxx",
- " - name: JAVA_OPTS",
- " value: -Dkeycloak.profile=preview",
- "",
"",
`To test your theme locally you can spin up a Keycloak ${containerKeycloakVersion} container image with the theme pre loaded by running:`,
"",
`👉 $ .${pathSep}${pathRelative(
- reactAppRootDirPath,
+ buildOptions.reactAppRootDirPath,
pathJoin(buildOptions.keycloakifyBuildDirPath, generateStartKeycloakTestingContainer.basename)
)} 👈`,
- "",
- `Test with different Keycloak versions by editing the .sh file. see available versions here: https://quay.io/repository/keycloak/keycloak?tab=tags`,
``,
`Once your container is up and running: `,
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
diff --git a/src/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode.ts b/src/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode.ts
deleted file mode 100644
index 512dcb3c..00000000
--- a/src/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
-
-export function replaceImportsFromStaticInJsCode(params: { jsCode: string }): { fixedJsCode: string } {
- /*
- NOTE:
-
- When we have urlOrigin defined it means that
- we are building with --external-assets
- so we have to make sur that the fixed js code will run
- inside and outside keycloak.
-
- When urlOrigin isn't defined we can assume the fixedJsCode
- will always run in keycloak context.
- */
-
- const { jsCode } = params;
-
- const getReplaceArgs = (language: "js" | "css"): Parameters => [
- new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=(function\\(([a-z]+)\\){return|([a-z]+)=>)"static\\/${language}\\/"`, "g"),
- (...[, n, u, matchedFunction, eForFunction]) => {
- const isArrowFunction = matchedFunction.includes("=>");
- const e = isArrowFunction ? matchedFunction.replace("=>", "").trim() : eForFunction;
-
- return `
- ${n}[(function(){
- var pd = Object.getOwnPropertyDescriptor(${n}, "p");
- if( pd === undefined || pd.configurable ){
- Object.defineProperty(${n}, "p", {
- get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
- set: function() {}
- });
- }
- return "${u}";
- })()] = ${isArrowFunction ? `${e} =>` : `function(${e}) { return `} "/build/static/${language}/"`
- .replace(/\s+/g, " ")
- .trim();
- }
- ];
-
- const fixedJsCode = jsCode
- .replace(...getReplaceArgs("js"))
- .replace(...getReplaceArgs("css"))
- .replace(/[a-zA-Z]+\.[a-zA-Z]+\+"static\//g, `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`)
- //TODO: Write a test case for this
- .replace(
- /".chunk.css",([a-zA-Z])+=[a-zA-Z]+\.[a-zA-Z]+\+([a-zA-Z]+),/,
- (...[, group1, group2]) => `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group2},`
- );
-
- return { fixedJsCode };
-}
diff --git a/src/bin/keycloakify/replacers/replaceImportsInCssCode.ts b/src/bin/keycloakify/replacers/replaceImportsInCssCode.ts
index de53e6f9..ae271970 100644
--- a/src/bin/keycloakify/replacers/replaceImportsInCssCode.ts
+++ b/src/bin/keycloakify/replacers/replaceImportsInCssCode.ts
@@ -1,6 +1,7 @@
import * as crypto from "crypto";
-import type { BuildOptions } from "../BuildOptions";
+import type { BuildOptions } from "../buildOptions";
import { assert } from "tsafe/assert";
+import { basenameOfTheKeycloakifyResourcesDir } from "../../constants";
export type BuildOptionsLike = {
urlPathname: string | undefined;
@@ -45,7 +46,7 @@ export function generateCssCodeToDefineGlobals(params: { cssGlobalsToDefine: Rec
`--${cssVariableName}:`,
cssGlobalsToDefine[cssVariableName].replace(
new RegExp(`url\\(${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`, "g"),
- "url(${url.resourcesPath}/build/"
+ `url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/`
)
].join(" ")
)
diff --git a/src/bin/keycloakify/replacers/replaceImportsInInlineCssCode.ts b/src/bin/keycloakify/replacers/replaceImportsInInlineCssCode.ts
index 88b3e0e8..3bb52c12 100644
--- a/src/bin/keycloakify/replacers/replaceImportsInInlineCssCode.ts
+++ b/src/bin/keycloakify/replacers/replaceImportsInInlineCssCode.ts
@@ -1,5 +1,6 @@
-import type { BuildOptions } from "../BuildOptions";
+import type { BuildOptions } from "../buildOptions";
import { assert } from "tsafe/assert";
+import { basenameOfTheKeycloakifyResourcesDir } from "../../constants";
export type BuildOptionsLike = {
urlPathname: string | undefined;
@@ -16,7 +17,7 @@ export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOp
buildOptions.urlPathname === undefined
? /url\(["']?\/([^/][^)"']+)["']?\)/g
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
- (...[, group]) => `url(\${url.resourcesPath}/build/${group})`
+ (...[, group]) => `url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/${group})`
);
return { fixedCssCode };
diff --git a/src/bin/keycloakify/replacers/replaceImportsInJsCode/index.ts b/src/bin/keycloakify/replacers/replaceImportsInJsCode/index.ts
new file mode 100644
index 00000000..93784124
--- /dev/null
+++ b/src/bin/keycloakify/replacers/replaceImportsInJsCode/index.ts
@@ -0,0 +1 @@
+export * from "./replaceImportsInJsCode";
diff --git a/src/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.ts b/src/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.ts
new file mode 100644
index 00000000..30bcad9b
--- /dev/null
+++ b/src/bin/keycloakify/replacers/replaceImportsInJsCode/replaceImportsInJsCode.ts
@@ -0,0 +1,66 @@
+import { assert } from "tsafe/assert";
+import type { BuildOptions } from "../../buildOptions";
+import { replaceImportsInJsCode_vite } from "./vite";
+import { replaceImportsInJsCode_webpack } from "./webpack";
+import * as fs from "fs";
+
+export type BuildOptionsLike = {
+ reactAppBuildDirPath: string;
+ assetsDirPath: string;
+ urlPathname: string | undefined;
+ bundler: "vite" | "webpack";
+};
+
+assert();
+
+export function replaceImportsInJsCode(params: { jsCode: string; buildOptions: BuildOptionsLike }) {
+ const { jsCode, buildOptions } = params;
+
+ const { fixedJsCode } = (() => {
+ switch (buildOptions.bundler) {
+ case "vite":
+ return replaceImportsInJsCode_vite({
+ jsCode,
+ buildOptions,
+ "basenameOfAssetsFiles": readAssetsDirSync({
+ "assetsDirPath": params.buildOptions.assetsDirPath
+ })
+ });
+ case "webpack":
+ return replaceImportsInJsCode_webpack({
+ jsCode,
+ buildOptions
+ });
+ }
+ })();
+
+ return { fixedJsCode };
+}
+
+const { readAssetsDirSync } = (() => {
+ let cache:
+ | {
+ assetsDirPath: string;
+ basenameOfAssetsFiles: string[];
+ }
+ | undefined = undefined;
+
+ function readAssetsDirSync(params: { assetsDirPath: string }): string[] {
+ const { assetsDirPath } = params;
+
+ if (cache !== undefined && cache.assetsDirPath === assetsDirPath) {
+ return cache.basenameOfAssetsFiles;
+ }
+
+ const basenameOfAssetsFiles = fs.readdirSync(assetsDirPath);
+
+ cache = {
+ assetsDirPath,
+ basenameOfAssetsFiles
+ };
+
+ return basenameOfAssetsFiles;
+ }
+
+ return { readAssetsDirSync };
+})();
diff --git a/src/bin/keycloakify/replacers/replaceImportsInJsCode/vite.ts b/src/bin/keycloakify/replacers/replaceImportsInJsCode/vite.ts
new file mode 100644
index 00000000..9a60aae2
--- /dev/null
+++ b/src/bin/keycloakify/replacers/replaceImportsInJsCode/vite.ts
@@ -0,0 +1,85 @@
+import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../../constants";
+import { assert } from "tsafe/assert";
+import type { BuildOptions } from "../../buildOptions";
+import * as nodePath from "path";
+import { replaceAll } from "../../../tools/String.prototype.replaceAll";
+
+export type BuildOptionsLike = {
+ reactAppBuildDirPath: string;
+ assetsDirPath: string;
+ urlPathname: string | undefined;
+};
+
+assert();
+
+export function replaceImportsInJsCode_vite(params: {
+ jsCode: string;
+ buildOptions: BuildOptionsLike;
+ basenameOfAssetsFiles: string[];
+ systemType?: "posix" | "win32";
+}): {
+ fixedJsCode: string;
+} {
+ const { jsCode, buildOptions, basenameOfAssetsFiles, systemType = nodePath.sep === "/" ? "posix" : "win32" } = params;
+
+ const { relative: pathRelative, sep: pathSep } = nodePath[systemType];
+
+ let fixedJsCode = jsCode;
+
+ replace_base_javacript_import: {
+ if (buildOptions.urlPathname === undefined) {
+ break replace_base_javacript_import;
+ }
+ // Optimization
+ if (!jsCode.includes(buildOptions.urlPathname)) {
+ break replace_base_javacript_import;
+ }
+
+ // Replace `Hv=function(e){return"/abcde12345/"+e}` by `Hv=function(e){return"/"+e}`
+ fixedJsCode = fixedJsCode.replace(
+ new RegExp(
+ `([\\w\\$][\\w\\d\\$]*)=function\\(([\\w\\$][\\w\\d\\$]*)\\)\\{return"${replaceAll(buildOptions.urlPathname, "/", "\\/")}"\\+\\2\\}`,
+ "g"
+ ),
+ (...[, funcName, paramName]) => `${funcName}=function(${paramName}){return"/"+${paramName}}`
+ );
+ }
+
+ replace_javascript_relatives_import_paths: {
+ // Example: "assets/ or "foo/bar/"
+ const staticDir = (() => {
+ let out = pathRelative(buildOptions.reactAppBuildDirPath, buildOptions.assetsDirPath);
+
+ out = replaceAll(out, pathSep, "/") + "/";
+
+ if (out === "/") {
+ throw new Error(`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`);
+ }
+
+ return out;
+ })();
+
+ // Optimization
+ if (!jsCode.includes(staticDir)) {
+ break replace_javascript_relatives_import_paths;
+ }
+
+ basenameOfAssetsFiles
+ .map(basenameOfAssetsFile => `${staticDir}${basenameOfAssetsFile}`)
+ .forEach(relativePathOfAssetFile => {
+ fixedJsCode = replaceAll(
+ fixedJsCode,
+ `"${relativePathOfAssetFile}"`,
+ `(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")`
+ );
+
+ fixedJsCode = replaceAll(
+ fixedJsCode,
+ `"${buildOptions.urlPathname ?? "/"}${relativePathOfAssetFile}"`,
+ `(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")`
+ );
+ });
+ }
+
+ return { fixedJsCode };
+}
diff --git a/src/bin/keycloakify/replacers/replaceImportsInJsCode/webpack.ts b/src/bin/keycloakify/replacers/replaceImportsInJsCode/webpack.ts
new file mode 100644
index 00000000..74a02f5d
--- /dev/null
+++ b/src/bin/keycloakify/replacers/replaceImportsInJsCode/webpack.ts
@@ -0,0 +1,76 @@
+import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../../constants";
+import { assert } from "tsafe/assert";
+import type { BuildOptions } from "../../buildOptions";
+import * as nodePath from "path";
+import { replaceAll } from "../../../tools/String.prototype.replaceAll";
+
+export type BuildOptionsLike = {
+ reactAppBuildDirPath: string;
+ assetsDirPath: string;
+ urlPathname: string | undefined;
+};
+
+assert();
+
+export function replaceImportsInJsCode_webpack(params: { jsCode: string; buildOptions: BuildOptionsLike; systemType?: "posix" | "win32" }): {
+ fixedJsCode: string;
+} {
+ const { jsCode, buildOptions, systemType = nodePath.sep === "/" ? "posix" : "win32" } = params;
+
+ const { relative: pathRelative, sep: pathSep } = nodePath[systemType];
+
+ let fixedJsCode = jsCode;
+
+ if (buildOptions.urlPathname !== undefined) {
+ // "__esModule",{value:!0})},n.p="/foo-bar/",function(){if("undefined" -> ... n.p="/" ...
+ fixedJsCode = fixedJsCode.replace(
+ new RegExp(`,([a-zA-Z]\\.[a-zA-Z])="${replaceAll(buildOptions.urlPathname, "/", "\\/")}",`, "g"),
+ (...[, assignTo]) => `,${assignTo}="/",`
+ );
+ }
+
+ // Example: "static/ or "foo/bar/"
+ const staticDir = (() => {
+ let out = pathRelative(buildOptions.reactAppBuildDirPath, buildOptions.assetsDirPath);
+
+ out = replaceAll(out, pathSep, "/") + "/";
+
+ if (out === "/") {
+ throw new Error(`The assetsDirPath must be a subdirectory of reactAppBuildDirPath`);
+ }
+
+ return out;
+ })();
+
+ const getReplaceArgs = (language: "js" | "css"): Parameters => [
+ new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=(function\\(([a-z]+)\\){return|([a-z]+)=>)"${staticDir.replace(/\//g, "\\/")}${language}\\/"`, "g"),
+ (...[, n, u, matchedFunction, eForFunction]) => {
+ const isArrowFunction = matchedFunction.includes("=>");
+ const e = isArrowFunction ? matchedFunction.replace("=>", "").trim() : eForFunction;
+
+ return `
+ ${n}[(function(){
+ var pd = Object.getOwnPropertyDescriptor(${n}, "p");
+ if( pd === undefined || pd.configurable ){
+ Object.defineProperty(${n}, "p", {
+ get: function() { return window.${nameOfTheGlobal}.url.resourcesPath; },
+ set: function() {}
+ });
+ }
+ return "${u}";
+ })()] = ${isArrowFunction ? `${e} =>` : `function(${e}) { return `} "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}${language}/"`
+ .replace(/\s+/g, " ")
+ .trim();
+ }
+ ];
+
+ fixedJsCode = fixedJsCode
+ .replace(...getReplaceArgs("js"))
+ .replace(...getReplaceArgs("css"))
+ .replace(
+ new RegExp(`[a-zA-Z]+\\.[a-zA-Z]+\\+"${staticDir.replace(/\//g, "\\/")}`, "g"),
+ `window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}`
+ );
+
+ return { fixedJsCode };
+}
diff --git a/src/bin/promptKeycloakVersion.ts b/src/bin/promptKeycloakVersion.ts
index 63e7b25c..52e14191 100644
--- a/src/bin/promptKeycloakVersion.ts
+++ b/src/bin/promptKeycloakVersion.ts
@@ -23,7 +23,6 @@ export async function promptKeycloakVersion() {
const tags = [
...(await getLatestsSemVersionedTag({
"count": 10,
- "doIgnoreBeta": true,
"owner": "keycloak",
"repo": "keycloak"
}).then(arr => arr.map(({ tag }) => tag))),
diff --git a/src/bin/tools/NpmModuleVersion.ts b/src/bin/tools/NpmModuleVersion.ts
deleted file mode 100644
index e517d4c8..00000000
--- a/src/bin/tools/NpmModuleVersion.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-export type NpmModuleVersion = {
- major: number;
- minor: number;
- patch: number;
- betaPreRelease?: number;
-};
-
-export namespace NpmModuleVersion {
- export function parse(versionStr: string): NpmModuleVersion {
- const match = versionStr.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-beta.([0-9]+))?/);
-
- if (!match) {
- throw new Error(`${versionStr} is not a valid NPM version`);
- }
-
- return {
- "major": parseInt(match[1]),
- "minor": parseInt(match[2]),
- "patch": parseInt(match[3]),
- ...(() => {
- const str = match[4];
- return str === undefined ? {} : { "betaPreRelease": parseInt(str) };
- })()
- };
- }
-
- export function stringify(v: NpmModuleVersion) {
- return `${v.major}.${v.minor}.${v.patch}${v.betaPreRelease === undefined ? "" : `-beta.${v.betaPreRelease}`}`;
- }
-
- /**
- *
- * v1 < v2 => -1
- * v1 === v2 => 0
- * v1 > v2 => 1
- *
- */
- export function compare(v1: NpmModuleVersion, v2: NpmModuleVersion): -1 | 0 | 1 {
- const sign = (diff: number): -1 | 0 | 1 => (diff === 0 ? 0 : diff < 0 ? -1 : 1);
- const noUndefined = (n: number | undefined) => n ?? Infinity;
-
- for (const level of ["major", "minor", "patch", "betaPreRelease"] as const) {
- if (noUndefined(v1[level]) !== noUndefined(v2[level])) {
- return sign(noUndefined(v1[level]) - noUndefined(v2[level]));
- }
- }
-
- return 0;
- }
-
- /*
- console.log(compare(parse("3.0.0-beta.3"), parse("3.0.0")) === -1 )
- console.log(compare(parse("3.0.0-beta.3"), parse("3.0.0-beta.4")) === -1 )
- console.log(compare(parse("3.0.0-beta.3"), parse("4.0.0")) === -1 )
- */
-
- export function bumpType(params: { versionBehindStr: string; versionAheadStr: string }): "major" | "minor" | "patch" | "betaPreRelease" | "same" {
- const versionAhead = parse(params.versionAheadStr);
- const versionBehind = parse(params.versionBehindStr);
-
- if (compare(versionBehind, versionAhead) === 1) {
- throw new Error(`Version regression ${versionBehind} -> ${versionAhead}`);
- }
-
- for (const level of ["major", "minor", "patch", "betaPreRelease"] as const) {
- if (versionBehind[level] !== versionAhead[level]) {
- return level;
- }
- }
-
- return "same";
- }
-}
diff --git a/src/bin/tools/OptionalIfCanBeUndefined.ts b/src/bin/tools/OptionalIfCanBeUndefined.ts
new file mode 100644
index 00000000..eef4d10a
--- /dev/null
+++ b/src/bin/tools/OptionalIfCanBeUndefined.ts
@@ -0,0 +1,12 @@
+type PropertiesThatCanBeUndefined> = {
+ [Key in keyof T]: undefined extends T[Key] ? Key : never;
+}[keyof T];
+
+/**
+ * OptionalIfCanBeUndefined<{ p1: string | undefined; p2: string; }>
+ * is
+ * { p1?: string | undefined; p2: string }
+ */
+export type OptionalIfCanBeUndefined> = {
+ [K in PropertiesThatCanBeUndefined]?: T[K];
+} & { [K in Exclude>]: T[K] };
diff --git a/src/bin/tools/SemVer.ts b/src/bin/tools/SemVer.ts
new file mode 100644
index 00000000..f8db9c3a
--- /dev/null
+++ b/src/bin/tools/SemVer.ts
@@ -0,0 +1,99 @@
+export type SemVer = {
+ major: number;
+ minor: number;
+ patch: number;
+ rc?: number;
+ parsedFrom: string;
+};
+
+export namespace SemVer {
+ const bumpTypes = ["major", "minor", "patch", "rc", "no bump"] as const;
+
+ export type BumpType = (typeof bumpTypes)[number];
+
+ export function parse(versionStr: string): SemVer {
+ const match = versionStr.match(/^v?([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(?:-rc.([0-9]+))?$/);
+
+ if (!match) {
+ throw new Error(`${versionStr} is not a valid semantic version`);
+ }
+
+ const semVer: Omit = {
+ "major": parseInt(match[1]),
+ "minor": parseInt(match[2]),
+ "patch": (() => {
+ const str = match[3];
+
+ return str === undefined ? 0 : parseInt(str);
+ })(),
+ ...(() => {
+ const str = match[4];
+ return str === undefined ? {} : { "rc": parseInt(str) };
+ })()
+ };
+
+ const initialStr = stringify(semVer);
+
+ Object.defineProperty(semVer, "parsedFrom", {
+ "enumerable": true,
+ "get": function () {
+ const currentStr = stringify(this);
+
+ if (currentStr !== initialStr) {
+ throw new Error(`SemVer.parsedFrom can't be read anymore, the version have been modified from ${initialStr} to ${currentStr}`);
+ }
+
+ return versionStr;
+ }
+ });
+
+ return semVer as any;
+ }
+
+ export function stringify(v: Omit): string {
+ return `${v.major}.${v.minor}.${v.patch}${v.rc === undefined ? "" : `-rc.${v.rc}`}`;
+ }
+
+ /**
+ *
+ * v1 < v2 => -1
+ * v1 === v2 => 0
+ * v1 > v2 => 1
+ *
+ */
+ export function compare(v1: SemVer, v2: SemVer): -1 | 0 | 1 {
+ const sign = (diff: number): -1 | 0 | 1 => (diff === 0 ? 0 : diff < 0 ? -1 : 1);
+ const noUndefined = (n: number | undefined) => n ?? Infinity;
+
+ for (const level of ["major", "minor", "patch", "rc"] as const) {
+ if (noUndefined(v1[level]) !== noUndefined(v2[level])) {
+ return sign(noUndefined(v1[level]) - noUndefined(v2[level]));
+ }
+ }
+
+ return 0;
+ }
+
+ /*
+ console.log(compare(parse("3.0.0-rc.3"), parse("3.0.0")) === -1 )
+ console.log(compare(parse("3.0.0-rc.3"), parse("3.0.0-rc.4")) === -1 )
+ console.log(compare(parse("3.0.0-rc.3"), parse("4.0.0")) === -1 )
+ */
+
+ export function bumpType(params: { versionBehind: string | SemVer; versionAhead: string | SemVer }): BumpType | "no bump" {
+ const versionAhead = typeof params.versionAhead === "string" ? parse(params.versionAhead) : params.versionAhead;
+ const versionBehind = typeof params.versionBehind === "string" ? parse(params.versionBehind) : params.versionBehind;
+
+ if (compare(versionBehind, versionAhead) === 1) {
+ throw new Error(`Version regression ${stringify(versionBehind)} -> ${stringify(versionAhead)}`);
+ }
+
+ for (const level of ["major", "minor", "patch", "rc"] as const) {
+ if (versionBehind[level] !== versionAhead[level]) {
+ return level;
+ }
+ }
+
+ return "no bump";
+ }
+}
diff --git a/src/bin/tools/String.prototype.replaceAll.ts b/src/bin/tools/String.prototype.replaceAll.ts
new file mode 100644
index 00000000..7fc1ebb8
--- /dev/null
+++ b/src/bin/tools/String.prototype.replaceAll.ts
@@ -0,0 +1,30 @@
+export function replaceAll(string: string, searchValue: string | RegExp, replaceValue: string): string {
+ if ((string as any).replaceAll !== undefined) {
+ return (string as any).replaceAll(searchValue, replaceValue);
+ }
+
+ // If the searchValue is a string
+ if (typeof searchValue === "string") {
+ // Escape special characters in the string to be used in a regex
+ var escapedSearchValue = searchValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ var regex = new RegExp(escapedSearchValue, "g");
+
+ return string.replace(regex, replaceValue);
+ }
+
+ // If the searchValue is a global RegExp, use it directly
+ if (searchValue instanceof RegExp && searchValue.global) {
+ return string.replace(searchValue, replaceValue);
+ }
+
+ // If the searchValue is a non-global RegExp, throw an error
+ if (searchValue instanceof RegExp) {
+ throw new TypeError("replaceAll must be called with a global RegExp");
+ }
+
+ // Convert searchValue to string if it's not a string or RegExp
+ var searchString = String(searchValue);
+ var regexFromString = new RegExp(searchString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
+
+ return string.replace(regexFromString, replaceValue);
+}
diff --git a/src/bin/tools/crawl.ts b/src/bin/tools/crawl.ts
index dd53a6d9..c83a6e54 100644
--- a/src/bin/tools/crawl.ts
+++ b/src/bin/tools/crawl.ts
@@ -1,17 +1,17 @@
import * as fs from "fs";
-import * as path from "path";
+import { join as pathJoin, relative as pathRelative } from "path";
-const crawlRec = (dir_path: string, paths: string[]) => {
- for (const file_name of fs.readdirSync(dir_path)) {
- const file_path = path.join(dir_path, file_name);
+const crawlRec = (dirPath: string, filePaths: string[]) => {
+ for (const basename of fs.readdirSync(dirPath)) {
+ const fileOrDirPath = pathJoin(dirPath, basename);
- if (fs.lstatSync(file_path).isDirectory()) {
- crawlRec(file_path, paths);
+ if (fs.lstatSync(fileOrDirPath).isDirectory()) {
+ crawlRec(fileOrDirPath, filePaths);
continue;
}
- paths.push(file_path);
+ filePaths.push(fileOrDirPath);
}
};
@@ -27,6 +27,6 @@ export function crawl(params: { dirPath: string; returnedPathsType: "absolute" |
case "absolute":
return filePaths;
case "relative to dirPath":
- return filePaths.map(filePath => path.relative(dirPath, filePath));
+ return filePaths.map(filePath => pathRelative(dirPath, filePath));
}
}
diff --git a/src/bin/tools/downloadAndUnzip.ts b/src/bin/tools/downloadAndUnzip.ts
deleted file mode 100644
index 4bff442b..00000000
--- a/src/bin/tools/downloadAndUnzip.ts
+++ /dev/null
@@ -1,232 +0,0 @@
-import { exec as execCallback } from "child_process";
-import { createHash } from "crypto";
-import { mkdir, readFile, stat, writeFile, unlink, rm } from "fs/promises";
-import fetch, { type FetchOptions } from "make-fetch-happen";
-import { dirname as pathDirname, join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
-import { assert } from "tsafe/assert";
-import { promisify } from "util";
-import { transformCodebase } from "./transformCodebase";
-import { unzip, zip } from "./unzip";
-
-const exec = promisify(execCallback);
-
-function generateFileNameFromURL(params: {
- url: string;
- preCacheTransform:
- | {
- actionCacheId: string;
- actionFootprint: string;
- }
- | undefined;
-}): string {
- const { preCacheTransform } = params;
-
- // Parse the URL
- const url = new URL(params.url);
-
- // Extract pathname and remove leading slashes
- let fileName = url.pathname.replace(/^\//, "").replace(/\//g, "_");
-
- // Optionally, add query parameters replacing special characters
- if (url.search) {
- fileName += url.search.replace(/[&=?]/g, "-");
- }
-
- // Replace any characters that are not valid in filenames
- fileName = fileName.replace(/[^a-zA-Z0-9-_]/g, "");
-
- // Trim or pad the fileName to a specific length
- fileName = fileName.substring(0, 50);
-
- add_pre_cache_transform: {
- if (preCacheTransform === undefined) {
- break add_pre_cache_transform;
- }
-
- // Sanitize actionCacheId the same way as other components
- const sanitizedActionCacheId = preCacheTransform.actionCacheId.replace(/[^a-zA-Z0-9-_]/g, "_");
-
- fileName += `_${sanitizedActionCacheId}_${createHash("sha256").update(preCacheTransform.actionFootprint).digest("hex").substring(0, 5)}`;
- }
-
- return fileName;
-}
-
-async function exists(path: string) {
- try {
- await stat(path);
- return true;
- } catch (error) {
- if ((error as Error & { code: string }).code === "ENOENT") return false;
- throw error;
- }
-}
-
-function ensureArray(arg0: T | T[]) {
- return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0];
-}
-
-function ensureSingleOrNone(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;
-
-const npmConfigReducer = (cfg: NPMConfig, [key, value]: [string, string]) =>
- key in cfg ? { ...cfg, [key]: [...ensureArray(cfg[key]), value] } : { ...cfg, [key]: value };
-
-/**
- * Get npm configuration as map
- */
-async function getNmpConfig() {
- return readNpmConfig().then(parseNpmConfig);
-}
-
-function readNpmConfig(): Promise {
- return (async function callee(depth: number): Promise {
- const cwd = pathResolve(pathJoin(...[process.cwd(), ...Array(depth).fill("..")]));
-
- let stdout: string;
-
- try {
- stdout = await exec("npm config get", { "encoding": "utf8", cwd }).then(({ stdout }) => stdout);
- } catch (error) {
- if (String(error).includes("ENOWORKSPACES")) {
- assert(cwd !== pathSep);
-
- return callee(depth + 1);
- }
-
- throw error;
- }
-
- return stdout;
- })(0);
-}
-
-function parseNpmConfig(stdout: string) {
- return stdout
- .split("\n")
- .filter(line => !line.startsWith(";"))
- .map(line => line.trim())
- .map(line => line.split("=", 2) as [string, string])
- .reduce(npmConfigReducer, {} as NPMConfig);
-}
-
-function maybeBoolean(arg0: string | undefined) {
- return typeof arg0 === "undefined" ? undefined : Boolean(arg0);
-}
-
-function chunks(arr: T[], size: number = 2) {
- return arr.map((_, i) => i % size == 0 && arr.slice(i, i + size)).filter(Boolean) as T[][];
-}
-
-async function readCafile(cafile: string) {
- const cafileContent = await readFile(cafile, "utf-8");
- return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(ca => ca.join("").replace(/^\n/, "").replace(/\n/g, "\\n"));
-}
-
-/**
- * Get proxy and ssl configuration from npm config files. Note that we don't care about
- * proxy config in env vars, because make-fetch-happen will do that for us.
- *
- * @returns proxy configuration
- */
-async function getFetchOptions(): Promise> {
- const cfg = await getNmpConfig();
-
- const proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]);
- const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
- 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 readCafile(cafile)));
-
- return { proxy, noProxy, strictSSL, cert, ca: ca.length === 0 ? undefined : ca };
-}
-
-export async function downloadAndUnzip(
- params: {
- url: string;
- destDirPath: string;
- specificDirsToExtract?: string[];
- preCacheTransform?: {
- actionCacheId: string;
- action: (params: { destDirPath: string }) => Promise;
- };
- } & (
- | {
- doUseCache: true;
- cacheDirPath: string;
- }
- | {
- doUseCache: false;
- }
- )
-) {
- const { url, destDirPath, specificDirsToExtract, preCacheTransform, ...rest } = params;
-
- const zipFileBasename = generateFileNameFromURL({
- url,
- "preCacheTransform":
- preCacheTransform === undefined
- ? undefined
- : {
- "actionCacheId": preCacheTransform.actionCacheId,
- "actionFootprint": preCacheTransform.action.toString()
- }
- });
-
- const cacheDirPath = !rest.doUseCache ? `tmp_${Math.random().toString().slice(2, 12)}` : rest.cacheDirPath;
-
- const zipFilePath = pathJoin(cacheDirPath, `${zipFileBasename}.zip`);
- const extractDirPath = pathJoin(cacheDirPath, `tmp_unzip_${zipFileBasename}`);
-
- if (!(await exists(zipFilePath))) {
- const opts = await getFetchOptions();
- const response = await fetch(url, opts);
- await mkdir(pathDirname(zipFilePath), { "recursive": true });
- /**
- * The correct way to fix this is to upgrade node-fetch beyond 3.2.5
- * (see https://github.com/node-fetch/node-fetch/issues/1295#issuecomment-1144061991.)
- * Unfortunately, octokit (a dependency of keycloakify) also uses node-fetch, and
- * does not support node-fetch 3.x. So we stick around with this band-aid until
- * octokit upgrades.
- */
- response.body?.setMaxListeners(Number.MAX_VALUE);
- assert(typeof response.body !== "undefined" && response.body != null);
- await writeFile(zipFilePath, response.body);
-
- if (specificDirsToExtract !== undefined || preCacheTransform !== undefined) {
- await unzip(zipFilePath, extractDirPath, specificDirsToExtract);
-
- await preCacheTransform?.action({
- "destDirPath": extractDirPath
- });
-
- await unlink(zipFilePath);
-
- await zip(extractDirPath, zipFilePath);
-
- await rm(extractDirPath, { "recursive": true });
- }
- }
-
- await unzip(zipFilePath, extractDirPath);
-
- transformCodebase({
- "srcDirPath": extractDirPath,
- "destDirPath": destDirPath
- });
-
- if (!rest.doUseCache) {
- await rm(cacheDirPath, { "recursive": true });
- } else {
- await rm(extractDirPath, { "recursive": true });
- }
-}
diff --git a/src/bin/tools/fetchProxyOptions.ts b/src/bin/tools/fetchProxyOptions.ts
new file mode 100644
index 00000000..a6e880fd
--- /dev/null
+++ b/src/bin/tools/fetchProxyOptions.ts
@@ -0,0 +1,73 @@
+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(arg0: T | T[]) {
+ return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0];
+}
+
+function ensureSingleOrNone(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;
+
+/**
+ * Get npm configuration as map
+ */
+async function getNmpConfig(params: { npmWorkspaceRootDirPath: string }) {
+ const { npmWorkspaceRootDirPath } = params;
+
+ const exec = promisify(execCallback);
+
+ const stdout = await exec("npm config get", { "encoding": "utf8", "cwd": npmWorkspaceRootDirPath }).then(({ stdout }) => stdout);
+
+ const npmConfigReducer = (cfg: NPMConfig, [key, value]: [string, string]) =>
+ key in cfg ? { ...cfg, [key]: [...ensureArray(cfg[key]), value] } : { ...cfg, [key]: value };
+
+ return stdout
+ .split("\n")
+ .filter(line => !line.startsWith(";"))
+ .map(line => line.trim())
+ .map(line => line.split("=", 2) as [string, string])
+ .reduce(npmConfigReducer, {} as NPMConfig);
+}
+
+export type ProxyFetchOptions = Pick;
+
+export async function getProxyFetchOptions(params: { npmWorkspaceRootDirPath: string }): Promise {
+ 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(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 };
+}
diff --git a/src/bin/tools/fs.existsAsync.ts b/src/bin/tools/fs.existsAsync.ts
new file mode 100644
index 00000000..359caabd
--- /dev/null
+++ b/src/bin/tools/fs.existsAsync.ts
@@ -0,0 +1,11 @@
+import * as fs from "fs/promises";
+
+export async function existsAsync(path: string) {
+ try {
+ await fs.stat(path);
+ return true;
+ } catch (error) {
+ if ((error as Error & { code: string }).code === "ENOENT") return false;
+ throw error;
+ }
+}
diff --git a/src/bin/tools/fs.rm.ts b/src/bin/tools/fs.rm.ts
new file mode 100644
index 00000000..4dd28c2b
--- /dev/null
+++ b/src/bin/tools/fs.rm.ts
@@ -0,0 +1,43 @@
+import * as fs from "fs/promises";
+import { join as pathJoin } from "path";
+import { SemVer } from "./SemVer";
+
+/**
+ * Polyfill of fs.rm(dirPath, { "recursive": true })
+ * For older version of Node
+ */
+export async function rm(dirPath: string, options: { recursive: true; force?: true }) {
+ if (SemVer.compare(SemVer.parse(process.version), SemVer.parse("14.14.0")) > 0) {
+ return fs.rm(dirPath, options);
+ }
+
+ const { force = true } = options;
+
+ if (force && !(await checkDirExists(dirPath))) {
+ return;
+ }
+
+ const removeDir_rec = async (dirPath: string) =>
+ Promise.all(
+ (await fs.readdir(dirPath)).map(async basename => {
+ const fileOrDirpath = pathJoin(dirPath, basename);
+
+ if ((await fs.lstat(fileOrDirpath)).isDirectory()) {
+ await removeDir_rec(fileOrDirpath);
+ } else {
+ await fs.unlink(fileOrDirpath);
+ }
+ })
+ );
+
+ await removeDir_rec(dirPath);
+}
+
+async function checkDirExists(dirPath: string) {
+ try {
+ await fs.access(dirPath, fs.constants.F_OK);
+ return true;
+ } catch {
+ return false;
+ }
+}
diff --git a/src/bin/tools/fs.rmSync.ts b/src/bin/tools/fs.rmSync.ts
new file mode 100644
index 00000000..2e9cd237
--- /dev/null
+++ b/src/bin/tools/fs.rmSync.ts
@@ -0,0 +1,34 @@
+import * as fs from "fs";
+import { join as pathJoin } from "path";
+import { SemVer } from "./SemVer";
+
+/**
+ * Polyfill of fs.rmSync(dirPath, { "recursive": true })
+ * For older version of Node
+ */
+export function rmSync(dirPath: string, options: { recursive: true; force?: true }) {
+ if (SemVer.compare(SemVer.parse(process.version), SemVer.parse("14.14.0")) > 0) {
+ fs.rmSync(dirPath, options);
+ return;
+ }
+
+ const { force = true } = options;
+
+ if (force && !fs.existsSync(dirPath)) {
+ return;
+ }
+
+ const removeDir_rec = (dirPath: string) =>
+ fs.readdirSync(dirPath).forEach(basename => {
+ const fileOrDirpath = pathJoin(dirPath, basename);
+
+ if (fs.lstatSync(fileOrDirpath).isDirectory()) {
+ removeDir_rec(fileOrDirpath);
+ return;
+ } else {
+ fs.unlinkSync(fileOrDirpath);
+ }
+ });
+
+ removeDir_rec(dirPath);
+}
diff --git a/src/bin/tools/getAbsoluteAndInOsFormatPath.ts b/src/bin/tools/getAbsoluteAndInOsFormatPath.ts
index 5b64edeb..dd686204 100644
--- a/src/bin/tools/getAbsoluteAndInOsFormatPath.ts
+++ b/src/bin/tools/getAbsoluteAndInOsFormatPath.ts
@@ -7,6 +7,8 @@ export function getAbsoluteAndInOsFormatPath(params: { pathIsh: string; cwd: str
pathOut = pathOut.replace(/\//g, pathSep);
+ pathOut = pathOut.endsWith(pathSep) ? pathOut.slice(0, -1) : pathOut;
+
if (!pathIsAbsolute(pathOut)) {
pathOut = pathJoin(cwd, pathOut);
}
diff --git a/src/bin/tools/getProjectRoot.ts b/src/bin/tools/getThisCodebaseRootDirPath.ts
similarity index 50%
rename from src/bin/tools/getProjectRoot.ts
rename to src/bin/tools/getThisCodebaseRootDirPath.ts
index 4d4ea107..f880ffc9 100644
--- a/src/bin/tools/getProjectRoot.ts
+++ b/src/bin/tools/getThisCodebaseRootDirPath.ts
@@ -1,19 +1,19 @@
import * as fs from "fs";
import * as path from "path";
-function getProjectRootRec(dirPath: string): string {
+function getThisCodebaseRootDirPath_rec(dirPath: string): string {
if (fs.existsSync(path.join(dirPath, "package.json"))) {
return dirPath;
}
- return getProjectRootRec(path.join(dirPath, ".."));
+ return getThisCodebaseRootDirPath_rec(path.join(dirPath, ".."));
}
let result: string | undefined = undefined;
-export function getProjectRoot(): string {
+export function getThisCodebaseRootDirPath(): string {
if (result !== undefined) {
return result;
}
- return (result = getProjectRootRec(__dirname));
+ return (result = getThisCodebaseRootDirPath_rec(__dirname));
}
diff --git a/src/bin/tools/grant-exec-perms.ts b/src/bin/tools/grant-exec-perms.ts
index 315f39bb..50abb0be 100644
--- a/src/bin/tools/grant-exec-perms.ts
+++ b/src/bin/tools/grant-exec-perms.ts
@@ -1,13 +1,15 @@
-import { getProjectRoot } from "./getProjectRoot";
+import { getThisCodebaseRootDirPath } from "./getThisCodebaseRootDirPath";
import { join as pathJoin } from "path";
import { constants } from "fs";
import { chmod, stat } from "fs/promises";
(async () => {
- const { bin } = await import(pathJoin(getProjectRoot(), "package.json"));
+ const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
+
+ const { bin } = await import(pathJoin(thisCodebaseRootDirPath, "package.json"));
const promises = Object.values(bin).map(async scriptPath => {
- const fullPath = pathJoin(getProjectRoot(), scriptPath);
+ const fullPath = pathJoin(thisCodebaseRootDirPath, scriptPath);
const oldMode = (await stat(fullPath)).mode;
const newMode = oldMode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH;
await chmod(fullPath, newMode);
diff --git a/src/bin/tools/octokit-addons/getLatestsSemVersionedTag.ts b/src/bin/tools/octokit-addons/getLatestsSemVersionedTag.ts
index f3fb505b..92c7ffb6 100644
--- a/src/bin/tools/octokit-addons/getLatestsSemVersionedTag.ts
+++ b/src/bin/tools/octokit-addons/getLatestsSemVersionedTag.ts
@@ -1,39 +1,39 @@
import { listTagsFactory } from "./listTags";
import type { Octokit } from "@octokit/rest";
-import { NpmModuleVersion } from "../NpmModuleVersion";
+import { SemVer } from "../SemVer";
export function getLatestsSemVersionedTagFactory(params: { octokit: Octokit }) {
const { octokit } = params;
- async function getLatestsSemVersionedTag(params: { owner: string; repo: string; doIgnoreBeta: boolean; count: number }): Promise<
+ async function getLatestsSemVersionedTag(params: { owner: string; repo: string; count: number }): Promise<
{
tag: string;
- version: NpmModuleVersion;
+ version: SemVer;
}[]
> {
- const { owner, repo, doIgnoreBeta, count } = params;
+ const { owner, repo, count } = params;
- const semVersionedTags: { tag: string; version: NpmModuleVersion }[] = [];
+ const semVersionedTags: { tag: string; version: SemVer }[] = [];
const { listTags } = listTagsFactory({ octokit });
for await (const tag of listTags({ owner, repo })) {
- let version: NpmModuleVersion;
+ let version: SemVer;
try {
- version = NpmModuleVersion.parse(tag.replace(/^[vV]?/, ""));
+ version = SemVer.parse(tag.replace(/^[vV]?/, ""));
} catch {
continue;
}
- if (doIgnoreBeta && version.betaPreRelease !== undefined) {
+ if (version.rc !== undefined) {
continue;
}
semVersionedTags.push({ tag, version });
}
- return semVersionedTags.sort(({ version: vX }, { version: vY }) => NpmModuleVersion.compare(vY, vX)).slice(0, count);
+ return semVersionedTags.sort(({ version: vX }, { version: vY }) => SemVer.compare(vY, vX)).slice(0, count);
}
return { getLatestsSemVersionedTag };
diff --git a/src/bin/tools/pathJoin.ts b/src/bin/tools/pathJoin.ts
deleted file mode 100644
index 58e1b6e7..00000000
--- a/src/bin/tools/pathJoin.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export function pathJoin(...path: string[]): string {
- return path
- .map((part, i) => (i === 0 ? part : part.replace(/^\/+/, "")))
- .map((part, i) => (i === path.length - 1 ? part : part.replace(/\/+$/, "")))
- .join(typeof process !== "undefined" && process.platform === "win32" ? "\\" : "/");
-}
diff --git a/src/bin/tools/readThisNpmProjectVersion.ts b/src/bin/tools/readThisNpmProjectVersion.ts
new file mode 100644
index 00000000..e99f97e8
--- /dev/null
+++ b/src/bin/tools/readThisNpmProjectVersion.ts
@@ -0,0 +1,12 @@
+import { getThisCodebaseRootDirPath } from "./getThisCodebaseRootDirPath";
+import { assert } from "tsafe/assert";
+import * as fs from "fs";
+import { join as pathJoin } from "path";
+
+export function readThisNpmProjectVersion(): string {
+ const version = JSON.parse(fs.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json")).toString("utf8"))["version"];
+
+ assert(typeof version === "string");
+
+ return version;
+}
diff --git a/src/bin/tools/transformCodebase.ts b/src/bin/tools/transformCodebase.ts
index 2064fe7d..57aa41f2 100644
--- a/src/bin/tools/transformCodebase.ts
+++ b/src/bin/tools/transformCodebase.ts
@@ -1,7 +1,7 @@
import * as fs from "fs";
import * as path from "path";
import { crawl } from "./crawl";
-import { id } from "tsafe/id";
+import { rmSync } from "../tools/fs.rmSync";
type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string; fileRelativePath: string }) =>
| {
@@ -10,18 +10,37 @@ type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string; file
}
| undefined;
-/** Apply a transformation function to every file of directory */
+/**
+ * Apply a transformation function to every file of directory
+ * If source and destination are the same this function can be used to apply the transformation in place
+ * like filtering out some files or modifying them.
+ * */
export function transformCodebase(params: { srcDirPath: string; destDirPath: string; transformSourceCode?: TransformSourceCode }) {
- const {
- srcDirPath,
- destDirPath,
- transformSourceCode = id(({ sourceCode }) => ({
- "modifiedSourceCode": sourceCode
- }))
- } = params;
+ const { srcDirPath, transformSourceCode } = params;
+
+ const isTargetSameAsSource = path.relative(srcDirPath, params.destDirPath) === "";
+
+ const destDirPath = isTargetSameAsSource ? path.join(srcDirPath, "..", "tmp_xOsPdkPsTdzPs34sOkHs") : params.destDirPath;
+
+ fs.mkdirSync(destDirPath, {
+ "recursive": true
+ });
for (const fileRelativePath of crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })) {
const filePath = path.join(srcDirPath, fileRelativePath);
+ const destFilePath = path.join(destDirPath, fileRelativePath);
+
+ // NOTE: Optimization, if we don't need to transform the file, just copy
+ // it using the lower level implementation.
+ if (transformSourceCode === undefined) {
+ fs.mkdirSync(path.dirname(destFilePath), {
+ "recursive": true
+ });
+
+ fs.copyFileSync(filePath, destFilePath);
+
+ continue;
+ }
const transformSourceCodeResult = transformSourceCode({
"sourceCode": fs.readFileSync(filePath),
@@ -33,15 +52,18 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
continue;
}
- fs.mkdirSync(path.dirname(path.join(destDirPath, fileRelativePath)), {
+ fs.mkdirSync(path.dirname(destFilePath), {
"recursive": true
});
const { newFileName, modifiedSourceCode } = transformSourceCodeResult;
- fs.writeFileSync(
- path.join(path.dirname(path.join(destDirPath, fileRelativePath)), newFileName ?? path.basename(fileRelativePath)),
- modifiedSourceCode
- );
+ fs.writeFileSync(path.join(path.dirname(destFilePath), newFileName ?? path.basename(destFilePath)), modifiedSourceCode);
+ }
+
+ if (isTargetSameAsSource) {
+ rmSync(srcDirPath, { "recursive": true });
+
+ fs.renameSync(destDirPath, srcDirPath);
}
}
diff --git a/src/lib/BASE_URL.ts b/src/lib/BASE_URL.ts
new file mode 100644
index 00000000..a5e70311
--- /dev/null
+++ b/src/lib/BASE_URL.ts
@@ -0,0 +1,44 @@
+import { assert } from "tsafe/assert";
+
+/**
+ * WARNING: Internal use only!!
+ * THIS DOES NOT WORK IN KEYCLOAK! It's only for resolving mock assets.
+ * This is just a way to know what's the base url that works
+ * both in webpack and vite.
+ * You can see this as a polyfill that return `import.meta.env.BASE_URL` when in Vite
+ * and when in Webpack returns the base url in the same format as vite does meaning
+ * "/" if hosted at root or "/foo/" when hosted under a subpath (always start and ends with a "/").
+ */
+export const BASE_URL = (() => {
+ vite: {
+ let BASE_URL: string;
+
+ try {
+ // @ts-expect-error
+ BASE_URL = import.meta.env.BASE_URL;
+
+ assert(typeof BASE_URL === "string");
+ } catch {
+ break vite;
+ }
+
+ return BASE_URL;
+ }
+
+ webpack: {
+ let BASE_URL: string;
+
+ try {
+ // @ts-expect-error
+ BASE_URL = process.env.PUBLIC_URL;
+
+ assert(typeof BASE_URL === "string");
+ } catch {
+ break webpack;
+ }
+
+ return BASE_URL === "" ? "/" : `${BASE_URL}/`;
+ }
+
+ throw new Error("Bundler not supported");
+})();
diff --git a/src/lib/isStorybook.ts b/src/lib/isStorybook.ts
new file mode 100644
index 00000000..f684e7c8
--- /dev/null
+++ b/src/lib/isStorybook.ts
@@ -0,0 +1 @@
+export const isStorybook = typeof window === "object" && Object.keys(window).find(key => key.startsWith("__STORYBOOK")) !== undefined;
diff --git a/src/lib/keycloakJsAdapter.ts b/src/lib/keycloakJsAdapter.ts
index 5a8cde53..9666bd3d 100644
--- a/src/lib/keycloakJsAdapter.ts
+++ b/src/lib/keycloakJsAdapter.ts
@@ -36,6 +36,10 @@ export declare namespace keycloak_js {
}
/**
+ * @deprecated: This will be removed in the next major version.
+ * If you use this, please copy paste the code into your project.
+ * Better yet migrate away from keycloak-js and use https://docs.oidc-spa.dev instead.
+ *
* NOTE: This is just a slightly modified version of the default adapter in keycloak-js
* The goal here is just to be able to inject search param in url before keycloak redirect.
* Our use case for it is to pass over the login screen the states of useGlobalState
diff --git a/src/login/kcContext/createGetKcContext.ts b/src/login/kcContext/createGetKcContext.ts
index 211ec856..fd218dbb 100644
--- a/src/login/kcContext/createGetKcContext.ts
+++ b/src/login/kcContext/createGetKcContext.ts
@@ -2,14 +2,13 @@ import type { KcContext, Attribute } from "./KcContext";
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import { deepAssign } from "keycloakify/tools/deepAssign";
+import { isStorybook } from "keycloakify/lib/isStorybook";
import { id } from "tsafe/id";
import { exclude } from "tsafe/exclude";
import { assert } from "tsafe/assert";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
-import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { symToStr } from "tsafe/symToStr";
-import { resources_common } from "keycloakify/bin/constants";
export function createGetKcContext(params?: {
mockData?: readonly DeepPartial>[];
@@ -31,7 +30,13 @@ export function createGetKcContext pageId === mockPageId);
@@ -147,8 +152,6 @@ export function createGetKcContext = [KcContextExtension] extends [never]
? KcContext
: AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>;
export function getKcContextFromWindow(): ExtendKcContext | undefined {
- return typeof window === "undefined" ? undefined : (window as any)[ftlValuesGlobalName];
+ return typeof window === "undefined" ? undefined : (window as any)[nameOfTheGlobal];
}
diff --git a/src/login/kcContext/kcContextMocks.ts b/src/login/kcContext/kcContextMocks.ts
index fb54dc96..5f40bd5a 100644
--- a/src/login/kcContext/kcContextMocks.ts
+++ b/src/login/kcContext/kcContextMocks.ts
@@ -1,10 +1,10 @@
import "minimal-polyfills/Object.fromEntries";
import type { KcContext, Attribute } from "./KcContext";
import { resources_common, keycloak_resources } from "keycloakify/bin/constants";
-import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { id } from "tsafe/id";
import { assert, type Equals } from "tsafe/assert";
import type { LoginThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
+import { BASE_URL } from "keycloakify/lib/BASE_URL";
const attributes: Attribute[] = [
{
@@ -100,9 +100,7 @@ const attributes: Attribute[] = [
const attributesByName = Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any;
-const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/";
-
-const resourcesPath = pathJoin(PUBLIC_URL, keycloak_resources, "login", "resources");
+const resourcesPath = `${BASE_URL}${keycloak_resources}/login/resources`;
export const kcContextCommonMock: KcContext.Common = {
"themeVersion": "0.0.0",
@@ -112,7 +110,7 @@ export const kcContextCommonMock: KcContext.Common = {
"url": {
"loginAction": "#",
resourcesPath,
- "resourcesCommonPath": pathJoin(resourcesPath, resources_common),
+ "resourcesCommonPath": `${resourcesPath}/${resources_common}`,
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg"
},
diff --git a/src/login/pages/PageProps.ts b/src/login/pages/PageProps.ts
index 6e513130..d2c80299 100644
--- a/src/login/pages/PageProps.ts
+++ b/src/login/pages/PageProps.ts
@@ -1,10 +1,11 @@
import type { I18n } from "keycloakify/login/i18n";
import { type TemplateProps, type ClassKey } from "keycloakify/login/TemplateProps";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
+import type { KcContext } from "keycloakify/account/kcContext";
-export type PageProps = {
+export type PageProps = {
Template: LazyOrNot<(props: TemplateProps) => JSX.Element | null>;
- kcContext: KcContext;
+ kcContext: NarowedKcContext;
i18n: I18nExtended;
doUseDefaultCss: boolean;
classes?: Partial>;
diff --git a/src/tsconfig.json b/src/tsconfig.json
index adb5124a..0a78fe20 100644
--- a/src/tsconfig.json
+++ b/src/tsconfig.json
@@ -14,7 +14,7 @@
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true
},
- "exclude": ["./bin"],
+ "exclude": ["./bin", "./vite-plugin"],
"references": [
{
"path": "./bin"
diff --git a/src/vite-plugin/index.ts b/src/vite-plugin/index.ts
new file mode 100644
index 00000000..447f98fe
--- /dev/null
+++ b/src/vite-plugin/index.ts
@@ -0,0 +1 @@
+export * from "./vite-plugin";
diff --git a/src/vite-plugin/tsconfig.json b/src/vite-plugin/tsconfig.json
new file mode 100644
index 00000000..3fd7aee3
--- /dev/null
+++ b/src/vite-plugin/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../../tsproject.json",
+ "compilerOptions": {
+ "module": "CommonJS",
+ "target": "ES2019",
+ "esModuleInterop": true,
+ "lib": ["es2019", "es2020.bigint", "es2020.string", "es2020.symbol.wellknown"],
+ "outDir": "../../dist/vite-plugin",
+ "rootDir": ".",
+ // https://github.com/vitejs/vite/issues/15112#issuecomment-1823908010
+ "skipLibCheck": true
+ },
+ "references": [
+ {
+ "path": "../bin"
+ }
+ ]
+}
diff --git a/src/vite-plugin/vite-plugin.ts b/src/vite-plugin/vite-plugin.ts
new file mode 100644
index 00000000..df645967
--- /dev/null
+++ b/src/vite-plugin/vite-plugin.ts
@@ -0,0 +1,151 @@
+import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
+import type { Plugin } from "vite";
+import * as fs from "fs";
+import { resolvedViteConfigJsonBasename, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir, keycloak_resources } from "../bin/constants";
+import type { ResolvedViteConfig } from "../bin/keycloakify/buildOptions/resolvedViteConfig";
+import { getCacheDirPath } from "../bin/keycloakify/buildOptions/getCacheDirPath";
+import { replaceAll } from "../bin/tools/String.prototype.replaceAll";
+import { id } from "tsafe/id";
+import { rm } from "../bin/tools/fs.rm";
+import { copyKeycloakResourcesToPublic } from "../bin/copy-keycloak-resources-to-public";
+import { assert } from "tsafe/assert";
+
+export function keycloakify() {
+ let reactAppRootDirPath: string | undefined = undefined;
+ let urlPathname: string | undefined = undefined;
+ let buildDirPath: string | undefined = undefined;
+ let command: "build" | "serve" | undefined = undefined;
+
+ const plugin = {
+ "name": "keycloakify" as const,
+ "configResolved": async resolvedConfig => {
+ command = resolvedConfig.command;
+
+ reactAppRootDirPath = resolvedConfig.root;
+ urlPathname = (() => {
+ let out = resolvedConfig.env.BASE_URL;
+
+ if (out.startsWith(".") && command === "build") {
+ throw new Error(
+ [
+ `BASE_URL=${out} is not supported By Keycloakify. Use an absolute URL instead.`,
+ `If this is a problem, please open an issue at https://github.com/keycloakify/keycloakify/issues/new`
+ ].join("\n")
+ );
+ }
+
+ if (out === undefined) {
+ return undefined;
+ }
+
+ if (!out.startsWith("/")) {
+ out = "/" + out;
+ }
+
+ if (!out.endsWith("/")) {
+ out += "/";
+ }
+
+ return out;
+ })();
+
+ buildDirPath = pathJoin(reactAppRootDirPath, resolvedConfig.build.outDir);
+
+ const { cacheDirPath } = getCacheDirPath({
+ reactAppRootDirPath
+ });
+
+ if (!fs.existsSync(cacheDirPath)) {
+ fs.mkdirSync(cacheDirPath, { "recursive": true });
+ }
+
+ fs.writeFileSync(
+ pathJoin(cacheDirPath, resolvedViteConfigJsonBasename),
+ Buffer.from(
+ JSON.stringify(
+ id({
+ "publicDir": pathRelative(reactAppRootDirPath, resolvedConfig.publicDir),
+ "assetsDir": resolvedConfig.build.assetsDir,
+ "buildDir": resolvedConfig.build.outDir,
+ urlPathname
+ }),
+ null,
+ 2
+ ),
+ "utf8"
+ )
+ );
+
+ await copyKeycloakResourcesToPublic({
+ "processArgv": ["--project", reactAppRootDirPath]
+ });
+ },
+ "transform": (code, id) => {
+ assert(command !== undefined);
+
+ if (command !== "build") {
+ return;
+ }
+
+ assert(reactAppRootDirPath !== undefined);
+
+ let transformedCode: string | undefined = undefined;
+
+ replace_import_meta_env_base_url_in_source_code: {
+ {
+ const isWithinSourceDirectory = id.startsWith(pathJoin(reactAppRootDirPath, "src") + pathSep);
+
+ if (!isWithinSourceDirectory) {
+ break replace_import_meta_env_base_url_in_source_code;
+ }
+ }
+
+ {
+ const isJavascriptFile = id.endsWith(".js") || id.endsWith(".jsx");
+ const isTypeScriptFile = id.endsWith(".ts") || id.endsWith(".tsx");
+
+ if (!isTypeScriptFile && !isJavascriptFile) {
+ break replace_import_meta_env_base_url_in_source_code;
+ }
+ }
+
+ if (transformedCode === undefined) {
+ transformedCode = code;
+ }
+
+ transformedCode = replaceAll(
+ transformedCode,
+ "import.meta.env.BASE_URL",
+ [
+ `(`,
+ `(window.${nameOfTheGlobal} === undefined || import.meta.env.MODE === "development")?`,
+ `"${urlPathname ?? "/"}":`,
+ `(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/")`,
+ `)`
+ ].join("")
+ );
+ }
+
+ if (transformedCode === undefined) {
+ return;
+ }
+
+ return {
+ "code": transformedCode
+ };
+ },
+ "closeBundle": async () => {
+ assert(command !== undefined);
+
+ if (command !== "build") {
+ return;
+ }
+
+ assert(buildDirPath !== undefined);
+
+ await rm(pathJoin(buildDirPath, keycloak_resources), { "recursive": true, "force": true });
+ }
+ } satisfies Plugin;
+
+ return plugin as any;
+}
diff --git a/test/bin/readStaticResourcesUsage.spec.ts b/test/bin/readStaticResourcesUsage.spec.ts
deleted file mode 100644
index 1cd0296e..00000000
--- a/test/bin/readStaticResourcesUsage.spec.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import { readPaths } from "keycloakify/bin/keycloakify/generateTheme/readStaticResourcesUsage";
-import { same } from "evt/tools/inDepth/same";
-import { expect, it, describe } from "vitest";
-
-describe("Ensure it's able to extract used Keycloak resources", () => {
- const expectedPaths = {
- "resourcesCommonFilePaths": [
- "node_modules/patternfly/dist/css/patternfly.min.css",
- "node_modules/patternfly/dist/css/patternfly-additions.min.css",
- "lib/zocial/zocial.css",
- "node_modules/jquery/dist/jquery.min.js"
- ]
- };
-
- it("works with coding style n°1", () => {
- const paths = readPaths({
- "rawSourceFile": `
- const { isReady } = usePrepareTemplate({
- "doFetchDefaultThemeResources": doUseDefaultCss,
- "styles": [
- \`\${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css\`,
- \`\${
- url.resourcesCommonPath
- }/node_modules/patternfly/dist/css/patternfly-additions.min.css\`,
- \`\${resourcesCommonPath }/lib/zocial/zocial.css\`,
- \`\${url.resourcesPath}/css/login.css\`
- ],
- "htmlClassName": getClassName("kcHtmlClass"),
- "bodyClassName": undefined
- });
-
- const { prLoaded, remove } = headInsert({
- "type": "javascript",
- "src": \`\${kcContext.url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js\`
- });
-
- `
- });
-
- expect(same(paths, expectedPaths)).toBe(true);
- });
-
- it("works with coding style n°2", () => {
- const paths = readPaths({
- "rawSourceFile": `
-
- const { isReady } = usePrepareTemplate({
- "doFetchDefaultThemeResources": doUseDefaultCss,
- "styles": [
- url.resourcesCommonPath + "/node_modules/patternfly/dist/css/patternfly.min.css",
- url.resourcesCommonPath + '/node_modules/patternfly/dist/css/patternfly-additions.min.css',
- url.resourcesCommonPath
- + "/lib/zocial/zocial.css",
- url.resourcesPath +
- '/css/login.css'
- ],
- "htmlClassName": getClassName("kcHtmlClass"),
- "bodyClassName": undefined
- });
-
- const { prLoaded, remove } = headInsert({
- "type": "javascript",
- "src": kcContext.url.resourcesCommonPath + "/node_modules/jquery/dist/jquery.min.js\"
- });
-
-
- `
- });
-
- expect(same(paths, expectedPaths)).toBe(true);
- });
-
- it("works with coding style n°3", () => {
- const paths = readPaths({
- "rawSourceFile": `
-
- const { isReady } = usePrepareTemplate({
- "doFetchDefaultThemeResources": doUseDefaultCss,
- "styles": [
- path.join(resourcesCommonPath,"/node_modules/patternfly/dist/css/patternfly.min.css"),
- path.join(url.resourcesCommonPath, '/node_modules/patternfly/dist/css/patternfly-additions.min.css'),
- path.join(url.resourcesCommonPath,
- "/lib/zocial/zocial.css"),
- pathJoin(
- url.resourcesPath,
- 'css/login.css'
- )
- ],
- "htmlClassName": getClassName("kcHtmlClass"),
- "bodyClassName": undefined
- });
-
- const { prLoaded, remove } = headInsert({
- "type": "javascript",
- "src": path.join(kcContext.url.resourcesCommonPath, "/node_modules/jquery/dist/jquery.min.js")
- });
-
-
- `
- });
-
- expect(same(paths, expectedPaths)).toBe(true);
- });
-});
diff --git a/test/bin/replaceImportFromStatic.spec.ts b/test/bin/replacers.spec.ts
similarity index 50%
rename from test/bin/replaceImportFromStatic.spec.ts
rename to test/bin/replacers.spec.ts
index 9930cde6..eee699a2 100644
--- a/test/bin/replaceImportFromStatic.spec.ts
+++ b/test/bin/replacers.spec.ts
@@ -1,45 +1,253 @@
-import { replaceImportsFromStaticInJsCode } from "keycloakify/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode";
+import { replaceImportsInJsCode_vite } from "keycloakify/bin/keycloakify/replacers/replaceImportsInJsCode/vite";
+import { replaceImportsInJsCode_webpack } from "keycloakify/bin/keycloakify/replacers/replaceImportsInJsCode/webpack";
import { 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 { isSameCode } from "../tools/isSameCode";
+import { basenameOfTheKeycloakifyResourcesDir, nameOfTheGlobal } from "keycloakify/bin/constants";
-describe("bin/js-transforms", () => {
- const jsCodeUntransformed = `
- function f() {
- return a.p+"static/js/" + ({}[e] || e) + "." + {
- 3: "0664cdc0"
- }[e] + ".chunk.js"
- }
-
- function sameAsF() {
- return a.p+"static/js/" + ({}[e] || e) + "." + {
- 3: "0664cdc0"
- }[e] + ".chunk.js"
- }
+describe("js replacer - vite", () => {
+ it("replaceImportsInJsCode_vite - 1", () => {
+ const before = `Uv="modulepreload",`;
+ const after = `,Wc={},`;
+ const jsCodeUntransformed = `${before}Hv=function(e){return"/foo-bar-baz/"+e}${after}`;
- __webpack_require__.u=function(e){return"static/js/" + e + "." + {
- 147: "6c5cee76",
- 787: "8da10fcf",
- 922: "be170a73"
- } [e] + ".chunk.js"
- }
+ const { fixedJsCode } = replaceImportsInJsCode_vite({
+ "jsCode": jsCodeUntransformed,
+ "basenameOfAssetsFiles": [],
+ "buildOptions": {
+ "reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/dist/",
+ "assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/assets/",
+ "urlPathname": "/foo-bar-baz/"
+ }
+ });
- t.miniCssF=function(e){return"static/css/"+e+"."+{
- 164:"dcfd7749",
- 908:"67c9ed2c"
- }[e]+".chunk.css"
+ const fixedJsCodeExpected = `${before}Hv=function(e){return"/"+e}${after}`;
+
+ expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
+ });
+
+ it("replaceImportsInJsCode_vite - 2", () => {
+ const before = `Uv="modulepreload",`;
+ const after = `,Wc={},`;
+ const jsCodeUntransformed = `${before}Hv=function(e){return"/foo/bar/baz/"+e}${after}`;
+
+ const { fixedJsCode } = replaceImportsInJsCode_vite({
+ "jsCode": jsCodeUntransformed,
+ "basenameOfAssetsFiles": [],
+ "buildOptions": {
+ "reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/dist/",
+ "assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/assets/",
+ "urlPathname": "/foo/bar/baz/"
+ }
+ });
+
+ const fixedJsCodeExpected = `${before}Hv=function(e){return"/"+e}${after}`;
+
+ expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
+ });
+
+ it("replaceImportsInJsCode_vite - 3", () => {
+ const jsCodeUntransformed = `
+ S="/assets/keycloakify-logo-mqjydaoZ.png",H=(()=>{
+
+ function __vite__mapDeps(indexes) {
+ if (!__vite__mapDeps.viteFileDeps) {
+ __vite__mapDeps.viteFileDeps = ["assets/Login-dJpPRzM4.js", "assets/index-XwzrZ5Gu.js"]
+ }
+ return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
+ }
+ `;
+
+ for (const { reactAppBuildDirPath, assetsDirPath, systemType } of [
+ {
+ "systemType": "posix",
+ "reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/dist",
+ "assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/assets"
+ },
+ {
+ "systemType": "win32",
+ "reactAppBuildDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist",
+ "assetsDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist\\assets"
+ }
+ ] as const) {
+ const { fixedJsCode } = replaceImportsInJsCode_vite({
+ "jsCode": jsCodeUntransformed,
+ "basenameOfAssetsFiles": ["Login-dJpPRzM4.js", "index-XwzrZ5Gu.js", "keycloakify-logo-mqjydaoZ.png"],
+ "buildOptions": {
+ reactAppBuildDirPath,
+ assetsDirPath,
+ "urlPathname": undefined
+ },
+ systemType
+ });
+
+ const fixedJsCodeExpected = `
+ S=(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/assets/keycloakify-logo-mqjydaoZ.png"),H=(()=>{
+
+ function __vite__mapDeps(indexes) {
+ if (!__vite__mapDeps.viteFileDeps) {
+ __vite__mapDeps.viteFileDeps = [
+ (window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/Login-dJpPRzM4.js"),
+ (window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/index-XwzrZ5Gu.js")
+ ]
+ }
+ return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
+ }
+ `;
+
+ expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
}
+ });
+
+ it("replaceImportsInJsCode_vite - 4", () => {
+ const jsCodeUntransformed = `
+ S="/foo/bar/keycloakify-logo-mqjydaoZ.png",H=(()=>{
+
+ function __vite__mapDeps(indexes) {
+ if (!__vite__mapDeps.viteFileDeps) {
+ __vite__mapDeps.viteFileDeps = ["foo/bar/Login-dJpPRzM4.js", "foo/bar/index-XwzrZ5Gu.js"]
+ }
+ return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
+ }
+ `;
+
+ for (const { reactAppBuildDirPath, assetsDirPath, systemType } of [
+ {
+ "systemType": "posix",
+ "reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/dist",
+ "assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/foo/bar"
+ },
+ {
+ "systemType": "win32",
+ "reactAppBuildDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist",
+ "assetsDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist\\foo\\bar"
+ }
+ ] as const) {
+ const { fixedJsCode } = replaceImportsInJsCode_vite({
+ "jsCode": jsCodeUntransformed,
+ "basenameOfAssetsFiles": ["Login-dJpPRzM4.js", "index-XwzrZ5Gu.js", "keycloakify-logo-mqjydaoZ.png"],
+ "buildOptions": {
+ reactAppBuildDirPath,
+ assetsDirPath,
+ "urlPathname": undefined
+ },
+ systemType
+ });
+
+ const fixedJsCodeExpected = `
+ S=(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/foo/bar/keycloakify-logo-mqjydaoZ.png"),H=(()=>{
+
+ function __vite__mapDeps(indexes) {
+ if (!__vite__mapDeps.viteFileDeps) {
+ __vite__mapDeps.viteFileDeps = [
+ (window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/foo/bar/Login-dJpPRzM4.js"),
+ (window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/foo/bar/index-XwzrZ5Gu.js")
+ ]
+ }
+ return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
+ }
+ `;
+
+ expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
+ }
+ });
+
+ it("replaceImportsInJsCode_vite - 5", () => {
+ const jsCodeUntransformed = `
+ S="/foo-bar-baz/assets/keycloakify-logo-mqjydaoZ.png",H=(()=>{
+
+ function __vite__mapDeps(indexes) {
+ if (!__vite__mapDeps.viteFileDeps) {
+ __vite__mapDeps.viteFileDeps = ["assets/Login-dJpPRzM4.js", "assets/index-XwzrZ5Gu.js"]
+ }
+ return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
+ }
+ `;
+
+ for (const { reactAppBuildDirPath, assetsDirPath, systemType } of [
+ {
+ "systemType": "posix",
+ "reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/dist",
+ "assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/assets"
+ },
+ {
+ "systemType": "win32",
+ "reactAppBuildDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist",
+ "assetsDirPath": "C:\\\\Users\\someone\\github\\keycloakify-starter\\dist\\assets"
+ }
+ ] as const) {
+ const { fixedJsCode } = replaceImportsInJsCode_vite({
+ "jsCode": jsCodeUntransformed,
+ "basenameOfAssetsFiles": ["Login-dJpPRzM4.js", "index-XwzrZ5Gu.js", "keycloakify-logo-mqjydaoZ.png"],
+ "buildOptions": {
+ reactAppBuildDirPath,
+ assetsDirPath,
+ "urlPathname": "/foo-bar-baz/"
+ },
+ systemType
+ });
+
+ const fixedJsCodeExpected = `
+ S=(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/assets/keycloakify-logo-mqjydaoZ.png"),H=(()=>{
+
+ function __vite__mapDeps(indexes) {
+ if (!__vite__mapDeps.viteFileDeps) {
+ __vite__mapDeps.viteFileDeps = [
+ (window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/Login-dJpPRzM4.js"),
+ (window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/assets/index-XwzrZ5Gu.js")
+ ]
+ }
+ return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
+ }
+ `;
+
+ expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
+ }
+ });
+});
+
+describe("js replacer - webpack", () => {
+ it("replaceImportsInJsCode_webpack - 1", () => {
+ const jsCodeUntransformed = `
+ function f() {
+ return a.p+"static/js/" + ({}[e] || e) + "." + {
+ 3: "0664cdc0"
+ }[e] + ".chunk.js"
+ }
- n.u=e=>"static/js/"+e+"."+{69:"4f205f87",128:"49264537",453:"b2fed72e",482:"f0106901"}[e]+".chunk.js"
+ function sameAsF() {
+ return a.p+"static/js/" + ({}[e] || e) + "." + {
+ 3: "0664cdc0"
+ }[e] + ".chunk.js"
+ }
+
+ __webpack_require__.u=function(e){return"static/js/" + e + "." + {
+ 147: "6c5cee76",
+ 787: "8da10fcf",
+ 922: "be170a73"
+ } [e] + ".chunk.js"
+ }
+
+ t.miniCssF=function(e){return"static/css/"+e+"."+{
+ 164:"dcfd7749",
+ 908:"67c9ed2c"
+ }[e]+".chunk.css"
+ }
- t.miniCssF=e=>"static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css"
- `;
- it("transforms standalone code properly", () => {
- const { fixedJsCode } = replaceImportsFromStaticInJsCode({
- "jsCode": jsCodeUntransformed
+ n.u=e=>"static/js/"+e+"."+{69:"4f205f87",128:"49264537",453:"b2fed72e",482:"f0106901"}[e]+".chunk.js"
+
+ t.miniCssF=e=>"static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css"
+ `;
+
+ const { fixedJsCode } = replaceImportsInJsCode_webpack({
+ "jsCode": jsCodeUntransformed,
+ "buildOptions": {
+ "reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/build",
+ "assetsDirPath": "/Users/someone/github/keycloakify-starter/build/static",
+ "urlPathname": undefined
+ }
});
const fixedJsCodeExpected = `
@@ -113,9 +321,49 @@ describe("bin/js-transforms", () => {
expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
});
+
+ it("replaceImportsInJsCode_webpack - 2", () => {
+ const before = `"__esModule",{value:!0})}`;
+ const after = `function(){if("undefined"`;
+
+ const jsCodeUntransformed = `${before},n.p="/foo-bar/",${after}`;
+
+ const { fixedJsCode } = replaceImportsInJsCode_webpack({
+ "jsCode": jsCodeUntransformed,
+ "buildOptions": {
+ "reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/build",
+ "assetsDirPath": "/Users/someone/github/keycloakify-starter/build/static",
+ "urlPathname": "/foo-bar/"
+ }
+ });
+
+ const fixedJsCodeExpected = `${before},n.p="/",${after}`;
+
+ expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
+ });
+
+ it("replaceImportsInJsCode_webpack - 3", () => {
+ const before = `"__esModule",{value:!0})}`;
+ const after = `function(){if("undefined"`;
+
+ const jsCodeUntransformed = `${before},n.p="/foo/bar/",${after}`;
+
+ const { fixedJsCode } = replaceImportsInJsCode_webpack({
+ "jsCode": jsCodeUntransformed,
+ "buildOptions": {
+ "reactAppBuildDirPath": "/Users/someone/github/keycloakify-starter/build",
+ "assetsDirPath": "/Users/someone/github/keycloakify-starter/dist/build/static",
+ "urlPathname": "/foo/bar/"
+ }
+ });
+
+ const fixedJsCodeExpected = `${before},n.p="/",${after}`;
+
+ expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true);
+ });
});
-describe("bin/css-transforms", () => {
+describe("css replacer", () => {
it("transforms absolute urls to css globals properly with no urlPathname", () => {
const { fixedCssCode, cssGlobalsToDefine } = replaceImportsInCssCode({
"cssCode": `
@@ -230,7 +478,7 @@ describe("bin/css-transforms", () => {
});
});
-describe("bin/css-inline-transforms", () => {
+describe("inline css replacer", () => {
describe("no url pathName", () => {
const cssCode = `
@font-face {
diff --git a/test/bin/setupSampleReactProject.spec.ts b/test/bin/setupSampleReactProject.spec.ts.disabled
similarity index 100%
rename from test/bin/setupSampleReactProject.spec.ts
rename to test/bin/setupSampleReactProject.spec.ts.disabled
diff --git a/test/bin/setupSampleReactProject.ts b/test/bin/setupSampleReactProject.ts
index 5ea6daf1..79e7516a 100644
--- a/test/bin/setupSampleReactProject.ts
+++ b/test/bin/setupSampleReactProject.ts
@@ -1,9 +1,16 @@
-import { downloadAndUnzip } from "keycloakify/bin/tools/downloadAndUnzip";
+import { downloadAndUnzip } from "keycloakify/bin/downloadAndUnzip";
+import { join as pathJoin } from "path";
+import { getThisCodebaseRootDirPath } from "keycloakify/bin/tools/getThisCodebaseRootDirPath";
export async function setupSampleReactProject(destDirPath: string) {
+ const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
+
await downloadAndUnzip({
"url": "https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
"destDirPath": destDirPath,
- "doUseCache": false
+ "buildOptions": {
+ "cacheDirPath": pathJoin(thisCodebaseRootDirPath, "node_modules", ".cache", "keycloakify"),
+ "npmWorkspaceRootDirPath": thisCodebaseRootDirPath
+ }
});
}
diff --git a/test/tsconfig.json b/test/tsconfig.json
index 8c6c685d..3c10636a 100644
--- a/test/tsconfig.json
+++ b/test/tsconfig.json
@@ -4,20 +4,17 @@
"target": "es5",
"lib": ["es2015", "DOM", "ES2019.Object"],
"esModuleInterop": true,
- "declaration": true,
- "outDir": "../dist_test",
- "sourceMap": true,
- "newLine": "LF",
"noUnusedLocals": true,
"noUnusedParameters": true,
- "incremental": false,
"strict": true,
"downlevelIteration": true,
"jsx": "react-jsx",
"noFallthroughCasesInSwitch": true,
"paths": {
"keycloakify/*": ["../src/*"]
- }
+ },
+ // https://github.com/vitejs/vite/issues/15112#issuecomment-1823908010
+ "skipLibCheck": true
},
"include": ["../src", "."]
}
diff --git a/vitest.config.ts b/vitest.config.ts
index d3941fb6..bad5ed77 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,11 +1,10 @@
-///
-import { defineConfig } from "vite";
-import path from "path";
+import { defineConfig } from "vitest/config";
+import { resolve as pathResolve } from "path";
export default defineConfig({
"test": {
"alias": {
- "keycloakify": path.resolve(__dirname, "./src")
+ "keycloakify": pathResolve(__dirname, "./src")
},
"watchExclude": ["**/node_modules/**", "**/dist/**", "**/sample_react_project/**"]
}
diff --git a/yarn.lock b/yarn.lock
index aa4ca2a6..9e512cc1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1332,116 +1332,231 @@
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb"
integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==
+"@esbuild/aix-ppc64@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f"
+ integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==
+
"@esbuild/android-arm64@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.17.tgz#164b054d58551f8856285f386e1a8f45d9ba3a31"
integrity sha512-jaJ5IlmaDLFPNttv0ofcwy/cfeY4bh/n705Tgh+eLObbGtQBK3EPAu+CzL95JVE4nFAliyrnEu0d32Q5foavqg==
+"@esbuild/android-arm64@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4"
+ integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==
+
"@esbuild/android-arm@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.17.tgz#1b3b5a702a69b88deef342a7a80df4c894e4f065"
integrity sha512-E6VAZwN7diCa3labs0GYvhEPL2M94WLF8A+czO8hfjREXxba8Ng7nM5VxV+9ihNXIY1iQO1XxUU4P7hbqbICxg==
+"@esbuild/android-arm@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824"
+ integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==
+
"@esbuild/android-x64@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.17.tgz#6781527e3c4ea4de532b149d18a2167f06783e7f"
integrity sha512-446zpfJ3nioMC7ASvJB1pszHVskkw4u/9Eu8s5yvvsSDTzYh4p4ZIRj0DznSl3FBF0Z/mZfrKXTtt0QCoFmoHA==
+"@esbuild/android-x64@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d"
+ integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==
+
"@esbuild/darwin-arm64@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.17.tgz#c5961ef4d3c1cc80dafe905cc145b5a71d2ac196"
integrity sha512-m/gwyiBwH3jqfUabtq3GH31otL/0sE0l34XKpSIqR7NjQ/XHQ3lpmQHLHbG8AHTGCw8Ao059GvV08MS0bhFIJQ==
+"@esbuild/darwin-arm64@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e"
+ integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==
+
"@esbuild/darwin-x64@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.17.tgz#b81f3259cc349691f67ae30f7b333a53899b3c20"
integrity sha512-4utIrsX9IykrqYaXR8ob9Ha2hAY2qLc6ohJ8c0CN1DR8yWeMrTgYFjgdeQ9LIoTOfLetXjuCu5TRPHT9yKYJVg==
+"@esbuild/darwin-x64@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd"
+ integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==
+
"@esbuild/freebsd-arm64@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.17.tgz#db846ad16cf916fd3acdda79b85ea867cb100e87"
integrity sha512-4PxjQII/9ppOrpEwzQ1b0pXCsFLqy77i0GaHodrmzH9zq2/NEhHMAMJkJ635Ns4fyJPFOlHMz4AsklIyRqFZWA==
+"@esbuild/freebsd-arm64@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487"
+ integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==
+
"@esbuild/freebsd-x64@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.17.tgz#4dd99acbaaba00949d509e7c144b1b6ef9e1815b"
integrity sha512-lQRS+4sW5S3P1sv0z2Ym807qMDfkmdhUYX30GRBURtLTrJOPDpoU0kI6pVz1hz3U0+YQ0tXGS9YWveQjUewAJw==
+"@esbuild/freebsd-x64@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c"
+ integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==
+
"@esbuild/linux-arm64@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.17.tgz#7f9274140b2bb9f4230dbbfdf5dc2761215e30f6"
integrity sha512-2+pwLx0whKY1/Vqt8lyzStyda1v0qjJ5INWIe+d8+1onqQxHLLi3yr5bAa4gvbzhZqBztifYEu8hh1La5+7sUw==
+"@esbuild/linux-arm64@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b"
+ integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==
+
"@esbuild/linux-arm@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.17.tgz#5c8e44c2af056bb2147cf9ad13840220bcb8948b"
integrity sha512-biDs7bjGdOdcmIk6xU426VgdRUpGg39Yz6sT9Xp23aq+IEHDb/u5cbmu/pAANpDB4rZpY/2USPhCA+w9t3roQg==
+"@esbuild/linux-arm@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef"
+ integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==
+
"@esbuild/linux-ia32@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.17.tgz#18a6b3798658be7f46e9873fa0c8d4bec54c9212"
integrity sha512-IBTTv8X60dYo6P2t23sSUYym8fGfMAiuv7PzJ+0LcdAndZRzvke+wTVxJeCq4WgjppkOpndL04gMZIFvwoU34Q==
+"@esbuild/linux-ia32@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601"
+ integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==
+
"@esbuild/linux-loong64@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.17.tgz#a8d93514a47f7b4232716c9f02aeb630bae24c40"
integrity sha512-WVMBtcDpATjaGfWfp6u9dANIqmU9r37SY8wgAivuKmgKHE+bWSuv0qXEFt/p3qXQYxJIGXQQv6hHcm7iWhWjiw==
+"@esbuild/linux-loong64@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299"
+ integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==
+
"@esbuild/linux-mips64el@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.17.tgz#4784efb1c3f0eac8133695fa89253d558149ee1b"
integrity sha512-2kYCGh8589ZYnY031FgMLy0kmE4VoGdvfJkxLdxP4HJvWNXpyLhjOvxVsYjYZ6awqY4bgLR9tpdYyStgZZhi2A==
+"@esbuild/linux-mips64el@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec"
+ integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==
+
"@esbuild/linux-ppc64@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.17.tgz#ef6558ec5e5dd9dc16886343e0ccdb0699d70d3c"
integrity sha512-KIdG5jdAEeAKogfyMTcszRxy3OPbZhq0PPsW4iKKcdlbk3YE4miKznxV2YOSmiK/hfOZ+lqHri3v8eecT2ATwQ==
+"@esbuild/linux-ppc64@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8"
+ integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==
+
"@esbuild/linux-riscv64@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.17.tgz#13a87fdbcb462c46809c9d16bcf79817ecf9ce6f"
integrity sha512-Cj6uWLBR5LWhcD/2Lkfg2NrkVsNb2sFM5aVEfumKB2vYetkA/9Uyc1jVoxLZ0a38sUhFk4JOVKH0aVdPbjZQeA==
+"@esbuild/linux-riscv64@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf"
+ integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==
+
"@esbuild/linux-s390x@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.17.tgz#83cb16d1d3ac0dca803b3f031ba3dc13f1ec7ade"
integrity sha512-lK+SffWIr0XsFf7E0srBjhpkdFVJf3HEgXCwzkm69kNbRar8MhezFpkIwpk0qo2IOQL4JE4mJPJI8AbRPLbuOQ==
+"@esbuild/linux-s390x@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8"
+ integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==
+
"@esbuild/linux-x64@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.17.tgz#7bc400568690b688e20a0c94b2faabdd89ae1a79"
integrity sha512-XcSGTQcWFQS2jx3lZtQi7cQmDYLrpLRyz1Ns1DzZCtn898cWfm5Icx/DEWNcTU+T+tyPV89RQtDnI7qL2PObPg==
+"@esbuild/linux-x64@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78"
+ integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==
+
"@esbuild/netbsd-x64@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.17.tgz#1b5dcfbc4bfba80e67a11e9148de836af5b58b6c"
integrity sha512-RNLCDmLP5kCWAJR+ItLM3cHxzXRTe4N00TQyQiimq+lyqVqZWGPAvcyfUBM0isE79eEZhIuGN09rAz8EL5KdLA==
+"@esbuild/netbsd-x64@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b"
+ integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==
+
"@esbuild/openbsd-x64@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.17.tgz#e275098902291149a5dcd012c9ea0796d6b7adff"
integrity sha512-PAXswI5+cQq3Pann7FNdcpSUrhrql3wKjj3gVkmuz6OHhqqYxKvi6GgRBoaHjaG22HV/ZZEgF9TlS+9ftHVigA==
+"@esbuild/openbsd-x64@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0"
+ integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==
+
"@esbuild/sunos-x64@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.17.tgz#10603474866f64986c0370a2d4fe5a2bb7fee4f5"
integrity sha512-V63egsWKnx/4V0FMYkr9NXWrKTB5qFftKGKuZKFIrAkO/7EWLFnbBZNM1CvJ6Sis+XBdPws2YQSHF1Gqf1oj/Q==
+"@esbuild/sunos-x64@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30"
+ integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==
+
"@esbuild/win32-arm64@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.17.tgz#521a6d97ee0f96b7c435930353cc4e93078f0b54"
integrity sha512-YtUXLdVnd6YBSYlZODjWzH+KzbaubV0YVd6UxSfoFfa5PtNJNaW+1i+Hcmjpg2nEe0YXUCNF5bkKy1NnBv1y7Q==
+"@esbuild/win32-arm64@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae"
+ integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==
+
"@esbuild/win32-ia32@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.17.tgz#56f88462ebe82dad829dc2303175c0e0ccd8e38e"
integrity sha512-yczSLRbDdReCO74Yfc5tKG0izzm+lPMYyO1fFTcn0QNwnKmc3K+HdxZWLGKg4pZVte7XVgcFku7TIZNbWEJdeQ==
+"@esbuild/win32-ia32@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67"
+ integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==
+
"@esbuild/win32-x64@0.17.17":
version "0.17.17"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.17.tgz#2b577b976e6844106715bbe0cdc57cd1528063f9"
integrity sha512-FNZw7H3aqhF9OyRQbDDnzUApDXfC1N6fgBhkqEO2jvYCJ+DxMTfZVqg3AX0R1khg1wHTBRD5SdcibSJ+XF6bFg==
+"@esbuild/win32-x64@0.19.12":
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae"
+ integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==
+
"@eslint-community/eslint-utils@^4.2.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
@@ -1779,6 +1894,71 @@
schema-utils "^3.0.0"
source-map "^0.7.3"
+"@rollup/rollup-android-arm-eabi@4.9.6":
+ version "4.9.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz#66b8d9cb2b3a474d115500f9ebaf43e2126fe496"
+ integrity sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==
+
+"@rollup/rollup-android-arm64@4.9.6":
+ version "4.9.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz#46327d5b86420d2307946bec1535fdf00356e47d"
+ integrity sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==
+
+"@rollup/rollup-darwin-arm64@4.9.6":
+ version "4.9.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz#166987224d2f8b1e2fd28ee90c447d52271d5e90"
+ integrity sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==
+
+"@rollup/rollup-darwin-x64@4.9.6":
+ version "4.9.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz#a2e6e096f74ccea6e2f174454c26aef6bcdd1274"
+ integrity sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==
+
+"@rollup/rollup-linux-arm-gnueabihf@4.9.6":
+ version "4.9.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz#09fcd4c55a2d6160c5865fec708a8e5287f30515"
+ integrity sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==
+
+"@rollup/rollup-linux-arm64-gnu@4.9.6":
+ version "4.9.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz#19a3c0b6315c747ca9acf86e9b710cc2440f83c9"
+ integrity sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==
+
+"@rollup/rollup-linux-arm64-musl@4.9.6":
+ version "4.9.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz#94aaf95fdaf2ad9335983a4552759f98e6b2e850"
+ integrity sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==
+
+"@rollup/rollup-linux-riscv64-gnu@4.9.6":
+ version "4.9.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz#160510e63f4b12618af4013bddf1761cf9fc9880"
+ integrity sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==
+
+"@rollup/rollup-linux-x64-gnu@4.9.6":
+ version "4.9.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz#5ac5d068ce0726bd0a96ca260d5bd93721c0cb98"
+ integrity sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==
+
+"@rollup/rollup-linux-x64-musl@4.9.6":
+ version "4.9.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz#bafa759ab43e8eab9edf242a8259ffb4f2a57a5d"
+ integrity sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==
+
+"@rollup/rollup-win32-arm64-msvc@4.9.6":
+ version "4.9.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz#1cc3416682e5a20d8f088f26657e6e47f8db468e"
+ integrity sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==
+
+"@rollup/rollup-win32-ia32-msvc@4.9.6":
+ version "4.9.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz#7d2251e1aa5e8a1e47c86891fe4547a939503461"
+ integrity sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==
+
+"@rollup/rollup-win32-x64-msvc@4.9.6":
+ version "4.9.6"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz#2c1fb69e02a3f1506f52698cfdc3a8b6386df9a6"
+ integrity sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==
+
"@storybook/addon-a11y@^6.5.16":
version "6.5.16"
resolved "https://registry.yarnpkg.com/@storybook/addon-a11y/-/addon-a11y-6.5.16.tgz#9288a6c1d111fa4ec501d213100ffff91757d3fc"
@@ -2837,6 +3017,11 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
+"@types/estree@1.0.5":
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
+ integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
+
"@types/estree@^0.0.51":
version "0.0.51"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
@@ -5853,6 +6038,35 @@ esbuild@^0.17.5:
"@esbuild/win32-ia32" "0.17.17"
"@esbuild/win32-x64" "0.17.17"
+esbuild@^0.19.3:
+ version "0.19.12"
+ resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04"
+ integrity sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==
+ optionalDependencies:
+ "@esbuild/aix-ppc64" "0.19.12"
+ "@esbuild/android-arm" "0.19.12"
+ "@esbuild/android-arm64" "0.19.12"
+ "@esbuild/android-x64" "0.19.12"
+ "@esbuild/darwin-arm64" "0.19.12"
+ "@esbuild/darwin-x64" "0.19.12"
+ "@esbuild/freebsd-arm64" "0.19.12"
+ "@esbuild/freebsd-x64" "0.19.12"
+ "@esbuild/linux-arm" "0.19.12"
+ "@esbuild/linux-arm64" "0.19.12"
+ "@esbuild/linux-ia32" "0.19.12"
+ "@esbuild/linux-loong64" "0.19.12"
+ "@esbuild/linux-mips64el" "0.19.12"
+ "@esbuild/linux-ppc64" "0.19.12"
+ "@esbuild/linux-riscv64" "0.19.12"
+ "@esbuild/linux-s390x" "0.19.12"
+ "@esbuild/linux-x64" "0.19.12"
+ "@esbuild/netbsd-x64" "0.19.12"
+ "@esbuild/openbsd-x64" "0.19.12"
+ "@esbuild/sunos-x64" "0.19.12"
+ "@esbuild/win32-arm64" "0.19.12"
+ "@esbuild/win32-ia32" "0.19.12"
+ "@esbuild/win32-x64" "0.19.12"
+
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -6477,6 +6691,11 @@ fsevents@^2.1.2, fsevents@~2.3.2:
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+fsevents@~2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -8661,6 +8880,11 @@ nanoid@^3.3.1, nanoid@^3.3.6:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
+nanoid@^3.3.7:
+ version "3.3.7"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
+ integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
+
nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@@ -9545,6 +9769,15 @@ postcss@^8.2.15, postcss@^8.4.21:
picocolors "^1.0.0"
source-map-js "^1.0.2"
+postcss@^8.4.32:
+ version "8.4.33"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742"
+ integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==
+ dependencies:
+ nanoid "^3.3.7"
+ picocolors "^1.0.0"
+ source-map-js "^1.0.2"
+
powerhooks@^0.26.7:
version "0.26.7"
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.26.7.tgz#3c9709a6012207e073aa268a775b352905ea46f5"
@@ -10305,6 +10538,28 @@ rollup@^3.18.0:
optionalDependencies:
fsevents "~2.3.2"
+rollup@^4.2.0:
+ version "4.9.6"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.9.6.tgz#4515facb0318ecca254a2ee1315e22e09efc50a0"
+ integrity sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==
+ dependencies:
+ "@types/estree" "1.0.5"
+ optionalDependencies:
+ "@rollup/rollup-android-arm-eabi" "4.9.6"
+ "@rollup/rollup-android-arm64" "4.9.6"
+ "@rollup/rollup-darwin-arm64" "4.9.6"
+ "@rollup/rollup-darwin-x64" "4.9.6"
+ "@rollup/rollup-linux-arm-gnueabihf" "4.9.6"
+ "@rollup/rollup-linux-arm64-gnu" "4.9.6"
+ "@rollup/rollup-linux-arm64-musl" "4.9.6"
+ "@rollup/rollup-linux-riscv64-gnu" "4.9.6"
+ "@rollup/rollup-linux-x64-gnu" "4.9.6"
+ "@rollup/rollup-linux-x64-musl" "4.9.6"
+ "@rollup/rollup-win32-arm64-msvc" "4.9.6"
+ "@rollup/rollup-win32-ia32-msvc" "4.9.6"
+ "@rollup/rollup-win32-x64-msvc" "4.9.6"
+ fsevents "~2.3.2"
+
rsvp@^4.8.4:
version "4.8.5"
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
@@ -11951,6 +12206,17 @@ vite-node@0.29.8:
optionalDependencies:
fsevents "~2.3.2"
+vite@^5.0.12:
+ version "5.0.12"
+ resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.12.tgz#8a2ffd4da36c132aec4adafe05d7adde38333c47"
+ integrity sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==
+ dependencies:
+ esbuild "^0.19.3"
+ postcss "^8.4.32"
+ rollup "^4.2.0"
+ optionalDependencies:
+ fsevents "~2.3.3"
+
vitest@^0.29.8:
version "0.29.8"
resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.29.8.tgz#9c13cfa007c3511e86c26e1fe9a686bb4dbaec80"