Implement custom handler cli hook

This commit is contained in:
Joseph Garrone 2024-10-05 20:30:09 +02:00
parent e3bd7f3bc5
commit 35b012b937
15 changed files with 300 additions and 189 deletions

View File

@ -42,6 +42,7 @@
"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/customHandler.js",
"dist/bin/shared/*.d.ts", "dist/bin/shared/*.d.ts",
"dist/bin/shared/*.js.map", "dist/bin/shared/*.js.map",
"!dist/vite-plugin/", "!dist/vite-plugin/",

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,8 @@
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";
export async function command(params: { cliCommandOptions: CliCommandOptions }) { export async function command(params: { buildContext: BuildContext }) {
const { cliCommandOptions } = params; const { buildContext } = params;
const buildContext = getBuildContext({ cliCommandOptions });
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,5 +1,4 @@
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";
@ -8,10 +7,8 @@ import * as fs from "fs";
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig"; import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
import { generateKcGenTs } from "../shared/generateKcGenTs"; import { generateKcGenTs } from "../shared/generateKcGenTs";
export async function command(params: { cliCommandOptions: CliCommandOptions }) { export async function command(params: { buildContext: BuildContext }) {
const { cliCommandOptions } = params; const { buildContext } = params;
const buildContext = getBuildContext({ cliCommandOptions });
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account"); const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");

View File

@ -1,15 +1,12 @@
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";
export async function command(params: { cliCommandOptions: CliCommandOptions }) { export async function command(params: { buildContext: BuildContext }) {
const { cliCommandOptions } = params; const { buildContext } = params;
const buildContext = getBuildContext({ cliCommandOptions });
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,10 @@ 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 { callHandlerIfAny } from "./shared/customHandler_caller";
import { getBuildContext } from "./shared/buildContext";
export type CliCommandOptions = { type CliCommandOptions = {
projectDirPath: string | undefined; projectDirPath: string | undefined;
}; };
@ -69,115 +71,154 @@ program
}) })
.task({ .task({
skip, skip,
handler: async cliCommandOptions => { handler: async ({ projectDirPath }) => {
const buildContext = getBuildContext({ projectDirPath });
const { command } = await import("./keycloakify"); const { command } = await import("./keycloakify");
await command({ cliCommandOptions }); await command({ buildContext });
} }
}); });
program {
.command<{ const commandName = "start-keycloak";
port: number | undefined;
keycloakVersion: string | undefined;
realmJsonFilePath: string | undefined;
}>({
name: "start-keycloak",
description:
"Spin up a pre configured Docker image of Keycloak to test your theme."
})
.option({
key: "port",
name: (() => {
const name = "port";
optionsKeys.push(name); program
.command<{
port: number | undefined;
keycloakVersion: string | undefined;
realmJsonFilePath: string | undefined;
}>({
name: commandName,
description:
"Spin up a pre configured Docker image of Keycloak to test your theme."
})
.option({
key: "port",
name: (() => {
const name = "port";
return name; optionsKeys.push(name);
})(),
description: ["Keycloak server port.", "Example `--port 8085`"].join(" "),
defaultValue: undefined
})
.option({
key: "keycloakVersion",
name: (() => {
const name = "keycloak-version";
optionsKeys.push(name); return name;
})(),
description: ["Keycloak server port.", "Example `--port 8085`"].join(" "),
defaultValue: undefined
})
.option({
key: "keycloakVersion",
name: (() => {
const name = "keycloak-version";
return name; optionsKeys.push(name);
})(),
description: [
"Use a specific version of Keycloak.",
"Example `--keycloak-version 21.1.1`"
].join(" "),
defaultValue: undefined
})
.option({
key: "realmJsonFilePath",
name: (() => {
const name = "import";
optionsKeys.push(name); return name;
})(),
description: [
"Use a specific version of Keycloak.",
"Example `--keycloak-version 21.1.1`"
].join(" "),
defaultValue: undefined
})
.option({
key: "realmJsonFilePath",
name: (() => {
const name = "import";
return name; optionsKeys.push(name);
})(),
defaultValue: undefined,
description: [
"Import your own realm configuration file",
"Example `--import path/to/myrealm-realm.json`"
].join(" ")
})
.task({
skip,
handler: async cliCommandOptions => {
const { command } = await import("./start-keycloak");
await command({ cliCommandOptions }); return name;
} })(),
}); defaultValue: undefined,
description: [
"Import your own realm configuration file",
"Example `--import path/to/myrealm-realm.json`"
].join(" ")
})
.task({
skip,
handler: async ({
projectDirPath,
keycloakVersion,
port,
realmJsonFilePath
}) => {
const buildContext = getBuildContext({ projectDirPath });
program const { command } = await import("./start-keycloak");
.command({
name: "eject-page",
description: "Eject a Keycloak page."
})
.task({
skip,
handler: async cliCommandOptions => {
const { command } = await import("./eject-page");
await command({ cliCommandOptions }); await command({
} buildContext,
}); cliCommandOptions: { keycloakVersion, port, realmJsonFilePath }
});
}
});
}
program {
.command({ const commandName = "eject-page";
name: "add-story",
description: "Add *.stories.tsx file for a specific page to in your Storybook."
})
.task({
skip,
handler: async cliCommandOptions => {
const { command } = await import("./add-story");
await command({ cliCommandOptions }); program
} .command({
}); name: commandName,
description: "Eject a Keycloak page."
})
.task({
skip,
handler: async ({ projectDirPath }) => {
const buildContext = getBuildContext({ projectDirPath });
program callHandlerIfAny({ buildContext, commandName });
.command({
name: "initialize-email-theme",
description: "Initialize an email theme."
})
.task({
skip,
handler: async cliCommandOptions => {
const { command } = await import("./initialize-email-theme");
await command({ cliCommandOptions }); const { command } = await import("./eject-page");
}
}); await command({ buildContext });
}
});
}
{
const commandName = "add-story";
program
.command({
name: commandName,
description:
"Add *.stories.tsx file for a specific page to in your Storybook."
})
.task({
skip,
handler: async ({ projectDirPath }) => {
const buildContext = getBuildContext({ projectDirPath });
callHandlerIfAny({ buildContext, commandName });
const { command } = await import("./add-story");
await command({ buildContext });
}
});
}
{
const comandName = "initialize-login-theme";
program
.command({
name: comandName,
description: "Initialize an email theme."
})
.task({
skip,
handler: async ({ projectDirPath }) => {
const buildContext = getBuildContext({ projectDirPath });
const { command } = await import("./initialize-email-theme");
await command({ buildContext });
}
});
}
program program
.command({ .command({
@ -186,42 +227,58 @@ program
}) })
.task({ .task({
skip, skip,
handler: async cliCommandOptions => { handler: async ({ projectDirPath }) => {
const buildContext = getBuildContext({ projectDirPath });
const { command } = await import("./initialize-account-theme"); const { command } = await import("./initialize-account-theme");
await command({ cliCommandOptions }); await command({ buildContext });
} }
}); });
program {
.command({ const commandName = "copy-keycloak-resources-to-public";
name: "copy-keycloak-resources-to-public",
description:
"(Webpack/Create-React-App only) Copy Keycloak default theme resources to the public directory."
})
.task({
skip,
handler: async cliCommandOptions => {
const { command } = await import("./copy-keycloak-resources-to-public");
await command({ cliCommandOptions }); program
} .command({
}); name: commandName,
description:
"(Webpack/Create-React-App only) Copy Keycloak default theme resources to the public directory."
})
.task({
skip,
handler: async ({ projectDirPath }) => {
const buildContext = getBuildContext({ projectDirPath });
program const { command } = await import("./copy-keycloak-resources-to-public");
.command({
name: "update-kc-gen",
description:
"(Webpack/Create-React-App only) Create/update the kc.gen.ts file in your project."
})
.task({
skip,
handler: async cliCommandOptions => {
const { command } = await import("./update-kc-gen");
await command({ cliCommandOptions }); await command({ buildContext });
} }
}); });
}
{
const commandName = "update-kc-gen";
program
.command({
name: commandName,
description:
"(Webpack/Create-React-App only) Create/update the kc.gen.ts file in your project."
})
.task({
skip,
handler: async ({ projectDirPath }) => {
const buildContext = getBuildContext({ projectDirPath });
callHandlerIfAny({ buildContext, commandName });
const { command } = await import("./update-kc-gen");
await command({ buildContext });
}
});
}
// Fallback to build command if no command is provided // Fallback to build command if no command is provided
{ {

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";
@ -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,35 @@
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";
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,47 @@
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 { is } from "tsafe/is";
import { dirname as pathDirname } from "path";
import * as fs from "fs";
assert<Equals<ApiVersion, "v1">>();
export function callHandlerIfAny(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) {
assert(is<child_process.ExecException>(error));
if (error.code === NOT_IMPLEMENTED_EXIT_CODE) {
return;
}
process.exit(error.code);
}
process.exit(0);
}

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: {
port: number | undefined; buildContext: BuildContext;
keycloakVersion: string | undefined; cliCommandOptions: {
realmJsonFilePath: string | undefined; port: number | undefined;
}; keycloakVersion: 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,13 +1,8 @@
import type { CliCommandOptions } from "./main"; import type { BuildContext } from "./shared/buildContext";
import { getBuildContext } from "./shared/buildContext";
import { generateKcGenTs } from "./shared/generateKcGenTs"; import { generateKcGenTs } from "./shared/generateKcGenTs";
export async function command(params: { cliCommandOptions: CliCommandOptions }) { export async function command(params: { buildContext: BuildContext }) {
const { cliCommandOptions } = params; const { buildContext } = params;
const buildContext = getBuildContext({
cliCommandOptions
});
await generateKcGenTs({ buildContext }); await generateKcGenTs({ buildContext });
} }

View File

@ -122,9 +122,7 @@ export function keycloakify(params: keycloakify.Params) {
} }
const buildContext = getBuildContext({ const buildContext = getBuildContext({
cliCommandOptions: { projectDirPath
projectDirPath
}
}); });
copyKeycloakResourcesToPublic({ buildContext }), copyKeycloakResourcesToPublic({ buildContext }),