-
Notifications
You must be signed in to change notification settings - Fork 143
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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.`) | ||
|
@@ -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.") | ||
|
@@ -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")) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you have this also check for There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
@@ -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== | ||
|
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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 thereadFile
and handling the error instead of theexists
+readFile
since that's a timing race condition.There was a problem hiding this comment.
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.)