keycloakify 11.3.19 → 11.3.20

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 +675 -0
  2. package/bin/356.index.js +689 -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 +71 -0
  47. package/src/bin/postinstall/index.ts +1 -0
  48. package/src/bin/postinstall/installUiModulesPeerDependencies.ts +157 -0
  49. package/src/bin/postinstall/managedGitignoreFile.ts +135 -0
  50. package/src/bin/postinstall/postinstall.ts +79 -0
  51. package/src/bin/postinstall/uiModuleMeta.ts +303 -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
@@ -0,0 +1,303 @@
1
+ import { assert, type Equals } from "tsafe/assert";
2
+ import { id } from "tsafe/id";
3
+ import { z } from "zod";
4
+ import { join as pathJoin, dirname as pathDirname } from "path";
5
+ import * as fsPr from "fs/promises";
6
+ import type { BuildContext } from "../shared/buildContext";
7
+ import { is } from "tsafe/is";
8
+ import { existsAsync } from "../tools/fs.existsAsync";
9
+ import { listInstalledModules } from "../tools/listInstalledModules";
10
+ import { crawlAsync } from "../tools/crawlAsync";
11
+ import { getIsPrettierAvailable, getPrettier } from "../tools/runPrettier";
12
+ import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
13
+ import {
14
+ getUiModuleFileSourceCodeReadyToBeCopied,
15
+ type BuildContextLike as BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied
16
+ } from "./getUiModuleFileSourceCodeReadyToBeCopied";
17
+ import * as crypto from "crypto";
18
+ import { KEYCLOAK_THEME } from "../shared/constants";
19
+
20
+ export type UiModuleMeta = {
21
+ moduleName: string;
22
+ version: string;
23
+ files: {
24
+ fileRelativePath: string;
25
+ hash: string;
26
+ copyableFilePath: string;
27
+ }[];
28
+ peerDependencies: Record<string, string>;
29
+ };
30
+
31
+ const zUiModuleMeta = (() => {
32
+ type ExpectedType = UiModuleMeta;
33
+
34
+ const zTargetType = z.object({
35
+ moduleName: z.string(),
36
+ version: z.string(),
37
+ files: z.array(
38
+ z.object({
39
+ fileRelativePath: z.string(),
40
+ hash: z.string(),
41
+ copyableFilePath: z.string()
42
+ })
43
+ ),
44
+ peerDependencies: z.record(z.string())
45
+ });
46
+
47
+ type InferredType = z.infer<typeof zTargetType>;
48
+
49
+ assert<Equals<InferredType, ExpectedType>>();
50
+
51
+ return id<z.ZodType<ExpectedType>>(zTargetType);
52
+ })();
53
+
54
+ type ParsedCacheFile = {
55
+ keycloakifyVersion: string;
56
+ prettierConfigHash: string | null;
57
+ thisFilePath: string;
58
+ uiModuleMetas: UiModuleMeta[];
59
+ };
60
+
61
+ const zParsedCacheFile = (() => {
62
+ type ExpectedType = ParsedCacheFile;
63
+
64
+ const zTargetType = z.object({
65
+ keycloakifyVersion: z.string(),
66
+ prettierConfigHash: z.union([z.string(), z.null()]),
67
+ thisFilePath: z.string(),
68
+ uiModuleMetas: z.array(zUiModuleMeta)
69
+ });
70
+
71
+ type InferredType = z.infer<typeof zTargetType>;
72
+
73
+ assert<Equals<InferredType, ExpectedType>>();
74
+
75
+ return id<z.ZodType<ExpectedType>>(zTargetType);
76
+ })();
77
+
78
+ const CACHE_FILE_RELATIVE_PATH = pathJoin("ui-modules", "cache.json");
79
+
80
+ export type BuildContextLike =
81
+ BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied & {
82
+ cacheDirPath: string;
83
+ packageJsonFilePath: string;
84
+ projectDirPath: string;
85
+ };
86
+
87
+ assert<BuildContext extends BuildContextLike ? true : false>();
88
+
89
+ export async function getUiModuleMetas(params: {
90
+ buildContext: BuildContextLike;
91
+ }): Promise<UiModuleMeta[]> {
92
+ const { buildContext } = params;
93
+
94
+ const cacheFilePath = pathJoin(buildContext.cacheDirPath, CACHE_FILE_RELATIVE_PATH);
95
+
96
+ const keycloakifyVersion = readThisNpmPackageVersion();
97
+
98
+ const prettierConfigHash = await (async () => {
99
+ if (!(await getIsPrettierAvailable())) {
100
+ return null;
101
+ }
102
+
103
+ const { configHash } = await getPrettier();
104
+
105
+ return configHash;
106
+ })();
107
+
108
+ const installedUiModules = await (async () => {
109
+ const installedModulesWithKeycloakifyInTheName = await listInstalledModules({
110
+ packageJsonFilePath: buildContext.packageJsonFilePath,
111
+ projectDirPath: buildContext.packageJsonFilePath,
112
+ filter: ({ moduleName }) =>
113
+ moduleName.includes("keycloakify") && moduleName !== "keycloakify"
114
+ });
115
+
116
+ return Promise.all(
117
+ installedModulesWithKeycloakifyInTheName.filter(async ({ dirPath }) =>
118
+ existsAsync(pathJoin(dirPath, KEYCLOAK_THEME))
119
+ )
120
+ );
121
+ })();
122
+
123
+ const cacheContent = await (async () => {
124
+ if (!(await existsAsync(cacheFilePath))) {
125
+ return undefined;
126
+ }
127
+
128
+ return await fsPr.readFile(cacheFilePath);
129
+ })();
130
+
131
+ const uiModuleMetas_cacheUpToDate: UiModuleMeta[] = await (async () => {
132
+ const parsedCacheFile: ParsedCacheFile | undefined = await (async () => {
133
+ if (cacheContent === undefined) {
134
+ return undefined;
135
+ }
136
+
137
+ const cacheContentStr = cacheContent.toString("utf8");
138
+
139
+ let parsedCacheFile: unknown;
140
+
141
+ try {
142
+ parsedCacheFile = JSON.parse(cacheContentStr);
143
+ } catch {
144
+ return undefined;
145
+ }
146
+
147
+ try {
148
+ zParsedCacheFile.parse(parsedCacheFile);
149
+ } catch {
150
+ return undefined;
151
+ }
152
+
153
+ assert(is<ParsedCacheFile>(parsedCacheFile));
154
+
155
+ return parsedCacheFile;
156
+ })();
157
+
158
+ if (parsedCacheFile === undefined) {
159
+ return [];
160
+ }
161
+
162
+ if (parsedCacheFile.keycloakifyVersion !== keycloakifyVersion) {
163
+ return [];
164
+ }
165
+
166
+ if (parsedCacheFile.prettierConfigHash !== prettierConfigHash) {
167
+ return [];
168
+ }
169
+
170
+ if (parsedCacheFile.thisFilePath !== cacheFilePath) {
171
+ return [];
172
+ }
173
+
174
+ const uiModuleMetas_cacheUpToDate = parsedCacheFile.uiModuleMetas.filter(
175
+ uiModuleMeta => {
176
+ const correspondingInstalledUiModule = installedUiModules.find(
177
+ installedUiModule =>
178
+ installedUiModule.moduleName === uiModuleMeta.moduleName
179
+ );
180
+
181
+ if (correspondingInstalledUiModule === undefined) {
182
+ return false;
183
+ }
184
+
185
+ return correspondingInstalledUiModule.version === uiModuleMeta.version;
186
+ }
187
+ );
188
+
189
+ return uiModuleMetas_cacheUpToDate;
190
+ })();
191
+
192
+ const uiModuleMetas = await Promise.all(
193
+ installedUiModules.map(
194
+ async ({
195
+ moduleName,
196
+ version,
197
+ peerDependencies,
198
+ dirPath
199
+ }): Promise<UiModuleMeta> => {
200
+ use_cache: {
201
+ const uiModuleMeta_cache = uiModuleMetas_cacheUpToDate.find(
202
+ uiModuleMeta => uiModuleMeta.moduleName === moduleName
203
+ );
204
+
205
+ if (uiModuleMeta_cache === undefined) {
206
+ break use_cache;
207
+ }
208
+
209
+ return uiModuleMeta_cache;
210
+ }
211
+
212
+ const files: UiModuleMeta["files"] = [];
213
+
214
+ {
215
+ const srcDirPath = pathJoin(dirPath, KEYCLOAK_THEME);
216
+
217
+ await crawlAsync({
218
+ dirPath: srcDirPath,
219
+ returnedPathsType: "relative to dirPath",
220
+ onFileFound: async fileRelativePath => {
221
+ const sourceCode =
222
+ await getUiModuleFileSourceCodeReadyToBeCopied({
223
+ buildContext,
224
+ fileRelativePath,
225
+ isForEjection: false,
226
+ uiModuleDirPath: dirPath,
227
+ uiModuleName: moduleName,
228
+ uiModuleVersion: version
229
+ });
230
+
231
+ const hash = computeHash(sourceCode);
232
+
233
+ const copyableFilePath = pathJoin(
234
+ pathDirname(cacheFilePath),
235
+ KEYCLOAK_THEME,
236
+ fileRelativePath
237
+ );
238
+
239
+ {
240
+ const dirPath = pathDirname(copyableFilePath);
241
+
242
+ if (!(await existsAsync(dirPath))) {
243
+ await fsPr.mkdir(dirPath, { recursive: true });
244
+ }
245
+ }
246
+
247
+ fsPr.writeFile(copyableFilePath, sourceCode);
248
+
249
+ files.push({
250
+ fileRelativePath,
251
+ hash,
252
+ copyableFilePath
253
+ });
254
+ }
255
+ });
256
+ }
257
+
258
+ return id<UiModuleMeta>({
259
+ moduleName,
260
+ version,
261
+ files,
262
+ peerDependencies
263
+ });
264
+ }
265
+ )
266
+ );
267
+
268
+ update_cache: {
269
+ const parsedCacheFile = id<ParsedCacheFile>({
270
+ keycloakifyVersion,
271
+ prettierConfigHash,
272
+ thisFilePath: cacheFilePath,
273
+ uiModuleMetas
274
+ });
275
+
276
+ const cacheContent_new = Buffer.from(
277
+ JSON.stringify(parsedCacheFile, null, 2),
278
+ "utf8"
279
+ );
280
+
281
+ if (cacheContent !== undefined && cacheContent_new.equals(cacheContent)) {
282
+ break update_cache;
283
+ }
284
+
285
+ create_dir: {
286
+ const dirPath = pathDirname(cacheFilePath);
287
+
288
+ if (await existsAsync(dirPath)) {
289
+ break create_dir;
290
+ }
291
+
292
+ await fsPr.mkdir(dirPath, { recursive: true });
293
+ }
294
+
295
+ await fsPr.writeFile(cacheFilePath, cacheContent_new);
296
+ }
297
+
298
+ return uiModuleMetas;
299
+ }
300
+
301
+ export function computeHash(data: Buffer) {
302
+ return crypto.createHash("sha256").update(data).digest("hex");
303
+ }
@@ -18,9 +18,8 @@ import {
18
18
  import type { KeycloakVersionRange } from "./KeycloakVersionRange";
19
19
  import { exclude } from "tsafe";
20
20
  import { crawl } from "../tools/crawl";
21
- import { THEME_TYPES } from "./constants";
21
+ import { THEME_TYPES, KEYCLOAK_THEME, type ThemeType } from "./constants";
22
22
  import { objectEntries } from "tsafe/objectEntries";
23
- import { type ThemeType } from "./constants";
24
23
  import { id } from "tsafe/id";
25
24
  import chalk from "chalk";
26
25
  import { getProxyFetchOptions, type FetchOptionsLike } from "../tools/fetchProxyOptions";
@@ -52,6 +51,7 @@ export type BuildContext = {
52
51
  account:
53
52
  | { isImplemented: false }
54
53
  | { isImplemented: true; type: "Single-Page" | "Multi-Page" };
54
+ admin: { isImplemented: boolean };
55
55
  };
56
56
  packageJsonFilePath: string;
57
57
  bundler: "vite" | "webpack";
@@ -146,7 +146,10 @@ export function getBuildContext(params: {
146
146
  returnedPathsType: "relative to dirPath"
147
147
  })
148
148
  .map(fileRelativePath => {
149
- for (const themeSrcDirBasename of ["keycloak-theme", "keycloak_theme"]) {
149
+ for (const themeSrcDirBasename of [
150
+ KEYCLOAK_THEME,
151
+ KEYCLOAK_THEME.replace(/-/g, "_")
152
+ ]) {
150
153
  const split = fileRelativePath.split(themeSrcDirBasename);
151
154
  if (split.length === 2) {
152
155
  return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
@@ -172,7 +175,7 @@ export function getBuildContext(params: {
172
175
  [
173
176
  `Can't locate your Keycloak theme source directory in .${pathSep}${pathRelative(process.cwd(), srcDirPath)}`,
174
177
  `Make sure to either use the Keycloakify CLI in the root of your Keycloakify project or use the --project CLI option`,
175
- `If you are collocating your Keycloak theme with your app you must have a directory named 'keycloak-theme' or 'keycloak_theme' in your 'src' directory`
178
+ `If you are collocating your Keycloak theme with your app you must have a directory named '${KEYCLOAK_THEME}' or '${KEYCLOAK_THEME.replace(/-/g, "_")}' in your 'src' directory`
176
179
  ].join("\n")
177
180
  )
178
181
  );
@@ -448,7 +451,10 @@ export function getBuildContext(params: {
448
451
  isImplemented: true,
449
452
  type: buildOptions.accountThemeImplementation
450
453
  };
451
- })()
454
+ })(),
455
+ admin: {
456
+ isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "admin"))
457
+ }
452
458
  };
453
459
 
454
460
  if (
@@ -4,7 +4,7 @@ export const WELL_KNOWN_DIRECTORY_BASE_NAME = {
4
4
  DIST: "dist"
5
5
  } as const;
6
6
 
7
- export const THEME_TYPES = ["login", "account"] as const;
7
+ export const THEME_TYPES = ["login", "account", "admin"] as const;
8
8
 
9
9
  export type ThemeType = (typeof THEME_TYPES)[number];
10
10
 
@@ -76,3 +76,5 @@ export const CUSTOM_HANDLER_ENV_NAMES = {
76
76
  COMMAND_NAME: "KEYCLOAKIFY_COMMAND_NAME",
77
77
  BUILD_CONTEXT: "KEYCLOAKIFY_BUILD_CONTEXT"
78
78
  };
79
+
80
+ export const KEYCLOAK_THEME = "keycloak-theme";
@@ -11,6 +11,7 @@ export type CommandName =
11
11
  | "eject-page"
12
12
  | "add-story"
13
13
  | "initialize-account-theme"
14
+ | "initialize-admin-theme"
14
15
  | "initialize-email-theme"
15
16
  | "copy-keycloak-resources-to-public";
16
17
 
@@ -8,7 +8,7 @@ import {
8
8
  ApiVersion
9
9
  } from "./customHandler";
10
10
  import * as child_process from "child_process";
11
- import { sep as pathSep } from "path";
11
+ import { getNodeModulesBinDirPath } from "../tools/nodeModulesBinDirPath";
12
12
  import * as fs from "fs";
13
13
 
14
14
  assert<Equals<ApiVersion, "v1">>();
@@ -19,32 +19,7 @@ export function maybeDelegateCommandToCustomHandler(params: {
19
19
  }): { hasBeenHandled: boolean } {
20
20
  const { commandName, buildContext } = params;
21
21
 
22
- const nodeModulesBinDirPath = (() => {
23
- const binPath = process.argv[1];
24
-
25
- const segments: string[] = [".bin"];
26
-
27
- let foundNodeModules = false;
28
-
29
- for (const segment of binPath.split(pathSep).reverse()) {
30
- skip_segment: {
31
- if (foundNodeModules) {
32
- break skip_segment;
33
- }
34
-
35
- if (segment === "node_modules") {
36
- foundNodeModules = true;
37
- break skip_segment;
38
- }
39
-
40
- continue;
41
- }
42
-
43
- segments.unshift(segment);
44
- }
45
-
46
- return segments.join(pathSep);
47
- })();
22
+ const nodeModulesBinDirPath = getNodeModulesBinDirPath();
48
23
 
49
24
  if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) {
50
25
  return { hasBeenHandled: false };
@@ -0,0 +1,36 @@
1
+ import child_process from "child_process";
2
+ import chalk from "chalk";
3
+
4
+ export function exitIfUncommittedChanges(params: { projectDirPath: string }) {
5
+ const { projectDirPath } = params;
6
+
7
+ let hasUncommittedChanges: boolean | undefined = undefined;
8
+
9
+ try {
10
+ hasUncommittedChanges =
11
+ child_process
12
+ .execSync(`git status --porcelain`, {
13
+ cwd: projectDirPath
14
+ })
15
+ .toString()
16
+ .trim() !== "";
17
+ } catch {
18
+ // Probably not a git repository
19
+ return;
20
+ }
21
+
22
+ if (!hasUncommittedChanges) {
23
+ return;
24
+ }
25
+ console.warn(
26
+ [
27
+ chalk.red(
28
+ "Please commit or stash your changes before running this command.\n"
29
+ ),
30
+ "This command will modify your project's files so it's better to have a clean working directory",
31
+ "so that you can easily see what has been changed and revert if needed."
32
+ ].join(" ")
33
+ );
34
+
35
+ process.exit(-1);
36
+ }
@@ -0,0 +1,51 @@
1
+ import * as fsPr from "fs/promises";
2
+ import { join as pathJoin, relative as pathRelative } from "path";
3
+ import { assert, type Equals } from "tsafe/assert";
4
+
5
+ /** List all files in a given directory return paths relative to the dir_path */
6
+ export async function crawlAsync(params: {
7
+ dirPath: string;
8
+ returnedPathsType: "absolute" | "relative to dirPath";
9
+ onFileFound: (filePath: string) => void;
10
+ }) {
11
+ const { dirPath, returnedPathsType, onFileFound } = params;
12
+
13
+ await crawlAsyncRec({
14
+ dirPath,
15
+ onFileFound: ({ filePath }) => {
16
+ switch (returnedPathsType) {
17
+ case "absolute":
18
+ onFileFound(filePath);
19
+ return;
20
+ case "relative to dirPath":
21
+ onFileFound(pathRelative(dirPath, filePath));
22
+ return;
23
+ }
24
+ assert<Equals<typeof returnedPathsType, never>>();
25
+ }
26
+ });
27
+ }
28
+
29
+ async function crawlAsyncRec(params: {
30
+ dirPath: string;
31
+ onFileFound: (params: { filePath: string }) => void;
32
+ }) {
33
+ const { dirPath, onFileFound } = params;
34
+
35
+ await Promise.all(
36
+ (await fsPr.readdir(dirPath)).map(async basename => {
37
+ const fileOrDirPath = pathJoin(dirPath, basename);
38
+
39
+ const isDirectory = await fsPr
40
+ .lstat(fileOrDirPath)
41
+ .then(stat => stat.isDirectory());
42
+
43
+ if (isDirectory) {
44
+ await crawlAsyncRec({ dirPath: fileOrDirPath, onFileFound });
45
+ return;
46
+ }
47
+
48
+ onFileFound({ filePath: fileOrDirPath });
49
+ })
50
+ );
51
+ }
@@ -0,0 +1,51 @@
1
+ import { join as pathJoin } from "path";
2
+ import { existsAsync } from "./fs.existsAsync";
3
+ import * as child_process from "child_process";
4
+ import { assert } from "tsafe/assert";
5
+
6
+ export async function getInstalledModuleDirPath(params: {
7
+ moduleName: string;
8
+ packageJsonDirPath: string;
9
+ projectDirPath: string;
10
+ }) {
11
+ const { moduleName, packageJsonDirPath, projectDirPath } = params;
12
+
13
+ common_case: {
14
+ const dirPath = pathJoin(
15
+ ...[packageJsonDirPath, "node_modules", ...moduleName.split("/")]
16
+ );
17
+
18
+ if (!(await existsAsync(dirPath))) {
19
+ break common_case;
20
+ }
21
+
22
+ return dirPath;
23
+ }
24
+
25
+ node_modules_at_root_case: {
26
+ if (projectDirPath === packageJsonDirPath) {
27
+ break node_modules_at_root_case;
28
+ }
29
+
30
+ const dirPath = pathJoin(
31
+ ...[projectDirPath, "node_modules", ...moduleName.split("/")]
32
+ );
33
+
34
+ if (!(await existsAsync(dirPath))) {
35
+ break node_modules_at_root_case;
36
+ }
37
+
38
+ return dirPath;
39
+ }
40
+
41
+ const dirPath = child_process
42
+ .execSync(`npm list ${moduleName}`, {
43
+ cwd: packageJsonDirPath
44
+ })
45
+ .toString("utf8")
46
+ .trim();
47
+
48
+ assert(dirPath !== "");
49
+
50
+ return dirPath;
51
+ }
@@ -0,0 +1,131 @@
1
+ import { assert, type Equals } from "tsafe/assert";
2
+ import { id } from "tsafe/id";
3
+ import { z } from "zod";
4
+ import { join as pathJoin, dirname as pathDirname } from "path";
5
+ import * as fsPr from "fs/promises";
6
+ import { is } from "tsafe/is";
7
+ import { getInstalledModuleDirPath } from "../tools/getInstalledModuleDirPath";
8
+ import { exclude } from "tsafe/exclude";
9
+
10
+ export async function listInstalledModules(params: {
11
+ packageJsonFilePath: string;
12
+ projectDirPath: string;
13
+ filter: (params: { moduleName: string }) => boolean;
14
+ }): Promise<
15
+ {
16
+ moduleName: string;
17
+ version: string;
18
+ dirPath: string;
19
+ peerDependencies: Record<string, string>;
20
+ }[]
21
+ > {
22
+ const { packageJsonFilePath, projectDirPath, filter } = params;
23
+
24
+ const parsedPackageJson = await readPackageJsonDependencies({
25
+ packageJsonFilePath
26
+ });
27
+
28
+ const uiModuleNames = (
29
+ [parsedPackageJson.dependencies, parsedPackageJson.devDependencies] as const
30
+ )
31
+ .filter(exclude(undefined))
32
+ .map(obj => Object.keys(obj))
33
+ .flat()
34
+ .filter(moduleName => filter({ moduleName }));
35
+
36
+ const result = await Promise.all(
37
+ uiModuleNames.map(async moduleName => {
38
+ const dirPath = await getInstalledModuleDirPath({
39
+ moduleName,
40
+ packageJsonDirPath: pathDirname(packageJsonFilePath),
41
+ projectDirPath
42
+ });
43
+
44
+ const { version, peerDependencies } =
45
+ await readPackageJsonVersionAndPeerDependencies({
46
+ packageJsonFilePath: pathJoin(dirPath, "package.json")
47
+ });
48
+
49
+ return { moduleName, version, peerDependencies, dirPath } as const;
50
+ })
51
+ );
52
+
53
+ return result;
54
+ }
55
+
56
+ const { readPackageJsonDependencies } = (() => {
57
+ type ParsedPackageJson = {
58
+ dependencies?: Record<string, string>;
59
+ devDependencies?: Record<string, string>;
60
+ };
61
+
62
+ const zParsedPackageJson = (() => {
63
+ type TargetType = ParsedPackageJson;
64
+
65
+ const zTargetType = z.object({
66
+ dependencies: z.record(z.string()).optional(),
67
+ devDependencies: z.record(z.string()).optional()
68
+ });
69
+
70
+ assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
71
+
72
+ return id<z.ZodType<TargetType>>(zTargetType);
73
+ })();
74
+
75
+ async function readPackageJsonDependencies(params: { packageJsonFilePath: string }) {
76
+ const { packageJsonFilePath } = params;
77
+
78
+ const parsedPackageJson = JSON.parse(
79
+ (await fsPr.readFile(packageJsonFilePath)).toString("utf8")
80
+ );
81
+
82
+ zParsedPackageJson.parse(parsedPackageJson);
83
+
84
+ assert(is<ParsedPackageJson>(parsedPackageJson));
85
+
86
+ return parsedPackageJson;
87
+ }
88
+
89
+ return { readPackageJsonDependencies };
90
+ })();
91
+
92
+ const { readPackageJsonVersionAndPeerDependencies } = (() => {
93
+ type ParsedPackageJson = {
94
+ version: string;
95
+ peerDependencies?: Record<string, string>;
96
+ };
97
+
98
+ const zParsedPackageJson = (() => {
99
+ type TargetType = ParsedPackageJson;
100
+
101
+ const zTargetType = z.object({
102
+ version: z.string(),
103
+ peerDependencies: z.record(z.string()).optional()
104
+ });
105
+
106
+ assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
107
+
108
+ return id<z.ZodType<TargetType>>(zTargetType);
109
+ })();
110
+
111
+ async function readPackageJsonVersionAndPeerDependencies(params: {
112
+ packageJsonFilePath: string;
113
+ }): Promise<{ version: string; peerDependencies: Record<string, string> }> {
114
+ const { packageJsonFilePath } = params;
115
+
116
+ const parsedPackageJson = JSON.parse(
117
+ (await fsPr.readFile(packageJsonFilePath)).toString("utf8")
118
+ );
119
+
120
+ zParsedPackageJson.parse(parsedPackageJson);
121
+
122
+ assert(is<ParsedPackageJson>(parsedPackageJson));
123
+
124
+ return {
125
+ version: parsedPackageJson.version,
126
+ peerDependencies: parsedPackageJson.peerDependencies ?? {}
127
+ };
128
+ }
129
+
130
+ return { readPackageJsonVersionAndPeerDependencies };
131
+ })();