@vizejs/plugin-sdk 0.515.0 → 0.516.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.
package/README.md CHANGED
@@ -1,10 +1,59 @@
1
1
  # @vizejs/plugin-sdk
2
2
 
3
3
  Author synchronous rules, fact providers, native template transforms and compiled
4
- output hooks. Install `@vizejs/native` **0.515.0 or newer** to execute plugins.
4
+ output hooks. Install `@vizejs/native` **0.516.0 or newer** to execute plugins.
5
5
  The optional native peer lets this package install independently for authoring;
6
6
  its helpers alone do not compile or lint Vue files. Node 22 or newer is required.
7
7
 
8
+ ## Typed visitor capability descriptors
9
+
10
+ `@vizejs/plugin-sdk/visitor` defines the stable versioned descriptor boundary for
11
+ visitor-style plugins. The v1 capability vocabulary covers SFC, template,
12
+ script, style, virtual TypeScript, diagnostics, lint, format and devtools trace
13
+ events. Each declared capability requires a matching synchronous callback, and
14
+ TypeScript narrows the callback payload to that capability.
15
+
16
+ ```ts
17
+ import {
18
+ VISITOR_API_VERSION,
19
+ defineVisitorPlugin,
20
+ negotiateVisitorPlugin,
21
+ } from "@vizejs/plugin-sdk/visitor";
22
+
23
+ const observer = defineVisitorPlugin({
24
+ apiVersion: VISITOR_API_VERSION,
25
+ name: "virtual-ts-observer",
26
+ version: "1.0.0",
27
+ capabilities: ["virtual-ts", "diagnostics"] as const,
28
+ cacheInputs: [],
29
+ visitors: {
30
+ "virtual-ts"(event) {
31
+ console.log(event.payload.virtualFile, event.payload.mappings.length);
32
+ },
33
+ diagnostics(event) {
34
+ console.log(event.payload.diagnostics.length);
35
+ },
36
+ },
37
+ });
38
+
39
+ negotiateVisitorPlugin(observer, {
40
+ apiVersion: VISITOR_API_VERSION,
41
+ capabilities: ["virtual-ts", "diagnostics"],
42
+ });
43
+ ```
44
+
45
+ Definitions fail closed on unknown or duplicate capabilities, missing or
46
+ undeclared callbacks, asynchronous functions, reserved cache-input names and API
47
+ version mismatches. `runVisitorFixture` freezes one authored event and enforces
48
+ the synchronous return contract for package tests.
49
+
50
+ This module only creates immutable descriptors and author fixtures. It does not
51
+ register callbacks in the native compiler, linter or language server, so importing
52
+ it adds no work to those paths. A production host must explicitly negotiate the
53
+ descriptor and owns cancellation, deadlines, isolation and event construction.
54
+ Virtual TypeScript and devtools trace observers use separate opt-in host
55
+ entrypoints; importing this module never registers either callback globally.
56
+
8
57
  ## Lint rules and autofixes
9
58
 
10
59
  ```js
@@ -222,3 +271,80 @@ synchronously in the calling Node process and have no execution deadline.
222
271
  Freezing batches and auditing output do not isolate those callbacks. The measured
223
272
  proxy handle remains for spike compatibility. `@vizejs/extension-sdk` is the
224
273
  separate versioned WIT guest interface.
274
+
275
+ ## Virtual TypeScript visitors
276
+
277
+ `defineVisitorPlugin` descriptors with only the `virtual-ts` capability can be
278
+ negotiated for the native type-check host. The ordinary `typeCheck` entrypoint
279
+ remains plugin-free; callers opt in through the separate visitor entrypoint.
280
+
281
+ ```js
282
+ import { typeCheckWithVirtualTsVisitors } from "@vizejs/native";
283
+ import {
284
+ VISITOR_API_VERSION,
285
+ createVirtualTsVisitorHost,
286
+ defineVisitorPlugin,
287
+ } from "@vizejs/plugin-sdk/visitor";
288
+
289
+ const observer = defineVisitorPlugin({
290
+ apiVersion: VISITOR_API_VERSION,
291
+ name: "virtual-ts-observer",
292
+ version: "1",
293
+ capabilities: ["virtual-ts"],
294
+ cacheInputs: [],
295
+ visitors: {
296
+ "virtual-ts"({ payload }) {
297
+ console.log(payload.virtualFile, payload.code, payload.mappings);
298
+ },
299
+ },
300
+ });
301
+
302
+ typeCheckWithVirtualTsVisitors(source, [createVirtualTsVisitorHost(observer)], {
303
+ filename: "src/App.vue",
304
+ });
305
+ ```
306
+
307
+ The callback receives immutable generated code and source/generated UTF-8 byte
308
+ ranges after the real type-check projection is built. Returning a value,
309
+ requesting another capability, or forging an incomplete host identity refuses
310
+ the invocation.
311
+
312
+ ## Devtools trace visitors
313
+
314
+ `devtools-trace` descriptors attach explicitly to a request-local
315
+ `@vizejs/devtools` recorder. The recorder serializes each committed trace event
316
+ through the negotiated adapter; disabled recorders do not invoke configured
317
+ hosts, and ordinary recorders without `visitorHosts` do not inspect plugin
318
+ descriptors.
319
+
320
+ ```js
321
+ import { createDevtoolsTraceRecorder } from "@vizejs/devtools";
322
+ import {
323
+ VISITOR_API_VERSION,
324
+ createDevtoolsTraceVisitorHost,
325
+ defineVisitorPlugin,
326
+ } from "@vizejs/plugin-sdk/visitor";
327
+
328
+ const observer = defineVisitorPlugin({
329
+ apiVersion: VISITOR_API_VERSION,
330
+ name: "trace-observer",
331
+ version: "1",
332
+ capabilities: ["devtools-trace"],
333
+ cacheInputs: [],
334
+ visitors: {
335
+ "devtools-trace"({ file, payload }) {
336
+ console.log(file, payload.events);
337
+ },
338
+ },
339
+ });
340
+
341
+ const trace = createDevtoolsTraceRecorder({
342
+ sessionId: "request-1",
343
+ visitorHosts: [createDevtoolsTraceVisitorHost(observer)],
344
+ });
345
+ ```
346
+
347
+ Every delivered event has a deterministic session/sequence ID, a stage derived
348
+ from its trace kind, millisecond timestamps normalized to nanoseconds, and the
349
+ remaining recorder fields in immutable `data`. A visitor exception or non-void
350
+ result refuses the record before it enters the snapshot.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizejs/plugin-sdk",
3
- "version": "0.515.0",
3
+ "version": "0.516.0",
4
4
  "description": "Deterministic authoring and isolated execution for Vize JavaScript plugins",
5
5
  "homepage": "https://github.com/ubugeeei-prod/vize",
6
6
  "bugs": {
@@ -24,7 +24,9 @@
24
24
  "README.md",
25
25
  "sandbox.js",
26
26
  "sandbox-worker.js",
27
- "sandbox.d.ts"
27
+ "sandbox.d.ts",
28
+ "visitor.js",
29
+ "visitor.d.ts"
28
30
  ],
29
31
  "type": "module",
30
32
  "sideEffects": false,
@@ -41,13 +43,17 @@
41
43
  "./sandbox": {
42
44
  "types": "./sandbox.d.ts",
43
45
  "default": "./sandbox.js"
46
+ },
47
+ "./visitor": {
48
+ "types": "./visitor.d.ts",
49
+ "default": "./visitor.js"
44
50
  }
45
51
  },
46
52
  "publishConfig": {
47
53
  "access": "public"
48
54
  },
49
55
  "devDependencies": {
50
- "@vizejs/native": "0.515.0"
56
+ "@vizejs/native": "0.516.0"
51
57
  },
52
58
  "peerDependencies": {
53
59
  "@vizejs/native": ">=0.429.0"
package/visitor.d.ts ADDED
@@ -0,0 +1,213 @@
1
+ import type { PluginNode } from "./index.js";
2
+ import type { JsonValue } from "./stages.js";
3
+
4
+ export declare const VISITOR_API_VERSION: 1;
5
+ export declare const VISITOR_CAPABILITIES: readonly [
6
+ "sfc",
7
+ "template",
8
+ "script",
9
+ "style",
10
+ "virtual-ts",
11
+ "diagnostics",
12
+ "lint",
13
+ "format",
14
+ "devtools-trace",
15
+ ];
16
+ export type VisitorCapability = (typeof VISITOR_CAPABILITIES)[number];
17
+ export type VisitorErrorCode =
18
+ | "invalid-definition"
19
+ | "invalid-name"
20
+ | "invalid-version"
21
+ | "unsupported-api-version"
22
+ | "invalid-capabilities"
23
+ | "unknown-capability"
24
+ | "duplicate-capability"
25
+ | "invalid-visitors"
26
+ | "missing-visitor"
27
+ | "undeclared-visitor"
28
+ | "async-visitor"
29
+ | "invalid-cache-inputs"
30
+ | "invalid-plugin"
31
+ | "invalid-host-contract"
32
+ | "unsupported-capability"
33
+ | "undeclared-capability"
34
+ | "invalid-event"
35
+ | "invalid-visitor-result";
36
+
37
+ export declare class VisitorPluginValidationError extends TypeError {
38
+ readonly code: VisitorErrorCode;
39
+ readonly field: string;
40
+ }
41
+
42
+ export interface VisitorRange {
43
+ readonly start: number;
44
+ readonly end: number;
45
+ readonly encoding: "utf8";
46
+ }
47
+ export interface VisitorDiagnostic {
48
+ readonly code: string;
49
+ readonly message: string;
50
+ readonly severity: "error" | "warning" | "information" | "hint";
51
+ readonly range: VisitorRange;
52
+ readonly source?: string;
53
+ }
54
+ export interface VisitorFix {
55
+ readonly range: VisitorRange;
56
+ readonly text: string;
57
+ }
58
+ export interface VisitorSfcBlock {
59
+ readonly kind: "template" | "script" | "script-setup" | "style" | "custom";
60
+ readonly language: string | null;
61
+ readonly range: VisitorRange;
62
+ readonly attributes: Readonly<Record<string, string | null>>;
63
+ }
64
+ export interface VisitorSfcPayload {
65
+ readonly source: string;
66
+ readonly blocks: readonly VisitorSfcBlock[];
67
+ }
68
+ export interface VisitorTemplatePayload {
69
+ readonly nodes: readonly PluginNode[];
70
+ }
71
+ export interface VisitorScriptPayload {
72
+ readonly source: string;
73
+ readonly language: "js" | "jsx" | "ts" | "tsx";
74
+ /** Host-owned serialized syntax data. Its schema belongs to the negotiated host. */
75
+ readonly syntax?: JsonValue;
76
+ }
77
+ export interface VisitorStylePayload {
78
+ readonly source: string;
79
+ readonly language: string;
80
+ readonly scoped: boolean;
81
+ readonly module: boolean | string;
82
+ }
83
+ export interface VisitorVirtualTsMapping {
84
+ readonly source: VisitorRange;
85
+ readonly generated: VisitorRange;
86
+ }
87
+ export interface VisitorVirtualTsPayload {
88
+ readonly sourceFile: string;
89
+ readonly virtualFile: string;
90
+ readonly code: string;
91
+ readonly mappings: readonly VisitorVirtualTsMapping[];
92
+ }
93
+ export interface VisitorDiagnosticsPayload {
94
+ readonly diagnostics: readonly VisitorDiagnostic[];
95
+ }
96
+ export interface VisitorLintPayload extends VisitorDiagnosticsPayload {
97
+ readonly fixes: readonly VisitorFix[];
98
+ }
99
+ export interface VisitorFormatPayload {
100
+ readonly source: string;
101
+ readonly formatted: string;
102
+ }
103
+ export interface VisitorDevtoolsTraceEvent {
104
+ readonly id: string;
105
+ readonly parentId: string | null;
106
+ readonly name: string;
107
+ readonly stage: string;
108
+ readonly startNs: number;
109
+ readonly durationNs: number;
110
+ readonly data?: JsonValue;
111
+ }
112
+ export interface VisitorDevtoolsTracePayload {
113
+ readonly events: readonly VisitorDevtoolsTraceEvent[];
114
+ }
115
+ export interface VisitorPayloads {
116
+ readonly sfc: VisitorSfcPayload;
117
+ readonly template: VisitorTemplatePayload;
118
+ readonly script: VisitorScriptPayload;
119
+ readonly style: VisitorStylePayload;
120
+ readonly "virtual-ts": VisitorVirtualTsPayload;
121
+ readonly diagnostics: VisitorDiagnosticsPayload;
122
+ readonly lint: VisitorLintPayload;
123
+ readonly format: VisitorFormatPayload;
124
+ readonly "devtools-trace": VisitorDevtoolsTracePayload;
125
+ }
126
+ export interface VisitorEvent<C extends VisitorCapability = VisitorCapability> {
127
+ readonly apiVersion: 1;
128
+ readonly capability: C;
129
+ readonly file: string;
130
+ readonly payload: VisitorPayloads[C];
131
+ }
132
+ export type VisitorHandler<C extends VisitorCapability> = (event: VisitorEvent<C>) => void;
133
+ export type VisitorHandlers<C extends readonly VisitorCapability[]> = {
134
+ readonly [K in C[number]]: VisitorHandler<K>;
135
+ };
136
+ export interface VisitorCacheInput {
137
+ readonly name: string;
138
+ readonly value: string;
139
+ }
140
+ export interface VisitorPluginDefinition<C extends readonly VisitorCapability[]> {
141
+ readonly apiVersion: 1;
142
+ readonly name: string;
143
+ readonly version: string;
144
+ readonly capabilities: C;
145
+ /** Explicit list of every captured configuration or ambient input. */
146
+ readonly cacheInputs: readonly VisitorCacheInput[];
147
+ readonly visitors: VisitorHandlers<C>;
148
+ }
149
+ export interface VisitorPlugin<
150
+ C extends readonly VisitorCapability[] = readonly VisitorCapability[],
151
+ > {
152
+ readonly kind: "vize.visitor-plugin";
153
+ readonly apiVersion: 1;
154
+ readonly name: string;
155
+ readonly version: string;
156
+ readonly capabilities: C;
157
+ readonly cacheInputs: readonly VisitorCacheInput[];
158
+ readonly visitors: VisitorHandlers<C>;
159
+ readonly fingerprint: string;
160
+ }
161
+ export interface VisitorHostContract<
162
+ C extends readonly VisitorCapability[] = readonly VisitorCapability[],
163
+ > {
164
+ readonly apiVersion: 1;
165
+ readonly capabilities: C;
166
+ }
167
+ export interface NegotiatedVisitorContract<C extends readonly VisitorCapability[]> {
168
+ readonly apiVersion: 1;
169
+ readonly capabilities: C;
170
+ }
171
+ export interface VirtualTsVisitorHostPlugin {
172
+ readonly name: string;
173
+ readonly version: string;
174
+ readonly fingerprint: string;
175
+ /** Host-only serialized boundary consumed by `typeCheckWithVirtualTsVisitors`. */
176
+ readonly run: (eventJson: string) => string;
177
+ }
178
+ export interface DevtoolsTraceVisitorHostPlugin {
179
+ readonly name: string;
180
+ readonly version: string;
181
+ readonly fingerprint: string;
182
+ /** Host-only serialized boundary consumed by `createDevtoolsTraceRecorder`. */
183
+ readonly run: (deliveryJson: string) => string;
184
+ }
185
+
186
+ /** Define an immutable descriptor. This does not register a runtime hook. */
187
+ export declare function defineVisitorPlugin<const C extends readonly VisitorCapability[]>(
188
+ definition: VisitorPluginDefinition<C>,
189
+ ): VisitorPlugin<C>;
190
+ /** Fail closed unless the host provides the exact API version and every declared capability. */
191
+ export declare function negotiateVisitorPlugin<const C extends readonly VisitorCapability[]>(
192
+ plugin: VisitorPlugin<C>,
193
+ host: VisitorHostContract,
194
+ ): NegotiatedVisitorContract<C>;
195
+ /** Invoke one visitor in an author fixture; production lifecycle remains host-owned. */
196
+ export declare function runVisitorFixture<
197
+ C extends readonly VisitorCapability[],
198
+ K extends C[number],
199
+ >(plugin: VisitorPlugin<C>, capability: K, event: VisitorEvent<K>): void;
200
+ /**
201
+ * Negotiate a virtual-TypeScript-only descriptor and adapt it to the native
202
+ * type-check visitor boundary. Descriptors with any unsupported capability fail closed.
203
+ */
204
+ export declare function createVirtualTsVisitorHost(
205
+ plugin: VisitorPlugin<readonly ["virtual-ts"]>,
206
+ ): VirtualTsVisitorHostPlugin;
207
+ /**
208
+ * Negotiate a devtools-trace-only descriptor and adapt it to the opt-in
209
+ * `@vizejs/devtools` recorder boundary. Mixed-capability descriptors fail closed.
210
+ */
211
+ export declare function createDevtoolsTraceVisitorHost(
212
+ plugin: VisitorPlugin<readonly ["devtools-trace"]>,
213
+ ): DevtoolsTraceVisitorHostPlugin;
package/visitor.js ADDED
@@ -0,0 +1,370 @@
1
+ // Capability-only visitor authoring contracts. Native pipeline registration is
2
+ // deliberately separate so importing this module cannot affect compiler paths.
3
+ import { createHash } from "node:crypto";
4
+ import { readFileSync } from "node:fs";
5
+ import { SDK_FINGERPRINT } from "./identity.js";
6
+
7
+ export const VISITOR_API_VERSION = 1;
8
+ export const VISITOR_CAPABILITIES = Object.freeze([
9
+ "sfc",
10
+ "template",
11
+ "script",
12
+ "style",
13
+ "virtual-ts",
14
+ "diagnostics",
15
+ "lint",
16
+ "format",
17
+ "devtools-trace",
18
+ ]);
19
+
20
+ const CAPABILITY_ORDER = new Map(
21
+ VISITOR_CAPABILITIES.map((capability, index) => [capability, index]),
22
+ );
23
+ const VISITOR_RUNTIME_FINGERPRINT = createHash("sha256")
24
+ .update(readFileSync(new URL("visitor.js", import.meta.url)))
25
+ .digest("hex");
26
+ const plugins = new WeakSet();
27
+
28
+ export class VisitorPluginValidationError extends TypeError {
29
+ constructor(code, field, message) {
30
+ super(`[visitor/${code}] ${field}: ${message}`);
31
+ this.name = "VisitorPluginValidationError";
32
+ this.code = code;
33
+ this.field = field;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Define an immutable v1 visitor descriptor without registering it in a Vize
39
+ * runtime. A host must explicitly negotiate the descriptor before invocation.
40
+ */
41
+ export function defineVisitorPlugin(definition) {
42
+ record(definition, "definition", "invalid-definition");
43
+ const name = nonempty(definition.name, "name", "invalid-name");
44
+ const version = nonempty(definition.version, "version", "invalid-version");
45
+ apiVersion(definition.apiVersion, "apiVersion");
46
+ const capabilities = capabilitiesOf(definition.capabilities, "capabilities", true);
47
+ const visitors = visitorsOf(definition.visitors, capabilities);
48
+ const cacheInputs = cacheInputsOf(definition.cacheInputs);
49
+ const callbacks = capabilities.map((capability) => [capability, String(visitors[capability])]);
50
+ const fingerprint = createHash("sha256")
51
+ .update(
52
+ JSON.stringify([
53
+ SDK_FINGERPRINT,
54
+ VISITOR_RUNTIME_FINGERPRINT,
55
+ VISITOR_API_VERSION,
56
+ name,
57
+ version,
58
+ capabilities,
59
+ cacheInputs,
60
+ callbacks,
61
+ ]),
62
+ )
63
+ .digest("hex");
64
+ const plugin = immutable({
65
+ kind: "vize.visitor-plugin",
66
+ apiVersion: VISITOR_API_VERSION,
67
+ name,
68
+ version,
69
+ capabilities,
70
+ cacheInputs,
71
+ visitors,
72
+ fingerprint,
73
+ });
74
+ plugins.add(plugin);
75
+ return plugin;
76
+ }
77
+
78
+ /** Validate a host contract and fail closed when any requested capability is absent. */
79
+ export function negotiateVisitorPlugin(plugin, host) {
80
+ if (!plugins.has(plugin)) {
81
+ fail("invalid-plugin", "plugin", "must be created by defineVisitorPlugin");
82
+ }
83
+ record(host, "host", "invalid-host-contract");
84
+ apiVersion(host.apiVersion, "host.apiVersion");
85
+ const available = capabilitiesOf(host.capabilities, "host.capabilities", false);
86
+ const supported = new Set(available);
87
+ const missing = plugin.capabilities.filter((capability) => !supported.has(capability));
88
+ if (missing.length > 0) {
89
+ fail(
90
+ "unsupported-capability",
91
+ "host.capabilities",
92
+ `missing ${missing.map((capability) => JSON.stringify(capability)).join(", ")}`,
93
+ );
94
+ }
95
+ return immutable({ apiVersion: VISITOR_API_VERSION, capabilities: [...plugin.capabilities] });
96
+ }
97
+
98
+ /**
99
+ * Invoke one negotiated visitor in an author fixture. This is not a compiler or
100
+ * linter registration path; production hosts own cancellation and isolation.
101
+ */
102
+ export function runVisitorFixture(plugin, capability, event) {
103
+ if (!plugins.has(plugin)) {
104
+ fail("invalid-plugin", "plugin", "must be created by defineVisitorPlugin");
105
+ }
106
+ if (!CAPABILITY_ORDER.has(capability)) {
107
+ fail("unknown-capability", "capability", `${JSON.stringify(capability)} is not supported`);
108
+ }
109
+ if (!plugin.capabilities.includes(capability)) {
110
+ fail(
111
+ "undeclared-capability",
112
+ "capability",
113
+ `${JSON.stringify(capability)} is not declared by ${JSON.stringify(plugin.name)}`,
114
+ );
115
+ }
116
+ record(event, "event", "invalid-event");
117
+ apiVersion(event.apiVersion, "event.apiVersion");
118
+ if (event.capability !== capability) {
119
+ fail(
120
+ "invalid-event",
121
+ "event.capability",
122
+ `expected ${JSON.stringify(capability)}, received ${JSON.stringify(event.capability)}`,
123
+ );
124
+ }
125
+ nonempty(event.file, "event.file", "invalid-event");
126
+ if (!("payload" in event)) fail("invalid-event", "event.payload", "is required");
127
+ const result = plugin.visitors[capability](immutable(event));
128
+ if (result?.then) {
129
+ fail(
130
+ "async-visitor",
131
+ `visitors.${capability}`,
132
+ "returned a Promise; asynchronous visitors are unsupported",
133
+ );
134
+ }
135
+ if (result !== undefined) {
136
+ fail("invalid-visitor-result", `visitors.${capability}`, "must return undefined");
137
+ }
138
+ }
139
+
140
+ /** Adapt one negotiated virtual-TypeScript-only descriptor to the native host boundary. */
141
+ export function createVirtualTsVisitorHost(plugin) {
142
+ negotiateVisitorPlugin(plugin, {
143
+ apiVersion: VISITOR_API_VERSION,
144
+ capabilities: ["virtual-ts"],
145
+ });
146
+ return immutable({
147
+ name: plugin.name,
148
+ version: plugin.version,
149
+ fingerprint: plugin.fingerprint,
150
+ run(eventJson) {
151
+ if (typeof eventJson !== "string") {
152
+ fail("invalid-event", "event", "must be serialized JSON");
153
+ }
154
+ let event;
155
+ try {
156
+ event = JSON.parse(eventJson);
157
+ } catch {
158
+ fail("invalid-event", "event", "must be valid JSON");
159
+ }
160
+ runVisitorFixture(plugin, "virtual-ts", event);
161
+ return "";
162
+ },
163
+ });
164
+ }
165
+
166
+ /** Adapt one negotiated devtools-trace-only descriptor to the recorder boundary. */
167
+ export function createDevtoolsTraceVisitorHost(plugin) {
168
+ negotiateVisitorPlugin(plugin, {
169
+ apiVersion: VISITOR_API_VERSION,
170
+ capabilities: ["devtools-trace"],
171
+ });
172
+ return immutable({
173
+ name: plugin.name,
174
+ version: plugin.version,
175
+ fingerprint: plugin.fingerprint,
176
+ run(deliveryJson) {
177
+ if (typeof deliveryJson !== "string") {
178
+ fail("invalid-event", "delivery", "must be serialized JSON");
179
+ }
180
+ let delivery;
181
+ try {
182
+ delivery = JSON.parse(deliveryJson);
183
+ } catch {
184
+ fail("invalid-event", "delivery", "must be valid JSON");
185
+ }
186
+ record(delivery, "delivery", "invalid-event");
187
+ if (delivery.schemaVersion !== 1) {
188
+ fail("invalid-event", "delivery.schemaVersion", "must equal 1");
189
+ }
190
+ const sessionId = nonempty(delivery.sessionId, "delivery.sessionId", "invalid-event");
191
+ record(delivery.event, "delivery.event", "invalid-event");
192
+ const { event } = delivery;
193
+ if (!Number.isInteger(event.sequence) || event.sequence < 1) {
194
+ fail("invalid-event", "delivery.event.sequence", "must be a positive integer");
195
+ }
196
+ if (
197
+ typeof event.timestamp !== "number" ||
198
+ !Number.isFinite(event.timestamp) ||
199
+ event.timestamp < 0
200
+ ) {
201
+ fail("invalid-event", "delivery.event.timestamp", "must be a non-negative finite number");
202
+ }
203
+ const kind = nonempty(event.kind, "delivery.event.kind", "invalid-event");
204
+ const durationMs = event.durationMs ?? 0;
205
+ if (typeof durationMs !== "number" || !Number.isFinite(durationMs) || durationMs < 0) {
206
+ fail("invalid-event", "delivery.event.durationMs", "must be a non-negative finite number");
207
+ }
208
+ const { sequence, timestamp } = event;
209
+ const startNs = timestamp * 1_000_000;
210
+ const durationNs = durationMs * 1_000_000;
211
+ if (!Number.isFinite(startNs) || !Number.isFinite(durationNs)) {
212
+ fail("invalid-event", "delivery.event", "cannot be represented in nanoseconds");
213
+ }
214
+ const data = Object.fromEntries(
215
+ Object.entries(event).filter(
216
+ ([key]) =>
217
+ key !== "sequence" && key !== "timestamp" && key !== "kind" && key !== "durationMs",
218
+ ),
219
+ );
220
+ const source =
221
+ typeof event.source === "string" && event.source.trim() !== ""
222
+ ? event.source
223
+ : `vize-devtools://${encodeURIComponent(sessionId)}`;
224
+ const separator = kind.indexOf(":");
225
+ runVisitorFixture(plugin, "devtools-trace", {
226
+ apiVersion: VISITOR_API_VERSION,
227
+ capability: "devtools-trace",
228
+ file: source,
229
+ payload: {
230
+ events: [
231
+ {
232
+ id: `${sessionId}:${sequence}`,
233
+ parentId: null,
234
+ name: kind,
235
+ stage: separator === -1 ? kind : kind.slice(0, separator),
236
+ startNs,
237
+ durationNs,
238
+ data,
239
+ },
240
+ ],
241
+ },
242
+ });
243
+ return "";
244
+ },
245
+ });
246
+ }
247
+
248
+ function capabilitiesOf(value, field, requireNonempty) {
249
+ if (!Array.isArray(value) || (requireNonempty && value.length === 0)) {
250
+ fail(
251
+ "invalid-capabilities",
252
+ field,
253
+ requireNonempty ? "must be a non-empty array" : "must be an array",
254
+ );
255
+ }
256
+ const seen = new Set();
257
+ for (const [index, capability] of value.entries()) {
258
+ if (typeof capability !== "string" || !CAPABILITY_ORDER.has(capability)) {
259
+ fail(
260
+ "unknown-capability",
261
+ `${field}[${index}]`,
262
+ `${JSON.stringify(capability)} is not supported`,
263
+ );
264
+ }
265
+ if (seen.has(capability)) {
266
+ fail(
267
+ "duplicate-capability",
268
+ field,
269
+ `${JSON.stringify(capability)} is declared more than once`,
270
+ );
271
+ }
272
+ seen.add(capability);
273
+ }
274
+ return Object.freeze([...seen]);
275
+ }
276
+
277
+ function visitorsOf(value, capabilities) {
278
+ record(value, "visitors", "invalid-visitors");
279
+ const declared = new Set(capabilities);
280
+ for (const capability of Object.keys(value)) {
281
+ if (!CAPABILITY_ORDER.has(capability)) {
282
+ fail(
283
+ "unknown-capability",
284
+ `visitors.${capability}`,
285
+ `${JSON.stringify(capability)} is not supported`,
286
+ );
287
+ }
288
+ if (!declared.has(capability)) {
289
+ fail(
290
+ "undeclared-visitor",
291
+ `visitors.${capability}`,
292
+ "has no matching capability declaration",
293
+ );
294
+ }
295
+ }
296
+ const visitors = {};
297
+ for (const capability of capabilities) {
298
+ const callback = value[capability];
299
+ if (typeof callback !== "function") {
300
+ fail("missing-visitor", `visitors.${capability}`, "must be a synchronous function");
301
+ }
302
+ if (callback.constructor?.name === "AsyncFunction") {
303
+ fail("async-visitor", `visitors.${capability}`, "asynchronous visitors are unsupported");
304
+ }
305
+ visitors[capability] = callback;
306
+ }
307
+ return Object.freeze(visitors);
308
+ }
309
+
310
+ function cacheInputsOf(value) {
311
+ if (!Array.isArray(value)) {
312
+ fail("invalid-cache-inputs", "cacheInputs", "must be an explicit array");
313
+ }
314
+ const inputs = [];
315
+ const names = new Set();
316
+ for (const [index, input] of value.entries()) {
317
+ record(input, `cacheInputs[${index}]`, "invalid-cache-inputs");
318
+ const name = nonempty(input.name, `cacheInputs[${index}].name`, "invalid-cache-inputs");
319
+ if (name.startsWith("@vize/")) {
320
+ fail("invalid-cache-inputs", `cacheInputs[${index}].name`, "uses a reserved host prefix");
321
+ }
322
+ if (names.has(name)) {
323
+ fail(
324
+ "invalid-cache-inputs",
325
+ "cacheInputs",
326
+ `contains duplicate name ${JSON.stringify(name)}`,
327
+ );
328
+ }
329
+ if (typeof input.value !== "string") {
330
+ fail("invalid-cache-inputs", `cacheInputs[${index}].value`, "must be a string");
331
+ }
332
+ names.add(name);
333
+ inputs.push(Object.freeze({ name, value: input.value }));
334
+ }
335
+ return Object.freeze(inputs.sort((left, right) => left.name.localeCompare(right.name)));
336
+ }
337
+
338
+ function apiVersion(value, field) {
339
+ if (value !== VISITOR_API_VERSION) {
340
+ fail(
341
+ "unsupported-api-version",
342
+ field,
343
+ `${JSON.stringify(value)} is unsupported; expected ${VISITOR_API_VERSION}`,
344
+ );
345
+ }
346
+ }
347
+
348
+ function nonempty(value, field, code) {
349
+ if (typeof value !== "string" || value.trim() === "")
350
+ fail(code, field, "must be a non-empty string");
351
+ return value;
352
+ }
353
+
354
+ function record(value, field, code) {
355
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
356
+ fail(code, field, "must be an object");
357
+ }
358
+ }
359
+
360
+ function fail(code, field, message) {
361
+ throw new VisitorPluginValidationError(code, field, message);
362
+ }
363
+
364
+ function immutable(value) {
365
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
366
+ for (const child of Object.values(value)) immutable(child);
367
+ Object.freeze(value);
368
+ }
369
+ return value;
370
+ }