@vmz/vmz 0.3.6 → 0.3.8
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 +58 -55
- package/bin/vmz.js +9 -1
- package/delivery-runtime-files.json +38 -38
- package/dist/cli/cli.js +21 -17
- package/dist/dev/config-cycle-worker.d.ts +7 -0
- package/dist/dev/config-cycle-worker.js +83 -0
- package/dist/dev/config-cycle.d.ts +45 -0
- package/dist/dev/config-cycle.js +152 -0
- package/dist/dev/dev-session.d.ts +11 -0
- package/dist/dev/dev-session.js +89 -15
- package/dist/dev/dev-watch-roots.d.ts +7 -0
- package/dist/dev/dev-watch-roots.js +65 -2
- package/dist/document/document-build.js +25 -25
- package/dist/document/document-designs.d.ts +1 -1
- package/dist/document/document-designs.js +31 -119
- package/dist/document/document-markdown.d.ts +1 -1
- package/dist/document/document-markdown.js +7 -21
- package/dist/workspace/plugin-host.d.ts +3 -5
- package/dist/workspace/plugin-host.js +85 -49
- package/dist/workspace/public-api.d.ts +5 -1
- package/dist/workspace/public-api.js +6 -22
- package/dist/workspace/wechat-packaging.d.ts +9 -2
- package/dist/workspace/wechat-packaging.js +15 -20
- package/locales/en-US/cli.json +126 -126
- package/locales/en-US/diag.json +7 -1
- package/locales/zh-CN/diag.json +3 -1
- package/package.json +23 -21
package/dist/dev/dev-session.js
CHANGED
|
@@ -14,13 +14,20 @@ import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import { buildIntegratedDocuments, projectHasDocuments } from '../document/document-integrate.js';
|
|
16
16
|
import { createWorkspace, resolveNativePath } from '../workspace/public-api.js';
|
|
17
|
+
import { materializeWechatPackaging } from '../workspace/wechat-packaging.js';
|
|
17
18
|
import { emitLocaleRuntimeModules, localeHasErrors } from '../locale/locale-check.js';
|
|
18
19
|
import { log } from '../workspace/log.js';
|
|
19
|
-
import { coalesceRootBurst, collectDevWatchRoots, classifyWatchRoot, isDependencyPath, mergeDirtySets,
|
|
20
|
+
import { coalesceRootBurst, collectDevWatchRoots, classifyWatchRoot, isDependencyPath, mergeDirtySets, collectConfigDependencies, configCandidatePaths, configFingerprintMap, } from './dev-watch-roots.js';
|
|
21
|
+
import { createConfigCycle } from './config-cycle.js';
|
|
20
22
|
import { buildReloadPayload, filterGenerationIgnore, parseHmrPlan, registerWrittenOutputsIgnore, shouldSoftReload, staticRevisionFromArtifact, } from './dev-incremental.js';
|
|
21
23
|
import { diffFingerprints, fileFingerprintMap } from '../workspace/watch-diff.js';
|
|
22
24
|
/** Track serve-host so a failed/killed launcher cannot leave an orphan port. */
|
|
23
25
|
const DEV_HOST_PID = '.vmz-dev-host.pid';
|
|
26
|
+
function cycleFailure(report) {
|
|
27
|
+
const detail = report.error?.args?.detail ?? 'config build process failed';
|
|
28
|
+
const error = new Error(detail);
|
|
29
|
+
return report.error ? Object.assign(error, { diagnostic: report.error }) : error;
|
|
30
|
+
}
|
|
24
31
|
export function createDevSession(options) {
|
|
25
32
|
const project = options.project;
|
|
26
33
|
const outDir = options.outDir;
|
|
@@ -28,9 +35,13 @@ export function createDevSession(options) {
|
|
|
28
35
|
const port = options.port ?? 5173;
|
|
29
36
|
const pollMs = Math.max(50, options.pollMs ?? 300);
|
|
30
37
|
const wechatPreview = options.target === 'mini-program-wechat';
|
|
38
|
+
let delivery = options.delivery ?? null;
|
|
31
39
|
const createWs = options.createWorkspaceFn ?? createWorkspace;
|
|
32
40
|
const spawnHost = options.spawnHostFn ?? defaultSpawnHost;
|
|
33
41
|
const softReload = options.softReloadFn ?? defaultSoftReload;
|
|
42
|
+
const makeConfigCycle = options.configCycleFn ?? createConfigCycle;
|
|
43
|
+
const configCandidates = configCandidatePaths(project);
|
|
44
|
+
let configCycle = null;
|
|
34
45
|
const ws = createWs({ root: project, outDir });
|
|
35
46
|
let child = null;
|
|
36
47
|
let stopped = false;
|
|
@@ -119,6 +130,8 @@ export function createDevSession(options) {
|
|
|
119
130
|
}
|
|
120
131
|
let report;
|
|
121
132
|
try {
|
|
133
|
+
// Same prepared snapshot for this build cycle; native pack never reads config.
|
|
134
|
+
materializeWechatPackaging(project, delivery);
|
|
122
135
|
const raw = ws.lowerMiniprogramWechatPackaging();
|
|
123
136
|
report = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
124
137
|
}
|
|
@@ -191,15 +204,15 @@ export function createDevSession(options) {
|
|
|
191
204
|
if (!existsSync(src)) {
|
|
192
205
|
throw new Error(`vmz dev: missing src/ under ${project}`);
|
|
193
206
|
}
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
207
|
+
// Config evaluation + plugin contributions run in a detached build process.
|
|
208
|
+
// Functions/plugin objects never cross the boundary; only the data report does.
|
|
209
|
+
if (configCandidates.some((p) => existsSync(p))) {
|
|
210
|
+
configCycle = makeConfigCycle({ project, outDir, ...(options.configCycleWorkerPath ? { workerPath: options.configCycleWorkerPath } : {}) });
|
|
211
|
+
const report = await configCycle.start();
|
|
212
|
+
if (!report.ok)
|
|
213
|
+
throw cycleFailure(report);
|
|
214
|
+
delivery = report.delivery ?? delivery;
|
|
215
|
+
log.info(`config build process ready${report.plugins.length ? ` (${report.plugins.map((p) => p.name).join(', ')})` : ''}`);
|
|
203
216
|
}
|
|
204
217
|
log.info('initial build (N-API workspace, full)…');
|
|
205
218
|
const initialPair = planAndRebuild();
|
|
@@ -238,7 +251,14 @@ export function createDevSession(options) {
|
|
|
238
251
|
const designsRoot = path.join(project, 'designs');
|
|
239
252
|
const watched = collectDevWatchRoots({ project, outDir });
|
|
240
253
|
const publicRoot = watched.publicRoot;
|
|
241
|
-
|
|
254
|
+
/** Candidate config files plus their local reachable deps; refreshed after each cycle. */
|
|
255
|
+
const refreshConfigPaths = () => {
|
|
256
|
+
const deps = configCandidates
|
|
257
|
+
.filter((p) => existsSync(p))
|
|
258
|
+
.flatMap((p) => collectConfigDependencies(p, project));
|
|
259
|
+
return [...new Set([...configCandidates, ...deps])];
|
|
260
|
+
};
|
|
261
|
+
let configPaths = refreshConfigPaths();
|
|
242
262
|
/** @type {string[]} */
|
|
243
263
|
const watchRoots = [...watched.roots];
|
|
244
264
|
/** @type {string[]} */
|
|
@@ -359,14 +379,66 @@ export function createDevSession(options) {
|
|
|
359
379
|
continue;
|
|
360
380
|
}
|
|
361
381
|
if (batch.configDirty) {
|
|
362
|
-
log.info('vmz.config change detected —
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
382
|
+
log.info('vmz.config change detected — restarting the config build process…');
|
|
383
|
+
let cycleReport = null;
|
|
384
|
+
if (configCandidates.some((p) => existsSync(p))) {
|
|
385
|
+
let failure = null;
|
|
386
|
+
try {
|
|
387
|
+
if (!configCycle) {
|
|
388
|
+
configCycle = makeConfigCycle({
|
|
389
|
+
project,
|
|
390
|
+
outDir,
|
|
391
|
+
...(options.configCycleWorkerPath ? { workerPath: options.configCycleWorkerPath } : {}),
|
|
392
|
+
});
|
|
393
|
+
cycleReport = await configCycle.start();
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
// Old process is cancelled and awaited before the new one writes artifacts.
|
|
397
|
+
cycleReport = await configCycle.restart();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
failure = err instanceof Error ? err : new Error(String(err));
|
|
402
|
+
}
|
|
403
|
+
configPaths = refreshConfigPaths();
|
|
404
|
+
configFingerprints = configFingerprintMap(configPaths);
|
|
405
|
+
commitFingerprints();
|
|
406
|
+
if (failure) {
|
|
407
|
+
log.warn(`config build process restart failed: ${failure.message}`);
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (cycleReport && !cycleReport.ok) {
|
|
411
|
+
if (cycleReport.error)
|
|
412
|
+
log.diagnostic(cycleReport.error);
|
|
413
|
+
else
|
|
414
|
+
log.error('config build process failed');
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (cycleReport)
|
|
418
|
+
delivery = cycleReport.delivery ?? delivery;
|
|
419
|
+
}
|
|
420
|
+
else if (configCycle) {
|
|
421
|
+
await configCycle.cancel();
|
|
422
|
+
configCycle = null;
|
|
423
|
+
configPaths = refreshConfigPaths();
|
|
424
|
+
configFingerprints = configFingerprintMap(configPaths);
|
|
425
|
+
commitFingerprints();
|
|
426
|
+
log.warn('vmz.config removed — config build process stopped, last artifacts kept');
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
configFingerprints = configFingerprintMap(configPaths);
|
|
431
|
+
commitFingerprints();
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
366
434
|
const report = rebuild();
|
|
367
435
|
if (printReport(report, 'rebuild')) {
|
|
368
436
|
lastOutputRevision = report.outputRevision ?? report.output_revision ?? null;
|
|
369
437
|
await emitPublicAndTrack();
|
|
438
|
+
if (wechatPreview)
|
|
439
|
+
packWechatPreview();
|
|
440
|
+
else
|
|
441
|
+
await respawnHost('config-changed');
|
|
370
442
|
}
|
|
371
443
|
continue;
|
|
372
444
|
}
|
|
@@ -605,6 +677,8 @@ export function createDevSession(options) {
|
|
|
605
677
|
stopped = true;
|
|
606
678
|
killChild(child);
|
|
607
679
|
child = null;
|
|
680
|
+
await configCycle?.cancel().catch(() => { });
|
|
681
|
+
configCycle = null;
|
|
608
682
|
clearDevHostPid(outDir, null);
|
|
609
683
|
try {
|
|
610
684
|
ws.dispose();
|
|
@@ -7,8 +7,15 @@
|
|
|
7
7
|
*/
|
|
8
8
|
/** Author-facing vs generated dev input bucket. */
|
|
9
9
|
export type DevInputKind = 'author' | 'dependency' | 'public' | 'devControl' | 'locales' | 'docs' | 'designs' | 'other';
|
|
10
|
+
/** All supported `vmz.config.*` candidate paths, existing or not (so creation is detected). */
|
|
11
|
+
export declare function configCandidatePaths(project: string): string[];
|
|
10
12
|
/** `vmz.config.*` at project root (devControl). */
|
|
11
13
|
export declare function collectDevConfigPaths(project: string): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Reachable local ESM dependencies of a config file (bounded, project-root only).
|
|
16
|
+
* These changes restart the dev config build process.
|
|
17
|
+
*/
|
|
18
|
+
export declare function collectConfigDependencies(configPath: string, project: string): string[];
|
|
12
19
|
/** Fingerprint map for explicit config files (not directory walks). */
|
|
13
20
|
export declare function configFingerprintMap(configPaths: string[]): Map<string, string>;
|
|
14
21
|
export interface DirtySet {
|
|
@@ -8,11 +8,74 @@
|
|
|
8
8
|
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { diffFingerprints, fileFingerprintMap } from '../workspace/watch-diff.js';
|
|
11
|
-
const CONFIG_BASENAMES = ['vmz.config.ts', 'vmz.config.js', 'vmz.config.mjs', 'vmz.config.cjs'];
|
|
11
|
+
const CONFIG_BASENAMES = ['vmz.config.ts', 'vmz.config.mts', 'vmz.config.js', 'vmz.config.mjs', 'vmz.config.cjs'];
|
|
12
|
+
const LOCAL_MODULE_EXTENSIONS = ['.ts', '.mts', '.js', '.mjs', '.cts', '.cjs'];
|
|
13
|
+
/** All supported `vmz.config.*` candidate paths, existing or not (so creation is detected). */
|
|
14
|
+
export function configCandidatePaths(project) {
|
|
15
|
+
const root = path.resolve(project);
|
|
16
|
+
return CONFIG_BASENAMES.map((name) => path.join(root, name));
|
|
17
|
+
}
|
|
12
18
|
/** `vmz.config.*` at project root (devControl). */
|
|
13
19
|
export function collectDevConfigPaths(project) {
|
|
20
|
+
return configCandidatePaths(project).filter((p) => existsSync(p));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reachable local ESM dependencies of a config file (bounded, project-root only).
|
|
24
|
+
* These changes restart the dev config build process.
|
|
25
|
+
*/
|
|
26
|
+
export function collectConfigDependencies(configPath, project) {
|
|
14
27
|
const root = path.resolve(project);
|
|
15
|
-
|
|
28
|
+
const underRoot = (file) => file === root || file.startsWith(root + path.sep);
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
const out = [];
|
|
31
|
+
const visit = (file, depth) => {
|
|
32
|
+
if (depth > 24 || seen.size > 200 || !existsSync(file) || !underRoot(file))
|
|
33
|
+
return;
|
|
34
|
+
if (seen.has(file))
|
|
35
|
+
return;
|
|
36
|
+
seen.add(file);
|
|
37
|
+
let text;
|
|
38
|
+
try {
|
|
39
|
+
text = readFileSync(file, 'utf8');
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const specs = new Set();
|
|
45
|
+
for (const match of text.matchAll(/\b(?:import|export)\s+(?:[^'";]*?\s+from\s+)?['"]([^'"]+)['"]/g)) {
|
|
46
|
+
if (match[1])
|
|
47
|
+
specs.add(match[1]);
|
|
48
|
+
}
|
|
49
|
+
for (const match of text.matchAll(/\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g)) {
|
|
50
|
+
if (match[1])
|
|
51
|
+
specs.add(match[1]);
|
|
52
|
+
}
|
|
53
|
+
for (const spec of specs) {
|
|
54
|
+
if (!spec.startsWith('.'))
|
|
55
|
+
continue;
|
|
56
|
+
const base = path.resolve(path.dirname(file), spec);
|
|
57
|
+
const candidates = [
|
|
58
|
+
base,
|
|
59
|
+
...LOCAL_MODULE_EXTENSIONS.map((ext) => base + ext),
|
|
60
|
+
...LOCAL_MODULE_EXTENSIONS.map((ext) => path.join(base, `index${ext}`)),
|
|
61
|
+
];
|
|
62
|
+
for (const candidate of candidates) {
|
|
63
|
+
try {
|
|
64
|
+
if (existsSync(candidate) && statSync(candidate).isFile()) {
|
|
65
|
+
if (!seen.has(candidate))
|
|
66
|
+
out.push(candidate);
|
|
67
|
+
visit(candidate, depth + 1);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* keep looking */
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
visit(path.resolve(configPath), 0);
|
|
78
|
+
return out;
|
|
16
79
|
}
|
|
17
80
|
/** Fingerprint map for explicit config files (not directory walks). */
|
|
18
81
|
export function configFingerprintMap(configPaths) {
|
|
@@ -66,7 +66,7 @@ export async function buildDocuments(opts) {
|
|
|
66
66
|
assertIntegratedDistReady(appDistDir);
|
|
67
67
|
}
|
|
68
68
|
fs.mkdirSync(outDir, { recursive: true });
|
|
69
|
-
const designs = resolveDocumentDesignsCss(projectRoot);
|
|
69
|
+
const designs = resolveDocumentDesignsCss(projectRoot, appDistDir);
|
|
70
70
|
let designsHref = null;
|
|
71
71
|
if (designs.css && designs.href) {
|
|
72
72
|
const cssPath = path.join(outDir, designs.href);
|
|
@@ -195,20 +195,20 @@ function buildDocumentSlotHtml({ nav, bodyHtml, headings, htmlRel, route, routin
|
|
|
195
195
|
.map((h) => ` <li class="h${h.level}"><a href="#${esc(h.id)}">${esc(h.text)}</a></li>`)
|
|
196
196
|
.join('\n')}\n </ol>\n </nav>\n`
|
|
197
197
|
: '';
|
|
198
|
-
const docsNav = ` <nav aria-label="Documents" class="doc-subnav">
|
|
199
|
-
<ul>
|
|
200
|
-
${navItems}
|
|
201
|
-
</ul>
|
|
198
|
+
const docsNav = ` <nav aria-label="Documents" class="doc-subnav">
|
|
199
|
+
<ul>
|
|
200
|
+
${navItems}
|
|
201
|
+
</ul>
|
|
202
202
|
</nav>`;
|
|
203
|
-
return ` <aside class="doc-sidebar">
|
|
204
|
-
${docsNav}
|
|
205
|
-
${searchShellHtml}
|
|
206
|
-
</aside>
|
|
207
|
-
<div class="doc-content">
|
|
208
|
-
${toc}<main id="main">
|
|
209
|
-
${bodyHtml}
|
|
210
|
-
${playgroundShellHtml}
|
|
211
|
-
</main>
|
|
203
|
+
return ` <aside class="doc-sidebar">
|
|
204
|
+
${docsNav}
|
|
205
|
+
${searchShellHtml}
|
|
206
|
+
</aside>
|
|
207
|
+
<div class="doc-content">
|
|
208
|
+
${toc}<main id="main">
|
|
209
|
+
${bodyHtml}
|
|
210
|
+
${playgroundShellHtml}
|
|
211
|
+
</main>
|
|
212
212
|
</div>`;
|
|
213
213
|
}
|
|
214
214
|
/**
|
|
@@ -242,17 +242,17 @@ function renderStaticHtml({ title, locale, route, nav, bodyHtml, headings, desig
|
|
|
242
242
|
.map((h) => ` <li class="h${h.level}"><a href="#${esc(h.id)}">${esc(h.text)}</a></li>`)
|
|
243
243
|
.join('\n')}\n </ol>\n </nav>\n`
|
|
244
244
|
: '';
|
|
245
|
-
bodyInner = ` <a class="skip-link" href="#main">Skip to content</a>
|
|
246
|
-
<nav aria-label="Documents" class="doc-subnav">
|
|
247
|
-
<ul>
|
|
248
|
-
${navItems}
|
|
249
|
-
</ul>
|
|
250
|
-
</nav>
|
|
251
|
-
${searchShellHtml}
|
|
252
|
-
${toc}<main id="main">
|
|
253
|
-
${bodyHtml}
|
|
254
|
-
${playgroundShellHtml}
|
|
255
|
-
</main>
|
|
245
|
+
bodyInner = ` <a class="skip-link" href="#main">Skip to content</a>
|
|
246
|
+
<nav aria-label="Documents" class="doc-subnav">
|
|
247
|
+
<ul>
|
|
248
|
+
${navItems}
|
|
249
|
+
</ul>
|
|
250
|
+
</nav>
|
|
251
|
+
${searchShellHtml}
|
|
252
|
+
${toc}<main id="main">
|
|
253
|
+
${bodyHtml}
|
|
254
|
+
${playgroundShellHtml}
|
|
255
|
+
</main>
|
|
256
256
|
`;
|
|
257
257
|
}
|
|
258
258
|
const native = requireNativeAddon();
|
|
@@ -3,4 +3,4 @@ export interface DocumentDesignsCssResult {
|
|
|
3
3
|
source: string | null;
|
|
4
4
|
href: string | null;
|
|
5
5
|
}
|
|
6
|
-
export declare function resolveDocumentDesignsCss(projectRoot: string): DocumentDesignsCssResult;
|
|
6
|
+
export declare function resolveDocumentDesignsCss(projectRoot: string, appDistDir?: string | null): DocumentDesignsCssResult;
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Document — wire project `/designs`
|
|
3
|
-
*
|
|
2
|
+
* Document — wire project `/designs` and the compiled app stylesheet into
|
|
3
|
+
* static document assets.
|
|
4
|
+
*
|
|
5
|
+
* Layout chrome comes from `designs/document/chrome.css`. Theme tokens are not
|
|
6
|
+
* re-derived here: they come from the app build output (`vmz-designs.css` /
|
|
7
|
+
* `vmz.css` under `appDistDir`), which is the single compiled artifact.
|
|
4
8
|
*/
|
|
5
9
|
import fs from 'node:fs';
|
|
6
10
|
import path from 'node:path';
|
|
7
|
-
export function resolveDocumentDesignsCss(projectRoot) {
|
|
11
|
+
export function resolveDocumentDesignsCss(projectRoot, appDistDir) {
|
|
8
12
|
const designsDir = path.join(projectRoot, 'designs');
|
|
9
13
|
if (!fs.existsSync(designsDir) || !fs.statSync(designsDir).isDirectory()) {
|
|
10
14
|
return { css: '', source: null, href: null };
|
|
11
15
|
}
|
|
12
16
|
const parts = [];
|
|
13
17
|
const sources = [];
|
|
14
|
-
// Layout first so @import
|
|
15
|
-
const layoutCandidates = [
|
|
18
|
+
// Layout first so @import stays at stylesheet top; compiled tokens follow.
|
|
19
|
+
const layoutCandidates = [
|
|
20
|
+
path.join(designsDir, 'document', 'chrome.css'),
|
|
21
|
+
path.join(designsDir, 'styles', 'document.css'),
|
|
22
|
+
];
|
|
16
23
|
for (const p of layoutCandidates) {
|
|
17
24
|
if (!fs.existsSync(p))
|
|
18
25
|
continue;
|
|
@@ -20,122 +27,27 @@ export function resolveDocumentDesignsCss(projectRoot) {
|
|
|
20
27
|
sources.push(path.relative(projectRoot, p).replace(/\\/g, '/'));
|
|
21
28
|
break;
|
|
22
29
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
sources.push(path.relative(projectRoot, p).replace(/\\/g, '/'));
|
|
28
|
-
break;
|
|
30
|
+
if (appDistDir) {
|
|
31
|
+
const compiled = [path.join(appDistDir, 'vmz-designs.css'), path.join(appDistDir, 'vmz.css')].find((p) => fs.existsSync(p));
|
|
32
|
+
if (!compiled) {
|
|
33
|
+
throw new Error(`vmz.document: app dist ${appDistDir} is missing vmz-designs.css / vmz.css; build the app before the documents`);
|
|
29
34
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const styleCandidates = [path.join(designsDir, 'styles', 'index.css')];
|
|
39
|
-
for (const p of styleCandidates) {
|
|
40
|
-
if (fs.existsSync(p)) {
|
|
41
|
-
return {
|
|
42
|
-
css: fs.readFileSync(p, 'utf8'),
|
|
43
|
-
source: path.relative(projectRoot, p).replace(/\\/g, '/'),
|
|
44
|
-
href: 'assets/vmz-designs.css',
|
|
45
|
-
};
|
|
35
|
+
parts.push(fs.readFileSync(compiled, 'utf8'));
|
|
36
|
+
sources.push(path.relative(projectRoot, compiled).replace(/\\/g, '/'));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const entry = path.join(designsDir, 'styles', 'index.css');
|
|
40
|
+
if (fs.existsSync(entry)) {
|
|
41
|
+
parts.push(fs.readFileSync(entry, 'utf8'));
|
|
42
|
+
sources.push(path.relative(projectRoot, entry).replace(/\\/g, '/'));
|
|
46
43
|
}
|
|
47
44
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return { css: emitted, source: 'designs/', href: 'assets/vmz-designs.css' };
|
|
51
|
-
}
|
|
52
|
-
return { css: '', source: 'designs/', href: null };
|
|
53
|
-
}
|
|
54
|
-
function emitMinimalDesignsCss(designsDir) {
|
|
55
|
-
const vars = {};
|
|
56
|
-
const tokenDir = path.join(designsDir, 'tokens');
|
|
57
|
-
if (fs.existsSync(tokenDir)) {
|
|
58
|
-
walkJson(tokenDir, (obj, prefix) => {
|
|
59
|
-
collectStyleThemeEntries(obj, vars);
|
|
60
|
-
flattenTokens(obj, prefix, vars);
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
const themeJson = path.join(designsDir, 'theme.json');
|
|
64
|
-
if (fs.existsSync(themeJson)) {
|
|
65
|
-
try {
|
|
66
|
-
const theme = JSON.parse(fs.readFileSync(themeJson, 'utf8'));
|
|
67
|
-
collectStyleThemeEntries(theme, vars);
|
|
68
|
-
flattenTokens(theme, '', vars);
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
/* ignore */
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
const keys = Object.keys(vars).sort();
|
|
75
|
-
if (!keys.length) {
|
|
76
|
-
// Presence of /designs still warrants a readable baseline sheet.
|
|
77
|
-
return `/* vmz.document designs baseline */\n:root { color-scheme: light; }\nbody { font-family: system-ui, sans-serif; line-height: 1.5; margin: 0; }\nmain { max-width: 48rem; margin: 0 auto; padding: 1.5rem; }\nnav { padding: 1rem 1.5rem; border-bottom: 1px solid #ddd; }\n`;
|
|
78
|
-
}
|
|
79
|
-
const lines = keys.map((k) => ` ${cssVar(k)}: ${vars[k]};`);
|
|
80
|
-
return `/* vmz.document designs from /designs */\n:root {\n${lines.join('\n')}\n}\nbody { font-family: var(--font-sans, system-ui, sans-serif); line-height: 1.5; margin: 0; color: var(--color-fg, #111); background: var(--color-bg, #fff); }\nmain { max-width: 48rem; margin: 0 auto; padding: 1.5rem; }\nnav { padding: 1rem 1.5rem; border-bottom: 1px solid var(--color-border, #ddd); }\n`;
|
|
81
|
-
}
|
|
82
|
-
function cssVar(key) {
|
|
83
|
-
const name = String(key)
|
|
84
|
-
.replace(/[^a-zA-Z0-9_-]+/g, '-')
|
|
85
|
-
.replace(/^-|-$/g, '');
|
|
86
|
-
return `--${name}`;
|
|
87
|
-
}
|
|
88
|
-
/** Style Theme entries (`key.path` + `value`) → `vmz-*` CSS vars (same naming as application compile). */
|
|
89
|
-
function collectStyleThemeEntries(obj, out) {
|
|
90
|
-
if (obj == null || typeof obj !== 'object' || Array.isArray(obj))
|
|
91
|
-
return;
|
|
92
|
-
const entries = obj.entries;
|
|
93
|
-
if (!Array.isArray(entries))
|
|
94
|
-
return;
|
|
95
|
-
for (const e of entries) {
|
|
96
|
-
const pathParts = e?.key?.path;
|
|
97
|
-
if (!Array.isArray(pathParts) || pathParts.length === 0)
|
|
98
|
-
continue;
|
|
99
|
-
if (typeof e.value !== 'string' && typeof e.value !== 'number')
|
|
100
|
-
continue;
|
|
101
|
-
const dotted = pathParts.map(String).join('-');
|
|
102
|
-
out[`vmz-${dotted}`] = String(e.value);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
function flattenTokens(obj, prefix, out) {
|
|
106
|
-
if (obj == null || typeof obj !== 'object' || Array.isArray(obj))
|
|
107
|
-
return;
|
|
108
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
109
|
-
if (k === 'entries')
|
|
110
|
-
continue;
|
|
111
|
-
const key = prefix ? `${prefix}-${k}` : k;
|
|
112
|
-
if (v != null && typeof v === 'object' && !Array.isArray(v)) {
|
|
113
|
-
if ('value' in v && (typeof v.value === 'string' || typeof v.value === 'number')) {
|
|
114
|
-
out[key] = String(v.value);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
flattenTokens(v, key, out);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
else if (typeof v === 'string' || typeof v === 'number') {
|
|
121
|
-
out[key] = String(v);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
function walkJson(dir, fn) {
|
|
126
|
-
if (!fs.existsSync(dir))
|
|
127
|
-
return;
|
|
128
|
-
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
129
|
-
const full = path.join(dir, ent.name);
|
|
130
|
-
if (ent.isDirectory())
|
|
131
|
-
walkJson(full, fn);
|
|
132
|
-
else if (ent.isFile() && ent.name.endsWith('.json')) {
|
|
133
|
-
try {
|
|
134
|
-
fn(JSON.parse(fs.readFileSync(full, 'utf8')), '');
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
/* ignore */
|
|
138
|
-
}
|
|
139
|
-
}
|
|
45
|
+
if (!parts.length) {
|
|
46
|
+
return { css: '', source: 'designs/', href: null };
|
|
140
47
|
}
|
|
48
|
+
return {
|
|
49
|
+
css: parts.join('\n\n'),
|
|
50
|
+
source: sources.join('+'),
|
|
51
|
+
href: 'assets/vmz-designs.css',
|
|
52
|
+
};
|
|
141
53
|
}
|
|
@@ -1,36 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Document — resolve engines.markdown via the official plugin runtime.
|
|
2
|
+
* Document — resolve engines.markdown via the official plugin runtime export.
|
|
3
3
|
*/
|
|
4
|
-
import { createRequire } from 'node:module';
|
|
5
|
-
import { existsSync } from 'node:fs';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { importMaybeTs } from '../workspace/plugin-host.js';
|
|
9
|
-
const require = createRequire(import.meta.url);
|
|
10
4
|
export async function resolveMarkdownEngine(opts = {}) {
|
|
11
5
|
const id = opts.engines?.markdown || 'markdown-it';
|
|
12
6
|
if (id !== 'markdown-it') {
|
|
13
7
|
throw new Error(`unsupported engines.markdown ${JSON.stringify(id)} (markdown-it only)`);
|
|
14
8
|
}
|
|
15
|
-
let
|
|
9
|
+
let mod;
|
|
16
10
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
// Public JS runtime export of the plugin package; never read the package directory.
|
|
12
|
+
mod = await import('@vmz/plugin-markdown-it/runtime');
|
|
19
13
|
}
|
|
20
|
-
catch {
|
|
21
|
-
// Developer mode: monorepo source checkout.
|
|
22
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
-
const fallback = path.resolve(here, '../../../plugins/vmz-plugin-markdown-it/runtime.ts');
|
|
24
|
-
if (existsSync(fallback))
|
|
25
|
-
runtimeFile = fallback;
|
|
26
|
-
}
|
|
27
|
-
if (!runtimeFile || !existsSync(runtimeFile)) {
|
|
14
|
+
catch (err) {
|
|
28
15
|
throw new Error('markdown engine needs `@vmz/plugin-markdown-it` (optional peer of `@vmz/vmz`).\n' +
|
|
29
|
-
' Install: pnpm add -D @vmz/plugin-markdown-it');
|
|
16
|
+
' Install: pnpm add -D @vmz/plugin-markdown-it', { cause: err });
|
|
30
17
|
}
|
|
31
|
-
const mod = await importMaybeTs(runtimeFile);
|
|
32
18
|
if (typeof mod.analyzeMarkdown !== 'function') {
|
|
33
|
-
throw new Error(
|
|
19
|
+
throw new Error('cannot load markdown engine markdown-it: runtime export is missing analyzeMarkdown');
|
|
34
20
|
}
|
|
35
21
|
return {
|
|
36
22
|
engine: id,
|
|
@@ -4,11 +4,9 @@
|
|
|
4
4
|
import { contentHash, defineConfig, definePlugin } from '@vmz/plugin';
|
|
5
5
|
export { contentHash, defineConfig, definePlugin };
|
|
6
6
|
export declare const PLUGIN_PROTOCOL = "0.1.0";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*/
|
|
11
|
-
export declare function importMaybeTs(full: any): Promise<any>;
|
|
7
|
+
export declare const CONFIG_NAMES: string[];
|
|
8
|
+
export declare const CONFIG_LOAD_FAILED = "vmz::config::load_failed";
|
|
9
|
+
export declare const PLUGIN_ENTRY_INVALID = "vmz::plugin::invalid_entry";
|
|
12
10
|
/**
|
|
13
11
|
* Load `vmz.config.*` from project root (+ optional root `vmz.plugin.*`).
|
|
14
12
|
* @param {string} project
|