1 |
|
2 | import commander from "commander";
|
3 | import globCb from "glob";
|
4 | import {exists, mkdir, readdir, readFile, stat, writeFile} from "mz/fs";
|
5 | import {dirname, join, relative} from "path";
|
6 | import {promisify} from "util";
|
7 |
|
8 | import { transform} from "./index";
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | const glob = promisify(globCb);
|
21 |
|
22 | export default function run() {
|
23 | commander
|
24 | .description(`Sucrase: super-fast Babel alternative.`)
|
25 | .usage("[options] <srcDir>")
|
26 | .option(
|
27 | "-d, --out-dir <out>",
|
28 | "Compile an input directory of modules into an output directory.",
|
29 | )
|
30 | .option(
|
31 | "-p, --project <dir>",
|
32 | "Compile a TypeScript project, will read from tsconfig.json in <dir>",
|
33 | )
|
34 | .option("--out-extension <extension>", "File extension to use for all output files.", "js")
|
35 | .option("--exclude-dirs <paths>", "Names of directories that should not be traversed.")
|
36 | .option("-t, --transforms <transforms>", "Comma-separated list of transforms to run.")
|
37 | .option("-q, --quiet", "Don't print the names of converted files.")
|
38 | .option(
|
39 | "--enable-legacy-typescript-module-interop",
|
40 | "Use default TypeScript ESM/CJS interop strategy.",
|
41 | )
|
42 | .option("--enable-legacy-babel5-module-interop", "Use Babel 5 ESM/CJS interop strategy.")
|
43 | .option("--jsx-pragma <string>", "Element creation function, defaults to `React.createElement`")
|
44 | .option("--jsx-fragment-pragma <string>", "Fragment component, defaults to `React.Fragment`")
|
45 | .option("--production", "Disable debugging information from JSX in output.")
|
46 | .parse(process.argv);
|
47 |
|
48 | if (commander.project) {
|
49 | if (
|
50 | commander.outDir ||
|
51 | commander.transforms ||
|
52 | commander.args[0] ||
|
53 | commander.enableLegacyTypescriptModuleInterop
|
54 | ) {
|
55 | console.error(
|
56 | "If TypeScript project is specified, out directory, transforms, source " +
|
57 | "directory, and --enable-legacy-typescript-module-interop may not be specified.",
|
58 | );
|
59 | process.exit(1);
|
60 | }
|
61 | } else {
|
62 | if (!commander.outDir) {
|
63 | console.error("Out directory is required");
|
64 | process.exit(1);
|
65 | }
|
66 |
|
67 | if (!commander.transforms) {
|
68 | console.error("Transforms option is required.");
|
69 | process.exit(1);
|
70 | }
|
71 |
|
72 | if (!commander.args[0]) {
|
73 | console.error("Source directory is required.");
|
74 | process.exit(1);
|
75 | }
|
76 | }
|
77 |
|
78 | const options = {
|
79 | outDirPath: commander.outDir,
|
80 | srcDirPath: commander.args[0],
|
81 | project: commander.project,
|
82 | outExtension: commander.outExtension,
|
83 | excludeDirs: commander.excludeDirs ? commander.excludeDirs.split(",") : [],
|
84 | quiet: commander.quiet,
|
85 | sucraseOptions: {
|
86 | transforms: commander.transforms ? commander.transforms.split(",") : [],
|
87 | enableLegacyTypeScriptModuleInterop: commander.enableLegacyTypescriptModuleInterop,
|
88 | enableLegacyBabel5ModuleInterop: commander.enableLegacyBabel5ModuleInterop,
|
89 | jsxPragma: commander.jsxPragma || "React.createElement",
|
90 | jsxFragmentPragma: commander.jsxFragmentPragma || "React.Fragment",
|
91 | production: commander.production,
|
92 | },
|
93 | };
|
94 |
|
95 | buildDirectory(options).catch((e) => {
|
96 | process.exitCode = 1;
|
97 | console.error(e);
|
98 | });
|
99 | }
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 | async function findFiles(options) {
|
107 | const outDirPath = options.outDirPath;
|
108 | const srcDirPath = options.srcDirPath;
|
109 |
|
110 | const extensions = options.sucraseOptions.transforms.includes("typescript")
|
111 | ? [".ts", ".tsx"]
|
112 | : [".js", ".jsx"];
|
113 |
|
114 | if (!(await exists(outDirPath))) {
|
115 | await mkdir(outDirPath);
|
116 | }
|
117 |
|
118 | const outArr = [];
|
119 | for (const child of await readdir(srcDirPath)) {
|
120 | if (["node_modules", ".git"].includes(child) || options.excludeDirs.includes(child)) {
|
121 | continue;
|
122 | }
|
123 | const srcChildPath = join(srcDirPath, child);
|
124 | const outChildPath = join(outDirPath, child);
|
125 | if ((await stat(srcChildPath)).isDirectory()) {
|
126 | const innerOptions = {...options};
|
127 | innerOptions.srcDirPath = srcChildPath;
|
128 | innerOptions.outDirPath = outChildPath;
|
129 | const innerFiles = await findFiles(innerOptions);
|
130 | outArr.push(...innerFiles);
|
131 | } else if (extensions.some((ext) => srcChildPath.endsWith(ext))) {
|
132 | const outPath = outChildPath.replace(/\.\w+$/, `.${options.outExtension}`);
|
133 | outArr.push({
|
134 | srcPath: srcChildPath,
|
135 | outPath,
|
136 | });
|
137 | }
|
138 | }
|
139 |
|
140 | return outArr;
|
141 | }
|
142 |
|
143 | async function runGlob(options) {
|
144 | const tsConfigPath = join(options.project, "tsconfig.json");
|
145 |
|
146 | let str;
|
147 | try {
|
148 | str = await readFile(tsConfigPath, "utf8");
|
149 | } catch (err) {
|
150 | console.error("Could not find project tsconfig.json");
|
151 | console.error(` --project=${options.project}`);
|
152 | console.error(err);
|
153 | process.exit(1);
|
154 | }
|
155 | const json = JSON.parse(str);
|
156 |
|
157 | const foundFiles = [];
|
158 |
|
159 | const files = json.files;
|
160 | const include = json.include;
|
161 |
|
162 | const absProject = join(process.cwd(), options.project);
|
163 | const outDirs = [];
|
164 |
|
165 | if (!(await exists(options.outDirPath))) {
|
166 | await mkdir(options.outDirPath);
|
167 | }
|
168 |
|
169 | if (files) {
|
170 | for (const file of files) {
|
171 | if (file.endsWith(".d.ts")) {
|
172 | continue;
|
173 | }
|
174 | if (!file.endsWith(".ts") && !file.endsWith(".js")) {
|
175 | continue;
|
176 | }
|
177 |
|
178 | const srcFile = join(absProject, file);
|
179 | const outFile = join(options.outDirPath, file);
|
180 | const outPath = outFile.replace(/\.\w+$/, `.${options.outExtension}`);
|
181 |
|
182 | const outDir = dirname(outPath);
|
183 | if (!outDirs.includes(outDir)) {
|
184 | outDirs.push(outDir);
|
185 | }
|
186 |
|
187 | foundFiles.push({
|
188 | srcPath: srcFile,
|
189 | outPath,
|
190 | });
|
191 | }
|
192 | }
|
193 | if (include) {
|
194 | for (const pattern of include) {
|
195 | const globFiles = await glob(join(absProject, pattern));
|
196 | for (const file of globFiles) {
|
197 | if (!file.endsWith(".ts") && !file.endsWith(".js")) {
|
198 | continue;
|
199 | }
|
200 | if (file.endsWith(".d.ts")) {
|
201 | continue;
|
202 | }
|
203 |
|
204 | const relativeFile = relative(absProject, file);
|
205 | const outFile = join(options.outDirPath, relativeFile);
|
206 | const outPath = outFile.replace(/\.\w+$/, `.${options.outExtension}`);
|
207 |
|
208 | const outDir = dirname(outPath);
|
209 | if (!outDirs.includes(outDir)) {
|
210 | outDirs.push(outDir);
|
211 | }
|
212 |
|
213 | foundFiles.push({
|
214 | srcPath: file,
|
215 | outPath,
|
216 | });
|
217 | }
|
218 | }
|
219 | }
|
220 |
|
221 | for (const outDirPath of outDirs) {
|
222 | if (!(await exists(outDirPath))) {
|
223 | await mkdir(outDirPath);
|
224 | }
|
225 | }
|
226 |
|
227 |
|
228 |
|
229 | return foundFiles;
|
230 | }
|
231 |
|
232 | async function updateOptionsFromProject(options) {
|
233 | |
234 |
|
235 |
|
236 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 | const tsConfigPath = join(options.project, "tsconfig.json");
|
242 |
|
243 | let str;
|
244 | try {
|
245 | str = await readFile(tsConfigPath, "utf8");
|
246 | } catch (err) {
|
247 | console.error("Could not find project tsconfig.json");
|
248 | console.error(` --project=${options.project}`);
|
249 | console.error(err);
|
250 | process.exit(1);
|
251 | }
|
252 | const json = JSON.parse(str);
|
253 | const sucraseOpts = options.sucraseOptions;
|
254 | if (!sucraseOpts.transforms.includes("typescript")) {
|
255 | sucraseOpts.transforms.push("typescript");
|
256 | }
|
257 |
|
258 | const compilerOpts = json.compilerOptions;
|
259 | if (compilerOpts.outDir) {
|
260 | options.outDirPath = join(process.cwd(), options.project, compilerOpts.outDir);
|
261 | }
|
262 | if (compilerOpts.esModuleInterop !== true) {
|
263 | sucraseOpts.enableLegacyTypeScriptModuleInterop = true;
|
264 | }
|
265 | if (compilerOpts.module === "commonjs") {
|
266 | if (!sucraseOpts.transforms.includes("imports")) {
|
267 | sucraseOpts.transforms.push("imports");
|
268 | }
|
269 | }
|
270 | }
|
271 |
|
272 | async function buildDirectory(options) {
|
273 | let files;
|
274 | if (options.outDirPath && options.srcDirPath) {
|
275 | files = await findFiles(options);
|
276 | } else if (options.project) {
|
277 | await updateOptionsFromProject(options);
|
278 | files = await runGlob(options);
|
279 | } else {
|
280 | console.error("Project or Source directory required.");
|
281 | process.exit(1);
|
282 | }
|
283 |
|
284 | for (const file of files) {
|
285 | await buildFile(file.srcPath, file.outPath, options);
|
286 | }
|
287 | }
|
288 |
|
289 | async function buildFile(srcPath, outPath, options) {
|
290 | if (!options.quiet) {
|
291 | console.log(`${srcPath} -> ${outPath}`);
|
292 | }
|
293 | const code = (await readFile(srcPath)).toString();
|
294 | const transformedCode = transform(code, {...options.sucraseOptions, filePath: srcPath}).code;
|
295 | await writeFile(outPath, transformedCode);
|
296 | }
|