@vmz/vmz 0.0.0 → 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 +86 -4
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Document D2 Evidence — fence check + API refs from Program Graph.
|
|
4
|
+
* Design: 规划设计/vmz/19 §4 · §8 D2
|
|
5
|
+
*
|
|
6
|
+
* Not a Doc IR: filesystem/manifest projection + Workspace/Program Graph queries.
|
|
7
|
+
*/
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { DIAG, DOCUMENT_EVIDENCE_SCHEMA } from './document-schema.js';
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} info
|
|
16
|
+
* @returns {{ lang: string, run: string | null, source: string | null, playground: boolean }}
|
|
17
|
+
*/
|
|
18
|
+
export function parseFenceInfo(info) {
|
|
19
|
+
const parts = String(info || '')
|
|
20
|
+
.trim()
|
|
21
|
+
.split(/\s+/)
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
const lang = (parts[0] || '').toLowerCase();
|
|
24
|
+
/** @type {string | null} */
|
|
25
|
+
let run = null;
|
|
26
|
+
/** @type {string | null} */
|
|
27
|
+
let source = null;
|
|
28
|
+
let playground = false;
|
|
29
|
+
for (const p of parts.slice(1)) {
|
|
30
|
+
if (p === 'run')
|
|
31
|
+
run = 'compile';
|
|
32
|
+
else if (p.startsWith('run='))
|
|
33
|
+
run = p.slice(4) || 'compile';
|
|
34
|
+
else if (p.startsWith('source='))
|
|
35
|
+
source = p.slice(7).replace(/^["']|["']$/g, '');
|
|
36
|
+
else if (p === 'playground')
|
|
37
|
+
playground = true;
|
|
38
|
+
}
|
|
39
|
+
return { lang, run, source, playground };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} href
|
|
43
|
+
* @returns {string | null} symbol query (chunkId or name)
|
|
44
|
+
*/
|
|
45
|
+
export function parseApiHref(href) {
|
|
46
|
+
const h = String(href || '').trim();
|
|
47
|
+
if (h.startsWith('vmz-api:'))
|
|
48
|
+
return h.slice('vmz-api:'.length).replace(/^\/+/, '');
|
|
49
|
+
if (h.startsWith('api:'))
|
|
50
|
+
return h.slice('api:'.length).replace(/^\/+/, '');
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} projectRoot
|
|
55
|
+
* @returns {Array<{ chunkId: string, name: string, path: string, capabilities: string[], programPath: string }>}
|
|
56
|
+
*/
|
|
57
|
+
export function loadProgramApiIndex(projectRoot) {
|
|
58
|
+
const outDir = path.join(projectRoot, 'dist');
|
|
59
|
+
/** @type {Array<{ chunkId: string, name: string, path: string, capabilities: string[], programPath: string }>} */
|
|
60
|
+
const rows = [];
|
|
61
|
+
if (!fs.existsSync(outDir))
|
|
62
|
+
return rows;
|
|
63
|
+
walkFiles(outDir, (file) => {
|
|
64
|
+
if (!file.endsWith('.program.json'))
|
|
65
|
+
return;
|
|
66
|
+
let root;
|
|
67
|
+
try {
|
|
68
|
+
root = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const units = Array.isArray(root.units) ? root.units : [];
|
|
74
|
+
for (const unit of units) {
|
|
75
|
+
const chunkId = unit?.deployment?.chunkId || unit?.name || path.basename(file, '.program.json');
|
|
76
|
+
const name = unit?.name || chunkId;
|
|
77
|
+
const caps = [];
|
|
78
|
+
const list = unit?.server?.capabilities;
|
|
79
|
+
if (Array.isArray(list)) {
|
|
80
|
+
for (const c of list) {
|
|
81
|
+
if (c?.method)
|
|
82
|
+
caps.push(String(c.method));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
rows.push({
|
|
86
|
+
chunkId: String(chunkId),
|
|
87
|
+
name: String(name),
|
|
88
|
+
path: String(root.source || file),
|
|
89
|
+
capabilities: caps,
|
|
90
|
+
programPath: file,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return rows;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Resolve API symbol against Program Graph index.
|
|
98
|
+
* @param {ReturnType<typeof loadProgramApiIndex>} index
|
|
99
|
+
* @param {string} query
|
|
100
|
+
*/
|
|
101
|
+
export function resolveApiSymbol(index, query) {
|
|
102
|
+
const q = String(query || '').trim();
|
|
103
|
+
if (!q)
|
|
104
|
+
return { status: 'missing', matches: [] };
|
|
105
|
+
const exact = index.filter((r) => r.chunkId === q || r.name === q);
|
|
106
|
+
if (exact.length === 1)
|
|
107
|
+
return { status: 'ok', matches: exact };
|
|
108
|
+
if (exact.length > 1)
|
|
109
|
+
return { status: 'ambiguous', matches: exact };
|
|
110
|
+
const loose = index.filter((r) => r.chunkId.endsWith(`/${q}`) || r.chunkId.endsWith(q) || r.name.toLowerCase() === q.toLowerCase());
|
|
111
|
+
if (loose.length === 1)
|
|
112
|
+
return { status: 'ok', matches: loose };
|
|
113
|
+
if (loose.length > 1)
|
|
114
|
+
return { status: 'ambiguous', matches: loose };
|
|
115
|
+
return { status: 'missing', matches: [] };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* @param {import('./document-schema.js').DocumentManifest} manifest
|
|
119
|
+
* @param {{
|
|
120
|
+
* analyzeMarkdown: Function,
|
|
121
|
+
* projectRoot: string,
|
|
122
|
+
* createWorkspace?: Function,
|
|
123
|
+
* ensureProgramGraph?: boolean,
|
|
124
|
+
* }} ctx
|
|
125
|
+
*/
|
|
126
|
+
export async function enrichDocumentEvidence(manifest, ctx) {
|
|
127
|
+
const projectRoot = path.resolve(ctx.projectRoot || manifest.root);
|
|
128
|
+
/** @type {import('./document-schema.js').DocumentDiagnostic[]} */
|
|
129
|
+
const diagnostics = [...(manifest.diagnostics || [])];
|
|
130
|
+
/** @type {any[]} */
|
|
131
|
+
const fenceRecords = [];
|
|
132
|
+
/** @type {any[]} */
|
|
133
|
+
const apiRefs = [];
|
|
134
|
+
/** @type {any[]} */
|
|
135
|
+
const testSelections = [];
|
|
136
|
+
// Collect fences + api links from pages.
|
|
137
|
+
/** @type {Array<{ page: any, fences: any[], apiQueries: string[], sourcePath: string }>} */
|
|
138
|
+
const pages = [];
|
|
139
|
+
for (const page of manifest.pages) {
|
|
140
|
+
const abs = path.isAbsolute(page.sourcePath) ? page.sourcePath : path.join(manifest.root, page.sourcePath);
|
|
141
|
+
const source = fs.existsSync(abs) ? fs.readFileSync(abs, 'utf8') : '';
|
|
142
|
+
const analyzed = ctx.analyzeMarkdown(source);
|
|
143
|
+
const fences = Array.isArray(analyzed.fences) ? analyzed.fences : [];
|
|
144
|
+
const apiQueries = [];
|
|
145
|
+
for (const link of analyzed.links || []) {
|
|
146
|
+
const q = parseApiHref(link.href);
|
|
147
|
+
if (q)
|
|
148
|
+
apiQueries.push(q);
|
|
149
|
+
}
|
|
150
|
+
pages.push({ page, fences, apiQueries, sourcePath: page.sourcePath });
|
|
151
|
+
}
|
|
152
|
+
const needsGraph = pages.some((p) => p.apiQueries.length > 0) || pages.some((p) => p.fences.some((f) => parseFenceInfo(f.info).lang === 'vmz'));
|
|
153
|
+
if (needsGraph && ctx.ensureProgramGraph !== false && typeof ctx.createWorkspace === 'function') {
|
|
154
|
+
try {
|
|
155
|
+
await ensureSrcProgramGraph(projectRoot, ctx.createWorkspace, diagnostics);
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
diagnostics.push({
|
|
159
|
+
code: DIAG.FENCE_CHECK,
|
|
160
|
+
severity: 'error',
|
|
161
|
+
message: `project build for evidence failed: ${e.message || e}`,
|
|
162
|
+
path: projectRoot,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const apiIndex = loadProgramApiIndex(projectRoot);
|
|
167
|
+
for (const { page, fences, apiQueries, sourcePath } of pages) {
|
|
168
|
+
for (const fence of fences) {
|
|
169
|
+
const meta = parseFenceInfo(fence.info);
|
|
170
|
+
const rec = {
|
|
171
|
+
lang: meta.lang,
|
|
172
|
+
info: fence.info,
|
|
173
|
+
lineStart: fence.lineStart,
|
|
174
|
+
lineEnd: fence.lineEnd,
|
|
175
|
+
pageKey: page.identity.pageKey,
|
|
176
|
+
locale: page.identity.locale,
|
|
177
|
+
path: sourcePath,
|
|
178
|
+
run: meta.run,
|
|
179
|
+
source: meta.source,
|
|
180
|
+
playground: meta.playground,
|
|
181
|
+
status: 'skipped',
|
|
182
|
+
};
|
|
183
|
+
if (!meta.lang ||
|
|
184
|
+
meta.lang === 'text' ||
|
|
185
|
+
meta.lang === 'md' ||
|
|
186
|
+
meta.lang === 'bash' ||
|
|
187
|
+
meta.lang === 'sh' ||
|
|
188
|
+
meta.lang === 'shell' ||
|
|
189
|
+
meta.lang === 'json' ||
|
|
190
|
+
meta.lang === 'css' ||
|
|
191
|
+
meta.lang === 'html') {
|
|
192
|
+
rec.status = 'highlight';
|
|
193
|
+
fenceRecords.push(rec);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (meta.lang === 'vmz') {
|
|
197
|
+
const result = await checkVmzFence({
|
|
198
|
+
projectRoot,
|
|
199
|
+
fence,
|
|
200
|
+
meta,
|
|
201
|
+
createWorkspace: ctx.createWorkspace,
|
|
202
|
+
sourcePath,
|
|
203
|
+
});
|
|
204
|
+
Object.assign(rec, result.record);
|
|
205
|
+
diagnostics.push(...result.diagnostics);
|
|
206
|
+
if (result.testSelection)
|
|
207
|
+
testSelections.push(result.testSelection);
|
|
208
|
+
fenceRecords.push(rec);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (meta.lang === 'ts' || meta.lang === 'typescript' || meta.lang === 'js' || meta.lang === 'javascript') {
|
|
212
|
+
const result = checkScriptFence({ fence, meta, sourcePath, page });
|
|
213
|
+
Object.assign(rec, result.record);
|
|
214
|
+
diagnostics.push(...result.diagnostics);
|
|
215
|
+
fenceRecords.push(rec);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
rec.status = 'unsupported';
|
|
219
|
+
diagnostics.push({
|
|
220
|
+
code: DIAG.FENCE_UNSUPPORTED,
|
|
221
|
+
severity: 'warning',
|
|
222
|
+
message: `fence lang \`${meta.lang}\` is highlight-only (no sandbox contribution)`,
|
|
223
|
+
path: `${sourcePath}:${fence.lineStart}`,
|
|
224
|
+
});
|
|
225
|
+
fenceRecords.push(rec);
|
|
226
|
+
}
|
|
227
|
+
for (const query of apiQueries) {
|
|
228
|
+
const resolved = resolveApiSymbol(apiIndex, query);
|
|
229
|
+
const ref = {
|
|
230
|
+
query,
|
|
231
|
+
pageKey: page.identity.pageKey,
|
|
232
|
+
locale: page.identity.locale,
|
|
233
|
+
path: sourcePath,
|
|
234
|
+
status: resolved.status,
|
|
235
|
+
matches: resolved.matches.map((m) => ({
|
|
236
|
+
chunkId: m.chunkId,
|
|
237
|
+
name: m.name,
|
|
238
|
+
source: m.path,
|
|
239
|
+
capabilities: m.capabilities,
|
|
240
|
+
stableId: { kind: 'chunk', id: m.chunkId },
|
|
241
|
+
})),
|
|
242
|
+
};
|
|
243
|
+
if (resolved.status === 'missing') {
|
|
244
|
+
diagnostics.push({
|
|
245
|
+
code: DIAG.API_MISSING,
|
|
246
|
+
severity: 'error',
|
|
247
|
+
message: `API symbol not found in Program Graph: ${query}`,
|
|
248
|
+
path: sourcePath,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
else if (resolved.status === 'ambiguous') {
|
|
252
|
+
diagnostics.push({
|
|
253
|
+
code: DIAG.API_AMBIGUOUS,
|
|
254
|
+
severity: 'error',
|
|
255
|
+
message: `API symbol ambiguous (${resolved.matches.map((m) => m.chunkId).join(', ')}): ${query}`,
|
|
256
|
+
path: sourcePath,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
apiRefs.push(ref);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const hasErrors = diagnostics.some((d) => d.severity === 'error');
|
|
263
|
+
const evidence = {
|
|
264
|
+
schema: DOCUMENT_EVIDENCE_SCHEMA,
|
|
265
|
+
fences: fenceRecords,
|
|
266
|
+
apiRefs,
|
|
267
|
+
testSelections,
|
|
268
|
+
status: hasErrors ? 'failed' : fenceRecords.length || apiRefs.length ? 'ready' : 'empty',
|
|
269
|
+
};
|
|
270
|
+
return { diagnostics, evidence };
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* @param {{ projectRoot: string, fence: any, meta: any, createWorkspace?: Function, sourcePath: string }} opts
|
|
274
|
+
*/
|
|
275
|
+
async function checkVmzFence(opts) {
|
|
276
|
+
/** @type {import('./document-schema.js').DocumentDiagnostic[]} */
|
|
277
|
+
const diagnostics = [];
|
|
278
|
+
const { fence, meta, projectRoot, sourcePath } = opts;
|
|
279
|
+
let body = fence.content;
|
|
280
|
+
let label = `inline@${sourcePath}:${fence.lineStart}`;
|
|
281
|
+
if (meta.source) {
|
|
282
|
+
const abs = path.isAbsolute(meta.source) ? meta.source : path.join(projectRoot, meta.source);
|
|
283
|
+
if (!fs.existsSync(abs)) {
|
|
284
|
+
diagnostics.push({
|
|
285
|
+
code: DIAG.FENCE_SOURCE_MISSING,
|
|
286
|
+
severity: 'error',
|
|
287
|
+
message: `fence source missing: ${meta.source}`,
|
|
288
|
+
path: `${sourcePath}:${fence.lineStart}`,
|
|
289
|
+
});
|
|
290
|
+
return {
|
|
291
|
+
record: { status: 'failed', detail: 'source_missing' },
|
|
292
|
+
diagnostics,
|
|
293
|
+
testSelection: null,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
body = fs.readFileSync(abs, 'utf8');
|
|
297
|
+
label = meta.source;
|
|
298
|
+
}
|
|
299
|
+
if (typeof opts.createWorkspace !== 'function') {
|
|
300
|
+
diagnostics.push({
|
|
301
|
+
code: DIAG.FENCE_CHECK,
|
|
302
|
+
severity: 'error',
|
|
303
|
+
message: 'createWorkspace unavailable for vmz fence check',
|
|
304
|
+
path: `${sourcePath}:${fence.lineStart}`,
|
|
305
|
+
});
|
|
306
|
+
return { record: { status: 'failed' }, diagnostics, testSelection: null };
|
|
307
|
+
}
|
|
308
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'vmz-d2-fence-'));
|
|
309
|
+
try {
|
|
310
|
+
const rel = 'src/components/FenceExample.vmz';
|
|
311
|
+
const abs = path.join(tmp, rel);
|
|
312
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
313
|
+
// Ensure minimal valid SFC if fence is a fragment; prefer full SFC bodies in docs.
|
|
314
|
+
const content = body.includes('<template') ? body : wrapVmzFragment(body);
|
|
315
|
+
fs.writeFileSync(abs, content, 'utf8');
|
|
316
|
+
const outDir = path.join(tmp, 'dist');
|
|
317
|
+
const ws = opts.createWorkspace({ root: tmp, outDir });
|
|
318
|
+
const report = ws.check(true);
|
|
319
|
+
const errors = (report.diagnostics || []).filter((d) => d.severity === 'error' || d.severity === 'Error');
|
|
320
|
+
if (errors.length) {
|
|
321
|
+
diagnostics.push({
|
|
322
|
+
code: DIAG.FENCE_CHECK,
|
|
323
|
+
severity: 'error',
|
|
324
|
+
message: `vmz fence check failed (${label}): ${errors[0]?.message || 'error'}`,
|
|
325
|
+
path: `${sourcePath}:${fence.lineStart}`,
|
|
326
|
+
});
|
|
327
|
+
ws.dispose?.();
|
|
328
|
+
return { record: { status: 'failed', detail: 'check' }, diagnostics, testSelection: null };
|
|
329
|
+
}
|
|
330
|
+
/** @type {any} */
|
|
331
|
+
let testSelection = null;
|
|
332
|
+
if (meta.run) {
|
|
333
|
+
const mode = meta.run === 'logic' || meta.run === 'browser' ? meta.run : 'compile';
|
|
334
|
+
const build = ws.build(false);
|
|
335
|
+
const buildErrs = (build.diagnostics || []).filter((d) => d.severity === 'error' || d.severity === 'Error');
|
|
336
|
+
if (buildErrs.length) {
|
|
337
|
+
diagnostics.push({
|
|
338
|
+
code: DIAG.FENCE_RUN_FAILED,
|
|
339
|
+
severity: 'error',
|
|
340
|
+
message: `vmz fence run=${mode} build failed (${label}): ${buildErrs[0]?.message || 'error'}`,
|
|
341
|
+
path: `${sourcePath}:${fence.lineStart}`,
|
|
342
|
+
});
|
|
343
|
+
ws.dispose?.();
|
|
344
|
+
return { record: { status: 'failed', detail: 'run_build' }, diagnostics, testSelection: null };
|
|
345
|
+
}
|
|
346
|
+
const prog = path.join(outDir, 'components', 'FenceExample.program.json');
|
|
347
|
+
if (!fs.existsSync(prog)) {
|
|
348
|
+
diagnostics.push({
|
|
349
|
+
code: DIAG.FENCE_RUN_FAILED,
|
|
350
|
+
severity: 'error',
|
|
351
|
+
message: `vmz fence run=${mode} missing program.json (${label})`,
|
|
352
|
+
path: `${sourcePath}:${fence.lineStart}`,
|
|
353
|
+
});
|
|
354
|
+
ws.dispose?.();
|
|
355
|
+
return { record: { status: 'failed', detail: 'run_program' }, diagnostics, testSelection: null };
|
|
356
|
+
}
|
|
357
|
+
testSelection = {
|
|
358
|
+
schema: 'vmz.dx.test_selection.v0',
|
|
359
|
+
reason: `document fence run=${mode} @ ${sourcePath}:${fence.lineStart}`,
|
|
360
|
+
testIds: [`document.fence.${pageKeySafe(sourcePath)}.${fence.lineStart}`],
|
|
361
|
+
affectedChunkIds: ['components/FenceExample'],
|
|
362
|
+
status: 'ready',
|
|
363
|
+
mode,
|
|
364
|
+
};
|
|
365
|
+
ws.dispose?.();
|
|
366
|
+
return {
|
|
367
|
+
record: { status: 'ok', detail: `run=${mode}`, source: label },
|
|
368
|
+
diagnostics,
|
|
369
|
+
testSelection,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
ws.dispose?.();
|
|
373
|
+
return { record: { status: 'ok', detail: 'check', source: label }, diagnostics, testSelection: null };
|
|
374
|
+
}
|
|
375
|
+
finally {
|
|
376
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function wrapVmzFragment(body) {
|
|
380
|
+
const trimmed = String(body || '').trim();
|
|
381
|
+
if (trimmed.startsWith('<')) {
|
|
382
|
+
return `<template>\n${trimmed}\n</template>\n<script client>\nexport default class FenceExample {}\n</script>\n`;
|
|
383
|
+
}
|
|
384
|
+
return `<template><p>ok</p></template>\n<script client>\n${trimmed}\n</script>\n`;
|
|
385
|
+
}
|
|
386
|
+
function pageKeySafe(p) {
|
|
387
|
+
return String(p || 'page').replace(/[^\w.-]+/g, '_');
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* TS/JS fence: oxc-aligned surface via TypeScript parse (syntax only; no execute).
|
|
391
|
+
*/
|
|
392
|
+
function checkScriptFence({ fence, meta, sourcePath, page }) {
|
|
393
|
+
/** @type {import('./document-schema.js').DocumentDiagnostic[]} */
|
|
394
|
+
const diagnostics = [];
|
|
395
|
+
try {
|
|
396
|
+
const ts = require('typescript');
|
|
397
|
+
const isTs = meta.lang === 'ts' || meta.lang === 'typescript';
|
|
398
|
+
const fileName = isTs ? 'fence.ts' : 'fence.js';
|
|
399
|
+
const kind = isTs ? ts.ScriptKind.TS : ts.ScriptKind.JS;
|
|
400
|
+
const sf = ts.createSourceFile(fileName, fence.content, ts.ScriptTarget.Latest, true, kind);
|
|
401
|
+
// createSourceFile does not throw on syntax errors — scan for parse diagnostics via transpile.
|
|
402
|
+
const out = ts.transpileModule(fence.content, {
|
|
403
|
+
compilerOptions: {
|
|
404
|
+
target: ts.ScriptTarget.ES2022,
|
|
405
|
+
module: ts.ModuleKind.ESNext,
|
|
406
|
+
strict: false,
|
|
407
|
+
},
|
|
408
|
+
reportDiagnostics: true,
|
|
409
|
+
fileName,
|
|
410
|
+
});
|
|
411
|
+
const errs = (out.diagnostics || []).filter((d) => d.category === ts.DiagnosticCategory.Error);
|
|
412
|
+
if (errs.length) {
|
|
413
|
+
const msg = ts.flattenDiagnosticMessageText(errs[0].messageText, '\n');
|
|
414
|
+
diagnostics.push({
|
|
415
|
+
code: DIAG.FENCE_CHECK,
|
|
416
|
+
severity: 'error',
|
|
417
|
+
message: `${meta.lang} fence check failed: ${msg}`,
|
|
418
|
+
path: `${sourcePath}:${fence.lineStart}`,
|
|
419
|
+
});
|
|
420
|
+
return { record: { status: 'failed', detail: 'syntax' }, diagnostics };
|
|
421
|
+
}
|
|
422
|
+
// Touch sf to keep parse path honest.
|
|
423
|
+
if (!sf || sf.kind == null) {
|
|
424
|
+
diagnostics.push({
|
|
425
|
+
code: DIAG.FENCE_CHECK,
|
|
426
|
+
severity: 'error',
|
|
427
|
+
message: `${meta.lang} fence parse produced empty SourceFile`,
|
|
428
|
+
path: `${sourcePath}:${fence.lineStart}`,
|
|
429
|
+
});
|
|
430
|
+
return { record: { status: 'failed' }, diagnostics };
|
|
431
|
+
}
|
|
432
|
+
return { record: { status: 'ok', detail: 'syntax' }, diagnostics };
|
|
433
|
+
}
|
|
434
|
+
catch (e) {
|
|
435
|
+
diagnostics.push({
|
|
436
|
+
code: DIAG.FENCE_CHECK,
|
|
437
|
+
severity: 'error',
|
|
438
|
+
message: `${meta.lang} fence check unavailable: ${e.message || e}`,
|
|
439
|
+
path: `${sourcePath}:${fence.lineStart}`,
|
|
440
|
+
pageKey: page?.identity?.pageKey,
|
|
441
|
+
});
|
|
442
|
+
return { record: { status: 'failed', detail: 'engine' }, diagnostics };
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function walkFiles(dir, fn) {
|
|
446
|
+
if (!fs.existsSync(dir))
|
|
447
|
+
return;
|
|
448
|
+
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
449
|
+
const full = path.join(dir, ent.name);
|
|
450
|
+
if (ent.isDirectory())
|
|
451
|
+
walkFiles(full, fn);
|
|
452
|
+
else if (ent.isFile())
|
|
453
|
+
fn(full);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Build only project src .vmz files into dist for API Program Graph queries.
|
|
458
|
+
* Avoids coupling document evidence to site /designs theme diagnostics.
|
|
459
|
+
*/
|
|
460
|
+
async function ensureSrcProgramGraph(projectRoot, createWorkspace, diagnostics) {
|
|
461
|
+
const srcDir = path.join(projectRoot, 'src');
|
|
462
|
+
if (!fs.existsSync(srcDir))
|
|
463
|
+
return;
|
|
464
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'vmz-d2-api-'));
|
|
465
|
+
try {
|
|
466
|
+
copyDir(srcDir, path.join(tmp, 'src'));
|
|
467
|
+
const outDir = path.join(tmp, 'dist');
|
|
468
|
+
const ws = createWorkspace({ root: tmp, outDir });
|
|
469
|
+
const report = ws.build(false);
|
|
470
|
+
const errors = (report.diagnostics || []).filter((d) => d.severity === 'error' || d.severity === 'Error');
|
|
471
|
+
if (errors.length) {
|
|
472
|
+
diagnostics.push({
|
|
473
|
+
code: DIAG.FENCE_CHECK,
|
|
474
|
+
severity: 'error',
|
|
475
|
+
message: `src Program Graph build failed: ${errors[0]?.message || 'error'}`,
|
|
476
|
+
path: projectRoot,
|
|
477
|
+
});
|
|
478
|
+
ws.dispose?.();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
ws.dispose?.();
|
|
482
|
+
// Materialize program artifacts into project dist for loadProgramApiIndex.
|
|
483
|
+
const destDist = path.join(projectRoot, 'dist');
|
|
484
|
+
copyDir(outDir, destDist);
|
|
485
|
+
}
|
|
486
|
+
finally {
|
|
487
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function copyDir(from, to) {
|
|
491
|
+
fs.mkdirSync(to, { recursive: true });
|
|
492
|
+
for (const ent of fs.readdirSync(from, { withFileTypes: true })) {
|
|
493
|
+
const src = path.join(from, ent.name);
|
|
494
|
+
const dst = path.join(to, ent.name);
|
|
495
|
+
if (ent.isDirectory())
|
|
496
|
+
copyDir(src, dst);
|
|
497
|
+
else
|
|
498
|
+
fs.copyFileSync(src, dst);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/** Lazy note: callers pass `createWorkspace` from `./index.js` (see document-cmd / document-build). */
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integrated DocumentMount — build /documents into the host app dist so
|
|
3
|
+
* routeBase (e.g. /d) is served as static HTML next to SSR pages.
|
|
4
|
+
* Design: 规划设计/vmz/19 · Integrated DocumentMount (same project, no separate package).
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} projectRoot
|
|
8
|
+
*/
|
|
9
|
+
export declare function projectHasDocuments(projectRoot: any): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Build integrated documents into the application outDir (URL-aligned).
|
|
12
|
+
* @param {{ projectRoot: string, outDir: string, strict?: boolean }} opts
|
|
13
|
+
* @returns {Promise<{ ok: boolean, skipped?: boolean, pages?: number, error?: string }>}
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildIntegratedDocuments(opts: any): Promise<{
|
|
16
|
+
ok: boolean;
|
|
17
|
+
skipped: boolean;
|
|
18
|
+
error?: undefined;
|
|
19
|
+
pages?: undefined;
|
|
20
|
+
} | {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
error: string;
|
|
23
|
+
pages: number;
|
|
24
|
+
skipped?: undefined;
|
|
25
|
+
} | {
|
|
26
|
+
ok: boolean;
|
|
27
|
+
pages: number;
|
|
28
|
+
skipped?: undefined;
|
|
29
|
+
error?: undefined;
|
|
30
|
+
} | {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
error: string;
|
|
33
|
+
skipped?: undefined;
|
|
34
|
+
pages?: undefined;
|
|
35
|
+
}>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Integrated DocumentMount — build /documents into the host app dist so
|
|
4
|
+
* routeBase (e.g. /d) is served as static HTML next to SSR pages.
|
|
5
|
+
* Design: 规划设计/vmz/19 · Integrated DocumentMount (same project, no separate package).
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { buildDocuments } from './document-build.js';
|
|
10
|
+
import { resolveDocumentsRoot } from './document-check.js';
|
|
11
|
+
import { log } from './log.js';
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} projectRoot
|
|
14
|
+
*/
|
|
15
|
+
export function projectHasDocuments(projectRoot) {
|
|
16
|
+
const root = resolveDocumentsRoot(projectRoot);
|
|
17
|
+
return fs.existsSync(root) && fs.statSync(root).isDirectory();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build integrated documents into the application outDir (URL-aligned).
|
|
21
|
+
* @param {{ projectRoot: string, outDir: string, strict?: boolean }} opts
|
|
22
|
+
* @returns {Promise<{ ok: boolean, skipped?: boolean, pages?: number, error?: string }>}
|
|
23
|
+
*/
|
|
24
|
+
export async function buildIntegratedDocuments(opts) {
|
|
25
|
+
const projectRoot = path.resolve(opts.projectRoot);
|
|
26
|
+
const outDir = path.resolve(opts.outDir);
|
|
27
|
+
if (!projectHasDocuments(projectRoot)) {
|
|
28
|
+
return { ok: true, skipped: true };
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const result = await buildDocuments({
|
|
32
|
+
projectRoot,
|
|
33
|
+
outDir,
|
|
34
|
+
strict: Boolean(opts.strict),
|
|
35
|
+
});
|
|
36
|
+
if (!result.ok) {
|
|
37
|
+
const errs = (result.manifest?.diagnostics || []).filter((d) => d.severity === 'error');
|
|
38
|
+
for (const d of errs.slice(0, 12)) {
|
|
39
|
+
log.error(`${d.code}: ${d.message}`);
|
|
40
|
+
}
|
|
41
|
+
return { ok: false, error: 'document diagnostics', pages: 0 };
|
|
42
|
+
}
|
|
43
|
+
writeMountRootRedirects(result.manifest, outDir);
|
|
44
|
+
log.info(`document mount: pages=${result.pages.length} → ${path.relative(process.cwd(), outDir) || '.'}`);
|
|
45
|
+
return { ok: true, pages: result.pages.length };
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
49
|
+
log.error(`document mount failed: ${msg}`);
|
|
50
|
+
return { ok: false, error: msg };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Emit `{routeBase}/index.html` → defaultLocale landing (for /d/ and /docs/).
|
|
55
|
+
* @param {import('./document-schema.js').DocumentManifest} manifest
|
|
56
|
+
* @param {string} outDir
|
|
57
|
+
*/
|
|
58
|
+
function writeMountRootRedirects(manifest, outDir) {
|
|
59
|
+
const defaultLocale = manifest.defaultLocale || manifest.locales?.[0];
|
|
60
|
+
if (!defaultLocale)
|
|
61
|
+
return;
|
|
62
|
+
for (const mount of manifest.mounts || []) {
|
|
63
|
+
if (!mount?.routeBase || mount.routeBase === '/')
|
|
64
|
+
continue;
|
|
65
|
+
const base = String(mount.routeBase).replace(/\/$/, '');
|
|
66
|
+
const target = `${base}/${defaultLocale}/`;
|
|
67
|
+
const relDir = base.replace(/^\//, '');
|
|
68
|
+
const abs = path.join(outDir, relDir, 'index.html');
|
|
69
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
70
|
+
const html = `<!DOCTYPE html>
|
|
71
|
+
<html lang="${escapeAttr(defaultLocale)}">
|
|
72
|
+
<head>
|
|
73
|
+
<meta charset="utf-8" />
|
|
74
|
+
<meta http-equiv="refresh" content="0;url=${escapeAttr(target)}" />
|
|
75
|
+
<link rel="canonical" href="${escapeAttr(target)}" />
|
|
76
|
+
<title>Documents</title>
|
|
77
|
+
</head>
|
|
78
|
+
<body>
|
|
79
|
+
<p><a href="${escapeAttr(target)}">Continue to ${escapeAttr(defaultLocale)} docs</a></p>
|
|
80
|
+
</body>
|
|
81
|
+
</html>
|
|
82
|
+
`;
|
|
83
|
+
fs.writeFileSync(abs, html, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** @param {string} s */
|
|
87
|
+
function escapeAttr(s) {
|
|
88
|
+
return String(s).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
|
89
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip tags / collapse whitespace for search body text.
|
|
3
|
+
* @param {string} html
|
|
4
|
+
*/
|
|
5
|
+
export declare function htmlToSearchText(html: any): string;
|
|
6
|
+
/**
|
|
7
|
+
* @param {{
|
|
8
|
+
* manifest: any,
|
|
9
|
+
* enriched: { byId: Map<string, any> },
|
|
10
|
+
* evidence: any,
|
|
11
|
+
* version?: string | null,
|
|
12
|
+
* }} opts
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildDocumentSearch(opts: any): {
|
|
15
|
+
schema: string;
|
|
16
|
+
status: string;
|
|
17
|
+
version: any;
|
|
18
|
+
records: any[];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Island-only resume plan for document surfaces.
|
|
22
|
+
* @param {{
|
|
23
|
+
* evidence: any,
|
|
24
|
+
* searchHref?: string,
|
|
25
|
+
* fenceBodies?: Map<string, string>,
|
|
26
|
+
* }} opts
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildDocumentIslands(opts: any): {
|
|
29
|
+
schema: string;
|
|
30
|
+
hydrate: string;
|
|
31
|
+
fullPageHydrate: boolean;
|
|
32
|
+
islands: {
|
|
33
|
+
name: string;
|
|
34
|
+
kind: string;
|
|
35
|
+
resume: string;
|
|
36
|
+
index: any;
|
|
37
|
+
}[];
|
|
38
|
+
status: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Relative href from an HTML page to a root artifact (posix).
|
|
42
|
+
* @param {string} htmlRel
|
|
43
|
+
* @param {string} artifactName
|
|
44
|
+
*/
|
|
45
|
+
export declare function artifactHrefFromHtml(htmlRel: any, artifactName: any): string;
|
|
46
|
+
/**
|
|
47
|
+
* Stable map key for fence body lookup.
|
|
48
|
+
* @param {{ locale: string, pageKey: string, lineStart: number }} f
|
|
49
|
+
*/
|
|
50
|
+
export declare function fenceBodyKey(f: any): string;
|
|
51
|
+
/**
|
|
52
|
+
* Collect fence bodies from analyzeMarkdown results for playground islands.
|
|
53
|
+
* @param {Map<string, { fences?: any[] }>} analyzedByPageId locale:pageKey → analyze result
|
|
54
|
+
* @param {any[]} pages manifest.pages
|
|
55
|
+
*/
|
|
56
|
+
export declare function collectFenceBodies(analyzedByPageId: any, pages: any): Map<any, any>;
|
|
57
|
+
/**
|
|
58
|
+
* Render SSR island shells (no script — resume later).
|
|
59
|
+
* @param {{
|
|
60
|
+
* islands: any,
|
|
61
|
+
* searchIndexHref: string,
|
|
62
|
+
* pageKey: string,
|
|
63
|
+
* locale: string,
|
|
64
|
+
* }} opts
|
|
65
|
+
*/
|
|
66
|
+
export declare function renderIslandShellsHtml(opts: any): {
|
|
67
|
+
searchHtml: string;
|
|
68
|
+
playgroundHtml: any;
|
|
69
|
+
};
|