2024-10-05 20:30:09 +02:00
|
|
|
import type { BuildContext } from "../shared/buildContext";
|
2024-06-09 08:50:59 +02:00
|
|
|
import { exclude } from "tsafe/exclude";
|
2024-12-13 12:07:21 +01:00
|
|
|
import {
|
|
|
|
CONTAINER_NAME,
|
|
|
|
KEYCLOAKIFY_SPA_DEV_SERVER_PORT,
|
2024-12-15 11:38:50 +01:00
|
|
|
KEYCLOAKIFY_LOGIN_JAR_BASENAME,
|
|
|
|
TEST_APP_URL
|
2024-12-13 12:07:21 +01:00
|
|
|
} from "../shared/constants";
|
2024-05-20 15:34:07 +02:00
|
|
|
import { SemVer } from "../tools/SemVer";
|
2024-08-17 23:20:52 +02:00
|
|
|
import { assert, type Equals } from "tsafe/assert";
|
2024-05-17 05:13:41 +02:00
|
|
|
import * as fs from "fs";
|
2024-06-12 10:50:00 +02:00
|
|
|
import {
|
|
|
|
join as pathJoin,
|
|
|
|
relative as pathRelative,
|
|
|
|
sep as pathSep,
|
2024-12-14 14:36:11 +01:00
|
|
|
basename as pathBasename
|
2024-06-12 10:50:00 +02:00
|
|
|
} 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";
|
2024-05-20 15:34:07 +02:00
|
|
|
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
|
|
|
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
|
|
|
import cliSelect from "cli-select";
|
2024-05-20 19:30:04 +02:00
|
|
|
import * as runExclusive from "run-exclusive";
|
2024-05-25 22:36:03 +02:00
|
|
|
import { extractArchive } from "../tools/extractArchive";
|
2024-05-26 19:29:12 +02:00
|
|
|
import { appBuild } from "./appBuild";
|
|
|
|
import { keycloakifyBuild } from "./keycloakifyBuild";
|
2024-06-10 07:57:12 +02:00
|
|
|
import { isInside } from "../tools/isInside";
|
|
|
|
import { existsAsync } from "../tools/fs.existsAsync";
|
|
|
|
import { rm } from "../tools/fs.rm";
|
2024-08-17 23:20:52 +02:00
|
|
|
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
|
2024-11-30 16:58:48 +01:00
|
|
|
import { startViteDevServer } from "./startViteDevServer";
|
2024-12-14 14:36:11 +01:00
|
|
|
import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig";
|
2024-12-15 08:53:54 +01:00
|
|
|
import { getSupportedDockerImageTags } from "./getSupportedDockerImageTags";
|
2024-12-14 14:36:11 +01:00
|
|
|
import { getRealmConfig } from "./realmConfig";
|
2024-05-16 09:20:37 +02:00
|
|
|
|
2024-10-05 20:30:09 +02:00
|
|
|
export async function command(params: {
|
|
|
|
buildContext: BuildContext;
|
|
|
|
cliCommandOptions: {
|
|
|
|
port: number | undefined;
|
|
|
|
keycloakVersion: string | undefined;
|
|
|
|
realmJsonFilePath: string | undefined;
|
|
|
|
};
|
|
|
|
}) {
|
2024-05-18 11:40:09 +02:00
|
|
|
exit_if_docker_not_installed: {
|
2024-09-11 11:07:06 +02:00
|
|
|
let commandOutput: string | undefined = undefined;
|
2024-05-18 11:09:04 +02:00
|
|
|
|
|
|
|
try {
|
2024-09-11 11:07:06 +02:00
|
|
|
commandOutput = child_process
|
|
|
|
.execSync("docker --version", {
|
|
|
|
stdio: ["ignore", "pipe", "ignore"]
|
|
|
|
})
|
|
|
|
?.toString("utf8");
|
2024-05-18 11:09:04 +02:00
|
|
|
} catch {}
|
|
|
|
|
2024-09-11 11:07:06 +02:00
|
|
|
if (commandOutput?.includes("Docker") || commandOutput?.includes("podman")) {
|
2024-05-18 11:40:09 +02:00
|
|
|
break exit_if_docker_not_installed;
|
2024-05-18 11:09:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
[
|
|
|
|
`${chalk.red("Docker required.")}`,
|
2024-05-20 15:48:51 +02:00
|
|
|
`Install it with Docker Desktop: ${chalk.bold.underline(
|
|
|
|
"https://www.docker.com/products/docker-desktop/"
|
|
|
|
)}`,
|
2024-05-18 11:09:04 +02:00
|
|
|
`(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-20 15:48:51 +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
|
|
|
}
|
|
|
|
|
2024-05-20 15:48:51 +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-10-05 20:30:09 +02:00
|
|
|
const { cliCommandOptions, buildContext } = params;
|
2024-05-16 09:20:37 +02:00
|
|
|
|
2024-12-15 08:53:54 +01:00
|
|
|
const availableTags = await getSupportedDockerImageTags({
|
2024-12-14 14:36:11 +01:00
|
|
|
buildContext
|
|
|
|
});
|
|
|
|
|
2024-08-17 23:20:52 +02:00
|
|
|
const { dockerImageTag } = await (async () => {
|
2024-06-16 01:29:15 +02:00
|
|
|
if (cliCommandOptions.keycloakVersion !== undefined) {
|
2024-12-14 14:36:11 +01:00
|
|
|
const cliCommandOptions_keycloakVersion = cliCommandOptions.keycloakVersion;
|
|
|
|
|
|
|
|
const tag = availableTags.find(tag =>
|
|
|
|
tag.startsWith(cliCommandOptions_keycloakVersion)
|
|
|
|
);
|
|
|
|
|
|
|
|
if (tag === undefined) {
|
|
|
|
console.log(
|
|
|
|
chalk.red(
|
|
|
|
[
|
|
|
|
`We could not find a Keycloak Docker image for ${cliCommandOptions_keycloakVersion}`,
|
|
|
|
`Example of valid values: --keycloak-version 26, --keycloak-version 26.0.7`
|
|
|
|
].join("\n")
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
return { dockerImageTag: tag };
|
2024-08-17 23:20:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (buildContext.startKeycloakOptions.dockerImage !== undefined) {
|
2024-06-16 01:29:15 +02:00
|
|
|
return {
|
2024-08-17 23:20:52 +02:00
|
|
|
dockerImageTag: buildContext.startKeycloakOptions.dockerImage.tag
|
2024-06-16 01:29:15 +02:00
|
|
|
};
|
|
|
|
}
|
2024-05-17 05:13:41 +02:00
|
|
|
|
2024-06-16 01:29:15 +02:00
|
|
|
console.log(
|
2024-08-13 00:12:08 +02:00
|
|
|
[
|
|
|
|
chalk.cyan(
|
|
|
|
"On which version of Keycloak do you want to test your theme?"
|
|
|
|
),
|
|
|
|
chalk.gray(
|
2024-12-14 14:36:11 +01:00
|
|
|
"You can also explicitly provide the version with `npx keycloakify start-keycloak --keycloak-version 26` (or any other version)"
|
2024-08-13 00:12:08 +02:00
|
|
|
)
|
|
|
|
].join("\n")
|
2024-06-16 01:29:15 +02:00
|
|
|
);
|
2024-05-17 05:13:41 +02:00
|
|
|
|
2024-12-14 14:36:11 +01:00
|
|
|
const { value: tag } = await cliSelect<string>({
|
|
|
|
values: availableTags
|
|
|
|
}).catch(() => {
|
|
|
|
process.exit(-1);
|
2024-06-16 01:29:15 +02:00
|
|
|
});
|
2024-05-17 05:13:41 +02:00
|
|
|
|
2024-12-14 14:36:11 +01:00
|
|
|
console.log(`→ ${tag}`);
|
2024-05-17 05:13:41 +02:00
|
|
|
|
2024-12-14 14:36:11 +01:00
|
|
|
return { dockerImageTag: tag };
|
2024-05-17 05:13:41 +02:00
|
|
|
})();
|
|
|
|
|
2024-08-17 23:20:52 +02:00
|
|
|
const keycloakMajorVersionNumber = (() => {
|
2024-12-14 14:36:11 +01:00
|
|
|
const [wrap] = getSupportedKeycloakMajorVersions()
|
2024-08-17 23:20:52 +02:00
|
|
|
.map(majorVersionNumber => ({
|
|
|
|
majorVersionNumber,
|
2024-12-14 14:36:11 +01:00
|
|
|
index: dockerImageTag.indexOf(`${majorVersionNumber}`)
|
2024-08-17 23:20:52 +02:00
|
|
|
}))
|
|
|
|
.filter(({ index }) => index !== -1)
|
|
|
|
.sort((a, b) => a.index - b.index);
|
|
|
|
|
|
|
|
if (wrap === undefined) {
|
2024-12-14 14:36:11 +01:00
|
|
|
try {
|
|
|
|
const version = SemVer.parse(dockerImageTag);
|
|
|
|
|
|
|
|
console.error(
|
|
|
|
chalk.yellow(
|
|
|
|
`Keycloak version ${version.major} is not supported, supported versions are ${getSupportedKeycloakMajorVersions().join(", ")}`
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
process.exit(1);
|
|
|
|
} catch {
|
2024-12-14 14:44:30 +01:00
|
|
|
// NOTE: Latest version
|
|
|
|
const [n] = getSupportedKeycloakMajorVersions();
|
|
|
|
|
2024-12-14 14:36:11 +01:00
|
|
|
console.warn(
|
|
|
|
chalk.yellow(
|
2024-12-14 14:44:30 +01:00
|
|
|
`Could not determine the major Keycloak version number from the docker image tag ${dockerImageTag}. Assuming ${n}`
|
2024-12-14 14:36:11 +01:00
|
|
|
)
|
|
|
|
);
|
2024-12-14 14:44:30 +01:00
|
|
|
return n;
|
2024-12-14 14:36:11 +01:00
|
|
|
}
|
2024-08-17 23:20:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return wrap.majorVersionNumber;
|
|
|
|
})();
|
2024-05-17 05:13:41 +02:00
|
|
|
|
2024-12-14 14:36:11 +01:00
|
|
|
const { clientName, onRealmConfigChange, realmJsonFilePath, realmName, username } =
|
|
|
|
await getRealmConfig({
|
|
|
|
keycloakMajorVersionNumber,
|
|
|
|
realmJsonFilePath_userProvided: await (async () => {
|
|
|
|
if (cliCommandOptions.realmJsonFilePath !== undefined) {
|
|
|
|
return getAbsoluteAndInOsFormatPath({
|
|
|
|
pathIsh: cliCommandOptions.realmJsonFilePath,
|
|
|
|
cwd: process.cwd()
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (buildContext.startKeycloakOptions.realmJsonFilePath !== undefined) {
|
|
|
|
assert(
|
|
|
|
await existsAsync(
|
|
|
|
buildContext.startKeycloakOptions.realmJsonFilePath
|
|
|
|
),
|
|
|
|
`${pathRelative(process.cwd(), buildContext.startKeycloakOptions.realmJsonFilePath)} does not exist`
|
|
|
|
);
|
|
|
|
return buildContext.startKeycloakOptions.realmJsonFilePath;
|
|
|
|
}
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
})(),
|
|
|
|
buildContext
|
|
|
|
});
|
|
|
|
|
2024-06-15 14:30:18 +02:00
|
|
|
{
|
|
|
|
const { isAppBuildSuccess } = await appBuild({
|
|
|
|
buildContext
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!isAppBuildSuccess) {
|
|
|
|
console.log(
|
|
|
|
chalk.red(
|
2024-06-23 21:23:06 +02:00
|
|
|
`App build failed, exiting. Try building your app (e.g 'npm run build') and see what's wrong.`
|
2024-06-15 14:30:18 +02:00
|
|
|
)
|
|
|
|
);
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
|
2024-06-16 01:29:15 +02:00
|
|
|
buildForKeycloakMajorVersionNumber: keycloakMajorVersionNumber,
|
2024-06-15 14:30:18 +02:00
|
|
|
buildContext
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!isKeycloakifyBuildSuccess) {
|
|
|
|
console.log(
|
|
|
|
chalk.red(
|
2024-06-16 01:29:15 +02:00
|
|
|
`Keycloakify build failed, exiting. Try running 'npx keycloakify build' and see what's wrong.`
|
2024-06-15 14:30:18 +02:00
|
|
|
)
|
|
|
|
);
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-16 01:29:15 +02:00
|
|
|
const jarFilePath = fs
|
|
|
|
.readdirSync(buildContext.keycloakifyBuildDirPath)
|
|
|
|
.filter(fileBasename => fileBasename.endsWith(".jar"))
|
|
|
|
.map(fileBasename => pathJoin(buildContext.keycloakifyBuildDirPath, fileBasename))
|
|
|
|
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0];
|
|
|
|
|
|
|
|
assert(jarFilePath !== undefined);
|
|
|
|
|
2024-12-14 14:36:11 +01:00
|
|
|
const extensionJarFilePaths = [
|
|
|
|
pathJoin(
|
|
|
|
getThisCodebaseRootDirPath(),
|
|
|
|
"src",
|
|
|
|
"bin",
|
|
|
|
"start-keycloak",
|
|
|
|
KEYCLOAKIFY_LOGIN_JAR_BASENAME
|
|
|
|
),
|
|
|
|
...(await Promise.all(
|
|
|
|
buildContext.startKeycloakOptions.extensionJars.map(async extensionJar => {
|
|
|
|
switch (extensionJar.type) {
|
|
|
|
case "path": {
|
|
|
|
assert(
|
|
|
|
await existsAsync(extensionJar.path),
|
|
|
|
`${extensionJar.path} does not exist`
|
|
|
|
);
|
|
|
|
return extensionJar.path;
|
|
|
|
}
|
|
|
|
case "url": {
|
|
|
|
const { archiveFilePath } = await downloadAndExtractArchive({
|
|
|
|
cacheDirPath: buildContext.cacheDirPath,
|
|
|
|
fetchOptions: buildContext.fetchOptions,
|
|
|
|
url: extensionJar.url,
|
|
|
|
uniqueIdOfOnArchiveFile: "no extraction",
|
|
|
|
onArchiveFile: async () => {}
|
|
|
|
});
|
|
|
|
return archiveFilePath;
|
|
|
|
}
|
2024-08-17 23:20:52 +02:00
|
|
|
}
|
2024-12-14 14:36:11 +01:00
|
|
|
assert<Equals<typeof extensionJar, never>>(false);
|
|
|
|
})
|
|
|
|
))
|
|
|
|
];
|
2024-08-25 19:02:00 +02:00
|
|
|
|
2024-06-10 07:57:12 +02:00
|
|
|
async function extractThemeResourcesFromJar() {
|
2024-05-26 19:29:12 +02:00
|
|
|
await extractArchive({
|
|
|
|
archiveFilePath: jarFilePath,
|
2024-06-10 07:57:12 +02:00
|
|
|
onArchiveFile: async ({ relativeFilePathInArchive, writeFile }) => {
|
|
|
|
if (isInside({ dirPath: "theme", filePath: relativeFilePathInArchive })) {
|
|
|
|
await writeFile({
|
|
|
|
filePath: pathJoin(
|
|
|
|
buildContext.keycloakifyBuildDirPath,
|
|
|
|
relativeFilePathInArchive
|
|
|
|
)
|
|
|
|
});
|
2024-05-25 22:36:03 +02:00
|
|
|
}
|
|
|
|
}
|
2024-05-26 19:29:12 +02:00
|
|
|
});
|
2024-06-10 07:57:12 +02:00
|
|
|
}
|
2024-05-26 19:29:12 +02:00
|
|
|
|
2024-06-10 07:57:12 +02:00
|
|
|
{
|
|
|
|
const destDirPath = pathJoin(buildContext.keycloakifyBuildDirPath, "theme");
|
|
|
|
if (await existsAsync(destDirPath)) {
|
|
|
|
await rm(destDirPath, { recursive: true });
|
|
|
|
}
|
|
|
|
}
|
2024-05-26 19:29:12 +02:00
|
|
|
|
2024-06-10 07:57:12 +02:00
|
|
|
await extractThemeResourcesFromJar();
|
2024-05-25 22:36:03 +02:00
|
|
|
|
2024-06-16 01:29:15 +02:00
|
|
|
const jarFilePath_cacheDir = pathJoin(
|
|
|
|
buildContext.cacheDirPath,
|
|
|
|
pathBasename(jarFilePath)
|
|
|
|
);
|
2024-06-11 17:19:36 +02:00
|
|
|
|
2024-06-16 02:17:55 +02:00
|
|
|
fs.copyFileSync(jarFilePath, jarFilePath_cacheDir);
|
2024-06-11 17:19:36 +02:00
|
|
|
|
2024-05-26 19:40:13 +02:00
|
|
|
try {
|
2024-07-13 19:33:59 +02:00
|
|
|
child_process.execSync(`docker rm --force ${CONTAINER_NAME}`, {
|
2024-05-26 19:40:13 +02:00
|
|
|
stdio: "ignore"
|
|
|
|
});
|
|
|
|
} catch {}
|
|
|
|
|
2024-12-14 14:36:11 +01:00
|
|
|
const port = cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? 8080;
|
2024-08-19 00:00:08 +02:00
|
|
|
|
2024-12-02 00:41:12 +01:00
|
|
|
const doStartDevServer = (() => {
|
2024-11-30 16:58:48 +01:00
|
|
|
const hasSpaUi =
|
|
|
|
buildContext.implementedThemeTypes.admin.isImplemented ||
|
|
|
|
(buildContext.implementedThemeTypes.account.isImplemented &&
|
|
|
|
buildContext.implementedThemeTypes.account.type === "Single-Page");
|
|
|
|
|
|
|
|
if (!hasSpaUi) {
|
2024-12-02 00:41:12 +01:00
|
|
|
return false;
|
2024-11-30 16:58:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (buildContext.bundler !== "vite") {
|
|
|
|
console.log(
|
|
|
|
chalk.yellow(
|
|
|
|
[
|
|
|
|
`WARNING: Since you are using ${buildContext.bundler} instead of Vite,`,
|
|
|
|
`you'll have to wait serval seconds for the changes you made on your account or admin theme to be reflected in the browser.\n`,
|
|
|
|
`For a better development experience, consider migrating to Vite.`
|
|
|
|
].join(" ")
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
2024-12-02 00:41:12 +01:00
|
|
|
return false;
|
2024-11-30 16:58:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (keycloakMajorVersionNumber < 25) {
|
|
|
|
console.log(
|
|
|
|
chalk.yellow(
|
|
|
|
[
|
|
|
|
`WARNING: Your account or admin theme can't be tested with hot module replacement on Keycloak ${keycloakMajorVersionNumber}.`,
|
|
|
|
`This mean that you'll have to wait serval seconds for the changes to be reflected in the browser.`,
|
|
|
|
`For a better development experience, select a more recent version of Keycloak.`
|
|
|
|
].join("\n")
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
2024-12-02 00:41:12 +01:00
|
|
|
return false;
|
2024-11-30 16:58:48 +01:00
|
|
|
}
|
|
|
|
|
2024-12-02 00:41:12 +01:00
|
|
|
return true;
|
2024-11-30 16:58:48 +01:00
|
|
|
})();
|
|
|
|
|
2024-12-02 00:41:12 +01:00
|
|
|
let devServerPort: number | undefined = undefined;
|
|
|
|
|
|
|
|
if (doStartDevServer) {
|
|
|
|
const { port } = await startViteDevServer({ buildContext });
|
|
|
|
|
|
|
|
devServerPort = port;
|
2024-11-30 16:58:48 +01:00
|
|
|
}
|
|
|
|
|
2024-08-18 20:56:25 +02:00
|
|
|
const SPACE_PLACEHOLDER = "SPACE_PLACEHOLDER_xKLmdPd";
|
|
|
|
|
2024-08-17 23:20:52 +02:00
|
|
|
const dockerRunArgs: string[] = [
|
2024-08-19 00:00:08 +02:00
|
|
|
`-p${SPACE_PLACEHOLDER}${port}:8080`,
|
2024-08-18 20:56:25 +02:00
|
|
|
`--name${SPACE_PLACEHOLDER}${CONTAINER_NAME}`,
|
2024-12-15 18:15:36 +01:00
|
|
|
...(keycloakMajorVersionNumber >= 26
|
2024-12-15 11:57:45 +01:00
|
|
|
? [
|
|
|
|
`-e${SPACE_PLACEHOLDER}KC_BOOTSTRAP_ADMIN_USERNAME=admin`,
|
|
|
|
`-e${SPACE_PLACEHOLDER}KC_BOOTSTRAP_ADMIN_PASSWORD=admin`
|
|
|
|
]
|
|
|
|
: [
|
|
|
|
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN=admin`,
|
|
|
|
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN_PASSWORD=admin`
|
|
|
|
]),
|
2024-11-30 16:58:48 +01:00
|
|
|
...(devServerPort === undefined
|
|
|
|
? []
|
|
|
|
: [
|
|
|
|
`-e${SPACE_PLACEHOLDER}${KEYCLOAKIFY_SPA_DEV_SERVER_PORT}=${devServerPort}`
|
|
|
|
]),
|
2024-08-19 02:10:59 +02:00
|
|
|
...(buildContext.startKeycloakOptions.dockerExtraArgs.length === 0
|
|
|
|
? []
|
|
|
|
: [
|
|
|
|
buildContext.startKeycloakOptions.dockerExtraArgs.join(
|
|
|
|
SPACE_PLACEHOLDER
|
|
|
|
)
|
|
|
|
]),
|
2024-08-17 23:20:52 +02:00
|
|
|
...(realmJsonFilePath === undefined
|
|
|
|
? []
|
|
|
|
: [
|
2024-12-14 14:36:11 +01:00
|
|
|
`-v${SPACE_PLACEHOLDER}"${realmJsonFilePath}":/opt/keycloak/data/import/${realmName}-realm.json`
|
2024-08-17 23:20:52 +02:00
|
|
|
]),
|
2024-10-19 02:28:11 +02:00
|
|
|
`-v${SPACE_PLACEHOLDER}"${jarFilePath_cacheDir}":/opt/keycloak/providers/keycloak-theme.jar`,
|
2024-08-17 23:20:52 +02:00
|
|
|
...extensionJarFilePaths.map(
|
|
|
|
jarFilePath =>
|
2024-10-19 02:28:11 +02:00
|
|
|
`-v${SPACE_PLACEHOLDER}"${jarFilePath}":/opt/keycloak/providers/${pathBasename(jarFilePath)}`
|
2024-08-17 23:20:52 +02:00
|
|
|
),
|
|
|
|
...(keycloakMajorVersionNumber <= 20
|
2024-08-18 20:56:25 +02:00
|
|
|
? [`-e${SPACE_PLACEHOLDER}JAVA_OPTS=-Dkeycloak.profile=preview`]
|
2024-08-17 23:20:52 +02:00
|
|
|
: []),
|
|
|
|
...[
|
|
|
|
...buildContext.themeNames,
|
|
|
|
...(fs.existsSync(
|
2024-09-08 12:00:07 +02:00
|
|
|
pathJoin(buildContext.keycloakifyBuildDirPath, "theme", "account-v1")
|
2024-08-17 23:20:52 +02:00
|
|
|
)
|
2024-09-08 12:00:07 +02:00
|
|
|
? ["account-v1"]
|
2024-08-17 23:20:52 +02:00
|
|
|
: [])
|
|
|
|
]
|
|
|
|
.map(themeName => ({
|
|
|
|
localDirPath: pathJoin(
|
|
|
|
buildContext.keycloakifyBuildDirPath,
|
|
|
|
"theme",
|
|
|
|
themeName
|
|
|
|
),
|
|
|
|
containerDirPath: `/opt/keycloak/themes/${themeName}`
|
|
|
|
}))
|
|
|
|
.map(
|
|
|
|
({ localDirPath, containerDirPath }) =>
|
2024-10-19 02:28:11 +02:00
|
|
|
`-v${SPACE_PLACEHOLDER}"${localDirPath}":${containerDirPath}:rw`
|
2024-08-17 23:20:52 +02:00
|
|
|
),
|
|
|
|
...buildContext.environmentVariables
|
|
|
|
.map(({ name }) => ({ name, envValue: process.env[name] }))
|
|
|
|
.map(({ name, envValue }) =>
|
|
|
|
envValue === undefined ? undefined : { name, envValue }
|
|
|
|
)
|
|
|
|
.filter(exclude(undefined))
|
|
|
|
.map(
|
|
|
|
({ name, envValue }) =>
|
2024-08-18 20:56:25 +02:00
|
|
|
`--env${SPACE_PLACEHOLDER}${name}='${envValue.replace(/'/g, "'\\''")}'`
|
2024-08-17 23:20:52 +02:00
|
|
|
),
|
|
|
|
`${buildContext.startKeycloakOptions.dockerImage?.reference ?? "quay.io/keycloak/keycloak"}:${dockerImageTag}`,
|
|
|
|
"start-dev",
|
|
|
|
...(21 <= keycloakMajorVersionNumber && keycloakMajorVersionNumber < 24
|
|
|
|
? ["--features=declarative-user-profile"]
|
|
|
|
: []),
|
|
|
|
...(realmJsonFilePath === undefined ? [] : ["--import-realm"]),
|
2024-08-19 02:10:59 +02:00
|
|
|
...(buildContext.startKeycloakOptions.keycloakExtraArgs.length === 0
|
|
|
|
? []
|
|
|
|
: [
|
|
|
|
buildContext.startKeycloakOptions.keycloakExtraArgs.join(
|
|
|
|
SPACE_PLACEHOLDER
|
|
|
|
)
|
|
|
|
])
|
2024-08-17 23:20:52 +02:00
|
|
|
];
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
chalk.blue(
|
|
|
|
[
|
|
|
|
`$ docker run \\`,
|
2024-08-18 20:56:25 +02:00
|
|
|
...dockerRunArgs
|
|
|
|
.map(arg => arg.replace(new RegExp(SPACE_PLACEHOLDER, "g"), " "))
|
|
|
|
.map(
|
|
|
|
(line, i, arr) =>
|
|
|
|
` ${line}${arr.length - 1 === i ? "" : " \\"}`
|
|
|
|
)
|
2024-08-17 23:20:52 +02:00
|
|
|
].join("\n")
|
|
|
|
)
|
|
|
|
);
|
2024-05-20 02:27:40 +02:00
|
|
|
|
2024-08-17 23:20:52 +02:00
|
|
|
const child = child_process.spawn(
|
|
|
|
"docker",
|
2024-08-18 20:56:25 +02:00
|
|
|
["run", ...dockerRunArgs.map(line => line.split(SPACE_PLACEHOLDER)).flat()],
|
2024-08-17 23:20:52 +02:00
|
|
|
{ shell: true }
|
|
|
|
);
|
2024-05-17 05:13:41 +02:00
|
|
|
|
2024-12-14 14:36:11 +01:00
|
|
|
child.stdout.on("data", async data => {
|
|
|
|
if (data.toString("utf8").includes("keycloakify-logging: REALM_CONFIG_CHANGED")) {
|
|
|
|
await onRealmConfigChange();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
process.stdout.write(data);
|
|
|
|
});
|
2024-05-18 07:53:06 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2024-06-09 09:15:16 +02:00
|
|
|
const srcDirPath = pathJoin(buildContext.projectDirPath, "src");
|
2024-05-20 02:27:40 +02:00
|
|
|
|
2024-05-18 10:02:14 +02:00
|
|
|
{
|
2024-08-19 00:25:41 +02:00
|
|
|
const kcHttpRelativePath = (() => {
|
|
|
|
const match = buildContext.startKeycloakOptions.dockerExtraArgs
|
|
|
|
.join(" ")
|
|
|
|
.match(/KC_HTTP_RELATIVE_PATH=([^ ]+)/);
|
|
|
|
|
|
|
|
if (match === null) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return match[1];
|
|
|
|
})();
|
|
|
|
|
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(
|
|
|
|
[
|
2024-06-16 02:17:55 +02:00
|
|
|
"",
|
|
|
|
`The ftl files from ${chalk.bold(
|
|
|
|
`.${pathSep}${pathRelative(process.cwd(), pathJoin(buildContext.keycloakifyBuildDirPath, "theme"))}`
|
|
|
|
)} are mounted in the Keycloak container.`,
|
2024-05-26 19:55:59 +02:00
|
|
|
"",
|
|
|
|
`Keycloak Admin console: ${chalk.cyan.bold(
|
2024-08-19 00:25:41 +02:00
|
|
|
`http://localhost:${port}${kcHttpRelativePath ?? ""}`
|
2024-05-26 19:55:59 +02:00
|
|
|
)}`,
|
|
|
|
`- user: ${chalk.cyan.bold("admin")}`,
|
|
|
|
`- password: ${chalk.cyan.bold("admin")}`,
|
|
|
|
"",
|
2024-05-18 10:02:14 +02:00
|
|
|
"",
|
|
|
|
`${chalk.green("Your theme is accessible at:")}`,
|
2024-05-20 15:48:51 +02:00
|
|
|
`${chalk.green("➜")} ${chalk.cyan.bold(
|
2024-08-19 00:25:41 +02:00
|
|
|
(() => {
|
2024-12-15 11:38:50 +01:00
|
|
|
const url = new URL(TEST_APP_URL);
|
2024-08-19 00:25:41 +02:00
|
|
|
|
2024-12-14 14:36:11 +01:00
|
|
|
if (port !== 8080) {
|
2024-08-19 00:25:41 +02:00
|
|
|
url.searchParams.set("port", `${port}`);
|
|
|
|
}
|
|
|
|
if (kcHttpRelativePath !== undefined) {
|
|
|
|
url.searchParams.set(
|
|
|
|
"kcHttpRelativePath",
|
|
|
|
kcHttpRelativePath
|
|
|
|
);
|
|
|
|
}
|
2024-12-14 14:36:11 +01:00
|
|
|
if (realmName !== "myrealm") {
|
|
|
|
url.searchParams.set("realm", realmName);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (clientName !== "myclient") {
|
|
|
|
url.searchParams.set("client", clientName);
|
|
|
|
}
|
2024-08-19 00:25:41 +02:00
|
|
|
|
|
|
|
return url.href;
|
|
|
|
})()
|
2024-05-20 15:48:51 +02:00
|
|
|
)}`,
|
2024-05-20 10:16:38 +02:00
|
|
|
"",
|
|
|
|
"You can login with the following credentials:",
|
2024-12-14 14:36:11 +01:00
|
|
|
`- username: ${chalk.cyan.bold(username)}`,
|
2024-05-20 10:16:38 +02:00
|
|
|
`- password: ${chalk.cyan.bold("password123")}`,
|
2024-05-20 02:27:40 +02:00
|
|
|
"",
|
2024-05-20 15:48:51 +02:00
|
|
|
`Watching for changes in ${chalk.bold(
|
2024-06-09 09:15:16 +02:00
|
|
|
`.${pathSep}${pathRelative(process.cwd(), buildContext.projectDirPath)}`
|
2024-05-20 15:48:51 +02:00
|
|
|
)}`
|
2024-05-18 10:02:14 +02:00
|
|
|
].join("\n")
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
child.stdout.on("data", handler);
|
|
|
|
}
|
|
|
|
|
2024-05-20 02:27:40 +02:00
|
|
|
{
|
2024-05-26 19:29:12 +02:00
|
|
|
const runFullBuild = runExclusive.build(async () => {
|
2024-05-20 19:30:04 +02:00
|
|
|
console.log(chalk.cyan("Detected changes in the theme. Rebuilding ..."));
|
2024-05-20 15:34:07 +02:00
|
|
|
|
2024-05-26 19:29:12 +02:00
|
|
|
const { isAppBuildSuccess } = await appBuild({
|
2024-06-09 09:15:16 +02:00
|
|
|
buildContext
|
2024-05-26 19:29:12 +02:00
|
|
|
});
|
2024-05-20 02:27:40 +02:00
|
|
|
|
2024-05-26 19:29:12 +02:00
|
|
|
if (!isAppBuildSuccess) {
|
|
|
|
return;
|
2024-05-20 19:30:04 +02:00
|
|
|
}
|
2024-05-20 02:27:40 +02:00
|
|
|
|
2024-05-26 19:29:12 +02:00
|
|
|
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
|
2024-06-16 01:29:15 +02:00
|
|
|
buildForKeycloakMajorVersionNumber: keycloakMajorVersionNumber,
|
2024-06-09 09:15:16 +02:00
|
|
|
buildContext
|
2024-05-26 19:29:12 +02:00
|
|
|
});
|
2024-05-20 02:42:57 +02:00
|
|
|
|
2024-05-26 19:29:12 +02:00
|
|
|
if (!isKeycloakifyBuildSuccess) {
|
|
|
|
return;
|
|
|
|
}
|
2024-05-20 19:30:04 +02:00
|
|
|
|
2024-06-10 07:57:12 +02:00
|
|
|
await extractThemeResourcesFromJar();
|
2024-05-20 19:30:04 +02:00
|
|
|
|
2024-05-26 19:29:12 +02:00
|
|
|
console.log(chalk.green("Theme rebuilt and updated in Keycloak."));
|
2024-05-20 19:30:04 +02:00
|
|
|
});
|
|
|
|
|
2024-05-20 21:58:47 +02:00
|
|
|
const { waitForDebounce } = waitForDebounceFactory({ delay: 400 });
|
2024-05-20 19:30:04 +02:00
|
|
|
|
|
|
|
chokidar
|
2024-05-28 00:55:46 +02:00
|
|
|
.watch(
|
|
|
|
[
|
|
|
|
srcDirPath,
|
2024-06-09 09:15:16 +02:00
|
|
|
buildContext.publicDirPath,
|
|
|
|
pathJoin(buildContext.projectDirPath, "package.json"),
|
|
|
|
pathJoin(buildContext.projectDirPath, "vite.config.ts"),
|
|
|
|
pathJoin(buildContext.projectDirPath, "vite.config.js"),
|
|
|
|
pathJoin(buildContext.projectDirPath, "index.html"),
|
2024-05-28 00:55:46 +02:00
|
|
|
pathJoin(getThisCodebaseRootDirPath(), "src")
|
|
|
|
],
|
|
|
|
{
|
|
|
|
ignoreInitial: true
|
|
|
|
}
|
|
|
|
)
|
|
|
|
.on("all", async (...[, filePath]) => {
|
2024-11-30 16:58:48 +01:00
|
|
|
ignore_account_spa: {
|
|
|
|
const doImplementAccountSpa =
|
|
|
|
buildContext.implementedThemeTypes.account.isImplemented &&
|
|
|
|
buildContext.implementedThemeTypes.account.type === "Single-Page";
|
|
|
|
|
|
|
|
if (!doImplementAccountSpa) {
|
|
|
|
break ignore_account_spa;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
!isInside({
|
|
|
|
dirPath: pathJoin(buildContext.themeSrcDirPath, "account"),
|
|
|
|
filePath
|
|
|
|
})
|
|
|
|
) {
|
|
|
|
break ignore_account_spa;
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
ignore_admin: {
|
|
|
|
if (!buildContext.implementedThemeTypes.admin.isImplemented) {
|
|
|
|
break ignore_admin;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
!isInside({
|
|
|
|
dirPath: pathJoin(buildContext.themeSrcDirPath, "admin"),
|
|
|
|
filePath
|
|
|
|
})
|
|
|
|
) {
|
|
|
|
break ignore_admin;
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-05-28 00:55:46 +02:00
|
|
|
console.log(`Detected changes in ${filePath}`);
|
|
|
|
|
2024-05-20 19:30:04 +02:00
|
|
|
await waitForDebounce();
|
|
|
|
|
2024-05-26 19:29:12 +02:00
|
|
|
runFullBuild();
|
2024-05-20 15:48:51 +02:00
|
|
|
});
|
2024-05-20 02:27:40 +02:00
|
|
|
}
|
2024-05-16 09:20:37 +02:00
|
|
|
}
|