diff --git a/packages/babel-cli/src/babel/dir.js b/packages/babel-cli/src/babel/dir.js index 7356574504b7..8be4d4291609 100644 --- a/packages/babel-cli/src/babel/dir.js +++ b/packages/babel-cli/src/babel/dir.js @@ -6,6 +6,7 @@ import { sync as makeDirSync } from "make-dir"; import slash from "slash"; import path from "path"; import fs from "fs"; +import { DEFAULT_EXTENSIONS } from "@babel/core"; import * as util from "./util"; import { type CmdOptions } from "./options"; @@ -34,7 +35,10 @@ export default async function ({ ): Promise<$Keys> { let relative = path.relative(base, src); - if (!util.isCompilableExtension(relative, cliOptions.extensions)) { + if ( + !util.BABEL_SUPPORTS_EXTENSIONS_OPTION && + !util.isCompilableExtension(relative, cliOptions.extensions) + ) { return FILE_TYPE.NON_COMPILABLE; } @@ -48,17 +52,28 @@ export default async function ({ const dest = getDest(relative, base); try { - const res = await util.compile( - src, + const config = await util.loadPartialConfig( defaults( { sourceFileName: slash(path.relative(dest + "/..", src)), + filename: src, + ...(util.BABEL_SUPPORTS_EXTENSIONS_OPTION && { + // TODO(Babel 8): At some point @babel/core will default to DEFAULT_EXTENSIONS + // instead of ["*"], and we can avoid setting it here. + extensions: cliOptions.extensions ?? DEFAULT_EXTENSIONS, + }), }, babelOptions, ), ); - if (!res) return FILE_TYPE.IGNORED; + if (!config) return FILE_TYPE.IGNORED; + + const res = await util.compile(src, config.options); + + // If loadPartialConfig didn't return null, it's because the file wasn't ignored. + // Thus, if compiling the file returns null, it's because the extension isn't supported. + if (!res) return FILE_TYPE.NON_COMPILABLE; // we've requested explicit sourcemaps to be written to disk if ( diff --git a/packages/babel-cli/src/babel/file.js b/packages/babel-cli/src/babel/file.js index 89e780acd908..1b75237701d6 100644 --- a/packages/babel-cli/src/babel/file.js +++ b/packages/babel-cli/src/babel/file.js @@ -226,6 +226,7 @@ export default async function ({ }) .on("all", function (type: string, filename: string): void { if ( + !util.BABEL_SUPPORTS_EXTENSIONS_OPTION && !util.isCompilableExtension(filename, cliOptions.extensions) && !filenames.includes(filename) ) { diff --git a/packages/babel-cli/src/babel/util.js b/packages/babel-cli/src/babel/util.js index e105a1f43850..bf6d7b1e6195 100644 --- a/packages/babel-cli/src/babel/util.js +++ b/packages/babel-cli/src/babel/util.js @@ -5,6 +5,11 @@ import * as babel from "@babel/core"; import path from "path"; import fs from "fs"; +// Technically we could use the "semver" package here, but (for exmaple) +// parseFloat("4.23.6") returns 4.23 so it's "good enough" +export const BABEL_SUPPORTS_EXTENSIONS_OPTION = + parseFloat(babel.version) >= 7.11; + export function chmod(src: string, dest: string): void { fs.chmodSync(dest, fs.statSync(src).mode); } @@ -32,6 +37,10 @@ export function readdirForCompilable( includeDotfiles: boolean, altExts?: Array, ): Array { + if (BABEL_SUPPORTS_EXTENSIONS_OPTION) { + return readdir(dirname, includeDotfiles); + } + return readdir(dirname, includeDotfiles, function (filename) { return isCompilableExtension(filename, altExts); }); @@ -44,6 +53,12 @@ export function isCompilableExtension( filename: string, altExts?: Array, ): boolean { + if (!BABEL_SUPPORTS_EXTENSIONS_OPTION) { + throw new Error( + "Internal @babel/cli error: isCompilableExtension is only supported with old @babel/core versions.", + ); + } + const exts = altExts || babel.DEFAULT_EXTENSIONS; const ext = path.extname(filename); return exts.includes(ext); @@ -76,6 +91,20 @@ export function transform( }); } +export function loadPartialConfig(opts: Object): Promise { + opts = { + ...opts, + caller: CALLER, + }; + + return new Promise((resolve, reject) => { + babel.loadPartialConfig(opts, (err, result) => { + if (err) reject(err); + else resolve(result); + }); + }); +} + export function compile( filename: string, opts: Object | Function, diff --git a/packages/babel-cli/test/fixtures/babel/--copy-files --no-copy-ignored with only/in-files/src/foo.txt b/packages/babel-cli/test/fixtures/babel/--copy-files --no-copy-ignored with only/in-files/src/foo.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/babel-cli/test/fixtures/babel/--copy-files --no-copy-ignored with only/options.json b/packages/babel-cli/test/fixtures/babel/--copy-files --no-copy-ignored with only/options.json index d8b67e9dca82..095c7b945517 100644 --- a/packages/babel-cli/test/fixtures/babel/--copy-files --no-copy-ignored with only/options.json +++ b/packages/babel-cli/test/fixtures/babel/--copy-files --no-copy-ignored with only/options.json @@ -5,7 +5,7 @@ "lib", "--copy-files", "--only", - "src/foo/*", + "src/foo/*,src/README.md", "--no-copy-ignored", "--verbose" ] diff --git a/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/in-files/src/.babelrc.json b/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/in-files/src/.babelrc.json new file mode 100644 index 000000000000..5b7a5b2bc65e --- /dev/null +++ b/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/in-files/src/.babelrc.json @@ -0,0 +1,3 @@ +{ + "extensions": [".tsx"] +} diff --git a/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/in-files/src/bar.ts b/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/in-files/src/bar.ts new file mode 100644 index 000000000000..6c650fc54be1 --- /dev/null +++ b/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/in-files/src/bar.ts @@ -0,0 +1 @@ +x; diff --git a/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/in-files/src/baz.js b/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/in-files/src/baz.js new file mode 100644 index 000000000000..6c650fc54be1 --- /dev/null +++ b/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/in-files/src/baz.js @@ -0,0 +1 @@ +x; diff --git a/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/in-files/src/foo.tsx b/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/in-files/src/foo.tsx new file mode 100644 index 000000000000..6c650fc54be1 --- /dev/null +++ b/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/in-files/src/foo.tsx @@ -0,0 +1 @@ +x; diff --git a/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/options.json b/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/options.json new file mode 100644 index 000000000000..0aeb1496b74e --- /dev/null +++ b/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/options.json @@ -0,0 +1,3 @@ +{ + "args": ["src", "--out-dir", "lib"] +} diff --git a/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/out-files/lib/baz.js b/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/out-files/lib/baz.js new file mode 100644 index 000000000000..0ef60f5643e7 --- /dev/null +++ b/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/out-files/lib/baz.js @@ -0,0 +1,3 @@ +"use strict"; + +x; diff --git a/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/out-files/lib/foo.js b/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/out-files/lib/foo.js new file mode 100644 index 000000000000..0ef60f5643e7 --- /dev/null +++ b/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/out-files/lib/foo.js @@ -0,0 +1,3 @@ +"use strict"; + +x; diff --git a/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/stdout.txt b/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/stdout.txt new file mode 100644 index 000000000000..409cabd35c3f --- /dev/null +++ b/packages/babel-cli/test/fixtures/babel/dir uses extensions from config file/stdout.txt @@ -0,0 +1 @@ +Successfully compiled 2 files with Babel (123ms). diff --git a/packages/babel-cli/test/fixtures/babel/env - SHOW_CONFIG_FOR-windows/stdout.txt b/packages/babel-cli/test/fixtures/babel/env - SHOW_CONFIG_FOR-windows/stdout.txt index 5d5860f58f82..18d44dcb6d9a 100644 --- a/packages/babel-cli/test/fixtures/babel/env - SHOW_CONFIG_FOR-windows/stdout.txt +++ b/packages/babel-cli/test/fixtures/babel/env - SHOW_CONFIG_FOR-windows/stdout.txt @@ -16,6 +16,14 @@ config /.babelrc programmatic options from @babel/cli { "sourceFileName": "../src/foo.js", + "filename": "src//foo.js", + "extensions": [ + ".js", + ".jsx", + ".es6", + ".es", + ".mjs" + ], "presets": [ "//packages//babel-preset-react" ], @@ -26,7 +34,6 @@ programmatic options from @babel/cli ], "caller": { "name": "@babel/cli" - }, - "filename": "src//foo.js" + } } Successfully compiled 1 file with Babel (123ms). diff --git a/packages/babel-core/src/config/full.js b/packages/babel-core/src/config/full.js index 75fbe293c7c9..cd8e3b4780aa 100644 --- a/packages/babel-core/src/config/full.js +++ b/packages/babel-core/src/config/full.js @@ -4,7 +4,7 @@ import gensync, { type Handler } from "gensync"; import { forwardAsync } from "../gensync-utils/async"; import { mergeOptions } from "./util"; -import * as context from "../index"; +import * as babelContext from "../index"; import Plugin from "./plugin"; import { getItemDescriptor } from "./item"; import { @@ -70,7 +70,7 @@ export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig( return null; } - const optionDefaults = {}; + const optionDefaults: ValidatedOptions = {}; const { plugins, presets } = options; @@ -161,6 +161,8 @@ export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig( const opts: Object = optionDefaults; mergeOptions(opts, options); + if (!isCompilableFile(context.filename, opts.extensions)) return null; + yield* enhanceError(context, function* loadPluginDescriptors() { pluginDescriptorsByPass[0].unshift(...initialPluginsDescriptors); @@ -229,7 +231,7 @@ const loadDescriptor = makeWeakCache(function* ( let item = value; if (typeof value === "function") { const api = { - ...context, + ...babelContext, ...makeAPI(cache), }; try { @@ -395,3 +397,8 @@ function chain(a, b) { } }; } + +function isCompilableFile(filename, extensions) { + if (filename == null) return true; + return extensions.some(ext => ext === "*" || filename.endsWith(ext)); +} diff --git a/packages/babel-core/src/config/partial.js b/packages/babel-core/src/config/partial.js index e69c9cd4c453..d4b1ef5aa868 100644 --- a/packages/babel-core/src/config/partial.js +++ b/packages/babel-core/src/config/partial.js @@ -115,11 +115,19 @@ export default function* loadPrivatePartialConfig( const configChain = yield* buildRootChain(args, context); if (!configChain) return null; - const options = {}; + const options: ValidatedOptions = {}; configChain.options.forEach(opts => { mergeOptions(options, opts); }); + // If the programmatic options or config files don't set the "extensions" + // option, default to ["*"] for backward compatibility reasons. + // Note: this default value is set _before_ loading the presets, so it's + // safe to add the "extensions" option to a preset in a minor version. + // TODO(Babel 8): The default should be babel.DEFAULT_EXTENSIONS + // TODO: Use ??= once flow supports it. + options.extensions = options.extensions ?? ["*"]; + // Tack the passes onto the object itself so that, if this object is // passed back to Babel a second time, it will be in the right structure // to not change behavior. diff --git a/packages/babel-core/src/config/util.js b/packages/babel-core/src/config/util.js index b9d3af143dd3..467a71d5ca5d 100644 --- a/packages/babel-core/src/config/util.js +++ b/packages/babel-core/src/config/util.js @@ -2,6 +2,10 @@ import type { ValidatedOptions } from "./validation/options"; +function unique(...args: T[]): T[] { + return Array.from(new Set(args)); +} + export function mergeOptions( target: ValidatedOptions, source: ValidatedOptions, @@ -15,6 +19,11 @@ export function mergeOptions( const generatorOpts = source.generatorOpts; const targetObj = (target.generatorOpts = target.generatorOpts || {}); mergeDefaultFields(targetObj, generatorOpts); + } else if (k === "extensions" && target.extensions && source.extensions) { + target.extensions = unique( + ...(target.extensions ?? []), + ...source.extensions, + ); } else { const val = source[k]; if (val !== undefined) target[k] = (val: any); diff --git a/packages/babel-core/src/config/validation/option-assertions.js b/packages/babel-core/src/config/validation/option-assertions.js index f3962f18945f..eb7391f6ddc4 100644 --- a/packages/babel-core/src/config/validation/option-assertions.js +++ b/packages/babel-core/src/config/validation/option-assertions.js @@ -3,7 +3,7 @@ import type { ConfigFileSearch, BabelrcSearch, - IgnoreList, + FileExtension, IgnoreItem, PluginList, PluginItem, @@ -217,17 +217,20 @@ export function assertArray( return value; } -export function assertIgnoreList( - loc: OptionPath, - value: mixed, -): IgnoreList | void { - const arr = assertArray(loc, value); - if (arr) { - arr.forEach((item, i) => assertIgnoreItem(access(loc, i), item)); - } - return (arr: any); +function createArrayAssertion( + assertValid: (loc: GeneralPath, value: mixed) => T, +): (loc: GeneralPath, value: mixed) => $ReadOnlyArray | void { + return (loc, value) => { + const arr = assertArray(loc, value); + if (arr) arr.forEach((item, i) => assertValid(access(loc, i), item)); + return (arr: any); + }; } -function assertIgnoreItem(loc: GeneralPath, value: mixed): IgnoreItem { + +export const assertIgnoreList = createArrayAssertion(function assertIgnoreItem( + loc: GeneralPath, + value: mixed, +): IgnoreItem { if ( typeof value !== "string" && typeof value !== "function" && @@ -240,7 +243,26 @@ function assertIgnoreItem(loc: GeneralPath, value: mixed): IgnoreItem { ); } return value; -} +}); + +export const assertExtensionsList = createArrayAssertion( + function assertExtension( + loc: GeneralPath, + value: mixed, + ): FileExtension | "*" { + if (value === "*") return value; + + if (typeof value !== "string") { + throw new Error( + `${msg(loc)} must be an array of string values, or undefined`, + ); + } + if (!value.startsWith(".")) { + throw new Error(`${msg(loc)} must start with a '.' (dot)`); + } + return (value: any); + }, +); export function assertConfigApplicableTest( loc: OptionPath, diff --git a/packages/babel-core/src/config/validation/options.js b/packages/babel-core/src/config/validation/options.js index 27caf3bcc1e2..8207dde17320 100644 --- a/packages/babel-core/src/config/validation/options.js +++ b/packages/babel-core/src/config/validation/options.js @@ -14,6 +14,7 @@ import { assertCallerMetadata, assertInputSourceMap, assertIgnoreList, + assertExtensionsList, assertPluginList, assertConfigApplicableTest, assertConfigFileSearch, @@ -114,6 +115,10 @@ const COMMON_VALIDATORS: ValidatorSet = { $PropertyType, >), + extensions: (assertExtensionsList: Validator< + $PropertyType, + >), + retainLines: (assertBoolean: Validator< $PropertyType, >), @@ -195,9 +200,12 @@ export type ValidatedOptions = { extends?: string, env?: EnvSet, + overrides?: OverridesList, + + // Options to enable/disable processing of some files ignore?: IgnoreList, only?: IgnoreList, - overrides?: OverridesList, + extensions?: ExtensionsList, // Generally verify if a given config object should be applied to the given file. test?: ConfigApplicableTest, @@ -252,6 +260,9 @@ export type EnvSet = { export type IgnoreItem = string | Function | RegExp; export type IgnoreList = $ReadOnlyArray; +export opaque type FileExtension = string; +export type ExtensionsList = $ReadOnlyArray; + export type PluginOptions = {} | void | false; export type PluginTarget = string | {} | Function; export type PluginItem = diff --git a/packages/babel-core/src/index.js b/packages/babel-core/src/index.js index 23d023b5cabd..b721969aeb73 100644 --- a/packages/babel-core/src/index.js +++ b/packages/babel-core/src/index.js @@ -1,5 +1,7 @@ // @flow +import type { ExtensionsList } from "./config/validation/options"; + export { default as File } from "./transformation/file/file"; export { default as buildExternalHelpers } from "./tools/build-external-helpers"; export { resolvePlugin, resolvePreset } from "./config/files"; @@ -41,13 +43,9 @@ export { parse, parseSync, parseAsync } from "./parse"; * Recommended set of compilable extensions. Not used in @babel/core directly, but meant as * as an easy source for tooling making use of @babel/core. */ -export const DEFAULT_EXTENSIONS = Object.freeze([ - ".js", - ".jsx", - ".es6", - ".es", - ".mjs", -]); +export const DEFAULT_EXTENSIONS: ExtensionsList = Object.freeze( + ([".js", ".jsx", ".es6", ".es", ".mjs"]: any), +); // For easier backward-compatibility, provide an API like the one we exposed in Babel 6. import { loadOptions } from "./config"; diff --git a/packages/babel-core/test/api.js b/packages/babel-core/test/api.js index bf714bc6e9f5..07bf76e1e5e0 100644 --- a/packages/babel-core/test/api.js +++ b/packages/babel-core/test/api.js @@ -688,6 +688,115 @@ describe("api", function () { ]); }); + it("extensions option", function () { + return Promise.all([ + transformAsync("", { + filename: "bar.js", + }).then(assertNotIgnored), + + transformAsync("", { + filename: "bar.mjs", + }).then(assertNotIgnored), + + transformAsync("", { + filename: "bar.jsx", + }).then(assertNotIgnored), + + transformAsync("", { + filename: undefined, + }).then(assertNotIgnored), + + transformAsync("", { + filename: "bar.ts", + }).then(assertNotIgnored), + + transformAsync("", { + extensions: [".js", ".mjs"], + filename: "bar.ts", + }).then(assertIgnored), + + transformAsync("", { + extensions: [".ts"], + filename: "bar.ts", + }).then(assertNotIgnored), + + transformAsync("", { + extensions: [".ts"], + filename: "bar.js", + }).then(assertIgnored), + + transformAsync("", { + extensions: ["*"], + filename: "bar.ts", + }).then(assertNotIgnored), + + transformAsync("", { + extensions: ["*"], + filename: "bar.js", + }).then(assertNotIgnored), + + transformAsync("", { + extensions: ["*"], + filename: undefined, + }).then(assertNotIgnored), + + transformAsync("", { + extensions: ["*"], + filename: "bar", + }).then(assertNotIgnored), + + // Presets can add supported extensions but they don't prevent the + // default value of ["*"] from being set. + + transformAsync("", { + extensions: [".js"], + filename: "bar.js", + presets: [() => ({ extensions: [".ts"], plugins: [] })], + }).then(assertNotIgnored), + + transformAsync("", { + extensions: [".js"], + filename: "bar.ts", + presets: [() => ({ extensions: [".ts"], plugins: [] })], + }).then(assertNotIgnored), + + transformAsync("", { + extensions: [".js"], + filename: "bar.tsx", + presets: [() => ({ extensions: [".ts"], plugins: [] })], + }).then(assertIgnored), + + transformAsync("", { + filename: "bar.tsx", + presets: [() => ({ extensions: [".ts"], plugins: [] })], + }).then(assertNotIgnored), + + // Test with 'extensions' option in config file + + transformAsync("", { + configFile: `${__dirname}/fixtures/api/config-with-ts-extension/babel.config.json`, + filename: "foo.ts", + }).then(assertNotIgnored), + + transformAsync("", { + configFile: `${__dirname}/fixtures/api/config-with-ts-extension/babel.config.json`, + filename: "foo.tsx", + }).then(assertIgnored), + + transformAsync("", { + extensions: [".tsx"], + configFile: `${__dirname}/fixtures/api/config-with-ts-extension/babel.config.json`, + filename: "foo.tsx", + }).then(assertNotIgnored), + + transformAsync("", { + extensions: ["*"], + configFile: `${__dirname}/fixtures/api/config-with-ts-extension/babel.config.json`, + filename: "foo.tsx", + }).then(assertNotIgnored), + ]); + }); + describe("env option", function () { const oldBabelEnv = process.env.BABEL_ENV; const oldNodeEnv = process.env.NODE_ENV; diff --git a/packages/babel-core/test/config-chain.js b/packages/babel-core/test/config-chain.js index 911e813734fd..e412a4ac8766 100644 --- a/packages/babel-core/test/config-chain.js +++ b/packages/babel-core/test/config-chain.js @@ -1011,6 +1011,7 @@ describe("buildConfigChain", function () { plugins: [], presets: [], cloneInputAst: true, + extensions: ["*"], }); const realEnv = process.env.NODE_ENV; const realBabelEnv = process.env.BABEL_ENV; diff --git a/packages/babel-core/test/fixtures/api/config-with-ts-extension/babel.config.json b/packages/babel-core/test/fixtures/api/config-with-ts-extension/babel.config.json new file mode 100644 index 000000000000..d0e04ca75140 --- /dev/null +++ b/packages/babel-core/test/fixtures/api/config-with-ts-extension/babel.config.json @@ -0,0 +1,3 @@ +{ + "extensions": [".ts"] +} diff --git a/packages/babel-node/src/_babel-node.js b/packages/babel-node/src/_babel-node.js index e2b61c9af04b..41abbe159946 100644 --- a/packages/babel-node/src/_babel-node.js +++ b/packages/babel-node/src/_babel-node.js @@ -125,6 +125,7 @@ const _eval = function (code, filename) { if (!code) return undefined; code = babel.transform(code, { + extensions: ["*"], filename: filename, presets: program.presets, plugins: (program.plugins || []).concat([replPlugin]), diff --git a/packages/babel-preset-typescript/src/index.js b/packages/babel-preset-typescript/src/index.js index 74f885371395..b65c5709af33 100644 --- a/packages/babel-preset-typescript/src/index.js +++ b/packages/babel-preset-typescript/src/index.js @@ -15,6 +15,10 @@ export default declare( ) => { api.assertVersion(7); + // Technically we could use the "semver" package here, but (for exmaple) + // parseFloat("4.23.6") returns 4.23 so it's "good enough" + const BABEL_SUPPORTS_EXTENSIONS_OPTION = parseFloat(api.version) >= 7.11; + if (typeof allExtensions !== "boolean") { throw new Error(".allExtensions must be a boolean, or undefined"); } @@ -36,6 +40,9 @@ export default declare( }); return { + ...(BABEL_SUPPORTS_EXTENSIONS_OPTION && { + extensions: [".ts", ".tsx"], + }), overrides: allExtensions ? [ { diff --git a/packages/babel-register/package.json b/packages/babel-register/package.json index ae504f6c527b..d0aee0c2cfff 100644 --- a/packages/babel-register/package.json +++ b/packages/babel-register/package.json @@ -29,6 +29,7 @@ "devDependencies": { "@babel/core": "workspace:^7.11.5", "@babel/plugin-transform-modules-commonjs": "workspace:^7.10.4", + "@babel/preset-typescript": "workspace:^7.10.4", "browserify": "^16.5.2", "default-require-extensions": "^2.0.0" } diff --git a/packages/babel-register/src/node.js b/packages/babel-register/src/node.js index ed527cdac549..72fafee1d126 100644 --- a/packages/babel-register/src/node.js +++ b/packages/babel-register/src/node.js @@ -6,7 +6,73 @@ import * as babel from "@babel/core"; import { OptionManager, DEFAULT_EXTENSIONS } from "@babel/core"; import { addHook } from "pirates"; import fs from "fs"; -import path from "path"; +import path, { extname } from "path"; + +// Technically we could use the "semver" package here, but (for exmaple) +// parseFloat("4.23.6") returns 4.23 so it's "good enough" +const BABEL_SUPPORTS_EXTENSIONS_OPTION = parseFloat(babel.version) >= 7.11; + +// The "pirates" library, that we use to register require hooks, does +// not allow hooking into all the loaded files, but requries us to +// specify the extensions upfront. +// In order to hook into all of them, we need to do two things: +// 1) Add hooks for all the already registered extensions, defined in +// Module._extensions. By doing so, we can shadow the loaders already +// defined. +// 2) Node throws an error when requireing .mjs unless a hook has been +// defined. For compatibility reason (@babel/register can load .mjs files) +// we add the extension to the list. +// 3) Node fallbacks to the .js loader for unknown extensions, however +// pirates will only run our hook if it actually matches the extension +// (without a fallback mechanism). However, it checks if an extension +// has been registered by checking 'extensions.indexOf(...)'. +// And... we can make it always return true! ^-^ +// Since this is not technically part of the public API of "pirates", the +// version in package.json is fixed to avoid untested updates. +function generateExtensionsArray(exts) { + return Object.defineProperty(Array.from(exts), "indexOf", { + configurable: true, + writable: true, + enumerable: false, + value: () => true, + }); +} + +// Node.js's algorithm tries to automatically resolve the extension of the +// required file. This means that if you have, for example, require("./foo") +// it can load foo.js (even if ".js" is not specified). +// This doesn't only work with predefined extensions: whenever a new extension +// hook is registered, Node.js can resolve it. +// For this reason, we need to register new extensions as soon as we know about +// them. This means: +// 1) When we compile a file and see a new extension in its `extensions` option. +// 2) When a file with a new explicit extension is loaded +// +// In practice, this means that to load foo.ts you have to use +// node -r @babel/register ./foo.ts +// instead of just +// node -r @babel/register ./foo +const knownExtensions = new Set(); +function registerNewExtensions(extensions, filename) { + if (!BABEL_SUPPORTS_EXTENSIONS_OPTION) return; + + const prevSize = knownExtensions.size; + + if (filename) { + const ext = extname(filename); + if (ext) knownExtensions.add(ext); + } + + if (extensions) { + extensions.forEach(ext => { + if (ext && ext !== "*") knownExtensions.add(ext); + }); + } + + if (knownExtensions.size !== prevSize) { + hookExtensions(generateExtensionsArray(knownExtensions)); + } +} const maps = {}; let transformOpts = {}; @@ -47,9 +113,11 @@ function compile(code, filename) { }, ); - // Bail out ASAP if the file has been ignored. + // Bail out ASAP if the file has been ignored or has an unsupported extension if (opts === null) return code; + registerNewExtensions(opts.extensions, filename); + let cacheKey = `${JSON.stringify(opts)}:${babel.version}`; const env = babel.getEnv(false); @@ -107,10 +175,17 @@ register(); export default function register(opts?: Object = {}) { // Clone to avoid mutating the arguments object with the 'delete's below. - opts = { - ...opts, - }; - hookExtensions(opts.extensions || DEFAULT_EXTENSIONS); + opts = { ...opts }; + + if (BABEL_SUPPORTS_EXTENSIONS_OPTION) { + // TODO(Babel 8): At some point @babel/core will default to DEFAULT_EXTENSIONS + // instead of ["*"], and we can avoid setting it here. + opts.extensions ??= DEFAULT_EXTENSIONS; + + registerNewExtensions(opts.extensions); + } else { + hookExtensions(opts.extensions ?? DEFAULT_EXTENSIONS); + } if (opts.cache === false && cache) { registerCache.clear(); @@ -120,8 +195,10 @@ export default function register(opts?: Object = {}) { cache = registerCache.get(); } - delete opts.extensions; delete opts.cache; + if (!BABEL_SUPPORTS_EXTENSIONS_OPTION) { + delete opts.extensions; + } transformOpts = { ...opts, diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/babel.config.json b/packages/babel-register/test/fixtures/integration/default-extensions/babel.config.json new file mode 100644 index 000000000000..0aa5ca51a010 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/babel.config.json @@ -0,0 +1,3 @@ +{ + "plugins": ["../logger"] +} diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/foo.cjs b/packages/babel-register/test/fixtures/integration/default-extensions/foo.cjs new file mode 100644 index 000000000000..330530fe1f41 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/foo.cjs @@ -0,0 +1 @@ +console.log("DONE: foo.cjs"); diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/foo.es b/packages/babel-register/test/fixtures/integration/default-extensions/foo.es new file mode 100644 index 000000000000..f30fa10263e7 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/foo.es @@ -0,0 +1 @@ +console.log("DONE: foo.es"); diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/foo.es6 b/packages/babel-register/test/fixtures/integration/default-extensions/foo.es6 new file mode 100644 index 000000000000..c616f9f06ef7 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/foo.es6 @@ -0,0 +1 @@ +console.log("DONE: foo.es6"); diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/foo.jsx b/packages/babel-register/test/fixtures/integration/default-extensions/foo.jsx new file mode 100644 index 000000000000..d575d566d47a --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/foo.jsx @@ -0,0 +1 @@ +console.log("DONE: foo.jsx"); diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/foo.mjs b/packages/babel-register/test/fixtures/integration/default-extensions/foo.mjs new file mode 100644 index 000000000000..cc2f2aeae020 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/foo.mjs @@ -0,0 +1 @@ +console.log("DONE: foo.mjs"); diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/foo.ts b/packages/babel-register/test/fixtures/integration/default-extensions/foo.ts new file mode 100644 index 000000000000..32ba8015fd49 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/foo.ts @@ -0,0 +1 @@ +console.log("DONE: foo.ts"); diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/foo.tsx b/packages/babel-register/test/fixtures/integration/default-extensions/foo.tsx new file mode 100644 index 000000000000..d39873b6909a --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/foo.tsx @@ -0,0 +1 @@ +console.log("DONE: foo.tsx"); diff --git a/packages/babel-register/test/fixtures/integration/default-extensions/index.js b/packages/babel-register/test/fixtures/integration/default-extensions/index.js new file mode 100644 index 000000000000..154e3b733489 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/default-extensions/index.js @@ -0,0 +1,11 @@ +require("./foo.jsx"); +require("./foo.es6"); +require("./foo.es"); +require("./foo.mjs"); + +// Not enabled by default +require("./foo.ts"); +require("./foo.tsx"); +require("./foo.cjs"); + +console.log("DONE: index.js"); diff --git a/packages/babel-register/test/fixtures/integration/load-ts-defined-in-preset/babel.config.json b/packages/babel-register/test/fixtures/integration/load-ts-defined-in-preset/babel.config.json new file mode 100644 index 000000000000..93e37d980fef --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/load-ts-defined-in-preset/babel.config.json @@ -0,0 +1,4 @@ +{ + "plugins": ["../logger"], + "presets": ["@babel/typescript"] +} diff --git a/packages/babel-register/test/fixtures/integration/load-ts-defined-in-preset/foo.ts b/packages/babel-register/test/fixtures/integration/load-ts-defined-in-preset/foo.ts new file mode 100644 index 000000000000..32ba8015fd49 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/load-ts-defined-in-preset/foo.ts @@ -0,0 +1 @@ +console.log("DONE: foo.ts"); diff --git a/packages/babel-register/test/fixtures/integration/load-ts-defined-in-preset/index.js b/packages/babel-register/test/fixtures/integration/load-ts-defined-in-preset/index.js new file mode 100644 index 000000000000..83bf0994a783 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/load-ts-defined-in-preset/index.js @@ -0,0 +1,3 @@ +require("./foo.ts"); + +console.log("DONE: index.js"); diff --git a/packages/babel-register/test/fixtures/integration/load-ts/babel.config.json b/packages/babel-register/test/fixtures/integration/load-ts/babel.config.json new file mode 100644 index 000000000000..d1db72a50f40 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/load-ts/babel.config.json @@ -0,0 +1,4 @@ +{ + "extensions": [".js", ".ts"], + "plugins": ["../logger"] +} diff --git a/packages/babel-register/test/fixtures/integration/load-ts/foo.ts b/packages/babel-register/test/fixtures/integration/load-ts/foo.ts new file mode 100644 index 000000000000..32ba8015fd49 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/load-ts/foo.ts @@ -0,0 +1 @@ +console.log("DONE: foo.ts"); diff --git a/packages/babel-register/test/fixtures/integration/load-ts/index.js b/packages/babel-register/test/fixtures/integration/load-ts/index.js new file mode 100644 index 000000000000..83bf0994a783 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/load-ts/index.js @@ -0,0 +1,3 @@ +require("./foo.ts"); + +console.log("DONE: index.js"); diff --git a/packages/babel-register/test/fixtures/integration/logger.js b/packages/babel-register/test/fixtures/integration/logger.js new file mode 100644 index 000000000000..83d5b81d5b49 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/logger.js @@ -0,0 +1,10 @@ +module.exports = function () { + return { + pre() { + const filename = this.filename + .replace(__dirname, "") + .replace(/\\/g, "/"); + console.log(`LOADED: ${JSON.stringify(filename)}`); + }, + }; +}; diff --git a/packages/babel-register/test/fixtures/integration/no-load-ts/babel.config.json b/packages/babel-register/test/fixtures/integration/no-load-ts/babel.config.json new file mode 100644 index 000000000000..43728ca07711 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/no-load-ts/babel.config.json @@ -0,0 +1,4 @@ +{ + "extensions": [".js"], + "plugins": ["../logger"] +} diff --git a/packages/babel-register/test/fixtures/integration/no-load-ts/foo.ts b/packages/babel-register/test/fixtures/integration/no-load-ts/foo.ts new file mode 100644 index 000000000000..32ba8015fd49 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/no-load-ts/foo.ts @@ -0,0 +1 @@ +console.log("DONE: foo.ts"); diff --git a/packages/babel-register/test/fixtures/integration/no-load-ts/index.js b/packages/babel-register/test/fixtures/integration/no-load-ts/index.js new file mode 100644 index 000000000000..83bf0994a783 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/no-load-ts/index.js @@ -0,0 +1,3 @@ +require("./foo.ts"); + +console.log("DONE: index.js"); diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/babel.config.json b/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/babel.config.json new file mode 100644 index 000000000000..d1db72a50f40 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/babel.config.json @@ -0,0 +1,4 @@ +{ + "extensions": [".js", ".ts"], + "plugins": ["../logger"] +} diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/foo.ts b/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/foo.ts new file mode 100644 index 000000000000..32ba8015fd49 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/foo.ts @@ -0,0 +1 @@ +console.log("DONE: foo.ts"); diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/index.js b/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/index.js new file mode 100644 index 000000000000..ffd1086b7c98 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-config/index.js @@ -0,0 +1,3 @@ +require("./foo"); // <-- No extensino here! + +console.log("DONE: index.js"); diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/babel.config.json b/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/babel.config.json new file mode 100644 index 000000000000..2cfc4d53c604 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/babel.config.json @@ -0,0 +1,4 @@ +{ + "extensions": ["*"], + "plugins": ["../logger"] +} diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/foo.ts b/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/foo.ts new file mode 100644 index 000000000000..32ba8015fd49 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/foo.ts @@ -0,0 +1 @@ +console.log("DONE: foo.ts"); diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/index.ts b/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/index.ts new file mode 100644 index 000000000000..c7b2c26c353e --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-main-file/index.ts @@ -0,0 +1,3 @@ +require("./foo"); // <-- No extensino here! + +console.log("DONE: index.ts"); diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/babel.config.json b/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/babel.config.json new file mode 100644 index 000000000000..2cfc4d53c604 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/babel.config.json @@ -0,0 +1,4 @@ +{ + "extensions": ["*"], + "plugins": ["../logger"] +} diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/bar.ts b/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/bar.ts new file mode 100644 index 000000000000..1cb1cf93c6b6 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/bar.ts @@ -0,0 +1 @@ +console.log("DONE: bar.ts"); diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/foo.ts b/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/foo.ts new file mode 100644 index 000000000000..021b83e292c6 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/foo.ts @@ -0,0 +1,3 @@ +require("./bar"); // <-- no extension! + +console.log("DONE: foo.ts"); diff --git a/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/index.js b/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/index.js new file mode 100644 index 000000000000..83bf0994a783 --- /dev/null +++ b/packages/babel-register/test/fixtures/integration/resolve-extension-from-required-file/index.js @@ -0,0 +1,3 @@ +require("./foo.ts"); + +console.log("DONE: index.js"); diff --git a/packages/babel-register/test/index.js b/packages/babel-register/test/index.js index c83bb5e04c88..2a2bf2973be1 100644 --- a/packages/babel-register/test/index.js +++ b/packages/babel-register/test/index.js @@ -37,11 +37,6 @@ jest.mock("source-map-support", () => { }; }); -const defaultOptions = { - exts: [".js", ".jsx", ".es6", ".es", ".mjs"], - ignoreNodeModules: false, -}; - function cleanCache() { try { fs.unlinkSync(testCacheFilename); @@ -93,7 +88,8 @@ describe("@babel/register", function () { setupRegister(); expect(typeof currentHook).toBe("function"); - expect(currentOptions).toEqual(defaultOptions); + expect(currentOptions.exts).toContain(".js"); + expect(currentOptions.ignoreNodeModules).toBe(false); }); test("unregisters hook correctly", () => { diff --git a/packages/babel-register/test/integration.js b/packages/babel-register/test/integration.js new file mode 100644 index 000000000000..a165e675b7c4 --- /dev/null +++ b/packages/babel-register/test/integration.js @@ -0,0 +1,126 @@ +import { exec as execCb } from "child_process"; + +// TODO(Babel 8): Use util.promisify(execCb) +const exec = (...args) => + new Promise((resolve, reject) => { + execCb(...args, (error, stdout, stderr) => { + if (error) reject(error); + else resolve({ stdout, stderr }); + }); + }); + +function fixture(name, file = "index.js") { + const cwd = `${__dirname}/fixtures/integration/${name}`; + return exec(`node -r ${__dirname}/.. ${cwd}/${file}`, { + cwd, + env: { ...process.env, BABEL_DISABLE_CACHE: true }, + }); +} + +describe("integration tests", function () { + it("can hook into extensions defined by the config", async () => { + const { stdout, stderr } = await fixture("load-ts"); + + expect(stdout).toMatchInlineSnapshot(` + "LOADED: \\"/load-ts/index.js\\" + LOADED: \\"/load-ts/foo.ts\\" + DONE: foo.ts + DONE: index.js + " + `); + expect(stderr).toMatchInlineSnapshot(`""`); + }); + + it("does not hook into unknown extensions", async () => { + const { stdout, stderr } = await fixture("no-load-ts"); + + expect(stdout).toMatchInlineSnapshot(` + "LOADED: \\"/no-load-ts/index.js\\" + DONE: foo.ts + DONE: index.js + " + `); + expect(stderr).toMatchInlineSnapshot(`""`); + }); + + it("can hook into extensions defined by a preset", async () => { + const { stdout, stderr } = await fixture("load-ts-defined-in-preset"); + + expect(stdout).toMatchInlineSnapshot(` + "LOADED: \\"/load-ts-defined-in-preset/index.js\\" + LOADED: \\"/load-ts-defined-in-preset/foo.ts\\" + DONE: foo.ts + DONE: index.js + " + `); + expect(stderr).toMatchInlineSnapshot(`""`); + }); + + it("hooks into the default extensions", async () => { + const { stdout, stderr } = await fixture("default-extensions"); + + expect(stdout).toMatchInlineSnapshot(` + "LOADED: \\"/default-extensions/index.js\\" + LOADED: \\"/default-extensions/foo.jsx\\" + DONE: foo.jsx + LOADED: \\"/default-extensions/foo.es6\\" + DONE: foo.es6 + LOADED: \\"/default-extensions/foo.es\\" + DONE: foo.es + LOADED: \\"/default-extensions/foo.mjs\\" + DONE: foo.mjs + DONE: foo.ts + DONE: foo.tsx + DONE: foo.cjs + DONE: index.js + " + `); + expect(stderr).toMatchInlineSnapshot(`""`); + }); + + it("node resolves extensions that it gets from the Babel config", async () => { + const { stdout, stderr } = await fixture("resolve-extension-from-config"); + + expect(stdout).toMatchInlineSnapshot(` + "LOADED: \\"/resolve-extension-from-config/index.js\\" + LOADED: \\"/resolve-extension-from-config/foo.ts\\" + DONE: foo.ts + DONE: index.js + " + `); + expect(stderr).toMatchInlineSnapshot(`""`); + }); + + it("node resolves extensions that it gets from the main file", async () => { + const { stdout, stderr } = await fixture( + "resolve-extension-from-main-file", + "index.ts", + ); + + expect(stdout).toMatchInlineSnapshot(` + "LOADED: \\"/resolve-extension-from-main-file/index.ts\\" + LOADED: \\"/resolve-extension-from-main-file/foo.ts\\" + DONE: foo.ts + DONE: index.ts + " + `); + expect(stderr).toMatchInlineSnapshot(`""`); + }); + + it("node resolves extensions that it gets from a required file", async () => { + const { stdout, stderr } = await fixture( + "resolve-extension-from-required-file", + ); + + expect(stdout).toMatchInlineSnapshot(` + "LOADED: \\"/resolve-extension-from-required-file/index.js\\" + LOADED: \\"/resolve-extension-from-required-file/foo.ts\\" + LOADED: \\"/resolve-extension-from-required-file/bar.ts\\" + DONE: bar.ts + DONE: foo.ts + DONE: index.js + " + `); + expect(stderr).toMatchInlineSnapshot(`""`); + }); +}); diff --git a/yarn.lock b/yarn.lock index 06e0567b2421..cbd8429cfd04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3126,6 +3126,7 @@ __metadata: dependencies: "@babel/core": "workspace:^7.11.5" "@babel/plugin-transform-modules-commonjs": "workspace:^7.10.4" + "@babel/preset-typescript": "workspace:^7.10.4" browserify: ^16.5.2 default-require-extensions: ^2.0.0 find-cache-dir: ^2.0.0