From 0e93d4ed0918f81a0fcf7672b46a9c794c617e51 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sat, 26 Oct 2024 21:23:18 +0200 Subject: [PATCH 01/21] Implement admin theme support (checkpoint) --- scripts/generate-i18n-messages.ts | 2 +- src/bin/add-story.ts | 17 +- src/bin/eject-page.ts | 24 +-- .../initialize-account-theme.ts | 36 +---- .../initialize-admin-theme/copyBoilerplate.ts | 19 +++ src/bin/initialize-admin-theme/index.ts | 1 + .../initialize-admin-theme.ts | 60 +++++++ .../initializeAdminTheme.ts | 150 ++++++++++++++++++ .../initialize-admin-theme/src/KcContext.ts | 7 + src/bin/initialize-admin-theme/src/KcPage.tsx | 11 ++ .../generateMessageProperties.ts | 2 +- .../generateResources/generateResources.ts | 73 ++++++--- src/bin/main.ts | 14 ++ src/bin/shared/buildContext.ts | 6 +- src/bin/shared/constants.ts | 2 +- src/bin/shared/customHandler.ts | 1 + src/bin/shared/exitIfUncommittedChanges.ts | 36 +++++ src/bin/tsconfig.json | 2 +- src/bin/update-kc-gen.ts | 6 + test/tsconfig.json | 5 +- 20 files changed, 401 insertions(+), 73 deletions(-) create mode 100644 src/bin/initialize-admin-theme/copyBoilerplate.ts create mode 100644 src/bin/initialize-admin-theme/index.ts create mode 100644 src/bin/initialize-admin-theme/initialize-admin-theme.ts create mode 100644 src/bin/initialize-admin-theme/initializeAdminTheme.ts create mode 100644 src/bin/initialize-admin-theme/src/KcContext.ts create mode 100644 src/bin/initialize-admin-theme/src/KcPage.tsx create mode 100644 src/bin/shared/exitIfUncommittedChanges.ts diff --git a/scripts/generate-i18n-messages.ts b/scripts/generate-i18n-messages.ts index a50ff2e6..e6fc3104 100644 --- a/scripts/generate-i18n-messages.ts +++ b/scripts/generate-i18n-messages.ts @@ -37,7 +37,7 @@ async function generateI18nMessages() { const record: { [themeType: string]: { [language: string]: Dictionary } } = {}; - for (const themeType of THEME_TYPES) { + for (const themeType of THEME_TYPES.filter(themeType => themeType !== "admin")) { const { extractedDirPath } = await downloadKeycloakDefaultTheme({ keycloakVersionId: (() => { switch (themeType) { diff --git a/src/bin/add-story.ts b/src/bin/add-story.ts index 66ab40ff..dc528a5e 100644 --- a/src/bin/add-story.ts +++ b/src/bin/add-story.ts @@ -5,8 +5,7 @@ import { ACCOUNT_THEME_PAGE_IDS, type LoginThemePageId, type AccountThemePageId, - THEME_TYPES, - type ThemeType + THEME_TYPES } from "./shared/constants"; import { capitalize } from "tsafe/capitalize"; import * as fs from "fs"; @@ -39,6 +38,8 @@ export async function command(params: { buildContext: BuildContext }) { return buildContext.implementedThemeTypes.account.isImplemented; case "login": return buildContext.implementedThemeTypes.login.isImplemented; + case "admin": + return buildContext.implementedThemeTypes.admin.isImplemented; } assert>(false); }); @@ -49,7 +50,7 @@ export async function command(params: { buildContext: BuildContext }) { return values[0]; } - const { value } = await cliSelect({ + const { value } = await cliSelect({ values }).catch(() => { process.exit(-1); @@ -68,6 +69,16 @@ export async function command(params: { buildContext: BuildContext }) { ); process.exit(0); + return; + } + + if (themeType === "admin") { + console.log( + `${chalk.red("✗")} Sorry, there is no Storybook support for the Account UI.` + ); + + process.exit(0); + return; } console.log(`→ ${themeType}`); diff --git a/src/bin/eject-page.ts b/src/bin/eject-page.ts index 2cb2d9af..279f03e1 100644 --- a/src/bin/eject-page.ts +++ b/src/bin/eject-page.ts @@ -7,8 +7,7 @@ import { ACCOUNT_THEME_PAGE_IDS, type LoginThemePageId, type AccountThemePageId, - THEME_TYPES, - type ThemeType + THEME_TYPES } from "./shared/constants"; import { capitalize } from "tsafe/capitalize"; import * as fs from "fs"; @@ -46,6 +45,8 @@ export async function command(params: { buildContext: BuildContext }) { return buildContext.implementedThemeTypes.account.isImplemented; case "login": return buildContext.implementedThemeTypes.login.isImplemented; + case "admin": + return buildContext.implementedThemeTypes.admin.isImplemented; } assert>(false); }); @@ -56,7 +57,7 @@ export async function command(params: { buildContext: BuildContext }) { return values[0]; } - const { value } = await cliSelect({ + const { value } = await cliSelect({ values }).catch(() => { process.exit(-1); @@ -66,21 +67,22 @@ export async function command(params: { buildContext: BuildContext }) { })(); if ( - themeType === "account" && - (assert(buildContext.implementedThemeTypes.account.isImplemented), - buildContext.implementedThemeTypes.account.type === "Single-Page") + themeType === "admin" || + (themeType === "account" && + (assert(buildContext.implementedThemeTypes.account.isImplemented), + buildContext.implementedThemeTypes.account.type === "Single-Page")) ) { const srcDirPath = pathJoin( pathDirname(buildContext.packageJsonFilePath), "node_modules", "@keycloakify", - "keycloak-account-ui", + `keycloak-${themeType}-ui`, "src" ); console.log( [ - `There isn't an interactive CLI to eject components of the Single-Page Account theme.`, + `There isn't an interactive CLI to eject components of the ${themeType} UI.`, `You can however copy paste into your codebase the any file or directory from the following source directory:`, ``, `${chalk.bold(pathJoin(pathRelative(process.cwd(), srcDirPath)))}`, @@ -89,7 +91,8 @@ export async function command(params: { buildContext: BuildContext }) { ); eject_entrypoint: { - const kcAccountUiTsxFileRelativePath = "KcAccountUi.tsx"; + const kcAccountUiTsxFileRelativePath = + `Kc${capitalize(themeType)}Ui.tsx` as const; const accountThemeSrcDirPath = pathJoin( buildContext.themeSrcDirPath, @@ -120,7 +123,7 @@ export async function command(params: { buildContext: BuildContext }) { ).replace(/.tsx$/, ""); const modifiedKcPageTsxCode = kcPageTsxCode.replace( - `@keycloakify/keycloak-account-ui/${componentName}`, + `@keycloakify/keycloak-${themeType}-ui/${componentName}`, `./${componentName}` ); @@ -146,6 +149,7 @@ export async function command(params: { buildContext: BuildContext }) { } process.exit(0); + return; } console.log(`→ ${themeType}`); diff --git a/src/bin/initialize-account-theme/initialize-account-theme.ts b/src/bin/initialize-account-theme/initialize-account-theme.ts index dd7c61c8..0caead70 100644 --- a/src/bin/initialize-account-theme/initialize-account-theme.ts +++ b/src/bin/initialize-account-theme/initialize-account-theme.ts @@ -1,12 +1,12 @@ import type { BuildContext } from "../shared/buildContext"; import cliSelect from "cli-select"; -import child_process from "child_process"; import chalk from "chalk"; import { join as pathJoin, relative as pathRelative } from "path"; import * as fs from "fs"; import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig"; import { command as updateKcGenCommand } from "../update-kc-gen"; import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate"; +import { exitIfUncommittedChanges } from "../shared/exitIfUncommittedChanges"; export async function command(params: { buildContext: BuildContext }) { const { buildContext } = params; @@ -38,37 +38,9 @@ export async function command(params: { buildContext: BuildContext }) { process.exit(-1); } - exit_if_uncommitted_changes: { - let hasUncommittedChanges: boolean | undefined = undefined; - - try { - hasUncommittedChanges = - child_process - .execSync(`git status --porcelain`, { - cwd: buildContext.projectDirPath - }) - .toString() - .trim() !== ""; - } catch { - // Probably not a git repository - break exit_if_uncommitted_changes; - } - - if (!hasUncommittedChanges) { - break exit_if_uncommitted_changes; - } - console.warn( - [ - chalk.red( - "Please commit or stash your changes before running this command.\n" - ), - "This command will modify your project's files so it's better to have a clean working directory", - "so that you can easily see what has been changed and revert if needed." - ].join(" ") - ); - - process.exit(-1); - } + exitIfUncommittedChanges({ + projectDirPath: buildContext.projectDirPath + }); const { value: accountThemeType } = await cliSelect({ values: ["Single-Page" as const, "Multi-Page" as const] diff --git a/src/bin/initialize-admin-theme/copyBoilerplate.ts b/src/bin/initialize-admin-theme/copyBoilerplate.ts new file mode 100644 index 00000000..f2e6e7ca --- /dev/null +++ b/src/bin/initialize-admin-theme/copyBoilerplate.ts @@ -0,0 +1,19 @@ +import * as fs from "fs"; +import { join as pathJoin } from "path"; +import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath"; + +export function copyBoilerplate(params: { adminThemeSrcDirPath: string }) { + const { adminThemeSrcDirPath } = params; + + fs.cpSync( + pathJoin( + getThisCodebaseRootDirPath(), + "src", + "bin", + "initialize-admin-theme", + "src" + ), + adminThemeSrcDirPath, + { recursive: true } + ); +} diff --git a/src/bin/initialize-admin-theme/index.ts b/src/bin/initialize-admin-theme/index.ts new file mode 100644 index 00000000..b229b5f4 --- /dev/null +++ b/src/bin/initialize-admin-theme/index.ts @@ -0,0 +1 @@ +export * from "./initialize-admin-theme"; diff --git a/src/bin/initialize-admin-theme/initialize-admin-theme.ts b/src/bin/initialize-admin-theme/initialize-admin-theme.ts new file mode 100644 index 00000000..e6ba503b --- /dev/null +++ b/src/bin/initialize-admin-theme/initialize-admin-theme.ts @@ -0,0 +1,60 @@ +import type { BuildContext } from "../shared/buildContext"; +import chalk from "chalk"; +import { join as pathJoin, relative as pathRelative } from "path"; +import * as fs from "fs"; +import { command as updateKcGenCommand } from "../update-kc-gen"; +import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate"; +import { exitIfUncommittedChanges } from "../shared/exitIfUncommittedChanges"; +import { initializeAdminTheme } from "./initializeAdminTheme"; + +export async function command(params: { buildContext: BuildContext }) { + const { buildContext } = params; + + const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({ + commandName: "initialize-admin-theme", + buildContext + }); + + if (hasBeenHandled) { + return; + } + + const adminThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "admin"); + + if ( + fs.existsSync(adminThemeSrcDirPath) && + fs.readdirSync(adminThemeSrcDirPath).length > 0 + ) { + console.warn( + chalk.red( + `There is already a ${pathRelative( + process.cwd(), + adminThemeSrcDirPath + )} directory in your project. Aborting.` + ) + ); + + process.exit(-1); + } + + exitIfUncommittedChanges({ + projectDirPath: buildContext.projectDirPath + }); + + await initializeAdminTheme({ + adminThemeSrcDirPath, + buildContext + }); + + await updateKcGenCommand({ + buildContext: { + ...buildContext, + implementedThemeTypes: { + ...buildContext.implementedThemeTypes, + admin: { + isImplemented: true + } + } + } + }); +} diff --git a/src/bin/initialize-admin-theme/initializeAdminTheme.ts b/src/bin/initialize-admin-theme/initializeAdminTheme.ts new file mode 100644 index 00000000..8498eefa --- /dev/null +++ b/src/bin/initialize-admin-theme/initializeAdminTheme.ts @@ -0,0 +1,150 @@ +import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; +import type { BuildContext } from "../shared/buildContext"; +import * as fs from "fs"; +import chalk from "chalk"; +import { + getLatestsSemVersionedTag, + type BuildContextLike as BuildContextLike_getLatestsSemVersionedTag +} from "../shared/getLatestsSemVersionedTag"; +import { SemVer } from "../tools/SemVer"; +import fetch from "make-fetch-happen"; +import { z } from "zod"; +import { assert, type Equals } from "tsafe/assert"; +import { is } from "tsafe/is"; +import { id } from "tsafe/id"; +import { npmInstall } from "../tools/npmInstall"; +import { copyBoilerplate } from "./copyBoilerplate"; +import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath"; + +type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & { + fetchOptions: BuildContext["fetchOptions"]; + packageJsonFilePath: string; +}; + +assert(); + +export async function initializeAdminTheme(params: { + adminThemeSrcDirPath: string; + buildContext: BuildContextLike; +}) { + const { adminThemeSrcDirPath, buildContext } = params; + + const OWNER = "keycloakify"; + const REPO = "keycloak-admin-ui"; + + const [semVersionedTag] = await getLatestsSemVersionedTag({ + owner: OWNER, + repo: REPO, + count: 1, + doIgnoreReleaseCandidates: false, + buildContext + }); + + const dependencies = await fetch( + `https://raw.githubusercontent.com/${OWNER}/${REPO}/${semVersionedTag.tag}/dependencies.gen.json`, + buildContext.fetchOptions + ) + .then(r => r.json()) + .then( + (() => { + type Dependencies = { + dependencies: Record; + devDependencies?: Record; + }; + + const zDependencies = (() => { + type TargetType = Dependencies; + + const zTargetType = z.object({ + dependencies: z.record(z.string()), + devDependencies: z.record(z.string()).optional() + }); + + assert, TargetType>>(); + + return id>(zTargetType); + })(); + + return o => zDependencies.parse(o); + })() + ); + + dependencies.dependencies["@keycloakify/keycloak-admin-ui"] = SemVer.stringify( + semVersionedTag.version + ); + + const parsedPackageJson = (() => { + type ParsedPackageJson = { + dependencies?: Record; + devDependencies?: Record; + }; + + const zParsedPackageJson = (() => { + type TargetType = ParsedPackageJson; + + const zTargetType = z.object({ + dependencies: z.record(z.string()).optional(), + devDependencies: z.record(z.string()).optional() + }); + + assert, TargetType>>(); + + return id>(zTargetType); + })(); + const parsedPackageJson = JSON.parse( + fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8") + ); + + zParsedPackageJson.parse(parsedPackageJson); + + assert(is(parsedPackageJson)); + + return parsedPackageJson; + })(); + + parsedPackageJson.dependencies = { + ...parsedPackageJson.dependencies, + ...dependencies.dependencies + }; + + parsedPackageJson.devDependencies = { + ...parsedPackageJson.devDependencies, + ...dependencies.devDependencies + }; + + if (Object.keys(parsedPackageJson.devDependencies).length === 0) { + delete parsedPackageJson.devDependencies; + } + + fs.writeFileSync( + buildContext.packageJsonFilePath, + JSON.stringify(parsedPackageJson, undefined, 4) + ); + + run_npm_install: { + if ( + JSON.parse( + fs + .readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json")) + .toString("utf8") + )["version"] === "0.0.0" + ) { + //NOTE: Linked version + break run_npm_install; + } + + npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) }); + } + + copyBoilerplate({ adminThemeSrcDirPath }); + + console.log( + [ + chalk.green("The Admin theme has been successfully initialized."), + `Using Admin UI of Keycloak version: ${chalk.bold(semVersionedTag.tag.split("-")[0])}`, + `Directory created: ${chalk.bold(pathRelative(process.cwd(), adminThemeSrcDirPath))}`, + `Dependencies added to your project's package.json: `, + chalk.bold(JSON.stringify(dependencies, null, 2)) + ].join("\n") + ); +} diff --git a/src/bin/initialize-admin-theme/src/KcContext.ts b/src/bin/initialize-admin-theme/src/KcContext.ts new file mode 100644 index 00000000..e86dc980 --- /dev/null +++ b/src/bin/initialize-admin-theme/src/KcContext.ts @@ -0,0 +1,7 @@ +import type { KcContextLike } from "@keycloakify/keycloak-admin-ui"; +import type { KcEnvName } from "../kc.gen"; + +export type KcContext = KcContextLike & { + themeType: "admin"; + properties: Record; +}; diff --git a/src/bin/initialize-admin-theme/src/KcPage.tsx b/src/bin/initialize-admin-theme/src/KcPage.tsx new file mode 100644 index 00000000..f5f08f1f --- /dev/null +++ b/src/bin/initialize-admin-theme/src/KcPage.tsx @@ -0,0 +1,11 @@ +import { lazy } from "react"; +import { KcAdminUiLoader } from "@keycloakify/keycloak-admin-ui"; +import type { KcContext } from "./KcContext"; + +const KcAdminUi = lazy(() => import("@keycloakify/keycloak-admin-ui/KcAdminUi")); + +export default function KcPage(props: { kcContext: KcContext }) { + const { kcContext } = props; + + return ; +} diff --git a/src/bin/keycloakify/generateResources/generateMessageProperties.ts b/src/bin/keycloakify/generateResources/generateMessageProperties.ts index fc65b2fa..7ccd3003 100644 --- a/src/bin/keycloakify/generateResources/generateMessageProperties.ts +++ b/src/bin/keycloakify/generateResources/generateMessageProperties.ts @@ -22,7 +22,7 @@ assert(); export function generateMessageProperties(params: { buildContext: BuildContextLike; - themeType: ThemeType; + themeType: Exclude; }): { languageTags: string[]; writeMessagePropertiesFiles: (params: { diff --git a/src/bin/keycloakify/generateResources/generateResources.ts b/src/bin/keycloakify/generateResources/generateResources.ts index b56878a2..d549967a 100644 --- a/src/bin/keycloakify/generateResources/generateResources.ts +++ b/src/bin/keycloakify/generateResources/generateResources.ts @@ -19,7 +19,8 @@ import { type ThemeType, LOGIN_THEME_PAGE_IDS, ACCOUNT_THEME_PAGE_IDS, - WELL_KNOWN_DIRECTORY_BASE_NAME + WELL_KNOWN_DIRECTORY_BASE_NAME, + THEME_TYPES } from "../../shared/constants"; import { assert, type Equals } from "tsafe/assert"; import { readFieldNameUsage } from "./readFieldNameUsage"; @@ -78,15 +79,29 @@ export async function generateResources(params: { Record void> > = {}; - for (const themeType of ["login", "account"] as const) { + for (const themeType of THEME_TYPES) { if (!buildContext.implementedThemeTypes[themeType].isImplemented) { continue; } - const isForAccountSpa = - themeType === "account" && - (assert(buildContext.implementedThemeTypes.account.isImplemented), - buildContext.implementedThemeTypes.account.type === "Single-Page"); + const getAccountThemeType = () => { + assert(themeType === "account"); + + assert(buildContext.implementedThemeTypes.account.isImplemented); + + return buildContext.implementedThemeTypes.account.type; + }; + + const isSpa = (() => { + switch (themeType) { + case "login": + return false; + case "account": + return getAccountThemeType() === "Single-Page"; + case "admin": + return true; + } + })(); const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType }); @@ -101,7 +116,7 @@ export async function generateResources(params: { rmSync(destDirPath, { recursive: true, force: true }); if ( - themeType === "account" && + themeType !== "login" && buildContext.implementedThemeTypes.login.isImplemented ) { // NOTE: We prevent doing it twice, it has been done for the login theme. @@ -194,10 +209,14 @@ export async function generateResources(params: { case "login": return LOGIN_THEME_PAGE_IDS; case "account": - return isForAccountSpa ? ["index.ftl"] : ACCOUNT_THEME_PAGE_IDS; + return getAccountThemeType() === "Single-Page" + ? ["index.ftl"] + : ACCOUNT_THEME_PAGE_IDS; + case "admin": + return ["index.ftl"]; } })(), - ...(isForAccountSpa + ...(isSpa ? [] : readExtraPagesNames({ themeType, @@ -215,10 +234,12 @@ export async function generateResources(params: { let languageTags: string[] | undefined = undefined; i18n_messages_generation: { - if (isForAccountSpa) { + if (isSpa) { break i18n_messages_generation; } + assert(themeType !== "admin"); + const wrap = generateMessageProperties({ buildContext, themeType @@ -231,16 +252,15 @@ export async function generateResources(params: { writeMessagePropertiesFiles; } - bring_in_account_v3_i18n_messages: { - if (!buildContext.implementedThemeTypes.account.isImplemented) { - break bring_in_account_v3_i18n_messages; - } - if (buildContext.implementedThemeTypes.account.type !== "Single-Page") { - break bring_in_account_v3_i18n_messages; + bring_in_spas_messages: { + if (!isSpa) { + break bring_in_spas_messages; } + assert(themeType !== "login"); + const accountUiDirPath = child_process - .execSync("npm list @keycloakify/keycloak-account-ui --parseable", { + .execSync(`npm list @keycloakify/keycloak-${themeType}-ui --parseable`, { cwd: pathDirname(buildContext.packageJsonFilePath) }) .toString("utf8") @@ -255,7 +275,7 @@ export async function generateResources(params: { } const messagesDirPath_dest = pathJoin( - getThemeTypeDirPath({ themeName, themeType: "account" }), + getThemeTypeDirPath({ themeName, themeType }), "messages" ); @@ -267,7 +287,7 @@ export async function generateResources(params: { apply_theme_changes: { const messagesDirPath_theme = pathJoin( buildContext.themeSrcDirPath, - "account", + themeType, "messages" ); @@ -316,7 +336,7 @@ export async function generateResources(params: { } keycloak_static_resources: { - if (isForAccountSpa) { + if (isSpa) { break keycloak_static_resources; } @@ -339,13 +359,22 @@ export async function generateResources(params: { `parent=${(() => { switch (themeType) { case "account": - return isForAccountSpa ? "base" : "account-v1"; + switch (getAccountThemeType()) { + case "Multi-Page": + return "account-v1"; + case "Single-Page": + return "base"; + } case "login": return "keycloak"; + case "admin": + return "base"; } assert>(false); })()}`, - ...(isForAccountSpa ? ["deprecatedMode=false"] : []), + ...(themeType === "account" && getAccountThemeType() === "Single-Page" + ? ["deprecatedMode=false"] + : []), ...(buildContext.extraThemeProperties ?? []), ...buildContext.environmentVariables.map( ({ name, default: defaultValue }) => diff --git a/src/bin/main.ts b/src/bin/main.ts index a6abe1f5..0047b715 100644 --- a/src/bin/main.ts +++ b/src/bin/main.ts @@ -197,6 +197,20 @@ program } }); +program + .command({ + name: "initialize-admin-theme", + description: "Initialize the admin theme." + }) + .task({ + skip, + handler: async ({ projectDirPath }) => { + const { command } = await import("./initialize-admin-theme"); + + await command({ buildContext: getBuildContext({ projectDirPath }) }); + } + }); + program .command({ name: "copy-keycloak-resources-to-public", diff --git a/src/bin/shared/buildContext.ts b/src/bin/shared/buildContext.ts index 89afa6f9..4cb8a552 100644 --- a/src/bin/shared/buildContext.ts +++ b/src/bin/shared/buildContext.ts @@ -52,6 +52,7 @@ export type BuildContext = { account: | { isImplemented: false } | { isImplemented: true; type: "Single-Page" | "Multi-Page" }; + admin: { isImplemented: boolean }; }; packageJsonFilePath: string; bundler: "vite" | "webpack"; @@ -448,7 +449,10 @@ export function getBuildContext(params: { isImplemented: true, type: buildOptions.accountThemeImplementation }; - })() + })(), + admin: { + isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "admin")) + } }; if ( diff --git a/src/bin/shared/constants.ts b/src/bin/shared/constants.ts index d2137b14..cfddb472 100644 --- a/src/bin/shared/constants.ts +++ b/src/bin/shared/constants.ts @@ -4,7 +4,7 @@ export const WELL_KNOWN_DIRECTORY_BASE_NAME = { DIST: "dist" } as const; -export const THEME_TYPES = ["login", "account"] as const; +export const THEME_TYPES = ["login", "account", "admin"] as const; export type ThemeType = (typeof THEME_TYPES)[number]; diff --git a/src/bin/shared/customHandler.ts b/src/bin/shared/customHandler.ts index 7c0b9e1c..9e2cafa5 100644 --- a/src/bin/shared/customHandler.ts +++ b/src/bin/shared/customHandler.ts @@ -11,6 +11,7 @@ export type CommandName = | "eject-page" | "add-story" | "initialize-account-theme" + | "initialize-admin-theme" | "initialize-email-theme" | "copy-keycloak-resources-to-public"; diff --git a/src/bin/shared/exitIfUncommittedChanges.ts b/src/bin/shared/exitIfUncommittedChanges.ts new file mode 100644 index 00000000..6cf9b8c9 --- /dev/null +++ b/src/bin/shared/exitIfUncommittedChanges.ts @@ -0,0 +1,36 @@ +import child_process from "child_process"; +import chalk from "chalk"; + +export function exitIfUncommittedChanges(params: { projectDirPath: string }) { + const { projectDirPath } = params; + + let hasUncommittedChanges: boolean | undefined = undefined; + + try { + hasUncommittedChanges = + child_process + .execSync(`git status --porcelain`, { + cwd: projectDirPath + }) + .toString() + .trim() !== ""; + } catch { + // Probably not a git repository + return; + } + + if (!hasUncommittedChanges) { + return; + } + console.warn( + [ + chalk.red( + "Please commit or stash your changes before running this command.\n" + ), + "This command will modify your project's files so it's better to have a clean working directory", + "so that you can easily see what has been changed and revert if needed." + ].join(" ") + ); + + process.exit(-1); +} diff --git a/src/bin/tsconfig.json b/src/bin/tsconfig.json index b1a87db0..eda3bac6 100644 --- a/src/bin/tsconfig.json +++ b/src/bin/tsconfig.json @@ -10,5 +10,5 @@ "rootDir": "." }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["initialize-account-theme/src"] + "exclude": ["initialize-account-theme/src", "initialize-admin-theme/src"] } diff --git a/src/bin/update-kc-gen.ts b/src/bin/update-kc-gen.ts index 985b3710..24e8d95b 100644 --- a/src/bin/update-kc-gen.ts +++ b/src/bin/update-kc-gen.ts @@ -22,6 +22,7 @@ export async function command(params: { buildContext: BuildContext }) { const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented; const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented; + const hasAdminTheme = buildContext.implementedThemeTypes.admin.isImplemented; const newContent = [ ``, @@ -54,6 +55,7 @@ export async function command(params: { buildContext: BuildContext }) { `export type KcContext =`, hasLoginTheme && ` | import("./login/KcContext").KcContext`, hasAccountTheme && ` | import("./account/KcContext").KcContext`, + hasAdminTheme && ` | import("./admin/KcContext").KcContext`, ` ;`, ``, `declare global {`, @@ -66,6 +68,8 @@ export async function command(params: { buildContext: BuildContext }) { `export const KcLoginPage = lazy(() => import("./login/KcPage"));`, hasAccountTheme && `export const KcAccountPage = lazy(() => import("./account/KcPage"));`, + hasAdminTheme && + `export const KcAdminPage = lazy(() => import("./admin/KcPage"));`, ``, `export function KcPage(`, ` props: {`, @@ -82,6 +86,8 @@ export async function command(params: { buildContext: BuildContext }) { ` case "login": return ;`, hasAccountTheme && ` case "account": return ;`, + hasAdminTheme && + ` case "admin": return ;`, ` }`, ` })()}`, ` `, diff --git a/test/tsconfig.json b/test/tsconfig.json index b47111ab..ae92c7bf 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -17,5 +17,8 @@ "skipLibCheck": true }, "include": ["../src", "."], - "exclude": ["../src/bin/initialize-account-theme/src"] + "exclude": [ + "../src/bin/initialize-account-theme/src", + "../src/bin/initialize-admin-theme/src" + ] } From b0aa0feab5b6bbe255d3af0540bf54f8d927dba6 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sat, 26 Oct 2024 22:06:37 +0200 Subject: [PATCH 02/21] up --- .../generateResources/generateResources.ts | 11 +++++++---- .../generateResources/readFieldNameUsage.ts | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/bin/keycloakify/generateResources/generateResources.ts b/src/bin/keycloakify/generateResources/generateResources.ts index d549967a..5fff1408 100644 --- a/src/bin/keycloakify/generateResources/generateResources.ts +++ b/src/bin/keycloakify/generateResources/generateResources.ts @@ -197,10 +197,13 @@ export async function generateResources(params: { buildContext, keycloakifyVersion: readThisNpmPackageVersion(), themeType, - fieldNames: readFieldNameUsage({ - themeSrcDirPath: buildContext.themeSrcDirPath, - themeType - }) + fieldNames: isSpa + ? [] + : (assert(themeType !== "admin"), + readFieldNameUsage({ + themeSrcDirPath: buildContext.themeSrcDirPath, + themeType + })) }); [ diff --git a/src/bin/keycloakify/generateResources/readFieldNameUsage.ts b/src/bin/keycloakify/generateResources/readFieldNameUsage.ts index 7f5b65ee..478c9348 100644 --- a/src/bin/keycloakify/generateResources/readFieldNameUsage.ts +++ b/src/bin/keycloakify/generateResources/readFieldNameUsage.ts @@ -7,7 +7,7 @@ import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPa /** Assumes the theme type exists */ export function readFieldNameUsage(params: { themeSrcDirPath: string; - themeType: ThemeType; + themeType: Exclude; }): string[] { const { themeSrcDirPath, themeType } = params; From db3732028092b69365ba0c340a203359ce00b803 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 27 Oct 2024 00:10:39 +0200 Subject: [PATCH 03/21] up --- src/bin/eject-page.ts | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/bin/eject-page.ts b/src/bin/eject-page.ts index 279f03e1..a138cdc8 100644 --- a/src/bin/eject-page.ts +++ b/src/bin/eject-page.ts @@ -91,36 +91,27 @@ export async function command(params: { buildContext: BuildContext }) { ); eject_entrypoint: { - const kcAccountUiTsxFileRelativePath = - `Kc${capitalize(themeType)}Ui.tsx` as const; + const kcUiTsxFileRelativePath = `Kc${capitalize(themeType)}Ui.tsx` as const; - const accountThemeSrcDirPath = pathJoin( - buildContext.themeSrcDirPath, - "account" - ); + const themeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, themeType); - const targetFilePath = pathJoin( - accountThemeSrcDirPath, - kcAccountUiTsxFileRelativePath - ); + const targetFilePath = pathJoin(themeSrcDirPath, kcUiTsxFileRelativePath); if (fs.existsSync(targetFilePath)) { break eject_entrypoint; } - fs.cpSync( - pathJoin(srcDirPath, kcAccountUiTsxFileRelativePath), - targetFilePath - ); + fs.cpSync(pathJoin(srcDirPath, kcUiTsxFileRelativePath), targetFilePath); { - const kcPageTsxFilePath = pathJoin(accountThemeSrcDirPath, "KcPage.tsx"); + const kcPageTsxFilePath = pathJoin(themeSrcDirPath, "KcPage.tsx"); const kcPageTsxCode = fs.readFileSync(kcPageTsxFilePath).toString("utf8"); - const componentName = pathBasename( - kcAccountUiTsxFileRelativePath - ).replace(/.tsx$/, ""); + const componentName = pathBasename(kcUiTsxFileRelativePath).replace( + /.tsx$/, + "" + ); const modifiedKcPageTsxCode = kcPageTsxCode.replace( `@keycloakify/keycloak-${themeType}-ui/${componentName}`, @@ -142,8 +133,8 @@ export async function command(params: { buildContext: BuildContext }) { [ `To help you get started ${chalk.bold(pathRelative(process.cwd(), targetFilePath))} has been copied into your project.`, `The next step is usually to eject ${chalk.bold(routesTsxFilePath)}`, - `with \`cp ${routesTsxFilePath} ${pathRelative(process.cwd(), accountThemeSrcDirPath)}\``, - `then update the import of routes in ${kcAccountUiTsxFileRelativePath}.` + `with \`cp ${routesTsxFilePath} ${pathRelative(process.cwd(), themeSrcDirPath)}\``, + `then update the import of routes in ${kcUiTsxFileRelativePath}.` ].join("\n") ); } From af7a45d12577e0b5817a0a4652c4d0da76485f54 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sat, 2 Nov 2024 22:39:03 +0100 Subject: [PATCH 04/21] checkpoint --- scripts/build/main.ts | 4 +- src/bin/shared/constants.ts | 2 + src/bin/shared/customHandler_delegate.ts | 29 +-- .../getSourceCodeToCopyInUserCodebase.ts | 62 +++++ src/bin/sync-ui-modules/index.ts | 1 + src/bin/sync-ui-modules/listOfEjectedFiles.ts | 3 + src/bin/sync-ui-modules/sync-ui-modules.ts | 13 + src/bin/sync-ui-modules/uiModulesMeta.ts | 225 ++++++++++++++++++ src/bin/tools/crawlAsync.ts | 51 ++++ src/bin/tools/getInstalledModuleDirPath.ts | 53 +++++ src/bin/tools/listInstalledModules.ts | 114 +++++++++ src/bin/tools/nodeModulesBinDirPath.ts | 38 +++ src/bin/tools/readThisNpmPackageVersion.ts | 8 + src/bin/tools/runFormat.ts | 71 ------ src/bin/tools/runPrettier.ts | 77 ++++++ src/bin/update-kc-gen.ts | 41 ++-- 16 files changed, 677 insertions(+), 115 deletions(-) create mode 100644 src/bin/sync-ui-modules/getSourceCodeToCopyInUserCodebase.ts create mode 100644 src/bin/sync-ui-modules/index.ts create mode 100644 src/bin/sync-ui-modules/listOfEjectedFiles.ts create mode 100644 src/bin/sync-ui-modules/sync-ui-modules.ts create mode 100644 src/bin/sync-ui-modules/uiModulesMeta.ts create mode 100644 src/bin/tools/crawlAsync.ts create mode 100644 src/bin/tools/getInstalledModuleDirPath.ts create mode 100644 src/bin/tools/listInstalledModules.ts create mode 100644 src/bin/tools/nodeModulesBinDirPath.ts delete mode 100644 src/bin/tools/runFormat.ts create mode 100644 src/bin/tools/runPrettier.ts diff --git a/scripts/build/main.ts b/scripts/build/main.ts index 027e80d8..097c886a 100644 --- a/scripts/build/main.ts +++ b/scripts/build/main.ts @@ -40,7 +40,9 @@ import { vendorFrontendDependencies } from "./vendorFrontendDependencies"; ); } - run(`npx ncc build ${join("dist", "bin", "main.js")} -o ${join("dist", "ncc_out")}`); + run( + `npx ncc build ${join("dist", "bin", "main.js")} --external prettier -o ${join("dist", "ncc_out")}` + ); transformCodebase({ srcDirPath: join("dist", "ncc_out"), diff --git a/src/bin/shared/constants.ts b/src/bin/shared/constants.ts index cfddb472..ea54c411 100644 --- a/src/bin/shared/constants.ts +++ b/src/bin/shared/constants.ts @@ -76,3 +76,5 @@ export const CUSTOM_HANDLER_ENV_NAMES = { COMMAND_NAME: "KEYCLOAKIFY_COMMAND_NAME", BUILD_CONTEXT: "KEYCLOAKIFY_BUILD_CONTEXT" }; + +export const KC_GEN_FILE_PATH_RELATIVE_TO_THEME_SRC_DIR = "kc-gen.tsx"; diff --git a/src/bin/shared/customHandler_delegate.ts b/src/bin/shared/customHandler_delegate.ts index 4a719f34..ed38152b 100644 --- a/src/bin/shared/customHandler_delegate.ts +++ b/src/bin/shared/customHandler_delegate.ts @@ -8,7 +8,7 @@ import { ApiVersion } from "./customHandler"; import * as child_process from "child_process"; -import { sep as pathSep } from "path"; +import { getNodeModulesBinDirPath } from "../tools/nodeModulesBinDirPath"; import * as fs from "fs"; assert>(); @@ -19,32 +19,7 @@ export function maybeDelegateCommandToCustomHandler(params: { }): { hasBeenHandled: boolean } { const { commandName, buildContext } = params; - const nodeModulesBinDirPath = (() => { - const binPath = process.argv[1]; - - const segments: string[] = [".bin"]; - - let foundNodeModules = false; - - for (const segment of binPath.split(pathSep).reverse()) { - skip_segment: { - if (foundNodeModules) { - break skip_segment; - } - - if (segment === "node_modules") { - foundNodeModules = true; - break skip_segment; - } - - continue; - } - - segments.unshift(segment); - } - - return segments.join(pathSep); - })(); + const nodeModulesBinDirPath = getNodeModulesBinDirPath(); if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) { return { hasBeenHandled: false }; diff --git a/src/bin/sync-ui-modules/getSourceCodeToCopyInUserCodebase.ts b/src/bin/sync-ui-modules/getSourceCodeToCopyInUserCodebase.ts new file mode 100644 index 00000000..8306e67d --- /dev/null +++ b/src/bin/sync-ui-modules/getSourceCodeToCopyInUserCodebase.ts @@ -0,0 +1,62 @@ +import { getIsPrettierAvailable, runPrettier } from "../tools/runPrettier"; +import * as fsPr from "fs/promises"; +import { join as pathJoin, sep as pathSep } from "path"; +import { assert } from "tsafe/assert"; +import type { BuildContext } from "../shared/buildContext"; + +export type BuildContextLike = { + themeSrcDirPath: string; +}; + +assert(); + +export async function getSourceCodeToCopyInUserCodebase(params: { + buildContext: BuildContextLike; + relativeFromDirPath: string; + fileRelativePath: string; + commentData: { + isForEjection: boolean; + uiModuleName: string; + uiModuleVersion: string; + }; +}): Promise { + const { buildContext, relativeFromDirPath, fileRelativePath, commentData } = params; + + let sourceCode = ( + await fsPr.readFile(pathJoin(relativeFromDirPath, fileRelativePath)) + ).toString("utf8"); + + const comment = (() => { + if (commentData.isForEjection) { + return [ + `/*`, + ` This file was ejected from ${commentData.uiModuleName} version ${commentData.uiModuleVersion}.`, + `*/` + ].join("\n"); + } else { + return [ + `/*`, + ` WARNING: Before modifying this file run the following command:`, + ` \`npx keycloakify eject-file ${fileRelativePath.split(pathSep).join("/")}\``, + ` `, + ` This file comes from ${commentData.uiModuleName} version ${commentData.uiModuleVersion}.`, + `*/` + ]; + } + })(); + + sourceCode = [comment, ``, sourceCode].join("\n"); + + format: { + if (!(await getIsPrettierAvailable())) { + break format; + } + + sourceCode = await runPrettier({ + filePath: pathJoin(buildContext.themeSrcDirPath, fileRelativePath), + sourceCode + }); + } + + return sourceCode; +} diff --git a/src/bin/sync-ui-modules/index.ts b/src/bin/sync-ui-modules/index.ts new file mode 100644 index 00000000..45d420bd --- /dev/null +++ b/src/bin/sync-ui-modules/index.ts @@ -0,0 +1 @@ +export * from "./sync-ui-modules"; diff --git a/src/bin/sync-ui-modules/listOfEjectedFiles.ts b/src/bin/sync-ui-modules/listOfEjectedFiles.ts new file mode 100644 index 00000000..ccfe8046 --- /dev/null +++ b/src/bin/sync-ui-modules/listOfEjectedFiles.ts @@ -0,0 +1,3 @@ +export async function getListOfEjectedFiles(params: {}): Promise {} + +export async function writeListOfEjectedFiles(params: {}) {} diff --git a/src/bin/sync-ui-modules/sync-ui-modules.ts b/src/bin/sync-ui-modules/sync-ui-modules.ts new file mode 100644 index 00000000..abd1c4c8 --- /dev/null +++ b/src/bin/sync-ui-modules/sync-ui-modules.ts @@ -0,0 +1,13 @@ +import type { BuildContext } from "./shared/buildContext"; +import { assert, type Equals } from "tsafe/assert"; +import { id } from "tsafe/id"; +import { is } from "tsafe/is"; +import { z } from "zod"; +import { join as pathJoin } from "path"; +import { existsAsync } from "./tools/fs.existsAsync"; + +import * as fsPr from "fs/promises"; + +export async function command(params: { buildContext: BuildContext }) { + const { buildContext } = params; +} diff --git a/src/bin/sync-ui-modules/uiModulesMeta.ts b/src/bin/sync-ui-modules/uiModulesMeta.ts new file mode 100644 index 00000000..b87d09b5 --- /dev/null +++ b/src/bin/sync-ui-modules/uiModulesMeta.ts @@ -0,0 +1,225 @@ +import { assert, type Equals } from "tsafe/assert"; +import { id } from "tsafe/id"; +import { z } from "zod"; +import { join as pathJoin } from "path"; +import * as fsPr from "fs/promises"; +import type { BuildContext } from "../shared/buildContext"; +import { is } from "tsafe/is"; +import { existsAsync } from "../tools/fs.existsAsync"; +import { listInstalledModules } from "../tools/listInstalledModules"; +import { crawlAsync } from "../tools/crawlAsync"; +import { getIsPrettierAvailable, getPrettierAndConfig } from "../tools/runPrettier"; +import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion"; +import { + getSourceCodeToCopyInUserCodebase, + type BuildContextLike as BuildContextLike_getSourceCodeToCopyInUserCodebase +} from "./getSourceCodeToCopyInUserCodebase"; +import * as crypto from "crypto"; + +export type UiModulesMeta = { + keycloakifyVersion: string; + prettierConfigHash: string | null; + entries: UiModulesMeta.Entry[]; +}; + +export namespace UiModulesMeta { + export type Entry = { + moduleName: string; + version: string; + files: { + fileRelativePath: string; + hash: string; + }[]; + }; +} + +const zUiModuleMetasEntry = (() => { + type ExpectedType = UiModulesMeta.Entry; + + const zTargetType = z.object({ + moduleName: z.string(), + version: z.string(), + files: z.array( + z.object({ + fileRelativePath: z.string(), + hash: z.string() + }) + ) + }); + + type InferredType = z.infer; + + assert>(); + + return id>(zTargetType); +})(); + +const zUiModulesMeta = (() => { + type ExpectedType = UiModulesMeta; + + const zTargetType = z.object({ + keycloakifyVersion: z.string(), + prettierConfigHash: z.union([z.string(), z.null()]), + entries: z.array(zUiModuleMetasEntry) + }); + + type InferredType = z.infer; + + assert>(); + + return id>(zTargetType); +})(); + +const RELATIVE_FILE_PATH = pathJoin("uiModulesMeta.json"); + +export type BuildContextLike = BuildContextLike_getSourceCodeToCopyInUserCodebase & { + cacheDirPath: string; + packageJsonFilePath: string; + projectDirPath: string; +}; + +assert(); + +export async function readOrCreateUiModulesMeta(params: { + buildContext: BuildContextLike; +}): Promise { + const { buildContext } = params; + + const filePath = pathJoin(buildContext.cacheDirPath, RELATIVE_FILE_PATH); + + const keycloakifyVersion = readThisNpmPackageVersion(); + + const prettierConfigHash = await (async () => { + if (!(await getIsPrettierAvailable())) { + return null; + } + + const { config } = await getPrettierAndConfig(); + + return crypto.createHash("sha256").update(JSON.stringify(config)).digest("hex"); + })(); + + const installedUiModules = await listInstalledModules({ + packageJsonFilePath: buildContext.packageJsonFilePath, + projectDirPath: buildContext.packageJsonFilePath, + filter: ({ moduleName }) => + moduleName.includes("keycloakify") && moduleName.endsWith("-ui") + }); + + const upToDateEntries: UiModulesMeta.Entry[] = await (async () => { + const uiModulesMeta_cache: UiModulesMeta | undefined = await (async () => { + if (!(await existsAsync(filePath))) { + return undefined; + } + + const contentStr = (await fsPr.readFile(filePath)).toString("utf8"); + + let uiModuleMeta: unknown; + + try { + uiModuleMeta = JSON.parse(contentStr); + } catch { + return undefined; + } + + try { + zUiModulesMeta.parse(uiModuleMeta); + } catch { + return undefined; + } + + assert(is(uiModuleMeta)); + + return uiModuleMeta; + })(); + + if (uiModulesMeta_cache === undefined) { + return []; + } + + if (uiModulesMeta_cache.keycloakifyVersion !== keycloakifyVersion) { + return []; + } + + if (uiModulesMeta_cache.prettierConfigHash !== prettierConfigHash) { + return []; + } + + const upToDateEntries = uiModulesMeta_cache.entries.filter(entry => { + const correspondingInstalledUiModule = installedUiModules.find( + installedUiModule => installedUiModule.moduleName === entry.moduleName + ); + + if (correspondingInstalledUiModule === undefined) { + return false; + } + + return correspondingInstalledUiModule.version === entry.version; + }); + + return upToDateEntries; + })(); + + const entries = await Promise.all( + installedUiModules.map( + async ({ moduleName, version, dirPath }): Promise => { + use_cache: { + const cachedEntry = upToDateEntries.find( + entry => entry.moduleName === moduleName + ); + + if (cachedEntry === undefined) { + break use_cache; + } + + return cachedEntry; + } + + const files: UiModulesMeta.Entry["files"] = []; + + { + const srcDirPath = pathJoin(dirPath, "src"); + + await crawlAsync({ + dirPath: srcDirPath, + returnedPathsType: "relative to dirPath", + onFileFound: async fileRelativePath => { + const sourceCode = await getSourceCodeToCopyInUserCodebase({ + buildContext, + relativeFromDirPath: srcDirPath, + fileRelativePath, + commentData: { + isForEjection: false, + uiModuleName: moduleName, + uiModuleVersion: version + } + }); + + const hash = crypto + .createHash("sha256") + .update(sourceCode) + .digest("hex"); + + files.push({ + fileRelativePath, + hash + }); + } + }); + } + + return id({ + files, + moduleName, + version + }); + } + ) + ); + + return id({ + keycloakifyVersion, + prettierConfigHash, + entries + }); +} diff --git a/src/bin/tools/crawlAsync.ts b/src/bin/tools/crawlAsync.ts new file mode 100644 index 00000000..e1fda91b --- /dev/null +++ b/src/bin/tools/crawlAsync.ts @@ -0,0 +1,51 @@ +import * as fsPr from "fs/promises"; +import { join as pathJoin, relative as pathRelative } from "path"; +import { assert, type Equals } from "tsafe/assert"; + +/** List all files in a given directory return paths relative to the dir_path */ +export async function crawlAsync(params: { + dirPath: string; + returnedPathsType: "absolute" | "relative to dirPath"; + onFileFound: (filePath: string) => void; +}) { + const { dirPath, returnedPathsType, onFileFound } = params; + + await crawlAsyncRec({ + dirPath, + onFileFound: ({ filePath }) => { + switch (returnedPathsType) { + case "absolute": + onFileFound(filePath); + return; + case "relative to dirPath": + onFileFound(pathRelative(dirPath, filePath)); + return; + } + assert>(); + } + }); +} + +async function crawlAsyncRec(params: { + dirPath: string; + onFileFound: (params: { filePath: string }) => void; +}) { + const { dirPath, onFileFound } = params; + + await Promise.all( + (await fsPr.readdir(dirPath)).map(async basename => { + const fileOrDirPath = pathJoin(dirPath, basename); + + const isDirectory = await fsPr + .lstat(fileOrDirPath) + .then(stat => stat.isDirectory()); + + if (isDirectory) { + await crawlAsyncRec({ dirPath: fileOrDirPath, onFileFound }); + return; + } + + onFileFound({ filePath: fileOrDirPath }); + }) + ); +} diff --git a/src/bin/tools/getInstalledModuleDirPath.ts b/src/bin/tools/getInstalledModuleDirPath.ts new file mode 100644 index 00000000..260c3c5e --- /dev/null +++ b/src/bin/tools/getInstalledModuleDirPath.ts @@ -0,0 +1,53 @@ +import { dirname as pathDirname, join as pathJoin } from "path"; +import { existsAsync } from "./fs.existsAsync"; +import * as child_process from "child_process"; +import { assert } from "tsafe/assert"; + +export async function getInstalledModuleDirPath(params: { + moduleName: string; + packageJsonFilePath: string; + projectDirPath: string; +}) { + const { moduleName, packageJsonFilePath, projectDirPath } = params; + + const packageJsonDirPath = pathDirname(packageJsonFilePath); + + common_case: { + const dirPath = pathJoin( + ...[packageJsonDirPath, "node_modules", ...moduleName.split("/")] + ); + + if (!(await existsAsync(dirPath))) { + break common_case; + } + + return dirPath; + } + + node_modules_at_root_case: { + if (projectDirPath === packageJsonDirPath) { + break node_modules_at_root_case; + } + + const dirPath = pathJoin( + ...[projectDirPath, "node_modules", ...moduleName.split("/")] + ); + + if (!(await existsAsync(dirPath))) { + break node_modules_at_root_case; + } + + return dirPath; + } + + const dirPath = child_process + .execSync(`npm list ${moduleName}`, { + cwd: packageJsonDirPath + }) + .toString("utf8") + .trim(); + + assert(dirPath !== ""); + + return dirPath; +} diff --git a/src/bin/tools/listInstalledModules.ts b/src/bin/tools/listInstalledModules.ts new file mode 100644 index 00000000..5cecad83 --- /dev/null +++ b/src/bin/tools/listInstalledModules.ts @@ -0,0 +1,114 @@ +import { assert, type Equals } from "tsafe/assert"; +import { id } from "tsafe/id"; +import { z } from "zod"; +import { join as pathJoin } from "path"; +import * as fsPr from "fs/promises"; +import { is } from "tsafe/is"; +import { getInstalledModuleDirPath } from "../tools/getInstalledModuleDirPath"; + +export async function listInstalledModules(params: { + packageJsonFilePath: string; + projectDirPath: string; + filter: (params: { moduleName: string }) => boolean; +}): Promise<{ moduleName: string; version: string; dirPath: string }[]> { + const { packageJsonFilePath, projectDirPath, filter } = params; + + const parsedPackageJson = await readPackageJsonDependencies({ + packageJsonFilePath + }); + + const uiModuleNames = ( + [parsedPackageJson.dependencies, parsedPackageJson.devDependencies] as const + ) + .filter(obj => obj !== undefined) + .map(obj => Object.keys(obj)) + .flat() + .filter(moduleName => filter({ moduleName })); + const result = await Promise.all( + uiModuleNames.map(async moduleName => { + const dirPath = await getInstalledModuleDirPath({ + moduleName, + packageJsonFilePath, + projectDirPath + }); + + const { version } = await readPackageJsonVersion({ + packageJsonFilePath: pathJoin(dirPath, "package.json") + }); + + return { moduleName, version, dirPath } as const; + }) + ); + + return result; +} + +const { readPackageJsonDependencies } = (() => { + type ParsedPackageJson = { + dependencies?: Record; + devDependencies?: Record; + }; + + const zParsedPackageJson = (() => { + type TargetType = ParsedPackageJson; + + const zTargetType = z.object({ + dependencies: z.record(z.string()).optional(), + devDependencies: z.record(z.string()).optional() + }); + + assert, TargetType>>(); + + return id>(zTargetType); + })(); + + async function readPackageJsonDependencies(params: { packageJsonFilePath: string }) { + const { packageJsonFilePath } = params; + + const parsedPackageJson = JSON.parse( + (await fsPr.readFile(packageJsonFilePath)).toString("utf8") + ); + + zParsedPackageJson.parse(parsedPackageJson); + + assert(is(parsedPackageJson)); + + return parsedPackageJson; + } + + return { readPackageJsonDependencies }; +})(); + +const { readPackageJsonVersion } = (() => { + type ParsedPackageJson = { + version: string; + }; + + const zParsedPackageJson = (() => { + type TargetType = ParsedPackageJson; + + const zTargetType = z.object({ + version: z.string() + }); + + assert, TargetType>>(); + + return id>(zTargetType); + })(); + + async function readPackageJsonVersion(params: { packageJsonFilePath: string }) { + const { packageJsonFilePath } = params; + + const parsedPackageJson = JSON.parse( + (await fsPr.readFile(packageJsonFilePath)).toString("utf8") + ); + + zParsedPackageJson.parse(parsedPackageJson); + + assert(is(parsedPackageJson)); + + return parsedPackageJson; + } + + return { readPackageJsonVersion }; +})(); diff --git a/src/bin/tools/nodeModulesBinDirPath.ts b/src/bin/tools/nodeModulesBinDirPath.ts new file mode 100644 index 00000000..d9f7b130 --- /dev/null +++ b/src/bin/tools/nodeModulesBinDirPath.ts @@ -0,0 +1,38 @@ +import { sep as pathSep } from "path"; + +let cache: string | undefined = undefined; + +export function getNodeModulesBinDirPath() { + if (cache !== undefined) { + return cache; + } + + const binPath = process.argv[1]; + + const segments: string[] = [".bin"]; + + let foundNodeModules = false; + + for (const segment of binPath.split(pathSep).reverse()) { + skip_segment: { + if (foundNodeModules) { + break skip_segment; + } + + if (segment === "node_modules") { + foundNodeModules = true; + break skip_segment; + } + + continue; + } + + segments.unshift(segment); + } + + const nodeModulesBinDirPath = segments.join(pathSep); + + cache = nodeModulesBinDirPath; + + return nodeModulesBinDirPath; +} diff --git a/src/bin/tools/readThisNpmPackageVersion.ts b/src/bin/tools/readThisNpmPackageVersion.ts index b493c991..b8e2bf95 100644 --- a/src/bin/tools/readThisNpmPackageVersion.ts +++ b/src/bin/tools/readThisNpmPackageVersion.ts @@ -3,7 +3,13 @@ import { assert } from "tsafe/assert"; import * as fs from "fs"; import { join as pathJoin } from "path"; +let cache: string | undefined = undefined; + export function readThisNpmPackageVersion(): string { + if (cache !== undefined) { + return cache; + } + const version = JSON.parse( fs .readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json")) @@ -12,5 +18,7 @@ export function readThisNpmPackageVersion(): string { assert(typeof version === "string"); + cache = version; + return version; } diff --git a/src/bin/tools/runFormat.ts b/src/bin/tools/runFormat.ts deleted file mode 100644 index b7a4d787..00000000 --- a/src/bin/tools/runFormat.ts +++ /dev/null @@ -1,71 +0,0 @@ -import * as fs from "fs"; -import { dirname as pathDirname } from "path"; -import { assert, Equals } from "tsafe/assert"; -import chalk from "chalk"; -import { id } from "tsafe/id"; -import { z } from "zod"; -import { is } from "tsafe/is"; -import * as child_process from "child_process"; - -export function runFormat(params: { packageJsonFilePath: string }) { - const { packageJsonFilePath } = params; - - const parsedPackageJson = (() => { - type ParsedPackageJson = { - scripts?: Record; - }; - - const zParsedPackageJson = (() => { - type TargetType = ParsedPackageJson; - - const zTargetType = z.object({ - scripts: z.record(z.string()).optional() - }); - - assert, TargetType>>(); - - return id>(zTargetType); - })(); - - const parsedPackageJson = JSON.parse( - fs.readFileSync(packageJsonFilePath).toString("utf8") - ); - - zParsedPackageJson.parse(parsedPackageJson); - - assert(is(parsedPackageJson)); - - return parsedPackageJson; - })(); - - const { scripts } = parsedPackageJson; - - if (scripts === undefined) { - return; - } - - for (const scriptName of ["format", "lint"]) { - if (!(scriptName in scripts)) { - continue; - } - - const command = `npm run ${scriptName}`; - - console.log(chalk.grey(`$ ${command}`)); - - try { - child_process.execSync(`npm run ${scriptName}`, { - stdio: "inherit", - cwd: pathDirname(packageJsonFilePath) - }); - } catch { - console.log( - chalk.yellow( - `\`${command}\` failed, it does not matter, please format your code manually, continuing...` - ) - ); - } - - return; - } -} diff --git a/src/bin/tools/runPrettier.ts b/src/bin/tools/runPrettier.ts new file mode 100644 index 00000000..98901ef0 --- /dev/null +++ b/src/bin/tools/runPrettier.ts @@ -0,0 +1,77 @@ +import { getNodeModulesBinDirPath } from "./nodeModulesBinDirPath"; +import { join as pathJoin } from "path"; +import * as fsPr from "fs/promises"; +import { id } from "tsafe/id"; +import { assert } from "tsafe/assert"; +import chalk from "chalk"; + +getIsPrettierAvailable.cache = id(undefined); + +export async function getIsPrettierAvailable(): Promise { + if (getIsPrettierAvailable.cache !== undefined) { + return getIsPrettierAvailable.cache; + } + + const nodeModulesBinDirPath = getNodeModulesBinDirPath(); + + const prettierBinPath = pathJoin(nodeModulesBinDirPath, "prettier"); + + const stats = await fsPr.stat(prettierBinPath).catch(() => undefined); + + const isPrettierAvailable = stats?.isFile() ?? false; + + getIsPrettierAvailable.cache = isPrettierAvailable; + + return isPrettierAvailable; +} + +type PrettierAndConfig = { + prettier: typeof import("prettier"); + config: import("prettier").Options | null; +}; + +getPrettierAndConfig.cache = id(undefined); + +export async function getPrettierAndConfig(): Promise { + assert(getIsPrettierAvailable()); + + if (getPrettierAndConfig.cache !== undefined) { + return getPrettierAndConfig.cache; + } + + const prettier = await import("prettier"); + + const prettierAndConfig: PrettierAndConfig = { + prettier, + config: await prettier.resolveConfig(pathJoin(getNodeModulesBinDirPath(), "..")) + }; + + getPrettierAndConfig.cache = prettierAndConfig; + + return prettierAndConfig; +} + +export async function runPrettier(params: { + sourceCode: string; + filePath: string; +}): Promise { + const { sourceCode, filePath } = params; + + let formattedSourceCode: string; + + try { + const { prettier, config } = await getPrettierAndConfig(); + + formattedSourceCode = await prettier.format(sourceCode, { ...config, filePath }); + } catch (error) { + console.log( + chalk.red( + `You probably need to upgrade the version of prettier in your project` + ) + ); + + throw error; + } + + return formattedSourceCode; +} diff --git a/src/bin/update-kc-gen.ts b/src/bin/update-kc-gen.ts index 24e8d95b..9eb9b5c2 100644 --- a/src/bin/update-kc-gen.ts +++ b/src/bin/update-kc-gen.ts @@ -1,10 +1,11 @@ import type { BuildContext } from "./shared/buildContext"; +import { KC_GEN_FILE_PATH_RELATIVE_TO_THEME_SRC_DIR } from "./shared/constants"; import * as fs from "fs/promises"; import { join as pathJoin } from "path"; import { existsAsync } from "./tools/fs.existsAsync"; import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate"; -import { runFormat } from "./tools/runFormat"; import * as crypto from "crypto"; +import { getIsPrettierAvailable, runPrettier } from "./tools/runPrettier"; export async function command(params: { buildContext: BuildContext }) { const { buildContext } = params; @@ -18,13 +19,16 @@ export async function command(params: { buildContext: BuildContext }) { return; } - const filePath = pathJoin(buildContext.themeSrcDirPath, `kc.gen.tsx`); + const filePath = pathJoin( + buildContext.themeSrcDirPath, + KC_GEN_FILE_PATH_RELATIVE_TO_THEME_SRC_DIR + ); const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented; const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented; const hasAdminTheme = buildContext.implementedThemeTypes.admin.isImplemented; - const newContent = [ + let newContent = [ ``, `/* eslint-disable */`, ``, @@ -114,20 +118,25 @@ export async function command(params: { buildContext: BuildContext }) { return; } - await fs.writeFile( - filePath, - Buffer.from( - [ - `// This file is auto-generated by the \`update-kc-gen\` command. Do not edit it manually.`, - `// Hash: ${hash}`, - ``, - newContent - ].join("\n"), - "utf8" - ) - ); + newContent = [ + `// This file is auto-generated by the \`update-kc-gen\` command. Do not edit it manually.`, + `// Hash: ${hash}`, + ``, + newContent + ].join("\n"); - runFormat({ packageJsonFilePath: buildContext.packageJsonFilePath }); + format: { + if (!(await getIsPrettierAvailable())) { + break format; + } + + newContent = await runPrettier({ + filePath, + sourceCode: newContent + }); + } + + await fs.writeFile(filePath, Buffer.from(newContent, "utf8")); delete_legacy_file: { const legacyFilePath = filePath.replace(/tsx$/, "ts"); From d7455fd100f0bce460f4b33c6371ec229df6399a Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 3 Nov 2024 00:25:28 +0100 Subject: [PATCH 05/21] Only format kc-gen file --- src/bin/shared/constants.ts | 2 -- src/bin/update-kc-gen.ts | 6 +----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/bin/shared/constants.ts b/src/bin/shared/constants.ts index ea54c411..cfddb472 100644 --- a/src/bin/shared/constants.ts +++ b/src/bin/shared/constants.ts @@ -76,5 +76,3 @@ export const CUSTOM_HANDLER_ENV_NAMES = { COMMAND_NAME: "KEYCLOAKIFY_COMMAND_NAME", BUILD_CONTEXT: "KEYCLOAKIFY_BUILD_CONTEXT" }; - -export const KC_GEN_FILE_PATH_RELATIVE_TO_THEME_SRC_DIR = "kc-gen.tsx"; diff --git a/src/bin/update-kc-gen.ts b/src/bin/update-kc-gen.ts index 9eb9b5c2..f839ac7f 100644 --- a/src/bin/update-kc-gen.ts +++ b/src/bin/update-kc-gen.ts @@ -1,5 +1,4 @@ import type { BuildContext } from "./shared/buildContext"; -import { KC_GEN_FILE_PATH_RELATIVE_TO_THEME_SRC_DIR } from "./shared/constants"; import * as fs from "fs/promises"; import { join as pathJoin } from "path"; import { existsAsync } from "./tools/fs.existsAsync"; @@ -19,10 +18,7 @@ export async function command(params: { buildContext: BuildContext }) { return; } - const filePath = pathJoin( - buildContext.themeSrcDirPath, - KC_GEN_FILE_PATH_RELATIVE_TO_THEME_SRC_DIR - ); + const filePath = pathJoin(buildContext.themeSrcDirPath, "kc-gen.tsx"); const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented; const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented; From 93fcf96cde39847a9a194dcfa89492e72ec76eed Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 3 Nov 2024 01:56:41 +0100 Subject: [PATCH 06/21] checkpoint --- src/bin/sync-ui-modules/listOfEjectedFiles.ts | 3 - .../sync-ui-modules/managedGitignoreFile.ts | 84 +++++++ .../{uiModulesMeta.ts => uiModuleMeta.ts} | 231 +++++++++++------- src/bin/tools/listInstalledModules.ts | 36 ++- 4 files changed, 250 insertions(+), 104 deletions(-) delete mode 100644 src/bin/sync-ui-modules/listOfEjectedFiles.ts create mode 100644 src/bin/sync-ui-modules/managedGitignoreFile.ts rename src/bin/sync-ui-modules/{uiModulesMeta.ts => uiModuleMeta.ts} (53%) diff --git a/src/bin/sync-ui-modules/listOfEjectedFiles.ts b/src/bin/sync-ui-modules/listOfEjectedFiles.ts deleted file mode 100644 index ccfe8046..00000000 --- a/src/bin/sync-ui-modules/listOfEjectedFiles.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function getListOfEjectedFiles(params: {}): Promise {} - -export async function writeListOfEjectedFiles(params: {}) {} diff --git a/src/bin/sync-ui-modules/managedGitignoreFile.ts b/src/bin/sync-ui-modules/managedGitignoreFile.ts new file mode 100644 index 00000000..d7cf10bb --- /dev/null +++ b/src/bin/sync-ui-modules/managedGitignoreFile.ts @@ -0,0 +1,84 @@ +import * as fsPr from "fs/promises"; +import { join as pathJoin, sep as pathSep, dirname as pathDirname } from "path"; +import { assert } from "tsafe/assert"; +import type { BuildContext } from "../shared/buildContext"; +import type { UiModuleMeta } from "./uiModuleMeta"; +import { existsAsync } from "../tools/fs.existsAsync"; + +export type BuildContextLike = { + themeSrcDirPath: string; +}; + +assert(); + +const DELIMITER_START = `# === Ejected files start ===`; +const DELIMITER_END = `# === Ejected files end =====`; + +export async function writeManagedGitignoreFile(params: { + buildContext: BuildContextLike; + uiModuleMetas: UiModuleMeta[]; + ejectedFilesRelativePaths: string[]; +}): Promise { + const { buildContext, uiModuleMetas, ejectedFilesRelativePaths } = params; + + if (uiModuleMetas.length === 0) { + return; + } + + const filePath = pathJoin(buildContext.themeSrcDirPath, ".gitignore"); + + const content_new = Buffer.from( + [ + `# This file is managed by Keycloakify, do not edit it manually.`, + ``, + DELIMITER_START, + ...ejectedFilesRelativePaths.map(fileRelativePath => + fileRelativePath.split(pathSep).join("/") + ), + DELIMITER_END, + ``, + ...uiModuleMetas + .map(uiModuleMeta => [ + `# === ${uiModuleMeta.moduleName} v${uiModuleMeta.version} ===`, + ...uiModuleMeta.files + .map(({ fileRelativePath }) => fileRelativePath) + .filter( + fileRelativePath => + !ejectedFilesRelativePaths.includes(fileRelativePath) + ) + .map( + fileRelativePath => + `/${fileRelativePath.split(pathSep).join("/").replace(/^\.\//, "")}` + ), + + `` + ]) + .flat() + ].join("\n"), + "utf8" + ); + + const content_current = await (async () => { + if (!(await existsAsync(filePath))) { + return undefined; + } + + return await fsPr.readFile(filePath); + })(); + + if (content_current !== undefined && content_current.equals(content_new)) { + return; + } + + create_dir: { + const dirPath = pathDirname(filePath); + + if (await existsAsync(dirPath)) { + break create_dir; + } + + await fsPr.mkdir(dirPath, { recursive: true }); + } + + await fsPr.writeFile(filePath, content_new); +} diff --git a/src/bin/sync-ui-modules/uiModulesMeta.ts b/src/bin/sync-ui-modules/uiModuleMeta.ts similarity index 53% rename from src/bin/sync-ui-modules/uiModulesMeta.ts rename to src/bin/sync-ui-modules/uiModuleMeta.ts index b87d09b5..9347e326 100644 --- a/src/bin/sync-ui-modules/uiModulesMeta.ts +++ b/src/bin/sync-ui-modules/uiModuleMeta.ts @@ -1,7 +1,7 @@ import { assert, type Equals } from "tsafe/assert"; import { id } from "tsafe/id"; import { z } from "zod"; -import { join as pathJoin } from "path"; +import { join as pathJoin, sep as pathSep, dirname as pathDirname } from "path"; import * as fsPr from "fs/promises"; import type { BuildContext } from "../shared/buildContext"; import { is } from "tsafe/is"; @@ -16,25 +16,18 @@ import { } from "./getSourceCodeToCopyInUserCodebase"; import * as crypto from "crypto"; -export type UiModulesMeta = { - keycloakifyVersion: string; - prettierConfigHash: string | null; - entries: UiModulesMeta.Entry[]; +export type UiModuleMeta = { + moduleName: string; + version: string; + files: { + fileRelativePath: string; + hash: string; + }[]; + peerDependencies: Record; }; -export namespace UiModulesMeta { - export type Entry = { - moduleName: string; - version: string; - files: { - fileRelativePath: string; - hash: string; - }[]; - }; -} - -const zUiModuleMetasEntry = (() => { - type ExpectedType = UiModulesMeta.Entry; +const zUiModuleMeta = (() => { + type ExpectedType = UiModuleMeta; const zTargetType = z.object({ moduleName: z.string(), @@ -44,7 +37,8 @@ const zUiModuleMetasEntry = (() => { fileRelativePath: z.string(), hash: z.string() }) - ) + ), + peerDependencies: z.record(z.string()) }); type InferredType = z.infer; @@ -54,13 +48,21 @@ const zUiModuleMetasEntry = (() => { return id>(zTargetType); })(); -const zUiModulesMeta = (() => { - type ExpectedType = UiModulesMeta; +type ParsedCacheFile = { + keycloakifyVersion: string; + prettierConfigHash: string | null; + pathSep: string; + uiModuleMetas: UiModuleMeta[]; +}; + +const zParsedCacheFile = (() => { + type ExpectedType = ParsedCacheFile; const zTargetType = z.object({ keycloakifyVersion: z.string(), prettierConfigHash: z.union([z.string(), z.null()]), - entries: z.array(zUiModuleMetasEntry) + pathSep: z.string(), + uiModuleMetas: z.array(zUiModuleMeta) }); type InferredType = z.infer; @@ -70,7 +72,7 @@ const zUiModulesMeta = (() => { return id>(zTargetType); })(); -const RELATIVE_FILE_PATH = pathJoin("uiModulesMeta.json"); +const CACHE_FILE_BASENAME = "uiModulesMeta.json"; export type BuildContextLike = BuildContextLike_getSourceCodeToCopyInUserCodebase & { cacheDirPath: string; @@ -80,12 +82,12 @@ export type BuildContextLike = BuildContextLike_getSourceCodeToCopyInUserCodebas assert(); -export async function readOrCreateUiModulesMeta(params: { +export async function getUiModuleMetas(params: { buildContext: BuildContextLike; -}): Promise { +}): Promise { const { buildContext } = params; - const filePath = pathJoin(buildContext.cacheDirPath, RELATIVE_FILE_PATH); + const cacheFilePath = pathJoin(buildContext.cacheDirPath, CACHE_FILE_BASENAME); const keycloakifyVersion = readThisNpmPackageVersion(); @@ -106,76 +108,96 @@ export async function readOrCreateUiModulesMeta(params: { moduleName.includes("keycloakify") && moduleName.endsWith("-ui") }); - const upToDateEntries: UiModulesMeta.Entry[] = await (async () => { - const uiModulesMeta_cache: UiModulesMeta | undefined = await (async () => { - if (!(await existsAsync(filePath))) { - return undefined; - } - - const contentStr = (await fsPr.readFile(filePath)).toString("utf8"); - - let uiModuleMeta: unknown; - - try { - uiModuleMeta = JSON.parse(contentStr); - } catch { - return undefined; - } - - try { - zUiModulesMeta.parse(uiModuleMeta); - } catch { - return undefined; - } - - assert(is(uiModuleMeta)); - - return uiModuleMeta; - })(); - - if (uiModulesMeta_cache === undefined) { - return []; + const cacheContent = await (async () => { + if (!(await existsAsync(cacheFilePath))) { + return undefined; } - if (uiModulesMeta_cache.keycloakifyVersion !== keycloakifyVersion) { - return []; - } - - if (uiModulesMeta_cache.prettierConfigHash !== prettierConfigHash) { - return []; - } - - const upToDateEntries = uiModulesMeta_cache.entries.filter(entry => { - const correspondingInstalledUiModule = installedUiModules.find( - installedUiModule => installedUiModule.moduleName === entry.moduleName - ); - - if (correspondingInstalledUiModule === undefined) { - return false; - } - - return correspondingInstalledUiModule.version === entry.version; - }); - - return upToDateEntries; + return await fsPr.readFile(cacheFilePath); })(); - const entries = await Promise.all( + const uiModuleMetas_cacheUpToDate: UiModuleMeta[] = await (async () => { + const parsedCacheFile: ParsedCacheFile | undefined = await (async () => { + if (cacheContent === undefined) { + return undefined; + } + + const cacheContentStr = cacheContent.toString("utf8"); + + let parsedCacheFile: unknown; + + try { + parsedCacheFile = JSON.parse(cacheContentStr); + } catch { + return undefined; + } + + try { + zParsedCacheFile.parse(parsedCacheFile); + } catch { + return undefined; + } + + assert(is(parsedCacheFile)); + + return parsedCacheFile; + })(); + + if (parsedCacheFile === undefined) { + return []; + } + + if (parsedCacheFile.keycloakifyVersion !== keycloakifyVersion) { + return []; + } + + if (parsedCacheFile.prettierConfigHash !== prettierConfigHash) { + return []; + } + + if (parsedCacheFile.pathSep !== pathSep) { + return []; + } + + const uiModuleMetas_cacheUpToDate = parsedCacheFile.uiModuleMetas.filter( + uiModuleMeta => { + const correspondingInstalledUiModule = installedUiModules.find( + installedUiModule => + installedUiModule.moduleName === uiModuleMeta.moduleName + ); + + if (correspondingInstalledUiModule === undefined) { + return false; + } + + return correspondingInstalledUiModule.version === uiModuleMeta.version; + } + ); + + return uiModuleMetas_cacheUpToDate; + })(); + + const uiModuleMetas = await Promise.all( installedUiModules.map( - async ({ moduleName, version, dirPath }): Promise => { + async ({ + moduleName, + version, + peerDependencies, + dirPath + }): Promise => { use_cache: { - const cachedEntry = upToDateEntries.find( - entry => entry.moduleName === moduleName + const uiModuleMeta_cache = uiModuleMetas_cacheUpToDate.find( + uiModuleMeta => uiModuleMeta.moduleName === moduleName ); - if (cachedEntry === undefined) { + if (uiModuleMeta_cache === undefined) { break use_cache; } - return cachedEntry; + return uiModuleMeta_cache; } - const files: UiModulesMeta.Entry["files"] = []; + const files: UiModuleMeta["files"] = []; { const srcDirPath = pathJoin(dirPath, "src"); @@ -208,18 +230,45 @@ export async function readOrCreateUiModulesMeta(params: { }); } - return id({ - files, + return id({ moduleName, - version + version, + files, + peerDependencies }); } ) ); - return id({ - keycloakifyVersion, - prettierConfigHash, - entries - }); + update_cache: { + const parsedCacheFile = id({ + keycloakifyVersion, + prettierConfigHash, + pathSep, + uiModuleMetas + }); + + const cacheContent_new = Buffer.from( + JSON.stringify(parsedCacheFile, null, 2), + "utf8" + ); + + if (cacheContent !== undefined && cacheContent_new.equals(cacheContent)) { + break update_cache; + } + + create_dir: { + const dirPath = pathDirname(cacheFilePath); + + if (await existsAsync(dirPath)) { + break create_dir; + } + + await fsPr.mkdir(dirPath, { recursive: true }); + } + + await fsPr.writeFile(cacheFilePath, cacheContent_new); + } + + return uiModuleMetas; } diff --git a/src/bin/tools/listInstalledModules.ts b/src/bin/tools/listInstalledModules.ts index 5cecad83..ea78956e 100644 --- a/src/bin/tools/listInstalledModules.ts +++ b/src/bin/tools/listInstalledModules.ts @@ -10,7 +10,14 @@ export async function listInstalledModules(params: { packageJsonFilePath: string; projectDirPath: string; filter: (params: { moduleName: string }) => boolean; -}): Promise<{ moduleName: string; version: string; dirPath: string }[]> { +}): Promise< + { + moduleName: string; + version: string; + dirPath: string; + peerDependencies: Record; + }[] +> { const { packageJsonFilePath, projectDirPath, filter } = params; const parsedPackageJson = await readPackageJsonDependencies({ @@ -24,6 +31,7 @@ export async function listInstalledModules(params: { .map(obj => Object.keys(obj)) .flat() .filter(moduleName => filter({ moduleName })); + const result = await Promise.all( uiModuleNames.map(async moduleName => { const dirPath = await getInstalledModuleDirPath({ @@ -32,11 +40,12 @@ export async function listInstalledModules(params: { projectDirPath }); - const { version } = await readPackageJsonVersion({ - packageJsonFilePath: pathJoin(dirPath, "package.json") - }); + const { version, peerDependencies } = + await readPackageJsonVersionAndPeerDependencies({ + packageJsonFilePath: pathJoin(dirPath, "package.json") + }); - return { moduleName, version, dirPath } as const; + return { moduleName, version, peerDependencies, dirPath } as const; }) ); @@ -79,16 +88,18 @@ const { readPackageJsonDependencies } = (() => { return { readPackageJsonDependencies }; })(); -const { readPackageJsonVersion } = (() => { +const { readPackageJsonVersionAndPeerDependencies } = (() => { type ParsedPackageJson = { version: string; + peerDependencies?: Record; }; const zParsedPackageJson = (() => { type TargetType = ParsedPackageJson; const zTargetType = z.object({ - version: z.string() + version: z.string(), + peerDependencies: z.record(z.string()).optional() }); assert, TargetType>>(); @@ -96,7 +107,9 @@ const { readPackageJsonVersion } = (() => { return id>(zTargetType); })(); - async function readPackageJsonVersion(params: { packageJsonFilePath: string }) { + async function readPackageJsonVersionAndPeerDependencies(params: { + packageJsonFilePath: string; + }): Promise<{ version: string; peerDependencies: Record }> { const { packageJsonFilePath } = params; const parsedPackageJson = JSON.parse( @@ -107,8 +120,11 @@ const { readPackageJsonVersion } = (() => { assert(is(parsedPackageJson)); - return parsedPackageJson; + return { + version: parsedPackageJson.version, + peerDependencies: parsedPackageJson.peerDependencies ?? {} + }; } - return { readPackageJsonVersion }; + return { readPackageJsonVersionAndPeerDependencies }; })(); From a60a0d069610bb9f507534c2bebd4ed7347c8e16 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sat, 9 Nov 2024 09:35:41 +0100 Subject: [PATCH 07/21] checkpoint --- .../initialize-admin-theme/copyBoilerplate.ts | 19 --- src/bin/initialize-admin-theme/index.ts | 1 - .../initialize-admin-theme.ts | 60 ------- .../initializeAdminTheme.ts | 150 ----------------- .../initialize-admin-theme/src/KcContext.ts | 7 - src/bin/initialize-admin-theme/src/KcPage.tsx | 11 -- .../installUiModulesPeerDependencies.ts | 157 ++++++++++++++++++ .../sync-ui-modules/managedGitignoreFile.ts | 53 +++++- src/bin/tools/npmInstall.ts | 38 ++--- 9 files changed, 228 insertions(+), 268 deletions(-) delete mode 100644 src/bin/initialize-admin-theme/copyBoilerplate.ts delete mode 100644 src/bin/initialize-admin-theme/index.ts delete mode 100644 src/bin/initialize-admin-theme/initialize-admin-theme.ts delete mode 100644 src/bin/initialize-admin-theme/initializeAdminTheme.ts delete mode 100644 src/bin/initialize-admin-theme/src/KcContext.ts delete mode 100644 src/bin/initialize-admin-theme/src/KcPage.tsx create mode 100644 src/bin/sync-ui-modules/installUiModulesPeerDependencies.ts diff --git a/src/bin/initialize-admin-theme/copyBoilerplate.ts b/src/bin/initialize-admin-theme/copyBoilerplate.ts deleted file mode 100644 index f2e6e7ca..00000000 --- a/src/bin/initialize-admin-theme/copyBoilerplate.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as fs from "fs"; -import { join as pathJoin } from "path"; -import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath"; - -export function copyBoilerplate(params: { adminThemeSrcDirPath: string }) { - const { adminThemeSrcDirPath } = params; - - fs.cpSync( - pathJoin( - getThisCodebaseRootDirPath(), - "src", - "bin", - "initialize-admin-theme", - "src" - ), - adminThemeSrcDirPath, - { recursive: true } - ); -} diff --git a/src/bin/initialize-admin-theme/index.ts b/src/bin/initialize-admin-theme/index.ts deleted file mode 100644 index b229b5f4..00000000 --- a/src/bin/initialize-admin-theme/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./initialize-admin-theme"; diff --git a/src/bin/initialize-admin-theme/initialize-admin-theme.ts b/src/bin/initialize-admin-theme/initialize-admin-theme.ts deleted file mode 100644 index e6ba503b..00000000 --- a/src/bin/initialize-admin-theme/initialize-admin-theme.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { BuildContext } from "../shared/buildContext"; -import chalk from "chalk"; -import { join as pathJoin, relative as pathRelative } from "path"; -import * as fs from "fs"; -import { command as updateKcGenCommand } from "../update-kc-gen"; -import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate"; -import { exitIfUncommittedChanges } from "../shared/exitIfUncommittedChanges"; -import { initializeAdminTheme } from "./initializeAdminTheme"; - -export async function command(params: { buildContext: BuildContext }) { - const { buildContext } = params; - - const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({ - commandName: "initialize-admin-theme", - buildContext - }); - - if (hasBeenHandled) { - return; - } - - const adminThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "admin"); - - if ( - fs.existsSync(adminThemeSrcDirPath) && - fs.readdirSync(adminThemeSrcDirPath).length > 0 - ) { - console.warn( - chalk.red( - `There is already a ${pathRelative( - process.cwd(), - adminThemeSrcDirPath - )} directory in your project. Aborting.` - ) - ); - - process.exit(-1); - } - - exitIfUncommittedChanges({ - projectDirPath: buildContext.projectDirPath - }); - - await initializeAdminTheme({ - adminThemeSrcDirPath, - buildContext - }); - - await updateKcGenCommand({ - buildContext: { - ...buildContext, - implementedThemeTypes: { - ...buildContext.implementedThemeTypes, - admin: { - isImplemented: true - } - } - } - }); -} diff --git a/src/bin/initialize-admin-theme/initializeAdminTheme.ts b/src/bin/initialize-admin-theme/initializeAdminTheme.ts deleted file mode 100644 index 8498eefa..00000000 --- a/src/bin/initialize-admin-theme/initializeAdminTheme.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; -import type { BuildContext } from "../shared/buildContext"; -import * as fs from "fs"; -import chalk from "chalk"; -import { - getLatestsSemVersionedTag, - type BuildContextLike as BuildContextLike_getLatestsSemVersionedTag -} from "../shared/getLatestsSemVersionedTag"; -import { SemVer } from "../tools/SemVer"; -import fetch from "make-fetch-happen"; -import { z } from "zod"; -import { assert, type Equals } from "tsafe/assert"; -import { is } from "tsafe/is"; -import { id } from "tsafe/id"; -import { npmInstall } from "../tools/npmInstall"; -import { copyBoilerplate } from "./copyBoilerplate"; -import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath"; - -type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & { - fetchOptions: BuildContext["fetchOptions"]; - packageJsonFilePath: string; -}; - -assert(); - -export async function initializeAdminTheme(params: { - adminThemeSrcDirPath: string; - buildContext: BuildContextLike; -}) { - const { adminThemeSrcDirPath, buildContext } = params; - - const OWNER = "keycloakify"; - const REPO = "keycloak-admin-ui"; - - const [semVersionedTag] = await getLatestsSemVersionedTag({ - owner: OWNER, - repo: REPO, - count: 1, - doIgnoreReleaseCandidates: false, - buildContext - }); - - const dependencies = await fetch( - `https://raw.githubusercontent.com/${OWNER}/${REPO}/${semVersionedTag.tag}/dependencies.gen.json`, - buildContext.fetchOptions - ) - .then(r => r.json()) - .then( - (() => { - type Dependencies = { - dependencies: Record; - devDependencies?: Record; - }; - - const zDependencies = (() => { - type TargetType = Dependencies; - - const zTargetType = z.object({ - dependencies: z.record(z.string()), - devDependencies: z.record(z.string()).optional() - }); - - assert, TargetType>>(); - - return id>(zTargetType); - })(); - - return o => zDependencies.parse(o); - })() - ); - - dependencies.dependencies["@keycloakify/keycloak-admin-ui"] = SemVer.stringify( - semVersionedTag.version - ); - - const parsedPackageJson = (() => { - type ParsedPackageJson = { - dependencies?: Record; - devDependencies?: Record; - }; - - const zParsedPackageJson = (() => { - type TargetType = ParsedPackageJson; - - const zTargetType = z.object({ - dependencies: z.record(z.string()).optional(), - devDependencies: z.record(z.string()).optional() - }); - - assert, TargetType>>(); - - return id>(zTargetType); - })(); - const parsedPackageJson = JSON.parse( - fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8") - ); - - zParsedPackageJson.parse(parsedPackageJson); - - assert(is(parsedPackageJson)); - - return parsedPackageJson; - })(); - - parsedPackageJson.dependencies = { - ...parsedPackageJson.dependencies, - ...dependencies.dependencies - }; - - parsedPackageJson.devDependencies = { - ...parsedPackageJson.devDependencies, - ...dependencies.devDependencies - }; - - if (Object.keys(parsedPackageJson.devDependencies).length === 0) { - delete parsedPackageJson.devDependencies; - } - - fs.writeFileSync( - buildContext.packageJsonFilePath, - JSON.stringify(parsedPackageJson, undefined, 4) - ); - - run_npm_install: { - if ( - JSON.parse( - fs - .readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json")) - .toString("utf8") - )["version"] === "0.0.0" - ) { - //NOTE: Linked version - break run_npm_install; - } - - npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) }); - } - - copyBoilerplate({ adminThemeSrcDirPath }); - - console.log( - [ - chalk.green("The Admin theme has been successfully initialized."), - `Using Admin UI of Keycloak version: ${chalk.bold(semVersionedTag.tag.split("-")[0])}`, - `Directory created: ${chalk.bold(pathRelative(process.cwd(), adminThemeSrcDirPath))}`, - `Dependencies added to your project's package.json: `, - chalk.bold(JSON.stringify(dependencies, null, 2)) - ].join("\n") - ); -} diff --git a/src/bin/initialize-admin-theme/src/KcContext.ts b/src/bin/initialize-admin-theme/src/KcContext.ts deleted file mode 100644 index e86dc980..00000000 --- a/src/bin/initialize-admin-theme/src/KcContext.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { KcContextLike } from "@keycloakify/keycloak-admin-ui"; -import type { KcEnvName } from "../kc.gen"; - -export type KcContext = KcContextLike & { - themeType: "admin"; - properties: Record; -}; diff --git a/src/bin/initialize-admin-theme/src/KcPage.tsx b/src/bin/initialize-admin-theme/src/KcPage.tsx deleted file mode 100644 index f5f08f1f..00000000 --- a/src/bin/initialize-admin-theme/src/KcPage.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { lazy } from "react"; -import { KcAdminUiLoader } from "@keycloakify/keycloak-admin-ui"; -import type { KcContext } from "./KcContext"; - -const KcAdminUi = lazy(() => import("@keycloakify/keycloak-admin-ui/KcAdminUi")); - -export default function KcPage(props: { kcContext: KcContext }) { - const { kcContext } = props; - - return ; -} diff --git a/src/bin/sync-ui-modules/installUiModulesPeerDependencies.ts b/src/bin/sync-ui-modules/installUiModulesPeerDependencies.ts new file mode 100644 index 00000000..60dbb946 --- /dev/null +++ b/src/bin/sync-ui-modules/installUiModulesPeerDependencies.ts @@ -0,0 +1,157 @@ +import { assert, type Equals } from "tsafe/assert"; +import { is } from "tsafe/is"; +import type { BuildContext } from "../shared/buildContext"; +import type { UiModuleMeta } from "./uiModuleMeta"; +import { z } from "zod"; +import { id } from "tsafe/id"; +import * as fsPr from "fs/promises"; +import { SemVer } from "../tools/SemVer"; +import { same } from "evt/tools/inDepth/same"; +import { runPrettier, getIsPrettierAvailable } from "../tools/runPrettier"; +import { npmInstall } from "../tools/npmInstall"; + +export type BuildContextLike = { + packageJsonFilePath: string; +}; + +assert(); + +export type UiModuleMetaLike = { + moduleName: string; + peerDependencies: Record; +}; + +assert(); + +export async function installUiModulesPeerDependencies(params: { + buildContext: BuildContextLike; + uiModuleMetas: UiModuleMetaLike[]; +}): Promise { + const { buildContext, uiModuleMetas } = params; + + const { uiModulesPerDependencies } = (() => { + const uiModulesPerDependencies: Record = {}; + + for (const { peerDependencies } of uiModuleMetas) { + for (const [peerDependencyName, versionRange_candidate] of Object.entries( + peerDependencies + )) { + const versionRange = (() => { + const versionRange_current = + uiModulesPerDependencies[peerDependencyName]; + + if (versionRange_current === undefined) { + return versionRange_candidate; + } + + if (versionRange_current === "*") { + return versionRange_candidate; + } + + if (versionRange_candidate === "*") { + return versionRange_current; + } + + const { versionRange } = [ + versionRange_current, + versionRange_candidate + ] + .map(versionRange => ({ + versionRange, + semVer: SemVer.parse( + (() => { + if ( + versionRange.startsWith("^") || + versionRange.startsWith("~") + ) { + return versionRange.slice(1); + } + + return versionRange; + })() + ) + })) + .sort((a, b) => SemVer.compare(b.semVer, a.semVer))[0]; + + return versionRange; + })(); + + uiModulesPerDependencies[peerDependencyName] = versionRange; + } + } + + return { uiModulesPerDependencies }; + })(); + + const parsedPackageJson = await (async () => { + type ParsedPackageJson = { + dependencies?: Record; + devDependencies?: Record; + }; + + const zParsedPackageJson = (() => { + type TargetType = ParsedPackageJson; + + const zParsedPackageJson = z.object({ + dependencies: z.record(z.string()).optional(), + devDependencies: z.record(z.string()).optional() + }); + + type InferredType = z.infer; + + assert>(); + + return id>(zParsedPackageJson); + })(); + + const parsedPackageJson = JSON.parse( + (await fsPr.readFile(buildContext.packageJsonFilePath)).toString("utf8") + ); + + zParsedPackageJson.parse(parsedPackageJson); + + assert(is(parsedPackageJson)); + + return parsedPackageJson; + })(); + + const parsedPackageJson_before = JSON.parse(JSON.stringify(parsedPackageJson)); + + for (const [moduleName, versionRange] of Object.entries(uiModulesPerDependencies)) { + if (moduleName.startsWith("@types/")) { + (parsedPackageJson.devDependencies ??= {})[moduleName] = versionRange; + continue; + } + + if (parsedPackageJson.devDependencies !== undefined) { + delete parsedPackageJson.devDependencies[moduleName]; + } + + (parsedPackageJson.dependencies ??= {})[moduleName] = versionRange; + } + + if (same(parsedPackageJson, parsedPackageJson_before)) { + return; + } + + let packageJsonContentStr = JSON.stringify(parsedPackageJson, null, 2); + + format: { + if (!(await getIsPrettierAvailable())) { + break format; + } + + packageJsonContentStr = await runPrettier({ + sourceCode: packageJsonContentStr, + filePath: buildContext.packageJsonFilePath + }); + } + + await fsPr.writeFile(buildContext.packageJsonFilePath, packageJsonContentStr); + + npmInstall({ + packageJsonDirPath: buildContext.packageJsonFilePath + }); + + process.exit(0); +} diff --git a/src/bin/sync-ui-modules/managedGitignoreFile.ts b/src/bin/sync-ui-modules/managedGitignoreFile.ts index d7cf10bb..2c2f9405 100644 --- a/src/bin/sync-ui-modules/managedGitignoreFile.ts +++ b/src/bin/sync-ui-modules/managedGitignoreFile.ts @@ -1,9 +1,15 @@ import * as fsPr from "fs/promises"; -import { join as pathJoin, sep as pathSep, dirname as pathDirname } from "path"; +import { + join as pathJoin, + sep as pathSep, + dirname as pathDirname, + relative as pathRelative +} from "path"; import { assert } from "tsafe/assert"; import type { BuildContext } from "../shared/buildContext"; import type { UiModuleMeta } from "./uiModuleMeta"; import { existsAsync } from "../tools/fs.existsAsync"; +import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath"; export type BuildContextLike = { themeSrcDirPath: string; @@ -82,3 +88,48 @@ export async function writeManagedGitignoreFile(params: { await fsPr.writeFile(filePath, content_new); } + +export async function readManagedGitignoreFile(params: { + buildContext: BuildContextLike; +}): Promise<{ + ejectedFilesRelativePaths: string[]; +}> { + const { buildContext } = params; + + const filePath = pathJoin(buildContext.themeSrcDirPath, ".gitignore"); + + if (!(await existsAsync(filePath))) { + return { ejectedFilesRelativePaths: [] }; + } + + const contentStr = (await fsPr.readFile(filePath)).toString("utf8"); + + const payload = (() => { + const index_start = contentStr.indexOf(DELIMITER_START); + const index_end = contentStr.indexOf(DELIMITER_END); + + if (index_start === -1 || index_end === -1) { + return undefined; + } + + return contentStr.slice(index_start + DELIMITER_START.length, index_end).trim(); + })(); + + if (payload === undefined) { + return { ejectedFilesRelativePaths: [] }; + } + + const ejectedFilesRelativePaths = payload + .split("\n") + .map(line => line.trim()) + .filter(line => line !== "") + .map(line => + getAbsoluteAndInOsFormatPath({ + cwd: buildContext.themeSrcDirPath, + pathIsh: line + }) + ) + .map(filePath => pathRelative(buildContext.themeSrcDirPath, filePath)); + + return { ejectedFilesRelativePaths }; +} diff --git a/src/bin/tools/npmInstall.ts b/src/bin/tools/npmInstall.ts index a95a99df..2d62e129 100644 --- a/src/bin/tools/npmInstall.ts +++ b/src/bin/tools/npmInstall.ts @@ -23,6 +23,10 @@ export function npmInstall(params: { packageJsonDirPath: string }) { { binName: "bun", lockFileBasename: "bun.lockdb" + }, + { + binName: "deno", + lockFileBasename: "deno.lock" } ] as const; @@ -37,27 +41,23 @@ export function npmInstall(params: { packageJsonDirPath: string }) { } } - return undefined; + throw new Error( + "No lock file found, cannot tell which package manager to use for installing dependencies." + ); })(); - install_dependencies: { - if (packageManagerBinName === undefined) { - break install_dependencies; - } + console.log(`Installing the new dependencies...`); - console.log(`Installing the new dependencies...`); - - try { - child_process.execSync(`${packageManagerBinName} install`, { - cwd: packageJsonDirPath, - stdio: "inherit" - }); - } catch { - console.log( - chalk.yellow( - `\`${packageManagerBinName} install\` failed, continuing anyway...` - ) - ); - } + try { + child_process.execSync(`${packageManagerBinName} install`, { + cwd: packageJsonDirPath, + stdio: "inherit" + }); + } catch { + console.log( + chalk.yellow( + `\`${packageManagerBinName} install\` failed, continuing anyway...` + ) + ); } } From a73281d46dd07014fa54414d52afcf17b0fd29ef Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sat, 9 Nov 2024 14:02:19 +0100 Subject: [PATCH 08/21] Checkpoint --- scripts/build/main.ts | 2 +- src/bin/add-story.ts | 21 ++-- src/bin/eject-page.ts | 19 ++-- src/bin/main.ts | 28 +++--- ...tUiModuleFileSourceCodeReadyToBeCopied.ts} | 38 ++++--- src/bin/postinstall/index.ts | 1 + .../installUiModulesPeerDependencies.ts | 0 .../managedGitignoreFile.ts | 0 src/bin/postinstall/postinstall.ts | 79 +++++++++++++++ .../uiModuleMeta.ts | 99 ++++++++++++------- src/bin/shared/buildContext.ts | 10 +- src/bin/shared/constants.ts | 2 + src/bin/sync-ui-modules/index.ts | 1 - src/bin/sync-ui-modules/sync-ui-modules.ts | 13 --- src/bin/tools/getInstalledModuleDirPath.ts | 8 +- src/bin/tools/listInstalledModules.ts | 7 +- 16 files changed, 224 insertions(+), 104 deletions(-) rename src/bin/{sync-ui-modules/getSourceCodeToCopyInUserCodebase.ts => postinstall/getUiModuleFileSourceCodeReadyToBeCopied.ts} (57%) create mode 100644 src/bin/postinstall/index.ts rename src/bin/{sync-ui-modules => postinstall}/installUiModulesPeerDependencies.ts (100%) rename src/bin/{sync-ui-modules => postinstall}/managedGitignoreFile.ts (100%) create mode 100644 src/bin/postinstall/postinstall.ts rename src/bin/{sync-ui-modules => postinstall}/uiModuleMeta.ts (71%) delete mode 100644 src/bin/sync-ui-modules/index.ts delete mode 100644 src/bin/sync-ui-modules/sync-ui-modules.ts diff --git a/scripts/build/main.ts b/scripts/build/main.ts index 097c886a..2fdcdbc6 100644 --- a/scripts/build/main.ts +++ b/scripts/build/main.ts @@ -115,7 +115,7 @@ import { vendorFrontendDependencies } from "./vendorFrontendDependencies"; } run( - `npx ncc build ${join("dist", "vite-plugin", "index.js")} -o ${join( + `npx ncc build ${join("dist", "vite-plugin", "index.js")} --external prettier -o ${join( "dist", "ncc_out" )}` diff --git a/src/bin/add-story.ts b/src/bin/add-story.ts index dc528a5e..8b82e80e 100644 --- a/src/bin/add-story.ts +++ b/src/bin/add-story.ts @@ -14,7 +14,7 @@ import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase"; import { assert, Equals } from "tsafe/assert"; import type { BuildContext } from "./shared/buildContext"; import chalk from "chalk"; -import { runFormat } from "./tools/runFormat"; +import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier"; import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate"; export async function command(params: { buildContext: BuildContext }) { @@ -119,7 +119,7 @@ export async function command(params: { buildContext: BuildContext }) { process.exit(-1); } - const componentCode = fs + let sourceCode = fs .readFileSync( pathJoin( getThisCodebaseRootDirPath(), @@ -133,6 +133,17 @@ export async function command(params: { buildContext: BuildContext }) { .replace('import React from "react";\n', "") .replace(/from "[./]+dist\//, 'from "keycloakify/'); + run_prettier: { + if (!(await getIsPrettierAvailable())) { + break run_prettier; + } + + sourceCode = await runPrettier({ + filePath: targetFilePath, + sourceCode: sourceCode + }); + } + { const targetDirPath = pathDirname(targetFilePath); @@ -141,11 +152,7 @@ export async function command(params: { buildContext: BuildContext }) { } } - fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8")); - - runFormat({ - packageJsonFilePath: buildContext.packageJsonFilePath - }); + fs.writeFileSync(targetFilePath, Buffer.from(sourceCode, "utf8")); console.log( [ diff --git a/src/bin/eject-page.ts b/src/bin/eject-page.ts index a138cdc8..da9c16d2 100644 --- a/src/bin/eject-page.ts +++ b/src/bin/eject-page.ts @@ -22,7 +22,7 @@ import { assert, Equals } from "tsafe/assert"; import type { BuildContext } from "./shared/buildContext"; import chalk from "chalk"; import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate"; -import { runFormat } from "./tools/runFormat"; +import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier"; export async function command(params: { buildContext: BuildContext }) { const { buildContext } = params; @@ -217,7 +217,7 @@ export async function command(params: { buildContext: BuildContext }) { process.exit(-1); } - const componentCode = fs + let componentCode = fs .readFileSync( pathJoin( getThisCodebaseRootDirPath(), @@ -229,6 +229,17 @@ export async function command(params: { buildContext: BuildContext }) { ) .toString("utf8"); + run_prettier: { + if (!(await getIsPrettierAvailable())) { + break run_prettier; + } + + componentCode = await runPrettier({ + filePath: targetFilePath, + sourceCode: componentCode + }); + } + { const targetDirPath = pathDirname(targetFilePath); @@ -239,10 +250,6 @@ export async function command(params: { buildContext: BuildContext }) { fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8")); - runFormat({ - packageJsonFilePath: buildContext.packageJsonFilePath - }); - console.log( `${chalk.green("✓")} ${chalk.bold( pathJoin(".", pathRelative(process.cwd(), targetFilePath)) diff --git a/src/bin/main.ts b/src/bin/main.ts index 0047b715..15c73167 100644 --- a/src/bin/main.ts +++ b/src/bin/main.ts @@ -197,20 +197,6 @@ program } }); -program - .command({ - name: "initialize-admin-theme", - description: "Initialize the admin theme." - }) - .task({ - skip, - handler: async ({ projectDirPath }) => { - const { command } = await import("./initialize-admin-theme"); - - await command({ buildContext: getBuildContext({ projectDirPath }) }); - } - }); - program .command({ name: "copy-keycloak-resources-to-public", @@ -241,6 +227,20 @@ program } }); +program + .command({ + name: "postinstall", + description: "Initialize all the Keycloakify UI modules installed in the project." + }) + .task({ + skip, + handler: async ({ projectDirPath }) => { + const { command } = await import("./postinstall"); + + await command({ buildContext: getBuildContext({ projectDirPath }) }); + } + }); + // Fallback to build command if no command is provided { const [, , ...rest] = process.argv; diff --git a/src/bin/sync-ui-modules/getSourceCodeToCopyInUserCodebase.ts b/src/bin/postinstall/getUiModuleFileSourceCodeReadyToBeCopied.ts similarity index 57% rename from src/bin/sync-ui-modules/getSourceCodeToCopyInUserCodebase.ts rename to src/bin/postinstall/getUiModuleFileSourceCodeReadyToBeCopied.ts index 8306e67d..6855c0b1 100644 --- a/src/bin/sync-ui-modules/getSourceCodeToCopyInUserCodebase.ts +++ b/src/bin/postinstall/getUiModuleFileSourceCodeReadyToBeCopied.ts @@ -3,6 +3,7 @@ import * as fsPr from "fs/promises"; import { join as pathJoin, sep as pathSep } from "path"; import { assert } from "tsafe/assert"; import type { BuildContext } from "../shared/buildContext"; +import { KEYCLOAK_THEME } from "../shared/constants"; export type BuildContextLike = { themeSrcDirPath: string; @@ -10,27 +11,32 @@ export type BuildContextLike = { assert(); -export async function getSourceCodeToCopyInUserCodebase(params: { +export async function getUiModuleFileSourceCodeReadyToBeCopied(params: { buildContext: BuildContextLike; - relativeFromDirPath: string; fileRelativePath: string; - commentData: { - isForEjection: boolean; - uiModuleName: string; - uiModuleVersion: string; - }; -}): Promise { - const { buildContext, relativeFromDirPath, fileRelativePath, commentData } = params; + isForEjection: boolean; + uiModuleDirPath: string; + uiModuleName: string; + uiModuleVersion: string; +}): Promise { + const { + buildContext, + uiModuleDirPath, + fileRelativePath, + isForEjection, + uiModuleName, + uiModuleVersion + } = params; let sourceCode = ( - await fsPr.readFile(pathJoin(relativeFromDirPath, fileRelativePath)) + await fsPr.readFile(pathJoin(uiModuleDirPath, KEYCLOAK_THEME, fileRelativePath)) ).toString("utf8"); const comment = (() => { - if (commentData.isForEjection) { + if (isForEjection) { return [ `/*`, - ` This file was ejected from ${commentData.uiModuleName} version ${commentData.uiModuleVersion}.`, + ` This file was ejected from ${uiModuleName} version ${uiModuleVersion}.`, `*/` ].join("\n"); } else { @@ -39,7 +45,7 @@ export async function getSourceCodeToCopyInUserCodebase(params: { ` WARNING: Before modifying this file run the following command:`, ` \`npx keycloakify eject-file ${fileRelativePath.split(pathSep).join("/")}\``, ` `, - ` This file comes from ${commentData.uiModuleName} version ${commentData.uiModuleVersion}.`, + ` This file comes from ${uiModuleName} version ${uiModuleVersion}.`, `*/` ]; } @@ -47,16 +53,18 @@ export async function getSourceCodeToCopyInUserCodebase(params: { sourceCode = [comment, ``, sourceCode].join("\n"); + const destFilePath = pathJoin(buildContext.themeSrcDirPath, fileRelativePath); + format: { if (!(await getIsPrettierAvailable())) { break format; } sourceCode = await runPrettier({ - filePath: pathJoin(buildContext.themeSrcDirPath, fileRelativePath), + filePath: destFilePath, sourceCode }); } - return sourceCode; + return Buffer.from(sourceCode, "utf8"); } diff --git a/src/bin/postinstall/index.ts b/src/bin/postinstall/index.ts new file mode 100644 index 00000000..7f1cc290 --- /dev/null +++ b/src/bin/postinstall/index.ts @@ -0,0 +1 @@ +export * from "./postinstall"; diff --git a/src/bin/sync-ui-modules/installUiModulesPeerDependencies.ts b/src/bin/postinstall/installUiModulesPeerDependencies.ts similarity index 100% rename from src/bin/sync-ui-modules/installUiModulesPeerDependencies.ts rename to src/bin/postinstall/installUiModulesPeerDependencies.ts diff --git a/src/bin/sync-ui-modules/managedGitignoreFile.ts b/src/bin/postinstall/managedGitignoreFile.ts similarity index 100% rename from src/bin/sync-ui-modules/managedGitignoreFile.ts rename to src/bin/postinstall/managedGitignoreFile.ts diff --git a/src/bin/postinstall/postinstall.ts b/src/bin/postinstall/postinstall.ts new file mode 100644 index 00000000..6b06f8e4 --- /dev/null +++ b/src/bin/postinstall/postinstall.ts @@ -0,0 +1,79 @@ +import type { BuildContext } from "../shared/buildContext"; +import { getUiModuleMetas, computeHash } from "./uiModuleMeta"; +import { installUiModulesPeerDependencies } from "./installUiModulesPeerDependencies"; +import { + readManagedGitignoreFile, + writeManagedGitignoreFile +} from "./managedGitignoreFile"; +import { dirname as pathDirname } from "path"; +import { join as pathJoin } from "path"; +import { existsAsync } from "../tools/fs.existsAsync"; +import * as fsPr from "fs/promises"; + +export async function command(params: { buildContext: BuildContext }) { + const { buildContext } = params; + + const uiModuleMetas = await getUiModuleMetas({ buildContext }); + + await installUiModulesPeerDependencies({ + buildContext, + uiModuleMetas + }); + + const { ejectedFilesRelativePaths } = await readManagedGitignoreFile({ + buildContext + }); + + await writeManagedGitignoreFile({ + buildContext, + ejectedFilesRelativePaths, + uiModuleMetas + }); + + await Promise.all( + uiModuleMetas + .map(uiModuleMeta => + Promise.all( + uiModuleMeta.files.map( + async ({ fileRelativePath, copyableFilePath, hash }) => { + if (ejectedFilesRelativePaths.includes(fileRelativePath)) { + return; + } + + const destFilePath = pathJoin( + buildContext.themeSrcDirPath, + fileRelativePath + ); + + skip_condition: { + if (!(await existsAsync(destFilePath))) { + break skip_condition; + } + + const destFileHash = computeHash( + await fsPr.readFile(destFilePath) + ); + + if (destFileHash !== hash) { + break skip_condition; + } + + return; + } + + { + const dirName = pathDirname(copyableFilePath); + + if (!(await existsAsync(dirName))) { + await fsPr.mkdir(dirName, { recursive: true }); + } + } + + await fsPr.copyFile(copyableFilePath, destFilePath); + } + ) + ) + ) + .flat() + ); +} diff --git a/src/bin/sync-ui-modules/uiModuleMeta.ts b/src/bin/postinstall/uiModuleMeta.ts similarity index 71% rename from src/bin/sync-ui-modules/uiModuleMeta.ts rename to src/bin/postinstall/uiModuleMeta.ts index 9347e326..24cdcd33 100644 --- a/src/bin/sync-ui-modules/uiModuleMeta.ts +++ b/src/bin/postinstall/uiModuleMeta.ts @@ -1,7 +1,7 @@ import { assert, type Equals } from "tsafe/assert"; import { id } from "tsafe/id"; import { z } from "zod"; -import { join as pathJoin, sep as pathSep, dirname as pathDirname } from "path"; +import { join as pathJoin, dirname as pathDirname } from "path"; import * as fsPr from "fs/promises"; import type { BuildContext } from "../shared/buildContext"; import { is } from "tsafe/is"; @@ -11,10 +11,11 @@ import { crawlAsync } from "../tools/crawlAsync"; import { getIsPrettierAvailable, getPrettierAndConfig } from "../tools/runPrettier"; import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion"; import { - getSourceCodeToCopyInUserCodebase, - type BuildContextLike as BuildContextLike_getSourceCodeToCopyInUserCodebase -} from "./getSourceCodeToCopyInUserCodebase"; + getUiModuleFileSourceCodeReadyToBeCopied, + type BuildContextLike as BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied +} from "./getUiModuleFileSourceCodeReadyToBeCopied"; import * as crypto from "crypto"; +import { KEYCLOAK_THEME } from "../shared/constants"; export type UiModuleMeta = { moduleName: string; @@ -22,6 +23,7 @@ export type UiModuleMeta = { files: { fileRelativePath: string; hash: string; + copyableFilePath: string; }[]; peerDependencies: Record; }; @@ -35,7 +37,8 @@ const zUiModuleMeta = (() => { files: z.array( z.object({ fileRelativePath: z.string(), - hash: z.string() + hash: z.string(), + copyableFilePath: z.string() }) ), peerDependencies: z.record(z.string()) @@ -51,7 +54,7 @@ const zUiModuleMeta = (() => { type ParsedCacheFile = { keycloakifyVersion: string; prettierConfigHash: string | null; - pathSep: string; + thisFilePath: string; uiModuleMetas: UiModuleMeta[]; }; @@ -61,7 +64,7 @@ const zParsedCacheFile = (() => { const zTargetType = z.object({ keycloakifyVersion: z.string(), prettierConfigHash: z.union([z.string(), z.null()]), - pathSep: z.string(), + thisFilePath: z.string(), uiModuleMetas: z.array(zUiModuleMeta) }); @@ -72,13 +75,14 @@ const zParsedCacheFile = (() => { return id>(zTargetType); })(); -const CACHE_FILE_BASENAME = "uiModulesMeta.json"; +const CACHE_FILE_RELATIVE_PATH = pathJoin("ui-modules", "cache.json"); -export type BuildContextLike = BuildContextLike_getSourceCodeToCopyInUserCodebase & { - cacheDirPath: string; - packageJsonFilePath: string; - projectDirPath: string; -}; +export type BuildContextLike = + BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied & { + cacheDirPath: string; + packageJsonFilePath: string; + projectDirPath: string; + }; assert(); @@ -87,7 +91,7 @@ export async function getUiModuleMetas(params: { }): Promise { const { buildContext } = params; - const cacheFilePath = pathJoin(buildContext.cacheDirPath, CACHE_FILE_BASENAME); + const cacheFilePath = pathJoin(buildContext.cacheDirPath, CACHE_FILE_RELATIVE_PATH); const keycloakifyVersion = readThisNpmPackageVersion(); @@ -101,12 +105,20 @@ export async function getUiModuleMetas(params: { return crypto.createHash("sha256").update(JSON.stringify(config)).digest("hex"); })(); - const installedUiModules = await listInstalledModules({ - packageJsonFilePath: buildContext.packageJsonFilePath, - projectDirPath: buildContext.packageJsonFilePath, - filter: ({ moduleName }) => - moduleName.includes("keycloakify") && moduleName.endsWith("-ui") - }); + const installedUiModules = await (async () => { + const installedModulesWithKeycloakifyInTheName = await listInstalledModules({ + packageJsonFilePath: buildContext.packageJsonFilePath, + projectDirPath: buildContext.packageJsonFilePath, + filter: ({ moduleName }) => + moduleName.includes("keycloakify") && moduleName !== "keycloakify" + }); + + return Promise.all( + installedModulesWithKeycloakifyInTheName.filter(async ({ dirPath }) => + existsAsync(pathJoin(dirPath, KEYCLOAK_THEME)) + ) + ); + })(); const cacheContent = await (async () => { if (!(await existsAsync(cacheFilePath))) { @@ -155,7 +167,7 @@ export async function getUiModuleMetas(params: { return []; } - if (parsedCacheFile.pathSep !== pathSep) { + if (parsedCacheFile.thisFilePath !== cacheFilePath) { return []; } @@ -200,31 +212,44 @@ export async function getUiModuleMetas(params: { const files: UiModuleMeta["files"] = []; { - const srcDirPath = pathJoin(dirPath, "src"); + const srcDirPath = pathJoin(dirPath, KEYCLOAK_THEME); await crawlAsync({ dirPath: srcDirPath, returnedPathsType: "relative to dirPath", onFileFound: async fileRelativePath => { - const sourceCode = await getSourceCodeToCopyInUserCodebase({ - buildContext, - relativeFromDirPath: srcDirPath, - fileRelativePath, - commentData: { + const sourceCode = + await getUiModuleFileSourceCodeReadyToBeCopied({ + buildContext, + fileRelativePath, isForEjection: false, + uiModuleDirPath: dirPath, uiModuleName: moduleName, uiModuleVersion: version - } - }); + }); - const hash = crypto - .createHash("sha256") - .update(sourceCode) - .digest("hex"); + const hash = computeHash(sourceCode); + + const copyableFilePath = pathJoin( + pathDirname(cacheFilePath), + KEYCLOAK_THEME, + fileRelativePath + ); + + { + const dirPath = pathDirname(copyableFilePath); + + if (!(await existsAsync(dirPath))) { + await fsPr.mkdir(dirPath, { recursive: true }); + } + } + + fsPr.writeFile(copyableFilePath, sourceCode); files.push({ fileRelativePath, - hash + hash, + copyableFilePath }); } }); @@ -244,7 +269,7 @@ export async function getUiModuleMetas(params: { const parsedCacheFile = id({ keycloakifyVersion, prettierConfigHash, - pathSep, + thisFilePath: cacheFilePath, uiModuleMetas }); @@ -272,3 +297,7 @@ export async function getUiModuleMetas(params: { return uiModuleMetas; } + +export function computeHash(data: Buffer) { + return crypto.createHash("sha256").update(data).digest("hex"); +} diff --git a/src/bin/shared/buildContext.ts b/src/bin/shared/buildContext.ts index 4cb8a552..5ae3f925 100644 --- a/src/bin/shared/buildContext.ts +++ b/src/bin/shared/buildContext.ts @@ -18,9 +18,8 @@ import { import type { KeycloakVersionRange } from "./KeycloakVersionRange"; import { exclude } from "tsafe"; import { crawl } from "../tools/crawl"; -import { THEME_TYPES } from "./constants"; +import { THEME_TYPES, KEYCLOAK_THEME, type ThemeType } from "./constants"; import { objectEntries } from "tsafe/objectEntries"; -import { type ThemeType } from "./constants"; import { id } from "tsafe/id"; import chalk from "chalk"; import { getProxyFetchOptions, type FetchOptionsLike } from "../tools/fetchProxyOptions"; @@ -147,7 +146,10 @@ export function getBuildContext(params: { returnedPathsType: "relative to dirPath" }) .map(fileRelativePath => { - for (const themeSrcDirBasename of ["keycloak-theme", "keycloak_theme"]) { + for (const themeSrcDirBasename of [ + KEYCLOAK_THEME, + KEYCLOAK_THEME.replace(/-/g, "_") + ]) { const split = fileRelativePath.split(themeSrcDirBasename); if (split.length === 2) { return pathJoin(srcDirPath, split[0] + themeSrcDirBasename); @@ -173,7 +175,7 @@ export function getBuildContext(params: { [ `Can't locate your Keycloak theme source directory in .${pathSep}${pathRelative(process.cwd(), srcDirPath)}`, `Make sure to either use the Keycloakify CLI in the root of your Keycloakify project or use the --project CLI option`, - `If you are collocating your Keycloak theme with your app you must have a directory named 'keycloak-theme' or 'keycloak_theme' in your 'src' directory` + `If you are collocating your Keycloak theme with your app you must have a directory named '${KEYCLOAK_THEME}' or '${KEYCLOAK_THEME.replace(/-/g, "_")}' in your 'src' directory` ].join("\n") ) ); diff --git a/src/bin/shared/constants.ts b/src/bin/shared/constants.ts index cfddb472..6514115f 100644 --- a/src/bin/shared/constants.ts +++ b/src/bin/shared/constants.ts @@ -76,3 +76,5 @@ export const CUSTOM_HANDLER_ENV_NAMES = { COMMAND_NAME: "KEYCLOAKIFY_COMMAND_NAME", BUILD_CONTEXT: "KEYCLOAKIFY_BUILD_CONTEXT" }; + +export const KEYCLOAK_THEME = "keycloak-theme"; diff --git a/src/bin/sync-ui-modules/index.ts b/src/bin/sync-ui-modules/index.ts deleted file mode 100644 index 45d420bd..00000000 --- a/src/bin/sync-ui-modules/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./sync-ui-modules"; diff --git a/src/bin/sync-ui-modules/sync-ui-modules.ts b/src/bin/sync-ui-modules/sync-ui-modules.ts deleted file mode 100644 index abd1c4c8..00000000 --- a/src/bin/sync-ui-modules/sync-ui-modules.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { BuildContext } from "./shared/buildContext"; -import { assert, type Equals } from "tsafe/assert"; -import { id } from "tsafe/id"; -import { is } from "tsafe/is"; -import { z } from "zod"; -import { join as pathJoin } from "path"; -import { existsAsync } from "./tools/fs.existsAsync"; - -import * as fsPr from "fs/promises"; - -export async function command(params: { buildContext: BuildContext }) { - const { buildContext } = params; -} diff --git a/src/bin/tools/getInstalledModuleDirPath.ts b/src/bin/tools/getInstalledModuleDirPath.ts index 260c3c5e..7915e6e9 100644 --- a/src/bin/tools/getInstalledModuleDirPath.ts +++ b/src/bin/tools/getInstalledModuleDirPath.ts @@ -1,16 +1,14 @@ -import { dirname as pathDirname, join as pathJoin } from "path"; +import { join as pathJoin } from "path"; import { existsAsync } from "./fs.existsAsync"; import * as child_process from "child_process"; import { assert } from "tsafe/assert"; export async function getInstalledModuleDirPath(params: { moduleName: string; - packageJsonFilePath: string; + packageJsonDirPath: string; projectDirPath: string; }) { - const { moduleName, packageJsonFilePath, projectDirPath } = params; - - const packageJsonDirPath = pathDirname(packageJsonFilePath); + const { moduleName, packageJsonDirPath, projectDirPath } = params; common_case: { const dirPath = pathJoin( diff --git a/src/bin/tools/listInstalledModules.ts b/src/bin/tools/listInstalledModules.ts index ea78956e..ef2f9116 100644 --- a/src/bin/tools/listInstalledModules.ts +++ b/src/bin/tools/listInstalledModules.ts @@ -1,10 +1,11 @@ import { assert, type Equals } from "tsafe/assert"; import { id } from "tsafe/id"; import { z } from "zod"; -import { join as pathJoin } from "path"; +import { join as pathJoin, dirname as pathDirname } from "path"; import * as fsPr from "fs/promises"; import { is } from "tsafe/is"; import { getInstalledModuleDirPath } from "../tools/getInstalledModuleDirPath"; +import { exclude } from "tsafe/exclude"; export async function listInstalledModules(params: { packageJsonFilePath: string; @@ -27,7 +28,7 @@ export async function listInstalledModules(params: { const uiModuleNames = ( [parsedPackageJson.dependencies, parsedPackageJson.devDependencies] as const ) - .filter(obj => obj !== undefined) + .filter(exclude(undefined)) .map(obj => Object.keys(obj)) .flat() .filter(moduleName => filter({ moduleName })); @@ -36,7 +37,7 @@ export async function listInstalledModules(params: { uiModuleNames.map(async moduleName => { const dirPath = await getInstalledModuleDirPath({ moduleName, - packageJsonFilePath, + packageJsonDirPath: pathDirname(packageJsonFilePath), projectDirPath }); From 63877d53be791da6a2d9f408019afcf486b544bf Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sat, 9 Nov 2024 20:33:53 +0100 Subject: [PATCH 09/21] Eject file command implementation --- src/bin/eject-file.ts | 68 +++++++++++++++++++ src/bin/main.ts | 33 +++++++++ ...etUiModuleFileSourceCodeReadyToBeCopied.ts | 11 +-- 3 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 src/bin/eject-file.ts diff --git a/src/bin/eject-file.ts b/src/bin/eject-file.ts new file mode 100644 index 00000000..18d70ca9 --- /dev/null +++ b/src/bin/eject-file.ts @@ -0,0 +1,68 @@ +import type { BuildContext } from "./shared/buildContext"; +import { getUiModuleFileSourceCodeReadyToBeCopied } from "./postinstall/getUiModuleFileSourceCodeReadyToBeCopied"; +import { getAbsoluteAndInOsFormatPath } from "./tools/getAbsoluteAndInOsFormatPath"; +import { relative as pathRelative, dirname as pathDirname, join as pathJoin } from "path"; +import { getUiModuleMetas } from "./postinstall/uiModuleMeta"; +import { getInstalledModuleDirPath } from "./tools/getInstalledModuleDirPath"; +import * as fsPr from "fs/promises"; +import { + readManagedGitignoreFile, + writeManagedGitignoreFile +} from "./postinstall/managedGitignoreFile"; + +export async function command(params: { + buildContext: BuildContext; + cliCommandOptions: { + file: string; + }; +}) { + const { buildContext, cliCommandOptions } = params; + + const fileRelativePath = pathRelative( + buildContext.themeSrcDirPath, + getAbsoluteAndInOsFormatPath({ + cwd: buildContext.themeSrcDirPath, + pathIsh: cliCommandOptions.file + }) + ); + + const uiModuleMetas = await getUiModuleMetas({ buildContext }); + + const uiModuleMeta = uiModuleMetas.find(({ files }) => + files.map(({ fileRelativePath }) => fileRelativePath).includes(fileRelativePath) + ); + + if (!uiModuleMeta) { + throw new Error(`No UI module found for the file ${fileRelativePath}`); + } + + const uiModuleDirPath = await getInstalledModuleDirPath({ + moduleName: uiModuleMeta.moduleName, + packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath), + projectDirPath: buildContext.projectDirPath + }); + + const sourceCode = await getUiModuleFileSourceCodeReadyToBeCopied({ + buildContext, + fileRelativePath, + isForEjection: true, + uiModuleName: uiModuleMeta.moduleName, + uiModuleDirPath, + uiModuleVersion: uiModuleMeta.version + }); + + await fsPr.writeFile( + pathJoin(buildContext.themeSrcDirPath, fileRelativePath), + sourceCode + ); + + const { ejectedFilesRelativePaths } = await readManagedGitignoreFile({ + buildContext + }); + + await writeManagedGitignoreFile({ + buildContext, + uiModuleMetas, + ejectedFilesRelativePaths: [...ejectedFilesRelativePaths, fileRelativePath] + }); +} diff --git a/src/bin/main.ts b/src/bin/main.ts index 15c73167..ff79adad 100644 --- a/src/bin/main.ts +++ b/src/bin/main.ts @@ -241,6 +241,39 @@ program } }); +program + .command<{ + file: string; + }>({ + name: "eject-file", + description: "Take ownership over a given file" + }) + .option({ + key: "file", + name: (() => { + const name = "file"; + + optionsKeys.push(name); + + return name; + })(), + description: [ + "Relative path of the file relative to the directory of your keycloak theme source", + "Example `--file src/login/page/Login.tsx`" + ].join(" ") + }) + .task({ + skip, + handler: async ({ projectDirPath, file }) => { + const { command } = await import("./eject-file"); + + await command({ + buildContext: getBuildContext({ projectDirPath }), + cliCommandOptions: { file } + }); + } + }); + // Fallback to build command if no command is provided { const [, , ...rest] = process.argv; diff --git a/src/bin/postinstall/getUiModuleFileSourceCodeReadyToBeCopied.ts b/src/bin/postinstall/getUiModuleFileSourceCodeReadyToBeCopied.ts index 6855c0b1..f6c47890 100644 --- a/src/bin/postinstall/getUiModuleFileSourceCodeReadyToBeCopied.ts +++ b/src/bin/postinstall/getUiModuleFileSourceCodeReadyToBeCopied.ts @@ -36,16 +36,17 @@ export async function getUiModuleFileSourceCodeReadyToBeCopied(params: { if (isForEjection) { return [ `/*`, - ` This file was ejected from ${uiModuleName} version ${uiModuleVersion}.`, + `This file was ejected from ${uiModuleName} version ${uiModuleVersion}.`, `*/` ].join("\n"); } else { return [ `/*`, - ` WARNING: Before modifying this file run the following command:`, - ` \`npx keycloakify eject-file ${fileRelativePath.split(pathSep).join("/")}\``, - ` `, - ` This file comes from ${uiModuleName} version ${uiModuleVersion}.`, + `WARNING: Before modifying this file run the following command:`, + ``, + `npx keycloakify eject-file --file ${fileRelativePath.split(pathSep).join("/")}\``, + ``, + `This file comes from ${uiModuleName} version ${uiModuleVersion}.`, `*/` ]; } From b43c02f279892d723a3f6b9c0ca42611e6d50eb9 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 10 Nov 2024 10:35:19 +0100 Subject: [PATCH 10/21] Release candidate --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5143fec6..bac3efe7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "keycloakify", - "version": "11.3.12", + "version": "11.4.0-rc.0", "description": "Framework to create custom Keycloak UIs", "repository": { "type": "git", From 2ad36a8137cf005f4de5764de18a5647371ce540 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sat, 16 Nov 2024 21:37:14 +0100 Subject: [PATCH 11/21] Fix runPrettier --- src/bin/tools/runPrettier.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/bin/tools/runPrettier.ts b/src/bin/tools/runPrettier.ts index 98901ef0..7029c8f9 100644 --- a/src/bin/tools/runPrettier.ts +++ b/src/bin/tools/runPrettier.ts @@ -62,7 +62,19 @@ export async function runPrettier(params: { try { const { prettier, config } = await getPrettierAndConfig(); - formattedSourceCode = await prettier.format(sourceCode, { ...config, filePath }); + const { ignored, inferredParser } = await prettier.getFileInfo(filePath, { + resolveConfig: true + }); + + if (ignored) { + return sourceCode; + } + + formattedSourceCode = await prettier.format(sourceCode, { + ...config, + filePath, + parser: inferredParser ?? undefined + }); } catch (error) { console.log( chalk.red( From c593f5cb97a8a6e37394662169c0a1397b6c63d8 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sat, 16 Nov 2024 21:37:53 +0100 Subject: [PATCH 12/21] Re-sync version with main --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bac3efe7..a4bf9676 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "keycloakify", - "version": "11.4.0-rc.0", + "version": "11.3.19", "description": "Framework to create custom Keycloak UIs", "repository": { "type": "git", From c423e4caccb8ccf3222193184779dedd649735ce Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 17 Nov 2024 03:24:41 +0100 Subject: [PATCH 13/21] Adapt the npmInstall script so that it works when packages are linked --- scripts/link-in-app.ts | 65 ++- .../initializeAccountTheme_singlePage.ts | 18 +- src/bin/tools/npmInstall.ts | 389 +++++++++++++++++- 3 files changed, 454 insertions(+), 18 deletions(-) diff --git a/scripts/link-in-app.ts b/scripts/link-in-app.ts index 3dfbbd57..0f304862 100644 --- a/scripts/link-in-app.ts +++ b/scripts/link-in-app.ts @@ -125,7 +125,54 @@ if (testAppPaths.length === 0) { process.exit(-1); } -testAppPaths.forEach(testAppPath => execSync("yarn install", { cwd: testAppPath })); +testAppPaths.forEach(testAppPath => { + const packageJsonFilePath = pathJoin(testAppPath, "package.json"); + + const packageJsonContent = fs.readFileSync(packageJsonFilePath); + + const parsedPackageJson = JSON.parse(packageJsonContent.toString("utf8")) as { + scripts?: Record; + }; + + let hasPostInstallOrPrepareScript = false; + + if (parsedPackageJson.scripts !== undefined) { + for (const scriptName of ["postinstall", "prepare"]) { + if (parsedPackageJson.scripts[scriptName] === undefined) { + continue; + } + + hasPostInstallOrPrepareScript = true; + + delete parsedPackageJson.scripts[scriptName]; + } + } + + if (hasPostInstallOrPrepareScript) { + fs.writeFileSync( + packageJsonFilePath, + Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8") + ); + } + + const restorePackageJson = () => { + if (!hasPostInstallOrPrepareScript) { + return; + } + + fs.writeFileSync(packageJsonFilePath, packageJsonContent); + }; + + try { + execSync("yarn install", { cwd: testAppPath }); + } catch (error) { + restorePackageJson(); + + throw error; + } + + restorePackageJson(); +}); console.log("=== Linking common dependencies ==="); @@ -172,4 +219,20 @@ testAppPaths.forEach(testAppPath => }) ); +testAppPaths.forEach(testAppPath => { + const { scripts = {} } = JSON.parse( + fs.readFileSync(pathJoin(testAppPath, "package.json")).toString("utf8") + ) as { + scripts?: Record; + }; + + for (const scriptName of ["postinstall", "prepare"]) { + if (scripts[scriptName] === undefined) { + continue; + } + + execSync(`yarn run ${scriptName}`, { cwd: testAppPath }); + } +}); + export {}; diff --git a/src/bin/initialize-account-theme/initializeAccountTheme_singlePage.ts b/src/bin/initialize-account-theme/initializeAccountTheme_singlePage.ts index 6c6a1714..77984d3b 100644 --- a/src/bin/initialize-account-theme/initializeAccountTheme_singlePage.ts +++ b/src/bin/initialize-account-theme/initializeAccountTheme_singlePage.ts @@ -1,4 +1,4 @@ -import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; +import { relative as pathRelative, dirname as pathDirname } from "path"; import type { BuildContext } from "../shared/buildContext"; import * as fs from "fs"; import chalk from "chalk"; @@ -14,7 +14,6 @@ import { is } from "tsafe/is"; import { id } from "tsafe/id"; import { npmInstall } from "../tools/npmInstall"; import { copyBoilerplate } from "./copyBoilerplate"; -import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath"; type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & { fetchOptions: BuildContext["fetchOptions"]; @@ -121,20 +120,7 @@ export async function initializeAccountTheme_singlePage(params: { JSON.stringify(parsedPackageJson, undefined, 4) ); - run_npm_install: { - if ( - JSON.parse( - fs - .readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json")) - .toString("utf8") - )["version"] === "0.0.0" - ) { - //NOTE: Linked version - break run_npm_install; - } - - npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) }); - } + npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) }); copyBoilerplate({ accountThemeType: "Single-Page", diff --git a/src/bin/tools/npmInstall.ts b/src/bin/tools/npmInstall.ts index 2d62e129..c1995351 100644 --- a/src/bin/tools/npmInstall.ts +++ b/src/bin/tools/npmInstall.ts @@ -1,7 +1,14 @@ import * as fs from "fs"; -import { join as pathJoin } from "path"; +import { join as pathJoin, dirname as pathDirname } from "path"; import * as child_process from "child_process"; import chalk from "chalk"; +import { z } from "zod"; +import { assert, type Equals } from "tsafe/assert"; +import { id } from "tsafe/id"; +import { is } from "tsafe/is"; +import { objectKeys } from "tsafe/objectKeys"; +import { getAbsoluteAndInOsFormatPath } from "./getAbsoluteAndInOsFormatPath"; +import { exclude } from "tsafe/exclude"; export function npmInstall(params: { packageJsonDirPath: string }) { const { packageJsonDirPath } = params; @@ -48,6 +55,27 @@ export function npmInstall(params: { packageJsonDirPath: string }) { console.log(`Installing the new dependencies...`); + install_without_breaking_links: { + if (packageManagerBinName !== "yarn") { + break install_without_breaking_links; + } + + const garronejLinkInfos = getGarronejLinkInfos({ packageJsonDirPath }); + + if (garronejLinkInfos === undefined) { + break install_without_breaking_links; + } + + console.log(chalk.green("Installing in a way that won't break the links...")); + + installWithoutBreakingLinks({ + packageJsonDirPath, + garronejLinkInfos + }); + + return; + } + try { child_process.execSync(`${packageManagerBinName} install`, { cwd: packageJsonDirPath, @@ -61,3 +89,362 @@ export function npmInstall(params: { packageJsonDirPath: string }) { ); } } + +function getGarronejLinkInfos(params: { + packageJsonDirPath: string; +}): { linkedModuleNames: string[]; yarnHomeDirPath: string } | undefined { + const { packageJsonDirPath } = params; + + const nodeModuleDirPath = pathJoin(packageJsonDirPath, "node_modules"); + + if (!fs.existsSync(nodeModuleDirPath)) { + return undefined; + } + + const linkedModuleNames: string[] = []; + + let yarnHomeDirPath: string | undefined = undefined; + + const getIsLinkedByGarronejScript = (path: string) => { + let realPath: string; + + try { + realPath = fs.readlinkSync(path); + } catch { + return false; + } + + const doesIncludeYarnHome = realPath.includes(".yarn_home"); + + if (!doesIncludeYarnHome) { + return false; + } + + set_yarnHomeDirPath: { + if (yarnHomeDirPath !== undefined) { + break set_yarnHomeDirPath; + } + + const [firstElement] = getAbsoluteAndInOsFormatPath({ + pathIsh: realPath, + cwd: pathDirname(path) + }).split(".yarn_home"); + + yarnHomeDirPath = pathJoin(firstElement, ".yarn_home"); + } + + return true; + }; + + for (const basename of fs.readdirSync(nodeModuleDirPath)) { + const path = pathJoin(nodeModuleDirPath, basename); + + if (fs.lstatSync(path).isSymbolicLink()) { + if (basename.startsWith("@")) { + return undefined; + } + + if (!getIsLinkedByGarronejScript(path)) { + return undefined; + } + + linkedModuleNames.push(basename); + continue; + } + + if (!fs.lstatSync(path).isDirectory()) { + continue; + } + + if (basename.startsWith("@")) { + for (const subBasename of fs.readdirSync(path)) { + const subPath = pathJoin(path, subBasename); + + if (!fs.lstatSync(subPath).isSymbolicLink()) { + continue; + } + + if (!getIsLinkedByGarronejScript(subPath)) { + return undefined; + } + + linkedModuleNames.push(`${basename}/${subBasename}`); + } + } + } + + if (yarnHomeDirPath === undefined) { + return undefined; + } + + return { linkedModuleNames, yarnHomeDirPath }; +} + +function installWithoutBreakingLinks(params: { + packageJsonDirPath: string; + garronejLinkInfos: Exclude, undefined>; +}) { + const { + packageJsonDirPath, + garronejLinkInfos: { linkedModuleNames, yarnHomeDirPath } + } = params; + + const parsedPackageJson = (() => { + const packageJsonFilePath = pathJoin(packageJsonDirPath, "package.json"); + + type ParsedPackageJson = { + scripts?: Record; + }; + + const zParsedPackageJson = (() => { + type TargetType = ParsedPackageJson; + + const zTargetType = z.object({ + scripts: z.record(z.string()).optional() + }); + + type InferredType = z.infer; + + assert>; + + return id>(zTargetType); + })(); + + const parsedPackageJson = JSON.parse( + fs.readFileSync(packageJsonFilePath).toString("utf8") + ) as unknown; + + zParsedPackageJson.parse(parsedPackageJson); + assert(is(parsedPackageJson)); + + return parsedPackageJson; + })(); + + const isImplementedScriptByName = { + postinstall: false, + prepare: false + }; + + delete_postinstall_script: { + if (parsedPackageJson.scripts === undefined) { + break delete_postinstall_script; + } + + for (const scriptName of objectKeys(isImplementedScriptByName)) { + if (parsedPackageJson.scripts[scriptName] === undefined) { + continue; + } + + isImplementedScriptByName[scriptName] = true; + + delete parsedPackageJson.scripts[scriptName]; + } + } + + const tmpProjectDirPath = pathJoin(yarnHomeDirPath, "tmpProject"); + + if (fs.existsSync(tmpProjectDirPath)) { + fs.rmdirSync(tmpProjectDirPath, { recursive: true }); + } + + fs.mkdirSync(tmpProjectDirPath, { recursive: true }); + + fs.writeFileSync( + pathJoin(tmpProjectDirPath, "package.json"), + JSON.stringify(parsedPackageJson, undefined, 4) + ); + + const YARN_LOCK = "yarn.lock"; + + fs.copyFileSync( + pathJoin(packageJsonDirPath, YARN_LOCK), + pathJoin(tmpProjectDirPath, YARN_LOCK) + ); + + child_process.execSync(`yarn install`, { + cwd: tmpProjectDirPath, + stdio: "inherit" + }); + + // NOTE: Moving the modules from the tmp project to the actual project + // without messing up the links. + { + const { getAreSameVersions } = (() => { + type ParsedPackageJson = { + version: string; + }; + + const zParsedPackageJson = (() => { + type TargetType = ParsedPackageJson; + + const zTargetType = z.object({ + version: z.string() + }); + + type InferredType = z.infer; + + assert>; + + return id>(zTargetType); + })(); + + function readVersion(params: { moduleDirPath: string }): string { + const { moduleDirPath } = params; + + const packageJsonFilePath = pathJoin(moduleDirPath, "package.json"); + + const packageJson = JSON.parse( + fs.readFileSync(packageJsonFilePath).toString("utf8") + ); + + zParsedPackageJson.parse(packageJson); + assert(is(packageJson)); + + return packageJson.version; + } + + function getAreSameVersions(params: { + moduleDirPath_a: string; + moduleDirPath_b: string; + }): boolean { + const { moduleDirPath_a, moduleDirPath_b } = params; + + return ( + readVersion({ moduleDirPath: moduleDirPath_a }) === + readVersion({ moduleDirPath: moduleDirPath_b }) + ); + } + + return { getAreSameVersions }; + })(); + + const nodeModulesDirPath_tmpProject = pathJoin(tmpProjectDirPath, "node_modules"); + const nodeModulesDirPath = pathJoin(packageJsonDirPath, "node_modules"); + + const modulePaths = fs + .readdirSync(nodeModulesDirPath_tmpProject) + .map(basename => { + if (basename.startsWith(".")) { + return undefined; + } + + const path = pathJoin(nodeModulesDirPath_tmpProject, basename); + + if (basename.startsWith("@")) { + return fs + .readdirSync(path) + .map(subBasename => { + if (subBasename.startsWith(".")) { + return undefined; + } + + const subPath = pathJoin(path, subBasename); + + if (!fs.lstatSync(subPath).isDirectory()) { + return undefined; + } + + return { + moduleName: `${basename}/${subBasename}`, + moduleDirPath_tmpProject: subPath, + moduleDirPath: pathJoin( + nodeModulesDirPath, + basename, + subBasename + ) + }; + }) + .filter(exclude(undefined)); + } + + if (!fs.lstatSync(path).isDirectory()) { + return undefined; + } + + return [ + { + moduleName: basename, + moduleDirPath_tmpProject: path, + moduleDirPath: pathJoin(nodeModulesDirPath, basename) + } + ]; + }) + .filter(exclude(undefined)) + .flat(); + + for (const { + moduleName, + moduleDirPath, + moduleDirPath_tmpProject + } of modulePaths) { + if (linkedModuleNames.includes(moduleName)) { + continue; + } + + let doesTargetModuleExist = false; + + skip_condition: { + if (!fs.existsSync(moduleDirPath)) { + break skip_condition; + } + + doesTargetModuleExist = true; + + const areSameVersions = getAreSameVersions({ + moduleDirPath_a: moduleDirPath, + moduleDirPath_b: moduleDirPath_tmpProject + }); + + if (!areSameVersions) { + break skip_condition; + } + + continue; + } + + if (doesTargetModuleExist) { + fs.rmdirSync(moduleDirPath, { recursive: true }); + } + + fs.renameSync(moduleDirPath_tmpProject, moduleDirPath); + } + + move_bin: { + const binDirPath_tmpProject = pathJoin(nodeModulesDirPath_tmpProject, ".bin"); + const binDirPath = pathJoin(nodeModulesDirPath, ".bin"); + + if (!fs.existsSync(binDirPath_tmpProject)) { + break move_bin; + } + + for (const basename of fs.readdirSync(binDirPath_tmpProject)) { + const path_tmpProject = pathJoin(binDirPath_tmpProject, basename); + const path = pathJoin(binDirPath, basename); + + if (fs.existsSync(path)) { + continue; + } + + fs.renameSync(path_tmpProject, path); + } + } + } + + fs.cpSync( + pathJoin(tmpProjectDirPath, YARN_LOCK), + pathJoin(packageJsonDirPath, YARN_LOCK) + ); + + fs.rmdirSync(tmpProjectDirPath, { recursive: true }); + + for (const scriptName of objectKeys(isImplementedScriptByName)) { + if (!isImplementedScriptByName[scriptName]) { + continue; + } + + child_process.execSync(`yarn run ${scriptName}`, { + cwd: packageJsonDirPath, + stdio: "inherit" + }); + } +} From c03623875a4a856486d9289fde6c4e2047fd6010 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 17 Nov 2024 03:35:01 +0100 Subject: [PATCH 14/21] The admin theme does not support traditional eject --- src/bin/eject-page.ts | 15 +++++++++++---- src/bin/main.ts | 5 ++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/bin/eject-page.ts b/src/bin/eject-page.ts index da9c16d2..f85491d5 100644 --- a/src/bin/eject-page.ts +++ b/src/bin/eject-page.ts @@ -66,11 +66,18 @@ export async function command(params: { buildContext: BuildContext }) { return value; })(); + if (themeType === "admin") { + console.log( + "Use `npx keycloakify eject-file` command instead, see documentation" + ); + + process.exit(-1); + } + if ( - themeType === "admin" || - (themeType === "account" && - (assert(buildContext.implementedThemeTypes.account.isImplemented), - buildContext.implementedThemeTypes.account.type === "Single-Page")) + themeType === "account" && + (assert(buildContext.implementedThemeTypes.account.isImplemented), + buildContext.implementedThemeTypes.account.type === "Single-Page") ) { const srcDirPath = pathJoin( pathDirname(buildContext.packageJsonFilePath), diff --git a/src/bin/main.ts b/src/bin/main.ts index ff79adad..578079ab 100644 --- a/src/bin/main.ts +++ b/src/bin/main.ts @@ -246,7 +246,10 @@ program file: string; }>({ name: "eject-file", - description: "Take ownership over a given file" + description: [ + "WARNING: Not usable yet, will be used for future features", + "Take ownership over a given file" + ].join(" ") }) .option({ key: "file", From b8d4daf4c12dc648d2c32ace6d6e33d1a74ab4ee Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 17 Nov 2024 03:38:38 +0100 Subject: [PATCH 15/21] Temporarely restore runFormat (for merge conflicts) --- src/bin/tools/runFormat.ts | 76 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/bin/tools/runFormat.ts diff --git a/src/bin/tools/runFormat.ts b/src/bin/tools/runFormat.ts new file mode 100644 index 00000000..fd2eb108 --- /dev/null +++ b/src/bin/tools/runFormat.ts @@ -0,0 +1,76 @@ +import * as fs from "fs"; +import { dirname as pathDirname } from "path"; +import { assert, Equals } from "tsafe/assert"; +import chalk from "chalk"; +import { id } from "tsafe/id"; +import { z } from "zod"; +import { is } from "tsafe/is"; +import * as child_process from "child_process"; + +export function runFormat(params: { packageJsonFilePath: string }) { + const { packageJsonFilePath } = params; + + const parsedPackageJson = (() => { + type ParsedPackageJson = { + scripts?: Record; + }; + + const zParsedPackageJson = (() => { + type TargetType = ParsedPackageJson; + + const zTargetType = z.object({ + scripts: z.record(z.string()).optional() + }); + + assert, TargetType>>(); + + return id>(zTargetType); + })(); + + const parsedPackageJson = JSON.parse( + fs.readFileSync(packageJsonFilePath).toString("utf8") + ); + + zParsedPackageJson.parse(parsedPackageJson); + + assert(is(parsedPackageJson)); + + return parsedPackageJson; + })(); + + const { scripts } = parsedPackageJson; + + if (scripts === undefined) { + return; + } + + const scriptKeys = Object.keys(scripts); + const scriptNames = scriptKeys.filter(scriptKey => + scriptKey.trim().match(/^(lint|format)/) + ); + + for (const scriptName of scriptNames) { + if (!(scriptName in scripts)) { + continue; + } + + const command = `npm run ${scriptName}`; + + console.log(chalk.grey(`$ ${command}`)); + + try { + child_process.execSync(`npm run ${scriptName}`, { + stdio: "inherit", + cwd: pathDirname(packageJsonFilePath) + }); + } catch { + console.log( + chalk.yellow( + `\`${command}\` failed, it does not matter, please format your code manually, continuing...` + ) + ); + } + + return; + } +} From b6d2154d567231cfc08bc6eb876f585e74ecdea2 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 17 Nov 2024 16:45:14 +0100 Subject: [PATCH 16/21] Fix usage of deprecated node api --- src/bin/tools/npmInstall.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/bin/tools/npmInstall.ts b/src/bin/tools/npmInstall.ts index c1995351..e2f689d0 100644 --- a/src/bin/tools/npmInstall.ts +++ b/src/bin/tools/npmInstall.ts @@ -9,6 +9,7 @@ import { is } from "tsafe/is"; import { objectKeys } from "tsafe/objectKeys"; import { getAbsoluteAndInOsFormatPath } from "./getAbsoluteAndInOsFormatPath"; import { exclude } from "tsafe/exclude"; +import { rmSync } from "./fs.rmSync"; export function npmInstall(params: { packageJsonDirPath: string }) { const { packageJsonDirPath } = params; @@ -244,7 +245,7 @@ function installWithoutBreakingLinks(params: { const tmpProjectDirPath = pathJoin(yarnHomeDirPath, "tmpProject"); if (fs.existsSync(tmpProjectDirPath)) { - fs.rmdirSync(tmpProjectDirPath, { recursive: true }); + rmSync(tmpProjectDirPath, { recursive: true }); } fs.mkdirSync(tmpProjectDirPath, { recursive: true }); @@ -403,7 +404,15 @@ function installWithoutBreakingLinks(params: { } if (doesTargetModuleExist) { - fs.rmdirSync(moduleDirPath, { recursive: true }); + rmSync(moduleDirPath, { recursive: true }); + } + + { + const dirPath = pathDirname(moduleDirPath); + + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } } fs.renameSync(moduleDirPath_tmpProject, moduleDirPath); @@ -435,7 +444,7 @@ function installWithoutBreakingLinks(params: { pathJoin(packageJsonDirPath, YARN_LOCK) ); - fs.rmdirSync(tmpProjectDirPath, { recursive: true }); + rmSync(tmpProjectDirPath, { recursive: true }); for (const scriptName of objectKeys(isImplementedScriptByName)) { if (!isImplementedScriptByName[scriptName]) { From 7c3c6d3643018b7d0aa9776121e3dd55a137420e Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 17 Nov 2024 16:46:25 +0100 Subject: [PATCH 17/21] Fix misnamed kc.gen --- src/bin/update-kc-gen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/update-kc-gen.ts b/src/bin/update-kc-gen.ts index 847ddd46..21fa3fd5 100644 --- a/src/bin/update-kc-gen.ts +++ b/src/bin/update-kc-gen.ts @@ -18,7 +18,7 @@ export async function command(params: { buildContext: BuildContext }) { return; } - const filePath = pathJoin(buildContext.themeSrcDirPath, "kc-gen.tsx"); + const filePath = pathJoin(buildContext.themeSrcDirPath, "kc.gen.tsx"); const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented; const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented; From 32fb1e2f71a166ef50fd44936dc1c166ed5c539c Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 17 Nov 2024 19:22:34 +0100 Subject: [PATCH 18/21] Fix runPrettier script --- src/bin/postinstall/uiModuleMeta.ts | 6 ++--- src/bin/tools/runPrettier.ts | 37 +++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/bin/postinstall/uiModuleMeta.ts b/src/bin/postinstall/uiModuleMeta.ts index 24cdcd33..8ad49514 100644 --- a/src/bin/postinstall/uiModuleMeta.ts +++ b/src/bin/postinstall/uiModuleMeta.ts @@ -8,7 +8,7 @@ import { is } from "tsafe/is"; import { existsAsync } from "../tools/fs.existsAsync"; import { listInstalledModules } from "../tools/listInstalledModules"; import { crawlAsync } from "../tools/crawlAsync"; -import { getIsPrettierAvailable, getPrettierAndConfig } from "../tools/runPrettier"; +import { getIsPrettierAvailable, getPrettier } from "../tools/runPrettier"; import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion"; import { getUiModuleFileSourceCodeReadyToBeCopied, @@ -100,9 +100,9 @@ export async function getUiModuleMetas(params: { return null; } - const { config } = await getPrettierAndConfig(); + const { configHash } = await getPrettier(); - return crypto.createHash("sha256").update(JSON.stringify(config)).digest("hex"); + return configHash; })(); const installedUiModules = await (async () => { diff --git a/src/bin/tools/runPrettier.ts b/src/bin/tools/runPrettier.ts index 7029c8f9..134ae213 100644 --- a/src/bin/tools/runPrettier.ts +++ b/src/bin/tools/runPrettier.ts @@ -4,6 +4,7 @@ import * as fsPr from "fs/promises"; import { id } from "tsafe/id"; import { assert } from "tsafe/assert"; import chalk from "chalk"; +import * as crypto from "crypto"; getIsPrettierAvailable.cache = id(undefined); @@ -25,28 +26,42 @@ export async function getIsPrettierAvailable(): Promise { return isPrettierAvailable; } -type PrettierAndConfig = { +type PrettierAndConfigHash = { prettier: typeof import("prettier"); - config: import("prettier").Options | null; + configHash: string; }; -getPrettierAndConfig.cache = id(undefined); +getPrettier.cache = id(undefined); -export async function getPrettierAndConfig(): Promise { +export async function getPrettier(): Promise { assert(getIsPrettierAvailable()); - if (getPrettierAndConfig.cache !== undefined) { - return getPrettierAndConfig.cache; + if (getPrettier.cache !== undefined) { + return getPrettier.cache; } const prettier = await import("prettier"); - const prettierAndConfig: PrettierAndConfig = { + const configHash = await (async () => { + const configFilePath = await prettier.resolveConfigFile( + pathJoin(getNodeModulesBinDirPath(), "..") + ); + + if (configFilePath === null) { + return ""; + } + + const data = await fsPr.readFile(configFilePath); + + return crypto.createHash("sha256").update(data).digest("hex"); + })(); + + const prettierAndConfig: PrettierAndConfigHash = { prettier, - config: await prettier.resolveConfig(pathJoin(getNodeModulesBinDirPath(), "..")) + configHash }; - getPrettierAndConfig.cache = prettierAndConfig; + getPrettier.cache = prettierAndConfig; return prettierAndConfig; } @@ -60,7 +75,7 @@ export async function runPrettier(params: { let formattedSourceCode: string; try { - const { prettier, config } = await getPrettierAndConfig(); + const { prettier } = await getPrettier(); const { ignored, inferredParser } = await prettier.getFileInfo(filePath, { resolveConfig: true @@ -70,6 +85,8 @@ export async function runPrettier(params: { return sourceCode; } + const config = await prettier.resolveConfig(filePath); + formattedSourceCode = await prettier.format(sourceCode, { ...config, filePath, From 7f5eabb639ea52c935ec6753644b8f4334ba8181 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 17 Nov 2024 19:25:53 +0100 Subject: [PATCH 19/21] Format page when ejecting for the account --- src/bin/eject-page.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/bin/eject-page.ts b/src/bin/eject-page.ts index f85491d5..e411c9a6 100644 --- a/src/bin/eject-page.ts +++ b/src/bin/eject-page.ts @@ -83,13 +83,13 @@ export async function command(params: { buildContext: BuildContext }) { pathDirname(buildContext.packageJsonFilePath), "node_modules", "@keycloakify", - `keycloak-${themeType}-ui`, + `keycloak-account-ui`, "src" ); console.log( [ - `There isn't an interactive CLI to eject components of the ${themeType} UI.`, + `There isn't an interactive CLI to eject components of the Account SPA UI.`, `You can however copy paste into your codebase the any file or directory from the following source directory:`, ``, `${chalk.bold(pathJoin(pathRelative(process.cwd(), srcDirPath)))}`, @@ -98,9 +98,9 @@ export async function command(params: { buildContext: BuildContext }) { ); eject_entrypoint: { - const kcUiTsxFileRelativePath = `Kc${capitalize(themeType)}Ui.tsx` as const; + const kcUiTsxFileRelativePath = `KcAccountUi.tsx` as const; - const themeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, themeType); + const themeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account"); const targetFilePath = pathJoin(themeSrcDirPath, kcUiTsxFileRelativePath); @@ -120,11 +120,22 @@ export async function command(params: { buildContext: BuildContext }) { "" ); - const modifiedKcPageTsxCode = kcPageTsxCode.replace( - `@keycloakify/keycloak-${themeType}-ui/${componentName}`, + let modifiedKcPageTsxCode = kcPageTsxCode.replace( + `@keycloakify/keycloak-account-ui/${componentName}`, `./${componentName}` ); + run_prettier: { + if (!(await getIsPrettierAvailable())) { + break run_prettier; + } + + modifiedKcPageTsxCode = await runPrettier({ + filePath: kcPageTsxFilePath, + sourceCode: modifiedKcPageTsxCode + }); + } + fs.writeFileSync( kcPageTsxFilePath, Buffer.from(modifiedKcPageTsxCode, "utf8") From 32f471624a0f526014a7d339d015555885c51002 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 17 Nov 2024 19:31:29 +0100 Subject: [PATCH 20/21] Remove ignored file that where removed --- test/tsconfig.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/tsconfig.json b/test/tsconfig.json index ae92c7bf..b47111ab 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -17,8 +17,5 @@ "skipLibCheck": true }, "include": ["../src", "."], - "exclude": [ - "../src/bin/initialize-account-theme/src", - "../src/bin/initialize-admin-theme/src" - ] + "exclude": ["../src/bin/initialize-account-theme/src"] } From 30149ff1f2cf4b266f338d3948672bdf998a479b Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 17 Nov 2024 19:33:44 +0100 Subject: [PATCH 21/21] Remove ignored file that where removed --- src/bin/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/tsconfig.json b/src/bin/tsconfig.json index eda3bac6..b1a87db0 100644 --- a/src/bin/tsconfig.json +++ b/src/bin/tsconfig.json @@ -10,5 +10,5 @@ "rootDir": "." }, "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["initialize-account-theme/src", "initialize-admin-theme/src"] + "exclude": ["initialize-account-theme/src"] }