Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ Lumi Pakkanen <lumi.pakkanen@gmail.com> (https://github.com/frostburn/)
Steven Spungin <steven@spungin.tv> (https://github.com/flamenco/)
XenoS (https://github.com/XenoS-ITA)
Samuel Bronson (https://github.com/SamB)
knoan (https://github.com/knoan)
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Released: TBD

### New features

- Added `--multi-output <dir>` CLI option to compile multiple grammars to
separate output files instead of merging them.
[#650](https://github.com/peggyjs/peggy/issues/650)

### Bug fixes

5.0.6
Expand Down
10 changes: 10 additions & 0 deletions bin/opts.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const PROG_OPTIONS = [
"extraOptionsFile",
"input",
"library",
"multiOutput",
"output",
"plugin",
"returnTypes",
Expand All @@ -43,7 +44,9 @@ const PROG_OPTIONS = [
* @property {string|string[]} [input]
* @property {string[]} inputFiles
* @property {boolean} [library]
* @property {string} [multiOutput]
* @property {string} [output]
* @property {string} [outputDir]
* @property {string[]} [plugin]
* @property {string} [returnTypes]
* @property {boolean|string} [sourceMap]
Expand Down Expand Up @@ -287,6 +290,13 @@ async function refineOptions(cmd, inputFiles, cliOptions) {
cmd.error("Can't watch stdin");
}

if (progOptions.multiOutput) {
if (progOptions.inputFiles.includes("-")) {
cmd.error("Can't use --multi-output with stdin input");
}
progOptions.outputDir = progOptions.multiOutput;
}

if (!progOptions.outputFile) {
if (!progOptions.inputFiles.includes("-")) {
let inFile = progOptions.inputFiles[0];
Expand Down
49 changes: 48 additions & 1 deletion bin/peggy-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const {
addExtraOptionsJSON, refineOptions,
} = require("./opts.js");
const {
isER, commaArg, moreJSON, readStream, mkFileDir,
isER, commaArg, moreJSON, readStream, mkFileDir, replaceExt,
} = require("./utils.js");
const Module = require("module");
const assert = require("assert");
Expand Down Expand Up @@ -151,6 +151,10 @@ class PeggyCLI extends Command {
});
}).hideHelp())
.option("-o, --output <file>", "Output file for generated parser. Use '-' for stdout (the default is a file next to the input file with the extension change to '.js', unless a test is specified, in which case no parser is output without this option)")
.addOption(
new Option("--multi-output <dir>", "Output directory for separate parser files (one per input grammar)")
.conflicts("output")
)
.option(
"--plugin <module>",
"Comma-separated list of plugins. (can be specified multiple times)",
Expand Down Expand Up @@ -556,6 +560,49 @@ return util.inspect(results, {
}
};

// Multi-output mode: compile each grammar separately
if (this.progOptions.outputDir) {
for (const singleSource of sources) {
const baseName = path.basename(
singleSource.source,
path.extname(singleSource.source)
);
const outFile = path.join(this.progOptions.outputDir, baseName + ".js");

// Set per-file output paths
this.progOptions.outputFile = outFile;
this.progOptions.outputJS = outFile;
if (this.progOptions.dts) {
this.progOptions.dtsFile = replaceExt(outFile, ".d.ts");
}
if (this.progOptions.sourceMap !== undefined) {
this.progOptions.sourceMap = outFile + ".map";
}

this.parserOptions.grammarSource = singleSource.source;

errorText = `generating parser for "${singleSource.source}"`;
this.verbose("CLI", errorText);
const generated = peggy.generate([singleSource], this.parserOptions);

errorText = `writing output "${outFile}"`;
this.verbose("CLI", errorText);
await mkFileDir(outFile);
const outputStream = fs.createWriteStream(outFile);

if (this.progOptions.ast) {
const json = JSON.stringify(generated, null, 2);
await this.writeOutput(outputStream, json);
} else {
assert(generated.code);
const mappedSource = await this.writeSourceMap(generated.code);
await this.writeOutput(outputStream, mappedSource);
await this.writeDTS(generated);
}
}
return 0;
}

const source = peggy.generate(
sources,
this.parserOptions
Expand Down
6 changes: 6 additions & 0 deletions docs/documentation.html
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@ <h3 id="generating-a-parser-command-line">Command Line</h3>
<dd>File to send output to. Defaults to input file name with
extension changed to <code>.js</code>, or stdout if no input file is given.</dd>

<dt><code>--multi-output &lt;dir&gt;</code></dt>
<dd>Output directory for separate parser files. When specified, each input
grammar is compiled to a separate output file in the given directory
instead of being merged into a single parser. Cannot be used with
<code>-o/--output</code> or stdin input.</dd>

<dt><code>--plugin</code></dt>
<dd>Makes Peggy use a specified plugin (can be specified multiple
times).</dd>
Expand Down
87 changes: 87 additions & 0 deletions test/cli/run.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,8 @@ Options:
'.js', unless a test is specified, in which
case no parser is output without this
option)
--multi-output <dir> Output directory for separate parser files
(one per input grammar)
--plugin <module> Comma-separated list of plugins. (can be
specified multiple times)
-m, --source-map [mapfile] Generate a source map. If name is not
Expand Down Expand Up @@ -1606,4 +1608,89 @@ error: WARN(check): An expression may not consume any input and may always match
});
});
});

describe("--multi-output", () => {
const grammar1 = path.join(tmpDir, "multi1.peggy");
const grammar2 = path.join(tmpDir, "multi2.peggy");
let multiOutDir = "";

beforeAll(async () => {
await fs.promises.writeFile(grammar1, "start = 'hello'");
await fs.promises.writeFile(grammar2, "start = 'world'");
multiOutDir = path.join(tmpDir, "multi-out");
await fs.promises.mkdir(multiOutDir, { recursive: true });
});

afterEach(async () => {
// Clean up generated files
const files = await fs.promises.readdir(multiOutDir);
for (const f of files) {
await fs.promises.unlink(path.join(multiOutDir, f));
}
});

it("compiles multiple grammars to separate files", async () => {
await exec({
args: ["--multi-output", multiOutDir, grammar1, grammar2],
exitCode: 0,
});
const out1 = path.join(multiOutDir, "multi1.js");
const out2 = path.join(multiOutDir, "multi2.js");
expect(fs.existsSync(out1)).toBe(true);
expect(fs.existsSync(out2)).toBe(true);
});

it("generates DTS per file when --dts enabled", async () => {
await exec({
args: ["--multi-output", multiOutDir, "--dts", grammar1, grammar2],
exitCode: 0,
});
expect(fs.existsSync(path.join(multiOutDir, "multi1.d.ts"))).toBe(true);
expect(fs.existsSync(path.join(multiOutDir, "multi2.d.ts"))).toBe(true);
});

it("generates source maps per file when --source-map enabled", async () => {
await exec({
args: [
"--multi-output", multiOutDir, "--source-map",
"--", grammar1, grammar2,
],
exitCode: 0,
});
expect(fs.existsSync(path.join(multiOutDir, "multi1.js.map"))).toBe(true);
expect(fs.existsSync(path.join(multiOutDir, "multi2.js.map"))).toBe(true);
});

it("outputs AST JSON per file when --ast enabled", async () => {
await exec({
args: ["--multi-output", multiOutDir, "--ast", grammar1, grammar2],
exitCode: 0,
});
const ast1 = JSON.parse(
fs.readFileSync(path.join(multiOutDir, "multi1.js"), "utf8")
);
const ast2 = JSON.parse(
fs.readFileSync(path.join(multiOutDir, "multi2.js"), "utf8")
);
expect(ast1.type).toBe("grammar");
expect(ast2.type).toBe("grammar");
});

it("errors when combined with --output", async () => {
await exec({
args: ["--multi-output", multiOutDir, "--output", "foo.js", grammar1],
exitCode: 1,
error: /cannot be used with/,
});
});

it("errors when used with stdin", async () => {
await exec({
args: ["--multi-output", multiOutDir, "-"],
stdin: "start = '1'",
exitCode: 1,
error: /Can't use --multi-output with stdin/,
});
});
});
});