Skip to content

Commit 43dce19

Browse files
feat: Support .cts as configuration file (#15283)
Co-authored-by: Nicolò Ribaudo <[email protected]>
1 parent bca362a commit 43dce19

12 files changed

Lines changed: 408 additions & 54 deletions

File tree

babel.config.js

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -464,17 +464,30 @@ function pluginPolyfillsOldNode({ template, types: t }) {
464464
},
465465
};
466466
}
467+
468+
/**
469+
* @param {import("@babel/core")} pluginAPI
470+
* @returns {import("@babel/core").PluginObj}
471+
*/
467472
function pluginToggleBooleanFlag({ types: t }, { name, value }) {
473+
function check(test) {
474+
let keepConsequent = value;
475+
476+
if (test.isUnaryExpression({ operator: "!" })) {
477+
test = test.get("argument");
478+
keepConsequent = !keepConsequent;
479+
}
480+
return {
481+
test,
482+
keepConsequent,
483+
};
484+
}
485+
468486
return {
469487
visitor: {
470488
"IfStatement|ConditionalExpression"(path) {
471-
let test = path.get("test");
472-
let keepConsequent = value;
473-
474-
if (test.isUnaryExpression({ operator: "!" })) {
475-
test = test.get("argument");
476-
keepConsequent = !keepConsequent;
477-
}
489+
// eslint-disable-next-line prefer-const
490+
let { test, keepConsequent } = check(path.get("test"));
478491

479492
// yarn-plugin-conditions injects bool(process.env.BABEL_8_BREAKING)
480493
// tests, to properly cast the env variable to a boolean.
@@ -494,6 +507,26 @@ function pluginToggleBooleanFlag({ types: t }, { name, value }) {
494507
: path.node.alternate || t.emptyStatement()
495508
);
496509
},
510+
LogicalExpression(path) {
511+
const { test, keepConsequent } = check(path.get("left"));
512+
513+
if (!test.matchesPattern(name)) return;
514+
515+
switch (path.node.operator) {
516+
case "&&":
517+
path.replaceWith(
518+
keepConsequent ? path.node.right : t.booleanLiteral(false)
519+
);
520+
break;
521+
case "||":
522+
path.replaceWith(
523+
keepConsequent ? t.booleanLiteral(true) : path.node.right
524+
);
525+
break;
526+
default:
527+
throw path.buildCodeFrameError("This check could not be stripped.");
528+
}
529+
},
497530
MemberExpression(path) {
498531
if (path.matchesPattern(name)) {
499532
throw path.buildCodeFrameError("This check could not be stripped.");

packages/babel-core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@
7474
"@types/gensync": "^1.0.0",
7575
"@types/resolve": "^1.3.2",
7676
"@types/semver": "^5.4.0",
77-
"rimraf": "^3.0.0"
77+
"rimraf": "^3.0.0",
78+
"ts-node": "^10.9.1"
7879
},
7980
"conditions": {
8081
"BABEL_8_BREAKING": [

packages/babel-core/src/config/files/configuration.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { CacheConfigurator } from "../caching";
99
import { makeConfigAPI } from "../helpers/config-api";
1010
import type { ConfigAPI } from "../helpers/config-api";
1111
import { makeStaticFileCache } from "./utils";
12-
import loadCjsOrMjsDefault from "./module-types";
12+
import loadCodeDefault from "./module-types";
1313
import pathPatternToRegex from "../pattern-to-regex";
1414
import type { FilePackageData, RelativeConfig, ConfigFile } from "./types";
1515
import type { CallerMetadata } from "../validation/options";
@@ -28,20 +28,22 @@ export const ROOT_CONFIG_FILENAMES = [
2828
"babel.config.cjs",
2929
"babel.config.mjs",
3030
"babel.config.json",
31+
"babel.config.cts",
3132
];
3233
const RELATIVE_CONFIG_FILENAMES = [
3334
".babelrc",
3435
".babelrc.js",
3536
".babelrc.cjs",
3637
".babelrc.mjs",
3738
".babelrc.json",
39+
".babelrc.cts",
3840
];
3941

4042
const BABELIGNORE_FILENAME = ".babelignore";
4143

4244
const LOADING_CONFIGS = new Set();
4345

44-
const readConfigJS = makeStrongCache(function* readConfigJS(
46+
const readConfigCode = makeStrongCache(function* readConfigCode(
4547
filepath: string,
4648
cache: CacheConfigurator<{
4749
envName: string;
@@ -70,7 +72,7 @@ const readConfigJS = makeStrongCache(function* readConfigJS(
7072
let options: unknown;
7173
try {
7274
LOADING_CONFIGS.add(filepath);
73-
options = yield* loadCjsOrMjsDefault(
75+
options = yield* loadCodeDefault(
7476
filepath,
7577
"You appear to be using a native ECMAScript module configuration " +
7678
"file, which is only supported when running Babel asynchronously.",
@@ -313,9 +315,15 @@ function readConfig(
313315
caller: CallerMetadata | undefined,
314316
): Handler<ConfigFile | null> {
315317
const ext = path.extname(filepath);
316-
return ext === ".js" || ext === ".cjs" || ext === ".mjs"
317-
? readConfigJS(filepath, { envName, caller })
318-
: readConfigJSON5(filepath);
318+
switch (ext) {
319+
case ".js":
320+
case ".cjs":
321+
case ".mjs":
322+
case ".cts":
323+
return readConfigCode(filepath, { envName, caller });
324+
default:
325+
return readConfigJSON5(filepath);
326+
}
319327
}
320328

321329
export function* resolveShowConfigPath(

packages/babel-core/src/config/files/module-types.ts

Lines changed: 100 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import semver from "semver";
88
import { endHiddenCallStack } from "../../errors/rewrite-stack-trace";
99
import ConfigError from "../../errors/config-error";
1010

11+
import type { InputOptions } from "..";
12+
import { transformFileSync } from "../../transform-file";
13+
1114
const require = createRequire(import.meta.url);
1215

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

26-
export default function* loadCjsOrMjsDefault(
29+
export default function* loadCodeDefault(
2730
filepath: string,
2831
asyncError: string,
2932
// TODO(Babel 8): Remove this
3033
fallbackToTranspiledModule: boolean = false,
3134
): Handler<unknown> {
32-
switch (guessJSModuleType(filepath)) {
33-
case "cjs":
35+
switch (path.extname(filepath)) {
36+
case ".cjs":
3437
return loadCjsDefault(filepath, fallbackToTranspiledModule);
35-
case "unknown":
38+
case ".mjs":
39+
break;
40+
case ".cts":
41+
return loadCtsDefault(filepath);
42+
default:
3643
try {
3744
return loadCjsDefault(filepath, fallbackToTranspiledModule);
3845
} catch (e) {
3946
if (e.code !== "ERR_REQUIRE_ESM") throw e;
4047
}
41-
// fall through
42-
case "mjs":
43-
if (yield* isAsync()) {
44-
return yield* waitFor(loadMjsDefault(filepath));
45-
}
46-
throw new ConfigError(asyncError, filepath);
4748
}
49+
if (yield* isAsync()) {
50+
return yield* waitFor(loadMjsDefault(filepath));
51+
}
52+
throw new ConfigError(asyncError, filepath);
4853
}
4954

50-
function guessJSModuleType(filename: string): "cjs" | "mjs" | "unknown" {
51-
switch (path.extname(filename)) {
52-
case ".cjs":
53-
return "cjs";
54-
case ".mjs":
55-
return "mjs";
56-
default:
57-
return "unknown";
55+
function loadCtsDefault(filepath: string) {
56+
const ext = ".cts";
57+
const hasTsSupport = !!(
58+
require.extensions[".ts"] ||
59+
require.extensions[".cts"] ||
60+
require.extensions[".mts"]
61+
);
62+
63+
let handler: NodeJS.RequireExtensions[""];
64+
65+
if (!hasTsSupport) {
66+
const opts: InputOptions = {
67+
babelrc: false,
68+
configFile: false,
69+
sourceType: "script",
70+
sourceMaps: "inline",
71+
presets: [
72+
[
73+
getTSPreset(filepath),
74+
{
75+
disallowAmbiguousJSXLike: true,
76+
allExtensions: true,
77+
onlyRemoveTypeImports: true,
78+
optimizeConstEnums: true,
79+
...(!process.env.BABEL_8_BREAKING && {
80+
allowDeclareFields: true,
81+
}),
82+
},
83+
],
84+
],
85+
};
86+
87+
handler = function (m, filename) {
88+
// If we want to support `.ts`, `.d.ts` must be handled specially.
89+
if (handler && filename.endsWith(ext)) {
90+
// @ts-expect-error Undocumented API
91+
return m._compile(
92+
transformFileSync(filename, {
93+
...opts,
94+
filename,
95+
}).code,
96+
filename,
97+
);
98+
}
99+
return require.extensions[".js"](m, filename);
100+
};
101+
require.extensions[ext] = handler;
102+
}
103+
try {
104+
return endHiddenCallStack(require)(filepath);
105+
} finally {
106+
if (!hasTsSupport) {
107+
if (require.extensions[ext] === handler) delete require.extensions[ext];
108+
handler = undefined;
109+
}
58110
}
59111
}
60112

@@ -69,8 +121,7 @@ function loadCjsDefault(filepath: string, fallbackToTranspiledModule: boolean) {
69121
async function loadMjsDefault(filepath: string) {
70122
if (!import_) {
71123
throw new ConfigError(
72-
"Internal error: Native ECMAScript modules aren't supported" +
73-
" by this platform.\n",
124+
"Internal error: Native ECMAScript modules aren't supported by this platform.\n",
74125
filepath,
75126
);
76127
}
@@ -80,3 +131,32 @@ async function loadMjsDefault(filepath: string) {
80131
const module = await endHiddenCallStack(import_)(pathToFileURL(filepath));
81132
return module.default;
82133
}
134+
135+
function getTSPreset(filepath: string) {
136+
try {
137+
// eslint-disable-next-line import/no-extraneous-dependencies
138+
return require("@babel/preset-typescript");
139+
} catch (error) {
140+
if (error.code !== "MODULE_NOT_FOUND") throw error;
141+
142+
let message =
143+
"You appear to be using a .cts file as Babel configuration, but the `@babel/preset-typescript` package was not found: please install it!";
144+
145+
if (process.versions.pnp) {
146+
// Using Yarn PnP, which doesn't allow requiring packages that are not
147+
// explicitly specified as dependencies.
148+
// TODO(Babel 8): Explicitly add `@babel/preset-typescript` as an
149+
// optional peer dependency of `@babel/core`.
150+
message += `
151+
If you are using Yarn Plug'n'Play, you may also need to add the following configuration to your .yarnrc.yml file:
152+
153+
packageExtensions:
154+
\t"@babel/core@*":
155+
\t\tpeerDependencies:
156+
\t\t\t"@babel/preset-typescript": "*"
157+
`;
158+
}
159+
160+
throw new ConfigError(message, filepath);
161+
}
162+
}

packages/babel-core/src/config/files/plugins.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import buildDebug from "debug";
66
import path from "path";
77
import gensync, { type Handler } from "gensync";
88
import { isAsync } from "../../gensync-utils/async";
9-
import loadCjsOrMjsDefault, { supportsESM } from "./module-types";
9+
import loadCodeDefault, { supportsESM } from "./module-types";
1010
import { fileURLToPath, pathToFileURL } from "url";
1111

1212
import importMetaResolve from "./import-meta-resolve";
@@ -217,7 +217,7 @@ function* requireModule(type: string, name: string): Handler<unknown> {
217217
if (!process.env.BABEL_8_BREAKING) {
218218
LOADING_MODULES.add(name);
219219
}
220-
return yield* loadCjsOrMjsDefault(
220+
return yield* loadCodeDefault(
221221
name,
222222
`You appear to be using a native ECMAScript module ${type}, ` +
223223
"which is only supported when running Babel asynchronously.",
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { injcectVirtualStackFrame, expectedError } from "./rewrite-stack-trace";
1+
import { injectVirtualStackFrame, expectedError } from "./rewrite-stack-trace";
22

33
export default class ConfigError extends Error {
44
constructor(message: string, filename?: string) {
55
super(message);
66
expectedError(this);
7-
if (filename) injcectVirtualStackFrame(this, filename);
7+
if (filename) injectVirtualStackFrame(this, filename);
88
}
99
}

packages/babel-core/src/errors/rewrite-stack-trace.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* This file uses the iternal V8 Stack Trace API (https://v8.dev/docs/stack-trace-api)
2+
* This file uses the internal V8 Stack Trace API (https://v8.dev/docs/stack-trace-api)
33
* to provide utilities to rewrite the stack trace.
44
* When this API is not present, all the functions in this file become noops.
55
*
@@ -33,10 +33,10 @@
3333
* - If e() throws an error, then its shown call stack will be "e, f"
3434
*
3535
* Additionally, an error can inject additional "virtual" stack frames using the
36-
* injcectVirtualStackFrame(error, filename) function: those are injected as a
36+
* injectVirtualStackFrame(error, filename) function: those are injected as a
3737
* replacement of the hidden frames.
38-
* In the example above, if we called injcectVirtualStackFrame(err, "h") and
39-
* injcectVirtualStackFrame(err, "i") on the expected error thrown by c(), its
38+
* In the example above, if we called injectVirtualStackFrame(err, "h") and
39+
* injectVirtualStackFrame(err, "i") on the expected error thrown by c(), its
4040
* shown call stack would have been "h, i, e, f".
4141
* This can be useful, for example, to report config validation errors as if they
4242
* were directly thrown in the config file.
@@ -46,8 +46,8 @@ const ErrorToString = Function.call.bind(Error.prototype.toString);
4646

4747
const SUPPORTED = !!Error.captureStackTrace;
4848

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

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

@@ -70,7 +70,7 @@ function CallSite(filename: string): CallSite {
7070
} as CallSite);
7171
}
7272

73-
export function injcectVirtualStackFrame(error: Error, filename: string) {
73+
export function injectVirtualStackFrame(error: Error, filename: string) {
7474
if (!SUPPORTED) return;
7575

7676
let frames = virtualFrames.get(error);
@@ -97,7 +97,7 @@ export function beginHiddenCallStack<A extends unknown[], R>(
9797
return fn(...args);
9898
},
9999
"name",
100-
{ value: STOP_HIDNG },
100+
{ value: STOP_HIDING },
101101
);
102102
}
103103

@@ -111,7 +111,7 @@ export function endHiddenCallStack<A extends unknown[], R>(
111111
return fn(...args);
112112
},
113113
"name",
114-
{ value: START_HIDNG },
114+
{ value: START_HIDING },
115115
);
116116
}
117117

@@ -144,9 +144,9 @@ function setupPrepareStackTrace() {
144144
: "unknown";
145145
for (let i = 0; i < trace.length; i++) {
146146
const name = trace[i].getFunctionName();
147-
if (name === START_HIDNG) {
147+
if (name === START_HIDING) {
148148
status = "hiding";
149-
} else if (name === STOP_HIDNG) {
149+
} else if (name === STOP_HIDING) {
150150
if (status === "hiding") {
151151
status = "showing";
152152
if (virtualFrames.has(err)) {

0 commit comments

Comments
 (0)