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
22 changes: 22 additions & 0 deletions .github/scripts/install-playwright.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail

# The Azure Ubuntu mirror can spend the entire job budget downloading WebKit
# dependencies. Use Ubuntu's archive while retaining apt's signature checks.
if [[ "${RUNNER_OS:-}" == "Linux" ]]; then
# Hosted images also use mirror+file URIs. Updating only .sources leaves
# that indirection free to select Azure again.
if [[ -f /etc/apt/apt-mirrors.txt ]]; then
printf '%s\n' 'https://archive.ubuntu.com/ubuntu/' |
sudo tee /etc/apt/apt-mirrors.txt >/dev/null
fi
for sources in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do
if [[ -f "$sources" ]]; then
sudo sed -i \
's|http://azure.archive.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g; s|https://azure.archive.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' \
"$sources"
fi
done
fi

npx playwright install --with-deps "$@"
12 changes: 7 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ jobs:
- suite: workers
commands: |
npm run test:node-inventory
npm run test:events
npm run test:node-expansion
npm run test:workers -- --allowOnly=false
npm run test:browser
- suite: runtime
Expand Down Expand Up @@ -219,7 +221,7 @@ jobs:
working-directory: demo
- run: npm run typecheck
working-directory: demo
- run: npx playwright install --with-deps chromium webkit
- run: bash ../.github/scripts/install-playwright.sh chromium webkit
working-directory: demo
- name: Run ${{ matrix.suite }} contracts
run: ${{ matrix.commands }}
Expand Down Expand Up @@ -307,7 +309,7 @@ jobs:
working-directory: demo
- run: npm run typecheck
working-directory: demo
- run: npx playwright install --with-deps chromium webkit
- run: bash ../.github/scripts/install-playwright.sh chromium webkit
working-directory: demo
- run: npm run test:d1-migration
working-directory: demo
Expand Down Expand Up @@ -342,7 +344,7 @@ jobs:
working-directory: demo
- run: npm run typecheck
working-directory: demo
- run: npx playwright install --with-deps chromium webkit
- run: bash ../.github/scripts/install-playwright.sh chromium webkit
working-directory: demo
- run: npm run test:runtime:fallback
working-directory: demo
Expand All @@ -365,7 +367,7 @@ jobs:
cache-dependency-path: package.json
- run: npm install
- run: npm run build
- run: npx playwright install --with-deps chromium
- run: bash .github/scripts/install-playwright.sh chromium
- run: npm run test:package
- name: Verify packed native plugin discovery
run: node demo/tests/scripts/prepare-native-package.mjs
Expand Down Expand Up @@ -396,7 +398,7 @@ jobs:
- name: Check the original Worker in workerd
run: npm run test:upstream:workerd -- --reporter=default --reporter=json --outputFile=artifacts/upstream/workerd-results.json
working-directory: demo
- run: npx playwright install --with-deps chromium webkit
- run: bash ../.github/scripts/install-playwright.sh chromium webkit
working-directory: demo
- name: Build and test the browser adapter against the same expectations
run: npm run test:upstream:browser
Expand Down
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ demo/tests/native-embedding/android/app/src/main/assets/
demo/tests/native-embedding/android/build/
demo/tests/native-embedding/android/app/build/
demo/tests/native-embedding/android/.gradle/

# Generated pinned stream implementation.
src/compat/stream-web/workers-ponyfill.js
14 changes: 14 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@ not ship it behind an experimental flag as a substitute for finishing it. Design
notes and reproducible experiments can be reviewed independently without adding
unfinished production behavior.

### Explicit WebView events profile

The approved events work targets the current WebView architecture with documented
engine boundaries, rather than replacing the engine to seek unrestricted Workers
equivalence. The events adapter and its process event/warning/nextTick integration
may be accepted against the [WebView events profile](src/compat/events/README.md).
This is a scoped exception to unrestricted whole-module equivalence above, not a
blanket exception for new incomplete Node modules. Keep the existing process and
async-hooks limitations visible. Require the shared behavioral contracts, public
Vite/esbuild imports, an independently installed tarball, and iOS/Android page and
Dedicated Worker evidence for this profile. Do not label the complete Node
compatibility inventory or unrestricted Workers behavior **Complete** as a result.
Publishing and release authorization remain separate.

### 0.1.0 Preview exception

0.1.0 Preview may include existing compatibility implementations with the limitations documented in the [compatibility inventory](docs/compatibility.md). Their inclusion does not establish complete module compatibility or certify untested behavior. Keep missing exports, unsupported operations and verification boundaries visible; a passing application scenario does not make its dependencies complete.
Expand Down
8 changes: 7 additions & 1 deletion demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,20 @@
"test:runtime:fallback:dev": "playwright test --config tests/playwright.stream-fallback-dev.config.ts",
"test:service-bindings": "node --test tests/experiments/services/integration.test.mjs",
"test:cloudflare-bindings": "node --test tests/experiments/cloudflare/integration.test.mjs",
"test:node-completion": "npm run test:node-inventory && node tests/experiments/node-compat/completion-report.mjs",
"test:node-boundaries": "node --test tests/experiments/node-compat/*.probe.mjs",
"test:opennext": "node --test tests/opennext/integration.test.mjs && vite build --config tests/opennext/local/vite.config.ts && node tests/opennext/local/esbuild.mjs && node --test tests/opennext/local.test.mjs",
"test:opennext:split": "vite build --config tests/opennext/split/vite.config.ts && node tests/opennext/split/esbuild.mjs && node --test --test-concurrency=1 tests/opennext/split/*.test.mjs",
"test:void": "node --test tests/void/integration.test.mjs",
"test:void:routes": "npm --prefix void run build && node --test tests/void/routes/acceptance.test.mjs",
"test:upstream:opennext:prepare": "node tests/upstream/opennext/prepare.mjs",
"test:upstream:opennext": "node --test tests/upstream/opennext/acceptance.test.mjs",
"test:upstream:opennext:native": "node --test tests/upstream/opennext/native.test.mjs"
"test:upstream:opennext:native": "node --test tests/upstream/opennext/native.test.mjs",
"test:events": "node --test tests/experiments/node-compat/events-baseline.probe.mjs tests/experiments/node-compat/events-surface.probe.mjs tests/experiments/node-compat/events-contracts.probe.mjs",
"test:events:native:prepare": "node tests/experiments/node-compat/prepare-events-consumer.mjs",
"test:events:native": "node tests/experiments/node-compat/events-native.mjs",
"test:node-expansion": "node --test tests/experiments/node-compat/expansion-contracts.probe.mjs",
"test:node-expansion:native": "node tests/experiments/node-compat/expansion-native.mjs"
},
"dependencies": {
"@capacitor/core": "^8.5.3",
Expand Down
270 changes: 270 additions & 0 deletions demo/tests/experiments/node-compat/completion-report.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import assert from "node:assert/strict";
import { readFile, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import path from "node:path";

const assessments = [
{
modules: ["events"],
priority: 1,
category: "implementation-and-host-integration",
next: "Public events imports implement the explicitly scoped WebView profile, including process warning delivery and nextTick scheduling. Keep native async and V8 inspection boundaries explicit; this is not unrestricted Workers or process-module certification. See the profile and generated acceptance evidence.",
evidence: [
"surface-gaps.json",
"events-baseline.probe.mjs",
"events-surface.probe.mjs",
"events-contracts.probe.mjs",
"events-native.mjs",
"../../../../src/compat/events/README.md",
"../../../../src/compat/async-hooks/README.md",
],
},
{
modules: ["crypto", "zlib"],
priority: 2,
category: "implementation-gap",
next: "Inventory algorithms, key/codec classes, synchronous and streaming behavior. Evaluate JS/Wasm implementations against all pinned operations; random/hash/HKDF/scrypt, owned secret/asymmetric keys, RSA/EC/Ed25519 signatures, prime generation/checking X509Certificate parsing/fields/name checks/signature checks and Brotli/Zstd profiles now supplement dependency polyfills. Pinned Workers X509 key-check restrictions are documented. Additional key-generation options, remaining algorithms and complete codec contracts remain open. No technical-impossibility claim has been established.",
evidence: [
"surface-gaps.json",
"expansion-contracts.probe.mjs",
"expansion-native.mjs",
"../../../../src/compat/expansion/README.md",
],
},
{
modules: ["fs", "fs/promises"],
priority: 2,
category: "implementation-gap",
next: "Compare the Workers virtual filesystem, descriptors, FileHandle, writes, permissions and watch behavior. Request-local memory-backed /tmp now supports writes, descriptors and FileHandle; permission no-ops, statfs and embedded-file copying are tested; compare remaining cross-volume operations, descriptors and stream semantics.",
evidence: [
"../../../../src/compat/fs/index.ts",
"../../../../src/compat/fs/promises/index.ts",
],
},
{
modules: ["process", "console"],
priority: 2,
category: "implementation-and-host-integration",
next: "Classify each export as functional or a pinned Workers stub; verify scheduling, streams, events and host-specific introspection independently. Do not substitute standalone Node behavior.",
evidence: ["surface-gaps.json"],
},
{
modules: ["util", "util/types", "sys", "assert", "assert/strict"],
priority: 3,
category: "engine-boundary",
next: "Separate ordinary JS algorithms and sys alias routing from unobservable proxy/promise state, call sites and host brands. Closing the export list will not complete these modules or assertion diagnostics.",
evidence: [
"engine-boundaries.probe.mjs",
"../../../../src/compat/util/README.md",
],
},
{
modules: ["async_hooks", "diagnostics_channel"],
priority: 3,
category: "engine-boundary",
next: "Test native async continuation and concurrent request isolation. Build-time lowering supports a bounded profile but changes observable Promise behavior; do not classify arbitrary async code as complete.",
evidence: ["engine-boundaries.probe.mjs", "async-transform.probe.mjs"],
},
{
modules: ["worker_threads"],
priority: 3,
category: "engine-boundary",
next: "Reconcile MessagePort scheduling, trusted event branding and serialization. Native and synthetic ports have different observable behavior; the current adapter does not supply Node threads.",
evidence: ["message-port-boundaries.probe.mjs"],
},
{
modules: ["stream/web"],
priority: 2,
category: "behavior-gap",
next: "The owned stream implementation now matches tested Workers tee cancellation reasons and pending pipeTo abort timing. Complete the remaining operation/state inventory and native integration acceptance; raw host boundary probes describe host forwarding, not this adapter.",
evidence: ["stream-boundaries.probe.mjs"],
},
{
modules: ["module"],
priority: 3,
category: "implementation-and-host-integration",
next: "Audit Workers module resolution and loader/stub behavior. VFS JSON loading and bundling are the current limits; separately evaluate executable resolution and engine compile-cache APIs.",
evidence: ["../../../../src/compat/module/index.ts", "surface-gaps.json"],
},
{
modules: [
"tls",
"_tls_wrap",
"_stream_wrap",
"_http_common",
"_http_outgoing",
"_http_server",
"http",
"https",
"net",
"_tls_common",
"_http_agent",
"_http_client",
"_http_incoming",
],
priority: 2,
category: "implementation-and-native-transport",
next: "Direct TLS and same-connection STARTTLS exchange/hostname rejection are tested in page and Dedicated Worker on both OSes. TLS 1.2/1.3 and repeated write-half-close are covered on both OSes; custom Duplex wrapping remains unsupported. Audit remaining class/prototype/state/event contracts; these scenarios do not certify the whole TLS/server API.",
evidence: ["surface-gaps.json", "../../../../docs/compatibility.md"],
},
{
modules: ["dns", "dns/promises"],
priority: 2,
category: "behavior-gap",
next: "Verify Workers synthetic Service Binding/Hyperdrive addresses as well as DNS-over-HTTPS and errors; public DNS success does not cover these overrides.",
evidence: ["../../../../docs/compatibility.md"],
},
{
modules: [
"child_process",
"cluster",
"dgram",
"vm",
"v8",
"wasi",
"sqlite",
"inspector",
"inspector/promises",
"repl",
"tty",
"trace_events",
],
priority: 3,
category: "workers-stub-profile",
next: "Audit every operation against the pinned profile, preserving actual rejection/no-op behavior. A module in this group is not necessarily wholly nonfunctional; retained functional methods and validation still require acceptance.",
evidence: [
"../../../../src/compat/README.md",
"engine-boundaries.probe.mjs",
],
},
];

export function completionReport(baseline, reports) {
const names = Object.keys(baseline.modules).sort();
const covered = new Map();
for (const item of assessments)
for (const name of item.modules) {
assert.ok(names.includes(name), `Unknown assessment module: ${name}`);
assert.ok(!covered.has(name), `Duplicate assessment: ${name}`);
covered.set(name, item);
}
for (const [engine, report] of Object.entries(reports)) {
assert.deepEqual(
report.profile,
baseline.profile,
`${engine}: mismatched Workers profile`,
);
assert.deepEqual(
Object.keys(report.modules).sort(),
names,
`${engine}: incomplete inventory`,
);
}
assert.deepEqual(Object.keys(reports).sort(), ["chromium", "webkit"]);
const modules = Object.fromEntries(
names.map((name) => {
const engines = Object.fromEntries(
Object.entries(reports).map(([engine, report]) => {
const value = report.modules[name];
if (!value.buildError && !value.evaluationError) {
assert.ok(
value.surface &&
Array.isArray(value.missing) &&
Array.isArray(value.extra),
`${engine}/${name}: missing surface evidence`,
);
assert.ok(
value.typeMismatches && typeof value.typeMismatches === "object",
`${engine}/${name}: missing type audit`,
);
}
return [
engine,
{
missing: value.missing ?? [],
extra: value.extra ?? [],
typeMismatches: value.typeMismatches ?? {},
...(value.buildError ? { buildError: value.buildError } : {}),
...(value.evaluationError
? { evaluationError: value.evaluationError }
: {}),
},
];
}),
);
const gaps = Object.values(engines).some(
(value) =>
value.buildError ||
value.evaluationError ||
value.missing.length ||
value.extra.length ||
Object.keys(value.typeMismatches).length,
);
const { modules: ignored, ...assessment } = covered.get(name) ?? {
priority: 2,
category: "verification-gap",
next: "Existing contracts and matching exports do not certify a whole module. Finish operation/prototype inventory, invalid inputs, state, cleanup, build integration and iOS/Android acceptance before promotion.",
evidence: ["../../../../src/compat/README.md"],
};
return [
name,
{
status: gaps ? "surface-gaps" : "surface-aligned-not-certified",
...assessment,
engines,
},
];
}),
);
return {
profile: baseline.profile,
scope:
"Import/export and type audit plus implementation triage. Not whole-module behavioral or native acceptance. Priority is an investigation order, not a feasibility guarantee.",
summary: {
modules: names.length,
withSurfaceGaps: Object.values(modules).filter(
(m) => m.status === "surface-gaps",
).length,
surfaceAligned: Object.values(modules).filter(
(m) => m.status === "surface-aligned-not-certified",
).length,
certifiedCompleteByThisAudit: 0,
},
modules,
};
}

if (
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
const read = async (url) => JSON.parse(await readFile(url, "utf8"));
const artifacts = new URL(
"../../../artifacts/node-inventory/",
import.meta.url,
);
const baseline = await read(new URL("exports.json", import.meta.url));
const reports = Object.fromEntries(
await Promise.all(
["chromium", "webkit"].map(async (engine) => [
engine,
await read(new URL(`${engine}-surface.json`, artifacts)),
]),
),
);
const report = completionReport(baseline, reports);
await writeFile(
new URL("completion.json", artifacts),
JSON.stringify(report, null, 2) + "\n",
);
console.log(JSON.stringify(report.summary, null, 2));
console.log(
"Report: demo/artifacts/node-inventory/completion.json; successful generation is not compatibility acceptance.",
);
if (process.argv.includes("--require-complete")) {
console.error(
"No whole-module certification is supplied by an import/export audit.",
);
process.exitCode = 1;
}
}
Loading
Loading