diff --git a/index.js b/index.js index 9bbac981..24a40be7 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,7 @@ const { _getExtensions, _parentName, _shouldHighlightCode, + isBabel8, } = require("./lib/babel-options-util"); const VersionChecker = require('ember-cli-version-checker'); @@ -139,8 +140,12 @@ module.exports = { }; if (shouldCompileModules) { - options.moduleIds = true; - options.getModuleId = require("./lib/relative-module-paths").getRelativeModulePath; + // In Babel 8, the root-level `moduleIds` and `getModuleId` options have + // been moved to plugin options (handled in _getModulesPlugin). + if (!isBabel8()) { + options.moduleIds = true; + options.getModuleId = require("./lib/relative-module-paths").getRelativeModulePath; + } } options.highlightCode = _shouldHighlightCode(this.parent); @@ -230,14 +235,20 @@ module.exports = { }, _getHelpersPlugin() { + const runtimeOptions = { + version: this._getHelperVersion(), + regenerator: false, + }; + + // In Babel 8, the `useESModules` option has been removed. + if (!isBabel8()) { + runtimeOptions.useESModules = true; + } + return [ [ require.resolve('@babel/plugin-transform-runtime'), - { - version: this._getHelperVersion(), - regenerator: false, - useESModules: true - } + runtimeOptions, ] ] }, diff --git a/lib/babel-options-util.js b/lib/babel-options-util.js index 5572edad..d6bf19fb 100644 --- a/lib/babel-options-util.js +++ b/lib/babel-options-util.js @@ -2,6 +2,7 @@ const VersionChecker = require("ember-cli-version-checker"); const resolvePackagePath = require("resolve-package-path"); const clone = require("clone"); const semver = require("semver"); +const { isBabel8 } = require("./babel-version"); const APP_BABEL_RUNTIME_VERSION = new WeakMap(); @@ -27,6 +28,13 @@ function _getPresetEnv(config, project) { delete presetOptions.plugins; delete presetOptions.postTransformPlugins; + // In Babel 8, the `loose` and `spec` options have been removed from + // @babel/preset-env in favor of top-level `assumptions`. + if (isBabel8()) { + delete presetOptions.loose; + delete presetOptions.spec; + } + return [require.resolve("@babel/preset-env"), presetOptions]; } @@ -34,11 +42,20 @@ function _getModulesPlugin() { const resolvePath = require("./relative-module-paths") .resolveRelativeModulePath; + const amdOptions = { noInterop: true }; + + // In Babel 8, the root-level `moduleIds` and `getModuleId` options have + // been moved to plugin options. + if (isBabel8()) { + amdOptions.moduleIds = true; + amdOptions.getModuleId = require("./relative-module-paths").getRelativeModulePath; + } + return [ [require.resolve("babel-plugin-module-resolver"), { resolvePath }], [ require.resolve("@babel/plugin-transform-modules-amd"), - { noInterop: true }, + amdOptions, ], ]; } @@ -274,14 +291,22 @@ function _getEmberDataPackagesPolyfill(config, parent) { } } function _getHelpersPlugin(project) { + const runtimeOptions = { + version: _getHelperVersion(project), + regenerator: false, + }; + + // In Babel 8, the `useESModules` option has been removed from + // @babel/plugin-transform-runtime. ESM is now auto-detected via + // package.json#exports. + if (!isBabel8()) { + runtimeOptions.useESModules = true; + } + return [ [ require.resolve("@babel/plugin-transform-runtime"), - { - version: _getHelperVersion(project), - regenerator: false, - useESModules: true, - }, + runtimeOptions, ], ]; } @@ -320,9 +345,12 @@ function _addDecoratorPlugins(plugins, options, config, parent, project) { ); } } else { + // In Babel 8, unknown options cause errors, so we omit the `legacy` + // option that was passed in Babel 7. + const staticBlockOptions = isBabel8() ? {} : { legacy: true }; addPlugin( plugins, - [require.resolve("@babel/plugin-transform-class-static-block"), { legacy: true }], + [require.resolve("@babel/plugin-transform-class-static-block"), staticBlockOptions], _buildClassFeaturePluginConstraints( { before: ["@babel/plugin-proposal-decorators"], @@ -343,9 +371,13 @@ function _addDecoratorPlugins(plugins, options, config, parent, project) { ); } } else { + // In Babel 8, the `legacy: true` option is replaced by `version: "legacy"`. + const decoratorOptions = isBabel8() + ? { version: "legacy" } + : { legacy: true }; addPlugin( plugins, - [require.resolve("@babel/plugin-proposal-decorators"), { legacy: true }], + [require.resolve("@babel/plugin-proposal-decorators"), decoratorOptions], _buildClassFeaturePluginConstraints( { before: ["@babel/plugin-transform-class-properties"], @@ -449,11 +481,14 @@ function _addTypeScriptPlugin(plugins, parent, project) { ); } } else { + // In Babel 8, `allowDeclareFields` is always enabled and the option has + // been removed. Passing it would cause a validation error. + const tsOptions = isBabel8() ? {} : { allowDeclareFields: true }; addPlugin( plugins, [ require.resolve("@babel/plugin-transform-typescript"), - { allowDeclareFields: true }, + tsOptions, ], { before: [ @@ -641,4 +676,5 @@ module.exports = { _getModulesPlugin, _getPresetEnv, _shouldHighlightCode, + isBabel8, }; diff --git a/lib/babel-version.js b/lib/babel-version.js new file mode 100644 index 00000000..135fc362 --- /dev/null +++ b/lib/babel-version.js @@ -0,0 +1,53 @@ +'use strict'; + +const semver = require("semver"); +const resolvePackagePath = require("resolve-package-path"); + +let _babelMajorVersion; + +/** + * Returns the major version of the installed @babel/core package. + * The result is cached for the lifetime of the process. + * + * @returns {number} The major version (e.g. 7 or 8) + */ +function getBabelMajorVersion() { + if (_babelMajorVersion !== undefined) return _babelMajorVersion; + + const pkgPath = resolvePackagePath("@babel/core", __dirname); + if (pkgPath) { + const pkg = require(pkgPath); + _babelMajorVersion = semver.major(pkg.version); + } else { + _babelMajorVersion = 7; + } + return _babelMajorVersion; +} + +/** + * Returns true if @babel/core v8 or higher is installed. + * + * @returns {boolean} + */ +function isBabel8() { + return getBabelMajorVersion() >= 8; +} + +/** + * Reset the cached version (used in tests). + */ +function _resetBabelMajorVersion() { + _babelMajorVersion = undefined; +} + +/** + * Override the cached Babel major version (used in tests). + * Pass `undefined` to reset to auto-detection. + * + * @param {number|undefined} version + */ +function _overrideBabelMajorVersion(version) { + _babelMajorVersion = version; +} + +module.exports = { getBabelMajorVersion, isBabel8, _resetBabelMajorVersion, _overrideBabelMajorVersion }; diff --git a/lib/ember-plugins.js b/lib/ember-plugins.js index 5cd21b00..438f214f 100644 --- a/lib/ember-plugins.js +++ b/lib/ember-plugins.js @@ -1,5 +1,6 @@ const semver = require("semver"); const resolvePackagePath = require("resolve-package-path"); +const { isBabel8 } = require("./babel-version"); function _getDebugMacroPlugins(appRoot) { const isProduction = process.env.EMBER_ENV === "production"; @@ -161,6 +162,11 @@ function _getModuleResolutionPlugins(config) { function _getProposalDecoratorsAndClassPlugins(config) { if (!config.shouldIgnoreDecoratorAndClassPlugins) { + // In Babel 8, `legacy: true` is replaced by `version: "legacy"`. + const decoratorOptions = isBabel8() + ? { version: "legacy" } + : { legacy: true }; + return [ /** * Required for apps to use `@ember/template-compliation` @@ -173,7 +179,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", decoratorOptions], ["@babel/plugin-transform-class-properties"], ]; } diff --git a/node-tests/get-babel-options-test.js b/node-tests/get-babel-options-test.js index 2889e877..b6f883fe 100644 --- a/node-tests/get-babel-options-test.js +++ b/node-tests/get-babel-options-test.js @@ -6,7 +6,11 @@ let { _addTypeScriptPlugin, _getAddonProvidedConfig, _addDecoratorPlugins, + _getHelpersPlugin, + _getPresetEnv, + _getModulesPlugin, } = require("../lib/babel-options-util"); +const { _overrideBabelMajorVersion } = require("../lib/babel-version"); let Addon = CoreObject.extend(AddonMixin); @@ -196,4 +200,144 @@ describe("get-babel-options", function () { expect(result.options).to.not.equal(babelOptions); }); }); + + describe("Babel 8 compatibility", function () { + afterEach(function () { + _overrideBabelMajorVersion(undefined); + }); + + describe("_addDecoratorPlugins (Babel 8)", function () { + it("should use version:'legacy' instead of legacy:true for decorators", function () { + _overrideBabelMajorVersion(8); + + let plugins = _addDecoratorPlugins( + [], + {}, + {}, + this.addon.parent, + this.addon.project + ); + + let decoratorPlugin = plugins.find( + (p) => Array.isArray(p) && String(p[0]).includes("plugin-proposal-decorators") + ); + + expect(decoratorPlugin).to.exist; + expect(decoratorPlugin[1]).to.deep.equal({ version: "legacy" }); + expect(decoratorPlugin[1]).to.not.have.property("legacy"); + }); + + it("should not pass legacy:true to static-block plugin", function () { + _overrideBabelMajorVersion(8); + + let plugins = _addDecoratorPlugins( + [], + {}, + {}, + this.addon.parent, + this.addon.project + ); + + let staticBlockPlugin = plugins.find( + (p) => Array.isArray(p) && String(p[0]).includes("plugin-transform-class-static-block") + ); + + expect(staticBlockPlugin).to.exist; + expect(staticBlockPlugin[1]).to.deep.equal({}); + }); + }); + + describe("_addTypeScriptPlugin (Babel 8)", function () { + it("should not pass allowDeclareFields option", function () { + _overrideBabelMajorVersion(8); + + let plugins = _addTypeScriptPlugin( + [], + this.addon.parent, + this.addon.project + ); + + let tsPlugin = plugins.find( + (p) => Array.isArray(p) && String(p[0]).includes("plugin-transform-typescript") + ); + + expect(tsPlugin).to.exist; + expect(tsPlugin[1]).to.deep.equal({}); + expect(tsPlugin[1]).to.not.have.property("allowDeclareFields"); + }); + }); + + describe("_getHelpersPlugin (Babel 8)", function () { + it("should not include useESModules option", function () { + _overrideBabelMajorVersion(8); + + let project = this.addon.project; + let result = _getHelpersPlugin(project); + + expect(result).to.have.lengthOf(1); + let pluginOptions = result[0][1]; + expect(pluginOptions).to.not.have.property("useESModules"); + expect(pluginOptions).to.have.property("regenerator", false); + }); + }); + + describe("_getPresetEnv (Babel 8)", function () { + it("should strip loose and spec options", function () { + _overrideBabelMajorVersion(8); + + let project = { + targets: { browsers: ["last 2 versions"] }, + }; + let config = { + options: { + loose: true, + spec: true, + }, + }; + + let result = _getPresetEnv(config, project); + + expect(result[1]).to.not.have.property("loose"); + expect(result[1]).to.not.have.property("spec"); + expect(result[1]).to.have.property("modules", false); + }); + }); + + describe("_getModulesPlugin (Babel 8)", function () { + it("should include moduleIds and getModuleId in AMD plugin options", function () { + _overrideBabelMajorVersion(8); + + let result = _getModulesPlugin(); + + expect(result).to.have.lengthOf(2); + + let amdPlugin = result[1]; + expect(amdPlugin[1]).to.have.property("noInterop", true); + expect(amdPlugin[1]).to.have.property("moduleIds", true); + expect(amdPlugin[1]).to.have.property("getModuleId"); + expect(amdPlugin[1].getModuleId).to.be.a("function"); + }); + }); + + describe("_addDecoratorPlugins (Babel 7 still works)", function () { + it("should use legacy:true for decorators in Babel 7", function () { + _overrideBabelMajorVersion(7); + + let plugins = _addDecoratorPlugins( + [], + {}, + {}, + this.addon.parent, + this.addon.project + ); + + let decoratorPlugin = plugins.find( + (p) => Array.isArray(p) && String(p[0]).includes("plugin-proposal-decorators") + ); + + expect(decoratorPlugin).to.exist; + expect(decoratorPlugin[1]).to.deep.equal({ legacy: true }); + }); + }); + }); }); diff --git a/package.json b/package.json index e018e6c0..e36c052e 100644 --- a/package.json +++ b/package.json @@ -40,17 +40,17 @@ "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/runtime": "7.12.18", + "@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 || ^8.0.0", "amd-name-resolver": "^1.3.1", "babel-plugin-debug-macros": "^0.3.4", "babel-plugin-ember-data-packages-polyfill": "^0.1.2", @@ -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/pnpm-lock.yaml b/pnpm-lock.yaml index fcda29fe..44e2fcaf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,38 +6,38 @@ settings: dependencies: '@babel/helper-compilation-targets': - specifier: ^7.20.7 + specifier: ^7.20.7 || ^8.0.0 version: 7.28.6 '@babel/plugin-proposal-decorators': - specifier: ^7.20.13 + specifier: ^7.20.13 || ^8.0.0 version: 7.29.0(@babel/core@7.29.0) '@babel/plugin-transform-class-properties': - specifier: ^7.16.5 + specifier: ^7.16.5 || ^8.0.0 version: 7.28.6(@babel/core@7.29.0) '@babel/plugin-transform-class-static-block': - specifier: ^7.22.11 + specifier: ^7.22.11 || ^8.0.0 version: 7.28.6(@babel/core@7.29.0) '@babel/plugin-transform-modules-amd': - specifier: ^7.20.11 + specifier: ^7.20.11 || ^8.0.0 version: 7.27.1(@babel/core@7.29.0) '@babel/plugin-transform-private-methods': - specifier: ^7.16.5 + specifier: ^7.16.5 || ^8.0.0 version: 7.28.6(@babel/core@7.29.0) '@babel/plugin-transform-private-property-in-object': - specifier: ^7.20.5 + specifier: ^7.20.5 || ^8.0.0 version: 7.28.6(@babel/core@7.29.0) '@babel/plugin-transform-runtime': - specifier: ^7.13.9 + specifier: ^7.13.9 || ^8.0.0 version: 7.29.0(@babel/core@7.29.0) '@babel/plugin-transform-typescript': - specifier: ^7.20.13 + specifier: ^7.20.13 || ^8.0.0 version: 7.28.6(@babel/core@7.29.0) '@babel/preset-env': - specifier: ^7.20.2 + specifier: ^7.20.2 || ^8.0.0 version: 7.29.0(@babel/core@7.29.0) '@babel/runtime': - specifier: 7.12.18 - version: 7.12.18 + specifier: ^7.12.18 || ^8.0.0 + version: 7.28.6 amd-name-resolver: specifier: ^1.3.1 version: 1.3.1 @@ -1305,11 +1305,11 @@ packages: resolution: {integrity: sha512-BogPQ7ciE6SYAUPtlm9tWbgI9+2AgqSam6QivMgXgAT+fKbgppaj4ZX15MHeLC1PVF5sNk70huBu20XxWOs8Cg==} dependencies: regenerator-runtime: 0.13.11 + dev: true /@babel/runtime@7.28.6: resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} - dev: true /@babel/template@7.28.6: resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} @@ -9425,6 +9425,7 @@ packages: /regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + dev: true /regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}