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 to indicate dependencies on random files #11741

Closed
wants to merge 11 commits into from
94 changes: 55 additions & 39 deletions packages/babel-cli/src/babel/dir.ts
Expand Up @@ -21,7 +21,9 @@ export default async function ({
cliOptions,
babelOptions,
}: CmdOptions): Promise<void> {
const filenames = cliOptions.filenames;
const absoluteFilePaths = cliOptions.filenames.map(name =>
path.resolve(name),
);

async function write(
src: string,
Expand Down Expand Up @@ -150,6 +152,9 @@ export default async function ({
startTime = null;
}, 100);

// Look at corresponding comment in file.js
if (cliOptions.watch) util.watchMode();

if (!cliOptions.skipInitialBuild) {
if (cliOptions.deleteDirOnStart) {
util.deleteDir(cliOptions.outDir);
Expand All @@ -173,44 +178,55 @@ 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,
},
});
let processing = 0;

// 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();
});
});
});
util.onDependencyFileChanged(async (changedFilePath: string) => {
if (
!util.isCompilableExtension(changedFilePath, cliOptions.extensions) &&
// See comment on corresponding code in file.js
!absoluteFilePaths.includes(changedFilePath)
) {
return;
}
processing++;
if (startTime === null) startTime = process.hrtime();

/**
* The relative path from @var base to @var changedFilePath
* will be path of @var changedFilePath in the output directory.
*/
let base = null;
for (const filePath of absoluteFilePaths) {
if (changedFilePath === filePath) {
// Case: "babel --watch src/bar/foo.js --out-dir dist"
// We want src/bar/foo.js --> dist/foo.js
base = path.dirname(changedFilePath);
break;
} else if (util.isChildPath(changedFilePath, filePath)) {
// Case: "babel --watch src/ --out-dir dist"
// src/foo/bar.js changes
// We want src/foo/bar.js --> dist/foo/bar.js
base = filePath;
break;
}
}

if (base === null) {
throw new Error(
`path: ${changedFilePath} was not equal to/a child path of any of these paths: ${absoluteFilePaths}`,
);
}

try {
await handleFile(changedFilePath, base);
compiledFiles++;
} catch (err) {
console.error(err);
}

processing--;
if (processing === 0 && !cliOptions.quiet) logSuccess();
}, false);
util.watchFiles(absoluteFilePaths);
}
}
49 changes: 23 additions & 26 deletions packages/babel-cli/src/babel/file.ts
Expand Up @@ -193,40 +193,37 @@ export default async function ({
}

async function files(filenames: Array<string>): Promise<void> {
// We need to set watch mode before the initial compilation
// so external dependencies are registered during the first compilation pass.
if (cliOptions.watch) util.watchMode();
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;
}
util.onDependencyFileChanged((filename: string | null) => {
if (
filename !== null &&
!util.isCompilableExtension(filename, cliOptions.extensions) &&
// Used in the case: babel --watch foo.ts --out-file compiled.js
// In this, case ".ts" is not a compilable extension (since the user didn't pass
// the --extensions flag), but, we still want to watch "foo.ts" anyway.
!filenames.includes(filename)
) {
return;
}

if (type === "add" || type === "change") {
if (cliOptions.verbose) {
console.log(type + " " + filename);
}
if (cliOptions.verbose) {
if (filename === null) {
console.log(`recompiling: external dependency changed`);
} else console.log(`compiling: ${filename}`);
}

walk(filenames).catch(err => {
console.error(err);
});
}
walk(filenames).catch(err => {
console.error(err);
});
}, true);
util.watchFiles(filenames);
}
}

Expand Down
136 changes: 123 additions & 13 deletions packages/babel-cli/src/babel/util.ts
Expand Up @@ -4,6 +4,10 @@ import path from "path";
import fs from "fs";
import { createRequire } from "module";

/**
* Set the file permissions of dest to the file permissions
* of src.
*/
export function chmod(src: string, dest: string): void {
try {
fs.chmodSync(dest, fs.statSync(src).mode);
Expand Down Expand Up @@ -60,7 +64,7 @@ const CALLER = {
name: "@babel/cli",
};

export function transform(
export async function transform(
filename: string,
code: string,
opts: any,
Expand All @@ -71,26 +75,132 @@ export function transform(
filename,
};

return new Promise((resolve, reject) => {
babel.transform(code, opts, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
const result = await babel.transformAsync(code, opts);
if (isWatchMode) watchNewExternalDependencies(filename);
return result;
}

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) => {
babel.transformFile(filename, opts, (err, result) => {
if (err) reject(err);
else resolve(result);
const result = await babel.transformFileAsync(filename, opts);
if (isWatchMode) watchNewExternalDependencies(filename);
return result;
}

let isWatchMode = false;

export function watchMode() {
isWatchMode = true;
}

/**
* Check if @param child is a child of @param parent
* Both paths must be absolute/resolved. (No "..")
*/
export function isChildPath(child: string, parent: string): boolean {
return (
child.length > parent.length + 1 && child.startsWith(parent + path.sep)
);
}

function subtract(minuend: Set<string>, subtrahend: Set<string>): string[] {
const diff = [];
for (const e of minuend) {
if (!subtrahend.has(e)) diff.push(e);
}
return diff;
}

/**
* Register new external dependencies with the file system
* watcher (chokidar).
*/
const watchNewExternalDependencies = (() => {
let prevDeps = null;
return (filePath: string) => {
// make the file path absolute because
// dependencies are registered with absolute file paths
filePath = path.resolve(filePath);
const prevDepsForFile =
prevDeps === null ? new Set() : prevDeps.get(filePath) || new Set();
const newDeps = babel.getDependencies();
const newDepsForFile = newDeps.get(filePath) || new Set();
const unwatchedDepsForFile = subtract(newDepsForFile, prevDepsForFile);
for (const dep of unwatchedDepsForFile) {
watchFiles(dep);
}
prevDeps = newDeps;
};
})();

const getWatcher = (() => {
// Use a closure to ensure the file watcher is only created once
// and never re-assigned. A const global variable isn't sufficient
// because we only want to create the file watcher if the user passes
// the --watch option, and a const variable must always be initialized.
let watcher = undefined;
return () => {
if (watcher) return watcher;
const { FSWatcher } = requireChokidar();
watcher = new FSWatcher({
disableGlobbing: true,
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 50,
pollInterval: 10,
},
});
});
return watcher;
};
})();

/**
* Call @param callback whenever a dependency (source file)/
* external dependency (non-source file) changes.
*
* Handles mapping external dependencies to their corresponding
* dependencies.
*/
export function onDependencyFileChanged(
callback: (filename_: string | null) => Promise<void>,
sourceFilesAreCompiledIntoASingleFile: boolean,
): void {
/**
*
* @param filePath The path of a file that has changed.
* It will never be a path to a directory.
* */
async function onFileChanged(filePath: string) {
// see corresponding line in registerNewExternalDependencies
filePath = path.resolve(filePath);
const externalFileDeps = babel.getExternalDependencies();
if (externalFileDeps.has(filePath)) {
if (sourceFilesAreCompiledIntoASingleFile) {
// When using --out-file, Babel traverses all the files every time
// so there's no point in calling the callback multiple times. The callback
// for --out-file knows to recompile no matter what if it receives null.
return await callback(null);
} else {
for (const dependent of externalFileDeps.get(filePath)) {
await callback(dependent);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be improved by Promise.all: the external deps can be compiled in parallel.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JLHwung can it actually? what if a dependency depends on another dependency?

If thats not possible, you are right. It should be changed to Promise.all @vedantroy

Copy link
Contributor Author

@vedantroy vedantroy Nov 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be changed to Promise.all (the API doesn't support a chain of deps), alas I don't have tons of bandwidth to do this right now so if @JLHwung wants to change it, that'd be great.

}
}
}
return await callback(filePath);
}
["add", "change"].forEach(type => getWatcher().on(type, onFileChanged));
}

export function watchFiles(filenameOrFilenames: string | string[]): void {
getWatcher().add(filenameOrFilenames);
}

export function deleteDir(path: string): void {
Expand Down