From 494ae83c6ed515df9eca6bc2fa6b5ff5172b36b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Fri, 27 Sep 2019 17:20:09 +0200 Subject: [PATCH] Prepare @babel/core for asynchronicity --- lib/third-party-libs.js.flow | 1 + packages/babel-core/package.json | 1 + packages/babel-core/src/config/caching.js | 329 ++++++++++++++---- .../babel-core/src/config/config-chain.js | 144 ++++---- .../src/config/config-descriptors.js | 14 +- .../src/config/files/configuration.js | 192 +++++----- .../src/config/files/index-browser.js | 25 +- .../babel-core/src/config/files/package.js | 5 +- packages/babel-core/src/config/files/utils.js | 24 +- packages/babel-core/src/config/full.js | 213 ++++++------ packages/babel-core/src/config/index.js | 7 +- packages/babel-core/src/config/partial.js | 32 +- packages/babel-core/src/config/util.js | 11 + .../babel-core/src/gensync-utils/async.js | 110 ++++++ packages/babel-core/src/gensync-utils/fs.js | 14 + .../babel-core/src/gensync-utils/resolve.js | 9 + packages/babel-core/src/parse.js | 67 +--- packages/babel-core/src/transform-ast.js | 69 ++-- packages/babel-core/src/transform-file.js | 95 ++--- packages/babel-core/src/transform.js | 59 +--- .../src/transformation/block-hoist-plugin.js | 2 +- .../babel-core/src/transformation/index.js | 29 +- packages/babel-core/test/async.js | 214 ++++++++++++ packages/babel-core/test/caching-api.js | 98 ++++-- packages/babel-core/test/config-loading.js | 4 +- .../async/config-cache/babel.config.js | 12 + .../fixtures/async/config-cache/plugin.js | 9 + .../babel.config.js | 11 + .../config-file-async-function/plugin.js | 9 + .../async/config-file-promise/babel.config.js | 5 + .../async/config-file-promise/plugin.js | 9 + .../async/plugin-inherits/babel.config.js | 3 + .../fixtures/async/plugin-inherits/plugin.js | 10 + .../fixtures/async/plugin-inherits/plugin2.js | 13 + .../async/plugin-post/babel.config.js | 3 + .../test/fixtures/async/plugin-post/plugin.js | 15 + .../fixtures/async/plugin-pre/babel.config.js | 3 + .../test/fixtures/async/plugin-pre/plugin.js | 15 + .../fixtures/async/plugin/babel.config.js | 3 + .../test/fixtures/async/plugin/plugin.js | 13 + 40 files changed, 1279 insertions(+), 622 deletions(-) create mode 100644 packages/babel-core/src/gensync-utils/async.js create mode 100644 packages/babel-core/src/gensync-utils/fs.js create mode 100644 packages/babel-core/src/gensync-utils/resolve.js create mode 100644 packages/babel-core/test/async.js create mode 100644 packages/babel-core/test/fixtures/async/config-cache/babel.config.js create mode 100644 packages/babel-core/test/fixtures/async/config-cache/plugin.js create mode 100644 packages/babel-core/test/fixtures/async/config-file-async-function/babel.config.js create mode 100644 packages/babel-core/test/fixtures/async/config-file-async-function/plugin.js create mode 100644 packages/babel-core/test/fixtures/async/config-file-promise/babel.config.js create mode 100644 packages/babel-core/test/fixtures/async/config-file-promise/plugin.js create mode 100644 packages/babel-core/test/fixtures/async/plugin-inherits/babel.config.js create mode 100644 packages/babel-core/test/fixtures/async/plugin-inherits/plugin.js create mode 100644 packages/babel-core/test/fixtures/async/plugin-inherits/plugin2.js create mode 100644 packages/babel-core/test/fixtures/async/plugin-post/babel.config.js create mode 100644 packages/babel-core/test/fixtures/async/plugin-post/plugin.js create mode 100644 packages/babel-core/test/fixtures/async/plugin-pre/babel.config.js create mode 100644 packages/babel-core/test/fixtures/async/plugin-pre/plugin.js create mode 100644 packages/babel-core/test/fixtures/async/plugin/babel.config.js create mode 100644 packages/babel-core/test/fixtures/async/plugin/plugin.js diff --git a/lib/third-party-libs.js.flow b/lib/third-party-libs.js.flow index f2c81c237d72..8ab9ea075424 100644 --- a/lib/third-party-libs.js.flow +++ b/lib/third-party-libs.js.flow @@ -4,6 +4,7 @@ declare module "resolve" { declare export default { + (string, {| basedir: string |}, (err: ?Error, res: string) => void): void; sync: (string, {| basedir: string |}) => string; }; } diff --git a/packages/babel-core/package.json b/packages/babel-core/package.json index f4dd6723634d..f6f7ac761e5b 100644 --- a/packages/babel-core/package.json +++ b/packages/babel-core/package.json @@ -42,6 +42,7 @@ "@babel/types": "^7.6.3", "convert-source-map": "^1.1.0", "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", "json5": "^2.1.0", "lodash": "^4.17.13", "resolve": "^1.3.2", diff --git a/packages/babel-core/src/config/caching.js b/packages/babel-core/src/config/caching.js index 5b915e7f7767..fcad6e8fccbe 100644 --- a/packages/babel-core/src/config/caching.js +++ b/packages/babel-core/src/config/caching.js @@ -1,5 +1,17 @@ // @flow +import gensync, { type Handler } from "gensync"; +import { + maybeAsync, + isAsync, + onFirstPause, + waitFor, + isThenable, +} from "../gensync-utils/async"; +import { isIterableIterator } from "./util"; + +export type { CacheConfigurator }; + export type SimpleCacheConfigurator = SimpleCacheConfiguratorFn & SimpleCacheConfiguratorObj; @@ -14,91 +26,223 @@ type SimpleCacheConfiguratorObj = { invalidate: (handler: () => T) => T, }; -type CacheEntry = Array<{ +export type CacheEntry = Array<{ value: ResultT, - valid: SideChannel => boolean, + valid: SideChannel => Handler, }>; -export type { CacheConfigurator }; +const synchronize = ( + gen: (...ArgsT) => Handler, + // $FlowIssue https://github.com/facebook/flow/issues/7279 +): ((...args: ArgsT) => ResultT) => { + return gensync(gen).sync; +}; -/** - * Given a function with a single argument, cache its results based on its argument and how it - * configures its caching behavior. Cached values are stored strongly. - */ -export function makeStrongCache( +// eslint-disable-next-line require-yield, no-unused-vars +function* genTrue(data: any) { + return true; +} + +export function makeWeakCache( + handler: (ArgT, CacheConfigurator) => Handler | ResultT, +): (ArgT, SideChannel) => Handler { + return makeCachedFunction(WeakMap, handler); +} + +export function makeWeakCacheSync( handler: (ArgT, CacheConfigurator) => ResultT, ): (ArgT, SideChannel) => ResultT { - return makeCachedFunction(new Map(), handler); + return synchronize<[ArgT, SideChannel], ResultT>( + makeWeakCache(handler), + ); } -/** - * Given a function with a single argument, cache its results based on its argument and how it - * configures its caching behavior. Cached values are stored weakly and the function argument must be - * an object type. - */ -export function makeWeakCache< - ArgT: {} | Array<*> | $ReadOnlyArray<*>, - ResultT, - SideChannel, ->( +export function makeStrongCache( + handler: (ArgT, CacheConfigurator) => Handler | ResultT, +): (ArgT, SideChannel) => Handler { + return makeCachedFunction(Map, handler); +} + +export function makeStrongCacheSync( handler: (ArgT, CacheConfigurator) => ResultT, ): (ArgT, SideChannel) => ResultT { - return makeCachedFunction(new WeakMap(), handler); + return synchronize<[ArgT, SideChannel], ResultT>( + makeStrongCache(handler), + ); +} + +/* NOTE: Part of the logic explained in this comment is explained in the + * getCachedValueOrWait and setupAsyncLocks functions. + * + * > There are only two hard things in Computer Science: cache invalidation and naming things. + * > -- Phil Karlton + * + * I don't know if Phil was also thinking about handling a cache whose invalidation function is + * defined asynchronously is considered, but it is REALLY hard to do correctly. + * + * The implemented logic (only when gensync is run asynchronously) is the following: + * 1. If there is a valid cache associated to the current "arg" parameter, + * a. RETURN the cached value + * 3. If there is a FinishLock associated to the current "arg" parameter representing a valid cache, + * a. Wait for that lock to be released + * b. RETURN the value associated with that lock + * 5. Start executing the function to be cached + * a. If it pauses on a promise, then + * i. Let FinishLock be a new lock + * ii. Store FinishLock as associated to the current "arg" parameter + * iii. Wait for the function to finish executing + * iv. Release FinishLock + * v. Send the function result to anyone waiting on FinishLock + * 6. Store the result in the cache + * 7. RETURN the result + */ +function makeCachedFunction( + CallCache: Class, + handler: (ArgT, CacheConfigurator) => Handler | ResultT, +): (ArgT, SideChannel) => Handler { + const callCacheSync = new CallCache(); + const callCacheAsync = new CallCache(); + const futureCache = new CallCache(); + + return function* cachedFunction(arg: ArgT, data: SideChannel) { + const asyncContext = yield* isAsync(); + const callCache = asyncContext ? callCacheAsync : callCacheSync; + + const cached = yield* getCachedValueOrWait( + asyncContext, + callCache, + futureCache, + arg, + data, + ); + if (cached.valid) return cached.value; + + const cache = new CacheConfigurator(data); + + const handlerResult = handler(arg, cache); + + let finishLock: ?Lock; + let value: ResultT; + + if (isIterableIterator(handlerResult)) { + // Flow refines handlerResult to Generator + const gen = (handlerResult: Generator<*, ResultT, *>); + + value = yield* onFirstPause(gen, () => { + finishLock = setupAsyncLocks(cache, futureCache, arg); + }); + } else { + // $FlowIgnore doesn't refine handlerResult to ResultT + value = (handlerResult: ResultT); + } + + updateFunctionCache(callCache, cache, arg, value); + + if (finishLock) { + futureCache.delete(arg); + finishLock.release(value); + } + + return value; + }; } type CacheMap = | Map> | WeakMap>; -function makeCachedFunction< +function* getCachedValue< ArgT, ResultT, SideChannel, // $FlowIssue https://github.com/facebook/flow/issues/4528 Cache: CacheMap, >( - callCache: Cache, - handler: (ArgT, CacheConfigurator) => ResultT, -): (ArgT, SideChannel) => ResultT { - return function cachedFunction(arg, data) { - let cachedValue: CacheEntry | void = callCache.get( - arg, - ); - - if (cachedValue) { - for (const { value, valid } of cachedValue) { - if (valid(data)) return value; - } + cache: Cache, + arg: ArgT, + data: SideChannel, +): Handler<{ valid: true, value: ResultT } | { valid: false, value: null }> { + const cachedValue: CacheEntry | void = cache.get(arg); + + if (cachedValue) { + for (const { value, valid } of cachedValue) { + if (yield* valid(data)) return { valid: true, value }; } + } - const cache = new CacheConfigurator(data); + return { valid: false, value: null }; +} + +function* getCachedValueOrWait( + asyncContext: boolean, + callCache: CacheMap, + futureCache: CacheMap, SideChannel>, + arg: ArgT, + data: SideChannel, +): Handler<{ valid: true, value: ResultT } | { valid: false, value: null }> { + const cached = yield* getCachedValue(callCache, arg, data); + if (cached.valid) { + return cached; + } - const value = handler(arg, cache); - - if (!cache.configured()) cache.forever(); - - cache.deactivate(); - - switch (cache.mode()) { - case "forever": - cachedValue = [{ value, valid: () => true }]; - callCache.set(arg, cachedValue); - break; - case "invalidate": - cachedValue = [{ value, valid: cache.validator() }]; - callCache.set(arg, cachedValue); - break; - case "valid": - if (cachedValue) { - cachedValue.push({ value, valid: cache.validator() }); - } else { - cachedValue = [{ value, valid: cache.validator() }]; - callCache.set(arg, cachedValue); - } + if (asyncContext) { + const cached = yield* getCachedValue(futureCache, arg, data); + if (cached.valid) { + const value = yield* waitFor(cached.value.promise); + return { valid: true, value }; } + } - return value; - }; + return { valid: false, value: null }; +} + +function setupAsyncLocks( + config: CacheConfigurator, + futureCache: CacheMap, SideChannel>, + arg: ArgT, +): Lock { + const finishLock = new Lock(); + + updateFunctionCache(futureCache, config, arg, finishLock); + + return finishLock; +} + +function updateFunctionCache< + ArgT, + ResultT, + SideChannel, + // $FlowIssue https://github.com/facebook/flow/issues/4528 + Cache: CacheMap, +>( + cache: Cache, + config: CacheConfigurator, + arg: ArgT, + value: ResultT, +) { + if (!config.configured()) config.forever(); + + let cachedValue: CacheEntry | void = cache.get(arg); + + config.deactivate(); + + switch (config.mode()) { + case "forever": + cachedValue = [{ value, valid: genTrue }]; + cache.set(arg, cachedValue); + break; + case "invalidate": + cachedValue = [{ value, valid: config.validator() }]; + cache.set(arg, cachedValue); + break; + case "valid": + if (cachedValue) { + cachedValue.push({ value, valid: config.validator() }); + } else { + cachedValue = [{ value, valid: config.validator() }]; + cache.set(arg, cachedValue); + } + } } class CacheConfigurator { @@ -109,7 +253,7 @@ class CacheConfigurator { _configured: boolean = false; - _pairs: Array<[mixed, (SideChannel) => mixed]> = []; + _pairs: Array<[mixed, (SideChannel) => Handler]> = []; _data: SideChannel; @@ -162,30 +306,36 @@ class CacheConfigurator { this._configured = true; const key = handler(this._data); - this._pairs.push([key, handler]); + + const fn = maybeAsync( + handler, + `You appear to be using an async cache handler, but Babel has been called synchronously`, + ); + + if (isThenable(key)) { + return key.then(key => { + this._pairs.push([key, fn]); + return key; + }); + } + + this._pairs.push([key, fn]); return key; } invalidate(handler: SideChannel => T): T { - if (!this._active) { - throw new Error("Cannot change caching after evaluation has completed."); - } - if (this._never || this._forever) { - throw new Error( - "Caching has already been configured with .never or .forever()", - ); - } this._invalidate = true; - this._configured = true; - - const key = handler(this._data); - this._pairs.push([key, handler]); - return key; + return this.using(handler); } - validator(): SideChannel => boolean { + validator(): SideChannel => Handler { const pairs = this._pairs; - return (data: SideChannel) => pairs.every(([key, fn]) => key === fn(data)); + return function*(data: SideChannel) { + for (const [key, fn] of pairs) { + if (key !== (yield* fn(data))) return false; + } + return true; + }; } deactivate() { @@ -219,8 +369,18 @@ function makeSimpleConfigurator( // Types are limited here so that in the future these values can be used // as part of Babel's caching logic. -type SimpleType = string | boolean | number | null | void; +type SimpleType = string | boolean | number | null | void | Promise; export function assertSimpleType(value: mixed): SimpleType { + if (isThenable(value)) { + throw new Error( + `You appear to be using an async cache handler, ` + + `which your current version of Babel does not support. ` + + `We may add support for this in the future, ` + + `but if you're on the most recent version of @babel/core and still ` + + `seeing this error, then you'll need to synchronously handle your caching logic.`, + ); + } + if ( value != null && typeof value !== "string" && @@ -233,3 +393,20 @@ export function assertSimpleType(value: mixed): SimpleType { } return value; } + +class Lock { + released: boolean = false; + promise: Promise; + _resolve: (value: T) => void; + + constructor() { + this.promise = new Promise(resolve => { + this._resolve = resolve; + }); + } + + release(value: T) { + this.released = true; + this._resolve(value); + } +} diff --git a/packages/babel-core/src/config/config-chain.js b/packages/babel-core/src/config/config-chain.js index cb0fd1e1a4ac..1af3f35d2371 100644 --- a/packages/babel-core/src/config/config-chain.js +++ b/packages/babel-core/src/config/config-chain.js @@ -2,6 +2,7 @@ import path from "path"; import buildDebug from "debug"; +import type { Handler } from "gensync"; import { validate, type ValidatedOptions, @@ -24,7 +25,7 @@ import { type FilePackageData, } from "./files"; -import { makeWeakCache, makeStrongCache } from "./caching"; +import { makeWeakCacheSync, makeStrongCacheSync } from "./caching"; import { createCachedDescriptors, @@ -57,11 +58,11 @@ export type ConfigContext = { /** * Build a config chain for a given preset. */ -export function buildPresetChain( +export function* buildPresetChain( arg: PresetInstance, context: *, -): ConfigChain | null { - const chain = buildPresetChainWalker(arg, context); +): Handler { + const chain = yield* buildPresetChainWalker(arg, context); if (!chain) return null; return { @@ -82,11 +83,11 @@ export const buildPresetChainWalker: ( overridesEnv: (preset, index, envName) => loadPresetOverridesEnvDescriptors(preset)(index)(envName), }); -const loadPresetDescriptors = makeWeakCache((preset: PresetInstance) => +const loadPresetDescriptors = makeWeakCacheSync((preset: PresetInstance) => buildRootDescriptors(preset, preset.alias, createUncachedDescriptors), ); -const loadPresetEnvDescriptors = makeWeakCache((preset: PresetInstance) => - makeStrongCache((envName: string) => +const loadPresetEnvDescriptors = makeWeakCacheSync((preset: PresetInstance) => + makeStrongCacheSync((envName: string) => buildEnvDescriptors( preset, preset.alias, @@ -95,20 +96,21 @@ const loadPresetEnvDescriptors = makeWeakCache((preset: PresetInstance) => ), ), ); -const loadPresetOverridesDescriptors = makeWeakCache((preset: PresetInstance) => - makeStrongCache((index: number) => - buildOverrideDescriptors( - preset, - preset.alias, - createUncachedDescriptors, - index, +const loadPresetOverridesDescriptors = makeWeakCacheSync( + (preset: PresetInstance) => + makeStrongCacheSync((index: number) => + buildOverrideDescriptors( + preset, + preset.alias, + createUncachedDescriptors, + index, + ), ), - ), ); -const loadPresetOverridesEnvDescriptors = makeWeakCache( +const loadPresetOverridesEnvDescriptors = makeWeakCacheSync( (preset: PresetInstance) => - makeStrongCache((index: number) => - makeStrongCache((envName: string) => + makeStrongCacheSync((index: number) => + makeStrongCacheSync((envName: string) => buildOverrideEnvDescriptors( preset, preset.alias, @@ -129,11 +131,11 @@ export type RootConfigChain = ConfigChain & { /** * Build a config chain for Babel's full root configuration. */ -export function buildRootChain( +export function* buildRootChain( opts: ValidatedOptions, context: ConfigContext, -): RootConfigChain | null { - const programmaticChain = loadProgrammaticChain( +): Handler { + const programmaticChain = yield* loadProgrammaticChain( { options: opts, dirname: context.cwd, @@ -144,14 +146,18 @@ export function buildRootChain( let configFile; if (typeof opts.configFile === "string") { - configFile = loadConfig( + configFile = yield* loadConfig( opts.configFile, context.cwd, context.envName, context.caller, ); } else if (opts.configFile !== false) { - configFile = findRootConfig(context.root, context.envName, context.caller); + configFile = yield* findRootConfig( + context.root, + context.envName, + context.caller, + ); } let { babelrc, babelrcRoots } = opts; @@ -160,7 +166,7 @@ export function buildRootChain( const configFileChain = emptyChain(); if (configFile) { const validatedFile = validateConfigFile(configFile); - const result = loadFileChain(validatedFile, context); + const result = yield* loadFileChain(validatedFile, context); if (!result) return null; // Allow config files to toggle `.babelrc` resolution on and off and @@ -178,7 +184,7 @@ export function buildRootChain( const pkgData = typeof context.filename === "string" - ? findPackageData(context.filename) + ? yield* findPackageData(context.filename) : null; let ignoreFile, babelrcFile; @@ -189,7 +195,7 @@ export function buildRootChain( pkgData && babelrcLoadEnabled(context, pkgData, babelrcRoots, babelrcRootsDirectory) ) { - ({ ignore: ignoreFile, config: babelrcFile } = findRelativeConfig( + ({ ignore: ignoreFile, config: babelrcFile } = yield* findRelativeConfig( pkgData, context.envName, context.caller, @@ -203,7 +209,10 @@ export function buildRootChain( } if (babelrcFile) { - const result = loadFileChain(validateBabelrcFile(babelrcFile), context); + const result = yield* loadFileChain( + validateBabelrcFile(babelrcFile), + context, + ); if (!result) return null; mergeChain(fileChain, result); @@ -268,13 +277,15 @@ function babelrcLoadEnabled( }); } -const validateConfigFile = makeWeakCache((file: ConfigFile): ValidatedFile => ({ - filepath: file.filepath, - dirname: file.dirname, - options: validate("configfile", file.options), -})); +const validateConfigFile = makeWeakCacheSync( + (file: ConfigFile): ValidatedFile => ({ + filepath: file.filepath, + dirname: file.dirname, + options: validate("configfile", file.options), + }), +); -const validateBabelrcFile = makeWeakCache( +const validateBabelrcFile = makeWeakCacheSync( (file: ConfigFile): ValidatedFile => ({ filepath: file.filepath, dirname: file.dirname, @@ -282,11 +293,13 @@ const validateBabelrcFile = makeWeakCache( }), ); -const validateExtendFile = makeWeakCache((file: ConfigFile): ValidatedFile => ({ - filepath: file.filepath, - dirname: file.dirname, - options: validate("extendsfile", file.options), -})); +const validateExtendFile = makeWeakCacheSync( + (file: ConfigFile): ValidatedFile => ({ + filepath: file.filepath, + dirname: file.dirname, + options: validate("extendsfile", file.options), + }), +); /** * Build a config chain for just the programmatic options passed into Babel. @@ -317,11 +330,11 @@ const loadFileChain = makeChainWalker({ overridesEnv: (file, index, envName) => loadFileOverridesEnvDescriptors(file)(index)(envName), }); -const loadFileDescriptors = makeWeakCache((file: ValidatedFile) => +const loadFileDescriptors = makeWeakCacheSync((file: ValidatedFile) => buildRootDescriptors(file, file.filepath, createUncachedDescriptors), ); -const loadFileEnvDescriptors = makeWeakCache((file: ValidatedFile) => - makeStrongCache((envName: string) => +const loadFileEnvDescriptors = makeWeakCacheSync((file: ValidatedFile) => + makeStrongCacheSync((envName: string) => buildEnvDescriptors( file, file.filepath, @@ -330,8 +343,8 @@ const loadFileEnvDescriptors = makeWeakCache((file: ValidatedFile) => ), ), ); -const loadFileOverridesDescriptors = makeWeakCache((file: ValidatedFile) => - makeStrongCache((index: number) => +const loadFileOverridesDescriptors = makeWeakCacheSync((file: ValidatedFile) => + makeStrongCacheSync((index: number) => buildOverrideDescriptors( file, file.filepath, @@ -340,18 +353,19 @@ const loadFileOverridesDescriptors = makeWeakCache((file: ValidatedFile) => ), ), ); -const loadFileOverridesEnvDescriptors = makeWeakCache((file: ValidatedFile) => - makeStrongCache((index: number) => - makeStrongCache((envName: string) => - buildOverrideEnvDescriptors( - file, - file.filepath, - createUncachedDescriptors, - index, - envName, +const loadFileOverridesEnvDescriptors = makeWeakCacheSync( + (file: ValidatedFile) => + makeStrongCacheSync((index: number) => + makeStrongCacheSync((envName: string) => + buildOverrideEnvDescriptors( + file, + file.filepath, + createUncachedDescriptors, + index, + envName, + ), ), ), - ), ); function buildRootDescriptors({ dirname, options }, alias, descriptors) { @@ -410,8 +424,12 @@ function makeChainWalker({ env: (ArgT, string) => OptionsAndDescriptors | null, overrides: (ArgT, number) => OptionsAndDescriptors, overridesEnv: (ArgT, number, string) => OptionsAndDescriptors | null, -}): (ArgT, ConfigContext, Set | void) => ConfigChain | null { - return (input, context, files = new Set()) => { +}): ( + ArgT, + ConfigContext, + Set | void, +) => Handler { + return function*(input, context, files = new Set()) { const { dirname } = input; const flattenedConfigs = []; @@ -455,7 +473,9 @@ function makeChainWalker({ const chain = emptyChain(); for (const op of flattenedConfigs) { - if (!mergeExtendsChain(chain, op.options, dirname, context, files)) { + if ( + !(yield* mergeExtendsChain(chain, op.options, dirname, context, files)) + ) { return null; } @@ -465,16 +485,16 @@ function makeChainWalker({ }; } -function mergeExtendsChain( +function* mergeExtendsChain( chain: ConfigChain, opts: ValidatedOptions, dirname: string, context: ConfigContext, files: Set, -): boolean { +): Handler { if (opts.extends === undefined) return true; - const file = loadConfig( + const file = yield* loadConfig( opts.extends, dirname, context.envName, @@ -490,7 +510,11 @@ function mergeExtendsChain( } files.add(file); - const fileChain = loadFileChain(validateExtendFile(file), context, files); + const fileChain = yield* loadFileChain( + validateExtendFile(file), + context, + files, + ); files.delete(file); if (!fileChain) return false; diff --git a/packages/babel-core/src/config/config-descriptors.js b/packages/babel-core/src/config/config-descriptors.js index d9218a87c020..fa60d47d7139 100644 --- a/packages/babel-core/src/config/config-descriptors.js +++ b/packages/babel-core/src/config/config-descriptors.js @@ -5,8 +5,8 @@ import { loadPlugin, loadPreset } from "./files"; import { getItemDescriptor } from "./item"; import { - makeWeakCache, - makeStrongCache, + makeWeakCacheSync, + makeStrongCacheSync, type CacheConfigurator, } from "./caching"; @@ -130,11 +130,11 @@ export function createUncachedDescriptors( } const PRESET_DESCRIPTOR_CACHE = new WeakMap(); -const createCachedPresetDescriptors = makeWeakCache( +const createCachedPresetDescriptors = makeWeakCacheSync( (items: PluginList, cache: CacheConfigurator) => { const dirname = cache.using(dir => dir); - return makeStrongCache((alias: string) => - makeStrongCache((passPerPreset: boolean) => + return makeStrongCacheSync((alias: string) => + makeStrongCacheSync((passPerPreset: boolean) => createPresetDescriptors(items, dirname, alias, passPerPreset).map( // Items are cached using the overall preset array identity when // possibly, but individual descriptors are also cached if a match @@ -147,10 +147,10 @@ const createCachedPresetDescriptors = makeWeakCache( ); const PLUGIN_DESCRIPTOR_CACHE = new WeakMap(); -const createCachedPluginDescriptors = makeWeakCache( +const createCachedPluginDescriptors = makeWeakCacheSync( (items: PluginList, cache: CacheConfigurator) => { const dirname = cache.using(dir => dir); - return makeStrongCache((alias: string) => + return makeStrongCacheSync((alias: string) => createPluginDescriptors(items, dirname, alias).map( // Items are cached using the overall plugin array identity when // possibly, but individual descriptors are also cached if a match diff --git a/packages/babel-core/src/config/files/configuration.js b/packages/babel-core/src/config/files/configuration.js index 515171ceba94..40fca638b55b 100644 --- a/packages/babel-core/src/config/files/configuration.js +++ b/packages/babel-core/src/config/files/configuration.js @@ -2,12 +2,11 @@ import buildDebug from "debug"; import path from "path"; -import fs from "fs"; import json5 from "json5"; -import resolve from "resolve"; +import gensync, { type Handler } from "gensync"; import { makeStrongCache, - makeWeakCache, + makeWeakCacheSync, type CacheConfigurator, } from "../caching"; import makeAPI, { type PluginAPI } from "../helpers/config-api"; @@ -16,6 +15,9 @@ import pathPatternToRegex from "../pattern-to-regex"; import type { FilePackageData, RelativeConfig, ConfigFile } from "./types"; import type { CallerMetadata } from "../validation/options"; +import * as fs from "../../gensync-utils/fs"; +import resolve from "../../gensync-utils/resolve"; + const debug = buildDebug("babel:config:loading:files:configuration"); const ROOT_CONFIG_FILENAMES = [ @@ -27,13 +29,14 @@ const RELATIVE_CONFIG_FILENAMES = [".babelrc", ".babelrc.js", ".babelrc.cjs"]; const BABELIGNORE_FILENAME = ".babelignore"; -export function findConfigUpwards(rootDir: string): string | null { +export function* findConfigUpwards(rootDir: string): Handler { let dirname = rootDir; while (true) { - const configFileFound = ROOT_CONFIG_FILENAMES.some(filename => - fs.existsSync(path.join(dirname, filename)), - ); - if (configFileFound) return dirname; + for (const filename of ROOT_CONFIG_FILENAMES) { + if (yield* fs.exists(path.join(dirname, filename))) { + return dirname; + } + } const nextDir = path.dirname(dirname); if (dirname === nextDir) break; @@ -43,11 +46,11 @@ export function findConfigUpwards(rootDir: string): string | null { return null; } -export function findRelativeConfig( +export function* findRelativeConfig( packageData: FilePackageData, envName: string, caller: CallerMetadata | void, -): RelativeConfig { +): Handler { let config = null; let ignore = null; @@ -55,7 +58,7 @@ export function findRelativeConfig( for (const loc of packageData.directories) { if (!config) { - config = loadOneConfig( + config = yield* loadOneConfig( RELATIVE_CONFIG_FILENAMES, loc, envName, @@ -68,7 +71,7 @@ export function findRelativeConfig( if (!ignore) { const ignoreLoc = path.join(loc, BABELIGNORE_FILENAME); - ignore = readIgnoreConfig(ignoreLoc); + ignore = yield* readIgnoreConfig(ignoreLoc); if (ignore) { debug("Found ignore %o from %o.", ignore.filepath, dirname); @@ -83,26 +86,28 @@ export function findRootConfig( dirname: string, envName: string, caller: CallerMetadata | void, -): ConfigFile | null { +): Handler { return loadOneConfig(ROOT_CONFIG_FILENAMES, dirname, envName, caller); } -function loadOneConfig( +function* loadOneConfig( names: string[], dirname: string, envName: string, caller: CallerMetadata | void, previousConfig?: ConfigFile | null = null, -): ConfigFile | null { - const config = names.reduce((previousConfig: ConfigFile | null, name) => { - const filepath = path.resolve(dirname, name); - const config = readConfig(filepath, envName, caller); - +): Handler { + const configs = yield* gensync.all( + names.map(filename => + readConfig(path.join(dirname, filename), envName, caller), + ), + ); + const config = configs.reduce((previousConfig: ConfigFile | null, config) => { if (config && previousConfig) { throw new Error( `Multiple configuration files found. Please remove one:\n` + ` - ${path.basename(previousConfig.filepath)}\n` + - ` - ${name}\n` + + ` - ${config.filepath}\n` + `from ${dirname}`, ); } @@ -116,15 +121,15 @@ function loadOneConfig( return config; } -export function loadConfig( +export function* loadConfig( name: string, dirname: string, envName: string, caller: CallerMetadata | void, -): ConfigFile { - const filepath = resolve.sync(name, { basedir: dirname }); +): Handler { + const filepath = yield* resolve(name, { basedir: dirname }); - const conf = readConfig(filepath, envName, caller); + const conf = yield* readConfig(filepath, envName, caller); if (!conf) { throw new Error(`Config file ${filepath} contains no configuration data`); } @@ -137,7 +142,7 @@ export function loadConfig( * Read the given config file, returning the result. Returns null if no config was found, but will * throw if there are parsing errors while loading a config. */ -function readConfig(filepath, envName, caller): ConfigFile | null { +function readConfig(filepath, envName, caller) { const ext = path.extname(filepath); return ext === ".js" || ext === ".cjs" ? readConfigJS(filepath, { envName, caller }) @@ -146,81 +151,84 @@ function readConfig(filepath, envName, caller): ConfigFile | null { const LOADING_CONFIGS = new Set(); -const readConfigJS = makeStrongCache( - ( - filepath: string, - cache: CacheConfigurator<{ - envName: string, - caller: CallerMetadata | void, - }>, - ) => { - if (!fs.existsSync(filepath)) { - cache.forever(); - return null; - } +const readConfigJS = makeStrongCache(function* readConfigJS( + filepath: string, + cache: CacheConfigurator<{ + envName: string, + caller: CallerMetadata | void, + }>, +): Handler { + if (!fs.exists.sync(filepath)) { + cache.forever(); + return null; + } - // The `require()` call below can make this code reentrant if a require hook like @babel/register has been - // loaded into the system. That would cause Babel to attempt to compile the `.babelrc.js` file as it loads - // below. To cover this case, we auto-ignore re-entrant config processing. - if (LOADING_CONFIGS.has(filepath)) { - cache.never(); - - debug("Auto-ignoring usage of config %o.", filepath); - return { - filepath, - dirname: path.dirname(filepath), - options: {}, - }; - } + // The `require()` call below can make this code reentrant if a require hook like @babel/register has been + // loaded into the system. That would cause Babel to attempt to compile the `.babelrc.js` file as it loads + // below. To cover this case, we auto-ignore re-entrant config processing. + if (LOADING_CONFIGS.has(filepath)) { + cache.never(); - let options; - try { - LOADING_CONFIGS.add(filepath); - - // $FlowIssue - const configModule = (require(filepath): mixed); - options = - configModule && configModule.__esModule - ? configModule.default || undefined - : configModule; - } catch (err) { - err.message = `${filepath}: Error while loading config - ${err.message}`; - throw err; - } finally { - LOADING_CONFIGS.delete(filepath); - } + debug("Auto-ignoring usage of config %o.", filepath); + return { + filepath, + dirname: path.dirname(filepath), + options: {}, + }; + } - if (typeof options === "function") { - options = ((options: any): (api: PluginAPI) => {})(makeAPI(cache)); + let options; + try { + LOADING_CONFIGS.add(filepath); + + yield* []; // If we want to allow mjs configs imported using `import()` + // $FlowIssue + const configModule = (require(filepath): mixed); + options = + configModule && configModule.__esModule + ? configModule.default || undefined + : configModule; + } catch (err) { + err.message = `${filepath}: Error while loading config - ${err.message}`; + throw err; + } finally { + LOADING_CONFIGS.delete(filepath); + } - if (!cache.configured()) throwConfigError(); - } + let assertCache = false; + if (typeof options === "function") { + yield* []; // if we want to make it possible to use async configs + options = ((options: any): (api: PluginAPI) => {})(makeAPI(cache)); - if (!options || typeof options !== "object" || Array.isArray(options)) { - throw new Error( - `${filepath}: Configuration should be an exported JavaScript object.`, - ); - } + assertCache = true; + } - if (typeof options.then === "function") { - throw new Error( - `You appear to be using an async configuration, ` + - `which your current version of Babel does not support. ` + - `We may add support for this in the future, ` + - `but if you're on the most recent version of @babel/core and still ` + - `seeing this error, then you'll need to synchronously return your config.`, - ); - } + if (!options || typeof options !== "object" || Array.isArray(options)) { + throw new Error( + `${filepath}: Configuration should be an exported JavaScript object.`, + ); + } - return { - filepath, - dirname: path.dirname(filepath), - options, - }; - }, -); + if (typeof options.then === "function") { + throw new Error( + `You appear to be using an async configuration, ` + + `which your current version of Babel does not support. ` + + `We may add support for this in the future, ` + + `but if you're on the most recent version of @babel/core and still ` + + `seeing this error, then you'll need to synchronously return your config.`, + ); + } + + if (assertCache && !cache.configured()) throwConfigError(); + + return { + filepath, + dirname: path.dirname(filepath), + options, + }; +}); -const packageToBabelConfig = makeWeakCache( +const packageToBabelConfig = makeWeakCacheSync( (file: ConfigFile): ConfigFile | null => { const babel = file.options[("babel": string)]; diff --git a/packages/babel-core/src/config/files/index-browser.js b/packages/babel-core/src/config/files/index-browser.js index 1d2adccaa811..6124a4c1b65d 100644 --- a/packages/babel-core/src/config/files/index-browser.js +++ b/packages/babel-core/src/config/files/index-browser.js @@ -1,5 +1,7 @@ // @flow +import type { Handler } from "gensync"; + import type { ConfigFile, IgnoreFile, @@ -11,13 +13,15 @@ import type { CallerMetadata } from "../validation/options"; export type { ConfigFile, IgnoreFile, RelativeConfig, FilePackageData }; -export function findConfigUpwards( +// eslint-disable-next-line require-yield +export function* findConfigUpwards( rootDir: string, // eslint-disable-line no-unused-vars -): string | null { +): Handler { return null; } -export function findPackageData(filepath: string): FilePackageData { +// eslint-disable-next-line require-yield +export function* findPackageData(filepath: string): Handler { return { filepath, directories: [], @@ -26,28 +30,31 @@ export function findPackageData(filepath: string): FilePackageData { }; } -export function findRelativeConfig( +// eslint-disable-next-line require-yield +export function* findRelativeConfig( pkgData: FilePackageData, // eslint-disable-line no-unused-vars envName: string, // eslint-disable-line no-unused-vars caller: CallerMetadata | void, // eslint-disable-line no-unused-vars -): RelativeConfig { +): Handler { return { pkg: null, config: null, ignore: null }; } -export function findRootConfig( +// eslint-disable-next-line require-yield +export function* findRootConfig( dirname: string, // eslint-disable-line no-unused-vars envName: string, // eslint-disable-line no-unused-vars caller: CallerMetadata | void, // eslint-disable-line no-unused-vars -): ConfigFile | null { +): Handler { return null; } -export function loadConfig( +// eslint-disable-next-line require-yield +export function* loadConfig( name: string, dirname: string, envName: string, // eslint-disable-line no-unused-vars caller: CallerMetadata | void, // eslint-disable-line no-unused-vars -): ConfigFile { +): Handler { throw new Error(`Cannot load ${name} relative to ${dirname} in a browser`); } diff --git a/packages/babel-core/src/config/files/package.js b/packages/babel-core/src/config/files/package.js index 8e370a472d56..278ddfb852f5 100644 --- a/packages/babel-core/src/config/files/package.js +++ b/packages/babel-core/src/config/files/package.js @@ -1,6 +1,7 @@ // @flow import path from "path"; +import type { Handler } from "gensync"; import { makeStaticFileCache } from "./utils"; import type { ConfigFile, FilePackageData } from "./types"; @@ -12,7 +13,7 @@ const PACKAGE_FILENAME = "package.json"; * of Babel's config requires general package information to decide when to * search for .babelrc files */ -export function findPackageData(filepath: string): FilePackageData { +export function* findPackageData(filepath: string): Handler { let pkg = null; const directories = []; let isPackage = true; @@ -21,7 +22,7 @@ export function findPackageData(filepath: string): FilePackageData { while (!pkg && path.basename(dirname) !== "node_modules") { directories.push(dirname); - pkg = readConfigPackage(path.join(dirname, PACKAGE_FILENAME)); + pkg = yield* readConfigPackage(path.join(dirname, PACKAGE_FILENAME)); const nextLoc = path.dirname(dirname); if (dirname === nextLoc) { diff --git a/packages/babel-core/src/config/files/utils.js b/packages/babel-core/src/config/files/utils.js index db202259d5e8..61c3b24a7a4a 100644 --- a/packages/babel-core/src/config/files/utils.js +++ b/packages/babel-core/src/config/files/utils.js @@ -1,24 +1,32 @@ // @flow -import fs from "fs"; -import { makeStrongCache } from "../caching"; +import type { Gensync, Handler } from "gensync"; + +import { makeStrongCache, type CacheConfigurator } from "../caching"; +import * as fs from "../../gensync-utils/fs"; +import nodeFs from "fs"; export function makeStaticFileCache( fn: (string, string) => T, -): string => T | null { - return makeStrongCache((filepath, cache) => { - if (cache.invalidate(() => fileMtime(filepath)) === null) { +): Gensync<[string], T | null> { + return (makeStrongCache(function*( + filepath: string, + cache: CacheConfigurator, + ): Handler { + const cached = cache.invalidate(() => fileMtime(filepath)); + + if (cached === null) { cache.forever(); return null; } - return fn(filepath, fs.readFileSync(filepath, "utf8")); - }); + return fn(filepath, yield* fs.readFile(filepath, "utf8")); + }): Gensync); } function fileMtime(filepath: string): number | null { try { - return +fs.statSync(filepath).mtime; + return +nodeFs.statSync(filepath).mtime; } catch (e) { if (e.code !== "ENOENT" && e.code !== "ENOTDIR") throw e; } diff --git a/packages/babel-core/src/config/full.js b/packages/babel-core/src/config/full.js index d38aec4ba698..ab24a1936b87 100644 --- a/packages/babel-core/src/config/full.js +++ b/packages/babel-core/src/config/full.js @@ -1,5 +1,8 @@ // @flow +import gensync, { type Handler } from "gensync"; +import { forwardAsync } from "../gensync-utils/async"; + import { mergeOptions } from "./util"; import * as context from "../index"; import Plugin from "./plugin"; @@ -12,7 +15,11 @@ import { } from "./config-chain"; import type { UnloadedDescriptor } from "./config-descriptors"; import traverse from "@babel/traverse"; -import { makeWeakCache, type CacheConfigurator } from "./caching"; +import { + makeWeakCache, + makeWeakCacheSync, + type CacheConfigurator, +} from "./caching"; import { validate, type CallerMetadata } from "./validation/options"; import { validatePluginObject } from "./validation/plugins"; import makeAPI from "./helpers/config-api"; @@ -45,10 +52,10 @@ type SimpleContext = { caller: CallerMetadata | void, }; -export default function loadFullConfig( +export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig( inputOpts: mixed, -): ResolvedConfig | null { - const result = loadPrivatePartialConfig(inputOpts); +): Handler { + const result = yield* loadPrivatePartialConfig(inputOpts); if (!result) { return null; } @@ -63,28 +70,29 @@ export default function loadFullConfig( throw new Error("Assertion failure - plugins and presets exist"); } - const ignored = (function recurseDescriptors( + const ignored = yield* (function* recurseDescriptors( config: { plugins: Array, presets: Array, }, pass: Array, ) { - const plugins = config.plugins.reduce((acc, descriptor) => { + const plugins = []; + for (const descriptor of config.plugins) { if (descriptor.options !== false) { - acc.push(loadPluginDescriptor(descriptor, context)); + plugins.push(yield* loadPluginDescriptor(descriptor, context)); } - return acc; - }, []); - const presets = config.presets.reduce((acc, descriptor) => { + } + + const presets = []; + for (const descriptor of config.presets) { if (descriptor.options !== false) { - acc.push({ - preset: loadPresetDescriptor(descriptor, context), + presets.push({ + preset: yield* loadPresetDescriptor(descriptor, context), pass: descriptor.ownPass ? [] : pass, }); } - return acc; - }, []); + } // resolve presets if (presets.length > 0) { @@ -99,7 +107,7 @@ export default function loadFullConfig( for (const { preset, pass } of presets) { if (!preset) return true; - const ignored = recurseDescriptors( + const ignored = yield* recurseDescriptors( { plugins: preset.plugins, presets: preset.presets, @@ -165,61 +173,61 @@ export default function loadFullConfig( options: opts, passes: passes, }; -} +}); /** * Load a generic plugin/preset from the given descriptor loaded from the config object. */ -const loadDescriptor = makeWeakCache( - ( - { value, options, dirname, alias }: UnloadedDescriptor, - cache: CacheConfigurator, - ): LoadedDescriptor => { - // Disabled presets should already have been filtered out - if (options === false) throw new Error("Assertion failure"); - - options = options || {}; - - let item = value; - if (typeof value === "function") { - const api = { - ...context, - ...makeAPI(cache), - }; - try { - item = value(api, options, dirname); - } catch (e) { - if (alias) { - e.message += ` (While processing: ${JSON.stringify(alias)})`; - } - throw e; +const loadDescriptor = makeWeakCache(function*( + { value, options, dirname, alias }: UnloadedDescriptor, + cache: CacheConfigurator, +): Handler { + // Disabled presets should already have been filtered out + if (options === false) throw new Error("Assertion failure"); + + options = options || {}; + + let item = value; + if (typeof value === "function") { + const api = { + ...context, + ...makeAPI(cache), + }; + try { + item = value(api, options, dirname); + } catch (e) { + if (alias) { + e.message += ` (While processing: ${JSON.stringify(alias)})`; } + throw e; } + } - if (!item || typeof item !== "object") { - throw new Error("Plugin/Preset did not return an object."); - } + if (!item || typeof item !== "object") { + throw new Error("Plugin/Preset did not return an object."); + } - if (typeof item.then === "function") { - throw new Error( - `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.`, - ); - } + if (typeof item.then === "function") { + yield* []; // if we want to support async plugins - return { value: item, options, dirname, alias }; - }, -); + throw new Error( + `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.`, + ); + } + + return { value: item, options, dirname, alias }; +}); /** * Instantiate a plugin for the given descriptor, returning the plugin/options pair. */ -function loadPluginDescriptor( +function* loadPluginDescriptor( descriptor: UnloadedDescriptor, context: SimpleContext, -): Plugin { +): Handler { if (descriptor.value instanceof Plugin) { if (descriptor.options) { throw new Error( @@ -230,54 +238,55 @@ function loadPluginDescriptor( return descriptor.value; } - return instantiatePlugin(loadDescriptor(descriptor, context), context); + return yield* instantiatePlugin( + yield* loadDescriptor(descriptor, context), + context, + ); } -const instantiatePlugin = makeWeakCache( - ( - { value, options, dirname, alias }: LoadedDescriptor, - cache: CacheConfigurator, - ): Plugin => { - const pluginObj = validatePluginObject(value); +const instantiatePlugin = makeWeakCache(function*( + { value, options, dirname, alias }: LoadedDescriptor, + cache: CacheConfigurator, +): Handler { + const pluginObj = validatePluginObject(value); - const plugin = { - ...pluginObj, - }; - if (plugin.visitor) { - plugin.visitor = traverse.explode({ - ...plugin.visitor, - }); - } + const plugin = { + ...pluginObj, + }; + if (plugin.visitor) { + plugin.visitor = traverse.explode({ + ...plugin.visitor, + }); + } - if (plugin.inherits) { - const inheritsDescriptor = { - name: undefined, - alias: `${alias}$inherits`, - value: plugin.inherits, - options, - dirname, - }; + if (plugin.inherits) { + const inheritsDescriptor = { + name: undefined, + alias: `${alias}$inherits`, + value: plugin.inherits, + options, + dirname, + }; + const inherits = yield* forwardAsync(loadPluginDescriptor, run => { // If the inherited plugin changes, reinstantiate this plugin. - const inherits = cache.invalidate(data => - loadPluginDescriptor(inheritsDescriptor, data), - ); + return cache.invalidate(data => run(inheritsDescriptor, data)); + }); + + plugin.pre = chain(inherits.pre, plugin.pre); + plugin.post = chain(inherits.post, plugin.post); + plugin.manipulateOptions = chain( + inherits.manipulateOptions, + plugin.manipulateOptions, + ); + plugin.visitor = traverse.visitors.merge([ + inherits.visitor || {}, + plugin.visitor || {}, + ]); + } - plugin.pre = chain(inherits.pre, plugin.pre); - plugin.post = chain(inherits.post, plugin.post); - plugin.manipulateOptions = chain( - inherits.manipulateOptions, - plugin.manipulateOptions, - ); - plugin.visitor = traverse.visitors.merge([ - inherits.visitor || {}, - plugin.visitor || {}, - ]); - } - - return new Plugin(plugin, options, alias); - }, -); + return new Plugin(plugin, options, alias); +}); const validateIfOptionNeedsFilename = ( options: ValidatedOptions, @@ -318,16 +327,16 @@ const validatePreset = ( /** * Generate a config object that will act as the root of a new nested config. */ -const loadPresetDescriptor = ( +function* loadPresetDescriptor( descriptor: UnloadedDescriptor, context: ConfigContext, -): ConfigChain | null => { - const preset = instantiatePreset(loadDescriptor(descriptor, context)); +): Handler { + const preset = instantiatePreset(yield* loadDescriptor(descriptor, context)); validatePreset(preset, context, descriptor); - return buildPresetChain(preset, context); -}; + return yield* buildPresetChain(preset, context); +} -const instantiatePreset = makeWeakCache( +const instantiatePreset = makeWeakCacheSync( ({ value, dirname, alias }: LoadedDescriptor): PresetInstance => { return { options: validate("preset", value), diff --git a/packages/babel-core/src/config/index.js b/packages/babel-core/src/config/index.js index 51016d34c757..413eac86902d 100644 --- a/packages/babel-core/src/config/index.js +++ b/packages/babel-core/src/config/index.js @@ -8,12 +8,15 @@ export type { Plugin, } from "./full"; +import { loadPartialConfig as loadPartialConfigRunner } from "./partial"; + export { loadFullConfig as default }; -export { loadPartialConfig } from "./partial"; export type { PartialConfig } from "./partial"; +export const loadPartialConfig = loadPartialConfigRunner.sync; + export function loadOptions(opts: {}): Object | null { - const config = loadFullConfig(opts); + const config = loadFullConfig.sync(opts); return config ? config.options : null; } diff --git a/packages/babel-core/src/config/partial.js b/packages/babel-core/src/config/partial.js index 9f5f85ed4edc..513f18f4ae90 100644 --- a/packages/babel-core/src/config/partial.js +++ b/packages/babel-core/src/config/partial.js @@ -1,6 +1,7 @@ // @flow import path from "path"; +import gensync, { type Handler } from "gensync"; import Plugin from "./plugin"; import { mergeOptions } from "./util"; import { createItemFromDescriptor } from "./item"; @@ -14,18 +15,21 @@ import { import { findConfigUpwards, type ConfigFile, type IgnoreFile } from "./files"; -function resolveRootMode(rootDir: string, rootMode: RootMode): string { +function* resolveRootMode( + rootDir: string, + rootMode: RootMode, +): Handler { switch (rootMode) { case "root": return rootDir; case "upward-optional": { - const upwardRootDir = findConfigUpwards(rootDir); + const upwardRootDir = yield* findConfigUpwards(rootDir); return upwardRootDir === null ? rootDir : upwardRootDir; } case "upward": { - const upwardRootDir = findConfigUpwards(rootDir); + const upwardRootDir = yield* findConfigUpwards(rootDir); if (upwardRootDir !== null) return upwardRootDir; throw Object.assign( @@ -44,15 +48,17 @@ function resolveRootMode(rootDir: string, rootMode: RootMode): string { } } -export default function loadPrivatePartialConfig( - inputOpts: mixed, -): { +type PrivPartialConfig = { options: ValidatedOptions, context: ConfigContext, ignore: IgnoreFile | void, babelrc: ConfigFile | void, config: ConfigFile | void, -} | null { +}; + +export default function* loadPrivatePartialConfig( + inputOpts: mixed, +): Handler { if ( inputOpts != null && (typeof inputOpts !== "object" || Array.isArray(inputOpts)) @@ -70,7 +76,7 @@ export default function loadPrivatePartialConfig( caller, } = args; const absoluteCwd = path.resolve(cwd); - const absoluteRootDir = resolveRootMode( + const absoluteRootDir = yield* resolveRootMode( path.resolve(absoluteCwd, rootDir), rootMode, ); @@ -86,7 +92,7 @@ export default function loadPrivatePartialConfig( caller, }; - const configChain = buildRootChain(args, context); + const configChain = yield* buildRootChain(args, context); if (!configChain) return null; const options = {}; @@ -122,8 +128,10 @@ export default function loadPrivatePartialConfig( }; } -export function loadPartialConfig(inputOpts: mixed): PartialConfig | null { - const result = loadPrivatePartialConfig(inputOpts); +export const loadPartialConfig = gensync<[any], PartialConfig | null>(function*( + inputOpts: mixed, +): Handler { + const result: ?PrivPartialConfig = yield* loadPrivatePartialConfig(inputOpts); if (!result) return null; const { options, babelrc, ignore, config } = result; @@ -143,7 +151,7 @@ export function loadPartialConfig(inputOpts: mixed): PartialConfig | null { ignore ? ignore.filepath : undefined, config ? config.filepath : undefined, ); -} +}); export type { PartialConfig }; diff --git a/packages/babel-core/src/config/util.js b/packages/babel-core/src/config/util.js index f64975216db3..b9d3af143dd3 100644 --- a/packages/babel-core/src/config/util.js +++ b/packages/babel-core/src/config/util.js @@ -28,3 +28,14 @@ function mergeDefaultFields(target: T, source: T) { if (val !== undefined) target[k] = (val: any); } } + +export function isIterableIterator(value: mixed): boolean %checks { + return ( + /*:: value instanceof Generator && */ + // /*:: "@@iterator" in value && */ + !!value && + typeof value.next === "function" && + // $FlowIgnore + typeof value[Symbol.iterator] === "function" + ); +} diff --git a/packages/babel-core/src/gensync-utils/async.js b/packages/babel-core/src/gensync-utils/async.js new file mode 100644 index 000000000000..84edcc394135 --- /dev/null +++ b/packages/babel-core/src/gensync-utils/async.js @@ -0,0 +1,110 @@ +// @flow + +import gensync, { type Gensync, type Handler } from "gensync"; + +type MaybePromise = T | Promise; + +const id = x => x; + +const runGenerator = gensync(function*(item) { + return yield* item; +}); + +// This Gensync returns true if the current execution contect is +// asynchronous, otherwise it returns false. +export const isAsync = gensync<[], boolean>({ + sync: () => false, + errback: cb => cb(null, true), +}); + +// This function wraps any functions (which could be either synchronous or +// asynchronous) with a Gensync. If the wrapped function returns a promise +// but the current execution context is synchronous, it will throw the +// provided error. +// This is used to handle user-provided functions which could be asynchronous. +export function maybeAsync( + fn: (...args: Args) => T, + message: string, +): Gensync { + return gensync({ + sync(...args) { + const result = fn.apply(this, args); + if (isThenable(result)) throw new Error(message); + return result; + }, + async(...args) { + return Promise.resolve(fn.apply(this, args)); + }, + }); +} + +const withKind = (gensync<[any], any>({ + sync: cb => cb("sync"), + async: cb => cb("async"), +}): (cb: (kind: "sync" | "async") => MaybePromise) => Handler); + +// This function wraps a generator (or a Gensync) into another function which, +// when called, will run the provided generator in a sync or async way, depending +// on the execution context where this forwardAsync function is called. +// This is useful, for example, when passing a callback to a function which isn't +// aware of gensync, but it only knows about synchronous and asynchronous functions. +// An example is cache.using, which being exposed to the user must be as simple as +// possible: +// yield* forwardAsync(gensyncFn, wrappedFn => +// cache.using(x => { +// // Here we don't know about gensync. wrappedFn is a +// // normal sync or async function +// return wrappedFn(x); +// }) +// ) +export function forwardAsync( + action: (...args: ActionArgs) => Handler, + cb: ( + adapted: (...args: ActionArgs) => MaybePromise, + ) => MaybePromise, +): Handler { + const g = gensync(action); + return withKind(kind => { + const adapted = g[kind]; + return cb(adapted); + }); +} + +// If the given generator is executed asynchronously, the first time that it +// is paused (i.e. When it yields a gensync generator which can't be run +// synchronously), call the "firstPause" callback. +export const onFirstPause = (gensync<[any, any], any>({ + name: "onFirstPause", + arity: 2, + sync: function(item) { + return runGenerator.sync(item); + }, + errback: function(item, firstPause, cb) { + let completed = false; + + runGenerator.errback(item, (err, value) => { + completed = true; + cb(err, value); + }); + + if (!completed) { + firstPause(); + } + }, +}): (gen: Generator<*, T, *>, cb: Function) => Handler); + +// Wait for the given promise to be resolved +export const waitFor = (gensync<[any], any>({ + sync: id, + async: id, +}): (p: T | Promise) => Handler); + +export function isThenable(val: mixed): boolean %checks { + return ( + /*:: val instanceof Promise && */ + !!val && + (typeof val === "object" || typeof val === "function") && + !!val.then && + typeof val.then === "function" + ); +} diff --git a/packages/babel-core/src/gensync-utils/fs.js b/packages/babel-core/src/gensync-utils/fs.js new file mode 100644 index 000000000000..29cea9b43571 --- /dev/null +++ b/packages/babel-core/src/gensync-utils/fs.js @@ -0,0 +1,14 @@ +// @flow + +import fs from "fs"; +import gensync from "gensync"; + +export const readFile = gensync<[string, "utf8"], string>({ + sync: fs.readFileSync, + errback: fs.readFile, +}); + +export const exists = gensync<[string], boolean>({ + sync: fs.existsSync, + errback: (path, cb) => fs.exists(path, res => cb(null, res)), +}); diff --git a/packages/babel-core/src/gensync-utils/resolve.js b/packages/babel-core/src/gensync-utils/resolve.js new file mode 100644 index 000000000000..d16f6e7c4b2c --- /dev/null +++ b/packages/babel-core/src/gensync-utils/resolve.js @@ -0,0 +1,9 @@ +// @flow + +import resolve from "resolve"; +import gensync from "gensync"; + +export default gensync<[string, {| basedir: string |}], string>({ + sync: resolve.sync, + errback: resolve, +}); diff --git a/packages/babel-core/src/parse.js b/packages/babel-core/src/parse.js index b3397a1a2e23..6bed88c79eee 100644 --- a/packages/babel-core/src/parse.js +++ b/packages/babel-core/src/parse.js @@ -1,5 +1,7 @@ // @flow +import gensync from "gensync"; + import loadConfig, { type InputOptions } from "./config"; import normalizeFile from "./transformation/normalize-file"; import normalizeOptions from "./transformation/normalize-opts"; @@ -22,6 +24,18 @@ type Parse = { (code: string, opts: ?InputOptions): ParseResult | null, }; +const parseRunner = gensync<[string, ?InputOptions], ParseResult | null>( + function* parse(code, opts) { + const config = yield* loadConfig(opts); + + if (config === null) { + return null; + } + + return normalizeFile(config.passes, normalizeOptions(config), code).ast; + }, +); + export const parse: Parse = (function parse(code, opts, callback) { if (typeof opts === "function") { callback = opts; @@ -30,55 +44,10 @@ export const parse: Parse = (function parse(code, opts, callback) { // For backward-compat with Babel 7's early betas, we allow sync parsing when // no callback is given. Will be dropped in some future Babel major version. - if (callback === undefined) return parseSync(code, opts); - - const config = loadConfig(opts); - - if (config === null) { - return null; - } + if (callback === undefined) return parseRunner.sync(code, opts); - // Reassign to keep Flowtype happy. - const cb = callback; - - // Just delaying the transform one tick for now to simulate async behavior - // but more async logic may land here eventually. - process.nextTick(() => { - let ast = null; - try { - const cfg = loadConfig(opts); - if (cfg === null) return cb(null, null); - - ast = normalizeFile(cfg.passes, normalizeOptions(cfg), code).ast; - } catch (err) { - return cb(err); - } - - cb(null, ast); - }); + parseRunner.errback(code, opts, callback); }: Function); -export function parseSync( - code: string, - opts?: InputOptions, -): ParseResult | null { - const config = loadConfig(opts); - - if (config === null) { - return null; - } - - return normalizeFile(config.passes, normalizeOptions(config), code).ast; -} - -export function parseAsync( - code: string, - opts?: InputOptions, -): Promise { - return new Promise((res, rej) => { - parse(code, opts, (err, result) => { - if (err == null) res(result); - else rej(err); - }); - }); -} +export const parseSync = parseRunner.sync; +export const parseAsync = parseRunner.async; diff --git a/packages/babel-core/src/transform-ast.js b/packages/babel-core/src/transform-ast.js index 101177dcece9..9a40bda90d3a 100644 --- a/packages/babel-core/src/transform-ast.js +++ b/packages/babel-core/src/transform-ast.js @@ -1,9 +1,10 @@ // @flow -import loadConfig, { type InputOptions } from "./config"; +import gensync from "gensync"; + +import loadConfig, { type InputOptions, type ResolvedConfig } from "./config"; import { - runSync, - runAsync, + run, type FileResult, type FileResultCallback, } from "./transformation"; @@ -24,6 +25,18 @@ type TransformFromAst = { (ast: AstRoot, code: string, opts: ?InputOptions): FileResult | null, }; +const transformFromAstRunner = gensync< + [AstRoot, string, ?InputOptions], + FileResult | null, +>(function*(ast, code, opts) { + const config: ResolvedConfig | null = yield* loadConfig(opts); + if (config === null) return null; + + if (!ast) throw new Error("No AST given"); + + return yield* run(config, code, ast); +}); + export const transformFromAst: TransformFromAst = (function transformFromAst( ast, code, @@ -37,50 +50,12 @@ export const transformFromAst: TransformFromAst = (function transformFromAst( // For backward-compat with Babel 6, we allow sync transformation when // no callback is given. Will be dropped in some future Babel major version. - if (callback === undefined) return transformFromAstSync(ast, code, opts); - - // Reassign to keep Flowtype happy. - const cb = callback; - - // Just delaying the transform one tick for now to simulate async behavior - // but more async logic may land here eventually. - process.nextTick(() => { - let cfg; - try { - cfg = loadConfig(opts); - if (cfg === null) return cb(null, null); - } catch (err) { - return cb(err); - } - - if (!ast) return cb(new Error("No AST given")); + if (callback === undefined) { + return transformFromAstRunner.sync(ast, code, opts); + } - runAsync(cfg, code, ast, cb); - }); + transformFromAstRunner.errback(ast, code, opts, callback); }: Function); -export function transformFromAstSync( - ast: AstRoot, - code: string, - opts: ?InputOptions, -): FileResult | null { - const config = loadConfig(opts); - if (config === null) return null; - - if (!ast) throw new Error("No AST given"); - - return runSync(config, code, ast); -} - -export function transformFromAstAsync( - ast: AstRoot, - code: string, - opts: ?InputOptions, -): Promise { - return new Promise((res, rej) => { - transformFromAst(ast, code, opts, (err, result) => { - if (err == null) res(result); - else rej(err); - }); - }); -} +export const transformFromAstSync = transformFromAstRunner.sync; +export const transformFromAstAsync = transformFromAstRunner.async; diff --git a/packages/babel-core/src/transform-file.js b/packages/babel-core/src/transform-file.js index d3c9ab640fc3..9d2c3e6ae406 100644 --- a/packages/babel-core/src/transform-file.js +++ b/packages/babel-core/src/transform-file.js @@ -1,13 +1,14 @@ // @flow -import fs from "fs"; -import loadConfig, { type InputOptions } from "./config"; +import gensync from "gensync"; + +import loadConfig, { type InputOptions, type ResolvedConfig } from "./config"; import { - runSync, - runAsync, + run, type FileResult, type FileResultCallback, } from "./transformation"; +import * as fs from "./gensync-utils/fs"; import typeof * as transformFileBrowserType from "./transform-file-browser"; import typeof * as transformFileType from "./transform-file"; @@ -22,74 +23,26 @@ type TransformFile = { (filename: string, opts: ?InputOptions, callback: FileResultCallback): void, }; -export const transformFile: TransformFile = (function transformFile( - filename, - opts, - callback, -) { - let options; - if (typeof opts === "function") { - callback = opts; - opts = undefined; - } - - if (opts == null) { - options = { filename }; - } else if (opts && typeof opts === "object") { - options = { - ...opts, - filename, - }; - } - - process.nextTick(() => { - let cfg; - try { - cfg = loadConfig(options); - if (cfg === null) return callback(null, null); - } catch (err) { - return callback(err); +const transformFileRunner = gensync<[string, ?InputOptions], FileResult | null>( + function*(filename, opts) { + let options; + if (opts == null) { + options = { filename }; + } else if (opts && typeof opts === "object") { + options = { + ...opts, + filename, + }; } - // Reassignment to keep Flow happy. - const config = cfg; - - fs.readFile(filename, "utf8", function(err, code: string) { - if (err) return callback(err, null); - - runAsync(config, code, null, callback); - }); - }); -}: Function); - -export function transformFileSync( - filename: string, - opts: ?InputOptions, -): FileResult | null { - let options; - if (opts == null) { - options = { filename }; - } else if (opts && typeof opts === "object") { - options = { - ...opts, - filename, - }; - } - - const config = loadConfig(options); - if (config === null) return null; + const config: ResolvedConfig | null = yield* loadConfig(options); + if (config === null) return null; - return runSync(config, fs.readFileSync(filename, "utf8")); -} + const code = yield* fs.readFile(filename, "utf8"); + return yield* run(config, code); + }, +); -export function transformFileAsync( - filename: string, - opts: ?InputOptions, -): Promise { - return new Promise((res, rej) => { - transformFile(filename, opts, (err, result) => { - if (err == null) res(result); - else rej(err); - }); - }); -} +export const transformFile: TransformFile = transformFileRunner.errback; +export const transformFileSync = transformFileRunner.sync; +export const transformFileAsync = transformFileRunner.async; diff --git a/packages/babel-core/src/transform.js b/packages/babel-core/src/transform.js index 4a2f0e5c1691..0ace438c29fa 100644 --- a/packages/babel-core/src/transform.js +++ b/packages/babel-core/src/transform.js @@ -1,8 +1,10 @@ // @flow -import loadConfig, { type InputOptions } from "./config"; + +import gensync from "gensync"; + +import loadConfig, { type InputOptions, type ResolvedConfig } from "./config"; import { - runSync, - runAsync, + run, type FileResult, type FileResultCallback, } from "./transformation"; @@ -16,6 +18,15 @@ type Transform = { (code: string, opts: ?InputOptions): FileResult | null, }; +const transformRunner = gensync<[string, ?InputOptions], FileResult | null>( + function* transform(code, opts) { + const config: ResolvedConfig | null = yield* loadConfig(opts); + if (config === null) return null; + + return yield* run(config, code); + }, +); + export const transform: Transform = (function transform(code, opts, callback) { if (typeof opts === "function") { callback = opts; @@ -24,44 +35,10 @@ export const transform: Transform = (function transform(code, opts, callback) { // For backward-compat with Babel 6, we allow sync transformation when // no callback is given. Will be dropped in some future Babel major version. - if (callback === undefined) return transformSync(code, opts); + if (callback === undefined) return transformRunner.sync(code, opts); - // Reassign to keep Flowtype happy. - const cb = callback; - - // Just delaying the transform one tick for now to simulate async behavior - // but more async logic may land here eventually. - process.nextTick(() => { - let cfg; - try { - cfg = loadConfig(opts); - if (cfg === null) return cb(null, null); - } catch (err) { - return cb(err); - } - - runAsync(cfg, code, null, cb); - }); + transformRunner.errback(code, opts, callback); }: Function); -export function transformSync( - code: string, - opts: ?InputOptions, -): FileResult | null { - const config = loadConfig(opts); - if (config === null) return null; - - return runSync(config, code); -} - -export function transformAsync( - code: string, - opts: ?InputOptions, -): Promise { - return new Promise((res, rej) => { - transform(code, opts, (err, result) => { - if (err == null) res(result); - else rej(err); - }); - }); -} +export const transformSync = transformRunner.sync; +export const transformAsync = transformRunner.async; diff --git a/packages/babel-core/src/transformation/block-hoist-plugin.js b/packages/babel-core/src/transformation/block-hoist-plugin.js index 8e8fe69699f0..49ecf5a18556 100644 --- a/packages/babel-core/src/transformation/block-hoist-plugin.js +++ b/packages/babel-core/src/transformation/block-hoist-plugin.js @@ -11,7 +11,7 @@ export default function loadBlockHoistPlugin(): Plugin { // Lazy-init the internal plugin to remove the init-time circular // dependency between plugins being passed @babel/core's export object, // which loads this file, and this 'loadConfig' loading plugins. - const config = loadConfig({ + const config = loadConfig.sync({ babelrc: false, configFile: false, plugins: [blockHoistPlugin], diff --git a/packages/babel-core/src/transformation/index.js b/packages/babel-core/src/transformation/index.js index e2eccd894537..c77d8c478018 100644 --- a/packages/babel-core/src/transformation/index.js +++ b/packages/babel-core/src/transformation/index.js @@ -1,6 +1,7 @@ // @flow import traverse from "@babel/traverse"; import typeof { SourceMap } from "convert-source-map"; +import type { Handler } from "gensync"; import type { ResolvedConfig, PluginPasses } from "../config"; @@ -25,29 +26,11 @@ export type FileResult = { map: SourceMap | null, }; -export function runAsync( +export function* run( config: ResolvedConfig, code: string, ast: ?(BabelNodeFile | BabelNodeProgram), - callback: Function, -) { - let result; - try { - result = runSync(config, code, ast); - } catch (err) { - return callback(err); - } - - // We don't actually care about calling this synchronously here because it is - // already running within a .nextTick handler from the transform calls above. - return callback(null, result); -} - -export function runSync( - config: ResolvedConfig, - code: string, - ast: ?(BabelNodeFile | BabelNodeProgram), -): FileResult { +): Handler { const file = normalizeFile( config.passes, normalizeOptions(config), @@ -57,7 +40,7 @@ export function runSync( const opts = file.opts; try { - transformFile(file, config.passes); + yield* transformFile(file, config.passes); } catch (e) { e.message = `${opts.filename ?? "unknown"}: ${e.message}`; if (!e.code) { @@ -89,7 +72,7 @@ export function runSync( }; } -function transformFile(file: File, pluginPasses: PluginPasses): void { +function* transformFile(file: File, pluginPasses: PluginPasses): Handler { for (const pluginPairs of pluginPasses) { const passPairs = []; const passes = []; @@ -108,6 +91,7 @@ function transformFile(file: File, pluginPasses: PluginPasses): void { if (fn) { const result = fn.call(pass, file); + yield* []; if (isThenable(result)) { throw new Error( `You appear to be using an plugin with an async .pre, ` + @@ -132,6 +116,7 @@ function transformFile(file: File, pluginPasses: PluginPasses): void { if (fn) { const result = fn.call(pass, file); + yield* []; if (isThenable(result)) { throw new Error( `You appear to be using an plugin with an async .post, ` + diff --git a/packages/babel-core/test/async.js b/packages/babel-core/test/async.js new file mode 100644 index 000000000000..3d834c9e637d --- /dev/null +++ b/packages/babel-core/test/async.js @@ -0,0 +1,214 @@ +import path from "path"; +import * as babel from ".."; + +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; + testFn(...args); +}; + +describe("asynchronicity", () => { + const base = path.join(__dirname, "fixtures", "async"); + let cwd; + + beforeEach(function() { + cwd = process.cwd(); + process.chdir(base); + }); + + afterEach(function() { + process.chdir(cwd); + }); + + describe("config file", () => { + describe("async function", () => { + nodeGte8("called synchronously", () => { + process.chdir("config-file-async-function"); + + expect(() => + babel.transformSync(""), + ).toThrowErrorMatchingInlineSnapshot( + `"You appear to be using an async configuration, which your current version of Babel does` + + ` not support. We may add support for this in the future, but if you're on the most recent` + + ` version of @babel/core and still seeing this error, then you'll need to synchronously` + + ` return your config."`, + ); + }); + + nodeGte8("called asynchronously", async () => { + process.chdir("config-file-async-function"); + + await expect( + babel.transformAsync(""), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"You appear to be using an async configuration, which your current version of Babel does` + + ` not support. We may add support for this in the future, but if you're on the most recent` + + ` version of @babel/core and still seeing this error, then you'll need to synchronously` + + ` return your config."`, + ); + }); + }); + + describe("promise", () => { + it("called synchronously", () => { + process.chdir("config-file-promise"); + + expect(() => + babel.transformSync(""), + ).toThrowErrorMatchingInlineSnapshot( + `"You appear to be using an async configuration, which your current version of Babel does` + + ` not support. We may add support for this in the future, but if you're on the most recent` + + ` version of @babel/core and still seeing this error, then you'll need to synchronously` + + ` return your config."`, + ); + }); + + it("called asynchronously", async () => { + process.chdir("config-file-promise"); + + await expect( + babel.transformAsync(""), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"You appear to be using an async configuration, which your current version of Babel does` + + ` not support. We may add support for this in the future, but if you're on the most recent` + + ` version of @babel/core and still seeing this error, then you'll need to synchronously` + + ` return your config."`, + ); + }); + }); + + describe("cache.using", () => { + nodeGte8("called synchronously", () => { + process.chdir("config-cache"); + + expect(() => + babel.transformSync(""), + ).toThrowErrorMatchingInlineSnapshot( + `"You appear to be using an async cache handler, which your current version of Babel does` + + ` not support. We may add support for this in the future, but if you're on the most recent` + + ` version of @babel/core and still seeing this error, then you'll need to synchronously` + + ` handle your caching logic."`, + ); + }); + + nodeGte8("called asynchronously", async () => { + process.chdir("config-cache"); + + await expect( + babel.transformAsync(""), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"You appear to be using an async cache handler, which your current version of Babel does` + + ` not support. We may add support for this in the future, but if you're on the most recent` + + ` version of @babel/core and still seeing this error, then you'll need to synchronously` + + ` handle your caching logic."`, + ); + }); + }); + }); + + describe("plugin", () => { + describe("factory function", () => { + 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."`, + ); + }); + + 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."`, + ); + }); + }); + + describe(".pre", () => { + nodeGte8("called synchronously", () => { + process.chdir("plugin-pre"); + + expect(() => + babel.transformSync(""), + ).toThrowErrorMatchingInlineSnapshot( + `"unknown: You appear to be using an plugin with an async .pre, 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."`, + ); + }); + + nodeGte8("called asynchronously", async () => { + process.chdir("plugin-pre"); + + await expect( + babel.transformAsync(""), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"unknown: You appear to be using an plugin with an async .pre, 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."`, + ); + }); + }); + + describe(".post", () => { + nodeGte8("called synchronously", () => { + process.chdir("plugin-post"); + + expect(() => + babel.transformSync(""), + ).toThrowErrorMatchingInlineSnapshot( + `"unknown: You appear to be using an plugin with an async .post, 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."`, + ); + }); + + nodeGte8("called asynchronously", async () => { + process.chdir("plugin-post"); + + await expect( + babel.transformAsync(""), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"unknown: You appear to be using an plugin with an async .post, 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."`, + ); + }); + }); + + describe("inherits", () => { + 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."`, + ); + }); + + 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."`, + ); + }); + }); + }); +}); diff --git a/packages/babel-core/test/caching-api.js b/packages/babel-core/test/caching-api.js index 0318759fe861..f8d442c150b4 100644 --- a/packages/babel-core/test/caching-api.js +++ b/packages/babel-core/test/caching-api.js @@ -1,10 +1,12 @@ -import { makeStrongCache } from "../lib/config/caching"; +import gensync from "gensync"; +import { makeStrongCacheSync, makeStrongCache } from "../lib/config/caching"; +import { waitFor } from "../lib/gensync-utils/async"; describe("caching API", () => { it("should allow permacaching with .forever()", () => { let count = 0; - const fn = makeStrongCache((arg, cache) => { + const fn = makeStrongCacheSync((arg, cache) => { cache.forever(); return { arg, count: count++ }; }); @@ -21,7 +23,7 @@ describe("caching API", () => { it("should allow disabling caching with .never()", () => { let count = 0; - const fn = makeStrongCache((arg, cache) => { + const fn = makeStrongCacheSync((arg, cache) => { cache.never(); return { arg, count: count++ }; }); @@ -41,7 +43,7 @@ describe("caching API", () => { let count = 0; let other = "default"; - const fn = makeStrongCache((arg, cache) => { + const fn = makeStrongCacheSync((arg, cache) => { const val = cache.using(() => other); return { arg, val, count: count++ }; @@ -82,7 +84,7 @@ describe("caching API", () => { let count = 0; let other = "default"; - const fn = makeStrongCache((arg, cache) => { + const fn = makeStrongCacheSync((arg, cache) => { const val = cache.invalidate(() => other); return { arg, val, count: count++ }; @@ -124,7 +126,7 @@ describe("caching API", () => { let other = "default"; let another = "another"; - const fn = makeStrongCache((arg, cache) => { + const fn = makeStrongCacheSync((arg, cache) => { const val = cache.using(() => other); const val2 = cache.invalidate(() => another); @@ -223,7 +225,7 @@ describe("caching API", () => { it("should auto-permacache by default", () => { let count = 0; - const fn = makeStrongCache(arg => ({ arg, count: count++ })); + const fn = makeStrongCacheSync(arg => ({ arg, count: count++ })); expect(fn("one")).toEqual({ arg: "one", count: 0 }); expect(fn("one")).toBe(fn("one")); @@ -235,7 +237,7 @@ describe("caching API", () => { }); it("should throw if you set permacaching and use .using", () => { - const fn = makeStrongCache((arg, cache) => { + const fn = makeStrongCacheSync((arg, cache) => { cache.forever(); cache.using(() => null); @@ -245,7 +247,7 @@ describe("caching API", () => { }); it("should throw if you set permacaching and use .invalidate", () => { - const fn = makeStrongCache((arg, cache) => { + const fn = makeStrongCacheSync((arg, cache) => { cache.forever(); cache.invalidate(() => null); @@ -255,7 +257,7 @@ describe("caching API", () => { }); it("should throw if you set permacaching and use .never", () => { - const fn = makeStrongCache((arg, cache) => { + const fn = makeStrongCacheSync((arg, cache) => { cache.forever(); cache.never(); @@ -265,7 +267,7 @@ describe("caching API", () => { }); it("should throw if you set no caching and use .using", () => { - const fn = makeStrongCache((arg, cache) => { + const fn = makeStrongCacheSync((arg, cache) => { cache.never(); cache.using(() => null); @@ -275,7 +277,7 @@ describe("caching API", () => { }); it("should throw if you set no caching and use .invalidate", () => { - const fn = makeStrongCache((arg, cache) => { + const fn = makeStrongCacheSync((arg, cache) => { cache.never(); cache.invalidate(() => null); @@ -285,7 +287,7 @@ describe("caching API", () => { }); it("should throw if you set no caching and use .never", () => { - const fn = makeStrongCache((arg, cache) => { + const fn = makeStrongCacheSync((arg, cache) => { cache.never(); cache.using(() => null); @@ -295,7 +297,7 @@ describe("caching API", () => { }); it("should throw if you configure .forever after exiting", () => { - const fn = makeStrongCache((arg, cache) => cache); + const fn = makeStrongCacheSync((arg, cache) => cache); expect(() => fn().forever()).toThrow( /Cannot change caching after evaluation/, @@ -303,7 +305,7 @@ describe("caching API", () => { }); it("should throw if you configure .never after exiting", () => { - const fn = makeStrongCache((arg, cache) => cache); + const fn = makeStrongCacheSync((arg, cache) => cache); expect(() => fn().never()).toThrow( /Cannot change caching after evaluation/, @@ -311,7 +313,7 @@ describe("caching API", () => { }); it("should throw if you configure .using after exiting", () => { - const fn = makeStrongCache((arg, cache) => cache); + const fn = makeStrongCacheSync((arg, cache) => cache); expect(() => fn().using(() => null)).toThrow( /Cannot change caching after evaluation/, @@ -319,7 +321,7 @@ describe("caching API", () => { }); it("should throw if you configure .invalidate after exiting", () => { - const fn = makeStrongCache((arg, cache) => cache); + const fn = makeStrongCacheSync((arg, cache) => cache); expect(() => fn().invalidate(() => null)).toThrow( /Cannot change caching after evaluation/, @@ -330,7 +332,7 @@ describe("caching API", () => { it("should allow permacaching with cache(true)", () => { let count = 0; - const fn = makeStrongCache((arg, cache) => { + const fn = makeStrongCacheSync((arg, cache) => { cache = cache.simple(); cache(true); @@ -349,7 +351,7 @@ describe("caching API", () => { it("should allow disabling caching with cache(false)", () => { let count = 0; - const fn = makeStrongCache((arg, cache) => { + const fn = makeStrongCacheSync((arg, cache) => { cache = cache.simple(); cache(false); @@ -371,7 +373,7 @@ describe("caching API", () => { let count = 0; let other = "default"; - const fn = makeStrongCache((arg, cache) => { + const fn = makeStrongCacheSync((arg, cache) => { cache = cache.simple(); const val = cache(() => other); @@ -410,4 +412,60 @@ describe("caching API", () => { expect(fn("two")).toBe(fn("two")); }); }); + + describe("async", () => { + const wait = gensync({ + sync: () => {}, + errback: (t, cb) => setTimeout(cb, t), + }); + + it("should throw if the cache is configured asynchronously", async () => { + const fn = gensync( + makeStrongCache(function*(arg, cache) { + yield* wait(1000); + cache.never(); + return { arg }; + }), + ).async; + + await expect(fn("bar")).rejects.toThrowErrorMatchingInlineSnapshot( + `"Cannot change caching after evaluation has completed."`, + ); + }); + + it("should allow asynchronous cache invalidation functions", async () => { + const fn = gensync( + makeStrongCache(function*(arg, cache) { + yield* waitFor( + cache.using(async () => { + await wait.async(50); + return "x"; + }), + ); + return { arg }; + }), + ).async; + + const [res1, res2] = await Promise.all([fn("foo"), fn("foo")]); + + expect(res1).toBe(res2); + }); + + it("should allow synchronous yield before cache configuration", async () => { + const fn = gensync( + makeStrongCache(function*(arg, cache) { + yield* gensync({ + sync: () => 2, + errback: cb => cb(null, 2), + })(); + cache.forever(); + return { arg }; + }), + ).async; + + const [res1, res2] = await Promise.all([fn("foo"), fn("foo")]); + + expect(res1).toBe(res2); + }); + }); }); diff --git a/packages/babel-core/test/config-loading.js b/packages/babel-core/test/config-loading.js index 1014a10f8e71..4f61315404e4 100644 --- a/packages/babel-core/test/config-loading.js +++ b/packages/babel-core/test/config-loading.js @@ -1,6 +1,8 @@ -import loadConfig, { loadPartialConfig } from "../lib/config"; +import loadConfigRunner, { loadPartialConfig } from "../lib/config"; import path from "path"; +const loadConfig = loadConfigRunner.sync; + describe("@babel/core config loading", () => { const FILEPATH = path.join( __dirname, diff --git a/packages/babel-core/test/fixtures/async/config-cache/babel.config.js b/packages/babel-core/test/fixtures/async/config-cache/babel.config.js new file mode 100644 index 000000000000..f17724435869 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/config-cache/babel.config.js @@ -0,0 +1,12 @@ +const wait = t => new Promise(r => setTimeout(r, t)); + +module.exports = function(api) { + api.cache.using(async () => { + await wait(50); + return 2; + }) + + return { + plugins: ["./plugin"], + }; +}; diff --git a/packages/babel-core/test/fixtures/async/config-cache/plugin.js b/packages/babel-core/test/fixtures/async/config-cache/plugin.js new file mode 100644 index 000000000000..f3164a2364f9 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/config-cache/plugin.js @@ -0,0 +1,9 @@ +module.exports = function plugin({ types: t }) { + return { + visitor: { + Program(path) { + path.pushContainer("body", t.stringLiteral("success")); + }, + }, + }; +}; diff --git a/packages/babel-core/test/fixtures/async/config-file-async-function/babel.config.js b/packages/babel-core/test/fixtures/async/config-file-async-function/babel.config.js new file mode 100644 index 000000000000..14e85e8e700f --- /dev/null +++ b/packages/babel-core/test/fixtures/async/config-file-async-function/babel.config.js @@ -0,0 +1,11 @@ +const wait = t => new Promise(r => setTimeout(r, t)); + +module.exports = async function(api) { + await wait(50); + + api.cache.never(); + + return { + plugins: ["./plugin"], + }; +}; diff --git a/packages/babel-core/test/fixtures/async/config-file-async-function/plugin.js b/packages/babel-core/test/fixtures/async/config-file-async-function/plugin.js new file mode 100644 index 000000000000..f3164a2364f9 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/config-file-async-function/plugin.js @@ -0,0 +1,9 @@ +module.exports = function plugin({ types: t }) { + return { + visitor: { + Program(path) { + path.pushContainer("body", t.stringLiteral("success")); + }, + }, + }; +}; diff --git a/packages/babel-core/test/fixtures/async/config-file-promise/babel.config.js b/packages/babel-core/test/fixtures/async/config-file-promise/babel.config.js new file mode 100644 index 000000000000..dffa9f6fdbe1 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/config-file-promise/babel.config.js @@ -0,0 +1,5 @@ +const wait = t => new Promise(r => setTimeout(r, t)); + +module.exports = wait(50).then(() => ({ + plugins: ["./plugin"], +})); diff --git a/packages/babel-core/test/fixtures/async/config-file-promise/plugin.js b/packages/babel-core/test/fixtures/async/config-file-promise/plugin.js new file mode 100644 index 000000000000..f3164a2364f9 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/config-file-promise/plugin.js @@ -0,0 +1,9 @@ +module.exports = function plugin({ types: t }) { + return { + visitor: { + Program(path) { + path.pushContainer("body", t.stringLiteral("success")); + }, + }, + }; +}; diff --git a/packages/babel-core/test/fixtures/async/plugin-inherits/babel.config.js b/packages/babel-core/test/fixtures/async/plugin-inherits/babel.config.js new file mode 100644 index 000000000000..8b5df0db23c0 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/plugin-inherits/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: ["./plugin"], +}; diff --git a/packages/babel-core/test/fixtures/async/plugin-inherits/plugin.js b/packages/babel-core/test/fixtures/async/plugin-inherits/plugin.js new file mode 100644 index 000000000000..6f7f0b197f46 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/plugin-inherits/plugin.js @@ -0,0 +1,10 @@ +module.exports = function plugin({ types: t }) { + return { + inherits: require("./plugin2"), + visitor: { + Program(path) { + path.pushContainer("body", t.stringLiteral("success")); + }, + }, + }; +}; diff --git a/packages/babel-core/test/fixtures/async/plugin-inherits/plugin2.js b/packages/babel-core/test/fixtures/async/plugin-inherits/plugin2.js new file mode 100644 index 000000000000..f881463f9814 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/plugin-inherits/plugin2.js @@ -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 2")); + }, + }, + }; +}; diff --git a/packages/babel-core/test/fixtures/async/plugin-post/babel.config.js b/packages/babel-core/test/fixtures/async/plugin-post/babel.config.js new file mode 100644 index 000000000000..8b5df0db23c0 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/plugin-post/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: ["./plugin"], +}; diff --git a/packages/babel-core/test/fixtures/async/plugin-post/plugin.js b/packages/babel-core/test/fixtures/async/plugin-post/plugin.js new file mode 100644 index 000000000000..8ceebfd644d4 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/plugin-post/plugin.js @@ -0,0 +1,15 @@ +const wait = t => new Promise(r => setTimeout(r, t)); + +module.exports = function plugin({ types: t }) { + return { + async post() { + await wait(50); + }, + + visitor: { + Program(path) { + path.pushContainer("body", t.stringLiteral("success")); + }, + }, + }; +}; diff --git a/packages/babel-core/test/fixtures/async/plugin-pre/babel.config.js b/packages/babel-core/test/fixtures/async/plugin-pre/babel.config.js new file mode 100644 index 000000000000..8b5df0db23c0 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/plugin-pre/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: ["./plugin"], +}; diff --git a/packages/babel-core/test/fixtures/async/plugin-pre/plugin.js b/packages/babel-core/test/fixtures/async/plugin-pre/plugin.js new file mode 100644 index 000000000000..d354a0a60fb7 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/plugin-pre/plugin.js @@ -0,0 +1,15 @@ +const wait = t => new Promise(r => setTimeout(r, t)); + +module.exports = function plugin({ types: t }) { + return { + async pre() { + await wait(50); + }, + + visitor: { + Program(path) { + path.pushContainer("body", t.stringLiteral("success")); + }, + }, + }; +}; diff --git a/packages/babel-core/test/fixtures/async/plugin/babel.config.js b/packages/babel-core/test/fixtures/async/plugin/babel.config.js new file mode 100644 index 000000000000..8b5df0db23c0 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/plugin/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: ["./plugin"], +}; diff --git a/packages/babel-core/test/fixtures/async/plugin/plugin.js b/packages/babel-core/test/fixtures/async/plugin/plugin.js new file mode 100644 index 000000000000..9777dd80ae42 --- /dev/null +++ b/packages/babel-core/test/fixtures/async/plugin/plugin.js @@ -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")); + }, + }, + }; +};