Implement initialize-admin-theme command

This commit is contained in:
Joseph Garrone 2024-12-22 17:09:15 +01:00
parent 94b7d2b85b
commit 13c21e8910
7 changed files with 274 additions and 12 deletions

View File

@ -119,7 +119,9 @@ export async function initializeAccountTheme_singlePage(params: {
JSON.stringify(parsedPackageJson, undefined, 4) JSON.stringify(parsedPackageJson, undefined, 4)
); );
npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) }); await npmInstall({
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
});
copyBoilerplate({ copyBoilerplate({
accountThemeType: "Single-Page", accountThemeType: "Single-Page",

View File

@ -0,0 +1,143 @@
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 { addPostinstallScriptIfNotPresent } from "./shared/addPostinstallScriptIfNotPresent";
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 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);
}
}
const parsedPackageJson = (() => {
type ParsedPackageJson = {
scripts?: Record<string, string | undefined>;
dependencies?: Record<string, string | undefined>;
devDependencies?: Record<string, string | undefined>;
};
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<Equals<z.infer<typeof zTargetType>, TargetType>>;
return id<z.ZodType<TargetType>>(zTargetType);
})();
const parsedPackageJson = JSON.parse(
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
);
zParsedPackageJson.parse(parsedPackageJson);
assert(is<ParsedPackageJson>(parsedPackageJson));
return parsedPackageJson;
})();
addPostinstallScriptIfNotPresent({
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-admin-ui";
const version = (
JSON.parse(
child_process
.execSync(`npm show ${moduleName} versions --json`)
.toString("utf8")
.trim()
) as string[]
).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)
});
}

View File

@ -191,7 +191,7 @@ program
program program
.command({ .command({
name: "initialize-account-theme", name: "initialize-account-theme",
description: "Initialize the account theme." description: "Initialize an Account Single-Page or Multi-Page custom Account UI."
}) })
.task({ .task({
skip, skip,
@ -202,6 +202,20 @@ program
} }
}); });
program
.command({
name: "initialize-admin-theme",
description: "Initialize an Admin Console custom UI."
})
.task({
skip,
handler: async ({ projectDirPath }) => {
const { command } = await import("./initialize-admin-theme");
await command({ buildContext: getBuildContext({ projectDirPath }) });
}
});
program program
.command({ .command({
name: "copy-keycloak-resources-to-public", name: "copy-keycloak-resources-to-public",

View File

@ -149,7 +149,7 @@ export async function installUiModulesPeerDependencies(params: {
await fsPr.writeFile(buildContext.packageJsonFilePath, packageJsonContentStr); await fsPr.writeFile(buildContext.packageJsonFilePath, packageJsonContentStr);
npmInstall({ await npmInstall({
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
}); });

View File

@ -0,0 +1,65 @@
import { dirname as pathDirname, relative as pathRelative, sep as pathSep } from "path";
import { assert } from "tsafe/assert";
import type { BuildContext } from "./buildContext";
export type BuildContextLike = {
projectDirPath: string;
packageJsonFilePath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export function addPostinstallScriptIfNotPresent(params: {
parsedPackageJson: { scripts?: Record<string, string | undefined> };
buildContext: BuildContextLike;
}) {
const { parsedPackageJson, buildContext } = params;
const scripts = (parsedPackageJson.scripts ??= {});
const cmd_base = "keycloakify postinstall";
const projectCliOptionValue = (() => {
const packageJsonDirPath = pathDirname(buildContext.packageJsonFilePath);
const relativePath = pathRelative(
packageJsonDirPath,
buildContext.projectDirPath
);
if (relativePath === "") {
return undefined;
}
return relativePath.split(pathSep).join("/");
})();
const generateCmd = (params: { cmd_preexisting: string | undefined }) => {
const { cmd_preexisting } = params;
let cmd = cmd_preexisting === undefined ? "" : `${cmd_preexisting} && `;
cmd += cmd_base;
if (projectCliOptionValue !== undefined) {
cmd += ` -p ${projectCliOptionValue}`;
}
return cmd;
};
for (const scriptName of ["postinstall", "prepare"]) {
const cmd_preexisting = scripts[scriptName];
if (cmd_preexisting === undefined) {
continue;
}
if (cmd_preexisting.includes(cmd_base)) {
scripts[scriptName] = generateCmd({ cmd_preexisting });
return;
}
}
scripts["postinstall"] = generateCmd({ cmd_preexisting: scripts["postinstall"] });
}

View File

@ -12,6 +12,7 @@ export type CommandName =
| "add-story" | "add-story"
| "initialize-account-theme" | "initialize-account-theme"
| "initialize-admin-theme" | "initialize-admin-theme"
| "initialize-admin-theme"
| "initialize-email-theme" | "initialize-email-theme"
| "copy-keycloak-resources-to-public"; | "copy-keycloak-resources-to-public";

View File

@ -9,8 +9,9 @@ import { objectKeys } from "tsafe/objectKeys";
import { getAbsoluteAndInOsFormatPath } from "./getAbsoluteAndInOsFormatPath"; import { getAbsoluteAndInOsFormatPath } from "./getAbsoluteAndInOsFormatPath";
import { exclude } from "tsafe/exclude"; import { exclude } from "tsafe/exclude";
import { rmSync } from "./fs.rmSync"; import { rmSync } from "./fs.rmSync";
import { Deferred } from "evt/tools/Deferred";
export function npmInstall(params: { packageJsonDirPath: string }) { export async function npmInstall(params: { packageJsonDirPath: string }) {
const { packageJsonDirPath } = params; const { packageJsonDirPath } = params;
const packageManagerBinName = (() => { const packageManagerBinName = (() => {
@ -68,7 +69,7 @@ export function npmInstall(params: { packageJsonDirPath: string }) {
console.log(chalk.green("Installing in a way that won't break the links...")); console.log(chalk.green("Installing in a way that won't break the links..."));
installWithoutBreakingLinks({ await installWithoutBreakingLinks({
packageJsonDirPath, packageJsonDirPath,
garronejLinkInfos garronejLinkInfos
}); });
@ -77,9 +78,9 @@ export function npmInstall(params: { packageJsonDirPath: string }) {
} }
try { try {
child_process.execSync(`${packageManagerBinName} install`, { await runPackageManagerInstall({
cwd: packageJsonDirPath, packageManagerBinName,
stdio: "inherit" cwd: packageJsonDirPath
}); });
} catch { } catch {
console.log( console.log(
@ -90,6 +91,42 @@ export function npmInstall(params: { packageJsonDirPath: string }) {
} }
} }
async function runPackageManagerInstall(params: {
packageManagerBinName: string;
cwd: string;
}) {
const { packageManagerBinName, cwd } = params;
const dCompleted = new Deferred<void>();
const child = child_process.spawn(packageManagerBinName, ["install"], {
cwd,
env: process.env,
shell: true
});
child.stdout.on("data", data => {
if (data.toString("utf8").includes("has unmet peer dependency")) {
return;
}
process.stdout.write(data);
});
child.stderr.on("data", data => process.stderr.write(data));
child.on("exit", code => {
if (code !== 0) {
dCompleted.reject(new Error(`Failed with code ${code}`));
return;
}
dCompleted.resolve();
});
await dCompleted.pr;
}
function getGarronejLinkInfos(params: { function getGarronejLinkInfos(params: {
packageJsonDirPath: string; packageJsonDirPath: string;
}): { linkedModuleNames: string[]; yarnHomeDirPath: string } | undefined { }): { linkedModuleNames: string[]; yarnHomeDirPath: string } | undefined {
@ -180,7 +217,7 @@ function getGarronejLinkInfos(params: {
return { linkedModuleNames, yarnHomeDirPath }; return { linkedModuleNames, yarnHomeDirPath };
} }
function installWithoutBreakingLinks(params: { async function installWithoutBreakingLinks(params: {
packageJsonDirPath: string; packageJsonDirPath: string;
garronejLinkInfos: Exclude<ReturnType<typeof getGarronejLinkInfos>, undefined>; garronejLinkInfos: Exclude<ReturnType<typeof getGarronejLinkInfos>, undefined>;
}) { }) {
@ -261,9 +298,9 @@ function installWithoutBreakingLinks(params: {
pathJoin(tmpProjectDirPath, YARN_LOCK) pathJoin(tmpProjectDirPath, YARN_LOCK)
); );
child_process.execSync(`yarn install`, { await runPackageManagerInstall({
cwd: tmpProjectDirPath, packageManagerBinName: "yarn",
stdio: "inherit" cwd: tmpProjectDirPath
}); });
// NOTE: Moving the modules from the tmp project to the actual project // NOTE: Moving the modules from the tmp project to the actual project