keycloakify
Version:
Framework to create custom Keycloak UIs
256 lines (226 loc) • 9.71 kB
text/typescript
import type { BuildContext } from "./shared/buildContext";
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 * as crypto from "crypto";
import { getIsPrettierAvailable, runPrettier } from "./tools/runPrettier";
import { assert, type Equals } from "tsafe/assert";
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "./shared/constants";
import { readPackageJsonDependencies } from "./tools/listInstalledModules";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
run_copy_assets_to_public: {
if (buildContext.bundler !== "webpack") {
break run_copy_assets_to_public;
}
const { command } = await import("./copy-keycloak-resources-to-public");
await command({ buildContext });
}
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
commandName: "update-kc-gen",
buildContext
});
if (hasBeenHandled) {
return;
}
const filePath = pathJoin(buildContext.themeSrcDirPath, "kc.gen.tsx");
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
const hasAdminTheme = buildContext.implementedThemeTypes.admin.isImplemented;
let newContent: string;
set_new_content: {
if (!hasLoginTheme && !hasAccountTheme && !hasAdminTheme) {
newContent = [
``,
`/* eslint-disable */`,
``,
`// @ts-nocheck`,
``,
`// noinspection JSUnusedGlobalSymbols`,
``,
`export function KcPage(_props: { kcContext: any; }){`,
` return null;`,
`}`,
``,
`declare global {`,
` interface Window {`,
` kcContext?: KcContext;`,
` }`,
`}`,
``
].join("\n");
break set_new_content;
}
newContent = [
``,
`/* eslint-disable */`,
``,
`// @ts-nocheck`,
``,
`// noinspection JSUnusedGlobalSymbols`,
``,
`import { lazy, Suspense, type ReactNode } from "react";`,
``,
`export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
``,
`export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`,
``,
`export type KcEnvName = ${buildContext.environmentVariables.length === 0 ? "never" : buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(" | ")};`,
``,
`export const kcEnvNames: KcEnvName[] = [${buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(", ")}];`,
``,
`export const kcEnvDefaults: Record<KcEnvName, string> = ${JSON.stringify(
Object.fromEntries(
buildContext.environmentVariables.map(
({ name, default: defaultValue }) => [name, defaultValue]
)
),
null,
2
)};`,
``,
`/**`,
` * NOTE: Do not import this type except maybe in your entrypoint. `,
` * If you need to import the KcContext import it either from src/login/KcContext.ts or src/account/KcContext.ts.`,
` * Depending on the theme type you are working on.`,
` */`,
`export type KcContext =`,
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
hasAdminTheme && ` | import("./admin/KcContext").KcContext`,
` ;`,
``,
`declare global {`,
` interface Window {`,
` kcContext?: KcContext;`,
` }`,
`}`,
``,
hasLoginTheme &&
`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: {`,
` kcContext: KcContext;`,
` fallback?: ReactNode;`,
` }`,
`) {`,
` const { kcContext, fallback } = props;`,
` return (`,
` <Suspense fallback={fallback}>`,
` {(() => {`,
` switch (kcContext.themeType) {`,
hasLoginTheme &&
` case "login": return <KcLoginPage kcContext={kcContext} />;`,
hasAccountTheme &&
` case "account": return <KcAccountPage kcContext={kcContext} />;`,
hasAdminTheme &&
` case "admin": return <KcAdminPage kcContext={kcContext} />;`,
` }`,
` })()}`,
` </Suspense>`,
` );`,
`}`,
``,
...(() => {
const { bundler } = buildContext;
switch (bundler) {
case "vite":
return [
"// NOTE: This is exported here only because in Webpack environnement it works differently",
`export const BASE_URL = import.meta.env.BASE_URL`,
""
];
case "webpack":
return [
"// NOTE: This is a polyfill for `import.meta.env.BASE_URL` as it's not available in Webpack environment.",
"export const BASE_URL =",
` window.kcContext === undefined || process.env.NODE_ENV === "development"`,
` ? process.env.PUBLIC_URL === ""`,
` ? "/"`,
` : \`${process.env.PUBLIC_URL}/\``,
` : \`\${kcContext["x-keycloakify"].resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/\`;`,
""
];
default:
assert<Equals<typeof bundler, never>>(false);
}
})(),
await (async () => {
const { dependencies, devDependencies } =
await readPackageJsonDependencies({
packageJsonFilePath: buildContext.packageJsonFilePath
});
const moduleNames = Object.keys({
...dependencies,
...devDependencies
});
const moduleName = (() => {
for (const moduleName_candidate of [
"@storybook/react-vite",
"@storybook/react-webpack5",
"@storybook/react"
]) {
if (moduleNames.includes(moduleName_candidate)) {
return moduleName_candidate;
}
}
return undefined;
})();
if (moduleName === undefined) {
return false as const;
}
return [
`// NOTE: This is only exported here because you're supposed to import type from different packages`,
`// Depending of if you are using Vite, Webpack, ect...`,
`export type { Meta, StoryObj } from "${moduleName}";`,
``
].join("\n");
})()
]
.filter(item => {
assert<Equals<typeof item, string | false>>;
return typeof item === "string";
})
.join("\n");
}
const hash = crypto.createHash("sha256").update(newContent).digest("hex");
skip_if_no_changes: {
if (!(await existsAsync(filePath))) {
break skip_if_no_changes;
}
const currentContent = (await fs.readFile(filePath)).toString("utf8");
if (!currentContent.includes(hash)) {
break skip_if_no_changes;
}
return;
}
newContent = [
`// This file is auto-generated by the \`update-kc-gen\` command. Do not edit it manually.`,
`// Hash: ${hash}`,
``,
newContent
].join("\n");
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");
if (!(await existsAsync(legacyFilePath))) {
break delete_legacy_file;
}
await fs.unlink(legacyFilePath);
}
}