diff --git a/src/bin/eject-page.ts b/src/bin/eject-page.ts index ddaf152e..580a6bf9 100644 --- a/src/bin/eject-page.ts +++ b/src/bin/eject-page.ts @@ -11,12 +11,7 @@ import { } from "./shared/constants"; import { capitalize } from "tsafe/capitalize"; import * as fs from "fs"; -import { - join as pathJoin, - relative as pathRelative, - dirname as pathDirname, - basename as pathBasename -} from "path"; +import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase"; import { assert, Equals } from "tsafe/assert"; import type { BuildContext } from "./shared/buildContext"; @@ -77,85 +72,16 @@ export async function command(params: { buildContext: BuildContext }) { (assert(buildContext.implementedThemeTypes.account.isImplemented), buildContext.implementedThemeTypes.account.type === "Single-Page") ) { - const srcDirPath = pathJoin( - pathDirname(buildContext.packageJsonFilePath), - "node_modules", - "@keycloakify", - `keycloak-account-ui`, - "src" - ); - console.log( - [ - `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)))}`, - `` - ].join("\n") + chalk.yellow( + [ + "You are implementing a Single-Page Account theme.", + "The eject-page command isn't applicable in this context" + ].join("\n") + ) ); - eject_entrypoint: { - const kcUiTsxFileRelativePath = `KcAccountUi.tsx` as const; - - const themeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account"); - - const targetFilePath = pathJoin(themeSrcDirPath, kcUiTsxFileRelativePath); - - if (fs.existsSync(targetFilePath)) { - break eject_entrypoint; - } - - fs.cpSync(pathJoin(srcDirPath, kcUiTsxFileRelativePath), targetFilePath); - - { - const kcPageTsxFilePath = pathJoin(themeSrcDirPath, "KcPage.tsx"); - - const kcPageTsxCode = fs.readFileSync(kcPageTsxFilePath).toString("utf8"); - - const componentName = pathBasename(kcUiTsxFileRelativePath).replace( - /.tsx$/, - "" - ); - - 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") - ); - } - - const routesTsxFilePath = pathRelative( - process.cwd(), - pathJoin(srcDirPath, "routes.tsx") - ); - - console.log( - [ - `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(), themeSrcDirPath)}\``, - `then update the import of routes in ${kcUiTsxFileRelativePath}.` - ].join("\n") - ); - } - - process.exit(0); + process.exit(1); return; } diff --git a/src/bin/initialize-account-theme/copyBoilerplate.ts b/src/bin/initialize-account-theme/copyBoilerplate.ts deleted file mode 100644 index 983af445..00000000 --- a/src/bin/initialize-account-theme/copyBoilerplate.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as fs from "fs"; -import { join as pathJoin } from "path"; -import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath"; -import { assert, type Equals } from "tsafe/assert"; - -export function copyBoilerplate(params: { - accountThemeType: "Single-Page" | "Multi-Page"; - accountThemeSrcDirPath: string; -}) { - const { accountThemeType, accountThemeSrcDirPath } = params; - - fs.cpSync( - pathJoin( - getThisCodebaseRootDirPath(), - "src", - "bin", - "initialize-account-theme", - "src", - (() => { - switch (accountThemeType) { - case "Single-Page": - return "single-page"; - case "Multi-Page": - return "multi-page"; - } - assert>(false); - })() - ), - accountThemeSrcDirPath, - { recursive: true } - ); -} diff --git a/src/bin/initialize-account-theme/initialize-account-theme.ts b/src/bin/initialize-account-theme/initialize-account-theme.ts index 0caead70..e14b3858 100644 --- a/src/bin/initialize-account-theme/initialize-account-theme.ts +++ b/src/bin/initialize-account-theme/initialize-account-theme.ts @@ -7,6 +7,7 @@ import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeIm import { command as updateKcGenCommand } from "../update-kc-gen"; import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate"; import { exitIfUncommittedChanges } from "../shared/exitIfUncommittedChanges"; +import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath"; export async function command(params: { buildContext: BuildContext }) { const { buildContext } = params; @@ -50,24 +51,24 @@ export async function command(params: { buildContext: BuildContext }) { switch (accountThemeType) { case "Multi-Page": - { - const { initializeAccountTheme_multiPage } = await import( - "./initializeAccountTheme_multiPage" - ); - - await initializeAccountTheme_multiPage({ - accountThemeSrcDirPath - }); - } + fs.cpSync( + pathJoin( + getThisCodebaseRootDirPath(), + "src", + "bin", + "initialize-account-theme", + "multi-page-boilerplate" + ), + accountThemeSrcDirPath, + { recursive: true } + ); break; case "Single-Page": { - const { initializeAccountTheme_singlePage } = await import( - "./initializeAccountTheme_singlePage" - ); + const { initializeSpa } = await import("../shared/initializeSpa"); - await initializeAccountTheme_singlePage({ - accountThemeSrcDirPath, + await initializeSpa({ + themeType: "account", buildContext }); } diff --git a/src/bin/initialize-account-theme/initializeAccountTheme_multiPage.ts b/src/bin/initialize-account-theme/initializeAccountTheme_multiPage.ts deleted file mode 100644 index d3dfe78d..00000000 --- a/src/bin/initialize-account-theme/initializeAccountTheme_multiPage.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { relative as pathRelative } from "path"; -import chalk from "chalk"; -import { copyBoilerplate } from "./copyBoilerplate"; - -export async function initializeAccountTheme_multiPage(params: { - accountThemeSrcDirPath: string; -}) { - const { accountThemeSrcDirPath } = params; - - copyBoilerplate({ - accountThemeType: "Multi-Page", - accountThemeSrcDirPath - }); - - console.log( - [ - chalk.green("The Multi-Page account theme has been initialized."), - `Directory created: ${chalk.bold(pathRelative(process.cwd(), accountThemeSrcDirPath))}` - ].join("\n") - ); -} diff --git a/src/bin/initialize-account-theme/initializeAccountTheme_singlePage.ts b/src/bin/initialize-account-theme/initializeAccountTheme_singlePage.ts deleted file mode 100644 index e2a2b535..00000000 --- a/src/bin/initialize-account-theme/initializeAccountTheme_singlePage.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { relative as pathRelative, dirname as pathDirname } from "path"; -import type { BuildContext } from "../shared/buildContext"; -import * as fs from "fs"; -import chalk from "chalk"; -import fetch from "make-fetch-happen"; -import { z } from "zod"; -import { assert, type Equals, is } from "tsafe/assert"; -import { id } from "tsafe/id"; -import { npmInstall } from "../tools/npmInstall"; -import { copyBoilerplate } from "./copyBoilerplate"; - -type BuildContextLike = { - fetchOptions: BuildContext["fetchOptions"]; - packageJsonFilePath: string; -}; - -assert(); - -export async function initializeAccountTheme_singlePage(params: { - accountThemeSrcDirPath: string; - buildContext: BuildContextLike; -}) { - const { accountThemeSrcDirPath, buildContext } = params; - - const OWNER = "keycloakify"; - const REPO = "keycloak-account-ui"; - - const version = "26.0.6-rc.1"; - - const dependencies = await fetch( - `https://raw.githubusercontent.com/${OWNER}/${REPO}/v${version}/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-account-ui"] = 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) - ); - - await npmInstall({ - packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) - }); - - copyBoilerplate({ - accountThemeType: "Single-Page", - accountThemeSrcDirPath - }); - - console.log( - [ - chalk.green( - "The Single-Page account theme has been successfully initialized." - ), - `Using Account UI of Keycloak version: ${chalk.bold(version.split("-")[0])}`, - `Directory created: ${chalk.bold(pathRelative(process.cwd(), accountThemeSrcDirPath))}`, - `Dependencies added to your project's package.json: `, - chalk.bold(JSON.stringify(dependencies, null, 2)) - ].join("\n") - ); -} diff --git a/src/bin/initialize-account-theme/src/multi-page/KcContext.ts b/src/bin/initialize-account-theme/multi-page-boilerplate/KcContext.ts similarity index 100% rename from src/bin/initialize-account-theme/src/multi-page/KcContext.ts rename to src/bin/initialize-account-theme/multi-page-boilerplate/KcContext.ts diff --git a/src/bin/initialize-account-theme/src/multi-page/KcPage.tsx b/src/bin/initialize-account-theme/multi-page-boilerplate/KcPage.tsx similarity index 100% rename from src/bin/initialize-account-theme/src/multi-page/KcPage.tsx rename to src/bin/initialize-account-theme/multi-page-boilerplate/KcPage.tsx diff --git a/src/bin/initialize-account-theme/src/multi-page/KcPageStory.tsx b/src/bin/initialize-account-theme/multi-page-boilerplate/KcPageStory.tsx similarity index 100% rename from src/bin/initialize-account-theme/src/multi-page/KcPageStory.tsx rename to src/bin/initialize-account-theme/multi-page-boilerplate/KcPageStory.tsx diff --git a/src/bin/initialize-account-theme/src/multi-page/i18n.ts b/src/bin/initialize-account-theme/multi-page-boilerplate/i18n.ts similarity index 100% rename from src/bin/initialize-account-theme/src/multi-page/i18n.ts rename to src/bin/initialize-account-theme/multi-page-boilerplate/i18n.ts diff --git a/src/bin/initialize-account-theme/src/single-page/KcContext.ts b/src/bin/initialize-account-theme/src/single-page/KcContext.ts deleted file mode 100644 index db2be626..00000000 --- a/src/bin/initialize-account-theme/src/single-page/KcContext.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { KcContextLike } from "@keycloakify/keycloak-account-ui"; -import type { KcEnvName } from "../kc.gen"; - -export type KcContext = KcContextLike & { - themeType: "account"; - properties: Record; -}; diff --git a/src/bin/initialize-account-theme/src/single-page/KcPage.tsx b/src/bin/initialize-account-theme/src/single-page/KcPage.tsx deleted file mode 100644 index e086f00d..00000000 --- a/src/bin/initialize-account-theme/src/single-page/KcPage.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { lazy } from "react"; -import { KcAccountUiLoader } from "@keycloakify/keycloak-account-ui"; -import type { KcContext } from "./KcContext"; - -const KcAccountUi = lazy(() => import("@keycloakify/keycloak-account-ui/KcAccountUi")); - -export default function KcPage(props: { kcContext: KcContext }) { - const { kcContext } = props; - - return ; -} diff --git a/src/bin/initialize-admin-theme.ts b/src/bin/initialize-admin-theme.ts index 24706406..4aff0d25 100644 --- a/src/bin/initialize-admin-theme.ts +++ b/src/bin/initialize-admin-theme.ts @@ -1,15 +1,8 @@ -import { dirname as pathDirname, join as pathJoin, relative as pathRelative } from "path"; import type { BuildContext } from "./shared/buildContext"; -import * as fs from "fs"; import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate"; -import { assert, is, type Equals } from "tsafe/assert"; -import { id } from "tsafe/id"; -import { addSyncExtensionsToPostinstallScript } from "./shared/addSyncExtensionsToPostinstallScript"; -import { getIsPrettierAvailable, runPrettier } from "./tools/runPrettier"; -import { npmInstall } from "./tools/npmInstall"; -import * as child_process from "child_process"; -import { z } from "zod"; -import chalk from "chalk"; +import { initializeSpa } from "./shared/initializeSpa"; +import { exitIfUncommittedChanges } from "./shared/exitIfUncommittedChanges"; +import { command as updateKcGenCommand } from "./update-kc-gen"; export async function command(params: { buildContext: BuildContext }) { const { buildContext } = params; @@ -23,124 +16,24 @@ export async function command(params: { buildContext: BuildContext }) { return; } - { - const adminThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "admin"); + exitIfUncommittedChanges({ + projectDirPath: buildContext.projectDirPath + }); - 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); - } - } - - const parsedPackageJson = (() => { - type ParsedPackageJson = { - scripts?: Record; - dependencies?: Record; - devDependencies?: Record; - }; - - const zParsedPackageJson = (() => { - type TargetType = ParsedPackageJson; - - const zTargetType = z.object({ - scripts: z.record(z.union([z.string(), z.undefined()])).optional(), - dependencies: z.record(z.union([z.string(), z.undefined()])).optional(), - devDependencies: z.record(z.union([z.string(), z.undefined()])).optional() - }); - - assert, TargetType>>; - - return id>(zTargetType); - })(); - const parsedPackageJson = JSON.parse( - fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8") - ); - - zParsedPackageJson.parse(parsedPackageJson); - - assert(is(parsedPackageJson)); - - return parsedPackageJson; - })(); - - addSyncExtensionsToPostinstallScript({ - parsedPackageJson, + await initializeSpa({ + themeType: "admin", buildContext }); - const uiSharedMajor = (() => { - const dependencies = { - ...parsedPackageJson.devDependencies, - ...parsedPackageJson.dependencies - }; - - const version = dependencies["@keycloakify/keycloak-ui-shared"]; - - if (version === undefined) { - return undefined; + await updateKcGenCommand({ + buildContext: { + ...buildContext, + implementedThemeTypes: { + ...buildContext.implementedThemeTypes, + admin: { + isImplemented: true + } + } } - - const match = version.match(/^[^~]?(\d+)\./); - - if (match === null) { - return undefined; - } - - return match[1]; - })(); - - const moduleName = "@keycloakify/keycloak-admin-ui"; - - const version = ( - JSON.parse( - child_process - .execSync(`npm show ${moduleName} versions --json`) - .toString("utf8") - .trim() - ) as string[] - ) - .reverse() - .filter(version => !version.includes("-")) - .find(version => - uiSharedMajor === undefined ? true : version.startsWith(`${uiSharedMajor}.`) - ); - - assert(version !== undefined); - - (parsedPackageJson.dependencies ??= {})[moduleName] = `~${version}`; - - if (parsedPackageJson.devDependencies !== undefined) { - delete parsedPackageJson.devDependencies[moduleName]; - } - - { - let sourceCode = JSON.stringify(parsedPackageJson, undefined, 2); - - if (await getIsPrettierAvailable()) { - sourceCode = await runPrettier({ - sourceCode, - filePath: buildContext.packageJsonFilePath - }); - } - - fs.writeFileSync( - buildContext.packageJsonFilePath, - Buffer.from(sourceCode, "utf8") - ); - } - - await npmInstall({ - packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) }); } diff --git a/src/bin/shared/addSyncExtensionsToPostinstallScript.ts b/src/bin/shared/initializeSpa/addSyncExtensionsToPostinstallScript.ts similarity index 97% rename from src/bin/shared/addSyncExtensionsToPostinstallScript.ts rename to src/bin/shared/initializeSpa/addSyncExtensionsToPostinstallScript.ts index 78d71353..d8963f74 100644 --- a/src/bin/shared/addSyncExtensionsToPostinstallScript.ts +++ b/src/bin/shared/initializeSpa/addSyncExtensionsToPostinstallScript.ts @@ -1,6 +1,6 @@ import { dirname as pathDirname, relative as pathRelative, sep as pathSep } from "path"; import { assert } from "tsafe/assert"; -import type { BuildContext } from "./buildContext"; +import type { BuildContext } from "../buildContext"; export type BuildContextLike = { projectDirPath: string; diff --git a/src/bin/shared/initializeSpa/index.ts b/src/bin/shared/initializeSpa/index.ts new file mode 100644 index 00000000..d1feadc5 --- /dev/null +++ b/src/bin/shared/initializeSpa/index.ts @@ -0,0 +1 @@ +export * from "./initializeSpa"; diff --git a/src/bin/shared/initializeSpa/initializeSpa.ts b/src/bin/shared/initializeSpa/initializeSpa.ts new file mode 100644 index 00000000..cbf595d8 --- /dev/null +++ b/src/bin/shared/initializeSpa/initializeSpa.ts @@ -0,0 +1,149 @@ +import { dirname as pathDirname, join as pathJoin, relative as pathRelative } from "path"; +import type { BuildContext } from "../buildContext"; +import * as fs from "fs"; +import { assert, is, type Equals } from "tsafe/assert"; +import { id } from "tsafe/id"; +import { + addSyncExtensionsToPostinstallScript, + type BuildContextLike as BuildContextLike_addSyncExtensionsToPostinstallScript +} from "./addSyncExtensionsToPostinstallScript"; +import { getIsPrettierAvailable, runPrettier } from "../../tools/runPrettier"; +import { npmInstall } from "../../tools/npmInstall"; +import * as child_process from "child_process"; +import { z } from "zod"; +import chalk from "chalk"; + +export type BuildContextLike = BuildContextLike_addSyncExtensionsToPostinstallScript & { + themeSrcDirPath: string; + packageJsonFilePath: string; +}; + +assert(); + +export async function initializeSpa(params: { + themeType: "account" | "admin"; + buildContext: BuildContextLike; +}) { + const { themeType, buildContext } = params; + + { + const themeTypeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, themeType); + + if ( + fs.existsSync(themeTypeSrcDirPath) && + fs.readdirSync(themeTypeSrcDirPath).length > 0 + ) { + console.warn( + chalk.red( + `There is already a ${pathRelative( + process.cwd(), + themeTypeSrcDirPath + )} directory in your project. Aborting.` + ) + ); + + process.exit(-1); + } + } + + const parsedPackageJson = (() => { + type ParsedPackageJson = { + scripts?: Record; + dependencies?: Record; + devDependencies?: Record; + }; + + const zParsedPackageJson = (() => { + type TargetType = ParsedPackageJson; + + const zTargetType = z.object({ + scripts: z.record(z.union([z.string(), z.undefined()])).optional(), + dependencies: z.record(z.union([z.string(), z.undefined()])).optional(), + devDependencies: z.record(z.union([z.string(), z.undefined()])).optional() + }); + + assert, TargetType>>; + + return id>(zTargetType); + })(); + const parsedPackageJson = JSON.parse( + fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8") + ); + + zParsedPackageJson.parse(parsedPackageJson); + + assert(is(parsedPackageJson)); + + return parsedPackageJson; + })(); + + addSyncExtensionsToPostinstallScript({ + parsedPackageJson, + buildContext + }); + + const uiSharedMajor = (() => { + const dependencies = { + ...parsedPackageJson.devDependencies, + ...parsedPackageJson.dependencies + }; + + const version = dependencies["@keycloakify/keycloak-ui-shared"]; + + if (version === undefined) { + return undefined; + } + + const match = version.match(/^[^~]?(\d+)\./); + + if (match === null) { + return undefined; + } + + return match[1]; + })(); + + const moduleName = `@keycloakify/keycloak-${themeType}-ui`; + + const version = ( + JSON.parse( + child_process + .execSync(`npm show ${moduleName} versions --json`) + .toString("utf8") + .trim() + ) as string[] + ) + .reverse() + .filter(version => !version.includes("-")) + .find(version => + uiSharedMajor === undefined ? true : version.startsWith(`${uiSharedMajor}.`) + ); + + assert(version !== undefined); + + (parsedPackageJson.dependencies ??= {})[moduleName] = `~${version}`; + + if (parsedPackageJson.devDependencies !== undefined) { + delete parsedPackageJson.devDependencies[moduleName]; + } + + { + let sourceCode = JSON.stringify(parsedPackageJson, undefined, 2); + + if (await getIsPrettierAvailable()) { + sourceCode = await runPrettier({ + sourceCode, + filePath: buildContext.packageJsonFilePath + }); + } + + fs.writeFileSync( + buildContext.packageJsonFilePath, + Buffer.from(sourceCode, "utf8") + ); + } + + await npmInstall({ + packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) + }); +} diff --git a/src/bin/tsconfig.json b/src/bin/tsconfig.json index b1a87db0..0b4e8785 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/multi-page-boilerplate"] }