diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7082a81..4e76d45 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,17 @@
### Build
+- The minified module bundle `dist/sd-expression-engine.min.js` keeps the
+ state of its `require` wrapper (module cache, factories, previous `require`)
+ in its own closure: `@rollup/plugin-terser` defaults to `toplevel: true` for
+ the cjs output and turned the wrapper into global variables, so two minified
+ module bundles in one scope overwrote each other (`Maximum call stack size
+ exceeded`, `r is not a function`). Terser now runs with `toplevel: false`;
+ the plain and standalone files are unchanged.
+ `test/contract/bundle-isolation.test.js` checks the globals, the cache and
+ the `require` chain of the module bundle. `npm run test:bundle-set` loads
+ the module bundles of sd-utils, sd-random, sd-model and this package
+ together in all load orders (`test/bundle-set`).
- The `dist/` bundles are built with Rollup and npm scripts instead of
gulp/browserify (`npm run build`). Same files, formats and globals:
`dist/sd-expression-engine(.min).js` defines the global `require` exposing
diff --git a/README.md b/README.md
index 1092b34..c92ee2d 100644
--- a/README.md
+++ b/README.md
@@ -93,12 +93,26 @@ artifact, chosen with `SD_EXPRESSION_ENGINE_TARGET`:
`SD_EXPRESSION_ENGINE_DIST=
` tests the bundles of another `dist/` directory.
+For the `module` targets `test/contract/bundle-isolation.test.js` loads the module bundle as a script into a fresh
+global scope and checks that it adds only `require`, keeps its exports and
+cache when another `require` bundle is loaded after it, passes other names
+along the `require` chain (`MODULE_NOT_FOUND` for unknown ones) and evaluates
+the module on the first `require`.
+`npm run test:bundle-set` (`test/bundle-set`) loads the module bundles of
+sd-utils, sd-random, sd-model and this package, plain and minified, into one
+global scope in all load orders and uses each library; `--sd-utils=`,
+`--sd-random=`, `--sd-model=`, `--sd-expression-engine=` take a `dist/`
+directory, a tarball or `published[@version]` (defaults: the installed
+sd-utils / sd-random, the published sd-model, `./dist`). `npm run test:build`
+runs it on the fresh build (`--sd-model=<.tgz>` for an unreleased sd-model).
+
```bash
npm test # all five targets on ./dist and the sources, then Jasmine
SD_EXPRESSION_ENGINE_TARGET=module node --test test/contract/ # one target
node test/contract/run.js --targets=module,source
node test/contract/run.js --dist= # another dist/ directory
npm run test:published # the bundles of sd-expression-engine@0.1.17 from npm
+npm run test:bundle-set # the four module bundles in one global scope
npx jasmine --config=test/jasmine.json # the Jasmine specs (test/specs) in Node
```
diff --git a/package.json b/package.json
index 7daeaa8..ed209ef 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"test": "node test/contract/run.js && jasmine --config=test/jasmine.json",
"test:published": "node test/contract/run.js --published=0.1.17",
"test:build": "node scripts/check-build.js",
+ "test:bundle-set": "node test/bundle-set/run.js",
"release": "release-it --verbose"
},
"repository": {
diff --git a/rollup.config.mjs b/rollup.config.mjs
index 5390f6c..f1a5d2e 100644
--- a/rollup.config.mjs
+++ b/rollup.config.mjs
@@ -54,7 +54,9 @@ function outputs(file, output) {
const common = Object.assign({exports: 'auto'}, output);
return [
Object.assign({file: file + '.js', sourcemap: 'inline'}, common),
- Object.assign({file: file + '.min.js', sourcemap: true, plugins: [terser()]}, common)
+ // toplevel: false, as for the other formats: @rollup/plugin-terser turns it on for cjs,
+ // which moves the variables of the require wrapper into the global scope
+ Object.assign({file: file + '.min.js', sourcemap: true, plugins: [terser({toplevel: false})]}, common)
];
}
diff --git a/scripts/check-build.js b/scripts/check-build.js
index 95cd515..c877b13 100644
--- a/scripts/check-build.js
+++ b/scripts/check-build.js
@@ -12,7 +12,9 @@
// 5. the contract tests against the four fresh bundles (dist/standalone and
// dist/sd-expression-engine.js after dist/sd-utils.js and dist/sd-random.js,
// plain and minified) and the sources bundled with browserify (target
-// `source`), and the Jasmine specs
+// `source`), the Jasmine specs, and test/bundle-set: the module bundles of
+// sd-utils, sd-random, sd-model and the fresh ones in one global scope
+// (installed sd-utils / sd-random, sd-model from --sd-model or the registry)
// 6. `npm pack`: the tarball contains the entry points, sources and dist; it is
// installed into an empty consumer that bundles require('sd-expression-engine')
// with browserify + babelify (the package's browserify.transform field, like
@@ -24,11 +26,12 @@
// outside dist/ changes
//
// Usage: node scripts/check-build.js [--keep] [--skip-consumer]
-// [--sd-utils=<.tgz>] [--sd-random=<.tgz>]
+// [--sd-utils=<.tgz>] [--sd-random=<.tgz>] [--sd-model=<.tgz>]
// --sd-utils / --sd-random install these tarballs in the copy and in the
// consumer instead of the locked / published versions (integration
// check of unreleased packages); package.json and the lockfile of
// the working tree are not changed
+// --sd-model the sd-model tarball for test/bundle-set (default: the published one)
'use strict';
const fs = require('fs');
@@ -234,6 +237,19 @@ function sourceMapProblem(file, minified, projectDir) {
return null;
}
+function runBundleSet(dir) {
+ const sdModel = option('--sd-model') ? ['--sd-model=' + path.resolve(option('--sd-model'))] : [];
+ const r = run(process.execPath, [path.join('test', 'bundle-set', 'run.js'), '--test-reporter=tap'].concat(sdModel), {cwd: dir});
+ const count = name => (r.log.match(new RegExp('^# ' + name + ' (\\d+)', 'm')) || [])[1];
+ const summary = 'tests ' + count('tests') + ', pass ' + count('pass') + ', fail ' + count('fail');
+ if (r.status === 0 && count('fail') === '0' && Number(count('pass')) > 0) {
+ ok('bundle set (module bundles of sd-utils, sd-random, sd-model and the fresh ones in one scope): ' + summary);
+ } else {
+ console.error(r.log.split('\n').filter(l => /^(sd-|not ok)|unexpected|gave|instead|error/.test(l)).slice(0, 30).join('\n'));
+ fail('bundle set: ' + summary);
+ }
+}
+
function runTests(dir, target, env) {
// TAP: the default reporter of Node 24 prints no summary lines to parse
const files = fs.readdirSync(path.join(dir, 'test', 'contract')).filter(f => f.endsWith('.test.js')).map(f => path.join('test', 'contract', f));
@@ -328,6 +344,7 @@ function main() {
runTests(copy, target);
}
runJasmine(copy);
+ runBundleSet(copy);
// 6 (before 7, which breaks the sources of its own copy)
if (!flag('--skip-consumer')) {
diff --git a/test/bundle-set/bundle-set.test.js b/test/bundle-set/bundle-set.test.js
new file mode 100644
index 0000000..f265672
--- /dev/null
+++ b/test/bundle-set/bundle-set.test.js
@@ -0,0 +1,212 @@
+// The module bundles of sd-utils, sd-random, sd-model and sd-expression-engine
+// (dist/.js and dist/.min.js) loaded as classic scripts into one
+// global scope, as