@akanjs/devkit 1.0.20 → 2.1.0-rc.1

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 (195) hide show
  1. package/README.ko.md +65 -0
  2. package/README.md +62 -6
  3. package/aiEditor.ts +304 -0
  4. package/akanApp/akanApp.host.ts +393 -0
  5. package/akanApp/index.ts +1 -0
  6. package/akanConfig/akanConfig.test.ts +236 -0
  7. package/akanConfig/akanConfig.ts +384 -0
  8. package/akanConfig/index.ts +2 -0
  9. package/akanConfig/types.ts +23 -0
  10. package/applicationBuildReporter.ts +69 -0
  11. package/applicationBuildRunner.ts +302 -0
  12. package/applicationReleasePackager.ts +206 -0
  13. package/artifact/implicitRootLayout.ts +155 -0
  14. package/artifact/index.ts +1 -0
  15. package/artifact/routeSeedIndex.test.ts +98 -0
  16. package/artifact/routeSeedIndex.ts +130 -0
  17. package/auth.ts +41 -0
  18. package/builder.ts +164 -0
  19. package/capacitor.base.config.ts +88 -0
  20. package/capacitorApp.ts +440 -0
  21. package/commandDecorators/argMeta.ts +102 -0
  22. package/commandDecorators/command.ts +351 -0
  23. package/commandDecorators/commandBuilder.ts +224 -0
  24. package/commandDecorators/commandDecorators.test.ts +212 -0
  25. package/commandDecorators/commandMeta.ts +7 -0
  26. package/commandDecorators/dependencyBuilder.ts +100 -0
  27. package/{esm/src/commandDecorators/helpFormatter.js → commandDecorators/helpFormatter.ts} +100 -47
  28. package/{esm/src/commandDecorators/index.js → commandDecorators/index.ts} +4 -2
  29. package/commandDecorators/targetMeta.ts +31 -0
  30. package/commandDecorators/types.ts +10 -0
  31. package/constants.ts +25 -0
  32. package/createTunnel.ts +36 -0
  33. package/dependencyScanner.ts +357 -0
  34. package/devkitUtils.test.ts +259 -0
  35. package/executors.test.ts +315 -0
  36. package/executors.ts +1390 -0
  37. package/{esm/src/extractDeps.js → extractDeps.ts} +26 -20
  38. package/{esm/src/fileEditor.js → fileEditor.ts} +51 -32
  39. package/fileSys.ts +39 -0
  40. package/frontendBuild/allRoutesBuilder.ts +103 -0
  41. package/frontendBuild/buildRouteClient.test.ts +190 -0
  42. package/frontendBuild/clientBuildTypes.ts +114 -0
  43. package/frontendBuild/clientEntriesBundler.ts +303 -0
  44. package/frontendBuild/clientEntryDiscovery.ts +199 -0
  45. package/frontendBuild/csrArtifactBuilder.ts +237 -0
  46. package/frontendBuild/cssCompiler.ts +286 -0
  47. package/frontendBuild/cssImportResolver.ts +116 -0
  48. package/frontendBuild/fontOptimizer.ts +427 -0
  49. package/frontendBuild/frontendBuild.test.ts +204 -0
  50. package/frontendBuild/hmrChangeClassifier.ts +28 -0
  51. package/frontendBuild/hmrWatcher.ts +102 -0
  52. package/frontendBuild/index.ts +18 -0
  53. package/frontendBuild/pagesBundleBuilder.ts +137 -0
  54. package/frontendBuild/pagesEntrySourceGenerator.ts +37 -0
  55. package/frontendBuild/precompressArtifacts.ts +59 -0
  56. package/frontendBuild/routeClientBuilder.ts +290 -0
  57. package/frontendBuild/routesManifestArtifactSerializer.ts +62 -0
  58. package/frontendBuild/ssrBaseArtifactBuilder.ts +139 -0
  59. package/frontendBuild/vendorSpecifiers.ts +16 -0
  60. package/frontendBuild/watchRootResolver.ts +28 -0
  61. package/getCredentials.ts +19 -0
  62. package/getDirname.ts +3 -0
  63. package/getModelFileData.ts +59 -0
  64. package/getRelatedCnsts.ts +313 -0
  65. package/guideline.ts +19 -0
  66. package/incrementalBuilder/incrementalBuilder.host.test.ts +51 -0
  67. package/incrementalBuilder/incrementalBuilder.host.ts +152 -0
  68. package/incrementalBuilder/incrementalBuilder.proc.ts +331 -0
  69. package/incrementalBuilder/index.ts +1 -0
  70. package/{esm/src/index.js → index.ts} +28 -15
  71. package/lint/no-deep-internal-import.grit +25 -0
  72. package/lint/no-import-client-functions.grit +32 -0
  73. package/lint/no-import-external-library.grit +21 -0
  74. package/lint/no-js-private-class-method.grit +42 -0
  75. package/lint/no-use-client-in-server.grit +7 -0
  76. package/lint/non-scalar-props-restricted.grit +13 -0
  77. package/linter.ts +271 -0
  78. package/mobile/index.ts +1 -0
  79. package/mobile/mobileTarget.test.ts +53 -0
  80. package/mobile/mobileTarget.ts +88 -0
  81. package/package.json +48 -31
  82. package/prompter.ts +72 -0
  83. package/scanInfo.ts +606 -0
  84. package/selectModel.ts +11 -0
  85. package/{esm/src/spinner.js → spinner.ts} +22 -28
  86. package/{esm/src/capacitorApp.js → src/capacitorApp.ts} +82 -81
  87. package/sshTunnel.ts +152 -0
  88. package/{esm/src/streamAi.js → streamAi.ts} +18 -12
  89. package/transforms/barrelAnalyzer.ts +278 -0
  90. package/transforms/barrelImportsPlugin.ts +504 -0
  91. package/transforms/externalizeFrameworkPlugin.ts +185 -0
  92. package/transforms/index.ts +5 -0
  93. package/transforms/rscUseClientTransform.ts +59 -0
  94. package/transforms/transforms.test.ts +208 -0
  95. package/transforms/useClientBundlePlugin.ts +47 -0
  96. package/tsconfig.json +37 -0
  97. package/typeChecker.ts +264 -0
  98. package/types.ts +44 -0
  99. package/ui/MultiScrollList.tsx +242 -0
  100. package/ui/ScrollList.tsx +107 -0
  101. package/ui/index.ts +2 -0
  102. package/{esm/src/uploadRelease.js → uploadRelease.ts} +50 -34
  103. package/{esm/src/useStdoutDimensions.js → useStdoutDimensions.ts} +5 -5
  104. package/cjs/index.js +0 -21
  105. package/cjs/src/aiEditor.js +0 -311
  106. package/cjs/src/auth.js +0 -72
  107. package/cjs/src/builder.js +0 -114
  108. package/cjs/src/capacitorApp.js +0 -313
  109. package/cjs/src/commandDecorators/argMeta.js +0 -88
  110. package/cjs/src/commandDecorators/command.js +0 -324
  111. package/cjs/src/commandDecorators/commandMeta.js +0 -30
  112. package/cjs/src/commandDecorators/helpFormatter.js +0 -211
  113. package/cjs/src/commandDecorators/index.js +0 -31
  114. package/cjs/src/commandDecorators/targetMeta.js +0 -57
  115. package/cjs/src/commandDecorators/types.js +0 -15
  116. package/cjs/src/constants.js +0 -46
  117. package/cjs/src/createTunnel.js +0 -49
  118. package/cjs/src/dependencyScanner.js +0 -220
  119. package/cjs/src/executors.js +0 -964
  120. package/cjs/src/extractDeps.js +0 -103
  121. package/cjs/src/fileEditor.js +0 -120
  122. package/cjs/src/getCredentials.js +0 -44
  123. package/cjs/src/getDirname.js +0 -38
  124. package/cjs/src/getModelFileData.js +0 -66
  125. package/cjs/src/getRelatedCnsts.js +0 -260
  126. package/cjs/src/guideline.js +0 -15
  127. package/cjs/src/index.js +0 -65
  128. package/cjs/src/linter.js +0 -238
  129. package/cjs/src/prompter.js +0 -85
  130. package/cjs/src/scanInfo.js +0 -491
  131. package/cjs/src/selectModel.js +0 -46
  132. package/cjs/src/spinner.js +0 -93
  133. package/cjs/src/streamAi.js +0 -62
  134. package/cjs/src/typeChecker.js +0 -207
  135. package/cjs/src/types.js +0 -15
  136. package/cjs/src/uploadRelease.js +0 -112
  137. package/cjs/src/useStdoutDimensions.js +0 -43
  138. package/esm/index.js +0 -1
  139. package/esm/src/aiEditor.js +0 -282
  140. package/esm/src/auth.js +0 -42
  141. package/esm/src/builder.js +0 -81
  142. package/esm/src/commandDecorators/argMeta.js +0 -54
  143. package/esm/src/commandDecorators/command.js +0 -290
  144. package/esm/src/commandDecorators/commandMeta.js +0 -7
  145. package/esm/src/commandDecorators/targetMeta.js +0 -33
  146. package/esm/src/commandDecorators/types.js +0 -0
  147. package/esm/src/constants.js +0 -17
  148. package/esm/src/createTunnel.js +0 -26
  149. package/esm/src/dependencyScanner.js +0 -187
  150. package/esm/src/executors.js +0 -928
  151. package/esm/src/getCredentials.js +0 -11
  152. package/esm/src/getDirname.js +0 -5
  153. package/esm/src/getModelFileData.js +0 -33
  154. package/esm/src/getRelatedCnsts.js +0 -221
  155. package/esm/src/guideline.js +0 -0
  156. package/esm/src/linter.js +0 -205
  157. package/esm/src/prompter.js +0 -51
  158. package/esm/src/scanInfo.js +0 -455
  159. package/esm/src/selectModel.js +0 -13
  160. package/esm/src/typeChecker.js +0 -174
  161. package/esm/src/types.js +0 -0
  162. package/index.d.ts +0 -1
  163. package/src/aiEditor.d.ts +0 -50
  164. package/src/auth.d.ts +0 -9
  165. package/src/builder.d.ts +0 -18
  166. package/src/capacitorApp.d.ts +0 -39
  167. package/src/commandDecorators/argMeta.d.ts +0 -67
  168. package/src/commandDecorators/command.d.ts +0 -2
  169. package/src/commandDecorators/commandMeta.d.ts +0 -2
  170. package/src/commandDecorators/helpFormatter.d.ts +0 -3
  171. package/src/commandDecorators/index.d.ts +0 -6
  172. package/src/commandDecorators/targetMeta.d.ts +0 -19
  173. package/src/commandDecorators/types.d.ts +0 -1
  174. package/src/constants.d.ts +0 -26
  175. package/src/createTunnel.d.ts +0 -8
  176. package/src/dependencyScanner.d.ts +0 -23
  177. package/src/executors.d.ts +0 -296
  178. package/src/extractDeps.d.ts +0 -7
  179. package/src/fileEditor.d.ts +0 -16
  180. package/src/getCredentials.d.ts +0 -12
  181. package/src/getDirname.d.ts +0 -1
  182. package/src/getModelFileData.d.ts +0 -16
  183. package/src/getRelatedCnsts.d.ts +0 -53
  184. package/src/guideline.d.ts +0 -19
  185. package/src/index.d.ts +0 -23
  186. package/src/linter.d.ts +0 -109
  187. package/src/prompter.d.ts +0 -14
  188. package/src/scanInfo.d.ts +0 -82
  189. package/src/selectModel.d.ts +0 -1
  190. package/src/spinner.d.ts +0 -20
  191. package/src/streamAi.d.ts +0 -6
  192. package/src/typeChecker.d.ts +0 -52
  193. package/src/types.d.ts +0 -31
  194. package/src/uploadRelease.d.ts +0 -10
  195. package/src/useStdoutDimensions.d.ts +0 -1
@@ -0,0 +1,5 @@
1
+ export * from "./barrelAnalyzer";
2
+ export * from "./barrelImportsPlugin";
3
+ export * from "./externalizeFrameworkPlugin";
4
+ export * from "./rscUseClientTransform";
5
+ export * from "./useClientBundlePlugin";
@@ -0,0 +1,59 @@
1
+ import path from "node:path";
2
+
3
+ // Pure "use client" transform: when `source` starts with the `"use client"`
4
+ // directive, replace its exports with `registerClientReference` stubs so the
5
+ // RSC renderer can serialize them as client component references instead of
6
+ // trying to run them on the server.
7
+
8
+ // Matches `"use client"` or `'use client'` at the start of a file,
9
+ // optionally after leading whitespace and JS comments.
10
+ const USE_CLIENT_RE = /^\s*(?:\/\*[\s\S]*?\*\/\s*|\/\/[^\n]*\n\s*)*["']use client["']/;
11
+ const IMPLICIT_ROOT_LAYOUT_RE =
12
+ /[/\\]\.akan[/\\]generated[/\\](?:implicit-root-layout|root-layouts[/\\].*__root_layout)\.(tsx|ts|jsx|js)$/;
13
+
14
+ export interface UseClientTransformArgs {
15
+ path: string;
16
+ workspaceRoot?: string;
17
+ }
18
+
19
+ export function toClientReferencePath(absPath: string, workspaceRoot: string): string {
20
+ return path.relative(path.resolve(workspaceRoot), path.resolve(absPath)).split(path.sep).join("/");
21
+ }
22
+
23
+ /**
24
+ * Returns the stubbed module source if `source` is a client module, else null.
25
+ * The returned source is TypeScript-compatible (loader "ts" is safe).
26
+ */
27
+ export function transformUseClient(source: string, args: UseClientTransformArgs): string | null {
28
+ if (!USE_CLIENT_RE.test(source)) return null;
29
+ if (IMPLICIT_ROOT_LAYOUT_RE.test(args.path)) return null;
30
+ const transpiler = new Bun.Transpiler({ loader: loaderFor(args.path) });
31
+ const { exports } = transpiler.scan(source);
32
+ if (exports.length === 0) return null;
33
+
34
+ const referencePath = args.workspaceRoot ? toClientReferencePath(args.path, args.workspaceRoot) : args.path;
35
+ const filePathLit = JSON.stringify(referencePath);
36
+ const lines: string[] = [`import { registerClientReference } from "react-server-dom-webpack/server.node";`];
37
+
38
+ for (const name of exports) {
39
+ const nameLit = JSON.stringify(name);
40
+ const errMsg = JSON.stringify(
41
+ `Attempted to call '${name}' from '${referencePath}' on the server, but it is a client-only export.`,
42
+ );
43
+ const proxy = `() => { throw new Error(${errMsg}); }`;
44
+ const binding =
45
+ name === "default"
46
+ ? `export default registerClientReference(${proxy}, ${filePathLit}, ${nameLit});`
47
+ : `export const ${name} = registerClientReference(${proxy}, ${filePathLit}, ${nameLit});`;
48
+ lines.push(binding);
49
+ }
50
+
51
+ return lines.join("\n");
52
+ }
53
+
54
+ function loaderFor(absPath: string): "ts" | "tsx" | "js" | "jsx" {
55
+ if (absPath.endsWith(".tsx")) return "tsx";
56
+ if (absPath.endsWith(".jsx")) return "jsx";
57
+ if (absPath.endsWith(".ts")) return "ts";
58
+ return "js";
59
+ }
@@ -0,0 +1,208 @@
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 { BarrelAnalyzer } from "./barrelAnalyzer";
6
+ import { rewriteBarrelImports } from "./barrelImportsPlugin";
7
+ import { toClientReferencePath, transformUseClient } from "./rscUseClientTransform";
8
+
9
+ const tempRoots: string[] = [];
10
+
11
+ const makeTempRoot = async () => {
12
+ const root = await mkdtemp(path.join(os.tmpdir(), "akan-devkit-transform-"));
13
+ tempRoots.push(root);
14
+ return root;
15
+ };
16
+
17
+ const write = async (filePath: string, content: string) => {
18
+ await mkdir(path.dirname(filePath), { recursive: true });
19
+ await writeFile(filePath, content);
20
+ };
21
+
22
+ afterEach(async () => {
23
+ await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
24
+ });
25
+
26
+ describe("transformUseClient", () => {
27
+ test("returns null for non-client modules and generated root layouts", () => {
28
+ expect(transformUseClient("export const value = 1;", { path: "/repo/app/page.tsx" })).toBeNull();
29
+ expect(
30
+ transformUseClient('"use client"; export const value = 1;', {
31
+ path: "/repo/apps/demo/.akan/generated/implicit-root-layout.tsx",
32
+ }),
33
+ ).toBeNull();
34
+ expect(
35
+ transformUseClient('"use client"; export const value = 1;', {
36
+ path: "/repo/apps/demo/.akan/generated/root-layouts/admin__root_layout.tsx",
37
+ }),
38
+ ).toBeNull();
39
+ });
40
+
41
+ test("stubs named and default exports as RSC client references", () => {
42
+ const source = [
43
+ "// comment before directive",
44
+ '"use client";',
45
+ "export const Button = () => null;",
46
+ "export function useThing() { return null; }",
47
+ "export default function DefaultButton() { return null; }",
48
+ "",
49
+ ].join("\n");
50
+
51
+ const transformed = transformUseClient(source, {
52
+ path: "/repo/apps/demo/components/Button.tsx",
53
+ workspaceRoot: "/repo",
54
+ });
55
+
56
+ expect(transformed).toContain('import { registerClientReference } from "react-server-dom-webpack/server.node";');
57
+ expect(transformed).toContain('"apps/demo/components/Button.tsx"');
58
+ expect(transformed).toContain("export const Button = registerClientReference");
59
+ expect(transformed).toContain("export const useThing = registerClientReference");
60
+ expect(transformed).toContain("export default registerClientReference");
61
+ expect(toClientReferencePath("/repo/apps/demo/Button.tsx", "/repo")).toBe("apps/demo/Button.tsx");
62
+ });
63
+ });
64
+
65
+ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
66
+ test("analyzes local, named, aliased, and star re-exports while skipping type and namespace exports", async () => {
67
+ const root = await makeTempRoot();
68
+ const pkgDir = path.join(root, "pkg");
69
+ await write(
70
+ path.join(pkgDir, "index.ts"),
71
+ [
72
+ 'export { A, B as Bee, type TypeOnly } from "./leaf";',
73
+ 'export * from "./star";',
74
+ 'export * as ns from "./namespace";',
75
+ "export const Local = 1;",
76
+ "export type LocalType = string;",
77
+ "",
78
+ ].join("\n"),
79
+ );
80
+ await write(
81
+ path.join(pkgDir, "leaf.ts"),
82
+ ["export const A = 1;", "export const B = 2;", "export type TypeOnly = string;", ""].join("\n"),
83
+ );
84
+ await write(path.join(pkgDir, "star.ts"), "export const Star = 3;\n");
85
+ await write(path.join(pkgDir, "namespace.ts"), "export const Hidden = 4;\n");
86
+
87
+ const analyzer = new BarrelAnalyzer({
88
+ resolvePackage: async () => ({ pkgName: "@scope/pkg", entryFile: path.join(pkgDir, "index.ts"), pkgDir }),
89
+ });
90
+
91
+ const map = await analyzer.analyze("@scope/pkg");
92
+ expect(map?.get("A")).toEqual({ subpath: "@scope/pkg/leaf", originalName: "A" });
93
+ expect(map?.get("Bee")).toEqual({ subpath: "@scope/pkg/leaf", originalName: "B" });
94
+ expect(map?.get("Star")).toEqual({ subpath: "@scope/pkg/star", originalName: "Star" });
95
+ expect(map?.get("Local")).toEqual({ subpath: "@scope/pkg", originalName: "Local" });
96
+ expect(map?.has("TypeOnly")).toBe(false);
97
+ expect(map?.has("ns")).toBe(false);
98
+ });
99
+
100
+ test("rewrites flattenable named imports and preserves default, type, and unknown imports", async () => {
101
+ const analyzer = {
102
+ analyze: async () =>
103
+ new Map([
104
+ ["A", { subpath: "@scope/pkg/leaf", originalName: "A" }],
105
+ ["Bee", { subpath: "@scope/pkg/leaf", originalName: "B" }],
106
+ ]),
107
+ } as BarrelAnalyzer;
108
+
109
+ const rewritten = await rewriteBarrelImports(
110
+ 'import DefaultExport, { A, Bee as LocalBee, type Shape, Missing } from "@scope/pkg";\nconsole.log(A);',
111
+ ["@scope/pkg"],
112
+ analyzer,
113
+ );
114
+
115
+ expect(rewritten).toBe(
116
+ [
117
+ 'import DefaultExport, { type Shape, Missing } from "@scope/pkg";',
118
+ 'import { A, B as LocalBee } from "@scope/pkg/leaf";',
119
+ "console.log(A);",
120
+ ].join("\n"),
121
+ );
122
+
123
+ expect(await rewriteBarrelImports('import * as pkg from "@scope/pkg";', ["@scope/pkg"], analyzer)).toBeNull();
124
+ expect(await rewriteBarrelImports('import type { A } from "@scope/pkg";', ["@scope/pkg"], analyzer)).toBeNull();
125
+ expect(await rewriteBarrelImports('import { A } from "@other/pkg";', ["@scope/pkg"], analyzer)).toBeNull();
126
+ });
127
+
128
+ test("preserves generated client barrel side effects when flattening app client imports", async () => {
129
+ const analyzer = {
130
+ analyze: async () => new Map([["st", { subpath: "@apps/demo/lib/st", originalName: "st" }]]),
131
+ } as BarrelAnalyzer;
132
+
133
+ const rewritten = await rewriteBarrelImports(
134
+ 'import { st } from "@apps/demo/client";\nvoid st;\n',
135
+ ["@apps/demo/client"],
136
+ analyzer,
137
+ );
138
+
139
+ expect(rewritten).toBe(
140
+ ['import "@apps/demo/client";', 'import { st } from "@apps/demo/lib/st";', "void st;\n"].join("\n"),
141
+ );
142
+ });
143
+
144
+ test("does not rewrite import-looking code inside template literals", async () => {
145
+ const analyzer = {
146
+ analyze: async () => new Map([["AkanApp", { subpath: "akanjs/server/akanApp", originalName: "AkanApp" }]]),
147
+ } as BarrelAnalyzer;
148
+
149
+ const source = [
150
+ 'import { Code } from "@apps/docs/ui";',
151
+ "export const Example = () => (",
152
+ " <Code.Snippet",
153
+ " code={`",
154
+ 'import { AkanApp } from "akanjs/server";',
155
+ "",
156
+ "void new AkanApp().start();",
157
+ "`}",
158
+ " />",
159
+ ");",
160
+ "",
161
+ ].join("\n");
162
+
163
+ expect(await rewriteBarrelImports(source, ["akanjs/server"], analyzer)).toBeNull();
164
+ });
165
+
166
+ test("rewrites single-package Akan facet barrels to leaf subpaths", async () => {
167
+ const analyzer = {
168
+ analyze: async () =>
169
+ new Map([["BottomInset", { subpath: "akanjs/ui/Layout/BottomInset", originalName: "BottomInset" }]]),
170
+ } as BarrelAnalyzer;
171
+
172
+ const rewritten = await rewriteBarrelImports('import { BottomInset } from "akanjs/ui";\n', ["akanjs/ui"], analyzer);
173
+
174
+ expect(rewritten).toBe('import { BottomInset } from "akanjs/ui/Layout/BottomInset";\n');
175
+ });
176
+
177
+ test("preserves concrete file paths for package-exported barrels", async () => {
178
+ const root = await makeTempRoot();
179
+ await write(
180
+ path.join(root, "node_modules/akanjs/ui/index.ts"),
181
+ 'export { Link } from "./Link";\nexport { System } from "./System";\n',
182
+ );
183
+ await write(path.join(root, "node_modules/akanjs/ui/Link/index.tsx"), "export const Link = () => null;\n");
184
+ await write(path.join(root, "node_modules/akanjs/ui/System/index.tsx"), "export const System = () => null;\n");
185
+
186
+ const analyzer = new BarrelAnalyzer({
187
+ resolvePackage: async () => ({
188
+ pkgName: "akanjs/ui",
189
+ entryFile: path.join(root, "node_modules/akanjs/ui/index.ts"),
190
+ pkgDir: path.join(root, "node_modules/akanjs/ui"),
191
+ preserveFilePath: true,
192
+ }),
193
+ });
194
+
195
+ const rewritten = await rewriteBarrelImports(
196
+ 'import { Link, System } from "akanjs/ui";\n',
197
+ ["akanjs/ui"],
198
+ analyzer,
199
+ );
200
+
201
+ expect(rewritten).toBe(
202
+ `${[
203
+ 'import { Link } from "akanjs/ui/Link/index.tsx";',
204
+ 'import { System } from "akanjs/ui/System/index.tsx";',
205
+ ].join("\n")}\n`,
206
+ );
207
+ });
208
+ });
@@ -0,0 +1,47 @@
1
+ import type { BunPlugin } from "bun";
2
+ import { transformUseClient } from "./rscUseClientTransform";
3
+
4
+ /**
5
+ * BunPlugin that stubs `"use client"` modules so the server bundle sees
6
+ * `registerClientReference(proxy, absPath, name)` stubs instead of the
7
+ * original client code.
8
+ *
9
+ * The bundle produced by `PagesBundleBuilder` is loaded in the RSC worker
10
+ * with `--conditions react-server`; client components are never supposed
11
+ * to execute there. The resulting stub references keep the client
12
+ * component manifest lookups working at render time while ensuring the
13
+ * server bundle never drags the real client source graph in.
14
+ *
15
+ * Important: the second argument to `registerClientReference` must match the
16
+ * key the client manifest uses. When `workspaceRoot` is provided, both sides
17
+ * use workspace-relative keys so production artifacts are portable.
18
+ */
19
+ export function createUseClientBundlePlugin(options: { workspaceRoot?: string } = {}): BunPlugin {
20
+ return {
21
+ name: "akan-use-client-bundle",
22
+ setup(build) {
23
+ build.onLoad({ filter: /\.(tsx|ts|jsx|js)$/ }, async (args) => {
24
+ if (args.path.includes("/node_modules/")) return undefined;
25
+ let source: string;
26
+ try {
27
+ source = await Bun.file(args.path).text();
28
+ } catch {
29
+ return undefined;
30
+ }
31
+ const stubbed = transformUseClient(source, {
32
+ path: args.path,
33
+ workspaceRoot: options.workspaceRoot,
34
+ });
35
+ if (stubbed === null) return undefined;
36
+ return { contents: stubbed, loader: loaderFor(args.path) };
37
+ });
38
+ },
39
+ };
40
+ }
41
+
42
+ function loaderFor(absPath: string): "ts" | "tsx" | "js" | "jsx" {
43
+ if (absPath.endsWith(".tsx")) return "tsx";
44
+ if (absPath.endsWith(".jsx")) return "jsx";
45
+ if (absPath.endsWith(".ts")) return "ts";
46
+ return "js";
47
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": [
4
+ "ESNext",
5
+ "DOM"
6
+ ],
7
+ "target": "ESNext",
8
+ "module": "esnext",
9
+ "moduleDetection": "force",
10
+ "jsx": "react-jsx",
11
+ "allowJs": true,
12
+ "types": [
13
+ "bun"
14
+ ],
15
+ "moduleResolution": "bundler",
16
+ "allowImportingTsExtensions": true,
17
+ "verbatimModuleSyntax": true,
18
+ "noEmit": true,
19
+ "strict": true,
20
+ "skipLibCheck": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedSideEffectImports": false,
23
+ "noUncheckedIndexedAccess": false,
24
+ "noImplicitOverride": true,
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false,
28
+ "experimentalDecorators": true,
29
+ "paths": {}
30
+ },
31
+ "extends": "../../../tsconfig.json",
32
+ "files": [],
33
+ "include": [
34
+ "**/*.ts",
35
+ "**/*.tsx"
36
+ ]
37
+ }
package/typeChecker.ts ADDED
@@ -0,0 +1,264 @@
1
+ import { readFileSync } from "node:fs";
2
+ import * as path from "node:path";
3
+ import chalk from "chalk";
4
+ import * as ts from "typescript";
5
+
6
+ import type { Executor } from "./executors";
7
+
8
+ export interface ProjectTypecheckResult {
9
+ configPath: string;
10
+ diagnostics: ts.Diagnostic[];
11
+ errors: ts.Diagnostic[];
12
+ warnings: ts.Diagnostic[];
13
+ message: string;
14
+ }
15
+
16
+ export class TypeChecker {
17
+ readonly configPath: string;
18
+ readonly configFile: { config?: any; error?: ts.Diagnostic };
19
+ readonly config: ts.ParsedCommandLine;
20
+ constructor(executor: Executor) {
21
+ const configPath = this.#findConfigFile(executor.cwdPath);
22
+ if (!configPath) throw new Error("No tsconfig.json found in the project");
23
+ this.configPath = configPath;
24
+ this.configFile = ts.readConfigFile(this.configPath, (fileName) => ts.sys.readFile(fileName));
25
+ const parsedConfig = ts.parseJsonConfigFileContent(
26
+ this.configFile.config,
27
+ ts.sys,
28
+ path.dirname(this.configPath),
29
+ undefined,
30
+ this.configPath,
31
+ );
32
+
33
+ if (parsedConfig.errors.length > 0) {
34
+ const errorMessages = parsedConfig.errors
35
+ .map((error) => ts.flattenDiagnosticMessageText(error.messageText, "\n"))
36
+ .join("\n");
37
+ throw new Error(`Error parsing tsconfig.json:\n${errorMessages}`);
38
+ }
39
+ this.config = parsedConfig;
40
+ }
41
+ /**
42
+ * Find tsconfig.json by walking up the directory tree
43
+ */
44
+ #findConfigFile(searchPath: string): string | undefined {
45
+ return ts.findConfigFile(searchPath, (fileName) => ts.sys.fileExists(fileName), "tsconfig.json");
46
+ }
47
+
48
+ /**
49
+ * Type-check a single TypeScript file
50
+ * @param filePath - Path to the TypeScript file to check
51
+ * @returns Array of diagnostic messages
52
+ */
53
+ check(filePath: string): {
54
+ diagnostics: ts.Diagnostic[];
55
+ errors: ts.Diagnostic[];
56
+ warnings: ts.Diagnostic[];
57
+ fileDiagnostics: ts.Diagnostic[];
58
+ fileErrors: ts.Diagnostic[];
59
+ fileWarnings: ts.Diagnostic[];
60
+ } {
61
+ const program = ts.createProgram([filePath], this.config.options);
62
+ const diagnostics = [
63
+ ...program.getSemanticDiagnostics(),
64
+ ...program.getSyntacticDiagnostics(),
65
+ // Only check declaration diagnostics when declaration emit is enabled
66
+ ...(this.config.options.declaration ? program.getDeclarationDiagnostics() : []),
67
+ ];
68
+ const errors = diagnostics.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
69
+ const warnings = diagnostics.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Warning);
70
+ const fileDiagnostics = diagnostics.filter((diagnostic) => diagnostic.file?.fileName === filePath);
71
+ const fileErrors = fileDiagnostics.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
72
+ const fileWarnings = fileDiagnostics.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Warning);
73
+ return { diagnostics, errors, warnings, fileDiagnostics, fileErrors, fileWarnings };
74
+ }
75
+
76
+ /**
77
+ * Format diagnostics for console output
78
+ * @param diagnostics - Array of TypeScript diagnostics
79
+ * @returns Formatted string
80
+ */
81
+ formatDiagnostics(diagnostics: ts.Diagnostic[]): string {
82
+ if (diagnostics.length === 0) return chalk.bold("✅ No type errors found");
83
+
84
+ const output: string[] = [];
85
+ let errorCount = 0;
86
+ let warningCount = 0;
87
+ let suggestionCount = 0;
88
+
89
+ // Group diagnostics by file
90
+ const diagnosticsByFile = new Map<string, ts.Diagnostic[]>();
91
+ diagnostics.forEach((diagnostic) => {
92
+ if (diagnostic.category === ts.DiagnosticCategory.Error) errorCount++;
93
+ else if (diagnostic.category === ts.DiagnosticCategory.Warning) warningCount++;
94
+ else if (diagnostic.category === ts.DiagnosticCategory.Suggestion) suggestionCount++;
95
+
96
+ if (diagnostic.file) {
97
+ const fileName = diagnostic.file.fileName;
98
+ if (!diagnosticsByFile.has(fileName)) diagnosticsByFile.set(fileName, []);
99
+ const fileDiagnostics = diagnosticsByFile.get(fileName);
100
+ if (fileDiagnostics) fileDiagnostics.push(diagnostic);
101
+ } else {
102
+ if (!diagnosticsByFile.has("")) diagnosticsByFile.set("", []);
103
+ const fileDiagnostics = diagnosticsByFile.get("");
104
+ if (fileDiagnostics) fileDiagnostics.push(diagnostic);
105
+ }
106
+ });
107
+
108
+ // Format diagnostics by file
109
+ diagnosticsByFile.forEach((fileDiagnostics, fileName) => {
110
+ if (fileName) output.push(`\n${chalk.cyan(fileName)}`);
111
+
112
+ fileDiagnostics.forEach((diagnostic) => {
113
+ const categoryText =
114
+ diagnostic.category === ts.DiagnosticCategory.Error
115
+ ? "error"
116
+ : diagnostic.category === ts.DiagnosticCategory.Warning
117
+ ? "warning"
118
+ : "suggestion";
119
+ const categoryColor =
120
+ diagnostic.category === ts.DiagnosticCategory.Error
121
+ ? chalk.red
122
+ : diagnostic.category === ts.DiagnosticCategory.Warning
123
+ ? chalk.yellow
124
+ : chalk.blue;
125
+ const icon =
126
+ diagnostic.category === ts.DiagnosticCategory.Error
127
+ ? "❌"
128
+ : diagnostic.category === ts.DiagnosticCategory.Warning
129
+ ? "⚠️"
130
+ : "💡";
131
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
132
+ const tsCode = chalk.dim(`(TS${diagnostic.code})`);
133
+
134
+ if (diagnostic.file && diagnostic.start !== undefined) {
135
+ const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
136
+
137
+ output.push(`\n ${icon} ${categoryColor(categoryText)}: ${message} ${tsCode}`);
138
+ output.push(` ${chalk.gray("at")} ${fileName}:${chalk.bold(`${line + 1}:${character + 1}`)}`);
139
+
140
+ // Show source line with underline
141
+ const sourceLines = diagnostic.file.text.split("\n");
142
+ if (line < sourceLines.length) {
143
+ const sourceLine = sourceLines[line];
144
+ const lineNumber = (line + 1).toString().padStart(5, " ");
145
+
146
+ output.push(`\n${chalk.dim(`${lineNumber} |`)} ${sourceLine}`);
147
+
148
+ // Create underline with squiggly for TypeScript
149
+ const underlinePrefix = " ".repeat(character);
150
+ const length = diagnostic.length ?? 1;
151
+ const underline = "~".repeat(Math.max(1, length));
152
+
153
+ output.push(
154
+ `${chalk.dim(`${" ".repeat(lineNumber.length)} |`)} ${underlinePrefix}${categoryColor(underline)}`,
155
+ );
156
+ }
157
+ } else output.push(`\n ${icon} ${categoryColor(categoryText)}: ${message} ${tsCode}`);
158
+ });
159
+ });
160
+
161
+ const summary = [] as string[];
162
+ if (errorCount > 0) summary.push(chalk.red(`${errorCount} error(s)`));
163
+ if (warningCount > 0) summary.push(chalk.yellow(`${warningCount} warning(s)`));
164
+ if (suggestionCount > 0) summary.push(chalk.blue(`${suggestionCount} suggestion(s)`));
165
+
166
+ return `\n${summary.join(", ")} found${output.join("\n")}`;
167
+ }
168
+
169
+ /**
170
+ * Get detailed diagnostic information with code snippet
171
+ * @param filePath - Path to the TypeScript file to check
172
+ * @returns Object containing diagnostics and detailed information
173
+ */
174
+ getDetailedDiagnostics(filePath: string): {
175
+ diagnostics: ts.Diagnostic[];
176
+ details: { line: number; column: number; message: string; code: number; codeSnippet?: string }[];
177
+ } {
178
+ const { diagnostics } = this.check(filePath);
179
+ const sourceFile = ts.createSourceFile(filePath, readFileSync(filePath, "utf8"), ts.ScriptTarget.Latest, true);
180
+
181
+ const details = diagnostics.map((diagnostic) => {
182
+ if (diagnostic.file && diagnostic.start !== undefined) {
183
+ const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
184
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
185
+
186
+ const lines = sourceFile.text.split("\n");
187
+ const codeSnippet = line < lines.length ? lines[line] : undefined;
188
+ return { line: line + 1, column: character + 1, message, code: diagnostic.code, codeSnippet };
189
+ }
190
+
191
+ return {
192
+ line: 0,
193
+ column: 0,
194
+ message: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
195
+ code: diagnostic.code,
196
+ };
197
+ });
198
+
199
+ return { diagnostics, details };
200
+ }
201
+
202
+ /**
203
+ * Check if a file has type errors
204
+ * @param filePath - Path to the TypeScript file to check
205
+ * @returns true if there are no type errors, false otherwise
206
+ */
207
+ hasNoTypeErrors(filePath: string): boolean {
208
+ try {
209
+ const { diagnostics } = this.check(filePath);
210
+ return diagnostics.length === 0;
211
+ } catch (error) {
212
+ return false;
213
+ }
214
+ }
215
+
216
+ static checkProject(configPath: string): ProjectTypecheckResult {
217
+ const parsedConfig = TypeChecker.parseConfig(configPath);
218
+ const host = ts.createIncrementalCompilerHost(parsedConfig.options);
219
+ const builderProgram = ts.createIncrementalProgram({
220
+ rootNames: parsedConfig.fileNames,
221
+ options: parsedConfig.options,
222
+ projectReferences: parsedConfig.projectReferences,
223
+ configFileParsingDiagnostics: parsedConfig.errors,
224
+ host,
225
+ });
226
+ const program = builderProgram.getProgram();
227
+ const diagnostics = [...ts.getPreEmitDiagnostics(program), ...builderProgram.emit().diagnostics];
228
+ const errors = diagnostics.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
229
+ const warnings = diagnostics.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Warning);
230
+ return {
231
+ configPath,
232
+ diagnostics,
233
+ errors,
234
+ warnings,
235
+ message: TypeChecker.formatDiagnosticMessages(diagnostics),
236
+ };
237
+ }
238
+
239
+ static parseConfig(configPath: string): ts.ParsedCommandLine {
240
+ const configFile = ts.readConfigFile(configPath, (fileName) => ts.sys.readFile(fileName));
241
+ const configDiagnostics = configFile.error ? [configFile.error] : [];
242
+ if (!configFile.config) {
243
+ const message = TypeChecker.formatDiagnosticMessages(configDiagnostics);
244
+ throw new Error(message || `Error reading tsconfig.json: ${configPath}`);
245
+ }
246
+ const parsedConfig = ts.parseJsonConfigFileContent(
247
+ configFile.config,
248
+ ts.sys,
249
+ path.dirname(configPath),
250
+ undefined,
251
+ configPath,
252
+ );
253
+ if (parsedConfig.errors.length > 0) throw new Error(TypeChecker.formatDiagnosticMessages(parsedConfig.errors));
254
+ return parsedConfig;
255
+ }
256
+
257
+ static formatDiagnosticMessages(diagnostics: ts.Diagnostic[]): string {
258
+ return ts.formatDiagnosticsWithColorAndContext(diagnostics, {
259
+ getCanonicalFileName: (fileName) => fileName,
260
+ getCurrentDirectory: () => process.cwd(),
261
+ getNewLine: () => ts.sys.newLine,
262
+ });
263
+ }
264
+ }
package/types.ts ADDED
@@ -0,0 +1,44 @@
1
+ export interface PackageJson {
2
+ name: string;
3
+ type?: "module" | "commonjs";
4
+ version: string;
5
+ main?: string;
6
+ description: string;
7
+ scripts?: Record<string, string>;
8
+ dependencies?: Record<string, string>;
9
+ devDependencies?: Record<string, string>;
10
+ peerDependencies?: Record<string, string>;
11
+ peerDependenciesMeta?: Record<string, { optional?: boolean }>;
12
+ optionalDependencies?: Record<string, string>;
13
+ engines?: Record<string, string>;
14
+ exports?: Record<string, string | Record<string, string>>;
15
+ bun?: {
16
+ platform?: "node" | "browser" | "bun";
17
+ };
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ export interface TsConfigJson {
22
+ extends?: string;
23
+ compilerOptions: {
24
+ target: string;
25
+ paths?: Record<string, string[]>;
26
+ };
27
+ references?: {
28
+ path: string;
29
+ }[];
30
+ }
31
+
32
+ export interface FileContent {
33
+ filePath: string;
34
+ content: string;
35
+ }
36
+
37
+ export interface BaseDevEnv {
38
+ workspaceRoot: string | undefined;
39
+ repoName: string;
40
+ serveDomain: string;
41
+ env: "testing" | "debug" | "develop" | "main" | "local";
42
+ portOffset: number;
43
+ appName?: string | undefined;
44
+ }