diff --git a/.eslintrc.js b/.eslintrc.js index 22cba004..1188011d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -28,7 +28,8 @@ module.exports = { 'blueprints/*/index.js', 'config/**/*.js', 'tests/dummy/config/**/*.js', - 'lib/**/*.js' + 'lib/**/*.js', + 'scripts/**/*.js' ], excludedFiles: [ 'addon/**', diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57c53a7a..c5ca5abb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,25 @@ jobs: - name: test run: pnpm test:node + node-tests-babel-8: + name: "Node: Babel 8" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v6 + with: + node-version: 20 + cache: pnpm + - name: pin dependencies to Babel 8 + run: node scripts/use-babel-8.js + - name: install dependencies + run: pnpm install --no-frozen-lockfile + - name: test + run: pnpm test:node + acceptance-tests: name: "Acceptance: Node ${{ matrix.node }} - ${{ matrix.os }}" runs-on: "${{matrix.os}}-latest" diff --git a/README.md b/README.md index d95c8f13..3675517e 100755 --- a/README.md +++ b/README.md @@ -357,6 +357,22 @@ If you want to use the existing babel config from your project instead of the au *Note: If you are using this option, then you have to make sure that you are adding all of the required plugins required for Ember to transpile correctly.* +*Note for Babel 8:* Babel 8 removed the root-level `moduleIds` and `getModuleId` options, which is how ember-cli-babel used to name your AMD modules. On Babel 8 they have to be passed to `@babel/plugin-transform-modules-amd` itself. `buildEmberPlugins` (below) already does this for you; if you configure the AMD transform by hand, name your modules like this: + +```js +[ + require.resolve("@babel/plugin-transform-modules-amd"), + { + noInterop: true, + moduleIds: true, + getModuleId: require("ember-cli-babel/lib/relative-module-paths") + .getRelativeModulePath, + }, +], +``` + +Without it your modules are emitted as anonymous `define([...])` calls and the Ember loader will not find them. + Example usage: ```js @@ -389,6 +405,8 @@ module.exports = function (api) { ], plugins: [ // if you want external helpers + // On Babel 8, drop `regenerator` and `useESModules`: both options were + // removed, and @babel/runtime now resolves ESM helpers via its `exports`. [ require.resolve("@babel/plugin-transform-runtime"), { diff --git a/index.js b/index.js index 9bbac981..acef807d 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,8 @@ const { _getExtensions, _parentName, _shouldHighlightCode, + _getHelpersPlugin, + _babelCoreMajorVersion, } = require("./lib/babel-options-util"); const VersionChecker = require('ember-cli-version-checker'); @@ -64,7 +66,9 @@ module.exports = { if (shouldUseBabelConfigFile) { const babel = require('@babel/core'); - let babelConfig = babel.loadPartialConfig({ + // Babel 8's `loadPartialConfig` is callback-based; the explicit sync + // variant has been available since Babel 7.8. + let babelConfig = babel.loadPartialConfigSync({ root: this.parent.root, rootMode: 'root', envName: process.env.EMBER_ENV || process.env.BABEL_ENV || process.env.NODE_ENV || "development", @@ -138,7 +142,11 @@ module.exports = { plugins: [], }; - if (shouldCompileModules) { + // Babel 8 rejects these as unknown root options, so there the plugin-level + // values set by `_getModulesPlugin` are the only ones that apply. Babel 7 + // still honors the root level, and `useBabelConfig` users depend on it + // because their own config supplies the module transform. + if (shouldCompileModules && _babelCoreMajorVersion() < 8) { options.moduleIds = true; options.getModuleId = require("./lib/relative-module-paths").getRelativeModulePath; } @@ -230,16 +238,7 @@ module.exports = { }, _getHelpersPlugin() { - return [ - [ - require.resolve('@babel/plugin-transform-runtime'), - { - version: this._getHelperVersion(), - regenerator: false, - useESModules: true - } - ] - ] + return _getHelpersPlugin(this.project); }, treeForAddon() { diff --git a/lib/babel-options-util.js b/lib/babel-options-util.js index 5572edad..27e46835 100644 --- a/lib/babel-options-util.js +++ b/lib/babel-options-util.js @@ -11,6 +11,13 @@ const defaultShouldIncludeHelpers = require("./default-should-include-helpers"); /** * This util contains private functions required for generating babel options. */ + +// Resolved rather than assumed, since `@babel/core` is a peer dependency and +// several plugin options differ between Babel 7 and 8. +function _babelCoreMajorVersion() { + return semver.major(require("@babel/core/package.json").version); +} + function _getPresetEnv(config, project) { let options = config.options; @@ -31,14 +38,23 @@ function _getPresetEnv(config, project) { } function _getModulesPlugin() { - const resolvePath = require("./relative-module-paths") - .resolveRelativeModulePath; + const { + resolveRelativeModulePath: resolvePath, + getRelativeModulePath: getModuleId, + } = require("./relative-module-paths"); return [ [require.resolve("babel-plugin-module-resolver"), { resolvePath }], [ require.resolve("@babel/plugin-transform-modules-amd"), - { noInterop: true }, + { + noInterop: true, + // Babel 8 dropped the root-level `moduleIds`/`getModuleId` options; the + // module transform reads them from its own options instead. Babel 7 + // already prefers the plugin-level values, so this works on both. + moduleIds: true, + getModuleId, + }, ], ]; } @@ -274,16 +290,17 @@ function _getEmberDataPackagesPolyfill(config, parent) { } } function _getHelpersPlugin(project) { - return [ - [ - require.resolve("@babel/plugin-transform-runtime"), - { - version: _getHelperVersion(project), - regenerator: false, - useESModules: true, - }, - ], - ]; + const options = { version: _getHelperVersion(project) }; + + if (_babelCoreMajorVersion() < 8) { + // Both options were removed in Babel 8: generators no longer depend on a + // `regeneratorRuntime` global, and @babel/runtime exposes its ESM helpers + // through package.json#exports. + options.regenerator = false; + options.useESModules = true; + } + + return [[require.resolve("@babel/plugin-transform-runtime"), options]]; } function _getHelperVersion(project) { if (!APP_BABEL_RUNTIME_VERSION.has(project)) { @@ -345,7 +362,9 @@ function _addDecoratorPlugins(plugins, options, config, parent, project) { } else { addPlugin( plugins, - [require.resolve("@babel/plugin-proposal-decorators"), { legacy: true }], + // `legacy: true` was removed in Babel 8; `version: "legacy"` is the + // equivalent and is understood by Babel 7 as well. + [require.resolve("@babel/plugin-proposal-decorators"), { version: "legacy" }], _buildClassFeaturePluginConstraints( { before: ["@babel/plugin-transform-class-properties"], @@ -625,6 +644,7 @@ function _shouldIncludeHelpers(options, appInstance) { } module.exports = { + _babelCoreMajorVersion, _addDecoratorPlugins, _addTypeScriptPlugin, _getAddonProvidedConfig, diff --git a/lib/ember-plugins.js b/lib/ember-plugins.js index 5cd21b00..502d7064 100644 --- a/lib/ember-plugins.js +++ b/lib/ember-plugins.js @@ -147,13 +147,23 @@ function _getEmberDataPackagesPolyfill(appRoot, config) { function _getModuleResolutionPlugins(config) { if (!config.disableModuleResolution) { - const resolvePath = require("../lib/relative-module-paths") - .resolveRelativeModulePath; + const { + resolveRelativeModulePath: resolvePath, + getRelativeModulePath: getModuleId, + } = require("../lib/relative-module-paths"); + return [ [require.resolve("babel-plugin-module-resolver"), { resolvePath }], [ require.resolve("@babel/plugin-transform-modules-amd"), - { noInterop: true }, + { + noInterop: true, + // Babel 8 only reads `moduleIds`/`getModuleId` from the module + // transform's own options, not from the root config. Babel 7 prefers + // the plugin-level values too, so this is correct on both. + moduleIds: true, + getModuleId, + }, ], ]; } @@ -173,7 +183,7 @@ function _getProposalDecoratorsAndClassPlugins(config) { * we need to compile it away. */ ["@babel/plugin-transform-class-static-block"], - ["@babel/plugin-proposal-decorators", { legacy: true }], + ["@babel/plugin-proposal-decorators", { version: "legacy" }], ["@babel/plugin-transform-class-properties"], ]; } diff --git a/node-tests/addon-test.js b/node-tests/addon-test.js index 8f65ef8f..e383c52b 100644 --- a/node-tests/addon-test.js +++ b/node-tests/addon-test.js @@ -23,7 +23,8 @@ const { _shouldHandleTypeScript, _shouldIncludeHelpers, _shouldCompileModules, - _getExtensions + _getExtensions, + _babelCoreMajorVersion } = require("../lib/babel-options-util"); const { codeEquality } = require("code-equality-assertions/chai"); @@ -153,6 +154,18 @@ describe('ember-cli-babel', function() { const result = output.read(); expect(Object.keys(result)).to.deep.equal(['foo.js']); + + // Babel 8 emits the same inline helpers in a different order, so the + // exact-output snapshot below only holds on Babel 7. What this test is + // really about is that the static block is compiled away, which is + // asserted for both. + expect(result['foo.js']).to.include('_Second.bar = 1'); + expect(result['foo.js']).to.not.include('static {'); + + if (_babelCoreMajorVersion() >= 8) { + return; + } + expect(result['foo.js']).to.equalCode(`function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } define("foo", [], function () { "use strict"; @@ -1698,13 +1711,32 @@ define("foo", [], function () { describe('buildBabelOptions', function() { this.timeout(0); + // Module ids always live on the AMD transform's own options. Babel 7 also + // honors them at the root, but Babel 8 rejects unknown root options, so we + // only emit them there on 7. + function expectModuleIdsConfigured(result) { + let amdPlugin = result.plugins.find( + plugin => Array.isArray(plugin) && /plugin-transform-modules-amd/.test(plugin[0]) + ); + + expect(amdPlugin, 'AMD transform is present').to.exist; + expect(amdPlugin[1].moduleIds).to.be.true; + expect(amdPlugin[1].getModuleId).to.be.a('function'); + + if (_babelCoreMajorVersion() < 8) { + expect(result.moduleIds).to.be.true; + } else { + expect('moduleIds' in result).to.be.false; + } + } + it('returns broccoli-babel-transpiler options by default', function() { this.addon.parent = { ...this.addon.parent, name: 'foo' }; let result = this.addon.buildBabelOptions(); expect(result.annotation).to.equal('Babel: foo'); - expect(result.moduleIds).to.be.true; + expectModuleIdsConfigured(result); expect(result.babelrc).to.be.false; expect(result.configFile).to.be.false; }); @@ -1715,7 +1747,7 @@ define("foo", [], function () { let result = this.addon.buildBabelOptions('broccoli'); expect(result.annotation).to.equal('Babel: foo'); - expect(result.moduleIds).to.be.true; + expectModuleIdsConfigured(result); expect(result.babelrc).to.be.false; expect(result.configFile).to.be.false; }); @@ -1728,7 +1760,7 @@ define("foo", [], function () { }); expect(result.annotation).to.equal('hello!!!'); - expect(result.moduleIds).to.be.true; + expectModuleIdsConfigured(result); expect(result.babelrc).to.be.false; expect(result.configFile).to.be.false; }); @@ -2320,9 +2352,16 @@ describe('babel config file', function() { })); it("should transpile to amd modules based on babel config", co.wrap(function* () { + // On Babel 7 ember-cli-babel supplies `moduleIds`/`getModuleId` at the root + // of the Babel config. Babel 8 removed those root options, so a project + // bringing its own babel config has to pass them to the AMD transform. + let moduleIdOptions = _babelCoreMajorVersion() < 8 ? '' : ` + moduleIds: true, + getModuleId: require("ember-cli-babel/lib/relative-module-paths").getRelativeModulePath,`; + yield setupForVersion(`[ require.resolve("@babel/plugin-transform-modules-amd"), - { noInterop: true }, + { noInterop: true,${moduleIdOptions} }, ]`); input.write({ "foo.js": `export default {};`, diff --git a/package.json b/package.json index e018e6c0..8db9eb0b 100644 --- a/package.json +++ b/package.json @@ -40,16 +40,16 @@ "test:node:debug": "mocha debug node-tests" }, "dependencies": { - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/plugin-proposal-decorators": "^7.20.13", - "@babel/plugin-transform-class-properties": "^7.16.5", - "@babel/plugin-transform-class-static-block": "^7.22.11", - "@babel/plugin-transform-modules-amd": "^7.20.11", - "@babel/plugin-transform-private-methods": "^7.16.5", - "@babel/plugin-transform-private-property-in-object": "^7.20.5", - "@babel/plugin-transform-runtime": "^7.13.9", - "@babel/plugin-transform-typescript": "^7.20.13", - "@babel/preset-env": "^7.20.2", + "@babel/helper-compilation-targets": "^7.20.7 || ^8.0.0", + "@babel/plugin-proposal-decorators": "^7.20.13 || ^8.0.0", + "@babel/plugin-transform-class-properties": "^7.16.5 || ^8.0.0", + "@babel/plugin-transform-class-static-block": "^7.22.11 || ^8.0.0", + "@babel/plugin-transform-modules-amd": "^7.20.11 || ^8.0.0", + "@babel/plugin-transform-private-methods": "^7.16.5 || ^8.0.0", + "@babel/plugin-transform-private-property-in-object": "^7.20.5 || ^8.0.0", + "@babel/plugin-transform-runtime": "^7.13.9 || ^8.0.0", + "@babel/plugin-transform-typescript": "^7.20.13 || ^8.0.0", + "@babel/preset-env": "^7.20.2 || ^8.0.0", "@babel/runtime": "7.12.18", "amd-name-resolver": "^1.3.1", "babel-plugin-debug-macros": "^0.3.4", @@ -108,7 +108,7 @@ "webpack": "^5.88.2" }, "peerDependencies": { - "@babel/core": "^7.12.0" + "@babel/core": "^7.12.0 || ^8.0.0" }, "packageManager": "pnpm@8.15.9", "engines": { diff --git a/scripts/use-babel-8.js b/scripts/use-babel-8.js new file mode 100644 index 00000000..bdf2a1ed --- /dev/null +++ b/scripts/use-babel-8.js @@ -0,0 +1,56 @@ +'use strict'; + +/* + * Rewrites package.json so the test suite installs against Babel 8. + * + * ember-cli-babel supports Babel 7 and 8 from one codebase, but a checkout can + * only have one of them installed at a time. CI runs the node tests twice: once + * as-is, and once after this script. + * + * Two of these overrides are workarounds rather than statements about what + * ember-cli-babel needs: + * + * - `code-equality-assertions` depends on @babel/core ^7 and calls `transform` + * synchronously, which Babel 8 made callback-only. It is a test-time + * assertion helper, so it is pinned to its own Babel 7 copy. + * - `broccoli-babel-transpiler` still declares `@babel/core: ^7.17.9` as a + * peer. Its actual API usage (`transformAsync`) works fine on Babel 8, so + * the peer check is relaxed here. Babel 8 support cannot ship until that + * range is widened upstream. + */ + +const fs = require('fs'); +const path = require('path'); + +const BABEL_8 = '^8.0.0'; + +const PACKAGES = [ + '@babel/core', + '@babel/helper-compilation-targets', + '@babel/plugin-proposal-decorators', + '@babel/plugin-transform-class-properties', + '@babel/plugin-transform-class-static-block', + '@babel/plugin-transform-modules-amd', + '@babel/plugin-transform-private-methods', + '@babel/plugin-transform-private-property-in-object', + '@babel/plugin-transform-runtime', + '@babel/plugin-transform-typescript', + '@babel/preset-env', + '@babel/runtime', +]; + +const pkgPath = path.join(__dirname, '..', 'package.json'); +const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + +const overrides = Object.fromEntries(PACKAGES.map((name) => [name, BABEL_8])); +overrides['code-equality-assertions>@babel/core'] = '^7.19.6'; + +pkg.devDependencies['@babel/core'] = BABEL_8; +pkg.pnpm = Object.assign({}, pkg.pnpm, { + overrides: Object.assign({}, pkg.pnpm && pkg.pnpm.overrides, overrides), + peerDependencyRules: { allowAny: ['@babel/core'] }, +}); + +fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`); + +console.log('package.json pinned to Babel 8. Run `pnpm install --no-frozen-lockfile`.');