import { setupVitePluginIfNeeded } from "./setupVitePluginIfNeeded";
import { setupEslint } from "./setupEslint";
import { getBuildContext } from "../shared/buildContext";
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
import { z } from "zod";
import { assert, type Equals, is } from "tsafe/assert";
import { id } from "tsafe/id";
import * as fs from "fs/promises";
import { runPrettier, getIsPrettierAvailable } from "../tools/runPrettier";
import cliSelect from "cli-select";
import { THEME_TYPES } from "../shared/constants";
import chalk from "chalk";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { existsAsync } from "../tools/fs.existsAsync";
import { KEYCLOAK_THEME } from "../shared/constants";
export async function command(params: { projectDirPath: string }) {
const { projectDirPath } = params;
await setupVitePluginIfNeeded({ projectDirPath });
let buildContext = getBuildContext({ projectDirPath });
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
commandName: "init",
buildContext
});
if (hasBeenHandled) {
return;
}
await setupEslint({ projectDirPath });
let doAddRunDevScript = false;
setup_src: {
if (buildContext.bundler !== "vite") {
break setup_src;
}
const srcDirPath = pathJoin(buildContext.projectDirPath, "src");
const mainTsxFilePath = pathJoin(srcDirPath, "main.tsx");
if (!(await existsAsync(mainTsxFilePath))) {
break setup_src;
}
const isAlreadySetup = await (async () => {
if (buildContext.themeSrcDirPath !== srcDirPath) {
return true;
}
{
const basenames = await fs.readdir(srcDirPath);
for (const basename of basenames) {
const path = pathJoin(srcDirPath, basename);
if (!(await fs.stat(path)).isFile()) {
continue;
}
if ((await fs.readFile(path)).toString("utf8").includes("./kc.gen")) {
return true;
}
}
}
for (const themeType of [...THEME_TYPES, "email"]) {
if (!(await existsAsync(pathJoin(srcDirPath, themeType)))) {
continue;
}
return true;
}
return false;
})();
if (isAlreadySetup) {
break setup_src;
}
const doSetupAsStandalone = await (async () => {
const relativeProjectDirPath = pathRelative(
process.cwd(),
buildContext.projectDirPath
);
console.log(
chalk.cyan(
[
relativeProjectDirPath === ""
? "This Vite project"
: `The Vite project at ${relativeProjectDirPath}`,
"is it dedicated *only* to building a Keycloak theme?"
].join(" ")
)
);
const YES = "Yes — this project is only for the Keycloak theme (recommended)";
const NO =
"No — I'm building an app and a Keycloak theme in the same project (advanced)";
const { value } = await cliSelect({ values: [YES, NO] }).catch(() => {
process.exit(-1);
});
console.log(`${value}\n`);
return value === YES;
})();
let files: { relativeFilePath: string; fileContent: string }[];
if (doSetupAsStandalone) {
const viteEnvDTsFilePath = pathJoin(
buildContext.themeSrcDirPath,
"vite-env.d.ts"
);
const viteEnvDTsContent = (await existsAsync(viteEnvDTsFilePath))
? await fs.readFile(viteEnvDTsFilePath)
: `/// \n`;
await fs.rm(srcDirPath, { recursive: true });
await fs.mkdir(srcDirPath);
await fs.writeFile(viteEnvDTsFilePath, viteEnvDTsContent);
files = [
{
relativeFilePath: "main.tsx",
fileContent: [
`if (window.kcContext !== undefined) {`,
` import("./main-kc");`,
`} else {`,
` import("./main-kc.dev");`,
`}`
].join("\n")
},
{
relativeFilePath: "main-kc.dev.tsx",
fileContent: `export {};\n`
},
{
relativeFilePath: "main-kc.tsx",
fileContent: [
`import { createRoot } from "react-dom/client";`,
`import { StrictMode } from "react";`,
`import { KcPage } from "./kc.gen";`,
``,
`if (!window.kcContext) {`,
` throw new Error("No Keycloak context");`,
`}`,
``,
`createRoot(document.getElementById("root")!).render(`,
` `,
` `,
` `,
`);`
].join("\n")
}
];
} else {
doAddRunDevScript = true;
await fs.copyFile(
mainTsxFilePath,
pathJoin(buildContext.themeSrcDirPath, "main-app.tsx")
);
files = [
{
relativeFilePath: "main.tsx",
fileContent: [
`if (window.kcContext !== undefined) {`,
` import("./keycloak-theme/main");`,
`} else if (import.meta.env.VITE_KC_DEV === "true") {`,
` import("./keycloak-theme/main.dev");`,
`} else {`,
` import("./main-app");`,
`}`,
``
].join("\n")
},
{
relativeFilePath: pathJoin(KEYCLOAK_THEME, "main.dev.tsx"),
fileContent: `export {};\n`
},
{
relativeFilePath: pathJoin(KEYCLOAK_THEME, "main.tsx"),
fileContent: [
`import { createRoot } from "react-dom/client";`,
`import { StrictMode } from "react";`,
`import { KcPage } from "./kc.gen";`,
``,
`if (!window.kcContext) {`,
` throw new Error("No Keycloak context");`,
`}`,
``,
`createRoot(document.getElementById("root")!).render(`,
` `,
` `,
` `,
`);`,
``
].join("\n")
}
];
}
for (let { relativeFilePath, fileContent } of files) {
const filePath = pathJoin(srcDirPath, relativeFilePath);
{
const dirPath = pathDirname(filePath);
if (!(await existsAsync(dirPath))) {
await fs.mkdir(dirPath, { recursive: true });
}
}
run_prettier: {
if (!(await getIsPrettierAvailable())) {
break run_prettier;
}
fileContent = await runPrettier({
filePath: filePath,
sourceCode: fileContent
});
}
await fs.writeFile(filePath, Buffer.from(fileContent, "utf8"));
}
}
add_script: {
const parsedPackageJson = await (async () => {
type ParsedPackageJson = {
scripts: Record;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
scripts: z.record(z.string(), z.string())
});
assert, TargetType>>();
return id>(zTargetType);
})();
const parsedPackageJson = JSON.parse(
(await fs.readFile(buildContext.packageJsonFilePath)).toString("utf8")
);
zParsedPackageJson.parse(parsedPackageJson);
assert(is(parsedPackageJson));
return parsedPackageJson;
})();
const SCRIPT_NAME = "build-keycloak-theme";
if (SCRIPT_NAME in parsedPackageJson.scripts) {
break add_script;
}
parsedPackageJson.scripts[SCRIPT_NAME] = "npm run build && keycloakify build";
if (doAddRunDevScript) {
parsedPackageJson.scripts["dev-keycloak-theme"] =
"VITE_KC_DEV=true npm run dev";
}
let packageJson_content = JSON.stringify(parsedPackageJson, null, 2);
run_prettier: {
if (!(await getIsPrettierAvailable())) {
break run_prettier;
}
packageJson_content = await runPrettier({
filePath: buildContext.packageJsonFilePath,
sourceCode: packageJson_content
});
}
await fs.writeFile(
buildContext.packageJsonFilePath,
Buffer.from(packageJson_content, "utf8")
);
}
const themeType = await (async () => {
const values = ([...THEME_TYPES, "email"] as const).filter(themeType => {
const wrap = buildContext.implementedThemeTypes[themeType];
return !wrap.isImplemented && !wrap.isImplemented_native;
});
if (values.length === 0) {
return undefined;
}
console.log(chalk.cyan(`Which theme theme type would you like to initialize?`));
const { value } = await cliSelect({
values
}).catch(() => {
process.exit(-1);
});
console.log(value);
return value;
})();
if (themeType === undefined) {
console.log(
chalk.gray("You already have implemented a theme type of every kind, exiting")
);
process.exit(0);
}
buildContext = getBuildContext({ projectDirPath });
switch (themeType) {
case "account":
{
const { command } = await import("../initialize-account-theme");
await command({ buildContext });
}
return;
case "admin":
{
const { command } = await import("../initialize-admin-theme");
await command({ buildContext });
}
return;
case "email":
{
const { command } = await import("../initialize-email-theme");
await command({ buildContext });
}
return;
case "login":
{
const { command } = await import("../initialize-login-theme");
await command({ buildContext });
}
return;
}
assert>;
}