@cosmicdrift/kumiko-feature-ast 0.0.0-stage → 0.390.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/LICENSE +21 -0
- package/README.md +11 -2
- package/dist/codemod/index.d.ts +2 -0
- package/dist/codemod/index.js +1 -0
- package/dist/codemod/pipeline-codemod.d.ts +50 -0
- package/dist/codemod/pipeline-codemod.js +536 -0
- package/dist/entity-field-types.d.ts +13 -0
- package/dist/entity-field-types.js +35 -0
- package/dist/extractors/ai-steps.d.ts +6 -0
- package/dist/extractors/ai-steps.js +254 -0
- package/dist/extractors/events.d.ts +5 -0
- package/dist/extractors/events.js +275 -0
- package/dist/extractors/handlers.d.ts +25 -0
- package/dist/extractors/handlers.js +358 -0
- package/dist/extractors/hooks.d.ts +24 -0
- package/dist/extractors/hooks.js +244 -0
- package/dist/extractors/index.d.ts +14 -0
- package/dist/extractors/index.js +13 -0
- package/dist/extractors/jobs-routes.d.ts +7 -0
- package/dist/extractors/jobs-routes.js +145 -0
- package/dist/extractors/projections-screens.d.ts +10 -0
- package/dist/extractors/projections-screens.js +195 -0
- package/dist/extractors/resolve-import.d.ts +21 -0
- package/dist/extractors/resolve-import.js +180 -0
- package/dist/extractors/round1.d.ts +11 -0
- package/dist/extractors/round1.js +87 -0
- package/dist/extractors/round2.d.ts +7 -0
- package/dist/extractors/round2.js +191 -0
- package/dist/extractors/round3.d.ts +23 -0
- package/dist/extractors/round3.js +292 -0
- package/dist/extractors/round5.d.ts +9 -0
- package/dist/extractors/round5.js +68 -0
- package/dist/extractors/round6.d.ts +4 -0
- package/dist/extractors/round6.js +17 -0
- package/dist/extractors/shared.d.ts +94 -0
- package/dist/extractors/shared.js +343 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +10 -0
- package/dist/parse.d.ts +31 -0
- package/dist/parse.js +485 -0
- package/dist/patch.d.ts +212 -0
- package/dist/patch.js +611 -0
- package/dist/patcher.d.ts +211 -0
- package/dist/patcher.js +281 -0
- package/dist/pattern-change-schema.d.ts +1237 -0
- package/dist/pattern-change-schema.js +1072 -0
- package/dist/pattern-id-matchers.d.ts +7 -0
- package/dist/pattern-id-matchers.js +211 -0
- package/dist/pattern-id-schema.d.ts +251 -0
- package/dist/pattern-id-schema.js +166 -0
- package/dist/pattern-library/index.d.ts +2 -0
- package/dist/pattern-library/index.js +3 -0
- package/dist/pattern-library/library.d.ts +14 -0
- package/dist/pattern-library/library.js +94 -0
- package/dist/pattern-library/mixed-schemas.d.ts +16 -0
- package/dist/pattern-library/mixed-schemas.js +559 -0
- package/dist/pattern-library/opaque-schemas.d.ts +7 -0
- package/dist/pattern-library/opaque-schemas.js +115 -0
- package/dist/pattern-library/shared-fields.d.ts +116 -0
- package/dist/pattern-library/shared-fields.js +87 -0
- package/dist/pattern-library/static-schemas.d.ts +21 -0
- package/dist/pattern-library/static-schemas.js +487 -0
- package/dist/pattern-library/types.d.ts +158 -0
- package/dist/pattern-library/types.js +36 -0
- package/dist/patterns.d.ts +315 -0
- package/dist/patterns.js +51 -0
- package/dist/render.d.ts +47 -0
- package/dist/render.js +666 -0
- package/dist/source-location.d.ts +17 -0
- package/dist/source-location.js +27 -0
- package/package.json +58 -4
- package/src/changes.json +21 -0
- package/src/scripts/codemod/README.md +238 -0
- package/src/scripts/codemod/codemod-support.ts +352 -0
- package/src/scripts/codemod/collect-source-files.ts +82 -0
- package/src/scripts/codemod/crypto-shredding-testing-move.ts +119 -0
- package/src/scripts/codemod/ctx-namespaces-annotations.ts +177 -0
- package/src/scripts/codemod/ctx-namespaces-contracts.ts +102 -0
- package/src/scripts/codemod/ctx-namespaces-detect.ts +189 -0
- package/src/scripts/codemod/ctx-namespaces-edits.ts +110 -0
- package/src/scripts/codemod/ctx-namespaces-mapping.ts +150 -0
- package/src/scripts/codemod/ctx-namespaces-rename.ts +23 -0
- package/src/scripts/codemod/ctx-namespaces-structural-params.ts +65 -0
- package/src/scripts/codemod/ctx-namespaces-structural-types.ts +247 -0
- package/src/scripts/codemod/ctx-namespaces-structural.ts +314 -0
- package/src/scripts/codemod/ctx-namespaces-transform.ts +226 -0
- package/src/scripts/codemod/ctx-namespaces.ts +170 -0
- package/src/scripts/codemod/db-factory.ts +810 -0
- package/src/scripts/codemod/feature-ast-package-manifest.ts +95 -0
- package/src/scripts/codemod/feature-ast-package.ts +236 -0
- package/src/scripts/codemod/feature-factory.ts +429 -0
- package/src/scripts/codemod/feature-layout.ts +424 -0
- package/src/scripts/codemod/import-extensions.ts +118 -0
- package/src/scripts/codemod/migrate-cross-tenant.ts +328 -0
- package/src/scripts/codemod/migrate-db-raw.ts +401 -0
- package/src/scripts/codemod/migrate-open-to-all.ts +235 -0
- package/src/scripts/codemod/object-form.ts +746 -0
- package/src/scripts/codemod/package-merge.ts +284 -0
- package/src/scripts/codemod/pii-personal-migration.ts +816 -0
- package/src/scripts/codemod/remove-engine-payload-casts.ts +318 -0
- package/src/scripts/codemod/s3-imports.ts +236 -0
- package/src/scripts/codemod/tenant-db-options.ts +454 -0
- package/src/scripts/codemod/testing-injection.ts +215 -0
- package/src/scripts/codemod/testing-move-targets.ts +161 -0
- package/src/scripts/codemod/testing-move.ts +542 -0
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +411 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marc Frost
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @cosmicdrift/kumiko-feature-ast
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Feature-file AST for Kumiko: parse, patch and render feature definitions, plus the pattern library that describes every feature pattern.
|
|
4
|
+
|
|
5
|
+
## Entry points
|
|
6
|
+
|
|
7
|
+
- `@cosmicdrift/kumiko-feature-ast` - `parseFeatureFile`, `renderFeatureFile`, the patcher and `PATTERN_LIBRARY`.
|
|
8
|
+
- `@cosmicdrift/kumiko-feature-ast/codemod` - the pipeline codemod (Bun only, uses ts-morph).
|
|
9
|
+
|
|
10
|
+
## Codemods
|
|
11
|
+
|
|
12
|
+
The upgrade codemods live in `src/scripts/codemod/`. They run through `kumiko-upgrade` or `kumiko codemod`; you rarely call them directly.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { analyzeFile, analyzeHandlerArrow, convertFile, generatePerformBlock, runCodemod, scanForCandidates, } from "./pipeline-codemod.js";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Project } from "ts-morph";
|
|
2
|
+
export type CodemodResult = {
|
|
3
|
+
readonly filePath: string;
|
|
4
|
+
readonly status: "converted" | "skipped" | "error";
|
|
5
|
+
readonly reason?: string;
|
|
6
|
+
};
|
|
7
|
+
export type CodemodReport = {
|
|
8
|
+
readonly results: readonly CodemodResult[];
|
|
9
|
+
readonly converted: number;
|
|
10
|
+
readonly skipped: number;
|
|
11
|
+
readonly errors: number;
|
|
12
|
+
};
|
|
13
|
+
export type CodemodOptions = {
|
|
14
|
+
readonly dryRun?: boolean;
|
|
15
|
+
readonly verbose?: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type GuardConfig = {
|
|
18
|
+
readonly condition: string;
|
|
19
|
+
readonly failureReturn: string;
|
|
20
|
+
readonly successReturn: string;
|
|
21
|
+
};
|
|
22
|
+
export type ParsedHandlerInfo = {
|
|
23
|
+
readonly handlerBodyText: string;
|
|
24
|
+
readonly isStaticReturn: boolean;
|
|
25
|
+
readonly isSimpleExecutorCreate: boolean;
|
|
26
|
+
readonly isSimpleExecutorUpdate: boolean;
|
|
27
|
+
readonly hasConditionalLogic: boolean;
|
|
28
|
+
readonly executorName?: string;
|
|
29
|
+
readonly executorCreateVar?: string;
|
|
30
|
+
readonly executorCreateArgs?: string[];
|
|
31
|
+
readonly executorUpdateVar?: string;
|
|
32
|
+
readonly isExpressionBodyCreate: boolean;
|
|
33
|
+
readonly isExpressionBodyUpdate: boolean;
|
|
34
|
+
readonly expressionBodyArgs: string[] | undefined;
|
|
35
|
+
readonly isGuardedCreate: boolean;
|
|
36
|
+
readonly guardConfig: GuardConfig | undefined;
|
|
37
|
+
};
|
|
38
|
+
export declare function analyzeHandlerArrow(arrowText: string): ParsedHandlerInfo;
|
|
39
|
+
export declare function generatePerformBlock(analysis: ParsedHandlerInfo, schemaType: string, indent: string): string | null;
|
|
40
|
+
export declare function scanForCandidates(rootDir: string): FileAnalysis[];
|
|
41
|
+
export type FileAnalysis = {
|
|
42
|
+
readonly filePath: string;
|
|
43
|
+
readonly pattern: "free-form-write" | "pipeline-write" | "query-handler" | "other";
|
|
44
|
+
readonly convertible: boolean;
|
|
45
|
+
readonly reason: string;
|
|
46
|
+
readonly handlerLine?: number;
|
|
47
|
+
};
|
|
48
|
+
export declare function analyzeFile(filePath: string): FileAnalysis | null;
|
|
49
|
+
export declare function runCodemod(rootDir: string, options?: CodemodOptions): Promise<CodemodReport>;
|
|
50
|
+
export declare function convertFile(filePath: string, project?: Project, options?: CodemodOptions): Promise<CodemodResult>;
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
// biome-ignore-all lint/style/noNonNullAssertion: regex capture-group indexing — match-success implies non-null capture
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { Glob } from "bun";
|
|
5
|
+
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
6
|
+
export function analyzeHandlerArrow(arrowText) {
|
|
7
|
+
const text = arrowText.trim();
|
|
8
|
+
let handlerBodyText;
|
|
9
|
+
let isStaticReturn = false;
|
|
10
|
+
let isSimpleExecutorCreate = false;
|
|
11
|
+
let isSimpleExecutorUpdate = false;
|
|
12
|
+
let hasConditionalLogic = false;
|
|
13
|
+
let executorName;
|
|
14
|
+
let executorCreateVar;
|
|
15
|
+
let executorCreateArgs;
|
|
16
|
+
let executorUpdateVar;
|
|
17
|
+
let isExpressionBodyCreate = false;
|
|
18
|
+
let isExpressionBodyUpdate = false;
|
|
19
|
+
let expressionBodyArgs;
|
|
20
|
+
let isGuardedCreate = false;
|
|
21
|
+
let guardConfig;
|
|
22
|
+
if (text.startsWith("async (")) {
|
|
23
|
+
const arrowIdx = text.indexOf("=>");
|
|
24
|
+
if (arrowIdx === -1) {
|
|
25
|
+
return {
|
|
26
|
+
handlerBodyText: text,
|
|
27
|
+
isStaticReturn: false,
|
|
28
|
+
isSimpleExecutorCreate: false,
|
|
29
|
+
isSimpleExecutorUpdate: false,
|
|
30
|
+
hasConditionalLogic: false,
|
|
31
|
+
isExpressionBodyCreate: false,
|
|
32
|
+
isExpressionBodyUpdate: false,
|
|
33
|
+
expressionBodyArgs: undefined,
|
|
34
|
+
isGuardedCreate: false,
|
|
35
|
+
guardConfig: undefined,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const afterArrow = text.slice(arrowIdx + 2).trim();
|
|
39
|
+
if (afterArrow.startsWith("(")) {
|
|
40
|
+
isStaticReturn = true;
|
|
41
|
+
handlerBodyText = afterArrow;
|
|
42
|
+
}
|
|
43
|
+
else if (afterArrow.startsWith("{")) {
|
|
44
|
+
handlerBodyText = afterArrow;
|
|
45
|
+
const lines = handlerBodyText
|
|
46
|
+
.split("\n")
|
|
47
|
+
.map((l) => l.trim())
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
// Detect conditional logic (if/try/for/while/catch)
|
|
50
|
+
hasConditionalLogic = lines.some((l) => l.startsWith("if ") ||
|
|
51
|
+
l.startsWith("try ") ||
|
|
52
|
+
l.startsWith("for ") ||
|
|
53
|
+
l.startsWith("while ") ||
|
|
54
|
+
l.startsWith("} else"));
|
|
55
|
+
const [, createVar, createExecutor] = handlerBodyText.match(/const\s+(\w+)\s*=\s*await\s+(\w+)\.create\s*\(/) ?? [];
|
|
56
|
+
const updateMatch = handlerBodyText.match(/const\s+(\w+)\s*=\s*await\s+(\w+)\.update\s*\(/);
|
|
57
|
+
const hasSimpleReturn = /return\s*\{[^}]*isSuccess:\s*true/.test(handlerBodyText);
|
|
58
|
+
if (createVar !== undefined &&
|
|
59
|
+
createExecutor !== undefined &&
|
|
60
|
+
hasSimpleReturn &&
|
|
61
|
+
!hasConditionalLogic) {
|
|
62
|
+
isSimpleExecutorCreate = true;
|
|
63
|
+
executorName = createExecutor;
|
|
64
|
+
executorCreateVar = createVar;
|
|
65
|
+
executorCreateArgs = extractArgs(handlerBodyText, createExecutor, "create");
|
|
66
|
+
}
|
|
67
|
+
else if (createVar !== undefined &&
|
|
68
|
+
createExecutor !== undefined &&
|
|
69
|
+
hasSimpleReturn &&
|
|
70
|
+
hasConditionalLogic) {
|
|
71
|
+
// Try to match the guard pattern: if (!<var>.isSuccess) { return fail } return success
|
|
72
|
+
const guard = extractGuardedPattern(handlerBodyText, createVar);
|
|
73
|
+
if (guard) {
|
|
74
|
+
isGuardedCreate = true;
|
|
75
|
+
guardConfig = guard;
|
|
76
|
+
executorName = createExecutor;
|
|
77
|
+
executorCreateVar = createVar;
|
|
78
|
+
executorCreateArgs = extractArgs(handlerBodyText, createExecutor, "create");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (updateMatch &&
|
|
82
|
+
hasSimpleReturn &&
|
|
83
|
+
!hasConditionalLogic &&
|
|
84
|
+
!isSimpleExecutorCreate &&
|
|
85
|
+
!isGuardedCreate) {
|
|
86
|
+
isSimpleExecutorUpdate = true;
|
|
87
|
+
executorName = updateMatch[2];
|
|
88
|
+
executorUpdateVar = updateMatch[1];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
handlerBodyText = afterArrow;
|
|
93
|
+
// Expression body — check for member-call patterns like
|
|
94
|
+
// `crud.create(event.payload)` or `crud.update({ id, changes })`.
|
|
95
|
+
const exprCallMatch = afterArrow.match(/^(\w+)\.(create|update)\s*\(/);
|
|
96
|
+
if (exprCallMatch) {
|
|
97
|
+
executorName = exprCallMatch[1];
|
|
98
|
+
const method = exprCallMatch[2];
|
|
99
|
+
expressionBodyArgs = extractExprArgs(afterArrow, executorName, method);
|
|
100
|
+
if (method === "create") {
|
|
101
|
+
isExpressionBodyCreate = true;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
isExpressionBodyUpdate = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (afterArrow.startsWith("(") && afterArrow.endsWith(")")) {
|
|
108
|
+
isStaticReturn = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
handlerBodyText = text;
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
handlerBodyText,
|
|
117
|
+
isStaticReturn,
|
|
118
|
+
isSimpleExecutorCreate,
|
|
119
|
+
isSimpleExecutorUpdate,
|
|
120
|
+
hasConditionalLogic,
|
|
121
|
+
executorName,
|
|
122
|
+
executorCreateVar,
|
|
123
|
+
executorCreateArgs,
|
|
124
|
+
executorUpdateVar,
|
|
125
|
+
isExpressionBodyCreate,
|
|
126
|
+
isExpressionBodyUpdate,
|
|
127
|
+
expressionBodyArgs,
|
|
128
|
+
isGuardedCreate,
|
|
129
|
+
guardConfig,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function extractArgs(body, executorVar, method) {
|
|
133
|
+
const pattern = new RegExp(`const\\s+\\w+\\s*=\\s*await\\s+${escapeRegex(executorVar)}\\.${method}\\s*\\(`);
|
|
134
|
+
const match = body.match(pattern);
|
|
135
|
+
if (!match)
|
|
136
|
+
return undefined;
|
|
137
|
+
const startIdx = (match.index ?? 0) + match[0].length;
|
|
138
|
+
let depth = 1;
|
|
139
|
+
let endIdx = startIdx;
|
|
140
|
+
while (endIdx < body.length && depth > 0) {
|
|
141
|
+
if (body[endIdx] === "(")
|
|
142
|
+
depth++;
|
|
143
|
+
else if (body[endIdx] === ")")
|
|
144
|
+
depth--;
|
|
145
|
+
if (depth > 0)
|
|
146
|
+
endIdx++;
|
|
147
|
+
}
|
|
148
|
+
const argsStr = body.slice(startIdx, endIdx);
|
|
149
|
+
return splitTopLevel(argsStr);
|
|
150
|
+
}
|
|
151
|
+
function escapeRegex(str) {
|
|
152
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
153
|
+
}
|
|
154
|
+
function splitTopLevel(args) {
|
|
155
|
+
const result = [];
|
|
156
|
+
let depth = 0;
|
|
157
|
+
let current = "";
|
|
158
|
+
for (const ch of args) {
|
|
159
|
+
if (ch === "(" || ch === "{" || ch === "[")
|
|
160
|
+
depth++;
|
|
161
|
+
else if (ch === ")" || ch === "}" || ch === "]")
|
|
162
|
+
depth--;
|
|
163
|
+
if (ch === "," && depth === 0) {
|
|
164
|
+
result.push(current.trim());
|
|
165
|
+
current = "";
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
current += ch;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (current.trim())
|
|
172
|
+
result.push(current.trim());
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
function extractExprArgs(body, executorVar, method) {
|
|
176
|
+
const escaped = executorVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
177
|
+
const pattern = new RegExp(`${escaped}\\.${method}\\s*\\(`);
|
|
178
|
+
const match = body.match(pattern);
|
|
179
|
+
if (!match)
|
|
180
|
+
return undefined;
|
|
181
|
+
const startIdx = (match.index ?? 0) + match[0].length;
|
|
182
|
+
let depth = 1;
|
|
183
|
+
let endIdx = startIdx;
|
|
184
|
+
while (endIdx < body.length && depth > 0) {
|
|
185
|
+
if (body[endIdx] === "(")
|
|
186
|
+
depth++;
|
|
187
|
+
else if (body[endIdx] === ")")
|
|
188
|
+
depth--;
|
|
189
|
+
if (depth > 0)
|
|
190
|
+
endIdx++;
|
|
191
|
+
}
|
|
192
|
+
const argsStr = body.slice(startIdx, endIdx);
|
|
193
|
+
return splitTopLevel(argsStr);
|
|
194
|
+
}
|
|
195
|
+
function transpileEventRefs(argsStr) {
|
|
196
|
+
// Transform `event.<prop>` → `ctx.event.<prop>` inside a resolver fn.
|
|
197
|
+
// The original handler has `event` as a parameter; inside the pipeline
|
|
198
|
+
// resolver, `event` lives on `ctx`, not as a standalone binding.
|
|
199
|
+
// `ctx` references stay as-is (the resolver also receives ctx).
|
|
200
|
+
return argsStr.replace(/\bevent\./g, "ctx.event.");
|
|
201
|
+
}
|
|
202
|
+
function transpileGuardRefs(body, varName) {
|
|
203
|
+
// Transform `<var>` → `ctx.steps.result` in guard body text.
|
|
204
|
+
// The original handler references the executor result via
|
|
205
|
+
// `result.isSuccess`, `result.error`, `result.data`, or standalone
|
|
206
|
+
// `return result`. Inside the compute step, executor result lives at
|
|
207
|
+
// `ctx.steps.result`. `\b` word boundary ensures we only match the
|
|
208
|
+
// exact variable name.
|
|
209
|
+
return body.replace(new RegExp(`\\b${escapeRegex(varName)}\\b`, "g"), "ctx.steps.result");
|
|
210
|
+
}
|
|
211
|
+
function extractGuardedPattern(body, varName) {
|
|
212
|
+
const escaped = escapeRegex(varName);
|
|
213
|
+
// Match: if (!<varName>.isSuccess) { return ... }
|
|
214
|
+
const ifOpenPattern = new RegExp(`if\\s*\\(!${escaped}\\.isSuccess\\)\\s*\\{`);
|
|
215
|
+
const ifOpenMatch = body.match(ifOpenPattern);
|
|
216
|
+
if (!ifOpenMatch)
|
|
217
|
+
return undefined;
|
|
218
|
+
// Extract condition
|
|
219
|
+
const condPattern = new RegExp(`if\\s*\\((!${escaped}\\.isSuccess)\\)`);
|
|
220
|
+
const condMatch = body.match(condPattern);
|
|
221
|
+
if (!condMatch)
|
|
222
|
+
return undefined;
|
|
223
|
+
const condition = condMatch[1];
|
|
224
|
+
// Extract if body (between { and matching })
|
|
225
|
+
const ifBodyStart = (ifOpenMatch.index ?? 0) + ifOpenMatch[0].length;
|
|
226
|
+
let depth = 1;
|
|
227
|
+
let ifBodyEnd = ifBodyStart;
|
|
228
|
+
while (ifBodyEnd < body.length && depth > 0) {
|
|
229
|
+
if (body[ifBodyEnd] === "{")
|
|
230
|
+
depth++;
|
|
231
|
+
else if (body[ifBodyEnd] === "}")
|
|
232
|
+
depth--;
|
|
233
|
+
if (depth > 0)
|
|
234
|
+
ifBodyEnd++;
|
|
235
|
+
}
|
|
236
|
+
const ifBodyContent = body.slice(ifBodyStart, ifBodyEnd).trim();
|
|
237
|
+
const failRetMatch = ifBodyContent.match(/return\s*([^;]+)\s*;/);
|
|
238
|
+
if (!failRetMatch)
|
|
239
|
+
return undefined;
|
|
240
|
+
const failureReturn = failRetMatch[1].trim();
|
|
241
|
+
// Extract trailing return after the if block
|
|
242
|
+
const afterIf = body.slice(ifBodyEnd + 1).trim();
|
|
243
|
+
const succRetMatch = afterIf.match(/return\s*([^;]+)\s*;/);
|
|
244
|
+
if (!succRetMatch)
|
|
245
|
+
return undefined;
|
|
246
|
+
const successReturn = succRetMatch[1].trim();
|
|
247
|
+
return { condition, failureReturn, successReturn };
|
|
248
|
+
}
|
|
249
|
+
export function generatePerformBlock(analysis, schemaType, indent) {
|
|
250
|
+
const steps = [];
|
|
251
|
+
if (analysis.isStaticReturn) {
|
|
252
|
+
let body = analysis.handlerBodyText.trim();
|
|
253
|
+
if (body.startsWith("(") && body.endsWith(")")) {
|
|
254
|
+
body = body.slice(1, -1);
|
|
255
|
+
}
|
|
256
|
+
steps.push(`r.step.return((ctx) => (${body}))`);
|
|
257
|
+
}
|
|
258
|
+
else if (analysis.isSimpleExecutorCreate &&
|
|
259
|
+
analysis.executorName &&
|
|
260
|
+
analysis.executorCreateArgs) {
|
|
261
|
+
const dataArg = analysis.executorCreateArgs[0] ?? "{}";
|
|
262
|
+
steps.push(`r.step.aggregate.create("result", { executor: ${analysis.executorName}, data: (ctx) => ${dataArg} })`);
|
|
263
|
+
steps.push(`r.step.return((ctx) => ({ isSuccess: true, data: ctx.steps.result }))`);
|
|
264
|
+
}
|
|
265
|
+
else if (analysis.isSimpleExecutorUpdate && analysis.executorName) {
|
|
266
|
+
steps.push(`r.step.aggregate.update("result", { executor: ${analysis.executorName}, id: event.payload.id, changes: event.payload.changes })`);
|
|
267
|
+
steps.push(`r.step.return((ctx) => ({ isSuccess: true, data: ctx.steps.result }))`);
|
|
268
|
+
}
|
|
269
|
+
else if (analysis.isExpressionBodyCreate &&
|
|
270
|
+
analysis.executorName &&
|
|
271
|
+
analysis.expressionBodyArgs) {
|
|
272
|
+
const dataArg = transpileEventRefs(analysis.expressionBodyArgs[0] ?? "{}");
|
|
273
|
+
steps.push(`r.step.aggregate.create("result", { executor: ${analysis.executorName}, data: (ctx) => ${dataArg} })`);
|
|
274
|
+
steps.push(`r.step.return((ctx) => ({ isSuccess: true, data: ctx.steps.result }))`);
|
|
275
|
+
}
|
|
276
|
+
else if (analysis.isExpressionBodyUpdate &&
|
|
277
|
+
analysis.executorName &&
|
|
278
|
+
analysis.expressionBodyArgs) {
|
|
279
|
+
const argStr = transpileEventRefs(analysis.expressionBodyArgs[0] ?? "{}");
|
|
280
|
+
steps.push(`r.step.compute("result", (ctx) => ${analysis.executorName}.update(${argStr}))`);
|
|
281
|
+
steps.push(`r.step.return((ctx) => ({ isSuccess: true, data: ctx.steps.result }))`);
|
|
282
|
+
}
|
|
283
|
+
else if (analysis.isGuardedCreate &&
|
|
284
|
+
analysis.executorName &&
|
|
285
|
+
analysis.executorCreateArgs &&
|
|
286
|
+
analysis.guardConfig) {
|
|
287
|
+
const varName = analysis.executorCreateVar ?? "result";
|
|
288
|
+
const dataArg = analysis.executorCreateArgs[0] ?? "{}";
|
|
289
|
+
const guard = analysis.guardConfig;
|
|
290
|
+
const condition = guard.condition; // already references varName
|
|
291
|
+
const failureReturn = transpileGuardRefs(guard.failureReturn, varName);
|
|
292
|
+
const successReturn = transpileGuardRefs(guard.successReturn, varName);
|
|
293
|
+
// The condition references the original var name — transpile it too
|
|
294
|
+
const condTranspiled = transpileGuardRefs(condition, varName);
|
|
295
|
+
steps.push(`r.step.aggregate.create("result", { executor: ${analysis.executorName}, data: (ctx) => ${dataArg} })`);
|
|
296
|
+
steps.push(`r.step.compute("outcome", (ctx) => {
|
|
297
|
+
if (${condTranspiled}) {
|
|
298
|
+
return ${failureReturn};
|
|
299
|
+
}
|
|
300
|
+
return ${successReturn};
|
|
301
|
+
})`);
|
|
302
|
+
steps.push(`r.step.return((ctx) => ctx.steps.outcome)`);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
const stepIndent = indent;
|
|
308
|
+
const stepsStr = steps.map((s) => `${stepIndent} ${s}`).join(",\n");
|
|
309
|
+
const pipelineType = schemaType ? `<${schemaType}, unknown>` : "";
|
|
310
|
+
return [
|
|
311
|
+
`perform: stepsPipeline${pipelineType}(({ event, r }) => [`,
|
|
312
|
+
stepsStr,
|
|
313
|
+
` ${stepIndent}]),`,
|
|
314
|
+
].join("\n");
|
|
315
|
+
}
|
|
316
|
+
function inferSchemaType(objLiteral) {
|
|
317
|
+
const schemaProp = objLiteral.getProperty("schema");
|
|
318
|
+
if (!schemaProp)
|
|
319
|
+
return "unknown";
|
|
320
|
+
if (!Node.isPropertyAssignment(schemaProp))
|
|
321
|
+
return "unknown";
|
|
322
|
+
const init = schemaProp.getInitializer();
|
|
323
|
+
if (!init)
|
|
324
|
+
return "unknown";
|
|
325
|
+
const text = init.getText();
|
|
326
|
+
// Only return simple names (like `InvoiceSchema`), not inline definitions
|
|
327
|
+
if (/^[A-Za-z_$][\w$.]*$/.test(text)) {
|
|
328
|
+
return `typeof ${text}`;
|
|
329
|
+
}
|
|
330
|
+
return ""; // Empty = omit type params
|
|
331
|
+
}
|
|
332
|
+
function findFiles(rootDir) {
|
|
333
|
+
const glob = new Glob("**/*.write.ts");
|
|
334
|
+
const files = [];
|
|
335
|
+
for (const file of glob.scanSync(rootDir)) {
|
|
336
|
+
files.push(join(rootDir, file));
|
|
337
|
+
}
|
|
338
|
+
return files.sort();
|
|
339
|
+
}
|
|
340
|
+
export function scanForCandidates(rootDir) {
|
|
341
|
+
const files = findFiles(rootDir);
|
|
342
|
+
const results = [];
|
|
343
|
+
for (const file of files) {
|
|
344
|
+
const analysis = analyzeFile(file);
|
|
345
|
+
if (analysis)
|
|
346
|
+
results.push(analysis);
|
|
347
|
+
}
|
|
348
|
+
return results;
|
|
349
|
+
}
|
|
350
|
+
export function analyzeFile(filePath) {
|
|
351
|
+
if (!existsSync(filePath))
|
|
352
|
+
return null;
|
|
353
|
+
try {
|
|
354
|
+
const content = readFileSync(filePath, "utf8");
|
|
355
|
+
if (!content.includes("defineWriteHandler") && !content.includes("defineQueryHandler")) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
if (content.includes("defineWriteHandler") && content.includes("perform:")) {
|
|
359
|
+
return {
|
|
360
|
+
filePath,
|
|
361
|
+
pattern: "pipeline-write",
|
|
362
|
+
convertible: false,
|
|
363
|
+
reason: "already_uses_pipeline_form",
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
if (content.includes("defineWriteHandler")) {
|
|
367
|
+
return {
|
|
368
|
+
filePath,
|
|
369
|
+
pattern: "free-form-write",
|
|
370
|
+
convertible: true,
|
|
371
|
+
reason: "free_form_write_handler",
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (content.includes("defineQueryHandler")) {
|
|
375
|
+
return {
|
|
376
|
+
filePath,
|
|
377
|
+
pattern: "query-handler",
|
|
378
|
+
convertible: false,
|
|
379
|
+
reason: "query_handlers_not_convertible",
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
export async function runCodemod(rootDir, options = {}) {
|
|
389
|
+
const project = new Project({
|
|
390
|
+
skipAddingFilesFromTsConfig: true,
|
|
391
|
+
skipFileDependencyResolution: true,
|
|
392
|
+
});
|
|
393
|
+
const candidates = scanForCandidates(rootDir);
|
|
394
|
+
const writeHandlers = candidates.filter((c) => c.pattern === "free-form-write");
|
|
395
|
+
console.log(`\n Scanning ${rootDir}...`);
|
|
396
|
+
console.log(` Found ${writeHandlers.length} file(s) with free-form write handlers.\n`);
|
|
397
|
+
if (writeHandlers.length === 0) {
|
|
398
|
+
return { results: [], converted: 0, skipped: 0, errors: 0 };
|
|
399
|
+
}
|
|
400
|
+
const results = [];
|
|
401
|
+
for (const candidate of writeHandlers) {
|
|
402
|
+
const result = await convertFile(candidate.filePath, project, options);
|
|
403
|
+
results.push(result);
|
|
404
|
+
}
|
|
405
|
+
const converted = results.filter((r) => r.status === "converted").length;
|
|
406
|
+
const skipped = results.filter((r) => r.status === "skipped").length;
|
|
407
|
+
const errors = results.filter((r) => r.status === "error").length;
|
|
408
|
+
console.log(`\n Results: ${converted} converted, ${skipped} skipped, ${errors} errors\n`);
|
|
409
|
+
if (options.verbose) {
|
|
410
|
+
for (const r of results) {
|
|
411
|
+
if (r.status === "error") {
|
|
412
|
+
console.log(` ✗ ${relative(process.cwd(), r.filePath)}: ${r.reason}`);
|
|
413
|
+
}
|
|
414
|
+
else if (r.status === "converted") {
|
|
415
|
+
console.log(` ✓ ${relative(process.cwd(), r.filePath)}`);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
console.log(` - ${relative(process.cwd(), r.filePath)}: ${r.reason}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return { results, converted, skipped, errors };
|
|
423
|
+
}
|
|
424
|
+
function contentHasPipelineImport(content) {
|
|
425
|
+
// Check the actual import line, not later usage of the word `pipeline`
|
|
426
|
+
// in the perform block. Single-line import matching only (standard
|
|
427
|
+
// formatting in this codebase — never multi-line).
|
|
428
|
+
const importLine = content
|
|
429
|
+
.split("\n")
|
|
430
|
+
.find((l) => l.includes("import") && l.includes("@cosmicdrift/kumiko-framework/engine"));
|
|
431
|
+
return !!importLine && importLine.includes("stepsPipeline");
|
|
432
|
+
}
|
|
433
|
+
function ensurePipelineImport(content) {
|
|
434
|
+
if (contentHasPipelineImport(content))
|
|
435
|
+
return null;
|
|
436
|
+
// Single-line regex is safe — imports in this codebase are always
|
|
437
|
+
// `import { ... } from "@cosmicdrift/kumiko-framework/engine"`.
|
|
438
|
+
const importRegex = /import\s*\{([^}]*)\}\s*from\s*["']@cosmicdrift\/kumiko-framework\/engine["']/;
|
|
439
|
+
const match = content.match(importRegex);
|
|
440
|
+
if (match) {
|
|
441
|
+
const existingImports = (match[1] ?? "").trim();
|
|
442
|
+
const newImports = existingImports ? `${existingImports}, stepsPipeline` : "stepsPipeline";
|
|
443
|
+
return content.replace(importRegex, `import { ${newImports} } from "@cosmicdrift/kumiko-framework/engine"`);
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
export async function convertFile(filePath, project, options = {}) {
|
|
448
|
+
try {
|
|
449
|
+
const contentBefore = readFileSync(filePath, "utf8");
|
|
450
|
+
const proj = project ??
|
|
451
|
+
new Project({
|
|
452
|
+
skipAddingFilesFromTsConfig: true,
|
|
453
|
+
skipFileDependencyResolution: true,
|
|
454
|
+
});
|
|
455
|
+
const sourceFile = proj.addSourceFileAtPath(filePath);
|
|
456
|
+
const handlerCalls = findDefineWriteHandlerCalls(sourceFile);
|
|
457
|
+
if (handlerCalls.length === 0) {
|
|
458
|
+
return { filePath, status: "skipped", reason: "no_define_write_handler_calls" };
|
|
459
|
+
}
|
|
460
|
+
let content = contentBefore;
|
|
461
|
+
let hadChanges = false;
|
|
462
|
+
for (const call of handlerCalls) {
|
|
463
|
+
const arg = call.getArguments()[0];
|
|
464
|
+
if (!arg || arg.getKind() !== SyntaxKind.ObjectLiteralExpression) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const objLiteral = arg.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
468
|
+
if (!objLiteral)
|
|
469
|
+
continue;
|
|
470
|
+
const handlerProp = objLiteral.getProperty("handler");
|
|
471
|
+
if (!handlerProp)
|
|
472
|
+
continue;
|
|
473
|
+
const propAssign = handlerProp.asKind(SyntaxKind.PropertyAssignment);
|
|
474
|
+
if (!propAssign)
|
|
475
|
+
continue;
|
|
476
|
+
const handlerInit = propAssign.getInitializer();
|
|
477
|
+
if (!handlerInit)
|
|
478
|
+
continue;
|
|
479
|
+
const analysis = analyzeHandlerArrow(handlerInit.getText());
|
|
480
|
+
if (!analysis.isStaticReturn &&
|
|
481
|
+
!analysis.isSimpleExecutorCreate &&
|
|
482
|
+
!analysis.isSimpleExecutorUpdate &&
|
|
483
|
+
!analysis.isExpressionBodyCreate &&
|
|
484
|
+
!analysis.isExpressionBodyUpdate &&
|
|
485
|
+
!analysis.isGuardedCreate) {
|
|
486
|
+
if (options.verbose) {
|
|
487
|
+
console.log(` ~ ${relative(process.cwd(), filePath)}: non-trivial handler, skipping`);
|
|
488
|
+
}
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
const schemaType = inferSchemaType(objLiteral);
|
|
492
|
+
const indent = propAssign.getIndentationText() ?? " ";
|
|
493
|
+
const performBlock = generatePerformBlock(analysis, schemaType, indent);
|
|
494
|
+
if (!performBlock)
|
|
495
|
+
continue;
|
|
496
|
+
const start = propAssign.getStart();
|
|
497
|
+
let end = propAssign.getEnd();
|
|
498
|
+
// Consume trailing comma after the property assignment
|
|
499
|
+
if (end < content.length && content[end] === ",") {
|
|
500
|
+
end++;
|
|
501
|
+
}
|
|
502
|
+
content = content.slice(0, start) + performBlock + content.slice(end);
|
|
503
|
+
hadChanges = true;
|
|
504
|
+
}
|
|
505
|
+
if (hadChanges) {
|
|
506
|
+
const importResult = ensurePipelineImport(content);
|
|
507
|
+
if (importResult) {
|
|
508
|
+
content = importResult;
|
|
509
|
+
}
|
|
510
|
+
else if (options.verbose) {
|
|
511
|
+
console.log(` ~ ${filePath}: stepsPipeline import not needed or already present`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (hadChanges && !options.dryRun) {
|
|
515
|
+
writeFileSync(filePath, content, "utf8");
|
|
516
|
+
}
|
|
517
|
+
const status = hadChanges ? "converted" : "skipped";
|
|
518
|
+
const reason = hadChanges
|
|
519
|
+
? "handler replaced with perform: stepsPipeline(...)"
|
|
520
|
+
: "no convertible handler";
|
|
521
|
+
return { filePath, status, reason };
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
return {
|
|
525
|
+
filePath,
|
|
526
|
+
status: "error",
|
|
527
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function findDefineWriteHandlerCalls(sourceFile) {
|
|
532
|
+
return sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => {
|
|
533
|
+
const expr = call.getExpression();
|
|
534
|
+
return expr.getText() === "defineWriteHandler";
|
|
535
|
+
});
|
|
536
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type UnknownEntityFieldType = {
|
|
2
|
+
readonly fieldName: string;
|
|
3
|
+
readonly type: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Scans `definition.fields.<name>.type` for values outside the FieldDefinition
|
|
7
|
+
* catalogue. Tolerant of RawRefSentinel at every level (definition itself,
|
|
8
|
+
* `fields`, a single field, or its `type`) — those are unresolvable source
|
|
9
|
+
* references, not validated field shapes, and readDataLiteralNode already
|
|
10
|
+
* chose not to fail the whole extraction over them.
|
|
11
|
+
*/
|
|
12
|
+
export declare function findUnknownEntityFieldTypes(definition: unknown): readonly UnknownEntityFieldType[];
|
|
13
|
+
export declare function describeUnknownFieldType(entry: UnknownEntityFieldType): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Shared field-type validation for EntityDefinition. Used by both the
|
|
2
|
+
// static extractor (extractEntity, parse-time ParseError) and the runtime
|
|
3
|
+
// PatternChange validator (pattern-change-schema.ts) so the two never drift
|
|
4
|
+
// into reporting different "unknown field type" catalogues.
|
|
5
|
+
import { FIELD_TYPE_NAMES } from "@cosmicdrift/kumiko-types/fields";
|
|
6
|
+
import { isPlainObject, isRawRefSentinel } from "./extractors/shared.js";
|
|
7
|
+
/**
|
|
8
|
+
* Scans `definition.fields.<name>.type` for values outside the FieldDefinition
|
|
9
|
+
* catalogue. Tolerant of RawRefSentinel at every level (definition itself,
|
|
10
|
+
* `fields`, a single field, or its `type`) — those are unresolvable source
|
|
11
|
+
* references, not validated field shapes, and readDataLiteralNode already
|
|
12
|
+
* chose not to fail the whole extraction over them.
|
|
13
|
+
*/
|
|
14
|
+
export function findUnknownEntityFieldTypes(definition) {
|
|
15
|
+
if (isRawRefSentinel(definition) || !isPlainObject(definition))
|
|
16
|
+
return [];
|
|
17
|
+
const fields = definition["fields"];
|
|
18
|
+
if (isRawRefSentinel(fields) || !isPlainObject(fields))
|
|
19
|
+
return [];
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
22
|
+
if (isRawRefSentinel(fieldDef) || !isPlainObject(fieldDef))
|
|
23
|
+
continue;
|
|
24
|
+
const type = fieldDef["type"];
|
|
25
|
+
if (isRawRefSentinel(type) || typeof type !== "string")
|
|
26
|
+
continue;
|
|
27
|
+
if (!FIELD_TYPE_NAMES.includes(type)) {
|
|
28
|
+
out.push({ fieldName, type });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
export function describeUnknownFieldType(entry) {
|
|
34
|
+
return `unknown field type "${entry.type}"; expected one of: ${FIELD_TYPE_NAMES.join(", ")}`;
|
|
35
|
+
}
|