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

Support reading information from tsconfig.json #509

Merged
merged 3 commits into from
Mar 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "^7.7.0",
"@babel/preset-typescript": "^7.7.2",
"@types/glob": "7.1.1",
"@types/mocha": "^5.2.7",
"@types/mz": "^0.0.32",
"@types/node": "^12.12.7",
Expand All @@ -80,6 +81,7 @@
},
"dependencies": {
"commander": "^4.0.0",
"glob": "7.1.6",
"lines-and-columns": "^1.1.6",
"mz": "^2.7.0",
"pirates": "^4.0.1",
Expand Down
205 changes: 189 additions & 16 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
/* eslint-disable no-console */
import commander from "commander";
import globCb from "glob";
import {exists, mkdir, readdir, readFile, stat, writeFile} from "mz/fs";
import {join} from "path";
import {dirname, join, relative} from "path";
import {promisify} from "util";

import {Options, transform} from "./index";

interface CLIOptions {
outDirPath: string;
srcDirPath: string;
project: string;
outExtension: string;
excludeDirs: Array<string>;
quiet: boolean;
sucraseOptions: Options;
}

const glob = promisify(globCb);

export default function run(): void {
commander
.description(`Sucrase: super-fast Babel alternative.`)
Expand All @@ -20,6 +27,10 @@ export default function run(): void {
"-d, --out-dir <out>",
"Compile an input directory of modules into an output directory.",
)
.option(
"-p, --project <dir>",
"Compile a typescript project, will read from tsconfig.json in <dir>",
)
.option("--out-extension <extension>", "File extension to use for all output files.", "js")
.option("--exclude-dirs <paths>", "Names of directories that should not be traversed.")
.option("-t, --transforms <transforms>", "Comma-separated list of transforms to run.")
Expand All @@ -33,67 +44,229 @@ export default function run(): void {
.option("--jsx-fragment-pragma <string>", "Fragment component, defaults to `React.Fragment`")
.parse(process.argv);

if (!commander.outDir) {
if (!commander.outDir && !commander.project) {
console.error("Out directory is required");
process.exit(1);
}

if (!commander.transforms) {
if (!commander.transforms && !commander.project) {
console.error("Transforms option is required.");
process.exit(1);
}

if (!commander.args[0]) {
if (!commander.args[0] && !commander.project) {
console.error("Source directory is required.");
process.exit(1);
}

const outDir = commander.outDir;
const srcDir = commander.args[0];

const options: CLIOptions = {
outDirPath: commander.outDir,
srcDirPath: commander.args[0],
project: commander.project,
outExtension: commander.outExtension,
excludeDirs: commander.excludeDirs ? commander.excludeDirs.split(",") : [],
quiet: commander.quiet,
sucraseOptions: {
transforms: commander.transforms.split(","),
transforms: commander.transforms ? commander.transforms.split(",") : [],
enableLegacyTypeScriptModuleInterop: commander.enableLegacyTypescriptModuleInterop,
enableLegacyBabel5ModuleInterop: commander.enableLegacyBabel5ModuleInterop,
jsxPragma: commander.jsxPragma || "React.createElement",
jsxFragmentPragma: commander.jsxFragmentPragma || "React.Fragment",
},
};

buildDirectory(srcDir, outDir, options).catch((e) => {
buildDirectory(options).catch((e) => {
process.exitCode = 1;
console.error(e);
});
}

async function buildDirectory(
srcDirPath: string,
outDirPath: string,
options: CLIOptions,
): Promise<void> {
interface FileInfo {
srcPath: string;
outPath: string;
}

async function findFiles(options: CLIOptions): Promise<Array<FileInfo>> {
const outDirPath = options.outDirPath;
const srcDirPath = options.srcDirPath;

const extensions = options.sucraseOptions.transforms.includes("typescript")
? [".ts", ".tsx"]
: [".js", ".jsx"];

if (!(await exists(outDirPath))) {
await mkdir(outDirPath);
}

const outArr: Array<FileInfo> = [];
for (const child of await readdir(srcDirPath)) {
if (["node_modules", ".git"].includes(child) || options.excludeDirs.includes(child)) {
continue;
}
const srcChildPath = join(srcDirPath, child);
const outChildPath = join(outDirPath, child);
if ((await stat(srcChildPath)).isDirectory()) {
await buildDirectory(srcChildPath, outChildPath, options);
const innerOptions = {...options};
innerOptions.srcDirPath = srcChildPath;
innerOptions.outDirPath = outChildPath;
const innerFiles = await findFiles(innerOptions);
outArr.push(...innerFiles);
} else if (extensions.some((ext) => srcChildPath.endsWith(ext))) {
const outPath = outChildPath.replace(/\.\w+$/, `.${options.outExtension}`);
await buildFile(srcChildPath, outPath, options);
outArr.push({
srcPath: srcChildPath,
outPath,
});
}
}

return outArr;
}

async function runGlob(options: CLIOptions): Promise<Array<FileInfo>> {
const tsConfigPath = join(options.project, "tsconfig.json");

let str;
try {
str = await readFile(tsConfigPath, "utf8");
} catch (err) {
Copy link
Owner

Choose a reason for hiding this comment

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

Using try/catch feels a little weird here. I think I'd prefer to use exists and give a friendly message if that fails, and if there's some other error, it can make its way up as a real crash with stack trace.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

exists is deprecated ( https://nodejs.org/api/fs.html#fs_fs_exists_path_callback ). The documentation recommends just trying the readFile and handling the error instead of the exists + readFile since that's a timing race condition.

Copy link
Owner

Choose a reason for hiding this comment

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

Interesting, fair enough. If possible, it would be nice to detect if it's a "file not found" error (maybe err.code === 'ENOENT' like in the docs) and re-throw if it's an unrecognized error.

(In this case, I'm not convinced that the race condition actually matters, since the worst case is an uglier error message, and there are plenty of similar cross-file race conditions that can't be reasonably handled, but I guess it's nice to make the ugly error impossible.)

console.error("Could not find project tsconfig.json");
console.error(` --project=${options.project}`);
console.error(err);
process.exit(1);
}
const json = JSON.parse(str);

const foundFiles: Array<FileInfo> = [];

const files = json.files;
const include = json.include;

const absProject = join(process.cwd(), options.project);
const outDirs: Array<string> = [];

if (!(await exists(options.outDirPath))) {
await mkdir(options.outDirPath);
}

if (files) {
for (const file of files) {
if (file.endsWith(".d.ts")) {
continue;
}
if (!file.endsWith(".ts") && !file.endsWith(".js")) {
Copy link
Owner

Choose a reason for hiding this comment

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

Could you have this also check for .tsx and .jsx files?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will fix.

continue;
}

const srcFile = join(absProject, file);
const outFile = join(options.outDirPath, file);
const outPath = outFile.replace(/\.\w+$/, `.${options.outExtension}`);

const outDir = dirname(outPath);
if (!outDirs.includes(outDir)) {
outDirs.push(outDir);
}

foundFiles.push({
srcPath: srcFile,
outPath,
});
}
}
if (include) {
for (const pattern of include) {
const globFiles = await glob(join(absProject, pattern));
for (const file of globFiles) {
if (!file.endsWith(".ts") && !file.endsWith(".js")) {
continue;
}
if (file.endsWith(".d.ts")) {
continue;
}

const relativeFile = relative(absProject, file);
const outFile = join(options.outDirPath, relativeFile);
const outPath = outFile.replace(/\.\w+$/, `.${options.outExtension}`);

const outDir = dirname(outPath);
if (!outDirs.includes(outDir)) {
outDirs.push(outDir);
}

foundFiles.push({
srcPath: file,
outPath,
});
}
}
}

for (const outDirPath of outDirs) {
if (!(await exists(outDirPath))) {
await mkdir(outDirPath);
}
}

// TODO: read exclude

return foundFiles;
}

async function updateOptionsFromProject(options: CLIOptions): Promise<void> {
/**
* Read the project information and assign the following.
* - outDirPath
* - transform: imports
* - transform: typescript
* - enableLegacyTypescriptModuleInterop: true/false.
*/

const tsConfigPath = join(options.project, "tsconfig.json");

let str;
try {
str = await readFile(tsConfigPath, "utf8");
Copy link
Owner

Choose a reason for hiding this comment

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

It would be nice to factor the code so we don't need to do this twice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

willfix.

} catch (err) {
console.error("Could not find project tsconfig.json");
console.error(` --project=${options.project}`);
console.error(err);
process.exit(1);
}
const json = JSON.parse(str);
const sucraseOpts = options.sucraseOptions;
if (!sucraseOpts.transforms.includes("typescript")) {
sucraseOpts.transforms.push("typescript");
}

const compilerOpts = json.compilerOptions;
if (compilerOpts.outDir) {
options.outDirPath = join(process.cwd(), options.project, compilerOpts.outDir);
Copy link
Owner

Choose a reason for hiding this comment

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

Stylistically, I'm not a fan of mutating objects after they're created like this. I think I'd prefer to have this function accept a project path and return a CLIOptions object or a Partial<CLIOptions> object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll look at fixing that.

}
if (compilerOpts.esModuleInterop !== true) {
sucraseOpts.enableLegacyTypeScriptModuleInterop = true;
}
if (compilerOpts.module === "commonjs") {
if (!sucraseOpts.transforms.includes("imports")) {
sucraseOpts.transforms.push("imports");
}
}
}

async function buildDirectory(options: CLIOptions): Promise<void> {
let files: Array<FileInfo>;
if (options.outDirPath && options.srcDirPath) {
files = await findFiles(options);
} else if (options.project) {
await updateOptionsFromProject(options);
files = await runGlob(options);
} else {
console.error("Project or Source directory required.");
process.exit(1);
}

for (const file of files) {
await buildFile(file.srcPath, file.outPath, options);
}
}

async function buildFile(srcPath: string, outPath: string, options: CLIOptions): Promise<void> {
Expand Down
21 changes: 20 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -345,11 +345,30 @@
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==

"@types/events@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==

"@types/[email protected]":
version "7.1.1"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
dependencies:
"@types/events" "*"
"@types/minimatch" "*"
"@types/node" "*"

"@types/json-schema@^7.0.3":
version "7.0.3"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==

"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==

"@types/mocha@^5.2.7":
version "5.2.7"
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea"
Expand Down Expand Up @@ -1618,7 +1637,7 @@ [email protected]:
once "^1.3.0"
path-is-absolute "^1.0.0"

glob@^7.0.0, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4:
glob@7.1.6, glob@^7.0.0, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
Expand Down