Skip to content

Commit 7d63d2f

Browse files
Allow plugins/presets to indicate external dependencies (#14065)
Co-authored-by: Vedant Roy <[email protected]>
1 parent abba9f2 commit 7d63d2f

30 files changed

Lines changed: 797 additions & 140 deletions

File tree

packages/babel-cli/src/babel/dir.ts

Lines changed: 34 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from "path";
33
import fs from "fs";
44

55
import * as util from "./util";
6+
import * as watcher from "./watcher";
67
import type { CmdOptions } from "./options";
78

89
const FILE_TYPE = Object.freeze({
@@ -21,8 +22,6 @@ export default async function ({
2122
cliOptions,
2223
babelOptions,
2324
}: CmdOptions): Promise<void> {
24-
const filenames = cliOptions.filenames;
25-
2625
async function write(
2726
src: string,
2827
base: string,
@@ -66,7 +65,7 @@ export default async function ({
6665
util.chmod(src, dest);
6766

6867
if (cliOptions.verbose) {
69-
console.log(src + " -> " + dest);
68+
console.log(path.relative(process.cwd(), src) + " -> " + dest);
7069
}
7170

7271
return FILE_TYPE.COMPILED;
@@ -150,6 +149,8 @@ export default async function ({
150149
startTime = null;
151150
}, 100);
152151

152+
if (cliOptions.watch) watcher.enable({ enableGlobbing: true });
153+
153154
if (!cliOptions.skipInitialBuild) {
154155
if (cliOptions.deleteDirOnStart) {
155156
util.deleteDir(cliOptions.outDir);
@@ -173,43 +174,36 @@ export default async function ({
173174
}
174175

175176
if (cliOptions.watch) {
176-
const chokidar = util.requireChokidar();
177-
178-
filenames.forEach(function (filenameOrDir: string): void {
179-
const watcher = chokidar.watch(filenameOrDir, {
180-
persistent: true,
181-
ignoreInitial: true,
182-
awaitWriteFinish: {
183-
stabilityThreshold: 50,
184-
pollInterval: 10,
185-
},
186-
});
187-
188-
// This, alongside with debounce, allows us to only log
189-
// when we are sure that all the files have been compiled.
190-
let processing = 0;
191-
192-
["add", "change"].forEach(function (type: string): void {
193-
watcher.on(type, async function (filename: string) {
194-
processing++;
195-
if (startTime === null) startTime = process.hrtime();
196-
197-
try {
198-
await handleFile(
199-
filename,
200-
filename === filenameOrDir
201-
? path.dirname(filenameOrDir)
202-
: filenameOrDir,
203-
);
204-
205-
compiledFiles++;
206-
} catch (err) {
207-
console.error(err);
208-
}
209-
210-
processing--;
211-
if (processing === 0 && !cliOptions.quiet) logSuccess();
212-
});
177+
// This, alongside with debounce, allows us to only log
178+
// when we are sure that all the files have been compiled.
179+
let processing = 0;
180+
181+
cliOptions.filenames.forEach(filenameOrDir => {
182+
watcher.watch(filenameOrDir);
183+
184+
watcher.onFilesChange(async filenames => {
185+
processing++;
186+
if (startTime === null) startTime = process.hrtime();
187+
188+
try {
189+
const written = await Promise.all(
190+
filenames.map(filename =>
191+
handleFile(
192+
filename,
193+
filename === filenameOrDir
194+
? path.dirname(filenameOrDir)
195+
: filenameOrDir,
196+
),
197+
),
198+
);
199+
200+
compiledFiles += written.filter(Boolean).length;
201+
} catch (err) {
202+
console.error(err);
203+
}
204+
205+
processing--;
206+
if (processing === 0 && !cliOptions.quiet) logSuccess();
213207
});
214208
});
215209
}

packages/babel-cli/src/babel/file.ts

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import fs from "fs";
66

77
import * as util from "./util";
88
import type { CmdOptions } from "./options";
9+
import * as watcher from "./watcher";
910

1011
type CompilationOutput = {
1112
code: string;
@@ -123,7 +124,7 @@ export default async function ({
123124
async function stdin(): Promise<void> {
124125
const code = await readStdin();
125126

126-
const res = await util.transform(cliOptions.filename, code, {
127+
const res = await util.transformRepl(cliOptions.filename, code, {
127128
...babelOptions,
128129
sourceFileName: "stdin",
129130
});
@@ -193,40 +194,33 @@ export default async function ({
193194
}
194195

195196
async function files(filenames: Array<string>): Promise<void> {
197+
if (cliOptions.watch) {
198+
watcher.enable({ enableGlobbing: false });
199+
}
200+
196201
if (!cliOptions.skipInitialBuild) {
197202
await walk(filenames);
198203
}
199204

200205
if (cliOptions.watch) {
201-
const chokidar = util.requireChokidar();
202-
chokidar
203-
.watch(filenames, {
204-
disableGlobbing: true,
205-
persistent: true,
206-
ignoreInitial: true,
207-
awaitWriteFinish: {
208-
stabilityThreshold: 50,
209-
pollInterval: 10,
210-
},
211-
})
212-
.on("all", function (type: string, filename: string): void {
213-
if (
214-
!util.isCompilableExtension(filename, cliOptions.extensions) &&
215-
!filenames.includes(filename)
216-
) {
217-
return;
218-
}
219-
220-
if (type === "add" || type === "change") {
221-
if (cliOptions.verbose) {
222-
console.log(type + " " + filename);
223-
}
206+
filenames.forEach(watcher.watch);
207+
208+
watcher.onFilesChange((changes, event, cause) => {
209+
const actionableChange = changes.some(
210+
filename =>
211+
util.isCompilableExtension(filename, cliOptions.extensions) ||
212+
filenames.includes(filename),
213+
);
214+
if (!actionableChange) return;
215+
216+
if (cliOptions.verbose) {
217+
console.log(`${event} ${cause}`);
218+
}
224219

225-
walk(filenames).catch(err => {
226-
console.error(err);
227-
});
228-
}
220+
walk(filenames).catch(err => {
221+
console.error(err);
229222
});
223+
});
230224
}
231225
}
232226

packages/babel-cli/src/babel/util.ts

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import readdirRecursive from "fs-readdir-recursive";
22
import * as babel from "@babel/core";
33
import path from "path";
44
import fs from "fs";
5-
import { createRequire } from "module";
5+
6+
import * as watcher from "./watcher";
67

78
export function chmod(src: string, dest: string): void {
89
try {
@@ -60,7 +61,7 @@ const CALLER = {
6061
name: "@babel/cli",
6162
};
6263

63-
export function transform(
64+
export function transformRepl(
6465
filename: string,
6566
code: string,
6667
opts: any,
@@ -79,18 +80,31 @@ export function transform(
7980
});
8081
}
8182

82-
export function compile(filename: string, opts: any | Function): Promise<any> {
83+
export async function compile(
84+
filename: string,
85+
opts: any | Function,
86+
): Promise<any> {
8387
opts = {
8488
...opts,
8589
caller: CALLER,
8690
};
8791

88-
return new Promise((resolve, reject) => {
92+
// TODO (Babel 8): Use `babel.transformFileAsync`
93+
const result: any = await new Promise((resolve, reject) => {
8994
babel.transformFile(filename, opts, (err, result) => {
9095
if (err) reject(err);
9196
else resolve(result);
9297
});
9398
});
99+
100+
if (result) {
101+
if (!process.env.BABEL_8_BREAKING) {
102+
if (!result.externalDependencies) return result;
103+
}
104+
watcher.updateExternalDependencies(filename, result.externalDependencies);
105+
}
106+
107+
return result;
94108
}
95109

96110
export function deleteDir(path: string): void {
@@ -114,24 +128,6 @@ process.on("uncaughtException", function (err) {
114128
process.exitCode = 1;
115129
});
116130

117-
export function requireChokidar(): any {
118-
const require = createRequire(import.meta.url);
119-
120-
try {
121-
return process.env.BABEL_8_BREAKING
122-
? require("chokidar")
123-
: parseInt(process.versions.node) >= 8
124-
? require("chokidar")
125-
: require("@nicolo-ribaudo/chokidar-2");
126-
} catch (err) {
127-
console.error(
128-
"The optional dependency chokidar failed to install and is required for " +
129-
"--watch. Chokidar is likely not supported on your platform.",
130-
);
131-
throw err;
132-
}
133-
}
134-
135131
export function withExtension(filename: string, ext: string = ".js") {
136132
const newBasename = path.basename(filename, path.extname(filename)) + ext;
137133
return path.join(path.dirname(filename), newBasename);
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { createRequire } from "module";
2+
import path from "path";
3+
4+
const fileToDeps = new Map<string, Set<string>>();
5+
const depToFiles = new Map<string, Set<string>>();
6+
7+
let isWatchMode = false;
8+
let watcher;
9+
10+
export function enable({ enableGlobbing }: { enableGlobbing: boolean }) {
11+
isWatchMode = true;
12+
13+
const { FSWatcher } = requireChokidar();
14+
15+
watcher = new FSWatcher({
16+
disableGlobbing: !enableGlobbing,
17+
persistent: true,
18+
ignoreInitial: true,
19+
awaitWriteFinish: {
20+
stabilityThreshold: 50,
21+
pollInterval: 10,
22+
},
23+
});
24+
25+
watcher.on("unlink", unwatchFile);
26+
}
27+
28+
export function watch(filename: string): void {
29+
if (!isWatchMode) {
30+
throw new Error(
31+
"Internal Babel error: .watch called when not in watch mode.",
32+
);
33+
}
34+
35+
watcher.add(path.resolve(filename));
36+
}
37+
38+
/**
39+
* Call @param callback whenever a dependency (source file)/
40+
* external dependency (non-source file) changes.
41+
*
42+
* Handles mapping external dependencies to their corresponding
43+
* dependencies.
44+
*/
45+
export function onFilesChange(
46+
callback: (filenames: string[], event: string, cause: string) => void,
47+
): void {
48+
if (!isWatchMode) {
49+
throw new Error(
50+
"Internal Babel error: .onFilesChange called when not in watch mode.",
51+
);
52+
}
53+
54+
watcher.on("all", (event, filename) => {
55+
if (event !== "change" && event !== "add") return;
56+
57+
const absoluteFile = path.resolve(filename);
58+
callback(
59+
[absoluteFile, ...(depToFiles.get(absoluteFile) ?? [])],
60+
event,
61+
absoluteFile,
62+
);
63+
});
64+
}
65+
66+
export function updateExternalDependencies(
67+
filename: string,
68+
dependencies: Set<string>,
69+
) {
70+
if (!isWatchMode) return;
71+
72+
// Use absolute paths
73+
const absFilename = path.resolve(filename);
74+
const absDependencies = new Set(
75+
Array.from(dependencies, dep => path.resolve(dep)),
76+
);
77+
78+
if (fileToDeps.has(absFilename)) {
79+
for (const dep of fileToDeps.get(absFilename)) {
80+
if (!absDependencies.has(dep)) {
81+
removeFileDependency(absFilename, dep);
82+
}
83+
}
84+
}
85+
for (const dep of absDependencies) {
86+
if (!depToFiles.has(dep)) {
87+
depToFiles.set(dep, new Set());
88+
89+
watcher.add(dep);
90+
}
91+
depToFiles.get(dep).add(absFilename);
92+
}
93+
94+
fileToDeps.set(absFilename, absDependencies);
95+
}
96+
97+
function removeFileDependency(filename: string, dep: string) {
98+
depToFiles.get(dep).delete(filename);
99+
100+
if (depToFiles.get(dep).size === 0) {
101+
depToFiles.delete(dep);
102+
103+
watcher.unwatch(dep);
104+
}
105+
}
106+
107+
function unwatchFile(filename: string) {
108+
if (!fileToDeps.has(filename)) return;
109+
110+
for (const dep of fileToDeps.get(filename)) {
111+
removeFileDependency(filename, dep);
112+
}
113+
fileToDeps.delete(filename);
114+
}
115+
116+
function requireChokidar(): any {
117+
// @ts-expect-error - TS is not configured to support import.meta.
118+
const require = createRequire(import.meta.url);
119+
120+
try {
121+
return process.env.BABEL_8_BREAKING
122+
? require("chokidar")
123+
: parseInt(process.versions.node) >= 8
124+
? require("chokidar")
125+
: require("@nicolo-ribaudo/chokidar-2");
126+
} catch (err) {
127+
console.error(
128+
"The optional dependency chokidar failed to install and is required for " +
129+
"--watch. Chokidar is likely not supported on your platform.",
130+
);
131+
throw err;
132+
}
133+
}

0 commit comments

Comments
 (0)