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