wasm-game-ts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/hash.d.ts +3 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +15 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/inputs.d.ts +61 -0
- package/dist/inputs.d.ts.map +1 -0
- package/dist/inputs.js +143 -0
- package/dist/inputs.js.map +1 -0
- package/dist/keycodes.d.ts +3 -0
- package/dist/keycodes.d.ts.map +1 -0
- package/dist/keycodes.js +73 -0
- package/dist/keycodes.js.map +1 -0
- package/dist/package.d.ts +45 -0
- package/dist/package.d.ts.map +1 -0
- package/dist/package.js +765 -0
- package/dist/package.js.map +1 -0
- package/dist/types.d.ts +104 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/dist/wasm-game.d.ts +22 -0
- package/dist/wasm-game.d.ts.map +1 -0
- package/dist/wasm-game.js +164 -0
- package/dist/wasm-game.js.map +1 -0
- package/dist/worker/assets/OFL.txt +88 -0
- package/dist/worker/assets/PressStart2P-Regular.ttf +0 -0
- package/dist/worker/fonts.d.ts +2 -0
- package/dist/worker/fonts.d.ts.map +1 -0
- package/dist/worker/fonts.js +159 -0
- package/dist/worker/fonts.js.map +1 -0
- package/dist/worker/renderer.d.ts +34 -0
- package/dist/worker/renderer.d.ts.map +1 -0
- package/dist/worker/renderer.js +504 -0
- package/dist/worker/renderer.js.map +1 -0
- package/dist/worker/snapshotter.d.ts +17 -0
- package/dist/worker/snapshotter.d.ts.map +1 -0
- package/dist/worker/snapshotter.js +190 -0
- package/dist/worker/snapshotter.js.map +1 -0
- package/dist/worker/wasm-host.d.ts +18 -0
- package/dist/worker/wasm-host.d.ts.map +1 -0
- package/dist/worker/wasm-host.js +42 -0
- package/dist/worker/wasm-host.js.map +1 -0
- package/dist/worker/worker.d.ts +2 -0
- package/dist/worker/worker.d.ts.map +1 -0
- package/dist/worker/worker.js +311 -0
- package/dist/worker/worker.js.map +1 -0
- package/package.json +33 -0
package/dist/package.js
ADDED
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
let cachedPreprocess = null;
|
|
2
|
+
async function getPreprocessFn() {
|
|
3
|
+
if (!cachedPreprocess) {
|
|
4
|
+
cachedPreprocess = import('preprocess-wasm-bytes').then((m) => {
|
|
5
|
+
const fn = m?.preprocess_wasm_bytes;
|
|
6
|
+
if (typeof fn !== 'function') {
|
|
7
|
+
throw new Error('preprocess-wasm-bytes missing export preprocess_wasm_bytes(bytes)');
|
|
8
|
+
}
|
|
9
|
+
return fn;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
return cachedPreprocess;
|
|
13
|
+
}
|
|
14
|
+
async function preprocessWasmIfNeeded(path, bytes) {
|
|
15
|
+
// Only wasm files
|
|
16
|
+
if (!/\.wasm$/i.test(path))
|
|
17
|
+
return bytes;
|
|
18
|
+
//return bytes; // return early for now, TESTING
|
|
19
|
+
try {
|
|
20
|
+
//console.log('preprocessWasmIfNeeded', path);
|
|
21
|
+
const preprocess = await getPreprocessFn();
|
|
22
|
+
// Optional: user can enable verbose logging by setting:
|
|
23
|
+
// globalThis.__WASM_PREPROCESS_DEBUG__ = true;
|
|
24
|
+
// before calling the picker/import APIs.
|
|
25
|
+
console.time(`[preprocess] ${path}`);
|
|
26
|
+
const out = preprocess(bytes, { debug: true });
|
|
27
|
+
console.timeEnd(`[preprocess] ${path}`);
|
|
28
|
+
if (!(out instanceof Uint8Array)) {
|
|
29
|
+
console.warn('[ImmutablePackage] preprocess_wasm_bytes returned non-Uint8Array; using original wasm:', path);
|
|
30
|
+
return bytes;
|
|
31
|
+
}
|
|
32
|
+
// Helpful debug line (safe even if they don't enable the package debug flag)
|
|
33
|
+
console.log('[ImmutablePackage] Preprocessed wasm:', path, `(${bytes.byteLength} -> ${out.byteLength} bytes)`);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
console.warn('[ImmutablePackage] Failed to preprocess wasm; using original bytes:', path, err);
|
|
38
|
+
return bytes;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function cloneToArrayBuffer(u8) {
|
|
42
|
+
const ab = new ArrayBuffer(u8.byteLength);
|
|
43
|
+
new Uint8Array(ab).set(u8);
|
|
44
|
+
return ab;
|
|
45
|
+
}
|
|
46
|
+
function normalizePath(p) {
|
|
47
|
+
// keep it simple/deterministic: forward slashes, no leading "./"
|
|
48
|
+
let s = p.replace(/\\/g, '/');
|
|
49
|
+
while (s.startsWith('./'))
|
|
50
|
+
s = s.slice(2);
|
|
51
|
+
while (s.startsWith('/'))
|
|
52
|
+
s = s.slice(1);
|
|
53
|
+
// collapse '//' -> '/'
|
|
54
|
+
s = s.replace(/\/{2,}/g, '/');
|
|
55
|
+
return s;
|
|
56
|
+
}
|
|
57
|
+
function normalizeSlashes(p) {
|
|
58
|
+
return p.replace(/\\/g, '/');
|
|
59
|
+
}
|
|
60
|
+
function stripFirstDir(p) {
|
|
61
|
+
const i = p.indexOf('/');
|
|
62
|
+
return i === -1 ? p : p.slice(i + 1);
|
|
63
|
+
}
|
|
64
|
+
function isCodeFile(path) {
|
|
65
|
+
// Mirrors the old framework’s intent: avoid slurping source trees.
|
|
66
|
+
return /\.(rs|ts|tsx|js|jsx|c|cpp|h|hpp|m|py|java|cs|lock|d|o|toml)$/i.test(path);
|
|
67
|
+
}
|
|
68
|
+
function isTargetRootWasm(relPath) {
|
|
69
|
+
// EXACTLY:
|
|
70
|
+
// target/wasm32-unknown-unknown/{debug|release}/<file>.wasm
|
|
71
|
+
const m = relPath.match(/^target\/wasm32-unknown-unknown\/(debug|release)\/[^/]+\.wasm$/i);
|
|
72
|
+
if (!m)
|
|
73
|
+
return { ok: false, isRelease: false };
|
|
74
|
+
return { ok: true, isRelease: m[1].toLowerCase() === 'release' };
|
|
75
|
+
}
|
|
76
|
+
function shouldIncludePath(relPath) {
|
|
77
|
+
// Skip obvious junk/heavy dirs
|
|
78
|
+
if (relPath.startsWith('.git/'))
|
|
79
|
+
return false;
|
|
80
|
+
if (relPath.startsWith('node_modules/'))
|
|
81
|
+
return false;
|
|
82
|
+
if (relPath.endsWith('.DS_Store'))
|
|
83
|
+
return false;
|
|
84
|
+
// IMPORTANT: Ignore ALL of target/ here. We handle the ONE allowed wasm separately.
|
|
85
|
+
if (relPath.startsWith('target/'))
|
|
86
|
+
return false;
|
|
87
|
+
// Skip code/config
|
|
88
|
+
if (isCodeFile(relPath))
|
|
89
|
+
return false;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
function shouldSkipDir(relDir) {
|
|
93
|
+
// relDir is normalized (forward slashes, no leading slash). Usually ends with "/".
|
|
94
|
+
if (!relDir)
|
|
95
|
+
return true;
|
|
96
|
+
// Always skip these entire trees (huge / irrelevant)
|
|
97
|
+
if (relDir.startsWith('.git/'))
|
|
98
|
+
return true;
|
|
99
|
+
if (relDir.startsWith('node_modules/'))
|
|
100
|
+
return true;
|
|
101
|
+
// Hard rule: ignore everything in target/ except these directories:
|
|
102
|
+
// target/
|
|
103
|
+
// target/wasm32-unknown-unknown/
|
|
104
|
+
// target/wasm32-unknown-unknown/debug/
|
|
105
|
+
// target/wasm32-unknown-unknown/release/
|
|
106
|
+
if (relDir.startsWith('target/')) {
|
|
107
|
+
if (relDir === 'target/')
|
|
108
|
+
return false;
|
|
109
|
+
if (relDir === 'target/wasm32-unknown-unknown/')
|
|
110
|
+
return false;
|
|
111
|
+
if (relDir === 'target/wasm32-unknown-unknown/debug/')
|
|
112
|
+
return false;
|
|
113
|
+
if (relDir === 'target/wasm32-unknown-unknown/release/')
|
|
114
|
+
return false;
|
|
115
|
+
return true; // skip EVERYTHING else under target/
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
function extLower(path) {
|
|
120
|
+
const idx = path.lastIndexOf('.');
|
|
121
|
+
return idx >= 0 ? path.slice(idx + 1).toLowerCase() : '';
|
|
122
|
+
}
|
|
123
|
+
function guessMime(ext) {
|
|
124
|
+
switch (ext) {
|
|
125
|
+
case 'wasm': return 'application/wasm';
|
|
126
|
+
case 'json': return 'application/json';
|
|
127
|
+
case 'txt': return 'text/plain';
|
|
128
|
+
case 'png': return 'image/png';
|
|
129
|
+
case 'jpg':
|
|
130
|
+
case 'jpeg': return 'image/jpeg';
|
|
131
|
+
case 'gif': return 'image/gif';
|
|
132
|
+
case 'webp': return 'image/webp';
|
|
133
|
+
case 'wav': return 'audio/wav';
|
|
134
|
+
case 'mp3': return 'audio/mpeg';
|
|
135
|
+
case 'ogg': return 'audio/ogg';
|
|
136
|
+
default: return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function detectKind(ext) {
|
|
140
|
+
if (ext === 'wasm')
|
|
141
|
+
return 'wasm';
|
|
142
|
+
if (ext === 'json')
|
|
143
|
+
return 'json';
|
|
144
|
+
if (ext === 'txt' || ext === 'md')
|
|
145
|
+
return 'text';
|
|
146
|
+
if (ext === 'png' || ext === 'jpg' || ext === 'jpeg' || ext === 'gif' || ext === 'webp')
|
|
147
|
+
return 'image';
|
|
148
|
+
if (ext === 'wav' || ext === 'mp3' || ext === 'ogg')
|
|
149
|
+
return 'audio';
|
|
150
|
+
return 'binary';
|
|
151
|
+
}
|
|
152
|
+
function tryParsePngSize(bytes) {
|
|
153
|
+
// PNG header parse is deterministic: width/height are big-endian at offsets 16/20 (IHDR data)
|
|
154
|
+
// Signature: 8 bytes: 89 50 4E 47 0D 0A 1A 0A
|
|
155
|
+
if (bytes.length < 24)
|
|
156
|
+
return null;
|
|
157
|
+
if (bytes[0] !== 0x89 || bytes[1] !== 0x50 || bytes[2] !== 0x4e || bytes[3] !== 0x47 ||
|
|
158
|
+
bytes[4] !== 0x0d || bytes[5] !== 0x0a || bytes[6] !== 0x1a || bytes[7] !== 0x0a)
|
|
159
|
+
return null;
|
|
160
|
+
// chunk type "IHDR" should begin at 12
|
|
161
|
+
if (bytes[12] !== 0x49 || bytes[13] !== 0x48 || bytes[14] !== 0x44 || bytes[15] !== 0x52)
|
|
162
|
+
return null;
|
|
163
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
164
|
+
const w = view.getUint32(16, false);
|
|
165
|
+
const h = view.getUint32(20, false);
|
|
166
|
+
if (w === 0 || h === 0)
|
|
167
|
+
return null;
|
|
168
|
+
return { w, h };
|
|
169
|
+
}
|
|
170
|
+
function tryParseWavDurationMs(bytes) {
|
|
171
|
+
// Deterministic WAV header parse (RIFF/WAVE with fmt + data chunks)
|
|
172
|
+
if (bytes.length < 44)
|
|
173
|
+
return null;
|
|
174
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
175
|
+
const tag = (o) => String.fromCharCode(bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]);
|
|
176
|
+
if (tag(0) !== 'RIFF' || tag(8) !== 'WAVE')
|
|
177
|
+
return null;
|
|
178
|
+
let off = 12;
|
|
179
|
+
let sampleRate = 0;
|
|
180
|
+
let blockAlign = 0;
|
|
181
|
+
let dataSize = 0;
|
|
182
|
+
while (off + 8 <= bytes.length) {
|
|
183
|
+
const id = tag(off);
|
|
184
|
+
const size = view.getUint32(off + 4, true);
|
|
185
|
+
off += 8;
|
|
186
|
+
if (off + size > bytes.length)
|
|
187
|
+
break;
|
|
188
|
+
if (id === 'fmt ') {
|
|
189
|
+
// PCM fmt chunk
|
|
190
|
+
// audioFormat u16, numChannels u16, sampleRate u32, byteRate u32, blockAlign u16, bitsPerSample u16
|
|
191
|
+
if (size >= 16) {
|
|
192
|
+
sampleRate = view.getUint32(off + 4, true);
|
|
193
|
+
blockAlign = view.getUint16(off + 12, true);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else if (id === 'data') {
|
|
197
|
+
dataSize = size;
|
|
198
|
+
}
|
|
199
|
+
off += size;
|
|
200
|
+
// chunks are word-aligned (pad byte)
|
|
201
|
+
if (off & 1)
|
|
202
|
+
off += 1;
|
|
203
|
+
}
|
|
204
|
+
if (sampleRate <= 0 || blockAlign <= 0 || dataSize <= 0)
|
|
205
|
+
return null;
|
|
206
|
+
const seconds = dataSize / (sampleRate * blockAlign);
|
|
207
|
+
return Math.round(seconds * 1000);
|
|
208
|
+
}
|
|
209
|
+
function buildManifest(files) {
|
|
210
|
+
// bombcrate expects a UTF-8 JSON manifest (written into WASM memory during init_end()).
|
|
211
|
+
// It contains ONLY files (no directories) so there are never any empty folders.
|
|
212
|
+
const entries = files.map((f) => ({
|
|
213
|
+
id: f.id,
|
|
214
|
+
path: f.path,
|
|
215
|
+
meta: {
|
|
216
|
+
// Keep key order stable; undefined fields are omitted by JSON.stringify.
|
|
217
|
+
kind: f.meta.kind,
|
|
218
|
+
byteLength: f.meta.byteLength,
|
|
219
|
+
mime: f.meta.mime,
|
|
220
|
+
audioDurationMs: f.meta.audioDurationMs,
|
|
221
|
+
imageWidth: f.meta.imageWidth,
|
|
222
|
+
imageHeight: f.meta.imageHeight,
|
|
223
|
+
},
|
|
224
|
+
}));
|
|
225
|
+
// Stable output (helpful for debugging + deterministic packaging).
|
|
226
|
+
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
227
|
+
const manifest = {
|
|
228
|
+
version: 1,
|
|
229
|
+
files: entries,
|
|
230
|
+
};
|
|
231
|
+
const text = JSON.stringify(manifest);
|
|
232
|
+
return new TextEncoder().encode(text);
|
|
233
|
+
}
|
|
234
|
+
async function* walkDir(dir, prefix) {
|
|
235
|
+
// File System Access API
|
|
236
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
237
|
+
const entries = dir.entries?.();
|
|
238
|
+
for await (const [name, handle] of entries) {
|
|
239
|
+
if (handle.kind === 'file') {
|
|
240
|
+
yield { path: prefix + name, handle: handle };
|
|
241
|
+
}
|
|
242
|
+
else if (handle.kind === 'directory') {
|
|
243
|
+
const dirPath = normalizePath(`${prefix}${name}/`);
|
|
244
|
+
if (shouldSkipDir(dirPath))
|
|
245
|
+
continue;
|
|
246
|
+
yield* walkDir(handle, dirPath);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async function pickSingleFileViaInput(accept) {
|
|
251
|
+
if (typeof document === 'undefined') {
|
|
252
|
+
throw new Error('File picker requires a browser DOM (document is undefined).');
|
|
253
|
+
}
|
|
254
|
+
return await new Promise((resolve, reject) => {
|
|
255
|
+
const input = document.createElement('input');
|
|
256
|
+
input.type = 'file';
|
|
257
|
+
input.accept = accept;
|
|
258
|
+
input.multiple = false;
|
|
259
|
+
// Keep it out of sight but in the DOM (Safari can be picky about this).
|
|
260
|
+
input.style.position = 'fixed';
|
|
261
|
+
input.style.left = '-9999px';
|
|
262
|
+
input.style.top = '0';
|
|
263
|
+
const cleanup = () => {
|
|
264
|
+
input.onchange = null;
|
|
265
|
+
input.onerror = null;
|
|
266
|
+
input.remove();
|
|
267
|
+
};
|
|
268
|
+
input.onchange = () => {
|
|
269
|
+
const file = input.files && input.files[0] ? input.files[0] : null;
|
|
270
|
+
cleanup();
|
|
271
|
+
if (!file)
|
|
272
|
+
reject(new Error('No file selected.'));
|
|
273
|
+
else
|
|
274
|
+
resolve(file);
|
|
275
|
+
};
|
|
276
|
+
input.onerror = () => {
|
|
277
|
+
cleanup();
|
|
278
|
+
reject(new Error('File picker failed.'));
|
|
279
|
+
};
|
|
280
|
+
document.body.appendChild(input);
|
|
281
|
+
input.click();
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
export class ImmutablePackage {
|
|
285
|
+
constructor(files, wasmId) {
|
|
286
|
+
this.files = files;
|
|
287
|
+
this.wasmId = wasmId;
|
|
288
|
+
this.byId = new Map(files.map((f) => [f.id, f]));
|
|
289
|
+
this.byPath = new Map(files.map((f) => [f.path, f]));
|
|
290
|
+
}
|
|
291
|
+
/** True if we can pick a folder in this browser (either API or webkitdirectory). */
|
|
292
|
+
static supportsFolderPicker() {
|
|
293
|
+
const g = globalThis;
|
|
294
|
+
if (typeof g.showDirectoryPicker === 'function')
|
|
295
|
+
return true;
|
|
296
|
+
// webkitdirectory detection
|
|
297
|
+
if (typeof document !== 'undefined') {
|
|
298
|
+
const input = document.createElement('input');
|
|
299
|
+
return ('webkitdirectory' in input) || ('directory' in input);
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
/** Safari/Brave-friendly folder picker: <input type="file" webkitdirectory multiple> */
|
|
304
|
+
static pickFolderViaWebkitInput() {
|
|
305
|
+
if (typeof document === 'undefined') {
|
|
306
|
+
throw new Error('pickFolderViaWebkitInput() must be called in a browser.');
|
|
307
|
+
}
|
|
308
|
+
return new Promise((resolve, reject) => {
|
|
309
|
+
const input = document.createElement('input');
|
|
310
|
+
input.type = 'file';
|
|
311
|
+
input.multiple = true;
|
|
312
|
+
// Non-standard but widely supported
|
|
313
|
+
input.setAttribute('webkitdirectory', '');
|
|
314
|
+
input.setAttribute('directory', '');
|
|
315
|
+
// Keep it off-screen but in-DOM (Safari can be picky)
|
|
316
|
+
input.style.position = 'fixed';
|
|
317
|
+
input.style.left = '-10000px';
|
|
318
|
+
input.style.top = '-10000px';
|
|
319
|
+
document.body.appendChild(input);
|
|
320
|
+
let done = false;
|
|
321
|
+
let sawBlur = false;
|
|
322
|
+
const cleanup = () => {
|
|
323
|
+
try {
|
|
324
|
+
window.removeEventListener('focus', onFocus, true);
|
|
325
|
+
window.removeEventListener('blur', onBlur, true);
|
|
326
|
+
}
|
|
327
|
+
catch { }
|
|
328
|
+
input.remove();
|
|
329
|
+
};
|
|
330
|
+
const finish = (files) => {
|
|
331
|
+
if (done)
|
|
332
|
+
return;
|
|
333
|
+
done = true;
|
|
334
|
+
cleanup();
|
|
335
|
+
if (!files || files.length === 0) {
|
|
336
|
+
reject(new Error('Folder selection canceled.'));
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
resolve(files);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
input.addEventListener('change', () => {
|
|
343
|
+
finish(input.files);
|
|
344
|
+
}, { once: true });
|
|
345
|
+
const onBlur = () => {
|
|
346
|
+
sawBlur = true;
|
|
347
|
+
};
|
|
348
|
+
const onFocus = () => {
|
|
349
|
+
// Only run cancel logic if we actually saw the window blur (i.e., picker likely opened).
|
|
350
|
+
if (!sawBlur)
|
|
351
|
+
return;
|
|
352
|
+
const start = Date.now();
|
|
353
|
+
const maxWaitMs = 8000; // generous; prevents false cancels
|
|
354
|
+
const stepMs = 100;
|
|
355
|
+
const poll = () => {
|
|
356
|
+
if (done)
|
|
357
|
+
return;
|
|
358
|
+
const files = input.files;
|
|
359
|
+
if (files && files.length > 0) {
|
|
360
|
+
finish(files);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (Date.now() - start >= maxWaitMs) {
|
|
364
|
+
finish(null); // treat as cancel
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
setTimeout(poll, stepMs);
|
|
368
|
+
};
|
|
369
|
+
// Start polling very soon after focus returns
|
|
370
|
+
setTimeout(poll, 0);
|
|
371
|
+
};
|
|
372
|
+
window.addEventListener('blur', onBlur, true);
|
|
373
|
+
window.addEventListener('focus', onFocus, true);
|
|
374
|
+
input.click();
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Build a package from a FileList (e.g. returned by webkitdirectory picker).
|
|
379
|
+
* This is also handy if you ever add an explicit <input webkitdirectory> in the demo HTML.
|
|
380
|
+
*/
|
|
381
|
+
static async fromFileList(fileList, options = {}) {
|
|
382
|
+
const files = [];
|
|
383
|
+
let nextId = 1;
|
|
384
|
+
// Collect ONLY root target wasm candidates; do NOT include them yet.
|
|
385
|
+
const targetWasmCandidates = [];
|
|
386
|
+
for (const f of Array.from(fileList)) {
|
|
387
|
+
const raw = f.webkitRelativePath ? String(f.webkitRelativePath) : f.name;
|
|
388
|
+
let relPath = normalizeSlashes(raw);
|
|
389
|
+
// Remove the selected folder name prefix so both pickers align
|
|
390
|
+
relPath = stripFirstDir(relPath);
|
|
391
|
+
relPath = normalizePath(relPath);
|
|
392
|
+
if (!relPath)
|
|
393
|
+
continue;
|
|
394
|
+
// HARD RULE: ignore everything in target/ except the ONE allowed wasm candidate(s)
|
|
395
|
+
if (relPath.startsWith('target/')) {
|
|
396
|
+
const cand = isTargetRootWasm(relPath);
|
|
397
|
+
if (cand.ok) {
|
|
398
|
+
targetWasmCandidates.push({
|
|
399
|
+
path: relPath,
|
|
400
|
+
file: f,
|
|
401
|
+
lastModified: Number.isFinite(f.lastModified) ? f.lastModified : 0,
|
|
402
|
+
isRelease: cand.isRelease,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
continue; // ignore ALL other target files
|
|
406
|
+
}
|
|
407
|
+
// Normal non-target filtering
|
|
408
|
+
if (!shouldIncludePath(relPath))
|
|
409
|
+
continue;
|
|
410
|
+
const buf = await f.arrayBuffer();
|
|
411
|
+
let bytes = new Uint8Array(buf);
|
|
412
|
+
const ext = extLower(relPath);
|
|
413
|
+
const kind = detectKind(ext);
|
|
414
|
+
if (kind === 'wasm') {
|
|
415
|
+
bytes = await preprocessWasmIfNeeded(relPath, bytes);
|
|
416
|
+
}
|
|
417
|
+
const mime = f.type || guessMime(ext);
|
|
418
|
+
const meta = {
|
|
419
|
+
kind,
|
|
420
|
+
byteLength: bytes.byteLength,
|
|
421
|
+
mime,
|
|
422
|
+
};
|
|
423
|
+
if (kind === 'image' && ext === 'png') {
|
|
424
|
+
const sz = tryParsePngSize(bytes);
|
|
425
|
+
if (sz) {
|
|
426
|
+
meta.imageWidth = sz.w;
|
|
427
|
+
meta.imageHeight = sz.h;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (kind === 'audio' && ext === 'wav') {
|
|
431
|
+
const dur = tryParseWavDurationMs(bytes);
|
|
432
|
+
if (dur != null)
|
|
433
|
+
meta.audioDurationMs = dur;
|
|
434
|
+
}
|
|
435
|
+
const id = nextId++;
|
|
436
|
+
files.push({ id, path: relPath, bytes, meta });
|
|
437
|
+
}
|
|
438
|
+
if (files.length === 0 && targetWasmCandidates.length === 0) {
|
|
439
|
+
throw new Error('Selected folder contains no files.');
|
|
440
|
+
}
|
|
441
|
+
// Decide which wasm becomes the package entrypoint
|
|
442
|
+
let wasmId = 0;
|
|
443
|
+
if (options.wasmPath) {
|
|
444
|
+
const wanted = normalizePath(options.wasmPath);
|
|
445
|
+
const hit = targetWasmCandidates.find((c) => c.path === wanted);
|
|
446
|
+
if (hit) {
|
|
447
|
+
const ab = await hit.file.arrayBuffer();
|
|
448
|
+
let bytes = new Uint8Array(ab);
|
|
449
|
+
// preprocess ONLY this one file
|
|
450
|
+
bytes = await preprocessWasmIfNeeded(hit.path, bytes);
|
|
451
|
+
const id = nextId++;
|
|
452
|
+
files.push({
|
|
453
|
+
id,
|
|
454
|
+
path: hit.path,
|
|
455
|
+
bytes,
|
|
456
|
+
meta: {
|
|
457
|
+
kind: 'wasm',
|
|
458
|
+
byteLength: bytes.byteLength,
|
|
459
|
+
mime: hit.file.type || guessMime('wasm') || 'application/wasm',
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
wasmId = id;
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
// Or wanted might be a non-target file we already included
|
|
466
|
+
const existing = files.find((x) => x.path === wanted);
|
|
467
|
+
if (!existing)
|
|
468
|
+
throw new Error(`wasmPath not found in folder: ${wanted}`);
|
|
469
|
+
if (existing.meta.kind !== 'wasm')
|
|
470
|
+
throw new Error(`wasmPath is not a .wasm file: ${wanted}`);
|
|
471
|
+
wasmId = existing.id;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
// Default selection: choose ONE target wasm if present
|
|
476
|
+
if (targetWasmCandidates.length > 0) {
|
|
477
|
+
targetWasmCandidates.sort((a, b) => {
|
|
478
|
+
if (b.lastModified !== a.lastModified)
|
|
479
|
+
return b.lastModified - a.lastModified;
|
|
480
|
+
if (a.isRelease !== b.isRelease)
|
|
481
|
+
return a.isRelease ? -1 : 1; // prefer release
|
|
482
|
+
return a.path.localeCompare(b.path);
|
|
483
|
+
});
|
|
484
|
+
const chosen = targetWasmCandidates[0];
|
|
485
|
+
const ab = await chosen.file.arrayBuffer();
|
|
486
|
+
let bytes = new Uint8Array(ab);
|
|
487
|
+
// preprocess ONLY this one file
|
|
488
|
+
bytes = await preprocessWasmIfNeeded(chosen.path, bytes);
|
|
489
|
+
const id = nextId++;
|
|
490
|
+
files.push({
|
|
491
|
+
id,
|
|
492
|
+
path: chosen.path,
|
|
493
|
+
bytes,
|
|
494
|
+
meta: {
|
|
495
|
+
kind: 'wasm',
|
|
496
|
+
byteLength: bytes.byteLength,
|
|
497
|
+
mime: chosen.file.type || guessMime('wasm') || 'application/wasm',
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
wasmId = id;
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
// fallback: if no target wasm candidates were found, pick the first wasm already included
|
|
504
|
+
const firstWasm = files.find((x) => x.meta.kind === 'wasm');
|
|
505
|
+
if (!firstWasm) {
|
|
506
|
+
throw new Error('No target wasm found. Expected one of:\n' +
|
|
507
|
+
'- target/wasm32-unknown-unknown/release/<name>.wasm\n' +
|
|
508
|
+
'- target/wasm32-unknown-unknown/debug/<name>.wasm');
|
|
509
|
+
}
|
|
510
|
+
wasmId = firstWasm.id;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// Add manifest as id=-1 (JSON)
|
|
514
|
+
const manifestBytes = buildManifest(files);
|
|
515
|
+
files.push({
|
|
516
|
+
id: -1,
|
|
517
|
+
path: '__manifest__',
|
|
518
|
+
bytes: manifestBytes,
|
|
519
|
+
meta: { kind: 'json', byteLength: manifestBytes.byteLength, mime: 'application/json' },
|
|
520
|
+
});
|
|
521
|
+
return new ImmutablePackage(files, wasmId);
|
|
522
|
+
}
|
|
523
|
+
listWasmFiles() {
|
|
524
|
+
return this.files.filter((f) => f.meta.kind === 'wasm' && f.id !== -1);
|
|
525
|
+
}
|
|
526
|
+
getById(id) {
|
|
527
|
+
return this.byId.get(id | 0);
|
|
528
|
+
}
|
|
529
|
+
getByPath(path) {
|
|
530
|
+
return this.byPath.get(normalizePath(path));
|
|
531
|
+
}
|
|
532
|
+
getBytesById(id) {
|
|
533
|
+
const f = this.getById(id);
|
|
534
|
+
return f ? f.bytes : null;
|
|
535
|
+
}
|
|
536
|
+
toWorkerPayload(opts = {}) {
|
|
537
|
+
const transfer = opts.transfer ?? true;
|
|
538
|
+
const transferables = [];
|
|
539
|
+
const files = this.files.map((f) => {
|
|
540
|
+
// IMPORTANT: ensure this is a real ArrayBuffer (not ArrayBufferLike)
|
|
541
|
+
// and contains exactly the bytes (no offsets).
|
|
542
|
+
const bytes = cloneToArrayBuffer(f.bytes);
|
|
543
|
+
if (transfer)
|
|
544
|
+
transferables.push(bytes);
|
|
545
|
+
return { id: f.id, path: f.path, bytes, meta: f.meta };
|
|
546
|
+
});
|
|
547
|
+
const wasmId = (opts.wasmIdOverride ?? this.wasmId) | 0;
|
|
548
|
+
return { pkg: { wasmId, files }, transferables };
|
|
549
|
+
}
|
|
550
|
+
static async fromDirectoryPicker(options = {}) {
|
|
551
|
+
const g = globalThis;
|
|
552
|
+
if (typeof g.showDirectoryPicker === 'function') {
|
|
553
|
+
let dir = null;
|
|
554
|
+
try {
|
|
555
|
+
dir = await g.showDirectoryPicker();
|
|
556
|
+
}
|
|
557
|
+
catch (e) {
|
|
558
|
+
// If user cancels, surface cancel (don’t fallback)
|
|
559
|
+
if (e?.name === 'AbortError')
|
|
560
|
+
throw new Error('Folder selection canceled.');
|
|
561
|
+
console.warn('[ImmutablePackage] showDirectoryPicker failed, falling back to webkitdirectory:', e?.name ?? e);
|
|
562
|
+
dir = null;
|
|
563
|
+
}
|
|
564
|
+
if (dir) {
|
|
565
|
+
// Optional permission request (some browsers are finicky)
|
|
566
|
+
try {
|
|
567
|
+
const req = dir.requestPermission;
|
|
568
|
+
if (typeof req === 'function') {
|
|
569
|
+
const perm = await req.call(dir, { mode: 'read' });
|
|
570
|
+
if (perm === 'denied')
|
|
571
|
+
throw new Error('Folder permission denied.');
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
catch (e) {
|
|
575
|
+
// If permission check fails, still try reading; let errors surface.
|
|
576
|
+
}
|
|
577
|
+
// IMPORTANT: if this fails, do NOT fallback (it hides the bug and triggers webkit issues).
|
|
578
|
+
return await ImmutablePackage.fromDirectoryHandle(dir, options);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const list = await this.pickFolderViaWebkitInput();
|
|
582
|
+
return await this.fromFileList(list, options);
|
|
583
|
+
}
|
|
584
|
+
static async fromWasmFilePicker() {
|
|
585
|
+
let file;
|
|
586
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
587
|
+
const wAny = globalThis;
|
|
588
|
+
if (typeof wAny.showOpenFilePicker === 'function') {
|
|
589
|
+
const [handle] = await wAny.showOpenFilePicker({
|
|
590
|
+
multiple: false,
|
|
591
|
+
excludeAcceptAllOption: true,
|
|
592
|
+
types: [
|
|
593
|
+
{
|
|
594
|
+
description: 'WebAssembly Module',
|
|
595
|
+
accept: {
|
|
596
|
+
'application/wasm': ['.wasm'],
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
});
|
|
601
|
+
file = await handle.getFile();
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
file = await pickSingleFileViaInput('.wasm');
|
|
605
|
+
}
|
|
606
|
+
const ab = await file.arrayBuffer();
|
|
607
|
+
let bytes = new Uint8Array(ab);
|
|
608
|
+
const norm = normalizePath(file.name);
|
|
609
|
+
const ext = extLower(norm);
|
|
610
|
+
const kind = detectKind(ext);
|
|
611
|
+
if (kind !== 'wasm')
|
|
612
|
+
throw new Error('Selected file is not a .wasm file.');
|
|
613
|
+
bytes = await preprocessWasmIfNeeded(norm, bytes);
|
|
614
|
+
const meta = {
|
|
615
|
+
kind,
|
|
616
|
+
byteLength: bytes.byteLength,
|
|
617
|
+
mime: file.type || guessMime(ext),
|
|
618
|
+
};
|
|
619
|
+
const pkgFile = {
|
|
620
|
+
id: 1,
|
|
621
|
+
path: norm,
|
|
622
|
+
bytes,
|
|
623
|
+
meta,
|
|
624
|
+
};
|
|
625
|
+
const files = [pkgFile];
|
|
626
|
+
// Add manifest as id=-1 (JSON)
|
|
627
|
+
const manifestBytes = buildManifest(files);
|
|
628
|
+
files.push({
|
|
629
|
+
id: -1,
|
|
630
|
+
path: '__manifest__',
|
|
631
|
+
bytes: manifestBytes,
|
|
632
|
+
meta: { kind: 'json', byteLength: manifestBytes.byteLength, mime: 'application/json' },
|
|
633
|
+
});
|
|
634
|
+
return new ImmutablePackage(files, pkgFile.id);
|
|
635
|
+
}
|
|
636
|
+
static async fromDirectoryHandle(dir, options = {}) {
|
|
637
|
+
const files = [];
|
|
638
|
+
let nextId = 1;
|
|
639
|
+
const targetWasmCandidates = [];
|
|
640
|
+
for await (const { path, handle } of walkDir(dir, '')) {
|
|
641
|
+
const norm = normalizePath(path);
|
|
642
|
+
if (!norm)
|
|
643
|
+
continue;
|
|
644
|
+
// HARD RULE: ignore all of target except root wasm candidates
|
|
645
|
+
if (norm.startsWith('target/')) {
|
|
646
|
+
const cand = isTargetRootWasm(norm);
|
|
647
|
+
if (cand.ok) {
|
|
648
|
+
// getFile() just to read lastModified (cheap; bytes not read)
|
|
649
|
+
const f = await handle.getFile();
|
|
650
|
+
targetWasmCandidates.push({
|
|
651
|
+
path: norm,
|
|
652
|
+
handle,
|
|
653
|
+
lastModified: Number.isFinite(f.lastModified) ? f.lastModified : 0,
|
|
654
|
+
isRelease: cand.isRelease,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (!shouldIncludePath(norm))
|
|
660
|
+
continue;
|
|
661
|
+
const ext = extLower(norm);
|
|
662
|
+
const kind = detectKind(ext);
|
|
663
|
+
const file = await handle.getFile();
|
|
664
|
+
const ab = await file.arrayBuffer();
|
|
665
|
+
let bytes = new Uint8Array(ab);
|
|
666
|
+
if (kind === 'wasm') {
|
|
667
|
+
bytes = await preprocessWasmIfNeeded(norm, bytes);
|
|
668
|
+
}
|
|
669
|
+
const mime = file.type || guessMime(ext);
|
|
670
|
+
const meta = {
|
|
671
|
+
kind,
|
|
672
|
+
byteLength: bytes.byteLength,
|
|
673
|
+
mime,
|
|
674
|
+
};
|
|
675
|
+
if (kind === 'image' && ext === 'png') {
|
|
676
|
+
const sz = tryParsePngSize(bytes);
|
|
677
|
+
if (sz) {
|
|
678
|
+
meta.imageWidth = sz.w;
|
|
679
|
+
meta.imageHeight = sz.h;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (kind === 'audio' && ext === 'wav') {
|
|
683
|
+
const dur = tryParseWavDurationMs(bytes);
|
|
684
|
+
if (dur != null)
|
|
685
|
+
meta.audioDurationMs = dur;
|
|
686
|
+
}
|
|
687
|
+
const id = nextId++;
|
|
688
|
+
files.push({ id, path: norm, bytes, meta });
|
|
689
|
+
}
|
|
690
|
+
if (files.length === 0 && targetWasmCandidates.length === 0) {
|
|
691
|
+
throw new Error('Selected folder contains no files.');
|
|
692
|
+
}
|
|
693
|
+
// Choose wasm
|
|
694
|
+
let wasmId = 0;
|
|
695
|
+
if (options.wasmPath) {
|
|
696
|
+
const wanted = normalizePath(options.wasmPath);
|
|
697
|
+
const hit = targetWasmCandidates.find((c) => c.path === wanted);
|
|
698
|
+
if (hit) {
|
|
699
|
+
const f = await hit.handle.getFile();
|
|
700
|
+
const ab = await f.arrayBuffer();
|
|
701
|
+
let bytes = new Uint8Array(ab);
|
|
702
|
+
bytes = await preprocessWasmIfNeeded(hit.path, bytes);
|
|
703
|
+
const id = nextId++;
|
|
704
|
+
files.push({
|
|
705
|
+
id,
|
|
706
|
+
path: hit.path,
|
|
707
|
+
bytes,
|
|
708
|
+
meta: { kind: 'wasm', byteLength: bytes.byteLength, mime: f.type || guessMime('wasm') || 'application/wasm' },
|
|
709
|
+
});
|
|
710
|
+
wasmId = id;
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
const existing = files.find((x) => x.path === wanted);
|
|
714
|
+
if (!existing)
|
|
715
|
+
throw new Error(`wasmPath not found in folder: ${wanted}`);
|
|
716
|
+
if (existing.meta.kind !== 'wasm')
|
|
717
|
+
throw new Error(`wasmPath is not a .wasm file: ${wanted}`);
|
|
718
|
+
wasmId = existing.id;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
if (targetWasmCandidates.length > 0) {
|
|
723
|
+
targetWasmCandidates.sort((a, b) => {
|
|
724
|
+
if (b.lastModified !== a.lastModified)
|
|
725
|
+
return b.lastModified - a.lastModified;
|
|
726
|
+
if (a.isRelease !== b.isRelease)
|
|
727
|
+
return a.isRelease ? -1 : 1;
|
|
728
|
+
return a.path.localeCompare(b.path);
|
|
729
|
+
});
|
|
730
|
+
const chosen = targetWasmCandidates[0];
|
|
731
|
+
const f = await chosen.handle.getFile();
|
|
732
|
+
const ab = await f.arrayBuffer();
|
|
733
|
+
let bytes = new Uint8Array(ab);
|
|
734
|
+
bytes = await preprocessWasmIfNeeded(chosen.path, bytes);
|
|
735
|
+
const id = nextId++;
|
|
736
|
+
files.push({
|
|
737
|
+
id,
|
|
738
|
+
path: chosen.path,
|
|
739
|
+
bytes,
|
|
740
|
+
meta: { kind: 'wasm', byteLength: bytes.byteLength, mime: f.type || guessMime('wasm') || 'application/wasm' },
|
|
741
|
+
});
|
|
742
|
+
wasmId = id;
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
const firstWasm = files.find((x) => x.meta.kind === 'wasm');
|
|
746
|
+
if (!firstWasm) {
|
|
747
|
+
throw new Error('No target wasm found. Expected one of:\n' +
|
|
748
|
+
'- target/wasm32-unknown-unknown/release/<name>.wasm\n' +
|
|
749
|
+
'- target/wasm32-unknown-unknown/debug/<name>.wasm');
|
|
750
|
+
}
|
|
751
|
+
wasmId = firstWasm.id;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
// Add manifest as id=-1 (JSON)
|
|
755
|
+
const manifestBytes = buildManifest(files);
|
|
756
|
+
files.push({
|
|
757
|
+
id: -1,
|
|
758
|
+
path: '__manifest__',
|
|
759
|
+
bytes: manifestBytes,
|
|
760
|
+
meta: { kind: 'json', byteLength: manifestBytes.byteLength, mime: 'application/json' },
|
|
761
|
+
});
|
|
762
|
+
return new ImmutablePackage(files, wasmId);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
//# sourceMappingURL=package.js.map
|