feat(shared): bake xlsx + utif + jszip + docx-preview into every tool

Removes every runtime CDN load. The "ship the record player with the
record" philosophy: a downloaded .html file works offline against any
file the user can open, with no network dependency at runtime.

Newly vendored under shared/vendor/:
  - xlsx.full.min.js (SheetJS, 928 KB) — XLSX/XLS preview
  - utif.min.js     (UTIF, 57 KB)      — TIFF preview

Already there but now used by mdedit too:
  - jszip.min.js, docx-preview.min.js

Call sites updated to drop the `await loadLibrary(URL)` pattern —
since the vendor JS is concatenated into the inline <script> at build
time, window.XLSX / window.JSZip / window.UTIF / window.docx are
available synchronously from page load.

Per-tool changes:

  - archive/build.sh:        +xlsx, +utif
  - classifier/build.sh:     +xlsx, +utif
  - transmittal/build.sh:    +xlsx, +utif
  - mdedit/build.sh:         +jszip, +docx-preview, +xlsx, +utif
                              (mdedit was the only tool not yet
                               bundling any of the preview deps)
  - browse/build.sh:         +utif
  - archive/js/table.js, classifier/js/preview.js,
    transmittal/js/files-preview.js, mdedit/js/file-tree.js (×2):
    drop the `await loadLibrary('…cdn…')` lines.
  - shared/preview-lib.js:
    drop the loadLibrary(UTIF) / loadLibrary(JSZip) wrappers; assume
    window.UTIF and window.JSZip are present.

Net bundle-size delta after baking:
  archive:     +990 KB → ~1.47 MB
  browse:       +57 KB → ~292 KB
  classifier:  +990 KB → ~1.43 MB
  mdedit:    +1100 KB → ~2.09 MB
  transmittal: +990 KB → ~1.63 MB

Docs (AGENTS.md, ARCHITECTURE.md) updated: removed the "runtime CDN
loading exception" paragraph and the table row that flagged xlsx as
CDN-loaded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-05-10 15:09:38 -05:00
parent 7ac2e1cc73
commit d6206b03e7
14 changed files with 1236 additions and 26 deletions

View file

@ -318,7 +318,7 @@ Use `git worktree` to run multiple agents on separate branches simultaneously wi
- Two-phase hydration: `populateStatic()` before publish, `hydrate()` on load of published file - Two-phase hydration: `populateStatic()` before publish, `hydrate()` on load of published file
- Reactive state via Proxy — `app.state.mode = 'view'` auto-notifies subscribers - Reactive state via Proxy — `app.state.mode = 'view'` auto-notifies subscribers
- Runtime CDN loads (jszip, docx-preview, xlsx) are allowed only for the optional DOCX/XLSX preview; core features work offline - No runtime CDN loads. Every vendor library (jszip, docx-preview, xlsx, UTIF, Toast UI) is bundled at build time via `concat_files`. The dist HTML is fully self-contained — "ship the record player with the record."
- Published payload stored in `<script id="transmittal-data" type="application/json">` - Published payload stored in `<script id="transmittal-data" type="application/json">`
## mdedit-specific ## mdedit-specific

View file

@ -210,20 +210,32 @@ Some tools bundle third-party libraries. These live in `tool/vendor/` and are co
|------|---------|------|-------| |------|---------|------|-------|
| mdedit | Toast UI Editor v3.2.2 | `vendor/toastui-editor-all.min.js` | Markdown editor with live preview | | mdedit | Toast UI Editor v3.2.2 | `vendor/toastui-editor-all.min.js` | Markdown editor with live preview |
| mdedit | Toast UI Editor CSS | `vendor/toastui-editor.min.css` | Editor stylesheet | | mdedit | Toast UI Editor CSS | `vendor/toastui-editor.min.css` | Editor stylesheet |
| transmittal | jszip, docx-preview, xlsx | CDN at runtime | Optional preview features; tool works without them | | shared | jszip | `shared/vendor/jszip.min.js` | ZIP read for previews + classifier hash-export |
| shared | docx-preview | `shared/vendor/docx-preview.min.js` | DOCX preview |
| shared | xlsx (SheetJS) | `shared/vendor/xlsx.full.min.js` | XLSX/XLS preview |
| shared | UTIF | `shared/vendor/utif.min.js` | TIFF preview |
**Runtime CDN loading exception**: The transmittal tool loads jszip, docx-preview, and xlsx from CDN at runtime via `loadLibrary()` forDOCX/XLSX preview functionality. These are **optional enhancements**—core transmittal functionality (JSON payload communication) works without them. This exception is documented here because: **No runtime CDN loads.** Every external dependency is vendored into
`shared/vendor/` (or, for mdedit's editor, `mdedit/vendor/`) and
concatenated into each tool's bundle at build time. Tools that need a
given library include the vendor path in their `build.sh`'s
`concat_files` JS list. The "ship the record player with the record"
philosophy: a downloaded `.html` file works offline against any file
the user can open, with no network dependency at runtime.
1. The core transmittal features (creating, signing, verifying SHA-256 digests) do not depend on these libraries Trade-off accepted: bundle sizes are larger. archive, classifier,
2. Preview functionality gracefully degrades if libraries fail to load transmittal land around 1.5 MB after gzip; mdedit lands around 2 MB
3. Bundling would significantly increase file size for rarely-used features because it carries Toast UI + jszip + docx-preview + xlsx + UTIF.
Justified by the offline-first guarantee: any tool downloaded from
`/releases/` works without network, against air-gapped archives,
forever. See ARCHITECTURE.md § "Why Single-File HTML Applications"
for the longer rationale.
**Rule**: Runtime CDN loading is allowed only when: `template.html` for tools with vendor deps loads those deps from CDN
- Features are strictly optional (graceful degradation) purely for **dev convenience** — opening a template.html directly in
- Core functionality works without the external library Chromium gives you a working tool without running a build. The build
- Library is clearly documented as non-essential script strips/replaces those CDN tags so the dist HTML has every
dependency inlined. No CDN URLs survive into the dist.
`template.html` for tools with vendor deps loads those deps from CDN for convenient local development. The build script replaces CDN tags with the bundled vendor files in the output.
### Development vs Production ### Development vs Production

View file

@ -38,6 +38,8 @@ concat_files \
concat_files \ concat_files \
"../shared/vendor/jszip.min.js" \ "../shared/vendor/jszip.min.js" \
"../shared/vendor/docx-preview.min.js" \ "../shared/vendor/docx-preview.min.js" \
"../shared/vendor/xlsx.full.min.js" \
"../shared/vendor/utif.min.js" \
"../shared/zddc.js" \ "../shared/zddc.js" \
"../shared/hash.js" \ "../shared/hash.js" \
"../shared/theme.js" \ "../shared/theme.js" \

View file

@ -632,8 +632,8 @@
if (!container) return; if (!container) return;
try { try {
await loadLibrary('https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js'); // XLSX is bundled into the dist HTML (shared/vendor/xlsx.full.min.js),
// so window.XLSX is available synchronously — no runtime load.
const arrayBuffer = await (file.handle const arrayBuffer = await (file.handle
? file.handle.getFile().then(f => f.arrayBuffer()) ? file.handle.getFile().then(f => f.arrayBuffer())
: fetch(file.url).then(r => r.arrayBuffer())); : fetch(file.url).then(r => r.arrayBuffer()));

View file

@ -34,6 +34,7 @@ concat_files \
# without an external HTTP dependency. # without an external HTTP dependency.
concat_files \ concat_files \
"../shared/vendor/jszip.min.js" \ "../shared/vendor/jszip.min.js" \
"../shared/vendor/utif.min.js" \
"../shared/zddc.js" \ "../shared/zddc.js" \
"../shared/zddc-filter.js" \ "../shared/zddc-filter.js" \
"../shared/theme.js" \ "../shared/theme.js" \

View file

@ -36,6 +36,8 @@ concat_files \
concat_files \ concat_files \
"../shared/vendor/jszip.min.js" \ "../shared/vendor/jszip.min.js" \
"../shared/vendor/docx-preview.min.js" \ "../shared/vendor/docx-preview.min.js" \
"../shared/vendor/xlsx.full.min.js" \
"../shared/vendor/utif.min.js" \
"../shared/zddc.js" \ "../shared/zddc.js" \
"../shared/hash.js" \ "../shared/hash.js" \
"../shared/zddc-source.js" \ "../shared/zddc-source.js" \

View file

@ -403,8 +403,8 @@
if (!container) return; if (!container) return;
try { try {
await loadLibrary('https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js'); // XLSX bundled into the dist HTML; window.XLSX is available
// synchronously, no runtime load needed.
const blob = await getFileBlob(file); const blob = await getFileBlob(file);
const arrayBuffer = await blob.arrayBuffer(); const arrayBuffer = await blob.arrayBuffer();
const workbook = XLSX.read(arrayBuffer, { type: 'array' }); const workbook = XLSX.read(arrayBuffer, { type: 'array' });

View file

@ -41,6 +41,10 @@ concat_files \
# JavaScript files to concatenate in order # JavaScript files to concatenate in order
concat_files \ concat_files \
"../shared/vendor/jszip.min.js" \
"../shared/vendor/docx-preview.min.js" \
"../shared/vendor/xlsx.full.min.js" \
"../shared/vendor/utif.min.js" \
"../shared/zddc.js" \ "../shared/zddc.js" \
"../shared/zddc-source.js" \ "../shared/zddc-source.js" \
"../shared/theme.js" \ "../shared/theme.js" \

View file

@ -630,9 +630,8 @@ async function displayDocxPreview(file, filePath, fileName, fileHandle, lastModi
editorInstances.set(filePath, instanceData); editorInstances.set(filePath, instanceData);
try { try {
await loadLibrary('https://cdn.jsdelivr.net/npm/jszip@3/dist/jszip.min.js'); // jszip + docx-preview bundled into the dist HTML; window.JSZip
await loadLibrary('https://cdn.jsdelivr.net/npm/docx-preview@latest/dist/docx-preview.min.js'); // and window.docx are available synchronously.
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
docxContainer.innerHTML = ''; docxContainer.innerHTML = '';
await window.docx.renderAsync(arrayBuffer, docxContainer); await window.docx.renderAsync(arrayBuffer, docxContainer);
@ -692,8 +691,8 @@ async function displayXlsxPreview(file, filePath, fileName, fileHandle, lastModi
editorInstances.set(filePath, instanceData); editorInstances.set(filePath, instanceData);
try { try {
await loadLibrary('https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js'); // XLSX bundled into the dist HTML; window.XLSX is available
// synchronously, no runtime load needed.
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const workbook = XLSX.read(arrayBuffer, { type: 'array' }); const workbook = XLSX.read(arrayBuffer, { type: 'array' });

View file

@ -119,7 +119,10 @@
opts = opts || {}; opts = opts || {};
injectStyles(doc, 'zddc-tiff-styles', TIFF_CSS); injectStyles(doc, 'zddc-tiff-styles', TIFF_CSS);
return loadLibrary('https://cdn.jsdelivr.net/npm/utif@3.1.0/UTIF.js').then(function () { // UTIF is bundled (shared/vendor/utif.min.js) — window.UTIF is
// available synchronously. Promise.resolve() keeps the existing
// .then() chain shape so callers don't need to change.
return Promise.resolve().then(function () {
var ifds; var ifds;
try { try {
ifds = window.UTIF.decode(arrayBuffer); ifds = window.UTIF.decode(arrayBuffer);
@ -384,9 +387,9 @@
opts = opts || {}; opts = opts || {};
injectStyles(doc, 'zddc-zip-styles', ZIP_CSS); injectStyles(doc, 'zddc-zip-styles', ZIP_CSS);
return loadLibrary('https://cdn.jsdelivr.net/npm/jszip@3/dist/jszip.min.js').then(function () { // JSZip is bundled in every tool that uses preview-lib (each
return window.JSZip.loadAsync(arrayBuffer); // tool's build.sh concatenates shared/vendor/jszip.min.js).
}).then(function (zip) { return window.JSZip.loadAsync(arrayBuffer).then(function (zip) {
var entries = []; var entries = [];
zip.forEach(function (relativePath, zipEntry) { zip.forEach(function (relativePath, zipEntry) {
if (zipEntry.dir) return; if (zipEntry.dir) return;

1160
shared/vendor/utif.min.js vendored Normal file

File diff suppressed because it is too large Load diff

24
shared/vendor/xlsx.full.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -46,6 +46,8 @@ concat_files \
concat_files \ concat_files \
"../shared/vendor/jszip.min.js" \ "../shared/vendor/jszip.min.js" \
"../shared/vendor/docx-preview.min.js" \ "../shared/vendor/docx-preview.min.js" \
"../shared/vendor/xlsx.full.min.js" \
"../shared/vendor/utif.min.js" \
"../shared/zddc.js" \ "../shared/zddc.js" \
"../shared/hash.js" \ "../shared/hash.js" \
"../shared/zddc-source.js" \ "../shared/zddc-source.js" \

View file

@ -216,7 +216,8 @@
return; return;
} }
try { try {
await loadLibrary('https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js'); // XLSX bundled into the dist HTML; window.XLSX is available
// synchronously, no runtime load needed.
var arrayBuffer = await getFileArrayBuffer(file); var arrayBuffer = await getFileArrayBuffer(file);
var workbook = XLSX.read(arrayBuffer, { type: 'array' }); var workbook = XLSX.read(arrayBuffer, { type: 'array' });
container.innerHTML = ''; container.innerHTML = '';