@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.
- package/CHANGELOG.md +353 -0
- package/abstractDoc.ts +1 -1
- package/agentAssets.ts +2 -2
- package/akanConfig/akanConfig.ts +2 -2
- package/akanMcpContract.ts +1 -1
- package/apiReference.test.ts +37 -0
- package/apiReference.ts +240 -0
- package/applicationBuildReporter.test.ts +25 -3
- package/applicationBuildReporter.ts +8 -1
- package/applicationBuildRunner.test.ts +34 -14
- package/applicationBuildRunner.ts +76 -14
- package/artifact/implicitRootLayout.test.ts +42 -0
- package/artifact/implicitRootLayout.ts +5 -6
- package/artifact/storeBootstrap.ts +24 -0
- package/cloud/cloudApi.test.ts +70 -7
- package/cloud/cloudApi.ts +10 -4
- package/cloud/constants.ts +3 -0
- package/cloud/globalConfig.test.ts +36 -0
- package/cloud/globalConfig.ts +10 -1
- package/codeAgent/skills/akan-debug/SKILL.md +2 -1
- package/codeAgent/skills/akan-page/SKILL.md +16 -7
- package/codeAgent/skills/akan-service/SKILL.md +48 -19
- package/commandDecorators/command.ts +55 -9
- package/commandDecorators/commandDecorators.test.ts +131 -2
- package/createTunnel.test.ts +12 -2
- package/declarationReader.ts +211 -0
- package/executors.test.ts +65 -8
- package/executors.ts +5 -3
- package/frameworkGraph.test.ts +121 -0
- package/frameworkGraph.ts +165 -0
- package/frontendBuild/allRoutesBuilder.test.ts +84 -0
- package/frontendBuild/allRoutesBuilder.ts +32 -12
- package/frontendBuild/buildRouteClient.test.ts +192 -2
- package/frontendBuild/bundleDefine.ts +4 -0
- package/frontendBuild/clientBuildTypes.ts +7 -1
- package/frontendBuild/clientEntriesBundler.test.ts +82 -0
- package/frontendBuild/clientEntriesBundler.ts +55 -4
- package/frontendBuild/csrArtifactBuilder.test.ts +23 -0
- package/frontendBuild/csrArtifactBuilder.ts +2 -1
- package/frontendBuild/fontOptimizer.test.ts +54 -0
- package/frontendBuild/fontOptimizer.ts +5 -4
- package/frontendBuild/index.ts +3 -0
- package/frontendBuild/routeClientBuilder.ts +6 -2
- package/frontendBuild/routeModulePreloads.test.ts +59 -0
- package/frontendBuild/routeModulePreloads.ts +52 -0
- package/frontendBuild/ssrBaseArtifactBuilder.ts +35 -10
- package/frontendBuild/ssrDevBundler.test.ts +26 -0
- package/frontendBuild/ssrDevBundler.ts +2 -1
- package/frontendBuild/vendorImportScanner.test.ts +71 -0
- package/frontendBuild/vendorImportScanner.ts +80 -0
- package/frontendBuild/vendorNarrowing.test.ts +103 -0
- package/frontendBuild/vendorNarrowing.ts +64 -0
- package/frontendBuild/vendorSpecifiers.ts +13 -0
- package/gitRevision.ts +6 -2
- package/incrementalBuilder/builderGraphPayloads.ts +60 -0
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +65 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +5 -36
- package/integration/devStability.integration.test.ts +1 -1
- package/integration/devStabilityHarness.test.ts +1 -1
- package/lint/no-deep-internal-import.grit +1 -1
- package/maintainabilityBaseline.ts +7 -2
- package/maintainabilityContract.test.ts +183 -0
- package/maintainabilityContract.ts +145 -30
- package/maintainabilityExceptionScanner.test.ts +13 -2
- package/maintainabilityExceptionScanner.ts +16 -6
- package/maintainabilityLimits.ts +12 -0
- package/maintainabilityReport.test.ts +35 -0
- package/maintainabilityReport.ts +29 -8
- package/mobile/nativePublicEnv.test.ts +5 -1
- package/package.json +3 -3
- package/preview/PreviewChrome.ts +3 -3
- package/preview/PreviewView.ts +26 -8
- package/preview/previewView.test.ts +87 -3
- package/routeJsBudget.test.ts +177 -0
- package/routeJsBudget.ts +222 -0
- package/structureScanner.test.ts +167 -0
- package/structureScanner.ts +164 -0
- package/transforms/barrelAnalyzer.ts +10 -3
- package/transforms/barrelImportsPlugin.ts +13 -7
- package/transforms/serverEnvPlugin.test.ts +1 -1
- package/transforms/serverEnvPlugin.ts +1 -1
- package/transforms/transforms.test.ts +64 -12
- package/transforms/tsconfigPackageResolver.test.ts +12 -0
- package/workflow/plan.ts +79 -17
- package/workflow/types.ts +1 -0
- package/workspaceGraph.test.ts +58 -0
- package/workspaceGraph.ts +32 -3
- package/workspaceLayout.ts +1 -0
- /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
|
-
|
|
104
|
-
if (
|
|
105
|
-
|
|
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`)
|
|
105
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
|
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
|
|
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
|
|
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?.
|
|
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("
|
|
288
|
-
const
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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");
|