@vizejs/plugin-sdk 0.514.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 +127 -1
- package/package.json +9 -3
- package/visitor.d.ts +213 -0
- package/visitor.js +370 -0
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.
|
|
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.
|
|
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.
|
|
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
|
+
}
|