Skip to content

Commit

Permalink
feat: Support .cts as configuration file (#15283)
Browse files Browse the repository at this point in the history
Co-authored-by: Nicol貌 Ribaudo <nicolo.ribaudo@gmail.com>
  • Loading branch information
liuxingbaoyu and nicolo-ribaudo committed Feb 18, 2023
1 parent bca362a commit 43dce19
Show file tree
Hide file tree
Showing 12 changed files with 408 additions and 54 deletions.
47 changes: 40 additions & 7 deletions babel.config.js
Expand Up @@ -464,17 +464,30 @@ function pluginPolyfillsOldNode({ template, types: t }) {
},
};
}

/**
* @param {import("@babel/core")} pluginAPI
* @returns {import("@babel/core").PluginObj}
*/
function pluginToggleBooleanFlag({ types: t }, { name, value }) {
function check(test) {
let keepConsequent = value;

if (test.isUnaryExpression({ operator: "!" })) {
test = test.get("argument");
keepConsequent = !keepConsequent;
}
return {
test,
keepConsequent,
};
}

return {
visitor: {
"IfStatement|ConditionalExpression"(path) {
let test = path.get("test");
let keepConsequent = value;

if (test.isUnaryExpression({ operator: "!" })) {
test = test.get("argument");
keepConsequent = !keepConsequent;
}
// eslint-disable-next-line prefer-const
let { test, keepConsequent } = check(path.get("test"));

// yarn-plugin-conditions injects bool(process.env.BABEL_8_BREAKING)
// tests, to properly cast the env variable to a boolean.
Expand All @@ -494,6 +507,26 @@ function pluginToggleBooleanFlag({ types: t }, { name, value }) {
: path.node.alternate || t.emptyStatement()
);
},
LogicalExpression(path) {
const { test, keepConsequent } = check(path.get("left"));

if (!test.matchesPattern(name)) return;

switch (path.node.operator) {
case "&&":
path.replaceWith(
keepConsequent ? path.node.right : t.booleanLiteral(false)
);
break;
case "||":
path.replaceWith(
keepConsequent ? t.booleanLiteral(true) : path.node.right
);
break;
default:
throw path.buildCodeFrameError("This check could not be stripped.");
}
},
MemberExpression(path) {
if (path.matchesPattern(name)) {
throw path.buildCodeFrameError("This check could not be stripped.");
Expand Down
3 changes: 2 additions & 1 deletion packages/babel-core/package.json
Expand Up @@ -74,7 +74,8 @@
"@types/gensync": "^1.0.0",
"@types/resolve": "^1.3.2",
"@types/semver": "^5.4.0",
"rimraf": "^3.0.0"
"rimraf": "^3.0.0",
"ts-node": "^10.9.1"
},
"conditions": {
"BABEL_8_BREAKING": [
Expand Down
20 changes: 14 additions & 6 deletions packages/babel-core/src/config/files/configuration.ts
Expand Up @@ -9,7 +9,7 @@ import type { CacheConfigurator } from "../caching";
import { makeConfigAPI } from "../helpers/config-api";
import type { ConfigAPI } from "../helpers/config-api";
import { makeStaticFileCache } from "./utils";
import loadCjsOrMjsDefault from "./module-types";
import loadCodeDefault from "./module-types";
import pathPatternToRegex from "../pattern-to-regex";
import type { FilePackageData, RelativeConfig, ConfigFile } from "./types";
import type { CallerMetadata } from "../validation/options";
Expand All @@ -28,20 +28,22 @@ export const ROOT_CONFIG_FILENAMES = [
"babel.config.cjs",
"babel.config.mjs",
"babel.config.json",
"babel.config.cts",
];
const RELATIVE_CONFIG_FILENAMES = [
".babelrc",
".babelrc.js",
".babelrc.cjs",
".babelrc.mjs",
".babelrc.json",
".babelrc.cts",
];

const BABELIGNORE_FILENAME = ".babelignore";

const LOADING_CONFIGS = new Set();

const readConfigJS = makeStrongCache(function* readConfigJS(
const readConfigCode = makeStrongCache(function* readConfigCode(
filepath: string,
cache: CacheConfigurator<{
envName: string;
Expand Down Expand Up @@ -70,7 +72,7 @@ const readConfigJS = makeStrongCache(function* readConfigJS(
let options: unknown;
try {
LOADING_CONFIGS.add(filepath);
options = yield* loadCjsOrMjsDefault(
options = yield* loadCodeDefault(
filepath,
"You appear to be using a native ECMAScript module configuration " +
"file, which is only supported when running Babel asynchronously.",
Expand Down Expand Up @@ -313,9 +315,15 @@ function readConfig(
caller: CallerMetadata | undefined,
): Handler<ConfigFile | null> {
const ext = path.extname(filepath);
return ext === ".js" || ext === ".cjs" || ext === ".mjs"
? readConfigJS(filepath, { envName, caller })
: readConfigJSON5(filepath);
switch (ext) {
case ".js":
case ".cjs":
case ".mjs":
case ".cts":
return readConfigCode(filepath, { envName, caller });
default:
return readConfigJSON5(filepath);
}
}

export function* resolveShowConfigPath(
Expand Down
120 changes: 100 additions & 20 deletions packages/babel-core/src/config/files/module-types.ts
Expand Up @@ -8,6 +8,9 @@ import semver from "semver";
import { endHiddenCallStack } from "../../errors/rewrite-stack-trace";
import ConfigError from "../../errors/config-error";

import type { InputOptions } from "..";
import { transformFileSync } from "../../transform-file";

const require = createRequire(import.meta.url);

let import_: ((specifier: string | URL) => any) | undefined;
Expand All @@ -23,38 +26,87 @@ export const supportsESM = semver.satisfies(
"^12.17 || >=13.2",
);

export default function* loadCjsOrMjsDefault(
export default function* loadCodeDefault(
filepath: string,
asyncError: string,
// TODO(Babel 8): Remove this
fallbackToTranspiledModule: boolean = false,
): Handler<unknown> {
switch (guessJSModuleType(filepath)) {
case "cjs":
switch (path.extname(filepath)) {
case ".cjs":
return loadCjsDefault(filepath, fallbackToTranspiledModule);
case "unknown":
case ".mjs":
break;
case ".cts":
return loadCtsDefault(filepath);
default:
try {
return loadCjsDefault(filepath, fallbackToTranspiledModule);
} catch (e) {
if (e.code !== "ERR_REQUIRE_ESM") throw e;
}
// fall through
case "mjs":
if (yield* isAsync()) {
return yield* waitFor(loadMjsDefault(filepath));
}
throw new ConfigError(asyncError, filepath);
}
if (yield* isAsync()) {
return yield* waitFor(loadMjsDefault(filepath));
}
throw new ConfigError(asyncError, filepath);
}

function guessJSModuleType(filename: string): "cjs" | "mjs" | "unknown" {
switch (path.extname(filename)) {
case ".cjs":
return "cjs";
case ".mjs":
return "mjs";
default:
return "unknown";
function loadCtsDefault(filepath: string) {
const ext = ".cts";
const hasTsSupport = !!(
require.extensions[".ts"] ||
require.extensions[".cts"] ||
require.extensions[".mts"]
);

let handler: NodeJS.RequireExtensions[""];

if (!hasTsSupport) {
const opts: InputOptions = {
babelrc: false,
configFile: false,
sourceType: "script",
sourceMaps: "inline",
presets: [
[
getTSPreset(filepath),
{
disallowAmbiguousJSXLike: true,
allExtensions: true,
onlyRemoveTypeImports: true,
optimizeConstEnums: true,
...(!process.env.BABEL_8_BREAKING && {
allowDeclareFields: true,
}),
},
],
],
};

handler = function (m, filename) {
// If we want to support `.ts`, `.d.ts` must be handled specially.
if (handler && filename.endsWith(ext)) {
// @ts-expect-error Undocumented API
return m._compile(
transformFileSync(filename, {
...opts,
filename,
}).code,
filename,
);
}
return require.extensions[".js"](m, filename);
};
require.extensions[ext] = handler;
}
try {
return endHiddenCallStack(require)(filepath);
} finally {
if (!hasTsSupport) {
if (require.extensions[ext] === handler) delete require.extensions[ext];
handler = undefined;
}
}
}

Expand All @@ -69,8 +121,7 @@ function loadCjsDefault(filepath: string, fallbackToTranspiledModule: boolean) {
async function loadMjsDefault(filepath: string) {
if (!import_) {
throw new ConfigError(
"Internal error: Native ECMAScript modules aren't supported" +
" by this platform.\n",
"Internal error: Native ECMAScript modules aren't supported by this platform.\n",
filepath,
);
}
Expand All @@ -80,3 +131,32 @@ async function loadMjsDefault(filepath: string) {
const module = await endHiddenCallStack(import_)(pathToFileURL(filepath));
return module.default;
}

function getTSPreset(filepath: string) {
try {
// eslint-disable-next-line import/no-extraneous-dependencies
return require("@babel/preset-typescript");
} catch (error) {
if (error.code !== "MODULE_NOT_FOUND") throw error;

let message =
"You appear to be using a .cts file as Babel configuration, but the `@babel/preset-typescript` package was not found: please install it!";

if (process.versions.pnp) {
// Using Yarn PnP, which doesn't allow requiring packages that are not
// explicitly specified as dependencies.
// TODO(Babel 8): Explicitly add `@babel/preset-typescript` as an
// optional peer dependency of `@babel/core`.
message += `
If you are using Yarn Plug'n'Play, you may also need to add the following configuration to your .yarnrc.yml file:
packageExtensions:
\t"@babel/core@*":
\t\tpeerDependencies:
\t\t\t"@babel/preset-typescript": "*"
`;
}

throw new ConfigError(message, filepath);
}
}
4 changes: 2 additions & 2 deletions packages/babel-core/src/config/files/plugins.ts
Expand Up @@ -6,7 +6,7 @@ import buildDebug from "debug";
import path from "path";
import gensync, { type Handler } from "gensync";
import { isAsync } from "../../gensync-utils/async";
import loadCjsOrMjsDefault, { supportsESM } from "./module-types";
import loadCodeDefault, { supportsESM } from "./module-types";
import { fileURLToPath, pathToFileURL } from "url";

import importMetaResolve from "./import-meta-resolve";
Expand Down Expand Up @@ -217,7 +217,7 @@ function* requireModule(type: string, name: string): Handler<unknown> {
if (!process.env.BABEL_8_BREAKING) {
LOADING_MODULES.add(name);
}
return yield* loadCjsOrMjsDefault(
return yield* loadCodeDefault(
name,
`You appear to be using a native ECMAScript module ${type}, ` +
"which is only supported when running Babel asynchronously.",
Expand Down
4 changes: 2 additions & 2 deletions packages/babel-core/src/errors/config-error.ts
@@ -1,9 +1,9 @@
import { injcectVirtualStackFrame, expectedError } from "./rewrite-stack-trace";
import { injectVirtualStackFrame, expectedError } from "./rewrite-stack-trace";

export default class ConfigError extends Error {
constructor(message: string, filename?: string) {
super(message);
expectedError(this);
if (filename) injcectVirtualStackFrame(this, filename);
if (filename) injectVirtualStackFrame(this, filename);
}
}
22 changes: 11 additions & 11 deletions packages/babel-core/src/errors/rewrite-stack-trace.ts
@@ -1,5 +1,5 @@
/**
* This file uses the iternal V8 Stack Trace API (https://v8.dev/docs/stack-trace-api)
* This file uses the internal V8 Stack Trace API (https://v8.dev/docs/stack-trace-api)
* to provide utilities to rewrite the stack trace.
* When this API is not present, all the functions in this file become noops.
*
Expand Down Expand Up @@ -33,10 +33,10 @@
* - If e() throws an error, then its shown call stack will be "e, f"
*
* Additionally, an error can inject additional "virtual" stack frames using the
* injcectVirtualStackFrame(error, filename) function: those are injected as a
* injectVirtualStackFrame(error, filename) function: those are injected as a
* replacement of the hidden frames.
* In the example above, if we called injcectVirtualStackFrame(err, "h") and
* injcectVirtualStackFrame(err, "i") on the expected error thrown by c(), its
* In the example above, if we called injectVirtualStackFrame(err, "h") and
* injectVirtualStackFrame(err, "i") on the expected error thrown by c(), its
* shown call stack would have been "h, i, e, f".
* This can be useful, for example, to report config validation errors as if they
* were directly thrown in the config file.
Expand All @@ -46,8 +46,8 @@ const ErrorToString = Function.call.bind(Error.prototype.toString);

const SUPPORTED = !!Error.captureStackTrace;

const START_HIDNG = "startHiding - secret - don't use this - v1";
const STOP_HIDNG = "stopHiding - secret - don't use this - v1";
const START_HIDING = "startHiding - secret - don't use this - v1";
const STOP_HIDING = "stopHiding - secret - don't use this - v1";

type CallSite = Parameters<typeof Error.prepareStackTrace>[1][number];

Expand All @@ -70,7 +70,7 @@ function CallSite(filename: string): CallSite {
} as CallSite);
}

export function injcectVirtualStackFrame(error: Error, filename: string) {
export function injectVirtualStackFrame(error: Error, filename: string) {
if (!SUPPORTED) return;

let frames = virtualFrames.get(error);
Expand All @@ -97,7 +97,7 @@ export function beginHiddenCallStack<A extends unknown[], R>(
return fn(...args);
},
"name",
{ value: STOP_HIDNG },
{ value: STOP_HIDING },
);
}

Expand All @@ -111,7 +111,7 @@ export function endHiddenCallStack<A extends unknown[], R>(
return fn(...args);
},
"name",
{ value: START_HIDNG },
{ value: START_HIDING },
);
}

Expand Down Expand Up @@ -144,9 +144,9 @@ function setupPrepareStackTrace() {
: "unknown";
for (let i = 0; i < trace.length; i++) {
const name = trace[i].getFunctionName();
if (name === START_HIDNG) {
if (name === START_HIDING) {
status = "hiding";
} else if (name === STOP_HIDNG) {
} else if (name === STOP_HIDING) {
if (status === "hiding") {
status = "showing";
if (virtualFrames.has(err)) {
Expand Down

0 comments on commit 43dce19

Please sign in to comment.