/
transform.js
177 lines (144 loc) · 5.49 KB
/
transform.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import fs from "fs";
import camelize from "camelize";
import {coreOptions as comalCoreOptions} from "comal";
const comalCoreOptionNames = Object.keys(comalCoreOptions);
import isGlob from "is-glob";
import cloneDeep from "lodash/cloneDeep";
import includes from "lodash/includes";
import omit from "lodash/omit";
import map from "lodash/map";
import pick from "lodash/pick";
import subarg from "subarg";
import usage from "./usage";
import usageAdvanced from "./usage/comal";
import baseArgs from "./base-args";
import argsWithComalOpts from "./convertComalOpts";
import convertPluginOpts from "./convertPluginOpts";
import stdin from "./stdin";
import stdout from "./stdout";
import transformTree from "./transformTree";
import watchTree from "./watchTree";
export default class TransformCli {
constructor(defaults = {}) {
this.name = "unknown";
this.logger = console;
this.defaults = omit(defaults, "_");
this.opts = cloneDeep(baseArgs);
this.destExt = ".js";
// TODO: collect unknown arguments in a separate object as args to pass to comal
// this.opts.unknown;
this.opts.boolean.push("watch", "serial");
this.opts.string.push("outfile", "extensions");
this.opts.alias["outfile"] = ["o"];
this.opts.alias["watch"] = ["w"];
this.opts.alias["extensions"] = ["x"];
this.opts.alias["plugin"] = ["p"];
this.opts.alias["generator"] = ["g"];
this.args = {}
}
transformSync(code, opts) {
// This method should be overridden by a subclass or instance
return {code: code, opts: opts};
}
prepare() {
// This method should be overridden by a subclass or instance
}
transformFile(file, opts, cb) {
// This method should be overridden by a subclass or instance
fs.readFile(file, opts, cb);
}
debug(...args) {
if (this.args.verbose) this.logger.warn(...args);
}
log(...args) {
if (!this.args.quiet) this.logger.log(...args);
}
parseArgs(argv) {
const comalUnknownOptionNames = [];
const minimistOpts = argsWithComalOpts(comalCoreOptions, {
...this.opts,
default: {...this.defaults, ...this.opts.default},
unknown(arg) {
const match = /^--([^=]+)=/.exec(arg) || /^--(?:no-)?(.+)/.exec(arg);
if (match) comalUnknownOptionNames.push(match[1]);
}
});
const args = this.args = subarg(argv, minimistOpts);
this.comalOpts = camelize(pick(args, comalCoreOptionNames, comalUnknownOptionNames));
}
run(argv, cb) {
if (includes(argv, "--help") || includes(argv, "-h")) {
if (subarg(argv, {alias: {"help": ["h"]}}).help === "advanced") {
return usageAdvanced(comalCoreOptions, cb);
} else {
return usage(this.name, cb);
}
}
this.parseArgs(argv);
const args = this.args;
// normalize input arguments
this.infiles = args._;
const useStdin = this.infiles.length === 0;
// normalize output arguments
this.outfiles = args.outfile == null ? [] : [].concat(args.outfile);
const useStdout = this.outfiles.length === 0;
// normalize arguments to pass to a comal api
const comalOpts = this.comalOpts;
if (args.plugin) {
try { comalOpts.plugins = convertPluginOpts(args.plugin) }
catch (e) { return cb(e); }
}
if (args.extensions && !useStdin) {
comalOpts.only = (comalOpts.only ? comalOpts.only + "," : "") +
map(args.extensions.split(","), (e) => "*" + e).join(",");
}
this.debug("will convert", useStdin ? '<stdin>' : this.infiles, "to", useStdout ? '<stdout>' : this.outfiles);
if (useStdin || useStdout) {
return this.runStdio(cb);
}
return this.runFiles(cb);
}
runStdio(cb) {
const args = this.args;
if (args.watch) return cb(new Error("Cannot use watch with stdio"));
const useStdin = this.infiles.length === 0;
const useStdout = this.outfiles.length === 0;
if (useStdin) {
// TODO: error if sourcemaps is true, i.e. is requested as a separate file.
}
if (useStdout && !useStdin && (this.infiles.length > 1 || isGlob(this.infiles[0]))) {
return cb(new Error("Cannot use more than one input file with stdout"));
}
if (useStdin && !useStdout && this.infiles.length > 1) {
return cb(new Error("Cannot write to more than one output file with stdin"));
}
// do the work
const read = useStdin ? stdin : (cb) => { fs.readFile(this.infiles[0], 'utf8', cb) };
const write = useStdout ? stdout : (data, cb) => { fs.writeFile(this.outfiles[0], data, 'utf8', cb); };
if (!useStdin && !this.comalOpts.filename) {
this.comalOpts.filename = this.infiles[0];
}
read((err, data) => {
if (err) {
if (err.code === "EISDIR") {
return cb(new Error("Cannot use a directory as input with stdout"));
}
return cb(err)
}
write(this.transformSync(data, this.comalOpts).code, cb);
});
}
runFiles(cb) {
const args = this.args;
if (this.outfiles.length !== 1 && this.outfiles.length !== this.infiles.length) {
return cb(new Error("Number of input files must equal number of output files, or output to a directory"));
}
this.prepare();
// TODO: move watch and transform into this class
if (args.watch) {
watchTree(this.transformFile.bind(this), {src: this.infiles, dest: this.outfiles, destExt: this.destExt}, {args, only: this.comalOpts.only}, cb);
} else {
transformTree(this.transformFile.bind(this), {src: this.infiles, dest: this.outfiles, destExt: this.destExt}, {args, only: this.comalOpts.only}, cb);
}
}
}