@akanjs/devkit 3.0.0-alpha.8 → 3.0.0-alpha.81

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 (172) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/agentsIndex.test.ts +10 -0
  5. package/agentsIndex.ts +47 -1
  6. package/aiEditor.ts +1 -1
  7. package/akanApp/BackendImportGraph.test.ts +120 -0
  8. package/akanApp/BackendImportGraph.ts +167 -0
  9. package/akanApp/akanApp.host.test.ts +16 -678
  10. package/akanApp/akanApp.host.ts +65 -562
  11. package/akanApp/devHostPolicy.test.ts +542 -0
  12. package/akanApp/devHostPolicy.ts +412 -0
  13. package/akanApp/index.ts +2 -0
  14. package/akanConfig/akanConfig.test.ts +182 -14
  15. package/akanConfig/akanConfig.ts +132 -47
  16. package/akanConfig/types.ts +8 -0
  17. package/akanContext.ts +70 -14
  18. package/akanMcpContract.ts +25 -4
  19. package/applicationBuildRunner.test.ts +1 -1
  20. package/applicationBuildRunner.ts +45 -21
  21. package/artifact/implicitRootLayout.test.ts +67 -0
  22. package/artifact/implicitRootLayout.ts +27 -7
  23. package/artifact/routeSeedIndex.test.ts +1 -0
  24. package/biome.base.json +340 -0
  25. package/biomeBase.ts +9 -0
  26. package/capacitorApp.ts +10 -0
  27. package/cloud/globalConfig.ts +9 -2
  28. package/commandDecorators/command.ts +30 -6
  29. package/commandDecorators/commandBuilder.ts +20 -5
  30. package/commandDecorators/commandDecorators.test.ts +115 -3
  31. package/dependencyScanner.test.ts +99 -0
  32. package/dependencyScanner.ts +27 -24
  33. package/devkitUtils.test.ts +0 -104
  34. package/executors.test.ts +132 -5
  35. package/executors.ts +89 -14
  36. package/fileEditor.ts +19 -19
  37. package/formSetterScanner.test.ts +80 -0
  38. package/formSetterScanner.ts +92 -0
  39. package/frontendBuild/autoImportSync.test.ts +58 -0
  40. package/frontendBuild/autoImportSync.ts +4 -0
  41. package/frontendBuild/buildRouteClient.test.ts +47 -24
  42. package/frontendBuild/clientBuildTypes.ts +4 -0
  43. package/frontendBuild/clientEntriesBundler.ts +4 -1
  44. package/frontendBuild/clientEntryDiscovery.ts +2 -2
  45. package/frontendBuild/csrArtifactBuilder.ts +116 -84
  46. package/frontendBuild/cssCompiler.ts +123 -12
  47. package/frontendBuild/cssImportResolver.ts +8 -7
  48. package/frontendBuild/fontOptimizer.ts +37 -18
  49. package/frontendBuild/fontPruner.test.ts +220 -0
  50. package/frontendBuild/fontPruner.ts +206 -0
  51. package/frontendBuild/frontendBuild.test.ts +178 -10
  52. package/frontendBuild/hmrWatcher.ts +1 -1
  53. package/frontendBuild/index.ts +1 -1
  54. package/frontendBuild/pagesBundleBuilder.ts +3 -3
  55. package/frontendBuild/pagesEntrySourceGenerator.ts +11 -88
  56. package/frontendBuild/routeClientBuilder.ts +12 -5
  57. package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
  58. package/frontendBuild/styleContract.ts +14 -20
  59. package/frontendBuild/themeValidator.ts +22 -17
  60. package/frontendBuild/vendorSpecifiers.ts +1 -0
  61. package/incrementalBuilder/devWatchBatch.test.ts +18 -20
  62. package/incrementalBuilder/devWatchBatch.ts +6 -2
  63. package/incrementalBuilder/incrementalBuilder.host.ts +1 -1
  64. package/incrementalBuilder/incrementalBuilder.proc.ts +8 -4
  65. package/index.ts +0 -5
  66. package/integration/devStabilityHarness.ts +2 -10
  67. package/libSource.test.ts +109 -0
  68. package/libSource.ts +126 -0
  69. package/lint/__fixtures__/README.md +40 -0
  70. package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
  71. package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
  72. package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
  73. package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
  74. package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
  75. package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
  76. package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
  77. package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
  78. package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
  79. package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
  80. package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
  81. package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
  82. package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
  83. package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
  84. package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
  85. package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
  86. package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
  87. package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
  88. package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
  89. package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
  90. package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
  91. package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
  92. package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
  93. package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
  94. package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
  95. package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
  96. package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
  97. package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
  98. package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
  99. package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
  100. package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
  101. package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
  102. package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
  103. package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
  104. package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
  105. package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
  106. package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
  107. package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
  108. package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
  109. package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
  110. package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
  111. package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
  112. package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
  113. package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
  114. package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
  115. package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
  116. package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
  117. package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
  118. package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
  119. package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
  120. package/lint/gritRules.test.ts +178 -0
  121. package/lint/no-arbitrary-color.grit +1 -1
  122. package/lint/no-async-component-in-ui.grit +35 -0
  123. package/lint/no-bang-comment-in-client.grit +23 -10
  124. package/lint/no-daisyui-legacy-class.grit +26 -9
  125. package/lint/no-deprecated-log-level.grit +17 -0
  126. package/lint/no-import-client-in-server.grit +48 -0
  127. package/lint/no-import-server-in-client.grit +45 -0
  128. package/lint/no-init-fetch-in-client.grit +47 -0
  129. package/lint/no-inline-color.grit +10 -8
  130. package/lint/no-interpolated-arbitrary-class.grit +3 -3
  131. package/lint/no-model-type-in-util-zone.grit +58 -0
  132. package/lint/no-raw-palette-class.grit +3 -3
  133. package/lint/no-unpublished-form-setter.grit +41 -0
  134. package/lint/non-scalar-props-restricted.grit +16 -7
  135. package/linter.test.ts +80 -0
  136. package/linter.ts +99 -25
  137. package/package.json +6 -6
  138. package/prompter.ts +9 -4
  139. package/qualityScanner.test.ts +116 -0
  140. package/qualityScanner.ts +89 -21
  141. package/recipeScanner.ts +4 -1
  142. package/repoIdentity.ts +42 -0
  143. package/scanInfo.ts +32 -26
  144. package/semver.test.ts +26 -0
  145. package/semver.ts +31 -0
  146. package/slicePlanner.test.ts +182 -0
  147. package/slicePlanner.ts +235 -0
  148. package/ssrScanner.test.ts +301 -0
  149. package/subspace.test.ts +418 -0
  150. package/subspace.ts +723 -0
  151. package/subspaceConfig.ts +76 -0
  152. package/transforms/asyncDefaultExportDetector.ts +103 -0
  153. package/transforms/barrelImportsPlugin.ts +1 -1
  154. package/transforms/externalizeFrameworkPlugin.ts +0 -1
  155. package/transforms/transforms.test.ts +5 -5
  156. package/transforms/tsconfigPackageResolver.test.ts +230 -0
  157. package/tsconfig.json +6 -2
  158. package/typeChecker.ts +1 -1
  159. package/types.ts +1 -0
  160. package/ui/ScrollList.tsx +6 -8
  161. package/uploadRelease.ts +2 -2
  162. package/workflow/executor.test.ts +146 -0
  163. package/workspaceLayout.test.ts +56 -4
  164. package/workspaceLayout.ts +49 -4
  165. package/builder.ts +0 -164
  166. package/extractDeps.ts +0 -86
  167. package/frontendBuild/styleGuard.test.ts +0 -165
  168. package/frontendBuild/styleGuard.ts +0 -322
  169. package/getCredentials.ts +0 -19
  170. package/getModelFileData.ts +0 -62
  171. package/src/capacitorApp.ts +0 -282
  172. package/streamAi.ts +0 -45
@@ -0,0 +1,146 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { WorkflowExecutor, workflowStepKey } from "./executor";
3
+ import type {
4
+ PrimitiveChangedFile,
5
+ WorkflowDiagnostic,
6
+ WorkflowPlan,
7
+ WorkflowStep,
8
+ WorkflowStepRegistry,
9
+ WorkflowStepResult,
10
+ } from "./types";
11
+
12
+ /**
13
+ * `WorkflowExecutor` takes its registry by constructor injection and its plan as data, so `apply` runs with
14
+ * no filesystem and no workspace — the `workspace` argument is what gates every post-apply check. These
15
+ * cover the ordering rules that `cli/workflow/workflow.test.ts` can only reach through a real scaffold.
16
+ */
17
+ const step = (id: string, tool = `tool:${id}`): WorkflowStep => ({
18
+ id,
19
+ tool,
20
+ title: id,
21
+ description: id,
22
+ });
23
+
24
+ // Every field spelled out rather than cast through `unknown`: `apply` reads `validation` before the first
25
+ // step runs, so a partial literal fails at runtime instead of at the type level.
26
+ const planOf = (steps: WorkflowStep[], diagnostics: WorkflowDiagnostic[] = []): WorkflowPlan => ({
27
+ schemaVersion: 1,
28
+ workflow: "add-field",
29
+ mode: "plan",
30
+ inputs: {},
31
+ optionalSurfaces: {},
32
+ steps,
33
+ predictedChanges: [],
34
+ validation: [],
35
+ diagnostics,
36
+ recommendations: [],
37
+ requiresApproval: true,
38
+ });
39
+
40
+ /**
41
+ * A registry that records **which key** each call resolved through, not just that a step ran — the
42
+ * resolution order is the thing under test, and every candidate key would otherwise record the same step id.
43
+ */
44
+ const changed = (path: string): PrimitiveChangedFile => ({ path, action: "modify", reason: "test" });
45
+
46
+ const recordingRegistry = (results: Record<string, WorkflowStepResult | undefined> = {}) => {
47
+ const calls: string[] = [];
48
+ const registry: WorkflowStepRegistry = {};
49
+ const on = (key: string) => {
50
+ registry[key] = async () => {
51
+ calls.push(key);
52
+ return results[key];
53
+ };
54
+ };
55
+ return { calls, registry, on };
56
+ };
57
+
58
+ describe("WorkflowExecutor.apply", () => {
59
+ test("runs no step when the plan already carries an error diagnostic", async () => {
60
+ const { calls, registry, on } = recordingRegistry();
61
+ on(workflowStepKey("add-field", "one"));
62
+ const plan = planOf([step("one")], [{ severity: "error", code: "plan-invalid", message: "bad inputs" }]);
63
+
64
+ const report = await new WorkflowExecutor(registry).apply(plan);
65
+
66
+ expect(calls).toEqual([]);
67
+ expect(report.diagnostics.map((diagnostic) => diagnostic.code)).toContain("plan-invalid");
68
+ });
69
+
70
+ test("a warning in the plan does not stop the steps", async () => {
71
+ const { calls, registry, on } = recordingRegistry();
72
+ on(workflowStepKey("add-field", "one"));
73
+ const plan = planOf([step("one")], [{ severity: "warning", code: "plan-note", message: "heads up" }]);
74
+
75
+ await new WorkflowExecutor(registry).apply(plan);
76
+
77
+ expect(calls).toEqual([workflowStepKey("add-field", "one")]);
78
+ });
79
+
80
+ test("resolves a runner by workflow-scoped key first, then tool, then bare step id", async () => {
81
+ const { calls, registry, on } = recordingRegistry();
82
+ // All three keys are registered for the same step, so only the winner records a call.
83
+ on(workflowStepKey("add-field", "scoped"));
84
+ on("tool:scoped");
85
+ on("scoped");
86
+ await new WorkflowExecutor(registry).apply(planOf([step("scoped")]));
87
+ expect(calls).toEqual([workflowStepKey("add-field", "scoped")]);
88
+
89
+ const byTool = recordingRegistry();
90
+ byTool.on("tool:viaTool");
91
+ byTool.on("viaTool");
92
+ await new WorkflowExecutor(byTool.registry).apply(planOf([step("viaTool")]));
93
+ expect(byTool.calls).toEqual(["tool:viaTool"]);
94
+
95
+ const byId = recordingRegistry();
96
+ byId.on("viaId");
97
+ await new WorkflowExecutor(byId.registry).apply(planOf([step("viaId")]));
98
+ expect(byId.calls).toEqual(["viaId"]);
99
+ });
100
+
101
+ test("an unsupported step is reported and abandons the rest of the plan", async () => {
102
+ const { calls, registry, on } = recordingRegistry();
103
+ on(workflowStepKey("add-field", "first"));
104
+ on(workflowStepKey("add-field", "third"));
105
+
106
+ const report = await new WorkflowExecutor(registry).apply(planOf([step("first"), step("second"), step("third")]));
107
+
108
+ expect(calls).toEqual([workflowStepKey("add-field", "first")]);
109
+ expect(report.diagnostics.map((diagnostic) => diagnostic.code)).toContain("workflow-step-unsupported");
110
+ expect(report.nextActions.map((action) => action.command)).toContain("akan workflow explain add-field");
111
+ });
112
+
113
+ test("accumulates changed files and diagnostics across steps, and stops at the first error", async () => {
114
+ const key = (id: string) => workflowStepKey("add-field", id);
115
+ const { calls, registry, on } = recordingRegistry({
116
+ [key("a")]: { changedFiles: [changed("apps/demo/lib/task/task.constant.ts")] },
117
+ [key("b")]: {
118
+ changedFiles: [changed("apps/demo/lib/task/task.dictionary.ts")],
119
+ diagnostics: [{ severity: "error", code: "step-failed", message: "no such module" }],
120
+ },
121
+ [key("c")]: { changedFiles: [changed("never-reached.ts")] },
122
+ });
123
+ on(key("a"));
124
+ on(key("b"));
125
+ on(key("c"));
126
+
127
+ const report = await new WorkflowExecutor(registry).apply(planOf([step("a"), step("b"), step("c")]));
128
+
129
+ expect(calls).toEqual([key("a"), key("b")]);
130
+ expect(report.changedFiles.map((file) => file.path)).toEqual([
131
+ "apps/demo/lib/task/task.constant.ts",
132
+ "apps/demo/lib/task/task.dictionary.ts",
133
+ ]);
134
+ expect(report.diagnostics.map((diagnostic) => diagnostic.code)).toContain("step-failed");
135
+ });
136
+
137
+ test("a runner that answers nothing is not an error", async () => {
138
+ const { calls, registry, on } = recordingRegistry();
139
+ on(workflowStepKey("add-field", "silent"));
140
+ const report = await new WorkflowExecutor(registry).apply(planOf([step("silent")]));
141
+
142
+ expect(calls).toEqual([workflowStepKey("add-field", "silent")]);
143
+ expect(report.diagnostics.filter((diagnostic) => diagnostic.severity === "error")).toEqual([]);
144
+ expect(report.changedFiles).toEqual([]);
145
+ });
146
+ });
@@ -1,5 +1,12 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { appRootAllowedDirs, appRootAllowedFiles, isScannedAppRootEntry } from "./workspaceLayout";
2
+ import {
3
+ appRootAllowedDirs,
4
+ appRootAllowedFiles,
5
+ isAllowedLibFacetRootFile,
6
+ isScannedRootEntry,
7
+ libRootAllowedDirs,
8
+ libRootAllowedFiles,
9
+ } from "./workspaceLayout";
3
10
 
4
11
  describe("app root layout allowlist", () => {
5
12
  test("admits the scoped agent guides sync writes into every app", () => {
@@ -19,8 +26,53 @@ describe("app root layout allowlist", () => {
19
26
  });
20
27
 
21
28
  test("skips dotfile artifacts the sync glob never sees, but keeps .akan", () => {
22
- expect(isScannedAppRootEntry(".DS_Store")).toBe(false);
23
- expect(isScannedAppRootEntry(".akan")).toBe(true);
24
- expect(isScannedAppRootEntry("lib")).toBe(true);
29
+ expect(isScannedRootEntry("app", ".DS_Store")).toBe(false);
30
+ expect(isScannedRootEntry("app", ".akan")).toBe(true);
31
+ expect(isScannedRootEntry("app", "lib")).toBe(true);
32
+ });
33
+ });
34
+
35
+ describe("lib root layout allowlist", () => {
36
+ test("admits what the libRoot template and scan write", () => {
37
+ for (const filename of ["AGENTS.md", "akan.config.ts", "akan.lib.json", "client.ts", "index.ts", "server.ts"]) {
38
+ expect(libRootAllowedFiles.has(filename)).toBe(true);
39
+ }
40
+ for (const dirname of ["common", "env", "lib", "page", "plugin", "private", "public", "srvkit", "ui", "webkit"]) {
41
+ expect(libRootAllowedDirs.has(dirname)).toBe(true);
42
+ }
43
+ });
44
+
45
+ test("rejects a lib root entry no facet owns", () => {
46
+ expect(libRootAllowedFiles.has("helper.ts")).toBe(false);
47
+ expect(libRootAllowedDirs.has("base")).toBe(false);
48
+ });
49
+
50
+ test("rejects the app-only run and mobile entries", () => {
51
+ for (const filename of ["main.ts", "capacitor.config.ts", "akan.app.json"]) {
52
+ expect(libRootAllowedFiles.has(filename)).toBe(false);
53
+ }
54
+ for (const dirname of [".akan", "android", "ios", "mobile", "script", "secrets"]) {
55
+ expect(libRootAllowedDirs.has(dirname)).toBe(false);
56
+ }
57
+ });
58
+
59
+ test("skips dotfile artifacts in a lib root too", () => {
60
+ expect(isScannedRootEntry("lib", ".gitignore")).toBe(false);
61
+ expect(isScannedRootEntry("lib", ".akan")).toBe(false);
62
+ expect(isScannedRootEntry("lib", "ui")).toBe(true);
63
+ });
64
+ });
65
+
66
+ describe("lib facet root allowlist", () => {
67
+ test("admits the generated support facets and a root signal test", () => {
68
+ expect(isAllowedLibFacetRootFile("cnst.ts")).toBe(true);
69
+ expect(isAllowedLibFacetRootFile("option.ts")).toBe(true);
70
+ expect(isAllowedLibFacetRootFile("user.signal.test.ts")).toBe(true);
71
+ expect(isAllowedLibFacetRootFile("user.signal.spec.ts")).toBe(true);
72
+ });
73
+
74
+ test("rejects a hand-written file beside the barrels", () => {
75
+ expect(isAllowedLibFacetRootFile("helper.ts")).toBe(false);
76
+ expect(isAllowedLibFacetRootFile("user.test.ts")).toBe(false);
25
77
  });
26
78
  });
@@ -4,10 +4,11 @@
4
4
  * 같은 규칙을 scanInfo(`akan sync`, hard error) · akanContext(`akan doctor`, diagnostic) ·
5
5
  * qualityScanner(`akan quality scan`, warning) 세 곳이 각자 복사해 두면서 실제로 어긋났다
6
6
  * (스코프 AGENTS.md/CLAUDE.md 는 sync 만 허용, `plugin` 은 문서에만, `secrets` 는 doctor 만 거부).
7
- * 규칙을 추가할 때는 이 파일만 고치고, 루트 AGENTS.md `.cursor/rules/akan-scan-conventions.mdc`
8
- * 의 목록도 같이 갱신한다.
7
+ * 규칙을 추가할 때는 이 파일만 고치고, 루트 AGENTS.md 목록도 같이 갱신한다.
9
8
  */
10
9
 
10
+ export type SysType = "app" | "lib";
11
+
11
12
  export const appRootAllowedFiles = new Set([
12
13
  // 스코프 에이전트 가이드 — scan(write) 이 유지하는 색인 + 마커 밖 hand-written 내용 (agentsIndex.ts)
13
14
  "AGENTS.md",
@@ -42,6 +43,43 @@ export const appRootAllowedDirs = new Set([
42
43
  "webkit",
43
44
  ]);
44
45
 
46
+ /**
47
+ * 라이브러리 루트는 앱 루트에서 실행/모바일 전용 항목(`main.ts`, `capacitor.config.ts`, `.akan`,
48
+ * `android`, `ios`, `mobile`, `script`, `secrets`)을 뺀 집합이다 — 라이브러리는 부팅되지도, 패키징되지도
49
+ * 않는다. `index.ts` 는 lib 에만 생성되고, `README.md` / `tsconfig.spec.json` 은 라이브러리가 패키지로
50
+ * 배포되기 때문에 남는다. `public` / `private` 은 syncAssets 가 앱으로 심링크하므로 라이브러리도 가진다.
51
+ */
52
+ export const libRootAllowedFiles = new Set([
53
+ "AGENTS.md",
54
+ "CLAUDE.md",
55
+ "README.md",
56
+ "akan.config.ts",
57
+ "akan.lib.json",
58
+ "client.ts",
59
+ "index.ts",
60
+ "package.json",
61
+ "server.ts",
62
+ "tsconfig.json",
63
+ "tsconfig.spec.json",
64
+ "tsconfig.tsbuildinfo",
65
+ ]);
66
+
67
+ export const libRootAllowedDirs = new Set([
68
+ "common",
69
+ "env",
70
+ "lib",
71
+ "page",
72
+ "plugin",
73
+ "private",
74
+ "public",
75
+ "srvkit",
76
+ "ui",
77
+ "webkit",
78
+ ]);
79
+
80
+ export const rootAllowedFiles = { app: appRootAllowedFiles, lib: libRootAllowedFiles } as const;
81
+ export const rootAllowedDirs = { app: appRootAllowedDirs, lib: libRootAllowedDirs } as const;
82
+
45
83
  export const libFacetRootAllowedFiles = new Set([
46
84
  "cnst.ts",
47
85
  "db.ts",
@@ -54,8 +92,15 @@ export const libFacetRootAllowedFiles = new Set([
54
92
  "useServer.ts",
55
93
  ]);
56
94
 
95
+ /** `<model>.signal.test.ts` — lib 파싯 루트에 놓이는 유일한 비생성 파일 (테스트가 배럴 전체를 부팅한다). */
96
+ const libFacetRootTestPattern = /^[A-Za-z][A-Za-z0-9_-]*\.signal\.(test|spec)\.(ts|tsx)$/;
97
+
98
+ export const isAllowedLibFacetRootFile = (filename: string) =>
99
+ libFacetRootAllowedFiles.has(filename) || libFacetRootTestPattern.test(filename);
100
+
57
101
  /**
58
- * scanSync 는 루트를 `Bun.Glob("*")` 로 읽어 dotfile 을 아예 보지 못한다. 디렉터리를 직접 읽는
102
+ * scanSync 는 루트를 `Bun.Glob("*")` 로 읽어 dotfile 을 아예 보지 못한다. 디렉터리를 직접 읽는
59
103
  * doctor 가 그 차이만큼 `.DS_Store` 같은 툴 산출물을 에러로 올리므로 같은 기준으로 걸러낸다.
60
104
  */
61
- export const isScannedAppRootEntry = (name: string) => !name.startsWith(".") || appRootAllowedDirs.has(name);
105
+ export const isScannedRootEntry = (type: SysType, name: string) =>
106
+ !name.startsWith(".") || rootAllowedDirs[type].has(name);
package/builder.ts DELETED
@@ -1,164 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { mkdir } from "node:fs/promises";
3
- import path from "node:path";
4
- import type { Loader } from "bun";
5
- import type { Executor } from "./executors";
6
- import { FileSys } from "./fileSys";
7
- import type { PackageJson } from "./types";
8
-
9
- /** Relative paths under these directories are not used as Bun.build entrypoints (Bun does not expand `**` in entrypoint strings; we scan with {@link Bun.Glob}). */
10
- const SKIP_ENTRY_DIR_SET = new Set(["node_modules", "dist", "build", ".git", ".next"]);
11
-
12
- const assetExtensions = [".css", ".md", ".js", ".png", ".ico", ".svg", ".json", ".template"];
13
- const assetLoader = Object.fromEntries(assetExtensions.map((ext) => [ext, "file" as const])) satisfies Record<
14
- string,
15
- Loader
16
- >;
17
-
18
- type BunBuildConfig = Parameters<typeof Bun.build>[0] & { bundle?: boolean };
19
- type ExportValue = string | string[] | { [condition: string]: ExportValue };
20
-
21
- interface BuildOptions {
22
- bundle?: boolean;
23
- additionalEntryPoints?: string[];
24
- }
25
-
26
- interface BuilderOptions {
27
- executor: Executor;
28
- distExecutor: Executor;
29
- pkgJson: PackageJson;
30
- rootPackageJson: PackageJson;
31
- }
32
- export class Builder {
33
- #executor: Executor;
34
- #distExecutor: Executor;
35
- #pkgJson: PackageJson;
36
-
37
- constructor({ executor, distExecutor, pkgJson }: BuilderOptions) {
38
- this.#executor = executor;
39
- this.#distExecutor = distExecutor;
40
- this.#pkgJson = pkgJson;
41
- }
42
- #globEntrypoints(cwd: string, pattern: string): string[] {
43
- const glob = new Bun.Glob(pattern);
44
- return Array.from(glob.scanSync({ cwd, onlyFiles: true }))
45
- .filter((relativePath) => {
46
- const segments = relativePath.split(path.sep);
47
- return !segments.some((segment) => SKIP_ENTRY_DIR_SET.has(segment));
48
- })
49
- .map((rel) => path.join(cwd, rel));
50
- }
51
- #globFiles(cwd: string, pattern = "**/*.*"): string[] {
52
- const glob = new Bun.Glob(pattern);
53
- return Array.from(glob.scanSync({ cwd, onlyFiles: true })).filter((relativePath) => {
54
- const segments = relativePath.split(path.sep);
55
- return !segments.some((segment) => SKIP_ENTRY_DIR_SET.has(segment));
56
- });
57
- }
58
-
59
- #resolveAdditionalEntrypoints(cwd: string, additionalEntryPoints: string[]): string[] {
60
- const out: string[] = [];
61
- for (const p of additionalEntryPoints) {
62
- if (p.includes("*")) {
63
- const rel = p.startsWith(`${cwd}/`) || p.startsWith(`${cwd}${path.sep}`) ? p.slice(cwd.length + 1) : p;
64
- out.push(...this.#globEntrypoints(cwd, rel));
65
- } else out.push(path.isAbsolute(p) ? p : path.join(cwd, p));
66
- }
67
- return out;
68
- }
69
- #getBuildOptions({ bundle = false, additionalEntryPoints = [] }: BuildOptions = {}): BunBuildConfig {
70
- const cwd = this.#executor.cwdPath;
71
- const entrypoints = [
72
- ...(bundle ? [path.join(cwd, "index.ts")] : this.#globEntrypoints(cwd, "**/*.{ts,tsx}")),
73
- ...this.#resolveAdditionalEntrypoints(cwd, additionalEntryPoints),
74
- ];
75
- return {
76
- root: cwd,
77
- entrypoints,
78
- bundle,
79
- packages: "external",
80
- splitting: false,
81
- target: this.#pkgJson.bun?.platform,
82
- format: "esm",
83
- outdir: this.#distExecutor.cwdPath,
84
- external: ["react", "react-dom"],
85
- loader: assetLoader,
86
- };
87
- }
88
- async #copySourceFiles() {
89
- const cwd = this.#executor.cwdPath;
90
- for (const relativePath of this.#globFiles(cwd)) {
91
- if (relativePath === "package.json") continue;
92
- const sourcePath = path.join(cwd, relativePath);
93
- const targetPath = path.join(this.#distExecutor.cwdPath, relativePath);
94
- await mkdir(path.dirname(targetPath), { recursive: true });
95
- await Bun.write(targetPath, Bun.file(sourcePath));
96
- }
97
- }
98
- #toPublishedPath(publishedPath: string, { useSource }: { useSource: boolean }) {
99
- const hasDotSlash = publishedPath.startsWith("./");
100
- const withoutFormatDir = publishedPath.replace(/^(?:\.\/)?(?:esm|cjs)\//, hasDotSlash ? "./" : "");
101
- if (!useSource) return withoutFormatDir;
102
- if (!hasDotSlash && withoutFormatDir === publishedPath) return publishedPath;
103
- const parsed = path.posix.parse(withoutFormatDir);
104
- if (![".js", ".mjs", ".cjs"].includes(parsed.ext)) return withoutFormatDir;
105
- const withoutExt = path.posix.join(parsed.dir, parsed.name);
106
- const sourcePath = withoutExt.startsWith("./") ? withoutExt.slice(2) : withoutExt;
107
- const sourceCandidates = [`${sourcePath}.ts`, `${sourcePath}.tsx`];
108
- const matchedSource = sourceCandidates.find((candidate) =>
109
- existsSync(path.join(this.#executor.cwdPath, candidate)),
110
- );
111
- if (!matchedSource) return withoutFormatDir;
112
- return hasDotSlash ? `./${matchedSource}` : matchedSource;
113
- }
114
- #normalizeExports(
115
- exportsValue: ExportValue | undefined,
116
- { useSource }: { useSource: boolean },
117
- ): ExportValue | undefined {
118
- if (!exportsValue) return exportsValue;
119
- if (typeof exportsValue === "string") return this.#toPublishedPath(exportsValue, { useSource });
120
- if (Array.isArray(exportsValue))
121
- return exportsValue.map((value) => this.#normalizeExports(value, { useSource }) as string);
122
- if (typeof exportsValue !== "object") return exportsValue;
123
-
124
- return Object.fromEntries(
125
- Object.entries(exportsValue)
126
- .filter(([condition]) => condition !== "require")
127
- .map(([condition, value]) => [condition, this.#normalizeExports(value, { useSource })])
128
- .filter((entry): entry is [string, ExportValue] => entry[1] !== undefined),
129
- );
130
- }
131
- #getPackageJson({ bundle = false }: BuildOptions = {}): PackageJson {
132
- const rootEntry = bundle ? "./index.js" : "./index.ts";
133
- const normalizedExports = this.#normalizeExports(this.#pkgJson.exports as ExportValue | undefined, {
134
- useSource: !bundle,
135
- });
136
- return {
137
- ...this.#pkgJson,
138
- type: "module",
139
- main: rootEntry,
140
- bin: this.#normalizeExports(this.#pkgJson.bin as ExportValue | undefined, { useSource: !bundle }),
141
- exports: {
142
- ...((typeof normalizedExports === "object" && !Array.isArray(normalizedExports)
143
- ? normalizedExports
144
- : {}) as Record<string, ExportValue>),
145
- ".": {
146
- import: rootEntry,
147
- types: bundle ? "./index.d.ts" : "./index.ts",
148
- },
149
- },
150
- };
151
- }
152
- async build(options: BuildOptions = {}) {
153
- if (await FileSys.dirExists(this.#distExecutor.cwdPath))
154
- await this.#distExecutor.exec(`rm -rf ${this.#distExecutor.cwdPath}`);
155
-
156
- if (options.bundle) {
157
- const buildResult = await Bun.build({ ...this.#getBuildOptions(options) });
158
- if (!buildResult.success) throw new AggregateError(buildResult.logs, "Bundle failed");
159
- } else await this.#copySourceFiles();
160
-
161
- const pkgPackageJson = this.#getPackageJson(options);
162
- await this.#distExecutor.setPackageJson(pkgPackageJson);
163
- }
164
- }
package/extractDeps.ts DELETED
@@ -1,86 +0,0 @@
1
- import type { PackageJson } from "./types";
2
-
3
- const NODE_NATIVE_MODULE_SET = new Set([
4
- "assert",
5
- "async_hooks",
6
- "buffer",
7
- "child_process",
8
- "cluster",
9
- "console",
10
- "constants",
11
- "crypto",
12
- "dgram",
13
- "dns",
14
- "domain",
15
- "events",
16
- "fs",
17
- "http",
18
- "http2",
19
- "https",
20
- "inspector",
21
- "module",
22
- "net",
23
- "os",
24
- "path",
25
- "perf_hooks",
26
- "process",
27
- "punycode",
28
- "querystring",
29
- "readline",
30
- "repl",
31
- "stream",
32
- "string_decoder",
33
- "timers",
34
- "tls",
35
- "trace_events",
36
- "tty",
37
- "url",
38
- "util",
39
- "v8",
40
- "vm",
41
- "wasi",
42
- "worker_threads",
43
- "zlib",
44
- ]);
45
-
46
- export const extractDependencies = (
47
- filepaths: { path: string; text: string }[],
48
- pacakgeJson: PackageJson,
49
- defaultDependencies: string[] = [],
50
- ) => {
51
- if (!pacakgeJson.dependencies) throw new Error("No dependencies found in package.json");
52
- const dependencies = new Set<string>(defaultDependencies);
53
-
54
- const existingDependencies = new Set<string>([
55
- ...Object.keys(pacakgeJson.dependencies ?? {}),
56
- ...Object.keys(pacakgeJson.devDependencies ?? {}),
57
- ]);
58
- const versionObj = {
59
- ...(pacakgeJson.dependencies ?? {}),
60
- ...(pacakgeJson.devDependencies ?? {}),
61
- };
62
-
63
- // Look for require statements: require('package-name') or import 'package-name'
64
- const requireRegex = /(?:require\s*\(|import\s*(?:[\w\s{},*]*\s+from\s*)?|import\s*\()\s*['"`]([^'"`]+)['"`]/g;
65
- for (const { text } of filepaths.filter(({ path }) => path.endsWith(".js") || path.endsWith(".ts"))) {
66
- let requireMatch = requireRegex.exec(text);
67
- while (requireMatch !== null) {
68
- const moduleName = requireMatch[1];
69
- if (!moduleName) throw new Error(`No module name found in ${text}`);
70
- const moduleNameParts = moduleName.split("/");
71
- const subModuleLength = moduleNameParts.length;
72
- for (let i = 0; i < subModuleLength; i++) {
73
- const libName = moduleNameParts.slice(0, i + 1).join("/");
74
- if (!NODE_NATIVE_MODULE_SET.has(libName) && existingDependencies.has(libName)) dependencies.add(libName);
75
- }
76
- requireMatch = requireRegex.exec(text);
77
- }
78
- }
79
- return Object.fromEntries(
80
- [...dependencies].sort().map((dep) => {
81
- const version = versionObj[dep];
82
- if (!version) throw new Error(`No version found for ${dep}`);
83
- return [dep, version];
84
- }),
85
- );
86
- };