@akanjs/devkit 3.0.0-alpha.10 → 3.0.0-alpha.100
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.
- package/CHANGELOG.md +15 -0
- package/DEV_RUNTIME_KNOBS.md +34 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/agentsIndex.test.ts +10 -0
- package/agentsIndex.ts +21 -1
- package/aiEditor.ts +1 -1
- package/akanApp/BackendImportGraph.test.ts +120 -0
- package/akanApp/BackendImportGraph.ts +167 -0
- package/akanApp/akanApp.host.test.ts +16 -678
- package/akanApp/akanApp.host.ts +113 -578
- package/akanApp/devHostPolicy.test.ts +542 -0
- package/akanApp/devHostPolicy.ts +442 -0
- package/akanApp/index.ts +2 -0
- package/akanConfig/akanConfig.test.ts +182 -14
- package/akanConfig/akanConfig.ts +143 -47
- package/akanConfig/types.ts +9 -0
- package/akanContext.ts +30 -14
- package/akanMcpContract.ts +25 -4
- package/appSelectionMemory.ts +28 -0
- package/applicationBuildRunner.test.ts +37 -2
- package/applicationBuildRunner.ts +52 -23
- package/artifact/implicitRootLayout.test.ts +67 -0
- package/artifact/implicitRootLayout.ts +27 -7
- package/artifact/routeSeedIndex.test.ts +1 -0
- package/biome.base.json +82 -43
- package/biomeBase.ts +1 -1
- package/capacitorApp.ts +10 -0
- package/cloud/globalConfig.ts +9 -2
- package/codegenLock.test.ts +137 -0
- package/codegenLock.ts +136 -0
- package/commandDecorators/argMeta.ts +9 -1
- package/commandDecorators/command.ts +94 -7
- package/commandDecorators/commandBuilder.ts +20 -5
- package/commandDecorators/commandDecorators.test.ts +115 -3
- package/commandDecorators/helpFormatter.ts +4 -0
- package/dependencyScanner.test.ts +99 -0
- package/dependencyScanner.ts +27 -24
- package/devkitUtils.test.ts +0 -104
- package/executors.test.ts +134 -4
- package/executors.ts +114 -25
- package/fileEditor.ts +19 -19
- package/fileSys.ts +19 -1
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/autoImportSync.test.ts +58 -0
- package/frontendBuild/autoImportSync.ts +7 -2
- package/frontendBuild/buildRouteClient.test.ts +47 -24
- package/frontendBuild/clientBuildTypes.ts +4 -0
- package/frontendBuild/clientEntriesBundler.ts +4 -1
- package/frontendBuild/clientEntryDiscovery.ts +2 -2
- package/frontendBuild/csrArtifactBuilder.ts +116 -84
- package/frontendBuild/cssCompiler.ts +123 -12
- package/frontendBuild/cssImportResolver.ts +8 -7
- package/frontendBuild/devGeneratedIndexSync.ts +3 -2
- package/frontendBuild/fontOptimizer.ts +37 -18
- package/frontendBuild/fontPruner.test.ts +220 -0
- package/frontendBuild/fontPruner.ts +206 -0
- package/frontendBuild/frontendBuild.test.ts +178 -10
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/index.ts +1 -1
- package/frontendBuild/pagesBundleBuilder.ts +3 -3
- package/frontendBuild/pagesEntrySourceGenerator.ts +11 -88
- package/frontendBuild/routeClientBuilder.ts +12 -5
- package/frontendBuild/sourceMtimeIndex.ts +1 -1
- package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
- package/frontendBuild/styleContract.ts +14 -20
- package/frontendBuild/themeValidator.ts +22 -17
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/frontendBuild/watchRootResolver.test.ts +129 -0
- package/frontendBuild/watchRootResolver.ts +66 -1
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/devWatchBatch.ts +6 -2
- package/incrementalBuilder/incrementalBuilder.host.ts +51 -7
- package/incrementalBuilder/incrementalBuilder.proc.ts +17 -6
- package/index.ts +0 -7
- package/integration/devStabilityHarness.ts +2 -10
- package/libSource.test.ts +112 -0
- package/libSource.ts +126 -0
- package/lint/__fixtures__/README.md +40 -0
- package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
- package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
- package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
- package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
- package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
- package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
- package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
- package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
- package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
- package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
- package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
- package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
- package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
- package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
- package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
- package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
- package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
- package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
- package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
- package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
- package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
- package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
- package/lint/gritRules.test.ts +178 -0
- package/lint/no-arbitrary-color.grit +1 -1
- package/lint/no-async-component-in-ui.grit +35 -0
- package/lint/no-bang-comment-in-client.grit +23 -10
- package/lint/no-daisyui-legacy-class.grit +26 -9
- package/lint/no-deprecated-log-level.grit +17 -0
- package/lint/no-init-fetch-in-client.grit +47 -0
- package/lint/no-inline-color.grit +10 -8
- package/lint/no-interpolated-arbitrary-class.grit +3 -3
- package/lint/no-model-type-in-util-zone.grit +58 -0
- package/lint/no-raw-palette-class.grit +3 -3
- package/lint/no-unpublished-form-setter.grit +41 -0
- package/lint/non-scalar-props-restricted.grit +16 -7
- package/linter.test.ts +80 -0
- package/linter.ts +99 -25
- package/package.json +5 -8
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +116 -0
- package/qualityScanner.ts +89 -21
- package/recipeScanner.ts +4 -1
- package/scanInfo.ts +32 -26
- package/semver.test.ts +26 -0
- package/semver.ts +31 -0
- package/slicePlanner.test.ts +182 -0
- package/slicePlanner.ts +235 -0
- package/ssrScanner.test.ts +301 -0
- package/subspace.test.ts +418 -0
- package/subspace.ts +723 -0
- package/subspaceConfig.ts +76 -0
- package/transforms/asyncDefaultExportDetector.ts +103 -0
- package/transforms/barrelImportsPlugin.ts +1 -1
- package/transforms/externalizeFrameworkPlugin.ts +0 -1
- package/transforms/transforms.test.ts +5 -5
- package/transforms/tsconfigPackageResolver.test.ts +230 -0
- package/tsconfig.json +6 -2
- package/typeChecker.ts +1 -1
- package/types.ts +1 -0
- package/uploadRelease.ts +2 -2
- package/workflow/executor.test.ts +146 -0
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +48 -2
- package/builder.ts +0 -164
- package/extractDeps.ts +0 -86
- package/frontendBuild/styleGuard.test.ts +0 -165
- package/frontendBuild/styleGuard.ts +0 -322
- package/getCredentials.ts +0 -19
- package/getModelFileData.ts +0 -62
- package/src/capacitorApp.ts +0 -282
- package/streamAi.ts +0 -45
- package/ui/MultiScrollList.tsx +0 -242
- package/ui/ScrollList.tsx +0 -107
- package/ui/index.ts +0 -2
- package/useStdoutDimensions.ts +0 -20
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
checkbox as inquirerCheckbox,
|
|
4
|
+
confirm as inquirerConfirm,
|
|
5
|
+
input as inquirerInput,
|
|
6
|
+
select as inquirerSelect,
|
|
7
|
+
} from "@inquirer/prompts";
|
|
3
8
|
import { Logger } from "akanjs/common";
|
|
4
9
|
import chalk from "chalk";
|
|
5
10
|
import { type Command, program } from "commander";
|
|
11
|
+
import { AppSelectionMemory } from "../appSelectionMemory";
|
|
6
12
|
import { AppExecutor, Executor, LibExecutor, ModuleExecutor, PkgExecutor, WorkspaceExecutor } from "../executors";
|
|
7
13
|
// Import the owning modules directly, never the root barrel: `..` re-exports all 41 devkit modules,
|
|
8
14
|
// so a barrel import here drags ink, @trapezedev/project, ssh2, @langchain/* and the cloud stack into
|
|
@@ -103,6 +109,27 @@ const prompts = async () => await import("@inquirer/prompts");
|
|
|
103
109
|
const select = ((config, context) => prompts().then((m) => m.select(config, context))) as typeof inquirerSelect;
|
|
104
110
|
const confirm = ((config, context) => prompts().then((m) => m.confirm(config, context))) as typeof inquirerConfirm;
|
|
105
111
|
const input = ((config, context) => prompts().then((m) => m.input(config, context))) as typeof inquirerInput;
|
|
112
|
+
const checkbox = ((config, context) => prompts().then((m) => m.checkbox(config, context))) as typeof inquirerCheckbox;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Rejects a value that is not one of the declared choices, in commander's own wording.
|
|
116
|
+
*
|
|
117
|
+
* Only a static choice list can be checked: a `DynamicEnum` resolves against the command context, which
|
|
118
|
+
* is not populated until the internal args are resolved, and it is the interactive `select` that consumes
|
|
119
|
+
* it. Comparison is stringly on purpose — the value still carries commander's raw string here, while a
|
|
120
|
+
* numeric choice list holds numbers.
|
|
121
|
+
*/
|
|
122
|
+
const assertEnumChoice = (argMeta: ArgMeta, value: unknown) => {
|
|
123
|
+
const enumChoices = argMeta.argsOption.enum;
|
|
124
|
+
if (!enumChoices || typeof enumChoices === "function") return;
|
|
125
|
+
const choices = normalizeEnumChoices(enumChoices);
|
|
126
|
+
if (choices.some((choice) => String(choice.value) === String(value))) return;
|
|
127
|
+
const label =
|
|
128
|
+
argMeta.type === "Option" ? `option '--${camelToKebabCase(argMeta.name)}'` : `argument '${argMeta.name}'`;
|
|
129
|
+
throw new Error(
|
|
130
|
+
`${label} argument '${String(value)}' is invalid. Allowed choices are ${choices.map((choice) => choice.name).join(", ")}.`,
|
|
131
|
+
);
|
|
132
|
+
};
|
|
106
133
|
|
|
107
134
|
const resolveEnumChoices = async (argMeta: ArgMeta, context: CommandContext) => {
|
|
108
135
|
const enumChoices = argMeta.argsOption.enum;
|
|
@@ -111,13 +138,15 @@ const resolveEnumChoices = async (argMeta: ArgMeta, context: CommandContext) =>
|
|
|
111
138
|
return enumChoices;
|
|
112
139
|
};
|
|
113
140
|
|
|
114
|
-
const getOptionValue = async (argMeta: ArgMeta, opt: Record<string, unknown>, context: CommandContext) => {
|
|
141
|
+
export const getOptionValue = async (argMeta: ArgMeta, opt: Record<string, unknown>, context: CommandContext) => {
|
|
115
142
|
const {
|
|
116
143
|
name,
|
|
117
144
|
argsOption: { enum: enumChoices, default: defaultValue, type, desc, nullable, example, ask },
|
|
118
145
|
} = argMeta;
|
|
119
|
-
if (opt[argMeta.name] !== undefined)
|
|
120
|
-
|
|
146
|
+
if (opt[argMeta.name] !== undefined) {
|
|
147
|
+
assertEnumChoice(argMeta, opt[argMeta.name]);
|
|
148
|
+
return convertArgValue(opt[argMeta.name] as string, type ?? "string");
|
|
149
|
+
} else if (defaultValue !== undefined) return defaultValue;
|
|
121
150
|
|
|
122
151
|
if (enumChoices) {
|
|
123
152
|
const choices = normalizeEnumChoices((await resolveEnumChoices(argMeta, context)) ?? []);
|
|
@@ -139,13 +168,15 @@ const getOptionValue = async (argMeta: ArgMeta, opt: Record<string, unknown>, co
|
|
|
139
168
|
}
|
|
140
169
|
};
|
|
141
170
|
|
|
142
|
-
const getArgumentValue = async (argMeta: ArgMeta, value: string | undefined) => {
|
|
171
|
+
export const getArgumentValue = async (argMeta: ArgMeta, value: string | undefined) => {
|
|
143
172
|
const {
|
|
144
173
|
name,
|
|
145
174
|
argsOption: { default: defaultValue, type, desc, nullable, example, ask },
|
|
146
175
|
} = argMeta;
|
|
147
|
-
if (value !== undefined)
|
|
148
|
-
|
|
176
|
+
if (value !== undefined) {
|
|
177
|
+
assertEnumChoice(argMeta, value);
|
|
178
|
+
return convertArgValue(value, type ?? "string");
|
|
179
|
+
} else if (defaultValue !== undefined) return defaultValue;
|
|
149
180
|
else if (nullable) return null;
|
|
150
181
|
|
|
151
182
|
const message = ask
|
|
@@ -184,6 +215,46 @@ const assertCurrentDirectoryIsWorkspaceRoot = async () => {
|
|
|
184
215
|
);
|
|
185
216
|
};
|
|
186
217
|
|
|
218
|
+
/** A variadic positional arrives as an array, and each entry may itself be a comma-separated list. */
|
|
219
|
+
const parseAppNameList = (value: string | string[] | undefined): string[] =>
|
|
220
|
+
(Array.isArray(value) ? value : value === undefined ? [] : [value])
|
|
221
|
+
.flatMap((entry) => entry.split(","))
|
|
222
|
+
.map((entry) => entry.trim())
|
|
223
|
+
.filter(Boolean);
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The `Apps` token: named apps, `all`, or a checkbox when the command line names none. Kept out of
|
|
227
|
+
* `getInternalArgumentValue` because it is the only internal arg whose positional is variadic, so it is
|
|
228
|
+
* the only one whose raw value is an array.
|
|
229
|
+
*/
|
|
230
|
+
export const getAppsArgumentValue = async (
|
|
231
|
+
value: string | string[] | undefined,
|
|
232
|
+
workspace: WorkspaceExecutor,
|
|
233
|
+
): Promise<AppExecutor[]> => {
|
|
234
|
+
const appNames = await workspace.getApps();
|
|
235
|
+
if (appNames.length === 0) throw new Error("No apps found in this workspace (apps/<appName>/akan.config.ts)");
|
|
236
|
+
const requested = parseAppNameList(value);
|
|
237
|
+
if (requested.includes("all")) return appNames.map((name) => AppExecutor.from(workspace, name));
|
|
238
|
+
if (requested.length) {
|
|
239
|
+
const unknown = requested.filter((name) => !appNames.includes(name));
|
|
240
|
+
if (unknown.length)
|
|
241
|
+
throw new Error(
|
|
242
|
+
`Unknown app${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}. Available: ${appNames.join(", ")}, all`,
|
|
243
|
+
);
|
|
244
|
+
return [...new Set(requested)].map((name) => AppExecutor.from(workspace, name));
|
|
245
|
+
}
|
|
246
|
+
if (appNames.length === 1 && appNames[0]) return [AppExecutor.from(workspace, appNames[0])];
|
|
247
|
+
const remembered = new Set(await AppSelectionMemory.read(workspace.workspaceRoot));
|
|
248
|
+
const picked = await checkbox<string>({
|
|
249
|
+
message: "Select the apps to run (space to toggle, enter to confirm)",
|
|
250
|
+
choices: appNames.map((name) => ({ name, value: name, checked: remembered.has(name) })),
|
|
251
|
+
// Enter with nothing ticked is a mis-keypress far more often than an intent to run nothing.
|
|
252
|
+
required: true,
|
|
253
|
+
});
|
|
254
|
+
await AppSelectionMemory.write(workspace.workspaceRoot, picked);
|
|
255
|
+
return picked.map((name) => AppExecutor.from(workspace, name));
|
|
256
|
+
};
|
|
257
|
+
|
|
187
258
|
export const getInternalArgumentValue = async (
|
|
188
259
|
argMeta: InternalArgMeta,
|
|
189
260
|
value: string | undefined,
|
|
@@ -329,6 +400,11 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
|
|
|
329
400
|
`[sys-name:module-name]`,
|
|
330
401
|
`${argMeta.type} in this workspace (apps|libs)/<sys-name>/lib/<module-name>`,
|
|
331
402
|
);
|
|
403
|
+
} else if (argMeta.type === "Apps") {
|
|
404
|
+
programCommand = programCommand.argument(
|
|
405
|
+
`[apps...]`,
|
|
406
|
+
`apps in this workspace apps/<appName>, or all (space- or comma-separated; omit to pick interactively)`,
|
|
407
|
+
);
|
|
332
408
|
} else {
|
|
333
409
|
const sysType = argMeta.type.toLowerCase();
|
|
334
410
|
programCommand = programCommand.argument(
|
|
@@ -357,6 +433,11 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
|
|
|
357
433
|
commandArgs[argMeta.idx] = await getOptionValue(argMeta, opt, commandContext);
|
|
358
434
|
else if (argMeta.type === "Argument")
|
|
359
435
|
commandArgs[argMeta.idx] = await getArgumentValue(argMeta, cmdArgs[argMeta.idx] as string);
|
|
436
|
+
else if (argMeta.type === "Apps")
|
|
437
|
+
commandArgs[argMeta.idx] = await getAppsArgumentValue(
|
|
438
|
+
cmdArgs[argMeta.idx] as string | string[] | undefined,
|
|
439
|
+
workspace,
|
|
440
|
+
);
|
|
360
441
|
else
|
|
361
442
|
commandArgs[argMeta.idx] = await getInternalArgumentValue(
|
|
362
443
|
argMeta as InternalArgMeta,
|
|
@@ -366,6 +447,12 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
|
|
|
366
447
|
// set app name to env
|
|
367
448
|
if (commandArgs[argMeta.idx] instanceof AppExecutor)
|
|
368
449
|
process.env.AKAN_PUBLIC_APP_NAME = (commandArgs[argMeta.idx] as AppExecutor).name;
|
|
450
|
+
//? Only when exactly one app resolved. Publishing a name while several are in play would make
|
|
451
|
+
//? every env-derived answer in this process belong to whichever app happened to be last.
|
|
452
|
+
else if (Array.isArray(commandArgs[argMeta.idx])) {
|
|
453
|
+
const apps = commandArgs[argMeta.idx] as AppExecutor[];
|
|
454
|
+
if (apps.length === 1 && apps[0]) process.env.AKAN_PUBLIC_APP_NAME = apps[0].name;
|
|
455
|
+
}
|
|
369
456
|
assignCommandContext(commandContext, argMeta, commandArgs[argMeta.idx]);
|
|
370
457
|
if ((opt as { verbose?: boolean }).verbose) Executor.setVerbose(true);
|
|
371
458
|
}
|
|
@@ -23,9 +23,24 @@ type PrimitiveValue<T extends PrimitiveArgType> = T extends StringConstructor
|
|
|
23
23
|
? number
|
|
24
24
|
: boolean;
|
|
25
25
|
type MaybeNullable<Value, Option> = Option extends { nullable: true } ? Value | null : Value;
|
|
26
|
+
/**
|
|
27
|
+
* The literal union a static `enum` declares, or `never` for a `DynamicEnum` — a function does not extend
|
|
28
|
+
* a readonly array, so a runtime-resolved choice list falls back to the primitive type. `{ label, value }`
|
|
29
|
+
* choices contribute their `value`.
|
|
30
|
+
*/
|
|
31
|
+
type EnumValue<Option> = Option extends { enum: infer Choices }
|
|
32
|
+
? Choices extends readonly (infer Choice)[]
|
|
33
|
+
? Choice extends { value: infer Value }
|
|
34
|
+
? Value
|
|
35
|
+
: Choice
|
|
36
|
+
: never
|
|
37
|
+
: never;
|
|
38
|
+
type ArgValue<Type extends PrimitiveArgType, Option> = [EnumValue<Option>] extends [never]
|
|
39
|
+
? PrimitiveValue<Type>
|
|
40
|
+
: EnumValue<Option>;
|
|
26
41
|
type AddArg<Params extends unknown[], Type extends PrimitiveArgType, Option> = [
|
|
27
42
|
...Params,
|
|
28
|
-
MaybeNullable<
|
|
43
|
+
MaybeNullable<ArgValue<Type, Option>, Option>,
|
|
29
44
|
];
|
|
30
45
|
type AddInternalArg<Params extends unknown[], Token extends InternalArgToken> = [...Params, Token["_value"]];
|
|
31
46
|
type AddInternalArgs<Params extends unknown[], Tokens extends readonly InternalArgToken[]> = Tokens extends readonly [
|
|
@@ -68,7 +83,7 @@ class TargetBuilder<Deps extends readonly DependencyCls[], Params extends unknow
|
|
|
68
83
|
this.#args = args;
|
|
69
84
|
}
|
|
70
85
|
|
|
71
|
-
arg<Type extends PrimitiveArgType, Option extends ArgsOption<Context> = ArgsOption<Context>>(
|
|
86
|
+
arg<Type extends PrimitiveArgType, const Option extends ArgsOption<Context> = ArgsOption<Context>>(
|
|
72
87
|
name: string,
|
|
73
88
|
type: Type,
|
|
74
89
|
argsOption: Option = {} as Option,
|
|
@@ -85,7 +100,7 @@ class TargetBuilder<Deps extends readonly DependencyCls[], Params extends unknow
|
|
|
85
100
|
]);
|
|
86
101
|
}
|
|
87
102
|
|
|
88
|
-
option<Type extends PrimitiveArgType, Option extends ArgsOption<Context> = ArgsOption<Context>>(
|
|
103
|
+
option<Type extends PrimitiveArgType, const Option extends ArgsOption<Context> = ArgsOption<Context>>(
|
|
89
104
|
name: string,
|
|
90
105
|
type: Type,
|
|
91
106
|
argsOption: Option = {} as Option,
|
|
@@ -135,12 +150,12 @@ type CommandBuilderContext<Deps extends readonly DependencyCls[]> = {
|
|
|
135
150
|
public: (targetOption?: Omit<TargetOption, "type">) => TargetBuilder<Deps>;
|
|
136
151
|
cloud: (targetOption?: Omit<TargetOption, "type">) => TargetBuilder<Deps>;
|
|
137
152
|
dev: (targetOption?: Omit<TargetOption, "type">) => TargetBuilder<Deps>;
|
|
138
|
-
arg: <Type extends PrimitiveArgType, Option extends ArgsOption<CommandContext> = ArgsOption<CommandContext>>(
|
|
153
|
+
arg: <Type extends PrimitiveArgType, const Option extends ArgsOption<CommandContext> = ArgsOption<CommandContext>>(
|
|
139
154
|
name: string,
|
|
140
155
|
type: Type,
|
|
141
156
|
argsOption?: Option,
|
|
142
157
|
) => ArgMeta;
|
|
143
|
-
option: <Type extends PrimitiveArgType, Option extends ArgsOption<CommandContext> = ArgsOption<CommandContext>>(
|
|
158
|
+
option: <Type extends PrimitiveArgType, const Option extends ArgsOption<CommandContext> = ArgsOption<CommandContext>>(
|
|
144
159
|
name: string,
|
|
145
160
|
type: Type,
|
|
146
161
|
argsOption?: Option,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { AppExecutor, WorkspaceExecutor } from "../executors";
|
|
3
|
-
import { App, getArgMetas, Lib, Module, Pkg, Sys, Workspace } from "./argMeta";
|
|
4
|
-
import { getInternalArgumentValue } from "./command";
|
|
3
|
+
import { App, type ArgMeta, type ArgsOption, getArgMetas, Lib, Module, Pkg, Sys, Workspace } from "./argMeta";
|
|
4
|
+
import { getArgumentValue, getInternalArgumentValue, getOptionValue } from "./command";
|
|
5
5
|
import { command } from "./commandBuilder";
|
|
6
6
|
import {
|
|
7
7
|
assertUniqueDependencies,
|
|
@@ -136,7 +136,7 @@ describe("command helper metadata", () => {
|
|
|
136
136
|
|
|
137
137
|
const app = await getInternalArgumentValue({ key: "", idx: 0, type: "App" }, undefined, workspace);
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
if (!(app instanceof AppExecutor)) throw new Error(`expected an AppExecutor, got ${app.constructor.name}`);
|
|
140
140
|
expect(app.name).toBe("single-command-test-app");
|
|
141
141
|
expect(app.workspace).toBe(workspace);
|
|
142
142
|
});
|
|
@@ -223,3 +223,115 @@ describe("command helper dependency injection", () => {
|
|
|
223
223
|
expect(descriptor?.writable).toBe(false);
|
|
224
224
|
});
|
|
225
225
|
});
|
|
226
|
+
|
|
227
|
+
describe("enum arg choices", () => {
|
|
228
|
+
const optionMeta = (enumChoices: ArgsOption["enum"]): ArgMeta => ({
|
|
229
|
+
name: "format",
|
|
230
|
+
argsOption: { type: "string", enum: enumChoices },
|
|
231
|
+
key: "run",
|
|
232
|
+
idx: 0,
|
|
233
|
+
type: "Option",
|
|
234
|
+
});
|
|
235
|
+
const argumentMeta = (enumChoices: ArgsOption["enum"]): ArgMeta => ({
|
|
236
|
+
...optionMeta(enumChoices),
|
|
237
|
+
name: "action",
|
|
238
|
+
type: "Argument",
|
|
239
|
+
});
|
|
240
|
+
const context = { values: {} };
|
|
241
|
+
|
|
242
|
+
test("passes a declared choice through", async () => {
|
|
243
|
+
expect(await getOptionValue(optionMeta(["text", "json"]), { format: "json" }, context)).toBe("json");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Before this check `akan quality --format yaml` reached the script as "yaml" while the declaration
|
|
247
|
+
// claimed the parameter was "text" | "json".
|
|
248
|
+
test("rejects an undeclared value in commander's wording", async () => {
|
|
249
|
+
await expect(getOptionValue(optionMeta(["text", "json"]), { format: "yaml" }, context)).rejects.toThrow(
|
|
250
|
+
"option '--format' argument 'yaml' is invalid. Allowed choices are text, json.",
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("names a positional argument as an argument", async () => {
|
|
255
|
+
await expect(getArgumentValue(argumentMeta(["scan", "ssr"]), "sssr")).rejects.toThrow(
|
|
256
|
+
"argument 'action' argument 'sssr' is invalid. Allowed choices are scan, ssr.",
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("kebab-cases a camelCase option name the way the flag is spelled", async () => {
|
|
261
|
+
await expect(
|
|
262
|
+
getOptionValue({ ...optionMeta(["apk", "aab"]), name: "assembleType" }, { assembleType: "ipa" }, context),
|
|
263
|
+
).rejects.toThrow("option '--assemble-type'");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("compares a labelled choice by its value, not its label", async () => {
|
|
267
|
+
const labelled = optionMeta([{ label: "JSON output", value: "json" }]);
|
|
268
|
+
expect(await getOptionValue(labelled, { format: "json" }, context)).toBe("json");
|
|
269
|
+
await expect(getOptionValue(labelled, { format: "JSON output" }, context)).rejects.toThrow("is invalid");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// A DynamicEnum resolves against a context that is not populated yet, and the interactive select is its
|
|
273
|
+
// only consumer, so an explicit value passes through unchecked rather than being rejected wrongly.
|
|
274
|
+
test("leaves a dynamic choice list unchecked", async () => {
|
|
275
|
+
expect(
|
|
276
|
+
await getOptionValue(
|
|
277
|
+
optionMeta(() => ["text"]),
|
|
278
|
+
{ format: "anything" },
|
|
279
|
+
context,
|
|
280
|
+
),
|
|
281
|
+
).toBe("anything");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("an option with no enum is unaffected", async () => {
|
|
285
|
+
expect(await getOptionValue(optionMeta(undefined), { format: "whatever" }, context)).toBe("whatever");
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe("enum arg type inference", () => {
|
|
290
|
+
afterEach(() => {
|
|
291
|
+
CommandContainer.clear();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Compile-time only. Nothing here can fail at runtime: if `enum` stops narrowing, the parameter widens
|
|
296
|
+
* to the primitive and these calls stop typechecking, so the regression surfaces in `akan typecheck`
|
|
297
|
+
* rather than in fifteen `as "a" | "b"` casts creeping back into the command files.
|
|
298
|
+
*/
|
|
299
|
+
const expectLiteral = <Expected>(_value: Expected) => undefined;
|
|
300
|
+
|
|
301
|
+
test("a static enum narrows the exec parameter to its declared choices", () => {
|
|
302
|
+
class InferenceRunner extends runner("inference") {}
|
|
303
|
+
class InferenceScript extends script("inference", [InferenceRunner]) {}
|
|
304
|
+
|
|
305
|
+
class InferenceCommand extends command("inference", [InferenceScript], ({ public: target }) => ({
|
|
306
|
+
run: target({ desc: "run" })
|
|
307
|
+
.arg("action", String, { enum: ["scan", "ssr"] })
|
|
308
|
+
.option("format", String, { enum: ["text", "json"], default: "text" })
|
|
309
|
+
.option("count", Number, { enum: [1, 2] })
|
|
310
|
+
.option("scope", String, { enum: ["all", "one"], nullable: true })
|
|
311
|
+
.option("label", String, { enum: [{ label: "JSON output", value: "json" }] })
|
|
312
|
+
.option("plain", String, { desc: "no choices declared" })
|
|
313
|
+
.option("dynamic", String, { enum: () => ["a", "b"] })
|
|
314
|
+
.exec(async (action, format, count, scope, label, plain, dynamic) => {
|
|
315
|
+
expectLiteral<"scan" | "ssr">(action);
|
|
316
|
+
expectLiteral<"text" | "json">(format);
|
|
317
|
+
expectLiteral<1 | 2>(count);
|
|
318
|
+
expectLiteral<"all" | "one" | null>(scope);
|
|
319
|
+
expectLiteral<"json">(label);
|
|
320
|
+
expectLiteral<string>(plain);
|
|
321
|
+
expectLiteral<string>(dynamic);
|
|
322
|
+
}),
|
|
323
|
+
})) {}
|
|
324
|
+
|
|
325
|
+
const [targetMeta] = getTargetMetas(InferenceCommand);
|
|
326
|
+
expect(targetMeta?.key).toBe("run");
|
|
327
|
+
expect(targetMeta?.args.map((arg) => ("name" in arg ? arg.name : arg.type))).toEqual([
|
|
328
|
+
"action",
|
|
329
|
+
"format",
|
|
330
|
+
"count",
|
|
331
|
+
"scope",
|
|
332
|
+
"label",
|
|
333
|
+
"plain",
|
|
334
|
+
"dynamic",
|
|
335
|
+
]);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
@@ -159,6 +159,7 @@ export const formatCommandHelp = (command: CommandCls, key: string) => {
|
|
|
159
159
|
.map((arg) => {
|
|
160
160
|
if (arg.type === "Workspace") return "";
|
|
161
161
|
if (arg.type === "Module") return "[sys:module]";
|
|
162
|
+
if (arg.type === "Apps") return "[apps...]";
|
|
162
163
|
if (arg.type === "Argument") {
|
|
163
164
|
return `[${camelToKebabCase(arg.name)}]`;
|
|
164
165
|
}
|
|
@@ -192,6 +193,9 @@ export const formatCommandHelp = (command: CommandCls, key: string) => {
|
|
|
192
193
|
} else if (arg.type === "Module") {
|
|
193
194
|
argName = "sys:module";
|
|
194
195
|
argDesc = "Module in format: app-name:module-name or lib-name:module-name";
|
|
196
|
+
} else if (arg.type === "Apps") {
|
|
197
|
+
argName = "apps...";
|
|
198
|
+
argDesc = "App names, space- or comma-separated, or all. Omit to pick them interactively";
|
|
195
199
|
} else {
|
|
196
200
|
argName = arg.type.toLowerCase();
|
|
197
201
|
argDesc = `${arg.type} name in this workspace`;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { TypeScriptDependencyScanner } from "./dependencyScanner";
|
|
6
|
+
import type { PackageJson } from "./types";
|
|
7
|
+
|
|
8
|
+
const tempRoots: string[] = [];
|
|
9
|
+
|
|
10
|
+
const makeTempRoot = async () => {
|
|
11
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "akan-devkit-depscan-"));
|
|
12
|
+
tempRoots.push(root);
|
|
13
|
+
return root;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const write = async (root: string, relPath: string, content: string) => {
|
|
17
|
+
const filePath = path.join(root, relPath);
|
|
18
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
19
|
+
await writeFile(filePath, content);
|
|
20
|
+
return filePath;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const scannerFor = async (root: string, dependencies: Record<string, string> = {}) =>
|
|
24
|
+
new TypeScriptDependencyScanner(path.join(root, "pkgs/@akanjs/devkit"), {
|
|
25
|
+
workspaceRoot: root,
|
|
26
|
+
tsconfig: { compilerOptions: { target: "esnext" } },
|
|
27
|
+
rootPackageJson: { dependencies } as unknown as PackageJson,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("TypeScriptDependencyScanner — getPackageBuildDependencies", () => {
|
|
35
|
+
test("collects imports from ordinary source files", async () => {
|
|
36
|
+
const root = await makeTempRoot();
|
|
37
|
+
await write(root, "pkgs/@akanjs/devkit/linter.ts", `import chalk from "chalk";\nexport const x = chalk;\n`);
|
|
38
|
+
const scanner = await scannerFor(root, { chalk: "^5.0.0" });
|
|
39
|
+
|
|
40
|
+
const { npmDeps, missingDeps } = await scanner.getPackageBuildDependencies("@akanjs/devkit");
|
|
41
|
+
|
|
42
|
+
expect(npmDeps).toEqual(["chalk"]);
|
|
43
|
+
expect(missingDeps).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("reports an import that root package.json does not version", async () => {
|
|
47
|
+
const root = await makeTempRoot();
|
|
48
|
+
await write(root, "pkgs/@akanjs/devkit/linter.ts", `import clsx from "clsx";\nexport const x = clsx;\n`);
|
|
49
|
+
const scanner = await scannerFor(root);
|
|
50
|
+
|
|
51
|
+
expect((await scanner.getPackageBuildDependencies("@akanjs/devkit")).missingDeps).toEqual(["clsx"]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("skips __fixtures__, whose imports are lint violations rather than dependencies", async () => {
|
|
55
|
+
const root = await makeTempRoot();
|
|
56
|
+
await write(
|
|
57
|
+
root,
|
|
58
|
+
"pkgs/@akanjs/devkit/lint/__fixtures__/no-import-external-library/bad.tsx",
|
|
59
|
+
`import clsx from "clsx";\nimport { cnst } from "@libs/shared/client";\nexport const x = [clsx, cnst];\n`,
|
|
60
|
+
);
|
|
61
|
+
const scanner = await scannerFor(root);
|
|
62
|
+
|
|
63
|
+
const { npmDeps, npmDevDeps, missingDeps } = await scanner.getPackageBuildDependencies("@akanjs/devkit");
|
|
64
|
+
|
|
65
|
+
expect(missingDeps).toEqual([]);
|
|
66
|
+
expect(npmDeps).toEqual([]);
|
|
67
|
+
expect(npmDevDeps).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("skips a nested __fixtures__ directory at any depth", async () => {
|
|
71
|
+
const root = await makeTempRoot();
|
|
72
|
+
await write(root, "pkgs/@akanjs/devkit/__fixtures__/bad.ts", `import a from "pkg-a";\nexport const x = a;\n`);
|
|
73
|
+
await write(root, "pkgs/@akanjs/devkit/a/b/__fixtures__/c/bad.ts", `import b from "pkg-b";\nexport const x = b;\n`);
|
|
74
|
+
const scanner = await scannerFor(root);
|
|
75
|
+
|
|
76
|
+
expect((await scanner.getPackageBuildDependencies("@akanjs/devkit")).missingDeps).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("keeps a directory whose name merely contains the fixtures marker", async () => {
|
|
80
|
+
const root = await makeTempRoot();
|
|
81
|
+
await write(
|
|
82
|
+
root,
|
|
83
|
+
"pkgs/@akanjs/devkit/__fixtures__helper/keep.ts",
|
|
84
|
+
`import a from "pkg-a";\nexport const x = a;\n`,
|
|
85
|
+
);
|
|
86
|
+
const scanner = await scannerFor(root);
|
|
87
|
+
|
|
88
|
+
expect((await scanner.getPackageBuildDependencies("@akanjs/devkit")).missingDeps).toEqual(["pkg-a"]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("skips test files but still reports their non-test siblings", async () => {
|
|
92
|
+
const root = await makeTempRoot();
|
|
93
|
+
await write(root, "pkgs/@akanjs/devkit/linter.test.ts", `import a from "pkg-a";\nexport const x = a;\n`);
|
|
94
|
+
await write(root, "pkgs/@akanjs/devkit/linter.ts", `import b from "pkg-b";\nexport const x = b;\n`);
|
|
95
|
+
const scanner = await scannerFor(root);
|
|
96
|
+
|
|
97
|
+
expect((await scanner.getPackageBuildDependencies("@akanjs/devkit")).missingDeps).toEqual(["pkg-b"]);
|
|
98
|
+
});
|
|
99
|
+
});
|
package/dependencyScanner.ts
CHANGED
|
@@ -7,6 +7,11 @@ import { FileSys } from "./fileSys";
|
|
|
7
7
|
import type { PackageJson, TsConfigJson } from "./types";
|
|
8
8
|
|
|
9
9
|
const testFileRegex = /\.(?:test|spec)\.[cm]?[tj]sx?$/;
|
|
10
|
+
// `__fixtures__` holds deliberately invalid sample sources for the lint-rule suites; tsconfig `exclude` and
|
|
11
|
+
// biome's `!` override already skip them, and their imports are violations rather than real dependencies.
|
|
12
|
+
const skipDirs = ["node_modules", "dist", "build", ".git", ".next", "public", "ios", "android", "__fixtures__"];
|
|
13
|
+
const isSkippedPath = (filePath: string) =>
|
|
14
|
+
skipDirs.some((dir) => filePath.includes(`/${dir}/`) || filePath.startsWith(`${dir}/`));
|
|
10
15
|
const builtinModuleSet = new Set([...builtinModules, ...builtinModules.map((mod) => `node:${mod}`)]);
|
|
11
16
|
const stripShebang = (source: string) => source.replace(/^#!.*(?:\r?\n|$)/, "");
|
|
12
17
|
|
|
@@ -17,10 +22,10 @@ export class TypeScriptDependencyScanner {
|
|
|
17
22
|
#visitedFiles = new Set<string>();
|
|
18
23
|
readonly #tsTranspiler = new Bun.Transpiler({ loader: "ts" });
|
|
19
24
|
readonly #tsxTranspiler = new Bun.Transpiler({ loader: "tsx" });
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
readonly #directory: string;
|
|
26
|
+
readonly #rootPackageJson: PackageJson;
|
|
27
|
+
readonly #ig: ReturnType<typeof ignore>;
|
|
28
|
+
readonly #workspaceRoot: string;
|
|
24
29
|
|
|
25
30
|
constructor(
|
|
26
31
|
directory: string,
|
|
@@ -30,10 +35,10 @@ export class TypeScriptDependencyScanner {
|
|
|
30
35
|
gitignorePatterns = [],
|
|
31
36
|
}: { workspaceRoot: string; tsconfig: TsConfigJson; rootPackageJson: PackageJson; gitignorePatterns?: string[] },
|
|
32
37
|
) {
|
|
33
|
-
this
|
|
34
|
-
this
|
|
35
|
-
this
|
|
36
|
-
this
|
|
38
|
+
this.#directory = directory;
|
|
39
|
+
this.#rootPackageJson = rootPackageJson;
|
|
40
|
+
this.#ig = ignore().add(gitignorePatterns);
|
|
41
|
+
this.#workspaceRoot = workspaceRoot;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
async getMonorepoDependencies(
|
|
@@ -41,7 +46,7 @@ export class TypeScriptDependencyScanner {
|
|
|
41
46
|
{ pkgs = [], libs = [] }: { pkgs?: string[]; libs?: string[] } = {},
|
|
42
47
|
): Promise<{ pkgDeps: string[]; libDeps: string[]; npmDeps: string[]; npmDevDeps: string[] }> {
|
|
43
48
|
const npmSet = new Set(
|
|
44
|
-
Object.keys({ ...this
|
|
49
|
+
Object.keys({ ...this.#rootPackageJson.dependencies, ...this.#rootPackageJson.devDependencies }),
|
|
45
50
|
);
|
|
46
51
|
const pkgPathSet = new Set(pkgs);
|
|
47
52
|
const libPathSet = new Set(libs.map((lib) => `@libs/${lib}`));
|
|
@@ -68,11 +73,11 @@ export class TypeScriptDependencyScanner {
|
|
|
68
73
|
): Promise<{ npmDeps: string[]; npmDevDeps: string[]; missingDeps: string[] }> {
|
|
69
74
|
const runtimeDeps = new Set<string>();
|
|
70
75
|
const devDeps = new Set<string>();
|
|
71
|
-
const sourceFiles = await this.#findTypeScriptFiles(this
|
|
76
|
+
const sourceFiles = await this.#findTypeScriptFiles(this.#directory, {
|
|
72
77
|
excludeBuildFiles: true,
|
|
73
78
|
excludeTestFiles: true,
|
|
74
79
|
});
|
|
75
|
-
const cssFiles = await this.#findCssFiles(this
|
|
80
|
+
const cssFiles = await this.#findCssFiles(this.#directory);
|
|
76
81
|
|
|
77
82
|
for (const filePath of sourceFiles) {
|
|
78
83
|
const fileContent = await FileSys.readText(filePath);
|
|
@@ -86,7 +91,7 @@ export class TypeScriptDependencyScanner {
|
|
|
86
91
|
this.#addNormalizedImports(runtimeDeps, this.#extractCssPluginImports(fileContent), projectName);
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
const buildFilePath = path.join(this
|
|
94
|
+
const buildFilePath = path.join(this.#directory, "build.ts");
|
|
90
95
|
if (await FileSys.fileExists(buildFilePath)) {
|
|
91
96
|
const fileContent = await FileSys.readText(buildFilePath);
|
|
92
97
|
const { imports, typeImports } = this.#extractImports(fileContent, buildFilePath);
|
|
@@ -95,7 +100,7 @@ export class TypeScriptDependencyScanner {
|
|
|
95
100
|
|
|
96
101
|
for (const dep of runtimeDeps) devDeps.delete(dep);
|
|
97
102
|
|
|
98
|
-
const rootDeps = { ...this
|
|
103
|
+
const rootDeps = { ...this.#rootPackageJson.dependencies, ...this.#rootPackageJson.devDependencies };
|
|
99
104
|
const missingDeps: string[] = [];
|
|
100
105
|
for (const dep of [...runtimeDeps, ...devDeps]) {
|
|
101
106
|
if (rootDeps[dep] || (await this.#hasWorkspacePackage(dep))) continue;
|
|
@@ -109,7 +114,7 @@ export class TypeScriptDependencyScanner {
|
|
|
109
114
|
}
|
|
110
115
|
|
|
111
116
|
async #hasWorkspacePackage(dep: string) {
|
|
112
|
-
const packageJsonPath = path.join(this
|
|
117
|
+
const packageJsonPath = path.join(this.#workspaceRoot, "pkgs", dep, "package.json");
|
|
113
118
|
if (!(await Bun.file(packageJsonPath).exists())) return false;
|
|
114
119
|
try {
|
|
115
120
|
const packageJson = await FileSys.readJson<PackageJson>(packageJsonPath);
|
|
@@ -156,9 +161,9 @@ export class TypeScriptDependencyScanner {
|
|
|
156
161
|
this.#fileTypeDependencies.clear();
|
|
157
162
|
this.#visitedFiles.clear();
|
|
158
163
|
|
|
159
|
-
const files = await this.#findTypeScriptFiles(this
|
|
164
|
+
const files = await this.#findTypeScriptFiles(this.#directory);
|
|
160
165
|
|
|
161
|
-
for (const file of files) await this.#analyzeFile(file, this
|
|
166
|
+
for (const file of files) await this.#analyzeFile(file, this.#directory);
|
|
162
167
|
|
|
163
168
|
return this.#fileDependencies;
|
|
164
169
|
}
|
|
@@ -171,17 +176,16 @@ export class TypeScriptDependencyScanner {
|
|
|
171
176
|
}: { excludeBuildFiles?: boolean; excludeTestFiles?: boolean } = {},
|
|
172
177
|
): Promise<string[]> {
|
|
173
178
|
const files: string[] = [];
|
|
174
|
-
const skipDirs = ["node_modules", "dist", "build", ".git", ".next", "public", "ios", "android"];
|
|
175
179
|
|
|
176
180
|
const glob = new Bun.Glob("**/*.{ts,tsx}");
|
|
177
181
|
for await (const filePath of glob.scan({ cwd: directory, onlyFiles: true })) {
|
|
178
|
-
if (
|
|
182
|
+
if (isSkippedPath(filePath)) continue;
|
|
179
183
|
if (excludeBuildFiles && filePath === "build.ts") continue;
|
|
180
184
|
if (excludeTestFiles && testFileRegex.test(filePath)) continue;
|
|
181
185
|
|
|
182
186
|
const fullPath = path.join(directory, filePath);
|
|
183
|
-
const relativePath = path.relative(this
|
|
184
|
-
if (this
|
|
187
|
+
const relativePath = path.relative(this.#workspaceRoot, fullPath);
|
|
188
|
+
if (this.#ig.ignores(relativePath)) continue;
|
|
185
189
|
|
|
186
190
|
files.push(fullPath);
|
|
187
191
|
}
|
|
@@ -190,14 +194,13 @@ export class TypeScriptDependencyScanner {
|
|
|
190
194
|
|
|
191
195
|
async #findCssFiles(directory: string): Promise<string[]> {
|
|
192
196
|
const files: string[] = [];
|
|
193
|
-
const skipDirs = ["node_modules", "dist", "build", ".git", ".next", "public", "ios", "android"];
|
|
194
197
|
const glob = new Bun.Glob("**/*.css");
|
|
195
198
|
for await (const filePath of glob.scan({ cwd: directory, onlyFiles: true })) {
|
|
196
|
-
if (
|
|
199
|
+
if (isSkippedPath(filePath)) continue;
|
|
197
200
|
|
|
198
201
|
const fullPath = path.join(directory, filePath);
|
|
199
|
-
const relativePath = path.relative(this
|
|
200
|
-
if (this
|
|
202
|
+
const relativePath = path.relative(this.#workspaceRoot, fullPath);
|
|
203
|
+
if (this.#ig.ignores(relativePath)) continue;
|
|
201
204
|
|
|
202
205
|
files.push(fullPath);
|
|
203
206
|
}
|