@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.
Files changed (75) hide show
  1. package/README.md +48 -2
  2. package/bin/vmz.js +4 -0
  3. package/dist/application-cmd.d.ts +22 -0
  4. package/dist/application-cmd.js +348 -0
  5. package/dist/bundler-adapter.d.ts +64 -0
  6. package/dist/bundler-adapter.js +111 -0
  7. package/dist/cli.d.ts +15 -0
  8. package/dist/cli.js +370 -0
  9. package/dist/dev-session.d.ts +35 -0
  10. package/dist/dev-session.js +290 -0
  11. package/dist/document-build.d.ts +99 -0
  12. package/dist/document-build.js +274 -0
  13. package/dist/document-check.d.ts +44 -0
  14. package/dist/document-check.js +246 -0
  15. package/dist/document-cmd.d.ts +9 -0
  16. package/dist/document-cmd.js +147 -0
  17. package/dist/document-designs.d.ts +9 -0
  18. package/dist/document-designs.js +126 -0
  19. package/dist/document-enrich.d.ts +23 -0
  20. package/dist/document-enrich.js +234 -0
  21. package/dist/document-evidence.d.ts +49 -0
  22. package/dist/document-evidence.js +501 -0
  23. package/dist/document-integrate.d.ts +35 -0
  24. package/dist/document-integrate.js +89 -0
  25. package/dist/document-interactive.d.ts +69 -0
  26. package/dist/document-interactive.js +255 -0
  27. package/dist/document-locale.d.ts +31 -0
  28. package/dist/document-locale.js +59 -0
  29. package/dist/document-markdown.d.ts +13 -0
  30. package/dist/document-markdown.js +39 -0
  31. package/dist/document-scan.d.ts +21 -0
  32. package/dist/document-scan.js +151 -0
  33. package/dist/document-schema.d.ts +87 -0
  34. package/dist/document-schema.js +88 -0
  35. package/dist/explain-cmd.d.ts +5 -0
  36. package/dist/explain-cmd.js +123 -0
  37. package/dist/index.d.ts +808 -0
  38. package/dist/index.js +569 -0
  39. package/dist/locale-check.d.ts +106 -0
  40. package/dist/locale-check.js +737 -0
  41. package/dist/locale-cmd.d.ts +5 -0
  42. package/dist/locale-cmd.js +443 -0
  43. package/dist/locale-delivery.d.ts +298 -0
  44. package/dist/locale-delivery.js +444 -0
  45. package/dist/locale-router.d.ts +207 -0
  46. package/dist/locale-router.js +508 -0
  47. package/dist/locale-runtime.d.ts +406 -0
  48. package/dist/locale-runtime.js +542 -0
  49. package/dist/locale-schema.d.ts +9 -0
  50. package/dist/locale-schema.js +10 -0
  51. package/dist/locale-tooling.d.ts +118 -0
  52. package/dist/locale-tooling.js +358 -0
  53. package/dist/log.d.ts +19 -0
  54. package/dist/log.js +42 -0
  55. package/dist/packages.d.ts +27 -0
  56. package/dist/packages.js +147 -0
  57. package/dist/plugin-host.d.ts +30 -0
  58. package/dist/plugin-host.js +370 -0
  59. package/dist/refactor-cmd.d.ts +8 -0
  60. package/dist/refactor-cmd.js +156 -0
  61. package/dist/resolve.d.ts +25 -0
  62. package/dist/resolve.js +56 -0
  63. package/dist/test-cmd.d.ts +9 -0
  64. package/dist/test-cmd.js +343 -0
  65. package/dist/test-compile.d.ts +2 -0
  66. package/dist/test-compile.js +3 -0
  67. package/dist/test-discover.d.ts +2 -0
  68. package/dist/test-discover.js +3 -0
  69. package/dist/test-logic.d.ts +2 -0
  70. package/dist/test-logic.js +3 -0
  71. package/dist/test-protocol.d.ts +2 -0
  72. package/dist/test-protocol.js +3 -0
  73. package/dist/watch-diff.d.ts +17 -0
  74. package/dist/watch-diff.js +56 -0
  75. package/package.json +81 -3
@@ -0,0 +1,358 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Locale I5 tooling: explain · diff · extract · pseudo · cross-host conformance.
4
+ * Design: 规划设计/vmz/28 §12–§13
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { DIAG_LOCALE_CONFORMANCE_DIVERGENCE, DIAG_LOCALE_EXPLAIN_UNKNOWN, DIAG_LOCALE_HARDCODED_TEXT, DIAG_LOCALE_PSEUDO_PRODUCTION_FORBIDDEN, DIAG_MESSAGE_DYNAMIC_ID_UNBOUNDED, FORMATTER_DATA_VERSION, LOCALE_CONFORMANCE_SCHEMA, LOCALE_DIFF_SCHEMA, LOCALE_EXPLAIN_SCHEMA, LOCALE_EXTRACT_SCHEMA, LOCALE_PSEUDO_SCHEMA, } from './locale-schema.js';
9
+ import { resolveMessageVariant } from './locale-runtime.js';
10
+ import { assertHostMessageInvariant, buildLocaleDeliveryResolution } from './locale-delivery.js';
11
+ /**
12
+ * Explain one MessageId: definition, params, variants, fallback, delivery reachability.
13
+ * @param {{
14
+ * messageId: string,
15
+ * locale?: string|null,
16
+ * deliveryId?: string|null,
17
+ * checkReport: any,
18
+ * }} input
19
+ */
20
+ export function explainLocaleMessage(input) {
21
+ /** @type {any[]} */
22
+ const diagnostics = [];
23
+ const messages = input.checkReport?.messageCatalog?.messages || [];
24
+ const node = messages.find((m) => m.messageId === input.messageId);
25
+ if (!node) {
26
+ diagnostics.push({
27
+ code: DIAG_LOCALE_EXPLAIN_UNKNOWN,
28
+ severity: 'error',
29
+ message: `unknown MessageId ${input.messageId}`,
30
+ });
31
+ return {
32
+ schema: LOCALE_EXPLAIN_SCHEMA,
33
+ status: 'failed',
34
+ messageId: input.messageId,
35
+ diagnostics,
36
+ };
37
+ }
38
+ const defaultLocale = input.checkReport?.manifest?.defaultLocale;
39
+ const requested = input.locale || defaultLocale;
40
+ const fallback = input.checkReport?.manifest?.fallback || {};
41
+ const resolution = resolveMessageVariant({
42
+ messageId: input.messageId,
43
+ requestedLocale: requested,
44
+ variants: node.variants,
45
+ fallback,
46
+ });
47
+ const base = node.variants?.[defaultLocale] || Object.values(node.variants || {})[0];
48
+ const deliveryId = input.deliveryId || 'delivery.web';
49
+ const delivery = buildLocaleDeliveryResolution({
50
+ host: 'web',
51
+ applicationId: 'app.locales',
52
+ deliveryId,
53
+ supportedLocales: (input.checkReport?.manifest?.locales || []).map((l) => l.id),
54
+ defaultLocale,
55
+ fallback,
56
+ messages,
57
+ reachableMessageIds: [input.messageId],
58
+ bundledLocales: [defaultLocale],
59
+ });
60
+ const inChunk = (delivery.lazyLocaleChunks || []).concat(delivery.bundledChunks || []).some((c) => c.messageIds?.includes(input.messageId));
61
+ return {
62
+ schema: LOCALE_EXPLAIN_SCHEMA,
63
+ status: 'ready',
64
+ messageId: input.messageId,
65
+ catalogId: node.catalogId,
66
+ params: base?.params || [],
67
+ variants: Object.fromEntries(Object.entries(node.variants || {}).map(([loc, v]) => [loc, { template: v.template, path: v.path, params: v.params }])),
68
+ requestedLocale: requested,
69
+ resolvedLocale: resolution.resolvedLocale,
70
+ fallbackPath: resolution.fallbackPath,
71
+ formatterDataVersion: FORMATTER_DATA_VERSION,
72
+ delivery: {
73
+ deliveryId,
74
+ reachable: inChunk,
75
+ catalogHash: delivery.messageCatalogHashes?.[resolution.resolvedLocale || defaultLocale] || null,
76
+ bundledLocales: delivery.bundledLocales,
77
+ },
78
+ diagnostics,
79
+ };
80
+ }
81
+ /**
82
+ * Diff two locales' catalogs.
83
+ * @param {{
84
+ * baseLocale: string,
85
+ * targetLocale: string,
86
+ * messages: Array<{ messageId: string, variants: Record<string, { template: string, params?: any[] }> }>,
87
+ * }} input
88
+ */
89
+ export function diffLocaleCatalogs(input) {
90
+ const base = input.baseLocale;
91
+ const target = input.targetLocale;
92
+ /** @type {any[]} */
93
+ const missingInTarget = [];
94
+ /** @type {any[]} */
95
+ const missingInBase = [];
96
+ /** @type {any[]} */
97
+ const changed = [];
98
+ /** @type {any[]} */
99
+ const paramMismatches = [];
100
+ const ids = new Set();
101
+ for (const m of input.messages || [])
102
+ ids.add(m.messageId);
103
+ for (const messageId of [...ids].sort()) {
104
+ const node = (input.messages || []).find((m) => m.messageId === messageId);
105
+ const bv = node?.variants?.[base];
106
+ const tv = node?.variants?.[target];
107
+ if (bv && !tv)
108
+ missingInTarget.push(messageId);
109
+ else if (!bv && tv)
110
+ missingInBase.push(messageId);
111
+ else if (bv && tv) {
112
+ if (bv.template !== tv.template) {
113
+ changed.push({ messageId, base: bv.template, target: tv.template });
114
+ }
115
+ const bp = JSON.stringify(bv.params || []);
116
+ const tp = JSON.stringify(tv.params || []);
117
+ if (bp !== tp) {
118
+ paramMismatches.push({ messageId, baseParams: bv.params || [], targetParams: tv.params || [] });
119
+ }
120
+ }
121
+ }
122
+ return {
123
+ schema: LOCALE_DIFF_SCHEMA,
124
+ status: 'ready',
125
+ baseLocale: base,
126
+ targetLocale: target,
127
+ missingInTarget,
128
+ missingInBase,
129
+ changed,
130
+ paramMismatches,
131
+ summary: {
132
+ missingInTarget: missingInTarget.length,
133
+ missingInBase: missingInBase.length,
134
+ changed: changed.length,
135
+ paramMismatches: paramMismatches.length,
136
+ },
137
+ };
138
+ }
139
+ /**
140
+ * Scan source for likely hardcoded UI text sinks (extract --check).
141
+ * Does not auto-generate MessageIds.
142
+ * @param {string} projectRoot
143
+ * @param {{ check?: boolean }} [opts]
144
+ */
145
+ export function extractHardcodedText(projectRoot, opts = {}) {
146
+ /** @type {any[]} */
147
+ const findings = [];
148
+ /** @type {any[]} */
149
+ const diagnostics = [];
150
+ const srcRoot = path.join(projectRoot, 'src');
151
+ if (!fs.existsSync(srcRoot)) {
152
+ return {
153
+ schema: LOCALE_EXTRACT_SCHEMA,
154
+ status: 'ready',
155
+ findings: [],
156
+ diagnostics: [],
157
+ };
158
+ }
159
+ /** @type {string[]} */
160
+ const files = [];
161
+ const walk = (dir) => {
162
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
163
+ const p = path.join(dir, ent.name);
164
+ if (ent.isDirectory()) {
165
+ if (ent.name === 'node_modules' || ent.name === 'dist')
166
+ continue;
167
+ walk(p);
168
+ }
169
+ else if (/\.(vmz|ts|tsx|js|jsx)$/.test(ent.name)) {
170
+ files.push(p);
171
+ }
172
+ }
173
+ };
174
+ walk(srcRoot);
175
+ // CJK or long quoted Latin UI-ish literals outside #locales imports.
176
+ const cjkRe = /['"`]([^'"`]*[\u4e00-\u9fff][^'"`]*)['"`]/g;
177
+ const uiLatinRe = /['"`]([A-Z][A-Za-z0-9 ,.!?]{8,})['"`]/g;
178
+ const dynamicIdRe = /(?<![A-Za-z0-9_$])(?:t|translate|i18n)\(\s*([^'")]+)\s*\)/g;
179
+ for (const fileAbs of files) {
180
+ const text = fs.readFileSync(fileAbs, 'utf8');
181
+ const rel = path.relative(projectRoot, fileAbs).replace(/\\/g, '/');
182
+ // Skip files that only re-export locales types.
183
+ if (rel.includes('locales-types'))
184
+ continue;
185
+ let m;
186
+ cjkRe.lastIndex = 0;
187
+ while ((m = cjkRe.exec(text))) {
188
+ const lit = m[1];
189
+ // Allow import paths / comments-ish short tokens
190
+ if (lit.includes('#locales/') || lit.includes('locales/'))
191
+ continue;
192
+ // Require a letter (Latin or CJK). Avoid `\W` without `u` — CJK is `\W` in ASCII mode.
193
+ if (!/\p{L}/u.test(lit))
194
+ continue;
195
+ findings.push({
196
+ path: rel,
197
+ kind: 'cjk_literal',
198
+ text: lit,
199
+ suggestion: 'Move UI copy into /locales catalog and import from #locales/*',
200
+ });
201
+ diagnostics.push({
202
+ code: DIAG_LOCALE_HARDCODED_TEXT,
203
+ severity: opts.check ? 'error' : 'warning',
204
+ message: `suspected hardcoded text ${JSON.stringify(lit)} in ${rel}`,
205
+ path: rel,
206
+ });
207
+ }
208
+ uiLatinRe.lastIndex = 0;
209
+ while ((m = uiLatinRe.exec(text))) {
210
+ const lit = m[1];
211
+ if (/^(http|https|application\/|text\/)/i.test(lit))
212
+ continue;
213
+ if (lit.includes('#locales/'))
214
+ continue;
215
+ findings.push({
216
+ path: rel,
217
+ kind: 'ui_literal',
218
+ text: lit,
219
+ suggestion: 'Prefer #locales/* MessageId over hardcoded UI English',
220
+ });
221
+ diagnostics.push({
222
+ code: DIAG_LOCALE_HARDCODED_TEXT,
223
+ severity: 'warning',
224
+ message: `suspected hardcoded UI string ${JSON.stringify(lit)} in ${rel}`,
225
+ path: rel,
226
+ });
227
+ }
228
+ dynamicIdRe.lastIndex = 0;
229
+ while ((m = dynamicIdRe.exec(text))) {
230
+ const arg = m[1].trim();
231
+ if (!/^['"`]/.test(arg)) {
232
+ diagnostics.push({
233
+ code: DIAG_MESSAGE_DYNAMIC_ID_UNBOUNDED,
234
+ severity: 'error',
235
+ message: `dynamic message id ${arg} is unbounded; use typed #locales/* exports`,
236
+ path: rel,
237
+ });
238
+ }
239
+ }
240
+ }
241
+ const hasErrors = diagnostics.some((d) => d.severity === 'error');
242
+ return {
243
+ schema: LOCALE_EXTRACT_SCHEMA,
244
+ status: hasErrors ? 'failed' : 'ready',
245
+ findings,
246
+ diagnostics,
247
+ };
248
+ }
249
+ /**
250
+ * Pseudo-localize a source locale for layout/overflow testing.
251
+ * Preserves ICU placeholders; marks provenance — never a production fallback.
252
+ * @param {{
253
+ * sourceLocale: string,
254
+ * messages: Array<{ messageId: string, variants: Record<string, { template: string }> }>,
255
+ * production?: boolean,
256
+ * }} input
257
+ */
258
+ export function pseudoLocalizeCatalog(input) {
259
+ /** @type {any[]} */
260
+ const diagnostics = [];
261
+ if (input.production) {
262
+ diagnostics.push({
263
+ code: DIAG_LOCALE_PSEUDO_PRODUCTION_FORBIDDEN,
264
+ severity: 'error',
265
+ message: 'pseudo locale must not be used as production fallback',
266
+ });
267
+ }
268
+ /** @type {Record<string, string>} */
269
+ const catalog = {};
270
+ for (const m of input.messages || []) {
271
+ const src = m.variants?.[input.sourceLocale]?.template;
272
+ if (src == null)
273
+ continue;
274
+ // Expand length ~30% with accented padding while keeping {placeholders}.
275
+ const parts = String(src).split(/(\{[^}]+\})/g);
276
+ const out = parts
277
+ .map((p) => {
278
+ if (p.startsWith('{') && p.endsWith('}'))
279
+ return p;
280
+ const stretched = p.replace(/[A-Za-z]/g, (ch) => `${ch}\u0301`);
281
+ return stretched + (p.trim() ? '·' : '');
282
+ })
283
+ .join('');
284
+ catalog[m.messageId] = `[!! ${out} !!]`;
285
+ }
286
+ return {
287
+ schema: LOCALE_PSEUDO_SCHEMA,
288
+ status: diagnostics.length ? 'failed' : 'ready',
289
+ sourceLocale: input.sourceLocale,
290
+ pseudoLocale: `pseudo-${input.sourceLocale}`,
291
+ provenance: 'dev-test-only',
292
+ catalog,
293
+ diagnostics,
294
+ };
295
+ }
296
+ /**
297
+ * Cross-host conformance: same MessageId set + catalog hashes + formatter version.
298
+ * @param {{
299
+ * manifest: any,
300
+ * messages: any[],
301
+ * routeIds?: string[],
302
+ * }} input
303
+ */
304
+ export function checkLocaleConformance(input) {
305
+ /** @type {any[]} */
306
+ const diagnostics = [];
307
+ const supported = (input.manifest?.locales || []).map((l) => l.id);
308
+ const defaultLocale = input.manifest?.defaultLocale;
309
+ const messages = input.messages || [];
310
+ const common = {
311
+ applicationId: 'app.locales-fixture',
312
+ planVersion: 'plan.v0',
313
+ supportedLocales: supported,
314
+ defaultLocale,
315
+ fallback: input.manifest?.fallback || {},
316
+ messages,
317
+ reachableMessageIds: messages.map((m) => m.messageId),
318
+ bundledLocales: [defaultLocale],
319
+ };
320
+ const web = buildLocaleDeliveryResolution({ ...common, host: 'web', deliveryId: 'delivery.web' });
321
+ const mini = buildLocaleDeliveryResolution({ ...common, host: 'mini', deliveryId: 'delivery.mini' });
322
+ const native = buildLocaleDeliveryResolution({
323
+ ...common,
324
+ host: 'native',
325
+ deliveryId: 'delivery.native',
326
+ });
327
+ diagnostics.push(...web.diagnostics, ...mini.diagnostics, ...native.diagnostics);
328
+ const inv = assertHostMessageInvariant([web, mini, native]);
329
+ if (!inv.ok) {
330
+ for (const d of inv.diagnostics) {
331
+ diagnostics.push({
332
+ code: DIAG_LOCALE_CONFORMANCE_DIVERGENCE,
333
+ severity: 'error',
334
+ message: d.message,
335
+ });
336
+ }
337
+ }
338
+ // RouteId surface: stable ids must not embed LocaleId.
339
+ for (const routeId of input.routeIds || []) {
340
+ if (supported.some((loc) => routeId.includes(loc))) {
341
+ diagnostics.push({
342
+ code: DIAG_LOCALE_CONFORMANCE_DIVERGENCE,
343
+ severity: 'error',
344
+ message: `RouteId ${routeId} must not embed LocaleId`,
345
+ });
346
+ }
347
+ }
348
+ const hasErrors = diagnostics.some((d) => d.severity === 'error');
349
+ return {
350
+ schema: LOCALE_CONFORMANCE_SCHEMA,
351
+ status: hasErrors ? 'failed' : 'ready',
352
+ hosts: ['web', 'mini', 'native'],
353
+ formatterDataVersion: FORMATTER_DATA_VERSION,
354
+ messageIds: messages.map((m) => m.messageId).sort(),
355
+ catalogHashes: web.messageCatalogHashes,
356
+ diagnostics,
357
+ };
358
+ }
package/dist/log.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Unified CLI logging / diagnostics (N2).
3
+ */
4
+ export declare const log: {
5
+ /** @param {...unknown} args */
6
+ info(...args: any[]): void;
7
+ /** @param {...unknown} args */
8
+ warn(...args: any[]): void;
9
+ /** @param {...unknown} args */
10
+ error(...args: any[]): void;
11
+ /** @param {{ severity: string, path: string, message: string }} d */
12
+ diagnostic(d: any): void;
13
+ /**
14
+ * @param {Array<{ severity: string, path: string, message: string }>} diagnostics
15
+ * @param {{ denyWarnings?: boolean }} [opts]
16
+ * @returns {number} failing count (errors, and warnings if denyWarnings)
17
+ */
18
+ diagnostics(diagnostics: any, opts?: {}): number;
19
+ };
package/dist/log.js ADDED
@@ -0,0 +1,42 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Unified CLI logging / diagnostics (N2).
4
+ */
5
+ /** @param {string} level */
6
+ function stamp(level) {
7
+ return `vmz ${level}`;
8
+ }
9
+ export const log = {
10
+ /** @param {...unknown} args */
11
+ info(...args) {
12
+ console.error(stamp('info'), ...args);
13
+ },
14
+ /** @param {...unknown} args */
15
+ warn(...args) {
16
+ console.error(stamp('warn'), ...args);
17
+ },
18
+ /** @param {...unknown} args */
19
+ error(...args) {
20
+ console.error(stamp('error'), ...args);
21
+ },
22
+ /** @param {{ severity: string, path: string, message: string }} d */
23
+ diagnostic(d) {
24
+ console.error(`${d.severity}: ${d.path}: ${d.message}`);
25
+ },
26
+ /**
27
+ * @param {Array<{ severity: string, path: string, message: string }>} diagnostics
28
+ * @param {{ denyWarnings?: boolean }} [opts]
29
+ * @returns {number} failing count (errors, and warnings if denyWarnings)
30
+ */
31
+ diagnostics(diagnostics, opts = {}) {
32
+ let failing = 0;
33
+ for (const d of diagnostics ?? []) {
34
+ this.diagnostic(d);
35
+ if (d.severity === 'error')
36
+ failing += 1;
37
+ else if (opts.denyWarnings && d.severity === 'warning')
38
+ failing += 1;
39
+ }
40
+ return failing;
41
+ },
42
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * npm / pnpm workspace package resolution helpers (N4.3).
3
+ * Design: `规划设计/vmz/14` — Node owns package resolution; Rust owns semantics.
4
+ */
5
+ /**
6
+ * @typedef {object} ResolvedPackage
7
+ * @property {string} name
8
+ * @property {string} root
9
+ * @property {boolean} [private]
10
+ * @property {boolean} hasSrc
11
+ * @property {string} [version]
12
+ */
13
+ /**
14
+ * Resolve workspace packages under a project (package.json workspaces or pnpm-workspace.yaml).
15
+ * Does not invent VMZ semantics — only filesystem / npm layout facts for plugins.
16
+ *
17
+ * @param {string} project
18
+ * @returns {ResolvedPackage[]}
19
+ */
20
+ export declare function resolveWorkspacePackages(project: any): any[];
21
+ /**
22
+ * Resolve a package name to an absolute root (workspace first, then node_modules).
23
+ * @param {string} project
24
+ * @param {string} name
25
+ * @returns {string | null}
26
+ */
27
+ export declare function resolvePackageRoot(project: any, name: any): any;
@@ -0,0 +1,147 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * npm / pnpm workspace package resolution helpers (N4.3).
4
+ * Design: `规划设计/vmz/14` — Node owns package resolution; Rust owns semantics.
5
+ */
6
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
7
+ import path from 'node:path';
8
+ /**
9
+ * @typedef {object} ResolvedPackage
10
+ * @property {string} name
11
+ * @property {string} root
12
+ * @property {boolean} [private]
13
+ * @property {boolean} hasSrc
14
+ * @property {string} [version]
15
+ */
16
+ /**
17
+ * Resolve workspace packages under a project (package.json workspaces or pnpm-workspace.yaml).
18
+ * Does not invent VMZ semantics — only filesystem / npm layout facts for plugins.
19
+ *
20
+ * @param {string} project
21
+ * @returns {ResolvedPackage[]}
22
+ */
23
+ export function resolveWorkspacePackages(project) {
24
+ const root = path.resolve(project);
25
+ const patterns = readWorkspacePatterns(root);
26
+ /** @type {Map<string, ResolvedPackage>} */
27
+ const out = new Map();
28
+ // Always include the project itself when it has package.json.
29
+ const self = readPkg(root);
30
+ if (self)
31
+ out.set(self.root, self);
32
+ for (const pattern of patterns) {
33
+ for (const dir of expandWorkspacePattern(root, pattern)) {
34
+ const pkg = readPkg(dir);
35
+ if (pkg)
36
+ out.set(pkg.root, pkg);
37
+ }
38
+ }
39
+ return [...out.values()].sort((a, b) => a.name.localeCompare(b.name));
40
+ }
41
+ /**
42
+ * Resolve a package name to an absolute root (workspace first, then node_modules).
43
+ * @param {string} project
44
+ * @param {string} name
45
+ * @returns {string | null}
46
+ */
47
+ export function resolvePackageRoot(project, name) {
48
+ const hit = resolveWorkspacePackages(project).find((p) => p.name === name);
49
+ if (hit)
50
+ return hit.root;
51
+ const nm = path.join(path.resolve(project), 'node_modules', ...name.split('/'));
52
+ if (existsSync(path.join(nm, 'package.json')))
53
+ return nm;
54
+ return null;
55
+ }
56
+ /**
57
+ * @param {string} root
58
+ * @returns {string[]}
59
+ */
60
+ function readWorkspacePatterns(root) {
61
+ const patterns = [];
62
+ const pkgPath = path.join(root, 'package.json');
63
+ if (existsSync(pkgPath)) {
64
+ try {
65
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
66
+ const ws = pkg.workspaces;
67
+ if (Array.isArray(ws))
68
+ patterns.push(...ws);
69
+ else if (ws && Array.isArray(ws.packages))
70
+ patterns.push(...ws.packages);
71
+ }
72
+ catch {
73
+ /* ignore */
74
+ }
75
+ }
76
+ const pnpm = path.join(root, 'pnpm-workspace.yaml');
77
+ if (existsSync(pnpm)) {
78
+ try {
79
+ const text = readFileSync(pnpm, 'utf8');
80
+ for (const line of text.split(/\r?\n/)) {
81
+ const m = line.match(/^\s*-\s*['"]?([^'"]+)['"]?\s*$/);
82
+ if (m)
83
+ patterns.push(m[1]);
84
+ }
85
+ }
86
+ catch {
87
+ /* ignore */
88
+ }
89
+ }
90
+ return [...new Set(patterns)];
91
+ }
92
+ /**
93
+ * Minimal glob: supports `packages/*`, `examples/*`, exact dirs. No `**`.
94
+ * @param {string} root
95
+ * @param {string} pattern
96
+ */
97
+ function expandWorkspacePattern(root, pattern) {
98
+ const cleaned = pattern.replace(/\\/g, '/').replace(/\/$/, '');
99
+ if (!cleaned.includes('*')) {
100
+ const dir = path.join(root, cleaned);
101
+ return existsSync(dir) ? [dir] : [];
102
+ }
103
+ const star = cleaned.indexOf('*');
104
+ const prefix = cleaned.slice(0, star).replace(/\/$/, '');
105
+ const suffix = cleaned.slice(star + 1); // e.g. "" or "/*" — we only support one *
106
+ if (suffix.includes('*'))
107
+ return [];
108
+ const base = path.join(root, prefix);
109
+ if (!existsSync(base))
110
+ return [];
111
+ /** @type {string[]} */
112
+ const dirs = [];
113
+ for (const name of readdirSync(base, { withFileTypes: true })) {
114
+ if (!name.isDirectory())
115
+ continue;
116
+ const dir = path.join(base, name.name);
117
+ if (suffix && !existsSync(path.join(dir, suffix.replace(/^\//, '')))) {
118
+ // suffix after * is path remainder like `/foo` — rare; skip strict check
119
+ }
120
+ dirs.push(dir);
121
+ }
122
+ return dirs;
123
+ }
124
+ /**
125
+ * @param {string} dir
126
+ * @returns {ResolvedPackage | null}
127
+ */
128
+ function readPkg(dir) {
129
+ const pkgPath = path.join(dir, 'package.json');
130
+ if (!existsSync(pkgPath))
131
+ return null;
132
+ try {
133
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
134
+ if (!pkg.name)
135
+ return null;
136
+ return {
137
+ name: pkg.name,
138
+ root: dir,
139
+ private: Boolean(pkg.private),
140
+ hasSrc: existsSync(path.join(dir, 'src')),
141
+ version: typeof pkg.version === 'string' ? pkg.version : undefined,
142
+ };
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Plugin protocol v1 helpers (N3) + typed config loading.
3
+ * Design: 瑙勫垝璁捐/vmz/14-Node-NAPI涓庢彃浠跺涓?md
4
+ */
5
+ import { contentHash, defineConfig, definePlugin } from '@vmz/plugin';
6
+ export { contentHash, defineConfig, definePlugin };
7
+ export declare const PLUGIN_PROTOCOL = "0.1.0";
8
+ /**
9
+ * @param {string} full
10
+ * @returns {Promise<any>}
11
+ */
12
+ export declare function importMaybeTs(full: any): Promise<any>;
13
+ /**
14
+ * Load `vmz.config.*` from project root (+ optional root `vmz.plugin.*`).
15
+ * @param {string} project
16
+ * @returns {Promise<{ plugins: import('@vmz/plugin').VmzPlugin[], engines: import('@vmz/plugin').VmzEngines, path: string | null, pluginPath: string | null }>}
17
+ */
18
+ export declare function loadVmzConfig(project: any): Promise<{
19
+ plugins: any[];
20
+ engines: {};
21
+ path: any;
22
+ pluginPath: any;
23
+ }>;
24
+ /**
25
+ * Collect + apply contribution batches for the given stages onto a Workspace.
26
+ * @param {import('../index.js').Workspace} workspace
27
+ * @param {import('@vmz/plugin').VmzPlugin[]} plugins
28
+ * @param {{ project: string, outDir: string, stages?: string[], engines?: import('@vmz/plugin').VmzEngines }} opts
29
+ */
30
+ export declare function applyPlugins(workspace: any, plugins: any, opts: any): Promise<any[]>;