@akanjs/devkit 3.1.2-rc.4 → 3.2.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 (89) hide show
  1. package/CHANGELOG.md +353 -0
  2. package/abstractDoc.ts +1 -1
  3. package/agentAssets.ts +2 -2
  4. package/akanConfig/akanConfig.ts +2 -2
  5. package/akanMcpContract.ts +1 -1
  6. package/apiReference.test.ts +37 -0
  7. package/apiReference.ts +240 -0
  8. package/applicationBuildReporter.test.ts +25 -3
  9. package/applicationBuildReporter.ts +8 -1
  10. package/applicationBuildRunner.test.ts +34 -14
  11. package/applicationBuildRunner.ts +76 -14
  12. package/artifact/implicitRootLayout.test.ts +42 -0
  13. package/artifact/implicitRootLayout.ts +5 -6
  14. package/artifact/storeBootstrap.ts +24 -0
  15. package/cloud/cloudApi.test.ts +70 -7
  16. package/cloud/cloudApi.ts +10 -4
  17. package/cloud/constants.ts +3 -0
  18. package/cloud/globalConfig.test.ts +36 -0
  19. package/cloud/globalConfig.ts +10 -1
  20. package/codeAgent/skills/akan-debug/SKILL.md +2 -1
  21. package/codeAgent/skills/akan-page/SKILL.md +16 -7
  22. package/codeAgent/skills/akan-service/SKILL.md +48 -19
  23. package/commandDecorators/command.ts +55 -9
  24. package/commandDecorators/commandDecorators.test.ts +131 -2
  25. package/createTunnel.test.ts +12 -2
  26. package/declarationReader.ts +211 -0
  27. package/executors.test.ts +65 -8
  28. package/executors.ts +5 -3
  29. package/frameworkGraph.test.ts +121 -0
  30. package/frameworkGraph.ts +165 -0
  31. package/frontendBuild/allRoutesBuilder.test.ts +84 -0
  32. package/frontendBuild/allRoutesBuilder.ts +32 -12
  33. package/frontendBuild/buildRouteClient.test.ts +192 -2
  34. package/frontendBuild/bundleDefine.ts +4 -0
  35. package/frontendBuild/clientBuildTypes.ts +7 -1
  36. package/frontendBuild/clientEntriesBundler.test.ts +82 -0
  37. package/frontendBuild/clientEntriesBundler.ts +55 -4
  38. package/frontendBuild/csrArtifactBuilder.test.ts +23 -0
  39. package/frontendBuild/csrArtifactBuilder.ts +2 -1
  40. package/frontendBuild/fontOptimizer.test.ts +54 -0
  41. package/frontendBuild/fontOptimizer.ts +5 -4
  42. package/frontendBuild/index.ts +3 -0
  43. package/frontendBuild/routeClientBuilder.ts +6 -2
  44. package/frontendBuild/routeModulePreloads.test.ts +59 -0
  45. package/frontendBuild/routeModulePreloads.ts +52 -0
  46. package/frontendBuild/ssrBaseArtifactBuilder.ts +35 -10
  47. package/frontendBuild/ssrDevBundler.test.ts +26 -0
  48. package/frontendBuild/ssrDevBundler.ts +2 -1
  49. package/frontendBuild/vendorImportScanner.test.ts +71 -0
  50. package/frontendBuild/vendorImportScanner.ts +80 -0
  51. package/frontendBuild/vendorNarrowing.test.ts +103 -0
  52. package/frontendBuild/vendorNarrowing.ts +64 -0
  53. package/frontendBuild/vendorSpecifiers.ts +13 -0
  54. package/gitRevision.ts +6 -2
  55. package/incrementalBuilder/builderGraphPayloads.ts +60 -0
  56. package/incrementalBuilder/incrementalBuilder.proc.test.ts +65 -1
  57. package/incrementalBuilder/incrementalBuilder.proc.ts +5 -36
  58. package/integration/devStability.integration.test.ts +1 -1
  59. package/integration/devStabilityHarness.test.ts +1 -1
  60. package/lint/no-deep-internal-import.grit +1 -1
  61. package/maintainabilityBaseline.ts +7 -2
  62. package/maintainabilityContract.test.ts +183 -0
  63. package/maintainabilityContract.ts +145 -30
  64. package/maintainabilityExceptionScanner.test.ts +13 -2
  65. package/maintainabilityExceptionScanner.ts +16 -6
  66. package/maintainabilityLimits.ts +12 -0
  67. package/maintainabilityReport.test.ts +35 -0
  68. package/maintainabilityReport.ts +29 -8
  69. package/mobile/nativePublicEnv.test.ts +5 -1
  70. package/package.json +3 -3
  71. package/preview/PreviewChrome.ts +3 -3
  72. package/preview/PreviewView.ts +26 -8
  73. package/preview/previewView.test.ts +87 -3
  74. package/routeJsBudget.test.ts +177 -0
  75. package/routeJsBudget.ts +222 -0
  76. package/structureScanner.test.ts +167 -0
  77. package/structureScanner.ts +164 -0
  78. package/transforms/barrelAnalyzer.ts +10 -3
  79. package/transforms/barrelImportsPlugin.ts +13 -7
  80. package/transforms/serverEnvPlugin.test.ts +1 -1
  81. package/transforms/serverEnvPlugin.ts +1 -1
  82. package/transforms/transforms.test.ts +64 -12
  83. package/transforms/tsconfigPackageResolver.test.ts +12 -0
  84. package/workflow/plan.ts +79 -17
  85. package/workflow/types.ts +1 -0
  86. package/workspaceGraph.test.ts +58 -0
  87. package/workspaceGraph.ts +32 -3
  88. package/workspaceLayout.ts +1 -0
  89. /package/integration/{devStabilityHarness.ts → devStabilityHarness.fixture.ts} +0 -0
@@ -0,0 +1,167 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import ts from "typescript";
3
+ import { StructureScanner } from "./structureScanner";
4
+
5
+ const measure = (source: string, file = "unit.ts") =>
6
+ new StructureScanner().measure(
7
+ ts.createSourceFile(
8
+ file,
9
+ source,
10
+ ts.ScriptTarget.Latest,
11
+ true,
12
+ file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
13
+ ),
14
+ );
15
+ const namesOf = (source: string) => measure(source).functions.map(({ name }) => name);
16
+
17
+ describe("StructureScanner functions", () => {
18
+ test("names a function by the classes and functions around it, and counts the lines it spans", () => {
19
+ const { functions } = measure(`export class Host {
20
+ #children = new Map();
21
+ constructor() {}
22
+ get status() {
23
+ return 1;
24
+ }
25
+ set status(value: number) {}
26
+ async start() {
27
+ const tick = () => {
28
+ return 1;
29
+ };
30
+ }
31
+ static #restart = function named() {};
32
+ }
33
+ export function build() {}
34
+ export default function () {}
35
+ `);
36
+
37
+ expect(functions).toEqual([
38
+ { name: "Host.constructor", line: 3, lineCount: 1 },
39
+ { name: "Host.get status", line: 4, lineCount: 3 },
40
+ { name: "Host.set status", line: 7, lineCount: 1 },
41
+ { name: "Host.start", line: 8, lineCount: 5 },
42
+ { name: "Host.start.tick", line: 9, lineCount: 3 },
43
+ { name: "Host.named", line: 13, lineCount: 1 },
44
+ { name: "build", line: 15, lineCount: 1 },
45
+ { name: "default", line: 16, lineCount: 1 },
46
+ ]);
47
+ });
48
+
49
+ test("names an anonymous function after what its call is bound to, else after the call", () => {
50
+ expect(
51
+ namesOf(`const rows = items.map((item) => item).filter(Boolean);
52
+ const View = memo(() => null);
53
+ const routes = { "/*": answering(async (req) => req), list: (() => []) as unknown };
54
+ describe("suite", () => {
55
+ useEffect(() => {});
56
+ holder.onExit = () => {};
57
+ });
58
+ export class ShowCommand extends command("show", ({ public: target }) => ({
59
+ show: target().exec(async function (format) {}),
60
+ })) {}
61
+ export default () => {};
62
+ `),
63
+ ).toEqual([
64
+ "rows",
65
+ "View",
66
+ "routes./*",
67
+ "routes.list",
68
+ "describe()",
69
+ "describe().useEffect()",
70
+ "describe().onExit",
71
+ "ShowCommand.command()",
72
+ "ShowCommand.command().show",
73
+ "default",
74
+ ]);
75
+ });
76
+
77
+ test("names a repeat like the first, whatever stands before it", () => {
78
+ expect(namesOf("test('a', () => {});\ntest('b', () => {});\ntest('c', () => {});\n")).toEqual([
79
+ "test()",
80
+ "test()",
81
+ "test()",
82
+ ]);
83
+ });
84
+
85
+ test("names an object literal's members after what the literal is bound to", () => {
86
+ expect(
87
+ namesOf(`const h = { foo() {} };
88
+ const k = { foo() {} };
89
+ const routes = { api: { list: () => [] } };
90
+ export function make() {
91
+ return { setX() {} };
92
+ }
93
+ register({ onDone() {} });
94
+ `),
95
+ ).toEqual(["h.foo", "k.foo", "routes.api.list", "make", "make.setX", "register().onDone"]);
96
+ });
97
+
98
+ test("names an inline JSX callback after its attribute, and a function child children", () => {
99
+ const { functions } = measure(
100
+ `export const Board = () => (
101
+ <List onClick={() => {}} render={(row) => row}>
102
+ {(row) => row}
103
+ </List>
104
+ );
105
+ `,
106
+ "Board.tsx",
107
+ );
108
+
109
+ expect(functions.map(({ name }) => name)).toEqual(["Board", "Board.onClick", "Board.render", "Board.children"]);
110
+ });
111
+
112
+ test("skips a signature without a body", () => {
113
+ expect(
114
+ namesOf(
115
+ "export function overload(a: string): void;\nexport function overload(a: unknown) {}\ninterface I { m(): void }\n",
116
+ ),
117
+ ).toEqual(["overload"]);
118
+ });
119
+ });
120
+
121
+ describe("StructureScanner classes", () => {
122
+ test("counts every member of a class or class expression, named like a function", () => {
123
+ const { classes } = measure(`export class Exception extends Error {
124
+ static BadRequest = class BadRequestException extends Exception {
125
+ constructor() {
126
+ super();
127
+ }
128
+ };
129
+ readonly code = 1;;
130
+ #secret = 2;
131
+ get status() {
132
+ return 3;
133
+ }
134
+ set status(value: number) {}
135
+ [key: string]: unknown;
136
+ }
137
+ const Mixin = class extends Base {};
138
+ export default class {}
139
+ `);
140
+
141
+ expect(classes).toEqual([
142
+ { name: "Exception", line: 1, memberCount: 5 },
143
+ { name: "Exception.BadRequestException", line: 2, memberCount: 1 },
144
+ { name: "Mixin", line: 15, memberCount: 0 },
145
+ { name: "default", line: 16, memberCount: 0 },
146
+ ]);
147
+ });
148
+
149
+ test("counts an overloaded method once, and neither an index signature nor a static block", () => {
150
+ const { classes } = measure(`export abstract class Store {
151
+ static {
152
+ Store.ready = true;
153
+ }
154
+ [key: string]: unknown;
155
+ constructor(name: string);
156
+ constructor(name: string, id?: number) {}
157
+ load(id: string): void;
158
+ load(ids: string[]): void;
159
+ load(arg: unknown) {}
160
+ static load() {}
161
+ abstract save(): void;
162
+ }
163
+ `);
164
+
165
+ expect(classes).toEqual([{ name: "Store", line: 1, memberCount: 4 }]);
166
+ });
167
+ });
@@ -0,0 +1,164 @@
1
+ import ts from "typescript";
2
+
3
+ export interface FunctionSize {
4
+ name: string;
5
+ line: number;
6
+ lineCount: number;
7
+ }
8
+
9
+ export interface ClassSize {
10
+ name: string;
11
+ line: number;
12
+ memberCount: number;
13
+ }
14
+
15
+ type FunctionNode =
16
+ | ts.FunctionDeclaration
17
+ | ts.MethodDeclaration
18
+ | ts.ConstructorDeclaration
19
+ | ts.GetAccessorDeclaration
20
+ | ts.SetAccessorDeclaration
21
+ | ts.FunctionExpression
22
+ | ts.ArrowFunction;
23
+
24
+ export class StructureScanner {
25
+ measure(sourceFile: ts.SourceFile): { functions: FunctionSize[]; classes: ClassSize[] } {
26
+ const functions: FunctionSize[] = [];
27
+ const classes: ClassSize[] = [];
28
+ const lineOf = (pos: number) => sourceFile.getLineAndCharacterOfPosition(pos).line + 1;
29
+ const visit = (node: ts.Node, owners: string[]) => {
30
+ if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
31
+ const name = StructureScanner.#classNameOf(node);
32
+ classes.push({
33
+ name: [...owners, name].join("."),
34
+ line: lineOf(node.getStart(sourceFile)),
35
+ memberCount: StructureScanner.#memberCountOf(node),
36
+ });
37
+ ts.forEachChild(node, (child) => visit(child, [...owners, name]));
38
+ return;
39
+ }
40
+ if (StructureScanner.#isFunction(node)) {
41
+ const name = StructureScanner.#functionNameOf(node);
42
+ const line = lineOf(node.getStart(sourceFile));
43
+ functions.push({
44
+ name: [...owners, name].join("."),
45
+ line,
46
+ lineCount: lineOf(node.getEnd()) - line + 1,
47
+ });
48
+ ts.forEachChild(node, (child) => visit(child, [...owners, name]));
49
+ return;
50
+ }
51
+ if (ts.isObjectLiteralExpression(node)) {
52
+ const name = StructureScanner.#boundNameOf(node);
53
+ ts.forEachChild(node, (child) => visit(child, name === "<anonymous>" ? owners : [...owners, name]));
54
+ return;
55
+ }
56
+ ts.forEachChild(node, (child) => visit(child, owners));
57
+ };
58
+ ts.forEachChild(sourceFile, (child) => visit(child, []));
59
+ return { functions, classes };
60
+ }
61
+
62
+ static #memberCountOf(node: ts.ClassDeclaration | ts.ClassExpression) {
63
+ const methods = new Set<string>();
64
+ return node.members.filter((member) => {
65
+ if (
66
+ ts.isSemicolonClassElement(member) ||
67
+ ts.isIndexSignatureDeclaration(member) ||
68
+ ts.isClassStaticBlockDeclaration(member)
69
+ )
70
+ return false;
71
+ if (!ts.isMethodDeclaration(member) && !ts.isConstructorDeclaration(member)) return true;
72
+ const isStatic = ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword);
73
+ const key = `${isStatic ? "static " : ""}${ts.isConstructorDeclaration(member) ? "constructor" : StructureScanner.#textOf(member.name)}`;
74
+ if (methods.has(key)) return false;
75
+ methods.add(key);
76
+ return true;
77
+ }).length;
78
+ }
79
+
80
+ static #isFunction(node: ts.Node): node is FunctionNode {
81
+ return (
82
+ (ts.isFunctionDeclaration(node) ||
83
+ ts.isMethodDeclaration(node) ||
84
+ ts.isConstructorDeclaration(node) ||
85
+ ts.isGetAccessorDeclaration(node) ||
86
+ ts.isSetAccessorDeclaration(node) ||
87
+ ts.isFunctionExpression(node) ||
88
+ ts.isArrowFunction(node)) &&
89
+ node.body !== undefined
90
+ );
91
+ }
92
+
93
+ static #functionNameOf(node: FunctionNode) {
94
+ if (ts.isConstructorDeclaration(node)) return "constructor";
95
+ if (ts.isGetAccessorDeclaration(node)) return `get ${StructureScanner.#textOf(node.name)}`;
96
+ if (ts.isSetAccessorDeclaration(node)) return `set ${StructureScanner.#textOf(node.name)}`;
97
+ if (node.name) return StructureScanner.#textOf(node.name);
98
+ return ts.isFunctionDeclaration(node) ? "default" : StructureScanner.#boundNameOf(node);
99
+ }
100
+
101
+ static #classNameOf(node: ts.ClassDeclaration | ts.ClassExpression) {
102
+ if (node.name) return node.name.text;
103
+ return ts.isClassDeclaration(node) ? "default" : StructureScanner.#boundNameOf(node);
104
+ }
105
+
106
+ static #boundNameOf(node: ts.Node) {
107
+ let current: ts.Node = StructureScanner.#outerOf(node);
108
+ let call: string | null = null;
109
+ while (ts.isCallExpression(current.parent) || ts.isNewExpression(current.parent)) {
110
+ const callee = current.parent.expression;
111
+ if (current === callee) break;
112
+ call ??= `${ts.isPropertyAccessExpression(callee) ? callee.name.text : ts.isIdentifier(callee) ? callee.text : "call"}()`;
113
+ current = StructureScanner.#outerOf(current.parent);
114
+ }
115
+ const { parent } = current;
116
+ if (
117
+ (ts.isVariableDeclaration(parent) ||
118
+ ts.isPropertyAssignment(parent) ||
119
+ ts.isPropertyDeclaration(parent) ||
120
+ ts.isParameter(parent) ||
121
+ ts.isBindingElement(parent)) &&
122
+ parent.name &&
123
+ !ts.isObjectBindingPattern(parent.name) &&
124
+ !ts.isArrayBindingPattern(parent.name)
125
+ )
126
+ return StructureScanner.#textOf(parent.name);
127
+ if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken)
128
+ return ts.isPropertyAccessExpression(parent.left)
129
+ ? parent.left.name.text
130
+ : ts.isIdentifier(parent.left)
131
+ ? parent.left.text
132
+ : "assigned";
133
+ if (ts.isExportAssignment(parent)) return "default";
134
+ if (ts.isJsxExpression(parent))
135
+ return ts.isJsxAttribute(parent.parent)
136
+ ? ts.isIdentifier(parent.parent.name)
137
+ ? parent.parent.name.text
138
+ : parent.parent.name.getText()
139
+ : "children";
140
+ return call ?? "<anonymous>";
141
+ }
142
+
143
+ static #outerOf(node: ts.Node) {
144
+ let current = node;
145
+ while (
146
+ ts.isParenthesizedExpression(current.parent) ||
147
+ ts.isAsExpression(current.parent) ||
148
+ ts.isSatisfiesExpression(current.parent) ||
149
+ ts.isNonNullExpression(current.parent) ||
150
+ ts.isTypeAssertionExpression(current.parent) ||
151
+ (ts.isPropertyAccessExpression(current.parent) && current.parent.expression === current) ||
152
+ (ts.isCallExpression(current.parent) && current.parent.expression === current)
153
+ )
154
+ current = current.parent;
155
+ return current;
156
+ }
157
+
158
+ static #textOf(name: ts.PropertyName | ts.BindingName) {
159
+ if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name) || ts.isStringLiteral(name)) return name.text;
160
+ if (ts.isNumericLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name)) return name.text;
161
+ if (ts.isComputedPropertyName(name)) return `[${name.expression.getText()}]`;
162
+ return "<pattern>";
163
+ }
164
+ }
@@ -5,6 +5,8 @@ import { parseNamedList } from "./moduleSyntax";
5
5
  export interface BarrelExportTarget {
6
6
  subpath: string;
7
7
  originalName: string;
8
+ /** `export * as name from`: an import of the name binds the whole module at `subpath`. */
9
+ namespace?: boolean;
8
10
  }
9
11
 
10
12
  export type BarrelExportMap = Map<string, BarrelExportTarget>;
@@ -100,9 +102,14 @@ export class BarrelAnalyzer {
100
102
  if (!isRelative(spec)) continue;
101
103
 
102
104
  if (star) {
103
- // A namespace re-export cannot be flattened into subpath imports, nor is it a local declaration.
104
- if (nsAs) authoritative.delete(nsAs);
105
- else starSpecs.push(spec);
105
+ if (!nsAs) starSpecs.push(spec);
106
+ else if (authoritative.has(nsAs)) {
107
+ attributed.add(nsAs);
108
+ const targetAbs = await this.#resolveRel(absFile, spec);
109
+ const targetSubpath = targetAbs ? this.#subpathFor(pkg, targetAbs) : null;
110
+ if (targetSubpath && !map.has(nsAs))
111
+ map.set(nsAs, { subpath: targetSubpath, originalName: nsAs, namespace: true });
112
+ }
106
113
  continue;
107
114
  }
108
115
 
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import type { BunPlugin } from "bun";
3
3
  import ts from "typescript";
4
+ import { StoreBootstrap } from "../artifact/storeBootstrap";
4
5
  import type { App } from "../commandDecorators";
5
6
  import { BarrelAnalyzer, type BarrelExportMap, type PackageEntry } from "./barrelAnalyzer";
6
7
  import { loaderFor, type NamedItem, parseNamedList } from "./moduleSyntax";
@@ -101,8 +102,9 @@ export const createTsconfigPackageResolver = async (
101
102
  if (!repl) continue;
102
103
  const replPath = repl.endsWith("/*") ? repl.slice(0, -1) : repl;
103
104
  const candidate = path.resolve(app.workspace.workspaceRoot, replPath + suffix);
104
- // A sibling file (`apps/minimal/client.ts`) is a facet barrel too: parent specifier, as above.
105
- for (const ext of CANDIDATE_EXTS) {
105
+ // A sibling file (`apps/minimal/client.ts`), or the file a specifier names with its extension
106
+ // (`akanjs/ui/System/index.tsx`), is a facet barrel too: parent specifier, as above.
107
+ for (const ext of CANDIDATE_EXTS.includes(path.extname(candidate)) ? ["", ...CANDIDATE_EXTS] : CANDIDATE_EXTS) {
106
108
  const file = `${candidate}${ext}`;
107
109
  if (await Bun.file(file).exists()) {
108
110
  const lastSlash = pkgName.lastIndexOf("/");
@@ -339,6 +341,7 @@ const rewriteSingleStatement = (stmt: ImportStatement, map: BarrelExportMap): st
339
341
 
340
342
  const remaining: NamedItem[] = [];
341
343
  const rewrites = new Map<string, NamedItem[]>();
344
+ const namespaceLines: string[] = [];
342
345
  for (const item of clause.named) {
343
346
  if (item.isType) {
344
347
  remaining.push(item);
@@ -349,15 +352,19 @@ const rewriteSingleStatement = (stmt: ImportStatement, map: BarrelExportMap): st
349
352
  remaining.push(item);
350
353
  continue;
351
354
  }
355
+ if (target.namespace) {
356
+ namespaceLines.push(`import * as ${item.local} from "${target.subpath}";`);
357
+ continue;
358
+ }
352
359
  const list = rewrites.get(target.subpath) ?? [];
353
360
  list.push({ imported: target.originalName, local: item.local, isType: false });
354
361
  rewrites.set(target.subpath, list);
355
362
  }
356
363
 
357
- if (rewrites.size === 0) return null;
364
+ if (rewrites.size === 0 && namespaceLines.length === 0) return null;
358
365
 
359
- const lines: string[] = [];
360
- if (shouldPreserveBarrelSideEffects(stmt.specifier)) lines.push(`import "${stmt.specifier}";`);
366
+ //? An app or lib client barrel's import-time side effects are its client runtime and store, not its module UI.
367
+ const lines: string[] = [...(StoreBootstrap.ofClientBarrel(stmt.specifier, map) ?? [])];
361
368
 
362
369
  if (clause.defaultImport || remaining.length > 0) {
363
370
  const parts: string[] = [];
@@ -369,12 +376,11 @@ const rewriteSingleStatement = (stmt: ImportStatement, map: BarrelExportMap): st
369
376
  for (const [subpath, items] of rewrites) {
370
377
  lines.push(`import { ${items.map(serializeNamedItem).join(", ")} } from "${subpath}";`);
371
378
  }
379
+ lines.push(...namespaceLines);
372
380
 
373
381
  return lines.join("\n");
374
382
  };
375
383
 
376
- const shouldPreserveBarrelSideEffects = (specifier: string): boolean => /^@(apps|libs)\/[^/]+\/client$/.test(specifier);
377
-
378
384
  const serializeNamedItem = (item: NamedItem): string => {
379
385
  const prefix = item.isType ? "type " : "";
380
386
  if (item.imported === item.local) return `${prefix}${item.imported}`;
@@ -86,7 +86,7 @@ describe("createServerEnvPlugin", () => {
86
86
  "env/env.server.develop.ts is not in this build: `akan build` bundles the server env of AKAN_PUBLIC_ENV=main alone",
87
87
  );
88
88
  expect(proc.stderr.toString()).toContain(
89
- "To run develop, build for it: AKAN_PUBLIC_ENV=develop in the workspace's .env, which outranks the shell, or --env develop on a desktop or mobile build.",
89
+ "To run develop, build for it with `--env develop` (`akan build` or a desktop or mobile build), or AKAN_PUBLIC_ENV=develop in the shell or the workspace's .env.",
90
90
  );
91
91
  });
92
92
 
@@ -54,7 +54,7 @@ export const createServerEnvPlugin = ({
54
54
  build.onLoad({ filter }, async (args) => {
55
55
  const name = path.basename(args.path, ".ts").slice("env.server.".length);
56
56
  const { exports } = new Bun.Transpiler({ loader: "ts" }).scan(await Bun.file(args.path).text());
57
- const message = `env/env.server.${name}.ts is not in this build: \`akan build\` bundles the server env of AKAN_PUBLIC_ENV=${environment} alone, the environment its image runs. To run ${name}, build for it: AKAN_PUBLIC_ENV=${name} in the workspace's .env, which outranks the shell, or --env ${name} on a desktop or mobile build.`;
57
+ const message = `env/env.server.${name}.ts is not in this build: \`akan build\` bundles the server env of AKAN_PUBLIC_ENV=${environment} alone, the environment its image runs. To run ${name}, build for it with \`--env ${name}\` (\`akan build\` or a desktop or mobile build), or AKAN_PUBLIC_ENV=${name} in the shell or the workspace's .env.`;
58
58
  return {
59
59
  loader: "js",
60
60
  contents: [
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { ApplicationBuildReporter } from "../applicationBuildReporter";
4
4
  import { tempDirs, writeText as write } from "../testHelpers";
5
5
  import { BarrelAnalyzer, type BarrelExportTarget } from "./barrelAnalyzer";
6
- import { rewriteBarrelImports } from "./barrelImportsPlugin";
6
+ import { createTsconfigPackageResolver, rewriteBarrelImports } from "./barrelImportsPlugin";
7
7
  import {
8
8
  hasUseClientDirective,
9
9
  scanUseClientExports,
@@ -140,7 +140,7 @@ describe('export * in a "use client" module', () => {
140
140
  });
141
141
 
142
142
  describe("BarrelAnalyzer and rewriteBarrelImports", () => {
143
- test("analyzes local, named, aliased, and star re-exports while skipping type and namespace exports", async () => {
143
+ test("analyzes local, named, aliased, star and namespace re-exports while skipping type exports", async () => {
144
144
  const root = await makeTempRoot();
145
145
  const pkgDir = path.join(root, "pkg");
146
146
  await write(
@@ -149,6 +149,7 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
149
149
  'export { A, B as Bee, type TypeOnly } from "./leaf";',
150
150
  'export * from "./star";',
151
151
  'export * as ns from "./namespace";',
152
+ 'export type * as typeNs from "./namespace";',
152
153
  "export const Local = 1;",
153
154
  "export type LocalType = string;",
154
155
  "",
@@ -171,7 +172,9 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
171
172
  expect(map?.get("Star")).toEqual({ subpath: "@scope/pkg/star", originalName: "Star" });
172
173
  expect(map?.get("Local")).toEqual({ subpath: "@scope/pkg", originalName: "Local" });
173
174
  expect(map?.has("TypeOnly")).toBe(false);
174
- expect(map?.has("ns")).toBe(false);
175
+ expect(map?.get("ns")).toEqual({ subpath: "@scope/pkg/namespace", originalName: "ns", namespace: true });
176
+ expect(map?.has("typeNs")).toBe(false);
177
+ expect(map?.has("Hidden")).toBe(false);
175
178
  });
176
179
 
177
180
  test("a name a module exports itself wins over the same name its star re-exports bring, as ES decides", async () => {
@@ -284,17 +287,66 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
284
287
  expect(await rewriteBarrelImports('import { A } from "@other/pkg";', ["@scope/pkg"], analyzer)).toBeNull();
285
288
  });
286
289
 
287
- test("preserves generated client barrel side effects when flattening app client imports", async () => {
288
- const analyzer = fakeAnalyzer([["st", { subpath: "@apps/demo/lib/st", originalName: "st" }]]);
290
+ test("keeps a client barrel's runtime and store, never its module UI, when flattening an app or lib import", async () => {
291
+ const root = await makeTempRoot();
292
+ for (const sys of ["apps/demo", "libs/kit"]) {
293
+ await write(
294
+ path.join(root, sys, "client.ts"),
295
+ [
296
+ 'export * as cnst from "./lib/cnst";',
297
+ 'export { msg, Err, usePage, fetch, sig } from "./lib/useClient";',
298
+ 'export { st, RootStore } from "./lib/st";',
299
+ 'export * as store from "./lib/st";',
300
+ 'export { Memo } from "./lib/memo";',
301
+ "",
302
+ ].join("\n"),
303
+ );
304
+ await write(path.join(root, sys, "lib/cnst.ts"), "export const Sample = 1;\n");
305
+ await write(path.join(root, sys, "lib/useClient.ts"), "export const { msg, Err, usePage, fetch, sig } = {};\n");
306
+ await write(path.join(root, sys, "lib/st.ts"), "export class RootStore {}\nexport const st = {};\n");
307
+ await write(path.join(root, sys, "lib/memo/index.ts"), "export const Memo = {};\n");
308
+ }
309
+ await write(path.join(root, "libs/hand/client.ts"), 'export { thing } from "./lib/thing";\n');
310
+ await write(path.join(root, "libs/hand/lib/thing.ts"), "export const thing = 1;\n");
311
+ const analyzer = new BarrelAnalyzer({
312
+ resolvePackage: await createTsconfigPackageResolver({
313
+ workspace: { workspaceRoot: root },
314
+ getTsConfig: async () => ({ compilerOptions: { paths: { "@apps/*": ["./apps/*"], "@libs/*": ["./libs/*"] } } }),
315
+ } as never),
316
+ });
317
+ const barrels = ["@apps/demo/client", "@libs/kit/client", "@libs/hand/client"];
318
+ const rewrite = async (source: string) => await rewriteBarrelImports(source, barrels, analyzer);
289
319
 
290
- const rewritten = await rewriteBarrelImports(
291
- 'import { st } from "@apps/demo/client";\nvoid st;\n',
292
- ["@apps/demo/client"],
293
- analyzer,
320
+ expect(await rewrite('import { Memo, st } from "@apps/demo/client";\n')).toBe(
321
+ [
322
+ 'import "@apps/demo/lib/useClient";',
323
+ 'import "@apps/demo/lib/st";',
324
+ 'import { Memo } from "@apps/demo/lib/memo";',
325
+ 'import { st } from "@apps/demo/lib/st";',
326
+ "",
327
+ ].join("\n"),
294
328
  );
295
-
296
- expect(rewritten).toBe(
297
- ['import "@apps/demo/client";', 'import { st } from "@apps/demo/lib/st";', "void st;\n"].join("\n"),
329
+ expect(await rewrite('import { cnst as c, st, store, type cnst as T } from "@apps/demo/client";\n')).toBe(
330
+ [
331
+ 'import "@apps/demo/lib/useClient";',
332
+ 'import "@apps/demo/lib/st";',
333
+ 'import { type cnst as T } from "@apps/demo/client";',
334
+ 'import { st } from "@apps/demo/lib/st";',
335
+ 'import * as c from "@apps/demo/lib/cnst";',
336
+ 'import * as store from "@apps/demo/lib/st";',
337
+ "",
338
+ ].join("\n"),
339
+ );
340
+ expect(await rewrite('import { RootStore as kit } from "@libs/kit/client";\n')).toBe(
341
+ [
342
+ 'import "@libs/kit/lib/useClient";',
343
+ 'import "@libs/kit/lib/st";',
344
+ 'import { RootStore as kit } from "@libs/kit/lib/st";',
345
+ "",
346
+ ].join("\n"),
347
+ );
348
+ expect(await rewrite('import { thing } from "@libs/hand/client";\n')).toBe(
349
+ ['import "@libs/hand/client";', 'import { thing } from "@libs/hand/lib/thing";', ""].join("\n"),
298
350
  );
299
351
  });
300
352
 
@@ -99,6 +99,18 @@ describe("createTsconfigPackageResolver — wildcard tsconfig mapping", () => {
99
99
  });
100
100
  });
101
101
 
102
+ test("resolves a specifier that names its file with an extension, as the generated root layout imports System", async () => {
103
+ const root = await makeTempRoot();
104
+ const entryFile = await write(root, "pkgs/akanjs/ui/System/index.tsx");
105
+ const resolve = await resolverFor(root, { "akanjs/ui/*": ["./pkgs/akanjs/ui/*"] });
106
+
107
+ expect(await resolve("akanjs/ui/System/index.tsx")).toEqual({
108
+ pkgName: "akanjs/ui/System",
109
+ entryFile,
110
+ pkgDir: path.join(root, "pkgs/akanjs/ui/System"),
111
+ });
112
+ });
113
+
102
114
  test("resolves a directory barrel and keeps the specifier", async () => {
103
115
  const root = await makeTempRoot();
104
116
  const entryFile = await write(root, "apps/minimal/index.tsx");