Merge pull request #683 from keycloakify/feat_custom_handler

Feat custom handler
This commit is contained in:
Joseph Garrone 2024-10-06 15:09:26 +02:00 committed by GitHub
commit 7d818f217a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 298 additions and 280 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "keycloakify", "name": "keycloakify",
"version": "11.2.10", "version": "11.3.0-rc.7",
"description": "Framework to create custom Keycloak UIs", "description": "Framework to create custom Keycloak UIs",
"repository": { "repository": {
"type": "git", "type": "git",
@ -38,12 +38,14 @@
"dist/", "dist/",
"!dist/tsconfig.tsbuildinfo", "!dist/tsconfig.tsbuildinfo",
"!dist/bin/", "!dist/bin/",
"dist/bin/**/*.d.ts",
"dist/bin/main.js", "dist/bin/main.js",
"dist/bin/*.index.js", "dist/bin/*.index.js",
"dist/bin/*.node", "dist/bin/*.node",
"dist/bin/shared/constants.js", "dist/bin/shared/constants.js",
"dist/bin/shared/*.d.ts", "dist/bin/shared/constants.js.map",
"dist/bin/shared/*.js.map", "dist/bin/shared/customHandler.js",
"dist/bin/shared/customHandler.js.map",
"!dist/vite-plugin/", "!dist/vite-plugin/",
"dist/vite-plugin/index.js", "dist/vite-plugin/index.js",
"dist/vite-plugin/index.d.ts", "dist/vite-plugin/index.d.ts",

View File

@ -13,16 +13,11 @@ import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase"; import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert"; import { assert, Equals } from "tsafe/assert";
import type { CliCommandOptions } from "./main"; import type { BuildContext } from "./shared/buildContext";
import { getBuildContext } from "./shared/buildContext";
import chalk from "chalk"; import chalk from "chalk";
export async function command(params: { cliCommandOptions: CliCommandOptions }) { export async function command(params: { buildContext: BuildContext }) {
const { cliCommandOptions } = params; const { buildContext } = params;
const buildContext = getBuildContext({
cliCommandOptions
});
console.log(chalk.cyan("Theme type:")); console.log(chalk.cyan("Theme type:"));

View File

@ -1,11 +1,14 @@
import { copyKeycloakResourcesToPublic } from "./shared/copyKeycloakResourcesToPublic"; import { copyKeycloakResourcesToPublic } from "./shared/copyKeycloakResourcesToPublic";
import { getBuildContext } from "./shared/buildContext"; import type { BuildContext } from "./shared/buildContext";
import type { CliCommandOptions } from "./main"; import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
export async function command(params: { cliCommandOptions: CliCommandOptions }) { export async function command(params: { buildContext: BuildContext }) {
const { cliCommandOptions } = params; const { buildContext } = params;
const buildContext = getBuildContext({ cliCommandOptions }); maybeDelegateCommandToCustomHandler({
commandName: "copy-keycloak-resources-to-public",
buildContext
});
copyKeycloakResourcesToPublic({ copyKeycloakResourcesToPublic({
buildContext buildContext

View File

@ -20,16 +20,11 @@ import {
} from "path"; } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase"; import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert"; import { assert, Equals } from "tsafe/assert";
import type { CliCommandOptions } from "./main"; import type { BuildContext } from "./shared/buildContext";
import { getBuildContext } from "./shared/buildContext";
import chalk from "chalk"; import chalk from "chalk";
export async function command(params: { cliCommandOptions: CliCommandOptions }) { export async function command(params: { buildContext: BuildContext }) {
const { cliCommandOptions } = params; const { buildContext } = params;
const buildContext = getBuildContext({
cliCommandOptions
});
console.log(chalk.cyan("Theme type:")); console.log(chalk.cyan("Theme type:"));

View File

@ -1,17 +1,20 @@
import { getBuildContext } from "../shared/buildContext"; import type { BuildContext } from "../shared/buildContext";
import type { CliCommandOptions } from "../main";
import cliSelect from "cli-select"; import cliSelect from "cli-select";
import child_process from "child_process"; import child_process from "child_process";
import chalk from "chalk"; import chalk from "chalk";
import { join as pathJoin, relative as pathRelative } from "path"; import { join as pathJoin, relative as pathRelative } from "path";
import * as fs from "fs"; import * as fs from "fs";
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig"; import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
import { generateKcGenTs } from "../shared/generateKcGenTs"; import { command as updateKcGenCommand } from "../update-kc-gen";
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
export async function command(params: { cliCommandOptions: CliCommandOptions }) { export async function command(params: { buildContext: BuildContext }) {
const { cliCommandOptions } = params; const { buildContext } = params;
const buildContext = getBuildContext({ cliCommandOptions }); maybeDelegateCommandToCustomHandler({
commandName: "initialize-account-theme",
buildContext
});
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account"); const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
@ -97,7 +100,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
updateAccountThemeImplementationInConfig({ buildContext, accountThemeType }); updateAccountThemeImplementationInConfig({ buildContext, accountThemeType });
await generateKcGenTs({ await updateKcGenCommand({
buildContext: { buildContext: {
...buildContext, ...buildContext,
implementedThemeTypes: { implementedThemeTypes: {

View File

@ -8,12 +8,14 @@ import { id } from "tsafe/id";
export type BuildContextLike = { export type BuildContextLike = {
bundler: BuildContext["bundler"]; bundler: BuildContext["bundler"];
projectDirPath: string;
packageJsonFilePath: string;
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export function updateAccountThemeImplementationInConfig(params: { export function updateAccountThemeImplementationInConfig(params: {
buildContext: BuildContext; buildContext: BuildContextLike;
accountThemeType: "Single-Page" | "Multi-Page"; accountThemeType: "Single-Page" | "Multi-Page";
}) { }) {
const { buildContext, accountThemeType } = params; const { buildContext, accountThemeType } = params;

View File

@ -1,15 +1,18 @@
import { join as pathJoin, relative as pathRelative } from "path"; import { join as pathJoin, relative as pathRelative } from "path";
import { transformCodebase } from "./tools/transformCodebase"; import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion"; import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
import { getBuildContext } from "./shared/buildContext"; import type { BuildContext } from "./shared/buildContext";
import * as fs from "fs"; import * as fs from "fs";
import type { CliCommandOptions } from "./main";
import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive"; import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
export async function command(params: { cliCommandOptions: CliCommandOptions }) { export async function command(params: { buildContext: BuildContext }) {
const { cliCommandOptions } = params; const { buildContext } = params;
const buildContext = getBuildContext({ cliCommandOptions }); maybeDelegateCommandToCustomHandler({
commandName: "initialize-email-theme",
buildContext
});
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email"); const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");

View File

@ -2,19 +2,16 @@ import { generateResources } from "./generateResources";
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path"; import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
import * as child_process from "child_process"; import * as child_process from "child_process";
import * as fs from "fs"; import * as fs from "fs";
import { getBuildContext } from "../shared/buildContext"; import type { BuildContext } from "../shared/buildContext";
import { VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES } from "../shared/constants"; import { VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES } from "../shared/constants";
import { buildJars } from "./buildJars"; import { buildJars } from "./buildJars";
import type { CliCommandOptions } from "../main";
import chalk from "chalk"; import chalk from "chalk";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion"; import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import * as os from "os"; import * as os from "os";
import { rmSync } from "../tools/fs.rmSync"; import { rmSync } from "../tools/fs.rmSync";
export async function command(params: { cliCommandOptions: CliCommandOptions }) { export async function command(params: { buildContext: BuildContext }) {
const { cliCommandOptions } = params; const { buildContext } = params;
const buildContext = getBuildContext({ cliCommandOptions });
exit_if_maven_not_installed: { exit_if_maven_not_installed: {
let commandOutput: Buffer | undefined = undefined; let commandOutput: Buffer | undefined = undefined;

View File

@ -4,8 +4,9 @@ import { termost } from "termost";
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion"; import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
import * as child_process from "child_process"; import * as child_process from "child_process";
import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx"; import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx";
import { getBuildContext } from "./shared/buildContext";
export type CliCommandOptions = { type CliCommandOptions = {
projectDirPath: string | undefined; projectDirPath: string | undefined;
}; };
@ -69,10 +70,10 @@ program
}) })
.task({ .task({
skip, skip,
handler: async cliCommandOptions => { handler: async ({ projectDirPath }) => {
const { command } = await import("./keycloakify"); const { command } = await import("./keycloakify");
await command({ cliCommandOptions }); await command({ buildContext: getBuildContext({ projectDirPath }) });
} }
}); });
@ -130,10 +131,13 @@ program
}) })
.task({ .task({
skip, skip,
handler: async cliCommandOptions => { handler: async ({ projectDirPath, keycloakVersion, port, realmJsonFilePath }) => {
const { command } = await import("./start-keycloak"); const { command } = await import("./start-keycloak");
await command({ cliCommandOptions }); await command({
buildContext: getBuildContext({ projectDirPath }),
cliCommandOptions: { keycloakVersion, port, realmJsonFilePath }
});
} }
}); });
@ -144,10 +148,10 @@ program
}) })
.task({ .task({
skip, skip,
handler: async cliCommandOptions => { handler: async ({ projectDirPath }) => {
const { command } = await import("./eject-page"); const { command } = await import("./eject-page");
await command({ cliCommandOptions }); await command({ buildContext: getBuildContext({ projectDirPath }) });
} }
}); });
@ -158,24 +162,24 @@ program
}) })
.task({ .task({
skip, skip,
handler: async cliCommandOptions => { handler: async ({ projectDirPath }) => {
const { command } = await import("./add-story"); const { command } = await import("./add-story");
await command({ cliCommandOptions }); await command({ buildContext: getBuildContext({ projectDirPath }) });
} }
}); });
program program
.command({ .command({
name: "initialize-email-theme", name: "initialize-login-theme",
description: "Initialize an email theme." description: "Initialize an email theme."
}) })
.task({ .task({
skip, skip,
handler: async cliCommandOptions => { handler: async ({ projectDirPath }) => {
const { command } = await import("./initialize-email-theme"); const { command } = await import("./initialize-email-theme");
await command({ cliCommandOptions }); await command({ buildContext: getBuildContext({ projectDirPath }) });
} }
}); });
@ -186,10 +190,10 @@ program
}) })
.task({ .task({
skip, skip,
handler: async cliCommandOptions => { handler: async ({ projectDirPath }) => {
const { command } = await import("./initialize-account-theme"); const { command } = await import("./initialize-account-theme");
await command({ cliCommandOptions }); await command({ buildContext: getBuildContext({ projectDirPath }) });
} }
}); });
@ -201,10 +205,10 @@ program
}) })
.task({ .task({
skip, skip,
handler: async cliCommandOptions => { handler: async ({ projectDirPath }) => {
const { command } = await import("./copy-keycloak-resources-to-public"); const { command } = await import("./copy-keycloak-resources-to-public");
await command({ cliCommandOptions }); await command({ buildContext: getBuildContext({ projectDirPath }) });
} }
}); });
@ -216,10 +220,10 @@ program
}) })
.task({ .task({
skip, skip,
handler: async cliCommandOptions => { handler: async ({ projectDirPath }) => {
const { command } = await import("./update-kc-gen"); const { command } = await import("./update-kc-gen");
await command({ cliCommandOptions }); await command({ buildContext: getBuildContext({ projectDirPath }) });
} }
}); });

View File

@ -7,7 +7,6 @@ import {
dirname as pathDirname dirname as pathDirname
} from "path"; } from "path";
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath"; import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
import type { CliCommandOptions } from "../main";
import { z } from "zod"; import { z } from "zod";
import * as fs from "fs"; import * as fs from "fs";
import { assert, type Equals } from "tsafe/assert"; import { assert, type Equals } from "tsafe/assert";
@ -24,7 +23,7 @@ import { objectEntries } from "tsafe/objectEntries";
import { type ThemeType } from "./constants"; import { type ThemeType } from "./constants";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
import chalk from "chalk"; import chalk from "chalk";
import { getProxyFetchOptions, type ProxyFetchOptions } from "../tools/fetchProxyOptions"; import { getProxyFetchOptions, type FetchOptionsLike } from "../tools/fetchProxyOptions";
import { is } from "tsafe/is"; import { is } from "tsafe/is";
export type BuildContext = { export type BuildContext = {
@ -43,7 +42,7 @@ export type BuildContext = {
* In this case the urlPathname will be "/my-app/" */ * In this case the urlPathname will be "/my-app/" */
urlPathname: string | undefined; urlPathname: string | undefined;
assetsDirPath: string; assetsDirPath: string;
fetchOptions: ProxyFetchOptions; fetchOptions: FetchOptionsLike;
kcContextExclusionsFtlCode: string | undefined; kcContextExclusionsFtlCode: string | undefined;
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string; themeSrcDirPath: string;
@ -129,14 +128,12 @@ export type ResolvedViteConfig = {
}; };
export function getBuildContext(params: { export function getBuildContext(params: {
cliCommandOptions: CliCommandOptions; projectDirPath: string | undefined;
}): BuildContext { }): BuildContext {
const { cliCommandOptions } = params;
const projectDirPath = const projectDirPath =
cliCommandOptions.projectDirPath !== undefined params.projectDirPath !== undefined
? getAbsoluteAndInOsFormatPath({ ? getAbsoluteAndInOsFormatPath({
pathIsh: cliCommandOptions.projectDirPath, pathIsh: params.projectDirPath,
cwd: process.cwd() cwd: process.cwd()
}) })
: process.cwd(); : process.cwd();

View File

@ -71,3 +71,8 @@ export type AccountThemePageId = (typeof ACCOUNT_THEME_PAGE_IDS)[number];
export const CONTAINER_NAME = "keycloak-keycloakify"; export const CONTAINER_NAME = "keycloak-keycloakify";
export const FALLBACK_LANGUAGE_TAG = "en"; export const FALLBACK_LANGUAGE_TAG = "en";
export const CUSTOM_HANDLER_ENV_NAMES = {
COMMAND_NAME: "KEYCLOAKIFY_COMMAND_NAME",
BUILD_CONTEXT: "KEYCLOAKIFY_BUILD_CONTEXT"
};

View File

@ -0,0 +1,41 @@
import { assert } from "tsafe/assert";
import type { BuildContext } from "./buildContext";
import { CUSTOM_HANDLER_ENV_NAMES } from "./constants";
export const BIN_NAME = "_keycloakify-custom-handler";
export const NOT_IMPLEMENTED_EXIT_CODE = 78;
export type CommandName =
| "update-kc-gen"
| "eject-page"
| "add-story"
| "initialize-account-theme"
| "initialize-email-theme"
| "copy-keycloak-resources-to-public";
export type ApiVersion = "v1";
export function readParams(params: { apiVersion: ApiVersion }) {
const { apiVersion } = params;
assert(apiVersion === "v1");
const commandName = (() => {
const envValue = process.env[CUSTOM_HANDLER_ENV_NAMES.COMMAND_NAME];
assert(envValue !== undefined);
return envValue as CommandName;
})();
const buildContext = (() => {
const envValue = process.env[CUSTOM_HANDLER_ENV_NAMES.BUILD_CONTEXT];
assert(envValue !== undefined);
return JSON.parse(envValue) as BuildContext;
})();
return { commandName, buildContext };
}

View File

@ -0,0 +1,46 @@
import { assert, type Equals } from "tsafe/assert";
import type { BuildContext } from "./buildContext";
import { CUSTOM_HANDLER_ENV_NAMES } from "./constants";
import {
NOT_IMPLEMENTED_EXIT_CODE,
type CommandName,
BIN_NAME,
ApiVersion
} from "./customHandler";
import * as child_process from "child_process";
import { dirname as pathDirname } from "path";
import * as fs from "fs";
assert<Equals<ApiVersion, "v1">>();
export function maybeDelegateCommandToCustomHandler(params: {
commandName: CommandName;
buildContext: BuildContext;
}) {
const { commandName, buildContext } = params;
if (!fs.readdirSync(pathDirname(process.argv[1])).includes(BIN_NAME)) {
return;
}
try {
child_process.execSync(`npx ${BIN_NAME}`, {
stdio: "inherit",
env: {
...process.env,
[CUSTOM_HANDLER_ENV_NAMES.COMMAND_NAME]: commandName,
[CUSTOM_HANDLER_ENV_NAMES.BUILD_CONTEXT]: JSON.stringify(buildContext)
}
});
} catch (error: any) {
const status = error.status;
if (status === NOT_IMPLEMENTED_EXIT_CODE) {
return;
}
process.exit(status);
}
process.exit(0);
}

View File

@ -1,175 +0,0 @@
import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import type { BuildContext } from "./buildContext";
import * as fs from "fs/promises";
import { join as pathJoin } from "path";
import { existsAsync } from "../tools/fs.existsAsync";
import { z } from "zod";
export type BuildContextLike = {
projectDirPath: string;
themeNames: string[];
environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string;
implementedThemeTypes: Pick<
BuildContext["implementedThemeTypes"],
"login" | "account"
>;
packageJsonFilePath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function generateKcGenTs(params: {
buildContext: BuildContextLike;
}): Promise<void> {
const { buildContext } = params;
const isReactProject: boolean = await (async () => {
const parsedPackageJson = await (async () => {
type ParsedPackageJson = {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
dependencies: z.record(z.string()).optional(),
devDependencies: z.record(z.string()).optional()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
return zParsedPackageJson.parse(
JSON.parse(
(await fs.readFile(buildContext.packageJsonFilePath)).toString("utf8")
)
);
})();
return (
{
...parsedPackageJson.dependencies,
...parsedPackageJson.devDependencies
}.react !== undefined
);
})();
const filePath = pathJoin(
buildContext.themeSrcDirPath,
`kc.gen.ts${isReactProject ? "x" : ""}`
);
const currentContent = (await existsAsync(filePath))
? await fs.readFile(filePath)
: undefined;
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
const newContent = Buffer.from(
[
`/* prettier-ignore-start */`,
``,
`/* eslint-disable */`,
``,
`// @ts-nocheck`,
``,
`// noinspection JSUnusedGlobalSymbols`,
``,
`// This file is auto-generated by Keycloakify`,
``,
isReactProject && `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
)};`,
``,
`export type KcContext =`,
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
` ;`,
``,
`declare global {`,
` interface Window {`,
` kcContext?: KcContext;`,
` }`,
`}`,
``,
...(!isReactProject
? []
: [
hasLoginTheme &&
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
hasAccountTheme &&
`export const KcAccountPage = lazy(() => import("./account/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} />;`,
` }`,
` })()}`,
` </Suspense>`,
` );`,
`}`
]),
``,
`/* prettier-ignore-end */`,
``
]
.filter(item => typeof item === "string")
.join("\n"),
"utf8"
);
if (currentContent !== undefined && currentContent.equals(newContent)) {
return;
}
await fs.writeFile(filePath, newContent);
delete_legacy_file: {
if (!isReactProject) {
break delete_legacy_file;
}
const legacyFilePath = filePath.replace(/tsx$/, "ts");
if (!(await existsAsync(legacyFilePath))) {
break delete_legacy_file;
}
await fs.unlink(legacyFilePath);
}
}

View File

@ -1,6 +1,5 @@
import { getBuildContext } from "../shared/buildContext"; import type { BuildContext } from "../shared/buildContext";
import { exclude } from "tsafe/exclude"; import { exclude } from "tsafe/exclude";
import type { CliCommandOptions as CliCommandOptions_common } from "../main";
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion"; import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
import { CONTAINER_NAME } from "../shared/constants"; import { CONTAINER_NAME } from "../shared/constants";
import { SemVer } from "../tools/SemVer"; import { SemVer } from "../tools/SemVer";
@ -29,13 +28,14 @@ import { existsAsync } from "../tools/fs.existsAsync";
import { rm } from "../tools/fs.rm"; import { rm } from "../tools/fs.rm";
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive"; import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
export type CliCommandOptions = CliCommandOptions_common & { export async function command(params: {
buildContext: BuildContext;
cliCommandOptions: {
port: number | undefined; port: number | undefined;
keycloakVersion: string | undefined; keycloakVersion: string | undefined;
realmJsonFilePath: string | undefined; realmJsonFilePath: string | undefined;
}; };
}) {
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
exit_if_docker_not_installed: { exit_if_docker_not_installed: {
let commandOutput: string | undefined = undefined; let commandOutput: string | undefined = undefined;
@ -88,9 +88,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
process.exit(1); process.exit(1);
} }
const { cliCommandOptions } = params; const { cliCommandOptions, buildContext } = params;
const buildContext = getBuildContext({ cliCommandOptions });
const { dockerImageTag } = await (async () => { const { dockerImageTag } = await (async () => {
if (cliCommandOptions.keycloakVersion !== undefined) { if (cliCommandOptions.keycloakVersion !== undefined) {

View File

@ -1,16 +1,18 @@
import { type FetchOptions } from "make-fetch-happen";
import * as child_process from "child_process"; import * as child_process from "child_process";
import * as fs from "fs"; import * as fs from "fs";
import { exclude } from "tsafe/exclude"; import { exclude } from "tsafe/exclude";
export type ProxyFetchOptions = Pick< export type FetchOptionsLike = {
FetchOptions, proxy: string | undefined;
"proxy" | "noProxy" | "strictSSL" | "cert" | "ca" noProxy: string | string[];
>; strictSSL: boolean;
cert: string | string[] | undefined;
ca: string[] | undefined;
};
export function getProxyFetchOptions(params: { export function getProxyFetchOptions(params: {
npmConfigGetCwd: string; npmConfigGetCwd: string;
}): ProxyFetchOptions { }): FetchOptionsLike {
const { npmConfigGetCwd } = params; const { npmConfigGetCwd } = params;
const cfg = (() => { const cfg = (() => {

View File

@ -1,13 +1,116 @@
import type { CliCommandOptions } from "./main"; import type { BuildContext } from "./shared/buildContext";
import { getBuildContext } from "./shared/buildContext"; import * as fs from "fs/promises";
import { generateKcGenTs } from "./shared/generateKcGenTs"; import { join as pathJoin } from "path";
import { existsAsync } from "./tools/fs.existsAsync";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
export async function command(params: { cliCommandOptions: CliCommandOptions }) { export async function command(params: { buildContext: BuildContext }) {
const { cliCommandOptions } = params; const { buildContext } = params;
const buildContext = getBuildContext({ maybeDelegateCommandToCustomHandler({
cliCommandOptions commandName: "update-kc-gen",
buildContext
}); });
await generateKcGenTs({ buildContext }); const filePath = pathJoin(buildContext.themeSrcDirPath, `kc.gen.tsx`);
const currentContent = (await existsAsync(filePath))
? await fs.readFile(filePath)
: undefined;
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
const newContent = Buffer.from(
[
`/* prettier-ignore-start */`,
``,
`/* eslint-disable */`,
``,
`// @ts-nocheck`,
``,
`// noinspection JSUnusedGlobalSymbols`,
``,
`// This file is auto-generated by Keycloakify`,
``,
`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
)};`,
``,
`export type KcContext =`,
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
` ;`,
``,
`declare global {`,
` interface Window {`,
` kcContext?: KcContext;`,
` }`,
`}`,
``,
hasLoginTheme &&
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
hasAccountTheme &&
`export const KcAccountPage = lazy(() => import("./account/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} />;`,
` }`,
` })()}`,
` </Suspense>`,
` );`,
`}`,
``,
`/* prettier-ignore-end */`,
``
]
.filter(item => typeof item === "string")
.join("\n"),
"utf8"
);
if (currentContent !== undefined && currentContent.equals(newContent)) {
return;
}
await fs.writeFile(filePath, newContent);
delete_legacy_file: {
const legacyFilePath = filePath.replace(/tsx$/, "ts");
if (!(await existsAsync(legacyFilePath))) {
break delete_legacy_file;
}
await fs.unlink(legacyFilePath);
}
} }

View File

@ -15,7 +15,7 @@ import {
type ResolvedViteConfig type ResolvedViteConfig
} from "../bin/shared/buildContext"; } from "../bin/shared/buildContext";
import MagicString from "magic-string"; import MagicString from "magic-string";
import { generateKcGenTs } from "../bin/shared/generateKcGenTs"; import { command as updateKcGenCommand } from "../bin/update-kc-gen";
export namespace keycloakify { export namespace keycloakify {
export type Params = BuildOptions & { export type Params = BuildOptions & {
@ -122,13 +122,12 @@ export function keycloakify(params: keycloakify.Params) {
} }
const buildContext = getBuildContext({ const buildContext = getBuildContext({
cliCommandOptions: {
projectDirPath projectDirPath
}
}); });
copyKeycloakResourcesToPublic({ buildContext }), copyKeycloakResourcesToPublic({ buildContext });
await generateKcGenTs({ buildContext });
await updateKcGenCommand({ buildContext });
}, },
transform: (code, id) => { transform: (code, id) => {
assert(command !== undefined); assert(command !== undefined);

View File

@ -115,7 +115,6 @@ export const WithFavoritePet: Story = {
) )
}; };
export const WithNewsletter: Story = { export const WithNewsletter: Story = {
render: () => ( render: () => (
<KcPageStory <KcPageStory
@ -132,7 +131,7 @@ export const WithNewsletter: Story = {
}, },
annotations: { annotations: {
inputOptionLabels: { inputOptionLabels: {
"yes": "I want my email inbox filled with spam" yes: "I want my email inbox filled with spam"
}, },
inputType: "multiselect-checkboxes" inputType: "multiselect-checkboxes"
}, },
@ -140,13 +139,12 @@ export const WithNewsletter: Story = {
readOnly: false readOnly: false
} satisfies Attribute } satisfies Attribute
} }
}, }
}} }}
/> />
) )
}; };
export const WithEmailAsUsername: Story = { export const WithEmailAsUsername: Story = {
render: () => ( render: () => (
<KcPageStory <KcPageStory