Support .mjs plugins/presets and async factories (#12266)

This commit is contained in:
Nicolò Ribaudo 2020-12-10 00:07:09 +01:00
parent bfd3f80bdb
commit 31ca15ef58
38 changed files with 512 additions and 163 deletions

View File

@ -55,7 +55,7 @@
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"escape-string-regexp": "condition:BABEL_8_BREAKING ? ^4.0.0 : ",
"gensync": "^1.0.0-beta.1",
"gensync": "^1.0.0-beta.2",
"json5": "^2.1.2",
"lodash": "^4.17.19",
"semver": "^5.4.1",

View File

@ -154,7 +154,7 @@ export function* buildRootChain(
programmaticLogger,
);
if (!programmaticChain) return null;
const programmaticReport = programmaticLogger.output();
const programmaticReport = yield* programmaticLogger.output();
let configFile;
if (typeof opts.configFile === "string") {
@ -186,7 +186,7 @@ export function* buildRootChain(
configFileLogger,
);
if (!result) return null;
configReport = configFileLogger.output();
configReport = yield* configFileLogger.output();
// Allow config files to toggle `.babelrc` resolution on and off and
// specify where the roots are.
@ -244,7 +244,7 @@ export function* buildRootChain(
if (!result) {
isIgnored = true;
} else {
babelRcReport = babelrcLogger.output();
babelRcReport = yield* babelrcLogger.output();
mergeChain(fileChain, result);
}
}
@ -599,7 +599,7 @@ function makeChainWalker<ArgT: { options: ValidatedOptions, dirname: string }>({
}
logger(config, index, envName);
mergeChainOpts(chain, config);
yield* mergeChainOpts(chain, config);
}
return chain;
};
@ -657,13 +657,13 @@ function mergeChain(target: ConfigChain, source: ConfigChain): ConfigChain {
return target;
}
function mergeChainOpts(
function* mergeChainOpts(
target: ConfigChain,
{ options, plugins, presets }: OptionsAndDescriptors,
): ConfigChain {
): Handler<ConfigChain> {
target.options.push(options);
target.plugins.push(...plugins());
target.presets.push(...presets());
target.plugins.push(...(yield* plugins()));
target.presets.push(...(yield* presets()));
return target;
}

View File

@ -1,5 +1,7 @@
// @flow
import gensync, { type Handler } from "gensync";
import { loadPlugin, loadPreset } from "./files";
import { getItemDescriptor } from "./item";
@ -7,6 +9,7 @@ import { getItemDescriptor } from "./item";
import {
makeWeakCacheSync,
makeStrongCacheSync,
makeStrongCache,
type CacheConfigurator,
} from "./caching";
@ -21,8 +24,8 @@ import type {
// the options object actually ends up being applicable.
export type OptionsAndDescriptors = {
options: ValidatedOptions,
plugins: () => Array<UnloadedDescriptor>,
presets: () => Array<UnloadedDescriptor>,
plugins: () => Handler<Array<UnloadedDescriptor>>,
presets: () => Handler<Array<UnloadedDescriptor>>,
};
// Represents a plugin or presets at a given location in a config object.
@ -63,6 +66,11 @@ export type ValidatedFile = {
options: ValidatedOptions,
};
// eslint-disable-next-line require-yield
function* handlerOf<T>(value: T): Handler<T> {
return value;
}
/**
* Create a set of descriptors from a given options object, preserving
* descriptor identity based on the identity of the plugin/preset arrays
@ -78,13 +86,13 @@ export function createCachedDescriptors(
options,
plugins: plugins
? () => createCachedPluginDescriptors(plugins, dirname)(alias)
: () => [],
: () => handlerOf([]),
presets: presets
? () =>
createCachedPresetDescriptors(presets, dirname)(alias)(
!!passPerPreset,
)
: () => [],
: () => handlerOf([]),
};
}
@ -105,9 +113,9 @@ export function createUncachedDescriptors(
return {
options,
plugins: () => {
*plugins() {
if (!plugins) {
plugins = createPluginDescriptors(
plugins = yield* createPluginDescriptors(
options.plugins || [],
dirname,
alias,
@ -115,9 +123,9 @@ export function createUncachedDescriptors(
}
return plugins;
},
presets: () => {
*presets() {
if (!presets) {
presets = createPresetDescriptors(
presets = yield* createPresetDescriptors(
options.presets || [],
dirname,
alias,
@ -134,14 +142,22 @@ const createCachedPresetDescriptors = makeWeakCacheSync(
(items: PluginList, cache: CacheConfigurator<string>) => {
const dirname = cache.using(dir => dir);
return makeStrongCacheSync((alias: string) =>
makeStrongCacheSync((passPerPreset: boolean) =>
createPresetDescriptors(items, dirname, alias, passPerPreset).map(
makeStrongCache(function* (
passPerPreset: boolean,
): Handler<Array<UnloadedDescriptor>> {
const descriptors = yield* createPresetDescriptors(
items,
dirname,
alias,
passPerPreset,
);
return descriptors.map(
// Items are cached using the overall preset array identity when
// possibly, but individual descriptors are also cached if a match
// can be found in the previously-used descriptor lists.
desc => loadCachedDescriptor(PRESET_DESCRIPTOR_CACHE, desc),
),
),
);
}),
);
},
);
@ -150,14 +166,17 @@ const PLUGIN_DESCRIPTOR_CACHE = new WeakMap();
const createCachedPluginDescriptors = makeWeakCacheSync(
(items: PluginList, cache: CacheConfigurator<string>) => {
const dirname = cache.using(dir => dir);
return makeStrongCacheSync((alias: string) =>
createPluginDescriptors(items, dirname, alias).map(
return makeStrongCache(function* (
alias: string,
): Handler<Array<UnloadedDescriptor>> {
const descriptors = yield* createPluginDescriptors(items, dirname, alias);
return descriptors.map(
// Items are cached using the overall plugin array identity when
// possibly, but individual descriptors are also cached if a match
// can be found in the previously-used descriptor lists.
desc => loadCachedDescriptor(PLUGIN_DESCRIPTOR_CACHE, desc),
),
);
);
});
},
);
@ -205,36 +224,44 @@ function loadCachedDescriptor(
return desc;
}
function createPresetDescriptors(
function* createPresetDescriptors(
items: PluginList,
dirname: string,
alias: string,
passPerPreset: boolean,
): Array<UnloadedDescriptor> {
return createDescriptors("preset", items, dirname, alias, passPerPreset);
): Handler<Array<UnloadedDescriptor>> {
return yield* createDescriptors(
"preset",
items,
dirname,
alias,
passPerPreset,
);
}
function createPluginDescriptors(
function* createPluginDescriptors(
items: PluginList,
dirname: string,
alias: string,
): Array<UnloadedDescriptor> {
return createDescriptors("plugin", items, dirname, alias);
): Handler<Array<UnloadedDescriptor>> {
return yield* createDescriptors("plugin", items, dirname, alias);
}
function createDescriptors(
function* createDescriptors(
type: "plugin" | "preset",
items: PluginList,
dirname: string,
alias: string,
ownPass?: boolean,
): Array<UnloadedDescriptor> {
const descriptors = items.map((item, index) =>
createDescriptor(item, dirname, {
type,
alias: `${alias}$${index}`,
ownPass: !!ownPass,
}),
): Handler<Array<UnloadedDescriptor>> {
const descriptors = yield* gensync.all(
items.map((item, index) =>
createDescriptor(item, dirname, {
type,
alias: `${alias}$${index}`,
ownPass: !!ownPass,
}),
),
);
assertNoDuplicates(descriptors);
@ -245,7 +272,7 @@ function createDescriptors(
/**
* Given a plugin/preset item, resolve it into a standard format.
*/
export function createDescriptor(
export function* createDescriptor(
pair: PluginItem,
dirname: string,
{
@ -257,7 +284,7 @@ export function createDescriptor(
alias: string,
ownPass?: boolean,
},
): UnloadedDescriptor {
): Handler<UnloadedDescriptor> {
const desc = getItemDescriptor(pair);
if (desc) {
return desc;
@ -285,7 +312,7 @@ export function createDescriptor(
const resolver = type === "plugin" ? loadPlugin : loadPreset;
const request = value;
({ filepath, value } = resolver(value, dirname));
({ filepath, value } = yield* resolver(value, dirname));
file = {
request,

View File

@ -80,7 +80,7 @@ export function resolvePreset(name: string, dirname: string): string | null {
export function loadPlugin(
name: string,
dirname: string,
): { filepath: string, value: mixed } {
): Handler<{ filepath: string, value: mixed }> {
throw new Error(
`Cannot load plugin ${name} relative to ${dirname} in a browser`,
);
@ -89,7 +89,7 @@ export function loadPlugin(
export function loadPreset(
name: string,
dirname: string,
): { filepath: string, value: mixed } {
): Handler<{ filepath: string, value: mixed }> {
throw new Error(
`Cannot load preset ${name} relative to ${dirname} in a browser`,
);

View File

@ -12,13 +12,15 @@ try {
export default function* loadCjsOrMjsDefault(
filepath: string,
asyncError: string,
// TODO(Babel 8): Remove this
fallbackToTranspiledModule: boolean = false,
): Handler<mixed> {
switch (guessJSModuleType(filepath)) {
case "cjs":
return loadCjsDefault(filepath);
return loadCjsDefault(filepath, fallbackToTranspiledModule);
case "unknown":
try {
return loadCjsDefault(filepath);
return loadCjsDefault(filepath, fallbackToTranspiledModule);
} catch (e) {
if (e.code !== "ERR_REQUIRE_ESM") throw e;
}
@ -42,10 +44,12 @@ function guessJSModuleType(filename: string): "cjs" | "mjs" | "unknown" {
}
}
function loadCjsDefault(filepath: string) {
function loadCjsDefault(filepath: string, fallbackToTranspiledModule: boolean) {
const module = (require(filepath): mixed);
// TODO (Babel 8): Remove "undefined" fallback
return module?.__esModule ? module.default || undefined : module;
return module?.__esModule
? // TODO (Babel 8): Remove "module" and "undefined" fallback
module.default || (fallbackToTranspiledModule ? module : undefined)
: module;
}
async function loadMjsDefault(filepath: string) {

View File

@ -6,6 +6,8 @@
import buildDebug from "debug";
import path from "path";
import { type Handler } from "gensync";
import loadCjsOrMjsDefault from "./module-types";
const debug = buildDebug("babel:config:loading:files:plugins");
@ -26,31 +28,31 @@ export function resolvePreset(name: string, dirname: string): string | null {
return resolveStandardizedName("preset", name, dirname);
}
export function loadPlugin(
export function* loadPlugin(
name: string,
dirname: string,
): { filepath: string, value: mixed } {
): Handler<{ filepath: string, value: mixed }> {
const filepath = resolvePlugin(name, dirname);
if (!filepath) {
throw new Error(`Plugin ${name} not found relative to ${dirname}`);
}
const value = requireModule("plugin", filepath);
const value = yield* requireModule("plugin", filepath);
debug("Loaded plugin %o from %o.", name, dirname);
return { filepath, value };
}
export function loadPreset(
export function* loadPreset(
name: string,
dirname: string,
): { filepath: string, value: mixed } {
): Handler<{ filepath: string, value: mixed }> {
const filepath = resolvePreset(name, dirname);
if (!filepath) {
throw new Error(`Preset ${name} not found relative to ${dirname}`);
}
const value = requireModule("preset", filepath);
const value = yield* requireModule("preset", filepath);
debug("Loaded preset %o from %o.", name, dirname);
@ -145,7 +147,7 @@ function resolveStandardizedName(
}
const LOADING_MODULES = new Set();
function requireModule(type: string, name: string): mixed {
function* requireModule(type: string, name: string): Handler<mixed> {
if (LOADING_MODULES.has(name)) {
throw new Error(
`Reentrant ${type} detected trying to load "${name}". This module is not ignored ` +
@ -156,7 +158,19 @@ function requireModule(type: string, name: string): mixed {
try {
LOADING_MODULES.add(name);
return require(name);
return (yield* loadCjsOrMjsDefault(
name,
`You appear to be using a native ECMAScript module ${type}, ` +
"which is only supported when running Babel asynchronously.",
// For backward compatiblity, we need to support malformed presets
// defined as separate named exports rather than a single default
// export.
// See packages/babel-core/test/fixtures/option-manager/presets/es2015_named.js
true,
): mixed);
} catch (err) {
err.message = `[BABEL]: ${err.message} (While processing: ${name})`;
throw err;
} finally {
LOADING_MODULES.delete(name);
}

View File

@ -1,7 +1,7 @@
// @flow
import gensync, { type Handler } from "gensync";
import { forwardAsync } from "../gensync-utils/async";
import { forwardAsync, maybeAsync, isThenable } from "../gensync-utils/async";
import { mergeOptions } from "./util";
import * as context from "../index";
@ -228,12 +228,17 @@ const loadDescriptor = makeWeakCache(function* (
let item = value;
if (typeof value === "function") {
const factory = maybeAsync(
value,
`You appear to be using an async plugin/preset, but Babel has been called synchronously`,
);
const api = {
...context,
...makeAPI(cache),
};
try {
item = value(api, options, dirname);
item = yield* factory(api, options, dirname);
} catch (e) {
if (alias) {
e.message += ` (While processing: ${JSON.stringify(alias)})`;
@ -246,14 +251,16 @@ const loadDescriptor = makeWeakCache(function* (
throw new Error("Plugin/Preset did not return an object.");
}
if (typeof item.then === "function") {
if (isThenable(item)) {
yield* []; // if we want to support async plugins
throw new Error(
`You appear to be using an async plugin, ` +
`You appear to be using a promise as a plugin, ` +
`which your current version of Babel does not support. ` +
`If you're using a published plugin, ` +
`you may need to upgrade your @babel/core version.`,
`you may need to upgrade your @babel/core version. ` +
`As an alternative, you can prefix the promise with "await". ` +
`(While processing: ${JSON.stringify(alias)})`,
);
}

View File

@ -2,6 +2,7 @@
/*:: declare var invariant; */
import type { Handler } from "gensync";
import type { PluginTarget, PluginOptions } from "./validation/options";
import path from "path";
@ -20,7 +21,7 @@ export function createItemFromDescriptor(desc: UnloadedDescriptor): ConfigItem {
* ideally, as recreating the config item will mean re-resolving the item
* and re-evaluating the plugin/preset function.
*/
export function createConfigItem(
export function* createConfigItem(
value:
| PluginTarget
| [PluginTarget, PluginOptions]
@ -32,8 +33,8 @@ export function createConfigItem(
dirname?: string,
type?: "preset" | "plugin",
} = {},
): ConfigItem {
const descriptor = createDescriptor(value, path.resolve(dirname), {
): Handler<ConfigItem> {
const descriptor = yield* createDescriptor(value, path.resolve(dirname), {
type,
alias: "programmatic item",
});

View File

@ -1,5 +1,7 @@
// @flow
import gensync, { type Handler } from "gensync";
import type {
OptionsAndDescriptors,
UnloadedDescriptor,
@ -49,17 +51,17 @@ const Formatter = {
return loc;
},
optionsAndDescriptors(opt: OptionsAndDescriptors) {
*optionsAndDescriptors(opt: OptionsAndDescriptors) {
const content = { ...opt.options };
// overrides and env will be printed as separated config items
delete content.overrides;
delete content.env;
// resolve to descriptors
const pluginDescriptors = [...opt.plugins()];
const pluginDescriptors = [...(yield* opt.plugins())];
if (pluginDescriptors.length) {
content.plugins = pluginDescriptors.map(d => descriptorToConfig(d));
}
const presetDescriptors = [...opt.presets()];
const presetDescriptors = [...(yield* opt.presets())];
if (presetDescriptors.length) {
content.presets = [...presetDescriptors].map(d => descriptorToConfig(d));
}
@ -114,7 +116,7 @@ export class ConfigPrinter {
});
};
}
static format(config: PrintableConfig): string {
static *format(config: PrintableConfig): Handler<string> {
let title = Formatter.title(
config.type,
config.callerName,
@ -122,12 +124,15 @@ export class ConfigPrinter {
);
const loc = Formatter.loc(config.index, config.envName);
if (loc) title += ` ${loc}`;
const content = Formatter.optionsAndDescriptors(config.content);
const content = yield* Formatter.optionsAndDescriptors(config.content);
return `${title}\n${content}`;
}
output(): string {
*output(): Handler<string> {
if (this._stack.length === 0) return "";
return this._stack.map(s => ConfigPrinter.format(s)).join("\n\n");
const configs = yield* gensync.all(
this._stack.map(s => ConfigPrinter.format(s)),
);
return configs.join("\n\n");
}
}

View File

@ -15,6 +15,7 @@ function assertNotIgnored(result) {
function parse(code, opts) {
return babel.parse(code, {
cwd: __dirname,
configFile: false,
...opts,
});
}
@ -22,6 +23,7 @@ function parse(code, opts) {
function transform(code, opts) {
return babel.transform(code, {
cwd: __dirname,
configFile: false,
...opts,
});
}
@ -31,6 +33,7 @@ function transformFile(filename, opts, cb) {
filename,
{
cwd: __dirname,
configFile: false,
...opts,
},
cb,
@ -39,6 +42,7 @@ function transformFile(filename, opts, cb) {
function transformFileSync(filename, opts) {
return babel.transformFileSync(filename, {
cwd: __dirname,
configFile: false,
...opts,
});
}
@ -46,6 +50,7 @@ function transformFileSync(filename, opts) {
function transformAsync(code, opts) {
return babel.transformAsync(code, {
cwd: __dirname,
configFile: false,
...opts,
});
}
@ -53,6 +58,7 @@ function transformAsync(code, opts) {
function transformFromAst(ast, code, opts) {
return babel.transformFromAst(ast, code, {
cwd: __dirname,
configFile: false,
...opts,
});
}

View File

@ -1,6 +1,12 @@
import path from "path";
import { join } from "path";
import * as babel from "..";
import {
spawnTransformAsync,
spawnTransformSync,
supportsESM,
} from "./helpers/esm";
const nodeGte8 = (...args) => {
// "minNodeVersion": "8.0.0" <-- For Ctrl+F when dropping node 6
const testFn = process.version.slice(0, 3) === "v6." ? it.skip : it;
@ -8,7 +14,7 @@ const nodeGte8 = (...args) => {
};
describe("asynchronicity", () => {
const base = path.join(__dirname, "fixtures", "async");
const base = join(__dirname, "fixtures", "async");
let cwd;
beforeEach(function () {
@ -111,25 +117,18 @@ describe("asynchronicity", () => {
nodeGte8("called synchronously", () => {
process.chdir("plugin");
expect(() =>
babel.transformSync(""),
).toThrowErrorMatchingInlineSnapshot(
`"[BABEL] unknown: You appear to be using an async plugin, which your current version of Babel` +
` does not support. If you're using a published plugin, you may need to upgrade your` +
` @babel/core version."`,
expect(() => babel.transformSync("")).toThrow(
`[BABEL] unknown: You appear to be using an async plugin/preset, but Babel` +
` has been called synchronously`,
);
});
nodeGte8("called asynchronously", async () => {
process.chdir("plugin");
await expect(
babel.transformAsync(""),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[BABEL] unknown: You appear to be using an async plugin, which your current version of Babel` +
` does not support. If you're using a published plugin, you may need to upgrade your` +
` @babel/core version."`,
);
await expect(babel.transformAsync("")).resolves.toMatchObject({
code: `"success"`,
});
});
});
@ -189,24 +188,108 @@ describe("asynchronicity", () => {
nodeGte8("called synchronously", () => {
process.chdir("plugin-inherits");
expect(() =>
babel.transformSync(""),
).toThrowErrorMatchingInlineSnapshot(
`"[BABEL] unknown: You appear to be using an async plugin, which your current version of Babel` +
` does not support. If you're using a published plugin, you may need to upgrade your` +
` @babel/core version."`,
expect(() => babel.transformSync("")).toThrow(
`[BABEL] unknown: You appear to be using an async plugin/preset, but Babel has been` +
` called synchronously`,
);
});
nodeGte8("called asynchronously", async () => {
process.chdir("plugin-inherits");
await expect(
babel.transformAsync(""),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[BABEL] unknown: You appear to be using an async plugin, which your current version of Babel` +
` does not support. If you're using a published plugin, you may need to upgrade your` +
` @babel/core version."`,
await expect(babel.transformAsync("")).resolves.toMatchObject({
code: `"success 2"\n"success"`,
});
});
});
(supportsESM ? describe : describe.skip)(".mjs files", () => {
it("called synchronously", async () => {
process.chdir("plugin-mjs-native");
await expect(spawnTransformSync()).rejects.toThrow(
`[BABEL]: You appear to be using a native ECMAScript module plugin, which is` +
` only supported when running Babel asynchronously.`,
);
});
it("called asynchronously", async () => {
process.chdir("plugin-mjs-native");
await expect(spawnTransformAsync()).resolves.toMatchObject({
code: `"success"`,
});
});
});
});
describe("preset", () => {
describe("factory function", () => {
nodeGte8("called synchronously", () => {
process.chdir("preset");
expect(() => babel.transformSync("")).toThrow(
`[BABEL] unknown: You appear to be using an async plugin/preset, ` +
`but Babel has been called synchronously`,
);
});
nodeGte8("called asynchronously", async () => {
process.chdir("preset");
await expect(babel.transformAsync("")).resolves.toMatchObject({
code: `"success"`,
});
});
});
describe("plugins", () => {
nodeGte8("called synchronously", () => {
process.chdir("preset-plugin-promise");
expect(() => babel.transformSync("")).toThrow(
`[BABEL] unknown: You appear to be using a promise as a plugin, which your` +
` current version of Babel does not support. If you're using a published` +
` plugin, you may need to upgrade your @babel/core version. As an` +
` alternative, you can prefix the promise with "await".`,
);
});
nodeGte8("called asynchronously", async () => {
process.chdir("preset-plugin-promise");
await expect(babel.transformAsync("")).rejects.toThrow(
`[BABEL] unknown: You appear to be using a promise as a plugin, which your` +
` current version of Babel does not support. If you're using a published` +
` plugin, you may need to upgrade your @babel/core version. As an` +
` alternative, you can prefix the promise with "await".`,
);
});
});
(supportsESM ? describe : describe.skip)(".mjs files", () => {
it("called synchronously", async () => {
process.chdir("preset-mjs-native");
await expect(spawnTransformSync()).rejects.toThrow(
`[BABEL]: You appear to be using a native ECMAScript module preset, which is` +
` only supported when running Babel asynchronously.`,
);
});
it("called asynchronously", async () => {
process.chdir("preset-mjs-native");
await expect(spawnTransformAsync()).resolves.toMatchObject({
code: `"success"`,
});
});
it("must use the 'default' export", async () => {
process.chdir("preset-mjs-named-exports-native");
await expect(spawnTransformAsync()).rejects.toThrow(
`Unexpected falsy value: undefined`,
);
});
});

View File

@ -1,32 +1,10 @@
import cp from "child_process";
import fs from "fs";
import os from "os";
import path from "path";
import util from "util";
import escapeRegExp from "lodash/escapeRegExp";
import * as babel from "../lib";
// "minNodeVersion": "10.0.0" <-- For Ctrl+F when dropping node 10
const supportsESM = parseInt(process.versions.node) >= 12;
const isMJS = file => path.extname(file) === ".mjs";
const skipUnsupportedESM = (esm, name) => {
if (esm && !supportsESM) {
console.warn(
`Skipping "${name}" because native ECMAScript modules are not supported.`,
);
return true;
}
// This can be removed when loadOptionsAsyncInSpawedProcess is removed.
if (esm && process.platform === "win32") {
console.warn(
`Skipping "${name}" because the ESM runner cannot be spawned on Windows.`,
);
return true;
}
return false;
};
import { isMJS, loadOptionsAsync, skipUnsupportedESM } from "./helpers/esm";
// TODO: In Babel 8, we can directly uses fs.promises which is supported by
// node 8+
@ -71,42 +49,6 @@ function loadOptions(opts) {
return babel.loadOptions({ cwd: __dirname, ...opts });
}
function loadOptionsAsync({ filename, cwd = __dirname }, mjs) {
if (mjs) {
// import() crashes with jest
return loadOptionsAsyncInSpawedProcess({ filename, cwd });
}
return babel.loadOptionsAsync({ filename, cwd });
}
// !!!! hack is coming !!!!
// Remove this function when https://github.com/nodejs/node/issues/35889 is resolved.
// Jest supports dynamic import(), but Node.js segfaults when using it in our tests.
async function loadOptionsAsyncInSpawedProcess({ filename, cwd }) {
const { stdout, stderr } = await util.promisify(cp.execFile)(
require.resolve("./fixtures/babel-load-options-async.mjs"),
// pass `cwd` as params as `process.cwd()` will normalize `cwd` on macOS
[filename, cwd],
{
cwd,
env: process.env,
},
);
const EXPERIMENTAL_WARNING = /\(node:\d+\) ExperimentalWarning: The ESM module loader is experimental\./;
if (stderr.replace(EXPERIMENTAL_WARNING, "").trim()) {
throw new Error(
"error is thrown in babel-load-options-async.mjs: stdout\n" +
stdout +
"\nstderr:\n" +
stderr,
);
}
return JSON.parse(stdout);
}
function pairs(items) {
const pairs = [];
for (let i = 0; i < items.length - 1; i++) {

View File

@ -0,0 +1,3 @@
module.exports = {
plugins: ["./plugin.mjs"],
};

View File

@ -0,0 +1,9 @@
export default function plugin({ types: t }) {
return {
visitor: {
Program(path) {
path.pushContainer("body", t.stringLiteral("success"));
},
},
};
}

View File

@ -0,0 +1,3 @@
module.exports = {
plugins: ["./plugin.mjs"],
};

View File

@ -0,0 +1,16 @@
// Until Jest supports native mjs, we must simulate it 🤷
module.exports = new Promise(resolve =>
resolve({
default: function plugin({ types: t }) {
return {
visitor: {
Program(path) {
path.pushContainer("body", t.stringLiteral("success"));
},
},
};
},
})
);
module.exports.__esModule = true;

View File

@ -0,0 +1,3 @@
module.exports = {
presets: ["./preset.mjs"],
};

View File

@ -0,0 +1,9 @@
export default function plugin({ types: t }) {
return {
visitor: {
Program(path) {
path.pushContainer("body", t.stringLiteral("success"));
},
},
};
}

View File

@ -0,0 +1 @@
export const plugins = ["./plugin.mjs"];

View File

@ -0,0 +1,3 @@
module.exports = {
presets: ["./preset.mjs"],
};

View File

@ -0,0 +1,16 @@
// Until Jest supports native mjs, we must simulate it 🤷
module.exports = new Promise(resolve =>
resolve({
default: function plugin({ types: t }) {
return {
visitor: {
Program(path) {
path.pushContainer("body", t.stringLiteral("success"));
},
},
};
},
})
);
module.exports.__esModule = true;

View File

@ -0,0 +1,8 @@
// Until Jest supports native mjs, we must simulate it 🤷
module.exports = new Promise(resolve =>
resolve({
plugins: ["./plugin.mjs"]
})
);
module.exports.__esModule = true;

View File

@ -0,0 +1,3 @@
module.exports = {
presets: ["./preset.mjs"],
};

View File

@ -0,0 +1,9 @@
export default function plugin({ types: t }) {
return {
visitor: {
Program(path) {
path.pushContainer("body", t.stringLiteral("success"));
},
},
};
}

View File

@ -0,0 +1,3 @@
export default () => ({
plugins: ["./plugin.mjs"]
});

View File

@ -0,0 +1,3 @@
module.exports = {
presets: ["./preset.mjs"],
};

View File

@ -0,0 +1,16 @@
// Until Jest supports native mjs, we must simulate it 🤷
module.exports = new Promise(resolve =>
resolve({
default: function plugin({ types: t }) {
return {
visitor: {
Program(path) {
path.pushContainer("body", t.stringLiteral("success"));
},
},
};
},
})
);
module.exports.__esModule = true;

View File

@ -0,0 +1,10 @@
// Until Jest supports native mjs, we must simulate it 🤷
module.exports = new Promise(resolve =>
resolve({
default: () => ({
plugins: ["./plugin.mjs"]
})
})
);
module.exports.__esModule = true;

View File

@ -0,0 +1,3 @@
module.exports = {
presets: ["./preset"],
};

View File

@ -0,0 +1,13 @@
const wait = t => new Promise(r => setTimeout(r, t));
module.exports = async function plugin({ types: t }) {
await wait(50);
return {
visitor: {
Program(path) {
path.pushContainer("body", t.stringLiteral("success"));
},
},
};
};

View File

@ -0,0 +1,10 @@
const wait = t => new Promise(r => setTimeout(r, t));
// "Dynamic import"
const import_ = path => Promise.resolve(require(path));
module.exports = function preset(api) {
return {
plugins: [import_("./plugin")],
};
};

View File

@ -0,0 +1,3 @@
module.exports = {
presets: ["./preset"],
};

View File

@ -0,0 +1,13 @@
const wait = t => new Promise(r => setTimeout(r, t));
module.exports = async function plugin({ types: t }) {
await wait(50);
return {
visitor: {
Program(path) {
path.pushContainer("body", t.stringLiteral("success"));
},
},
};
};

View File

@ -0,0 +1,9 @@
const wait = t => new Promise(r => setTimeout(r, t));
module.exports = async function preset(api) {
await wait(50);
return {
plugins: [require("./plugin")],
};
};

View File

@ -0,0 +1,11 @@
#!/usr/bin/env node
// Usage:
// babel-compile-async.js [filename]
import babel from "../../lib/index.js";
(async () => {
process.stdout.write(
JSON.stringify(await babel.transformAsync(""))
);
})();

View File

@ -0,0 +1,9 @@
#!/usr/bin/env node
// Usage:
// babel-compile-async.js [filename]
import babel from "../../lib/index.js";
process.stdout.write(
JSON.stringify(babel.transformSync(""))
);

View File

@ -0,0 +1,74 @@
import cp from "child_process";
import util from "util";
import path from "path";
import * as babel from "../../lib";
// "minNodeVersion": "10.0.0" <-- For Ctrl+F when dropping node 10
const nodeSupportsESM = parseInt(process.versions.node) >= 12;
const isWindows = process.platform === "win32";
export const supportsESM = nodeSupportsESM && !isWindows;
export const isMJS = file => path.extname(file) === ".mjs";
export const itESM = supportsESM ? it : it.skip;
export function skipUnsupportedESM(esm, name) {
if (esm && !nodeSupportsESM) {
console.warn(
`Skipping "${name}" because native ECMAScript modules are not supported.`,
);
return true;
}
// This can be removed when loadOptionsAsyncInSpawedProcess is removed.
if (esm && isWindows) {
console.warn(
`Skipping "${name}" because the ESM runner cannot be spawned on Windows.`,
);
return true;
}
return false;
}
export function loadOptionsAsync({ filename, cwd = __dirname }, mjs) {
if (mjs) {
// import() crashes with jest
return spawn("load-options-async", filename, cwd);
}
return babel.loadOptionsAsync({ filename, cwd });
}
export function spawnTransformAsync() {
// import() crashes with jest
return spawn("compile-async");
}
export function spawnTransformSync() {
// import() crashes with jest
return spawn("compile-sync");
}
// !!!! hack is coming !!!!
// Remove this function when https://github.com/nodejs/node/issues/35889 is resolved.
// Jest supports dynamic import(), but Node.js segfaults when using it in our tests.
async function spawn(runner, filename, cwd = process.cwd()) {
const { stdout, stderr } = await util.promisify(cp.execFile)(
require.resolve(`../fixtures/babel-${runner}.mjs`),
// pass `cwd` as params as `process.cwd()` will normalize `cwd` on macOS
[filename, cwd],
{ cwd, env: process.env },
);
const EXPERIMENTAL_WARNING = /\(node:\d+\) ExperimentalWarning: The ESM module loader is experimental\./;
if (stderr.replace(EXPERIMENTAL_WARNING, "").trim()) {
throw new Error(
`error is thrown in babel-${runner}.mjs: stdout\n` +
stdout +
"\nstderr:\n" +
stderr,
);
}
return JSON.parse(stdout);
}

View File

@ -141,7 +141,7 @@ __metadata:
convert-source-map: ^1.7.0
debug: ^4.1.0
escape-string-regexp: "condition:BABEL_8_BREAKING ? ^4.0.0 : "
gensync: ^1.0.0-beta.1
gensync: ^1.0.0-beta.2
json5: ^2.1.2
lodash: ^4.17.19
semver: ^5.4.1
@ -7375,10 +7375,10 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"gensync@npm:^1.0.0-beta.1":
version: 1.0.0-beta.1
resolution: "gensync@npm:1.0.0-beta.1"
checksum: 3d14f7c34fc903dd52c36d0879de2c4afde8315edccd630e97919c365819b32c06d98770ef87f7ba45686ee5d2bd5818354920187659b42828319f7cc3352fdb
"gensync@npm:^1.0.0-beta.1, gensync@npm:^1.0.0-beta.2":
version: 1.0.0-beta.2
resolution: "gensync@npm:1.0.0-beta.2"
checksum: d523437689c97b3aba9c5cdeca4677d5fff9a29d620db693fea40d852bad63563110f16979d0170248439dbcd2ecee0780fb2533d3f0519f019081aa10767c60
languageName: node
linkType: hard