Skip to content

Commit 819bb64

Browse files
authored
Migration to ECMAScript modules (ESM) (#2683)
This allowed us to finally consume ESM-only dependencies and has broken us free from some CJS shackes. Now produce the same API surface for CJS consumers, as well, by generating `./lib` * Modern ignores 😁 * test: add distribution harness * test: verify packed cjs and esm entrypoints * test: lock distribution api manifest * test: smoke test built pkg artifacts * docs: require contract tests for package migration * test: guard esm migration regressions * docs: require contract gate for esm migration * build: generate cjs lib from esm source entries * refactor: port root api surface to esm * build: clean port of root api to esm * docs: include implementation plans * fix: align lint and smoke tests with esm migration * refactor: complete esm port of all core components * refactor: finalize esm migration with sandbox and naming fixes * fix: finish esm migration stabilization * chore: stop tracking generated lib output * remove plans * prettier * linting * fix: make distribution tests self-contained * fix: build before coverage test bundle * refactor: move simple unit tests to src * refactor: flatten test and coverage script chains * refactor: use parallel mocha for node tests * test: restore fake timers cleanup * refactor: remove node test runner script * remove unneccessary clutter * fix: make mocha watch use polling * simplify * Increase coverage * Fix coverage by removing duplicated tests These were covering the generated lib/ folder. * Move shared util into esm dir * fix package dep issues * Adjust coverage * Upgrade all dependencies npx npm-check-updates -u
1 parent cd2bf5a commit 819bb64

128 files changed

Lines changed: 4280 additions & 1471 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.codecov.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
coverage:
2+
status:
3+
project:
4+
default:
5+
target: 97%
6+
patch:
7+
default:
8+
target: 95%
9+
ignore:
10+
- "lib/**/*"
11+
- "pkg/**/*"

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
coverage/
22
out/
33
pkg/
4+
lib/
45
tmp/
56
docs/_site/
67
docs/js/

.eslintrc.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
extends:
22
- "@sinonjs/eslint-config"
33

4+
parserOptions:
5+
ecmaVersion: 2022
6+
7+
env:
8+
es2022: true
9+
410
plugins:
511
- "@sinonjs/no-prototype-methods"
612

@@ -9,3 +15,30 @@ rules:
915
"jsdoc/require-param-type": off
1016
"jsdoc/require-jsdoc": off
1117
"jsdoc/tag-lines": off
18+
19+
overrides:
20+
- files:
21+
- "src/**/*.js"
22+
- "*.mjs"
23+
- "**/*.mjs"
24+
- "scripts/**/*.mjs"
25+
- "test/distribution/browser-global-smoke.js"
26+
- "test/es2015/check-esm-bundle-is-runnable.js"
27+
parserOptions:
28+
sourceType: module
29+
rules:
30+
"no-underscore-dangle":
31+
- error
32+
- allow:
33+
- "__dirname"
34+
- files:
35+
- "build.cjs"
36+
- "scripts/**/*.mjs"
37+
- "test/distribution/browser-global-smoke.js"
38+
- "test/es2015/check-esm-bundle-is-runnable.js"
39+
rules:
40+
"no-console": off
41+
- files:
42+
- "*.cjs"
43+
parserOptions:
44+
sourceType: script

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
out/
2+
lib/
23
pkg
34
tmp/
45
node_modules
@@ -10,3 +11,8 @@ docs/vendor/
1011
vendor/
1112
.bundle
1213
.worktrees/
14+
.gitnexus
15+
.claude/
16+
AGENTS.md
17+
CLAUDE.md
18+
docs/superpowers/

CONTRIBUTING.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,26 @@ Dev mode features:
143143

144144
Note that in dev mode tests run only in Node. Before creating your PR please ensure tests are passing in Phantom and WebWorker as well. To check this please use [Run the tests](#run-the-tests) instructions.
145145

146+
### Contract Tests
147+
148+
To ensure Sinon's published Node and browser APIs stay compatible, we use artifact-first contract tests.
149+
150+
```
151+
$ npm run test-contract
152+
```
153+
154+
- `test-distribution` validates the packed npm artifact for CJS and ESM consumers.
155+
- `test-pkg-browser-esm` validates `pkg/sinon-esm.js` in a browser module context.
156+
- `test-pkg-browser-global` validates `pkg/sinon.js` as a browser global.
157+
158+
These tests are mandatory for any source-layout refactors or API changes. Any intentional API change must update the manifest in `test/distribution/public-api-manifest.json` with explicit reviewer sign-off. Source format and build-system changes are allowed only when `npm run test-contract` remains green.
159+
146160
### Compiling a built version
147161

148162
Build requires Node. Under the hood [esbuild](https://esbuild.github.io/) is used.
149163

150164
To build run
151165

152166
$ node build.cjs
167+
168+
The `lib/` directory is generated output. Do not commit it; rebuild it locally with `npm run build` when you need fresh artifacts. The published npm tarball still includes `lib/` via the package allowlist.

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ If you have questions that are not covered by the documentation, you can [check
5959

6060
## Contribute?
6161

62-
See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how you can contribute to Sinon.JS
62+
See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how you can contribute to Sinon.JS. Artifact-first [contract tests](CONTRIBUTING.md#contract-tests) are used to protect public APIs during migration.
63+
The `lib/` directory is generated build output and is not committed to the repository; run `npm run build` to regenerate it locally when needed.
6364

6465
## Backers
6566

build.cjs

Lines changed: 70 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,24 @@
22
"use strict";
33
/* eslint-disable @sinonjs/no-prototype-methods/no-prototype-methods */
44
const fs = require("node:fs");
5+
const { execFileSync } = require("node:child_process");
56
const esbuild = require("esbuild");
67
const { umdWrapper } = require("esbuild-plugin-umd-wrapper");
78
const pkg = require("./package.json");
9+
10+
// Step 1: Run Rollup to generate lib/ from src/
11+
console.log("Generating lib/ from src/ using Rollup...");
12+
execFileSync("npx", ["rollup", "-c", "rollup.config.mjs"], {
13+
stdio: "inherit",
14+
});
15+
16+
// Step 1b: Mark the generated lib tree as CommonJS for Node.
17+
fs.writeFileSync(
18+
"lib/package.json",
19+
JSON.stringify({ type: "commonjs" }, null, 2),
20+
);
21+
22+
// Step 2: Load sinon from the generated lib
823
const sinon = require("./lib/sinon");
924

1025
// YYYY-MM-DD
@@ -56,46 +71,60 @@ async function makeBundle(entryPoint, config, done) {
5671
done(js);
5772
}
5873

59-
makeBundle(
60-
"./lib/sinon.js",
61-
{
62-
// Add inline source maps to the default bundle
63-
debug: true,
64-
format: "cjs",
65-
// Create a UMD wrapper and install the "sinon" global:
66-
standalone: "sinon",
67-
},
68-
function (bundle) {
69-
fs.writeFileSync("pkg/sinon.js", bundle); // WebWorker can only load js files
70-
},
71-
);
74+
async function buildAll() {
75+
await makeBundle(
76+
"./lib/sinon.js",
77+
{
78+
// Add inline source maps to the default bundle
79+
debug: true,
80+
format: "cjs",
81+
// Create a UMD wrapper and install the "sinon" global:
82+
standalone: "sinon",
83+
},
84+
function (bundle) {
85+
fs.writeFileSync("pkg/sinon.js", bundle); // WebWorker can only load js files
86+
},
87+
);
7288

73-
makeBundle(
74-
"./lib/sinon.js",
75-
{
76-
format: "cjs",
77-
// Create a UMD wrapper and install the "sinon" global:
78-
standalone: "sinon",
79-
},
80-
function (bundle) {
81-
fs.writeFileSync("pkg/sinon-no-sourcemaps.cjs", bundle);
82-
},
83-
);
89+
await makeBundle(
90+
"./lib/sinon.js",
91+
{
92+
format: "cjs",
93+
// Create a UMD wrapper and install the "sinon" global:
94+
standalone: "sinon",
95+
},
96+
function (bundle) {
97+
fs.writeFileSync("pkg/sinon-no-sourcemaps.cjs", bundle);
98+
},
99+
);
84100

85-
makeBundle(
86-
"./lib/sinon-esm.js",
87-
{
88-
format: "esm",
89-
},
90-
function (bundle) {
91-
var intro = "let sinon;";
92-
var outro = `\n${Object.keys(sinon)
93-
.map(function (key) {
94-
return `const _${key} = require_sinon().${key};\nexport { _${key} as ${key} };`;
95-
})
96-
.join("\n")}`;
97-
98-
var script = intro + bundle + outro;
99-
fs.writeFileSync("pkg/sinon-esm.js", script);
100-
},
101-
);
101+
await makeBundle(
102+
"./lib/sinon-esm.js",
103+
{
104+
format: "esm",
105+
},
106+
function (bundle) {
107+
var intro = "let sinon;\n";
108+
// Replace the bundle's own "export default" with a simple assignment to sinon
109+
var baseScript = bundle.replace(
110+
/export default [^;]+;/,
111+
"sinon = require_sinon_esm();\nif (sinon.default) sinon = sinon.default;",
112+
);
113+
114+
var outro = `\nexport default sinon;\n${Object.keys(sinon)
115+
.filter((key) => key !== "default")
116+
.map(function (key) {
117+
return `const _${key} = sinon.${key};\nexport { _${key} as ${key} };`;
118+
})
119+
.join("\n")}`;
120+
121+
var script = intro + baseScript + outro;
122+
fs.writeFileSync("pkg/sinon-esm.js", script);
123+
},
124+
);
125+
}
126+
127+
buildAll().catch((err) => {
128+
console.error(err);
129+
process.exit(1);
130+
});

docs/_data/related_libraries.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
- title: proxyquire — Proxies nodejs require in order to allow overriding dependencies during testing
22
url: https://github.com/thlorenz/proxyquire
3-
3+
44
- title: inject-loader - Webpack loader that allows overriding dependencies during testing
55
url: https://github.com/plasticine/inject-loader
66

docs/_includes/banner.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<div class="bg-info hidden" id="banner-message">
2-
These docs are from an older version of sinon.
3-
Do you want the <a href="/releases/latest/">latest</a> docs?
2+
These docs are from an older version of sinon. Do you want the
3+
<a href="/releases/latest/">latest</a> docs?
44
</div>
55

66
<script>
7-
if (site.showBanner) {
8-
document.getElementById("banner-message").classList.remove("hidden");
9-
}
7+
if (site.showBanner) {
8+
document.getElementById("banner-message").classList.remove("hidden");
9+
}
1010
</script>

docs/_includes/carbonads.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
<script async type="text/javascript" src="https://cdn.carbonads.com/carbon.js?serve=CE7D453M&placement=sinonjsorg" id="_carbonads_js"></script>
1+
<script
2+
async
3+
type="text/javascript"
4+
src="https://cdn.carbonads.com/carbon.js?serve=CE7D453M&placement=sinonjsorg"
5+
id="_carbonads_js"
6+
></script>

0 commit comments

Comments
 (0)