Skip to content

Commit 759470b

Browse files
committed
Allow multiple files on the command line, all of which get combined into one output. Not ready. Still needs change to source-map generation, for topLevelInitializer to be an array, and to make moving imports to the top of all topLevelInitializers (keeping track of source info) a JS-specific pass.
1 parent 736d6cc commit 759470b

6 files changed

Lines changed: 220 additions & 102 deletions

File tree

bin/peggy-cli.js

Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ class PeggyCLI extends Command {
9494

9595
/** @type {peggy.BuildOptionsBase} */
9696
this.argv = {};
97-
/** @type {string?} */
98-
this.inputFile = null;
97+
/** @type {string[]} */
98+
this.inputFiles = [];
9999
/** @type {string?} */
100100
this.outputFile = null;
101101
/** @type {object} */
@@ -115,7 +115,7 @@ class PeggyCLI extends Command {
115115

116116
this
117117
.version(peggy.VERSION, "-v, --version")
118-
.argument("[input_file]", 'Grammar file to read. Use "-" to read stdin.', "-")
118+
.argument("[input_file...]", 'Grammar file(s) to read. Use "-" to read stdin. If multiple files are given, they are combined in the given order to produce a single output.', ["-"])
119119
.allowExcessArguments(false)
120120
.addOption(
121121
new Option(
@@ -214,8 +214,8 @@ class PeggyCLI extends Command {
214214
.hideHelp()
215215
.default(false)
216216
)
217-
.action((inputFile, opts) => { // On parse()
218-
this.inputFile = inputFile;
217+
.action((inputFiles, opts) => { // On parse()
218+
this.inputFiles = inputFiles;
219219
this.argv = opts;
220220

221221
if ((typeof this.argv.startRule === "string")
@@ -284,21 +284,24 @@ class PeggyCLI extends Command {
284284
this.argv.output = "source";
285285
if ((this.args.length === 0) && this.progOptions.input) {
286286
// Allow command line to override config file.
287-
this.inputFile = this.progOptions.input;
287+
this.inputFiles = Array.isArray(this.progOptions.input)
288+
? this.progOptions.input
289+
: [this.progOptions.input];
288290
}
289291
this.outputFile = this.progOptions.output;
290292
this.outputJS = this.progOptions.output;
291293

292-
if ((this.inputFile === "-") && this.argv.watch) {
294+
if ((this.inputFiles.indexOf("-") !== -1) && this.argv.watch) {
293295
this.argv.watch = false; // Make error throw.
294296
this.error("Can't watch stdin");
295297
}
296298

297299
if (!this.outputFile) {
298-
if (this.inputFile !== "-") {
299-
this.outputJS = this.inputFile.slice(
300+
if (this.inputFiles.indexOf("-") === -1) {
301+
this.outputJS = this.inputFiles[0].slice(
300302
0,
301-
this.inputFile.length - path.extname(this.inputFile).length
303+
this.inputFiles[0].length
304+
- path.extname(this.inputFiles[0]).length
302305
) + ".js";
303306

304307
this.outputFile = ((typeof this.progOptions.test !== "string")
@@ -345,7 +348,7 @@ class PeggyCLI extends Command {
345348
}
346349
this.verbose("PARSER OPTIONS:", this.argv);
347350
this.verbose("PROGRAM OPTIONS:", this.progOptions);
348-
this.verbose('INPUT: "%s"', this.inputFile);
351+
this.verbose('INPUT: "%s"', this.inputFiles);
349352
this.verbose('OUTPUT: "%s"', this.outputFile);
350353
if (this.progOptions.verbose) {
351354
this.argv.info = (pass, msg) => PeggyCLI.print(this.std.err, `INFO(${pass}): ${msg}`);
@@ -561,7 +564,7 @@ class PeggyCLI extends Command {
561564
if (this.testFile === "-") {
562565
this.testText = await readStream(this.std.in);
563566
} else {
564-
this.testText = fs.readFileSync(this.testFile, "utf8");
567+
this.testText = await fs.promises.readFile(this.testFile, "utf8");
565568
}
566569
}
567570
if (typeof this.testText === "string") {
@@ -629,26 +632,29 @@ class PeggyCLI extends Command {
629632
* @returns {Promise<number>}
630633
*/
631634
async run() {
632-
let inputStream = undefined;
633-
634-
if (this.inputFile === "-") {
635-
this.std.in.resume();
636-
inputStream = this.std.in;
637-
this.argv.grammarSource = "stdin";
638-
} else {
639-
this.argv.grammarSource = this.inputFile;
640-
inputStream = fs.createReadStream(this.inputFile);
641-
}
635+
const sources = [];
642636

643637
let exitCode = 1;
644638
let errorText = "";
645-
let input = "";
646639
try {
647-
this.verbose("CLI", errorText = "reading input stream");
648-
input = await readStream(inputStream);
640+
for (const source of this.inputFiles) {
641+
const input = { source, text: null };
642+
this.verbose("CLI", errorText = `reading input "${source}"`);
643+
if (source === "-") {
644+
input.source = "stdin";
645+
this.std.in.resume();
646+
input.text = await readStream(this.std.in);
647+
} else {
648+
input.text = await fs.promises.readFile(source, "utf8");
649+
}
650+
sources.push(input);
651+
}
652+
653+
// This is wrong. It's a hack in place until source generation is fixed.
654+
this.argv.grammarSource = sources[0].source;
649655

650656
this.verbose("CLI", errorText = "parsing grammar");
651-
const source = peggy.generate(input, this.argv); // All of the real work.
657+
const source = peggy.generate(sources, this.argv); // All of the real work.
652658

653659
this.verbose("CLI", errorText = "open output stream");
654660
const outputStream = await this.openOutputStream();
@@ -669,10 +675,6 @@ class PeggyCLI extends Command {
669675
await this.test(mappedSource);
670676
}
671677
} catch (error) {
672-
const sources = [{
673-
source: this.argv.grammarSource,
674-
text: input,
675-
}];
676678
if (this.testGrammarSource) {
677679
sources.push({
678680
source: this.testGrammarSource,
@@ -711,19 +713,21 @@ class PeggyCLI extends Command {
711713
if (this.argv.watch) {
712714
const Watcher = require("./watcher.js"); // Lazy: usually not needed.
713715
const hasTest = this.progOptions.test || this.progOptions.testFile;
716+
const watchFiles = [...this.inputFiles];
717+
if (this.progOptions.testFile) {
718+
watchFiles.push(this.progOptions.testFile);
719+
}
720+
this.watcher = new Watcher(...watchFiles);
714721

715-
this.watcher = new Watcher(this.inputFile);
716-
717-
const that = this;
718-
this.watcher.on("change", async() => {
719-
PeggyCLI.print(this.std.err, `"${that.inputFile}" changed...`);
722+
this.watcher.on("change", async fn => {
723+
PeggyCLI.print(this.std.err, `"${fn}" changed...`);
720724
this.lastError = null;
721-
await that.run();
725+
await this.run();
722726

723-
if (that.lastError) {
724-
PeggyCLI.print(this.std.err, that.lastError);
727+
if (this.lastError) {
728+
PeggyCLI.print(this.std.err, this.lastError);
725729
} else if (!hasTest) {
726-
PeggyCLI.print(this.std.err, `Wrote: "${that.outputFile}"`);
730+
PeggyCLI.print(this.std.err, `Wrote: "${this.outputFile}"`);
727731
}
728732
});
729733

bin/watcher.js

Lines changed: 78 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const { EventEmitter } = require("events");
66

77
// This may have to be tweaked based on experience.
88
const DEBOUNCE_MS = 100;
9+
const CLOSING = Symbol("CLOSING");
10+
const ERROR = Symbol("ERROR");
911

1012
/**
1113
* Relatively feature-free file watcher that deals with some of the
@@ -18,51 +20,70 @@ const DEBOUNCE_MS = 100;
1820
*/
1921
class Watcher extends EventEmitter {
2022
/**
21-
* Creates an instance of Watcher.
23+
* Creates an instance of Watcher. This only works for files in a small
24+
* number of directories.
2225
*
23-
* @param {string} filename The file to watch. Should be a plain file,
24-
* not a directory, pipe, etc.
26+
* @param {string[]} filenames The files to watch. Should be one or more
27+
* strings, each of which is the name of a plain file, not a directory,
28+
* pipe, etc.
2529
*/
26-
constructor(filename) {
30+
constructor(...filenames) {
2731
super();
2832

29-
const rfile = path.resolve(filename);
30-
const { dir, base } = path.parse(rfile);
31-
let timeout = null;
33+
const resolved = new Set(filenames.map(fn => path.resolve(fn)));
34+
const dirs = new Set([...resolved].map(fn => path.dirname(fn)));
3235

33-
// eslint-disable-next-line func-style -- Needs this.
34-
const changed = (typ, fn) => {
35-
if (fn === base) {
36-
if (!timeout) {
37-
fs.stat(rfile, (er, stats) => {
38-
if (!er && stats.isFile()) {
39-
this.emit("change", stats);
40-
}
41-
});
42-
} else {
43-
clearTimeout(timeout);
36+
this.timeout = null;
37+
this.watchers = [];
38+
39+
for (const dir of dirs) {
40+
// eslint-disable-next-line func-style -- Needs "this"
41+
const changed = (_typ, fn) => {
42+
if (typeof this.timeout === "symbol") {
43+
return;
4444
}
45+
const filename = path.join(dir, fn);
46+
// Might be a different fil changing in one of the target dirs
47+
if (resolved.has(filename)) {
48+
if (!this.timeout) {
49+
fs.stat(filename, (er, stats) => {
50+
if (!er && stats.isFile()) {
51+
this.emit("change", filename, stats);
52+
}
53+
});
54+
} else {
55+
clearTimeout(this.timeout);
56+
}
4557

46-
// De-bounce
47-
timeout = setTimeout(() => {
48-
timeout = null;
49-
}, Watcher.interval);
50-
}
51-
};
52-
const closed = () => this.emit("close");
58+
// De-bounce, across all files
59+
this.timeout = setTimeout(() => {
60+
this.timeout = null;
61+
}, Watcher.interval);
62+
}
63+
};
5364

54-
this.watcher = fs.watch(dir);
55-
this.watcher.on("error", er => {
56-
this.watcher.off("close", closed);
57-
this.watcher.once("close", () => this.emit("error", er));
58-
this.watcher.close();
59-
this.watcher = null;
60-
});
61-
this.watcher.on("close", closed);
62-
this.watcher.on("change", changed);
65+
const w = fs.watch(dir);
66+
w.on("error", er => {
67+
const t = this.timeout;
68+
this.timeout = ERROR;
69+
if (t && (typeof t !== "symbol")) {
70+
clearTimeout(t);
71+
}
72+
this.emit("error", er);
73+
this.close();
74+
});
75+
w.on("change", changed);
76+
this.watchers.push(w);
77+
}
6378

6479
// Fire initial time if file exists.
65-
setImmediate(() => changed("rename", base));
80+
setImmediate(() => {
81+
if (this.watchers.length > 0) {
82+
// First watcher will correspond to the directory of the first filename.
83+
const w = this.watchers[0];
84+
w.emit("change", "initial", path.basename([...resolved][0]));
85+
}
86+
});
6687
}
6788

6889
/**
@@ -71,16 +92,31 @@ class Watcher extends EventEmitter {
7192
* @returns {Promise<void>} Always resolves.
7293
*/
7394
close() {
74-
return new Promise(resolve => {
75-
if (this.watcher) {
76-
this.watcher.once("close", resolve);
77-
this.watcher.close();
78-
} else {
79-
resolve();
95+
// Stop any more events from firing, immediately
96+
const t = this.timeout;
97+
98+
if (t) {
99+
if (typeof t !== "symbol") {
100+
this.timeout = CLOSING;
101+
clearTimeout(t);
102+
}
103+
}
104+
105+
const p = [];
106+
for (const w of this.watchers) {
107+
p.push(new Promise(resolve => {
108+
w.once("close", resolve);
109+
}));
110+
w.close();
111+
}
112+
return Promise.all(p).then(() => {
113+
this.watchers = [];
114+
if (t !== ERROR) {
115+
this.emit("close");
80116
}
81-
this.watcher = null;
82117
});
83118
}
84119
}
120+
85121
Watcher.interval = DEBOUNCE_MS;
86122
module.exports = Watcher;

lib/compiler/asts.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,50 @@ const asts = {
8686

8787
return consumes(node);
8888
},
89+
90+
combine(asts) {
91+
let combined = null;
92+
for (const ast of asts) {
93+
if (combined) {
94+
// Note: Change topLevelInitializer and initializer to be an array in
95+
// order to keep the location along with.
96+
if (ast.topLevelInitializer) {
97+
if (combined.topLevelInitializer) {
98+
// Try this and see if it's better than the other way in practice.
99+
const init
100+
= ast.topLevelInitializer
101+
+ "\n"
102+
+ combined.topLevelInitializer;
103+
104+
// Move imports to the top
105+
let imports = "";
106+
let nonImports = "";
107+
for (const line of init.split(/[\r\n]+/)) {
108+
if (/^\s*import/.test(line)) {
109+
imports += line;
110+
} else {
111+
nonImports += line;
112+
}
113+
}
114+
combined.topLevelInitializer = imports + "\n" + nonImports;
115+
} else {
116+
combined.topLevelInitializer = ast.topLevelInitializer;
117+
}
118+
}
119+
if (ast.initializer) {
120+
if (combined.initializer) {
121+
combined.initializer = ast.initializer + "\n" + combined.initializer;
122+
} else {
123+
combined.initializer = ast.initializer;
124+
}
125+
}
126+
combined.rules = combined.rules.concat(ast.rules);
127+
} else {
128+
combined = ast;
129+
}
130+
}
131+
return combined;
132+
},
89133
};
90134

91135
module.exports = asts;

0 commit comments

Comments
 (0)