keycloakify 10.0.0-rc.120 → 10.0.0-rc.122

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,12 @@ import {
12
12
  } from "./shared/constants";
13
13
  import { capitalize } from "tsafe/capitalize";
14
14
  import * as fs from "fs";
15
- import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
15
+ import {
16
+ join as pathJoin,
17
+ relative as pathRelative,
18
+ dirname as pathDirname,
19
+ basename as pathBasename
20
+ } from "path";
16
21
  import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
17
22
  import { assert, Equals } from "tsafe/assert";
18
23
  import type { CliCommandOptions } from "./main";
@@ -28,11 +33,114 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
28
33
 
29
34
  console.log(chalk.cyan("Theme type:"));
30
35
 
31
- const { value: themeType } = await cliSelect<ThemeType>({
32
- values: [...THEME_TYPES]
33
- }).catch(() => {
34
- process.exit(-1);
35
- });
36
+ const themeType = await (async () => {
37
+ const values = THEME_TYPES.filter(themeType => {
38
+ switch (themeType) {
39
+ case "account":
40
+ return buildContext.implementedThemeTypes.account.isImplemented;
41
+ case "login":
42
+ return buildContext.implementedThemeTypes.login.isImplemented;
43
+ }
44
+ assert<Equals<typeof themeType, never>>(false);
45
+ });
46
+
47
+ assert(values.length > 0, "No theme is implemented in this project");
48
+
49
+ if (values.length === 1) {
50
+ return values[0];
51
+ }
52
+
53
+ const { value } = await cliSelect<ThemeType>({
54
+ values
55
+ }).catch(() => {
56
+ process.exit(-1);
57
+ });
58
+
59
+ return value;
60
+ })();
61
+
62
+ if (
63
+ themeType === "account" &&
64
+ (assert(buildContext.implementedThemeTypes.account.isImplemented),
65
+ buildContext.implementedThemeTypes.account.type === "Single-Page")
66
+ ) {
67
+ const srcDirPath = pathJoin(
68
+ pathDirname(buildContext.packageJsonFilePath),
69
+ "node_modules",
70
+ "@keycloakify",
71
+ "keycloak-account-ui",
72
+ "src"
73
+ );
74
+
75
+ console.log(
76
+ [
77
+ `There isn't an interactive CLI to eject components of the Single-Page Account theme.`,
78
+ `You can however copy paste into your codebase the any file or directory from the following source directory:`,
79
+ ``,
80
+ `${chalk.bold(pathJoin(pathRelative(process.cwd(), srcDirPath)))}`,
81
+ ``
82
+ ].join("\n")
83
+ );
84
+
85
+ eject_entrypoint: {
86
+ const kcAccountUiTsxFileRelativePath = "KcAccountUi.tsx";
87
+
88
+ const accountThemeSrcDirPath = pathJoin(
89
+ buildContext.themeSrcDirPath,
90
+ "account"
91
+ );
92
+
93
+ const targetFilePath = pathJoin(
94
+ accountThemeSrcDirPath,
95
+ kcAccountUiTsxFileRelativePath
96
+ );
97
+
98
+ if (fs.existsSync(targetFilePath)) {
99
+ break eject_entrypoint;
100
+ }
101
+
102
+ fs.cpSync(
103
+ pathJoin(srcDirPath, kcAccountUiTsxFileRelativePath),
104
+ targetFilePath
105
+ );
106
+
107
+ {
108
+ const kcPageTsxFilePath = pathJoin(accountThemeSrcDirPath, "KcPage.tsx");
109
+
110
+ const kcPageTsxCode = fs.readFileSync(kcPageTsxFilePath).toString("utf8");
111
+
112
+ const componentName = pathBasename(
113
+ kcAccountUiTsxFileRelativePath
114
+ ).replace(/.tsx$/, "");
115
+
116
+ const modifiedKcPageTsxCode = kcPageTsxCode.replace(
117
+ `@keycloakify/keycloak-account-ui/${componentName}`,
118
+ `./${componentName}`
119
+ );
120
+
121
+ fs.writeFileSync(
122
+ kcPageTsxFilePath,
123
+ Buffer.from(modifiedKcPageTsxCode, "utf8")
124
+ );
125
+ }
126
+
127
+ const routesTsxFilePath = pathRelative(
128
+ process.cwd(),
129
+ pathJoin(srcDirPath, "routes.tsx")
130
+ );
131
+
132
+ console.log(
133
+ [
134
+ `To help you get started ${chalk.bold(pathRelative(process.cwd(), targetFilePath))} has been copied into your project.`,
135
+ `The next step is usually to eject ${chalk.bold(routesTsxFilePath)}`,
136
+ `with \`cp ${routesTsxFilePath} ${pathRelative(process.cwd(), accountThemeSrcDirPath)}\``,
137
+ `then update the import of routes in ${kcAccountUiTsxFileRelativePath}.`
138
+ ].join("\n")
139
+ );
140
+ }
141
+
142
+ process.exit(0);
143
+ }
36
144
 
37
145
  console.log(`→ ${themeType}`);
38
146
 
@@ -6,6 +6,7 @@ import chalk from "chalk";
6
6
  import { join as pathJoin, relative as pathRelative } from "path";
7
7
  import * as fs from "fs";
8
8
  import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
9
+ import { generateKcGenTs } from "../shared/generateKcGenTs";
9
10
 
10
11
  export async function command(params: { cliCommandOptions: CliCommandOptions }) {
11
12
  const { cliCommandOptions } = params;
@@ -14,7 +15,10 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
14
15
 
15
16
  const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
16
17
 
17
- if (fs.existsSync(accountThemeSrcDirPath)) {
18
+ if (
19
+ fs.existsSync(accountThemeSrcDirPath) &&
20
+ fs.readdirSync(accountThemeSrcDirPath).length > 0
21
+ ) {
18
22
  console.warn(
19
23
  chalk.red(
20
24
  `There is already a ${pathRelative(
@@ -92,4 +96,17 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
92
96
  }
93
97
 
94
98
  updateAccountThemeImplementationInConfig({ buildContext, accountThemeType });
99
+
100
+ await generateKcGenTs({
101
+ buildContext: {
102
+ ...buildContext,
103
+ implementedThemeTypes: {
104
+ ...buildContext.implementedThemeTypes,
105
+ account: {
106
+ isImplemented: true,
107
+ type: accountThemeType
108
+ }
109
+ }
110
+ }
111
+ });
95
112
  }
@@ -208,6 +208,18 @@ function decodeHtmlEntities(htmlStr){
208
208
  ) || (
209
209
  key == "attributes" &&
210
210
  areSamePath(path, ["realm"])
211
+ ) || (
212
+ xKeycloakify.pageId == "index.ftl" &&
213
+ xKeycloakify.themeType == "account" &&
214
+ areSamePath(path, ["realm"]) &&
215
+ ![
216
+ "name",
217
+ "registrationEmailAsUsername",
218
+ "editUsernameAllowed",
219
+ "isInternationalizationEnabled",
220
+ "identityFederationEnabled",
221
+ "userManagedAccessAllowed"
222
+ ]?seq_contains(key)
211
223
  )
212
224
  >
213
225
  <#-- <#local outSeq += ["/*" + path?join(".") + "." + key + " excluded*/"]> -->
@@ -1,14 +1,21 @@
1
- import { assert } from "tsafe/assert";
1
+ import { assert, type Equals } from "tsafe/assert";
2
+ import { id } from "tsafe/id";
2
3
  import type { BuildContext } from "./buildContext";
3
4
  import * as fs from "fs/promises";
4
5
  import { join as pathJoin } from "path";
5
6
  import { existsAsync } from "../tools/fs.existsAsync";
7
+ import { z } from "zod";
6
8
 
7
9
  export type BuildContextLike = {
8
10
  projectDirPath: string;
9
11
  themeNames: string[];
10
12
  environmentVariables: { name: string; default: string }[];
11
13
  themeSrcDirPath: string;
14
+ implementedThemeTypes: Pick<
15
+ BuildContext["implementedThemeTypes"],
16
+ "login" | "account"
17
+ >;
18
+ packageJsonFilePath: string;
12
19
  };
13
20
 
14
21
  assert<BuildContext extends BuildContextLike ? true : false>();
@@ -18,12 +25,53 @@ export async function generateKcGenTs(params: {
18
25
  }): Promise<void> {
19
26
  const { buildContext } = params;
20
27
 
21
- const filePath = pathJoin(buildContext.themeSrcDirPath, "kc.gen.ts");
28
+ const isReactProject: boolean = await (async () => {
29
+ const parsedPackageJson = await (async () => {
30
+ type ParsedPackageJson = {
31
+ dependencies?: Record<string, string>;
32
+ devDependencies?: Record<string, string>;
33
+ };
34
+
35
+ const zParsedPackageJson = (() => {
36
+ type TargetType = ParsedPackageJson;
37
+
38
+ const zTargetType = z.object({
39
+ dependencies: z.record(z.string()).optional(),
40
+ devDependencies: z.record(z.string()).optional()
41
+ });
42
+
43
+ assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
44
+
45
+ return id<z.ZodType<TargetType>>(zTargetType);
46
+ })();
47
+
48
+ return zParsedPackageJson.parse(
49
+ JSON.parse(
50
+ (await fs.readFile(buildContext.packageJsonFilePath)).toString("utf8")
51
+ )
52
+ );
53
+ })();
54
+
55
+ return (
56
+ {
57
+ ...parsedPackageJson.dependencies,
58
+ ...parsedPackageJson.devDependencies
59
+ }.react !== undefined
60
+ );
61
+ })();
62
+
63
+ const filePath = pathJoin(
64
+ buildContext.themeSrcDirPath,
65
+ `kc.gen.ts${isReactProject ? "x" : ""}`
66
+ );
22
67
 
23
68
  const currentContent = (await existsAsync(filePath))
24
69
  ? await fs.readFile(filePath)
25
70
  : undefined;
26
71
 
72
+ const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
73
+ const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
74
+
27
75
  const newContent = Buffer.from(
28
76
  [
29
77
  `/* prettier-ignore-start */`,
@@ -36,6 +84,8 @@ export async function generateKcGenTs(params: {
36
84
  ``,
37
85
  `// This file is auto-generated by Keycloakify`,
38
86
  ``,
87
+ isReactProject && `import { lazy, Suspense, type ReactNode } from "react";`,
88
+ ``,
39
89
  `export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
40
90
  ``,
41
91
  `export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`,
@@ -54,9 +104,52 @@ export async function generateKcGenTs(params: {
54
104
  2
55
105
  )};`,
56
106
  ``,
107
+ `export type KcContext =`,
108
+ hasLoginTheme && ` | import("./login/KcContext").KcContext`,
109
+ hasAccountTheme && ` | import("./account/KcContext").KcContext`,
110
+ ` ;`,
111
+ ``,
112
+ `declare global {`,
113
+ ` interface Window {`,
114
+ ` kcContext?: KcContext;`,
115
+ ` }`,
116
+ `}`,
117
+ ``,
118
+ ...(!isReactProject
119
+ ? []
120
+ : [
121
+ hasLoginTheme &&
122
+ `export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
123
+ hasAccountTheme &&
124
+ `export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
125
+ ``,
126
+ `export function KcPage(`,
127
+ ` props: {`,
128
+ ` kcContext: KcContext;`,
129
+ ` fallback?: ReactNode;`,
130
+ ` }`,
131
+ `) {`,
132
+ ` const { kcContext, fallback } = props;`,
133
+ ` return (`,
134
+ ` <Suspense fallback={fallback}>`,
135
+ ` {(() => {`,
136
+ ` switch (kcContext.themeType) {`,
137
+ hasLoginTheme &&
138
+ ` case "login": return <KcLoginPage kcContext={kcContext} />;`,
139
+ hasAccountTheme &&
140
+ ` case "account": return <KcAccountPage kcContext={kcContext} />;`,
141
+ ` }`,
142
+ ` })()}`,
143
+ ` </Suspense>`,
144
+ ` );`,
145
+ `}`
146
+ ]),
147
+ ``,
57
148
  `/* prettier-ignore-end */`,
58
149
  ``
59
- ].join("\n"),
150
+ ]
151
+ .filter(item => typeof item === "string")
152
+ .join("\n"),
60
153
  "utf8"
61
154
  );
62
155
 
@@ -65,4 +158,18 @@ export async function generateKcGenTs(params: {
65
158
  }
66
159
 
67
160
  await fs.writeFile(filePath, newContent);
161
+
162
+ delete_legacy_file: {
163
+ if (!isReactProject) {
164
+ break delete_legacy_file;
165
+ }
166
+
167
+ const legacyFilePath = filePath.replace(/tsx$/, "ts");
168
+
169
+ if (!(await existsAsync(legacyFilePath))) {
170
+ break delete_legacy_file;
171
+ }
172
+
173
+ await fs.unlink(legacyFilePath);
174
+ }
68
175
  }
@@ -15,6 +15,7 @@ export default meta;
15
15
 
16
16
  type Story = StoryObj<typeof meta>;
17
17
 
18
+ // NOTE: Enable in your Keycloak realm with: https://github.com/user-attachments/assets/5fc5e49e-a172-4cb0-897a-49baac284b47
18
19
  export const Default: Story = {
19
20
  render: () => (
20
21
  <KcPageStory