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,484 @@
|
|
|
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
|
+
import { nameToIdentifier } from "../gen/appTypes.js";
|
|
6
|
+
const MARKER = "blok-migrate: hand-migrate (node resolution)";
|
|
7
|
+
const HELPER_EXPORTS = {
|
|
8
|
+
"@blokjs/audit-log": "AuditLogNode",
|
|
9
|
+
"@blokjs/ctx-publish": "CtxPublishNode",
|
|
10
|
+
"@blokjs/ctx-publish-many": "CtxPublishManyNode",
|
|
11
|
+
"@blokjs/expr": "ExprNode",
|
|
12
|
+
"@blokjs/hmac-verify": "HmacVerifyNode",
|
|
13
|
+
"@blokjs/in-memory-kv": "InMemoryKvNode",
|
|
14
|
+
"@blokjs/json-schema": "JsonSchemaNode",
|
|
15
|
+
"@blokjs/jwt-verify": "JwtVerifyNode",
|
|
16
|
+
"@blokjs/llm-agent": "LlmAgentNode",
|
|
17
|
+
"@blokjs/llm-stream": "LlmStreamNode",
|
|
18
|
+
"@blokjs/log": "LogNode",
|
|
19
|
+
"@blokjs/metrics-emit": "MetricsEmitNode",
|
|
20
|
+
"@blokjs/pubsub-publish": "PubsubPublishNode",
|
|
21
|
+
"@blokjs/redis-kv": "RedisKvNode",
|
|
22
|
+
"@blokjs/respond": "RespondNode",
|
|
23
|
+
"@blokjs/sse-emit": "SseEmitNode",
|
|
24
|
+
"@blokjs/sse-publish": "SsePublishNode",
|
|
25
|
+
"@blokjs/sse-stream": "SseStreamNode",
|
|
26
|
+
"@blokjs/sse-subscribe": "SseSubscribeNode",
|
|
27
|
+
"@blokjs/throw": "ThrowNode",
|
|
28
|
+
"@blokjs/worker-publish": "WorkerPublishNode",
|
|
29
|
+
"@blokjs/ws-broadcast": "WsBroadcastNode",
|
|
30
|
+
"@blokjs/ws-close": "WsCloseNode",
|
|
31
|
+
"@blokjs/ws-reply": "WsReplyNode",
|
|
32
|
+
};
|
|
33
|
+
export async function migrateNodesTs(opts) {
|
|
34
|
+
const cwd = process.cwd();
|
|
35
|
+
const workflowsDir = path.resolve(cwd, opts.dir ?? "src/workflows");
|
|
36
|
+
const nodesFile = path.resolve(cwd, opts.nodes ?? "src/Nodes.ts");
|
|
37
|
+
const stubsDir = path.resolve(cwd, opts.stubs ?? "nodes-gen");
|
|
38
|
+
const dryRun = opts.dryRun === true;
|
|
39
|
+
const writeBackup = opts.backup !== false;
|
|
40
|
+
const deleteNodes = opts.deleteNodes === true;
|
|
41
|
+
console.log(color.cyan("\n๐ Nodes.ts removal codemod"));
|
|
42
|
+
console.log(color.dim("Rewrites handle-DSL step() string node refs to direct imports/runtime stubs.\n"));
|
|
43
|
+
const nodesSource = await readOptional(nodesFile);
|
|
44
|
+
if (!nodesSource) {
|
|
45
|
+
console.log(color.yellow(`No Nodes.ts found at ${path.relative(cwd, nodesFile)} โ module refs will be unresolved.`));
|
|
46
|
+
}
|
|
47
|
+
const resolver = {
|
|
48
|
+
modules: nodesSource ? await parseNodesMap(nodesSource, nodesFile) : new Map(),
|
|
49
|
+
runtimes: await parseRuntimeStubs(stubsDir),
|
|
50
|
+
};
|
|
51
|
+
const files = (await collectFiles(workflowsDir)).filter((file) => file.endsWith(".ts"));
|
|
52
|
+
const totals = { changed: 0, migrated: 0, marked: 0 };
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
const raw = await fsp.readFile(file, "utf8");
|
|
55
|
+
const result = migrateNodesTsSource(raw, file, resolver);
|
|
56
|
+
totals.migrated += result.stats.migrated;
|
|
57
|
+
totals.marked += result.stats.marked;
|
|
58
|
+
if (result.changed) {
|
|
59
|
+
totals.changed += 1;
|
|
60
|
+
if (!dryRun) {
|
|
61
|
+
if (writeBackup)
|
|
62
|
+
await fsp.writeFile(`${file}.bak`, raw);
|
|
63
|
+
await fsp.writeFile(file, result.value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
printFileResult(file, result.changed, result.stats);
|
|
67
|
+
}
|
|
68
|
+
const remaining = await countStringNodeRefs(workflowsDir);
|
|
69
|
+
if (deleteNodes && remaining === 0 && nodesSource && !dryRun) {
|
|
70
|
+
await fsp.rm(nodesFile);
|
|
71
|
+
console.log(color.green(`Deleted ${path.relative(cwd, nodesFile)} (no string workflow refs remain).`));
|
|
72
|
+
}
|
|
73
|
+
else if (deleteNodes && remaining > 0) {
|
|
74
|
+
console.log(color.yellow(`Keeping Nodes.ts: ${remaining} string workflow node ref(s) still remain.`));
|
|
75
|
+
}
|
|
76
|
+
console.log(`\nTotal: ${files.length} ยท ${color.green(`${dryRun ? "would change" : "changed"}: ${totals.changed}`)} ยท migrated: ${totals.migrated} ยท marked: ${totals.marked}`);
|
|
77
|
+
if (dryRun)
|
|
78
|
+
console.log(color.dim("Dry run โ no files written."));
|
|
79
|
+
}
|
|
80
|
+
export function migrateNodesTsSource(source, file, resolver) {
|
|
81
|
+
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
82
|
+
const replacements = [];
|
|
83
|
+
const markerPositions = new Set();
|
|
84
|
+
const imports = new ImportPlanner(source, file);
|
|
85
|
+
const stats = { migrated: 0, marked: 0 };
|
|
86
|
+
function visit(node) {
|
|
87
|
+
if (ts.isCallExpression(node) && isStepCall(node)) {
|
|
88
|
+
const useArg = node.arguments[1];
|
|
89
|
+
if (useArg && ts.isStringLiteralLike(useArg)) {
|
|
90
|
+
const use = useArg.text;
|
|
91
|
+
const type = stepType(node);
|
|
92
|
+
const resolved = resolveNode(use, type, resolver);
|
|
93
|
+
if (resolved.kind === "mark") {
|
|
94
|
+
if (!alreadyMarked(source, node.getStart())) {
|
|
95
|
+
markerPositions.add(statementStart(source, node));
|
|
96
|
+
stats.marked += 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const ident = imports.add(resolved);
|
|
101
|
+
replacements.push({ start: useArg.getStart(), end: useArg.getEnd(), text: ident });
|
|
102
|
+
stats.migrated += 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
ts.forEachChild(node, visit);
|
|
107
|
+
}
|
|
108
|
+
visit(sf);
|
|
109
|
+
let value = applyReplacements(source, [
|
|
110
|
+
...replacements,
|
|
111
|
+
...[...markerPositions].map((pos) => ({ start: pos, end: pos, text: markerFor(source, pos) })),
|
|
112
|
+
]);
|
|
113
|
+
value = imports.render(value);
|
|
114
|
+
return { value, changed: value !== source, stats };
|
|
115
|
+
}
|
|
116
|
+
export async function parseNodesMap(source, nodesFile) {
|
|
117
|
+
const sf = ts.createSourceFile(nodesFile, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
118
|
+
const imports = importBindings(sf, nodesFile);
|
|
119
|
+
const objects = objectVariables(sf);
|
|
120
|
+
const out = new Map();
|
|
121
|
+
const seenObjects = new Set();
|
|
122
|
+
async function addObject(name) {
|
|
123
|
+
if (seenObjects.has(name))
|
|
124
|
+
return;
|
|
125
|
+
seenObjects.add(name);
|
|
126
|
+
const obj = objects.get(name);
|
|
127
|
+
if (!obj)
|
|
128
|
+
return;
|
|
129
|
+
for (const prop of obj.properties) {
|
|
130
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
131
|
+
const key = propertyName(prop.name);
|
|
132
|
+
if (!key || !ts.isIdentifier(prop.initializer))
|
|
133
|
+
continue;
|
|
134
|
+
const ref = imports.get(prop.initializer.text);
|
|
135
|
+
if (ref)
|
|
136
|
+
out.set(key, ref);
|
|
137
|
+
}
|
|
138
|
+
else if (ts.isSpreadAssignment(prop) && ts.isIdentifier(prop.expression)) {
|
|
139
|
+
const spread = prop.expression.text;
|
|
140
|
+
const spreadImport = imports.get(spread);
|
|
141
|
+
if (spreadImport?.importPath === "@blokjs/helpers") {
|
|
142
|
+
for (const [key, importName] of Object.entries(HELPER_EXPORTS)) {
|
|
143
|
+
out.set(key, { kind: "named", importName, importPath: "@blokjs/helpers", originFile: nodesFile });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else if (spreadImport?.importPath.startsWith(".")) {
|
|
147
|
+
for (const [key, ref] of await parseDefaultObjectBarrel(spreadImport))
|
|
148
|
+
out.set(key, ref);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
await addObject(spread);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const name of objects.keys())
|
|
157
|
+
await addObject(name);
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
export async function parseRuntimeStubs(stubsDir) {
|
|
161
|
+
const files = await collectFiles(stubsDir);
|
|
162
|
+
const out = new Map();
|
|
163
|
+
for (const file of files.filter((f) => f.endsWith(".ts"))) {
|
|
164
|
+
const runtime = path.basename(file, ".ts");
|
|
165
|
+
const source = await fsp.readFile(file, "utf8");
|
|
166
|
+
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
167
|
+
for (const stmt of sf.statements) {
|
|
168
|
+
if (!ts.isVariableStatement(stmt))
|
|
169
|
+
continue;
|
|
170
|
+
if (!stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))
|
|
171
|
+
continue;
|
|
172
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
173
|
+
if (!ts.isIdentifier(decl.name) || !decl.initializer || !ts.isCallExpression(decl.initializer))
|
|
174
|
+
continue;
|
|
175
|
+
if (expressionText(decl.initializer.expression) !== "runtimeNode")
|
|
176
|
+
continue;
|
|
177
|
+
const nameArg = decl.initializer.arguments[0];
|
|
178
|
+
if (!nameArg || !ts.isStringLiteralLike(nameArg))
|
|
179
|
+
continue;
|
|
180
|
+
out.set(`${runtime}|${nameArg.text}`, {
|
|
181
|
+
exportName: decl.name.text,
|
|
182
|
+
importPath: file,
|
|
183
|
+
runtime,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
export async function countStringNodeRefs(root) {
|
|
191
|
+
let count = 0;
|
|
192
|
+
for (const file of await collectFiles(root)) {
|
|
193
|
+
if (file.endsWith(".json")) {
|
|
194
|
+
count += countJsonStringRefs(JSON.parse(await fsp.readFile(file, "utf8")));
|
|
195
|
+
}
|
|
196
|
+
else if (file.endsWith(".ts")) {
|
|
197
|
+
count += countTsStringRefs(await fsp.readFile(file, "utf8"), file);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return count;
|
|
201
|
+
}
|
|
202
|
+
function resolveNode(use, type, resolver) {
|
|
203
|
+
if (type?.startsWith("runtime.")) {
|
|
204
|
+
const ref = resolver.runtimes.get(`${type}|${use}`);
|
|
205
|
+
return ref ? { kind: "runtime", ref } : { kind: "mark", reason: "unresolved" };
|
|
206
|
+
}
|
|
207
|
+
const module = resolver.modules.get(use);
|
|
208
|
+
const runtimeMatches = [...resolver.runtimes.keys()].filter((key) => key.endsWith(`|${use}`));
|
|
209
|
+
if (module && runtimeMatches.length === 0)
|
|
210
|
+
return { kind: "module", ref: module };
|
|
211
|
+
return { kind: "mark", reason: module || runtimeMatches.length > 0 ? "ambiguous" : "unresolved" };
|
|
212
|
+
}
|
|
213
|
+
class ImportPlanner {
|
|
214
|
+
file;
|
|
215
|
+
used = new Set();
|
|
216
|
+
imports = [];
|
|
217
|
+
planned = new Map();
|
|
218
|
+
constructor(source, file) {
|
|
219
|
+
this.file = file;
|
|
220
|
+
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
221
|
+
for (const stmt of sf.statements) {
|
|
222
|
+
if (ts.isImportDeclaration(stmt) && stmt.importClause) {
|
|
223
|
+
const clause = stmt.importClause;
|
|
224
|
+
if (clause.name)
|
|
225
|
+
this.used.add(clause.name.text);
|
|
226
|
+
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
227
|
+
for (const el of clause.namedBindings.elements)
|
|
228
|
+
this.used.add(el.name.text);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
add(resolved) {
|
|
234
|
+
const importPath = resolved.kind === "runtime"
|
|
235
|
+
? relativeImport(this.file, resolved.ref.importPath)
|
|
236
|
+
: importPathFor(this.file, resolved.ref);
|
|
237
|
+
const exportName = resolved.kind === "runtime" ? resolved.ref.exportName : resolved.ref.importName;
|
|
238
|
+
const key = `${resolved.kind}:${importPath}:${exportName}`;
|
|
239
|
+
const planned = this.planned.get(key);
|
|
240
|
+
if (planned)
|
|
241
|
+
return planned;
|
|
242
|
+
const ident = this.used.has(exportName) || this.imports.some((i) => i.path !== importPath && i.named.has(exportName))
|
|
243
|
+
? uniqueIdentifier(`${exportName}${runtimeSuffix(resolved)}`, this.used)
|
|
244
|
+
: uniqueIdentifier(exportName, this.used);
|
|
245
|
+
let entry = this.imports.find((i) => i.path === importPath);
|
|
246
|
+
if (!entry) {
|
|
247
|
+
entry = { path: importPath, named: new Map() };
|
|
248
|
+
this.imports.push(entry);
|
|
249
|
+
}
|
|
250
|
+
if (resolved.kind === "module" && resolved.ref.kind === "default")
|
|
251
|
+
entry.default = ident;
|
|
252
|
+
else
|
|
253
|
+
entry.named.set(exportName, ident);
|
|
254
|
+
this.planned.set(key, ident);
|
|
255
|
+
return ident;
|
|
256
|
+
}
|
|
257
|
+
render(source) {
|
|
258
|
+
if (this.imports.length === 0)
|
|
259
|
+
return source;
|
|
260
|
+
const importLines = this.imports
|
|
261
|
+
.sort((a, b) => a.path.localeCompare(b.path))
|
|
262
|
+
.map((entry) => {
|
|
263
|
+
const named = [...entry.named]
|
|
264
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
265
|
+
.map(([exportName, local]) => (exportName === local ? exportName : `${exportName} as ${local}`));
|
|
266
|
+
if (entry.default && named.length > 0)
|
|
267
|
+
return `import ${entry.default}, { ${named.join(", ")} } from "${entry.path}";`;
|
|
268
|
+
if (entry.default)
|
|
269
|
+
return `import ${entry.default} from "${entry.path}";`;
|
|
270
|
+
return `import { ${named.join(", ")} } from "${entry.path}";`;
|
|
271
|
+
});
|
|
272
|
+
const insertAt = lastImportEnd(source);
|
|
273
|
+
return `${source.slice(0, insertAt)}${insertAt === 0 ? "" : "\n"}${importLines.join("\n")}\n${source.slice(insertAt)}`;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function importBindings(sf, originFile) {
|
|
277
|
+
const out = new Map();
|
|
278
|
+
for (const stmt of sf.statements) {
|
|
279
|
+
if (!ts.isImportDeclaration(stmt) || !stmt.importClause || !ts.isStringLiteral(stmt.moduleSpecifier))
|
|
280
|
+
continue;
|
|
281
|
+
const importPath = stmt.moduleSpecifier.text;
|
|
282
|
+
const clause = stmt.importClause;
|
|
283
|
+
if (clause.name)
|
|
284
|
+
out.set(clause.name.text, { kind: "default", importName: clause.name.text, importPath, originFile });
|
|
285
|
+
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
286
|
+
for (const el of clause.namedBindings.elements) {
|
|
287
|
+
out.set(el.name.text, {
|
|
288
|
+
kind: "named",
|
|
289
|
+
importName: (el.propertyName ?? el.name).text,
|
|
290
|
+
importPath,
|
|
291
|
+
originFile,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
function objectVariables(sf) {
|
|
299
|
+
const out = new Map();
|
|
300
|
+
for (const stmt of sf.statements) {
|
|
301
|
+
if (!ts.isVariableStatement(stmt))
|
|
302
|
+
continue;
|
|
303
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
304
|
+
if (ts.isIdentifier(decl.name) && decl.initializer && ts.isObjectLiteralExpression(decl.initializer)) {
|
|
305
|
+
out.set(decl.name.text, decl.initializer);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
async function parseDefaultObjectBarrel(spreadImport) {
|
|
312
|
+
const file = resolveImportFile(spreadImport.originFile, spreadImport.importPath);
|
|
313
|
+
const source = await readOptional(file);
|
|
314
|
+
if (!source)
|
|
315
|
+
return new Map();
|
|
316
|
+
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
317
|
+
const imports = importBindings(sf, file);
|
|
318
|
+
const objects = objectVariables(sf);
|
|
319
|
+
const defaultName = sf.statements.find(ts.isExportAssignment);
|
|
320
|
+
const obj = defaultName && ts.isIdentifier(defaultName.expression) ? objects.get(defaultName.expression.text) : undefined;
|
|
321
|
+
const out = new Map();
|
|
322
|
+
if (!obj)
|
|
323
|
+
return out;
|
|
324
|
+
for (const prop of obj.properties) {
|
|
325
|
+
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.initializer))
|
|
326
|
+
continue;
|
|
327
|
+
const key = propertyName(prop.name);
|
|
328
|
+
const ref = imports.get(prop.initializer.text);
|
|
329
|
+
if (key && ref)
|
|
330
|
+
out.set(key, ref);
|
|
331
|
+
}
|
|
332
|
+
return out;
|
|
333
|
+
}
|
|
334
|
+
function isStepCall(node) {
|
|
335
|
+
return expressionText(node.expression) === "step" && node.arguments.length >= 2;
|
|
336
|
+
}
|
|
337
|
+
function stepType(node) {
|
|
338
|
+
const opts = node.arguments[3];
|
|
339
|
+
if (!opts || !ts.isObjectLiteralExpression(opts))
|
|
340
|
+
return undefined;
|
|
341
|
+
const prop = opts.properties.find((p) => ts.isPropertyAssignment(p) && propertyName(p.name) === "type");
|
|
342
|
+
return prop && ts.isStringLiteralLike(prop.initializer) ? prop.initializer.text : undefined;
|
|
343
|
+
}
|
|
344
|
+
function countTsStringRefs(source, file) {
|
|
345
|
+
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
346
|
+
let count = 0;
|
|
347
|
+
function visit(node) {
|
|
348
|
+
if (ts.isCallExpression(node) && isStepCall(node) && ts.isStringLiteralLike(node.arguments[1]))
|
|
349
|
+
count += 1;
|
|
350
|
+
if (ts.isPropertyAssignment(node) &&
|
|
351
|
+
propertyName(node.name) === "use" &&
|
|
352
|
+
ts.isStringLiteralLike(node.initializer)) {
|
|
353
|
+
count += 1;
|
|
354
|
+
}
|
|
355
|
+
ts.forEachChild(node, visit);
|
|
356
|
+
}
|
|
357
|
+
visit(sf);
|
|
358
|
+
return count;
|
|
359
|
+
}
|
|
360
|
+
function countJsonStringRefs(value) {
|
|
361
|
+
if (Array.isArray(value))
|
|
362
|
+
return value.reduce((sum, item) => sum + countJsonStringRefs(item), 0);
|
|
363
|
+
if (!isPlainObject(value))
|
|
364
|
+
return 0;
|
|
365
|
+
let count = typeof value.use === "string" || typeof value.node === "string" ? 1 : 0;
|
|
366
|
+
for (const child of Object.values(value))
|
|
367
|
+
count += countJsonStringRefs(child);
|
|
368
|
+
return count;
|
|
369
|
+
}
|
|
370
|
+
function applyReplacements(source, replacements) {
|
|
371
|
+
return [...replacements]
|
|
372
|
+
.sort((a, b) => b.start - a.start)
|
|
373
|
+
.reduce((text, r) => `${text.slice(0, r.start)}${r.text}${text.slice(r.end)}`, source);
|
|
374
|
+
}
|
|
375
|
+
function statementStart(source, node) {
|
|
376
|
+
let current = node;
|
|
377
|
+
while (current.parent && !ts.isExpressionStatement(current))
|
|
378
|
+
current = current.parent;
|
|
379
|
+
const start = current.getStart();
|
|
380
|
+
return source.lastIndexOf("\n", start - 1) + 1;
|
|
381
|
+
}
|
|
382
|
+
function alreadyMarked(source, pos) {
|
|
383
|
+
const lineStart = source.lastIndexOf("\n", pos - 1) + 1;
|
|
384
|
+
return source.slice(Math.max(0, lineStart - 200), pos).includes(MARKER);
|
|
385
|
+
}
|
|
386
|
+
function markerFor(source, pos) {
|
|
387
|
+
const lineEnd = source.indexOf("\n", pos);
|
|
388
|
+
const line = source.slice(pos, lineEnd === -1 ? undefined : lineEnd);
|
|
389
|
+
const indent = line.match(/^\s*/)?.[0] ?? "";
|
|
390
|
+
return `${indent}// ${MARKER}\n`;
|
|
391
|
+
}
|
|
392
|
+
function expressionText(expr) {
|
|
393
|
+
return ts.isIdentifier(expr) ? expr.text : undefined;
|
|
394
|
+
}
|
|
395
|
+
function propertyName(name) {
|
|
396
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name))
|
|
397
|
+
return name.text;
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
function importPathFor(workflowFile, ref) {
|
|
401
|
+
return ref.importPath.startsWith(".")
|
|
402
|
+
? relativeImport(workflowFile, resolveImportFile(ref.originFile, ref.importPath))
|
|
403
|
+
: ref.importPath;
|
|
404
|
+
}
|
|
405
|
+
function resolveImportFile(fromFile, spec) {
|
|
406
|
+
if (!spec.startsWith("."))
|
|
407
|
+
return spec;
|
|
408
|
+
const base = path.resolve(path.dirname(fromFile), spec);
|
|
409
|
+
return path.extname(base) ? base : `${base}.ts`;
|
|
410
|
+
}
|
|
411
|
+
function relativeImport(fromFile, targetFile) {
|
|
412
|
+
let rel = path.relative(path.dirname(fromFile), targetFile).replace(/\\/g, "/").replace(/\.ts$/, "");
|
|
413
|
+
if (!rel.startsWith("."))
|
|
414
|
+
rel = `./${rel}`;
|
|
415
|
+
return rel;
|
|
416
|
+
}
|
|
417
|
+
function runtimeSuffix(resolved) {
|
|
418
|
+
if (resolved.kind !== "runtime")
|
|
419
|
+
return "";
|
|
420
|
+
return pascal(resolved.ref.runtime.replace(/^runtime\./, ""));
|
|
421
|
+
}
|
|
422
|
+
function uniqueIdentifier(base, used) {
|
|
423
|
+
let ident = /^[A-Za-z_$]/.test(base) ? base : `node_${base}`;
|
|
424
|
+
ident = ident.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
425
|
+
let next = ident;
|
|
426
|
+
let i = 2;
|
|
427
|
+
while (used.has(next))
|
|
428
|
+
next = `${ident}${i++}`;
|
|
429
|
+
used.add(next);
|
|
430
|
+
return next;
|
|
431
|
+
}
|
|
432
|
+
function pascal(value) {
|
|
433
|
+
return nameToIdentifier(value).replace(/^./, (s) => s.toUpperCase());
|
|
434
|
+
}
|
|
435
|
+
function lastImportEnd(source) {
|
|
436
|
+
const sf = ts.createSourceFile("source.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
437
|
+
let end = 0;
|
|
438
|
+
for (const stmt of sf.statements) {
|
|
439
|
+
if (ts.isImportDeclaration(stmt))
|
|
440
|
+
end = stmt.getEnd();
|
|
441
|
+
}
|
|
442
|
+
return end;
|
|
443
|
+
}
|
|
444
|
+
function isPlainObject(value) {
|
|
445
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
446
|
+
}
|
|
447
|
+
async function readOptional(file) {
|
|
448
|
+
try {
|
|
449
|
+
return await fsp.readFile(file, "utf8");
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async function collectFiles(root) {
|
|
456
|
+
const out = [];
|
|
457
|
+
await walk(root, out);
|
|
458
|
+
out.sort();
|
|
459
|
+
return out;
|
|
460
|
+
}
|
|
461
|
+
async function walk(dir, out) {
|
|
462
|
+
let entries;
|
|
463
|
+
try {
|
|
464
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
for (const entry of entries) {
|
|
470
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist")
|
|
471
|
+
continue;
|
|
472
|
+
const full = path.join(dir, entry.name);
|
|
473
|
+
if (entry.isDirectory())
|
|
474
|
+
await walk(full, out);
|
|
475
|
+
else if (entry.isFile() && /\.(json|ts)$/.test(entry.name))
|
|
476
|
+
out.push(full);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function printFileResult(file, changed, stats) {
|
|
480
|
+
const rel = path.relative(process.cwd(), file);
|
|
481
|
+
const icon = changed ? color.green("โ") : color.dim("โ");
|
|
482
|
+
const note = changed ? `migrated: ${stats.migrated}, marked: ${stats.marked}` : "unchanged";
|
|
483
|
+
console.log(` ${icon} ${color.cyan(rel)} ${color.dim(note)}`);
|
|
484
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { OptionValues } from "commander";
|
|
2
|
+
interface MigrationStats {
|
|
3
|
+
migrated: number;
|
|
4
|
+
marked: number;
|
|
5
|
+
}
|
|
6
|
+
export interface RefMigrationResult<T> {
|
|
7
|
+
value: T;
|
|
8
|
+
changed: boolean;
|
|
9
|
+
stats: MigrationStats;
|
|
10
|
+
}
|
|
11
|
+
export declare function migrateRefs(opts: OptionValues): Promise<void>;
|
|
12
|
+
export declare function migrateJsonText(raw: string): RefMigrationResult<string>;
|
|
13
|
+
export declare function migrateJsonWorkflow<T>(workflow: T): RefMigrationResult<T>;
|
|
14
|
+
export declare function migrateTsSource(source: string, fileName?: string): RefMigrationResult<string>;
|
|
15
|
+
export {};
|