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}
  • ", - " " - ].join("\n"), - [ - " <#if totp.policy.supportedApplications?has_content>", - " <#list totp.policy.supportedApplications as app>", - "
  • ${app}
  • ", - " ", - " " - ].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}
  • ", + " " + ].join("\n"), + [ + " <#if totp.policy.supportedApplications?has_content>", + " <#list totp.policy.supportedApplications as app>", + "
  • ${app}
  • ", + " ", + " " + ].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"