@akanjs/devkit 3.1.1-rc.9 → 3.1.2-rc.0
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 +117 -0
- package/agentAssets.test.ts +65 -0
- package/agentAssets.ts +166 -0
- package/agentsIndex.ts +4 -4
- package/akanApp/akanApp.host.test.ts +12 -0
- package/akanApp/akanApp.host.ts +12 -0
- package/akanConfig/akanConfig.test.ts +105 -0
- package/akanConfig/akanConfig.ts +21 -1
- package/akanConfig/productionLockfile.test.ts +31 -0
- package/akanConfig/productionLockfile.ts +2 -0
- package/akanContext.ts +129 -4
- package/akanMcpContract.test.ts +28 -1
- package/akanMcpContract.ts +51 -3
- package/applicationBuildRunner.test.ts +53 -0
- package/applicationBuildRunner.ts +7 -4
- package/artifact/implicitRootLayout.ts +4 -2
- package/biome.base.json +4 -1
- package/cloud/globalConfig.test.ts +34 -0
- package/cloud/globalConfig.ts +2 -2
- package/codeAgent/agent/AkanCodeEnv.ts +26 -0
- package/codeAgent/agent/AkanCodeServices.ts +5 -2
- package/codeAgent/agent/CodeAgent.ts +11 -0
- package/codeAgent/agent/CodeAgentGate.ts +156 -10
- package/codeAgent/agent/CodeAgentProxy.ts +5 -4
- package/codeAgent/agent/CodeAgentRpcHost.ts +3 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +102 -16
- package/codeAgent/agent/SubagentPool.ts +5 -0
- package/codeAgent/agent/WorkspaceTrust.ts +200 -0
- package/codeAgent/agent/akanCodePaths.ts +8 -1
- package/codeAgent/agent/akanSystemPrompt.ts +5 -1
- package/codeAgent/agent/codeAgent.test.ts +41 -2
- package/codeAgent/agent/codeAgentContract.test.ts +119 -0
- package/codeAgent/agent/codeAgentHosts.ts +30 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +53 -1
- package/codeAgent/agent/codeAgentRpcListener.test.ts +115 -13
- package/codeAgent/agent/stringArg.ts +1 -1
- package/codeAgent/feedback/PreviewFeedback.ts +2 -2
- package/codeAgent/feedback/VerifyFeedback.ts +3 -4
- package/codeAgent/index.ts +2 -2
- package/codeAgent/skills/akan-agent/SKILL.md +43 -0
- package/codeAgent/skills/akan-code/SKILL.md +38 -11
- package/codeAgent/skills/akan-debug/SKILL.md +41 -6
- package/codeAgent/skills/akan-deps/SKILL.md +1 -2
- package/codeAgent/skills/akan-err/SKILL.md +2 -1
- package/codeAgent/skills/akan-mcp/SKILL.md +40 -0
- package/codeAgent/skills/akan-page/SKILL.md +44 -0
- package/codeAgent/skills/akan-query/SKILL.md +39 -0
- package/codeAgent/skills/akan-runtime/SKILL.md +43 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +16 -6
- package/codeAgent/skills/akan-transport/SKILL.md +40 -0
- package/codeAgent/skills/akan-validate/SKILL.md +13 -11
- package/codeAgent/tools/AkanCli.ts +2 -1
- package/codeAgent/tools/AkanCodePlugins.ts +5 -1
- package/codeAgent/tools/AkanEditScope.ts +8 -3
- package/codeAgent/tools/AkanToolPack.ts +4 -10
- package/codeAgent/tools/AkanVerifier.ts +13 -31
- package/codeAgent/tools/McpServerConfig.ts +6 -4
- package/codeAgent/tools/McpSignIn.ts +7 -7
- package/codeAgent/tools/McpTokenStore.ts +63 -16
- package/codeAgent/tools/McpToolPack.ts +8 -1
- package/codeAgent/tools/WebToolPack.ts +50 -6
- package/codeAgent/tools/akanCodePlugins.test.ts +130 -60
- package/codeAgent/tools/mcpOAuth.test.ts +50 -14
- package/codeAgent/tools/mcpServerConfig.test.ts +130 -1
- package/commandDecorators/command.ts +4 -1
- package/commandDecorators/workspaceVersionCheck.test.ts +17 -7
- package/commandDecorators/workspaceVersionCheck.ts +4 -1
- package/commentDensityScanner.test.ts +121 -0
- package/commentDensityScanner.ts +101 -0
- package/componentSizeScanner.test.ts +36 -0
- package/componentSizeScanner.ts +52 -0
- package/componentWriteChainScanner.test.ts +137 -0
- package/componentWriteChainScanner.ts +104 -0
- package/conventionSet.test.ts +73 -0
- package/conventionSet.ts +67 -0
- package/createTunnel.test.ts +200 -2
- package/createTunnel.ts +80 -6
- package/csrE2e/ssrRegistry.e2e.test.ts +6 -1
- package/deprecatedCommentScanner.test.ts +54 -0
- package/deprecatedCommentScanner.ts +30 -0
- package/endpointReach.test.ts +58 -0
- package/endpointReach.ts +80 -0
- package/envSeed.test.ts +90 -0
- package/envSeed.ts +68 -0
- package/executors.test.ts +19 -5
- package/executors.ts +59 -4
- package/fileSys.test.ts +30 -2
- package/fileSys.ts +14 -0
- package/formSetterScanner.test.ts +44 -1
- package/formSetterScanner.ts +53 -8
- package/frontendBuild/csrArtifactBuilder.ts +7 -0
- package/frontendBuild/mcpAppViewBuilder.test.ts +63 -0
- package/frontendBuild/mcpAppViewBuilder.ts +7 -1
- package/frontendBuild/optionalPeers.test.ts +132 -0
- package/index.ts +1 -0
- package/integration/devStability.integration.test.ts +9 -5
- package/integration/ssrMemoryProbe.ts +6 -0
- package/kindBranchScanner.test.ts +89 -0
- package/kindBranchScanner.ts +158 -0
- package/lint/__fixtures__/no-oversized-list-limit/bad.tsx +18 -0
- package/lint/__fixtures__/no-oversized-list-limit/good.tsx +23 -0
- package/lint/no-oversized-list-limit.grit +73 -0
- package/lint/no-unguarded-endpoint.grit +5 -5
- package/linter.test.ts +12 -0
- package/linter.ts +35 -12
- package/maintainabilityBaseline.test.ts +280 -0
- package/maintainabilityBaseline.ts +220 -0
- package/maintainabilityContract.test.ts +846 -0
- package/maintainabilityContract.ts +404 -0
- package/maintainabilityExceptionScanner.test.ts +90 -0
- package/maintainabilityExceptionScanner.ts +85 -0
- package/maintainabilityLimits.ts +60 -0
- package/maintainabilityReport.test.ts +130 -0
- package/maintainabilityReport.ts +153 -0
- package/mobile/desktopRelease.test.ts +1 -1
- package/mobile/desktopRelease.ts +3 -1
- package/mobile/desktopServerStage.test.ts +1 -0
- package/mobile/desktopServerStage.ts +3 -1
- package/mobile/nativeApp.test.ts +20 -5
- package/mobile/nativeApp.ts +4 -1
- package/mobile/nativeConfig.test.ts +209 -5
- package/mobile/nativeConfig.ts +127 -6
- package/mobile/nativePublicEnv.test.ts +2 -0
- package/mobile/nativePublicEnv.ts +13 -0
- package/package.json +4 -2
- package/platformTest/LinuxDockerTarget.ts +3 -0
- package/{codeAgent/feedback → preview}/PreviewChrome.ts +13 -7
- package/preview/PreviewView.ts +219 -0
- package/preview/index.ts +2 -0
- package/preview/previewView.test.ts +76 -0
- package/prompter.ts +7 -0
- package/qualityScanner.test.ts +75 -0
- package/qualityScanner.ts +17 -1
- package/routeSourceValidator.test.ts +11 -2
- package/routeSourceValidator.ts +14 -2
- package/serviceLoopCallScanner.test.ts +56 -0
- package/serviceLoopCallScanner.ts +122 -0
- package/shellOnlyEnv.test.ts +65 -0
- package/shellOnlyEnv.ts +54 -0
- package/signalMutationIndex.test.ts +15 -0
- package/signalMutationIndex.ts +23 -82
- package/sourceComments.test.ts +45 -0
- package/sourceComments.ts +70 -0
- package/sshHostKeys.ts +59 -0
- package/storeAccessScanner.test.ts +84 -0
- package/storeAccessScanner.ts +71 -0
- package/storeSource.ts +160 -0
- package/storeWriteScanner.test.ts +292 -0
- package/storeWriteScanner.ts +50 -0
- package/subspace.test.ts +10 -0
- package/subspaceClone.ts +3 -1
- package/subspaceConfig.ts +6 -0
- package/testOnlyExportScanner.test.ts +83 -0
- package/testOnlyExportScanner.ts +124 -0
- package/transforms/barrelImportsPlugin.ts +10 -4
- package/transforms/mcpAppPageTrimmer.test.ts +8 -2
- package/transforms/mcpAppPageTrimmer.ts +5 -20
- package/transforms/pureLazyMarker.ts +1 -1
- package/typeChecker.ts +32 -0
- package/typecheck/projectTypecheck.test.ts +18 -0
- package/typecheck/projectTypecheck.ts +29 -5
- package/typecheck/typecheck.proc.ts +6 -0
- package/typecheck/typecheckFailure.ts +13 -0
- package/types.ts +1 -0
- package/workflow/artifacts.ts +16 -2
- package/workflow/executor.ts +22 -7
- package/workflow/guardCatalog.test.ts +59 -0
- package/workflow/guardCatalog.ts +108 -0
- package/workflow/index.ts +4 -0
- package/workflow/interactionScaffold.ts +122 -0
- package/workflow/mutationScaffold.ts +533 -0
- package/workflow/mutationSource.test.ts +108 -0
- package/workflow/mutationSource.ts +219 -0
- package/workflow/plan.ts +82 -0
- package/workflow/render.ts +19 -4
- package/workflow/source.test.ts +185 -0
- package/workflow/source.ts +151 -24
- package/workflow/sourceAst.ts +10 -48
- package/workflow/types.ts +18 -1
- package/workspaceLayout.ts +2 -0
- package/writePath.test.ts +109 -0
- package/writePath.ts +219 -0
- package/writePlacementScanner.test.ts +77 -0
- package/writePlacementScanner.ts +50 -0
- package/writingActionIndex.test.ts +99 -0
- package/writingActionIndex.ts +63 -0
- package/codeAgent/feedback/PreviewView.ts +0 -95
- /package/{codeAgent/feedback → preview}/previewChrome.test.ts +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import type { SourceFileInfo } from "./qualityScanner";
|
|
4
|
+
import { TestOnlyExportScanner } from "./testOnlyExportScanner";
|
|
5
|
+
|
|
6
|
+
const fileOf = (file: string, content: string): SourceFileInfo => ({
|
|
7
|
+
file,
|
|
8
|
+
absolutePath: `/repo/${file}`,
|
|
9
|
+
content,
|
|
10
|
+
sourceFile: ts.createSourceFile(
|
|
11
|
+
file,
|
|
12
|
+
content,
|
|
13
|
+
ts.ScriptTarget.Latest,
|
|
14
|
+
true,
|
|
15
|
+
file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
16
|
+
),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const scan = (files: Record<string, string>) =>
|
|
20
|
+
new TestOnlyExportScanner().scan(Object.entries(files).map(([file, content]) => fileOf(file, content)));
|
|
21
|
+
|
|
22
|
+
const layout = [
|
|
23
|
+
"export const groupLabelHeight = 30;",
|
|
24
|
+
"export const computeGroupBoxes = () => [groupLabelHeight];",
|
|
25
|
+
"export const groupEdgeStrips = (pad: number): number[] => (pad > 0 ? groupEdgeStrips(pad - 1) : []);",
|
|
26
|
+
"export const layoutBoard = () => 1;",
|
|
27
|
+
"",
|
|
28
|
+
].join("\n");
|
|
29
|
+
|
|
30
|
+
const layoutTest = [
|
|
31
|
+
'import { computeGroupBoxes, groupEdgeStrips, groupLabelHeight, layoutBoard } from "./boardLayout";',
|
|
32
|
+
"test(() => [computeGroupBoxes(), groupEdgeStrips(1), groupLabelHeight, layoutBoard()]);",
|
|
33
|
+
"",
|
|
34
|
+
].join("\n");
|
|
35
|
+
|
|
36
|
+
describe("TestOnlyExportScanner", () => {
|
|
37
|
+
test("flags an export only tests reference, even when it calls itself", () => {
|
|
38
|
+
const warnings = scan({
|
|
39
|
+
"apps/demo/webkit/boardLayout.ts": layout,
|
|
40
|
+
"apps/demo/webkit/boardLayout.test.ts": layoutTest,
|
|
41
|
+
"apps/demo/ui/Board.tsx":
|
|
42
|
+
'import { layoutBoard } from "@apps/demo/webkit";\nexport const Board = () => layoutBoard();\n',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(warnings.map((warning) => warning.message)).toEqual([
|
|
46
|
+
'Exported "computeGroupBoxes" is referenced only by tests: apps/demo/webkit/boardLayout.test.ts.',
|
|
47
|
+
'Exported "groupEdgeStrips" is referenced only by tests: apps/demo/webkit/boardLayout.test.ts.',
|
|
48
|
+
]);
|
|
49
|
+
expect(warnings[0]).toMatchObject({
|
|
50
|
+
rule: "akan.global.test-only-export",
|
|
51
|
+
scope: "global",
|
|
52
|
+
file: "apps/demo/webkit/boardLayout.ts",
|
|
53
|
+
line: 2,
|
|
54
|
+
locations: [{ file: "apps/demo/webkit/boardLayout.test.ts", line: 1 }],
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("passes an export another source file uses, or that a function in its own file uses", () => {
|
|
59
|
+
const warnings = scan({
|
|
60
|
+
"apps/demo/webkit/boardLayout.ts": layout,
|
|
61
|
+
"apps/demo/webkit/boardLayout.test.ts": layoutTest,
|
|
62
|
+
"apps/demo/ui/Board.tsx": [
|
|
63
|
+
'import * as webkit from "@apps/demo/webkit";',
|
|
64
|
+
"export const Board = () => [webkit.computeGroupBoxes(), webkit.groupEdgeStrips(2), webkit.layoutBoard()];",
|
|
65
|
+
"",
|
|
66
|
+
].join("\n"),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(warnings).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("passes an export no test references, and leaves lib facets and lib modules alone", () => {
|
|
73
|
+
expect(
|
|
74
|
+
scan({
|
|
75
|
+
"apps/demo/common/unused.ts": "export const unused = 1;\n",
|
|
76
|
+
"libs/demo/webkit/signedIn.ts": "export const signedIn = () => true;\n",
|
|
77
|
+
"libs/demo/webkit/signedIn.test.ts": 'import { signedIn } from "./signedIn";\ntest(() => signedIn());\n',
|
|
78
|
+
"apps/demo/lib/post/post.constant.ts": "export class PostInput {}\n",
|
|
79
|
+
"apps/demo/lib/post/post.signal.test.ts": "const input = new PostInput();\n",
|
|
80
|
+
}),
|
|
81
|
+
).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import type { QualityWarning, SourceFileInfo } from "./qualityScanner";
|
|
3
|
+
|
|
4
|
+
interface ExportedDeclaration {
|
|
5
|
+
name: string;
|
|
6
|
+
line: number;
|
|
7
|
+
statement: ts.Statement;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface TestReference {
|
|
11
|
+
file: string;
|
|
12
|
+
line: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class TestOnlyExportScanner {
|
|
16
|
+
static readonly rule = "akan.global.test-only-export";
|
|
17
|
+
static #facets = new Set(["common", "webkit", "srvkit", "ui"]);
|
|
18
|
+
|
|
19
|
+
scan(sourceFiles: SourceFileInfo[]): QualityWarning[] {
|
|
20
|
+
const liveFileNumByName = new Map<string, number>();
|
|
21
|
+
const testRefsByName = new Map<string, TestReference[]>();
|
|
22
|
+
for (const { file, sourceFile } of sourceFiles) {
|
|
23
|
+
const isTest = TestOnlyExportScanner.#isTest(file);
|
|
24
|
+
for (const [name, line] of TestOnlyExportScanner.#identifierLinesOf(sourceFile)) {
|
|
25
|
+
if (!isTest) liveFileNumByName.set(name, (liveFileNumByName.get(name) ?? 0) + 1);
|
|
26
|
+
else if (testRefsByName.has(name)) testRefsByName.get(name)?.push({ file, line });
|
|
27
|
+
else testRefsByName.set(name, [{ file, line }]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return sourceFiles
|
|
31
|
+
.filter(({ file }) => TestOnlyExportScanner.#isFacetSource(file))
|
|
32
|
+
.flatMap(({ file, sourceFile }) => {
|
|
33
|
+
const declarations = TestOnlyExportScanner.#exportedDeclarationsOf(sourceFile);
|
|
34
|
+
return declarations
|
|
35
|
+
.filter(({ name }) => (liveFileNumByName.get(name) ?? 0) <= 1)
|
|
36
|
+
.filter(({ name }) => !TestOnlyExportScanner.#isUsedInFile(sourceFile, name, declarations))
|
|
37
|
+
.flatMap(({ name, line }) => {
|
|
38
|
+
const testRefs = testRefsByName.get(name);
|
|
39
|
+
if (!testRefs?.length) return [];
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
rule: TestOnlyExportScanner.rule,
|
|
43
|
+
scope: "global" as const,
|
|
44
|
+
severity: "warning" as const,
|
|
45
|
+
file,
|
|
46
|
+
line,
|
|
47
|
+
message: `Exported "${name}" is referenced only by tests: ${testRefs.map((ref) => ref.file).join(", ")}.`,
|
|
48
|
+
locations: testRefs,
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static #isTest(file: string) {
|
|
56
|
+
return /\.(test|spec)\.tsx?$/.test(file);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
//? A lib's facets are public API for every workspace that installs it, so an export no app here calls is not dead.
|
|
60
|
+
static #isFacetSource(file: string) {
|
|
61
|
+
const segments = file.split("/");
|
|
62
|
+
if (segments[0] !== "apps") return false;
|
|
63
|
+
return TestOnlyExportScanner.#facets.has(segments[2] ?? "") && !TestOnlyExportScanner.#isTest(file);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static #identifierLinesOf(sourceFile: ts.SourceFile) {
|
|
67
|
+
const lines = new Map<string, number>();
|
|
68
|
+
const visit = (node: ts.Node) => {
|
|
69
|
+
if (ts.isIdentifier(node) && !lines.has(node.text))
|
|
70
|
+
lines.set(node.text, sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1);
|
|
71
|
+
ts.forEachChild(node, visit);
|
|
72
|
+
};
|
|
73
|
+
visit(sourceFile);
|
|
74
|
+
return lines;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static #exportedDeclarationsOf(sourceFile: ts.SourceFile): ExportedDeclaration[] {
|
|
78
|
+
return sourceFile.statements.flatMap((statement) => {
|
|
79
|
+
if (!TestOnlyExportScanner.#isNamedExport(statement)) return [];
|
|
80
|
+
return TestOnlyExportScanner.#declaredNamesOf(statement).map((identifier) => ({
|
|
81
|
+
name: identifier.text,
|
|
82
|
+
line: sourceFile.getLineAndCharacterOfPosition(identifier.getStart(sourceFile)).line + 1,
|
|
83
|
+
statement,
|
|
84
|
+
}));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static #isNamedExport(statement: ts.Statement) {
|
|
89
|
+
if (!ts.canHaveModifiers(statement)) return false;
|
|
90
|
+
const modifiers = ts.getModifiers(statement) ?? [];
|
|
91
|
+
return (
|
|
92
|
+
modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) &&
|
|
93
|
+
!modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static #declaredNamesOf(statement: ts.Statement): ts.Identifier[] {
|
|
98
|
+
if (ts.isVariableStatement(statement))
|
|
99
|
+
return statement.declarationList.declarations
|
|
100
|
+
.map((declaration) => declaration.name)
|
|
101
|
+
.filter((name): name is ts.Identifier => ts.isIdentifier(name));
|
|
102
|
+
if (
|
|
103
|
+
ts.isFunctionDeclaration(statement) ||
|
|
104
|
+
ts.isClassDeclaration(statement) ||
|
|
105
|
+
ts.isInterfaceDeclaration(statement) ||
|
|
106
|
+
ts.isTypeAliasDeclaration(statement) ||
|
|
107
|
+
ts.isEnumDeclaration(statement)
|
|
108
|
+
)
|
|
109
|
+
return statement.name ? [statement.name] : [];
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static #isUsedInFile(sourceFile: ts.SourceFile, name: string, declarations: ExportedDeclaration[]) {
|
|
114
|
+
const ownStatements = new Set(declarations.filter((entry) => entry.name === name).map((entry) => entry.statement));
|
|
115
|
+
let used = false;
|
|
116
|
+
const visit = (node: ts.Node) => {
|
|
117
|
+
if (used || ownStatements.has(node as ts.Statement)) return;
|
|
118
|
+
if (ts.isIdentifier(node) && node.text === name) used = true;
|
|
119
|
+
else ts.forEachChild(node, visit);
|
|
120
|
+
};
|
|
121
|
+
visit(sourceFile);
|
|
122
|
+
return used;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -12,14 +12,17 @@ export interface BarrelImportsPluginOptions {
|
|
|
12
12
|
pipeAfter?: (source: string, args: { path: string }) => string | Promise<string | null> | null;
|
|
13
13
|
/** Barrels the bundle leaves external: a rewritten import would inline the modules the import map already serves. */
|
|
14
14
|
external?: readonly string[];
|
|
15
|
+
/** The barrels to rewrite, instead of the app config's `barrelImports`. */
|
|
16
|
+
barrels?: readonly string[];
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export const createBarrelImportsPlugin = async (
|
|
18
20
|
app: App,
|
|
19
|
-
{ skipPath = defaultSkipPath, pipeAfter, external = [] }: BarrelImportsPluginOptions = {},
|
|
21
|
+
{ skipPath = defaultSkipPath, pipeAfter, external = [], barrels: only }: BarrelImportsPluginOptions = {},
|
|
20
22
|
): Promise<BunPlugin> => {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
+
const barrels = [...new Set(only ?? (await app.getConfig()).barrelImports)].filter(
|
|
24
|
+
(barrel) => barrel && !external.includes(barrel),
|
|
25
|
+
);
|
|
23
26
|
const analyzer = new BarrelAnalyzer({
|
|
24
27
|
resolvePackage: await createTsconfigPackageResolver(app),
|
|
25
28
|
});
|
|
@@ -37,7 +40,10 @@ export const createBarrelImportsPlugin = async (
|
|
|
37
40
|
async (args) => {
|
|
38
41
|
const realPath = args.path.replace(/\?v=\d+$/, "");
|
|
39
42
|
const loader = loaderFor(realPath);
|
|
40
|
-
|
|
43
|
+
const file = Bun.file(realPath);
|
|
44
|
+
//? A build's `files` entry has no copy on disk: Bun loads it itself.
|
|
45
|
+
if (!(await file.exists())) return undefined;
|
|
46
|
+
let source = await file.text();
|
|
41
47
|
if (skipPath(realPath)) return { contents: source, loader };
|
|
42
48
|
|
|
43
49
|
// Bun's macro evaluator races when a rewritten module holds `with { type: "macro" }` imports while another
|
|
@@ -2,9 +2,15 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { symlink } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { tempDirs, writeText } from "../testHelpers";
|
|
5
|
+
import { createBarrelImportsPlugin } from "./barrelImportsPlugin";
|
|
5
6
|
import { McpAppPageTrimmer } from "./mcpAppPageTrimmer";
|
|
6
7
|
|
|
7
8
|
const makeTempRoot = tempDirs("akan-mcp-app-trim-");
|
|
9
|
+
const trimmerPlugin = async (root: string, pagePaths: string[]) =>
|
|
10
|
+
await createBarrelImportsPlugin(
|
|
11
|
+
{ workspace: { workspaceRoot: root }, getTsConfig: async () => ({ compilerOptions: {} }) } as never,
|
|
12
|
+
{ barrels: [], pipeAfter: McpAppPageTrimmer.pipe(pagePaths) },
|
|
13
|
+
);
|
|
8
14
|
|
|
9
15
|
describe("McpAppPageTrimmer", () => {
|
|
10
16
|
test("hands every stage but the apps a function that draws nothing, and keeps the rest of the chain as written", () => {
|
|
@@ -51,7 +57,7 @@ describe("McpAppPageTrimmer", () => {
|
|
|
51
57
|
await writeText(path.join(root, "entry.ts"), 'import login from "./linked/login.tsx";\nexport { login };\n');
|
|
52
58
|
const result = await Bun.build({
|
|
53
59
|
entrypoints: [path.join(root, "entry.ts")],
|
|
54
|
-
plugins: [
|
|
60
|
+
plugins: [await trimmerPlugin(root, [path.join(root, "linked/login.tsx")])],
|
|
55
61
|
});
|
|
56
62
|
const [output] = result.outputs;
|
|
57
63
|
expect(result.success).toBe(true);
|
|
@@ -73,7 +79,7 @@ describe("McpAppPageTrimmer", () => {
|
|
|
73
79
|
await writeText(path.join(root, "entry.ts"), 'import trip from "./trip.tsx";\nconsole.info(trip);\n');
|
|
74
80
|
const result = await Bun.build({
|
|
75
81
|
entrypoints: [path.join(root, "entry.ts")],
|
|
76
|
-
plugins: [
|
|
82
|
+
plugins: [await trimmerPlugin(root, [path.join(root, "trip.tsx")])],
|
|
77
83
|
splitting: false,
|
|
78
84
|
});
|
|
79
85
|
const text = (await result.outputs[0]?.text()) ?? "";
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { realpathSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { BunPlugin } from "bun";
|
|
4
2
|
import ts from "typescript";
|
|
5
3
|
import { PureLazyMarker } from "./pureLazyMarker";
|
|
6
4
|
|
|
@@ -11,24 +9,11 @@ type ChainStage = ts.CallExpression & { expression: ts.PropertyAccessExpression
|
|
|
11
9
|
export class McpAppPageTrimmer {
|
|
12
10
|
static readonly #unused = new Set(["render", "head", "loading", "guard"]);
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (!pagePaths.length) return;
|
|
20
|
-
//? The bundler loads a module by the path a link points at: a synced lib page is one under `page/(libs)`.
|
|
21
|
-
const loadedPaths = new Set(
|
|
22
|
-
pagePaths.flatMap((pagePath) => [pagePath, McpAppPageTrimmer.#realPathOf(pagePath)]),
|
|
23
|
-
);
|
|
24
|
-
const filter = new RegExp(`^(?:${[...loadedPaths].map((pagePath) => RegExp.escape(pagePath)).join("|")})$`);
|
|
25
|
-
//? The bundler runs one onLoad per module, so this one also marks the page's own `lazy()` components.
|
|
26
|
-
build.onLoad({ filter }, async (args) => ({
|
|
27
|
-
contents: PureLazyMarker.mark(McpAppPageTrimmer.trim(await Bun.file(args.path).text(), args.path), args.path),
|
|
28
|
-
loader: path.extname(args.path).slice(1) as "tsx" | "ts" | "jsx" | "js",
|
|
29
|
-
}));
|
|
30
|
-
},
|
|
31
|
-
};
|
|
12
|
+
static pipe(pagePaths: string[]): (source: string, args: { path: string }) => string {
|
|
13
|
+
//? The bundler loads a module by the path a link points at: a synced lib page is one under `page/(libs)`.
|
|
14
|
+
const loadedPaths = new Set(pagePaths.flatMap((pagePath) => [pagePath, McpAppPageTrimmer.#realPathOf(pagePath)]));
|
|
15
|
+
return (source, { path: filePath }) =>
|
|
16
|
+
PureLazyMarker.mark(loadedPaths.has(filePath) ? McpAppPageTrimmer.trim(source, filePath) : source, filePath);
|
|
32
17
|
}
|
|
33
18
|
|
|
34
19
|
static trim(source: string, filePath: string): string {
|
|
@@ -25,7 +25,7 @@ export class PureLazyMarker {
|
|
|
25
25
|
|
|
26
26
|
static mark(source: string, filePath: string): string {
|
|
27
27
|
if (!source.includes("lazy(")) return source;
|
|
28
|
-
const kind = filePath
|
|
28
|
+
const kind = /\.[jt]sx$/.test(filePath) ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
29
29
|
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, kind);
|
|
30
30
|
const starts: number[] = [];
|
|
31
31
|
const visit = (node: ts.Node) => {
|
package/typeChecker.ts
CHANGED
|
@@ -13,6 +13,16 @@ export interface ProjectTypecheckResult {
|
|
|
13
13
|
message: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export interface TypecheckRecord {
|
|
17
|
+
/** Relative to the workspace root, `/`-separated; `null` for a diagnostic tied to no file. */
|
|
18
|
+
file: string | null;
|
|
19
|
+
line: number;
|
|
20
|
+
col: number;
|
|
21
|
+
code: string;
|
|
22
|
+
severity: "error" | "warning";
|
|
23
|
+
message: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
export class TypeChecker {
|
|
17
27
|
readonly configPath: string;
|
|
18
28
|
readonly configFile: { config?: any; error?: ts.Diagnostic };
|
|
@@ -198,6 +208,28 @@ export class TypeChecker {
|
|
|
198
208
|
return parsedConfig;
|
|
199
209
|
}
|
|
200
210
|
|
|
211
|
+
static toRecords(diagnostics: ts.Diagnostic[], root: string): TypecheckRecord[] {
|
|
212
|
+
return diagnostics
|
|
213
|
+
.filter(
|
|
214
|
+
(diagnostic) =>
|
|
215
|
+
diagnostic.category === ts.DiagnosticCategory.Error || diagnostic.category === ts.DiagnosticCategory.Warning,
|
|
216
|
+
)
|
|
217
|
+
.map((diagnostic) => {
|
|
218
|
+
const position =
|
|
219
|
+
diagnostic.file && diagnostic.start !== undefined
|
|
220
|
+
? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start)
|
|
221
|
+
: null;
|
|
222
|
+
return {
|
|
223
|
+
file: diagnostic.file ? path.relative(root, diagnostic.file.fileName).split(path.sep).join("/") : null,
|
|
224
|
+
line: position ? position.line + 1 : 0,
|
|
225
|
+
col: position ? position.character + 1 : 0,
|
|
226
|
+
code: `TS${diagnostic.code}`,
|
|
227
|
+
severity: diagnostic.category === ts.DiagnosticCategory.Error ? "error" : "warning",
|
|
228
|
+
message: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
201
233
|
static formatDiagnosticMessages(diagnostics: ts.Diagnostic[]): string {
|
|
202
234
|
return ts.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
203
235
|
getCanonicalFileName: (fileName) => fileName,
|
|
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { tempDirs, writeText as write } from "../testHelpers";
|
|
4
4
|
import { ProjectTypecheck } from "./projectTypecheck";
|
|
5
|
+
import { TypecheckFailure } from "./typecheckFailure";
|
|
5
6
|
|
|
6
7
|
const makeTempRoot = tempDirs("akan-devkit-project-typecheck-");
|
|
7
8
|
|
|
@@ -29,4 +30,21 @@ describe("ProjectTypecheck", () => {
|
|
|
29
30
|
expect((error as Error).message).toContain("lib/user/user.signal.test.ts");
|
|
30
31
|
expect((error as Error).message).not.toContain("old.test.ts");
|
|
31
32
|
}, 60_000);
|
|
33
|
+
|
|
34
|
+
test("hands back one record per diagnostic when asked for records", async () => {
|
|
35
|
+
const { root, dir } = await project();
|
|
36
|
+
const error = await new ProjectTypecheck(dir, root, { ...process.env })
|
|
37
|
+
.run(["../../server.ts"], { tests: true, report: "records" })
|
|
38
|
+
.catch((e: unknown) => e);
|
|
39
|
+
expect(error).toBeInstanceOf(TypecheckFailure);
|
|
40
|
+
const [record, ...rest] = (error as TypecheckFailure).records;
|
|
41
|
+
expect(rest).toEqual([]);
|
|
42
|
+
expect(record).toMatchObject({
|
|
43
|
+
file: "libs/demo/lib/user/user.signal.test.ts",
|
|
44
|
+
line: 1,
|
|
45
|
+
col: 14,
|
|
46
|
+
code: "TS2322",
|
|
47
|
+
severity: "error",
|
|
48
|
+
});
|
|
49
|
+
}, 60_000);
|
|
32
50
|
});
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { mkdir, rm } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import type { TypecheckRecord } from "../typeChecker";
|
|
4
|
+
import { TypecheckFailure } from "./typecheckFailure";
|
|
3
5
|
|
|
4
6
|
export interface ProjectTypecheckOptions {
|
|
5
7
|
clean?: boolean;
|
|
6
8
|
incremental?: boolean;
|
|
7
9
|
/** The project's own `*.test.ts` / `*.spec.ts` as well. */
|
|
8
10
|
tests?: boolean;
|
|
11
|
+
/** `records` fails with a `TypecheckFailure` carrying one record per diagnostic instead of the printed report. */
|
|
12
|
+
report?: "pretty" | "records";
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
//* tsc checks what the entries import, and nothing imports a test, so a test is checked only when it is included by
|
|
@@ -25,7 +29,10 @@ export class ProjectTypecheck {
|
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
/** `entries`: relative to the typecheck folder (`../../server.ts`). */
|
|
28
|
-
async run(
|
|
32
|
+
async run(
|
|
33
|
+
entries: string[],
|
|
34
|
+
{ clean = false, incremental = true, tests = false, report = "pretty" }: ProjectTypecheckOptions = {},
|
|
35
|
+
) {
|
|
29
36
|
const name = tests ? "tsconfig.tests" : "tsconfig";
|
|
30
37
|
await mkdir(this.typecheckDir, { recursive: true });
|
|
31
38
|
const tsconfig = {
|
|
@@ -38,14 +45,19 @@ export class ProjectTypecheck {
|
|
|
38
45
|
const tsconfigPath = path.join(this.typecheckDir, `${name}.json`);
|
|
39
46
|
await Bun.write(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}\n`);
|
|
40
47
|
if (clean) await rm(path.join(this.typecheckDir, `${name}.tsbuildinfo`), { force: true });
|
|
41
|
-
await this
|
|
48
|
+
await this.check(tsconfigPath, report);
|
|
42
49
|
return tsconfigPath;
|
|
43
50
|
}
|
|
44
51
|
|
|
45
|
-
async
|
|
52
|
+
async check(tsconfigPath: string, report: "pretty" | "records" = "pretty") {
|
|
46
53
|
const proc = Bun.spawn([process.execPath, await ProjectTypecheck.workerEntry(this.workspaceRoot)], {
|
|
47
54
|
cwd: this.workspaceRoot,
|
|
48
|
-
env: {
|
|
55
|
+
env: {
|
|
56
|
+
...this.env,
|
|
57
|
+
AKAN_COMMAND_TYPE: "typecheck",
|
|
58
|
+
AKAN_TYPECHECK_TSCONFIG: tsconfigPath,
|
|
59
|
+
...(report === "records" ? { AKAN_TYPECHECK_FORMAT: "json", AKAN_TYPECHECK_ROOT: this.workspaceRoot } : {}),
|
|
60
|
+
},
|
|
49
61
|
stdout: "pipe",
|
|
50
62
|
stderr: "pipe",
|
|
51
63
|
});
|
|
@@ -54,7 +66,19 @@ export class ProjectTypecheck {
|
|
|
54
66
|
new Response(proc.stderr).text(),
|
|
55
67
|
proc.exited,
|
|
56
68
|
]);
|
|
57
|
-
if (exitCode
|
|
69
|
+
if (exitCode === 0) return;
|
|
70
|
+
const records = report === "records" ? ProjectTypecheck.#parseRecords(stdout) : null;
|
|
71
|
+
if (records?.length) throw new TypecheckFailure(records);
|
|
72
|
+
throw new Error((stderr || stdout).trim() || `Typecheck failed with exit code ${exitCode}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static #parseRecords(stdout: string): TypecheckRecord[] | null {
|
|
76
|
+
try {
|
|
77
|
+
const parsed: unknown = JSON.parse(stdout);
|
|
78
|
+
return Array.isArray(parsed) ? (parsed as TypecheckRecord[]) : null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
58
82
|
}
|
|
59
83
|
|
|
60
84
|
//? The workspace's own devkit first, then an installed one, then the entry a bundle puts beside its chunks.
|
|
@@ -24,6 +24,12 @@ try {
|
|
|
24
24
|
const configPath = process.env.AKAN_TYPECHECK_TSCONFIG;
|
|
25
25
|
if (!configPath) throw new Error("AKAN_TYPECHECK_TSCONFIG is required");
|
|
26
26
|
const result = TypeChecker.checkProject(configPath);
|
|
27
|
+
if (process.env.AKAN_TYPECHECK_FORMAT === "json") {
|
|
28
|
+
//? Not process.cwd(): that is the realpath (/private/var on macOS) while TypeScript keeps the spelling it was given.
|
|
29
|
+
const root = process.env.AKAN_TYPECHECK_ROOT ?? process.cwd();
|
|
30
|
+
process.stdout.write(JSON.stringify(TypeChecker.toRecords(result.errors, root)));
|
|
31
|
+
process.exit(result.errors.length > 0 ? 1 : 0);
|
|
32
|
+
}
|
|
27
33
|
if (result.errors.length > 0) {
|
|
28
34
|
console.error(result.message);
|
|
29
35
|
process.exit(1);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TypecheckRecord } from "../typeChecker";
|
|
2
|
+
|
|
3
|
+
export class TypecheckFailure extends Error {
|
|
4
|
+
constructor(readonly records: TypecheckRecord[]) {
|
|
5
|
+
super(
|
|
6
|
+
records
|
|
7
|
+
.slice(0, 20)
|
|
8
|
+
.map((record) => `${record.file ?? "-"}:${record.line}:${record.col} ${record.code} ${record.message}`)
|
|
9
|
+
.join("\n") || "Typecheck failed",
|
|
10
|
+
);
|
|
11
|
+
this.name = "TypecheckFailure";
|
|
12
|
+
}
|
|
13
|
+
}
|
package/types.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface PackageJson {
|
|
|
12
12
|
optionalDependencies?: Record<string, string>;
|
|
13
13
|
trustedDependencies?: string[];
|
|
14
14
|
overrides?: Record<string, string>;
|
|
15
|
+
resolutions?: Record<string, string>;
|
|
15
16
|
engines?: Record<string, string>;
|
|
16
17
|
exports?: Record<string, string | Record<string, string>>;
|
|
17
18
|
bun?: {
|
package/workflow/artifacts.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { capitalize } from "akanjs/common";
|
|
2
2
|
import type { Workspace } from "../commandDecorators";
|
|
3
|
+
import { InteractionScaffold } from "./interactionScaffold";
|
|
3
4
|
import type {
|
|
4
5
|
GeneratedSyncState,
|
|
5
6
|
PrimitiveChangedFile,
|
|
@@ -156,11 +157,15 @@ export const workflowPathsForPlan = (plan: WorkflowPlan) => {
|
|
|
156
157
|
const app = planInputString(plan, "app");
|
|
157
158
|
const module = planInputString(plan, "module");
|
|
158
159
|
const moduleClass = module ? capitalize(module) : "<Module>";
|
|
160
|
+
const rawInteraction = planInputString(plan, "interaction");
|
|
161
|
+
const interaction = rawInteraction ? (InteractionScaffold.nameOf(rawInteraction) ?? rawInteraction) : null;
|
|
159
162
|
return plan.predictedChanges.map((change) =>
|
|
160
163
|
change.target
|
|
161
164
|
.replace(/^\*\//, app ? `apps/${app}/` : "")
|
|
162
165
|
.replaceAll("<module>", module || "<module>")
|
|
163
|
-
.replaceAll("<Module>", moduleClass)
|
|
166
|
+
.replaceAll("<Module>", moduleClass)
|
|
167
|
+
.replaceAll("<interaction>", interaction || "<interaction>")
|
|
168
|
+
.replaceAll("<Interaction>", interaction ? capitalize(interaction) : "<Interaction>"),
|
|
164
169
|
);
|
|
165
170
|
};
|
|
166
171
|
|
|
@@ -277,6 +282,15 @@ const statusForValidationKind = (
|
|
|
277
282
|
return matching.some((command) => command.status === "failed") ? "failed" : "passed";
|
|
278
283
|
};
|
|
279
284
|
|
|
285
|
+
//? `akan verify` syncs first and names a failed sync in its header line (`sync 1 error`), so one run answers both.
|
|
286
|
+
const generatedSyncStatus = (commands: readonly WorkflowValidationCommandResult[]): WorkflowValidationStatus => {
|
|
287
|
+
const sync = statusForValidationKind(commands, "sync");
|
|
288
|
+
const verify = commands.filter((command) => command.kind === "verify");
|
|
289
|
+
if (sync !== "unknown" || verify.length === 0) return sync;
|
|
290
|
+
const syncFailed = verify.some((command) => /^verify fail · .*\bsync \d+ errors?\b/m.test(command.stdout ?? ""));
|
|
291
|
+
return syncFailed ? "failed" : "passed";
|
|
292
|
+
};
|
|
293
|
+
|
|
280
294
|
const statusForCommands = (commands: readonly WorkflowValidationCommandResult[]): WorkflowValidationStatus => {
|
|
281
295
|
if (commands.length === 0) return "unknown";
|
|
282
296
|
return commandStatus(commands) === "failed" ? "failed" : "passed";
|
|
@@ -433,7 +447,7 @@ const createValidationStatuses = (
|
|
|
433
447
|
overallStatus,
|
|
434
448
|
summary: {
|
|
435
449
|
sourceChange: sourceStatus,
|
|
436
|
-
generatedSync:
|
|
450
|
+
generatedSync: generatedSyncStatus(commands),
|
|
437
451
|
validationCommands: validationCommandsStatus,
|
|
438
452
|
baseline: baselineStatus,
|
|
439
453
|
workspaceConfig: statusForScope(commands, diagnostics, scopes, "workspace-config"),
|
package/workflow/executor.ts
CHANGED
|
@@ -312,7 +312,7 @@ const checkAddFieldStructure = async (workspace: Workspace, plan: WorkflowPlan):
|
|
|
312
312
|
return { diagnostics, postApplyChecks };
|
|
313
313
|
};
|
|
314
314
|
|
|
315
|
-
const resolveWorkflowSys = async (workspace: Workspace, target: string | null): Promise<Sys | null> => {
|
|
315
|
+
export const resolveWorkflowSys = async (workspace: Workspace, target: string | null): Promise<Sys | null> => {
|
|
316
316
|
if (!target) return null;
|
|
317
317
|
const [apps, libs] = await workspace.getSyss();
|
|
318
318
|
if (apps.includes(target)) return AppExecutor.from(workspace, target);
|
|
@@ -375,10 +375,10 @@ const addMutationActionSurfaceInspection = (plan: WorkflowPlan): WorkflowStepRes
|
|
|
375
375
|
const moduleClassName = moduleComponentName(module);
|
|
376
376
|
return surfaceReview(plan, {
|
|
377
377
|
code: "add-mutation-action-surface-review",
|
|
378
|
-
target: `*/${moduleSourcePaths(module).
|
|
379
|
-
action: `
|
|
380
|
-
message: `Workflow apply does not write
|
|
381
|
-
reason: "Review action surface guidance before manually editing
|
|
378
|
+
target: `*/${moduleSourcePaths(module).util}`,
|
|
379
|
+
action: `Fill the ${moduleClassName} chain method (validate, mutate, return this) and replace its transition comment. After sync, call st.do.${mutation}(id) from the control in ${moduleClassName}.Util.tsx; a second write that must land with it is another with/after entry, never a second fetch in the store action.`,
|
|
380
|
+
message: `Workflow apply does not write UI code for ${module}.${mutation}; review which screen calls it.`,
|
|
381
|
+
reason: "Review action surface guidance before manually editing UI files.",
|
|
382
382
|
});
|
|
383
383
|
};
|
|
384
384
|
|
|
@@ -404,6 +404,7 @@ export const createWorkflowStepRegistry = ({
|
|
|
404
404
|
addEnumField,
|
|
405
405
|
addMutation,
|
|
406
406
|
addSlice,
|
|
407
|
+
createInteraction,
|
|
407
408
|
}: WorkflowPrimitiveOperations): WorkflowStepRegistry => {
|
|
408
409
|
const inspect = async () => undefined;
|
|
409
410
|
const commandOnly = async () => undefined;
|
|
@@ -484,17 +485,31 @@ export const createWorkflowStepRegistry = ({
|
|
|
484
485
|
),
|
|
485
486
|
[workflowStepKey("add-enum-field", "update-dictionary")]: inspect,
|
|
486
487
|
[workflowStepKey("add-enum-field", "update-option")]: inspect,
|
|
487
|
-
[workflowStepKey("add-mutation", "update-
|
|
488
|
+
[workflowStepKey("add-mutation", "update-document")]: async (_step, plan) =>
|
|
488
489
|
primitiveReportToWorkflowStepResult(
|
|
489
490
|
await addMutation({
|
|
490
491
|
app: workflowStringInput(plan.inputs.app),
|
|
491
492
|
module: workflowStringInput(plan.inputs.module),
|
|
492
493
|
mutation: workflowStringInput(plan.inputs.mutation),
|
|
494
|
+
guards: workflowStringArrayInput(plan.inputs.guard),
|
|
495
|
+
withCalls: workflowStringArrayInput(plan.inputs.with),
|
|
496
|
+
afterCalls: workflowStringArrayInput(plan.inputs.after),
|
|
493
497
|
}),
|
|
494
498
|
),
|
|
499
|
+
[workflowStepKey("add-mutation", "update-service")]: inspect,
|
|
495
500
|
[workflowStepKey("add-mutation", "update-signal")]: inspect,
|
|
496
|
-
[workflowStepKey("add-mutation", "update-
|
|
501
|
+
[workflowStepKey("add-mutation", "update-store")]: inspect,
|
|
502
|
+
[workflowStepKey("add-mutation", "update-dictionary")]: inspect,
|
|
503
|
+
[workflowStepKey("add-mutation", "add-signal-test")]: inspect,
|
|
504
|
+
[workflowStepKey("add-mutation", "connect-action-ui")]: async (_step, plan) =>
|
|
497
505
|
addMutationActionSurfaceInspection(plan),
|
|
506
|
+
[workflowStepKey("create-interaction", "create-interaction")]: async (_step, plan) =>
|
|
507
|
+
primitiveReportToWorkflowStepResult(
|
|
508
|
+
await createInteraction({
|
|
509
|
+
app: workflowStringInput(plan.inputs.app),
|
|
510
|
+
interaction: workflowStringInput(plan.inputs.interaction),
|
|
511
|
+
}),
|
|
512
|
+
),
|
|
498
513
|
[workflowStepKey("add-slice", "update-service-query")]: async (_step, plan) =>
|
|
499
514
|
primitiveReportToWorkflowStepResult(
|
|
500
515
|
await addSlice({
|