Implement admin theme support (checkpoint)
This commit is contained in:
parent
dc4eac1a04
commit
0e93d4ed09
@ -37,7 +37,7 @@ async function generateI18nMessages() {
|
|||||||
|
|
||||||
const record: { [themeType: string]: { [language: string]: Dictionary } } = {};
|
const record: { [themeType: string]: { [language: string]: Dictionary } } = {};
|
||||||
|
|
||||||
for (const themeType of THEME_TYPES) {
|
for (const themeType of THEME_TYPES.filter(themeType => themeType !== "admin")) {
|
||||||
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
|
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
|
||||||
keycloakVersionId: (() => {
|
keycloakVersionId: (() => {
|
||||||
switch (themeType) {
|
switch (themeType) {
|
||||||
|
@ -5,8 +5,7 @@ import {
|
|||||||
ACCOUNT_THEME_PAGE_IDS,
|
ACCOUNT_THEME_PAGE_IDS,
|
||||||
type LoginThemePageId,
|
type LoginThemePageId,
|
||||||
type AccountThemePageId,
|
type AccountThemePageId,
|
||||||
THEME_TYPES,
|
THEME_TYPES
|
||||||
type ThemeType
|
|
||||||
} from "./shared/constants";
|
} from "./shared/constants";
|
||||||
import { capitalize } from "tsafe/capitalize";
|
import { capitalize } from "tsafe/capitalize";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
@ -39,6 +38,8 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
return buildContext.implementedThemeTypes.account.isImplemented;
|
return buildContext.implementedThemeTypes.account.isImplemented;
|
||||||
case "login":
|
case "login":
|
||||||
return buildContext.implementedThemeTypes.login.isImplemented;
|
return buildContext.implementedThemeTypes.login.isImplemented;
|
||||||
|
case "admin":
|
||||||
|
return buildContext.implementedThemeTypes.admin.isImplemented;
|
||||||
}
|
}
|
||||||
assert<Equals<typeof themeType, never>>(false);
|
assert<Equals<typeof themeType, never>>(false);
|
||||||
});
|
});
|
||||||
@ -49,7 +50,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
return values[0];
|
return values[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { value } = await cliSelect<ThemeType>({
|
const { value } = await cliSelect({
|
||||||
values
|
values
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
@ -68,6 +69,16 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeType === "admin") {
|
||||||
|
console.log(
|
||||||
|
`${chalk.red("✗")} Sorry, there is no Storybook support for the Account UI.`
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`→ ${themeType}`);
|
console.log(`→ ${themeType}`);
|
||||||
|
@ -7,8 +7,7 @@ import {
|
|||||||
ACCOUNT_THEME_PAGE_IDS,
|
ACCOUNT_THEME_PAGE_IDS,
|
||||||
type LoginThemePageId,
|
type LoginThemePageId,
|
||||||
type AccountThemePageId,
|
type AccountThemePageId,
|
||||||
THEME_TYPES,
|
THEME_TYPES
|
||||||
type ThemeType
|
|
||||||
} from "./shared/constants";
|
} from "./shared/constants";
|
||||||
import { capitalize } from "tsafe/capitalize";
|
import { capitalize } from "tsafe/capitalize";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
@ -46,6 +45,8 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
return buildContext.implementedThemeTypes.account.isImplemented;
|
return buildContext.implementedThemeTypes.account.isImplemented;
|
||||||
case "login":
|
case "login":
|
||||||
return buildContext.implementedThemeTypes.login.isImplemented;
|
return buildContext.implementedThemeTypes.login.isImplemented;
|
||||||
|
case "admin":
|
||||||
|
return buildContext.implementedThemeTypes.admin.isImplemented;
|
||||||
}
|
}
|
||||||
assert<Equals<typeof themeType, never>>(false);
|
assert<Equals<typeof themeType, never>>(false);
|
||||||
});
|
});
|
||||||
@ -56,7 +57,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
return values[0];
|
return values[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { value } = await cliSelect<ThemeType>({
|
const { value } = await cliSelect({
|
||||||
values
|
values
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
@ -66,21 +67,22 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
themeType === "account" &&
|
themeType === "admin" ||
|
||||||
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
(themeType === "account" &&
|
||||||
buildContext.implementedThemeTypes.account.type === "Single-Page")
|
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
||||||
|
buildContext.implementedThemeTypes.account.type === "Single-Page"))
|
||||||
) {
|
) {
|
||||||
const srcDirPath = pathJoin(
|
const srcDirPath = pathJoin(
|
||||||
pathDirname(buildContext.packageJsonFilePath),
|
pathDirname(buildContext.packageJsonFilePath),
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"@keycloakify",
|
"@keycloakify",
|
||||||
"keycloak-account-ui",
|
`keycloak-${themeType}-ui`,
|
||||||
"src"
|
"src"
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
[
|
[
|
||||||
`There isn't an interactive CLI to eject components of the Single-Page Account theme.`,
|
`There isn't an interactive CLI to eject components of the ${themeType} UI.`,
|
||||||
`You can however copy paste into your codebase the any file or directory from the following source directory:`,
|
`You can however copy paste into your codebase the any file or directory from the following source directory:`,
|
||||||
``,
|
``,
|
||||||
`${chalk.bold(pathJoin(pathRelative(process.cwd(), srcDirPath)))}`,
|
`${chalk.bold(pathJoin(pathRelative(process.cwd(), srcDirPath)))}`,
|
||||||
@ -89,7 +91,8 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
eject_entrypoint: {
|
eject_entrypoint: {
|
||||||
const kcAccountUiTsxFileRelativePath = "KcAccountUi.tsx";
|
const kcAccountUiTsxFileRelativePath =
|
||||||
|
`Kc${capitalize(themeType)}Ui.tsx` as const;
|
||||||
|
|
||||||
const accountThemeSrcDirPath = pathJoin(
|
const accountThemeSrcDirPath = pathJoin(
|
||||||
buildContext.themeSrcDirPath,
|
buildContext.themeSrcDirPath,
|
||||||
@ -120,7 +123,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
).replace(/.tsx$/, "");
|
).replace(/.tsx$/, "");
|
||||||
|
|
||||||
const modifiedKcPageTsxCode = kcPageTsxCode.replace(
|
const modifiedKcPageTsxCode = kcPageTsxCode.replace(
|
||||||
`@keycloakify/keycloak-account-ui/${componentName}`,
|
`@keycloakify/keycloak-${themeType}-ui/${componentName}`,
|
||||||
`./${componentName}`
|
`./${componentName}`
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -146,6 +149,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`→ ${themeType}`);
|
console.log(`→ ${themeType}`);
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import type { BuildContext } from "../shared/buildContext";
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
import cliSelect from "cli-select";
|
import cliSelect from "cli-select";
|
||||||
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 { command as updateKcGenCommand } from "../update-kc-gen";
|
import { command as updateKcGenCommand } from "../update-kc-gen";
|
||||||
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
|
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
|
||||||
|
import { exitIfUncommittedChanges } from "../shared/exitIfUncommittedChanges";
|
||||||
|
|
||||||
export async function command(params: { buildContext: BuildContext }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
@ -38,37 +38,9 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
exit_if_uncommitted_changes: {
|
exitIfUncommittedChanges({
|
||||||
let hasUncommittedChanges: boolean | undefined = undefined;
|
projectDirPath: buildContext.projectDirPath
|
||||||
|
});
|
||||||
try {
|
|
||||||
hasUncommittedChanges =
|
|
||||||
child_process
|
|
||||||
.execSync(`git status --porcelain`, {
|
|
||||||
cwd: buildContext.projectDirPath
|
|
||||||
})
|
|
||||||
.toString()
|
|
||||||
.trim() !== "";
|
|
||||||
} catch {
|
|
||||||
// Probably not a git repository
|
|
||||||
break exit_if_uncommitted_changes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasUncommittedChanges) {
|
|
||||||
break exit_if_uncommitted_changes;
|
|
||||||
}
|
|
||||||
console.warn(
|
|
||||||
[
|
|
||||||
chalk.red(
|
|
||||||
"Please commit or stash your changes before running this command.\n"
|
|
||||||
),
|
|
||||||
"This command will modify your project's files so it's better to have a clean working directory",
|
|
||||||
"so that you can easily see what has been changed and revert if needed."
|
|
||||||
].join(" ")
|
|
||||||
);
|
|
||||||
|
|
||||||
process.exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { value: accountThemeType } = await cliSelect({
|
const { value: accountThemeType } = await cliSelect({
|
||||||
values: ["Single-Page" as const, "Multi-Page" as const]
|
values: ["Single-Page" as const, "Multi-Page" as const]
|
||||||
|
19
src/bin/initialize-admin-theme/copyBoilerplate.ts
Normal file
19
src/bin/initialize-admin-theme/copyBoilerplate.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import { join as pathJoin } from "path";
|
||||||
|
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
||||||
|
|
||||||
|
export function copyBoilerplate(params: { adminThemeSrcDirPath: string }) {
|
||||||
|
const { adminThemeSrcDirPath } = params;
|
||||||
|
|
||||||
|
fs.cpSync(
|
||||||
|
pathJoin(
|
||||||
|
getThisCodebaseRootDirPath(),
|
||||||
|
"src",
|
||||||
|
"bin",
|
||||||
|
"initialize-admin-theme",
|
||||||
|
"src"
|
||||||
|
),
|
||||||
|
adminThemeSrcDirPath,
|
||||||
|
{ recursive: true }
|
||||||
|
);
|
||||||
|
}
|
1
src/bin/initialize-admin-theme/index.ts
Normal file
1
src/bin/initialize-admin-theme/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./initialize-admin-theme";
|
60
src/bin/initialize-admin-theme/initialize-admin-theme.ts
Normal file
60
src/bin/initialize-admin-theme/initialize-admin-theme.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { join as pathJoin, relative as pathRelative } from "path";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { command as updateKcGenCommand } from "../update-kc-gen";
|
||||||
|
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
|
||||||
|
import { exitIfUncommittedChanges } from "../shared/exitIfUncommittedChanges";
|
||||||
|
import { initializeAdminTheme } from "./initializeAdminTheme";
|
||||||
|
|
||||||
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
|
const { buildContext } = params;
|
||||||
|
|
||||||
|
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||||
|
commandName: "initialize-admin-theme",
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasBeenHandled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "admin");
|
||||||
|
|
||||||
|
if (
|
||||||
|
fs.existsSync(adminThemeSrcDirPath) &&
|
||||||
|
fs.readdirSync(adminThemeSrcDirPath).length > 0
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
chalk.red(
|
||||||
|
`There is already a ${pathRelative(
|
||||||
|
process.cwd(),
|
||||||
|
adminThemeSrcDirPath
|
||||||
|
)} directory in your project. Aborting.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
exitIfUncommittedChanges({
|
||||||
|
projectDirPath: buildContext.projectDirPath
|
||||||
|
});
|
||||||
|
|
||||||
|
await initializeAdminTheme({
|
||||||
|
adminThemeSrcDirPath,
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateKcGenCommand({
|
||||||
|
buildContext: {
|
||||||
|
...buildContext,
|
||||||
|
implementedThemeTypes: {
|
||||||
|
...buildContext.implementedThemeTypes,
|
||||||
|
admin: {
|
||||||
|
isImplemented: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
150
src/bin/initialize-admin-theme/initializeAdminTheme.ts
Normal file
150
src/bin/initialize-admin-theme/initializeAdminTheme.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||||
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import {
|
||||||
|
getLatestsSemVersionedTag,
|
||||||
|
type BuildContextLike as BuildContextLike_getLatestsSemVersionedTag
|
||||||
|
} from "../shared/getLatestsSemVersionedTag";
|
||||||
|
import { SemVer } from "../tools/SemVer";
|
||||||
|
import fetch from "make-fetch-happen";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import { npmInstall } from "../tools/npmInstall";
|
||||||
|
import { copyBoilerplate } from "./copyBoilerplate";
|
||||||
|
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
||||||
|
|
||||||
|
type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & {
|
||||||
|
fetchOptions: BuildContext["fetchOptions"];
|
||||||
|
packageJsonFilePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
|
export async function initializeAdminTheme(params: {
|
||||||
|
adminThemeSrcDirPath: string;
|
||||||
|
buildContext: BuildContextLike;
|
||||||
|
}) {
|
||||||
|
const { adminThemeSrcDirPath, buildContext } = params;
|
||||||
|
|
||||||
|
const OWNER = "keycloakify";
|
||||||
|
const REPO = "keycloak-admin-ui";
|
||||||
|
|
||||||
|
const [semVersionedTag] = await getLatestsSemVersionedTag({
|
||||||
|
owner: OWNER,
|
||||||
|
repo: REPO,
|
||||||
|
count: 1,
|
||||||
|
doIgnoreReleaseCandidates: false,
|
||||||
|
buildContext
|
||||||
|
});
|
||||||
|
|
||||||
|
const dependencies = await fetch(
|
||||||
|
`https://raw.githubusercontent.com/${OWNER}/${REPO}/${semVersionedTag.tag}/dependencies.gen.json`,
|
||||||
|
buildContext.fetchOptions
|
||||||
|
)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(
|
||||||
|
(() => {
|
||||||
|
type Dependencies = {
|
||||||
|
dependencies: Record<string, string>;
|
||||||
|
devDependencies?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zDependencies = (() => {
|
||||||
|
type TargetType = Dependencies;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
dependencies: z.record(z.string()),
|
||||||
|
devDependencies: z.record(z.string()).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
|
return o => zDependencies.parse(o);
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
|
||||||
|
dependencies.dependencies["@keycloakify/keycloak-admin-ui"] = SemVer.stringify(
|
||||||
|
semVersionedTag.version
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsedPackageJson = (() => {
|
||||||
|
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);
|
||||||
|
})();
|
||||||
|
const parsedPackageJson = JSON.parse(
|
||||||
|
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
zParsedPackageJson.parse(parsedPackageJson);
|
||||||
|
|
||||||
|
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||||
|
|
||||||
|
return parsedPackageJson;
|
||||||
|
})();
|
||||||
|
|
||||||
|
parsedPackageJson.dependencies = {
|
||||||
|
...parsedPackageJson.dependencies,
|
||||||
|
...dependencies.dependencies
|
||||||
|
};
|
||||||
|
|
||||||
|
parsedPackageJson.devDependencies = {
|
||||||
|
...parsedPackageJson.devDependencies,
|
||||||
|
...dependencies.devDependencies
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Object.keys(parsedPackageJson.devDependencies).length === 0) {
|
||||||
|
delete parsedPackageJson.devDependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
buildContext.packageJsonFilePath,
|
||||||
|
JSON.stringify(parsedPackageJson, undefined, 4)
|
||||||
|
);
|
||||||
|
|
||||||
|
run_npm_install: {
|
||||||
|
if (
|
||||||
|
JSON.parse(
|
||||||
|
fs
|
||||||
|
.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json"))
|
||||||
|
.toString("utf8")
|
||||||
|
)["version"] === "0.0.0"
|
||||||
|
) {
|
||||||
|
//NOTE: Linked version
|
||||||
|
break run_npm_install;
|
||||||
|
}
|
||||||
|
|
||||||
|
npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) });
|
||||||
|
}
|
||||||
|
|
||||||
|
copyBoilerplate({ adminThemeSrcDirPath });
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
[
|
||||||
|
chalk.green("The Admin theme has been successfully initialized."),
|
||||||
|
`Using Admin UI of Keycloak version: ${chalk.bold(semVersionedTag.tag.split("-")[0])}`,
|
||||||
|
`Directory created: ${chalk.bold(pathRelative(process.cwd(), adminThemeSrcDirPath))}`,
|
||||||
|
`Dependencies added to your project's package.json: `,
|
||||||
|
chalk.bold(JSON.stringify(dependencies, null, 2))
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
}
|
7
src/bin/initialize-admin-theme/src/KcContext.ts
Normal file
7
src/bin/initialize-admin-theme/src/KcContext.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { KcContextLike } from "@keycloakify/keycloak-admin-ui";
|
||||||
|
import type { KcEnvName } from "../kc.gen";
|
||||||
|
|
||||||
|
export type KcContext = KcContextLike & {
|
||||||
|
themeType: "admin";
|
||||||
|
properties: Record<KcEnvName, string>;
|
||||||
|
};
|
11
src/bin/initialize-admin-theme/src/KcPage.tsx
Normal file
11
src/bin/initialize-admin-theme/src/KcPage.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { lazy } from "react";
|
||||||
|
import { KcAdminUiLoader } from "@keycloakify/keycloak-admin-ui";
|
||||||
|
import type { KcContext } from "./KcContext";
|
||||||
|
|
||||||
|
const KcAdminUi = lazy(() => import("@keycloakify/keycloak-admin-ui/KcAdminUi"));
|
||||||
|
|
||||||
|
export default function KcPage(props: { kcContext: KcContext }) {
|
||||||
|
const { kcContext } = props;
|
||||||
|
|
||||||
|
return <KcAdminUiLoader kcContext={kcContext} KcAdminUi={KcAdminUi} />;
|
||||||
|
}
|
@ -22,7 +22,7 @@ assert<BuildContext extends BuildContextLike ? true : false>();
|
|||||||
|
|
||||||
export function generateMessageProperties(params: {
|
export function generateMessageProperties(params: {
|
||||||
buildContext: BuildContextLike;
|
buildContext: BuildContextLike;
|
||||||
themeType: ThemeType;
|
themeType: Exclude<ThemeType, "admin">;
|
||||||
}): {
|
}): {
|
||||||
languageTags: string[];
|
languageTags: string[];
|
||||||
writeMessagePropertiesFiles: (params: {
|
writeMessagePropertiesFiles: (params: {
|
||||||
|
@ -19,7 +19,8 @@ import {
|
|||||||
type ThemeType,
|
type ThemeType,
|
||||||
LOGIN_THEME_PAGE_IDS,
|
LOGIN_THEME_PAGE_IDS,
|
||||||
ACCOUNT_THEME_PAGE_IDS,
|
ACCOUNT_THEME_PAGE_IDS,
|
||||||
WELL_KNOWN_DIRECTORY_BASE_NAME
|
WELL_KNOWN_DIRECTORY_BASE_NAME,
|
||||||
|
THEME_TYPES
|
||||||
} from "../../shared/constants";
|
} from "../../shared/constants";
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
import { readFieldNameUsage } from "./readFieldNameUsage";
|
import { readFieldNameUsage } from "./readFieldNameUsage";
|
||||||
@ -78,15 +79,29 @@ export async function generateResources(params: {
|
|||||||
Record<ThemeType, (params: { messageDirPath: string; themeName: string }) => void>
|
Record<ThemeType, (params: { messageDirPath: string; themeName: string }) => void>
|
||||||
> = {};
|
> = {};
|
||||||
|
|
||||||
for (const themeType of ["login", "account"] as const) {
|
for (const themeType of THEME_TYPES) {
|
||||||
if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
|
if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isForAccountSpa =
|
const getAccountThemeType = () => {
|
||||||
themeType === "account" &&
|
assert(themeType === "account");
|
||||||
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
|
||||||
buildContext.implementedThemeTypes.account.type === "Single-Page");
|
assert(buildContext.implementedThemeTypes.account.isImplemented);
|
||||||
|
|
||||||
|
return buildContext.implementedThemeTypes.account.type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSpa = (() => {
|
||||||
|
switch (themeType) {
|
||||||
|
case "login":
|
||||||
|
return false;
|
||||||
|
case "account":
|
||||||
|
return getAccountThemeType() === "Single-Page";
|
||||||
|
case "admin":
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
|
const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
|
||||||
|
|
||||||
@ -101,7 +116,7 @@ export async function generateResources(params: {
|
|||||||
rmSync(destDirPath, { recursive: true, force: true });
|
rmSync(destDirPath, { recursive: true, force: true });
|
||||||
|
|
||||||
if (
|
if (
|
||||||
themeType === "account" &&
|
themeType !== "login" &&
|
||||||
buildContext.implementedThemeTypes.login.isImplemented
|
buildContext.implementedThemeTypes.login.isImplemented
|
||||||
) {
|
) {
|
||||||
// NOTE: We prevent doing it twice, it has been done for the login theme.
|
// NOTE: We prevent doing it twice, it has been done for the login theme.
|
||||||
@ -194,10 +209,14 @@ export async function generateResources(params: {
|
|||||||
case "login":
|
case "login":
|
||||||
return LOGIN_THEME_PAGE_IDS;
|
return LOGIN_THEME_PAGE_IDS;
|
||||||
case "account":
|
case "account":
|
||||||
return isForAccountSpa ? ["index.ftl"] : ACCOUNT_THEME_PAGE_IDS;
|
return getAccountThemeType() === "Single-Page"
|
||||||
|
? ["index.ftl"]
|
||||||
|
: ACCOUNT_THEME_PAGE_IDS;
|
||||||
|
case "admin":
|
||||||
|
return ["index.ftl"];
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
...(isForAccountSpa
|
...(isSpa
|
||||||
? []
|
? []
|
||||||
: readExtraPagesNames({
|
: readExtraPagesNames({
|
||||||
themeType,
|
themeType,
|
||||||
@ -215,10 +234,12 @@ export async function generateResources(params: {
|
|||||||
let languageTags: string[] | undefined = undefined;
|
let languageTags: string[] | undefined = undefined;
|
||||||
|
|
||||||
i18n_messages_generation: {
|
i18n_messages_generation: {
|
||||||
if (isForAccountSpa) {
|
if (isSpa) {
|
||||||
break i18n_messages_generation;
|
break i18n_messages_generation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert(themeType !== "admin");
|
||||||
|
|
||||||
const wrap = generateMessageProperties({
|
const wrap = generateMessageProperties({
|
||||||
buildContext,
|
buildContext,
|
||||||
themeType
|
themeType
|
||||||
@ -231,16 +252,15 @@ export async function generateResources(params: {
|
|||||||
writeMessagePropertiesFiles;
|
writeMessagePropertiesFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
bring_in_account_v3_i18n_messages: {
|
bring_in_spas_messages: {
|
||||||
if (!buildContext.implementedThemeTypes.account.isImplemented) {
|
if (!isSpa) {
|
||||||
break bring_in_account_v3_i18n_messages;
|
break bring_in_spas_messages;
|
||||||
}
|
|
||||||
if (buildContext.implementedThemeTypes.account.type !== "Single-Page") {
|
|
||||||
break bring_in_account_v3_i18n_messages;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert(themeType !== "login");
|
||||||
|
|
||||||
const accountUiDirPath = child_process
|
const accountUiDirPath = child_process
|
||||||
.execSync("npm list @keycloakify/keycloak-account-ui --parseable", {
|
.execSync(`npm list @keycloakify/keycloak-${themeType}-ui --parseable`, {
|
||||||
cwd: pathDirname(buildContext.packageJsonFilePath)
|
cwd: pathDirname(buildContext.packageJsonFilePath)
|
||||||
})
|
})
|
||||||
.toString("utf8")
|
.toString("utf8")
|
||||||
@ -255,7 +275,7 @@ export async function generateResources(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messagesDirPath_dest = pathJoin(
|
const messagesDirPath_dest = pathJoin(
|
||||||
getThemeTypeDirPath({ themeName, themeType: "account" }),
|
getThemeTypeDirPath({ themeName, themeType }),
|
||||||
"messages"
|
"messages"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -267,7 +287,7 @@ export async function generateResources(params: {
|
|||||||
apply_theme_changes: {
|
apply_theme_changes: {
|
||||||
const messagesDirPath_theme = pathJoin(
|
const messagesDirPath_theme = pathJoin(
|
||||||
buildContext.themeSrcDirPath,
|
buildContext.themeSrcDirPath,
|
||||||
"account",
|
themeType,
|
||||||
"messages"
|
"messages"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -316,7 +336,7 @@ export async function generateResources(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
keycloak_static_resources: {
|
keycloak_static_resources: {
|
||||||
if (isForAccountSpa) {
|
if (isSpa) {
|
||||||
break keycloak_static_resources;
|
break keycloak_static_resources;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,13 +359,22 @@ export async function generateResources(params: {
|
|||||||
`parent=${(() => {
|
`parent=${(() => {
|
||||||
switch (themeType) {
|
switch (themeType) {
|
||||||
case "account":
|
case "account":
|
||||||
return isForAccountSpa ? "base" : "account-v1";
|
switch (getAccountThemeType()) {
|
||||||
|
case "Multi-Page":
|
||||||
|
return "account-v1";
|
||||||
|
case "Single-Page":
|
||||||
|
return "base";
|
||||||
|
}
|
||||||
case "login":
|
case "login":
|
||||||
return "keycloak";
|
return "keycloak";
|
||||||
|
case "admin":
|
||||||
|
return "base";
|
||||||
}
|
}
|
||||||
assert<Equals<typeof themeType, never>>(false);
|
assert<Equals<typeof themeType, never>>(false);
|
||||||
})()}`,
|
})()}`,
|
||||||
...(isForAccountSpa ? ["deprecatedMode=false"] : []),
|
...(themeType === "account" && getAccountThemeType() === "Single-Page"
|
||||||
|
? ["deprecatedMode=false"]
|
||||||
|
: []),
|
||||||
...(buildContext.extraThemeProperties ?? []),
|
...(buildContext.extraThemeProperties ?? []),
|
||||||
...buildContext.environmentVariables.map(
|
...buildContext.environmentVariables.map(
|
||||||
({ name, default: defaultValue }) =>
|
({ name, default: defaultValue }) =>
|
||||||
|
@ -197,6 +197,20 @@ program
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command({
|
||||||
|
name: "initialize-admin-theme",
|
||||||
|
description: "Initialize the admin theme."
|
||||||
|
})
|
||||||
|
.task({
|
||||||
|
skip,
|
||||||
|
handler: async ({ projectDirPath }) => {
|
||||||
|
const { command } = await import("./initialize-admin-theme");
|
||||||
|
|
||||||
|
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
program
|
program
|
||||||
.command({
|
.command({
|
||||||
name: "copy-keycloak-resources-to-public",
|
name: "copy-keycloak-resources-to-public",
|
||||||
|
@ -52,6 +52,7 @@ export type BuildContext = {
|
|||||||
account:
|
account:
|
||||||
| { isImplemented: false }
|
| { isImplemented: false }
|
||||||
| { isImplemented: true; type: "Single-Page" | "Multi-Page" };
|
| { isImplemented: true; type: "Single-Page" | "Multi-Page" };
|
||||||
|
admin: { isImplemented: boolean };
|
||||||
};
|
};
|
||||||
packageJsonFilePath: string;
|
packageJsonFilePath: string;
|
||||||
bundler: "vite" | "webpack";
|
bundler: "vite" | "webpack";
|
||||||
@ -448,7 +449,10 @@ export function getBuildContext(params: {
|
|||||||
isImplemented: true,
|
isImplemented: true,
|
||||||
type: buildOptions.accountThemeImplementation
|
type: buildOptions.accountThemeImplementation
|
||||||
};
|
};
|
||||||
})()
|
})(),
|
||||||
|
admin: {
|
||||||
|
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "admin"))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -4,7 +4,7 @@ export const WELL_KNOWN_DIRECTORY_BASE_NAME = {
|
|||||||
DIST: "dist"
|
DIST: "dist"
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const THEME_TYPES = ["login", "account"] as const;
|
export const THEME_TYPES = ["login", "account", "admin"] as const;
|
||||||
|
|
||||||
export type ThemeType = (typeof THEME_TYPES)[number];
|
export type ThemeType = (typeof THEME_TYPES)[number];
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ export type CommandName =
|
|||||||
| "eject-page"
|
| "eject-page"
|
||||||
| "add-story"
|
| "add-story"
|
||||||
| "initialize-account-theme"
|
| "initialize-account-theme"
|
||||||
|
| "initialize-admin-theme"
|
||||||
| "initialize-email-theme"
|
| "initialize-email-theme"
|
||||||
| "copy-keycloak-resources-to-public";
|
| "copy-keycloak-resources-to-public";
|
||||||
|
|
||||||
|
36
src/bin/shared/exitIfUncommittedChanges.ts
Normal file
36
src/bin/shared/exitIfUncommittedChanges.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import child_process from "child_process";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
export function exitIfUncommittedChanges(params: { projectDirPath: string }) {
|
||||||
|
const { projectDirPath } = params;
|
||||||
|
|
||||||
|
let hasUncommittedChanges: boolean | undefined = undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
hasUncommittedChanges =
|
||||||
|
child_process
|
||||||
|
.execSync(`git status --porcelain`, {
|
||||||
|
cwd: projectDirPath
|
||||||
|
})
|
||||||
|
.toString()
|
||||||
|
.trim() !== "";
|
||||||
|
} catch {
|
||||||
|
// Probably not a git repository
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasUncommittedChanges) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.warn(
|
||||||
|
[
|
||||||
|
chalk.red(
|
||||||
|
"Please commit or stash your changes before running this command.\n"
|
||||||
|
),
|
||||||
|
"This command will modify your project's files so it's better to have a clean working directory",
|
||||||
|
"so that you can easily see what has been changed and revert if needed."
|
||||||
|
].join(" ")
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
@ -10,5 +10,5 @@
|
|||||||
"rootDir": "."
|
"rootDir": "."
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "**/*.tsx"],
|
"include": ["**/*.ts", "**/*.tsx"],
|
||||||
"exclude": ["initialize-account-theme/src"]
|
"exclude": ["initialize-account-theme/src", "initialize-admin-theme/src"]
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
|
|
||||||
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
|
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
|
||||||
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
|
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
|
||||||
|
const hasAdminTheme = buildContext.implementedThemeTypes.admin.isImplemented;
|
||||||
|
|
||||||
const newContent = [
|
const newContent = [
|
||||||
``,
|
``,
|
||||||
@ -54,6 +55,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
`export type KcContext =`,
|
`export type KcContext =`,
|
||||||
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
|
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
|
||||||
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
|
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
|
||||||
|
hasAdminTheme && ` | import("./admin/KcContext").KcContext`,
|
||||||
` ;`,
|
` ;`,
|
||||||
``,
|
``,
|
||||||
`declare global {`,
|
`declare global {`,
|
||||||
@ -66,6 +68,8 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
|
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
|
||||||
hasAccountTheme &&
|
hasAccountTheme &&
|
||||||
`export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
|
`export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
|
||||||
|
hasAdminTheme &&
|
||||||
|
`export const KcAdminPage = lazy(() => import("./admin/KcPage"));`,
|
||||||
``,
|
``,
|
||||||
`export function KcPage(`,
|
`export function KcPage(`,
|
||||||
` props: {`,
|
` props: {`,
|
||||||
@ -82,6 +86,8 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
` case "login": return <KcLoginPage kcContext={kcContext} />;`,
|
` case "login": return <KcLoginPage kcContext={kcContext} />;`,
|
||||||
hasAccountTheme &&
|
hasAccountTheme &&
|
||||||
` case "account": return <KcAccountPage kcContext={kcContext} />;`,
|
` case "account": return <KcAccountPage kcContext={kcContext} />;`,
|
||||||
|
hasAdminTheme &&
|
||||||
|
` case "admin": return <KcAdminPage kcContext={kcContext} />;`,
|
||||||
` }`,
|
` }`,
|
||||||
` })()}`,
|
` })()}`,
|
||||||
` </Suspense>`,
|
` </Suspense>`,
|
||||||
|
@ -17,5 +17,8 @@
|
|||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": ["../src", "."],
|
"include": ["../src", "."],
|
||||||
"exclude": ["../src/bin/initialize-account-theme/src"]
|
"exclude": [
|
||||||
|
"../src/bin/initialize-account-theme/src",
|
||||||
|
"../src/bin/initialize-admin-theme/src"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user