Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support "extensions" option in config files and presets #12151

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 19 additions & 4 deletions packages/babel-cli/src/babel/dir.js
Expand Up @@ -6,6 +6,7 @@ import { sync as makeDirSync } from "make-dir";
import slash from "slash";
import path from "path";
import fs from "fs";
import { DEFAULT_EXTENSIONS } from "@babel/core";

import * as util from "./util";
import { type CmdOptions } from "./options";
Expand Down Expand Up @@ -34,7 +35,10 @@ export default async function ({
): Promise<$Keys<typeof FILE_TYPE>> {
let relative = path.relative(base, src);

if (!util.isCompilableExtension(relative, cliOptions.extensions)) {
if (
!util.BABEL_SUPPORTS_EXTENSIONS_OPTION &&
!util.isCompilableExtension(relative, cliOptions.extensions)
) {
return FILE_TYPE.NON_COMPILABLE;
}

Expand All @@ -48,17 +52,28 @@ export default async function ({
const dest = getDest(relative, base);

try {
const res = await util.compile(
src,
const config = await util.loadPartialConfig(
defaults(
{
sourceFileName: slash(path.relative(dest + "/..", src)),
filename: src,
...(util.BABEL_SUPPORTS_EXTENSIONS_OPTION && {
// TODO(Babel 8): At some point @babel/core will default to DEFAULT_EXTENSIONS
// instead of ["*"], and we can avoid setting it here.
extensions: cliOptions.extensions ?? DEFAULT_EXTENSIONS,
}),
},
babelOptions,
),
);

if (!res) return FILE_TYPE.IGNORED;
if (!config) return FILE_TYPE.IGNORED;

const res = await util.compile(src, config.options);

// If loadPartialConfig didn't return null, it's because the file wasn't ignored.
// Thus, if compiling the file returns null, it's because the extension isn't supported.
if (!res) return FILE_TYPE.NON_COMPILABLE;

// we've requested explicit sourcemaps to be written to disk
if (
Expand Down
1 change: 1 addition & 0 deletions packages/babel-cli/src/babel/file.js
Expand Up @@ -226,6 +226,7 @@ export default async function ({
})
.on("all", function (type: string, filename: string): void {
if (
!util.BABEL_SUPPORTS_EXTENSIONS_OPTION &&
!util.isCompilableExtension(filename, cliOptions.extensions) &&
!filenames.includes(filename)
) {
Expand Down
29 changes: 29 additions & 0 deletions packages/babel-cli/src/babel/util.js
Expand Up @@ -5,6 +5,11 @@ import * as babel from "@babel/core";
import path from "path";
import fs from "fs";

// Technically we could use the "semver" package here, but (for exmaple)
// parseFloat("4.23.6") returns 4.23 so it's "good enough"
export const BABEL_SUPPORTS_EXTENSIONS_OPTION =
parseFloat(babel.version) >= 7.11;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️

  • Before merging this PR, I need to add a warning in new-version in Makefile to replace 7.11 with the actual new version.


export function chmod(src: string, dest: string): void {
fs.chmodSync(dest, fs.statSync(src).mode);
}
Expand Down Expand Up @@ -32,6 +37,10 @@ export function readdirForCompilable(
includeDotfiles: boolean,
altExts?: Array<string>,
): Array<string> {
if (BABEL_SUPPORTS_EXTENSIONS_OPTION) {
return readdir(dirname, includeDotfiles);
}

return readdir(dirname, includeDotfiles, function (filename) {
return isCompilableExtension(filename, altExts);
});
Expand All @@ -44,6 +53,12 @@ export function isCompilableExtension(
filename: string,
altExts?: Array<string>,
): boolean {
if (!BABEL_SUPPORTS_EXTENSIONS_OPTION) {
throw new Error(
"Internal @babel/cli error: isCompilableExtension is only supported with old @babel/core versions.",
);
}

const exts = altExts || babel.DEFAULT_EXTENSIONS;
const ext = path.extname(filename);
return exts.includes(ext);
Expand Down Expand Up @@ -76,6 +91,20 @@ export function transform(
});
}

export function loadPartialConfig(opts: Object): Promise<Object> {
opts = {
...opts,
caller: CALLER,
};

return new Promise((resolve, reject) => {
babel.loadPartialConfig(opts, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
}

export function compile(
filename: string,
opts: Object | Function,
Expand Down
Expand Up @@ -5,7 +5,7 @@
"lib",
"--copy-files",
"--only",
"src/foo/*",
"src/foo/*,src/README.md",
"--no-copy-ignored",
"--verbose"
]
Expand Down
@@ -0,0 +1,3 @@
{
"extensions": [".tsx"]
}
@@ -0,0 +1 @@
x;
@@ -0,0 +1 @@
x;
@@ -0,0 +1 @@
x;
@@ -0,0 +1,3 @@
{
"args": ["src", "--out-dir", "lib"]
}
@@ -0,0 +1,3 @@
"use strict";

x;
@@ -0,0 +1,3 @@
"use strict";

x;
@@ -0,0 +1 @@
Successfully compiled 2 files with Babel (123ms).
Expand Up @@ -16,6 +16,14 @@ config <CWD>/.babelrc
programmatic options from @babel/cli
{
"sourceFileName": "../src/foo.js",
"filename": "src//foo.js",
"extensions": [
".js",
".jsx",
".es6",
".es",
".mjs"
],
"presets": [
"<ROOTDIR>//packages//babel-preset-react"
],
Expand All @@ -26,7 +34,6 @@ programmatic options from @babel/cli
],
"caller": {
"name": "@babel/cli"
},
"filename": "src//foo.js"
}
}
Successfully compiled 1 file with Babel (123ms).
13 changes: 10 additions & 3 deletions packages/babel-core/src/config/full.js
Expand Up @@ -4,7 +4,7 @@ import gensync, { type Handler } from "gensync";
import { forwardAsync } from "../gensync-utils/async";

import { mergeOptions } from "./util";
import * as context from "../index";
import * as babelContext from "../index";
import Plugin from "./plugin";
import { getItemDescriptor } from "./item";
import {
Expand Down Expand Up @@ -70,7 +70,7 @@ export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig(
return null;
}

const optionDefaults = {};
const optionDefaults: ValidatedOptions = {};

const { plugins, presets } = options;

Expand Down Expand Up @@ -161,6 +161,8 @@ export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig(
const opts: Object = optionDefaults;
mergeOptions(opts, options);

if (!isCompilableFile(context.filename, opts.extensions)) return null;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After #11907 is merged, I need to check how to make Babel report these files as "unsupported".
The we can use this info in @babel/cli/@babel/register instead of a custom ignore logic.

#11907 (comment)


yield* enhanceError(context, function* loadPluginDescriptors() {
pluginDescriptorsByPass[0].unshift(...initialPluginsDescriptors);

Expand Down Expand Up @@ -229,7 +231,7 @@ const loadDescriptor = makeWeakCache(function* (
let item = value;
if (typeof value === "function") {
const api = {
...context,
...babelContext,
...makeAPI(cache),
};
try {
Expand Down Expand Up @@ -395,3 +397,8 @@ function chain(a, b) {
}
};
}

function isCompilableFile(filename, extensions) {
if (filename == null) return true;
return extensions.some(ext => ext === "*" || filename.endsWith(ext));
}
10 changes: 9 additions & 1 deletion packages/babel-core/src/config/partial.js
Expand Up @@ -115,11 +115,19 @@ export default function* loadPrivatePartialConfig(
const configChain = yield* buildRootChain(args, context);
if (!configChain) return null;

const options = {};
const options: ValidatedOptions = {};
configChain.options.forEach(opts => {
mergeOptions(options, opts);
});

// If the programmatic options or config files don't set the "extensions"
// option, default to ["*"] for backward compatibility reasons.
// Note: this default value is set _before_ loading the presets, so it's
// safe to add the "extensions" option to a preset in a minor version.
// TODO(Babel 8): The default should be babel.DEFAULT_EXTENSIONS
// TODO: Use ??= once flow supports it.
options.extensions = options.extensions ?? ["*"];

// Tack the passes onto the object itself so that, if this object is
// passed back to Babel a second time, it will be in the right structure
// to not change behavior.
Expand Down
9 changes: 9 additions & 0 deletions packages/babel-core/src/config/util.js
Expand Up @@ -2,6 +2,10 @@

import type { ValidatedOptions } from "./validation/options";

function unique<T>(...args: T[]): T[] {
return Array.from(new Set(args));
}

export function mergeOptions(
target: ValidatedOptions,
source: ValidatedOptions,
Expand All @@ -15,6 +19,11 @@ export function mergeOptions(
const generatorOpts = source.generatorOpts;
const targetObj = (target.generatorOpts = target.generatorOpts || {});
mergeDefaultFields(targetObj, generatorOpts);
} else if (k === "extensions" && target.extensions && source.extensions) {
target.extensions = unique(
...(target.extensions ?? []),
...source.extensions,
);
} else {
const val = source[k];
if (val !== undefined) target[k] = (val: any);
Expand Down
46 changes: 34 additions & 12 deletions packages/babel-core/src/config/validation/option-assertions.js
Expand Up @@ -3,7 +3,7 @@
import type {
ConfigFileSearch,
BabelrcSearch,
IgnoreList,
FileExtension,
IgnoreItem,
PluginList,
PluginItem,
Expand Down Expand Up @@ -217,17 +217,20 @@ export function assertArray(
return value;
}

export function assertIgnoreList(
loc: OptionPath,
value: mixed,
): IgnoreList | void {
const arr = assertArray(loc, value);
if (arr) {
arr.forEach((item, i) => assertIgnoreItem(access(loc, i), item));
}
return (arr: any);
function createArrayAssertion<T>(
assertValid: (loc: GeneralPath, value: mixed) => T,
): (loc: GeneralPath, value: mixed) => $ReadOnlyArray<T> | void {
return (loc, value) => {
const arr = assertArray(loc, value);
if (arr) arr.forEach((item, i) => assertValid(access(loc, i), item));
return (arr: any);
};
}
function assertIgnoreItem(loc: GeneralPath, value: mixed): IgnoreItem {

export const assertIgnoreList = createArrayAssertion(function assertIgnoreItem(
loc: GeneralPath,
value: mixed,
): IgnoreItem {
if (
typeof value !== "string" &&
typeof value !== "function" &&
Expand All @@ -240,7 +243,26 @@ function assertIgnoreItem(loc: GeneralPath, value: mixed): IgnoreItem {
);
}
return value;
}
});

export const assertExtensionsList = createArrayAssertion(
function assertExtension(
loc: GeneralPath,
value: mixed,
): FileExtension | "*" {
if (value === "*") return value;

if (typeof value !== "string") {
throw new Error(
`${msg(loc)} must be an array of string values, or undefined`,
);
}
if (!value.startsWith(".")) {
throw new Error(`${msg(loc)} must start with a '.' (dot)`);
}
return (value: any);
},
);

export function assertConfigApplicableTest(
loc: OptionPath,
Expand Down
13 changes: 12 additions & 1 deletion packages/babel-core/src/config/validation/options.js
Expand Up @@ -14,6 +14,7 @@ import {
assertCallerMetadata,
assertInputSourceMap,
assertIgnoreList,
assertExtensionsList,
assertPluginList,
assertConfigApplicableTest,
assertConfigFileSearch,
Expand Down Expand Up @@ -114,6 +115,10 @@ const COMMON_VALIDATORS: ValidatorSet = {
$PropertyType<ValidatedOptions, "exclude">,
>),

extensions: (assertExtensionsList: Validator<
$PropertyType<ValidatedOptions, "extensions">,
>),

retainLines: (assertBoolean: Validator<
$PropertyType<ValidatedOptions, "retainLines">,
>),
Expand Down Expand Up @@ -195,9 +200,12 @@ export type ValidatedOptions = {

extends?: string,
env?: EnvSet<ValidatedOptions>,
overrides?: OverridesList,

// Options to enable/disable processing of some files
ignore?: IgnoreList,
only?: IgnoreList,
overrides?: OverridesList,
extensions?: ExtensionsList,

// Generally verify if a given config object should be applied to the given file.
test?: ConfigApplicableTest,
Expand Down Expand Up @@ -252,6 +260,9 @@ export type EnvSet<T> = {
export type IgnoreItem = string | Function | RegExp;
export type IgnoreList = $ReadOnlyArray<IgnoreItem>;

export opaque type FileExtension = string;
export type ExtensionsList = $ReadOnlyArray<FileExtension | "*">;

export type PluginOptions = {} | void | false;
export type PluginTarget = string | {} | Function;
export type PluginItem =
Expand Down
12 changes: 5 additions & 7 deletions packages/babel-core/src/index.js
@@ -1,5 +1,7 @@
// @flow

import type { ExtensionsList } from "./config/validation/options";

export { default as File } from "./transformation/file/file";
export { default as buildExternalHelpers } from "./tools/build-external-helpers";
export { resolvePlugin, resolvePreset } from "./config/files";
Expand Down Expand Up @@ -41,13 +43,9 @@ export { parse, parseSync, parseAsync } from "./parse";
* Recommended set of compilable extensions. Not used in @babel/core directly, but meant as
* as an easy source for tooling making use of @babel/core.
*/
export const DEFAULT_EXTENSIONS = Object.freeze([
".js",
".jsx",
".es6",
".es",
".mjs",
]);
export const DEFAULT_EXTENSIONS: ExtensionsList = Object.freeze(
([".js", ".jsx", ".es6", ".es", ".mjs"]: any),
);

// For easier backward-compatibility, provide an API like the one we exposed in Babel 6.
import { loadOptions } from "./config";
Expand Down