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