diff --git a/package.json b/package.json index 287740ef..c6e130ee 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/cli.ts b/src/cli.ts index aab20f60..df85a4cf 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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; 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 ", "Compile an input directory of modules into an output directory.", ) + .option( + "-p, --project ", + "Compile a typescript project, will read from tsconfig.json in ", + ) .option("--out-extension ", "File extension to use for all output files.", "js") .option("--exclude-dirs ", "Names of directories that should not be traversed.") .option("-t, --transforms ", "Comma-separated list of transforms to run.") @@ -33,30 +44,30 @@ export default function run(): void { .option("--jsx-fragment-pragma ", "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", @@ -64,23 +75,30 @@ export default function run(): void { }, }; - 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 { +interface FileInfo { + srcPath: string; + outPath: string; +} + +async function findFiles(options: CLIOptions): Promise> { + 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 = []; for (const child of await readdir(srcDirPath)) { if (["node_modules", ".git"].includes(child) || options.excludeDirs.includes(child)) { continue; @@ -88,12 +106,167 @@ async function buildDirectory( 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> { + 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 = []; + + const files = json.files; + const include = json.include; + + const absProject = join(process.cwd(), options.project); + const outDirs: Array = []; + + 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 { + /** + * 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 { + let files: Array; + 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 { diff --git a/yarn.lock b/yarn.lock index eba4a2ed..1ba18d4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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/glob@7.1.1": + 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 @@ glob@7.1.3: 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==