blokctl 1.1.0 โ 1.2.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/dist/commands/create/project.d.ts +2 -0
- package/dist/commands/create/project.js +13 -10
- package/dist/commands/create/utils/Examples.d.ts +2 -2
- package/dist/commands/create/utils/Examples.js +69 -103
- package/dist/commands/create/workflow.js +8 -6
- package/dist/commands/dev/index.js +26 -0
- package/dist/commands/install/node.d.ts +1 -0
- package/dist/commands/install/node.js +3 -39
- package/dist/commands/migrate/index.js +25 -0
- package/dist/commands/migrate/nodesTs.d.ts +31 -0
- package/dist/commands/migrate/nodesTs.js +484 -0
- package/dist/commands/migrate/refs.d.ts +15 -0
- package/dist/commands/migrate/refs.js +706 -0
- package/dist/commands/nodes/index.js +10 -0
- package/dist/commands/nodes/listNodes.d.ts +2 -0
- package/dist/commands/nodes/listNodes.js +20 -15
- package/dist/commands/nodes/syncNodes.d.ts +7 -0
- package/dist/commands/nodes/syncNodes.js +138 -0
- package/dist/commands/nodes/syncNodes.test.d.ts +1 -0
- package/dist/commands/nodes/syncNodes.test.js +322 -0
- package/dist/commands/publish/workflow.d.ts +1 -0
- package/dist/commands/publish/workflow.js +9 -0
- package/dist/commands/publish/workflow.test.d.ts +1 -0
- package/dist/commands/publish/workflow.test.js +22 -0
- package/dist/services/observability-stack.real-prometheus.test.d.ts +1 -0
- package/dist/services/observability-stack.real-prometheus.test.js +95 -0
- package/dist/services/observability-stack.real-tempo.test.d.ts +1 -0
- package/dist/services/observability-stack.real-tempo.test.js +86 -0
- package/dist/studio-dist/assets/index-QEvKRE4T.js +42 -0
- package/dist/studio-dist/index.html +1 -1
- package/package.json +3 -2
- package/dist/studio-dist/assets/index-CnFqCRQe.js +0 -42
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
import { promises as fsp } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import color from "picocolors";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
const MARKER = "blok-migrate: hand-migrate (dynamic expression / branch.when not handle-safe)";
|
|
6
|
+
export async function migrateRefs(opts) {
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
const root = path.resolve(cwd, opts.dir ?? ".");
|
|
9
|
+
const dryRun = opts.dryRun === true;
|
|
10
|
+
const writeBackup = opts.backup !== false;
|
|
11
|
+
console.log(color.cyan("\n๐ Field-aware ref codemod"));
|
|
12
|
+
console.log(color.dim("Rewrites pure step input refs to structural handles; marks dynamic expressions.\n"));
|
|
13
|
+
const files = await collectWorkflowFiles(root);
|
|
14
|
+
if (files.length === 0) {
|
|
15
|
+
console.log(color.yellow("No TS or JSON workflow files found."));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const totals = { changed: 0, migrated: 0, marked: 0, errors: 0 };
|
|
19
|
+
for (const file of files) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = await fsp.readFile(file, "utf8");
|
|
22
|
+
const result = file.endsWith(".json") ? migrateJsonText(raw) : migrateTsSource(raw, path.basename(file));
|
|
23
|
+
totals.migrated += result.stats.migrated;
|
|
24
|
+
totals.marked += result.stats.marked;
|
|
25
|
+
if (result.changed) {
|
|
26
|
+
totals.changed += 1;
|
|
27
|
+
if (!dryRun) {
|
|
28
|
+
if (writeBackup)
|
|
29
|
+
await fsp.writeFile(`${file}.bak`, raw);
|
|
30
|
+
await fsp.writeFile(file, result.value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
printFileResult(file, result.changed, result.stats);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
totals.errors += 1;
|
|
37
|
+
console.log(` ${color.red("โ")} ${color.cyan(path.relative(cwd, file))} ${color.red(err.message)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
console.log(`\nTotal: ${files.length} ยท ${color.green(`${dryRun ? "would change" : "changed"}: ${totals.changed}`)} ยท migrated refs: ${totals.migrated} ยท marked: ${totals.marked}`);
|
|
41
|
+
if (dryRun)
|
|
42
|
+
console.log(color.dim("Dry run โ no files written."));
|
|
43
|
+
if (totals.errors > 0)
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
export function migrateJsonText(raw) {
|
|
47
|
+
const parsed = JSON.parse(raw);
|
|
48
|
+
const result = migrateJsonWorkflow(parsed);
|
|
49
|
+
return {
|
|
50
|
+
value: `${JSON.stringify(result.value, null, "\t")}\n`,
|
|
51
|
+
changed: result.changed,
|
|
52
|
+
stats: result.stats,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function migrateJsonWorkflow(workflow) {
|
|
56
|
+
const stats = emptyStats();
|
|
57
|
+
const value = cloneJson(workflow);
|
|
58
|
+
if (isPlainObject(value) && Array.isArray(value.steps)) {
|
|
59
|
+
migrateJsonStepArray(value.steps, stats);
|
|
60
|
+
}
|
|
61
|
+
return { value, changed: stats.migrated + stats.marked > 0, stats };
|
|
62
|
+
}
|
|
63
|
+
export function migrateTsSource(source, fileName = "workflow.ts") {
|
|
64
|
+
const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
65
|
+
const replacements = [];
|
|
66
|
+
const markerPositions = new Set();
|
|
67
|
+
const helperImports = new Set();
|
|
68
|
+
const stats = emptyStats();
|
|
69
|
+
function visit(node) {
|
|
70
|
+
if (ts.isArrayLiteralExpression(node) && isStepArray(node)) {
|
|
71
|
+
migrateTsStepArray(node, source, replacements, markerPositions, helperImports, stats);
|
|
72
|
+
}
|
|
73
|
+
ts.forEachChild(node, visit);
|
|
74
|
+
}
|
|
75
|
+
visit(sf);
|
|
76
|
+
const withMarkers = [...markerPositions]
|
|
77
|
+
.sort((a, b) => b - a)
|
|
78
|
+
.reduce((text, pos) => `${text.slice(0, pos)}${markerFor(source, pos)}${text.slice(pos)}`, source);
|
|
79
|
+
const adjusted = replacements.map((r) => ({
|
|
80
|
+
...r,
|
|
81
|
+
start: r.start + insertedBefore(markerPositions, r.start, source),
|
|
82
|
+
end: r.end + insertedBefore(markerPositions, r.end, source),
|
|
83
|
+
}));
|
|
84
|
+
const rewritten = applyReplacements(withMarkers, adjusted);
|
|
85
|
+
const value = helperImports.size > 0 ? ensureHelperImports(rewritten, helperImports) : rewritten;
|
|
86
|
+
return { value, changed: value !== source, stats };
|
|
87
|
+
}
|
|
88
|
+
function migrateJsonStepArray(rawSteps, stats) {
|
|
89
|
+
const steps = rawSteps.filter(isPlainObject);
|
|
90
|
+
const infos = steps.map(readJsonStepInfo);
|
|
91
|
+
const byStateKey = new Map(infos.filter(isStepInfo).map((info) => [info.stateKey, info]));
|
|
92
|
+
for (let i = 0; i < steps.length; i++) {
|
|
93
|
+
const step = steps[i];
|
|
94
|
+
const info = infos[i];
|
|
95
|
+
const previous = previousConcreteInfo(infos, i);
|
|
96
|
+
const ctx = { previous, stepsByStateKey: byStateKey };
|
|
97
|
+
let marked = false;
|
|
98
|
+
const alreadyMarkedStep = jsonStepMarked(step);
|
|
99
|
+
if (isPlainObject(step.inputs)) {
|
|
100
|
+
marked ||= migrateJsonInputs(step.inputs, info?.use, ctx, stats, !alreadyMarkedStep);
|
|
101
|
+
}
|
|
102
|
+
if (isPlainObject(step.branch)) {
|
|
103
|
+
marked ||= migrateJsonBranchWhen(step.branch, stats, !alreadyMarkedStep);
|
|
104
|
+
}
|
|
105
|
+
if (marked)
|
|
106
|
+
markJsonStep(step);
|
|
107
|
+
recurseJsonControlFlow(step, stats);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function migrateJsonInputs(value, use, ctx, stats, canMark) {
|
|
111
|
+
if (isStructuralSentinel(value))
|
|
112
|
+
return false;
|
|
113
|
+
let marked = false;
|
|
114
|
+
if (Array.isArray(value)) {
|
|
115
|
+
for (let i = 0; i < value.length; i++) {
|
|
116
|
+
const result = migrateJsonValue(value[i], undefined, use, ctx, stats, canMark);
|
|
117
|
+
value[i] = result.value;
|
|
118
|
+
marked ||= result.marked;
|
|
119
|
+
}
|
|
120
|
+
return marked;
|
|
121
|
+
}
|
|
122
|
+
if (!isPlainObject(value))
|
|
123
|
+
return false;
|
|
124
|
+
for (const [key, child] of Object.entries(value)) {
|
|
125
|
+
const result = migrateJsonValue(child, key, use, ctx, stats, canMark);
|
|
126
|
+
value[key] = result.value;
|
|
127
|
+
marked ||= result.marked;
|
|
128
|
+
}
|
|
129
|
+
return marked;
|
|
130
|
+
}
|
|
131
|
+
function migrateJsonValue(value, key, use, ctx, stats, canMark) {
|
|
132
|
+
if (use === "@blokjs/expr" && key === "expression")
|
|
133
|
+
return { value, marked: false };
|
|
134
|
+
if (typeof value === "string") {
|
|
135
|
+
const parsed = parseRefValue(value, ctx);
|
|
136
|
+
if (parsed?.kind === "ref") {
|
|
137
|
+
stats.migrated += 1;
|
|
138
|
+
return { value: parsed.ref, marked: false };
|
|
139
|
+
}
|
|
140
|
+
if (parsed?.kind === "tpl") {
|
|
141
|
+
stats.migrated += 1;
|
|
142
|
+
return { value: parsed.tpl, marked: false };
|
|
143
|
+
}
|
|
144
|
+
if (parsed?.kind === "dynamic") {
|
|
145
|
+
if (!canMark)
|
|
146
|
+
return { value, marked: false };
|
|
147
|
+
stats.marked += 1;
|
|
148
|
+
return { value, marked: true };
|
|
149
|
+
}
|
|
150
|
+
return { value, marked: false };
|
|
151
|
+
}
|
|
152
|
+
if (Array.isArray(value) || isPlainObject(value)) {
|
|
153
|
+
const marked = migrateJsonInputs(value, use, ctx, stats, canMark);
|
|
154
|
+
return { value, marked };
|
|
155
|
+
}
|
|
156
|
+
return { value, marked: false };
|
|
157
|
+
}
|
|
158
|
+
function recurseJsonControlFlow(step, stats) {
|
|
159
|
+
if (isPlainObject(step.branch)) {
|
|
160
|
+
if (Array.isArray(step.branch.then))
|
|
161
|
+
migrateJsonStepArray(step.branch.then, stats);
|
|
162
|
+
if (Array.isArray(step.branch.else))
|
|
163
|
+
migrateJsonStepArray(step.branch.else, stats);
|
|
164
|
+
}
|
|
165
|
+
if (isPlainObject(step.forEach) && Array.isArray(step.forEach.do))
|
|
166
|
+
migrateJsonStepArray(step.forEach.do, stats);
|
|
167
|
+
if (isPlainObject(step.loop) && Array.isArray(step.loop.do))
|
|
168
|
+
migrateJsonStepArray(step.loop.do, stats);
|
|
169
|
+
if (isPlainObject(step.tryCatch)) {
|
|
170
|
+
if (Array.isArray(step.tryCatch.try))
|
|
171
|
+
migrateJsonStepArray(step.tryCatch.try, stats);
|
|
172
|
+
if (Array.isArray(step.tryCatch.catch))
|
|
173
|
+
migrateJsonStepArray(step.tryCatch.catch, stats);
|
|
174
|
+
if (Array.isArray(step.tryCatch.finally))
|
|
175
|
+
migrateJsonStepArray(step.tryCatch.finally, stats);
|
|
176
|
+
}
|
|
177
|
+
if (isPlainObject(step.switch) && Array.isArray(step.switch.cases)) {
|
|
178
|
+
for (const c of step.switch.cases) {
|
|
179
|
+
if (!isPlainObject(c))
|
|
180
|
+
continue;
|
|
181
|
+
if (Array.isArray(c.steps))
|
|
182
|
+
migrateJsonStepArray(c.steps, stats);
|
|
183
|
+
if (Array.isArray(c.do))
|
|
184
|
+
migrateJsonStepArray(c.do, stats);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (isPlainObject(step.switch) && Array.isArray(step.switch.default))
|
|
188
|
+
migrateJsonStepArray(step.switch.default, stats);
|
|
189
|
+
}
|
|
190
|
+
function migrateJsonBranchWhen(branch, stats, canMark) {
|
|
191
|
+
if (typeof branch.when !== "string")
|
|
192
|
+
return false;
|
|
193
|
+
const result = analyzeBranchWhen(branch.when);
|
|
194
|
+
if (result.kind === "convert") {
|
|
195
|
+
if (branch.when !== result.rawWhen) {
|
|
196
|
+
branch.when = result.rawWhen;
|
|
197
|
+
stats.migrated += 1;
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
if (result.kind !== "mark" || !canMark)
|
|
202
|
+
return false;
|
|
203
|
+
stats.marked += 1;
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
function migrateTsStepArray(array, source, replacements, markerPositions, helperImports, stats) {
|
|
207
|
+
const elements = array.elements.filter(ts.isObjectLiteralExpression);
|
|
208
|
+
const infos = elements.map(readTsStepInfo);
|
|
209
|
+
const byStateKey = new Map(infos.filter(isStepInfo).map((info) => [info.stateKey, info]));
|
|
210
|
+
for (let i = 0; i < elements.length; i++) {
|
|
211
|
+
const step = elements[i];
|
|
212
|
+
const info = infos[i];
|
|
213
|
+
const previous = previousConcreteInfo(infos, i);
|
|
214
|
+
const ctx = { previous, stepsByStateKey: byStateKey };
|
|
215
|
+
const beforeMarked = stats.marked;
|
|
216
|
+
const alreadyMarkedStep = alreadyMarked(source, step.getStart());
|
|
217
|
+
migrateTsBranchWhen(step, source, replacements, helperImports, stats, !alreadyMarkedStep);
|
|
218
|
+
const inputs = getProperty(step, "inputs");
|
|
219
|
+
if (inputs && ts.isObjectLiteralExpression(inputs.initializer)) {
|
|
220
|
+
migrateTsInputs(inputs.initializer, info?.use, ctx, source, replacements, stats, !alreadyMarkedStep);
|
|
221
|
+
}
|
|
222
|
+
if (stats.marked > beforeMarked && !alreadyMarkedStep)
|
|
223
|
+
markerPositions.add(step.getStart());
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function migrateTsBranchWhen(step, source, replacements, helperImports, stats, canMark) {
|
|
227
|
+
const branch = getProperty(step, "branch");
|
|
228
|
+
if (!branch || !ts.isObjectLiteralExpression(branch.initializer))
|
|
229
|
+
return;
|
|
230
|
+
const when = getProperty(branch.initializer, "when");
|
|
231
|
+
if (!when)
|
|
232
|
+
return;
|
|
233
|
+
const init = when.initializer;
|
|
234
|
+
if (isDollarPath(init))
|
|
235
|
+
return;
|
|
236
|
+
if (ts.isCallExpression(init) || !ts.isStringLiteralLike(init))
|
|
237
|
+
return;
|
|
238
|
+
const result = analyzeBranchWhen(init.text);
|
|
239
|
+
if (result.kind === "convert") {
|
|
240
|
+
replacements.push({ start: init.getStart(), end: init.getEnd(), text: result.tsExpr });
|
|
241
|
+
for (const helper of result.helpers)
|
|
242
|
+
helperImports.add(helper);
|
|
243
|
+
stats.migrated += 1;
|
|
244
|
+
}
|
|
245
|
+
else if (result.kind === "mark" && canMark) {
|
|
246
|
+
stats.marked += 1;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function migrateTsInputs(obj, use, ctx, source, replacements, stats, canMark) {
|
|
250
|
+
for (const prop of obj.properties) {
|
|
251
|
+
if (!ts.isPropertyAssignment(prop))
|
|
252
|
+
continue;
|
|
253
|
+
const key = propertyNameText(prop.name);
|
|
254
|
+
if (use === "@blokjs/expr" && key === "expression")
|
|
255
|
+
continue;
|
|
256
|
+
migrateTsExpression(prop.initializer, use, ctx, source, replacements, stats, canMark);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function migrateTsExpression(node, use, ctx, source, replacements, stats, canMark) {
|
|
260
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
261
|
+
if (isTsStructuralSentinel(node))
|
|
262
|
+
return;
|
|
263
|
+
migrateTsInputs(node, use, ctx, source, replacements, stats, canMark);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
267
|
+
for (const item of node.elements)
|
|
268
|
+
migrateTsExpression(item, use, ctx, source, replacements, stats, canMark);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const raw = tsExpressionValue(node, source);
|
|
272
|
+
if (!raw)
|
|
273
|
+
return;
|
|
274
|
+
const parsed = parseRefValue(raw, ctx);
|
|
275
|
+
if (parsed?.kind === "ref") {
|
|
276
|
+
replacements.push({ start: node.getStart(), end: node.getEnd(), text: refToTs(parsed.ref) });
|
|
277
|
+
stats.migrated += 1;
|
|
278
|
+
}
|
|
279
|
+
else if (parsed?.kind === "tpl") {
|
|
280
|
+
replacements.push({ start: node.getStart(), end: node.getEnd(), text: tplToTs(parsed.tpl) });
|
|
281
|
+
stats.migrated += 1;
|
|
282
|
+
}
|
|
283
|
+
else if (parsed?.kind === "dynamic") {
|
|
284
|
+
if (!canMark)
|
|
285
|
+
return;
|
|
286
|
+
stats.marked += 1;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function parseRefValue(value, ctx) {
|
|
290
|
+
if (value.startsWith("js/")) {
|
|
291
|
+
const expr = value.slice(3);
|
|
292
|
+
const tpl = parseTemplate(expr, ctx);
|
|
293
|
+
if (tpl)
|
|
294
|
+
return tpl;
|
|
295
|
+
const ref = parsePurePath(expr, ctx);
|
|
296
|
+
if (ref)
|
|
297
|
+
return { kind: "ref", ref };
|
|
298
|
+
return referencesRuntimeExpression(expr) ? { kind: "dynamic" } : null;
|
|
299
|
+
}
|
|
300
|
+
if (value.startsWith("$.")) {
|
|
301
|
+
const ref = parsePurePath(value, ctx);
|
|
302
|
+
return ref ? { kind: "ref", ref } : { kind: "dynamic" };
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
function analyzeBranchWhen(value) {
|
|
307
|
+
const expr = value.trim();
|
|
308
|
+
if (expr.length === 0)
|
|
309
|
+
return { kind: "none" };
|
|
310
|
+
if (expr.startsWith("$.") || expr.startsWith("js/"))
|
|
311
|
+
return { kind: "mark" };
|
|
312
|
+
const parsed = parseExpression(expr);
|
|
313
|
+
if (!parsed)
|
|
314
|
+
return referencesRuntimeExpression(expr) ? { kind: "mark" } : { kind: "none" };
|
|
315
|
+
const path = expressionToBranchPath(parsed);
|
|
316
|
+
if (path)
|
|
317
|
+
return { kind: "convert", rawWhen: path.raw, tsExpr: path.proxy, helpers: ["$"] };
|
|
318
|
+
if (!ts.isBinaryExpression(parsed))
|
|
319
|
+
return referencesRuntimeExpression(expr) ? { kind: "mark" } : { kind: "none" };
|
|
320
|
+
const op = binaryOperator(parsed.operatorToken.kind);
|
|
321
|
+
if (!op)
|
|
322
|
+
return { kind: "mark" };
|
|
323
|
+
const leftPath = expressionToBranchPath(parsed.left);
|
|
324
|
+
const rightPath = expressionToBranchPath(parsed.right);
|
|
325
|
+
if (!leftPath || rightPath)
|
|
326
|
+
return { kind: "mark" };
|
|
327
|
+
const literal = literalExpression(parsed.right);
|
|
328
|
+
if (!literal)
|
|
329
|
+
return { kind: "mark" };
|
|
330
|
+
if (op === "===" && literal.raw === "true") {
|
|
331
|
+
return { kind: "convert", rawWhen: leftPath.raw, tsExpr: leftPath.proxy, helpers: ["$"] };
|
|
332
|
+
}
|
|
333
|
+
const helper = helperForOperator(op);
|
|
334
|
+
return {
|
|
335
|
+
kind: "convert",
|
|
336
|
+
rawWhen: `${leftPath.raw} ${op} ${literal.raw}`,
|
|
337
|
+
tsExpr: `${helper}(${leftPath.proxy}, ${literal.ts})`,
|
|
338
|
+
helpers: ["$", helper],
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function binaryOperator(kind) {
|
|
342
|
+
switch (kind) {
|
|
343
|
+
case ts.SyntaxKind.EqualsEqualsEqualsToken:
|
|
344
|
+
return "===";
|
|
345
|
+
case ts.SyntaxKind.ExclamationEqualsEqualsToken:
|
|
346
|
+
return "!==";
|
|
347
|
+
case ts.SyntaxKind.GreaterThanToken:
|
|
348
|
+
return ">";
|
|
349
|
+
case ts.SyntaxKind.GreaterThanEqualsToken:
|
|
350
|
+
return ">=";
|
|
351
|
+
case ts.SyntaxKind.LessThanToken:
|
|
352
|
+
return "<";
|
|
353
|
+
case ts.SyntaxKind.LessThanEqualsToken:
|
|
354
|
+
return "<=";
|
|
355
|
+
default:
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function helperForOperator(op) {
|
|
360
|
+
switch (op) {
|
|
361
|
+
case "===":
|
|
362
|
+
return "eq";
|
|
363
|
+
case "!==":
|
|
364
|
+
return "ne";
|
|
365
|
+
case ">":
|
|
366
|
+
return "gt";
|
|
367
|
+
case ">=":
|
|
368
|
+
return "gte";
|
|
369
|
+
case "<":
|
|
370
|
+
return "lt";
|
|
371
|
+
case "<=":
|
|
372
|
+
return "lte";
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function literalExpression(expr) {
|
|
376
|
+
if (ts.isStringLiteralLike(expr) ||
|
|
377
|
+
ts.isNumericLiteral(expr) ||
|
|
378
|
+
expr.kind === ts.SyntaxKind.TrueKeyword ||
|
|
379
|
+
expr.kind === ts.SyntaxKind.FalseKeyword ||
|
|
380
|
+
expr.kind === ts.SyntaxKind.NullKeyword ||
|
|
381
|
+
(ts.isIdentifier(expr) && expr.text === "undefined")) {
|
|
382
|
+
const raw = expr.getText();
|
|
383
|
+
return { raw, ts: raw };
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
function expressionToBranchPath(expr) {
|
|
388
|
+
const path = expressionPath(expr);
|
|
389
|
+
if (!path)
|
|
390
|
+
return null;
|
|
391
|
+
const [root, ...rest] = path;
|
|
392
|
+
if (root !== "ctx")
|
|
393
|
+
return null;
|
|
394
|
+
const [field, ...tail] = rest;
|
|
395
|
+
if (field === "request" || field === "req")
|
|
396
|
+
return branchPath("ctx.request", "$.request", tail);
|
|
397
|
+
if (field === "state" || field === "vars")
|
|
398
|
+
return branchPath("ctx.state", "$.state", tail);
|
|
399
|
+
if (field === "response" || field === "prev")
|
|
400
|
+
return branchPath("ctx.response", "$.prev", tail);
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
function branchPath(rawRoot, proxyRoot, tail) {
|
|
404
|
+
const suffix = tail.map(accessSegment).join("");
|
|
405
|
+
return { raw: `${rawRoot}${suffix}`, proxy: `${proxyRoot}${suffix}` };
|
|
406
|
+
}
|
|
407
|
+
function accessSegment(seg) {
|
|
408
|
+
if (typeof seg === "number")
|
|
409
|
+
return `[${seg}]`;
|
|
410
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(seg) ? `.${seg}` : `[${JSON.stringify(seg)}]`;
|
|
411
|
+
}
|
|
412
|
+
function parseTemplate(expr, ctx) {
|
|
413
|
+
const wrapped = parseExpression(expr);
|
|
414
|
+
if (!wrapped)
|
|
415
|
+
return null;
|
|
416
|
+
if (ts.isNoSubstitutionTemplateLiteral(wrapped))
|
|
417
|
+
return null;
|
|
418
|
+
if (!ts.isTemplateExpression(wrapped))
|
|
419
|
+
return null;
|
|
420
|
+
const segments = [wrapped.head.text];
|
|
421
|
+
for (const span of wrapped.templateSpans) {
|
|
422
|
+
const ref = expressionToRef(span.expression, ctx);
|
|
423
|
+
if (!ref)
|
|
424
|
+
return { kind: "dynamic" };
|
|
425
|
+
segments.push(ref, span.literal.text);
|
|
426
|
+
}
|
|
427
|
+
return { kind: "tpl", tpl: { $tpl: segments } };
|
|
428
|
+
}
|
|
429
|
+
function parsePurePath(expr, ctx) {
|
|
430
|
+
if (expr.includes("?."))
|
|
431
|
+
return null;
|
|
432
|
+
const parsed = parseExpression(expr);
|
|
433
|
+
return parsed ? expressionToRef(parsed, ctx) : null;
|
|
434
|
+
}
|
|
435
|
+
function expressionToRef(expr, ctx) {
|
|
436
|
+
const path = expressionPath(expr);
|
|
437
|
+
if (!path)
|
|
438
|
+
return null;
|
|
439
|
+
const [root, ...rest] = path;
|
|
440
|
+
if (root === "$")
|
|
441
|
+
return dollarPathToRef(rest, ctx);
|
|
442
|
+
if (root !== "ctx")
|
|
443
|
+
return null;
|
|
444
|
+
return ctxPathToRef(rest, ctx);
|
|
445
|
+
}
|
|
446
|
+
function dollarPathToRef(pathParts, ctx) {
|
|
447
|
+
const [root, ...path] = pathParts;
|
|
448
|
+
if (root === "vars" && path[0] === "_worker_job")
|
|
449
|
+
return null;
|
|
450
|
+
if (root === "state" || root === "vars")
|
|
451
|
+
return statePathToRef(path, ctx);
|
|
452
|
+
if (root === "req" || root === "request")
|
|
453
|
+
return { $ref: { step: "@trigger", path } };
|
|
454
|
+
if (root === "prev" || root === "response")
|
|
455
|
+
return prevPathToRef(path, ctx);
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
function ctxPathToRef(pathParts, ctx) {
|
|
459
|
+
const [root, ...path] = pathParts;
|
|
460
|
+
if (root === "vars" && path[0] === "_worker_job")
|
|
461
|
+
return null;
|
|
462
|
+
if (root === "state" || root === "vars")
|
|
463
|
+
return statePathToRef(path, ctx);
|
|
464
|
+
if (root === "request" || root === "req")
|
|
465
|
+
return { $ref: { step: "@trigger", path } };
|
|
466
|
+
if (root === "prev")
|
|
467
|
+
return prevPathToRef(path, ctx);
|
|
468
|
+
if (root === "response" && path[0] === "data")
|
|
469
|
+
return prevPathToRef(path.slice(1), ctx);
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
function statePathToRef(pathParts, ctx) {
|
|
473
|
+
const [stateKey, ...path] = pathParts;
|
|
474
|
+
if (typeof stateKey !== "string")
|
|
475
|
+
return null;
|
|
476
|
+
const owner = ctx.stepsByStateKey.get(stateKey);
|
|
477
|
+
if (owner?.spread && typeof path[0] === "string") {
|
|
478
|
+
return { $ref: { step: path[0], path: path.slice(1) } };
|
|
479
|
+
}
|
|
480
|
+
return { $ref: { step: stateKey, path } };
|
|
481
|
+
}
|
|
482
|
+
function prevPathToRef(pathParts, ctx) {
|
|
483
|
+
if (!ctx.previous || ctx.previous.ephemeral)
|
|
484
|
+
return null;
|
|
485
|
+
const path = pathParts[0] === "data" ? pathParts.slice(1) : pathParts;
|
|
486
|
+
return { $ref: { step: ctx.previous.stateKey, path } };
|
|
487
|
+
}
|
|
488
|
+
function expressionPath(expr) {
|
|
489
|
+
if (ts.isIdentifier(expr))
|
|
490
|
+
return [expr.text];
|
|
491
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
492
|
+
const base = expressionPath(expr.expression);
|
|
493
|
+
return base ? [...base, expr.name.text] : null;
|
|
494
|
+
}
|
|
495
|
+
if (ts.isElementAccessExpression(expr)) {
|
|
496
|
+
const base = expressionPath(expr.expression);
|
|
497
|
+
const seg = elementSegment(expr.argumentExpression);
|
|
498
|
+
return base && seg !== null ? [...base, seg] : null;
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
function elementSegment(expr) {
|
|
503
|
+
if (!expr)
|
|
504
|
+
return null;
|
|
505
|
+
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr))
|
|
506
|
+
return expr.text;
|
|
507
|
+
if (ts.isNumericLiteral(expr))
|
|
508
|
+
return Number(expr.text);
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
function referencesRuntimeExpression(expr) {
|
|
512
|
+
return /\bctx\b|\$\.|process\.env|Date\.|Array\.|=>|\?\?|\|\||&&|\?|\.\.\.|new\s+|\bfunction\b/.test(expr);
|
|
513
|
+
}
|
|
514
|
+
function parseExpression(expr) {
|
|
515
|
+
const sf = ts.createSourceFile("expr.ts", `const __blok = ${expr};`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
516
|
+
const stmt = sf.statements[0];
|
|
517
|
+
if (!stmt || !ts.isVariableStatement(stmt))
|
|
518
|
+
return null;
|
|
519
|
+
const decl = stmt.declarationList.declarations[0];
|
|
520
|
+
return decl?.initializer ?? null;
|
|
521
|
+
}
|
|
522
|
+
function readJsonStepInfo(step) {
|
|
523
|
+
const id = typeof step.id === "string" ? step.id : typeof step.name === "string" ? step.name : undefined;
|
|
524
|
+
if (!id)
|
|
525
|
+
return undefined;
|
|
526
|
+
return {
|
|
527
|
+
id,
|
|
528
|
+
stateKey: typeof step.as === "string" ? step.as : id,
|
|
529
|
+
ephemeral: step.ephemeral === true,
|
|
530
|
+
spread: step.spread === true,
|
|
531
|
+
use: typeof step.use === "string" ? step.use : typeof step.node === "string" ? step.node : undefined,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function readTsStepInfo(step) {
|
|
535
|
+
const id = literalProperty(step, "id") ?? literalProperty(step, "name");
|
|
536
|
+
if (!id)
|
|
537
|
+
return undefined;
|
|
538
|
+
return {
|
|
539
|
+
id,
|
|
540
|
+
stateKey: literalProperty(step, "as") ?? id,
|
|
541
|
+
ephemeral: booleanProperty(step, "ephemeral") === true,
|
|
542
|
+
spread: booleanProperty(step, "spread") === true,
|
|
543
|
+
use: literalProperty(step, "use") ?? literalProperty(step, "node"),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function previousConcreteInfo(infos, index) {
|
|
547
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
548
|
+
if (infos[i])
|
|
549
|
+
return infos[i];
|
|
550
|
+
}
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
function getProperty(obj, name) {
|
|
554
|
+
return obj.properties.find((prop) => ts.isPropertyAssignment(prop) && propertyNameText(prop.name) === name);
|
|
555
|
+
}
|
|
556
|
+
function literalProperty(obj, name) {
|
|
557
|
+
const prop = getProperty(obj, name);
|
|
558
|
+
return prop && ts.isStringLiteralLike(prop.initializer) ? prop.initializer.text : undefined;
|
|
559
|
+
}
|
|
560
|
+
function booleanProperty(obj, name) {
|
|
561
|
+
const prop = getProperty(obj, name);
|
|
562
|
+
if (!prop)
|
|
563
|
+
return undefined;
|
|
564
|
+
if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword)
|
|
565
|
+
return true;
|
|
566
|
+
if (prop.initializer.kind === ts.SyntaxKind.FalseKeyword)
|
|
567
|
+
return false;
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
function propertyNameText(name) {
|
|
571
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name))
|
|
572
|
+
return name.text;
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
function tsExpressionValue(node, source) {
|
|
576
|
+
if (ts.isStringLiteralLike(node))
|
|
577
|
+
return node.text;
|
|
578
|
+
if (isDollarPath(node))
|
|
579
|
+
return source.slice(node.getStart(), node.getEnd());
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
function isDollarPath(node) {
|
|
583
|
+
return expressionPath(node)?.[0] === "$";
|
|
584
|
+
}
|
|
585
|
+
function isStepArray(array) {
|
|
586
|
+
const parent = array.parent;
|
|
587
|
+
if (!ts.isPropertyAssignment(parent))
|
|
588
|
+
return false;
|
|
589
|
+
const name = propertyNameText(parent.name);
|
|
590
|
+
return name === "steps" || name === "do" || name === "then" || name === "else" || name === "try" || name === "catch";
|
|
591
|
+
}
|
|
592
|
+
function isTsStructuralSentinel(obj) {
|
|
593
|
+
return obj.properties.some((prop) => ts.isPropertyAssignment(prop) &&
|
|
594
|
+
(propertyNameText(prop.name) === "$ref" || propertyNameText(prop.name) === "$tpl"));
|
|
595
|
+
}
|
|
596
|
+
function isStructuralSentinel(value) {
|
|
597
|
+
return isPlainObject(value) && ("$ref" in value || "$tpl" in value);
|
|
598
|
+
}
|
|
599
|
+
function markJsonStep(step) {
|
|
600
|
+
const ui = isPlainObject(step.ui) ? step.ui : {};
|
|
601
|
+
const notes = typeof ui.notes === "string" ? ui.notes : "";
|
|
602
|
+
if (notes.includes(MARKER))
|
|
603
|
+
return;
|
|
604
|
+
step.ui = { ...ui, notes: notes ? `${notes}\n${MARKER}` : MARKER };
|
|
605
|
+
}
|
|
606
|
+
function jsonStepMarked(step) {
|
|
607
|
+
return isPlainObject(step.ui) && typeof step.ui.notes === "string" && step.ui.notes.includes(MARKER);
|
|
608
|
+
}
|
|
609
|
+
function applyReplacements(source, replacements) {
|
|
610
|
+
return [...replacements]
|
|
611
|
+
.sort((a, b) => b.start - a.start)
|
|
612
|
+
.reduce((text, r) => `${text.slice(0, r.start)}${r.text}${text.slice(r.end)}`, source);
|
|
613
|
+
}
|
|
614
|
+
function alreadyMarked(source, pos) {
|
|
615
|
+
const lineStart = source.lastIndexOf("\n", pos - 1) + 1;
|
|
616
|
+
return source.slice(Math.max(0, lineStart - 200), pos).includes(MARKER);
|
|
617
|
+
}
|
|
618
|
+
function markerFor(source, pos) {
|
|
619
|
+
const lineStart = source.lastIndexOf("\n", pos - 1) + 1;
|
|
620
|
+
const indent = source.slice(lineStart, pos).match(/^\s*/)?.[0] ?? "";
|
|
621
|
+
return `${indent}// ${MARKER}\n`;
|
|
622
|
+
}
|
|
623
|
+
function insertedBefore(positions, offset, source) {
|
|
624
|
+
let inserted = 0;
|
|
625
|
+
for (const pos of positions) {
|
|
626
|
+
if (pos < offset)
|
|
627
|
+
inserted += markerFor(source, pos).length;
|
|
628
|
+
}
|
|
629
|
+
return inserted;
|
|
630
|
+
}
|
|
631
|
+
function ensureHelperImports(source, helpers) {
|
|
632
|
+
const ordered = ["$", "eq", "ne", "gt", "gte", "lt", "lte"].filter((name) => helpers.has(name));
|
|
633
|
+
if (ordered.length === 0)
|
|
634
|
+
return source;
|
|
635
|
+
const namedImport = /import\s*{([^}]*)}\s*from\s*["'](@blokjs\/(?:helper|core))["'];?/m;
|
|
636
|
+
const match = namedImport.exec(source);
|
|
637
|
+
if (!match)
|
|
638
|
+
return `import { ${ordered.join(", ")} } from "@blokjs/helper";\n${source}`;
|
|
639
|
+
const existing = new Set(match[1]
|
|
640
|
+
.split(",")
|
|
641
|
+
.map((part) => part
|
|
642
|
+
.trim()
|
|
643
|
+
.split(/\s+as\s+/)[0]
|
|
644
|
+
?.trim())
|
|
645
|
+
.filter(Boolean));
|
|
646
|
+
const missing = ordered.filter((name) => !existing.has(name));
|
|
647
|
+
if (missing.length === 0)
|
|
648
|
+
return source;
|
|
649
|
+
const current = match[1].trim();
|
|
650
|
+
const next = current ? `${current}, ${missing.join(", ")}` : missing.join(", ");
|
|
651
|
+
return `${source.slice(0, match.index)}import { ${next} } from "${match[2]}";${source.slice(match.index + match[0].length)}`;
|
|
652
|
+
}
|
|
653
|
+
function refToTs(ref) {
|
|
654
|
+
return `{ $ref: { step: ${JSON.stringify(ref.$ref.step)}, path: ${JSON.stringify(ref.$ref.path)} } }`;
|
|
655
|
+
}
|
|
656
|
+
function tplToTs(tpl) {
|
|
657
|
+
return `{ $tpl: [${tpl.$tpl.map((part) => (isStructuralRef(part) ? refToTs(part) : JSON.stringify(part))).join(", ")}] }`;
|
|
658
|
+
}
|
|
659
|
+
function emptyStats() {
|
|
660
|
+
return { migrated: 0, marked: 0 };
|
|
661
|
+
}
|
|
662
|
+
function cloneJson(value) {
|
|
663
|
+
return JSON.parse(JSON.stringify(value));
|
|
664
|
+
}
|
|
665
|
+
function isPlainObject(value) {
|
|
666
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
667
|
+
return false;
|
|
668
|
+
const proto = Object.getPrototypeOf(value);
|
|
669
|
+
return proto === Object.prototype || proto === null;
|
|
670
|
+
}
|
|
671
|
+
function isStepInfo(value) {
|
|
672
|
+
return value !== undefined;
|
|
673
|
+
}
|
|
674
|
+
function isStructuralRef(value) {
|
|
675
|
+
return isPlainObject(value) && isPlainObject(value.$ref) && typeof value.$ref.step === "string";
|
|
676
|
+
}
|
|
677
|
+
async function collectWorkflowFiles(root) {
|
|
678
|
+
const out = [];
|
|
679
|
+
await walk(root, out);
|
|
680
|
+
out.sort();
|
|
681
|
+
return out;
|
|
682
|
+
}
|
|
683
|
+
async function walk(dir, out) {
|
|
684
|
+
let entries;
|
|
685
|
+
try {
|
|
686
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
for (const entry of entries) {
|
|
692
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist")
|
|
693
|
+
continue;
|
|
694
|
+
const full = path.join(dir, entry.name);
|
|
695
|
+
if (entry.isDirectory())
|
|
696
|
+
await walk(full, out);
|
|
697
|
+
else if (entry.isFile() && /\.(json|ts)$/.test(entry.name))
|
|
698
|
+
out.push(full);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function printFileResult(file, changed, stats) {
|
|
702
|
+
const rel = path.relative(process.cwd(), file);
|
|
703
|
+
const icon = changed ? color.green("โ") : color.dim("โ");
|
|
704
|
+
const note = changed ? `refs: ${stats.migrated}, marked: ${stats.marked}` : "unchanged";
|
|
705
|
+
console.log(` ${icon} ${color.cyan(rel)} ${color.dim(note)}`);
|
|
706
|
+
}
|