Compare commits

..

24 Commits

Author SHA1 Message Date
96fc779ec8 Release candidate 2024-07-27 17:50:58 +02:00
9605e17e96 Fix generaion of entrypoint 2024-07-27 17:50:31 +02:00
111c1675f9 Release candidate 2024-07-27 01:14:03 +02:00
d547ec3126 #596 2024-07-27 01:13:47 +02:00
0ce6a7be7f #597 2024-07-27 01:07:04 +02:00
89d9208f44 Fix storybook build 2024-07-27 00:44:59 +02:00
3e80aaf242 Fix vitest setup 2024-07-25 20:03:03 +02:00
86c3159ded Release candidate 2024-07-25 19:58:24 +02:00
230e05abc0 Fix tsconfig exclusion 2024-07-25 19:53:36 +02:00
ff2e6e6432 Fix spelling in directory structure 2024-07-25 19:53:36 +02:00
dc00be9be6 Don't run npm install when linked 2024-07-25 19:53:36 +02:00
77249d8a58 Feat: Initialize account theme (before debug) 2024-07-25 19:53:36 +02:00
b9ee0afe7f Fix bug in download and extract archive 2024-07-25 19:53:36 +02:00
db23ab0bc2 Introduce build option: accountThemeImplementation 2024-07-25 19:53:36 +02:00
13dc47533c Release candidate 2024-07-24 17:00:11 +02:00
0091a888bc #594 2024-07-24 16:59:54 +02:00
724b585004 Release candidate 2024-07-23 14:58:54 +02:00
c0d127e4f4 #593 2024-07-23 14:58:39 +02:00
951c202fd0 Merge pull request #589 from lokmeinmatz/keycloak_24
fix: Typo InputFiledByType to InputFieldByType
2024-07-17 14:30:17 +02:00
a578b86715 Release candidate 2024-07-17 13:58:55 +02:00
b6b384854e Remove tsafe usage from ejectable page 2024-07-17 13:58:39 +02:00
dac937060d fix: Typo InputFiledByType to InputFieldByType 2024-07-17 09:35:59 +02:00
c628183773 Release candidate 2024-07-14 17:55:02 +02:00
eaacaa6966 (BREAKING CHANGE) When classes are overloaded disable default paterlyfly classes 2024-07-14 17:54:44 +02:00
40 changed files with 1345 additions and 469 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "keycloakify", "name": "keycloakify",
"version": "10.0.0-rc.114", "version": "10.0.0-rc.121",
"description": "Create Keycloak themes using React", "description": "Create Keycloak themes using React",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,16 +1,13 @@
import * as child_process from "child_process"; import * as child_process from "child_process";
import { join } from "path"; import { copyKeycloakResourcesToStorybookStaticDir } from "./copyKeycloakResourcesToStorybookStaticDir";
(async () => {
run("yarn build"); run("yarn build");
run(`node ${join("dist", "bin", "main.js")} copy-keycloak-resources-to-public`, { await copyKeycloakResourcesToStorybookStaticDir();
env: {
...process.env,
PUBLIC_DIR_PATH: join(".storybook", "static")
}
});
run("npx build-storybook"); run("npx build-storybook");
})();
function run(command: string, options?: { env?: NodeJS.ProcessEnv }) { function run(command: string, options?: { env?: NodeJS.ProcessEnv }) {
console.log(`$ ${command}`); console.log(`$ ${command}`);

View File

@ -0,0 +1,18 @@
import { join as pathJoin } from "path";
import { copyKeycloakResourcesToPublic } from "../src/bin/shared/copyKeycloakResourcesToPublic";
import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions";
import { LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT } from "../src/bin/shared/constants";
export async function copyKeycloakResourcesToStorybookStaticDir() {
await copyKeycloakResourcesToPublic({
buildContext: {
cacheDirPath: pathJoin(__dirname, "..", "node_modules", ".cache", "scripts"),
fetchOptions: getProxyFetchOptions({
npmConfigGetCwd: pathJoin(__dirname, "..")
}),
loginThemeResourcesFromKeycloakVersion:
LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT,
publicDirPath: pathJoin(__dirname, "..", ".storybook", "static")
}
});
}

View File

@ -44,6 +44,12 @@ const commonThirdPartyDeps = [
.replace(/"!dist\//g, '"!') .replace(/"!dist\//g, '"!')
.replace(/"!\.\/dist\//g, '"!./'); .replace(/"!\.\/dist\//g, '"!./');
modifiedPackageJsonContent = JSON.stringify(
{ ...JSON.parse(modifiedPackageJsonContent), version: "0.0.0" },
null,
4
);
fs.writeFileSync( fs.writeFileSync(
pathJoin(rootDirPath, "dist", "package.json"), pathJoin(rootDirPath, "dist", "package.json"),
Buffer.from(modifiedPackageJsonContent, "utf8") Buffer.from(modifiedPackageJsonContent, "utf8")

View File

@ -1,16 +1,11 @@
import * as child_process from "child_process"; import * as child_process from "child_process";
import * as fs from "fs";
import { join } from "path";
import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange"; import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange";
import { copyKeycloakResourcesToStorybookStaticDir } from "./copyKeycloakResourcesToStorybookStaticDir";
(async () => {
run("yarn build"); run("yarn build");
run(`node ${join("dist", "bin", "main.js")} copy-keycloak-resources-to-public`, { await copyKeycloakResourcesToStorybookStaticDir();
env: {
...process.env,
PUBLIC_DIR_PATH: join(".storybook", "static")
}
});
{ {
const child = child_process.spawn("npx", ["start-storybook", "-p", "6006"], { const child = child_process.spawn("npx", ["start-storybook", "-p", "6006"], {
@ -25,6 +20,7 @@ run(`node ${join("dist", "bin", "main.js")} copy-keycloak-resources-to-public`,
} }
startRebuildOnSrcChange(); startRebuildOnSrcChange();
})();
function run(command: string, options?: { env?: NodeJS.ProcessEnv }) { function run(command: string, options?: { env?: NodeJS.ProcessEnv }) {
console.log(`$ ${command}`); console.log(`$ ${command}`);

View File

@ -8,7 +8,7 @@ export default function FederatedIdentity(props: PageProps<Extract<KcContext, {
const { url, federatedIdentity, stateChecker } = kcContext; const { url, federatedIdentity, stateChecker } = kcContext;
const { msg } = i18n; const { msg } = i18n;
return ( return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="federatedIdentity"> <Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="social">
<div className="main-layout social"> <div className="main-layout social">
<div className="row"> <div className="row">
<div className="col-md-10"> <div className="col-md-10">

View File

@ -0,0 +1,32 @@
import * as fs from "fs";
import { join as pathJoin } from "path";
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
import { assert, type Equals } from "tsafe/assert";
export function copyBoilerplate(params: {
accountThemeType: "Single-Page" | "Multi-Page";
accountThemeSrcDirPath: string;
}) {
const { accountThemeType, accountThemeSrcDirPath } = params;
fs.cpSync(
pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"initialize-account-theme",
"src",
(() => {
switch (accountThemeType) {
case "Single-Page":
return "single-page";
case "Multi-Page":
return "multi-page";
}
assert<Equals<typeof accountThemeType, never>>(false);
})()
),
accountThemeSrcDirPath,
{ recursive: true }
);
}

View File

@ -0,0 +1 @@
export * from "./initialize-account-theme";

View File

@ -0,0 +1,112 @@
import { getBuildContext } from "../shared/buildContext";
import type { CliCommandOptions } from "../main";
import cliSelect from "cli-select";
import child_process from "child_process";
import chalk from "chalk";
import { join as pathJoin, relative as pathRelative } from "path";
import * as fs from "fs";
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
import { generateKcGenTs } from "../shared/generateKcGenTs";
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { cliCommandOptions } = params;
const buildContext = getBuildContext({ cliCommandOptions });
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
if (
fs.existsSync(accountThemeSrcDirPath) &&
fs.readdirSync(accountThemeSrcDirPath).length > 0
) {
console.warn(
chalk.red(
`There is already a ${pathRelative(
process.cwd(),
accountThemeSrcDirPath
)} directory in your project. Aborting.`
)
);
process.exit(-1);
}
exit_if_uncommitted_changes: {
let hasUncommittedChanges: boolean | undefined = undefined;
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({
values: ["Single-Page" as const, "Multi-Page" as const]
}).catch(() => {
process.exit(-1);
});
switch (accountThemeType) {
case "Multi-Page":
{
const { initializeAccountTheme_multiPage } = await import(
"./initializeAccountTheme_multiPage"
);
await initializeAccountTheme_multiPage({
accountThemeSrcDirPath
});
}
break;
case "Single-Page":
{
const { initializeAccountTheme_singlePage } = await import(
"./initializeAccountTheme_singlePage"
);
await initializeAccountTheme_singlePage({
accountThemeSrcDirPath,
buildContext
});
}
break;
}
updateAccountThemeImplementationInConfig({ buildContext, accountThemeType });
await generateKcGenTs({
buildContext: {
...buildContext,
implementedThemeTypes: {
...buildContext.implementedThemeTypes,
account: {
isImplemented: true,
type: accountThemeType
}
}
}
});
}

View File

@ -0,0 +1,21 @@
import { relative as pathRelative } from "path";
import chalk from "chalk";
import { copyBoilerplate } from "./copyBoilerplate";
export async function initializeAccountTheme_multiPage(params: {
accountThemeSrcDirPath: string;
}) {
const { accountThemeSrcDirPath } = params;
copyBoilerplate({
accountThemeType: "Multi-Page",
accountThemeSrcDirPath
});
console.log(
[
chalk.green("The Multi-Page account theme has been initialized."),
`Directory created: ${chalk.bold(pathRelative(process.cwd(), accountThemeSrcDirPath))}`
].join("\n")
);
}

View 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 } from "../shared/getLatestsSemVersionedTag";
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 = {
cacheDirPath: string;
fetchOptions: BuildContext["fetchOptions"];
packageJsonFilePath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function initializeAccountTheme_singlePage(params: {
accountThemeSrcDirPath: string;
buildContext: BuildContextLike;
}) {
const { accountThemeSrcDirPath, buildContext } = params;
const OWNER = "keycloakify";
const REPO = "keycloak-account-ui";
const [semVersionedTag] = await getLatestsSemVersionedTag({
cacheDirPath: buildContext.cacheDirPath,
owner: OWNER,
repo: REPO,
count: 1,
doIgnoreReleaseCandidates: false
});
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-account-ui"] = semVersionedTag.tag;
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({
accountThemeType: "Single-Page",
accountThemeSrcDirPath
});
console.log(
[
chalk.green(
"The Single-Page account theme has been successfully initialized."
),
`Using Account UI of Keycloak version: ${chalk.bold(semVersionedTag.tag.split("-")[0])}`,
`Directory created: ${chalk.bold(pathRelative(process.cwd(), accountThemeSrcDirPath))}`,
`Dependencies added to your project's package.json: `,
chalk.bold(JSON.stringify(dependencies, null, 2))
].join("\n")
);
}

View File

@ -0,0 +1,12 @@
/* eslint-disable @typescript-eslint/ban-types */
import type { ExtendKcContext } from "keycloakify/account";
import type { KcEnvName, ThemeName } from "../kc.gen";
export type KcContextExtension = {
themeName: ThemeName;
properties: Record<KcEnvName, string> & {};
};
export type KcContextExtensionPerPage = {};
export type KcContext = ExtendKcContext<KcContextExtension, KcContextExtensionPerPage>;

View File

@ -0,0 +1,25 @@
import { Suspense } from "react";
import type { ClassKey } from "keycloakify/account";
import type { KcContext } from "./KcContext";
import { useI18n } from "./i18n";
import DefaultPage from "keycloakify/account/DefaultPage";
import Template from "keycloakify/account/Template";
export default function KcPage(props: { kcContext: KcContext }) {
const { kcContext } = props;
const { i18n } = useI18n({ kcContext });
return (
<Suspense>
{(() => {
switch (kcContext.pageId) {
default:
return <DefaultPage kcContext={kcContext} i18n={i18n} classes={classes} Template={Template} doUseDefaultCss={true} />;
}
})()}
</Suspense>
);
}
const classes = {} satisfies { [key in ClassKey]?: string };

View File

@ -0,0 +1,38 @@
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import type { KcContext } from "./KcContext";
import { createGetKcContextMock } from "keycloakify/account/KcContext";
import type { KcContextExtension, KcContextExtensionPerPage } from "./KcContext";
import KcPage from "./KcPage";
import { themeNames, kcEnvDefaults } from "../kc.gen";
const kcContextExtension: KcContextExtension = {
themeName: themeNames[0],
properties: {
...kcEnvDefaults
}
};
const kcContextExtensionPerPage: KcContextExtensionPerPage = {};
export const { getKcContextMock } = createGetKcContextMock({
kcContextExtension,
kcContextExtensionPerPage,
overrides: {},
overridesPerPage: {}
});
export function createKcPageStory<PageId extends KcContext["pageId"]>(params: { pageId: PageId }) {
const { pageId } = params;
function KcPageStory(props: { kcContext?: DeepPartial<Extract<KcContext, { pageId: PageId }>> }) {
const { kcContext: overrides } = props;
const kcContextMock = getKcContextMock({
pageId,
overrides
});
return <KcPage kcContext={kcContextMock} />;
}
return { KcPageStory };
}

View File

@ -0,0 +1,5 @@
import { createUseI18n } from "keycloakify/account";
export const { useI18n, ofTypeI18n } = createUseI18n({});
export type I18n = typeof ofTypeI18n;

View File

@ -0,0 +1,7 @@
import type { KcContextLike } from "@keycloakify/keycloak-account-ui";
import type { KcEnvName } from "../kc.gen";
export type KcContext = KcContextLike & {
themeType: "account";
properties: Record<KcEnvName, string>;
};

View File

@ -0,0 +1,11 @@
import { lazy } from "react";
import { KcAccountUiLoader } from "@keycloakify/keycloak-account-ui";
import type { KcContext } from "./KcContext";
const KcAccountUi = lazy(() => import("@keycloakify/keycloak-account-ui/KcAccountUi"));
export default function KcPage(props: { kcContext: KcContext }) {
const { kcContext } = props;
return <KcAccountUiLoader kcContext={kcContext} KcAccountUi={KcAccountUi} />;
}

View File

@ -0,0 +1,92 @@
import { join as pathJoin } from "path";
import { assert, type Equals } from "tsafe/assert";
import type { BuildContext } from "../shared/buildContext";
import * as fs from "fs";
import chalk from "chalk";
import { z } from "zod";
import { id } from "tsafe/id";
export type BuildContextLike = {
bundler: BuildContext["bundler"];
};
assert<BuildContext extends BuildContextLike ? true : false>();
export function updateAccountThemeImplementationInConfig(params: {
buildContext: BuildContext;
accountThemeType: "Single-Page" | "Multi-Page";
}) {
const { buildContext, accountThemeType } = params;
switch (buildContext.bundler) {
case "vite":
{
const viteConfigPath = pathJoin(
buildContext.projectDirPath,
"vite.config.ts"
);
if (!fs.existsSync(viteConfigPath)) {
console.log(
chalk.bold(
`You must manually set the accountThemeImplementation to "${accountThemeType}" in your vite config`
)
);
break;
}
const viteConfigContent = fs
.readFileSync(viteConfigPath)
.toString("utf8");
const modifiedViteConfigContent = viteConfigContent.replace(
/accountThemeImplementation\s*:\s*"none"/,
`accountThemeImplementation: "${accountThemeType}"`
);
if (modifiedViteConfigContent === viteConfigContent) {
console.log(
chalk.bold(
`You must manually set the accountThemeImplementation to "${accountThemeType}" in your vite.config.ts`
)
);
break;
}
fs.writeFileSync(viteConfigPath, modifiedViteConfigContent);
}
break;
case "webpack":
{
const parsedPackageJson = (() => {
type ParsedPackageJson = {
keycloakify: Record<string, string>;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
keycloakify: z.record(z.string())
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
return zParsedPackageJson.parse(
JSON.parse(
fs
.readFileSync(buildContext.packageJsonFilePath)
.toString("utf8")
)
);
})();
parsedPackageJson.keycloakify.accountThemeImplementation =
accountThemeType;
}
break;
}
}

View File

@ -24,7 +24,7 @@ export type BuildContextLike = BuildContextLike_generatePom & {
artifactId: string; artifactId: string;
themeVersion: string; themeVersion: string;
cacheDirPath: string; cacheDirPath: string;
recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"]; implementedThemeTypes: BuildContext["implementedThemeTypes"];
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -135,7 +135,7 @@ export async function buildJar(params: {
} }
route_legacy_pages: { route_legacy_pages: {
if (!buildContext.recordIsImplementedByThemeType.login) { if (!buildContext.implementedThemeTypes.login.isImplemented) {
break route_legacy_pages; break route_legacy_pages;
} }

View File

@ -10,9 +10,8 @@ import type { BuildContext } from "../../shared/buildContext";
export type BuildContextLike = BuildContextLike_buildJar & { export type BuildContextLike = BuildContextLike_buildJar & {
projectDirPath: string; projectDirPath: string;
keycloakifyBuildDirPath: string; keycloakifyBuildDirPath: string;
recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"]; implementedThemeTypes: BuildContext["implementedThemeTypes"];
jarTargets: BuildContext["jarTargets"]; jarTargets: BuildContext["jarTargets"];
doUseAccountV3: boolean;
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -24,8 +23,8 @@ export async function buildJars(params: {
const { resourcesDirPath, buildContext } = params; const { resourcesDirPath, buildContext } = params;
const doesImplementAccountV1Theme = const doesImplementAccountV1Theme =
buildContext.recordIsImplementedByThemeType.account && buildContext.implementedThemeTypes.account.isImplemented &&
!buildContext.doUseAccountV3; buildContext.implementedThemeTypes.account.type === "Multi-Page";
await Promise.all( await Promise.all(
keycloakAccountV1Versions keycloakAccountV1Versions

View File

@ -53,10 +53,9 @@ export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
projectDirPath: string; projectDirPath: string;
projectBuildDirPath: string; projectBuildDirPath: string;
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"]; implementedThemeTypes: BuildContext["implementedThemeTypes"];
themeSrcDirPath: string; themeSrcDirPath: string;
bundler: { type: "vite" } | { type: "webpack" }; bundler: "vite" | "webpack";
doUseAccountV3: boolean;
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -74,11 +73,15 @@ export async function generateResourcesForMainTheme(params: {
}; };
for (const themeType of ["login", "account"] as const) { for (const themeType of ["login", "account"] as const) {
const isAccountV3 = themeType === "account" && buildContext.doUseAccountV3; if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
if (!buildContext.recordIsImplementedByThemeType[themeType]) {
continue; continue;
} }
const isForAccountSpa =
themeType === "account" &&
(assert(buildContext.implementedThemeTypes.account.isImplemented),
buildContext.implementedThemeTypes.account.type === "Single-Page");
const themeTypeDirPath = getThemeTypeDirPath({ themeType }); const themeTypeDirPath = getThemeTypeDirPath({ themeType });
apply_replacers_and_move_to_theme_resources: { apply_replacers_and_move_to_theme_resources: {
@ -93,7 +96,7 @@ export async function generateResourcesForMainTheme(params: {
if ( if (
themeType === "account" && themeType === "account" &&
buildContext.recordIsImplementedByThemeType.login 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.
@ -118,7 +121,7 @@ export async function generateResourcesForMainTheme(params: {
); );
if (fs.existsSync(dirPath)) { if (fs.existsSync(dirPath)) {
assert(buildContext.bundler.type === "webpack"); assert(buildContext.bundler === "webpack");
throw new Error( throw new Error(
[ [
@ -184,10 +187,10 @@ export async function generateResourcesForMainTheme(params: {
case "login": case "login":
return LOGIN_THEME_PAGE_IDS; return LOGIN_THEME_PAGE_IDS;
case "account": case "account":
return isAccountV3 ? ["index.ftl"] : ACCOUNT_THEME_PAGE_IDS; return isForAccountSpa ? ["index.ftl"] : ACCOUNT_THEME_PAGE_IDS;
} }
})(), })(),
...(isAccountV3 ...(isForAccountSpa
? [] ? []
: readExtraPagesNames({ : readExtraPagesNames({
themeType, themeType,
@ -203,7 +206,7 @@ export async function generateResourcesForMainTheme(params: {
}); });
i18n_messages_generation: { i18n_messages_generation: {
if (isAccountV3) { if (isForAccountSpa) {
break i18n_messages_generation; break i18n_messages_generation;
} }
@ -230,7 +233,7 @@ export async function generateResourcesForMainTheme(params: {
} }
keycloak_static_resources: { keycloak_static_resources: {
if (isAccountV3) { if (isForAccountSpa) {
break keycloak_static_resources; break keycloak_static_resources;
} }
@ -256,13 +259,13 @@ export async function generateResourcesForMainTheme(params: {
`parent=${(() => { `parent=${(() => {
switch (themeType) { switch (themeType) {
case "account": case "account":
return isAccountV3 ? "base" : ACCOUNT_V1_THEME_NAME; return isForAccountSpa ? "base" : ACCOUNT_V1_THEME_NAME;
case "login": case "login":
return "keycloak"; return "keycloak";
} }
assert<Equals<typeof themeType, never>>(false); assert<Equals<typeof themeType, never>>(false);
})()}`, })()}`,
...(isAccountV3 ? ["deprecatedMode=false"] : []), ...(isForAccountSpa ? ["deprecatedMode=false"] : []),
...(buildContext.extraThemeProperties ?? []), ...(buildContext.extraThemeProperties ?? []),
...buildContext.environmentVariables.map( ...buildContext.environmentVariables.map(
({ name, default: defaultValue }) => ({ name, default: defaultValue }) =>
@ -275,7 +278,7 @@ export async function generateResourcesForMainTheme(params: {
} }
email: { email: {
if (!buildContext.recordIsImplementedByThemeType.email) { if (!buildContext.implementedThemeTypes.email.isImplemented) {
break email; break email;
} }
@ -288,11 +291,11 @@ export async function generateResourcesForMainTheme(params: {
} }
bring_in_account_v1: { bring_in_account_v1: {
if (buildContext.doUseAccountV3) { if (!buildContext.implementedThemeTypes.account.isImplemented) {
break bring_in_account_v1; break bring_in_account_v1;
} }
if (!buildContext.recordIsImplementedByThemeType.account) { if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
break bring_in_account_v1; break bring_in_account_v1;
} }
@ -303,7 +306,10 @@ export async function generateResourcesForMainTheme(params: {
} }
bring_in_account_v3_i18n_messages: { bring_in_account_v3_i18n_messages: {
if (!buildContext.doUseAccountV3) { if (!buildContext.implementedThemeTypes.account.isImplemented) {
break bring_in_account_v3_i18n_messages;
}
if (buildContext.implementedThemeTypes.account.type !== "Single-Page") {
break bring_in_account_v3_i18n_messages; break bring_in_account_v3_i18n_messages;
} }
@ -340,12 +346,12 @@ export async function generateResourcesForMainTheme(params: {
metaInfKeycloakThemes.themes.push({ metaInfKeycloakThemes.themes.push({
name: themeName, name: themeName,
types: objectEntries(buildContext.recordIsImplementedByThemeType) types: objectEntries(buildContext.implementedThemeTypes)
.filter(([, isImplemented]) => isImplemented) .filter(([, { isImplemented }]) => isImplemented)
.map(([themeType]) => themeType) .map(([themeType]) => themeType)
}); });
if (buildContext.recordIsImplementedByThemeType.account) { if (buildContext.implementedThemeTypes.account.isImplemented) {
metaInfKeycloakThemes.themes.push({ metaInfKeycloakThemes.themes.push({
name: ACCOUNT_V1_THEME_NAME, name: ACCOUNT_V1_THEME_NAME,
types: ["account"] types: ["account"]

View File

@ -85,7 +85,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
}); });
run_post_build_script: { run_post_build_script: {
if (buildContext.bundler.type !== "vite") { if (buildContext.bundler !== "vite") {
break run_post_build_script; break run_post_build_script;
} }

View File

@ -8,7 +8,7 @@ export type BuildContextLike = {
projectBuildDirPath: string; projectBuildDirPath: string;
assetsDirPath: string; assetsDirPath: string;
urlPathname: string | undefined; urlPathname: string | undefined;
bundler: { type: "vite" } | { type: "webpack" }; bundler: "vite" | "webpack";
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -20,7 +20,7 @@ export function replaceImportsInJsCode(params: {
const { jsCode, buildContext } = params; const { jsCode, buildContext } = params;
const { fixedJsCode } = (() => { const { fixedJsCode } = (() => {
switch (buildContext.bundler.type) { switch (buildContext.bundler) {
case "vite": case "vite":
return replaceImportsInJsCode_vite({ return replaceImportsInJsCode_vite({
jsCode, jsCode,

View File

@ -179,6 +179,20 @@ program
} }
}); });
program
.command({
name: "initialize-account-theme",
description: "Initialize the account theme."
})
.task({
skip,
handler: async cliCommandOptions => {
const { command } = await import("./initialize-account-theme");
await command({ cliCommandOptions });
}
});
program program
.command({ .command({
name: "copy-keycloak-resources-to-public", name: "copy-keycloak-resources-to-public",

View File

@ -14,17 +14,16 @@ import { assert, type Equals } from "tsafe/assert";
import * as child_process from "child_process"; import * as child_process from "child_process";
import { import {
VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES, VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES,
BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME,
LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT
} from "./constants"; } from "./constants";
import type { KeycloakVersionRange } from "./KeycloakVersionRange"; import type { KeycloakVersionRange } from "./KeycloakVersionRange";
import { exclude } from "tsafe"; import { exclude } from "tsafe";
import { crawl } from "../tools/crawl"; import { crawl } from "../tools/crawl";
import { THEME_TYPES } from "./constants"; import { THEME_TYPES } from "./constants";
import { objectFromEntries } from "tsafe/objectFromEntries";
import { objectEntries } from "tsafe/objectEntries"; import { objectEntries } from "tsafe/objectEntries";
import { type ThemeType } from "./constants"; import { type ThemeType } from "./constants";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
import { symToStr } from "tsafe/symToStr";
import chalk from "chalk"; import chalk from "chalk";
import { getProxyFetchOptions, type ProxyFetchOptions } from "../tools/fetchProxyOptions"; import { getProxyFetchOptions, type ProxyFetchOptions } from "../tools/fetchProxyOptions";
@ -49,23 +48,23 @@ export type BuildContext = {
kcContextExclusionsFtlCode: string | undefined; kcContextExclusionsFtlCode: string | undefined;
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string; themeSrcDirPath: string;
recordIsImplementedByThemeType: Readonly<Record<ThemeType | "email", boolean>>; implementedThemeTypes: {
login: { isImplemented: boolean };
email: { isImplemented: boolean };
account:
| { isImplemented: false }
| { isImplemented: true; type: "Single-Page" | "Multi-Page" };
};
packageJsonFilePath: string;
bundler: "vite" | "webpack";
jarTargets: { jarTargets: {
keycloakVersionRange: KeycloakVersionRange; keycloakVersionRange: KeycloakVersionRange;
jarFileBasename: string; jarFileBasename: string;
}[]; }[];
bundler:
| {
type: "vite";
}
| {
type: "webpack";
packageJsonDirPath: string;
packageJsonScripts: Record<string, string>;
};
doUseAccountV3: boolean;
}; };
assert<Equals<keyof BuildContext["implementedThemeTypes"], ThemeType | "email">>();
export type BuildOptions = { export type BuildOptions = {
themeName?: string | string[]; themeName?: string | string[];
themeVersion?: string; themeVersion?: string;
@ -76,21 +75,30 @@ export type BuildOptions = {
loginThemeResourcesFromKeycloakVersion?: string; loginThemeResourcesFromKeycloakVersion?: string;
keycloakifyBuildDirPath?: string; keycloakifyBuildDirPath?: string;
kcContextExclusionsFtl?: string; kcContextExclusionsFtl?: string;
/** https://docs.keycloakify.dev/v/v10/targetting-specific-keycloak-versions */ } & BuildOptions.AccountThemeImplAndKeycloakVersionTargets;
keycloakVersionTargets?: BuildOptions.KeycloakVersionTargets;
doUseAccountV3?: boolean;
};
export namespace BuildOptions { export namespace BuildOptions {
export type KeycloakVersionTargets = export type AccountThemeImplAndKeycloakVersionTargets =
| ({ hasAccountTheme: true } & Record< | AccountThemeImplAndKeycloakVersionTargets.MultiPageApp
| AccountThemeImplAndKeycloakVersionTargets.SinglePageAppOrNone;
export namespace AccountThemeImplAndKeycloakVersionTargets {
export type MultiPageApp = {
accountThemeImplementation: "Multi-Page";
keycloakVersionTargets?: Record<
KeycloakVersionRange.WithAccountV1Theme, KeycloakVersionRange.WithAccountV1Theme,
string | boolean string | boolean
>) >;
| ({ hasAccountTheme: false } & Record< };
export type SinglePageAppOrNone = {
accountThemeImplementation: "Single-Page" | "none";
keycloakVersionTargets?: Record<
KeycloakVersionRange.WithoutAccountV1Theme, KeycloakVersionRange.WithoutAccountV1Theme,
string | boolean string | boolean
>); >;
};
}
} }
export type ResolvedViteConfig = { export type ResolvedViteConfig = {
@ -231,7 +239,6 @@ export function getBuildContext(params: {
projectBuildDirPath?: string; projectBuildDirPath?: string;
staticDirPathInProjectBuildDirPath?: string; staticDirPathInProjectBuildDirPath?: string;
publicDirPath?: string; publicDirPath?: string;
doUseAccountV3?: boolean;
}; };
type ParsedPackageJson = { type ParsedPackageJson = {
@ -241,20 +248,66 @@ export function getBuildContext(params: {
keycloakify?: BuildOptions_packageJson; keycloakify?: BuildOptions_packageJson;
}; };
const zParsedPackageJson = z.object({ const zMultiPageApp = (() => {
name: z.string().optional(), type TargetType =
version: z.string().optional(), BuildOptions.AccountThemeImplAndKeycloakVersionTargets.MultiPageApp;
homepage: z.string().optional(),
keycloakify: id<z.ZodType<BuildOptions_packageJson>>( const zTargetType = z.object({
(() => { accountThemeImplementation: z.literal("Multi-Page"),
const zBuildOptions_packageJson = z.object({ keycloakVersionTargets: z
extraThemeProperties: z.array(z.string()).optional(), .object({
artifactId: z.string().optional(), "21-and-below": z.union([z.boolean(), z.string()]),
groupId: z.string().optional(), "23": z.union([z.boolean(), z.string()]),
loginThemeResourcesFromKeycloakVersion: z.string().optional(), "24": z.union([z.boolean(), z.string()]),
projectBuildDirPath: z.string().optional(), "25-and-above": z.union([z.boolean(), z.string()])
keycloakifyBuildDirPath: z.string().optional(), })
kcContextExclusionsFtl: z.string().optional(), .optional()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
const zSinglePageApp = (() => {
type TargetType =
BuildOptions.AccountThemeImplAndKeycloakVersionTargets.SinglePageAppOrNone;
const zTargetType = z.object({
accountThemeImplementation: z.union([
z.literal("Single-Page"),
z.literal("none")
]),
keycloakVersionTargets: z
.object({
"21-and-below": z.union([z.boolean(), z.string()]),
"22-and-above": z.union([z.boolean(), z.string()])
})
.optional()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
const zAccountThemeImplAndKeycloakVersionTargets = (() => {
type TargetType = BuildOptions.AccountThemeImplAndKeycloakVersionTargets;
const zTargetType = z.union([zMultiPageApp, zSinglePageApp]);
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
const zBuildOptions = (() => {
type TargetType = BuildOptions;
const zTargetType = z.intersection(
z.object({
themeName: z.union([z.string(), z.array(z.string())]).optional(),
themeVersion: z.string().optional(),
environmentVariables: z environmentVariables: z
.array( .array(
z.object({ z.object({
@ -263,63 +316,52 @@ export function getBuildContext(params: {
}) })
) )
.optional(), .optional(),
themeName: z.union([z.string(), z.array(z.string())]).optional(), extraThemeProperties: z.array(z.string()).optional(),
themeVersion: z.string().optional(), artifactId: z.string().optional(),
staticDirPathInProjectBuildDirPath: z.string().optional(), groupId: z.string().optional(),
publicDirPath: z.string().optional(), loginThemeResourcesFromKeycloakVersion: z.string().optional(),
keycloakVersionTargets: id< keycloakifyBuildDirPath: z.string().optional(),
z.ZodType<BuildOptions.KeycloakVersionTargets> kcContextExclusionsFtl: z.string().optional()
>(
(() => {
const zKeycloakVersionTargets = z.union([
z.object({
hasAccountTheme: z.literal(true),
"21-and-below": z.union([
z.boolean(),
z.string()
]),
"23": z.union([z.boolean(), z.string()]),
"24": z.union([z.boolean(), z.string()]),
"25-and-above": z.union([z.boolean(), z.string()])
}), }),
zAccountThemeImplAndKeycloakVersionTargets
);
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
const zBuildOptions_packageJson = (() => {
type TargetType = BuildOptions_packageJson;
const zTargetType = z.intersection(
zBuildOptions,
z.object({ z.object({
hasAccountTheme: z.literal(false), projectBuildDirPath: z.string().optional(),
"21-and-below": z.union([ staticDirPathInProjectBuildDirPath: z.string().optional(),
z.boolean(), publicDirPath: z.string().optional()
z.string()
]),
"22-and-above": z.union([z.boolean(), z.string()])
}) })
]); );
{ assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
type Got = z.infer<typeof zKeycloakVersionTargets>;
type Expected = BuildOptions.KeycloakVersionTargets;
assert<Equals<Got, Expected>>();
}
return zKeycloakVersionTargets; return id<z.ZodType<TargetType>>(zTargetType);
})() })();
).optional(),
doUseAccountV3: z.boolean().optional() const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
name: z.string().optional(),
version: z.string().optional(),
homepage: z.string().optional(),
keycloakify: zBuildOptions_packageJson.optional()
}); });
{ assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
type Got = z.infer<typeof zBuildOptions_packageJson>;
type Expected = BuildOptions_packageJson;
assert<Equals<Got, Expected>>();
}
return zBuildOptions_packageJson; return id<z.ZodType<TargetType>>(zTargetType);
})() })();
).optional()
});
{
type Got = z.infer<typeof zParsedPackageJson>;
type Expected = ParsedPackageJson;
assert<Equals<Got, Expected>>();
}
const configurationPackageJsonFilePath = (() => { const configurationPackageJsonFilePath = (() => {
const rootPackageJsonFilePath = pathJoin(projectDirPath, "package.json"); const rootPackageJsonFilePath = pathJoin(projectDirPath, "package.json");
@ -334,17 +376,63 @@ export function getBuildContext(params: {
); );
})(); })();
const buildOptions = { const bundler = resolvedViteConfig !== undefined ? "vite" : "webpack";
...parsedPackageJson.keycloakify,
...resolvedViteConfig?.buildOptions if (bundler === "vite" && parsedPackageJson.keycloakify !== undefined) {
console.error(
chalk.red(
`In vite projects, provide your Keycloakify options in vite.config.ts, not in package.json`
)
);
process.exit(-1);
}
const buildOptions: BuildOptions = (() => {
switch (bundler) {
case "vite":
assert(resolvedViteConfig !== undefined);
return resolvedViteConfig.buildOptions;
case "webpack":
assert(parsedPackageJson.keycloakify !== undefined);
return parsedPackageJson.keycloakify;
}
assert<Equals<typeof bundler, never>>(false);
})();
const implementedThemeTypes: BuildContext["implementedThemeTypes"] = {
login: {
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "login"))
},
email: {
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "email"))
},
account: (() => {
if (buildOptions.accountThemeImplementation === "none") {
return { isImplemented: false };
}
return {
isImplemented: true,
type: buildOptions.accountThemeImplementation
};
})()
}; };
const recordIsImplementedByThemeType = objectFromEntries( if (
(["login", "account", "email"] as const).map(themeType => [ implementedThemeTypes.account.isImplemented &&
themeType, !fs.existsSync(pathJoin(themeSrcDirPath, "account"))
fs.existsSync(pathJoin(themeSrcDirPath, themeType)) ) {
]) console.error(
chalk.red(
[
`You have set 'accountThemeImplementation' to '${implementedThemeTypes.account.type}'`,
`but the 'account' directory is missing in your theme source directory`,
"Use the `npx keycloakify initialize-account-theme` command to create it"
].join(" ")
)
); );
process.exit(-1);
}
const themeNames = ((): [string, ...string[]] => { const themeNames = ((): [string, ...string[]] => {
if (buildOptions.themeName === undefined) { if (buildOptions.themeName === undefined) {
@ -371,13 +459,15 @@ export function getBuildContext(params: {
const projectBuildDirPath = (() => { const projectBuildDirPath = (() => {
webpack: { webpack: {
if (resolvedViteConfig !== undefined) { if (bundler !== "webpack") {
break webpack; break webpack;
} }
if (buildOptions.projectBuildDirPath !== undefined) { assert(parsedPackageJson.keycloakify !== undefined);
if (parsedPackageJson.keycloakify.projectBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({ return getAbsoluteAndInOsFormatPath({
pathIsh: buildOptions.projectBuildDirPath, pathIsh: parsedPackageJson.keycloakify.projectBuildDirPath,
cwd: projectDirPath cwd: projectDirPath
}); });
} }
@ -385,34 +475,15 @@ export function getBuildContext(params: {
return pathJoin(projectDirPath, "build"); return pathJoin(projectDirPath, "build");
} }
assert(bundler === "vite");
assert(resolvedViteConfig !== undefined);
return pathJoin(projectDirPath, resolvedViteConfig.buildDir); return pathJoin(projectDirPath, resolvedViteConfig.buildDir);
})(); })();
const bundler = resolvedViteConfig !== undefined ? "vite" : "webpack";
const doUseAccountV3 = buildOptions.doUseAccountV3 ?? false;
return { return {
bundler: bundler,
resolvedViteConfig !== undefined packageJsonFilePath,
? { type: "vite" }
: (() => {
const { scripts } = z
.object({
scripts: z.record(z.string()).optional()
})
.parse(
JSON.parse(
fs.readFileSync(packageJsonFilePath).toString("utf8")
)
);
return {
type: "webpack",
packageJsonDirPath: pathDirname(packageJsonFilePath),
packageJsonScripts: scripts ?? {}
};
})(),
themeVersion: buildOptions.themeVersion ?? parsedPackageJson.version ?? "0.0.0", themeVersion: buildOptions.themeVersion ?? parsedPackageJson.version ?? "0.0.0",
themeNames, themeNames,
extraThemeProperties: buildOptions.extraThemeProperties, extraThemeProperties: buildOptions.extraThemeProperties,
@ -436,7 +507,8 @@ export function getBuildContext(params: {
buildOptions.artifactId ?? buildOptions.artifactId ??
`${themeNames[0]}-keycloak-theme`, `${themeNames[0]}-keycloak-theme`,
loginThemeResourcesFromKeycloakVersion: loginThemeResourcesFromKeycloakVersion:
buildOptions.loginThemeResourcesFromKeycloakVersion ?? "24.0.4", buildOptions.loginThemeResourcesFromKeycloakVersion ??
LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT,
projectDirPath, projectDirPath,
projectBuildDirPath, projectBuildDirPath,
keycloakifyBuildDirPath: (() => { keycloakifyBuildDirPath: (() => {
@ -463,13 +535,15 @@ export function getBuildContext(params: {
} }
webpack: { webpack: {
if (resolvedViteConfig !== undefined) { if (bundler !== "webpack") {
break webpack; break webpack;
} }
if (buildOptions.publicDirPath !== undefined) { assert(parsedPackageJson.keycloakify !== undefined);
if (parsedPackageJson.keycloakify.publicDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({ return getAbsoluteAndInOsFormatPath({
pathIsh: buildOptions.publicDirPath, pathIsh: parsedPackageJson.keycloakify.publicDirPath,
cwd: projectDirPath cwd: projectDirPath
}); });
} }
@ -477,10 +551,12 @@ export function getBuildContext(params: {
return pathJoin(projectDirPath, "public"); return pathJoin(projectDirPath, "public");
} }
assert(bundler === "vite");
assert(resolvedViteConfig !== undefined);
return pathJoin(projectDirPath, resolvedViteConfig.publicDir); return pathJoin(projectDirPath, resolvedViteConfig.publicDir);
})(), })(),
cacheDirPath: (() => { cacheDirPath: pathJoin(
const cacheDirPath = pathJoin(
(() => { (() => {
if (process.env.XDG_CACHE_HOME !== undefined) { if (process.env.XDG_CACHE_HOME !== undefined) {
return getAbsoluteAndInOsFormatPath({ return getAbsoluteAndInOsFormatPath({
@ -496,13 +572,10 @@ export function getBuildContext(params: {
); );
})(), })(),
"keycloakify" "keycloakify"
); ),
return cacheDirPath;
})(),
urlPathname: (() => { urlPathname: (() => {
webpack: { webpack: {
if (resolvedViteConfig !== undefined) { if (bundler !== "webpack") {
break webpack; break webpack;
} }
@ -522,23 +595,35 @@ export function getBuildContext(params: {
return out === "/" ? undefined : out; return out === "/" ? undefined : out;
} }
assert(bundler === "vite");
assert(resolvedViteConfig !== undefined);
return resolvedViteConfig.urlPathname; return resolvedViteConfig.urlPathname;
})(), })(),
assetsDirPath: (() => { assetsDirPath: (() => {
webpack: { webpack: {
if (resolvedViteConfig !== undefined) { if (bundler !== "webpack") {
break webpack; break webpack;
} }
if (buildOptions.staticDirPathInProjectBuildDirPath !== undefined) { assert(parsedPackageJson.keycloakify !== undefined);
if (
parsedPackageJson.keycloakify.staticDirPathInProjectBuildDirPath !==
undefined
) {
getAbsoluteAndInOsFormatPath({ getAbsoluteAndInOsFormatPath({
pathIsh: buildOptions.staticDirPathInProjectBuildDirPath, pathIsh:
parsedPackageJson.keycloakify
.staticDirPathInProjectBuildDirPath,
cwd: projectBuildDirPath cwd: projectBuildDirPath
}); });
} }
return pathJoin(projectBuildDirPath, "static"); return pathJoin(projectBuildDirPath, "static");
} }
assert(bundler === "vite");
assert(resolvedViteConfig !== undefined);
return pathJoin(projectBuildDirPath, resolvedViteConfig.assetsDir); return pathJoin(projectBuildDirPath, resolvedViteConfig.assetsDir);
})(), })(),
@ -559,7 +644,7 @@ export function getBuildContext(params: {
return buildOptions.kcContextExclusionsFtl; return buildOptions.kcContextExclusionsFtl;
})(), })(),
environmentVariables: buildOptions.environmentVariables ?? [], environmentVariables: buildOptions.environmentVariables ?? [],
recordIsImplementedByThemeType, implementedThemeTypes,
themeSrcDirPath, themeSrcDirPath,
fetchOptions: getProxyFetchOptions({ fetchOptions: getProxyFetchOptions({
npmConfigGetCwd: (function callee(upCount: number): string { npmConfigGetCwd: (function callee(upCount: number): string {
@ -613,10 +698,10 @@ export function getBuildContext(params: {
} }
const keycloakVersionRange: KeycloakVersionRange = (() => { const keycloakVersionRange: KeycloakVersionRange = (() => {
const doesImplementAccountV1Theme = if (
!doUseAccountV3 && recordIsImplementedByThemeType.account; implementedThemeTypes.account.isImplemented &&
implementedThemeTypes.account.type === "Multi-Page"
if (doesImplementAccountV1Theme) { ) {
const keycloakVersionRange = (() => { const keycloakVersionRange = (() => {
if (buildForKeycloakMajorVersionNumber <= 21) { if (buildForKeycloakMajorVersionNumber <= 21) {
return "21-and-below" as const; return "21-and-below" as const;
@ -703,7 +788,10 @@ export function getBuildContext(params: {
const jarTargets_default = (() => { const jarTargets_default = (() => {
const jarTargets: BuildContext["jarTargets"] = []; const jarTargets: BuildContext["jarTargets"] = [];
if (!doUseAccountV3 && recordIsImplementedByThemeType.account) { if (
implementedThemeTypes.account.isImplemented &&
implementedThemeTypes.account.type === "Multi-Page"
) {
for (const keycloakVersionRange of [ for (const keycloakVersionRange of [
"21-and-below", "21-and-below",
"23", "23",
@ -748,79 +836,11 @@ export function getBuildContext(params: {
return jarTargets_default; return jarTargets_default;
} }
if (
buildOptions.keycloakVersionTargets.hasAccountTheme !== doUseAccountV3
? false
: recordIsImplementedByThemeType.account
) {
console.log(
chalk.red(
(() => {
const { keycloakVersionTargets } = buildOptions;
let message = `Bad ${symToStr({ keycloakVersionTargets })} configuration.\n`;
if (keycloakVersionTargets.hasAccountTheme) {
message +=
"Your codebase does not seem to implement an account theme ";
} else {
message += "Your codebase implements an account theme ";
}
const { hasAccountTheme } = keycloakVersionTargets;
message += `but you have set ${symToStr({ keycloakVersionTargets })}.${symToStr({ hasAccountTheme })}`;
message += ` to ${hasAccountTheme} in your `;
message += (() => {
switch (bundler) {
case "vite":
return "vite.config.ts";
case "webpack":
return "package.json";
}
assert<Equals<typeof bundler, never>>(false);
})();
message += `. Please set it to ${!hasAccountTheme} `;
message +=
"and fill up the relevant keycloak version ranges.\n";
message += "Example:\n";
message += JSON.stringify(
id<Pick<BuildOptions, "keycloakVersionTargets">>({
keycloakVersionTargets: {
hasAccountTheme:
recordIsImplementedByThemeType.account,
...objectFromEntries(
jarTargets_default.map(
({
keycloakVersionRange,
jarFileBasename
}) => [
keycloakVersionRange,
jarFileBasename
]
)
)
}
}),
null,
2
);
message +=
"\nSee: https://docs.keycloakify.dev/v/v10/targetting-specific-keycloak-versions";
return message;
})()
)
);
process.exit(1);
}
const jarTargets: BuildContext["jarTargets"] = []; const jarTargets: BuildContext["jarTargets"] = [];
const { hasAccountTheme, ...rest } = buildOptions.keycloakVersionTargets; for (const [keycloakVersionRange, jarNameOrBoolean] of objectEntries(
buildOptions.keycloakVersionTargets
for (const [keycloakVersionRange, jarNameOrBoolean] of objectEntries(rest)) { )) {
if (jarNameOrBoolean === false) { if (jarNameOrBoolean === false) {
continue; continue;
} }
@ -871,7 +891,6 @@ export function getBuildContext(params: {
} }
return jarTargets; return jarTargets;
})(), })()
doUseAccountV3
}; };
} }

View File

@ -69,3 +69,5 @@ 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 LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT = "24.0.4";

View File

@ -1,14 +1,21 @@
import { assert } from "tsafe/assert"; import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import type { BuildContext } from "./buildContext"; import type { BuildContext } from "./buildContext";
import * as fs from "fs/promises"; import * as fs from "fs/promises";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import { existsAsync } from "../tools/fs.existsAsync"; import { existsAsync } from "../tools/fs.existsAsync";
import { z } from "zod";
export type BuildContextLike = { export type BuildContextLike = {
projectDirPath: string; projectDirPath: string;
themeNames: string[]; themeNames: string[];
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string; themeSrcDirPath: string;
implementedThemeTypes: Pick<
BuildContext["implementedThemeTypes"],
"login" | "account"
>;
packageJsonFilePath: string;
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -18,12 +25,53 @@ export async function generateKcGenTs(params: {
}): Promise<void> { }): Promise<void> {
const { buildContext } = params; const { buildContext } = params;
const filePath = pathJoin(buildContext.themeSrcDirPath, "kc.gen.ts"); const isReactProject: boolean = await (async () => {
const parsedPackageJson = await (async () => {
type ParsedPackageJson = {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
dependencies: z.record(z.string()).optional(),
devDependencies: z.record(z.string()).optional()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
return zParsedPackageJson.parse(
JSON.parse(
(await fs.readFile(buildContext.packageJsonFilePath)).toString("utf8")
)
);
})();
return (
{
...parsedPackageJson.dependencies,
...parsedPackageJson.devDependencies
}.react !== undefined
);
})();
const filePath = pathJoin(
buildContext.themeSrcDirPath,
`kc.gen.ts${isReactProject ? "x" : ""}`
);
const currentContent = (await existsAsync(filePath)) const currentContent = (await existsAsync(filePath))
? await fs.readFile(filePath) ? await fs.readFile(filePath)
: undefined; : undefined;
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
const newContent = Buffer.from( const newContent = Buffer.from(
[ [
`/* prettier-ignore-start */`, `/* prettier-ignore-start */`,
@ -36,6 +84,8 @@ export async function generateKcGenTs(params: {
``, ``,
`// This file is auto-generated by Keycloakify`, `// This file is auto-generated by Keycloakify`,
``, ``,
isReactProject && `import { lazy, Suspense, type ReactNode } from "react";`,
``,
`export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`, `export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
``, ``,
`export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`, `export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`,
@ -54,9 +104,52 @@ export async function generateKcGenTs(params: {
2 2
)};`, )};`,
``, ``,
`export type KcContext =`,
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
` ;`,
``,
`declare global {`,
` interface Window {`,
` kcContext?: KcContext;`,
` }`,
`}`,
``,
...(!isReactProject
? []
: [
hasLoginTheme &&
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
hasAccountTheme &&
`export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
``,
`export function KcPage(`,
` props: {`,
` kcContext: KcContext;`,
` fallback?: ReactNode;`,
` }`,
`) {`,
` const { kcContext, fallback } = props;`,
` return (`,
` <Suspense fallback={fallback}>`,
` {(() => {`,
` switch (kcContext.themeType) {`,
hasLoginTheme &&
` case "login": return <KcLoginPage kcContext={kcContext} />;`,
hasAccountTheme &&
` case "account": return <KcAccountPage kcContext={kcContext} />;`,
` }`,
` })()}`,
` </Suspense>`,
` );`,
`}`
]),
``,
`/* prettier-ignore-end */`, `/* prettier-ignore-end */`,
`` ``
].join("\n"), ]
.filter(item => typeof item === "string")
.join("\n"),
"utf8" "utf8"
); );
@ -65,4 +158,18 @@ export async function generateKcGenTs(params: {
} }
await fs.writeFile(filePath, newContent); await fs.writeFile(filePath, newContent);
delete_legacy_file: {
if (!isReactProject) {
break delete_legacy_file;
}
const legacyFilePath = filePath.replace(/tsx$/, "ts");
if (!(await existsAsync(legacyFilePath))) {
break delete_legacy_file;
}
await fs.unlink(legacyFilePath);
}
} }

View File

@ -0,0 +1,180 @@
import { getLatestsSemVersionedTagFactory } from "../tools/octokit-addons/getLatestsSemVersionedTag";
import { Octokit } from "@octokit/rest";
import type { ReturnType } from "tsafe";
import type { Param0 } from "tsafe";
import { join as pathJoin, dirname as pathDirname } from "path";
import * as fs from "fs";
import { z } from "zod";
import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import type { SemVer } from "../tools/SemVer";
import { same } from "evt/tools/inDepth/same";
type GetLatestsSemVersionedTag = ReturnType<
typeof getLatestsSemVersionedTagFactory
>["getLatestsSemVersionedTag"];
type Params = Param0<GetLatestsSemVersionedTag>;
type R = ReturnType<GetLatestsSemVersionedTag>;
let getLatestsSemVersionedTag_stateless: GetLatestsSemVersionedTag | undefined =
undefined;
const CACHE_VERSION = 1;
type Cache = {
version: typeof CACHE_VERSION;
entries: {
time: number;
params: Params;
result: R;
}[];
};
export async function getLatestsSemVersionedTag({
cacheDirPath,
...params
}: Params & { cacheDirPath: string }): Promise<R> {
const cacheFilePath = pathJoin(cacheDirPath, "latest-sem-versioned-tags.json");
const cacheLookupResult = (() => {
const getResult_currentCache = (currentCacheEntries: Cache["entries"]) => ({
hasCachedResult: false as const,
currentCache: {
version: CACHE_VERSION,
entries: currentCacheEntries
}
});
if (!fs.existsSync(cacheFilePath)) {
return getResult_currentCache([]);
}
let cache_json;
try {
cache_json = fs.readFileSync(cacheFilePath).toString("utf8");
} catch {
return getResult_currentCache([]);
}
let cache_json_parsed: unknown;
try {
cache_json_parsed = JSON.parse(cache_json);
} catch {
return getResult_currentCache([]);
}
const zSemVer = (() => {
type TargetType = SemVer;
const zTargetType = z.object({
major: z.number(),
minor: z.number(),
patch: z.number(),
rc: z.number().optional(),
parsedFrom: z.string()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
const zCache = (() => {
type TargetType = Cache;
const zTargetType = z.object({
version: z.literal(CACHE_VERSION),
entries: z.array(
z.object({
time: z.number(),
params: z.object({
owner: z.string(),
repo: z.string(),
count: z.number(),
doIgnoreReleaseCandidates: z.boolean()
}),
result: z.array(
z.object({
tag: z.string(),
version: zSemVer
})
)
})
)
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
let cache: Cache;
try {
cache = zCache.parse(cache_json_parsed);
} catch {
return getResult_currentCache([]);
}
const cacheEntry = cache.entries.find(e => same(e.params, params));
if (cacheEntry === undefined) {
return getResult_currentCache(cache.entries);
}
if (Date.now() - cacheEntry.time > 3_600_000) {
return getResult_currentCache(cache.entries.filter(e => e !== cacheEntry));
}
return {
hasCachedResult: true as const,
cachedResult: cacheEntry.result
};
})();
if (cacheLookupResult.hasCachedResult) {
return cacheLookupResult.cachedResult;
}
const { currentCache } = cacheLookupResult;
getLatestsSemVersionedTag_stateless ??= (() => {
const octokit = (() => {
const githubToken = process.env.GITHUB_TOKEN;
const octokit = new Octokit(
githubToken === undefined ? undefined : { auth: githubToken }
);
return octokit;
})();
const { getLatestsSemVersionedTag } = getLatestsSemVersionedTagFactory({
octokit
});
return getLatestsSemVersionedTag;
})();
const result = await getLatestsSemVersionedTag_stateless(params);
currentCache.entries.push({
time: Date.now(),
params,
result
});
{
const dirPath = pathDirname(cacheFilePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
fs.writeFileSync(cacheFilePath, JSON.stringify(currentCache, null, 2));
return result;
}

View File

@ -1,11 +1,6 @@
import { getLatestsSemVersionedTagFactory } from "../tools/octokit-addons/getLatestsSemVersionedTag"; import { getLatestsSemVersionedTag } from "./getLatestsSemVersionedTag";
import { Octokit } from "@octokit/rest";
import cliSelect from "cli-select"; import cliSelect from "cli-select";
import { SemVer } from "../tools/SemVer"; import { SemVer } from "../tools/SemVer";
import { join as pathJoin, dirname as pathDirname } from "path";
import * as fs from "fs";
import type { ReturnType } from "tsafe";
import { id } from "tsafe/id";
export async function promptKeycloakVersion(params: { export async function promptKeycloakVersion(params: {
startingFromMajor: number | undefined; startingFromMajor: number | undefined;
@ -14,80 +9,16 @@ export async function promptKeycloakVersion(params: {
}) { }) {
const { startingFromMajor, excludeMajorVersions, cacheDirPath } = params; const { startingFromMajor, excludeMajorVersions, cacheDirPath } = params;
const { getLatestsSemVersionedTag } = (() => {
const { octokit } = (() => {
const githubToken = process.env.GITHUB_TOKEN;
const octokit = new Octokit(
githubToken === undefined ? undefined : { auth: githubToken }
);
return { octokit };
})();
const { getLatestsSemVersionedTag } = getLatestsSemVersionedTagFactory({
octokit
});
return { getLatestsSemVersionedTag };
})();
const semVersionedTagByMajor = new Map<number, { tag: string; version: SemVer }>(); const semVersionedTagByMajor = new Map<number, { tag: string; version: SemVer }>();
const semVersionedTags = await (async () => {
const cacheFilePath = pathJoin(cacheDirPath, "keycloak-versions.json");
type Cache = {
time: number;
semVersionedTags: ReturnType<typeof getLatestsSemVersionedTag>;
};
use_cache: {
if (!fs.existsSync(cacheFilePath)) {
break use_cache;
}
const cache: Cache = JSON.parse(
fs.readFileSync(cacheFilePath).toString("utf8")
);
if (Date.now() - cache.time > 3_600_000) {
fs.unlinkSync(cacheFilePath);
break use_cache;
}
return cache.semVersionedTags;
}
const semVersionedTags = await getLatestsSemVersionedTag({ const semVersionedTags = await getLatestsSemVersionedTag({
cacheDirPath,
count: 50, count: 50,
owner: "keycloak", owner: "keycloak",
repo: "keycloak" repo: "keycloak",
doIgnoreReleaseCandidates: true
}); });
{
const dirPath = pathDirname(cacheFilePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
fs.writeFileSync(
cacheFilePath,
JSON.stringify(
id<Cache>({
time: Date.now(),
semVersionedTags
}),
null,
2
)
);
return semVersionedTags;
})();
semVersionedTags.forEach(semVersionedTag => { semVersionedTags.forEach(semVersionedTag => {
if ( if (
startingFromMajor !== undefined && startingFromMajor !== undefined &&

View File

@ -5,12 +5,15 @@ import type { BuildContext } from "../shared/buildContext";
import chalk from "chalk"; import chalk from "chalk";
import { sep as pathSep, join as pathJoin } from "path"; import { sep as pathSep, join as pathJoin } from "path";
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath"; import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
import * as fs from "fs";
import { dirname as pathDirname, relative as pathRelative } from "path";
export type BuildContextLike = { export type BuildContextLike = {
projectDirPath: string; projectDirPath: string;
keycloakifyBuildDirPath: string; keycloakifyBuildDirPath: string;
bundler: BuildContext["bundler"]; bundler: BuildContext["bundler"];
projectBuildDirPath: string; projectBuildDirPath: string;
packageJsonFilePath: string;
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -20,7 +23,7 @@ export async function appBuild(params: {
}): Promise<{ isAppBuildSuccess: boolean }> { }): Promise<{ isAppBuildSuccess: boolean }> {
const { buildContext } = params; const { buildContext } = params;
switch (buildContext.bundler.type) { switch (buildContext.bundler) {
case "vite": case "vite":
return appBuild_vite({ buildContext }); return appBuild_vite({ buildContext });
case "webpack": case "webpack":
@ -33,7 +36,7 @@ async function appBuild_vite(params: {
}): Promise<{ isAppBuildSuccess: boolean }> { }): Promise<{ isAppBuildSuccess: boolean }> {
const { buildContext } = params; const { buildContext } = params;
assert(buildContext.bundler.type === "vite"); assert(buildContext.bundler === "vite");
const dIsSuccess = new Deferred<boolean>(); const dIsSuccess = new Deferred<boolean>();
@ -66,17 +69,18 @@ async function appBuild_webpack(params: {
}): Promise<{ isAppBuildSuccess: boolean }> { }): Promise<{ isAppBuildSuccess: boolean }> {
const { buildContext } = params; const { buildContext } = params;
assert(buildContext.bundler.type === "webpack"); assert(buildContext.bundler === "webpack");
const entries = Object.entries(buildContext.bundler.packageJsonScripts).filter( const entries = Object.entries(
([, scriptCommand]) => scriptCommand.includes("keycloakify build") (JSON.parse(fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8"))
); .scripts ?? {}) as Record<string, string>
).filter(([, scriptCommand]) => scriptCommand.includes("keycloakify build"));
if (entries.length === 0) { if (entries.length === 0) {
console.log( console.log(
chalk.red( chalk.red(
[ [
`You should have a script in your package.json at ${buildContext.bundler.packageJsonDirPath}`, `You should have a script in your package.json at ${pathRelative(process.cwd(), pathDirname(buildContext.packageJsonFilePath))}`,
`that includes the 'keycloakify build' command` `that includes the 'keycloakify build' command`
].join(" ") ].join(" ")
) )
@ -123,7 +127,7 @@ async function appBuild_webpack(params: {
process.exit(-1); process.exit(-1);
} }
let commandCwd = buildContext.bundler.packageJsonDirPath; let commandCwd = pathDirname(buildContext.packageJsonFilePath);
for (const subCommand of appBuildSubCommands) { for (const subCommand of appBuildSubCommands) {
const dIsSuccess = new Deferred<boolean>(); const dIsSuccess = new Deferred<boolean>();
@ -152,7 +156,7 @@ async function appBuild_webpack(params: {
return [ return [
pathJoin( pathJoin(
buildContext.bundler.packageJsonDirPath, pathDirname(buildContext.packageJsonFilePath),
"node_modules", "node_modules",
".bin" ".bin"
), ),

View File

@ -63,7 +63,7 @@ export async function downloadAndExtractArchive(params: {
}); });
} }
const extractDirBasename = `${archiveFileBasename.split(".")[0]}_${uniqueIdOfOnArchiveFile}_${crypto const extractDirBasename = `${archiveFileBasename.replace(/\.([^.]+)$/, (...[, ext]) => `_${ext}`)}_${uniqueIdOfOnArchiveFile}_${crypto
.createHash("sha256") .createHash("sha256")
.update(onArchiveFile.toString()) .update(onArchiveFile.toString())
.digest("hex") .digest("hex")
@ -85,7 +85,9 @@ export async function downloadAndExtractArchive(params: {
})() })()
) )
.map(async extractDirBasename => { .map(async extractDirBasename => {
await rm(pathJoin(cacheDirPath, extractDirBasename), { recursive: true }); await rm(pathJoin(cacheDirPath, extractDirBasename), {
recursive: true
});
await SuccessTracker.removeFromExtracted({ await SuccessTracker.removeFromExtracted({
cacheDirPath, cacheDirPath,
extractDirBasename extractDirBasename

View File

@ -0,0 +1,63 @@
import * as fs from "fs";
import { join as pathJoin } from "path";
import * as child_process from "child_process";
import chalk from "chalk";
export function npmInstall(params: { packageJsonDirPath: string }) {
const { packageJsonDirPath } = params;
const packageManagerBinName = (() => {
const packageMangers = [
{
binName: "yarn",
lockFileBasename: "yarn.lock"
},
{
binName: "npm",
lockFileBasename: "package-lock.json"
},
{
binName: "pnpm",
lockFileBasename: "pnpm-lock.yaml"
},
{
binName: "bun",
lockFileBasename: "bun.lockdb"
}
] as const;
for (const packageManager of packageMangers) {
if (
fs.existsSync(
pathJoin(packageJsonDirPath, packageManager.lockFileBasename)
) ||
fs.existsSync(pathJoin(process.cwd(), packageManager.lockFileBasename))
) {
return packageManager.binName;
}
}
return undefined;
})();
install_dependencies: {
if (packageManagerBinName === undefined) {
break install_dependencies;
}
console.log(`Installing the new dependencies...`);
try {
child_process.execSync(`${packageManagerBinName} install`, {
cwd: packageJsonDirPath,
stdio: "inherit"
});
} catch {
console.log(
chalk.yellow(
`\`${packageManagerBinName} install\` failed, continuing anyway...`
)
);
}
}
}

View File

@ -9,13 +9,14 @@ export function getLatestsSemVersionedTagFactory(params: { octokit: Octokit }) {
owner: string; owner: string;
repo: string; repo: string;
count: number; count: number;
doIgnoreReleaseCandidates: boolean;
}): Promise< }): Promise<
{ {
tag: string; tag: string;
version: SemVer; version: SemVer;
}[] }[]
> { > {
const { owner, repo, count } = params; const { owner, repo, count, doIgnoreReleaseCandidates } = params;
const semVersionedTags: { tag: string; version: SemVer }[] = []; const semVersionedTags: { tag: string; version: SemVer }[] = [];
@ -30,7 +31,7 @@ export function getLatestsSemVersionedTagFactory(params: { octokit: Octokit }) {
continue; continue;
} }
if (version.rc !== undefined) { if (doIgnoreReleaseCandidates && version.rc !== undefined) {
continue; continue;
} }

View File

@ -8,5 +8,7 @@
"moduleResolution": "node", "moduleResolution": "node",
"outDir": "../../dist/bin", "outDir": "../../dist/bin",
"rootDir": "." "rootDir": "."
} },
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["initialize-account-theme/src"]
} }

View File

@ -71,14 +71,10 @@ export function createGetKcClsx<ClassKey extends string>(params: {
transform: classKey => { transform: classKey => {
assert(is<ClassKey>(classKey)); assert(is<ClassKey>(classKey));
const className = classes?.[classKey];
return clsx( return clsx(
classKey, classKey,
doUseDefaultCss && !className?.split(" ").includes("CLEAR") classes?.[classKey] ??
? defaultClasses[classKey] (doUseDefaultCss ? defaultClasses[classKey] : undefined)
: undefined,
className
); );
} }
}); });

View File

@ -1,5 +1,5 @@
import { useEffect, useReducer, Fragment } from "react"; import { useEffect, useReducer, Fragment } from "react";
import { assert } from "tsafe/assert"; import { assert } from "keycloakify/tools/assert";
import type { KcClsx } from "keycloakify/login/lib/kcClsx"; import type { KcClsx } from "keycloakify/login/lib/kcClsx";
import { import {
useUserProfileForm, useUserProfileForm,
@ -70,7 +70,7 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps<
{advancedMsg(attribute.annotations.inputHelperTextBefore)} {advancedMsg(attribute.annotations.inputHelperTextBefore)}
</div> </div>
)} )}
<InputFiledByType <InputFieldByType
attribute={attribute} attribute={attribute}
valueOrValues={valueOrValues} valueOrValues={valueOrValues}
displayableErrors={displayableErrors} displayableErrors={displayableErrors}
@ -196,7 +196,7 @@ function FieldErrors(props: { attribute: Attribute; displayableErrors: FormField
); );
} }
type InputFiledByTypeProps = { type InputFieldByTypeProps = {
attribute: Attribute; attribute: Attribute;
valueOrValues: string | string[]; valueOrValues: string | string[];
displayableErrors: FormFieldError[]; displayableErrors: FormFieldError[];
@ -205,7 +205,7 @@ type InputFiledByTypeProps = {
kcClsx: KcClsx; kcClsx: KcClsx;
}; };
function InputFiledByType(props: InputFiledByTypeProps) { function InputFieldByType(props: InputFieldByTypeProps) {
const { attribute, valueOrValues } = props; const { attribute, valueOrValues } = props;
switch (attribute.annotations.inputType) { switch (attribute.annotations.inputType) {
@ -274,9 +274,11 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
); );
} }
function InputTag(props: InputFiledByTypeProps & { fieldIndex: number | undefined }) { function InputTag(props: InputFieldByTypeProps & { fieldIndex: number | undefined }) {
const { attribute, fieldIndex, kcClsx, dispatchFormAction, valueOrValues, i18n, displayableErrors } = props; const { attribute, fieldIndex, kcClsx, dispatchFormAction, valueOrValues, i18n, displayableErrors } = props;
const { advancedMsgStr } = i18n;
return ( return (
<> <>
<input <input
@ -305,7 +307,9 @@ function InputTag(props: InputFiledByTypeProps & { fieldIndex: number | undefine
aria-invalid={displayableErrors.find(error => error.fieldIndex === fieldIndex) !== undefined} aria-invalid={displayableErrors.find(error => error.fieldIndex === fieldIndex) !== undefined}
disabled={attribute.readOnly} disabled={attribute.readOnly}
autoComplete={attribute.autocomplete} autoComplete={attribute.autocomplete}
placeholder={attribute.annotations.inputTypePlaceholder} placeholder={
attribute.annotations.inputTypePlaceholder === undefined ? undefined : advancedMsgStr(attribute.annotations.inputTypePlaceholder)
}
pattern={attribute.annotations.inputTypePattern} pattern={attribute.annotations.inputTypePattern}
size={attribute.annotations.inputTypeSize === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeSize}`)} size={attribute.annotations.inputTypeSize === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeSize}`)}
maxLength={ maxLength={
@ -429,7 +433,7 @@ function AddRemoveButtonsMultiValuedAttribute(props: {
); );
} }
function InputTagSelects(props: InputFiledByTypeProps) { function InputTagSelects(props: InputFieldByTypeProps) {
const { attribute, dispatchFormAction, kcClsx, valueOrValues } = props; const { attribute, dispatchFormAction, kcClsx, valueOrValues } = props;
const { advancedMsg } = props.i18n; const { advancedMsg } = props.i18n;
@ -537,7 +541,7 @@ function InputTagSelects(props: InputFiledByTypeProps) {
); );
} }
function TextareaTag(props: InputFiledByTypeProps) { function TextareaTag(props: InputFieldByTypeProps) {
const { attribute, dispatchFormAction, kcClsx, displayableErrors, valueOrValues } = props; const { attribute, dispatchFormAction, kcClsx, displayableErrors, valueOrValues } = props;
assert(typeof valueOrValues === "string"); assert(typeof valueOrValues === "string");
@ -573,7 +577,7 @@ function TextareaTag(props: InputFiledByTypeProps) {
); );
} }
function SelectTag(props: InputFiledByTypeProps) { function SelectTag(props: InputFieldByTypeProps) {
const { attribute, dispatchFormAction, kcClsx, displayableErrors, i18n, valueOrValues } = props; const { attribute, dispatchFormAction, kcClsx, displayableErrors, i18n, valueOrValues } = props;
const { advancedMsg } = i18n; const { advancedMsg } = i18n;

View File

@ -429,6 +429,28 @@ export function useUserProfileForm(params: UseUserProfileFormParams): ReturnType
}); });
} }
trigger_password_confirm_validation_on_password_change: {
if (!doMakeUserConfirmPassword) {
break trigger_password_confirm_validation_on_password_change;
}
if (formAction.name !== "password") {
break trigger_password_confirm_validation_on_password_change;
}
state = reducer(state, {
action: "update",
name: "password-confirm",
valueOrValues: (() => {
const formFieldState = state.formFieldStates.find(({ attribute }) => attribute.name === "password-confirm");
assert(formFieldState !== undefined);
return formFieldState.valueOrValues;
})()
});
}
return; return;
case "focus lost": case "focus lost":
if (formFieldState.hasLostFocusAtLeastOnce instanceof Array) { if (formFieldState.hasLostFocusAtLeastOnce instanceof Array) {

View File

@ -32,6 +32,7 @@ export default function LoginUpdatePassword(props: PageProps<Extract<KcContext,
<label htmlFor="password-new" className={kcClsx("kcLabelClass")}> <label htmlFor="password-new" className={kcClsx("kcLabelClass")}>
{msg("passwordNew")} {msg("passwordNew")}
</label> </label>
</div>
<div className={kcClsx("kcInputWrapperClass")}> <div className={kcClsx("kcInputWrapperClass")}>
<PasswordWrapper kcClsx={kcClsx} i18n={i18n} passwordInputId="password-new"> <PasswordWrapper kcClsx={kcClsx} i18n={i18n} passwordInputId="password-new">
<input <input
@ -57,7 +58,6 @@ export default function LoginUpdatePassword(props: PageProps<Extract<KcContext,
)} )}
</div> </div>
</div> </div>
</div>
<div className={kcClsx("kcFormGroupClass")}> <div className={kcClsx("kcFormGroupClass")}>
<div className={kcClsx("kcLabelWrapperClass")}> <div className={kcClsx("kcLabelWrapperClass")}>
@ -89,16 +89,15 @@ export default function LoginUpdatePassword(props: PageProps<Extract<KcContext,
/> />
)} )}
</div> </div>
</div>
<div className={kcClsx("kcFormGroupClass")}> <div className={kcClsx("kcFormGroupClass")}>
<LogoutOtherSessions kcClsx={kcClsx} i18n={i18n} /> <LogoutOtherSessions kcClsx={kcClsx} i18n={i18n} />
<div id="kc-form-buttons" className={kcClsx("kcFormButtonsClass")}> <div id="kc-form-buttons" className={kcClsx("kcFormButtonsClass")}>
<input <input
className={kcClsx( className={kcClsx(
"kcButtonClass", "kcButtonClass",
"kcButtonPrimaryClass", "kcButtonPrimaryClass",
isAppInitiatedAction && "kcButtonBlockClass", !isAppInitiatedAction && "kcButtonBlockClass",
"kcButtonLargeClass" "kcButtonLargeClass"
)} )}
type="submit" type="submit"
@ -116,7 +115,6 @@ export default function LoginUpdatePassword(props: PageProps<Extract<KcContext,
)} )}
</div> </div>
</div> </div>
</div>
</form> </form>
</Template> </Template>
); );

View File

@ -18,12 +18,14 @@ import {
import MagicString from "magic-string"; import MagicString from "magic-string";
import { generateKcGenTs } from "../bin/shared/generateKcGenTs"; import { generateKcGenTs } from "../bin/shared/generateKcGenTs";
export namespace keycloakify {
export type Params = BuildOptions & { export type Params = BuildOptions & {
postBuild?: (buildContext: Omit<BuildContext, "bundler">) => Promise<void>; postBuild?: (buildContext: Omit<BuildContext, "bundler">) => Promise<void>;
}; };
}
export function keycloakify(params?: Params) { export function keycloakify(params: keycloakify.Params) {
const { postBuild, ...buildOptions } = params ?? {}; const { postBuild, ...buildOptions } = params;
let projectDirPath: string | undefined = undefined; let projectDirPath: string | undefined = undefined;
let urlPathname: string | undefined = undefined; let urlPathname: string | undefined = undefined;

View File

@ -16,5 +16,6 @@
// https://github.com/vitejs/vite/issues/15112#issuecomment-1823908010 // https://github.com/vitejs/vite/issues/15112#issuecomment-1823908010
"skipLibCheck": true "skipLibCheck": true
}, },
"include": ["../src", "."] "include": ["../src", "."],
"exclude": ["../src/bin/initialize-account-theme/src"]
} }