@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.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/CLAUDE.md +87 -0
- package/LICENSE.md +664 -0
- package/README.md +84 -2
- package/dist/dist-manifest.json +218 -0
- package/dist/eyes/index.js +109 -0
- package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
- package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
- package/dist/fat/pkg-node/package.json +12 -0
- package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
- package/dist/frqtl/compat/boot-node.js +34 -0
- package/dist/frqtl/compat/engine.js +127 -0
- package/dist/frqtl/compat/package-root.js +24 -0
- package/dist/frqtl/compat/providers/wasm-engine.js +2499 -0
- package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
- package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
- package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
- package/dist/frqtl/compat/renderer/Filters.js +163 -0
- package/dist/frqtl/compat/renderer/RenderFamily.js +84 -0
- package/dist/frqtl/compat/renderer/RenderMesh.js +99 -0
- package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
- package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
- package/dist/frqtl/compat/renderer/captureStill-common.js +258 -0
- package/dist/frqtl/compat/renderer/display-slot.js +96 -0
- package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
- package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
- package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
- package/dist/frqtl/compat/renderer/providers/index.js +22 -0
- package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
- package/dist/frqtl/compat/renderer/providers/three-provider.js +2470 -0
- package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
- package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
- package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
- package/dist/frqtl/compat/vendor.js +196 -0
- package/dist/frqtl/node-entry.js +17 -0
- package/dist/frqtl/objects/FrqtlApp.js +2920 -0
- package/dist/frqtl/objects/ObjectFactory.js +296 -0
- package/dist/frqtl/objects/RecipeContext.js +322 -0
- package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
- package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
- package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
- package/dist/frqtl/objects/instruments/capture-host.js +453 -0
- package/dist/frqtl/objects/instruments/capture.js +710 -0
- package/dist/frqtl/objects/instruments/hosted-eyes.js +984 -0
- package/dist/frqtl/objects/kinds-entry.js +14 -0
- package/dist/frqtl/objects/kinds.js +190 -0
- package/dist/frqtl/objects/objects/Box.js +273 -0
- package/dist/frqtl/objects/objects/Bullet.js +155 -0
- package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
- package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
- package/dist/frqtl/objects/objects/DirectedTunnel.js +411 -0
- package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
- package/dist/frqtl/objects/objects/LightSource.js +351 -0
- package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
- package/dist/frqtl/objects/objects/Tube.js +471 -0
- package/dist/frqtl/objects/objects/absorbRadiation.js +309 -0
- package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
- package/dist/frqtl/objects/objects/controlAction.js +89 -0
- package/dist/frqtl/objects/objects/controlBind.js +278 -0
- package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
- package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
- package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
- package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
- package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
- package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
- package/dist/frqtl/objects/objects/makeControlsForObject.js +820 -0
- package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
- package/dist/frqtl/objects/objects/slitLinerScatter.js +261 -0
- package/dist/frqtl/objects/objects/sustainedSource.js +298 -0
- package/dist/frqtl/objects/objects/tagger.js +228 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +468 -0
- package/dist/frqtl/objects/schema/index.js +256 -0
- package/dist/frqtl/objects/schema/scopes.js +457 -0
- package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
- package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
- package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
- package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
- package/dist/frqtl/objects/setup/_worldInit.js +162 -0
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
- package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
- package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
- package/dist/frqtl/objects/setup/setupConeEmitter3D.js +250 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit.js +326 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
- package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
- package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
- package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
- package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
- package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
- package/dist/frqtl/objects/setup/setupLightSourceLab.js +277 -0
- package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
- package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
- package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
- package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1258 -0
- package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
- package/dist/frqtl/objects/setup/setupTunnelLab.js +240 -0
- package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
- package/dist/frqtl/objects/setup/setupWaveInterference.js +177 -0
- package/dist/frqtl/objects/v1-replacements.js +29 -0
- package/dist/frqtl/package-core.js +23 -0
- package/dist/frqtl/parts/CameraControls.js +643 -0
- package/dist/frqtl/parts/CameraPath.js +575 -0
- package/dist/frqtl/parts/CollisionActions.js +34 -0
- package/dist/frqtl/parts/CollisionTypes.js +30 -0
- package/dist/frqtl/parts/DetectorWall.js +496 -0
- package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
- package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
- package/dist/frqtl/parts/FrqtlVision.js +818 -0
- package/dist/frqtl/parts/GraphingDetectorWall.js +489 -0
- package/dist/frqtl/parts/LargeObject.js +405 -0
- package/dist/frqtl/parts/LightColor.js +255 -0
- package/dist/frqtl/parts/LiveChart.js +285 -0
- package/dist/frqtl/parts/LogicGate.js +717 -0
- package/dist/frqtl/parts/OvenBox.js +611 -0
- package/dist/frqtl/parts/PointSource.js +762 -0
- package/dist/frqtl/parts/Rectangle.js +200 -0
- package/dist/frqtl/parts/SlitLightSource.js +234 -0
- package/dist/frqtl/parts/SlitScreen.js +628 -0
- package/dist/frqtl/parts/SphereEmitter.js +946 -0
- package/dist/frqtl/parts/Trace.js +508 -0
- package/dist/frqtl/parts/Tunnel.js +1057 -0
- package/dist/frqtl/parts/Wall.js +1211 -0
- package/dist/frqtl/parts/index.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall.js +32 -0
- package/dist/frqtl/scenes/atomic-model.js +88 -0
- package/dist/frqtl/scenes/big-bang.js +46 -0
- package/dist/frqtl/scenes/blackbody.js +81 -0
- package/dist/frqtl/scenes/circuit-sat.js +127 -0
- package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
- package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
- package/dist/frqtl/scenes/counting-wall.js +38 -0
- package/dist/frqtl/scenes/double-slit.js +31 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
- package/dist/frqtl/scenes/dse-one-slit.js +114 -0
- package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +243 -0
- package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
- package/dist/frqtl/scenes/dse-two-slits.js +132 -0
- package/dist/frqtl/scenes/empty.js +14 -0
- package/dist/frqtl/scenes/laser-buildup.js +49 -0
- package/dist/frqtl/scenes/mirror-3d.js +56 -0
- package/dist/frqtl/scenes/mirror-route.js +140 -0
- package/dist/frqtl/scenes/mirror.js +38 -0
- package/dist/frqtl/scenes/omni-3d.js +27 -0
- package/dist/frqtl/scenes/omni-source.js +22 -0
- package/dist/frqtl/scenes/self-interference.js +39 -0
- package/dist/frqtl/scenes/smoking-gun.js +122 -0
- package/dist/frqtl/scenes/straight-walk.js +22 -0
- package/dist/frqtl/scenes/sun.js +39 -0
- package/dist/frqtl/scenes/two-source.js +37 -0
- package/dist/frqtl/scenes/wave.js +26 -0
- package/dist/shim/bundle/frqnb.js +112 -0
- package/dist/shim/bundle/index.js +121 -0
- package/dist/shim/bundle/indexHtml.js +107 -0
- package/dist/shim/bundle/manifest.js +95 -0
- package/dist/shim/bundle/zip.js +243 -0
- package/dist/shim/bus.js +67 -0
- package/dist/shim/controls/index.js +627 -0
- package/dist/shim/controls/registry.js +274 -0
- package/dist/shim/engine/artifact-shell.js +211 -0
- package/dist/shim/engine/boot-phases.js +44 -0
- package/dist/shim/engine/engine-stack-boot.js +59 -0
- package/dist/shim/engine/router.js +690 -0
- package/dist/shim/experiment-frame/index.js +296 -0
- package/dist/shim/expr/ast-schema.js +369 -0
- package/dist/shim/field-descriptor/index.js +325 -0
- package/dist/shim/frame-data/index.js +224 -0
- package/dist/shim/index.js +67 -0
- package/dist/shim/notebook/canvas-guard.js +82 -0
- package/dist/shim/notebook/catalog.js +35 -0
- package/dist/shim/notebook/format.js +736 -0
- package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
- package/dist/shim/notebook/kernel.js +671 -0
- package/dist/shim/notebook/knob.js +150 -0
- package/dist/shim/notebook/module-resolver.js +260 -0
- package/dist/shim/notebook/notebook-knobs.js +65 -0
- package/dist/shim/notebook/produced-bindings.js +67 -0
- package/dist/shim/notebook/realm-registry.js +121 -0
- package/dist/shim/notebook/realm-transport.js +73 -0
- package/dist/shim/notebook/setup-builder.js +507 -0
- package/dist/shim/notebook/store.js +159 -0
- package/dist/shim/notebook/vendor-importmap.js +134 -0
- package/dist/shim/notebook/view-builder.js +189 -0
- package/dist/shim/notebook/view-ctx.js +100 -0
- package/dist/shim/notebook/view-handle.js +202 -0
- package/dist/shim/notebook/warmup.js +102 -0
- package/dist/shim/package.json +4 -0
- package/dist/shim/page-params/index.js +213 -0
- package/dist/shim/project/identity.js +112 -0
- package/dist/shim/project/index.js +19 -0
- package/dist/shim/project/link.js +402 -0
- package/dist/shim/project/node-specifier-hook.mjs +35 -0
- package/dist/shim/project/register-node.mjs +17 -0
- package/dist/shim/project/specifiers.js +106 -0
- package/dist/shim/registry/copy-gate.js +110 -0
- package/dist/shim/registry/exhibits.js +455 -0
- package/dist/shim/registry/experiments.js +215 -0
- package/dist/shim/registry/index.js +19 -0
- package/dist/shim/registry/scene-source.js +157 -0
- package/dist/shim/registry/walkthrough.js +804 -0
- package/dist/shim/render/capture-envelope.js +279 -0
- package/dist/shim/render/heat-colormap.js +92 -0
- package/dist/shim/render/render-stages.js +855 -0
- package/dist/shim/render/sun-look.js +33 -0
- package/dist/shim/render-state/apply.js +262 -0
- package/dist/shim/render-state/index.js +74 -0
- package/dist/shim/rpc/index.js +207 -0
- package/dist/shim/runtime/index.js +1023 -0
- package/dist/shim/storage/boot.js +69 -0
- package/dist/shim/storage/index.js +169 -0
- package/dist/shim/storage/local-storage-provider.js +230 -0
- package/dist/shim/storage/make-namespace-store.js +132 -0
- package/dist/shim/storage/modules.js +249 -0
- package/dist/shim/storage/projects.js +150 -0
- package/llms.txt +12 -0
- package/package.json +63 -4
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/shim/bundle/frqnb.js — the `.frqnb` teaching/gallery projection of a
|
|
5
|
+
* recipe bundle.
|
|
6
|
+
*
|
|
7
|
+
* Builds a notebook OBJECT (never hand-emitted `<cell>` markup) and serializes
|
|
8
|
+
* it THROUGH `lib/shim/notebook/format.js` `serialize` — the ONE notebook
|
|
9
|
+
* serializer (no second serializer, no CELL_ATTRS mirror). The projection is:
|
|
10
|
+
*
|
|
11
|
+
* 1. markdown prose cell — title + teaching blurb.
|
|
12
|
+
* 2. module cell (name './experiment.js') — carries the recipe source verbatim.
|
|
13
|
+
* 3. canvas cell — wired to '<module>' (a recipe is the module's default export), carrying the render
|
|
14
|
+
* intent (stage/seed/render3d/cameraPreset/renderOptions) ferried from the
|
|
15
|
+
* normalized renderState bag.
|
|
16
|
+
*
|
|
17
|
+
* Because it serializes through format.js, ANY new CELL_ATTR added there flows
|
|
18
|
+
* through this projection automatically — the conformance gate asserts it.
|
|
19
|
+
*
|
|
20
|
+
* The canvas cell carries `shape="recipe"` (the CELL_ATTRS discriminator owned by format.js):
|
|
21
|
+
* the module's default export is a RecipeContext recipe (its co-init call + `exp.addObject`), and
|
|
22
|
+
* the shim's setup-builder synthesizes `frqtl.RecipeContext` + `exp.build(buildExperiment)` for it.
|
|
23
|
+
*
|
|
24
|
+
* Layer 2 (physics): none — pure notebook projection.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
import { serialize } from '../notebook/format.js';
|
|
30
|
+
|
|
31
|
+
var MODULE_NAME = './experiment.js';
|
|
32
|
+
var CANVAS_ID = 'c-recipe';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fold a normalized renderState `render` array ([{key,value}]) into a plain
|
|
36
|
+
* `{key: value}` bag for the canvas cell's `renderOptions` (CELL_ATTRS 'opts' kind).
|
|
37
|
+
* @param {{key:string,value:*}[]} render
|
|
38
|
+
* @returns {object}
|
|
39
|
+
*/
|
|
40
|
+
function _renderOptionsBag(render) {
|
|
41
|
+
var bag = {};
|
|
42
|
+
if (Array.isArray(render)) {
|
|
43
|
+
for (var i = 0; i < render.length; i++) {
|
|
44
|
+
var row = render[i];
|
|
45
|
+
if (row && typeof row.key === 'string' && row.key.length > 0) {
|
|
46
|
+
bag[row.key] = row.value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return bag;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build the `.frqnb` notebook string for a recipe bundle.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} args
|
|
57
|
+
* @param {string} args.name — human-readable project name (notebook title + markdown heading).
|
|
58
|
+
* @param {string} args.source — the `experiment.js` recipe source (module cell body).
|
|
59
|
+
* @param {object} [args.renderState] — a NORMALIZED renderState bag (from
|
|
60
|
+
* `lib/shim/render-state` normalizeSavedRenderState). Render intent is ferried
|
|
61
|
+
* onto the canvas cell; absent → the canvas carries stage/seed defaults only.
|
|
62
|
+
* @param {string} [args.stage='600x400'] — canvas stage '<W>x<H>'.
|
|
63
|
+
* @param {string} [args.prose] — optional teaching blurb appended under the heading.
|
|
64
|
+
* @returns {string} the serialized `.frqnb` notebook.
|
|
65
|
+
*/
|
|
66
|
+
export function buildFrqnb(args) {
|
|
67
|
+
args = args || {};
|
|
68
|
+
if (typeof args.source !== 'string' || args.source.length === 0) {
|
|
69
|
+
throw new Error('[bundle/frqnb] buildFrqnb: args.source (experiment.js) is required.');
|
|
70
|
+
}
|
|
71
|
+
var name = (typeof args.name === 'string' && args.name.length > 0) ? args.name : 'FRQTL Recipe';
|
|
72
|
+
var rs = args.renderState || {};
|
|
73
|
+
var stage = (typeof args.stage === 'string' && args.stage.length > 0) ? args.stage : '600x400';
|
|
74
|
+
var seed = (rs.seed != null && rs.seed !== '') ? String(rs.seed) : '42';
|
|
75
|
+
|
|
76
|
+
var markdownBody = '# ' + name + '\n\n' +
|
|
77
|
+
(typeof args.prose === 'string' && args.prose.length > 0
|
|
78
|
+
? args.prose
|
|
79
|
+
: 'A FRQTL recipe. The module cell below is the `experiment.js` source of truth; ' +
|
|
80
|
+
'the canvas runs it live.');
|
|
81
|
+
|
|
82
|
+
var canvasCell = {
|
|
83
|
+
type: 'canvas',
|
|
84
|
+
id: CANVAS_ID,
|
|
85
|
+
setup: MODULE_NAME,
|
|
86
|
+
// The module's DEFAULT export is a `buildExperiment(exp)` RECIPE (RecipeContext
|
|
87
|
+
// form), NOT a `(frqtl,universe,options)` setup factory — declare it so the
|
|
88
|
+
// shim synthesizes the RecipeContext path (a recipe canvas names no export).
|
|
89
|
+
shape: 'recipe',
|
|
90
|
+
stage: stage,
|
|
91
|
+
seed: seed
|
|
92
|
+
};
|
|
93
|
+
// Ferry render intent from the normalized renderState bag onto the canvas cell.
|
|
94
|
+
if (rs.renderer === 'three') { canvasCell.render3d = true; } // three IS the 3D display
|
|
95
|
+
if (rs.camera && typeof rs.camera.preset === 'string' && rs.camera.preset.length > 0) {
|
|
96
|
+
canvasCell.cameraPreset = rs.camera.preset;
|
|
97
|
+
}
|
|
98
|
+
var renderOptions = _renderOptionsBag(rs.render);
|
|
99
|
+
if (Object.keys(renderOptions).length > 0) { canvasCell.renderOptions = renderOptions; }
|
|
100
|
+
|
|
101
|
+
var notebook = {
|
|
102
|
+
version: '1',
|
|
103
|
+
title: name,
|
|
104
|
+
cells: [
|
|
105
|
+
{ type: 'markdown', body: markdownBody },
|
|
106
|
+
{ type: 'module', name: MODULE_NAME, body: args.source },
|
|
107
|
+
canvasCell
|
|
108
|
+
]
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return serialize(notebook);
|
|
112
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/shim/bundle/index.js — the recipe-bundle assembler + importer
|
|
5
|
+
* The canonical owner of the bundle format
|
|
6
|
+
* (design §2): a folder that zips for download —
|
|
7
|
+
*
|
|
8
|
+
* experiment.js ← THE source of truth (the buildExperiment(exp) recipe)
|
|
9
|
+
* experiment.frqnb ← teaching/gallery projection (notebook/format.serialize)
|
|
10
|
+
* index.html ← standalone host (@descix/frqtl-web bootProject over the served folder)
|
|
11
|
+
* manifest.json ← Module envelope minus `source` + bundle-only fields (E1)
|
|
12
|
+
* preview.png ← optional golden screenshot
|
|
13
|
+
*
|
|
14
|
+
* PURE + DOM-free: emits/consumes `Uint8Array`. STUDIO's export button and any
|
|
15
|
+
* future DeSciX publisher CONSUME this — passing the generated `experiment.js`
|
|
16
|
+
* source + envelope + renderState DOWN (this module never imports codegen/
|
|
17
|
+
* save-state — those live above the shim; the arrow stays one-way). The browser
|
|
18
|
+
* `Blob`+`a.click` download trigger is a THIN feature-side helper.
|
|
19
|
+
*
|
|
20
|
+
* Layer 2 (physics): none — pure format/packaging.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
import { buildManifest, manifestToEnvelope } from './manifest.js';
|
|
26
|
+
import { buildFrqnb } from './frqnb.js';
|
|
27
|
+
import { buildIndexHtml } from './indexHtml.js';
|
|
28
|
+
import { zipStore, unzipStore, textBytes, bytesText } from './zip.js';
|
|
29
|
+
import { normalizeSavedRenderState } from '../render-state/index.js';
|
|
30
|
+
|
|
31
|
+
// ── Canonical bundle file names ──────────────────────────────────────────────
|
|
32
|
+
export var FILE_EXPERIMENT_JS = 'experiment.js';
|
|
33
|
+
export var FILE_EXPERIMENT_FRQNB = 'experiment.frqnb';
|
|
34
|
+
export var FILE_INDEX_HTML = 'index.html';
|
|
35
|
+
export var FILE_MANIFEST_JSON = 'manifest.json';
|
|
36
|
+
export var FILE_PREVIEW_PNG = 'preview.png';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Assemble a recipe bundle from a Module envelope + bundle inputs.
|
|
40
|
+
*
|
|
41
|
+
* Conceptually the dispatch's `(source, envelopeMeta, renderState, previewPngBytes,
|
|
42
|
+
* sdkVersion)`: here `source` + `renderState` + `envelopeMeta` arrive together AS
|
|
43
|
+
* the Module envelope (the exact shape STUDIO's Save path already builds), so the
|
|
44
|
+
* bundle↔envelope round-trip is a clean identity. `manifest` ferries the envelope
|
|
45
|
+
* minus `source` (E1) — no field re-enumeration.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} envelope — a `lib/shim/storage/modules.js` Module envelope
|
|
48
|
+
* (`{id,name,source,renderState,…}`; `source` = the recipe, REQUIRED).
|
|
49
|
+
* @param {object} opts
|
|
50
|
+
* @param {string} opts.sdkVersion — REQUIRED CDN pin (derived, never a literal).
|
|
51
|
+
* @param {Uint8Array|null} [opts.previewPngBytes] — preview image bytes, or null.
|
|
52
|
+
* @param {string} [opts.stage='600x400'] — canvas stage for the .frqnb projection.
|
|
53
|
+
* @param {string} [opts.prose] — optional teaching blurb for the .frqnb markdown.
|
|
54
|
+
* @returns {{files: {name:string, bytes:Uint8Array}[], zipBytes: Uint8Array}}
|
|
55
|
+
*/
|
|
56
|
+
export function assembleBundle(envelope, opts) {
|
|
57
|
+
if (!envelope || typeof envelope !== 'object') {
|
|
58
|
+
throw new Error('[bundle] assembleBundle(envelope): envelope must be an object.');
|
|
59
|
+
}
|
|
60
|
+
if (typeof envelope.source !== 'string' || envelope.source.length === 0) {
|
|
61
|
+
throw new Error('[bundle] assembleBundle: envelope.source (the recipe) is required.');
|
|
62
|
+
}
|
|
63
|
+
opts = opts || {};
|
|
64
|
+
var preview = (opts.previewPngBytes instanceof Uint8Array && opts.previewPngBytes.length > 0)
|
|
65
|
+
? opts.previewPngBytes : null;
|
|
66
|
+
|
|
67
|
+
var manifest = buildManifest(envelope, {
|
|
68
|
+
sdkVersion: opts.sdkVersion,
|
|
69
|
+
preview: preview ? FILE_PREVIEW_PNG : null
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
var normalizedRs = normalizeSavedRenderState(envelope.renderState);
|
|
73
|
+
|
|
74
|
+
var files = [
|
|
75
|
+
{ name: FILE_EXPERIMENT_JS, bytes: textBytes(envelope.source) },
|
|
76
|
+
{ name: FILE_EXPERIMENT_FRQNB, bytes: textBytes(buildFrqnb({
|
|
77
|
+
name: envelope.name, source: envelope.source,
|
|
78
|
+
renderState: normalizedRs, stage: opts.stage, prose: opts.prose
|
|
79
|
+
})) },
|
|
80
|
+
{ name: FILE_INDEX_HTML, bytes: textBytes(buildIndexHtml({
|
|
81
|
+
name: envelope.name, sdkVersion: opts.sdkVersion, renderState: normalizedRs,
|
|
82
|
+
scripts: Array.isArray(envelope.scripts) ? envelope.scripts : [] // the envelope's setup-factory delta
|
|
83
|
+
})) },
|
|
84
|
+
{ name: FILE_MANIFEST_JSON, bytes: textBytes(JSON.stringify(manifest, null, 2)) }
|
|
85
|
+
];
|
|
86
|
+
if (preview) { files.push({ name: FILE_PREVIEW_PNG, bytes: preview }); }
|
|
87
|
+
|
|
88
|
+
return { files: files, zipBytes: zipStore(files) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Import a recipe bundle: recover the Module envelope (source re-attached from
|
|
93
|
+
* `experiment.js` per E1) + the preview bytes.
|
|
94
|
+
*
|
|
95
|
+
* @param {Uint8Array} zipBytes — a bundle `.zip` (from `assembleBundle`).
|
|
96
|
+
* @returns {{envelope: object, previewPngBytes: Uint8Array|null}}
|
|
97
|
+
*/
|
|
98
|
+
export function importBundle(zipBytes) {
|
|
99
|
+
var entries = unzipStore(zipBytes);
|
|
100
|
+
var byName = Object.create(null);
|
|
101
|
+
for (var i = 0; i < entries.length; i++) { byName[entries[i].name] = entries[i].bytes; }
|
|
102
|
+
|
|
103
|
+
if (!byName[FILE_MANIFEST_JSON]) {
|
|
104
|
+
throw new Error('[bundle] importBundle: ' + FILE_MANIFEST_JSON + ' missing from the archive.');
|
|
105
|
+
}
|
|
106
|
+
if (!byName[FILE_EXPERIMENT_JS]) {
|
|
107
|
+
throw new Error('[bundle] importBundle: ' + FILE_EXPERIMENT_JS + ' missing from the archive.');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
var manifest;
|
|
111
|
+
try {
|
|
112
|
+
manifest = JSON.parse(bytesText(byName[FILE_MANIFEST_JSON]));
|
|
113
|
+
} catch (e) {
|
|
114
|
+
throw new Error('[bundle] importBundle: ' + FILE_MANIFEST_JSON + ' is not valid JSON — ' + (e && e.message));
|
|
115
|
+
}
|
|
116
|
+
var source = bytesText(byName[FILE_EXPERIMENT_JS]);
|
|
117
|
+
var envelope = manifestToEnvelope(manifest, source);
|
|
118
|
+
var previewPngBytes = byName[FILE_PREVIEW_PNG] || null;
|
|
119
|
+
|
|
120
|
+
return { envelope: envelope, previewPngBytes: previewPngBytes };
|
|
121
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/shim/bundle/indexHtml.js — the standalone `index.html` projection of a
|
|
5
|
+
* recipe bundle.
|
|
6
|
+
*
|
|
7
|
+
* A byte-template host that boots the bundle folder as a SERVED PROJECT: `bootProject(mount,
|
|
8
|
+
* { url: './', entry: 'experiment.js' }, { renderState })` from `@descix/frqtl-web@<pin>` on the CDN. The
|
|
9
|
+
* recipe is a sketch module whose imports name the published self-reference (`@descix/frqtl-web/...`);
|
|
10
|
+
* the page's import map carries those rows and the scene doors' row, rendered by the ONE table
|
|
11
|
+
* (`pageImportMapFor(<the pinned dist root>/)`, lib/shim/project/specifiers.js), so the sketch resolves
|
|
12
|
+
* to the SAME module URLs the boot loads. The pinned dist root is the router's one spelling
|
|
13
|
+
* (`cdnDistBaseFor`); the `@<pin>` is the passed-in `sdkVersion` (DERIVED at export time — never a
|
|
14
|
+
* literal here).
|
|
15
|
+
*
|
|
16
|
+
* DRY: `experiment.js` stays the ONE source of truth — the page links it where it is served rather than
|
|
17
|
+
* embedding a second copy. `renderState` (small, manifest-derived) is embedded inline as JSON. A page
|
|
18
|
+
* opened via `file://` cannot fetch the project: the error says to serve the folder over http.
|
|
19
|
+
*
|
|
20
|
+
* Layer 2 (physics): none — pure host projection.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
import { pageImportMapFor, FRQTL_WEB_BOOT } from '../project/specifiers.js';
|
|
26
|
+
import { cdnDistBaseFor } from '../engine/router.js';
|
|
27
|
+
|
|
28
|
+
function _escapeHtml(s) {
|
|
29
|
+
return String(s == null ? '' : s)
|
|
30
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Safe inline-JSON: escape `<` so a `</script>`-like payload can't break out. */
|
|
34
|
+
function _inlineJson(obj) {
|
|
35
|
+
return JSON.stringify(obj == null ? null : obj).replace(/</g, '\\u003c');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build the standalone `index.html` for a recipe bundle.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} args
|
|
42
|
+
* @param {string} args.name — project name (page title).
|
|
43
|
+
* @param {string} args.sdkVersion — the `@descix/frqtl-web@<pin>` version (REQUIRED).
|
|
44
|
+
* @param {object} [args.renderState] — normalized renderState bag (inlined as JSON;
|
|
45
|
+
* forwarded to bootProject's `{renderState}`).
|
|
46
|
+
* @param {string[]} [args.scripts] — the v4 envelope's setup-factory delta (inlined as JSON): nothing
|
|
47
|
+
* classic is left to load, so the page REFUSES a non-empty list at boot,
|
|
48
|
+
* naming the fix (import the factory in the sketch).
|
|
49
|
+
* @param {string} [args.recipeFile='experiment.js'] — the project's entry file.
|
|
50
|
+
* @returns {string} the complete `index.html`.
|
|
51
|
+
*/
|
|
52
|
+
export function buildIndexHtml(args) {
|
|
53
|
+
args = args || {};
|
|
54
|
+
if (typeof args.sdkVersion !== 'string' || args.sdkVersion.length === 0) {
|
|
55
|
+
throw new Error('[bundle/indexHtml] buildIndexHtml: args.sdkVersion is REQUIRED (the CDN pin).');
|
|
56
|
+
}
|
|
57
|
+
var name = (typeof args.name === 'string' && args.name.length > 0) ? args.name : 'FRQTL Recipe';
|
|
58
|
+
var recipeFile = (typeof args.recipeFile === 'string' && args.recipeFile.length > 0)
|
|
59
|
+
? args.recipeFile : 'experiment.js';
|
|
60
|
+
var importMapJson = _inlineJson(pageImportMapFor(cdnDistBaseFor(args.sdkVersion) + '/'));
|
|
61
|
+
var renderStateJson = _inlineJson(args.renderState || null);
|
|
62
|
+
var scriptsJson = _inlineJson(Array.isArray(args.scripts) ? args.scripts : []);
|
|
63
|
+
|
|
64
|
+
return [
|
|
65
|
+
'<!DOCTYPE html>',
|
|
66
|
+
'<html lang="en">',
|
|
67
|
+
'<head>',
|
|
68
|
+
' <meta charset="UTF-8">',
|
|
69
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
70
|
+
' <title>' + _escapeHtml(name) + ' — FRQTL</title>',
|
|
71
|
+
' <style>',
|
|
72
|
+
' html,body{margin:0;height:100%;background:#0b0b0f;color:#ddd;',
|
|
73
|
+
' font:14px/1.5 ui-sans-serif,system-ui,sans-serif;}',
|
|
74
|
+
' #frqtl-mount{position:relative;width:100vw;height:100vh;}',
|
|
75
|
+
' #frqtl-err{position:absolute;inset:0;padding:16px;white-space:pre-wrap;',
|
|
76
|
+
' font:12px/1.5 ui-monospace,Menlo,Consolas,monospace;color:#ff6b6b;',
|
|
77
|
+
' background:#1a0000;display:none;overflow:auto;}',
|
|
78
|
+
' </style>',
|
|
79
|
+
'</head>',
|
|
80
|
+
'<body>',
|
|
81
|
+
' <div id="frqtl-mount"></div>',
|
|
82
|
+
' <pre id="frqtl-err"></pre>',
|
|
83
|
+
' <script type="importmap">' + importMapJson + '</script>',
|
|
84
|
+
' <script type="module">',
|
|
85
|
+
' import { bootProject } from ' + JSON.stringify(FRQTL_WEB_BOOT.specifier) + ';',
|
|
86
|
+
' const RENDER_STATE = ' + renderStateJson + ';',
|
|
87
|
+
' const SCRIPTS = ' + scriptsJson + ';',
|
|
88
|
+
' const mount = document.getElementById("frqtl-mount");',
|
|
89
|
+
' const errBox = document.getElementById("frqtl-err");',
|
|
90
|
+
' function fail(title, detail){',
|
|
91
|
+
' errBox.style.display = "block";',
|
|
92
|
+
' errBox.textContent = title + "\\n\\n" + detail;',
|
|
93
|
+
' }',
|
|
94
|
+
' if (SCRIPTS.length > 0) {',
|
|
95
|
+
' fail("FRQTL project boot refused", "scripts " + JSON.stringify(SCRIPTS) + " is a classic setup-factory delta, and nothing classic is left to load — import the factory in the sketch (it imports what it uses from @descix/frqtl-web/...).");',
|
|
96
|
+
' } else if (location.protocol === "file:") {',
|
|
97
|
+
' fail("Serve this folder over http", "e.g. `npx serve` — a file:// page cannot fetch ' + _escapeHtml(recipeFile) + '.");',
|
|
98
|
+
' } else {',
|
|
99
|
+
' bootProject(mount, { url: new URL("./", location.href).href, entry: ' + JSON.stringify(recipeFile) + ' }, { renderState: RENDER_STATE })',
|
|
100
|
+
' .catch((e) => fail("FRQTL project boot failed", String(e && e.stack || e)));',
|
|
101
|
+
' }',
|
|
102
|
+
' </script>',
|
|
103
|
+
'</body>',
|
|
104
|
+
'</html>',
|
|
105
|
+
''
|
|
106
|
+
].join('\n');
|
|
107
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/shim/bundle/manifest.js — the bundle manifest as an ENVELOPE FERRY
|
|
5
|
+
* (E1).
|
|
6
|
+
*
|
|
7
|
+
* ── The rule (E1): manifest = the Module envelope minus `source` ─────────────
|
|
8
|
+
*
|
|
9
|
+
* The bundle is a projection of the `lib/shim/storage/modules.js` Module
|
|
10
|
+
* envelope. `experiment.js` = `module.source`. The manifest carries EVERYTHING
|
|
11
|
+
* ELSE from the envelope, ferried VERBATIM (`{...envelope}` minus `source`) plus
|
|
12
|
+
* a small set of bundle-only fields. Critically this module NEVER re-enumerates
|
|
13
|
+
* the envelope's field list (`{name: mod.name, renderState: mod.renderState, …}`)
|
|
14
|
+
* — that would be a schema mirror of modules.js. It spreads the whole envelope
|
|
15
|
+
* and deletes the ONE key (`source`) the sibling `experiment.js` file owns. Any
|
|
16
|
+
* field modules.js adds in future rides through automatically; `renderState`
|
|
17
|
+
* rides through as an ordinary envelope field, opaque (never parsed here).
|
|
18
|
+
*
|
|
19
|
+
* ── Bundle-only fields ───────────────────────────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* sdkVersion : string — the `@descix/frqtl-web@<pin>` the index.html imports.
|
|
22
|
+
* A VALUE passed IN by the caller (the feature side
|
|
23
|
+
* DERIVES it from the built SDK's package.json at export
|
|
24
|
+
* time — truth-in-labeling; NEVER a literal in bundle
|
|
25
|
+
* code). REQUIRED — FAIL-LOUD when absent.
|
|
26
|
+
* preview : string|null — the preview image filename ref (e.g. 'preview.png'),
|
|
27
|
+
* or null when no preview was captured.
|
|
28
|
+
* descix : {} — reserved inert publish block (DeSciX now-vs-later).
|
|
29
|
+
*
|
|
30
|
+
* Layer 2 (physics): none — pure metadata projection.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
/** The envelope key the sibling `experiment.js` file owns (never in the manifest). */
|
|
36
|
+
export var SOURCE_KEY = 'source';
|
|
37
|
+
|
|
38
|
+
/** Bundle-only manifest keys (NOT envelope fields — stripped on import). */
|
|
39
|
+
export var BUNDLE_ONLY_KEYS = ['sdkVersion', 'preview', 'descix'];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build the manifest object from a Module envelope + bundle-only inputs.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} envelope — a `lib/shim/storage/modules.js` Module envelope
|
|
45
|
+
* (MUST carry `source`; every other field is ferried verbatim).
|
|
46
|
+
* @param {object} opts
|
|
47
|
+
* @param {string} opts.sdkVersion — REQUIRED; the CDN pin (e.g. '0.6.0-p0.2').
|
|
48
|
+
* @param {string|null} [opts.preview] — preview filename ref, or null/omitted.
|
|
49
|
+
* @returns {object} the manifest (JSON-serializable).
|
|
50
|
+
*/
|
|
51
|
+
export function buildManifest(envelope, opts) {
|
|
52
|
+
if (!envelope || typeof envelope !== 'object') {
|
|
53
|
+
throw new Error('[bundle/manifest] buildManifest(envelope): envelope must be an object.');
|
|
54
|
+
}
|
|
55
|
+
opts = opts || {};
|
|
56
|
+
if (typeof opts.sdkVersion !== 'string' || opts.sdkVersion.length === 0) {
|
|
57
|
+
throw new Error('[bundle/manifest] buildManifest: opts.sdkVersion is REQUIRED (a derived value, ' +
|
|
58
|
+
'never a literal) — the index.html CDN pin depends on it.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Ferry the WHOLE envelope minus `source` (no per-field re-enumeration).
|
|
62
|
+
var manifest = Object.assign({}, envelope);
|
|
63
|
+
delete manifest[SOURCE_KEY];
|
|
64
|
+
|
|
65
|
+
// Bundle-only fields.
|
|
66
|
+
manifest.sdkVersion = opts.sdkVersion;
|
|
67
|
+
manifest.preview = (opts.preview != null) ? opts.preview : null;
|
|
68
|
+
manifest.descix = {}; // reserved inert publish block
|
|
69
|
+
|
|
70
|
+
return manifest;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Recover the Module envelope from a manifest + the `experiment.js` source.
|
|
75
|
+
* Inverse of `buildManifest`: strips the bundle-only fields and re-attaches
|
|
76
|
+
* `source`. The result is a full Module envelope (validatable by modules.js).
|
|
77
|
+
*
|
|
78
|
+
* @param {object} manifest — the parsed manifest.json object.
|
|
79
|
+
* @param {string} source — the `experiment.js` recipe source.
|
|
80
|
+
* @returns {object} the reconstructed Module envelope.
|
|
81
|
+
*/
|
|
82
|
+
export function manifestToEnvelope(manifest, source) {
|
|
83
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
84
|
+
throw new Error('[bundle/manifest] manifestToEnvelope(manifest): manifest must be an object.');
|
|
85
|
+
}
|
|
86
|
+
if (typeof source !== 'string') {
|
|
87
|
+
throw new Error('[bundle/manifest] manifestToEnvelope: source (experiment.js) must be a string.');
|
|
88
|
+
}
|
|
89
|
+
var envelope = Object.assign({}, manifest);
|
|
90
|
+
for (var i = 0; i < BUNDLE_ONLY_KEYS.length; i++) {
|
|
91
|
+
delete envelope[BUNDLE_ONLY_KEYS[i]];
|
|
92
|
+
}
|
|
93
|
+
envelope[SOURCE_KEY] = source;
|
|
94
|
+
return envelope;
|
|
95
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/shim/bundle/zip.js — a hand-rolled STORE-only ZIP writer + reader
|
|
5
|
+
* (R2).
|
|
6
|
+
*
|
|
7
|
+
* ── Why hand-rolled + STORE-only ─────────────────────────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* A bundle folder must download as ONE `.zip`. `jszip` was REJECTED (any dep
|
|
10
|
+
* reachable from features ships in the sdk dist — supply-chain surface in the
|
|
11
|
+
* lean shim). STORE (no compression, method 0) needs no deflate implementation,
|
|
12
|
+
* is trivially readable by every standard unzip, and bundles are small text
|
|
13
|
+
* files + one PNG. This module is PURE + DOM-free: it emits/consumes
|
|
14
|
+
* `Uint8Array` only. The browser download trigger (`Blob` + `a.click`) is a THIN
|
|
15
|
+
* feature-side helper (`lib/features/ide` — shim = bytes, feature = browser UX).
|
|
16
|
+
*
|
|
17
|
+
* ── Contract ─────────────────────────────────────────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* zipStore(entries) → Uint8Array
|
|
20
|
+
* entries: [{ name: string, bytes: Uint8Array }] (name = archive path)
|
|
21
|
+
*
|
|
22
|
+
* unzipStore(bytes) → [{ name: string, bytes: Uint8Array }]
|
|
23
|
+
* Reads a STORE-only archive (throws FAIL-LOUD on a compressed entry, a bad
|
|
24
|
+
* signature, or a CRC mismatch — never silently returns wrong bytes).
|
|
25
|
+
*
|
|
26
|
+
* textBytes(str) / bytesText(u8) — UTF-8 encode/decode helpers (a text
|
|
27
|
+
* bundle file's bytes ARE its UTF-8 encoding).
|
|
28
|
+
*
|
|
29
|
+
* Layer 2 (physics): none — pure byte format.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
var _enc = new TextEncoder();
|
|
35
|
+
var _dec = new TextDecoder();
|
|
36
|
+
|
|
37
|
+
/** @param {string} s @returns {Uint8Array} */
|
|
38
|
+
export function textBytes(s) { return _enc.encode(s == null ? '' : String(s)); }
|
|
39
|
+
|
|
40
|
+
/** @param {Uint8Array} u8 @returns {string} */
|
|
41
|
+
export function bytesText(u8) { return _dec.decode(u8); }
|
|
42
|
+
|
|
43
|
+
// ── CRC-32 (IEEE 802.3, standard ZIP polynomial 0xEDB88320) ──────────────────
|
|
44
|
+
|
|
45
|
+
var _CRC_TABLE = (function () {
|
|
46
|
+
var t = new Uint32Array(256);
|
|
47
|
+
for (var n = 0; n < 256; n++) {
|
|
48
|
+
var c = n;
|
|
49
|
+
for (var k = 0; k < 8; k++) {
|
|
50
|
+
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
|
|
51
|
+
}
|
|
52
|
+
t[n] = c >>> 0;
|
|
53
|
+
}
|
|
54
|
+
return t;
|
|
55
|
+
})();
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {Uint8Array} bytes
|
|
59
|
+
* @returns {number} unsigned 32-bit CRC.
|
|
60
|
+
*/
|
|
61
|
+
export function crc32(bytes) {
|
|
62
|
+
var crc = 0xFFFFFFFF;
|
|
63
|
+
for (var i = 0; i < bytes.length; i++) {
|
|
64
|
+
crc = (crc >>> 8) ^ _CRC_TABLE[(crc ^ bytes[i]) & 0xFF];
|
|
65
|
+
}
|
|
66
|
+
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── little-endian writers ────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function _u16(v) { return [v & 0xFF, (v >>> 8) & 0xFF]; }
|
|
72
|
+
function _u32(v) {
|
|
73
|
+
v = v >>> 0;
|
|
74
|
+
return [v & 0xFF, (v >>> 8) & 0xFF, (v >>> 16) & 0xFF, (v >>> 24) & 0xFF];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _concat(chunks) {
|
|
78
|
+
var total = 0, i;
|
|
79
|
+
for (i = 0; i < chunks.length; i++) { total += chunks[i].length; }
|
|
80
|
+
var out = new Uint8Array(total);
|
|
81
|
+
var off = 0;
|
|
82
|
+
for (i = 0; i < chunks.length; i++) { out.set(chunks[i], off); off += chunks[i].length; }
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── little-endian readers ────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
function _readU16(b, o) { return b[o] | (b[o + 1] << 8); }
|
|
89
|
+
function _readU32(b, o) { return (b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24)) >>> 0; }
|
|
90
|
+
|
|
91
|
+
var LOCAL_SIG = 0x04034b50;
|
|
92
|
+
var CENTRAL_SIG = 0x02014b50;
|
|
93
|
+
var EOCD_SIG = 0x06054b50;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build a STORE-only ZIP archive from a list of named byte entries.
|
|
97
|
+
*
|
|
98
|
+
* @param {{name:string, bytes:Uint8Array}[]} entries
|
|
99
|
+
* @returns {Uint8Array} the complete `.zip` bytes.
|
|
100
|
+
*/
|
|
101
|
+
export function zipStore(entries) {
|
|
102
|
+
if (!Array.isArray(entries)) {
|
|
103
|
+
throw new Error('[bundle/zip] zipStore(entries): entries must be an array.');
|
|
104
|
+
}
|
|
105
|
+
var localChunks = [];
|
|
106
|
+
var centralChunks = [];
|
|
107
|
+
var offset = 0; // running offset of each local header
|
|
108
|
+
|
|
109
|
+
for (var i = 0; i < entries.length; i++) {
|
|
110
|
+
var e = entries[i];
|
|
111
|
+
if (!e || typeof e.name !== 'string' || !(e.bytes instanceof Uint8Array)) {
|
|
112
|
+
throw new Error('[bundle/zip] zipStore: entry ' + i + ' must be {name:string, bytes:Uint8Array}.');
|
|
113
|
+
}
|
|
114
|
+
var nameBytes = textBytes(e.name);
|
|
115
|
+
var data = e.bytes;
|
|
116
|
+
var crc = crc32(data);
|
|
117
|
+
var size = data.length;
|
|
118
|
+
|
|
119
|
+
// Local file header.
|
|
120
|
+
var local = _concat([
|
|
121
|
+
new Uint8Array(_u32(LOCAL_SIG)),
|
|
122
|
+
new Uint8Array(_u16(20)), // version needed to extract (2.0)
|
|
123
|
+
new Uint8Array(_u16(0)), // general purpose bit flag
|
|
124
|
+
new Uint8Array(_u16(0)), // compression method: 0 = STORE
|
|
125
|
+
new Uint8Array(_u16(0)), // last mod time (fixed 0 — deterministic)
|
|
126
|
+
new Uint8Array(_u16(0)), // last mod date (fixed 0 — deterministic)
|
|
127
|
+
new Uint8Array(_u32(crc)),
|
|
128
|
+
new Uint8Array(_u32(size)), // compressed size (== uncompressed for STORE)
|
|
129
|
+
new Uint8Array(_u32(size)), // uncompressed size
|
|
130
|
+
new Uint8Array(_u16(nameBytes.length)),
|
|
131
|
+
new Uint8Array(_u16(0)), // extra field length
|
|
132
|
+
nameBytes,
|
|
133
|
+
data
|
|
134
|
+
]);
|
|
135
|
+
localChunks.push(local);
|
|
136
|
+
|
|
137
|
+
// Central directory record (references this local header's offset).
|
|
138
|
+
var central = _concat([
|
|
139
|
+
new Uint8Array(_u32(CENTRAL_SIG)),
|
|
140
|
+
new Uint8Array(_u16(20)), // version made by
|
|
141
|
+
new Uint8Array(_u16(20)), // version needed
|
|
142
|
+
new Uint8Array(_u16(0)), // flags
|
|
143
|
+
new Uint8Array(_u16(0)), // method: STORE
|
|
144
|
+
new Uint8Array(_u16(0)), // time
|
|
145
|
+
new Uint8Array(_u16(0)), // date
|
|
146
|
+
new Uint8Array(_u32(crc)),
|
|
147
|
+
new Uint8Array(_u32(size)),
|
|
148
|
+
new Uint8Array(_u32(size)),
|
|
149
|
+
new Uint8Array(_u16(nameBytes.length)),
|
|
150
|
+
new Uint8Array(_u16(0)), // extra length
|
|
151
|
+
new Uint8Array(_u16(0)), // comment length
|
|
152
|
+
new Uint8Array(_u16(0)), // disk number start
|
|
153
|
+
new Uint8Array(_u16(0)), // internal attrs
|
|
154
|
+
new Uint8Array(_u32(0)), // external attrs
|
|
155
|
+
new Uint8Array(_u32(offset)), // local header offset
|
|
156
|
+
nameBytes
|
|
157
|
+
]);
|
|
158
|
+
centralChunks.push(central);
|
|
159
|
+
|
|
160
|
+
offset += local.length;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
var localBlob = _concat(localChunks);
|
|
164
|
+
var centralBlob = _concat(centralChunks);
|
|
165
|
+
var eocd = _concat([
|
|
166
|
+
new Uint8Array(_u32(EOCD_SIG)),
|
|
167
|
+
new Uint8Array(_u16(0)), // disk number
|
|
168
|
+
new Uint8Array(_u16(0)), // disk with central dir
|
|
169
|
+
new Uint8Array(_u16(entries.length)), // CD records on this disk
|
|
170
|
+
new Uint8Array(_u16(entries.length)), // total CD records
|
|
171
|
+
new Uint8Array(_u32(centralBlob.length)), // central dir size
|
|
172
|
+
new Uint8Array(_u32(localBlob.length)), // central dir offset (== size of all local records)
|
|
173
|
+
new Uint8Array(_u16(0)) // comment length
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
return _concat([localBlob, centralBlob, eocd]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Read a STORE-only ZIP archive. Walks the central directory (the canonical
|
|
181
|
+
* index), resolves each entry's local header, and extracts its stored bytes.
|
|
182
|
+
*
|
|
183
|
+
* FAIL-LOUD: throws on a missing EOCD, a bad central/local signature, a
|
|
184
|
+
* non-STORE compression method, or a CRC-32 mismatch — never returns wrong bytes.
|
|
185
|
+
*
|
|
186
|
+
* @param {Uint8Array} bytes
|
|
187
|
+
* @returns {{name:string, bytes:Uint8Array}[]}
|
|
188
|
+
*/
|
|
189
|
+
export function unzipStore(bytes) {
|
|
190
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
191
|
+
throw new Error('[bundle/zip] unzipStore(bytes): bytes must be a Uint8Array.');
|
|
192
|
+
}
|
|
193
|
+
// Locate the End Of Central Directory record (scan backward for its signature).
|
|
194
|
+
var eocd = -1;
|
|
195
|
+
for (var p = bytes.length - 22; p >= 0; p--) {
|
|
196
|
+
if (_readU32(bytes, p) === EOCD_SIG) { eocd = p; break; }
|
|
197
|
+
}
|
|
198
|
+
if (eocd === -1) {
|
|
199
|
+
throw new Error('[bundle/zip] unzipStore: End Of Central Directory record not found (not a ZIP).');
|
|
200
|
+
}
|
|
201
|
+
var count = _readU16(bytes, eocd + 10);
|
|
202
|
+
var cdOffset = _readU32(bytes, eocd + 16);
|
|
203
|
+
|
|
204
|
+
var out = [];
|
|
205
|
+
var o = cdOffset;
|
|
206
|
+
for (var i = 0; i < count; i++) {
|
|
207
|
+
if (_readU32(bytes, o) !== CENTRAL_SIG) {
|
|
208
|
+
throw new Error('[bundle/zip] unzipStore: bad central directory signature at record ' + i + '.');
|
|
209
|
+
}
|
|
210
|
+
var method = _readU16(bytes, o + 10);
|
|
211
|
+
var crcStored = _readU32(bytes, o + 16);
|
|
212
|
+
var compSize = _readU32(bytes, o + 20);
|
|
213
|
+
var nameLen = _readU16(bytes, o + 28);
|
|
214
|
+
var extraLen = _readU16(bytes, o + 30);
|
|
215
|
+
var commentLen = _readU16(bytes, o + 32);
|
|
216
|
+
var localOff = _readU32(bytes, o + 42);
|
|
217
|
+
var name = bytesText(bytes.subarray(o + 46, o + 46 + nameLen));
|
|
218
|
+
|
|
219
|
+
if (method !== 0) {
|
|
220
|
+
throw new Error('[bundle/zip] unzipStore: entry "' + name + '" uses compression method ' +
|
|
221
|
+
method + ' — this reader is STORE-only (method 0).');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Resolve the local header to find the data start (its own name/extra lengths).
|
|
225
|
+
if (_readU32(bytes, localOff) !== LOCAL_SIG) {
|
|
226
|
+
throw new Error('[bundle/zip] unzipStore: bad local header signature for "' + name + '".');
|
|
227
|
+
}
|
|
228
|
+
var lNameLen = _readU16(bytes, localOff + 26);
|
|
229
|
+
var lExtraLen = _readU16(bytes, localOff + 28);
|
|
230
|
+
var dataStart = localOff + 30 + lNameLen + lExtraLen;
|
|
231
|
+
var data = bytes.slice(dataStart, dataStart + compSize);
|
|
232
|
+
|
|
233
|
+
var crcActual = crc32(data);
|
|
234
|
+
if (crcActual !== crcStored) {
|
|
235
|
+
throw new Error('[bundle/zip] unzipStore: CRC-32 mismatch for "' + name +
|
|
236
|
+
'" (stored ' + crcStored + ', computed ' + crcActual + ').');
|
|
237
|
+
}
|
|
238
|
+
out.push({ name: name, bytes: data });
|
|
239
|
+
|
|
240
|
+
o += 46 + nameLen + extraLen + commentLen;
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|