Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
774 changes: 774 additions & 0 deletions docs/architecture/SHELL_0_CONTRACTS_AND_MEASUREMENTS.md

Large diffs are not rendered by default.

11,263 changes: 11,263 additions & 0 deletions qa/shell-0-baseline/RESULTS.json

Large diffs are not rendered by default.

473 changes: 473 additions & 0 deletions qa/shell-0-baseline/measure.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions scripts/run-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const DIRS = [
'test/proto-resolution',
'test/proto-enrichment',
'test/phase-7c-windows',
'test/shell',
];

const files = [...EXPLICIT];
Expand Down
76 changes: 76 additions & 0 deletions spikes/shell-0-module-loading/NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# SHELL-0 module-loading spike — native ES modules over `file://` vs esbuild entry bundles

Evidence for `docs/architecture/SHELL_0_CONTRACTS_AND_MEASUREMENTS.md` §16–§17.
Isolated: nothing here is loaded by the product, and no production renderer
script changed module system.

```sh
node spikes/shell-0-module-loading/build.js # esbuild bundles, synthetic graph, asar -> out/
node spikes/shell-0-module-loading/run.js # one sandboxed Electron process via VisualQaRunner
```

`out/` (gitignored, regenerable) receives `BUILD.json` and `RESULTS.json`.

## Set-up

- `main.js` opens one hidden `BrowserWindow` with WRL Forge's renderer
settings: `contextIsolation: true`, `nodeIntegration: false`, Electron's
default sandbox (the run is **sandboxed**; no `--no-sandbox`), and pages
loaded with `loadFile`. It speaks the capture-server protocol, so
`qa/visual-qa/runner.js` drives it (launch cap 1, PID accounting, leak
check) under the visual-QA lock and the workspace guard.
- Every page carries a `<meta>` CSP with the editor page's `script-src 'self'
file: 'wasm-unsafe-eval'` (one page uses a strict `script-src 'self'`).
- `app/probe.js` (classic) records order, window- and element-level errors,
unhandled rejections and CSP violations; main also records console output.
- Timing graph: 42 files, ~320–337 KB, entry → 8 features → 4 leaves each +
a shared util. Every function is referenced at load, so the bundle cannot
tree-shake the comparison away. That is close to the editor page's
first-party load today (27 classic tags, 332 KB). Each strategy loaded 15
times in rotating order in the same process.

## Results (Electron 41.7.1, Linux 7.0, sandboxed)

| Probe | Result |
|---|---|
| `type="module"` from `file://` | loads and runs |
| Relative static imports, `.mjs`, dynamic `import()`, top-level `await` | all work |
| Origin | `self.origin` / `location.origin` = `"file://"`; `import.meta.url` is the real file URL |
| CSP `script-src 'self' file:` (editor page) | allowed, no violation |
| CSP `script-src 'self'` only | allowed, no violation |
| Inline `<script type="module">` | blocked (`script-src-elem`), console error — same as classic inline |
| Order with classic `defer` | document order: `classic-defer-1`, module, `classic-defer-2` |
| Module scope | top-level names stay module-scoped; no `window` leak |
| Top-level throw | `error` event with file, line, column; console error; stack shows the source file and line |
| Missing import | **only an element-level `error` event on the `<script>`; no console message, no window error** |
| Bare specifier (`import 'x_ite'`) | clear `TypeError: Failed to resolve module specifier` |
| Shared dual-format `src/` module, side-effect import | works (publishes its `window.*` global) |
| Same module imported **by name** | `SyntaxError: … does not provide an export named …` — CommonJS `src/` modules have no ES exports |
| From inside an `asar` (release-like packaging) | ESM graph and bundle both load and run |
| esbuild IIFE bundle (`chrome122`, linked sourcemap) | runs under the same CSP, no violation; `Error.stack` points at the bundle line (the map is applied by DevTools, not by V8) |

| 42-file graph, load → all executed | median | p10–p90 |
|---|---|---|
| classic `defer` tags (today) | 32.4 ms | 31.1–33.6 |
| native ESM | 33.5 ms | 30.8–34.3 |
| **one esbuild entry bundle** | **11.6 ms** | 10.9–13.5 |

| esbuild build (`BUILD.json`) | |
|---|---|
| 42-file graph → 1 file | 15 ms, 337 KB (+661 KB map) |
| Determinism | byte-identical across three builds (SHA-256) |

## Reading

- Native ESM over `file://` is **reliable** in Electron 41 under the product's
CSP and sandbox. It is a viable module system.
- It does **not** fit WRL Forge's shared code: `src/` is CommonJS so main and
`node:test` can `require` it, and a module cannot import a CommonJS file by
name. Native ESM would force `src/` to become dual-format ES modules, or
keep the `window.*` globals the shell is meant to retire.
- A missing import fails silently in the console, which is a worse developer
experience than a build error.
- One bundle loads the same graph ~2.9× faster than either 42 separate files
(about 21 ms saved per page load of an editor-sized graph).

Recommendation recorded in the SHELL-0 document: `ESBUILD_ENTRY_BUNDLES`.
14 changes: 14 additions & 0 deletions spikes/shell-0-module-loading/app/bundle-basic.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- SHELL-0 module-loading spike page. Evidence only; never shipped. -->
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' file: 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' file: data: blob:; object-src 'none'; base-uri 'none';">
<title>bundle-basic.html</title>
<script src="probe.js"></script>
<script defer src="classic-defer.js"></script>
<script defer src="../out/bundle-basic.js"></script>
<script defer src="classic-defer-2.js"></script>
</head>
<body></body>
</html>
2 changes: 2 additions & 0 deletions spikes/shell-0-module-loading/app/classic-defer-2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
'use strict';
window.__spike.order.push('classic-defer-2');
2 changes: 2 additions & 0 deletions spikes/shell-0-module-loading/app/classic-defer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
'use strict';
window.__spike.order.push('classic-defer-1');
12 changes: 12 additions & 0 deletions spikes/shell-0-module-loading/app/esm-bare.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- SHELL-0 module-loading spike page. Evidence only; never shipped. -->
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' file: 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' file: data: blob:; object-src 'none'; base-uri 'none';">
<title>esm-bare.html</title>
<script src="probe.js"></script>
<script type="module" src="esm/bare-specifier.js"></script>
</head>
<body></body>
</html>
14 changes: 14 additions & 0 deletions spikes/shell-0-module-loading/app/esm-basic.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- SHELL-0 module-loading spike page. Evidence only; never shipped. -->
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' file: 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' file: data: blob:; object-src 'none'; base-uri 'none';">
<title>esm-basic.html</title>
<script src="probe.js"></script>
<script defer src="classic-defer.js"></script>
<script type="module" src="esm/entry.js"></script>
<script defer src="classic-defer-2.js"></script>
</head>
<body></body>
</html>
12 changes: 12 additions & 0 deletions spikes/shell-0-module-loading/app/esm-cjs-named.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- SHELL-0 module-loading spike page. Evidence only; never shipped. -->
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' file: 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' file: data: blob:; object-src 'none'; base-uri 'none';">
<title>esm-cjs-named.html</title>
<script src="probe.js"></script>
<script type="module" src="esm/cjs-named.js"></script>
</head>
<body></body>
</html>
12 changes: 12 additions & 0 deletions spikes/shell-0-module-loading/app/esm-cjs-side-effect.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- SHELL-0 module-loading spike page. Evidence only; never shipped. -->
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' file: 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' file: data: blob:; object-src 'none'; base-uri 'none';">
<title>esm-cjs-side-effect.html</title>
<script src="probe.js"></script>
<script type="module" src="esm/cjs-side-effect.js"></script>
</head>
<body></body>
</html>
12 changes: 12 additions & 0 deletions spikes/shell-0-module-loading/app/esm-inline.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- SHELL-0 module-loading spike page. Evidence only; never shipped. -->
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' file: 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' file: data: blob:; object-src 'none'; base-uri 'none';">
<title>esm-inline.html</title>
<script src="probe.js"></script>
<script type="module">window.__spikeFinish({ inlineModuleRan: true });</script>
</head>
<body></body>
</html>
12 changes: 12 additions & 0 deletions spikes/shell-0-module-loading/app/esm-missing.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- SHELL-0 module-loading spike page. Evidence only; never shipped. -->
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' file: 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' file: data: blob:; object-src 'none'; base-uri 'none';">
<title>esm-missing.html</title>
<script src="probe.js"></script>
<script type="module" src="esm/missing-import.js"></script>
</head>
<body></body>
</html>
12 changes: 12 additions & 0 deletions spikes/shell-0-module-loading/app/esm-strict-self.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- SHELL-0 module-loading spike page. Evidence only; never shipped. -->
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; object-src 'none'; base-uri 'none';">
<title>esm-strict-self.html</title>
<script src="probe.js"></script>
<script type="module" src="esm/entry-min.js"></script>
</head>
<body></body>
</html>
12 changes: 12 additions & 0 deletions spikes/shell-0-module-loading/app/esm-throw.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- SHELL-0 module-loading spike page. Evidence only; never shipped. -->
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' file: 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' file: data: blob:; object-src 'none'; base-uri 'none';">
<title>esm-throw.html</title>
<script src="probe.js"></script>
<script type="module" src="esm/throws.js"></script>
</head>
<body></body>
</html>
3 changes: 3 additions & 0 deletions spikes/shell-0-module-loading/app/esm/bare-specifier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// A bare specifier: there is no Node-style resolution in the browser.
import 'x_ite';
window.__spikeFinish({ unexpectedlyRan: true });
17 changes: 17 additions & 0 deletions spikes/shell-0-module-loading/app/esm/bundle-entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// The same graph as entry.js, written for an esbuild IIFE entry bundle
// (IIFE output has no top-level await and no import.meta).
import { add, fromA } from './lib/a.js';
import { label } from './lib/b.mjs';

window.__spike.order.push('bundle');
import('./lib/lazy.js').then((lazy) => {
let stack = null;
try { throw new Error('stack-probe'); } catch (e) { stack = String(e.stack); }
window.__spikeFinish({
sum: add(2, 3), fromA, label, lazy: lazy.value,
currentScriptIsNull: document.currentScript === null,
moduleScoped: typeof window.add === 'undefined',
readyStateAtRun: document.readyState,
stack,
});
});
3 changes: 3 additions & 0 deletions spikes/shell-0-module-loading/app/esm/cjs-named.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// The same dual-format module imported by NAME: it has no ES exports.
import { createCommandRegistry } from '../../../../src/editor/command-registry.js';
window.__spikeFinish({ unexpectedlyRan: typeof createCommandRegistry });
8 changes: 8 additions & 0 deletions spikes/shell-0-module-loading/app/esm/cjs-side-effect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// A dual-format shared src/ module (CommonJS in Node, window.* in a classic
// script) imported as a module for its side effect.
import '../../../../src/editor/command-registry.js';
const api = window.WrlCommandRegistry;
window.__spikeFinish({
globalPublished: typeof api,
registryWorks: !!(api && api.createCommandRegistry && api.createCommandRegistry().list().length === 0),
});
2 changes: 2 additions & 0 deletions spikes/shell-0-module-loading/app/esm/entry-min.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { add } from './lib/a.js';
window.__spikeFinish({ sum: add(1, 1), strictSelfCspLoaded: true });
20 changes: 20 additions & 0 deletions spikes/shell-0-module-loading/app/esm/entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Native ES module entry: static relative imports, a .mjs import, a dynamic
// import, module scope, import.meta, origin and stack-trace evidence.
import { add, fromA } from './lib/a.js';
import { label } from './lib/b.mjs';

window.__spike.order.push('module');
const topLevelThis = this;
const lazy = await import('./lib/lazy.js');
let stack = null;
try { throw new Error('stack-probe'); } catch (e) { stack = String(e.stack); }
window.__spikeFinish({
sum: add(2, 3), fromA, label, lazy: lazy.value,
importMetaUrl: import.meta.url,
selfOrigin: self.origin, locationOrigin: location.origin, protocol: location.protocol,
currentScriptIsNull: document.currentScript === null,
topLevelThisUndefined: topLevelThis === undefined,
moduleScoped: typeof window.add === 'undefined',
readyStateAtRun: document.readyState,
stack,
});
2 changes: 2 additions & 0 deletions spikes/shell-0-module-loading/app/esm/lib/a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export function add(a, b) { return a + b; }
export const fromA = 'a.js';
2 changes: 2 additions & 0 deletions spikes/shell-0-module-loading/app/esm/lib/b.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// .mjs: MIME type is derived from the extension on file://.
export const label = 'b.mjs';
1 change: 1 addition & 0 deletions spikes/shell-0-module-loading/app/esm/lib/lazy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value = 'lazy.js (dynamic import)';
2 changes: 2 additions & 0 deletions spikes/shell-0-module-loading/app/esm/missing-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { nothing } from './lib/does-not-exist.js';
window.__spikeFinish({ unexpectedlyRan: nothing });
3 changes: 3 additions & 0 deletions spikes/shell-0-module-loading/app/esm/throws.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { add } from './lib/a.js';
add(1, 2);
throw new Error('module top-level failure');
25 changes: 25 additions & 0 deletions spikes/shell-0-module-loading/app/probe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use strict';
// SHELL-0 spike probe (classic script, loaded first on every spike page).
// Records execution order, errors (window AND element-level, via capture),
// unhandled rejections and CSP violations into window.__spike for main to read.
window.__spike = { order: [], errors: [], violations: [], done: false, data: {}, t: null };
window.addEventListener('error', function (e) {
if (e.target && e.target !== window) {
window.__spike.errors.push({ kind: 'element', tag: e.target.tagName, src: e.target.src || null });
return;
}
window.__spike.errors.push({ kind: 'window', message: e.message, filename: e.filename, lineno: e.lineno, colno: e.colno,
stack: e.error && e.error.stack ? String(e.error.stack) : null });
}, true);
window.addEventListener('unhandledrejection', function (e) {
var r = e.reason;
window.__spike.errors.push({ kind: 'rejection', message: String(r && r.message || r), stack: r && r.stack ? String(r.stack) : null });
});
document.addEventListener('securitypolicyviolation', function (e) {
window.__spike.violations.push({ directive: e.violatedDirective, blockedURI: e.blockedURI, sample: e.sample || null });
});
window.__spikeFinish = function (data) {
Object.assign(window.__spike.data, data || {});
window.__spike.t = performance.now();
window.__spike.done = true;
};
Loading
Loading