@ethisyscore/vite-plugin 1.5.2 → 1.6.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/dist/index.cjs +612 -49
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +220 -1
- package/dist/index.d.ts +220 -1
- package/dist/index.js +593 -14
- package/dist/index.js.map +1 -0
- package/package.json +14 -7
package/dist/index.js
CHANGED
|
@@ -1,13 +1,591 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { isAbsolute, resolve, sep, normalize } from 'path';
|
|
3
|
+
import { KNOWN_PRIMITIVES, KNOWN_OPERATORS, KNOWN_RULE_KINDS, SduiNode, ReactiveRule } from '@ethisyscore/protocol';
|
|
4
|
+
|
|
1
5
|
// src/index.ts
|
|
2
|
-
|
|
3
|
-
|
|
6
|
+
var KNOWN_PRIMITIVE_SET = new Set(KNOWN_PRIMITIVES);
|
|
7
|
+
var KNOWN_OPERATOR_SET = new Set(KNOWN_OPERATORS);
|
|
8
|
+
var KNOWN_RULE_KIND_SET = new Set(KNOWN_RULE_KINDS);
|
|
9
|
+
function toJsonPointer(path) {
|
|
10
|
+
if (path.length === 0) {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
return "/" + path.map((seg) => {
|
|
14
|
+
const s = String(seg);
|
|
15
|
+
return s.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
16
|
+
}).join("/");
|
|
17
|
+
}
|
|
18
|
+
function extractOffender(doc, issue) {
|
|
19
|
+
if (issue.code === "invalid_value" || issue.code === "invalid_enum_value") {
|
|
20
|
+
const received = issue.received;
|
|
21
|
+
if (typeof received === "string") {
|
|
22
|
+
return received;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
let cur = doc;
|
|
26
|
+
for (const seg of issue.path) {
|
|
27
|
+
if (cur === null || cur === void 0) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
cur = cur[seg];
|
|
31
|
+
}
|
|
32
|
+
if (cur && typeof cur === "object" && !Array.isArray(cur)) {
|
|
33
|
+
const keys = Object.keys(cur);
|
|
34
|
+
if (keys.length === 1 && !KNOWN_OPERATOR_SET.has(keys[0])) {
|
|
35
|
+
return keys[0];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (issue.path.length > 0) {
|
|
39
|
+
const last = issue.path[issue.path.length - 1];
|
|
40
|
+
if (typeof last === "string" && !KNOWN_RULE_KIND_SET.has(last) && !KNOWN_PRIMITIVE_SET.has(last)) {
|
|
41
|
+
return last;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
function zodIssuesToFailures(doc, issues, filePath) {
|
|
47
|
+
return issues.map((issue) => {
|
|
48
|
+
const pointer = toJsonPointer(issue.path);
|
|
49
|
+
const offender = extractOffender(doc, issue);
|
|
50
|
+
const offenderSuffix = offender ? ` (offender: '${offender}')` : "";
|
|
51
|
+
return {
|
|
52
|
+
filePath,
|
|
53
|
+
pointer,
|
|
54
|
+
offender,
|
|
55
|
+
message: `${filePath}${pointer ? ` at ${pointer}` : ""}: ${issue.message}${offenderSuffix}`
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function validateDeclarativeResource(doc, uri, filePath) {
|
|
60
|
+
const parsed = SduiNode.safeParse(doc);
|
|
61
|
+
if (parsed.success) {
|
|
62
|
+
return { ok: true };
|
|
63
|
+
}
|
|
64
|
+
const failures = zodIssuesToFailures(doc, parsed.error.issues, filePath);
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
failures: failures.map((f) => ({
|
|
68
|
+
...f,
|
|
69
|
+
message: `[resource ${uri}] ${f.message}`
|
|
70
|
+
}))
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function validateReactiveRule(rule, filePath) {
|
|
74
|
+
const parsed = ReactiveRule.safeParse(rule);
|
|
75
|
+
if (parsed.success) {
|
|
76
|
+
return { ok: true };
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
failures: zodIssuesToFailures(rule, parsed.error.issues, filePath)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/contract-a/emit.ts
|
|
85
|
+
function slugForUri(uri) {
|
|
86
|
+
return uri.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
87
|
+
}
|
|
88
|
+
function assertSafeRelativePath(value, label) {
|
|
89
|
+
const normalized = normalize(value);
|
|
90
|
+
if (isAbsolute(value) || isAbsolute(normalized)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`[ethisys-contract-a] ${label} "${value}" must be relative, not absolute.`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (normalized.split(/[\\/]/).includes("..")) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`[ethisys-contract-a] ${label} "${value}" must not contain path traversal.`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function formatFailures(failures) {
|
|
102
|
+
const lines = failures.map((f, i) => ` ${i + 1}. ${f.message}`);
|
|
103
|
+
return [
|
|
104
|
+
`[@ethisyscore/vite-plugin] Contract A schema validation failed:`,
|
|
105
|
+
...lines
|
|
106
|
+
].join("\n");
|
|
107
|
+
}
|
|
108
|
+
function ethisysContractAPlugin(options = {}) {
|
|
109
|
+
const outputPrefix = (options.outputPrefix ?? "sdui/").replace(/\/+$/, "/").replace(/^\/+/, "");
|
|
110
|
+
const explicitRoot = options.root;
|
|
111
|
+
const manifestRel = options.manifestPath ?? "feature.manifest.json";
|
|
112
|
+
let resolvedRoot;
|
|
113
|
+
let manifestAbsPath;
|
|
114
|
+
let validatedResources = [];
|
|
115
|
+
const resourcePayloads = /* @__PURE__ */ new Map();
|
|
116
|
+
function resolveRoot() {
|
|
117
|
+
if (resolvedRoot) {
|
|
118
|
+
return resolvedRoot;
|
|
119
|
+
}
|
|
120
|
+
resolvedRoot = explicitRoot ?? process.cwd();
|
|
121
|
+
manifestAbsPath = isAbsolute(manifestRel) ? manifestRel : resolve(resolvedRoot, manifestRel);
|
|
122
|
+
return resolvedRoot;
|
|
123
|
+
}
|
|
124
|
+
function readManifest() {
|
|
125
|
+
resolveRoot();
|
|
126
|
+
if (!existsSync(manifestAbsPath)) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const raw = readFileSync(manifestAbsPath, "utf-8");
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(raw);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`[@ethisyscore/vite-plugin] Failed to parse manifest at "${manifestAbsPath}": ${e.message}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function validate() {
|
|
139
|
+
validatedResources = [];
|
|
140
|
+
resourcePayloads.clear();
|
|
141
|
+
const manifest = readManifest();
|
|
142
|
+
if (manifest === null) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const failures = [];
|
|
146
|
+
for (const ref of manifest.resources ?? []) {
|
|
147
|
+
if (!ref.file) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
assertSafeRelativePath(ref.file, "Resource file path");
|
|
151
|
+
const filePath = resolve(resolvedRoot, ref.file);
|
|
152
|
+
if (!existsSync(filePath)) {
|
|
153
|
+
failures.push({
|
|
154
|
+
filePath,
|
|
155
|
+
pointer: "",
|
|
156
|
+
message: `[resource ${ref.uri}] file "${ref.file}" referenced by the manifest does not exist on disk (resolved: ${filePath}).`
|
|
157
|
+
});
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const rawJson = readFileSync(filePath, "utf-8");
|
|
161
|
+
let doc;
|
|
162
|
+
try {
|
|
163
|
+
doc = JSON.parse(rawJson);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
failures.push({
|
|
166
|
+
filePath,
|
|
167
|
+
pointer: "",
|
|
168
|
+
message: `[resource ${ref.uri}] failed to parse JSON: ${e.message}`
|
|
169
|
+
});
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const result = validateDeclarativeResource(doc, ref.uri, filePath);
|
|
173
|
+
if (!result.ok) {
|
|
174
|
+
failures.push(...result.failures);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const fileName = `${outputPrefix}${slugForUri(ref.uri)}.json`;
|
|
178
|
+
const payload = JSON.stringify(doc);
|
|
179
|
+
resourcePayloads.set(fileName, payload);
|
|
180
|
+
validatedResources.push({ uri: ref.uri, fileName });
|
|
181
|
+
}
|
|
182
|
+
for (const ruleRef of manifest.reactiveRules ?? []) {
|
|
183
|
+
const result = validateReactiveRule(ruleRef.rule, manifestAbsPath);
|
|
184
|
+
if (!result.ok) {
|
|
185
|
+
failures.push(
|
|
186
|
+
...result.failures.map((f) => ({
|
|
187
|
+
...f,
|
|
188
|
+
message: `[reactiveRule ${ruleRef.id}] ${f.message}`
|
|
189
|
+
}))
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (failures.length > 0) {
|
|
194
|
+
throw new Error(formatFailures(failures));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
name: "ethisys-contract-a",
|
|
199
|
+
enforce: "pre",
|
|
200
|
+
configResolved(config) {
|
|
201
|
+
if (!explicitRoot) {
|
|
202
|
+
resolvedRoot = config.root;
|
|
203
|
+
manifestAbsPath = isAbsolute(manifestRel) ? manifestRel : resolve(resolvedRoot, manifestRel);
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
buildStart() {
|
|
207
|
+
validate();
|
|
208
|
+
},
|
|
209
|
+
generateBundle() {
|
|
210
|
+
if (validatedResources.length === 0) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
for (const { fileName } of validatedResources) {
|
|
214
|
+
const source = resourcePayloads.get(fileName);
|
|
215
|
+
if (source === void 0) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
this.emitFile({ type: "asset", fileName, source });
|
|
219
|
+
}
|
|
220
|
+
const sorted = [...validatedResources].sort(
|
|
221
|
+
(a, b) => a.uri.localeCompare(b.uri)
|
|
222
|
+
);
|
|
223
|
+
const index = {
|
|
224
|
+
version: 1,
|
|
225
|
+
resources: sorted.map(({ uri, fileName }) => ({ uri, file: fileName }))
|
|
226
|
+
};
|
|
227
|
+
this.emitFile({
|
|
228
|
+
type: "asset",
|
|
229
|
+
fileName: `${outputPrefix}sdui-manifest.json`,
|
|
230
|
+
source: JSON.stringify(index)
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/contract-b/rewrite-imports.ts
|
|
237
|
+
var STATIC_IMPORT_RE = /^([ \t]*)import[ \t]+(.*?)[ \t]*;?[ \t]*$/gm;
|
|
238
|
+
function rewriteSafeImports(code, allowlist) {
|
|
239
|
+
const allowSet = new Set(allowlist);
|
|
240
|
+
return code.replace(STATIC_IMPORT_RE, (match, indent, body) => {
|
|
241
|
+
const rewrite = tryRewriteImport(body, allowSet);
|
|
242
|
+
return rewrite === null ? match : `${indent}${rewrite}`;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function convertNamedBindings(namedClause) {
|
|
246
|
+
const inner = namedClause.slice(1, -1).trim();
|
|
247
|
+
if (inner.length === 0) {
|
|
248
|
+
return "";
|
|
249
|
+
}
|
|
250
|
+
const parts = inner.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
251
|
+
const converted = parts.map((part) => {
|
|
252
|
+
const asMatch = part.match(/^(\w[\w$]*)\s+as\s+(\w[\w$]*)$/);
|
|
253
|
+
return asMatch === null ? part : `${asMatch[1]}: ${asMatch[2]}`;
|
|
254
|
+
});
|
|
255
|
+
return converted.join(", ");
|
|
256
|
+
}
|
|
257
|
+
function tryRewriteImport(body, allowSet) {
|
|
258
|
+
const sideEffect = body.match(/^["']([^"']+)["']$/);
|
|
259
|
+
if (sideEffect !== null) {
|
|
260
|
+
const spec = sideEffect[1];
|
|
261
|
+
if (!allowSet.has(spec)) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
return `await globalThis.__ethisysSafeImport(${JSON.stringify(spec)});`;
|
|
265
|
+
}
|
|
266
|
+
const defaultPlusNamed = body.match(
|
|
267
|
+
/^(\w[\w$]*)\s*,\s*(\{[^}]*\})\s+from\s+["']([^"']+)["']$/
|
|
268
|
+
);
|
|
269
|
+
if (defaultPlusNamed !== null) {
|
|
270
|
+
const defaultLocal = defaultPlusNamed[1];
|
|
271
|
+
const namedClause = defaultPlusNamed[2];
|
|
272
|
+
const spec = defaultPlusNamed[3];
|
|
273
|
+
if (!allowSet.has(spec)) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
const bindings = convertNamedBindings(namedClause);
|
|
277
|
+
const inner = bindings.length === 0 ? `default: ${defaultLocal}` : `default: ${defaultLocal}, ${bindings}`;
|
|
278
|
+
return `const { ${inner} } = await globalThis.__ethisysSafeImport(${JSON.stringify(spec)});`;
|
|
279
|
+
}
|
|
280
|
+
const defaultOnly = body.match(/^(\w[\w$]*)\s+from\s+["']([^"']+)["']$/);
|
|
281
|
+
if (defaultOnly !== null) {
|
|
282
|
+
const local = defaultOnly[1];
|
|
283
|
+
const spec = defaultOnly[2];
|
|
284
|
+
if (!allowSet.has(spec)) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
return `const { default: ${local} } = await globalThis.__ethisysSafeImport(${JSON.stringify(spec)});`;
|
|
288
|
+
}
|
|
289
|
+
const namedOnly = body.match(/^(\{[^}]*\})\s+from\s+["']([^"']+)["']$/);
|
|
290
|
+
if (namedOnly !== null) {
|
|
291
|
+
const namedClause = namedOnly[1];
|
|
292
|
+
const spec = namedOnly[2];
|
|
293
|
+
if (!allowSet.has(spec)) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const bindings = convertNamedBindings(namedClause);
|
|
297
|
+
return `const { ${bindings} } = await globalThis.__ethisysSafeImport(${JSON.stringify(spec)});`;
|
|
298
|
+
}
|
|
299
|
+
const namespaceOnly = body.match(/^\*\s+as\s+(\w[\w$]*)\s+from\s+["']([^"']+)["']$/);
|
|
300
|
+
if (namespaceOnly !== null) {
|
|
301
|
+
const local = namespaceOnly[1];
|
|
302
|
+
const spec = namespaceOnly[2];
|
|
303
|
+
if (!allowSet.has(spec)) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return `const ${local} = await globalThis.__ethisysSafeImport(${JSON.stringify(spec)});`;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/contract-b/worker-bundle.ts
|
|
312
|
+
var CONTRACT_B_SEMANTIC_PRIMITIVES = [
|
|
313
|
+
"Button",
|
|
314
|
+
"DataTable",
|
|
315
|
+
"Form",
|
|
316
|
+
"EntityPicker",
|
|
317
|
+
"CommandBar",
|
|
318
|
+
"Drawer",
|
|
319
|
+
"Modal",
|
|
320
|
+
"CanvasSurface",
|
|
321
|
+
"WebGLSurface"
|
|
322
|
+
];
|
|
323
|
+
var CONTRACT_B_RUNTIME_IMPORTS = [
|
|
324
|
+
"react",
|
|
325
|
+
"@ethisyscore/extension-runtime"
|
|
326
|
+
];
|
|
327
|
+
var CONTRACT_B_IMPORT_MAP_ALLOWLIST = Object.freeze([
|
|
328
|
+
...CONTRACT_B_RUNTIME_IMPORTS,
|
|
329
|
+
...CONTRACT_B_SEMANTIC_PRIMITIVES
|
|
330
|
+
]);
|
|
331
|
+
var ALLOWLIST_SET = new Set(CONTRACT_B_IMPORT_MAP_ALLOWLIST);
|
|
332
|
+
function assertHostOriginRelativePath(value, label) {
|
|
333
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`[ethisys-contract-b] ${label} "${value}" must be a host-origin relative path, not a remote URL.`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
if (/^(data|blob|javascript):/i.test(value)) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`[ethisys-contract-b] ${label} "${value}" must be a host-origin relative path, not a ${value.split(":")[0]}: URL.`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
const normalized = normalize(value);
|
|
344
|
+
if (isAbsolute(value) || isAbsolute(normalized)) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
`[ethisys-contract-b] ${label} "${value}" must be relative, not absolute.`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
if (normalized.split(/[\\/]/).includes("..")) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
`[ethisys-contract-b] ${label} "${value}" must not contain path traversal.`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
4
355
|
function slash(p) {
|
|
5
356
|
return sep === "\\" ? p.replace(/\\/g, "/") : p;
|
|
6
357
|
}
|
|
358
|
+
function ethisysContractBPlugin(options = {}) {
|
|
359
|
+
const explicitRoot = options.root;
|
|
360
|
+
const manifestRel = options.manifestPath ?? "feature.manifest.json";
|
|
361
|
+
const outputPrefix = (options.outputPrefix ?? "worker/").replace(/\/+$/, "/").replace(/^\/+/, "");
|
|
362
|
+
let resolvedRoot;
|
|
363
|
+
let manifestAbsPath;
|
|
364
|
+
let workerEntryAbs = null;
|
|
365
|
+
let workerEntryRel = null;
|
|
366
|
+
function resolveRoot() {
|
|
367
|
+
if (resolvedRoot) {
|
|
368
|
+
return resolvedRoot;
|
|
369
|
+
}
|
|
370
|
+
resolvedRoot = explicitRoot ?? process.cwd();
|
|
371
|
+
manifestAbsPath = isAbsolute(manifestRel) ? manifestRel : resolve(resolvedRoot, manifestRel);
|
|
372
|
+
return resolvedRoot;
|
|
373
|
+
}
|
|
374
|
+
function readManifest() {
|
|
375
|
+
resolveRoot();
|
|
376
|
+
if (!existsSync(manifestAbsPath)) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
const raw = readFileSync(manifestAbsPath, "utf-8");
|
|
380
|
+
try {
|
|
381
|
+
return JSON.parse(raw);
|
|
382
|
+
} catch (e) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
`[ethisys-contract-b] Failed to parse manifest at "${manifestAbsPath}": ${e.message}`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function validate() {
|
|
389
|
+
workerEntryAbs = null;
|
|
390
|
+
workerEntryRel = null;
|
|
391
|
+
const manifest = readManifest();
|
|
392
|
+
if (manifest === null) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (manifest.renderMode !== "remote-runtime") {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const entry = manifest.worker?.entry;
|
|
399
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
400
|
+
throw new Error(
|
|
401
|
+
`[ethisys-contract-b] manifest declares renderMode='remote-runtime' but no worker.entry. Set "worker": { "entry": "src/worker.ts" } (relative path).`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
assertHostOriginRelativePath(entry, "Worker entry");
|
|
405
|
+
const absPath = resolve(resolvedRoot, entry);
|
|
406
|
+
if (!existsSync(absPath)) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
`[ethisys-contract-b] Worker entry file "${entry}" does not exist on disk (resolved: ${absPath}).`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
workerEntryAbs = absPath;
|
|
412
|
+
workerEntryRel = entry;
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
name: "ethisys-contract-b",
|
|
416
|
+
// `pre` ordering: validation should fire before user-supplied plugins.
|
|
417
|
+
enforce: "pre",
|
|
418
|
+
configResolved(config) {
|
|
419
|
+
if (!explicitRoot) {
|
|
420
|
+
resolvedRoot = config.root;
|
|
421
|
+
manifestAbsPath = isAbsolute(manifestRel) ? manifestRel : resolve(resolvedRoot, manifestRel);
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
/**
|
|
425
|
+
* Declares the Rollup input table and locks the output to a single ESM
|
|
426
|
+
* module. Only fires when the manifest opts in (`renderMode: remote-runtime`).
|
|
427
|
+
*/
|
|
428
|
+
config(_config, { command }) {
|
|
429
|
+
if (command !== "build") {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
validate();
|
|
433
|
+
if (workerEntryAbs === null) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const externalSpecifiers = new Set(CONTRACT_B_IMPORT_MAP_ALLOWLIST);
|
|
437
|
+
return {
|
|
438
|
+
build: {
|
|
439
|
+
rollupOptions: {
|
|
440
|
+
input: { worker: slash(workerEntryAbs) },
|
|
441
|
+
external: (id) => externalSpecifiers.has(id),
|
|
442
|
+
// CRITICAL: single-file output for the worker bundle. The
|
|
443
|
+
// E4.S4 frozen import map presumes ONE module entry per plugin —
|
|
444
|
+
// chunk splitting or dynamic-import shards would defeat it.
|
|
445
|
+
output: {
|
|
446
|
+
format: "es",
|
|
447
|
+
inlineDynamicImports: true,
|
|
448
|
+
entryFileNames: `${outputPrefix}[name].js`,
|
|
449
|
+
chunkFileNames: `${outputPrefix}[name]-[hash].js`,
|
|
450
|
+
assetFileNames: `${outputPrefix}[name][extname]`
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
},
|
|
456
|
+
buildStart() {
|
|
457
|
+
validate();
|
|
458
|
+
},
|
|
459
|
+
/**
|
|
460
|
+
* Reject `data:` / `blob:` / `javascript:` URL specifiers at resolve time.
|
|
461
|
+
*
|
|
462
|
+
* Rollup normally externalises unknown URL schemes silently, which would
|
|
463
|
+
* mean a malicious `import("data:text/javascript,...")` survives the
|
|
464
|
+
* build untouched and only fails at runtime (where the bootstrap script's
|
|
465
|
+
* `safeImport` would catch it). We want build-time failure too — the
|
|
466
|
+
* import-map contract is enforced at BOTH ends.
|
|
467
|
+
*/
|
|
468
|
+
resolveId(source) {
|
|
469
|
+
if (workerEntryAbs === null) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
if (/^(data|blob|javascript):/i.test(source)) {
|
|
473
|
+
const scheme = source.split(":", 1)[0];
|
|
474
|
+
this.error(
|
|
475
|
+
`[ethisys-contract-b] Contract B bundle imports forbidden URL scheme '${scheme}:' (specifier: "${source.slice(0, 80)}${source.length > 80 ? "..." : ""}"). The worker realm rejects ${scheme}: imports at runtime; the build rejects them too.`
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
},
|
|
480
|
+
/**
|
|
481
|
+
* Rewrite the emitted worker chunk's static bare-specifier imports into
|
|
482
|
+
* top-level `await globalThis.__ethisysSafeImport(...)` calls. This is
|
|
483
|
+
* the build-time complement to the worker bootstrap's frozen IMPORT_MAP
|
|
484
|
+
* (defined in CoreConnect-Api's WorkerBootstrapScriptProvider.cs). The
|
|
485
|
+
* worker realm has no `<script type="importmap">` surface, so static
|
|
486
|
+
* bare specifiers reach the browser's native ES loader as
|
|
487
|
+
* `Failed to resolve module specifier` errors at module load time.
|
|
488
|
+
*
|
|
489
|
+
* Returns `null` (no transform) when the manifest is not Contract B —
|
|
490
|
+
* the Contract A path's emitted chunks are left untouched.
|
|
491
|
+
*/
|
|
492
|
+
renderChunk(code, chunk) {
|
|
493
|
+
if (workerEntryAbs === null) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
if (chunk.type !== "chunk") {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
const rewritten = rewriteSafeImports(code, CONTRACT_B_IMPORT_MAP_ALLOWLIST);
|
|
500
|
+
if (rewritten === code) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
return { code: rewritten };
|
|
504
|
+
},
|
|
505
|
+
/**
|
|
506
|
+
* Inspect the emitted bundle for out-of-allowlist bare specifiers and emit
|
|
507
|
+
* the per-bundle `worker-bundle.import-map.json` manifest.
|
|
508
|
+
*
|
|
509
|
+
* Because we marked allowlist specifiers as `external` in `config`, any
|
|
510
|
+
* other bare specifier that the plugin tried to import would have failed
|
|
511
|
+
* Rollup's resolver upstream — but we re-check the surviving `chunk.imports`
|
|
512
|
+
* here as belt-and-braces in case a future Rollup or Vite change relaxes
|
|
513
|
+
* the resolver path. Defence in depth at the build layer matches the
|
|
514
|
+
* defence in depth at the runtime layer (E4.S3 bootstrap).
|
|
515
|
+
*/
|
|
516
|
+
generateBundle(_outputOptions, bundle) {
|
|
517
|
+
if (workerEntryAbs === null || workerEntryRel === null) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const importedSpecifiers = /* @__PURE__ */ new Set();
|
|
521
|
+
const violations = [];
|
|
522
|
+
for (const [, chunk] of Object.entries(bundle)) {
|
|
523
|
+
if (chunk.type !== "chunk") {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
const candidates = [
|
|
527
|
+
...chunk.imports ?? [],
|
|
528
|
+
...chunk.dynamicImports ?? []
|
|
529
|
+
];
|
|
530
|
+
for (const spec of candidates) {
|
|
531
|
+
if (spec.startsWith(".") || spec.startsWith("/") || isAbsolute(spec)) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(spec)) {
|
|
535
|
+
violations.push(spec);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (!ALLOWLIST_SET.has(spec)) {
|
|
539
|
+
violations.push(spec);
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
importedSpecifiers.add(spec);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (violations.length > 0) {
|
|
546
|
+
const unique = [...new Set(violations)].sort();
|
|
547
|
+
this.error(
|
|
548
|
+
`[ethisys-contract-b] Worker bundle imports specifier(s) outside the frozen import-map allowlist: ${unique.map((s) => `"${s}"`).join(", ")}. Allowed bare specifiers: ${CONTRACT_B_IMPORT_MAP_ALLOWLIST.map((s) => `"${s}"`).join(", ")}.`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
const resolvedEntries = [...importedSpecifiers].sort();
|
|
552
|
+
const workerBundleManifest = {
|
|
553
|
+
version: 1,
|
|
554
|
+
renderMode: "remote-runtime",
|
|
555
|
+
worker: { entry: workerEntryRel }
|
|
556
|
+
};
|
|
557
|
+
this.emitFile({
|
|
558
|
+
type: "asset",
|
|
559
|
+
fileName: `${outputPrefix}worker-bundle.json`,
|
|
560
|
+
source: JSON.stringify(workerBundleManifest)
|
|
561
|
+
});
|
|
562
|
+
const importMapManifest = {
|
|
563
|
+
version: 1,
|
|
564
|
+
// Authoring contract: the bundle's bare-specifier surface area, in
|
|
565
|
+
// sorted order, restricted to the frozen allowlist.
|
|
566
|
+
imports: Object.fromEntries(resolvedEntries.map((spec) => [spec, null])),
|
|
567
|
+
// Mirror the canonical allowlist so consumers diffing manifests
|
|
568
|
+
// across plugin versions can see the contract the build verified
|
|
569
|
+
// against without re-reading SDK source.
|
|
570
|
+
allowlist: [...CONTRACT_B_IMPORT_MAP_ALLOWLIST]
|
|
571
|
+
};
|
|
572
|
+
this.emitFile({
|
|
573
|
+
type: "asset",
|
|
574
|
+
fileName: `${outputPrefix}worker-bundle.import-map.json`,
|
|
575
|
+
source: JSON.stringify(importMapManifest)
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/index.ts
|
|
582
|
+
function slash2(p) {
|
|
583
|
+
return sep === "\\" ? p.replace(/\\/g, "/") : p;
|
|
584
|
+
}
|
|
7
585
|
function escapeHtml(str) {
|
|
8
586
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
9
587
|
}
|
|
10
|
-
function
|
|
588
|
+
function assertSafeRelativePath2(value, label) {
|
|
11
589
|
const normalized = normalize(value);
|
|
12
590
|
if (isAbsolute(value) || isAbsolute(normalized)) {
|
|
13
591
|
throw new Error(
|
|
@@ -29,7 +607,7 @@ function ethisysManifestPlugin(options = {}) {
|
|
|
29
607
|
const htmlCache = /* @__PURE__ */ new Map();
|
|
30
608
|
function generateHtml(entry) {
|
|
31
609
|
if (entry.template) {
|
|
32
|
-
|
|
610
|
+
assertSafeRelativePath2(entry.template, "Template path");
|
|
33
611
|
const templatePath = resolve(rootDir, entry.template);
|
|
34
612
|
if (existsSync(templatePath)) {
|
|
35
613
|
let html = readFileSync(templatePath, "utf-8");
|
|
@@ -90,7 +668,7 @@ function ethisysManifestPlugin(options = {}) {
|
|
|
90
668
|
const entrypointSources = /* @__PURE__ */ new Map();
|
|
91
669
|
for (const entry of entries) {
|
|
92
670
|
const source = entry.source;
|
|
93
|
-
|
|
671
|
+
assertSafeRelativePath2(source, "Source path");
|
|
94
672
|
const sourcePath = resolve(rootDir, source);
|
|
95
673
|
if (!existsSync(sourcePath)) {
|
|
96
674
|
throw new Error(
|
|
@@ -126,7 +704,7 @@ function ethisysManifestPlugin(options = {}) {
|
|
|
126
704
|
}
|
|
127
705
|
seen.add(entry.entrypoint);
|
|
128
706
|
const name = entry.entrypoint.replace(/\.html$/, "");
|
|
129
|
-
input[name] =
|
|
707
|
+
input[name] = slash2(resolve(rootDir, entry.entrypoint));
|
|
130
708
|
}
|
|
131
709
|
return {
|
|
132
710
|
build: {
|
|
@@ -191,7 +769,7 @@ function ethisysManifestPlugin(options = {}) {
|
|
|
191
769
|
// Entire handler is wrapped in try/catch so JSON syntax errors in the
|
|
192
770
|
// manifest don't crash the dev server (common during active editing).
|
|
193
771
|
handleHotUpdate({ file, server }) {
|
|
194
|
-
if (
|
|
772
|
+
if (slash2(file) === slash2(manifestAbsPath)) {
|
|
195
773
|
try {
|
|
196
774
|
entries = readManifest();
|
|
197
775
|
validateEntries();
|
|
@@ -209,9 +787,9 @@ function ethisysManifestPlugin(options = {}) {
|
|
|
209
787
|
},
|
|
210
788
|
// Build: resolve virtual HTML module IDs (no physical files exist on disk)
|
|
211
789
|
resolveId(id) {
|
|
212
|
-
const normalizedId =
|
|
790
|
+
const normalizedId = slash2(id);
|
|
213
791
|
const match = entries.find(
|
|
214
|
-
(e) =>
|
|
792
|
+
(e) => slash2(resolve(rootDir, e.entrypoint)) === normalizedId
|
|
215
793
|
);
|
|
216
794
|
if (match) {
|
|
217
795
|
return id;
|
|
@@ -219,9 +797,9 @@ function ethisysManifestPlugin(options = {}) {
|
|
|
219
797
|
},
|
|
220
798
|
// Build: provide virtual HTML content for Rollup
|
|
221
799
|
load(id) {
|
|
222
|
-
const normalizedId =
|
|
800
|
+
const normalizedId = slash2(id);
|
|
223
801
|
const match = entries.find(
|
|
224
|
-
(e) =>
|
|
802
|
+
(e) => slash2(resolve(rootDir, e.entrypoint)) === normalizedId
|
|
225
803
|
);
|
|
226
804
|
if (match) {
|
|
227
805
|
return generateHtml(match);
|
|
@@ -229,6 +807,7 @@ function ethisysManifestPlugin(options = {}) {
|
|
|
229
807
|
}
|
|
230
808
|
};
|
|
231
809
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
810
|
+
|
|
811
|
+
export { CONTRACT_B_IMPORT_MAP_ALLOWLIST, CONTRACT_B_RUNTIME_IMPORTS, CONTRACT_B_SEMANTIC_PRIMITIVES, ethisysContractAPlugin, ethisysContractBPlugin, ethisysManifestPlugin, validateDeclarativeResource, validateReactiveRule };
|
|
812
|
+
//# sourceMappingURL=index.js.map
|
|
813
|
+
//# sourceMappingURL=index.js.map
|