@wcstack/typescript 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,682 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { resolve, dirname, join } from 'node:path';
4
+ import ts from 'typescript';
5
+ import { createRequire } from 'node:module';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ /**
9
+ * program.ts — open a state file with the TypeScript compiler API and locate the
10
+ * type of its `export default`.
11
+ *
12
+ * `defineState(x)` is an identity function, so the exported type is `x`'s type —
13
+ * but the call is unwrapped syntactically anyway: when `@wcstack/state` is not
14
+ * resolvable from the state file (a CDN-only page, a fixture in a temp dir), the
15
+ * call expression would type as `any` and every path would be lost, while the
16
+ * argument literal still carries the full object type.
17
+ */
18
+ /** Defaults used when no tsconfig.json is found — enough to type a plain state file. */
19
+ const DEFAULT_COMPILER_OPTIONS = {
20
+ target: ts.ScriptTarget.ESNext,
21
+ module: ts.ModuleKind.ESNext,
22
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
23
+ lib: ["lib.esnext.d.ts", "lib.dom.d.ts", "lib.dom.iterable.d.ts"],
24
+ strict: true,
25
+ };
26
+ /** The compiler API compares paths it attaches diagnostics to against `/`-separated ones; feed it those on Windows too. */
27
+ function toPosix(path) {
28
+ return path.replace(/\\/g, "/");
29
+ }
30
+ function resolveCompilerOptions(stateFile, options = {}) {
31
+ const configPath = options.tsconfig !== undefined
32
+ ? toPosix(resolve(options.tsconfig))
33
+ : ts.findConfigFile(toPosix(dirname(stateFile)), ts.sys.fileExists, "tsconfig.json");
34
+ let base = { ...DEFAULT_COMPILER_OPTIONS };
35
+ if (configPath !== undefined) {
36
+ let parsedOptions;
37
+ try {
38
+ const read = ts.readConfigFile(configPath, ts.sys.readFile);
39
+ if (read.error !== undefined) {
40
+ throw new Error(ts.flattenDiagnosticMessageText(read.error.messageText, "\n"));
41
+ }
42
+ parsedOptions = ts.parseJsonConfigFileContent(read.config, ts.sys, toPosix(dirname(configPath)), undefined, configPath).options;
43
+ }
44
+ catch (e) {
45
+ throw new Error(`cannot read ${configPath}: ${e.message}`);
46
+ }
47
+ base = { ...DEFAULT_COMPILER_OPTIONS, ...parsedOptions };
48
+ }
49
+ return {
50
+ ...base,
51
+ // The generator only reads types: never write, always accept JS (JSDoc types), keep it fast.
52
+ noEmit: true,
53
+ allowJs: true,
54
+ checkJs: true,
55
+ skipLibCheck: true,
56
+ declaration: false,
57
+ declarationMap: false,
58
+ sourceMap: false,
59
+ composite: false,
60
+ incremental: false,
61
+ tsBuildInfoFile: undefined,
62
+ };
63
+ }
64
+ /** Unwrap `defineState(x)` (any callee whose last identifier is `defineState`) and `satisfies` / `as`-free wrappers. */
65
+ function stateExpression(expression) {
66
+ let current = expression;
67
+ while (ts.isParenthesizedExpression(current))
68
+ current = current.expression;
69
+ if (ts.isCallExpression(current) && current.arguments.length > 0) {
70
+ const callee = current.expression;
71
+ const name = ts.isIdentifier(callee)
72
+ ? callee.text
73
+ : ts.isPropertyAccessExpression(callee)
74
+ ? callee.name.text
75
+ : undefined;
76
+ if (name === "defineState")
77
+ return current.arguments[0];
78
+ }
79
+ return current;
80
+ }
81
+ function loadStateFile(file, options = {}) {
82
+ const abs = resolve(file);
83
+ if (!existsSync(abs))
84
+ throw new Error(`cannot read ${file}: no such file`);
85
+ // Read up front so an unreadable file fails with a plain message, not a compiler internals one.
86
+ readFileSync(abs, "utf8");
87
+ const compilerOptions = resolveCompilerOptions(abs, options);
88
+ const program = ts.createProgram([abs], compilerOptions);
89
+ const sourceFile = program.getSourceFile(abs);
90
+ /* v8 ignore next -- the file exists and is a root name; the compiler always returns it */
91
+ if (sourceFile === undefined)
92
+ throw new Error(`cannot open ${file} as a TypeScript/JavaScript source`);
93
+ const syntax = program.getSyntacticDiagnostics(sourceFile);
94
+ if (syntax.length > 0) {
95
+ const lines = syntax.map((d) => {
96
+ // Syntactic diagnostics always carry their file and position; the fallback is type-level only.
97
+ /* v8 ignore next 2 */
98
+ const pos = d.file !== undefined && d.start !== undefined ? d.file.getLineAndCharacterOfPosition(d.start) : undefined;
99
+ const where = pos !== undefined ? `${file}:${pos.line + 1}:${pos.character + 1}` : file;
100
+ return `${where} ${ts.flattenDiagnosticMessageText(d.messageText, "\n")}`;
101
+ });
102
+ throw new Error(`syntax error(s) in ${file}:\n${lines.join("\n")}`);
103
+ }
104
+ const exportAssignment = sourceFile.statements.find((s) => ts.isExportAssignment(s) && !s.isExportEquals);
105
+ if (exportAssignment === undefined) {
106
+ throw new Error(`${file} has no \`export default\` — a wcstack state file exports its state object as the default export`);
107
+ }
108
+ const location = stateExpression(exportAssignment.expression);
109
+ const checker = program.getTypeChecker();
110
+ const type = checker.getTypeAtLocation(location);
111
+ const warnings = [];
112
+ if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) {
113
+ warnings.push(`the default export of ${file} has type \`${checker.typeToString(type)}\`; the generated stateSchema will be open ({})`);
114
+ }
115
+ return { program, checker, sourceFile, type, location, warnings };
116
+ }
117
+
118
+ /**
119
+ * typeToSchema.ts — TypeScript type → `stateSchema` (the JSON-Schema subset of
120
+ * docs/wcstack-manifest-schema.md §4: type / properties / required / items /
121
+ * enum / const / anyOf only).
122
+ *
123
+ * Rules (docs/app-testing-and-typescript-impl-plan.md §4-2-2):
124
+ * - `$`-prefixed keys are dropped (runtime namespaces, never data paths).
125
+ * - Members with call signatures (methods, function-valued properties) are dropped.
126
+ * - Getters contribute their return type. Path getters (`get "users.*.ageCategory"()`)
127
+ * are injected at the path they compute, so the validator sees them as members.
128
+ * - Arrays → `items`; unions split `null` out into `anyOf`; literal unions → `enum`.
129
+ * - Built-in / library object types (`Date`, `Map`, DOM types, …) become a **bare `{}`**
130
+ * — never `{ "type": "object" }`: the validator treats a bare `{}` as *unknown*
131
+ * (silent) and a typed object without the member as *nonexistent* (error).
132
+ * - Nesting stops at `maxDepth` (default 5 = the validator's candidate budget) with a bare `{}`.
133
+ */
134
+ const DEFAULT_MAX_DEPTH = 5;
135
+ /**
136
+ * Convert the type of a state object into a `stateSchema` node.
137
+ */
138
+ function stateTypeToSchema(checker, program, type, location, options = {}) {
139
+ const ctx = {
140
+ checker,
141
+ program,
142
+ location,
143
+ maxDepth: options.maxDepth ?? DEFAULT_MAX_DEPTH,
144
+ stack: new Set(),
145
+ pathGetters: [],
146
+ };
147
+ const root = convertType(type, 0, ctx, true);
148
+ for (const getter of ctx.pathGetters) {
149
+ injectPath(root, getter.segments, convertType(getter.type, getter.segments.length, ctx, false));
150
+ }
151
+ return root;
152
+ }
153
+ function convertType(type, depth, ctx, isRoot) {
154
+ const flags = type.flags;
155
+ if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never | ts.TypeFlags.TypeParameter | ts.TypeFlags.NonPrimitive)) {
156
+ return {};
157
+ }
158
+ if (flags & ts.TypeFlags.Null)
159
+ return { type: "null" };
160
+ if (flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void))
161
+ return {};
162
+ if (flags & ts.TypeFlags.Boolean)
163
+ return { type: "boolean" };
164
+ if (flags & ts.TypeFlags.BooleanLiteral)
165
+ return { type: "boolean", const: literalValue(type, ctx) };
166
+ if (flags & ts.TypeFlags.StringLiteral)
167
+ return { type: "string", const: type.value };
168
+ if (flags & ts.TypeFlags.NumberLiteral)
169
+ return { type: "number", const: type.value };
170
+ if (flags & (ts.TypeFlags.BigInt | ts.TypeFlags.BigIntLiteral | ts.TypeFlags.ESSymbolLike))
171
+ return {};
172
+ if (flags & ts.TypeFlags.String)
173
+ return { type: "string" };
174
+ if (flags & ts.TypeFlags.Number)
175
+ return { type: "number" };
176
+ if (type.isUnion())
177
+ return convertUnion(type, depth, ctx);
178
+ if (ctx.checker.isArrayType(type) || ctx.checker.isTupleType(type)) {
179
+ return convertArray(type, depth, ctx);
180
+ }
181
+ const callable = type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0;
182
+ if (callable && ctx.checker.getPropertiesOfType(type).length === 0)
183
+ return {};
184
+ const symbol = type.getSymbol() ?? type.aliasSymbol;
185
+ if (symbol !== undefined && isLibrarySymbol(symbol, ctx))
186
+ return {};
187
+ return convertObject(type, depth, ctx, isRoot);
188
+ }
189
+ function convertUnion(type, depth, ctx) {
190
+ const members = type.types.filter((t) => (t.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) === 0);
191
+ const hasNull = members.some((t) => (t.flags & ts.TypeFlags.Null) !== 0);
192
+ const nonNull = members.filter((t) => (t.flags & ts.TypeFlags.Null) === 0);
193
+ let node;
194
+ if (nonNull.length === 0) {
195
+ return { type: "null" };
196
+ }
197
+ else if (nonNull.every((t) => (t.flags & ts.TypeFlags.BooleanLiteral) !== 0) && nonNull.length === 2) {
198
+ node = { type: "boolean" };
199
+ }
200
+ else if (nonNull.every((t) => t.isLiteral() || (t.flags & ts.TypeFlags.BooleanLiteral) !== 0)) {
201
+ const values = nonNull.map((t) => literalValue(t, ctx));
202
+ const kinds = [...new Set(values.map((v) => typeof v))];
203
+ node = kinds.length === 1 && (kinds[0] === "string" || kinds[0] === "number" || kinds[0] === "boolean")
204
+ ? { type: kinds[0], enum: values }
205
+ : { enum: values };
206
+ }
207
+ else if (nonNull.length === 1) {
208
+ node = convertType(nonNull[0], depth, ctx, false);
209
+ }
210
+ else {
211
+ const converted = dedupeNodes(nonNull.map((t) => convertType(t, depth, ctx, false)));
212
+ node = converted.length === 1 ? converted[0] : { anyOf: converted };
213
+ }
214
+ if (!hasNull)
215
+ return node;
216
+ if (node.type === "null")
217
+ return node;
218
+ return node.anyOf !== undefined ? { anyOf: [...node.anyOf, { type: "null" }] } : { anyOf: [node, { type: "null" }] };
219
+ }
220
+ function convertArray(type, depth, ctx) {
221
+ const args = ctx.checker.getTypeArguments(type);
222
+ if (args.length === 0)
223
+ return { type: "array", items: {} };
224
+ if (ctx.checker.isTupleType(type)) {
225
+ const converted = dedupeNodes(args.map((t) => convertType(t, depth, ctx, false)));
226
+ return { type: "array", items: converted.length === 1 ? converted[0] : { anyOf: converted } };
227
+ }
228
+ return { type: "array", items: convertType(args[0], depth, ctx, false) };
229
+ }
230
+ function convertObject(type, depth, ctx, isRoot) {
231
+ if (depth >= ctx.maxDepth)
232
+ return {};
233
+ if (ctx.stack.has(type))
234
+ return {};
235
+ ctx.stack.add(type);
236
+ try {
237
+ const properties = {};
238
+ const required = [];
239
+ for (const prop of ctx.checker.getPropertiesOfType(type)) {
240
+ const name = prop.getName();
241
+ if (name.startsWith("$"))
242
+ continue;
243
+ if (isMethodSymbol(prop))
244
+ continue;
245
+ const propType = ctx.checker.getTypeOfSymbolAtLocation(prop, ctx.location);
246
+ if (isFunctionValued(propType, ctx))
247
+ continue;
248
+ if (isRoot && (name.includes(".") || name.includes("*"))) {
249
+ // A path getter declares a member at a nested position; inject it once the tree exists.
250
+ ctx.pathGetters.push({ segments: name.split("."), type: propType });
251
+ continue;
252
+ }
253
+ properties[name] = convertType(propType, depth + 1, ctx, false);
254
+ if (!isOptional(prop, propType))
255
+ required.push(name);
256
+ }
257
+ const node = { type: "object", properties };
258
+ if (required.length > 0)
259
+ node.required = required;
260
+ return node;
261
+ }
262
+ finally {
263
+ ctx.stack.delete(type);
264
+ }
265
+ }
266
+ /** Walk `segments` (`*` = array items) into a definite object and add the leaf; unknown (`{}`) containers stay unknown. */
267
+ function injectPath(root, segments, leaf) {
268
+ const last = segments[segments.length - 1];
269
+ if (last === "*" || last === "")
270
+ return;
271
+ let node = root;
272
+ for (const segment of segments.slice(0, -1)) {
273
+ node = segment === "*" ? node.items : descendObject(node)?.properties?.[segment];
274
+ if (node === undefined)
275
+ return;
276
+ node = unwrapNullable(node);
277
+ }
278
+ const container = descendObject(node);
279
+ if (container === undefined || container.properties === undefined)
280
+ return;
281
+ container.properties[last] = leaf;
282
+ }
283
+ /** For `anyOf: [object, null]` return the object member; otherwise the node itself. */
284
+ function unwrapNullable(node) {
285
+ if (node.anyOf === undefined)
286
+ return node;
287
+ const objects = node.anyOf.filter((n) => n.properties !== undefined || n.items !== undefined);
288
+ return objects.length === 1 ? objects[0] : node;
289
+ }
290
+ function descendObject(node) {
291
+ if (node === undefined)
292
+ return undefined;
293
+ const unwrapped = unwrapNullable(node);
294
+ return unwrapped.properties !== undefined ? unwrapped : undefined;
295
+ }
296
+ function isMethodSymbol(symbol) {
297
+ if (symbol.flags & ts.SymbolFlags.Method)
298
+ return true;
299
+ return (symbol.declarations ?? []).some((d) => ts.isMethodDeclaration(d) || ts.isMethodSignature(d) || ts.isFunctionDeclaration(d));
300
+ }
301
+ function isFunctionValued(type, ctx) {
302
+ const members = type.isUnion() ? type.types : [type];
303
+ return members.some((t) => t.getCallSignatures().length > 0 && ctx.checker.getPropertiesOfType(t).length === 0);
304
+ }
305
+ function isOptional(symbol, type) {
306
+ if (symbol.flags & ts.SymbolFlags.Optional)
307
+ return true;
308
+ return type.isUnion() && type.types.some((t) => (t.flags & ts.TypeFlags.Undefined) !== 0);
309
+ }
310
+ /** Declared in a default lib (`lib.*.d.ts`) or under node_modules → opaque `{}` (Date, Map, DOM types, third-party classes). */
311
+ function isLibrarySymbol(symbol, ctx) {
312
+ const declarations = symbol.declarations ?? [];
313
+ if (declarations.length === 0)
314
+ return false;
315
+ return declarations.every((d) => {
316
+ const sf = d.getSourceFile();
317
+ return ctx.program.isSourceFileDefaultLibrary(sf) || /[\\/]node_modules[\\/]/.test(sf.fileName);
318
+ });
319
+ }
320
+ function literalValue(type, ctx) {
321
+ if (type.flags & ts.TypeFlags.BooleanLiteral)
322
+ return ctx.checker.typeToString(type) === "true";
323
+ /* v8 ignore next 4 -- callers only pass literal types; the tail is the type-level fallback */
324
+ if (type.isLiteral()) {
325
+ const value = type.value;
326
+ return typeof value === "object" ? Number(`${value.negative ? "-" : ""}${value.base10Value}`) : value;
327
+ }
328
+ return undefined;
329
+ }
330
+ function dedupeNodes(nodes) {
331
+ const seen = new Set();
332
+ const out = [];
333
+ for (const n of nodes) {
334
+ const key = JSON.stringify(n);
335
+ if (seen.has(key))
336
+ continue;
337
+ seen.add(key);
338
+ out.push(n);
339
+ }
340
+ return out;
341
+ }
342
+
343
+ /**
344
+ * generate.ts — one call from a state file to its `stateSchema`.
345
+ */
346
+ function generateStateSchema(file, options = {}) {
347
+ const loaded = loadStateFile(file, options);
348
+ const schema = stateTypeToSchema(loaded.checker, loaded.program, loaded.type, loaded.location, options);
349
+ return { schema, warnings: loaded.warnings };
350
+ }
351
+
352
+ /**
353
+ * manifest.ts — build / merge / compare the `application` sidecar artifact.
354
+ *
355
+ * The manifest is a **derived artifact** (D9): the TypeScript type is the source
356
+ * of truth, `wcs-schema emit` writes the manifest, and `wcs-schema check`
357
+ * detects drift between the two in CI. `--merge` replaces exactly one
358
+ * `states[name].stateSchema` and keeps everything else (other states, filters,
359
+ * listContexts) — a hand-written schema for the same state does not survive,
360
+ * by design: there is no implicit merge in the sidecar spec (§5).
361
+ */
362
+ const APPLICATION_MANIFEST_FILENAME = "wcstack.manifest.json";
363
+ const SCHEMA_VERSION = 1;
364
+ const APPLICATION_NAMESPACE = "wcstack.application";
365
+ /**
366
+ * Create the manifest object for one state, or graft the state into `existing`
367
+ * (a parsed manifest object; envelope fields are filled in when absent).
368
+ */
369
+ function buildManifest(stateName, schema, existing) {
370
+ const base = existing !== null && typeof existing === "object" && !Array.isArray(existing)
371
+ ? { ...existing }
372
+ : {};
373
+ base.schemaVersion = typeof base.schemaVersion === "number" ? base.schemaVersion : SCHEMA_VERSION;
374
+ base.kind = "application";
375
+ const extensions = base.manifestExtensions !== null && typeof base.manifestExtensions === "object" && !Array.isArray(base.manifestExtensions)
376
+ ? { ...base.manifestExtensions }
377
+ : {};
378
+ const nsRaw = extensions[APPLICATION_NAMESPACE];
379
+ const ns = nsRaw !== null && typeof nsRaw === "object" && !Array.isArray(nsRaw) ? { ...nsRaw } : {};
380
+ ns.version = typeof ns.version === "number" ? ns.version : SCHEMA_VERSION;
381
+ const statesRaw = ns.states;
382
+ const states = statesRaw !== null && typeof statesRaw === "object" && !Array.isArray(statesRaw) ? { ...statesRaw } : {};
383
+ states[stateName] = { stateSchema: schema };
384
+ ns.states = states;
385
+ extensions[APPLICATION_NAMESPACE] = ns;
386
+ base.manifestExtensions = extensions;
387
+ return base;
388
+ }
389
+ /** Read `states[name].stateSchema` from a parsed manifest object, or undefined. */
390
+ function readStateSchema(manifest, stateName) {
391
+ if (manifest === null || typeof manifest !== "object")
392
+ return undefined;
393
+ const extensions = manifest.manifestExtensions;
394
+ if (extensions === null || typeof extensions !== "object")
395
+ return undefined;
396
+ const ns = extensions[APPLICATION_NAMESPACE];
397
+ if (ns === null || typeof ns !== "object")
398
+ return undefined;
399
+ const states = ns.states;
400
+ if (states === null || typeof states !== "object")
401
+ return undefined;
402
+ const entry = states[stateName];
403
+ if (entry === null || typeof entry !== "object")
404
+ return undefined;
405
+ return entry.stateSchema;
406
+ }
407
+ /** JSON with object keys sorted at every level — the canonical form used for comparison. */
408
+ function stableStringify(value) {
409
+ return JSON.stringify(sortKeys(value));
410
+ }
411
+ function sortKeys(value) {
412
+ if (Array.isArray(value))
413
+ return value.map(sortKeys);
414
+ if (value !== null && typeof value === "object") {
415
+ const out = {};
416
+ for (const key of Object.keys(value).sort()) {
417
+ out[key] = sortKeys(value[key]);
418
+ }
419
+ return out;
420
+ }
421
+ return value;
422
+ }
423
+ /**
424
+ * Compare the schema generated from the type with the one stored in `manifestText`.
425
+ * `changes` lists JSON pointers: `+ ptr` (only in generated), `- ptr` (only in
426
+ * manifest), `~ ptr` (both, different value).
427
+ */
428
+ function compareStateSchema(manifestText, stateName, generated) {
429
+ let parsed;
430
+ try {
431
+ parsed = JSON.parse(manifestText);
432
+ }
433
+ catch (e) {
434
+ return { kind: "broken", message: e.message };
435
+ }
436
+ const stored = readStateSchema(parsed, stateName);
437
+ if (stored === undefined)
438
+ return { kind: "missing-state" };
439
+ if (stableStringify(stored) === stableStringify(generated))
440
+ return { kind: "same" };
441
+ const a = flatten(generated);
442
+ const b = flatten(stored);
443
+ const changes = [];
444
+ for (const key of [...new Set([...a.keys(), ...b.keys()])].sort()) {
445
+ const inA = a.has(key);
446
+ const inB = b.has(key);
447
+ if (inA && !inB)
448
+ changes.push(`+ ${key}`);
449
+ else if (!inA && inB)
450
+ changes.push(`- ${key}`);
451
+ else if (a.get(key) !== b.get(key))
452
+ changes.push(`~ ${key}`);
453
+ }
454
+ return { kind: "differs", changes };
455
+ }
456
+ /** Leaf pointer → canonical JSON. Objects with children are not listed themselves; empty objects/arrays are leaves. */
457
+ function flatten(value, pointer = "", out = new Map()) {
458
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
459
+ const entries = Object.entries(value);
460
+ if (entries.length === 0) {
461
+ out.set(pointer || "/", "{}");
462
+ return out;
463
+ }
464
+ for (const [key, child] of entries) {
465
+ flatten(child, `${pointer}/${key.replace(/~/g, "~0").replace(/\//g, "~1")}`, out);
466
+ }
467
+ return out;
468
+ }
469
+ out.set(pointer || "/", stableStringify(value));
470
+ return out;
471
+ }
472
+
473
+ /**
474
+ * schemaCore.ts — the validator core bundle (`dist/schema-core.cjs`, built from
475
+ * packages/vscode-wcs by scripts/build-schema-core.mjs), loaded lazily.
476
+ *
477
+ * The bundle is a self-contained CJS file that requires neither `typescript` nor
478
+ * `vscode`; it is located relative to the running module so the same loader
479
+ * works from `dist/index.esm.js`, `dist/wcs-schema.mjs`, and the TypeScript
480
+ * sources under vitest (`src/…` → `../dist`).
481
+ */
482
+ const BUNDLE = "schema-core.cjs";
483
+ /** Candidate locations, nearest first: dist/ (built), ../dist (src/), ../../dist (src/cli/). */
484
+ function schemaCoreCandidates(fromUrl = import.meta.url) {
485
+ const here = dirname(fileURLToPath(fromUrl));
486
+ return [join(here, BUNDLE), join(here, "..", "dist", BUNDLE), join(here, "..", "..", "dist", BUNDLE)];
487
+ }
488
+ let cached;
489
+ function loadSchemaCore() {
490
+ if (cached !== undefined)
491
+ return cached;
492
+ const candidates = schemaCoreCandidates();
493
+ const found = candidates.find((p) => existsSync(p));
494
+ /* v8 ignore next 5 -- tests run after `npm run build`, so the bundle is always present */
495
+ if (found === undefined) {
496
+ throw new Error(`${BUNDLE} not found — run \`npm run build\` in packages/typescript (looked in: ${candidates.join(", ")})`);
497
+ }
498
+ cached = createRequire(import.meta.url)(found);
499
+ return cached;
500
+ }
501
+
502
+ var version = "1.32.0";
503
+ var pkg = {
504
+ version: version};
505
+
506
+ const VERSION = pkg.version;
507
+
508
+ /**
509
+ * wcsSchema.ts — the `wcs-schema` command.
510
+ *
511
+ * wcs-schema emit <state.ts|state.js> [--state=default] [--out=wcstack.manifest.json] [--merge] [--tsconfig=<path>] [--max-depth=5]
512
+ * wcs-schema check <state.ts|state.js> [--state=default] [--manifest=wcstack.manifest.json] [--tsconfig=<path>] [--max-depth=5]
513
+ *
514
+ * exit codes
515
+ * emit : 0 written / 2 usage, unreadable file, syntax error, or the generated manifest failed its self-check
516
+ * check: 0 manifest matches the type / 1 drift (changes listed on stderr) / 2 usage, unreadable file, broken or state-less manifest
517
+ *
518
+ * `--out=-` prints the manifest to stdout instead of writing a file. `--merge`
519
+ * keeps everything in an existing manifest except `states[<name>].stateSchema`.
520
+ *
521
+ * The generated artifact is always run through the validator core's
522
+ * `validateManifestArtifact` (dist/schema-core.cjs, built from vscode-wcs) so a
523
+ * generator bug can never write a manifest the validator would reject.
524
+ */
525
+ const USAGE = "usage: wcs-schema emit <state.ts|state.js> [--state=default] [--out=wcstack.manifest.json] [--merge] [--tsconfig=<path>] [--max-depth=5]\n" +
526
+ " wcs-schema check <state.ts|state.js> [--state=default] [--manifest=wcstack.manifest.json] [--tsconfig=<path>] [--max-depth=5]\n";
527
+ function parseArgs(argv) {
528
+ let command;
529
+ let file;
530
+ let state = "default";
531
+ let out = APPLICATION_MANIFEST_FILENAME;
532
+ let manifest = APPLICATION_MANIFEST_FILENAME;
533
+ let merge = false;
534
+ let tsconfig;
535
+ let maxDepth;
536
+ const unknown = [];
537
+ for (const arg of argv) {
538
+ if (arg === "--version" || arg === "-v")
539
+ command ??= "version";
540
+ else if (arg === "--help" || arg === "-h")
541
+ command ??= "help";
542
+ else if (arg.startsWith("--state="))
543
+ state = arg.slice("--state=".length);
544
+ else if (arg.startsWith("--out="))
545
+ out = arg.slice("--out=".length);
546
+ else if (arg.startsWith("--manifest="))
547
+ manifest = arg.slice("--manifest=".length);
548
+ else if (arg.startsWith("--tsconfig="))
549
+ tsconfig = arg.slice("--tsconfig=".length);
550
+ else if (arg.startsWith("--max-depth="))
551
+ maxDepth = Number(arg.slice("--max-depth=".length));
552
+ else if (arg === "--merge")
553
+ merge = true;
554
+ else if (arg.startsWith("-"))
555
+ unknown.push(arg);
556
+ else if (command === undefined && (arg === "emit" || arg === "check"))
557
+ command = arg;
558
+ else if (file === undefined)
559
+ file = arg;
560
+ else
561
+ unknown.push(arg);
562
+ }
563
+ return { command, file, state, out, manifest, merge, tsconfig, maxDepth, unknown };
564
+ }
565
+ function main(argv, io = defaultIo()) {
566
+ const args = parseArgs(argv);
567
+ if (args.command === "version") {
568
+ io.stdout(`${VERSION}\n`);
569
+ return 0;
570
+ }
571
+ if (args.command === "help") {
572
+ io.stdout(USAGE);
573
+ return 0;
574
+ }
575
+ if (args.command === undefined || args.file === undefined || args.unknown.length > 0) {
576
+ if (args.unknown.length > 0)
577
+ io.stderr(`unknown argument(s): ${args.unknown.join(" ")}\n`);
578
+ io.stderr(USAGE);
579
+ return 2;
580
+ }
581
+ if (args.maxDepth !== undefined && (!Number.isInteger(args.maxDepth) || args.maxDepth < 1)) {
582
+ io.stderr("--max-depth must be a positive integer\n");
583
+ return 2;
584
+ }
585
+ if (!/^[A-Za-z_$][\w$-]*$/.test(args.state)) {
586
+ io.stderr(`--state must be a state name (got "${args.state}")\n`);
587
+ return 2;
588
+ }
589
+ let generated;
590
+ try {
591
+ generated = generateStateSchema(resolve(io.cwd(), args.file), { tsconfig: args.tsconfig, maxDepth: args.maxDepth });
592
+ }
593
+ catch (e) {
594
+ io.stderr(`${e.message}\n`);
595
+ return 2;
596
+ }
597
+ for (const warning of generated.warnings)
598
+ io.stderr(`warning: ${warning}\n`);
599
+ return args.command === "emit" ? emit(args, generated.schema, io) : check(args, generated.schema, io);
600
+ }
601
+ function emit(args, schema, io) {
602
+ const toStdout = args.out === "-";
603
+ const outPath = toStdout ? undefined : resolve(io.cwd(), args.out);
604
+ let existing;
605
+ if (args.merge && outPath !== undefined && existsSync(outPath)) {
606
+ try {
607
+ existing = JSON.parse(readFileSync(outPath, "utf8"));
608
+ }
609
+ catch (e) {
610
+ io.stderr(`cannot merge into ${args.out}: ${e.message}\n`);
611
+ return 2;
612
+ }
613
+ }
614
+ const manifest = buildManifest(args.state, schema, existing);
615
+ const text = `${JSON.stringify(manifest, null, 2)}\n`;
616
+ // Self-check: the validator must accept what the generator wrote.
617
+ const core = loadSchemaCore();
618
+ const problems = core.validateManifestArtifact({ text, source: args.out });
619
+ for (const d of problems.filter((p) => p.severity !== "error")) {
620
+ io.stderr(`${d.severity} ${d.code} ${d.message}\n`);
621
+ }
622
+ const errors = problems.filter((p) => p.severity === "error");
623
+ if (errors.length > 0) {
624
+ for (const d of errors)
625
+ io.stderr(`error ${d.code} ${d.message}\n`);
626
+ io.stderr(`generated manifest failed its self-check (${errors.length} error(s)); nothing written\n`);
627
+ return 2;
628
+ }
629
+ if (toStdout) {
630
+ io.stdout(text);
631
+ return 0;
632
+ }
633
+ writeFileSync(outPath, text, "utf8");
634
+ io.stderr(`wrote ${args.out} (state "${args.state}")\n`);
635
+ return 0;
636
+ }
637
+ function check(args, schema, io) {
638
+ const manifestPath = resolve(io.cwd(), args.manifest);
639
+ if (!existsSync(manifestPath)) {
640
+ io.stderr(`cannot read ${args.manifest}: no such file (run \`wcs-schema emit\` first)\n`);
641
+ return 2;
642
+ }
643
+ const comparison = compareStateSchema(readFileSync(manifestPath, "utf8"), args.state, schema);
644
+ switch (comparison.kind) {
645
+ case "same":
646
+ io.stderr(`${args.manifest}: state "${args.state}" is up to date\n`);
647
+ return 0;
648
+ case "missing-state":
649
+ io.stderr(`${args.manifest}: state "${args.state}" has no stateSchema (run \`wcs-schema emit --merge\`)\n`);
650
+ return 2;
651
+ case "broken":
652
+ io.stderr(`${args.manifest}: broken JSON: ${comparison.message}\n`);
653
+ return 2;
654
+ case "differs":
655
+ io.stderr(`${args.manifest}: state "${args.state}" is out of date (${comparison.changes.length} change(s)):\n`);
656
+ for (const change of comparison.changes)
657
+ io.stderr(` ${change}\n`);
658
+ io.stderr(`run \`wcs-schema emit --merge --out=${args.manifest}\` to update it\n`);
659
+ return 1;
660
+ }
661
+ }
662
+ /* v8 ignore start -- process plumbing; exercised by running the built bin, not by unit tests */
663
+ function defaultIo() {
664
+ return {
665
+ stdout: (text) => process.stdout.write(text),
666
+ stderr: (text) => process.stderr.write(text),
667
+ cwd: () => process.cwd(),
668
+ };
669
+ }
670
+ // Entry point when executed as the `wcs-schema` bin (rollup emits dist/wcs-schema.mjs
671
+ // with a shebang). Under vitest this module is imported, and `process.argv[1]` is
672
+ // vitest's own entry, so the branch is not taken.
673
+ const invokedAsBin = typeof process !== "undefined"
674
+ && Array.isArray(process.argv)
675
+ && /wcs-schema(\.mjs)?$/.test(process.argv[1] ?? "");
676
+ if (invokedAsBin) {
677
+ process.exit(main(process.argv.slice(2)));
678
+ }
679
+ /* v8 ignore stop */
680
+
681
+ export { main, parseArgs };
682
+ //# sourceMappingURL=wcs-schema.mjs.map