Skip to content

Commit

Permalink
Allow plugins/presets to indicate external dependencies (#14065)
Browse files Browse the repository at this point in the history
Co-authored-by: Vedant Roy <vroy101@gmail.com>
  • Loading branch information
nicolo-ribaudo and vedantroy committed Feb 2, 2022
1 parent abba9f2 commit 7d63d2f
Show file tree
Hide file tree
Showing 30 changed files with 797 additions and 140 deletions.
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));
}

/**
* 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) => {
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;
}
}

0 comments on commit 7d63d2f

Please sign in to comment.