Merge pull request 'Hold the shipped source to ECMAScript alone, and run the suite on Deno and Bun' (#39) from runs-anywhere into main
CI / gate (push) Successful in 7s
CI / gate (push) Successful in 7s
This commit was merged in pull request #39.
This commit is contained in:
@@ -60,6 +60,15 @@ they never reach a consumer.
|
||||
|
||||
## 6. The package contract
|
||||
|
||||
- Runs on any ES2022 engine, not only Node — a browser as readily as a server. The shipped source
|
||||
is ECMAScript and nothing else: no host import, no host global, no DOM. `tsconfig.build.json` is
|
||||
that gate, typechecking the shipped files alone, so `node:fs`, `process` and an ES2024 method are
|
||||
compile errors here rather than a consumer's crash there. The standard is the line, never an
|
||||
engine list: one implementing it in part — Hermes is the live doubt, on §10's property escapes
|
||||
and on lookbehind — is out of scope rather than a bug. Node's test runner, the corpus reads and
|
||||
the build are the repo's own,
|
||||
never the library's, and `engines.node` states the floor the shipped JavaScript needs — `>=18` —
|
||||
never the higher one those repo-only tools want.
|
||||
- ESM only — no CommonJS build, no dual-package hazard.
|
||||
- One entrypoint: built JavaScript, `.d.ts` beside it. Do not add a TypeScript-source entrypoint —
|
||||
Node refuses to type-strip under `node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`),
|
||||
@@ -104,8 +113,9 @@ descends, so a document reports its first error in document order.
|
||||
differs from npm → publish and tag `vX.Y.Z`. No bump, no deploy; the bump is each shipping PR's
|
||||
deliberate semver judgment.
|
||||
- Renovate watches devDependencies, Docker pins and action tags; automerges everything on green CI.
|
||||
- Docker images pin the full patch version (`node:24.19.0-alpine3.24`, never `node:24`); actions
|
||||
pin semver tags.
|
||||
- Docker images pin the full patch version (`node:24.19.0-alpine3.24`, never `node:24`), as
|
||||
specific as the publisher tags: `oven/bun:1.4.0-alpine` pins Bun's patch and leaves the base
|
||||
floating because Bun publishes nothing narrower. Actions pin semver tags.
|
||||
|
||||
## 10. Tests first, in Docker
|
||||
|
||||
@@ -113,6 +123,14 @@ Test for the behaviour wanted first, then implement until green. `node --test`,
|
||||
Node, tsc and npm never run on the host — only via the pinned images (§9). Tests are independent,
|
||||
coverage does not decline, containers are torn down after a run.
|
||||
|
||||
The gate runs that same suite under Deno and Bun as well as Node, the three images pinned alike,
|
||||
and neither extra leg is Node's proof twice. Deno refuses an extensionless or directory specifier,
|
||||
so it holds the module graph to the fully-spelled form a browser can load; Bun runs
|
||||
JavaScriptCore, the one engine of the three that is not V8, where the Unicode property escapes
|
||||
emphasis matching leans on can disagree. Both refuse a run matching no test, so Node's is the only
|
||||
vacuous-green guard, and a test may reach only for what all three `node:` shims carry — the price
|
||||
of proving those engines over the corpus rather than over a smoke import.
|
||||
|
||||
The floors live in the `test` script, so `npm test` and the gate are one path: 100% of lines and
|
||||
functions, and a branch floor that only ever moves upward. It sits below 100 because the guards
|
||||
`noUncheckedIndexedAccess` and ADF's optional keys force — `?? []`, `?? {}`, `?.`, an index
|
||||
|
||||
@@ -64,4 +64,5 @@ Personas, never named consumers (AGENTS.md §7):
|
||||
## The package
|
||||
|
||||
ESM only, no runtime dependencies, public npmjs. Built JavaScript with `.d.ts` beside it.
|
||||
Contract: `AGENTS.md` §5–6.
|
||||
Pure ECMAScript at an ES2022 baseline, reaching for no host API; the test suite runs under Node,
|
||||
Deno and Bun. Contract: `AGENTS.md` §5–6.
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
image=node:24.19.0-alpine3.24
|
||||
in_node() { docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp -v "$PWD:/app" -w /app "$image" "$@"; }
|
||||
bun_image=oven/bun:1.4.0-alpine
|
||||
deno_image=denoland/deno:2.9.6
|
||||
node_image=node:24.19.0-alpine3.24
|
||||
in_image() {
|
||||
local image=$1 entrypoint=$2
|
||||
shift 2
|
||||
docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp -v "$PWD:/app" -w /app --entrypoint "$entrypoint" "$image" "$@"
|
||||
}
|
||||
|
||||
in_node npm ci
|
||||
in_node npm run typecheck
|
||||
in_image "$node_image" npm ci
|
||||
in_image "$node_image" npm run typecheck
|
||||
|
||||
if ! test_output=$(in_node npm test 2>&1); then
|
||||
if ! test_output=$(in_image "$node_image" npm test 2>&1); then
|
||||
printf '%s\n' "$test_output"
|
||||
exit 1
|
||||
fi
|
||||
@@ -17,3 +23,6 @@ if printf '%s' "$test_output" | grep -q 'ℹ tests 0'; then
|
||||
echo 'the gate ran zero tests — failing instead of a vacuous green'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
in_image "$deno_image" deno test --allow-read --no-check src/
|
||||
in_image "$bun_image" bun test src/
|
||||
|
||||
+2
-2
@@ -10,11 +10,11 @@
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node --test --experimental-test-coverage --test-coverage-exclude=\"src/**/*.test.ts\" --test-coverage-branches=97 --test-coverage-functions=100 --test-coverage-lines=100 \"src/**/*.test.ts\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.build.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "24.13.3",
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"automerge": true,
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
"datasourceTemplate": "docker",
|
||||
"depNameTemplate": "denoland/deno",
|
||||
"description": "Pin the Deno image ci.sh runs",
|
||||
"managerFilePatterns": ["ci.sh"],
|
||||
"matchStrings": ["denoland/deno:(?<currentValue>[0-9][^\\s\"']*)"],
|
||||
"versioningTemplate": "docker"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"datasourceTemplate": "docker",
|
||||
@@ -11,6 +20,15 @@
|
||||
"matchStrings": ["node:(?<currentValue>[0-9][^\\s\"']*)"],
|
||||
"versioningTemplate": "docker"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"datasourceTemplate": "docker",
|
||||
"depNameTemplate": "oven/bun",
|
||||
"description": "Pin the Bun image ci.sh runs",
|
||||
"managerFilePatterns": ["ci.sh"],
|
||||
"matchStrings": ["oven/bun:(?<currentValue>[0-9][^\\s\"']*)"],
|
||||
"versioningTemplate": "docker"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"datasourceTemplate": "docker",
|
||||
|
||||
@@ -139,10 +139,17 @@ numbering is the order the work was planned in, not the order it ships.
|
||||
additions are read here as one list before that freeze — nine sessions mint them
|
||||
independently, and one cause wearing two codes is breaking to undo after `0.1.0`. `0.1.0`
|
||||
is the markdown round-trip: both markdown directions, the types, `isAdfDocument`. The build
|
||||
lands here: a build tsconfig emitting JS and `.d.ts` to `dist/` (the dev config's
|
||||
`allowImportingTsExtensions` forces `noEmit`, so the build config needs
|
||||
`rewriteRelativeImportExtensions`), plus `exports`/`files` in `package.json`. The
|
||||
lands here: `tsconfig.build.json` gains emit of JS and `.d.ts` to `dist/` (its own
|
||||
`allowImportingTsExtensions` forces `noEmit`, so `rewriteRelativeImportExtensions` lands
|
||||
beside it), plus `exports`/`files` in `package.json`. The
|
||||
maintainer's bump PR also removes `private: true`, the guard against any earlier publish.
|
||||
§6's browser half is first checkable here, on the emitted `dist/index.js` a browser can
|
||||
load — the compile gate names no host API, and a real page converting the corpus is the
|
||||
other half. Headless Firefox is that page, settling both at once: the browser proof, and the
|
||||
only SpiderMonkey there is, `ci.sh`'s three legs being two V8s and a JavaScriptCore that is
|
||||
not Safari's. `engines.node` gets its one-line proof here
|
||||
too — `import('./dist/index.js')` under a pinned Node 18 image, which cannot run the
|
||||
suite that type stripping wants 22+ for, but proves exactly what the field claims.
|
||||
**Settled** (the maintainer, 2026-09-01): the round-trip proved over the checked-in corpus
|
||||
is what `0.1.0` ships on, and the open-ended proof work follows it rather than gating it —
|
||||
3k's spec suite and 4's generators and maintainer-supplied payloads are `0.2.0`, 4b's retry
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2022"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"target": "ES2022",
|
||||
"types": [],
|
||||
|
||||
"allowImportingTsExtensions": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
|
||||
"strict": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"exclude": ["src/**/*.test.ts"],
|
||||
"include": ["src"]
|
||||
}
|
||||
+3
-22
@@ -1,28 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"target": "ESNext",
|
||||
"types": ["node"],
|
||||
|
||||
"allowImportingTsExtensions": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
|
||||
"strict": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src"]
|
||||
"exclude": []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user