local-modules-ui.js (5343B)
1 import { BUNDLE_LIMIT } from './bundle.js'; 2 import { LOCAL_MODULE_BYTES, LOCAL_MODULE_COUNT } from './local-modules.js'; 3 // Hold local module files in page memory until removal or page departure. 4 export function localModulesUI(changed) { 5 const input = document.getElementById('local-module-file'); 6 const list = document.getElementById('local-module-list'); 7 const status = document.getElementById('local-module-status'); 8 const cancel = document.getElementById('cancel-local-module'); 9 const modules = new Map(); 10 let busy = false, checking = false, abort; 11 const render = () => { 12 input.disabled = busy || checking; 13 cancel.hidden = !checking; 14 list.replaceChildren(); 15 for (const [manifest, { label, module }] of modules) { 16 const row = document.createElement('li'), title = document.createElement('p'); 17 title.textContent = `${label} · ${module.names.length} export(s) · ${module.bytes.byteLength.toLocaleString()} bytes`; 18 const code = document.createElement('code'); 19 code.textContent = `!import ${manifest} as Local`; 20 const details = document.createElement('details'), summary = document.createElement('summary'), names = document.createElement('p'); 21 summary.textContent = 'Exports and bundle hash'; 22 names.textContent = `${module.names.join(', ')}\nBundle SHA-256: ${module.hash}`; 23 details.append(summary, names); 24 const copy = document.createElement('button'); 25 copy.textContent = 'Copy import'; 26 copy.onclick = () => { void (async () => { try { 27 await navigator.clipboard.writeText(code.textContent); 28 status.textContent = 'Import copied.'; 29 } 30 catch { 31 status.textContent = code.textContent; 32 } })(); }; 33 const remove = document.createElement('button'); 34 remove.textContent = 'Remove'; 35 remove.dataset['removeModule'] = manifest; 36 remove.disabled = busy || checking; 37 remove.onclick = () => { modules.delete(manifest); render(); status.textContent = 'Local module removed. Draft imports will be resolved again.'; changed(); }; 38 row.append(title, code, details, copy, remove); 39 list.append(row); 40 } 41 }; 42 cancel.onclick = () => abort?.(); 43 input.onchange = () => { 44 const file = input.files?.[0]; 45 input.value = ''; 46 if (!file || busy || checking) 47 return; 48 if (!file.size || file.size > BUNDLE_LIMIT) { 49 status.textContent = 'Choose a nonempty .arboricx file of at most 16 MiB.'; 50 return; 51 } 52 checking = true; 53 render(); 54 status.textContent = 'Checking module locally…'; 55 void (async () => { 56 let worker, active = true; 57 let timer; 58 try { 59 const current = new Worker(new URL('./local-module-worker.js', import.meta.url), { type: 'module' }); 60 worker = current; 61 const module = await new Promise((resolve, reject) => { 62 abort = () => reject(new Error('Local file check cancelled.')); 63 timer = setTimeout(() => reject(new Error('Local file check timed out.')), 60_000); 64 current.onmessage = ({ data }) => data.module ? resolve(data.module) : reject(new Error(data.error ?? 'Invalid local module')); 65 current.onerror = event => { event.preventDefault(); reject(new Error(event.message || 'Local file check failed')); }; 66 void file.arrayBuffer().then(bytes => { if (active) 67 current.postMessage(new Uint8Array(bytes)); }).catch(reject); 68 }); 69 if (modules.has(module.manifest)) { 70 status.textContent = 'This module manifest is already available locally.'; 71 return; 72 } 73 const total = [...modules.values()].reduce((n, item) => n + item.module.bytes.byteLength, module.bytes.byteLength); 74 if (modules.size >= LOCAL_MODULE_COUNT || total > LOCAL_MODULE_BYTES) 75 throw new Error('Local collection limit: 32 modules / 64 MiB. Remove a module first.'); 76 modules.set(module.manifest, { label: file.name.slice(0, 256), module }); 77 status.textContent = 'Available locally by manifest hash.'; 78 changed(); 79 } 80 catch (error) { 81 status.textContent = String(error); 82 } 83 finally { 84 active = false; 85 clearTimeout(timer); 86 worker?.terminate(); 87 abort = undefined; 88 checking = false; 89 render(); 90 } 91 })(); 92 }; 93 render(); 94 return { entries: () => [...modules.values()].map(({ label, module }) => ({ label, manifest: module.manifest, names: [...module.names] })), inputs: () => [...modules.values()].map(({ module }) => ({ manifest: module.manifest, bytes: module.bytes })), disable: value => { busy = value; input.disabled = busy || checking; for (const button of list.querySelectorAll('[data-remove-module]')) 95 button.disabled = busy || checking; } }; 96 }