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

Allow plugins/presets to indicate external dependencies #14065

Merged
merged 13 commits into from Feb 2, 2022
74 changes: 34 additions & 40 deletions packages/babel-cli/src/babel/dir.ts
Expand Up @@ -3,6 +3,7 @@ import path from "path";
import fs from "fs";

import * as util from "./util";
import * as watcher from "./watcher";
import type { CmdOptions } from "./options";

const FILE_TYPE = Object.freeze({
Expand All @@ -21,8 +22,6 @@ export default async function ({
cliOptions,
babelOptions,
}: CmdOptions): Promise<void> {
const filenames = cliOptions.filenames;

async function write(
src: string,
base: string,
Expand Down Expand Up @@ -66,7 +65,7 @@ export default async function ({
util.chmod(src, dest);

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

return FILE_TYPE.COMPILED;
Expand Down Expand Up @@ -150,6 +149,8 @@ export default async function ({
startTime = null;
}, 100);

if (cliOptions.watch) watcher.enable({ enableGlobbing: true });

if (!cliOptions.skipInitialBuild) {
if (cliOptions.deleteDirOnStart) {
util.deleteDir(cliOptions.outDir);
Expand All @@ -173,43 +174,36 @@ export default async function ({
}

if (cliOptions.watch) {
const chokidar = util.requireChokidar();

filenames.forEach(function (filenameOrDir: string): void {
const watcher = chokidar.watch(filenameOrDir, {
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 50,
pollInterval: 10,
},
});

// This, alongside with debounce, allows us to only log
// when we are sure that all the files have been compiled.
let processing = 0;

["add", "change"].forEach(function (type: string): void {
watcher.on(type, async function (filename: string) {
processing++;
if (startTime === null) startTime = process.hrtime();

try {
await handleFile(
filename,
filename === filenameOrDir
? path.dirname(filenameOrDir)
: filenameOrDir,
);

compiledFiles++;
} catch (err) {
console.error(err);
}

processing--;
if (processing === 0 && !cliOptions.quiet) logSuccess();
});
// This, alongside with debounce, allows us to only log
// when we are sure that all the files have been compiled.
let processing = 0;

cliOptions.filenames.forEach(filenameOrDir => {
watcher.watch(filenameOrDir);

watcher.onFilesChange(async filenames => {
processing++;
if (startTime === null) startTime = process.hrtime();

try {
const written = await Promise.all(
filenames.map(filename =>
handleFile(
filename,
filename === filenameOrDir
? path.dirname(filenameOrDir)
: filenameOrDir,
),
),
);

compiledFiles += written.filter(Boolean).length;
} catch (err) {
console.error(err);
}

processing--;
if (processing === 0 && !cliOptions.quiet) logSuccess();
});
});
}
Expand Down
50 changes: 22 additions & 28 deletions packages/babel-cli/src/babel/file.ts
Expand Up @@ -6,6 +6,7 @@ import fs from "fs";

import * as util from "./util";
import type { CmdOptions } from "./options";
import * as watcher from "./watcher";

type CompilationOutput = {
code: string;
Expand Down Expand Up @@ -123,7 +124,7 @@ export default async function ({
async function stdin(): Promise<void> {
const code = await readStdin();

const res = await util.transform(cliOptions.filename, code, {
const res = await util.transformRepl(cliOptions.filename, code, {
...babelOptions,
sourceFileName: "stdin",
});
Expand Down Expand Up @@ -193,40 +194,33 @@ export default async function ({
}

async function files(filenames: Array<string>): Promise<void> {
if (cliOptions.watch) {
watcher.enable({ enableGlobbing: false });
}

if (!cliOptions.skipInitialBuild) {
await walk(filenames);
}

if (cliOptions.watch) {
const chokidar = util.requireChokidar();
chokidar
.watch(filenames, {
disableGlobbing: true,
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 50,
pollInterval: 10,
},
})
.on("all", function (type: string, filename: string): void {
if (
!util.isCompilableExtension(filename, cliOptions.extensions) &&
!filenames.includes(filename)
) {
return;
}

if (type === "add" || type === "change") {
if (cliOptions.verbose) {
console.log(type + " " + filename);
}
filenames.forEach(watcher.watch);

watcher.onFilesChange((changes, event, cause) => {
const actionableChange = changes.some(
filename =>
util.isCompilableExtension(filename, cliOptions.extensions) ||
filenames.includes(filename),
);
if (!actionableChange) return;

if (cliOptions.verbose) {
console.log(`${event} ${cause}`);
}

walk(filenames).catch(err => {
console.error(err);
});
}
walk(filenames).catch(err => {
console.error(err);
});
});
}
}

Expand Down
40 changes: 18 additions & 22 deletions packages/babel-cli/src/babel/util.ts
Expand Up @@ -2,7 +2,8 @@ import readdirRecursive from "fs-readdir-recursive";
import * as babel from "@babel/core";
import path from "path";
import fs from "fs";
import { createRequire } from "module";

import * as watcher from "./watcher";

export function chmod(src: string, dest: string): void {
try {
Expand Down Expand Up @@ -60,7 +61,7 @@ const CALLER = {
name: "@babel/cli",
};

export function transform(
export function transformRepl(
filename: string,
code: string,
opts: any,
Expand All @@ -79,18 +80,31 @@ export function transform(
});
}

export function compile(filename: string, opts: any | Function): Promise<any> {
export async function compile(
filename: string,
opts: any | Function,
): Promise<any> {
opts = {
...opts,
caller: CALLER,
};

return new Promise((resolve, reject) => {
// TODO (Babel 8): Use `babel.transformFileAsync`
const result: any = await new Promise((resolve, reject) => {
babel.transformFile(filename, opts, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});

if (result) {
if (!process.env.BABEL_8_BREAKING) {
if (!result.externalDependencies) return result;
}
watcher.updateExternalDependencies(filename, result.externalDependencies);
}

return result;
}

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

export function requireChokidar(): any {
const require = createRequire(import.meta.url);

try {
return process.env.BABEL_8_BREAKING
? require("chokidar")
: parseInt(process.versions.node) >= 8
? require("chokidar")
: require("@nicolo-ribaudo/chokidar-2");
} catch (err) {
console.error(
"The optional dependency chokidar failed to install and is required for " +
"--watch. Chokidar is likely not supported on your platform.",
);
throw err;
}
}

export function withExtension(filename: string, ext: string = ".js") {
const newBasename = path.basename(filename, path.extname(filename)) + ext;
return path.join(path.dirname(filename), newBasename);
Expand Down
133 changes: 133 additions & 0 deletions packages/babel-cli/src/babel/watcher.ts
@@ -0,0 +1,133 @@
import { createRequire } from "module";
import path from "path";

const fileToDeps = new Map<string, Set<string>>();
const depToFiles = new Map<string, Set<string>>();

let isWatchMode = false;
let watcher;

export function enable({ enableGlobbing }: { enableGlobbing: boolean }) {
isWatchMode = true;

const { FSWatcher } = requireChokidar();

watcher = new FSWatcher({
disableGlobbing: !enableGlobbing,
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 50,
pollInterval: 10,
},
});

watcher.on("unlink", unwatchFile);
}

export function watch(filename: string): void {
if (!isWatchMode) {
throw new Error(
"Internal Babel error: .watch called when not in watch mode.",
);
}

watcher.add(path.resolve(filename));
nicolo-ribaudo marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Call @param callback whenever a dependency (source file)/
* external dependency (non-source file) changes.
*
* Handles mapping external dependencies to their corresponding
* dependencies.
*/
export function onFilesChange(
callback: (filenames: string[], event: string, cause: string) => void,
): void {
if (!isWatchMode) {
throw new Error(
"Internal Babel error: .onFilesChange called when not in watch mode.",
);
}

watcher.on("all", (event, filename) => {
nicolo-ribaudo marked this conversation as resolved.
Show resolved Hide resolved
if (event !== "change" && event !== "add") return;

const absoluteFile = path.resolve(filename);
callback(
[absoluteFile, ...(depToFiles.get(absoluteFile) ?? [])],
event,
absoluteFile,
);
});
}

export function updateExternalDependencies(
filename: string,
dependencies: Set<string>,
) {
if (!isWatchMode) return;

// Use absolute paths
const absFilename = path.resolve(filename);
const absDependencies = new Set(
Array.from(dependencies, dep => path.resolve(dep)),
);

if (fileToDeps.has(absFilename)) {
for (const dep of fileToDeps.get(absFilename)) {
if (!absDependencies.has(dep)) {
removeFileDependency(absFilename, dep);
}
}
}
for (const dep of absDependencies) {
if (!depToFiles.has(dep)) {
depToFiles.set(dep, new Set());

watcher.add(dep);
}
depToFiles.get(dep).add(absFilename);
}

fileToDeps.set(absFilename, absDependencies);
}

function removeFileDependency(filename: string, dep: string) {
depToFiles.get(dep).delete(filename);

if (depToFiles.get(dep).size === 0) {
depToFiles.delete(dep);

watcher.unwatch(dep);
}
}

function unwatchFile(filename: string) {
if (!fileToDeps.has(filename)) return;

for (const dep of fileToDeps.get(filename)) {
removeFileDependency(filename, dep);
}
fileToDeps.delete(filename);
}

function requireChokidar(): any {
// @ts-expect-error - TS is not configured to support import.meta.
const require = createRequire(import.meta.url);

try {
return process.env.BABEL_8_BREAKING
? require("chokidar")
: parseInt(process.versions.node) >= 8
? require("chokidar")
: require("@nicolo-ribaudo/chokidar-2");
} catch (err) {
console.error(
"The optional dependency chokidar failed to install and is required for " +
"--watch. Chokidar is likely not supported on your platform.",
);
throw err;
}
}