unexpectedly 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,13 +11,20 @@ npm install -g unexpectedly
11
11
  ## Usage
12
12
 
13
13
  ```
14
- unexpectedly [--defaultScript jsPattern] <...file|directory>
15
- jsPattern (default: "../$<base>.js"): find the JS file relative to
16
- the .tests file, replacing `$<base>` with the basename of the test file.
17
- This may be specified multiple times, and modifies all files/directories
18
- after.
19
- file: a file that contains tests
20
- directory (default: "."): a directory containing ".tests" files
14
+ Usage: unexpectedly [options] [...file]
15
+
16
+ Arguments:
17
+ ...file Files or directories to test (default:
18
+ "./test/")
19
+
20
+ Options:
21
+ -V, --version output the version number
22
+ -d,--defaultScript <replacement> Find the script from the file name.
23
+ Replace `$<base>` with the basename of the
24
+ file. (default: "../$<base>.js")
25
+ -f,--function <functionName> Use this function for testing in the
26
+ associated script (default: "test")
27
+ -h, --help display help for command
21
28
  ```
22
29
 
23
30
  ## Example
@@ -63,8 +70,10 @@ unexpectedly tests/
63
70
  ```js
64
71
  import {suite} from 'unexpectedly';
65
72
 
66
- // returns a Promise
67
- suite(directoryOrFile, jsPattern).catch(console.error)
73
+ await suite(directoryOrFile, {
74
+ defaultScript: '../$<base>.js',
75
+ function: 'test',
76
+ }).catch(console.error)
68
77
  ```
69
78
 
70
79
  ## Test language
@@ -101,6 +110,9 @@ Special globals include:
101
110
  instead of the default
102
111
  - "timeout": If any test in the file takes longer than this (in ms),
103
112
  it fails. Default: 2000
113
+ - "peggy": Treat the input as a compiled peggy grammar; look for a "parse"
114
+ function in the input script, provide better errors, and use the value
115
+ of the peggy global (if any) as the startRule.
104
116
 
105
117
  Environment variables can be set for the test code with `#!! name: value`.
106
118
 
@@ -1,23 +1,36 @@
1
1
  #!/usr/bin/env -S node --experimental-vm-modules --no-warnings
2
2
  /* eslint-disable no-console */
3
3
 
4
+ import {Command, Option} from 'commander';
5
+ import fs from 'node:fs/promises';
4
6
  import {suite} from '../index.js';
5
7
 
6
8
  let failures = 0;
7
9
 
8
10
  async function main() {
9
- let defaultScript = undefined;
10
- let nextDefault = false;
11
- for (const arg of process.argv.slice(2)) {
12
- if (nextDefault) {
13
- defaultScript = arg;
14
- nextDefault = false;
15
- } else if (arg === '--defaultScript') {
16
- nextDefault = true;
17
- } else {
18
- // eslint-disable-next-line require-atomic-updates
19
- failures += await suite(arg, defaultScript);
20
- }
11
+ const pkg = JSON.parse(await fs.readFile(
12
+ new URL('../package.json', import.meta.url),
13
+ 'utf-8'
14
+ ));
15
+
16
+ const program = new Command();
17
+ const opts = program
18
+ .version(pkg.version)
19
+ .argument('[...file]', 'Files or directories to test', './test/')
20
+ .option('-d,--defaultScript <replacement>', 'Find the script from the file name. Replace `$<base>` with the basename of the file.', '../$<base>.js')
21
+ .option('-f,--function <functionName>', 'Use this function for testing in the associated script', 'test')
22
+ .addOption(new Option('-s,--silent18', 'Silently skip mjs tests on node 18.').hideHelp())
23
+ .parse()
24
+ .opts();
25
+
26
+ const {args} = program;
27
+ if (args.length === 0) {
28
+ args.push('./test/');
29
+ }
30
+
31
+ for (const arg of args) {
32
+ // eslint-disable-next-line require-atomic-updates
33
+ failures += await suite(arg, opts);
21
34
  }
22
35
  }
23
36
 
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
+ import {Runner, SKIPPED} from './runner.js';
1
2
  import {hexlify, mapObj} from './utils.js';
2
3
  import Mocha from 'mocha';
3
- import {Runner} from './runner.js';
4
4
  import assert from 'node:assert';
5
5
  import fs from 'node:fs/promises';
6
6
  import {parse} from './testFile.peg.js';
@@ -13,6 +13,9 @@ async function coerce(actual, pt, runner) {
13
13
  if (typeof actual === 'string') {
14
14
  return [actual, expected];
15
15
  }
16
+ if (actual === SKIPPED) {
17
+ return [SKIPPED, SKIPPED];
18
+ }
16
19
  if (actual && (typeof actual === 'object') && actual.constructor) {
17
20
  if (/^0x/.test(expected)) {
18
21
  const {name} = actual.constructor;
@@ -42,18 +45,30 @@ async function coerce(actual, pt, runner) {
42
45
  return [actual, def];
43
46
  }
44
47
 
48
+ /**
49
+ * @typedef {object} SuiteOptions
50
+ * @property {string} [defaultScript='../$<base>.js'] Find the script from the
51
+ * file name. Replace `$<base>` with the basename of the file. For
52
+ * example, "foo.test" will use "../foo.js" by default.
53
+ * @property {string} [function='test'] Use this function for testing in the
54
+ * associated script.
55
+ */
56
+
45
57
  /**
46
58
  * Run a test suite from a file or directory.
47
59
  *
48
60
  * @param {string} [target='.'] The file or directory to read. If a
49
61
  * directory, reads all files ending in `.tests`.
50
- * @param {string} [defaultScript='../$<base>.js'] Find the script from the
51
- * file name. Replace $1 with the basename of the file. For example,
52
- * "foo.test" will use "../foo.js" by default.
62
+ * @param {SuiteOptions} [options={}] Options for processing the suite.
53
63
  * @returns {Promise<number>} Promise fulfills on completion, with number of
54
64
  * test failures.
55
65
  */
56
- export async function suite(target = '.', defaultScript = '../$<base>.js') {
66
+ export async function suite(target = '.', options = {}) {
67
+ options = {
68
+ defaultScript: '../$<base>.js',
69
+ function: 'test',
70
+ ...options,
71
+ };
57
72
  let dir = path.resolve(target);
58
73
  let files = [dir];
59
74
  const mocha = new Mocha();
@@ -90,34 +105,39 @@ export async function suite(target = '.', defaultScript = '../$<base>.js') {
90
105
  throw er;
91
106
  }
92
107
 
93
- const opts = {};
94
- let text = '';
95
- if (parsed.vars.inline) {
96
- opts.filename = f;
97
- text = parsed.vars.inline.value;
98
- opts.lineOffset = parsed.vars.inline.line - 1;
99
- opts.columnOffset = parsed.vars.inline.column;
100
- } else {
101
- opts.filename = parsed.vars.script ?
102
- path.resolve(dir, parsed.vars.script.value) :
103
- path.resolve(dir, f.replace(/(?<base>[^./]*)\.tests?$/, defaultScript));
104
- text = await fs.readFile(opts.filename, 'utf8');
105
- }
106
- if (parsed.vars.timeout) {
107
- msuite.timeout(parseInt(parsed.vars.timeout.value, 10));
108
- }
109
- opts.sandbox = mapObj(
110
- Object.entries(parsed.vars).filter(([k, v]) => !v.env),
111
- ([key, value]) => [key, value.value]
112
- );
113
- opts.env = mapObj(
114
- Object.entries(parsed.vars).filter(([k, v]) => v.env),
115
- ([key, value]) => [key, value.value]
116
- );
117
- const runner = new Runner(opts);
108
+ for (const pt of parsed) {
109
+ const opts = {
110
+ testFunction: options.function,
111
+ };
112
+ let text = '';
113
+ if (pt.vars.inline) {
114
+ opts.filename = f;
115
+ text = pt.vars.inline.value;
116
+ opts.lineOffset = pt.vars.inline.line - 1;
117
+ opts.columnOffset = pt.vars.inline.column;
118
+ } else {
119
+ opts.filename = pt.vars.script ?
120
+ path.resolve(dir, pt.vars.script.value) :
121
+ path.resolve(dir,
122
+ f.replace(/(?<base>[^\\/]*)\.tests?$/, options.defaultScript));
123
+ text = await fs.readFile(opts.filename, 'utf8');
124
+ }
125
+ if (pt.vars.timeout) {
126
+ msuite.timeout(parseInt(pt.vars.timeout.value, 10));
127
+ }
128
+ opts.context = mapObj(
129
+ Object.entries(pt.vars).filter(([k, v]) => !v.env),
130
+ ([key, value]) => [key, value.value]
131
+ );
132
+ opts.env = mapObj(
133
+ Object.entries(pt.vars).filter(([k, v]) => v.env),
134
+ ([key, value]) => [key, value.value]
135
+ );
136
+ opts.silent18 = Boolean(options.silent18);
137
+
138
+ const runner = new Runner(opts);
118
139
 
119
- for (const pt of parsed.tests) {
120
- const firstLine = (pt.expected === EXCEPTION) ? '!' : pt.expected.split(/\n/)[0];
140
+ const firstLine = (pt.expected === EXCEPTION) ? `! ${pt.inputs[0]}` : pt.expected.split(/\n/)[0];
121
141
  const t = new Mocha.Test(`line ${pt.line}: ${firstLine || '""'}`, async() => {
122
142
  let actual = null;
123
143
  try {
@@ -125,7 +145,12 @@ export async function suite(target = '.', defaultScript = '../$<base>.js') {
125
145
  __expected: pt.expected,
126
146
  __line: pt.line,
127
147
  __column: pt.column,
148
+ __offset: pt.offset,
128
149
  }, ...pt.inputs);
150
+ if (actual === SKIPPED) {
151
+ t.skip('Node 20.8 required');
152
+ return;
153
+ }
129
154
  } catch (e) {
130
155
  if (pt.expected !== EXCEPTION) {
131
156
  throw e;
@@ -135,6 +160,7 @@ export async function suite(target = '.', defaultScript = '../$<base>.js') {
135
160
  assert.notStrictEqual(pt.expected, EXCEPTION);
136
161
  assert.deepEqual.apply(null, await coerce(actual, pt, runner));
137
162
  });
163
+
138
164
  msuite.addTest(t);
139
165
  }
140
166
  }
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "unexpectedly",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Keep a bunch of test cases in a file, with their expected results",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "build": "peggy -o testFile.peg.js --format es testFile.pegjs",
8
+ "build": "peggy -o testFile.peg.js --format es testFile.peggy",
9
9
  "pretest": "npm run build",
10
10
  "test": "c8 npm run runtests",
11
- "runtests": "mocha && bin/unexpectedly.js test test/pkg_mjs/test test/pkg_cjs/test",
11
+ "runtests": "mocha && bin/unexpectedly.js -s test test/pkg_mjs/test test/pkg_cjs/test",
12
12
  "lint": "eslint --ext js,cjs,mjs . bin/*",
13
13
  "ci": "npm run lint && npm run test",
14
14
  "clean": "rimraf coverage testFile.peg.js"
@@ -32,23 +32,21 @@
32
32
  },
33
33
  "license": "MPL-2.0",
34
34
  "dependencies": {
35
- "mocha": "^10.2.0"
35
+ "@peggyjs/from-mem": "1.3.0",
36
+ "commander": "12.0.0",
37
+ "mocha": "^10.3.0",
38
+ "semver": "7.6.0"
36
39
  },
37
40
  "devDependencies": {
38
- "@cto.af/eslint-config": "3.0.2",
39
- "c8": "8.0.1",
40
- "eslint": "^8.54.0",
41
- "eslint-config-standard": "^17.1.0",
42
- "eslint-plugin-import": "^2.29.0",
43
- "eslint-plugin-jsdoc": "46.9.0",
44
- "eslint-plugin-n": "16.3.1",
45
- "eslint-plugin-node": "^11.1.0",
46
- "eslint-plugin-promise": "^6.1.1",
47
- "eslint-plugin-standard": "^4.1.0",
48
- "peggy": "^3.0.2",
41
+ "@cto.af/eslint-config": "3.1.0",
42
+ "c8": "9.1.0",
43
+ "eslint": "^8.57.0",
44
+ "eslint-plugin-jsdoc": "48.2.0",
45
+ "peggy": "^4.0.2",
49
46
  "rimraf": "5.0.5",
50
47
  "zx": "7.2.3"
51
48
  },
49
+ "packageManager": "pnpm@8.15.4",
52
50
  "engines": {
53
51
  "node": ">=18"
54
52
  }
package/runner.js CHANGED
@@ -1,38 +1,19 @@
1
- import {ModType} from './modtype.js';
2
- import {createRequire} from 'node:module';
3
- import {mapObj} from './utils.js';
1
+ import GrammarLocation from './vendor/peggy/grammar-location.cjs';
2
+ import fromMem from '@peggyjs/from-mem';
4
3
  import path from 'node:path';
5
- import vm from 'node:vm';
4
+ import semver from 'semver';
6
5
 
7
- const vmGlobals = new vm.Script('Object.getOwnPropertyNames(globalThis)')
8
- .runInNewContext()
9
- .filter(f => !f.startsWith('global'));
10
- vmGlobals.push('process', 'Buffer');
6
+ const PREFIX_MJS = 'export default ';
7
+ const PREFIX_CJS = 'module.exports = ';
8
+ const PREFIX_MJS_LEN = PREFIX_MJS.length;
9
+ const PREFIX_CJS_LEN = PREFIX_CJS.length;
11
10
 
12
- async function linker(specifier, referencingModule, cache) {
13
- // Note: not convinced the cache actually does anything, due to timing.
14
- if (cache?.has(specifier)) {
15
- return cache.get(specifier);
16
- }
17
- const m = await import(specifier);
18
- const exportNames = Object.keys(m);
19
- const imported = new vm.SyntheticModule(
20
- exportNames,
21
- // eslint-disable-next-line func-names
22
- function() {
23
- // eslint-disable-next-line no-invalid-this
24
- exportNames.forEach(key => this.setExport(key, m[key]));
25
- },
26
- {identifier: specifier, context: referencingModule.context}
27
- );
11
+ const is20 = semver.satisfies(process.version, '>=20.8');
28
12
 
29
- cache?.set(specifier, imported);
30
- return imported;
31
- }
13
+ export const SKIPPED = Symbol('SKIPPED');
32
14
 
33
15
  /**
34
- * Fancy "eval". This is NOT for adding security, even though we're using
35
- * node:vm. This is about providing:
16
+ * Fancy "eval". This is NOT for adding security. This is about providing:
36
17
  *
37
18
  * - Good backtraces with file/line/column information correct, even when the
38
19
  * code was inlined into a test.
@@ -42,111 +23,130 @@ async function linker(specifier, referencingModule, cache) {
42
23
  */
43
24
  export class Runner {
44
25
  #columnOffset;
26
+ #context;
45
27
  #dirname;
46
28
  #env;
47
29
  #filename;
48
30
  #lineOffset;
49
- #sandbox;
31
+ #silent18;
32
+ #testFunction;
50
33
  #type;
34
+ #skipped;
51
35
 
52
36
  constructor({
53
- filename = null, // Resolved
37
+ filename = null, // Fully-qualified
54
38
  lineOffset = 0,
55
39
  columnOffset = 0,
56
- sandbox = null,
40
+ context = {},
57
41
  env = {},
58
- type = null,
42
+ silent18 = false,
43
+ testFunction = 'test',
44
+ type = 'guess',
59
45
  } = {}) {
46
+ if (typeof filename !== 'string') {
47
+ throw new TypeError('filename not specified');
48
+ }
60
49
  this.#filename = filename;
61
50
  this.#dirname = path.dirname(filename);
62
51
  this.#env = env;
63
- this.#sandbox = sandbox;
52
+ this.#context = context;
64
53
  this.#columnOffset = columnOffset;
65
54
  this.#lineOffset = lineOffset;
55
+ this.#silent18 = silent18;
56
+ this.#skipped = null;
66
57
  this.#type = type;
58
+ this.#testFunction = testFunction;
59
+ }
60
+
61
+ get skipped() {
62
+ return this.#skipped;
67
63
  }
68
64
 
69
65
  async parse(text, extra = {}) {
70
- if (!this.#type) {
71
- this.#type = await ModType.find(this.#filename);
66
+ const format = this.#type === 'guess' ?
67
+ await fromMem.guessModuleType(this.#filename) :
68
+ this.#type;
69
+
70
+ let columnOffset = this.#columnOffset;
71
+ if (format === 'es') {
72
+ if (this.#silent18 && !is20) {
73
+ this.#skipped = true;
74
+ return SKIPPED;
75
+ }
76
+
77
+ if (text.indexOf('export') === -1) {
78
+ text = PREFIX_MJS + text; // Most common case
79
+ columnOffset -= PREFIX_MJS_LEN;
80
+ }
81
+ } else if (text.indexOf('exports') === -1) {
82
+ text = PREFIX_CJS + text; // Most common case
83
+ columnOffset -= PREFIX_CJS_LEN;
72
84
  }
73
- // Find package.json relative to test, decide whether we're commonjs or
74
- // module.
75
- const exports = {};
76
- const context = {
77
- ...mapObj(vmGlobals, g => (
78
- (typeof globalThis[g] === 'function') ?
79
- globalThis[g] :
80
- {...globalThis[g]}
81
- )),
82
- require: createRequire(this.#filename),
83
- exports,
84
- module: {
85
- exports,
85
+
86
+ return await fromMem(text, {
87
+ filename: this.#filename,
88
+ format,
89
+ env: this.#env,
90
+ lineOffset: this.#lineOffset,
91
+ columnOffset,
92
+ context: {
86
93
  filename: this.#filename,
87
94
  id: this.#filename,
88
95
  path: this.#dirname,
96
+ ...this.#context,
97
+ ...extra,
89
98
  },
90
- __filename: this.#filename,
91
- __dirname: this.#dirname,
92
- ...this.#sandbox,
93
- ...extra,
94
- };
95
- context.process.env = this.#env;
96
- context.global = context;
97
- context.globalThis = context;
98
- vm.createContext(context);
99
+ });
100
+ }
99
101
 
100
- let f = null;
101
- const imports = new Map();
102
+ async run(text, extra = {}, ...params) {
103
+ let f = await this.parse(text, extra);
104
+ if (f === SKIPPED) {
105
+ return f;
106
+ }
102
107
 
103
- if (this.#type === 'module') {
104
- if (text.indexOf('export') === -1) {
105
- text = `export default ${text}`; // Most common case
106
- }
108
+ const possibleFunctions = [
109
+ this.#context.function,
110
+ this.#testFunction,
111
+ 'default',
112
+ ];
107
113
 
108
- const mod = new vm.SourceTextModule(text, {
109
- context,
110
- id: this.#filename,
111
- lineOffset: this.#lineOffset,
112
- columnOffset: this.#columnOffset,
114
+ let source = null;
115
+ if ('peggy' in this.#context) {
116
+ source = new GrammarLocation(this.#filename, {
117
+ line: extra.__line,
118
+ column: extra.__column,
119
+ offset: extra.__offset,
113
120
  });
121
+ const popts = {
122
+ grammarSource: source,
123
+ };
124
+ if (this.#context.peggy?.length > 0) {
125
+ popts.startRule = this.#context.peggy;
126
+ }
127
+ params[1] = popts;
128
+ possibleFunctions.splice(1, 0, 'parse');
129
+ }
114
130
 
115
- await mod.link((specifier, referencingModule) => {
116
- if (path.isAbsolute(specifier) || !/^\.\.?\//.test(specifier)) {
117
- return linker(specifier, referencingModule, imports);
131
+ if (f && (typeof f !== 'function')) {
132
+ for (const possible of possibleFunctions) {
133
+ if (possible && (typeof f[possible] === 'function')) {
134
+ f = f[possible];
135
+ break;
118
136
  }
119
- return linker(
120
- path.resolve(path.dirname(this.#filename), specifier),
121
- referencingModule,
122
- imports
123
- );
124
- });
125
- await mod.evaluate();
126
- f = mod.namespace;
127
- } else {
128
- if (text.indexOf('exports') === -1) {
129
- text = `module.exports= ${text}`; // Most common case
130
137
  }
131
-
132
- const script = new vm.Script(text, {
133
- filename: this.#filename,
134
- lineOffset: this.#lineOffset,
135
- columnOffset: this.#columnOffset,
136
- });
137
- f = script.runInContext(context);
138
138
  }
139
- return f;
140
- }
141
-
142
- async run(text, extra = {}, ...params) {
143
- let f = await this.parse(text, extra);
144
139
  if (typeof f !== 'function') {
145
- f = f?.test ?? f?.default;
146
- if (typeof f !== 'function') {
147
- throw new Error('Must export function or {test}');
140
+ throw new Error('Must export function or {test}');
141
+ }
142
+
143
+ try {
144
+ return f(...params);
145
+ } catch (er) {
146
+ if (source && (typeof er.format === 'function')) {
147
+ er.message = er.format([{source, text: params[0]}]);
148
148
  }
149
+ throw er;
149
150
  }
150
- return f(...params);
151
151
  }
152
152
  }
package/testFile.peg.js CHANGED
@@ -1,8 +1,9 @@
1
- // Generated by Peggy 3.0.2.
1
+ // @generated by Peggy 4.0.2.
2
2
  //
3
3
  // https://peggyjs.org/
4
4
 
5
5
 
6
+
6
7
  import { mapObj } from './utils.js'
7
8
 
8
9
  function peg$subclass(child, parent) {
@@ -248,17 +249,22 @@ function peg$parse(input, options) {
248
249
  var peg$e37 = peg$literalExpectation("\r", false);
249
250
 
250
251
  var peg$f0 = function(all) {
251
- const vars = mapObj(all.filter(t => t && t.hasOwnProperty('name')),
252
- ({name, ...rest}) => [name, rest])
253
- return {
254
- tests: all.filter(t => t && t.hasOwnProperty('expected')),
255
- vars
256
- }
252
+ return all.filter(t => t && t.hasOwnProperty('expected'));
257
253
  };
258
254
  var peg$f1 = function(env, name, value) {
259
- value.name = name;
255
+ if (!value) {
256
+ const start = location().start;
257
+ value = {
258
+ value: undefined,
259
+ line: start.line,
260
+ column: start.column,
261
+ }
262
+ }
260
263
  value.env = Boolean(env);
261
- return value;
264
+ vars = {
265
+ ...vars,
266
+ [name]: value
267
+ }
262
268
  };
263
269
  var peg$f2 = function(value) {
264
270
  const start = location().start;
@@ -266,7 +272,7 @@ function peg$parse(input, options) {
266
272
  };
267
273
  var peg$f3 = function(expected, inputs) {
268
274
  const start = location().start;
269
- return { expected, inputs, line: start.line, column: start.column }
275
+ return { expected, inputs, vars, line: start.line, column: start.column }
270
276
  };
271
277
  var peg$f4 = function() { return options.EXCEPTION || "!" };
272
278
  var peg$f5 = function(info) { return info.join('') };
@@ -288,16 +294,16 @@ function peg$parse(input, options) {
288
294
  var peg$f21 = function() { return undefined };
289
295
  var peg$f22 = function() { return undefined};
290
296
  var peg$f23 = function() { return undefined };
291
- var peg$currPos = 0;
292
- var peg$savedPos = 0;
297
+ var peg$currPos = options.peg$currPos | 0;
298
+ var peg$savedPos = peg$currPos;
293
299
  var peg$posDetailsCache = [{ line: 1, column: 1 }];
294
- var peg$maxFailPos = 0;
295
- var peg$maxFailExpected = [];
296
- var peg$silentFails = 0;
300
+ var peg$maxFailPos = peg$currPos;
301
+ var peg$maxFailExpected = options.peg$maxFailExpected || [];
302
+ var peg$silentFails = options.peg$silentFails | 0;
297
303
 
298
304
  var peg$result;
299
305
 
300
- if ("startRule" in options) {
306
+ if (options.startRule) {
301
307
  if (!(options.startRule in peg$startRuleFunctions)) {
302
308
  throw new Error("Can't start parsing from rule \"" + options.startRule + "\".");
303
309
  }
@@ -372,9 +378,11 @@ function peg$parse(input, options) {
372
378
  if (details) {
373
379
  return details;
374
380
  } else {
375
- p = pos - 1;
376
- while (!peg$posDetailsCache[p]) {
377
- p--;
381
+ if (pos >= peg$posDetailsCache.length) {
382
+ p = peg$posDetailsCache.length - 1;
383
+ } else {
384
+ p = pos;
385
+ while (!peg$posDetailsCache[--p]) {}
378
386
  }
379
387
 
380
388
  details = peg$posDetailsCache[p];
@@ -504,7 +512,7 @@ function peg$parse(input, options) {
504
512
  }
505
513
 
506
514
  function peg$parsevar() {
507
- var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9;
515
+ var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10;
508
516
 
509
517
  s0 = peg$currPos;
510
518
  if (input.substr(peg$currPos, 2) === peg$c0) {
@@ -538,15 +546,20 @@ function peg$parse(input, options) {
538
546
  }
539
547
  if (s6 !== peg$FAILED) {
540
548
  s7 = peg$parse_();
541
- s8 = peg$parseval();
542
- if (s8 !== peg$FAILED) {
543
- s9 = peg$parse_();
544
- peg$savedPos = s0;
545
- s0 = peg$f1(s2, s4, s8);
549
+ s8 = peg$currPos;
550
+ s9 = peg$parseval();
551
+ if (s9 !== peg$FAILED) {
552
+ s10 = peg$parse_();
553
+ s8 = s9;
546
554
  } else {
547
- peg$currPos = s0;
548
- s0 = peg$FAILED;
555
+ peg$currPos = s8;
556
+ s8 = peg$FAILED;
549
557
  }
558
+ if (s8 === peg$FAILED) {
559
+ s8 = null;
560
+ }
561
+ peg$savedPos = s0;
562
+ s0 = peg$f1(s2, s4, s8);
550
563
  } else {
551
564
  peg$currPos = s0;
552
565
  s0 = peg$FAILED;
@@ -798,8 +811,8 @@ function peg$parse(input, options) {
798
811
  if (s0 === peg$FAILED) {
799
812
  s0 = peg$currPos;
800
813
  s1 = [];
801
- if (peg$r0.test(input.charAt(peg$currPos))) {
802
- s2 = input.charAt(peg$currPos);
814
+ s2 = input.charAt(peg$currPos);
815
+ if (peg$r0.test(s2)) {
803
816
  peg$currPos++;
804
817
  } else {
805
818
  s2 = peg$FAILED;
@@ -808,8 +821,8 @@ function peg$parse(input, options) {
808
821
  if (s2 !== peg$FAILED) {
809
822
  while (s2 !== peg$FAILED) {
810
823
  s1.push(s2);
811
- if (peg$r0.test(input.charAt(peg$currPos))) {
812
- s2 = input.charAt(peg$currPos);
824
+ s2 = input.charAt(peg$currPos);
825
+ if (peg$r0.test(s2)) {
813
826
  peg$currPos++;
814
827
  } else {
815
828
  s2 = peg$FAILED;
@@ -857,8 +870,8 @@ function peg$parse(input, options) {
857
870
  if (s0 === peg$FAILED) {
858
871
  s0 = peg$currPos;
859
872
  s1 = [];
860
- if (peg$r1.test(input.charAt(peg$currPos))) {
861
- s2 = input.charAt(peg$currPos);
873
+ s2 = input.charAt(peg$currPos);
874
+ if (peg$r1.test(s2)) {
862
875
  peg$currPos++;
863
876
  } else {
864
877
  s2 = peg$FAILED;
@@ -867,8 +880,8 @@ function peg$parse(input, options) {
867
880
  if (s2 !== peg$FAILED) {
868
881
  while (s2 !== peg$FAILED) {
869
882
  s1.push(s2);
870
- if (peg$r1.test(input.charAt(peg$currPos))) {
871
- s2 = input.charAt(peg$currPos);
883
+ s2 = input.charAt(peg$currPos);
884
+ if (peg$r1.test(s2)) {
872
885
  peg$currPos++;
873
886
  } else {
874
887
  s2 = peg$FAILED;
@@ -916,8 +929,8 @@ function peg$parse(input, options) {
916
929
  if (s0 === peg$FAILED) {
917
930
  s0 = peg$currPos;
918
931
  s1 = [];
919
- if (peg$r2.test(input.charAt(peg$currPos))) {
920
- s2 = input.charAt(peg$currPos);
932
+ s2 = input.charAt(peg$currPos);
933
+ if (peg$r2.test(s2)) {
921
934
  peg$currPos++;
922
935
  } else {
923
936
  s2 = peg$FAILED;
@@ -926,8 +939,8 @@ function peg$parse(input, options) {
926
939
  if (s2 !== peg$FAILED) {
927
940
  while (s2 !== peg$FAILED) {
928
941
  s1.push(s2);
929
- if (peg$r2.test(input.charAt(peg$currPos))) {
930
- s2 = input.charAt(peg$currPos);
942
+ s2 = input.charAt(peg$currPos);
943
+ if (peg$r2.test(s2)) {
931
944
  peg$currPos++;
932
945
  } else {
933
946
  s2 = peg$FAILED;
@@ -967,8 +980,8 @@ function peg$parse(input, options) {
967
980
  if (s1 !== peg$FAILED) {
968
981
  s2 = peg$currPos;
969
982
  s3 = [];
970
- if (peg$r3.test(input.charAt(peg$currPos))) {
971
- s4 = input.charAt(peg$currPos);
983
+ s4 = input.charAt(peg$currPos);
984
+ if (peg$r3.test(s4)) {
972
985
  peg$currPos++;
973
986
  } else {
974
987
  s4 = peg$FAILED;
@@ -977,8 +990,8 @@ function peg$parse(input, options) {
977
990
  if (s4 !== peg$FAILED) {
978
991
  while (s4 !== peg$FAILED) {
979
992
  s3.push(s4);
980
- if (peg$r3.test(input.charAt(peg$currPos))) {
981
- s4 = input.charAt(peg$currPos);
993
+ s4 = input.charAt(peg$currPos);
994
+ if (peg$r3.test(s4)) {
982
995
  peg$currPos++;
983
996
  } else {
984
997
  s4 = peg$FAILED;
@@ -1048,8 +1061,8 @@ function peg$parse(input, options) {
1048
1061
  if (s0 === peg$FAILED) {
1049
1062
  s0 = peg$currPos;
1050
1063
  s1 = [];
1051
- if (peg$r4.test(input.charAt(peg$currPos))) {
1052
- s2 = input.charAt(peg$currPos);
1064
+ s2 = input.charAt(peg$currPos);
1065
+ if (peg$r4.test(s2)) {
1053
1066
  peg$currPos++;
1054
1067
  } else {
1055
1068
  s2 = peg$FAILED;
@@ -1058,8 +1071,8 @@ function peg$parse(input, options) {
1058
1071
  if (s2 !== peg$FAILED) {
1059
1072
  while (s2 !== peg$FAILED) {
1060
1073
  s1.push(s2);
1061
- if (peg$r4.test(input.charAt(peg$currPos))) {
1062
- s2 = input.charAt(peg$currPos);
1074
+ s2 = input.charAt(peg$currPos);
1075
+ if (peg$r4.test(s2)) {
1063
1076
  peg$currPos++;
1064
1077
  } else {
1065
1078
  s2 = peg$FAILED;
@@ -1201,7 +1214,7 @@ function peg$parse(input, options) {
1201
1214
  }
1202
1215
 
1203
1216
  function peg$parsecomment() {
1204
- var s0, s1, s2, s3, s4;
1217
+ var s0, s1, s2, s3, s4, s5;
1205
1218
 
1206
1219
  s0 = peg$currPos;
1207
1220
  s1 = peg$parseWS();
@@ -1213,26 +1226,47 @@ function peg$parse(input, options) {
1213
1226
  if (peg$silentFails === 0) { peg$fail(peg$e27); }
1214
1227
  }
1215
1228
  if (s2 !== peg$FAILED) {
1216
- s3 = [];
1217
- if (peg$r5.test(input.charAt(peg$currPos))) {
1218
- s4 = input.charAt(peg$currPos);
1229
+ s3 = peg$currPos;
1230
+ peg$silentFails++;
1231
+ if (input.charCodeAt(peg$currPos) === 33) {
1232
+ s4 = peg$c1;
1219
1233
  peg$currPos++;
1220
1234
  } else {
1221
1235
  s4 = peg$FAILED;
1222
- if (peg$silentFails === 0) { peg$fail(peg$e28); }
1236
+ if (peg$silentFails === 0) { peg$fail(peg$e1); }
1223
1237
  }
1224
- while (s4 !== peg$FAILED) {
1225
- s3.push(s4);
1226
- if (peg$r5.test(input.charAt(peg$currPos))) {
1227
- s4 = input.charAt(peg$currPos);
1238
+ peg$silentFails--;
1239
+ if (s4 === peg$FAILED) {
1240
+ s3 = undefined;
1241
+ } else {
1242
+ peg$currPos = s3;
1243
+ s3 = peg$FAILED;
1244
+ }
1245
+ if (s3 !== peg$FAILED) {
1246
+ s4 = [];
1247
+ s5 = input.charAt(peg$currPos);
1248
+ if (peg$r5.test(s5)) {
1228
1249
  peg$currPos++;
1229
1250
  } else {
1230
- s4 = peg$FAILED;
1251
+ s5 = peg$FAILED;
1231
1252
  if (peg$silentFails === 0) { peg$fail(peg$e28); }
1232
1253
  }
1254
+ while (s5 !== peg$FAILED) {
1255
+ s4.push(s5);
1256
+ s5 = input.charAt(peg$currPos);
1257
+ if (peg$r5.test(s5)) {
1258
+ peg$currPos++;
1259
+ } else {
1260
+ s5 = peg$FAILED;
1261
+ if (peg$silentFails === 0) { peg$fail(peg$e28); }
1262
+ }
1263
+ }
1264
+ peg$savedPos = s0;
1265
+ s0 = peg$f20();
1266
+ } else {
1267
+ peg$currPos = s0;
1268
+ s0 = peg$FAILED;
1233
1269
  }
1234
- peg$savedPos = s0;
1235
- s0 = peg$f20();
1236
1270
  } else {
1237
1271
  peg$currPos = s0;
1238
1272
  s0 = peg$FAILED;
@@ -1247,8 +1281,8 @@ function peg$parse(input, options) {
1247
1281
  peg$silentFails++;
1248
1282
  s0 = peg$currPos;
1249
1283
  s1 = [];
1250
- if (peg$r6.test(input.charAt(peg$currPos))) {
1251
- s2 = input.charAt(peg$currPos);
1284
+ s2 = input.charAt(peg$currPos);
1285
+ if (peg$r6.test(s2)) {
1252
1286
  peg$currPos++;
1253
1287
  } else {
1254
1288
  s2 = peg$FAILED;
@@ -1256,8 +1290,8 @@ function peg$parse(input, options) {
1256
1290
  }
1257
1291
  while (s2 !== peg$FAILED) {
1258
1292
  s1.push(s2);
1259
- if (peg$r6.test(input.charAt(peg$currPos))) {
1260
- s2 = input.charAt(peg$currPos);
1293
+ s2 = input.charAt(peg$currPos);
1294
+ if (peg$r6.test(s2)) {
1261
1295
  peg$currPos++;
1262
1296
  } else {
1263
1297
  s2 = peg$FAILED;
@@ -1280,8 +1314,8 @@ function peg$parse(input, options) {
1280
1314
  peg$silentFails++;
1281
1315
  s0 = peg$currPos;
1282
1316
  s1 = [];
1283
- if (peg$r7.test(input.charAt(peg$currPos))) {
1284
- s2 = input.charAt(peg$currPos);
1317
+ s2 = input.charAt(peg$currPos);
1318
+ if (peg$r7.test(s2)) {
1285
1319
  peg$currPos++;
1286
1320
  } else {
1287
1321
  s2 = peg$FAILED;
@@ -1289,8 +1323,8 @@ function peg$parse(input, options) {
1289
1323
  }
1290
1324
  while (s2 !== peg$FAILED) {
1291
1325
  s1.push(s2);
1292
- if (peg$r7.test(input.charAt(peg$currPos))) {
1293
- s2 = input.charAt(peg$currPos);
1326
+ s2 = input.charAt(peg$currPos);
1327
+ if (peg$r7.test(s2)) {
1294
1328
  peg$currPos++;
1295
1329
  } else {
1296
1330
  s2 = peg$FAILED;
@@ -1313,8 +1347,8 @@ function peg$parse(input, options) {
1313
1347
  peg$silentFails++;
1314
1348
  s0 = peg$currPos;
1315
1349
  s1 = [];
1316
- if (peg$r6.test(input.charAt(peg$currPos))) {
1317
- s2 = input.charAt(peg$currPos);
1350
+ s2 = input.charAt(peg$currPos);
1351
+ if (peg$r6.test(s2)) {
1318
1352
  peg$currPos++;
1319
1353
  } else {
1320
1354
  s2 = peg$FAILED;
@@ -1323,8 +1357,8 @@ function peg$parse(input, options) {
1323
1357
  if (s2 !== peg$FAILED) {
1324
1358
  while (s2 !== peg$FAILED) {
1325
1359
  s1.push(s2);
1326
- if (peg$r6.test(input.charAt(peg$currPos))) {
1327
- s2 = input.charAt(peg$currPos);
1360
+ s2 = input.charAt(peg$currPos);
1361
+ if (peg$r6.test(s2)) {
1328
1362
  peg$currPos++;
1329
1363
  } else {
1330
1364
  s2 = peg$FAILED;
@@ -1386,8 +1420,20 @@ function peg$parse(input, options) {
1386
1420
  return s0;
1387
1421
  }
1388
1422
 
1423
+
1424
+ let vars = {};
1425
+
1389
1426
  peg$result = peg$startRuleFunction();
1390
1427
 
1428
+ if (options.peg$library) {
1429
+ return /** @type {any} */ ({
1430
+ peg$result,
1431
+ peg$currPos,
1432
+ peg$FAILED,
1433
+ peg$maxFailExpected,
1434
+ peg$maxFailPos
1435
+ });
1436
+ }
1391
1437
  if (peg$result !== peg$FAILED && peg$currPos === input.length) {
1392
1438
  return peg$result;
1393
1439
  } else {
@@ -1405,8 +1451,12 @@ function peg$parse(input, options) {
1405
1451
  }
1406
1452
  }
1407
1453
 
1454
+ const peg$allowedStartRules = [
1455
+ "start"
1456
+ ];
1457
+
1408
1458
  export {
1459
+ peg$allowedStartRules as StartRules,
1409
1460
  peg$SyntaxError as SyntaxError,
1410
-
1411
1461
  peg$parse as parse
1412
1462
  };
package/testFile.peggy ADDED
@@ -0,0 +1,111 @@
1
+ {{
2
+ import { mapObj } from './utils.js'
3
+ }}
4
+
5
+ {
6
+ let vars = {};
7
+ }
8
+ start
9
+ = all:(WS @line? _)|.., EOL| {
10
+ return all.filter(t => t && t.hasOwnProperty('expected'));
11
+ }
12
+
13
+ line
14
+ = var
15
+ / comment
16
+ / test
17
+ / '' // hack
18
+
19
+ var
20
+ = '#!' env:"!"? _ name:chunk _ ':' _ value:(@val _)? {
21
+ if (!value) {
22
+ const start = location().start;
23
+ value = {
24
+ value: undefined,
25
+ line: start.line,
26
+ column: start.column,
27
+ }
28
+ }
29
+ value.env = Boolean(env);
30
+ vars = {
31
+ ...vars,
32
+ [name]: value
33
+ }
34
+ }
35
+
36
+ val
37
+ = value:chunk {
38
+ const start = location().start;
39
+ return { value, line: start.line, column: start.column}
40
+ }
41
+
42
+ test
43
+ = expected:results inputs:(LWS @chunk)* comment? {
44
+ const start = location().start;
45
+ return { expected, inputs, vars, line: start.line, column: start.column }
46
+ }
47
+
48
+ results
49
+ = "!" { return options.EXCEPTION || "!" }
50
+ / chunk
51
+
52
+ chunk
53
+ = quoted
54
+ / hex
55
+ / word
56
+
57
+ quoted
58
+ = '"' info:dqchar* '"' { return info.join('') }
59
+ / "'" info:sqchar* "'" { return info.join('') }
60
+ / "`" info:bqchar* "`" { return info.join('') }
61
+
62
+ dqchar "characters that go inside double quotes, including escaped dquotes"
63
+ = '\\"' { return '"' }
64
+ / qchar
65
+ / $[^\\"]+
66
+
67
+ sqchar "characters that go inside single quotes, including escaped squotes"
68
+ = "\\'" { return "'" }
69
+ / qchar
70
+ / $[^\\']+
71
+
72
+ bqchar "characters that go inside back quotes, including escaped bquotes"
73
+ = "\\`" { return "`" }
74
+ / qchar
75
+ / $[^\\`]+
76
+
77
+ hex
78
+ = '0x' hx:$[a-fA-F0-9]+ { return '0x' + hx.toLowerCase(hx) }
79
+
80
+ word
81
+ = chars:wchar+ { return chars.join('') }
82
+
83
+ wchar "words can contain quoted bits"
84
+ = qchar+
85
+ / $[^ #:'"\t\r\n]+
86
+
87
+ qchar "other quoted characters, as needed"
88
+ = '\\\\' { return '\\' }
89
+ / '\\r' { return '\r' }
90
+ / '\\n' { return '\n' }
91
+ / '\\t' { return '\t' }
92
+ / '\\#' { return '#' }
93
+ / '\\:' { return ':' }
94
+ / '\\ ' { return ' ' }
95
+
96
+ comment
97
+ = WS "#" !"!" [^\r\n]* { return undefined }
98
+
99
+ _ "optional whitespace"
100
+ = [ \t]* { return undefined }
101
+
102
+ WS "optional whitespace, including newlines"
103
+ = [ \t\r\n]* { return undefined}
104
+
105
+ LWS "required whitespace"
106
+ = [ \t]+ { return undefined }
107
+
108
+ EOL "End of line"
109
+ = '\n'
110
+ / '\r\n'
111
+ / '\r'
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2010-2024 The Peggy AUTHORS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * When used as a grammarSource, allows grammars embedded in larger files to
5
+ * specify their offset. The start location is the first character in the
6
+ * grammar. The first line is often moved to the right by some number of
7
+ * columns, but subsequent lines all start at the first column.
8
+ */
9
+ class GrammarLocation {
10
+ /**
11
+ * Create an instance.
12
+ *
13
+ * @param {any} source The original grammarSource. Should be a string or
14
+ * have a toString() method.
15
+ * @param {import("./peg").Location} start The starting offset for the
16
+ * grammar in the larger file.
17
+ */
18
+ constructor(source, start) {
19
+ this.source = source;
20
+ this.start = start;
21
+ }
22
+
23
+ /**
24
+ * Coerce to a string.
25
+ *
26
+ * @returns {string} The source, stringified.
27
+ */
28
+ toString() {
29
+ return String(this.source);
30
+ }
31
+
32
+ /**
33
+ * Return a new Location offset from the given location by the start of the
34
+ * grammar.
35
+ *
36
+ * @param {import("./peg").Location} loc The location as if the start of the
37
+ * grammar was the start of the file.
38
+ * @returns {import("./peg").Location} The offset location.
39
+ */
40
+ offset(loc) {
41
+ return {
42
+ line: loc.line + this.start.line - 1,
43
+ column: (loc.line === 1)
44
+ ? loc.column + this.start.column - 1
45
+ : loc.column,
46
+ offset: loc.offset + this.start.offset,
47
+ };
48
+ }
49
+
50
+ /**
51
+ * If the range has a grammarSource that is a GrammarLocation, offset the
52
+ * start of that range by the GrammarLocation.
53
+ *
54
+ * @param {import("./peg").LocationRange} range The range to extract from.
55
+ * @returns {import("./peg").Location} The offset start if possible, or the
56
+ * original start.
57
+ */
58
+ static offsetStart(range) {
59
+ if (range.source && (typeof range.source.offset === "function")) {
60
+ return range.source.offset(range.start);
61
+ }
62
+ return range.start;
63
+ }
64
+
65
+ /**
66
+ * If the range has a grammarSource that is a GrammarLocation, offset the
67
+ * end of that range by the GrammarLocation.
68
+ *
69
+ * @param {import("./peg").LocationRange} range The range to extract from.
70
+ * @returns {import("./peg").Location} The offset end if possible, or the
71
+ * original end.
72
+ */
73
+ static offsetEnd(range) {
74
+ if (range.source && (typeof range.source.offset === "function")) {
75
+ return range.source.offset(range.end);
76
+ }
77
+ return range.end;
78
+ }
79
+ }
80
+
81
+ module.exports = GrammarLocation;
@@ -0,0 +1,4 @@
1
+ I don't want to take a runtime dependency on Peggy just for the
2
+ GrammarLocation class. If this works, I may refactor that into
3
+ @peggyjs/grammar-location, but I'm worried about making that change completely
4
+ transparent to Peggy users.
package/modtype.js DELETED
@@ -1,60 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
-
4
- export class ModType {
5
- static #cache = new Map(); // Filename => 'commonjs', 'module', 'unknown'
6
-
7
- static async #readPkg(dir) {
8
- const pkg = path.join(dir, 'package.json');
9
- try {
10
- const {type = 'commonjs'} = JSON.parse(await fs.readFile(pkg, 'utf8'));
11
- return type;
12
- } catch {
13
- return undefined;
14
- }
15
- }
16
-
17
- static async find(name, dir = false, root = undefined) {
18
- const prev = this.#cache.get(name);
19
- if (prev) {
20
- return prev;
21
- }
22
- let res = 'unknown';
23
-
24
- if (!root) {
25
- const p = path.parse(name);
26
- switch (p.ext) {
27
- case '.mjs':
28
- res = 'module';
29
- this.#cache.set(name, res);
30
- return res;
31
- case '.cjs':
32
- res = 'commonjs';
33
- this.#cache.set(name, res);
34
- return res;
35
- }
36
- ({root} = p);
37
- try {
38
- // Avoid problems if an existing directory name is passed in originally.
39
- dir = (await fs.stat(name)).isDirectory();
40
- } catch {
41
- // File doesn't exist. Safe to assume it's file-ish, keep dir = false.
42
- }
43
- }
44
-
45
- if (name !== root) { // Never go all the way to the root directory.
46
- const t = dir ? await this.#readPkg(name) : undefined;
47
- res = t ?? this.find(path.dirname(name), true, root);
48
- }
49
- this.#cache.set(name, res);
50
- return res;
51
- }
52
-
53
- static clear() {
54
- this.#cache.clear();
55
- }
56
-
57
- static get cacheSize() {
58
- return this.#cache.size;
59
- }
60
- }