@akanjs/devkit 3.2.0-rc.0 → 3.2.0-rc.10
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 +238 -11
- package/agentAssets.ts +1 -1
- package/akanApp/akanApp.host.test.ts +127 -0
- package/akanApp/akanApp.host.ts +50 -57
- package/akanApp/devHostPolicy.test.ts +11 -11
- package/akanApp/devHostPolicy.ts +2 -4
- package/akanContext.ts +2 -5
- package/akanMcpContract.ts +21 -10
- package/akanMcpToolError.ts +16 -0
- package/apiReport/apiDeclarationPrinter.ts +120 -0
- package/apiReport/apiReport.test.ts +497 -0
- package/apiReport/apiReport.ts +232 -0
- package/apiReport/apiReportDiff.ts +126 -0
- package/apiReport/apiReportPolicy.ts +65 -0
- package/apiReport/apiTargets.ts +311 -0
- package/apiReport/changesetBump.ts +19 -0
- package/applicationBuildReporter.test.ts +21 -0
- package/applicationBuildReporter.ts +8 -0
- package/applicationBuildRunner.test.ts +67 -0
- package/applicationBuildRunner.ts +6 -1
- package/artifact/implicitRootLayout.test.ts +4 -0
- package/artifact/implicitRootLayout.ts +13 -1
- package/artifact/storeBootstrap.ts +3 -2
- package/bareErrorScanner.ts +126 -0
- package/biome.base.json +2 -1
- package/codeAgent/skills/akan-debug/SKILL.md +7 -3
- package/codeAgent/skills/akan-err/SKILL.md +7 -0
- package/codeAgent/skills/akan-mcp/SKILL.md +7 -1
- package/codeAgent/skills/akan-scaffold/SKILL.md +3 -2
- package/codeAgent/skills/akan-transport/SKILL.md +2 -0
- package/codeAgent/skills/akan-validate/SKILL.md +1 -0
- package/codeAgent/tools/McpClient.ts +28 -6
- package/codeAgent/tools/McpToolPack.ts +1 -1
- package/codeAgent/tools/akanCodePlugins.test.ts +13 -1
- package/commandDecorators/argMeta.ts +4 -0
- package/commandDecorators/cliError.ts +72 -0
- package/commandDecorators/command.ts +186 -96
- package/commandDecorators/commandDecorators.test.ts +240 -17
- package/commandDecorators/exitCodeError.ts +6 -7
- package/commandDecorators/index.ts +1 -0
- package/commandDecorators/targetMeta.ts +3 -0
- package/conventionSet.test.ts +5 -2
- package/createTunnel.test.ts +12 -2
- package/csrE2e/csrE2eHarness.fixture.ts +1 -1
- package/errorCodeReference.test.ts +282 -0
- package/errorCodeReference.ts +71 -0
- package/errorCodeScan.ts +551 -0
- package/frontendBuild/allRoutesBuilder.test.ts +16 -4
- package/frontendBuild/allRoutesBuilder.ts +24 -6
- package/frontendBuild/buildRouteClient.test.ts +43 -0
- package/frontendBuild/bundleDefine.ts +2 -0
- package/frontendBuild/clientBuildTypes.ts +5 -2
- package/frontendBuild/clientEntryDiscovery.test.ts +80 -0
- package/frontendBuild/clientEntryDiscovery.ts +53 -9
- package/frontendBuild/csrArtifactBuilder.test.ts +48 -0
- package/frontendBuild/csrArtifactBuilder.ts +9 -3
- package/frontendBuild/csrDevBundler.ts +12 -2
- package/frontendBuild/csrDevModuleCompiler.test.ts +97 -0
- package/frontendBuild/csrDevModuleCompiler.ts +37 -8
- package/frontendBuild/csrDevPatcher.test.ts +89 -2
- package/frontendBuild/csrDevPatcher.ts +47 -7
- package/frontendBuild/csrDevTypes.ts +16 -0
- package/frontendBuild/cssCompiler.test.ts +63 -1
- package/frontendBuild/devBarrelImports.ts +132 -0
- package/frontendBuild/devRegistryBundler.ts +19 -4
- package/frontendBuild/mcpAppViewBuilder.test.ts +67 -0
- package/frontendBuild/nativeCore.test.ts +148 -0
- package/frontendBuild/precompressArtifacts.ts +1 -1
- package/frontendBuild/ssrBaseArtifactBuilder.test.ts +26 -0
- package/frontendBuild/ssrBaseArtifactBuilder.ts +23 -19
- package/frontendBuild/ssrDevBundler.ts +13 -2
- package/frontendBuild/vendorImportScanner.test.ts +24 -3
- package/frontendBuild/vendorImportScanner.ts +25 -10
- package/incrementalBuilder/incrementalBuilder.host.test.ts +30 -1
- package/incrementalBuilder/incrementalBuilder.host.ts +10 -22
- package/lint/__fixtures__/no-import-common-err/bad.tsx +13 -0
- package/lint/__fixtures__/no-import-common-err/good.tsx +12 -0
- package/lint/no-import-common-err.grit +31 -0
- package/lint/no-init-fetch-in-client.grit +1 -1
- package/lint/no-web-only-api-outside-webkit.grit +3 -3
- package/maintainabilityBaseline.test.ts +49 -0
- package/maintainabilityBaseline.ts +73 -8
- package/maintainabilityContract.test.ts +302 -1
- package/maintainabilityContract.ts +87 -22
- package/maintainabilityExceptionScanner.test.ts +14 -0
- package/maintainabilityExceptionScanner.ts +5 -1
- package/maintainabilityLimits.ts +12 -0
- package/maintainabilityReport.ts +20 -3
- package/moduleFieldNames.test.ts +56 -0
- package/moduleFieldNames.ts +25 -0
- package/package.json +3 -3
- package/preview/PreviewChrome.ts +3 -3
- package/preview/PreviewView.ts +48 -9
- package/preview/previewView.test.ts +154 -3
- package/routeJsBudget.test.ts +47 -11
- package/routeJsBudget.ts +88 -25
- package/transforms/barrelAnalyzer.ts +46 -1
- package/transforms/barrelImportsPlugin.ts +61 -45
- package/transforms/transforms.test.ts +67 -1
- package/typecheck/projectTypecheck.ts +5 -1
- package/typecheck/typecheck.proc.ts +3 -2
- package/typecheck/typecheckFailure.ts +6 -1
- package/workflow/artifacts.ts +25 -7
- package/workflow/fieldType.ts +92 -0
- package/workflow/index.ts +2 -0
- package/workflow/plainName.ts +28 -0
- package/workflow/plan.ts +67 -22
- package/workflow/source.ts +0 -5
- package/workspaceGraph.test.ts +200 -3
- package/workspaceGraph.ts +46 -8
|
@@ -4,7 +4,7 @@ import ts from "typescript";
|
|
|
4
4
|
import { StoreBootstrap } from "../artifact/storeBootstrap";
|
|
5
5
|
import type { App } from "../commandDecorators";
|
|
6
6
|
import { BarrelAnalyzer, type BarrelExportMap, type PackageEntry } from "./barrelAnalyzer";
|
|
7
|
-
import { loaderFor, type NamedItem
|
|
7
|
+
import { loaderFor, type NamedItem } from "./moduleSyntax";
|
|
8
8
|
|
|
9
9
|
export interface BarrelImportsPluginOptions {
|
|
10
10
|
/** Absolute paths whose content is returned unchanged (e.g. node_modules). */
|
|
@@ -234,10 +234,22 @@ const resolveFileCandidate = async (candidate: string): Promise<string | null> =
|
|
|
234
234
|
|
|
235
235
|
const MACRO_ATTR_RE = /with\s*\{\s*type\s*:\s*["']macro["']\s*\}/;
|
|
236
236
|
|
|
237
|
+
export interface RewriteBarrelImportsOptions {
|
|
238
|
+
/** Each replacement takes one line plus the newlines the statement spanned, so later lines keep their numbers. */
|
|
239
|
+
keepLines?: boolean;
|
|
240
|
+
specifierOf?: (subpath: string) => string;
|
|
241
|
+
/** Called for each import of a listed barrel with the value names whose rewrite reads its export map. */
|
|
242
|
+
onImport?: (barrel: string, names: string[]) => void;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export const workspaceBarrels = (barrels: readonly string[]): string[] =>
|
|
246
|
+
barrels.filter((barrel) => /^@(?:apps|libs)\//.test(barrel));
|
|
247
|
+
|
|
237
248
|
export const rewriteBarrelImports = async (
|
|
238
249
|
source: string,
|
|
239
250
|
barrels: string[],
|
|
240
251
|
analyzer: BarrelAnalyzer,
|
|
252
|
+
{ keepLines = false, specifierOf = (subpath: string) => subpath, onImport }: RewriteBarrelImportsOptions = {},
|
|
241
253
|
): Promise<string | null> => {
|
|
242
254
|
// Sound pre-filter before the costly parse: a static import cannot name a specifier absent from the text.
|
|
243
255
|
if (!barrels.some((barrel) => source.includes(barrel))) return null;
|
|
@@ -251,20 +263,32 @@ export const rewriteBarrelImports = async (
|
|
|
251
263
|
const stmt = statements[i];
|
|
252
264
|
if (!stmt) continue;
|
|
253
265
|
if (!barrels.includes(stmt.specifier)) continue;
|
|
266
|
+
const names = valueNamesOf(stmt.clause);
|
|
267
|
+
if (names.length > 0) onImport?.(stmt.specifier, names);
|
|
254
268
|
const map = await analyzer.analyze(stmt.specifier);
|
|
255
269
|
if (!map || map.size === 0) continue;
|
|
256
|
-
const
|
|
257
|
-
if (
|
|
270
|
+
const lines = rewriteSingleStatement(stmt, map, specifierOf);
|
|
271
|
+
if (lines === null) continue;
|
|
272
|
+
const replacement = keepLines
|
|
273
|
+
? `${lines.join(" ")}${"\n".repeat(source.slice(stmt.start, stmt.end).split("\n").length - 1)}`
|
|
274
|
+
: lines.join("\n");
|
|
258
275
|
out = out.slice(0, stmt.start) + replacement + out.slice(stmt.end);
|
|
259
276
|
changed = true;
|
|
260
277
|
}
|
|
261
278
|
return changed ? out : null;
|
|
262
279
|
};
|
|
263
280
|
|
|
281
|
+
interface ParsedClause {
|
|
282
|
+
defaultImport?: string;
|
|
283
|
+
namespaceImport?: string;
|
|
284
|
+
named?: NamedItem[];
|
|
285
|
+
typeOnly: boolean;
|
|
286
|
+
}
|
|
287
|
+
|
|
264
288
|
interface ImportStatement {
|
|
265
289
|
start: number;
|
|
266
290
|
end: number;
|
|
267
|
-
clause:
|
|
291
|
+
clause: ParsedClause | null;
|
|
268
292
|
specifier: string;
|
|
269
293
|
}
|
|
270
294
|
|
|
@@ -286,52 +310,42 @@ const findImportStatements = (source: string): ImportStatement[] => {
|
|
|
286
310
|
statements.push({
|
|
287
311
|
start: statement.getStart(sourceFile),
|
|
288
312
|
end: statement.end,
|
|
289
|
-
clause:
|
|
313
|
+
clause: clauseOf(importClause),
|
|
290
314
|
specifier: statement.moduleSpecifier.text,
|
|
291
315
|
});
|
|
292
316
|
}
|
|
293
317
|
return statements;
|
|
294
318
|
};
|
|
295
319
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const parsed: ParsedClause = { typeOnly };
|
|
311
|
-
const commaMatch = /^(\w+)\s*,\s*(.+)$/.exec(rest);
|
|
312
|
-
if (commaMatch) {
|
|
313
|
-
parsed.defaultImport = commaMatch[1];
|
|
314
|
-
rest = commaMatch[2] ?? "";
|
|
315
|
-
} else if (/^\w+$/.test(rest)) {
|
|
316
|
-
parsed.defaultImport = rest;
|
|
317
|
-
return parsed;
|
|
318
|
-
}
|
|
319
|
-
if (rest.startsWith("*")) {
|
|
320
|
-
const ns = /^\*\s+as\s+(\w+)$/.exec(rest);
|
|
321
|
-
if (!ns) return null;
|
|
322
|
-
parsed.namespaceImport = ns[1];
|
|
323
|
-
return parsed;
|
|
324
|
-
}
|
|
325
|
-
if (rest.startsWith("{")) {
|
|
326
|
-
const close = rest.indexOf("}");
|
|
327
|
-
if (close === -1) return null;
|
|
328
|
-
parsed.named = parseNamedList(rest.slice(1, close));
|
|
320
|
+
const clauseOf = (importClause: ts.ImportClause): ParsedClause | null => {
|
|
321
|
+
const parsed: ParsedClause = { typeOnly: importClause.phaseModifier === ts.SyntaxKind.TypeKeyword };
|
|
322
|
+
if (importClause.name) parsed.defaultImport = importClause.name.text;
|
|
323
|
+
const bindings = importClause.namedBindings;
|
|
324
|
+
if (!bindings) return parsed;
|
|
325
|
+
if (ts.isNamespaceImport(bindings)) return { ...parsed, namespaceImport: bindings.name.text };
|
|
326
|
+
const named: NamedItem[] = [];
|
|
327
|
+
for (const element of bindings.elements) {
|
|
328
|
+
if (element.propertyName && !ts.isIdentifier(element.propertyName)) return null;
|
|
329
|
+
named.push({
|
|
330
|
+
imported: (element.propertyName ?? element.name).text,
|
|
331
|
+
local: element.name.text,
|
|
332
|
+
isType: element.isTypeOnly,
|
|
333
|
+
});
|
|
329
334
|
}
|
|
330
|
-
return parsed;
|
|
335
|
+
return { ...parsed, named };
|
|
331
336
|
};
|
|
332
337
|
|
|
333
|
-
const
|
|
334
|
-
|
|
338
|
+
const valueNamesOf = (clause: ParsedClause | null): string[] => {
|
|
339
|
+
if (!clause || clause.namespaceImport || clause.typeOnly) return [];
|
|
340
|
+
return (clause.named ?? []).filter((item) => !item.isType).map((item) => item.imported);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const rewriteSingleStatement = (
|
|
344
|
+
stmt: ImportStatement,
|
|
345
|
+
map: BarrelExportMap,
|
|
346
|
+
specifierOf: (subpath: string) => string,
|
|
347
|
+
): string[] | null => {
|
|
348
|
+
const { clause } = stmt;
|
|
335
349
|
if (!clause) return null;
|
|
336
350
|
// Namespace imports need the whole barrel — cannot rewrite safely.
|
|
337
351
|
if (clause.namespaceImport) return null;
|
|
@@ -353,7 +367,7 @@ const rewriteSingleStatement = (stmt: ImportStatement, map: BarrelExportMap): st
|
|
|
353
367
|
continue;
|
|
354
368
|
}
|
|
355
369
|
if (target.namespace) {
|
|
356
|
-
namespaceLines.push(`import * as ${item.local} from
|
|
370
|
+
namespaceLines.push(`import * as ${item.local} from ${JSON.stringify(specifierOf(target.subpath))};`);
|
|
357
371
|
continue;
|
|
358
372
|
}
|
|
359
373
|
const list = rewrites.get(target.subpath) ?? [];
|
|
@@ -364,7 +378,9 @@ const rewriteSingleStatement = (stmt: ImportStatement, map: BarrelExportMap): st
|
|
|
364
378
|
if (rewrites.size === 0 && namespaceLines.length === 0) return null;
|
|
365
379
|
|
|
366
380
|
//? An app or lib client barrel's import-time side effects are its client runtime and store, not its module UI.
|
|
367
|
-
const lines
|
|
381
|
+
const lines = (StoreBootstrap.ofClientBarrel(stmt.specifier, map) ?? []).map(
|
|
382
|
+
(specifier) => `import ${JSON.stringify(specifierOf(specifier))};`,
|
|
383
|
+
);
|
|
368
384
|
|
|
369
385
|
if (clause.defaultImport || remaining.length > 0) {
|
|
370
386
|
const parts: string[] = [];
|
|
@@ -374,11 +390,11 @@ const rewriteSingleStatement = (stmt: ImportStatement, map: BarrelExportMap): st
|
|
|
374
390
|
}
|
|
375
391
|
|
|
376
392
|
for (const [subpath, items] of rewrites) {
|
|
377
|
-
lines.push(`import { ${items.map(serializeNamedItem).join(", ")} } from
|
|
393
|
+
lines.push(`import { ${items.map(serializeNamedItem).join(", ")} } from ${JSON.stringify(specifierOf(subpath))};`);
|
|
378
394
|
}
|
|
379
395
|
lines.push(...namespaceLines);
|
|
380
396
|
|
|
381
|
-
return lines
|
|
397
|
+
return lines;
|
|
382
398
|
};
|
|
383
399
|
|
|
384
400
|
const serializeNamedItem = (item: NamedItem): string => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { ApplicationBuildReporter } from "../applicationBuildReporter";
|
|
4
5
|
import { tempDirs, writeText as write } from "../testHelpers";
|
|
@@ -262,6 +263,27 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
262
263
|
expect(await rewriteBarrelImports(source, ["@libs/util/common"], analyzer)).toBeNull();
|
|
263
264
|
});
|
|
264
265
|
|
|
266
|
+
test("forgets an analysis when a file beside one it read changes, under the link or the real path", async () => {
|
|
267
|
+
const root = await makeTempRoot();
|
|
268
|
+
await write(path.join(root, "real/ui/index.ts"), 'export * from "./Foo";\n');
|
|
269
|
+
await write(path.join(root, "real/ui/Foo.ts"), "export const Foo = 1;\n");
|
|
270
|
+
await write(path.join(root, "real/other/Bar.ts"), "export const Bar = 1;\n");
|
|
271
|
+
fs.symlinkSync(path.join(root, "real"), path.join(root, "linked"));
|
|
272
|
+
const pkgDir = path.join(root, "linked/ui");
|
|
273
|
+
const analyzer = new BarrelAnalyzer({
|
|
274
|
+
resolvePackage: async () => ({ pkgName: "@scope/ui", entryFile: path.join(pkgDir, "index.ts"), pkgDir }),
|
|
275
|
+
});
|
|
276
|
+
const analyzed = async () => {
|
|
277
|
+
await analyzer.analyze("@scope/ui");
|
|
278
|
+
return analyzer;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
expect([...(await analyzed()).forget([path.join(root, "real/other/Bar.ts")]).keys()]).toEqual([]);
|
|
282
|
+
expect([...(await analyzed()).forget([path.join(root, "real/ui/Foo.ts")]).keys()]).toEqual(["@scope/ui"]);
|
|
283
|
+
expect([...(await analyzed()).forget([path.join(pkgDir, "Created.ts")]).keys()]).toEqual(["@scope/ui"]);
|
|
284
|
+
expect([...analyzer.forget([path.join(pkgDir, "Foo.ts")]).keys()]).toEqual([]);
|
|
285
|
+
});
|
|
286
|
+
|
|
265
287
|
test("rewrites flattenable named imports and preserves default, type, and unknown imports", async () => {
|
|
266
288
|
const analyzer = fakeAnalyzer([
|
|
267
289
|
["A", { subpath: "@scope/pkg/leaf", originalName: "A" }],
|
|
@@ -287,6 +309,27 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
287
309
|
expect(await rewriteBarrelImports('import { A } from "@other/pkg";', ["@scope/pkg"], analyzer)).toBeNull();
|
|
288
310
|
});
|
|
289
311
|
|
|
312
|
+
test("reads every name of an import whose braces hold comments, aliases and type-only names", async () => {
|
|
313
|
+
const analyzer = fakeAnalyzer([
|
|
314
|
+
["A", { subpath: "@scope/pkg/leaf", originalName: "A" }],
|
|
315
|
+
["st", { subpath: "@scope/pkg/store", originalName: "st" }],
|
|
316
|
+
]);
|
|
317
|
+
const rewrite = async (source: string) => await rewriteBarrelImports(source, ["@scope/pkg"], analyzer);
|
|
318
|
+
const narrowed = 'import { A } from "@scope/pkg/leaf";\nimport { st } from "@scope/pkg/store";\n';
|
|
319
|
+
|
|
320
|
+
expect(await rewrite('import {\n A, // the leaf\n st,\n} from "@scope/pkg";\n')).toBe(narrowed);
|
|
321
|
+
expect(await rewrite('import { A, /* the store */ st } from "@scope/pkg";\n')).toBe(narrowed);
|
|
322
|
+
expect(await rewrite('import { type Shape, A as Leaf, st as store } from "@scope/pkg";\n')).toBe(
|
|
323
|
+
[
|
|
324
|
+
'import { type Shape } from "@scope/pkg";',
|
|
325
|
+
'import { A as Leaf } from "@scope/pkg/leaf";',
|
|
326
|
+
'import { st as store } from "@scope/pkg/store";',
|
|
327
|
+
"",
|
|
328
|
+
].join("\n"),
|
|
329
|
+
);
|
|
330
|
+
expect(await rewrite('import { "A" as quoted } from "@scope/pkg";\n')).toBeNull();
|
|
331
|
+
});
|
|
332
|
+
|
|
290
333
|
test("keeps a client barrel's runtime and store, never its module UI, when flattening an app or lib import", async () => {
|
|
291
334
|
const root = await makeTempRoot();
|
|
292
335
|
for (const sys of ["apps/demo", "libs/kit"]) {
|
|
@@ -308,13 +351,15 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
308
351
|
}
|
|
309
352
|
await write(path.join(root, "libs/hand/client.ts"), 'export { thing } from "./lib/thing";\n');
|
|
310
353
|
await write(path.join(root, "libs/hand/lib/thing.ts"), "export const thing = 1;\n");
|
|
354
|
+
await write(path.join(root, "libs/half/client.ts"), 'export { st } from "./lib/st";\n');
|
|
355
|
+
await write(path.join(root, "libs/half/lib/st.ts"), "export const st = {};\n");
|
|
311
356
|
const analyzer = new BarrelAnalyzer({
|
|
312
357
|
resolvePackage: await createTsconfigPackageResolver({
|
|
313
358
|
workspace: { workspaceRoot: root },
|
|
314
359
|
getTsConfig: async () => ({ compilerOptions: { paths: { "@apps/*": ["./apps/*"], "@libs/*": ["./libs/*"] } } }),
|
|
315
360
|
} as never),
|
|
316
361
|
});
|
|
317
|
-
const barrels = ["@apps/demo/client", "@libs/kit/client", "@libs/hand/client"];
|
|
362
|
+
const barrels = ["@apps/demo/client", "@libs/kit/client", "@libs/hand/client", "@libs/half/client"];
|
|
318
363
|
const rewrite = async (source: string) => await rewriteBarrelImports(source, barrels, analyzer);
|
|
319
364
|
|
|
320
365
|
expect(await rewrite('import { Memo, st } from "@apps/demo/client";\n')).toBe(
|
|
@@ -348,6 +393,27 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
|
|
|
348
393
|
expect(await rewrite('import { thing } from "@libs/hand/client";\n')).toBe(
|
|
349
394
|
['import "@libs/hand/client";', 'import { thing } from "@libs/hand/lib/thing";', ""].join("\n"),
|
|
350
395
|
);
|
|
396
|
+
expect(await rewrite('import { st } from "@libs/half/client";\n')).toBe(
|
|
397
|
+
['import "@libs/half/client";', 'import { st } from "@libs/half/lib/st";', ""].join("\n"),
|
|
398
|
+
);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("keeps each statement's line count and writes the specifier specifierOf gives a target", async () => {
|
|
402
|
+
const analyzer = fakeAnalyzer([
|
|
403
|
+
["A", { subpath: "@scope/pkg/leaf", originalName: "A" }],
|
|
404
|
+
["ns", { subpath: "@scope/pkg/namespace", originalName: "ns", namespace: true }],
|
|
405
|
+
]);
|
|
406
|
+
const source = 'import {\n A,\n ns,\n} from "@scope/pkg";\nconsole.log(A, ns);\n';
|
|
407
|
+
|
|
408
|
+
const rewritten = await rewriteBarrelImports(source, ["@scope/pkg"], analyzer, {
|
|
409
|
+
keepLines: true,
|
|
410
|
+
specifierOf: (subpath) => `./${subpath.slice("@scope/pkg/".length)}.ts`,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
expect(rewritten).toBe(
|
|
414
|
+
'import { A } from "./leaf.ts"; import * as ns from "./namespace.ts";\n\n\n\nconsole.log(A, ns);\n',
|
|
415
|
+
);
|
|
416
|
+
expect(rewritten?.split("\n")).toHaveLength(source.split("\n").length);
|
|
351
417
|
});
|
|
352
418
|
|
|
353
419
|
test("does not rewrite import-looking code inside template literals", async () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, rm } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { CliError } from "../commandDecorators/cliError";
|
|
3
4
|
import type { TypecheckRecord } from "../typeChecker";
|
|
4
5
|
import { TypecheckFailure } from "./typecheckFailure";
|
|
5
6
|
|
|
@@ -69,7 +70,10 @@ export class ProjectTypecheck {
|
|
|
69
70
|
if (exitCode === 0) return;
|
|
70
71
|
const records = report === "records" ? ProjectTypecheck.#parseRecords(stdout) : null;
|
|
71
72
|
if (records?.length) throw new TypecheckFailure(records);
|
|
72
|
-
|
|
73
|
+
const output = (stderr || stdout).trim();
|
|
74
|
+
if (exitCode === TypecheckFailure.workerExitCode && report === "pretty")
|
|
75
|
+
throw new CliError("checkFailed", output || "Typecheck failed");
|
|
76
|
+
throw new Error(output || `Typecheck failed with exit code ${exitCode}`);
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
static #parseRecords(stdout: string): TypecheckRecord[] | null {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Logger } from "akanjs/common";
|
|
2
2
|
import { TypeChecker } from "../typeChecker";
|
|
3
|
+
import { TypecheckFailure } from "./typecheckFailure";
|
|
3
4
|
|
|
4
5
|
try {
|
|
5
6
|
const filePath = process.env.AKAN_TYPECHECK_FILE;
|
|
@@ -28,11 +29,11 @@ try {
|
|
|
28
29
|
//? Not process.cwd(): that is the realpath (/private/var on macOS) while TypeScript keeps the spelling it was given.
|
|
29
30
|
const root = process.env.AKAN_TYPECHECK_ROOT ?? process.cwd();
|
|
30
31
|
process.stdout.write(JSON.stringify(TypeChecker.toRecords(result.errors, root)));
|
|
31
|
-
process.exit(result.errors.length > 0 ?
|
|
32
|
+
process.exit(result.errors.length > 0 ? TypecheckFailure.workerExitCode : 0);
|
|
32
33
|
}
|
|
33
34
|
if (result.errors.length > 0) {
|
|
34
35
|
console.error(result.message);
|
|
35
|
-
process.exit(
|
|
36
|
+
process.exit(TypecheckFailure.workerExitCode);
|
|
36
37
|
}
|
|
37
38
|
} catch (error) {
|
|
38
39
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { CliError } from "../commandDecorators/cliError";
|
|
1
2
|
import type { TypecheckRecord } from "../typeChecker";
|
|
2
3
|
|
|
3
|
-
export class TypecheckFailure extends
|
|
4
|
+
export class TypecheckFailure extends CliError<"checkFailed"> {
|
|
5
|
+
//? The worker's exit when the project has type errors, so its crash, which exits 1, is told apart.
|
|
6
|
+
static readonly workerExitCode = 3;
|
|
7
|
+
|
|
4
8
|
constructor(readonly records: TypecheckRecord[]) {
|
|
5
9
|
super(
|
|
10
|
+
"checkFailed",
|
|
6
11
|
records
|
|
7
12
|
.slice(0, 20)
|
|
8
13
|
.map((record) => `${record.file ?? "-"}:${record.line}:${record.col} ${record.code} ${record.message}`)
|
package/workflow/artifacts.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
WorkflowNextActionCode,
|
|
17
17
|
WorkflowOverallStatus,
|
|
18
18
|
WorkflowPlan,
|
|
19
|
+
WorkflowRecommendation,
|
|
19
20
|
WorkflowRunArtifact,
|
|
20
21
|
WorkflowRunSource,
|
|
21
22
|
WorkflowValidationCommandResult,
|
|
@@ -94,11 +95,11 @@ export const createWorkflowApplyReport = ({
|
|
|
94
95
|
postApplyChecks?: WorkflowApplyReport["postApplyChecks"];
|
|
95
96
|
recommendations?: WorkflowApplyReport["recommendations"];
|
|
96
97
|
}): WorkflowApplyReport => {
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
action: inferNextActionCode(action, diagnostics)
|
|
101
|
-
|
|
98
|
+
const status = workflowStatus(diagnostics);
|
|
99
|
+
const validationCommands = status === "failed" ? [] : (recommendedValidationCommands ?? commands);
|
|
100
|
+
const nextActionsWithIntent = nextActions
|
|
101
|
+
.map((action) => ({ ...action, action: inferNextActionCode(action, diagnostics) }))
|
|
102
|
+
.filter((action) => status !== "failed" || action.action !== "validate");
|
|
102
103
|
if (sourceChangeBlocked(diagnostics) && !nextActionsWithIntent.some((action) => action.action === "blocked")) {
|
|
103
104
|
nextActionsWithIntent.unshift({
|
|
104
105
|
command: `akan workflow explain ${workflow}`,
|
|
@@ -110,12 +111,13 @@ export const createWorkflowApplyReport = ({
|
|
|
110
111
|
(left, right) => nextActionPriority(left, diagnostics) - nextActionPriority(right, diagnostics),
|
|
111
112
|
);
|
|
112
113
|
const sourceFilesChanged = uniqueBy(changedFiles, (file) => `${file.action}:${file.path}:${file.reason}`);
|
|
113
|
-
const generatedFilesSynced =
|
|
114
|
+
const generatedFilesSynced =
|
|
115
|
+
status === "failed" ? [] : uniqueBy(generatedFiles, (file) => `${file.action}:${file.path}:${file.reason}`);
|
|
114
116
|
return {
|
|
115
117
|
schemaVersion: 1,
|
|
116
118
|
workflow,
|
|
117
119
|
mode,
|
|
118
|
-
status
|
|
120
|
+
status,
|
|
119
121
|
summary: {
|
|
120
122
|
sourceFilesChanged,
|
|
121
123
|
generatedFilesSynced,
|
|
@@ -133,6 +135,11 @@ export const createWorkflowApplyReport = ({
|
|
|
133
135
|
};
|
|
134
136
|
};
|
|
135
137
|
|
|
138
|
+
export const withoutApplyHints = (recommendations: readonly WorkflowRecommendation[]) =>
|
|
139
|
+
recommendations.filter(
|
|
140
|
+
(recommendation) => recommendation.kind !== "auto-apply" && recommendation.kind !== "validation",
|
|
141
|
+
);
|
|
142
|
+
|
|
136
143
|
export const resolveWorkflowCommand = (command: string, plan: WorkflowPlan) => {
|
|
137
144
|
const target = typeof plan.inputs.app === "string" ? plan.inputs.app : "<app-or-lib>";
|
|
138
145
|
return command
|
|
@@ -526,6 +533,17 @@ export const createWorkflowValidationRunReport = async ({
|
|
|
526
533
|
};
|
|
527
534
|
|
|
528
535
|
export const createDryRunWorkflowApplyReport = (plan: WorkflowPlan) => {
|
|
536
|
+
if (workflowStatus(plan.diagnostics) === "failed")
|
|
537
|
+
return createWorkflowApplyReport({
|
|
538
|
+
workflow: plan.workflow,
|
|
539
|
+
mode: "dry-run",
|
|
540
|
+
changedFiles: [],
|
|
541
|
+
generatedFiles: [],
|
|
542
|
+
diagnostics: [...plan.diagnostics],
|
|
543
|
+
recommendations: withoutApplyHints(plan.recommendations),
|
|
544
|
+
nextActions: [],
|
|
545
|
+
plan,
|
|
546
|
+
});
|
|
529
547
|
const changedFiles: PrimitiveChangedFile[] = plan.predictedChanges.flatMap((change) => {
|
|
530
548
|
if (change.action !== "create" && change.action !== "modify") return [];
|
|
531
549
|
return [
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import type { Sys } from "../commandDecorators";
|
|
3
|
+
import { ensureBaseImport, ensureNamedImport, getSysRoot, moduleComponentName, moduleSourcePaths } from "./source";
|
|
4
|
+
import { sourceFileFor } from "./sourceAst";
|
|
5
|
+
import type { WorkflowDiagnostic } from "./types";
|
|
6
|
+
|
|
7
|
+
const builtInFieldTypes = new Set(["String", "Boolean", "Date", "Map"]);
|
|
8
|
+
const baseFieldTypes = new Set(["Int", "Float", "ID", "Any", "Binary", "ImageResult"]);
|
|
9
|
+
const fieldTypeReference = /^\[*([A-Za-z_$][\w$]*)((?:\.[A-Za-z_$][\w$]*)*)\]*$/;
|
|
10
|
+
|
|
11
|
+
export const ensureBaseTypeImport = (content: string, typeName: string) => {
|
|
12
|
+
const [, name = "", member] = fieldTypeReference.exec(typeName) ?? [];
|
|
13
|
+
if (member || !baseFieldTypes.has(name)) return content;
|
|
14
|
+
return ensureBaseImport(content, name);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const valueNamesOf = (content: string) => {
|
|
18
|
+
const names = new Set<string>();
|
|
19
|
+
for (const statement of sourceFileFor("source.ts", content).statements) {
|
|
20
|
+
if (ts.isImportDeclaration(statement)) {
|
|
21
|
+
const clause = statement.importClause;
|
|
22
|
+
if (!clause || clause.isTypeOnly) continue;
|
|
23
|
+
if (clause.name) names.add(clause.name.text);
|
|
24
|
+
const bindings = clause.namedBindings;
|
|
25
|
+
if (bindings && ts.isNamespaceImport(bindings)) names.add(bindings.name.text);
|
|
26
|
+
else for (const element of bindings?.elements ?? []) if (!element.isTypeOnly) names.add(element.name.text);
|
|
27
|
+
} else if (ts.isVariableStatement(statement)) {
|
|
28
|
+
for (const declaration of statement.declarationList.declarations)
|
|
29
|
+
if (ts.isIdentifier(declaration.name)) names.add(declaration.name.text);
|
|
30
|
+
} else if (
|
|
31
|
+
(ts.isClassDeclaration(statement) || ts.isEnumDeclaration(statement) || ts.isFunctionDeclaration(statement)) &&
|
|
32
|
+
statement.name
|
|
33
|
+
)
|
|
34
|
+
names.add(statement.name.text);
|
|
35
|
+
}
|
|
36
|
+
return names;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const fieldTypeSource = async (
|
|
40
|
+
sys: Sys,
|
|
41
|
+
module: string,
|
|
42
|
+
typeName: string,
|
|
43
|
+
constantContent: string,
|
|
44
|
+
): Promise<{ import?: { name: string; from: string }; diagnostic?: WorkflowDiagnostic }> => {
|
|
45
|
+
const reference = fieldTypeReference.exec(typeName);
|
|
46
|
+
if (!reference)
|
|
47
|
+
return {
|
|
48
|
+
diagnostic: {
|
|
49
|
+
severity: "error",
|
|
50
|
+
code: "primitive-field-type-unknown",
|
|
51
|
+
input: "type",
|
|
52
|
+
message: `Field type "${typeName}" is not a type name field() takes. Name one type, such as Address, or a list of one, such as [Address], then plan this again.`,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
const [, name = "", member] = reference;
|
|
56
|
+
if (builtInFieldTypes.has(name) || baseFieldTypes.has(name) || valueNamesOf(constantContent).has(name)) return {};
|
|
57
|
+
const [scalars, models] = await Promise.all([sys.getScalarModules(), sys.getDatabaseModules()]);
|
|
58
|
+
const scalar = member ? undefined : scalars.find((scalarName) => moduleComponentName(scalarName) === name);
|
|
59
|
+
if (scalar) return { import: { name, from: `../__scalar/${scalar}/${scalar}.constant` } };
|
|
60
|
+
const model = member ? undefined : models.find((modelName) => moduleComponentName(modelName) === name);
|
|
61
|
+
if (model) return { import: { name, from: `../${model}/${model}.constant` } };
|
|
62
|
+
const root = getSysRoot(sys);
|
|
63
|
+
const constantPath = `${root}/${moduleSourcePaths(module).constant}`;
|
|
64
|
+
const named = (kind: string, names: string[]) =>
|
|
65
|
+
names.length ? `Its ${kind} are ${names.map(moduleComponentName).join(", ")}.` : `It has no ${kind}.`;
|
|
66
|
+
return {
|
|
67
|
+
diagnostic: {
|
|
68
|
+
severity: "error",
|
|
69
|
+
code: "primitive-field-type-unknown",
|
|
70
|
+
input: "type",
|
|
71
|
+
message: [
|
|
72
|
+
`Field type "${typeName}" names ${name}, which is not a built-in or akanjs/base type, not imported by ${constantPath}, and not a scalar or model in ${root}.`,
|
|
73
|
+
named("scalars", scalars),
|
|
74
|
+
named("models", models),
|
|
75
|
+
`Create the scalar with the create-scalar workflow first, or import ${name} into ${constantPath}, then plan this again.`,
|
|
76
|
+
].join(" "),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const withFieldTypeImport = async (
|
|
82
|
+
sys: Sys,
|
|
83
|
+
module: string,
|
|
84
|
+
typeName: string,
|
|
85
|
+
content: string,
|
|
86
|
+
diagnostics: WorkflowDiagnostic[],
|
|
87
|
+
) => {
|
|
88
|
+
const withBaseImport = ensureBaseTypeImport(content, typeName);
|
|
89
|
+
const source = await fieldTypeSource(sys, module, typeName, withBaseImport);
|
|
90
|
+
if (source.diagnostic) diagnostics.push(source.diagnostic);
|
|
91
|
+
return source.import ? ensureNamedImport(withBaseImport, source.import.from, source.import.name) : withBaseImport;
|
|
92
|
+
};
|
package/workflow/index.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export * from "./artifacts";
|
|
2
2
|
export * from "./executor";
|
|
3
|
+
export * from "./fieldType";
|
|
3
4
|
export * from "./guardCatalog";
|
|
4
5
|
export * from "./interactionScaffold";
|
|
5
6
|
export * from "./keyRename";
|
|
6
7
|
export * from "./moduleIndex";
|
|
7
8
|
export * from "./mutationScaffold";
|
|
8
9
|
export * from "./mutationSource";
|
|
10
|
+
export * from "./plainName";
|
|
9
11
|
export * from "./plan";
|
|
10
12
|
export * from "./render";
|
|
11
13
|
export * from "./rolloutGate";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const sysNamePattern = /^[a-z0-9][a-zA-Z0-9_-]*$/;
|
|
2
|
+
export const identifierPattern = /^_{0,2}[a-zA-Z][a-zA-Z0-9_-]*$/;
|
|
3
|
+
const packageNamePattern = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
4
|
+
|
|
5
|
+
export type PlainNameKind = "app" | "lib" | "module" | "service" | "scalar";
|
|
6
|
+
|
|
7
|
+
export const isWorkflowSysName = (value: unknown): value is string =>
|
|
8
|
+
typeof value === "string" && sysNamePattern.test(value);
|
|
9
|
+
export const isWorkflowModuleName = (value: unknown): value is string =>
|
|
10
|
+
typeof value === "string" && identifierPattern.test(value);
|
|
11
|
+
|
|
12
|
+
export const plainNameProblem = (kind: PlainNameKind, name: string) => {
|
|
13
|
+
const isSys = kind === "app" || kind === "lib";
|
|
14
|
+
if (isSys ? isWorkflowSysName(name) : isWorkflowModuleName(name)) return null;
|
|
15
|
+
return `${JSON.stringify(name)} is no ${kind} name; use letters, digits, - and _, starting with ${isSys ? "a lowercase letter or digit" : "a letter"}`;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const assertPlainName = (kind: PlainNameKind, name: string) => {
|
|
19
|
+
const problem = plainNameProblem(kind, name);
|
|
20
|
+
if (problem) throw new Error(problem);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const assertPackageName = (name: string) => {
|
|
24
|
+
if (packageNamePattern.test(name)) return;
|
|
25
|
+
throw new Error(
|
|
26
|
+
`${JSON.stringify(name)} is no package name; use name or @scope/name, each of lowercase letters, digits, -, . and _, starting with a letter or digit`,
|
|
27
|
+
);
|
|
28
|
+
};
|