Skip to content

Commit

Permalink
Generate .d.ts files from CLI on demand. Fixes #477.
Browse files Browse the repository at this point in the history
  • Loading branch information
hildjj committed Jul 18, 2024
1 parent 2dd9c21 commit c10542e
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 30 deletions.
83 changes: 83 additions & 0 deletions bin/generated_template.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
export interface FilePosition {
offset: number;
line: number;
column: number;
}
export interface FileRange {
start: FilePosition;
end: FilePosition;
source: string;
}
export interface LiteralExpectation {
type: "literal";
text: string;
ignoreCase: boolean;
}
export interface ClassParts extends Array<string | ClassParts> {
}
export interface ClassExpectation {
type: "class";
parts: ClassParts;
inverted: boolean;
ignoreCase: boolean;
}
export interface AnyExpectation {
type: "any";
}
export interface EndExpectation {
type: "end";
}
export interface OtherExpectation {
type: "other";
description: string;
}
export type Expectation =
| AnyExpectation
| ClassExpectation
| EndExpectation
| LiteralExpectation
| OtherExpectation;

declare class _PeggySyntaxError extends Error {
static buildMessage(expected: Expectation[], found: string | null): string;
message: string;
expected: Expectation[];
found: string | null;
location: FileRange;
name: string;
constructor(
message: string,
expected: Expectation[],
found: string | null,
location: FileRange,
);
format(sources: {
source?: any;
text: string;
}[]): string;
}
export interface TraceEvent {
type: string;
rule: string;
result?: any;
location: FileRange;
}
export interface ParserTracer {
trace(event: TraceEvent): void;
}

export type StartRules = $$$StartRules$$$;
export interface ParseOptions<T extends StartRules = $$$DefaultStartRule$$$> {
grammarSource?: any;
startRule?: T;
tracer?: ParserTracer;
peg$library?: boolean;
peg$currPos?: number;
peg$silentFails?: number;
peg$maxFailExpected?: Expectation[];
[key: string]: any;
}

export declare const parse: typeof ParseFunction;
export declare const SyntaxError: typeof _PeggySyntaxError;
export type SyntaxError = _PeggySyntaxError;
95 changes: 69 additions & 26 deletions bin/peggy-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ exports.CommanderError = CommanderError;
exports.InvalidArgumentError = InvalidArgumentError;

// Options that aren't for the API directly:
const PROG_OPTIONS = ["ast", "dts", "dtsType", "input", "output", "sourceMap", "startRule", "test", "testFile", "verbose"];
const PROG_OPTIONS = ["ast", "dts", "returnTypes", "input", "output", "sourceMap", "startRule", "test", "testFile", "verbose"];
const MODULE_FORMATS = ["amd", "bare", "commonjs", "es", "globals", "umd"];
const MODULE_FORMATS_WITH_DEPS = ["amd", "commonjs", "es", "umd"];
const MODULE_FORMATS_WITH_GLOBAL = ["globals", "umd"];
Expand All @@ -36,7 +36,7 @@ function isER(er) {
* @returns {er is NodeJS.ErrnoException}
*/
function isErrno(er) {
return (er instanceof Error)
return (typeof er === "object")
&& (Object.prototype.hasOwnProperty.call(er, "code"));
}

Expand Down Expand Up @@ -169,8 +169,8 @@ async function ensureDirectoryExists(filename) {
/**
* @typedef {object} ProgOptions
* @property {boolean} [ast]
* @property {string|boolean} [dts]
* @property {string} [dtsType]
* @property {string} [dts]
* @property {string} [returnTypes]
* @property {string} [input]
* @property {string} [output]
* @property {boolean|string} [sourceMap]
Expand Down Expand Up @@ -222,8 +222,6 @@ class PeggyCLI extends Command {
this.lastError = null;
/** @type {import('./watcher.js')?} */
this.watcher = null;
/** @type {string?} */
this.dtsFile = null;

this
.version(peggy.VERSION, "-v, --version")
Expand Down Expand Up @@ -261,14 +259,9 @@ class PeggyCLI extends Command {
moreJSON
)
.option(
"--dts [filename]",
"--dts",
"Create a .d.ts to describe the generated parser."
)
.option(
"--dtsType <typeInfo>",
"Types returned for rules, as JSON object of the form {\"rule\": \"type\"}",
moreJSON
)
.option(
"-e, --export-var <variable>",
"Name of a global variable into which the parser object is assigned to when no module loader is detected."
Expand Down Expand Up @@ -306,6 +299,11 @@ class PeggyCLI extends Command {
"-m, --source-map [mapfile]",
"Generate a source map. If name is not specified, the source map will be named \"<input_file>.map\" if input is a file and \"source.map\" if input is a standard input. If the special filename `inline` is given, the sourcemap will be embedded in the output file as a data URI. If the filename is prefixed with `hidden:`, no mapping URL will be included so that the mapping can be specified with an HTTP SourceMap: header. This option conflicts with the `-t/--test` and `-T/--test-file` options unless `-o/--output` is also specified"
)
.option(
"--returnTypes <typeInfo>",
"Types returned for rules, as JSON object of the form {\"ruleName\": \"type\"}",
moreJSON
)
.option(
"-S, --start-rule <rule>",
"When testing, use the given rule as the start rule. If this rule is not in the allowed start rules, it will be added."
Expand Down Expand Up @@ -434,9 +432,8 @@ class PeggyCLI extends Command {
this.argv.output = "ast";
if ((this.args.length === 0) && this.progOptions.input) {
// Allow command line to override config file.
this.inputFiles = Array.isArray(this.progOptions.input)
? this.progOptions.input
: [this.progOptions.input];
// It can either be a single string or an array of strings.
this.inputFiles = [this.progOptions.input].flat();
}
this.outputFile = this.progOptions.output;
this.outputJS = this.progOptions.output;
Expand Down Expand Up @@ -473,18 +470,14 @@ class PeggyCLI extends Command {
}

if (this.progOptions.dts) {
if (typeof this.progOptions.dts === "string") {
this.dtsFile = this.progOptions.dts;
} else {
if (this.outputFile === "-") {
this.error("Must supply output file with --dts");
}
this.dtsFile = this.outputFile.slice(
0,
this.outputFile.length
- path.extname(this.outputFile).length
) + ".d.ts";
if (this.outputFile === "-") {
this.error("Must supply output file with --dts");
}
this.dtsFile = this.outputFile.slice(
0,
this.outputFile.length
- path.extname(this.outputFile).length
) + ".d.ts";
}

// If CLI parameter was defined, enable source map generation
Expand Down Expand Up @@ -757,6 +750,52 @@ class PeggyCLI extends Command {
});
}

/**
* @param {import("../lib/peg.js").ast.Grammar} ast
* @returns {Promise<void>}
*/
async writeDTS(ast) {
if (!this.dtsFile) {
return;
}
let template = await fs.promises.readFile(
path.join(__dirname, "generated_template.d.ts"), "utf8"
);
let startRules = (this.argv.allowedStartRules || [ast.rules[0].name]);
if (startRules.includes("*")) {
startRules = ast.rules.map(r => r.name);
}
const qsr = startRules.map(r => `"${r}"`);

template = template.replace("$$$StartRules$$$", qsr.join(" | "));
template = template.replace("$$$DefaultStartRule$$$", qsr[0]);

const out = fs.createWriteStream(this.dtsFile);
out.write(template);

const types = /** @type {Record<string, string>|undefined} */(
this.progOptions.returnTypes
) || {};
for (const sr of startRules) {
out.write(`
export function ParseFunction<Options extends ParseOptions<"${sr}">>(
input: string,
options?: Options,
): ${types[sr] || "any"};
`);
}

await /** @type {Promise<void>} */(new Promise((resolve, reject) => {
out.close(er => {
if (er) {
reject(er);
} else {
resolve();
}
});
}));
}

/**
* @param {string} source
* @returns {Promise<void>}
Expand Down Expand Up @@ -864,6 +903,10 @@ class PeggyCLI extends Command {
this.verbose("CLI", errorText);
await this.writeOutput(outputStream, mappedSource);

errorText = "writing .d.ts file";
this.verbose("CLI", errorText);
await this.writeDTS(source);

exitCode = 2;
errorText = "running test";
this.verbose("CLI", errorText);
Expand Down
3 changes: 3 additions & 0 deletions test/cli/fixtures/options.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ export default {
j: "jest",
commander: "commander",
},
returnTypes: {
foo: "string",
},
};
59 changes: 55 additions & 4 deletions test/cli/run.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,10 +369,8 @@ Options:
-D, --dependencies <json> Dependencies, in JSON object format with
variable:module pairs. (Can be specified
multiple times).
--dts [filename] Create a .d.ts to describe the generated
--dts Create a .d.ts to describe the generated
parser.
--dtsType <typeInfo> Types returned for rules, as JSON object of
the form {"rule": "type"}
-e, --export-var <variable> Name of a global variable into which the
parser object is assigned to when no module
loader is detected.
Expand Down Expand Up @@ -405,6 +403,8 @@ Options:
This option conflicts with the \`-t/--test\`
and \`-T/--test-file\` options unless
\`-o/--output\` is also specified
--returnTypes <typeInfo> Types returned for rules, as JSON object of
the form {"ruleName": "type"}
-S, --start-rule <rule> When testing, use the given rule as the
start rule. If this rule is not in the
allowed start rules, it will be added.
Expand Down Expand Up @@ -1479,5 +1479,56 @@ error: Rule "unknownRule" is not defined
const cli = new PeggyCLI();
expect(() => cli.parse([])).toThrow();
});
});

describe(".d.ts", () => {
const opts = path.join(__dirname, "fixtures", "options.mjs");
const grammar = path.join(__dirname, "fixtures", "simple.peggy");
const grammarJS = path.join(__dirname, "fixtures", "simple.js");
const grammarDTS = path.join(__dirname, "fixtures", "simple.d.ts");

it("creates .d.ts files", async() => {
await exec({
args: ["--dts", grammar],
exitCode: 0,
});
const dts = await fs.promises.readFile(grammarDTS, "utf8");
expect(dts).toMatch(/: any;\n$/);
});

it("uses returnTypes", async() => {
await exec({
args: ["--dts", "-c", opts, grammar],
exitCode: 0,
});
const dts = await fs.promises.readFile(grammarDTS, "utf8");
expect(dts).toMatch(/: string;\n$/);
});

it("generates overloads for allowed-start-rules='*'", async() => {
await exec({
args: ["--dts", "-c", opts, "--allowed-start-rules", "*", grammar],
exitCode: 0,
});
const dts = await fs.promises.readFile(grammarDTS, "utf8");
expect(dts).toMatch(/: string;\n$/);
});

it("errors with dts for stdin", async() => {
await exec({
args: ["--dts"],
stdin: "foo = '1'",
exitCode: 1,
error: /Must supply output file with --dts/,
});
});

afterAll(() => {
fs.unlink(grammarJS, () => {
// Ignored
});
fs.unlink(grammarDTS, () => {
// Ignored
});
});
});
});

0 comments on commit c10542e

Please sign in to comment.