From 2b163d8082766af0318aeb682d97526034742458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Mon, 11 Jul 2022 16:43:59 +0200 Subject: [PATCH] Prepare for compiling Babel to native ESM (#13414) * Move dynamic import in `@babel/core` to a CJS file * Allow compiling to ESM * Add `USE_ESM` compile-time flag * Fix `@babel/eslint-parser` * Provide CJS bundle of `@babel/parser` * Minor test fixes * Add CommonJS proxy to `@babel/core` for backward compat * Test ESM on CI * TODO: skip failing packages * Add missing `devDependency` to `@babel/helper-remap-async-to-generator` * Update contributing.md * Apply suggestions from self-review * Update Gulpfile.mjs * Prettier * Explicitly use node in makefile --- .eslintrc.cjs | 2 +- .github/workflows/ci.yml | 24 ++++ .github/workflows/e2e-tests-esm.yml | 82 ++++++++++++++ .gitignore | 2 + CONTRIBUTING.md | 35 ++++-- Gulpfile.mjs | 65 ++++++++++- Makefile | 14 ++- babel.config.js | 103 ++++++++++++++++-- constraints.pro | 3 +- eslint/babel-eslint-parser/src/client.cjs | 2 +- eslint/babel-eslint-parser/src/index.cjs | 4 +- .../src/worker/babel-core.cjs | 4 +- .../src/worker/handle-message.cjs | 2 +- .../test/integration/config-files.js | 22 ++-- jest.config.js | 10 ++ packages/babel-core/cjs-proxy.cjs | 29 +++++ packages/babel-core/package.json | 11 +- .../src/config/files/import-meta-resolve.ts | 2 +- .../config/files/{import.ts => import.cjs} | 4 +- .../src/config/files/module-types.ts | 2 +- .../package.json | 1 + .../scripts/generate-regenerator-runtime.js | 2 +- packages/babel-parser/index.cjs | 5 + packages/babel-parser/package.json | 17 ++- .../test/fixtures/general/issue-10339/exec.js | 2 +- packages/babel-preset-env/test/index.spec.js | 23 +++- scripts/set-module-type.js | 45 ++++++++ yarn.lock | 42 +++---- 28 files changed, 477 insertions(+), 82 deletions(-) create mode 100644 .github/workflows/e2e-tests-esm.yml create mode 100644 packages/babel-core/cjs-proxy.cjs rename packages/babel-core/src/config/files/{import.ts => import.cjs} (77%) create mode 100644 packages/babel-parser/index.cjs create mode 100755 scripts/set-module-type.js diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 471a7fa5111d..03b2f74382d6 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -54,7 +54,7 @@ module.exports = { "guard-for-in": "error", "import/extensions": ["error", { json: "always", cjs: "always" }], }, - globals: { PACKAGE_JSON: "readonly" }, + globals: { PACKAGE_JSON: "readonly", USE_ESM: "readonly" }, }, { files: [ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37eba6a9ba73..382fce0b8c10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,30 @@ jobs: - name: Upload coverage report uses: codecov/codecov-action@v1 + test-esm: + name: Test ESM version + needs: prepare-yarn-cache + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Use Node.js latest + uses: actions/setup-node@v3 + with: + node-version: "*" + cache: "yarn" + - name: Use ESM and build + run: make use-esm + env: + USE_ESM: true + - name: Test + # Hack: --color has supports-color@5 returned true for GitHub CI + # Remove once `chalk` is bumped to 4.0. + run: yarn jest --ci --color + env: + BABEL_ENV: test + USE_ESM: true + build: name: Build Babel Artifacts needs: prepare-yarn-cache diff --git a/.github/workflows/e2e-tests-esm.yml b/.github/workflows/e2e-tests-esm.yml new file mode 100644 index 000000000000..ee50612fa2c8 --- /dev/null +++ b/.github/workflows/e2e-tests-esm.yml @@ -0,0 +1,82 @@ +name: E2E tests (esm) + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + e2e-publish: + name: Publish to local Verdaccio registry + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Use Node.js latest + uses: actions/setup-node@v3 + with: + node-version: "*" + cache: "yarn" + - name: Use ESM + run: make use-esm + - name: Publish + run: ./scripts/integration-tests/publish-local.sh + env: + USE_ESM: true + - uses: actions/upload-artifact@v3 + with: + name: verdaccio-workspace + path: /tmp/verdaccio-workspace + + e2e-tests: + name: Test + needs: e2e-publish + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + project: + - create-react-app + steps: + - name: Get yarn1 cache directory path + id: yarn1-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Checkout code + uses: actions/checkout@v3 + - name: Use Node.js latest + uses: actions/setup-node@v3 + with: + node-version: "*" + - name: Get yarn3 cache directory path + id: yarn3-cache-dir-path + run: echo "::set-output name=dir::$(yarn config get cacheFolder)" + - name: Use yarn1 cache + uses: actions/cache@v3 + id: yarn1-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn1-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn1-e2e-breaking-${{ matrix.project }}-${{ hashFiles('**/yarn.lock') }} + restore-keys: ${{ runner.os }}-yarn1-e2e-breaking-${{ matrix.project }}- + - name: Use yarn3 cache + uses: actions/cache@v3 + id: yarn3-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn3-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn3-e2e-breaking-${{ matrix.project }}-${{ hashFiles('**/yarn.lock') }} + restore-keys: ${{ runner.os }}-yarn3-e2e-breaking-${{ matrix.project }}- + - name: Clean babel cache + run: | + rm -rf ${{ steps.yarn1-cache-dir-path.outputs.dir }}/*babel* + rm -rf ${{ steps.yarn3-cache-dir-path.outputs.dir }}/*babel* + - uses: actions/download-artifact@v3 + with: + name: verdaccio-workspace + path: /tmp/verdaccio-workspace + - name: Test + run: ./scripts/integration-tests/e2e-${{ matrix.project }}.sh + env: + USE_ESM: true diff --git a/.gitignore b/.gitignore index 11d0f00a0a8a..d28514220d56 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,5 @@ packages/babel-standalone/babel.min.js /test/runtime-integration/*/output.js /test/runtime-integration/*/absolute-output.js + +.module-type diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b341efc5dc5b..d64fe3b599e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -80,6 +80,21 @@ If you wish to build a copy of Babel for distribution, then run: $ make build-dist ``` +## Develop compiling to CommonJS or to ECMAScript modules + +Babel can currently be compiled both to CJS and to ESM. You can toggle between those two +modes by running one of the following commands: +```sh +make use-esm +``` +```sh +make use-cjs +``` + +Note that they need to recompile the whole monorepo, so please make sure to stop any running `make watch` process before running them. + +If you never run a `make use-*` (or if you delete the `.module-type` file that they generate), our build process defaults to CJS. + ### Running linting/tests #### Lint @@ -121,16 +136,17 @@ $ TEST_ONLY=babel-cli make test More options TEST_ONLY will also match substrings of the package name: - ```sh - # Run tests for the @babel/plugin-transform-classes package. - $ TEST_ONLY=babel-plugin-transform-classes make test - ``` +```sh +# Run tests for the @babel/plugin-transform-classes package. +$ TEST_ONLY=babel-plugin-transform-classes make test +``` - Or you can use Yarn: +Or you can use Yarn: + +```sh +$ yarn jest babel-cli +``` - ```sh - $ yarn jest babel-cli - ```
@@ -145,16 +161,19 @@ $ TEST_GREP=transformation make test Substitute spaces for hyphens and forward slashes when targeting specific test names: For example, for the following path: + ```sh packages/babel-plugin-transform-arrow-functions/test/fixtures/arrow-functions/destructuring-parameters ``` You can use: + ```sh $ TEST_GREP="arrow functions destructuring parameters" make test ``` Or you can directly use Yarn: + ```sh $ yarn jest -t "arrow functions destructuring parameters" ``` diff --git a/Gulpfile.mjs b/Gulpfile.mjs index 21aa3020848a..454967b9d829 100644 --- a/Gulpfile.mjs +++ b/Gulpfile.mjs @@ -25,6 +25,14 @@ import { resolve as importMetaResolve } from "import-meta-resolve"; import rollupBabelSource from "./scripts/rollup-plugin-babel-source.js"; import formatCode from "./scripts/utils/formatCode.js"; +let USE_ESM = false; +try { + const type = fs + .readFileSync(new URL(".module-type", import.meta.url), "utf-8") + .trim(); + USE_ESM = type === "module"; +} catch {} + const require = createRequire(import.meta.url); const monorepoRoot = path.dirname(fileURLToPath(import.meta.url)); @@ -480,10 +488,15 @@ const libBundles = [ "packages/babel-plugin-bugfix-safari-id-destructuring-collision-in-function-expression", ].map(src => ({ src, - format: "cjs", + format: USE_ESM ? "esm" : "cjs", dest: "lib", })); +const cjsBundles = [ + // This is used by Prettier, @babel/register and @babel/eslint-parser + { src: "packages/babel-parser" }, +]; + const dtsBundles = ["packages/babel-types"]; const standaloneBundle = [ @@ -581,6 +594,45 @@ ${fs.readFileSync(path.join(path.dirname(input), "license"), "utf8")}*/ ); }); +gulp.task("build-cjs-bundles", () => { + if (!USE_ESM) { + fancyLog( + chalk.yellow( + "Skipping CJS-compat bundles for ESM-based builds, because not compiling to ESM" + ) + ); + return Promise.resolve(); + } + return Promise.all( + cjsBundles.map(async ({ src, external = [] }) => { + const input = `./${src}/lib/index.js`; + const output = `./${src}/lib/index.cjs`; + + const bundle = await rollup({ + input, + external, + onwarn(warning, warn) { + if (warning.code === "CIRCULAR_DEPENDENCY") return; + warn(warning); + }, + plugins: [ + rollupCommonJs({ defaultIsModuleExports: true }), + rollupNodeResolve({ + extensions: [".js", ".mjs", ".cjs", ".json"], + preferBuiltins: true, + }), + ], + }); + + await bundle.write({ + file: output, + format: "cjs", + sourcemap: false, + }); + }) + ); +}); + gulp.task( "build", gulp.series( @@ -590,7 +642,7 @@ gulp.task( // rebuild @babel/types and @babel/helpers since // type-helpers and generated helpers may be changed "build-babel", - "generate-standalone" + gulp.parallel("generate-standalone", "build-cjs-bundles") ) ); @@ -612,7 +664,8 @@ gulp.task( gulp.series( "generate-type-helpers", // rebuild @babel/types since type-helpers may be changed - "build-no-bundle" + "build-no-bundle", + "build-cjs-bundles" ) ) ) @@ -631,5 +684,11 @@ gulp.task( "./packages/babel-helpers/src/helpers/*.js", gulp.task("generate-runtime-helpers") ); + if (USE_ESM) { + gulp.watch( + cjsBundles.map(({ src }) => `./${src}/lib/**.js`), + gulp.task("build-cjs-bundles") + ); + } }) ); diff --git a/Makefile b/Makefile index 0e6b0024a9d7..79d1be069148 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ YARN := yarn NODE := $(YARN) node -.PHONY: build build-dist watch lint fix clean test-clean test-only test test-ci publish bootstrap +.PHONY: build build-dist watch lint fix clean test-clean test-only test test-ci publish bootstrap use-esm use-cjs build: build-no-bundle ifneq ("$(BABEL_COVERAGE)", "true") @@ -24,6 +24,7 @@ ifneq ("$(BABEL_COVERAGE)", "true") endif build-bundle: clean clean-lib + node ./scripts/set-module-type.js $(YARN) gulp build $(MAKE) build-flow-typings $(MAKE) build-dist @@ -34,6 +35,7 @@ build-no-bundle-ci: bootstrap-only $(MAKE) build-dist build-no-bundle: clean clean-lib + node ./scripts/set-module-type.js BABEL_ENV=development $(YARN) gulp build-dev $(MAKE) build-flow-typings $(MAKE) build-dist @@ -186,6 +188,7 @@ prepublish: $(MAKE) bootstrap-only $(MAKE) prepublish-build IS_PUBLISH=true $(MAKE) test + node ./scripts/set-module-type.js clean new-version-checklist: # @echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" @@ -221,6 +224,7 @@ ifneq ("$(I_AM_USING_VERDACCIO)", "I_AM_SURE") endif $(YARN) release-tool version $(VERSION) --all --yes --tag-version-prefix="version-e2e-test-" $(MAKE) prepublish-build + node ./scripts/set-module-type.js clean YARN_NPM_PUBLISH_REGISTRY=http://localhost:4873 $(YARN) release-tool publish --yes --tag-version-prefix="version-e2e-test-" $(MAKE) clean @@ -230,6 +234,14 @@ bootstrap-only: clean-all bootstrap: bootstrap-only $(MAKE) generate-tsconfig build +use-cjs: + node ./scripts/set-module-type.js script + $(MAKE) bootstrap + +use-esm: + node ./scripts/set-module-type.js module + $(MAKE) bootstrap + clean-lib: $(foreach source, $(SOURCES), \ $(call clean-source-lib, $(source))) diff --git a/babel.config.js b/babel.config.js index 434bbe4d8963..d57134718f4d 100644 --- a/babel.config.js +++ b/babel.config.js @@ -11,6 +11,14 @@ function normalize(src) { module.exports = function (api) { const env = api.env(); + const outputType = api.cache.invalidate(() => { + try { + return fs.readFileSync(__dirname + "/.module-type", "utf-8").trim(); + } catch (_) { + return "script"; + } + }); + const sources = ["packages/*/src", "codemods/*/src", "eslint/*/src"]; const includeCoverage = process.env.BABEL_COVERAGE === "true"; @@ -54,7 +62,8 @@ module.exports = function (api) { }; let targets = {}; - let convertESM = true; + let convertESM = outputType === "script"; + let addImportExtension = !convertESM; let ignoreLib = true; let includeRegeneratorRuntime = false; let needsPolyfillsForOldNode = false; @@ -84,6 +93,7 @@ module.exports = function (api) { case "standalone": includeRegeneratorRuntime = true; convertESM = false; + addImportExtension = false; ignoreLib = false; // rollup-commonjs will converts node_modules to ESM unambiguousSources.push( @@ -95,6 +105,7 @@ module.exports = function (api) { break; case "rollup": convertESM = false; + addImportExtension = false; ignoreLib = false; // rollup-commonjs will converts node_modules to ESM unambiguousSources.push( @@ -179,10 +190,6 @@ module.exports = function (api) { pluginPackageJsonMacro, - process.env.STRIP_BABEL_8_FLAG && [ - pluginToggleBabel8Breaking, - { breaking: bool(process.env.BABEL_8_BREAKING) }, - ], needsPolyfillsForOldNode && pluginPolyfillsOldNode, ].filter(Boolean), overrides: [ @@ -216,7 +223,25 @@ module.exports = function (api) { { test: sources.map(normalize), assumptions: sourceAssumptions, - plugins: [transformNamedBabelTypesImportToDestructuring], + plugins: [ + transformNamedBabelTypesImportToDestructuring, + addImportExtension ? pluginAddImportExtension : null, + + [ + pluginToggleBooleanFlag, + { name: "USE_ESM", value: outputType === "module" }, + "flag-USE_ESM", + ], + + process.env.STRIP_BABEL_8_FLAG && [ + pluginToggleBooleanFlag, + { + name: "process.env.BABEL_8_BREAKING", + value: bool(process.env.BABEL_8_BREAKING), + }, + "flag-BABEL_8_BREAKING", + ], + ].filter(Boolean), }, convertESM && { test: lazyRequireSources.map(normalize), @@ -444,20 +469,19 @@ function pluginPolyfillsOldNode({ template, types: t }) { }, }; } - -function pluginToggleBabel8Breaking({ types: t }, { breaking }) { +function pluginToggleBooleanFlag({ types: t }, { name, value }) { return { visitor: { "IfStatement|ConditionalExpression"(path) { let test = path.get("test"); - let keepConsequent = breaking; + let keepConsequent = value; if (test.isUnaryExpression({ operator: "!" })) { test = test.get("argument"); keepConsequent = !keepConsequent; } - // yarn-plugin-conditions inject bool(process.env.BABEL_8_BREAKING) + // yarn-plugin-conditions injects bool(process.env.BABEL_8_BREAKING) // tests, to properly cast the env variable to a boolean. if ( test.isCallExpression() && @@ -467,7 +491,7 @@ function pluginToggleBabel8Breaking({ types: t }, { breaking }) { test = test.get("arguments")[0]; } - if (!test.matchesPattern("process.env.BABEL_8_BREAKING")) return; + if (!test.isIdentifier({ name }) && !test.matchesPattern(name)) return; path.replaceWith( keepConsequent @@ -476,7 +500,12 @@ function pluginToggleBabel8Breaking({ types: t }, { breaking }) { ); }, MemberExpression(path) { - if (path.matchesPattern("process.env.BABEL_8_BREAKING")) { + if (path.matchesPattern(name)) { + throw path.buildCodeFrameError("This check could not be stripped."); + } + }, + ReferencedIdentifier(path) { + if (path.node.name === name) { throw path.buildCodeFrameError("This check could not be stripped."); } }, @@ -658,6 +687,56 @@ function pluginImportMetaUrl({ types: t, template }) { }; } +function pluginAddImportExtension() { + return { + visitor: { + "ImportDeclaration|ExportDeclaration"({ node }) { + const { source } = node; + if (!source) return; + + if (source.value.startsWith(".") && !/\.[a-z]+$/.test(source.value)) { + const dir = pathUtils.dirname(this.filename); + + try { + const pkg = JSON.parse( + fs.readFileSync( + pathUtils.join(dir, `${source.value}/package.json`) + ), + "utf8" + ); + + if (pkg.main) source.value = pathUtils.join(source.value, pkg.main); + } catch (_) {} + + try { + if (fs.statSync(pathUtils.join(dir, source.value)).isFile()) return; + } catch (_) {} + + for (const [src, lib = src] of [["ts", "js"], ["js"], ["cjs"]]) { + try { + fs.statSync(pathUtils.join(dir, `${source.value}.${src}`)); + source.value += `.${lib}`; + return; + } catch (_) {} + } + + source.value += "/index.js"; + } + if ( + source.value.startsWith("lodash/") || + source.value.startsWith("core-js-compat/") || + source.value === "babel-plugin-dynamic-import-node/utils" + ) { + source.value += ".js"; + } + if (source.value.startsWith("@babel/preset-modules/")) { + source.value += "/index.js"; + } + }, + }, + }; +} + const tokenTypesMapping = new Map(); const tokenTypeSourcePath = "./packages/babel-parser/src/tokenizer/types.js"; diff --git a/constraints.pro b/constraints.pro index 8fdb6c7976d6..f557c2f4955c 100644 --- a/constraints.pro +++ b/constraints.pro @@ -81,8 +81,9 @@ gen_enforced_field(WorkspaceCwd, 'exports', '{ ".": "./lib/index.js", "./package % Exclude packages with more complex `exports` workspace_ident(WorkspaceCwd, WorkspaceIdent), WorkspaceIdent \= '@babel/compat-data', - WorkspaceIdent \= '@babel/plugin-transform-react-jsx', % TODO: Remove in Babel 8 WorkspaceIdent \= '@babel/helper-plugin-test-runner', % TODO: Remove in Babel 8 + WorkspaceIdent \= '@babel/parser', + WorkspaceIdent \= '@babel/plugin-transform-react-jsx', % TODO: Remove in Babel 8 WorkspaceIdent \= '@babel/standalone', \+ atom_concat('@babel/eslint-', _, WorkspaceIdent), \+ atom_concat('@babel/runtime', _, WorkspaceIdent). diff --git a/eslint/babel-eslint-parser/src/client.cjs b/eslint/babel-eslint-parser/src/client.cjs index 7925d35b5c00..40d491af1ee0 100644 --- a/eslint/babel-eslint-parser/src/client.cjs +++ b/eslint/babel-eslint-parser/src/client.cjs @@ -90,7 +90,7 @@ exports.WorkerClient = class WorkerClient extends Client { } }; -if (!process.env.BABEL_8_BREAKING) { +if (!USE_ESM) { exports.LocalClient = class LocalClient extends Client { static #handleMessage; diff --git a/eslint/babel-eslint-parser/src/index.cjs b/eslint/babel-eslint-parser/src/index.cjs index f713b839f20e..9725c52166f5 100644 --- a/eslint/babel-eslint-parser/src/index.cjs +++ b/eslint/babel-eslint-parser/src/index.cjs @@ -3,9 +3,7 @@ const analyzeScope = require("./analyze-scope.cjs"); const baseParse = require("./parse.cjs"); const { LocalClient, WorkerClient } = require("./client.cjs"); -const client = new ( - process.env.BABEL_8_BREAKING ? WorkerClient : LocalClient -)(); +const client = new (USE_ESM ? WorkerClient : LocalClient)(); exports.parse = function (code, options = {}) { return baseParse(code, normalizeESLintConfig(options), client); diff --git a/eslint/babel-eslint-parser/src/worker/babel-core.cjs b/eslint/babel-eslint-parser/src/worker/babel-core.cjs index 47486218db92..ebd71b5afd6d 100644 --- a/eslint/babel-eslint-parser/src/worker/babel-core.cjs +++ b/eslint/babel-eslint-parser/src/worker/babel-core.cjs @@ -10,8 +10,8 @@ function initialize(babel) { exports.createConfigItem = babel.createConfigItem; } -if (process.env.BABEL_8_BREAKING) { - exports.init = import("@babel/core").then(ns => initialize(ns.default)); +if (USE_ESM) { + exports.init = import("@babel/core").then(initialize); } else { initialize(require("@babel/core")); } diff --git a/eslint/babel-eslint-parser/src/worker/handle-message.cjs b/eslint/babel-eslint-parser/src/worker/handle-message.cjs index 7a198a847ff8..f2a82028840d 100644 --- a/eslint/babel-eslint-parser/src/worker/handle-message.cjs +++ b/eslint/babel-eslint-parser/src/worker/handle-message.cjs @@ -24,7 +24,7 @@ module.exports = function handleMessage(action, payload) { maybeParse(payload.code, options), ); case "MAYBE_PARSE_SYNC": - if (!process.env.BABEL_8_BREAKING) { + if (!USE_ESM) { return maybeParse( payload.code, normalizeBabelParseConfigSync(payload.options), diff --git a/eslint/babel-eslint-tests/test/integration/config-files.js b/eslint/babel-eslint-tests/test/integration/config-files.js index 3ed1a87d257f..8ef6a5b1ad0c 100644 --- a/eslint/babel-eslint-tests/test/integration/config-files.js +++ b/eslint/babel-eslint-tests/test/integration/config-files.js @@ -1,11 +1,22 @@ import eslint from "eslint"; import path from "path"; import { fileURLToPath } from "url"; +import fs from "fs"; + +let USE_ESM = false; +try { + const type = fs + .readFileSync(new URL("../../../../.module-type", import.meta.url), "utf-8") + .trim(); + USE_ESM = type === "module"; +} catch {} describe("Babel config files", () => { - const babel8 = process.env.BABEL_8_BREAKING ? it : it.skip; + const itESM = USE_ESM ? it : it.skip; + const itNode12upNoESM = + USE_ESM || parseInt(process.versions.node) < 12 ? it.skip : it; - babel8("works with babel.config.mjs", () => { + itESM("works with babel.config.mjs", () => { const engine = new eslint.CLIEngine({ ignore: false }); expect( engine.executeOnFiles([ @@ -17,12 +28,7 @@ describe("Babel config files", () => { ).toMatchObject({ errorCount: 0 }); }); - const babel7node12 = - process.env.BABEL_8_BREAKING || parseInt(process.versions.node) < 12 - ? it.skip - : it; - - babel7node12("experimental worker works with babel.config.mjs", () => { + itNode12upNoESM("experimental worker works with babel.config.mjs", () => { const engine = new eslint.CLIEngine({ ignore: false }); expect( engine.executeOnFiles([ diff --git a/jest.config.js b/jest.config.js index d03c48348873..984394e6f8e0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,4 @@ +const fs = require("fs"); const semver = require("semver"); const nodeVersion = process.versions.node; const supportsESMAndJestLightRunner = semver.satisfies( @@ -8,6 +9,12 @@ const supportsESMAndJestLightRunner = semver.satisfies( ); const isPublishBundle = process.env.IS_PUBLISH; +let LIB_USE_ESM = false; +try { + const type = fs.readFileSync(`${__dirname}/.module-type`, "utf-8").trim(); + LIB_USE_ESM = type === "module"; +} catch (_) {} + module.exports = { runner: supportsESMAndJestLightRunner ? "jest-light-runner" : "jest-runner", @@ -36,6 +43,9 @@ module.exports = { // Some tests require internal files of bundled packages, which are not available // in production builds. They are marked using the .skip-bundled.js extension. ...(isPublishBundle ? ["\\.skip-bundled\\.js$"] : []), + ...(LIB_USE_ESM + ? ["/babel-node/", "/babel-register/", "/babel-helpers/"] + : []), ], testEnvironment: "node", transformIgnorePatterns: [ diff --git a/packages/babel-core/cjs-proxy.cjs b/packages/babel-core/cjs-proxy.cjs new file mode 100644 index 000000000000..4bf8b5cb9ced --- /dev/null +++ b/packages/babel-core/cjs-proxy.cjs @@ -0,0 +1,29 @@ +"use strict"; + +const babelP = import("./lib/index.js"); + +const functionNames = [ + "createConfigItem", + "loadPartialConfig", + "loadOptions", + "transform", + "transformFile", + "transformFromAst", + "parse", +]; + +for (const name of functionNames) { + exports[`${name}Sync`] = function () { + throw new Error( + `"${name}Sync" is not supported when loading @babel/core using require()` + ); + }; + exports[name] = function (...args) { + babelP.then(babel => { + babel[name](...args); + }); + }; + exports[`${name}Async`] = function (...args) { + return babelP.then(babel => babel[`${name}Async`](...args)); + }; +} diff --git a/packages/babel-core/package.json b/packages/babel-core/package.json index 67a23cc21c55..c4e90b7f1cc2 100644 --- a/packages/babel-core/package.json +++ b/packages/babel-core/package.json @@ -50,7 +50,7 @@ "@babel/code-frame": "workspace:^", "@babel/generator": "workspace:^", "@babel/helper-compilation-targets": "workspace:^", - "@babel/helper-module-transforms": "condition:BABEL_8_BREAKING ? : workspace:^7.18.6", + "@babel/helper-module-transforms": "workspace:^", "@babel/helpers": "workspace:^", "@babel/parser": "workspace:^", "@babel/template": "workspace:^", @@ -84,7 +84,14 @@ ], "USE_ESM": [ { - "type": "module" + "type": "module", + "exports": { + ".": { + "require": "./cjs-proxy.cjs", + "default": "./lib/index.js" + }, + "./package.json": "./package.json" + } }, null ] diff --git a/packages/babel-core/src/config/files/import-meta-resolve.ts b/packages/babel-core/src/config/files/import-meta-resolve.ts index 591aec4d9de4..3807a0776bad 100644 --- a/packages/babel-core/src/config/files/import-meta-resolve.ts +++ b/packages/babel-core/src/config/files/import-meta-resolve.ts @@ -6,7 +6,7 @@ const require = createRequire(import.meta.url); let import_; try { // Node < 13.3 doesn't support import() syntax. - import_ = require("./import").default; + import_ = require("./import.cjs"); } catch {} // import.meta.resolve is only available in ESM, but this file is compiled to CJS. diff --git a/packages/babel-core/src/config/files/import.ts b/packages/babel-core/src/config/files/import.cjs similarity index 77% rename from packages/babel-core/src/config/files/import.ts rename to packages/babel-core/src/config/files/import.cjs index 460873bec19f..56793ef5bf77 100644 --- a/packages/babel-core/src/config/files/import.ts +++ b/packages/babel-core/src/config/files/import.cjs @@ -2,6 +2,6 @@ // import() isn't supported, we can try/catch around the require() call // when loading this file. -export default function import_(filepath: string) { +module.exports = function import_(filepath: string) { return import(filepath); -} +}; diff --git a/packages/babel-core/src/config/files/module-types.ts b/packages/babel-core/src/config/files/module-types.ts index 7ece0a7a8906..389054d381bf 100644 --- a/packages/babel-core/src/config/files/module-types.ts +++ b/packages/babel-core/src/config/files/module-types.ts @@ -10,7 +10,7 @@ const require = createRequire(import.meta.url); let import_: ((specifier: string | URL) => any) | undefined; try { // Old Node.js versions don't support import() syntax. - import_ = require("./import").default; + import_ = require("./import.cjs"); } catch {} export const supportsESM = semver.satisfies( diff --git a/packages/babel-helper-remap-async-to-generator/package.json b/packages/babel-helper-remap-async-to-generator/package.json index 4cb1715879c3..170b8dfc8367 100644 --- a/packages/babel-helper-remap-async-to-generator/package.json +++ b/packages/babel-helper-remap-async-to-generator/package.json @@ -20,6 +20,7 @@ "@babel/types": "workspace:^" }, "devDependencies": { + "@babel/core": "workspace:^", "@babel/traverse": "workspace:^" }, "peerDependencies": { diff --git a/packages/babel-helpers/scripts/generate-regenerator-runtime.js b/packages/babel-helpers/scripts/generate-regenerator-runtime.js index 91694b2cd9f2..b6bacf65e40a 100644 --- a/packages/babel-helpers/scripts/generate-regenerator-runtime.js +++ b/packages/babel-helpers/scripts/generate-regenerator-runtime.js @@ -5,7 +5,7 @@ import { createRequire } from "module"; const [parse, generate] = await Promise.all([ import("@babel/parser").then(ns => ns.parse), - import("@babel/generator").then(ns => ns.default.default), + import("@babel/generator").then(ns => ns.default.default || ns.default), ]).catch(error => Promise.reject( new Error( diff --git a/packages/babel-parser/index.cjs b/packages/babel-parser/index.cjs new file mode 100644 index 000000000000..89863a9f3658 --- /dev/null +++ b/packages/babel-parser/index.cjs @@ -0,0 +1,5 @@ +try { + module.exports = require("./lib/index.cjs"); +} catch { + module.exports = require("./lib/index.js"); +} diff --git a/packages/babel-parser/package.json b/packages/babel-parser/package.json index e364f944ca73..7cb6c186bce9 100644 --- a/packages/babel-parser/package.json +++ b/packages/babel-parser/package.json @@ -27,7 +27,8 @@ "files": [ "bin", "lib", - "typings" + "typings", + "index.cjs" ], "engines": { "node": ">=6.0.0" @@ -49,13 +50,23 @@ ], "USE_ESM": [ { - "type": "module" + "type": "module", + "exports": { + ".": { + "require": "./lib/index.cjs", + "default": "./lib/index.js" + }, + "./package.json": "./package.json" + } }, null ] }, "exports": { - ".": "./lib/index.js", + ".": { + "require": "./index.cjs", + "default": "./lib/index.js" + }, "./package.json": "./package.json" }, "type": "commonjs" diff --git a/packages/babel-plugin-transform-block-scoping/test/fixtures/general/issue-10339/exec.js b/packages/babel-plugin-transform-block-scoping/test/fixtures/general/issue-10339/exec.js index 1815a28ade50..3468fe3d5f77 100644 --- a/packages/babel-plugin-transform-block-scoping/test/fixtures/general/issue-10339/exec.js +++ b/packages/babel-plugin-transform-block-scoping/test/fixtures/general/issue-10339/exec.js @@ -15,7 +15,7 @@ let functionPath; return transformAsync(code, { configFile: false, plugins: [ - __dirname + "/../../../../lib", + __dirname + "/../../../../lib/index.js", { post({ path }) { programPath = path; diff --git a/packages/babel-preset-env/test/index.spec.js b/packages/babel-preset-env/test/index.spec.js index 67b44ca1d71a..33f2a8916768 100644 --- a/packages/babel-preset-env/test/index.spec.js +++ b/packages/babel-preset-env/test/index.spec.js @@ -1,7 +1,7 @@ // eslint-disable-next-line import/extensions import compatData from "@babel/compat-data/plugins"; -import babelPresetEnv from "../lib/index.js"; +import * as babelPresetEnv from "../lib/index.js"; import _removeRegeneratorEntryPlugin from "../lib/polyfills/regenerator.js"; import _pluginLegacyBabelPolyfill from "../lib/polyfills/babel-polyfill.js"; @@ -14,12 +14,25 @@ const pluginLegacyBabelPolyfill = const transformations = _transformations.default || _transformations; const availablePlugins = _availablePlugins.default || _availablePlugins; +// We need to load the correct plugins version (ESM or CJS), +// because our tests rely on function identity. +let pluginCoreJS2, pluginCoreJS3, pluginRegenerator; +import _pluginCoreJS2_esm from "babel-plugin-polyfill-corejs2"; +import _pluginCoreJS3_esm from "babel-plugin-polyfill-corejs3"; +import _pluginRegenerator_esm from "babel-plugin-polyfill-regenerator"; import { createRequire } from "module"; -const require = createRequire(import.meta.url); +// eslint-disable-next-line @babel/development-internal/require-default-import-fallback +if (/* commonjs */ _transformations.default) { + const require = createRequire(import.meta.url); -const pluginCoreJS2 = require("babel-plugin-polyfill-corejs2").default; -const pluginCoreJS3 = require("babel-plugin-polyfill-corejs3").default; -const pluginRegenerator = require("babel-plugin-polyfill-regenerator").default; + pluginCoreJS2 = require("babel-plugin-polyfill-corejs2").default; + pluginCoreJS3 = require("babel-plugin-polyfill-corejs3").default; + pluginRegenerator = require("babel-plugin-polyfill-regenerator").default; +} else { + pluginCoreJS2 = _pluginCoreJS2_esm; + pluginCoreJS3 = _pluginCoreJS3_esm; + pluginRegenerator = _pluginRegenerator_esm; +} describe("babel-preset-env", () => { describe("transformIncludesAndExcludes", () => { diff --git a/scripts/set-module-type.js b/scripts/set-module-type.js new file mode 100755 index 000000000000..a8062dc4533a --- /dev/null +++ b/scripts/set-module-type.js @@ -0,0 +1,45 @@ +#!/usr/bin/env node + +import fs from "fs"; + +const root = rel => new URL(`../${rel}`, import.meta.url).pathname; + +// prettier-ignore +let moduleType; +if (process.argv.length >= 3) { + moduleType = process.argv[2]; +} else if (fs.existsSync(root(".module-type"))) { + moduleType = fs.readFileSync(root(".module-type"), "utf-8").trim(); +} else { + moduleType = "script"; +} + +if (moduleType === "clean") { + try { + fs.unlinkSync(root(".module-type")); + } catch {} +} else if (moduleType === "module" || moduleType === "script") { + fs.writeFileSync(root(".module-type"), moduleType); +} else { + throw new Error(`Unknown module type: ${moduleType}`); +} + +["eslint", "codemods", "packages"] + .flatMap(dir => fs.readdirSync(root(dir)).map(file => root(`${dir}/${file}`))) + .filter( + dir => + fs.statSync(dir).isDirectory() && fs.existsSync(`${dir}/package.json`) + ) + .forEach(dir => { + if (moduleType === "clean") { + try { + fs.unlinkSync(`${dir}/lib/package.json`); + } catch {} + } else { + fs.mkdirSync(`${dir}/lib`, { recursive: true }); + fs.writeFileSync( + `${dir}/lib/package.json`, + `{ "type": "${moduleType}" }` + ); + } + }); diff --git a/yarn.lock b/yarn.lock index 39f7c56e9623..02f6fd57fdda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -327,7 +327,7 @@ __metadata: "@babel/code-frame": "workspace:^" "@babel/generator": "workspace:^" "@babel/helper-compilation-targets": "workspace:^" - "@babel/helper-module-transforms": "condition:BABEL_8_BREAKING ? : workspace:^7.18.6" + "@babel/helper-module-transforms": "workspace:^" "@babel/helper-transform-fixture-test-runner": "workspace:^" "@babel/helpers": "workspace:^" "@babel/parser": "workspace:^" @@ -749,30 +749,6 @@ __metadata: languageName: unknown linkType: soft -"@babel/helper-module-transforms-BABEL_8_BREAKING-false@npm:@babel/helper-module-transforms@workspace:^7.18.6, @babel/helper-module-transforms@workspace:^, @babel/helper-module-transforms@workspace:packages/babel-helper-module-transforms": - version: 0.0.0-use.local - resolution: "@babel/helper-module-transforms@workspace:packages/babel-helper-module-transforms" - dependencies: - "@babel/helper-environment-visitor": "workspace:^" - "@babel/helper-module-imports": "workspace:^" - "@babel/helper-simple-access": "workspace:^" - "@babel/helper-split-export-declaration": "workspace:^" - "@babel/helper-validator-identifier": "workspace:^" - "@babel/template": "workspace:^" - "@babel/traverse": "workspace:^" - "@babel/types": "workspace:^" - languageName: unknown - linkType: soft - -"@babel/helper-module-transforms@condition:BABEL_8_BREAKING ? : workspace:^7.18.6": - version: 0.0.0-condition-893c47 - resolution: "@babel/helper-module-transforms@condition:BABEL_8_BREAKING?:workspace:^7.18.6#893c47" - dependencies: - "@babel/helper-module-transforms-BABEL_8_BREAKING-false": "npm:@babel/helper-module-transforms@workspace:^7.18.6" - checksum: 7d7ccf481c03d5ba93f2c2fd3d736c67ad544fe53dddd5175790b1b5e0de4130b70566514c073fb51ec56a2ace0fdaa9102ac3cdd0790cc5ac5321142407321c - languageName: node - linkType: hard - "@babel/helper-module-transforms@npm:^7.12.1, @babel/helper-module-transforms@npm:^7.18.0": version: 7.18.0 resolution: "@babel/helper-module-transforms@npm:7.18.0" @@ -789,6 +765,21 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-transforms@workspace:^, @babel/helper-module-transforms@workspace:packages/babel-helper-module-transforms": + version: 0.0.0-use.local + resolution: "@babel/helper-module-transforms@workspace:packages/babel-helper-module-transforms" + dependencies: + "@babel/helper-environment-visitor": "workspace:^" + "@babel/helper-module-imports": "workspace:^" + "@babel/helper-simple-access": "workspace:^" + "@babel/helper-split-export-declaration": "workspace:^" + "@babel/helper-validator-identifier": "workspace:^" + "@babel/template": "workspace:^" + "@babel/traverse": "workspace:^" + "@babel/types": "workspace:^" + languageName: unknown + linkType: soft + "@babel/helper-optimise-call-expression@npm:^7.16.7": version: 7.16.7 resolution: "@babel/helper-optimise-call-expression@npm:7.16.7" @@ -844,6 +835,7 @@ __metadata: version: 0.0.0-use.local resolution: "@babel/helper-remap-async-to-generator@workspace:packages/babel-helper-remap-async-to-generator" dependencies: + "@babel/core": "workspace:^" "@babel/helper-annotate-as-pure": "workspace:^" "@babel/helper-environment-visitor": "workspace:^" "@babel/helper-wrap-function": "workspace:^"