keycloakify 11.3.19 → 11.3.21

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.
Files changed (67) hide show
  1. package/bin/124.index.js +676 -0
  2. package/bin/356.index.js +700 -0
  3. package/bin/40.index.js +41 -21
  4. package/bin/{903.index.js → 450.index.js} +2 -680
  5. package/bin/453.index.js +143 -85
  6. package/bin/573.index.js +51 -24
  7. package/bin/{599.index.js → 735.index.js} +438 -121
  8. package/bin/786.index.js +129 -78
  9. package/bin/805.index.js +674 -0
  10. package/bin/854.index.js +68 -0
  11. package/bin/{780.index.js → 921.index.js} +168 -109
  12. package/bin/932.index.js +41 -21
  13. package/bin/97.index.js +125 -75
  14. package/bin/eject-file.d.ts +7 -0
  15. package/bin/keycloakify/generateResources/generateMessageProperties.d.ts +1 -1
  16. package/bin/keycloakify/generateResources/readFieldNameUsage.d.ts +1 -1
  17. package/bin/main.js +70 -7
  18. package/bin/postinstall/getUiModuleFileSourceCodeReadyToBeCopied.d.ts +12 -0
  19. package/bin/postinstall/index.d.ts +1 -0
  20. package/bin/postinstall/installUiModulesPeerDependencies.d.ts +11 -0
  21. package/bin/postinstall/managedGitignoreFile.d.ts +14 -0
  22. package/bin/postinstall/postinstall.d.ts +4 -0
  23. package/bin/postinstall/uiModuleMeta.d.ts +21 -0
  24. package/bin/shared/buildContext.d.ts +3 -0
  25. package/bin/shared/constants.d.ts +2 -1
  26. package/bin/shared/constants.js +2 -1
  27. package/bin/shared/constants.js.map +1 -1
  28. package/bin/shared/customHandler.d.ts +1 -1
  29. package/bin/shared/customHandler.js.map +1 -1
  30. package/bin/shared/exitIfUncommittedChanges.d.ts +3 -0
  31. package/bin/tools/crawlAsync.d.ts +6 -0
  32. package/bin/tools/getInstalledModuleDirPath.d.ts +5 -0
  33. package/bin/tools/listInstalledModules.d.ts +12 -0
  34. package/bin/tools/nodeModulesBinDirPath.d.ts +1 -0
  35. package/bin/tools/runPrettier.d.ts +17 -0
  36. package/package.json +34 -6
  37. package/src/bin/add-story.ts +28 -10
  38. package/src/bin/eject-file.ts +68 -0
  39. package/src/bin/eject-page.ts +51 -31
  40. package/src/bin/initialize-account-theme/initialize-account-theme.ts +4 -32
  41. package/src/bin/initialize-account-theme/initializeAccountTheme_singlePage.ts +2 -16
  42. package/src/bin/keycloakify/generateResources/generateMessageProperties.ts +1 -1
  43. package/src/bin/keycloakify/generateResources/generateResources.ts +58 -26
  44. package/src/bin/keycloakify/generateResources/readFieldNameUsage.ts +1 -1
  45. package/src/bin/main.ts +50 -0
  46. package/src/bin/postinstall/getUiModuleFileSourceCodeReadyToBeCopied.ts +73 -0
  47. package/src/bin/postinstall/index.ts +1 -0
  48. package/src/bin/postinstall/installUiModulesPeerDependencies.ts +158 -0
  49. package/src/bin/postinstall/managedGitignoreFile.ts +136 -0
  50. package/src/bin/postinstall/postinstall.ts +79 -0
  51. package/src/bin/postinstall/uiModuleMeta.ts +309 -0
  52. package/src/bin/shared/buildContext.ts +11 -5
  53. package/src/bin/shared/constants.ts +3 -1
  54. package/src/bin/shared/customHandler.ts +1 -0
  55. package/src/bin/shared/customHandler_delegate.ts +2 -27
  56. package/src/bin/shared/exitIfUncommittedChanges.ts +36 -0
  57. package/src/bin/tools/crawlAsync.ts +51 -0
  58. package/src/bin/tools/getInstalledModuleDirPath.ts +51 -0
  59. package/src/bin/tools/listInstalledModules.ts +131 -0
  60. package/src/bin/tools/nodeModulesBinDirPath.ts +38 -0
  61. package/src/bin/tools/npmInstall.ts +411 -15
  62. package/src/bin/tools/readThisNpmPackageVersion.ts +8 -0
  63. package/src/bin/tools/runPrettier.ts +106 -0
  64. package/src/bin/update-kc-gen.ts +28 -17
  65. package/vite-plugin/index.js +9237 -9163
  66. package/bin/tools/runFormat.d.ts +0 -3
  67. package/src/bin/tools/runFormat.ts +0 -76
@@ -19,7 +19,8 @@ import {
19
19
  type ThemeType,
20
20
  LOGIN_THEME_PAGE_IDS,
21
21
  ACCOUNT_THEME_PAGE_IDS,
22
- WELL_KNOWN_DIRECTORY_BASE_NAME
22
+ WELL_KNOWN_DIRECTORY_BASE_NAME,
23
+ THEME_TYPES
23
24
  } from "../../shared/constants";
24
25
  import { assert, type Equals } from "tsafe/assert";
25
26
  import { readFieldNameUsage } from "./readFieldNameUsage";
@@ -78,15 +79,29 @@ export async function generateResources(params: {
78
79
  Record<ThemeType, (params: { messageDirPath: string; themeName: string }) => void>
79
80
  > = {};
80
81
 
81
- for (const themeType of ["login", "account"] as const) {
82
+ for (const themeType of THEME_TYPES) {
82
83
  if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
83
84
  continue;
84
85
  }
85
86
 
86
- const isForAccountSpa =
87
- themeType === "account" &&
88
- (assert(buildContext.implementedThemeTypes.account.isImplemented),
89
- buildContext.implementedThemeTypes.account.type === "Single-Page");
87
+ const getAccountThemeType = () => {
88
+ assert(themeType === "account");
89
+
90
+ assert(buildContext.implementedThemeTypes.account.isImplemented);
91
+
92
+ return buildContext.implementedThemeTypes.account.type;
93
+ };
94
+
95
+ const isSpa = (() => {
96
+ switch (themeType) {
97
+ case "login":
98
+ return false;
99
+ case "account":
100
+ return getAccountThemeType() === "Single-Page";
101
+ case "admin":
102
+ return true;
103
+ }
104
+ })();
90
105
 
91
106
  const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
92
107
 
@@ -101,7 +116,7 @@ export async function generateResources(params: {
101
116
  rmSync(destDirPath, { recursive: true, force: true });
102
117
 
103
118
  if (
104
- themeType === "account" &&
119
+ themeType !== "login" &&
105
120
  buildContext.implementedThemeTypes.login.isImplemented
106
121
  ) {
107
122
  // NOTE: We prevent doing it twice, it has been done for the login theme.
@@ -182,10 +197,13 @@ export async function generateResources(params: {
182
197
  buildContext,
183
198
  keycloakifyVersion: readThisNpmPackageVersion(),
184
199
  themeType,
185
- fieldNames: readFieldNameUsage({
186
- themeSrcDirPath: buildContext.themeSrcDirPath,
187
- themeType
188
- })
200
+ fieldNames: isSpa
201
+ ? []
202
+ : (assert(themeType !== "admin"),
203
+ readFieldNameUsage({
204
+ themeSrcDirPath: buildContext.themeSrcDirPath,
205
+ themeType
206
+ }))
189
207
  });
190
208
 
191
209
  [
@@ -194,10 +212,14 @@ export async function generateResources(params: {
194
212
  case "login":
195
213
  return LOGIN_THEME_PAGE_IDS;
196
214
  case "account":
197
- return isForAccountSpa ? ["index.ftl"] : ACCOUNT_THEME_PAGE_IDS;
215
+ return getAccountThemeType() === "Single-Page"
216
+ ? ["index.ftl"]
217
+ : ACCOUNT_THEME_PAGE_IDS;
218
+ case "admin":
219
+ return ["index.ftl"];
198
220
  }
199
221
  })(),
200
- ...(isForAccountSpa
222
+ ...(isSpa
201
223
  ? []
202
224
  : readExtraPagesNames({
203
225
  themeType,
@@ -215,10 +237,12 @@ export async function generateResources(params: {
215
237
  let languageTags: string[] | undefined = undefined;
216
238
 
217
239
  i18n_messages_generation: {
218
- if (isForAccountSpa) {
240
+ if (isSpa) {
219
241
  break i18n_messages_generation;
220
242
  }
221
243
 
244
+ assert(themeType !== "admin");
245
+
222
246
  const wrap = generateMessageProperties({
223
247
  buildContext,
224
248
  themeType
@@ -231,16 +255,15 @@ export async function generateResources(params: {
231
255
  writeMessagePropertiesFiles;
232
256
  }
233
257
 
234
- bring_in_account_v3_i18n_messages: {
235
- if (!buildContext.implementedThemeTypes.account.isImplemented) {
236
- break bring_in_account_v3_i18n_messages;
237
- }
238
- if (buildContext.implementedThemeTypes.account.type !== "Single-Page") {
239
- break bring_in_account_v3_i18n_messages;
258
+ bring_in_spas_messages: {
259
+ if (!isSpa) {
260
+ break bring_in_spas_messages;
240
261
  }
241
262
 
263
+ assert(themeType !== "login");
264
+
242
265
  const accountUiDirPath = child_process
243
- .execSync("npm list @keycloakify/keycloak-account-ui --parseable", {
266
+ .execSync(`npm list @keycloakify/keycloak-${themeType}-ui --parseable`, {
244
267
  cwd: pathDirname(buildContext.packageJsonFilePath)
245
268
  })
246
269
  .toString("utf8")
@@ -255,7 +278,7 @@ export async function generateResources(params: {
255
278
  }
256
279
 
257
280
  const messagesDirPath_dest = pathJoin(
258
- getThemeTypeDirPath({ themeName, themeType: "account" }),
281
+ getThemeTypeDirPath({ themeName, themeType }),
259
282
  "messages"
260
283
  );
261
284
 
@@ -267,7 +290,7 @@ export async function generateResources(params: {
267
290
  apply_theme_changes: {
268
291
  const messagesDirPath_theme = pathJoin(
269
292
  buildContext.themeSrcDirPath,
270
- "account",
293
+ themeType,
271
294
  "messages"
272
295
  );
273
296
 
@@ -316,7 +339,7 @@ export async function generateResources(params: {
316
339
  }
317
340
 
318
341
  keycloak_static_resources: {
319
- if (isForAccountSpa) {
342
+ if (isSpa) {
320
343
  break keycloak_static_resources;
321
344
  }
322
345
 
@@ -339,13 +362,22 @@ export async function generateResources(params: {
339
362
  `parent=${(() => {
340
363
  switch (themeType) {
341
364
  case "account":
342
- return isForAccountSpa ? "base" : "account-v1";
365
+ switch (getAccountThemeType()) {
366
+ case "Multi-Page":
367
+ return "account-v1";
368
+ case "Single-Page":
369
+ return "base";
370
+ }
343
371
  case "login":
344
372
  return "keycloak";
373
+ case "admin":
374
+ return "base";
345
375
  }
346
376
  assert<Equals<typeof themeType, never>>(false);
347
377
  })()}`,
348
- ...(isForAccountSpa ? ["deprecatedMode=false"] : []),
378
+ ...(themeType === "account" && getAccountThemeType() === "Single-Page"
379
+ ? ["deprecatedMode=false"]
380
+ : []),
349
381
  ...(buildContext.extraThemeProperties ?? []),
350
382
  ...buildContext.environmentVariables.map(
351
383
  ({ name, default: defaultValue }) =>
@@ -7,7 +7,7 @@ import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPa
7
7
  /** Assumes the theme type exists */
8
8
  export function readFieldNameUsage(params: {
9
9
  themeSrcDirPath: string;
10
- themeType: ThemeType;
10
+ themeType: Exclude<ThemeType, "admin">;
11
11
  }): string[] {
12
12
  const { themeSrcDirPath, themeType } = params;
13
13
 
package/src/bin/main.ts CHANGED
@@ -227,6 +227,56 @@ program
227
227
  }
228
228
  });
229
229
 
230
+ program
231
+ .command({
232
+ name: "postinstall",
233
+ description: "Initialize all the Keycloakify UI modules installed in the project."
234
+ })
235
+ .task({
236
+ skip,
237
+ handler: async ({ projectDirPath }) => {
238
+ const { command } = await import("./postinstall");
239
+
240
+ await command({ buildContext: getBuildContext({ projectDirPath }) });
241
+ }
242
+ });
243
+
244
+ program
245
+ .command<{
246
+ file: string;
247
+ }>({
248
+ name: "eject-file",
249
+ description: [
250
+ "WARNING: Not usable yet, will be used for future features",
251
+ "Take ownership over a given file"
252
+ ].join(" ")
253
+ })
254
+ .option({
255
+ key: "file",
256
+ name: (() => {
257
+ const name = "file";
258
+
259
+ optionsKeys.push(name);
260
+
261
+ return name;
262
+ })(),
263
+ description: [
264
+ "Relative path of the file relative to the directory of your keycloak theme source",
265
+ "Example `--file src/login/page/Login.tsx`"
266
+ ].join(" ")
267
+ })
268
+ .task({
269
+ skip,
270
+ handler: async ({ projectDirPath, file }) => {
271
+ const { command } = await import("./eject-file");
272
+
273
+ await command({
274
+ buildContext: getBuildContext({ projectDirPath }),
275
+ cliCommandOptions: { file }
276
+ });
277
+ }
278
+ });
279
+
230
280
  // Fallback to build command if no command is provided
231
281
  {
232
282
  const [, , ...rest] = process.argv;
@@ -0,0 +1,73 @@
1
+ import { getIsPrettierAvailable, runPrettier } from "../tools/runPrettier";
2
+ import * as fsPr from "fs/promises";
3
+ import { join as pathJoin, sep as pathSep } from "path";
4
+ import { assert } from "tsafe/assert";
5
+ import type { BuildContext } from "../shared/buildContext";
6
+ import { KEYCLOAK_THEME } from "../shared/constants";
7
+
8
+ export type BuildContextLike = {
9
+ themeSrcDirPath: string;
10
+ };
11
+
12
+ assert<BuildContext extends BuildContextLike ? true : false>();
13
+
14
+ export async function getUiModuleFileSourceCodeReadyToBeCopied(params: {
15
+ buildContext: BuildContextLike;
16
+ fileRelativePath: string;
17
+ isForEjection: boolean;
18
+ uiModuleDirPath: string;
19
+ uiModuleName: string;
20
+ uiModuleVersion: string;
21
+ }): Promise<Buffer> {
22
+ const {
23
+ buildContext,
24
+ uiModuleDirPath,
25
+ fileRelativePath,
26
+ isForEjection,
27
+ uiModuleName,
28
+ uiModuleVersion
29
+ } = params;
30
+
31
+ let sourceCode = (
32
+ await fsPr.readFile(pathJoin(uiModuleDirPath, KEYCLOAK_THEME, fileRelativePath))
33
+ ).toString("utf8");
34
+
35
+ const comment = (() => {
36
+ if (isForEjection) {
37
+ return [
38
+ `/*`,
39
+ ` * This file was ejected from ${uiModuleName} version ${uiModuleVersion}.`,
40
+ ` */`
41
+ ].join("\n");
42
+ } else {
43
+ return [
44
+ `/*`,
45
+ ` *`,
46
+ ` * WARNING: Before modifying this file run the following command:`,
47
+ ` * `,
48
+ ` * $ npx keycloakify eject-file --file ${fileRelativePath.split(pathSep).join("/")}`,
49
+ ` * `,
50
+ ` * This file comes from ${uiModuleName} version ${uiModuleVersion}.`,
51
+ ` *`,
52
+ ` */`
53
+ ].join("\n");
54
+ }
55
+ })();
56
+
57
+ sourceCode = [comment, ``, sourceCode].join("\n");
58
+
59
+ const destFilePath = pathJoin(buildContext.themeSrcDirPath, fileRelativePath);
60
+
61
+ format: {
62
+ if (!(await getIsPrettierAvailable())) {
63
+ break format;
64
+ }
65
+
66
+ sourceCode = await runPrettier({
67
+ filePath: destFilePath,
68
+ sourceCode
69
+ });
70
+ }
71
+
72
+ return Buffer.from(sourceCode, "utf8");
73
+ }
@@ -0,0 +1 @@
1
+ export * from "./postinstall";
@@ -0,0 +1,158 @@
1
+ import { assert, type Equals } from "tsafe/assert";
2
+ import { is } from "tsafe/is";
3
+ import type { BuildContext } from "../shared/buildContext";
4
+ import type { UiModuleMeta } from "./uiModuleMeta";
5
+ import { z } from "zod";
6
+ import { id } from "tsafe/id";
7
+ import * as fsPr from "fs/promises";
8
+ import { SemVer } from "../tools/SemVer";
9
+ import { same } from "evt/tools/inDepth/same";
10
+ import { runPrettier, getIsPrettierAvailable } from "../tools/runPrettier";
11
+ import { npmInstall } from "../tools/npmInstall";
12
+ import { dirname as pathDirname } from "path";
13
+
14
+ export type BuildContextLike = {
15
+ packageJsonFilePath: string;
16
+ };
17
+
18
+ assert<BuildContext extends BuildContextLike ? true : false>();
19
+
20
+ export type UiModuleMetaLike = {
21
+ moduleName: string;
22
+ peerDependencies: Record<string, string>;
23
+ };
24
+
25
+ assert<UiModuleMeta extends UiModuleMetaLike ? true : false>();
26
+
27
+ export async function installUiModulesPeerDependencies(params: {
28
+ buildContext: BuildContextLike;
29
+ uiModuleMetas: UiModuleMetaLike[];
30
+ }): Promise<void | never> {
31
+ const { buildContext, uiModuleMetas } = params;
32
+
33
+ const { uiModulesPerDependencies } = (() => {
34
+ const uiModulesPerDependencies: Record<string, string> = {};
35
+
36
+ for (const { peerDependencies } of uiModuleMetas) {
37
+ for (const [peerDependencyName, versionRange_candidate] of Object.entries(
38
+ peerDependencies
39
+ )) {
40
+ const versionRange = (() => {
41
+ const versionRange_current =
42
+ uiModulesPerDependencies[peerDependencyName];
43
+
44
+ if (versionRange_current === undefined) {
45
+ return versionRange_candidate;
46
+ }
47
+
48
+ if (versionRange_current === "*") {
49
+ return versionRange_candidate;
50
+ }
51
+
52
+ if (versionRange_candidate === "*") {
53
+ return versionRange_current;
54
+ }
55
+
56
+ const { versionRange } = [
57
+ versionRange_current,
58
+ versionRange_candidate
59
+ ]
60
+ .map(versionRange => ({
61
+ versionRange,
62
+ semVer: SemVer.parse(
63
+ (() => {
64
+ if (
65
+ versionRange.startsWith("^") ||
66
+ versionRange.startsWith("~")
67
+ ) {
68
+ return versionRange.slice(1);
69
+ }
70
+
71
+ return versionRange;
72
+ })()
73
+ )
74
+ }))
75
+ .sort((a, b) => SemVer.compare(b.semVer, a.semVer))[0];
76
+
77
+ return versionRange;
78
+ })();
79
+
80
+ uiModulesPerDependencies[peerDependencyName] = versionRange;
81
+ }
82
+ }
83
+
84
+ return { uiModulesPerDependencies };
85
+ })();
86
+
87
+ const parsedPackageJson = await (async () => {
88
+ type ParsedPackageJson = {
89
+ dependencies?: Record<string, string>;
90
+ devDependencies?: Record<string, string>;
91
+ };
92
+
93
+ const zParsedPackageJson = (() => {
94
+ type TargetType = ParsedPackageJson;
95
+
96
+ const zParsedPackageJson = z.object({
97
+ dependencies: z.record(z.string()).optional(),
98
+ devDependencies: z.record(z.string()).optional()
99
+ });
100
+
101
+ type InferredType = z.infer<typeof zParsedPackageJson>;
102
+
103
+ assert<Equals<InferredType, TargetType>>();
104
+
105
+ return id<z.ZodType<TargetType>>(zParsedPackageJson);
106
+ })();
107
+
108
+ const parsedPackageJson = JSON.parse(
109
+ (await fsPr.readFile(buildContext.packageJsonFilePath)).toString("utf8")
110
+ );
111
+
112
+ zParsedPackageJson.parse(parsedPackageJson);
113
+
114
+ assert(is<ParsedPackageJson>(parsedPackageJson));
115
+
116
+ return parsedPackageJson;
117
+ })();
118
+
119
+ const parsedPackageJson_before = JSON.parse(JSON.stringify(parsedPackageJson));
120
+
121
+ for (const [moduleName, versionRange] of Object.entries(uiModulesPerDependencies)) {
122
+ if (moduleName.startsWith("@types/")) {
123
+ (parsedPackageJson.devDependencies ??= {})[moduleName] = versionRange;
124
+ continue;
125
+ }
126
+
127
+ if (parsedPackageJson.devDependencies !== undefined) {
128
+ delete parsedPackageJson.devDependencies[moduleName];
129
+ }
130
+
131
+ (parsedPackageJson.dependencies ??= {})[moduleName] = versionRange;
132
+ }
133
+
134
+ if (same(parsedPackageJson, parsedPackageJson_before)) {
135
+ return;
136
+ }
137
+
138
+ let packageJsonContentStr = JSON.stringify(parsedPackageJson, null, 2);
139
+
140
+ format: {
141
+ if (!(await getIsPrettierAvailable())) {
142
+ break format;
143
+ }
144
+
145
+ packageJsonContentStr = await runPrettier({
146
+ sourceCode: packageJsonContentStr,
147
+ filePath: buildContext.packageJsonFilePath
148
+ });
149
+ }
150
+
151
+ await fsPr.writeFile(buildContext.packageJsonFilePath, packageJsonContentStr);
152
+
153
+ npmInstall({
154
+ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
155
+ });
156
+
157
+ process.exit(0);
158
+ }
@@ -0,0 +1,136 @@
1
+ import * as fsPr from "fs/promises";
2
+ import {
3
+ join as pathJoin,
4
+ sep as pathSep,
5
+ dirname as pathDirname,
6
+ relative as pathRelative
7
+ } from "path";
8
+ import { assert } from "tsafe/assert";
9
+ import type { BuildContext } from "../shared/buildContext";
10
+ import type { UiModuleMeta } from "./uiModuleMeta";
11
+ import { existsAsync } from "../tools/fs.existsAsync";
12
+ import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
13
+
14
+ export type BuildContextLike = {
15
+ themeSrcDirPath: string;
16
+ };
17
+
18
+ assert<BuildContext extends BuildContextLike ? true : false>();
19
+
20
+ const DELIMITER_START = `# === Ejected files start ===`;
21
+ const DELIMITER_END = `# === Ejected files end =====`;
22
+
23
+ export async function writeManagedGitignoreFile(params: {
24
+ buildContext: BuildContextLike;
25
+ uiModuleMetas: UiModuleMeta[];
26
+ ejectedFilesRelativePaths: string[];
27
+ }): Promise<void> {
28
+ const { buildContext, uiModuleMetas, ejectedFilesRelativePaths } = params;
29
+
30
+ if (uiModuleMetas.length === 0) {
31
+ return;
32
+ }
33
+
34
+ const filePath = pathJoin(buildContext.themeSrcDirPath, ".gitignore");
35
+
36
+ const content_new = Buffer.from(
37
+ [
38
+ `# This file is managed by Keycloakify, do not edit it manually.`,
39
+ ``,
40
+ DELIMITER_START,
41
+ ...ejectedFilesRelativePaths
42
+ .map(fileRelativePath => fileRelativePath.split(pathSep).join("/"))
43
+ .map(line => `# ${line}`),
44
+ DELIMITER_END,
45
+ ``,
46
+ ...uiModuleMetas
47
+ .map(uiModuleMeta => [
48
+ `# === ${uiModuleMeta.moduleName} v${uiModuleMeta.version} ===`,
49
+ ...uiModuleMeta.files
50
+ .map(({ fileRelativePath }) => fileRelativePath)
51
+ .filter(
52
+ fileRelativePath =>
53
+ !ejectedFilesRelativePaths.includes(fileRelativePath)
54
+ )
55
+ .map(
56
+ fileRelativePath =>
57
+ `/${fileRelativePath.split(pathSep).join("/").replace(/^\.\//, "")}`
58
+ ),
59
+
60
+ ``
61
+ ])
62
+ .flat()
63
+ ].join("\n"),
64
+ "utf8"
65
+ );
66
+
67
+ const content_current = await (async () => {
68
+ if (!(await existsAsync(filePath))) {
69
+ return undefined;
70
+ }
71
+
72
+ return await fsPr.readFile(filePath);
73
+ })();
74
+
75
+ if (content_current !== undefined && content_current.equals(content_new)) {
76
+ return;
77
+ }
78
+
79
+ create_dir: {
80
+ const dirPath = pathDirname(filePath);
81
+
82
+ if (await existsAsync(dirPath)) {
83
+ break create_dir;
84
+ }
85
+
86
+ await fsPr.mkdir(dirPath, { recursive: true });
87
+ }
88
+
89
+ await fsPr.writeFile(filePath, content_new);
90
+ }
91
+
92
+ export async function readManagedGitignoreFile(params: {
93
+ buildContext: BuildContextLike;
94
+ }): Promise<{
95
+ ejectedFilesRelativePaths: string[];
96
+ }> {
97
+ const { buildContext } = params;
98
+
99
+ const filePath = pathJoin(buildContext.themeSrcDirPath, ".gitignore");
100
+
101
+ if (!(await existsAsync(filePath))) {
102
+ return { ejectedFilesRelativePaths: [] };
103
+ }
104
+
105
+ const contentStr = (await fsPr.readFile(filePath)).toString("utf8");
106
+
107
+ const payload = (() => {
108
+ const index_start = contentStr.indexOf(DELIMITER_START);
109
+ const index_end = contentStr.indexOf(DELIMITER_END);
110
+
111
+ if (index_start === -1 || index_end === -1) {
112
+ return undefined;
113
+ }
114
+
115
+ return contentStr.slice(index_start + DELIMITER_START.length, index_end).trim();
116
+ })();
117
+
118
+ if (payload === undefined) {
119
+ return { ejectedFilesRelativePaths: [] };
120
+ }
121
+
122
+ const ejectedFilesRelativePaths = payload
123
+ .split("\n")
124
+ .map(line => line.trim())
125
+ .map(line => line.replace(/^# /, ""))
126
+ .filter(line => line !== "")
127
+ .map(line =>
128
+ getAbsoluteAndInOsFormatPath({
129
+ cwd: buildContext.themeSrcDirPath,
130
+ pathIsh: line
131
+ })
132
+ )
133
+ .map(filePath => pathRelative(buildContext.themeSrcDirPath, filePath));
134
+
135
+ return { ejectedFilesRelativePaths };
136
+ }
@@ -0,0 +1,79 @@
1
+ import type { BuildContext } from "../shared/buildContext";
2
+ import { getUiModuleMetas, computeHash } from "./uiModuleMeta";
3
+ import { installUiModulesPeerDependencies } from "./installUiModulesPeerDependencies";
4
+ import {
5
+ readManagedGitignoreFile,
6
+ writeManagedGitignoreFile
7
+ } from "./managedGitignoreFile";
8
+ import { dirname as pathDirname } from "path";
9
+ import { join as pathJoin } from "path";
10
+ import { existsAsync } from "../tools/fs.existsAsync";
11
+ import * as fsPr from "fs/promises";
12
+
13
+ export async function command(params: { buildContext: BuildContext }) {
14
+ const { buildContext } = params;
15
+
16
+ const uiModuleMetas = await getUiModuleMetas({ buildContext });
17
+
18
+ await installUiModulesPeerDependencies({
19
+ buildContext,
20
+ uiModuleMetas
21
+ });
22
+
23
+ const { ejectedFilesRelativePaths } = await readManagedGitignoreFile({
24
+ buildContext
25
+ });
26
+
27
+ await writeManagedGitignoreFile({
28
+ buildContext,
29
+ ejectedFilesRelativePaths,
30
+ uiModuleMetas
31
+ });
32
+
33
+ await Promise.all(
34
+ uiModuleMetas
35
+ .map(uiModuleMeta =>
36
+ Promise.all(
37
+ uiModuleMeta.files.map(
38
+ async ({ fileRelativePath, copyableFilePath, hash }) => {
39
+ if (ejectedFilesRelativePaths.includes(fileRelativePath)) {
40
+ return;
41
+ }
42
+
43
+ const destFilePath = pathJoin(
44
+ buildContext.themeSrcDirPath,
45
+ fileRelativePath
46
+ );
47
+
48
+ skip_condition: {
49
+ if (!(await existsAsync(destFilePath))) {
50
+ break skip_condition;
51
+ }
52
+
53
+ const destFileHash = computeHash(
54
+ await fsPr.readFile(destFilePath)
55
+ );
56
+
57
+ if (destFileHash !== hash) {
58
+ break skip_condition;
59
+ }
60
+
61
+ return;
62
+ }
63
+
64
+ {
65
+ const dirName = pathDirname(copyableFilePath);
66
+
67
+ if (!(await existsAsync(dirName))) {
68
+ await fsPr.mkdir(dirName, { recursive: true });
69
+ }
70
+ }
71
+
72
+ await fsPr.copyFile(copyableFilePath, destFilePath);
73
+ }
74
+ )
75
+ )
76
+ )
77
+ .flat()
78
+ );
79
+ }