@akanjs/devkit 3.0.0-beta.19 → 3.0.0-beta.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 (233) hide show
  1. package/abstractDoc.test.ts +6 -15
  2. package/abstractDoc.ts +1 -4
  3. package/agentsIndex.test.ts +4 -7
  4. package/agentsIndex.ts +6 -22
  5. package/akanApp/BackendImportGraph.test.ts +8 -22
  6. package/akanApp/BackendImportGraph.ts +1 -20
  7. package/akanApp/akanApp.host.test.ts +1 -3
  8. package/akanApp/akanApp.host.ts +64 -250
  9. package/akanApp/devHostPolicy.test.ts +10 -34
  10. package/akanApp/devHostPolicy.ts +12 -98
  11. package/akanConfig/akanConfig.test.ts +30 -39
  12. package/akanConfig/akanConfig.ts +40 -136
  13. package/akanContext.ts +106 -159
  14. package/akanMcpContract.ts +28 -73
  15. package/appSelectionMemory.ts +1 -4
  16. package/applicationBuildReporter.ts +37 -5
  17. package/applicationBuildRunner.ts +25 -59
  18. package/applicationReleasePackager.ts +24 -44
  19. package/applicationTestPreload.ts +3 -12
  20. package/artifact/implicitRootLayout.test.ts +3 -19
  21. package/artifact/implicitRootLayout.ts +34 -91
  22. package/artifact/routeSeedIndex.ts +11 -44
  23. package/biomeBase.ts +2 -5
  24. package/capacitorApp.test.ts +4 -14
  25. package/capacitorApp.ts +86 -235
  26. package/cloud/cloudApi.test.ts +202 -0
  27. package/cloud/cloudApi.ts +33 -57
  28. package/cloud/configLock.test.ts +64 -0
  29. package/cloud/configLock.ts +96 -0
  30. package/cloud/constants.ts +3 -2
  31. package/cloud/globalConfig.ts +64 -31
  32. package/codeAgent/agent/AkanCodeServices.ts +15 -57
  33. package/codeAgent/agent/AkanContextFiles.ts +5 -28
  34. package/codeAgent/agent/CodeAgent.ts +33 -108
  35. package/codeAgent/agent/CodeAgentAsks.ts +7 -30
  36. package/codeAgent/agent/CodeAgentEventMapper.ts +9 -52
  37. package/codeAgent/agent/CodeAgentGate.ts +8 -34
  38. package/codeAgent/agent/CodeAgentProxy.ts +4 -13
  39. package/codeAgent/agent/CodeAgentRpcHost.ts +3 -16
  40. package/codeAgent/agent/CodeAgentRpcListener.ts +2 -10
  41. package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -15
  42. package/codeAgent/agent/CodeAgentSuspended.ts +1 -8
  43. package/codeAgent/agent/CodeAgentUi.ts +2 -7
  44. package/codeAgent/agent/CodeMailbox.ts +7 -34
  45. package/codeAgent/agent/CodeSessionFork.ts +4 -19
  46. package/codeAgent/agent/CodeSessionIndex.ts +5 -31
  47. package/codeAgent/agent/SubagentPool.ts +48 -58
  48. package/codeAgent/agent/akanCodeModel.ts +5 -39
  49. package/codeAgent/agent/akanCodePaths.ts +4 -24
  50. package/codeAgent/agent/akanContextFiles.test.ts +0 -2
  51. package/codeAgent/agent/akanSystemPrompt.ts +1 -9
  52. package/codeAgent/agent/codeAgent.test.ts +178 -0
  53. package/codeAgent/agent/codeAgentContract.test.ts +5 -28
  54. package/codeAgent/agent/codeMailbox.test.ts +0 -4
  55. package/codeAgent/agent/codeSessionFork.test.ts +0 -2
  56. package/codeAgent/agent/codeSessionIndex.test.ts +9 -4
  57. package/codeAgent/agent/consoleToStderr.ts +2 -12
  58. package/codeAgent/agent/jsonLines.ts +29 -0
  59. package/codeAgent/agent/stringArg.ts +5 -0
  60. package/codeAgent/feedback/DevLogFeedback.ts +3 -18
  61. package/codeAgent/feedback/PreviewChrome.ts +5 -29
  62. package/codeAgent/feedback/PreviewFeedback.ts +2 -9
  63. package/codeAgent/feedback/PreviewView.ts +3 -15
  64. package/codeAgent/feedback/TurnFeedback.ts +2 -17
  65. package/codeAgent/feedback/VerifyFeedback.ts +1 -7
  66. package/codeAgent/feedback/previewChrome.test.ts +1 -1
  67. package/codeAgent/index.ts +2 -10
  68. package/codeAgent/skills/akan-code/SKILL.md +12 -4
  69. package/codeAgent/tools/AkanCli.ts +4 -16
  70. package/codeAgent/tools/AkanCodePlugins.ts +21 -43
  71. package/codeAgent/tools/AkanEditScope.ts +2 -12
  72. package/codeAgent/tools/AkanEnvKeys.ts +3 -11
  73. package/codeAgent/tools/AkanToolPack.ts +6 -28
  74. package/codeAgent/tools/AkanVerifier.ts +1 -8
  75. package/codeAgent/tools/AskToolPack.ts +3 -15
  76. package/codeAgent/tools/MailToolPack.ts +0 -9
  77. package/codeAgent/tools/McpClient.ts +3 -24
  78. package/codeAgent/tools/McpOAuth.ts +6 -41
  79. package/codeAgent/tools/McpOAuthLoopback.ts +2 -15
  80. package/codeAgent/tools/McpServerConfig.ts +7 -26
  81. package/codeAgent/tools/McpSignIn.ts +46 -49
  82. package/codeAgent/tools/McpTokenStore.ts +6 -17
  83. package/codeAgent/tools/McpToolPack.ts +107 -35
  84. package/codeAgent/tools/SessionToolPack.ts +1 -11
  85. package/codeAgent/tools/WebToolPack.ts +1 -7
  86. package/codeAgent/tools/akanCodePlugins.test.ts +231 -63
  87. package/codeAgent/tools/askToolPack.test.ts +0 -3
  88. package/codeAgent/tools/mcpOAuth.test.ts +315 -27
  89. package/codeAgent/tools/mcpServerConfig.test.ts +21 -14
  90. package/codegenLock.test.ts +4 -13
  91. package/codegenLock.ts +3 -17
  92. package/commandDecorators/argMeta.ts +1 -5
  93. package/commandDecorators/camelToKebabCase.ts +1 -0
  94. package/commandDecorators/command.ts +44 -102
  95. package/commandDecorators/commandBuilder.ts +30 -61
  96. package/commandDecorators/commandDecorators.test.ts +1 -9
  97. package/commandDecorators/dependencyBuilder.ts +10 -1
  98. package/commandDecorators/helpFormatter.ts +66 -144
  99. package/commandDecorators/index.ts +0 -2
  100. package/commandDecorators/targetMeta.ts +9 -7
  101. package/createTunnel.ts +7 -14
  102. package/dependencyScanner.test.ts +4 -16
  103. package/dependencyScanner.ts +27 -78
  104. package/devkitUtils.test.ts +47 -19
  105. package/executors.test.ts +114 -60
  106. package/executors.ts +165 -484
  107. package/fileEditor.ts +9 -37
  108. package/{getDirname.test.ts → fileSys.test.ts} +1 -1
  109. package/fileSys.ts +6 -8
  110. package/formSetterScanner.ts +3 -16
  111. package/frontendBuild/allRoutesBuilder.ts +7 -21
  112. package/frontendBuild/autoImportSync.test.ts +14 -38
  113. package/frontendBuild/autoImportSync.ts +15 -67
  114. package/frontendBuild/buildRouteClient.test.ts +10 -36
  115. package/frontendBuild/bundleDefine.ts +12 -0
  116. package/frontendBuild/clientBuildTypes.ts +5 -36
  117. package/frontendBuild/clientEntriesBundler.ts +9 -40
  118. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  119. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  120. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  121. package/frontendBuild/cssCandidateCache.ts +6 -41
  122. package/frontendBuild/cssCompiler.ts +29 -78
  123. package/frontendBuild/cssImportResolver.ts +5 -18
  124. package/frontendBuild/devChangePlanner.ts +7 -10
  125. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  126. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  127. package/frontendBuild/fontOptimizer.test.ts +5 -13
  128. package/frontendBuild/fontOptimizer.ts +15 -72
  129. package/frontendBuild/fontPruner.test.ts +5 -10
  130. package/frontendBuild/fontPruner.ts +5 -20
  131. package/frontendBuild/frontendBuild.test.ts +27 -52
  132. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  133. package/frontendBuild/hmrWatcher.test.ts +7 -31
  134. package/frontendBuild/hmrWatcher.ts +14 -71
  135. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  136. package/frontendBuild/precompressArtifacts.ts +5 -18
  137. package/frontendBuild/routeClientBuilder.ts +7 -47
  138. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  139. package/frontendBuild/sourceMtimeIndex.test.ts +37 -94
  140. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  141. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  142. package/frontendBuild/styleContract.ts +1 -8
  143. package/frontendBuild/themeValidator.ts +4 -26
  144. package/frontendBuild/watchRootResolver.test.ts +23 -42
  145. package/frontendBuild/watchRootResolver.ts +5 -27
  146. package/getRelatedCnsts.ts +12 -92
  147. package/incrementalBuilder/buildBatch.proc.ts +22 -57
  148. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  149. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  150. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  151. package/incrementalBuilder/builderChannel.test.ts +24 -49
  152. package/incrementalBuilder/builderChannel.ts +6 -37
  153. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  154. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  155. package/incrementalBuilder/devWatchBatch.test.ts +14 -18
  156. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  157. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  158. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  159. package/incrementalBuilder/incrementalBuilder.proc.ts +48 -161
  160. package/index.ts +3 -15
  161. package/integration/devResourceProbe.ts +50 -56
  162. package/integration/devStability.integration.test.ts +47 -194
  163. package/integration/devStabilityHarness.test.ts +9 -29
  164. package/integration/devStabilityHarness.ts +65 -330
  165. package/integration/ssrMemoryProbe.ts +41 -167
  166. package/libSource.test.ts +4 -15
  167. package/libSource.ts +19 -31
  168. package/lint/gritRules.test.ts +6 -23
  169. package/linter.test.ts +1 -2
  170. package/linter.ts +22 -115
  171. package/mobile/mobileTarget.ts +14 -13
  172. package/package.json +2 -2
  173. package/packageExports.test.ts +3 -18
  174. package/packageExportsMap.ts +3 -19
  175. package/piDependencyPins.test.ts +3 -14
  176. package/platformTest/IdleWatchedProcess.test.ts +5 -13
  177. package/platformTest/IdleWatchedProcess.ts +1 -5
  178. package/platformTest/LinuxDockerTarget.ts +20 -35
  179. package/platformTest/PlatformTestRun.test.ts +5 -9
  180. package/platformTest/PlatformTestTarget.ts +24 -2
  181. package/platformTest/TestSnapshot.test.ts +6 -14
  182. package/platformTest/TestSnapshot.ts +1 -5
  183. package/platformTest/WindowsSshTarget.ts +6 -22
  184. package/prompter.ts +3 -6
  185. package/qualityScanner.test.ts +6 -24
  186. package/qualityScanner.ts +196 -284
  187. package/recipeScanner.test.ts +7 -12
  188. package/recipeScanner.ts +9 -38
  189. package/repoIdentity.ts +2 -9
  190. package/routeSourceValidator.ts +7 -30
  191. package/scanInfo.ts +82 -214
  192. package/selectModel.ts +1 -2
  193. package/semver.ts +3 -5
  194. package/slicePlanner.test.ts +6 -24
  195. package/slicePlanner.ts +11 -32
  196. package/spinner.test.ts +3 -8
  197. package/spinner.ts +4 -16
  198. package/sshTunnel.ts +1 -6
  199. package/ssrScanner.test.ts +1 -6
  200. package/ssrScanner.ts +103 -113
  201. package/subspace.test.ts +6 -25
  202. package/subspace.ts +31 -109
  203. package/subspaceConfig.ts +3 -11
  204. package/testHelpers.ts +49 -34
  205. package/transforms/asyncDefaultExportDetector.ts +3 -11
  206. package/transforms/barrelAnalyzer.ts +43 -135
  207. package/transforms/barrelImportsPlugin.ts +38 -175
  208. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  209. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  210. package/transforms/moduleSyntax.ts +32 -0
  211. package/transforms/rscUseClientTransform.ts +35 -20
  212. package/transforms/transforms.test.ts +153 -44
  213. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  214. package/transforms/useClientBundlePlugin.ts +2 -22
  215. package/typeChecker.ts +7 -63
  216. package/uploadRelease.ts +10 -25
  217. package/workflow/artifacts.ts +18 -0
  218. package/workflow/executor.test.ts +1 -12
  219. package/workflow/executor.ts +85 -146
  220. package/workflow/index.ts +0 -1
  221. package/workflow/moduleIndex.test.ts +4 -15
  222. package/workflow/moduleIndex.ts +9 -49
  223. package/workflow/plan.ts +18 -25
  224. package/workflow/render.ts +80 -96
  225. package/workflow/source.ts +33 -56
  226. package/workflow/sourceAst.ts +50 -0
  227. package/workspaceLayout.ts +4 -19
  228. package/commandDecorators/commandMeta.ts +0 -7
  229. package/commandDecorators/types.ts +0 -10
  230. package/getDirname.ts +0 -4
  231. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  232. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  233. package/workflow/primitive.ts +0 -59
@@ -1,12 +1,11 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { mkdtemp, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
2
+ import { writeFile } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import { collectRecipeSources, findInlineRecipeDuplicates, type RecipeInfo, scanRecipes } from "./recipeScanner";
5
+ import { tempDirs } from "./testHelpers";
6
6
 
7
7
  const byName = (recipes: RecipeInfo[], name: string) => recipes.find((recipe) => recipe.name === name);
8
8
 
9
- // Mirrors pkgs/akanjs/ui/recipe/ (framework) — two recipes in one source, variant + size surfaces.
10
9
  // The scanner is per-source, so a folder of one-recipe files and a legacy multi-recipe file both parse.
11
10
  const FRAMEWORK = `
12
11
  import { recipe, tv } from "./recipeFactory";
@@ -24,7 +23,6 @@ export type ButtonVariants = NonNullable<Parameters<typeof buttonRecipe>[0]>;
24
23
  export const badgeRecipe = recipe(tv({ base: "rounded-full", variants: { variant: { default: "bg-muted", info: "bg-info" } } }));
25
24
  `;
26
25
 
27
- // Mirrors apps/minimal/ui/Recipe/ shapes — base-only (no variants) + single-variant, with per-export JSDoc.
28
26
  const APP = `
29
27
  import { recipe, tv } from "akanjs/ui";
30
28
  /** 전체 화면 배경/전경. 페이지 루트 컨테이너. */
@@ -97,12 +95,7 @@ describe("scanRecipes", () => {
97
95
  });
98
96
  });
99
97
 
100
- // Recipes moved from a flat `ui/Recipe.ts` to a `ui/Recipe/` folder. Three consumers (the AGENTS.md recipe
101
- // index, the recipeGate lint, the MCP module context) go through collectRecipeSources, and every one of them
102
- // degrades silently — empty list, no error — if it stops finding sources. These tests are that alarm.
103
- // The advisory exists to catch a look being re-authored inline. Requiring every base token only matched a
104
- // verbatim copy of the whole base — the one shape that never occurs in practice — so it reported nothing on
105
- // the near-copies it was built for, and silently, being advisory. These tests pin the ratio behaviour.
98
+ // Requiring every base token matched only a verbatim copy, which never occurs; these tests pin the ratio instead.
106
99
  describe("findInlineRecipeDuplicates", () => {
107
100
  // 8 tokens → ceil(8 * 0.7) = 6 must be reproduced.
108
101
  const EIGHT = `export const cardRecipe = recipe(tv({ base: "flex rounded-box border border-border bg-card p-4 text-card-foreground shadow-sm" }));`;
@@ -135,9 +128,11 @@ describe("findInlineRecipeDuplicates", () => {
135
128
  });
136
129
  });
137
130
 
131
+ // The AGENTS.md index, the recipeGate lint and the MCP module context all degrade silently if this finds nothing.
138
132
  describe("collectRecipeSources", () => {
133
+ const makeTempRoot = tempDirs("akan-recipe-");
139
134
  const seed = async (files: Record<string, string>) => {
140
- const root = await mkdtemp(path.join(tmpdir(), "akan-recipe-"));
135
+ const root = await makeTempRoot();
141
136
  for (const [rel, content] of Object.entries(files)) {
142
137
  const abs = path.join(root, rel);
143
138
  await Bun.write(abs, content);
@@ -176,7 +171,7 @@ describe("collectRecipeSources", () => {
176
171
  });
177
172
 
178
173
  test("returns nothing when neither shape exists, without throwing", async () => {
179
- const root = await mkdtemp(path.join(tmpdir(), "akan-recipe-"));
174
+ const root = await makeTempRoot();
180
175
  await writeFile(path.join(root, "placeholder"), "");
181
176
  expect(await collectRecipeSources(path.join(root, "ui"), "@apps/x/ui")).toEqual([]);
182
177
  });
package/recipeScanner.ts CHANGED
@@ -1,11 +1,7 @@
1
1
  import { readdir } from "node:fs/promises";
2
2
  import ts from "typescript";
3
3
 
4
- /**
5
- * A single Akan UI recipe discovered by scanning source. `variants` maps each variant key to its allowed option
6
- * names (e.g. `{ variant: ["primary", "ghost"], size: ["sm", "md"] }`); a base-only recipe has `variants: {}`.
7
- * `importFrom` is the module a consumer imports the recipe from (e.g. `@apps/minimal/ui`).
8
- */
4
+ /** `variants` maps each key to its option names (`{}` for a base-only recipe); `importFrom` is the consumer's module. */
9
5
  export interface RecipeInfo {
10
6
  name: string;
11
7
  importFrom: string;
@@ -22,13 +18,8 @@ export interface RecipeSource {
22
18
  importFrom: string;
23
19
  }
24
20
 
25
- /**
26
- * Collects every recipe source under a `ui` folder. Recipes live one-per-file in a `Recipe/` folder
27
- * (`recipe/` for the framework), so this reads the whole folder; the flat `Recipe.ts` is still read for
28
- * apps that have not moved yet. Every consumer of `scanRecipes` must go through here — three call sites
29
- * (AGENTS.md recipe index, `recipeGate` lint, MCP module context) hardcoded the flat path before, and each
30
- * one fails silently (empty list, no error) when the file is absent.
31
- */
21
+ // One recipe per file under `Recipe/` (`recipe/` for the framework), plus the legacy flat `Recipe.ts`. Every
22
+ // `scanRecipes` consumer goes through here: a hardcoded path fails silently (an empty list) when the file is absent.
32
23
  export const collectRecipeSources = async (
33
24
  uiDirPath: string,
34
25
  importFrom: string,
@@ -51,12 +42,8 @@ export const collectRecipeSources = async (
51
42
  return [flat, ...fromDir].filter((source): source is RecipeSource => !!source);
52
43
  };
53
44
 
54
- /**
55
- * Statically finds every `export const <name> = recipe(tv({ ... }))` across the given sources and extracts its
56
- * variant surface + leading JSDoc one-liner. Detection is by the `recipe(tv(...))` call shape (not by name suffix,
57
- * since base-only recipes like `appScreen` omit the `Recipe` suffix). Being AST-based, it never matches recipe
58
- * definitions that appear only inside string/template literals (e.g. code examples in docs pages).
59
- */
45
+ // Detected by the `recipe(tv(...))` call shape, not the name suffix (base-only recipes like `appScreen` omit it);
46
+ // AST-based, so code examples inside string literals never match.
60
47
  export const scanRecipes = (sources: RecipeSource[]): RecipeInfo[] => {
61
48
  const recipes: RecipeInfo[] = [];
62
49
  for (const source of sources) {
@@ -73,12 +60,13 @@ export const scanRecipes = (sources: RecipeSource[]): RecipeInfo[] => {
73
60
  if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
74
61
  const parsed = parseRecipeCall(declaration.initializer);
75
62
  if (!parsed) continue;
63
+ const doc = getLeadingDoc(source.content, statement);
76
64
  recipes.push({
77
65
  name: declaration.name.text,
78
66
  importFrom: source.importFrom,
79
67
  variants: parsed.variants,
80
68
  ...(parsed.defaultVariants ? { defaultVariants: parsed.defaultVariants } : {}),
81
- ...(getLeadingDoc(source.content, statement) ? { doc: getLeadingDoc(source.content, statement) } : {}),
69
+ ...(doc ? { doc } : {}),
82
70
  ...(parsed.base ? { base: parsed.base } : {}),
83
71
  });
84
72
  }
@@ -90,7 +78,6 @@ export const scanRecipes = (sources: RecipeSource[]): RecipeInfo[] => {
90
78
  const isExported = (statement: ts.VariableStatement): boolean =>
91
79
  statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
92
80
 
93
- /** Matches `recipe( tv( <ObjectLiteral> ) )` and returns the variant surface, or null for anything else. */
94
81
  const parseRecipeCall = (
95
82
  initializer: ts.Expression,
96
83
  ): { variants: Record<string, string[]>; defaultVariants?: Record<string, string>; base?: string } | null => {
@@ -148,27 +135,12 @@ export interface RecipeDuplicate {
148
135
  className: string;
149
136
  }
150
137
 
151
- /**
152
- * Fraction of a recipe's base tokens an inline className must reproduce to count as a duplicate.
153
- *
154
- * Requiring *every* token (the original rule) only caught a verbatim copy of the whole base, which is the one
155
- * form of duplication that essentially never happens: someone re-authoring a look reproduces the gist, not all
156
- * eight tokens. So the check passed on exactly the near-duplicates it existed to find, and silently — it is an
157
- * advisory, so nothing went red. A ratio catches those; false positives are cheap here for the same reason.
158
- */
138
+ // A re-authored look reproduces the gist, not every token, so a ratio (not all tokens) catches near-duplicates.
159
139
  const DUPLICATE_TOKEN_RATIO = 0.7;
160
140
 
161
- /** Minimum base tokens for a recipe to be worth fingerprinting at all. */
162
141
  const MIN_FINGERPRINT_TOKENS = 3;
163
142
 
164
- /**
165
- * SSOT advisory: finds JSX `className` string values that hand-rewrite a recipe's base fingerprint instead of
166
- * consuming the recipe. Only recipes whose base has 3+ distinctive tokens are checked — shorter fingerprints
167
- * (`grid gap-3` …) are generic utilities and would flood the report with false positives. A className counts as
168
- * a duplicate once it reproduces {@link DUPLICATE_TOKEN_RATIO} of those tokens, so a near-copy that drops or
169
- * swaps one still reports. AST-scoped to real `className` attributes, so class strings inside doc-example
170
- * template literals never match.
171
- */
143
+ // Shorter fingerprints (`grid gap-3`) are generic utilities that would flood the report with false positives.
172
144
  export const findInlineRecipeDuplicates = (
173
145
  recipes: RecipeInfo[],
174
146
  files: { path: string; content: string }[],
@@ -219,7 +191,6 @@ const stringValuesIn = (node: ts.Node): string[] => {
219
191
  return values;
220
192
  };
221
193
 
222
- /** The first non-empty line of the JSDoc/line comment immediately preceding the statement, markers stripped. */
223
194
  const getLeadingDoc = (fullText: string, node: ts.Node): string | undefined => {
224
195
  const ranges = ts.getLeadingCommentRanges(fullText, node.getFullStart());
225
196
  if (!ranges?.length) return undefined;
package/repoIdentity.ts CHANGED
@@ -24,15 +24,8 @@ const readRemoteName = (workspaceRoot: string): string | null => {
24
24
  }
25
25
  };
26
26
 
27
- /**
28
- * The repository's own name.
29
- *
30
- * Deriving it from the working directory made every generated file that names the repo — the AGENTS.md title, its
31
- * `- Repo:` line — depend on what each person happened to call the folder they cloned into, so one commit rendered
32
- * a different guide per developer and the diff never settled. The origin remote is the one identity every clone
33
- * shares. `AKAN_PUBLIC_REPO_NAME` is deliberately not consulted: that is a deployment namespace (queue prefixes,
34
- * cache keys, secret paths) which a monorepo hosting several products legitimately points somewhere else.
35
- */
27
+ // The origin remote, not the clone's folder name, so generated files agree across clones. `AKAN_PUBLIC_REPO_NAME` is a
28
+ // deployment namespace (queue prefixes, cache keys) and deliberately not consulted.
36
29
  export const resolveRepoName = (workspaceRoot: string): string => {
37
30
  const cached = resolved.get(workspaceRoot);
38
31
  if (cached) return cached;
@@ -24,15 +24,8 @@ interface ChainStage {
24
24
 
25
25
  const chainRoots = new Set(["page", "layout", "rootLayout"]);
26
26
 
27
- /**
28
- * Static enforcement of the `page/` route conventions, split out of `executors.ts` so that importing
29
- * an executor does not pull `typescript` (+65MB resident) into the module graph. Both validators need
30
- * a real AST — modifier inspection, `export *` rejection, statement-kind walking and call-expression
31
- * identity — so `Bun.Transpiler.scan()`, which only reports export names, cannot replace them.
32
- *
33
- * Import it dynamically (`await import("./routeSourceValidator")`) so long-lived processes that never
34
- * validate a route source stay lean.
35
- */
27
+ // Import dynamically: it pulls in `typescript` (+65MB resident). `Bun.Transpiler.scan()` reports only export names,
28
+ // too little for these checks.
36
29
  export class RouteSourceValidator {
37
30
  static validateRouteSourceExports(
38
31
  source: string,
@@ -104,12 +97,7 @@ export class RouteSourceValidator {
104
97
  };
105
98
  }
106
99
 
107
- /**
108
- * The default export as a `page()` / `layout()` / `rootLayout()` chain, or null when the module is the legacy
109
- * shape. Read off the source for the same reason `devOnly` is: the build enumerates routes without importing
110
- * them, and `akan sync` should name a `[projectId]` folder whose page declares no `.param("projectId")` before
111
- * the first request does.
112
- */
100
+ // Null for the legacy shape. Read off the source because the build enumerates routes without importing them.
113
101
  static #readChain(sourceFile: ts.SourceFile, filePath: string) {
114
102
  for (const statement of sourceFile.statements) {
115
103
  if (!ts.isExportAssignment(statement) || statement.isExportEquals) continue;
@@ -173,12 +161,8 @@ export class RouteSourceValidator {
173
161
  );
174
162
  }
175
163
 
176
- /**
177
- * `devOnly` decides whether the route exists in the production build at all, so it is read from the
178
- * source rather than from an evaluated module — the build never imports route files to enumerate them.
179
- * That is why only a literal is accepted: anything the parser cannot settle would otherwise ship a
180
- * route the author believed was excluded.
181
- */
164
+ // Only a literal is accepted: the build reads it without importing the route, and a value the parser cannot settle
165
+ // would ship a route its author believed excluded.
182
166
  static #readDevOnly(sourceFile: ts.SourceFile, filePath: string): boolean {
183
167
  for (const statement of sourceFile.statements) {
184
168
  if (!ts.isVariableStatement(statement)) continue;
@@ -222,13 +206,7 @@ export class RouteSourceValidator {
222
206
  return current;
223
207
  }
224
208
 
225
- /**
226
- * Statically enforces that a `_overrides.tsx` route file is a logic-free activation manifest: a plain module
227
- * (no `"use client"` — the framework generates the client wrapper) that only imports components and binds them
228
- * to slots through a single `export default override({ Modal: BrandModal })`. It must not declare components
229
- * inline or run logic — that keeps the override contract a thin binding layer rather than a second place to
230
- * author UI. Slot names and value types are validated at compile time by `override`.
231
- */
209
+ // A logic-free manifest: imports plus one `export default override({ ... })`; `override` types the slots.
232
210
  static validateOverridesSourceExports(source: string, filePath: string) {
233
211
  const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
234
212
  const fail = (message: string): never => {
@@ -236,9 +214,8 @@ export class RouteSourceValidator {
236
214
  };
237
215
  let defaultOverride: ts.ExportAssignment | null = null;
238
216
  for (const statement of sourceFile.statements) {
239
- // A "use client" directive is unnecessary (the framework wraps the manifest) but harmless if present.
217
+ //? A "use client" directive is unnecessary (the framework wraps the manifest) but harmless.
240
218
  if (ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)) continue;
241
- // The manifest imports the app components it binds; imports and type-only decls carry no runtime logic.
242
219
  if (ts.isImportDeclaration(statement)) continue;
243
220
  if (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) continue;
244
221
  if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
package/scanInfo.ts CHANGED
@@ -27,20 +27,13 @@ const serviceFileTypes = [
27
27
  "zone",
28
28
  ] as const;
29
29
  type ServiceFileType = (typeof serviceFileTypes)[number];
30
- const databaseFileTypes = [
31
- "constant",
32
- "dictionary",
33
- "document",
34
- "service",
35
- "signal",
36
- "store",
37
- "template",
38
- "unit",
39
- "util",
40
- "view",
41
- "zone",
42
- ] as const;
30
+ const databaseFileTypes = ["constant", "dictionary", "document", ...serviceFileTypes.slice(1)] as const;
43
31
  type DatabaseFileType = (typeof databaseFileTypes)[number];
32
+ const uiFileTypes = new Set<DatabaseFileType>(["template", "unit", "util", "view", "zone"]);
33
+ const fileTypeOf = (filename: string) =>
34
+ databaseFileTypes.find((type) =>
35
+ filename.endsWith(uiFileTypes.has(type) ? `.${type[0].toUpperCase()}${type.slice(1)}.tsx` : `.${type}.ts`),
36
+ );
44
37
 
45
38
  type ModuleKind = "database" | "service" | "scalar";
46
39
 
@@ -56,14 +49,11 @@ const moduleUiFileTypes = {
56
49
  service: new Set(["Util", "Zone"]),
57
50
  scalar: new Set(["Template", "Unit"]),
58
51
  } satisfies Record<ModuleKind, Set<string>>;
59
- const testFilePattern = /\.(test|spec)\.(ts|tsx)$/;
60
52
 
61
- // The dependency scanner needs `typescript` (~65MB resident). Only the scan/sync commands run it, so
62
- // load it on demand rather than through the module graph of every process that imports scan info.
53
+ // Lazy: the dependency scanner pulls in `typescript` (~65MB resident), which only scan and sync need.
63
54
  const createDependencyScanner = async (exec: AppExecutor | LibExecutor | PkgExecutor) =>
64
55
  (await import("./dependencyScanner")).TypeScriptDependencyScanner.from(exec);
65
56
 
66
- const isAllowedTestFile = (filename: string) => testFilePattern.test(filename);
67
57
  const getScanPath = (exec: AppExecutor | LibExecutor, relativePath: string) =>
68
58
  path.posix.join(`${exec.type}s`, exec.name, relativePath.split(path.sep).join("/"));
69
59
  async function clearGeneratedRootCapacitorConfigs(exec: AppExecutor | LibExecutor) {
@@ -85,33 +75,21 @@ async function assertScanConvention(exec: AppExecutor | LibExecutor, libRoot: {
85
75
  const allowedRootFiles: ReadonlySet<string> = rootAllowedFiles[exec.type];
86
76
  const allowedRootDirs: ReadonlySet<string> = rootAllowedDirs[exec.type];
87
77
  const { files, dirs } = await exec.getFilesAndDirs(".");
88
- files
89
- .filter((filename) => !allowedRootFiles.has(filename))
90
- .forEach((filename) => {
91
- addViolation(filename, `unsupported ${exec.type} root file`);
92
- });
93
- dirs
94
- .filter((dirname) => !allowedRootDirs.has(dirname))
95
- .forEach((dirname) => {
96
- addViolation(dirname, `unsupported ${exec.type} root folder`);
97
- });
78
+ for (const filename of files)
79
+ if (!allowedRootFiles.has(filename)) addViolation(filename, `unsupported ${exec.type} root file`);
80
+ for (const dirname of dirs)
81
+ if (!allowedRootDirs.has(dirname)) addViolation(dirname, `unsupported ${exec.type} root folder`);
98
82
 
99
- //* An app validates its page tree in `getPageKeys`, which a lib has no equivalent of — so a lib's own
100
- //* routes went unchecked until whichever app opted into them with `syncPageLibs` blew up on the sync.
83
+ //* A lib has no `getPageKeys`, so its own route files are validated here.
101
84
  if (exec.type === "lib")
102
85
  for (const { relativePath, reason } of await exec.getPageConventionViolations()) addViolation(relativePath, reason);
103
86
 
104
- libRoot.files
105
- .filter((filename) => !isAllowedLibFacetRootFile(filename))
106
- .forEach((filename) => {
87
+ for (const filename of libRoot.files)
88
+ if (!isAllowedLibFacetRootFile(filename))
107
89
  addViolation(path.join("lib", filename), "unsupported lib facet root file");
108
- });
109
-
110
- libRoot.dirs
111
- .filter((dirname) => dirname.startsWith("__") && !internalLibDirs.has(dirname))
112
- .forEach((dirname) => {
90
+ for (const dirname of libRoot.dirs)
91
+ if (dirname.startsWith("__") && !internalLibDirs.has(dirname))
113
92
  addViolation(path.join("lib", dirname), "unsupported internal lib folder");
114
- });
115
93
 
116
94
  const databaseDirs = libRoot.dirs.filter((dirname) => !dirname.startsWith("_"));
117
95
  const serviceDirs = libRoot.dirs.filter((dirname) => dirname.startsWith("_") && !dirname.startsWith("__"));
@@ -146,43 +124,23 @@ async function validateModuleFiles(
146
124
 
147
125
  const uiModuleName = moduleName[0].toUpperCase() + moduleName.slice(1);
148
126
 
149
- files.forEach((filename) => {
150
- const filePath = path.join(modulePath, filename);
151
- if (filename === "index.ts" || filename === "index.tsx" || isAllowedTestFile(filename)) return;
152
- if (filename === `${moduleName}.abstract.md`) return;
153
-
127
+ for (const filename of files) {
128
+ const scanPath = getScanPath(exec, path.join(modulePath, filename));
129
+ if (filename === "index.ts" || filename === "index.tsx" || /\.(test|spec)\.(ts|tsx)$/.test(filename)) continue;
130
+ if (filename === `${moduleName}.abstract.md`) continue;
154
131
  const uiMatch = filename.match(/^([A-Z][A-Za-z0-9]+)\.([A-Z][A-Za-z0-9]*)\.tsx$/);
155
- if (uiMatch) {
156
- const fileModuleName = uiMatch[1];
157
- const fileType = uiMatch[2];
158
- if (fileModuleName !== uiModuleName) {
159
- violations.push(
160
- `${getScanPath(exec, filePath)}: module name mismatch: expected '${uiModuleName}', got '${fileModuleName}'`,
161
- );
162
- }
163
- if (!moduleUiFileTypes[kind].has(fileType)) {
164
- violations.push(`${getScanPath(exec, filePath)}: unsupported ${kind} UI file`);
165
- }
166
- return;
167
- }
168
-
169
- const nonUiMatch = filename.match(/^([a-z][a-zA-Z0-9]*)\.([a-z][a-z0-9]*)\.ts$/);
170
- if (nonUiMatch) {
171
- const fileModuleName = nonUiMatch[1];
172
- const fileType = nonUiMatch[2];
173
- if (fileModuleName !== moduleName) {
174
- violations.push(
175
- `${getScanPath(exec, filePath)}: module name mismatch: expected '${moduleName}', got '${fileModuleName}'`,
176
- );
177
- }
178
- if (!moduleNonUiFileTypes[kind].has(fileType)) {
179
- violations.push(`${getScanPath(exec, filePath)}: unsupported ${kind} file`);
180
- }
181
- return;
132
+ const match = uiMatch ?? filename.match(/^([a-z][a-zA-Z0-9]*)\.([a-z][a-z0-9]*)\.ts$/);
133
+ if (!match) {
134
+ violations.push(`${scanPath}: unsupported module file`);
135
+ continue;
182
136
  }
183
-
184
- violations.push(`${getScanPath(exec, filePath)}: unsupported module file`);
185
- });
137
+ const [, fileModuleName, fileType] = match;
138
+ const expectedName = uiMatch ? uiModuleName : moduleName;
139
+ if (fileModuleName !== expectedName)
140
+ violations.push(`${scanPath}: module name mismatch: expected '${expectedName}', got '${fileModuleName}'`);
141
+ if (!(uiMatch ? moduleUiFileTypes : moduleNonUiFileTypes)[kind].has(fileType))
142
+ violations.push(`${scanPath}: unsupported ${kind}${uiMatch ? " UI" : ""} file`);
143
+ }
186
144
  }
187
145
 
188
146
  class ScanInfo {
@@ -239,51 +197,16 @@ class ScanInfo {
239
197
  } else databaseDirs.push(name);
240
198
  });
241
199
 
200
+ const collect = async (dir: string, name: string, kind: "databases" | "services" | "scalars") => {
201
+ for (const filename of await exec.readdir(dir)) {
202
+ const type = fileTypeOf(filename);
203
+ if (type) (files[type] as { [key in typeof kind]?: string[] })[kind]?.push(name);
204
+ }
205
+ };
242
206
  await Promise.all([
243
- ...databaseDirs.map(async (name) => {
244
- const filenames = await exec.readdir(path.join("lib", name));
245
- filenames.forEach((filename) => {
246
- if (filename.endsWith(".constant.ts")) files.constant.databases.push(name);
247
- else if (filename.endsWith(".dictionary.ts")) files.dictionary.databases.push(name);
248
- else if (filename.endsWith(".document.ts")) files.document.databases.push(name);
249
- else if (filename.endsWith(".service.ts")) files.service.databases.push(name);
250
- else if (filename.endsWith(".signal.ts")) files.signal.databases.push(name);
251
- else if (filename.endsWith(".store.ts")) files.store.databases.push(name);
252
- else if (filename.endsWith(".Template.tsx")) files.template.databases.push(name);
253
- else if (filename.endsWith(".Unit.tsx")) files.unit.databases.push(name);
254
- else if (filename.endsWith(".Util.tsx")) files.util.databases.push(name);
255
- else if (filename.endsWith(".View.tsx")) files.view.databases.push(name);
256
- else if (filename.endsWith(".Zone.tsx")) files.zone.databases.push(name);
257
- });
258
- }),
259
- ...serviceDirs.map(async (dirname) => {
260
- const name = dirname.slice(1);
261
- const filenames = await exec.readdir(path.join("lib", dirname));
262
- filenames.forEach((filename) => {
263
- if (filename.endsWith(".dictionary.ts")) files.dictionary.services.push(name);
264
- else if (filename.endsWith(".service.ts")) files.service.services.push(name);
265
- else if (filename.endsWith(".signal.ts")) files.signal.services.push(name);
266
- else if (filename.endsWith(".store.ts")) files.store.services.push(name);
267
- else if (filename.endsWith(".Template.tsx")) files.template.services.push(name);
268
- else if (filename.endsWith(".Unit.tsx")) files.unit.services.push(name);
269
- else if (filename.endsWith(".Util.tsx")) files.util.services.push(name);
270
- else if (filename.endsWith(".View.tsx")) files.view.services.push(name);
271
- else if (filename.endsWith(".Zone.tsx")) files.zone.services.push(name);
272
- });
273
- }),
274
- ...scalarDirs.map(async (name) => {
275
- const filenames = await exec.readdir(path.join("lib/__scalar", name));
276
- filenames.forEach((filename) => {
277
- if (filename.endsWith(".constant.ts")) files.constant.scalars.push(name);
278
- else if (filename.endsWith(".dictionary.ts")) files.dictionary.scalars.push(name);
279
- else if (filename.endsWith(".document.ts")) files.document.scalars.push(name);
280
- else if (filename.endsWith(".Template.tsx")) files.template.scalars.push(name);
281
- else if (filename.endsWith(".Unit.tsx")) files.unit.scalars.push(name);
282
- else if (filename.endsWith(".Util.tsx")) files.util.scalars.push(name);
283
- else if (filename.endsWith(".View.tsx")) files.view.scalars.push(name);
284
- else if (filename.endsWith(".Zone.tsx")) files.zone.scalars.push(name);
285
- });
286
- }),
207
+ ...databaseDirs.map((name) => collect(path.join("lib", name), name, "databases")),
208
+ ...serviceDirs.map((dirname) => collect(path.join("lib", dirname), dirname.slice(1), "services")),
209
+ ...scalarDirs.map((name) => collect(path.join("lib/__scalar", name), name, "scalars")),
287
210
  ]);
288
211
  const routes = exec.type === "lib" ? [] : await (exec as AppExecutor).getPageKeys();
289
212
  const common = {
@@ -309,35 +232,16 @@ class ScanInfo {
309
232
  constructor(scanResult: ScanResult) {
310
233
  this.name = scanResult.name;
311
234
  this.scanResult = scanResult;
312
- Object.entries(scanResult.files).forEach(([_key, value]) => {
313
- const key = _key as DatabaseFileType;
314
- const { databases, services, scalars } = value as {
315
- databases: string[];
316
- services?: string[];
317
- scalars?: string[];
318
- };
319
- databases.forEach((modelName) => {
320
- const model = this.database.get(modelName) ?? new Set<DatabaseFileType>();
321
- model.add(key);
322
- this.database.set(modelName, model);
323
- this.file[key].all.add(modelName);
324
- this.file[key].databases.add(modelName);
325
- });
326
- services?.forEach((serviceName) => {
327
- const service = this.service.get(serviceName) ?? new Set<ServiceFileType>();
328
- service.add(key as ServiceFileType);
329
- this.service.set(serviceName, service);
330
- this.file[key].all.add(serviceName);
331
- this.file[key].services.add(serviceName);
332
- });
333
- scalars?.forEach((scalarName) => {
334
- const scalar = this.scalar.get(scalarName) ?? new Set<ScalarFileType>();
335
- scalar.add(key as ScalarFileType);
336
- this.scalar.set(scalarName, scalar);
337
- this.file[key].all.add(scalarName);
338
- this.file[key].scalars.add(scalarName);
339
- });
340
- });
235
+ const modulesOf = { databases: this.database, services: this.service, scalars: this.scalar } as {
236
+ [key in "databases" | "services" | "scalars"]: Map<string, Set<DatabaseFileType>>;
237
+ };
238
+ for (const [key, groups] of Object.entries(scanResult.files) as [DatabaseFileType, { [kind: string]: string[] }][])
239
+ for (const kind of ["databases", "services", "scalars"] as const)
240
+ for (const name of groups[kind] ?? []) {
241
+ modulesOf[kind].set(name, (modulesOf[kind].get(name) ?? new Set()).add(key));
242
+ this.file[key].all.add(name);
243
+ this.file[key][kind].add(name);
244
+ }
341
245
  }
342
246
  getScanResult() {
343
247
  return this.scanResult;
@@ -351,6 +255,29 @@ class ScanInfo {
351
255
  getScalarModules() {
352
256
  return [...this.scalar.keys()];
353
257
  }
258
+
259
+ #sortedLibs: string[] | null = null;
260
+ protected sortedLibs(libDeps: string[]) {
261
+ if (this.#sortedLibs) return this.#sortedLibs;
262
+ const libIndices = LibInfo.getSortedLibIndices();
263
+ this.#sortedLibs = libDeps.sort((libNameA, libNameB) => {
264
+ const indexA = libIndices.get(libNameA);
265
+ const indexB = libIndices.get(libNameB);
266
+ if (indexA === undefined || indexB === undefined)
267
+ throw new Error(`LibInfo not found: ${libNameA} or ${libNameB}`);
268
+ return indexA - indexB;
269
+ });
270
+ return this.#sortedLibs;
271
+ }
272
+ protected sortedLibInfos(libDeps: string[]) {
273
+ return new Map(
274
+ this.sortedLibs(libDeps).map((libName) => {
275
+ const libInfo = LibInfo.libInfos.get(libName);
276
+ if (!libInfo) throw new Error(`LibInfo not found: ${libName}`);
277
+ return [libName, libInfo];
278
+ }),
279
+ );
280
+ }
354
281
  }
355
282
 
356
283
  const isAkanFrameworkDependency = (dep: string) => dep === "akanjs" || dep.startsWith("akanjs/");
@@ -362,7 +289,6 @@ export class AppInfo extends ScanInfo {
362
289
 
363
290
  static appInfos = new Map<string, AppInfo>();
364
291
  static async fromExecutor(exec: AppExecutor, options: { refresh?: boolean } = {}) {
365
- // cache check
366
292
  const existingAppInfo = AppInfo.appInfos.get(exec.name);
367
293
  if (existingAppInfo && !options.refresh) return existingAppInfo;
368
294
  const scanResult = await ScanInfo.getScanResult(exec);
@@ -408,30 +334,11 @@ export class AppInfo extends ScanInfo {
408
334
  return [...libSet];
409
335
  }
410
336
 
411
- #sortedLibs: string[] | null = null;
412
- #getSortedLibs() {
413
- if (this.#sortedLibs) return this.#sortedLibs;
414
- const libIndices = LibInfo.getSortedLibIndices();
415
- this.#sortedLibs = this.libDeps.sort((libNameA, libNameB) => {
416
- const indexA = libIndices.get(libNameA);
417
- const indexB = libIndices.get(libNameB);
418
- if (indexA === undefined || indexB === undefined)
419
- throw new Error(`LibInfo not found: ${libNameA} or ${libNameB}`);
420
- return indexA - indexB;
421
- });
422
- return this.#sortedLibs;
423
- }
424
337
  getLibs() {
425
- return this.#getSortedLibs();
338
+ return this.sortedLibs(this.libDeps);
426
339
  }
427
340
  getLibInfos() {
428
- return new Map(
429
- this.#getSortedLibs().map((libName) => {
430
- const libInfo = LibInfo.libInfos.get(libName);
431
- if (!libInfo) throw new Error(`LibInfo not found: ${libName}`);
432
- return [libName, libInfo];
433
- }),
434
- );
341
+ return this.sortedLibInfos(this.libDeps);
435
342
  }
436
343
  }
437
344
  export class LibInfo extends ScanInfo {
@@ -482,36 +389,16 @@ export class LibInfo extends ScanInfo {
482
389
  return this.scanResult as LibScanResult;
483
390
  }
484
391
 
485
- #sortedLibs: string[] | null = null;
486
- #getSortedLibs() {
487
- if (this.#sortedLibs) return this.#sortedLibs;
488
- const libs = LibInfo.getSortedLibIndices();
489
- this.#sortedLibs = this.scanResult.libDeps.sort((libNameA, libNameB) => {
490
- const indexA = libs.get(libNameA);
491
- const indexB = libs.get(libNameB);
492
- if (indexA === undefined || indexB === undefined)
493
- throw new Error(`LibInfo not found: ${libNameA} or ${libNameB}`);
494
- return indexA - indexB;
495
- });
496
- return this.#sortedLibs;
497
- }
498
392
  getLibs() {
499
- return this.#getSortedLibs();
393
+ return this.sortedLibs(this.scanResult.libDeps);
500
394
  }
501
395
  getLibInfo(libName: string) {
502
396
  if (!this.getScanResult().libDeps.includes(libName)) return undefined;
503
- const libSet = new Set(this.#getSortedLibs());
504
- if (!libSet.has(libName)) throw new Error(`LibInfo is invalid: ${libName}`);
397
+ if (!this.getLibs().includes(libName)) throw new Error(`LibInfo is invalid: ${libName}`);
505
398
  return LibInfo.libInfos.get(libName);
506
399
  }
507
400
  getLibInfos() {
508
- return new Map(
509
- this.#getSortedLibs().map((libName) => {
510
- const libInfo = LibInfo.libInfos.get(libName);
511
- if (!libInfo) throw new Error(`LibInfo not found: ${libName}`);
512
- return [libName, libInfo];
513
- }),
514
- );
401
+ return this.sortedLibInfos(this.scanResult.libDeps);
515
402
  }
516
403
  }
517
404
 
@@ -542,12 +429,7 @@ export class PkgInfo {
542
429
  .join("/");
543
430
  })
544
431
  .filter((pkg) => pkg !== exec.name);
545
- const pkgScanResult = {
546
- name: exec.name,
547
- pkgDeps,
548
- dependencies: [...npmDepSet],
549
- };
550
- return pkgScanResult;
432
+ return { name: exec.name, pkgDeps, dependencies: [...npmDepSet] };
551
433
  }
552
434
 
553
435
  static #pkgInfos = new Map<string, PkgInfo>();
@@ -585,23 +467,9 @@ export class WorkspaceInfo {
585
467
  const [appNames, libNames, pkgNames] = await Promise.all([exec.getApps(), exec.getLibs(), exec.getPkgs()]);
586
468
  // TODO: prevent duplicate scan by resolving the dependency graph
587
469
  const [appInfos, libInfos, pkgInfos] = await Promise.all([
588
- Promise.all(
589
- appNames.map(async (appName) => {
590
- const app = AppExecutor.from(exec, appName);
591
- return await app.scan();
592
- }),
593
- ),
594
- Promise.all(
595
- libNames.map(async (libName) => {
596
- const lib = LibExecutor.from(exec, libName);
597
- return await lib.scan();
598
- }),
599
- ),
600
- Promise.all(
601
- pkgNames.map(async (pkgName) => {
602
- return await PkgExecutor.from(exec, pkgName).scan();
603
- }),
604
- ),
470
+ Promise.all(appNames.map(async (appName) => await AppExecutor.from(exec, appName).scan())),
471
+ Promise.all(libNames.map(async (libName) => await LibExecutor.from(exec, libName).scan())),
472
+ Promise.all(pkgNames.map(async (pkgName) => await PkgExecutor.from(exec, pkgName).scan())),
605
473
  ]);
606
474
  const workspaceInfo = new WorkspaceInfo(
607
475
  new Map(appInfos.map((app) => [app.exec.name, app as AppInfo])),