@vmz/vmz 0.0.1 → 0.0.2
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/README.md +48 -2
- package/bin/vmz.js +4 -0
- package/dist/application-cmd.d.ts +22 -0
- package/dist/application-cmd.js +348 -0
- package/dist/bundler-adapter.d.ts +64 -0
- package/dist/bundler-adapter.js +111 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +370 -0
- package/dist/dev-session.d.ts +35 -0
- package/dist/dev-session.js +290 -0
- package/dist/document-build.d.ts +99 -0
- package/dist/document-build.js +274 -0
- package/dist/document-check.d.ts +44 -0
- package/dist/document-check.js +246 -0
- package/dist/document-cmd.d.ts +9 -0
- package/dist/document-cmd.js +147 -0
- package/dist/document-designs.d.ts +9 -0
- package/dist/document-designs.js +126 -0
- package/dist/document-enrich.d.ts +23 -0
- package/dist/document-enrich.js +234 -0
- package/dist/document-evidence.d.ts +49 -0
- package/dist/document-evidence.js +501 -0
- package/dist/document-integrate.d.ts +35 -0
- package/dist/document-integrate.js +89 -0
- package/dist/document-interactive.d.ts +69 -0
- package/dist/document-interactive.js +255 -0
- package/dist/document-locale.d.ts +31 -0
- package/dist/document-locale.js +59 -0
- package/dist/document-markdown.d.ts +13 -0
- package/dist/document-markdown.js +39 -0
- package/dist/document-scan.d.ts +21 -0
- package/dist/document-scan.js +151 -0
- package/dist/document-schema.d.ts +87 -0
- package/dist/document-schema.js +88 -0
- package/dist/explain-cmd.d.ts +5 -0
- package/dist/explain-cmd.js +123 -0
- package/dist/index.d.ts +808 -0
- package/dist/index.js +569 -0
- package/dist/locale-check.d.ts +106 -0
- package/dist/locale-check.js +737 -0
- package/dist/locale-cmd.d.ts +5 -0
- package/dist/locale-cmd.js +443 -0
- package/dist/locale-delivery.d.ts +298 -0
- package/dist/locale-delivery.js +444 -0
- package/dist/locale-router.d.ts +207 -0
- package/dist/locale-router.js +508 -0
- package/dist/locale-runtime.d.ts +406 -0
- package/dist/locale-runtime.js +542 -0
- package/dist/locale-schema.d.ts +9 -0
- package/dist/locale-schema.js +10 -0
- package/dist/locale-tooling.d.ts +118 -0
- package/dist/locale-tooling.js +358 -0
- package/dist/log.d.ts +19 -0
- package/dist/log.js +42 -0
- package/dist/packages.d.ts +27 -0
- package/dist/packages.js +147 -0
- package/dist/plugin-host.d.ts +30 -0
- package/dist/plugin-host.js +370 -0
- package/dist/refactor-cmd.d.ts +8 -0
- package/dist/refactor-cmd.js +156 -0
- package/dist/resolve.d.ts +25 -0
- package/dist/resolve.js +56 -0
- package/dist/test-cmd.d.ts +9 -0
- package/dist/test-cmd.js +343 -0
- package/dist/test-compile.d.ts +2 -0
- package/dist/test-compile.js +3 -0
- package/dist/test-discover.d.ts +2 -0
- package/dist/test-discover.js +3 -0
- package/dist/test-logic.d.ts +2 -0
- package/dist/test-logic.js +3 -0
- package/dist/test-protocol.d.ts +2 -0
- package/dist/test-protocol.js +3 -0
- package/dist/watch-diff.d.ts +17 -0
- package/dist/watch-diff.js +56 -0
- package/package.json +81 -3
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Plugin protocol v1 helpers (N3) + typed config loading.
|
|
4
|
+
* Design: 瑙勫垝璁捐/vmz/14-Node-NAPI涓庢彃浠跺涓?md
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
9
|
+
import { createJiti } from 'jiti';
|
|
10
|
+
import { PLUGIN_PROTOCOL as PLUGIN_PROTOCOL_PKG, contentHash, defineConfig, definePlugin } from '@vmz/plugin';
|
|
11
|
+
import { log } from './log.js';
|
|
12
|
+
import { resolveWorkspacePackages } from './packages.js';
|
|
13
|
+
export { contentHash, defineConfig, definePlugin };
|
|
14
|
+
export const PLUGIN_PROTOCOL = PLUGIN_PROTOCOL_PKG;
|
|
15
|
+
const CONFIG_NAMES = ['vmz.config.ts', 'vmz.config.mts', 'vmz.config.mjs', 'vmz.config.js'];
|
|
16
|
+
const ROOT_PLUGIN_NAMES = ['vmz.plugin.ts', 'vmz.plugin.mts', 'vmz.plugin.mjs', 'vmz.plugin.js'];
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} full
|
|
19
|
+
* @returns {Promise<any>}
|
|
20
|
+
*/
|
|
21
|
+
export async function importMaybeTs(full) {
|
|
22
|
+
const ext = path.extname(full).toLowerCase();
|
|
23
|
+
if (ext === '.ts' || ext === '.mts') {
|
|
24
|
+
const jiti = createJiti(import.meta.url, {
|
|
25
|
+
interopDefault: true,
|
|
26
|
+
moduleCache: false,
|
|
27
|
+
});
|
|
28
|
+
return jiti(full);
|
|
29
|
+
}
|
|
30
|
+
const mod = await import(pathToFileURL(full).href);
|
|
31
|
+
return mod.default ?? mod;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Load `vmz.config.*` from project root (+ optional root `vmz.plugin.*`).
|
|
35
|
+
* @param {string} project
|
|
36
|
+
* @returns {Promise<{ plugins: import('@vmz/plugin').VmzPlugin[], engines: import('@vmz/plugin').VmzEngines, path: string | null, pluginPath: string | null }>}
|
|
37
|
+
*/
|
|
38
|
+
export async function loadVmzConfig(project) {
|
|
39
|
+
/** @type {import('@vmz/plugin').VmzPlugin[]} */
|
|
40
|
+
const plugins = [];
|
|
41
|
+
/** @type {import('@vmz/plugin').VmzEngines} */
|
|
42
|
+
let engines = {};
|
|
43
|
+
/** @type {string | null} */
|
|
44
|
+
let configPath = null;
|
|
45
|
+
/** @type {string | null} */
|
|
46
|
+
let pluginPath = null;
|
|
47
|
+
for (const name of CONFIG_NAMES) {
|
|
48
|
+
const full = path.join(project, name);
|
|
49
|
+
if (!existsSync(full))
|
|
50
|
+
continue;
|
|
51
|
+
configPath = full;
|
|
52
|
+
const cfg = await importMaybeTs(full);
|
|
53
|
+
const raw = cfg?.plugins ?? [];
|
|
54
|
+
engines = cfg?.engines && typeof cfg.engines === 'object' ? { ...cfg.engines } : {};
|
|
55
|
+
for (const entry of raw) {
|
|
56
|
+
plugins.push(await resolvePluginEntry(project, entry));
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
for (const name of ROOT_PLUGIN_NAMES) {
|
|
61
|
+
const full = path.join(project, name);
|
|
62
|
+
if (!existsSync(full))
|
|
63
|
+
continue;
|
|
64
|
+
pluginPath = full;
|
|
65
|
+
plugins.push(await resolvePluginEntry(project, full));
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
return { plugins, engines, path: configPath, pluginPath };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} project
|
|
72
|
+
* @param {string | import('@vmz/plugin').VmzPlugin | Promise<import('@vmz/plugin').VmzPlugin>} entry
|
|
73
|
+
*/
|
|
74
|
+
async function resolvePluginEntry(project, entry) {
|
|
75
|
+
let value = entry;
|
|
76
|
+
if (typeof value === 'string') {
|
|
77
|
+
const resolved = path.isAbsolute(value) ? value : path.join(project, value);
|
|
78
|
+
value = await importMaybeTs(resolved);
|
|
79
|
+
if (value && typeof value === 'object' && 'default' in value && value.default) {
|
|
80
|
+
value = value.default;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
value = await value;
|
|
84
|
+
if (value?.manifest && (value.contribute || value.manifest.stages))
|
|
85
|
+
return value;
|
|
86
|
+
if (value?.name && value?.stages)
|
|
87
|
+
return definePlugin(value);
|
|
88
|
+
throw new Error(`invalid vmz plugin entry: ${String(entry)}`);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Collect + apply contribution batches for the given stages onto a Workspace.
|
|
92
|
+
* @param {import('../index.js').Workspace} workspace
|
|
93
|
+
* @param {import('@vmz/plugin').VmzPlugin[]} plugins
|
|
94
|
+
* @param {{ project: string, outDir: string, stages?: string[], engines?: import('@vmz/plugin').VmzEngines }} opts
|
|
95
|
+
*/
|
|
96
|
+
export async function applyPlugins(workspace, plugins, opts) {
|
|
97
|
+
const stages = opts.stages ?? ['workspace_resolve', 'source_adapter', 'analyzer', 'target'];
|
|
98
|
+
const packages = resolveWorkspacePackages(opts.project);
|
|
99
|
+
const engines = opts.engines ?? {};
|
|
100
|
+
/** @type {import('../index.js').ApplyContributionsReport[]} */
|
|
101
|
+
const reports = [];
|
|
102
|
+
/** @type {{ code: Set<string>, math: Set<string>, markdown: Set<string> }} */
|
|
103
|
+
const registered = {
|
|
104
|
+
code: new Set(),
|
|
105
|
+
math: new Set(),
|
|
106
|
+
markdown: new Set(),
|
|
107
|
+
};
|
|
108
|
+
for (const stage of stages) {
|
|
109
|
+
for (const plugin of plugins) {
|
|
110
|
+
if (!plugin.manifest.stages.includes(stage))
|
|
111
|
+
continue;
|
|
112
|
+
if (!plugin.contribute)
|
|
113
|
+
continue;
|
|
114
|
+
const ctx = {
|
|
115
|
+
project: opts.project,
|
|
116
|
+
outDir: opts.outDir,
|
|
117
|
+
stage,
|
|
118
|
+
protocol: PLUGIN_PROTOCOL,
|
|
119
|
+
packages,
|
|
120
|
+
engines,
|
|
121
|
+
};
|
|
122
|
+
const raw = await plugin.contribute(ctx);
|
|
123
|
+
const batches = Array.isArray(raw) ? raw : [raw];
|
|
124
|
+
for (const batch of batches) {
|
|
125
|
+
if (!batch || (batch.stage && batch.stage !== stage)) {
|
|
126
|
+
if (batch?.stage && batch.stage !== stage)
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
for (const item of batch.items ?? []) {
|
|
130
|
+
noteEngineRegistration(registered, item);
|
|
131
|
+
}
|
|
132
|
+
const payload = {
|
|
133
|
+
pluginName: plugin.manifest.name,
|
|
134
|
+
pluginVersion: plugin.manifest.version,
|
|
135
|
+
protocol: plugin.manifest.protocol ?? PLUGIN_PROTOCOL,
|
|
136
|
+
stage: batch.stage ?? stage,
|
|
137
|
+
cacheKey: batch.cacheKey ?? `${plugin.manifest.name}@${plugin.manifest.version}:${stage}`,
|
|
138
|
+
deterministic: batch.deterministic ?? plugin.manifest.deterministic ?? true,
|
|
139
|
+
items: (batch.items ?? []).map(normalizeItem),
|
|
140
|
+
};
|
|
141
|
+
const report = workspace.applyPluginContributions(payload);
|
|
142
|
+
reports.push(report);
|
|
143
|
+
if (report.rejected?.length) {
|
|
144
|
+
for (const r of report.rejected) {
|
|
145
|
+
log.warn(`plugin reject ${r.plugin}::${r.itemId}: ${r.reason}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
log.info(`plugin ${plugin.manifest.name} stage=${stage} accepted=${report.accepted}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (registered.math.size || registered.code.size) {
|
|
155
|
+
const facadeReports = await materializeEngineFacades(workspace, opts.project, engines, registered);
|
|
156
|
+
reports.push(...facadeReports);
|
|
157
|
+
}
|
|
158
|
+
return reports;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Only math/code/markdown register interchangeable engines (see design doc 23 §1.1).
|
|
162
|
+
* @param {{ code: Set<string>, math: Set<string>, markdown: Set<string> }} registered
|
|
163
|
+
* @param {any} item
|
|
164
|
+
*/
|
|
165
|
+
function noteEngineRegistration(registered, item) {
|
|
166
|
+
const engine = item?.engine;
|
|
167
|
+
if (!engine || typeof engine !== 'string')
|
|
168
|
+
return;
|
|
169
|
+
const kind = item.engineKind ?? item.engine_kind;
|
|
170
|
+
if (kind === 'math')
|
|
171
|
+
registered.math.add(engine);
|
|
172
|
+
else if (kind === 'code')
|
|
173
|
+
registered.code.add(engine);
|
|
174
|
+
else if (kind === 'markdown')
|
|
175
|
+
registered.markdown.add(engine);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Host-generated facades from registered engines + config defaults.
|
|
179
|
+
* @param {import('../index.js').Workspace} workspace
|
|
180
|
+
* @param {string} project
|
|
181
|
+
* @param {import('@vmz/plugin').VmzEngines} engines
|
|
182
|
+
* @param {{ code: Set<string>, math: Set<string>, markdown: Set<string> }} registered
|
|
183
|
+
*/
|
|
184
|
+
async function materializeEngineFacades(workspace, project, engines, registered) {
|
|
185
|
+
/** @type {import('../index.js').ApplyContributionsReport[]} */
|
|
186
|
+
const reports = [];
|
|
187
|
+
const items = [];
|
|
188
|
+
if (registered.math.size) {
|
|
189
|
+
const defaultMath = pickDefault(engines.math, registered.math, 'katex');
|
|
190
|
+
const content = buildMathFacade(defaultMath, [...registered.math]);
|
|
191
|
+
items.push(sourceItem('facade-math', 'src/components/Math.vmz', content));
|
|
192
|
+
}
|
|
193
|
+
if (registered.code.size) {
|
|
194
|
+
const defaultCode = pickDefault(engines.code, registered.code, 'shiki');
|
|
195
|
+
const content = buildCodeFacade(defaultCode, [...registered.code]);
|
|
196
|
+
items.push(sourceItem('facade-code', 'src/components/Code.vmz', content));
|
|
197
|
+
}
|
|
198
|
+
if (registered.markdown.size) {
|
|
199
|
+
const defaultMd = pickDefault(engines.markdown, registered.markdown, 'markdown-it');
|
|
200
|
+
const content = buildMarkdownFacade(defaultMd, [...registered.markdown]);
|
|
201
|
+
items.push(sourceItem('facade-markdown', 'src/components/Markdown.vmz', content));
|
|
202
|
+
}
|
|
203
|
+
if (!items.length)
|
|
204
|
+
return reports;
|
|
205
|
+
const cacheKey = [
|
|
206
|
+
'engine-facades',
|
|
207
|
+
[...registered.math].sort().join(','),
|
|
208
|
+
[...registered.code].sort().join(','),
|
|
209
|
+
[...registered.markdown].sort().join(','),
|
|
210
|
+
engines.math ?? '',
|
|
211
|
+
engines.code ?? '',
|
|
212
|
+
engines.markdown ?? '',
|
|
213
|
+
].join('|');
|
|
214
|
+
const report = workspace.applyPluginContributions({
|
|
215
|
+
pluginName: 'vmz:engine-facades',
|
|
216
|
+
pluginVersion: '0.1.0',
|
|
217
|
+
protocol: PLUGIN_PROTOCOL,
|
|
218
|
+
stage: 'workspace_resolve',
|
|
219
|
+
cacheKey,
|
|
220
|
+
deterministic: true,
|
|
221
|
+
items: items.map(normalizeItem),
|
|
222
|
+
});
|
|
223
|
+
reports.push(report);
|
|
224
|
+
if (report.rejected?.length) {
|
|
225
|
+
for (const r of report.rejected) {
|
|
226
|
+
log.warn(`plugin reject ${r.plugin}::${r.itemId}: ${r.reason}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
log.info(`plugin vmz:engine-facades stage=workspace_resolve accepted=${report.accepted}`);
|
|
231
|
+
}
|
|
232
|
+
return reports;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* @param {string | undefined} configured
|
|
236
|
+
* @param {Set<string>} registered
|
|
237
|
+
* @param {string} preferred
|
|
238
|
+
*/
|
|
239
|
+
function pickDefault(configured, registered, preferred) {
|
|
240
|
+
if (configured && registered.has(configured))
|
|
241
|
+
return configured;
|
|
242
|
+
if (registered.has(preferred))
|
|
243
|
+
return preferred;
|
|
244
|
+
return [...registered][0];
|
|
245
|
+
}
|
|
246
|
+
/** @param {string} id @param {string} path @param {string} content */
|
|
247
|
+
function sourceItem(id, path, content) {
|
|
248
|
+
return {
|
|
249
|
+
id,
|
|
250
|
+
kind: 'source',
|
|
251
|
+
path,
|
|
252
|
+
content,
|
|
253
|
+
contentHash: contentHash(content),
|
|
254
|
+
materialize: true,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* @param {string} defaultEngine
|
|
259
|
+
* @param {string[]} engines
|
|
260
|
+
*/
|
|
261
|
+
function buildMathFacade(defaultEngine, engines) {
|
|
262
|
+
const defaultLit = JSON.stringify(defaultEngine);
|
|
263
|
+
const branches = engines
|
|
264
|
+
.map((eng, i) => {
|
|
265
|
+
const tag = engineToTag(eng);
|
|
266
|
+
const kw = i === 0 ? 'if' : 'else-if';
|
|
267
|
+
const engLit = JSON.stringify(eng);
|
|
268
|
+
return ` <${tag} ${kw}={(engine || ${defaultLit}) === ${engLit}} tex={tex} display={display} />`;
|
|
269
|
+
})
|
|
270
|
+
.join('\n');
|
|
271
|
+
return `<template>
|
|
272
|
+
${branches}
|
|
273
|
+
</template>
|
|
274
|
+
|
|
275
|
+
<script client>
|
|
276
|
+
export default class Math {
|
|
277
|
+
public tex: string = '';
|
|
278
|
+
public display: boolean = false;
|
|
279
|
+
public engine: string | null = null;
|
|
280
|
+
}
|
|
281
|
+
</script>
|
|
282
|
+
`;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* @param {string} defaultEngine
|
|
286
|
+
* @param {string[]} engines
|
|
287
|
+
*/
|
|
288
|
+
function buildCodeFacade(defaultEngine, engines) {
|
|
289
|
+
const defaultLit = JSON.stringify(defaultEngine);
|
|
290
|
+
const branches = engines
|
|
291
|
+
.map((eng, i) => {
|
|
292
|
+
const tag = engineToTag(eng);
|
|
293
|
+
const kw = i === 0 ? 'if' : 'else-if';
|
|
294
|
+
const engLit = JSON.stringify(eng);
|
|
295
|
+
return ` <${tag} ${kw}={(engine || ${defaultLit}) === ${engLit}} code={code} lang={lang} theme={theme} />`;
|
|
296
|
+
})
|
|
297
|
+
.join('\n');
|
|
298
|
+
return `<template>
|
|
299
|
+
${branches}
|
|
300
|
+
</template>
|
|
301
|
+
|
|
302
|
+
<script client>
|
|
303
|
+
export default class Code {
|
|
304
|
+
public code: string = '';
|
|
305
|
+
public lang: string = 'text';
|
|
306
|
+
public theme: string | null = null;
|
|
307
|
+
public engine: string | null = null;
|
|
308
|
+
}
|
|
309
|
+
</script>
|
|
310
|
+
`;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* @param {string} defaultEngine
|
|
314
|
+
* @param {string[]} engines
|
|
315
|
+
*/
|
|
316
|
+
function buildMarkdownFacade(defaultEngine, engines) {
|
|
317
|
+
const defaultLit = JSON.stringify(defaultEngine);
|
|
318
|
+
const branches = engines
|
|
319
|
+
.map((eng, i) => {
|
|
320
|
+
const tag = engineToTag(eng);
|
|
321
|
+
const kw = i === 0 ? 'if' : 'else-if';
|
|
322
|
+
const engLit = JSON.stringify(eng);
|
|
323
|
+
return ` <${tag} ${kw}={(engine || ${defaultLit}) === ${engLit}} source={source} />`;
|
|
324
|
+
})
|
|
325
|
+
.join('\n');
|
|
326
|
+
return `<template>
|
|
327
|
+
${branches}
|
|
328
|
+
</template>
|
|
329
|
+
|
|
330
|
+
<script client>
|
|
331
|
+
export default class Markdown {
|
|
332
|
+
public source: string = '';
|
|
333
|
+
public engine: string | null = null;
|
|
334
|
+
}
|
|
335
|
+
</script>
|
|
336
|
+
`;
|
|
337
|
+
}
|
|
338
|
+
/** @param {string} engine */
|
|
339
|
+
function engineToTag(engine) {
|
|
340
|
+
const map = {
|
|
341
|
+
katex: 'Katex',
|
|
342
|
+
mathjax: 'Mathjax',
|
|
343
|
+
shiki: 'Shiki',
|
|
344
|
+
'markdown-it': 'MarkdownIt',
|
|
345
|
+
};
|
|
346
|
+
if (map[engine])
|
|
347
|
+
return map[engine];
|
|
348
|
+
return engine
|
|
349
|
+
.split(/[-_]/)
|
|
350
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
351
|
+
.join('');
|
|
352
|
+
}
|
|
353
|
+
/** @param {any} item */
|
|
354
|
+
function normalizeItem(item) {
|
|
355
|
+
return {
|
|
356
|
+
id: item.id,
|
|
357
|
+
kind: item.kind,
|
|
358
|
+
path: item.path,
|
|
359
|
+
content: item.content,
|
|
360
|
+
contentHash: item.contentHash ?? item.content_hash,
|
|
361
|
+
materialize: item.materialize,
|
|
362
|
+
severity: item.severity,
|
|
363
|
+
message: item.message,
|
|
364
|
+
code: item.code,
|
|
365
|
+
targetId: item.targetId ?? item.target_id,
|
|
366
|
+
targetKind: item.targetKind ?? item.target_kind ?? item.type,
|
|
367
|
+
manifestJson: item.manifestJson ?? item.manifest_json ?? (item.manifest != null ? JSON.stringify(item.manifest) : undefined),
|
|
368
|
+
detail: item.detail,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* `vmz refactor` — X1 RouteId/field safe rename (plan + atomic apply).
|
|
4
|
+
*/
|
|
5
|
+
import { createWorkspace } from './index.js';
|
|
6
|
+
import { log } from './log.js';
|
|
7
|
+
import { resolveWorkspaceDirs } from './resolve.js';
|
|
8
|
+
/**
|
|
9
|
+
* @param {string[]} argv args after `refactor`
|
|
10
|
+
* @returns {Promise<number>}
|
|
11
|
+
*/
|
|
12
|
+
export async function cmdRefactor(argv) {
|
|
13
|
+
const [sub, ...rest] = argv;
|
|
14
|
+
if (!sub || sub === 'help' || sub === '-h' || sub === '--help') {
|
|
15
|
+
printRefactorHelp();
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
if (sub === 'rename') {
|
|
19
|
+
return cmdRename(rest);
|
|
20
|
+
}
|
|
21
|
+
log.error(`unknown refactor subcommand \`${sub}\``);
|
|
22
|
+
printRefactorHelp();
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
function printRefactorHelp() {
|
|
26
|
+
console.log(`vmz refactor — workspace edit plans (X1)
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
vmz refactor rename --kind <route_id|field|method|component|capability> --from <id> --to <id> [path]
|
|
30
|
+
[--scope <chunk>] [--json] [--apply] [--explain]
|
|
31
|
+
|
|
32
|
+
Notes:
|
|
33
|
+
Returns WorkspaceEditPlan (\`vmz.dx.workspace_edit.v0\`).
|
|
34
|
+
route_id / field emit proven TextEdits; --apply writes atomically when status=ready.
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* @param {string[]} argv
|
|
39
|
+
*/
|
|
40
|
+
function parseRefactorArgs(argv) {
|
|
41
|
+
/** @type {Record<string, string | boolean> & { _: string[] }} */
|
|
42
|
+
const out = { _: [] };
|
|
43
|
+
for (let i = 0; i < argv.length; i++) {
|
|
44
|
+
const a = argv[i];
|
|
45
|
+
if (a.startsWith('--')) {
|
|
46
|
+
const eq = a.indexOf('=');
|
|
47
|
+
if (eq !== -1) {
|
|
48
|
+
out[a.slice(2, eq)] = a.slice(eq + 1);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const key = a.slice(2);
|
|
52
|
+
const next = argv[i + 1];
|
|
53
|
+
if (next && !next.startsWith('-')) {
|
|
54
|
+
out[key] = next;
|
|
55
|
+
i += 1;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
out[key] = true;
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
out._.push(a);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* @param {string[]} argv
|
|
68
|
+
*/
|
|
69
|
+
function cmdRename(argv) {
|
|
70
|
+
const args = parseRefactorArgs(argv);
|
|
71
|
+
const kind = typeof args.kind === 'string' ? args.kind : '';
|
|
72
|
+
const from = typeof args.from === 'string' ? args.from : '';
|
|
73
|
+
const to = typeof args.to === 'string' ? args.to : '';
|
|
74
|
+
const scope = typeof args.scope === 'string' ? args.scope : undefined;
|
|
75
|
+
const wantJson = args.json === true || typeof args.json === 'string';
|
|
76
|
+
const wantApply = args.apply === true;
|
|
77
|
+
const wantExplain = args.explain === true;
|
|
78
|
+
if (!kind || !from || !to) {
|
|
79
|
+
log.error('rename requires --kind, --from, and --to');
|
|
80
|
+
printRefactorHelp();
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
const pathArg = args._[0] ?? '.';
|
|
84
|
+
const { project, outDir } = resolveWorkspaceDirs({
|
|
85
|
+
path: pathArg,
|
|
86
|
+
outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
|
|
87
|
+
});
|
|
88
|
+
const intent = {
|
|
89
|
+
schema: 'vmz.dx.rename.v0',
|
|
90
|
+
kind,
|
|
91
|
+
from,
|
|
92
|
+
to,
|
|
93
|
+
...(scope ? { scope } : {}),
|
|
94
|
+
};
|
|
95
|
+
const ws = createWorkspace({ root: project, outDir });
|
|
96
|
+
try {
|
|
97
|
+
if (typeof ws.planRename !== 'function') {
|
|
98
|
+
log.error('planRename missing on Workspace — rebuild native (`pnpm napi:build`)');
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
let planRaw = ws.planRename(JSON.stringify(intent));
|
|
102
|
+
let plan;
|
|
103
|
+
try {
|
|
104
|
+
plan = JSON.parse(planRaw);
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
log.error(`plan_rename not JSON: ${e}`);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
if (wantApply) {
|
|
111
|
+
if (typeof ws.applyWorkspaceEdit !== 'function') {
|
|
112
|
+
log.error('applyWorkspaceEdit missing — rebuild native (`pnpm napi:build`)');
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
planRaw = ws.applyWorkspaceEdit(planRaw);
|
|
116
|
+
try {
|
|
117
|
+
plan = JSON.parse(planRaw);
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
log.error(`apply_workspace_edit not JSON: ${e}`);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (wantExplain && typeof ws.explainRenameChain === 'function') {
|
|
125
|
+
const explainRaw = ws.explainRenameChain(JSON.stringify(intent));
|
|
126
|
+
if (wantJson) {
|
|
127
|
+
console.log(JSON.stringify({ plan, explain: JSON.parse(explainRaw) }, null, 2));
|
|
128
|
+
return plan.status === 'rejected' ? 1 : 0;
|
|
129
|
+
}
|
|
130
|
+
const explain = JSON.parse(explainRaw);
|
|
131
|
+
log.info(`explain chain edges=${(explain.chain || []).length}`);
|
|
132
|
+
}
|
|
133
|
+
if (wantJson) {
|
|
134
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
log.info(`rename ${kind} \`${from}\` → \`${to}\` → ${plan.status}`);
|
|
138
|
+
for (const p of plan.preconditions || []) {
|
|
139
|
+
console.log(` precondition: ${p}`);
|
|
140
|
+
}
|
|
141
|
+
for (const e of plan.edits || []) {
|
|
142
|
+
console.log(` edit ${e.path} @${e.start}..${e.end} → ${JSON.stringify(e.newText)}`);
|
|
143
|
+
}
|
|
144
|
+
for (const d of plan.diagnostics || []) {
|
|
145
|
+
console.log(` ${d.severity || 'info'}: ${d.message}`);
|
|
146
|
+
}
|
|
147
|
+
if ((plan.edits || []).length === 0) {
|
|
148
|
+
console.log(' edits: (none)');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return plan.status === 'rejected' ? 1 : 0;
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
ws.dispose();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared project path resolution for the Node CLI host (N2).
|
|
3
|
+
* Design: `规划设计/vmz/14-Node-NAPI与插件宿主.md`
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} startDir
|
|
7
|
+
* @returns {string | null}
|
|
8
|
+
*/
|
|
9
|
+
export declare function findPackageJson(startDir: any): string;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve project root + out dir from CLI args / cwd.
|
|
12
|
+
* Prefers an explicit path; otherwise walks up for package.json with `src/`.
|
|
13
|
+
*
|
|
14
|
+
* @param {{ cwd?: string, path?: string, outDir?: string }} opts
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveWorkspaceDirs(opts?: {}): {
|
|
17
|
+
project: string;
|
|
18
|
+
outDir: any;
|
|
19
|
+
cwd: any;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} projectRoot
|
|
23
|
+
* @returns {{ name?: string, private?: boolean } | null}
|
|
24
|
+
*/
|
|
25
|
+
export declare function readPackageMeta(projectRoot: any): any;
|
package/dist/resolve.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Shared project path resolution for the Node CLI host (N2).
|
|
4
|
+
* Design: `规划设计/vmz/14-Node-NAPI与插件宿主.md`
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} startDir
|
|
10
|
+
* @returns {string | null}
|
|
11
|
+
*/
|
|
12
|
+
export function findPackageJson(startDir) {
|
|
13
|
+
let dir = path.resolve(startDir);
|
|
14
|
+
for (;;) {
|
|
15
|
+
const candidate = path.join(dir, 'package.json');
|
|
16
|
+
if (existsSync(candidate))
|
|
17
|
+
return candidate;
|
|
18
|
+
const parent = path.dirname(dir);
|
|
19
|
+
if (parent === dir)
|
|
20
|
+
return null;
|
|
21
|
+
dir = parent;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolve project root + out dir from CLI args / cwd.
|
|
26
|
+
* Prefers an explicit path; otherwise walks up for package.json with `src/`.
|
|
27
|
+
*
|
|
28
|
+
* @param {{ cwd?: string, path?: string, outDir?: string }} opts
|
|
29
|
+
*/
|
|
30
|
+
export function resolveWorkspaceDirs(opts = {}) {
|
|
31
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
32
|
+
const input = path.resolve(cwd, opts.path ?? '.');
|
|
33
|
+
let project = input;
|
|
34
|
+
if (!existsSync(path.join(project, 'src')) && !existsSync(path.join(project, 'package.json'))) {
|
|
35
|
+
const pkg = findPackageJson(cwd);
|
|
36
|
+
if (pkg)
|
|
37
|
+
project = path.dirname(pkg);
|
|
38
|
+
}
|
|
39
|
+
const outDir = opts.outDir ? (path.isAbsolute(opts.outDir) ? opts.outDir : path.join(project, opts.outDir)) : path.join(project, 'dist');
|
|
40
|
+
return { project, outDir, cwd };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} projectRoot
|
|
44
|
+
* @returns {{ name?: string, private?: boolean } | null}
|
|
45
|
+
*/
|
|
46
|
+
export function readPackageMeta(projectRoot) {
|
|
47
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
48
|
+
if (!existsSync(pkgPath))
|
|
49
|
+
return null;
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vmz test` command 鈥?discovery / build / filter / TestReport orchestration.
|
|
3
|
+
* Semantics live in `@vmz/test`.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* @param {Record<string, string | boolean> & { _: string[] }} args
|
|
7
|
+
* @returns {Promise<number>}
|
|
8
|
+
*/
|
|
9
|
+
export declare function cmdTest(args: any): Promise<0 | 1 | 2>;
|