browser-module-runtime 0.0.0-stage → 0.0.2
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 +65 -2
- package/html.d.ts +4 -0
- package/index.d.ts +107 -0
- package/package.json +40 -4
- package/src/build/analyze.js +54 -0
- package/src/build/pipeline.js +62 -0
- package/src/engine/compile-cache.js +379 -0
- package/src/engine/errors.js +41 -0
- package/src/engine/events.js +63 -0
- package/src/engine/package-resolution/index.js +179 -0
- package/src/engine/runtime.js +2044 -0
- package/src/engine/url-backend.js +40 -0
- package/src/html/index.d.ts +1 -0
- package/src/html/index.js +74 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +99 -0
- package/src/plugins/package-entries.js +30 -0
- package/src/resources/styles.js +18 -0
- package/src/web-components/index.d.ts +11 -0
- package/src/web-components/index.js +145 -0
- package/web-components.d.ts +1 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
const DEFAULT_LOCAL_STORAGE_PREFIX = "browser-module-runtime:compile-cache:";
|
|
2
|
+
const DEFAULT_IDB_DATABASE = "browser-module-runtime-compile-cache";
|
|
3
|
+
const DEFAULT_IDB_STORE = "artifacts";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export function createCompileCache(options = {}) {
|
|
7
|
+
if (options == null) options = {};
|
|
8
|
+
if (typeof options !== "object" || Array.isArray(options)) {
|
|
9
|
+
throw new TypeError("createCompileCache(options) expects an options object.");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const storage = String(options.storage ?? "indexeddb").toLowerCase();
|
|
13
|
+
let store;
|
|
14
|
+
if (storage === "indexeddb") {
|
|
15
|
+
store = createIndexedDBCompileCache({
|
|
16
|
+
...(options.database == null ? {} : { database: options.database }),
|
|
17
|
+
...(options.storeName == null ? {} : { storeName: options.storeName }),
|
|
18
|
+
...(options.databaseVersion == null ? {} : { databaseVersion: options.databaseVersion }),
|
|
19
|
+
...(options.indexedDB == null ? {} : { indexedDB: options.indexedDB }),
|
|
20
|
+
});
|
|
21
|
+
} else if (storage === "localstorage") {
|
|
22
|
+
store = createLocalStorageCompileCache({
|
|
23
|
+
...(options.prefix == null ? {} : { prefix: options.prefix }),
|
|
24
|
+
...(options.localStorage == null ? {} : { storage: options.localStorage }),
|
|
25
|
+
});
|
|
26
|
+
} else if (storage === "memory") {
|
|
27
|
+
store = createMemoryCompileCache({
|
|
28
|
+
...(options.records == null ? {} : { records: options.records }),
|
|
29
|
+
});
|
|
30
|
+
} else {
|
|
31
|
+
throw new TypeError(`Unknown compile cache storage ${JSON.stringify(options.storage)}. Expected "indexeddb", "localstorage", or "memory".`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
store,
|
|
36
|
+
namespace: String(options.name ?? options.namespace ?? "default"),
|
|
37
|
+
version: String(options.version ?? "1"),
|
|
38
|
+
...(options.maxEntries == null ? {} : { maxEntries: options.maxEntries }),
|
|
39
|
+
...(options.maxBytes == null ? {} : { maxBytes: options.maxBytes }),
|
|
40
|
+
...(options.maxAge == null ? {} : { maxAge: options.maxAge }),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createMemoryCompileCache(options = {}) {
|
|
45
|
+
const records = options.records instanceof Map ? options.records : new Map();
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
kind: "memory",
|
|
49
|
+
async get(key) {
|
|
50
|
+
return records.get(String(key));
|
|
51
|
+
},
|
|
52
|
+
async set(key, value) {
|
|
53
|
+
records.set(String(key), cloneValue(value));
|
|
54
|
+
},
|
|
55
|
+
async delete(key) {
|
|
56
|
+
return records.delete(String(key));
|
|
57
|
+
},
|
|
58
|
+
async clear(options = {}) {
|
|
59
|
+
const prefix = options.prefix == null ? null : String(options.prefix);
|
|
60
|
+
if (prefix == null) {
|
|
61
|
+
const count = records.size;
|
|
62
|
+
records.clear();
|
|
63
|
+
return count;
|
|
64
|
+
}
|
|
65
|
+
let count = 0;
|
|
66
|
+
for (const key of [...records.keys()]) {
|
|
67
|
+
if (key.startsWith(prefix)) {
|
|
68
|
+
records.delete(key);
|
|
69
|
+
count += 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return count;
|
|
73
|
+
},
|
|
74
|
+
async entries(options = {}) {
|
|
75
|
+
const prefix = options.prefix == null ? null : String(options.prefix);
|
|
76
|
+
const result = [];
|
|
77
|
+
for (const [key, value] of records) {
|
|
78
|
+
if (prefix == null || key.startsWith(prefix)) result.push([key, cloneValue(value)]);
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createLocalStorageCompileCache(options = {}) {
|
|
86
|
+
const prefix = String(options.prefix ?? DEFAULT_LOCAL_STORAGE_PREFIX);
|
|
87
|
+
const configuredStorage = options.storage;
|
|
88
|
+
|
|
89
|
+
function assertStorage() {
|
|
90
|
+
let storage = configuredStorage;
|
|
91
|
+
if (!storage) {
|
|
92
|
+
try { storage = globalThis.localStorage; } catch (error) { throw error; }
|
|
93
|
+
}
|
|
94
|
+
if (!storage || typeof storage.getItem !== "function") {
|
|
95
|
+
throw new Error("localStorage is unavailable in this environment.");
|
|
96
|
+
}
|
|
97
|
+
return storage;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const physical = key => `${prefix}${String(key)}`;
|
|
101
|
+
const logical = key => String(key).slice(prefix.length);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
kind: "localstorage",
|
|
105
|
+
async get(key) {
|
|
106
|
+
const raw = assertStorage().getItem(physical(key));
|
|
107
|
+
return raw == null ? undefined : JSON.parse(raw);
|
|
108
|
+
},
|
|
109
|
+
async set(key, value) {
|
|
110
|
+
assertStorage().setItem(physical(key), JSON.stringify(value));
|
|
111
|
+
},
|
|
112
|
+
async delete(key) {
|
|
113
|
+
const target = physical(key);
|
|
114
|
+
const store = assertStorage();
|
|
115
|
+
const existed = store.getItem(target) != null;
|
|
116
|
+
store.removeItem(target);
|
|
117
|
+
return existed;
|
|
118
|
+
},
|
|
119
|
+
async clear(options = {}) {
|
|
120
|
+
const store = assertStorage();
|
|
121
|
+
const logicalPrefix = options.prefix == null ? "" : String(options.prefix);
|
|
122
|
+
const targetPrefix = physical(logicalPrefix);
|
|
123
|
+
const keys = [];
|
|
124
|
+
for (let index = 0; index < store.length; index += 1) {
|
|
125
|
+
const key = store.key(index);
|
|
126
|
+
if (key != null && key.startsWith(targetPrefix)) keys.push(key);
|
|
127
|
+
}
|
|
128
|
+
for (const key of keys) store.removeItem(key);
|
|
129
|
+
return keys.length;
|
|
130
|
+
},
|
|
131
|
+
async entries(options = {}) {
|
|
132
|
+
const store = assertStorage();
|
|
133
|
+
const logicalPrefix = options.prefix == null ? "" : String(options.prefix);
|
|
134
|
+
const targetPrefix = physical(logicalPrefix);
|
|
135
|
+
const result = [];
|
|
136
|
+
for (let index = 0; index < store.length; index += 1) {
|
|
137
|
+
const key = store.key(index);
|
|
138
|
+
if (key == null || !key.startsWith(targetPrefix)) continue;
|
|
139
|
+
const raw = store.getItem(key);
|
|
140
|
+
if (raw == null) continue;
|
|
141
|
+
result.push([logical(key), JSON.parse(raw)]);
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function createIndexedDBCompileCache(options = {}) {
|
|
149
|
+
const database = String(options.database ?? DEFAULT_IDB_DATABASE);
|
|
150
|
+
const storeName = String(options.storeName ?? DEFAULT_IDB_STORE);
|
|
151
|
+
const databaseVersion = Number(options.databaseVersion ?? 1);
|
|
152
|
+
const indexedDBFactory = options.indexedDB ?? globalThis.indexedDB;
|
|
153
|
+
let dbPromise;
|
|
154
|
+
|
|
155
|
+
function openDatabase() {
|
|
156
|
+
if (dbPromise) return dbPromise;
|
|
157
|
+
if (!indexedDBFactory || typeof indexedDBFactory.open !== "function") {
|
|
158
|
+
return Promise.reject(new Error("IndexedDB is unavailable in this environment."));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
dbPromise = new Promise((resolve, reject) => {
|
|
162
|
+
const request = indexedDBFactory.open(database, databaseVersion);
|
|
163
|
+
request.onupgradeneeded = () => {
|
|
164
|
+
const db = request.result;
|
|
165
|
+
if (!db.objectStoreNames.contains(storeName)) db.createObjectStore(storeName);
|
|
166
|
+
};
|
|
167
|
+
request.onsuccess = () => resolve(request.result);
|
|
168
|
+
request.onerror = () => {
|
|
169
|
+
dbPromise = undefined;
|
|
170
|
+
reject(request.error ?? new Error(`Failed to open IndexedDB database ${database}.`));
|
|
171
|
+
};
|
|
172
|
+
request.onblocked = () => {
|
|
173
|
+
dbPromise = undefined;
|
|
174
|
+
reject(new Error(`Opening IndexedDB database ${database} was blocked.`));
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return dbPromise;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
kind: "indexeddb",
|
|
185
|
+
async get(key) {
|
|
186
|
+
const db = await openDatabase();
|
|
187
|
+
return new Promise((resolve, reject) => {
|
|
188
|
+
const tx = db.transaction(storeName, "readonly");
|
|
189
|
+
const request = tx.objectStore(storeName).get(String(key));
|
|
190
|
+
request.onsuccess = () => resolve(request.result);
|
|
191
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB cache read failed."));
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
async set(key, value) {
|
|
195
|
+
const db = await openDatabase();
|
|
196
|
+
return new Promise((resolve, reject) => {
|
|
197
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
198
|
+
tx.objectStore(storeName).put(value, String(key));
|
|
199
|
+
tx.oncomplete = () => resolve();
|
|
200
|
+
tx.onerror = () => reject(tx.error ?? new Error("IndexedDB cache write failed."));
|
|
201
|
+
tx.onabort = () => reject(tx.error ?? new Error("IndexedDB cache write was aborted."));
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
async delete(key) {
|
|
205
|
+
const db = await openDatabase();
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
208
|
+
const store = tx.objectStore(storeName);
|
|
209
|
+
const lookup = store.getKey(String(key));
|
|
210
|
+
let existed = false;
|
|
211
|
+
lookup.onsuccess = () => {
|
|
212
|
+
existed = lookup.result !== undefined;
|
|
213
|
+
store.delete(String(key));
|
|
214
|
+
};
|
|
215
|
+
lookup.onerror = () => reject(lookup.error ?? new Error("IndexedDB cache delete lookup failed."));
|
|
216
|
+
tx.oncomplete = () => resolve(existed);
|
|
217
|
+
tx.onerror = () => reject(tx.error ?? new Error("IndexedDB cache delete failed."));
|
|
218
|
+
tx.onabort = () => reject(tx.error ?? new Error("IndexedDB cache delete was aborted."));
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
async clear(options = {}) {
|
|
222
|
+
const prefix = options.prefix == null ? null : String(options.prefix);
|
|
223
|
+
const db = await openDatabase();
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
226
|
+
const store = tx.objectStore(storeName);
|
|
227
|
+
if (prefix == null || prefix === "") {
|
|
228
|
+
const countRequest = store.count();
|
|
229
|
+
let count = 0;
|
|
230
|
+
countRequest.onsuccess = () => { count = countRequest.result; };
|
|
231
|
+
store.clear();
|
|
232
|
+
tx.oncomplete = () => resolve(count);
|
|
233
|
+
tx.onerror = () => reject(tx.error ?? new Error("IndexedDB cache clear failed."));
|
|
234
|
+
tx.onabort = () => reject(tx.error ?? new Error("IndexedDB cache clear was aborted."));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let count = 0;
|
|
239
|
+
const request = store.openCursor();
|
|
240
|
+
request.onsuccess = () => {
|
|
241
|
+
const cursor = request.result;
|
|
242
|
+
if (!cursor) return;
|
|
243
|
+
if (String(cursor.key).startsWith(prefix)) {
|
|
244
|
+
cursor.delete();
|
|
245
|
+
count += 1;
|
|
246
|
+
}
|
|
247
|
+
cursor.continue();
|
|
248
|
+
};
|
|
249
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB cache clear cursor failed."));
|
|
250
|
+
tx.oncomplete = () => resolve(count);
|
|
251
|
+
tx.onerror = () => reject(tx.error ?? new Error("IndexedDB cache clear failed."));
|
|
252
|
+
tx.onabort = () => reject(tx.error ?? new Error("IndexedDB cache clear was aborted."));
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
async entries(options = {}) {
|
|
256
|
+
const prefix = options.prefix == null ? null : String(options.prefix);
|
|
257
|
+
const db = await openDatabase();
|
|
258
|
+
return new Promise((resolve, reject) => {
|
|
259
|
+
const tx = db.transaction(storeName, "readonly");
|
|
260
|
+
const request = tx.objectStore(storeName).openCursor();
|
|
261
|
+
const result = [];
|
|
262
|
+
request.onsuccess = () => {
|
|
263
|
+
const cursor = request.result;
|
|
264
|
+
if (!cursor) return;
|
|
265
|
+
const key = String(cursor.key);
|
|
266
|
+
if (prefix == null || key.startsWith(prefix)) result.push([key, cursor.value]);
|
|
267
|
+
cursor.continue();
|
|
268
|
+
};
|
|
269
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB cache entries cursor failed."));
|
|
270
|
+
tx.oncomplete = () => resolve(result);
|
|
271
|
+
tx.onerror = () => reject(tx.error ?? new Error("IndexedDB cache entries transaction failed."));
|
|
272
|
+
});
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function sha256(value) {
|
|
278
|
+
const bytes = new TextEncoder().encode(String(value));
|
|
279
|
+
const subtle = globalThis.crypto?.subtle;
|
|
280
|
+
if (subtle) {
|
|
281
|
+
const digest = await subtle.digest("SHA-256", bytes);
|
|
282
|
+
return toHex(new Uint8Array(digest));
|
|
283
|
+
}
|
|
284
|
+
return sha256Fallback(bytes);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function stableSerialize(value) {
|
|
288
|
+
return JSON.stringify(normalize(value));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function approximateByteSize(value) {
|
|
292
|
+
try {
|
|
293
|
+
return new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
294
|
+
} catch {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function normalize(value) {
|
|
300
|
+
if (value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
301
|
+
if (Array.isArray(value)) return value.map(normalize);
|
|
302
|
+
if (typeof value === "object") {
|
|
303
|
+
const result = {};
|
|
304
|
+
for (const key of Object.keys(value).sort()) {
|
|
305
|
+
const item = value[key];
|
|
306
|
+
if (item === undefined) continue;
|
|
307
|
+
result[key] = normalize(item);
|
|
308
|
+
}
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
return String(value);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function toHex(bytes) {
|
|
315
|
+
return [...bytes].map(byte => byte.toString(16).padStart(2, "0")).join("");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function sha256Fallback(bytes) {
|
|
319
|
+
const K = [
|
|
320
|
+
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
|
|
321
|
+
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
|
|
322
|
+
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
|
|
323
|
+
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
|
|
324
|
+
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
|
|
325
|
+
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
|
|
326
|
+
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
|
|
327
|
+
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2,
|
|
328
|
+
];
|
|
329
|
+
const H = [0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19];
|
|
330
|
+
const bitLength = bytes.length * 8;
|
|
331
|
+
const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64;
|
|
332
|
+
const data = new Uint8Array(paddedLength);
|
|
333
|
+
data.set(bytes);
|
|
334
|
+
data[bytes.length] = 0x80;
|
|
335
|
+
const view = new DataView(data.buffer);
|
|
336
|
+
const high = Math.floor(bitLength / 0x100000000);
|
|
337
|
+
const low = bitLength >>> 0;
|
|
338
|
+
view.setUint32(paddedLength - 8, high, false);
|
|
339
|
+
view.setUint32(paddedLength - 4, low, false);
|
|
340
|
+
const w = new Uint32Array(64);
|
|
341
|
+
|
|
342
|
+
for (let offset = 0; offset < paddedLength; offset += 64) {
|
|
343
|
+
for (let i = 0; i < 16; i += 1) w[i] = view.getUint32(offset + i * 4, false);
|
|
344
|
+
for (let i = 16; i < 64; i += 1) {
|
|
345
|
+
const a = w[i - 15];
|
|
346
|
+
const b = w[i - 2];
|
|
347
|
+
const s0 = rotr(a, 7) ^ rotr(a, 18) ^ (a >>> 3);
|
|
348
|
+
const s1 = rotr(b, 17) ^ rotr(b, 19) ^ (b >>> 10);
|
|
349
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
|
350
|
+
}
|
|
351
|
+
let [a,b,c,d,e,f,g,h] = H;
|
|
352
|
+
for (let i = 0; i < 64; i += 1) {
|
|
353
|
+
const S1 = rotr(e,6) ^ rotr(e,11) ^ rotr(e,25);
|
|
354
|
+
const ch = (e & f) ^ (~e & g);
|
|
355
|
+
const t1 = (h + S1 + ch + K[i] + w[i]) >>> 0;
|
|
356
|
+
const S0 = rotr(a,2) ^ rotr(a,13) ^ rotr(a,22);
|
|
357
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
358
|
+
const t2 = (S0 + maj) >>> 0;
|
|
359
|
+
h=g; g=f; f=e; e=(d+t1)>>>0; d=c; c=b; b=a; a=(t1+t2)>>>0;
|
|
360
|
+
}
|
|
361
|
+
H[0]=(H[0]+a)>>>0; H[1]=(H[1]+b)>>>0; H[2]=(H[2]+c)>>>0; H[3]=(H[3]+d)>>>0;
|
|
362
|
+
H[4]=(H[4]+e)>>>0; H[5]=(H[5]+f)>>>0; H[6]=(H[6]+g)>>>0; H[7]=(H[7]+h)>>>0;
|
|
363
|
+
}
|
|
364
|
+
const output = new Uint8Array(32);
|
|
365
|
+
const out = new DataView(output.buffer);
|
|
366
|
+
H.forEach((value, index) => out.setUint32(index * 4, value, false));
|
|
367
|
+
return toHex(output);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function rotr(value, bits) {
|
|
371
|
+
return (value >>> bits) | (value << (32 - bits));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function cloneValue(value) {
|
|
375
|
+
if (typeof structuredClone === "function") {
|
|
376
|
+
try { return structuredClone(value); } catch {}
|
|
377
|
+
}
|
|
378
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
379
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export class ModuleRuntimeError extends Error {
|
|
2
|
+
constructor(message, options = {}) {
|
|
3
|
+
super(message, options);
|
|
4
|
+
this.name = "ModuleRuntimeError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class ModuleResolutionError extends ModuleRuntimeError {
|
|
9
|
+
constructor(specifier, importer, message) {
|
|
10
|
+
super(message ?? `Could not resolve module ${JSON.stringify(specifier)} from ${JSON.stringify(importer ?? "<runtime>")}.`);
|
|
11
|
+
this.name = "ModuleResolutionError";
|
|
12
|
+
this.specifier = specifier;
|
|
13
|
+
this.importer = importer ?? null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class CircularDependencyError extends ModuleRuntimeError {
|
|
18
|
+
constructor(cycle) {
|
|
19
|
+
super(`Circular virtual module dependency is not supported yet: ${cycle.join(" -> ")}`);
|
|
20
|
+
this.name = "CircularDependencyError";
|
|
21
|
+
this.cycle = [...cycle];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ModuleCompileError extends ModuleRuntimeError {
|
|
26
|
+
constructor(id, cause) {
|
|
27
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
28
|
+
super(`Failed to compile module ${JSON.stringify(id)}: ${detail}`, { cause });
|
|
29
|
+
this.name = "ModuleCompileError";
|
|
30
|
+
this.id = id;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class ModuleWaitTimeoutError extends ModuleRuntimeError {
|
|
35
|
+
constructor(moduleId, timeout) {
|
|
36
|
+
super(`Timed out after ${timeout}ms waiting for runtime module ${JSON.stringify(moduleId)}.`);
|
|
37
|
+
this.name = "ModuleWaitTimeoutError";
|
|
38
|
+
this.moduleId = moduleId;
|
|
39
|
+
this.timeout = timeout;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export function createEventDispatcher(options = {}) {
|
|
2
|
+
const subscriptions = new Set();
|
|
3
|
+
const onListenerError = options.onListenerError;
|
|
4
|
+
|
|
5
|
+
return {
|
|
6
|
+
subscribe,
|
|
7
|
+
emit,
|
|
8
|
+
clear,
|
|
9
|
+
get size() {
|
|
10
|
+
return subscriptions.size;
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function subscribe(typesOrListener, maybeListener) {
|
|
15
|
+
let types;
|
|
16
|
+
let listener;
|
|
17
|
+
|
|
18
|
+
if (typeof typesOrListener === "function") {
|
|
19
|
+
listener = typesOrListener;
|
|
20
|
+
types = undefined;
|
|
21
|
+
} else {
|
|
22
|
+
listener = maybeListener;
|
|
23
|
+
if (typeof typesOrListener === "string") {
|
|
24
|
+
types = new Set([typesOrListener]);
|
|
25
|
+
} else if (Array.isArray(typesOrListener)) {
|
|
26
|
+
if (typesOrListener.length === 0 || typesOrListener.some(type => typeof type !== "string" || !type)) {
|
|
27
|
+
throw new TypeError("subscribe(types, listener) expects a non-empty event type or event type array.");
|
|
28
|
+
}
|
|
29
|
+
types = new Set(typesOrListener);
|
|
30
|
+
} else {
|
|
31
|
+
throw new TypeError("subscribe() expects a listener, event type + listener, or event type array + listener.");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof listener !== "function") {
|
|
36
|
+
throw new TypeError("subscribe() expects a listener function.");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const subscription = { types, listener };
|
|
40
|
+
subscriptions.add(subscription);
|
|
41
|
+
|
|
42
|
+
return () => subscriptions.delete(subscription);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function emit(event) {
|
|
46
|
+
for (const subscription of [...subscriptions]) {
|
|
47
|
+
if (subscription.types && !subscription.types.has(event.type)) continue;
|
|
48
|
+
try {
|
|
49
|
+
subscription.listener(event);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
try {
|
|
52
|
+
onListenerError?.(error, event);
|
|
53
|
+
} catch {
|
|
54
|
+
// Listener error reporting must never break the emitter.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function clear() {
|
|
61
|
+
subscriptions.clear();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/** Pure, per-runtime package resolution. No import-map mutation or network access. */
|
|
2
|
+
export class PackageVersionResolutionError extends Error {
|
|
3
|
+
constructor(code, message, details = {}) {
|
|
4
|
+
super(`${code}: ${message}`);
|
|
5
|
+
this.name = "PackageVersionResolutionError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
Object.assign(this, details);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const OWN_NAME = /^[a-z0-9][a-z0-9._~-]*$/;
|
|
12
|
+
const VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
13
|
+
const SAFE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
14
|
+
const error = (code, message, details) => new PackageVersionResolutionError(code, message, details);
|
|
15
|
+
|
|
16
|
+
export function parsePackageSpecifier(specifier) {
|
|
17
|
+
if (typeof specifier !== "string" || !specifier || /[\\%?#\s\x00-\x1f]/.test(specifier)) return undefined;
|
|
18
|
+
if (/^(?:\.|\/|#|[a-z][a-z\d+.-]*:)/i.test(specifier)) return undefined;
|
|
19
|
+
const segments = specifier.split("/");
|
|
20
|
+
const scoped = segments[0]?.startsWith("@");
|
|
21
|
+
const count = scoped ? 2 : 1;
|
|
22
|
+
const packageParts = segments.slice(0, count);
|
|
23
|
+
if (packageParts.length !== count || packageParts.some(part => !part)) return undefined;
|
|
24
|
+
if (scoped) {
|
|
25
|
+
if (!OWN_NAME.test(packageParts[0].slice(1)) || !OWN_NAME.test(packageParts[1])) return undefined;
|
|
26
|
+
} else if (!OWN_NAME.test(packageParts[0])) return undefined;
|
|
27
|
+
const subpathParts = segments.slice(count);
|
|
28
|
+
if (subpathParts.some(part => !part || part === "." || part === ".." || part.includes(".."))) return undefined;
|
|
29
|
+
return { packageName: packageParts.join("/"), subpath: subpathParts.length ? `/${subpathParts.join("/")}` : "" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function validateExactVersion(version, packageName = "package") {
|
|
33
|
+
if (typeof version !== "string" || !VERSION.test(version)) {
|
|
34
|
+
throw error("PACKAGE_VERSION_INVALID", `Expected an exact semver version for ${JSON.stringify(packageName)}, received ${JSON.stringify(version)}. Tags and ranges are not supported.`, { packageName, version });
|
|
35
|
+
}
|
|
36
|
+
return version;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizePackageProvider(value) {
|
|
40
|
+
if (value === "esm.sh" || value === "esm") return "esm.sh";
|
|
41
|
+
if (value === "jsdelivr" || value === "jsDelivr") return "jsdelivr";
|
|
42
|
+
throw error("PACKAGE_PROVIDER_UNSUPPORTED", `Unknown package provider ${JSON.stringify(value)}. Choose "esm.sh" or "jsdelivr".`, { provider: value });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function safeToken(token) {
|
|
46
|
+
return typeof token === "string" && SAFE_SEGMENT.test(token) && token !== "." && token !== ".." && !token.includes("..");
|
|
47
|
+
}
|
|
48
|
+
function validateGithub(repository, ref, name, value) {
|
|
49
|
+
const parts = typeof repository === "string" ? repository.split("/") : [];
|
|
50
|
+
if (parts.length !== 2 || !parts.every(safeToken) || !safeToken(ref)) {
|
|
51
|
+
throw error("PACKAGE_VALUE_INVALID", `Invalid GitHub source for ${JSON.stringify(name)}. Use gh/owner/repo@tag-or-commit (no URL query, fragment or path traversal).`, { packageName: name, source: "github", value });
|
|
52
|
+
}
|
|
53
|
+
return { repository, ref };
|
|
54
|
+
}
|
|
55
|
+
function validatePreview(locator, ref, name, value) {
|
|
56
|
+
const parts = typeof locator === "string" ? locator.split("/") : [];
|
|
57
|
+
if (!(parts.length === 1 || parts.length >= 3) || !parts.every(safeToken) || !safeToken(ref)) {
|
|
58
|
+
throw error("PACKAGE_VALUE_INVALID", `Invalid pkg.pr.new locator for ${JSON.stringify(name)}. Use pr/package@build or pr/owner/repo/package@build.`, { packageName: name, source: "pkg.pr.new", value });
|
|
59
|
+
}
|
|
60
|
+
return { locator, ref };
|
|
61
|
+
}
|
|
62
|
+
function sourceFromCompact(raw, provider, name) {
|
|
63
|
+
const github = raw.startsWith("gh/");
|
|
64
|
+
const preview = raw.startsWith("pr/");
|
|
65
|
+
if (!github && !preview) {
|
|
66
|
+
throw error("PACKAGE_VALUE_INVALID", `Unsupported package source value ${JSON.stringify(raw)} for ${JSON.stringify(name)}. Use an exact npm version, gh/owner/repo@ref, or pr/package@build.`, { packageName: name, value: raw });
|
|
67
|
+
}
|
|
68
|
+
if (provider !== "esm.sh") throw error("PACKAGE_SOURCE_UNSUPPORTED", `${provider} does not support ${github ? "GitHub" : "pkg.pr.new"} source imports. Select esm.sh.`, { provider, source: github ? "github" : "pkg.pr.new", packageName: name, value: raw });
|
|
69
|
+
const at = raw.lastIndexOf("@");
|
|
70
|
+
if (at < 0) throw error("PACKAGE_VALUE_INVALID", `Missing source ref/build identifier in ${JSON.stringify(raw)}.`, { packageName: name, value: raw });
|
|
71
|
+
const path = raw.slice(github ? 3 : 3, at);
|
|
72
|
+
const ref = raw.slice(at + 1);
|
|
73
|
+
return github ? { provider, source: "github", ...validateGithub(path, ref, name, raw), subpaths: false }
|
|
74
|
+
: { provider, source: "pkg.pr.new", ...validatePreview(path, ref, name, raw), subpaths: false };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Parse short strings before matching any import specifier, never while importing. */
|
|
78
|
+
export function parsePackageVersionValue(value, name, defaultProvider) {
|
|
79
|
+
if (typeof value !== "string" || !value) throw error("PACKAGE_VALUE_INVALID", `Expected a nonempty package version string for ${JSON.stringify(name)}.`, { packageName: name, value });
|
|
80
|
+
if (VERSION.test(value)) {
|
|
81
|
+
if (!defaultProvider) throw error("PACKAGE_PROVIDER_REQUIRED", `packages.provider is required for unprefixed version ${JSON.stringify(name)} (or specify esm.sh: / jsdelivr:).`, { packageName: name });
|
|
82
|
+
return { provider: defaultProvider, source: "npm", version: value, subpaths: true };
|
|
83
|
+
}
|
|
84
|
+
const colon = value.indexOf(":");
|
|
85
|
+
if (colon < 0) return { provider: defaultProvider, source: "npm", version: validateExactVersion(value, name), subpaths: true };
|
|
86
|
+
const provider = normalizePackageProvider(value.slice(0, colon));
|
|
87
|
+
const source = value.slice(colon + 1);
|
|
88
|
+
if (VERSION.test(source)) return { provider, source: "npm", version: source, subpaths: true };
|
|
89
|
+
return sourceFromCompact(source, provider, name);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeObject(value, name, defaultProvider) {
|
|
93
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw error("PACKAGE_VALUE_INVALID", `Package entry ${JSON.stringify(name)} must be an object.`, { packageName: name, value });
|
|
94
|
+
if (Object.prototype.hasOwnProperty.call(value, "url")) {
|
|
95
|
+
if (typeof value.url !== "string") throw error("PACKAGE_VALUE_INVALID", `URL entry ${JSON.stringify(name)} needs an HTTPS URL.`, { packageName: name });
|
|
96
|
+
let parsed;
|
|
97
|
+
try { parsed = new URL(value.url); } catch { /* below */ }
|
|
98
|
+
if (!parsed || parsed.protocol !== "https:" || parsed.username || parsed.password || value.subpaths === true || value.source || value.version || value.provider) {
|
|
99
|
+
throw error("PACKAGE_VALUE_INVALID", `Custom URL ${JSON.stringify(name)} must be HTTPS, credential-free and root-only.`, { packageName: name });
|
|
100
|
+
}
|
|
101
|
+
return { source: "url", url: parsed.href, subpaths: false };
|
|
102
|
+
}
|
|
103
|
+
const provider = value.provider == null ? defaultProvider : normalizePackageProvider(value.provider);
|
|
104
|
+
const source = value.source ?? "npm";
|
|
105
|
+
if (source === "npm") {
|
|
106
|
+
if (!provider) throw error("PACKAGE_PROVIDER_REQUIRED", `Package ${JSON.stringify(name)} needs a provider.`, { packageName: name });
|
|
107
|
+
return { provider, source, version: validateExactVersion(value.version, name), subpaths: value.subpaths !== false };
|
|
108
|
+
}
|
|
109
|
+
if (source !== "github" && source !== "pkg.pr.new") {
|
|
110
|
+
throw error("PACKAGE_SOURCE_UNSUPPORTED", `Unsupported package source ${JSON.stringify(source)} for ${JSON.stringify(name)}.`, { packageName: name, source, provider });
|
|
111
|
+
}
|
|
112
|
+
if (!provider) throw error("PACKAGE_PROVIDER_REQUIRED", `Package ${JSON.stringify(name)} needs a provider.`, { packageName: name });
|
|
113
|
+
if (provider !== "esm.sh") throw error("PACKAGE_SOURCE_UNSUPPORTED", `${provider} does not support ${source}.`, { provider, source, packageName: name });
|
|
114
|
+
if (source === "github") return { provider, source, ...validateGithub(value.repository, value.ref, name, value), subpaths: value.subpaths === true };
|
|
115
|
+
if (value.subpaths === true) throw error("PACKAGE_SUBPATH_UNSUPPORTED", `pkg.pr.new subpaths are not verified for ${JSON.stringify(name)}.`, { packageName: name, provider, source });
|
|
116
|
+
return { provider, source, ...validatePreview(value.package, value.ref, name, value), subpaths: false };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function normalizePackageOptions(options = {}, { inheritedProvider } = {}) {
|
|
120
|
+
if (options == null) options = {};
|
|
121
|
+
if (typeof options !== "object" || Array.isArray(options)) throw error("PACKAGE_NAME_INVALID", "packages must be an object.");
|
|
122
|
+
const versions = options.versions ?? {};
|
|
123
|
+
const entries = options.entries ?? {};
|
|
124
|
+
for (const [field, value] of [["versions", versions], ["entries", entries]]) {
|
|
125
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw error("PACKAGE_NAME_INVALID", `packages.${field} must be an object of package roots.`);
|
|
126
|
+
}
|
|
127
|
+
const provider = options.provider == null ? (inheritedProvider ? normalizePackageProvider(inheritedProvider) : undefined) : normalizePackageProvider(options.provider);
|
|
128
|
+
const pins = new Map();
|
|
129
|
+
for (const [name, value] of [...Object.entries(versions), ...Object.entries(entries)]) {
|
|
130
|
+
const parsed = parsePackageSpecifier(name);
|
|
131
|
+
if (!parsed || parsed.packageName !== name || parsed.subpath) throw error("PACKAGE_NAME_INVALID", `Package key ${JSON.stringify(name)} must be an exact npm-style package root (no trailing slash or subpath).`, { packageName: name });
|
|
132
|
+
if (pins.has(name)) throw error("PACKAGE_ENTRY_DUPLICATE", `Package ${JSON.stringify(name)} appears in both versions and entries. Use only one declaration.`, { packageName: name });
|
|
133
|
+
const normalized = Object.prototype.hasOwnProperty.call(versions, name)
|
|
134
|
+
? parsePackageVersionValue(value, name, provider)
|
|
135
|
+
: normalizeObject(value, name, provider);
|
|
136
|
+
pins.set(name, Object.freeze({ importName: name, ...normalized }));
|
|
137
|
+
}
|
|
138
|
+
return { provider, pins };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Provider-specific URL builders; importer matching is independent of their shape. */
|
|
142
|
+
export function buildPackageUrl(entry, subpath = "") {
|
|
143
|
+
const { provider, source, importName: packageName } = entry;
|
|
144
|
+
if (subpath && !entry.subpaths) throw error("PACKAGE_SUBPATH_UNSUPPORTED", `Subpath ${JSON.stringify(packageName + subpath)} is not supported for ${source} through ${provider ?? "custom URL"}.`, { provider, source, packageName, subpath });
|
|
145
|
+
if (source === "url") return entry.url;
|
|
146
|
+
if (source === "npm") {
|
|
147
|
+
if (provider === "esm.sh") return `https://esm.sh/${packageName}@${entry.version}${subpath}`;
|
|
148
|
+
if (subpath) throw error("PACKAGE_SUBPATH_UNSUPPORTED", `jsDelivr +esm subpath resolution is not verified for ${JSON.stringify(packageName + subpath)}.`, { provider, source, packageName, subpath });
|
|
149
|
+
return `https://cdn.jsdelivr.net/npm/${packageName}@${entry.version}/+esm`;
|
|
150
|
+
}
|
|
151
|
+
if (provider !== "esm.sh") throw error("PACKAGE_SOURCE_UNSUPPORTED", `${source} requires esm.sh.`, { provider, source, packageName });
|
|
152
|
+
if (source === "github") return `https://esm.sh/gh/${entry.repository}@${entry.ref}${subpath}`;
|
|
153
|
+
if (source === "pkg.pr.new") return `https://esm.sh/pr/${entry.locator}@${entry.ref}`;
|
|
154
|
+
throw error("PACKAGE_SOURCE_UNSUPPORTED", `Unknown package source ${JSON.stringify(source)}.`, { provider, source, packageName });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Optional internal Solid-family extension; both factories share exact package matching. */
|
|
158
|
+
export function createPackagePolicy(options, { inheritedProvider, familyRules } = {}) {
|
|
159
|
+
const normalized = normalizePackageOptions(options, { inheritedProvider });
|
|
160
|
+
const family = new Map(Object.entries(familyRules ?? {}));
|
|
161
|
+
function inspect(specifier) {
|
|
162
|
+
const parsed = parsePackageSpecifier(specifier);
|
|
163
|
+
if (!parsed) return undefined;
|
|
164
|
+
const owned = family.get(parsed.packageName);
|
|
165
|
+
if (owned) {
|
|
166
|
+
const ownedUrl = owned.subpathUrl ? owned.subpathUrl(parsed.subpath) : (parsed.subpath ? buildPackageUrl({ source: "npm", importName: parsed.packageName, version: owned.version, provider: owned.provider, subpaths: true }, parsed.subpath) : owned.url);
|
|
167
|
+
return Object.freeze({ specifier, packageName: parsed.packageName, subpath: parsed.subpath, version: owned.version, source: "npm", provider: owned.provider, url: ownedUrl, kind: "solid-family", implicitSubpath: Boolean(parsed.subpath) });
|
|
168
|
+
}
|
|
169
|
+
const entry = normalized.pins.get(parsed.packageName);
|
|
170
|
+
if (!entry) return undefined;
|
|
171
|
+
const inspected = { specifier, packageName: parsed.packageName, subpath: parsed.subpath, provider: entry.provider, source: entry.source, url: buildPackageUrl(entry, parsed.subpath), kind: "package", implicitSubpath: Boolean(parsed.subpath) };
|
|
172
|
+
if (entry.version) inspected.version = entry.version;
|
|
173
|
+
if (entry.ref) inspected.ref = entry.ref;
|
|
174
|
+
if (entry.repository) inspected.repository = entry.repository;
|
|
175
|
+
if (entry.locator) inspected.locator = entry.locator;
|
|
176
|
+
return Object.freeze(inspected);
|
|
177
|
+
}
|
|
178
|
+
return Object.freeze({ inspect, provider: normalized.provider, pins: normalized.pins });
|
|
179
|
+
}
|