2024-05-16 09:20:37 +02:00
|
|
|
import { readBuildOptions } from "./shared/buildOptions";
|
2024-05-18 11:40:09 +02:00
|
|
|
import type { CliCommandOptions as CliCommandOptions_common } from "./main";
|
2024-05-17 05:13:41 +02:00
|
|
|
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
|
|
|
|
import { readMetaInfKeycloakThemes } from "./shared/metaInfKeycloakThemes";
|
2024-05-20 02:27:40 +02:00
|
|
|
import { accountV1ThemeName, skipBuildJarsEnvName } from "./shared/constants";
|
2024-05-17 05:13:41 +02:00
|
|
|
import { SemVer } from "./tools/SemVer";
|
|
|
|
import type { KeycloakVersionRange } from "./shared/KeycloakVersionRange";
|
|
|
|
import { getJarFileBasename } from "./shared/getJarFileBasename";
|
|
|
|
import { assert, type Equals } from "tsafe/assert";
|
|
|
|
import * as fs from "fs";
|
2024-05-20 02:27:40 +02:00
|
|
|
import { join as pathJoin, relative as pathRelative, sep as pathSep, posix as pathPosix } from "path";
|
2024-05-17 05:13:41 +02:00
|
|
|
import * as child_process from "child_process";
|
2024-05-18 10:02:14 +02:00
|
|
|
import chalk from "chalk";
|
2024-05-20 02:27:40 +02:00
|
|
|
import chokidar from "chokidar";
|
|
|
|
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
|
|
|
|
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
|
|
|
|
import { Deferred } from "evt/tools/Deferred";
|
2024-05-16 09:20:37 +02:00
|
|
|
|
2024-05-18 11:40:09 +02:00
|
|
|
export type CliCommandOptions = CliCommandOptions_common & {
|
|
|
|
port: number;
|
|
|
|
keycloakVersion: string | undefined;
|
|
|
|
};
|
|
|
|
|
2024-05-16 09:20:37 +02:00
|
|
|
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
2024-05-18 11:40:09 +02:00
|
|
|
exit_if_docker_not_installed: {
|
2024-05-18 11:09:04 +02:00
|
|
|
let commandOutput: Buffer | undefined = undefined;
|
|
|
|
|
|
|
|
try {
|
2024-05-18 11:40:09 +02:00
|
|
|
commandOutput = child_process.execSync("docker --version", { "stdio": ["ignore", "pipe", "ignore"] });
|
2024-05-18 11:09:04 +02:00
|
|
|
} catch {}
|
|
|
|
|
2024-05-18 11:40:09 +02:00
|
|
|
if (commandOutput?.toString("utf8").includes("Docker")) {
|
|
|
|
break exit_if_docker_not_installed;
|
2024-05-18 11:09:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
[
|
|
|
|
`${chalk.red("Docker required.")}`,
|
|
|
|
`Install it with Docker Desktop: ${chalk.bold.underline("https://www.docker.com/products/docker-desktop/")}`,
|
|
|
|
`(or any other way)`
|
|
|
|
].join(" ")
|
|
|
|
);
|
|
|
|
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
|
2024-05-18 11:40:09 +02:00
|
|
|
exit_if_docker_not_running: {
|
2024-05-18 11:09:04 +02:00
|
|
|
let isDockerRunning: boolean;
|
|
|
|
|
|
|
|
try {
|
2024-05-18 11:40:09 +02:00
|
|
|
child_process.execSync("docker info", { "stdio": "ignore" });
|
2024-05-18 11:09:04 +02:00
|
|
|
isDockerRunning = true;
|
|
|
|
} catch {
|
|
|
|
isDockerRunning = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isDockerRunning) {
|
2024-05-18 11:40:09 +02:00
|
|
|
break exit_if_docker_not_running;
|
2024-05-18 11:09:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
console.log([`${chalk.red("Docker daemon is not running.")}`, `Please start Docker Desktop and try again.`].join(" "));
|
2024-05-18 11:40:09 +02:00
|
|
|
|
|
|
|
process.exit(1);
|
2024-05-18 11:09:04 +02:00
|
|
|
}
|
|
|
|
|
2024-05-16 09:20:37 +02:00
|
|
|
const { cliCommandOptions } = params;
|
|
|
|
|
|
|
|
const buildOptions = readBuildOptions({ cliCommandOptions });
|
|
|
|
|
2024-05-18 11:40:09 +02:00
|
|
|
exit_if_theme_not_built: {
|
|
|
|
if (fs.existsSync(buildOptions.keycloakifyBuildDirPath)) {
|
|
|
|
break exit_if_theme_not_built;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
[`${chalk.red("The theme has not been built.")}`, `Please run ${chalk.bold("npx vite && npx keycloakify build")} first.`].join(" ")
|
|
|
|
);
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
|
2024-05-17 05:13:41 +02:00
|
|
|
const metaInfKeycloakThemes = readMetaInfKeycloakThemes({
|
|
|
|
"keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath
|
|
|
|
});
|
|
|
|
|
|
|
|
const doesImplementAccountTheme = metaInfKeycloakThemes.themes.some(({ name }) => name === accountV1ThemeName);
|
|
|
|
|
|
|
|
const { keycloakVersion, keycloakMajorNumber } = await (async function getKeycloakMajor(): Promise<{
|
|
|
|
keycloakVersion: string;
|
|
|
|
keycloakMajorNumber: number;
|
|
|
|
}> {
|
2024-05-18 11:40:09 +02:00
|
|
|
if (cliCommandOptions.keycloakVersion !== undefined) {
|
|
|
|
return {
|
|
|
|
"keycloakVersion": cliCommandOptions.keycloakVersion,
|
|
|
|
"keycloakMajorNumber": SemVer.parse(cliCommandOptions.keycloakVersion).major
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-05-20 02:27:40 +02:00
|
|
|
console.log(chalk.cyan("On which version of Keycloak do you want to test your theme?"));
|
2024-05-18 11:40:09 +02:00
|
|
|
|
2024-05-17 05:13:41 +02:00
|
|
|
const { keycloakVersion } = await promptKeycloakVersion({
|
2024-05-18 08:11:20 +02:00
|
|
|
"startingFromMajor": 17,
|
|
|
|
"cacheDirPath": buildOptions.cacheDirPath
|
2024-05-17 05:13:41 +02:00
|
|
|
});
|
|
|
|
|
2024-05-20 02:27:40 +02:00
|
|
|
console.log(`→ ${keycloakVersion}`);
|
|
|
|
|
2024-05-17 05:13:41 +02:00
|
|
|
const keycloakMajorNumber = SemVer.parse(keycloakVersion).major;
|
|
|
|
|
|
|
|
if (doesImplementAccountTheme && keycloakMajorNumber === 22) {
|
2024-05-18 07:53:41 +02:00
|
|
|
console.log(
|
|
|
|
[
|
|
|
|
"Unfortunately, Keycloakify themes that implements an account theme do not work on Keycloak 22",
|
|
|
|
"Please select any other Keycloak version"
|
|
|
|
].join(" ")
|
|
|
|
);
|
2024-05-17 05:13:41 +02:00
|
|
|
return getKeycloakMajor();
|
|
|
|
}
|
|
|
|
|
|
|
|
return { keycloakVersion, keycloakMajorNumber };
|
|
|
|
})();
|
|
|
|
|
|
|
|
const keycloakVersionRange: KeycloakVersionRange = (() => {
|
|
|
|
if (doesImplementAccountTheme) {
|
|
|
|
const keycloakVersionRange = (() => {
|
|
|
|
if (keycloakMajorNumber <= 21) {
|
|
|
|
return "21-and-below" as const;
|
|
|
|
}
|
|
|
|
|
|
|
|
assert(keycloakMajorNumber !== 22);
|
|
|
|
|
|
|
|
if (keycloakMajorNumber === 23) {
|
|
|
|
return "23" as const;
|
|
|
|
}
|
|
|
|
|
|
|
|
return "24-and-above" as const;
|
|
|
|
})();
|
|
|
|
|
|
|
|
assert<Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithAccountTheme>>();
|
|
|
|
|
|
|
|
return keycloakVersionRange;
|
|
|
|
} else {
|
|
|
|
const keycloakVersionRange = (() => {
|
|
|
|
if (keycloakMajorNumber <= 21) {
|
|
|
|
return "21-and-below" as const;
|
|
|
|
}
|
|
|
|
|
|
|
|
return "22-and-above" as const;
|
|
|
|
})();
|
|
|
|
|
|
|
|
assert<Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithoutAccountTheme>>();
|
|
|
|
|
|
|
|
return keycloakVersionRange;
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
|
|
|
|
const { jarFileBasename } = getJarFileBasename({ keycloakVersionRange });
|
|
|
|
|
|
|
|
const mountTargets = buildOptions.themeNames
|
|
|
|
.map(themeName => {
|
|
|
|
const themeEntry = metaInfKeycloakThemes.themes.find(({ name }) => name === themeName);
|
|
|
|
|
|
|
|
assert(themeEntry !== undefined);
|
|
|
|
|
|
|
|
return themeEntry.types
|
|
|
|
.map(themeType => {
|
|
|
|
const localPathDirname = pathJoin(
|
|
|
|
buildOptions.keycloakifyBuildDirPath,
|
|
|
|
"src",
|
|
|
|
"main",
|
|
|
|
"resources",
|
|
|
|
"theme",
|
|
|
|
themeName,
|
|
|
|
themeType
|
|
|
|
);
|
|
|
|
|
|
|
|
return fs
|
|
|
|
.readdirSync(localPathDirname)
|
|
|
|
.filter(fileOrDirectoryBasename => !fileOrDirectoryBasename.endsWith(".properties"))
|
|
|
|
.map(fileOrDirectoryBasename => ({
|
|
|
|
"localPath": pathJoin(localPathDirname, fileOrDirectoryBasename),
|
|
|
|
"containerPath": pathPosix.join("/", "opt", "keycloak", "themes", themeName, themeType, fileOrDirectoryBasename)
|
|
|
|
}));
|
|
|
|
})
|
|
|
|
.flat();
|
|
|
|
})
|
|
|
|
.flat();
|
|
|
|
|
|
|
|
const containerName = "keycloak-keycloakify";
|
|
|
|
|
|
|
|
try {
|
2024-05-20 02:27:40 +02:00
|
|
|
child_process.execSync(`docker rm --force ${containerName}`, { "stdio": "ignore" });
|
2024-05-17 05:13:41 +02:00
|
|
|
} catch {}
|
|
|
|
|
2024-05-20 02:27:40 +02:00
|
|
|
const spawnParams = [
|
2024-05-17 05:13:41 +02:00
|
|
|
"docker",
|
|
|
|
[
|
|
|
|
"run",
|
2024-05-18 11:40:09 +02:00
|
|
|
...["-p", `${cliCommandOptions.port}:8080`],
|
2024-05-17 05:13:41 +02:00
|
|
|
...["--name", containerName],
|
|
|
|
...["-e", "KEYCLOAK_ADMIN=admin"],
|
|
|
|
...["-e", "KEYCLOAK_ADMIN_PASSWORD=admin"],
|
2024-05-18 07:53:06 +02:00
|
|
|
...["-v", `${pathJoin(buildOptions.keycloakifyBuildDirPath, jarFileBasename)}:/opt/keycloak/providers/keycloak-theme.jar`],
|
2024-05-17 05:13:41 +02:00
|
|
|
...(keycloakMajorNumber <= 20 ? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"] : []),
|
2024-05-18 07:53:06 +02:00
|
|
|
...mountTargets.map(({ localPath, containerPath }) => ["-v", `${localPath}:${containerPath}:rw`]).flat(),
|
|
|
|
`quay.io/keycloak/keycloak:${keycloakVersion}`,
|
2024-05-17 05:13:41 +02:00
|
|
|
"start-dev",
|
|
|
|
...(21 <= keycloakMajorNumber && keycloakMajorNumber < 24 ? ["--features=declarative-user-profile"] : [])
|
|
|
|
],
|
|
|
|
{
|
|
|
|
"cwd": buildOptions.keycloakifyBuildDirPath
|
|
|
|
}
|
2024-05-20 02:27:40 +02:00
|
|
|
] as const;
|
|
|
|
|
|
|
|
console.log(JSON.stringify(spawnParams, null, 2));
|
|
|
|
|
|
|
|
const child = child_process.spawn(...spawnParams);
|
2024-05-17 05:13:41 +02:00
|
|
|
|
2024-05-18 07:53:06 +02:00
|
|
|
child.stdout.on("data", data => process.stdout.write(data));
|
|
|
|
|
|
|
|
child.stderr.on("data", data => process.stderr.write(data));
|
2024-05-17 05:13:41 +02:00
|
|
|
|
2024-05-20 02:27:40 +02:00
|
|
|
child.on("exit", process.exit);
|
|
|
|
|
|
|
|
const { themeSrcDirPath } = getThemeSrcDirPath({ "reactAppRootDirPath": buildOptions.reactAppRootDirPath });
|
|
|
|
|
2024-05-18 10:02:14 +02:00
|
|
|
{
|
|
|
|
const handler = async (data: Buffer) => {
|
|
|
|
if (!data.toString("utf8").includes("Listening on: http://0.0.0.0:8080")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
child.stdout.off("data", handler);
|
|
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1_000));
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
[
|
|
|
|
"",
|
|
|
|
`${chalk.green("Your theme is accessible at:")}`,
|
|
|
|
`${chalk.green("➜")} ${chalk.cyan.bold("https://test.keycloakify.dev/")}`,
|
2024-05-20 02:27:40 +02:00
|
|
|
"",
|
|
|
|
`Keycloak Admin console: ${chalk.cyan.bold(`http://localhost:${cliCommandOptions.port}`)}`,
|
|
|
|
`- user: ${chalk.cyan.bold("admin")}`,
|
|
|
|
`- password: ${chalk.cyan.bold("admin")}`,
|
|
|
|
"",
|
|
|
|
`Watching for changes in ${chalk.bold(`.${pathSep}${pathRelative(process.cwd(), themeSrcDirPath)}`)} ...`
|
2024-05-18 10:02:14 +02:00
|
|
|
].join("\n")
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
child.stdout.on("data", handler);
|
|
|
|
}
|
|
|
|
|
2024-05-20 02:27:40 +02:00
|
|
|
{
|
|
|
|
const { waitForDebounce } = waitForDebounceFactory({ "delay": 400 });
|
|
|
|
|
|
|
|
chokidar.watch(themeSrcDirPath, { "ignoreInitial": true }).on("all", async (...eventArgs) => {
|
|
|
|
console.log({ eventArgs });
|
|
|
|
|
|
|
|
await waitForDebounce();
|
|
|
|
|
|
|
|
console.log(chalk.cyan("Detected changes in the theme. Rebuilding ..."));
|
|
|
|
|
|
|
|
const dViteBuildDone = new Deferred<void>();
|
|
|
|
|
|
|
|
{
|
|
|
|
const child = child_process.spawn("npx", ["vite"], {
|
|
|
|
"cwd": buildOptions.reactAppRootDirPath,
|
|
|
|
"env": process.env
|
|
|
|
});
|
|
|
|
|
|
|
|
child.stdout.on("data", data => process.stdout.write(data));
|
|
|
|
|
|
|
|
child.stderr.on("data", data => process.stderr.write(data));
|
|
|
|
|
|
|
|
child.on("exit", code => {
|
|
|
|
if (code === 0) {
|
|
|
|
dViteBuildDone.resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
await dViteBuildDone.pr;
|
|
|
|
|
|
|
|
{
|
|
|
|
const child = child_process.spawn("npx", ["keycloakify", "build"], {
|
|
|
|
"cwd": buildOptions.reactAppRootDirPath,
|
|
|
|
"env": {
|
|
|
|
...process.env,
|
|
|
|
[skipBuildJarsEnvName]: "true"
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
child.stdout.on("data", data => process.stdout.write(data));
|
|
|
|
|
|
|
|
child.stderr.on("data", data => process.stderr.write(data));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2024-05-16 09:20:37 +02:00
|
|
|
}
|