Skip to content

Commit

Permalink
Support reading information from tsconfig.json (#509)
Browse files Browse the repository at this point in the history
The current `sucrase` binary only supports a single `srcDir` instead of multiple.

My typescript project has a `./src` and ` ./test/` dir that are compiled to `build/src` & `build/test` ; to do that with sucrase I have to run sucrase twice.

This would lead to something like

```json
{
  "scripts": {
    "build": "sucrase src --out-dir build/src --transforms typescript,imports --enable-legacy-typescript-module-interop && sucrase test --out-dir build/test --transforms typescript,imports --enable-legacy-typescript-module-interop"
  }
}
```

With this PR I can just `sucrase -p .` ;

I also implemented reading `outDir` ; `transforms` ; & `legacy module interop` from `tsconfig.json` since that information also exists there.

I tested this locally on three typescript projects ( `fake-kms`, `fake-cloudwatch-logs`, `fake-api-gateway-lambda` ).
  • Loading branch information
Raynos authored Mar 23, 2020
1 parent 0d91fd2 commit 546d0bb
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 17 deletions.
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) {
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")) {
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");
} 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);
}
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 @@ -350,11 +350,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 @@ -1616,7 +1635,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

0 comments on commit 546d0bb

Please sign in to comment.