Skip to content

Commit 21d9456

Browse files
authored
[74] E2015 target fix (#75)
E2015 fix
1 parent 93de528 commit 21d9456

5 files changed

Lines changed: 157 additions & 2 deletions

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* Verifies the built `lib/` output, in two passes. Called by the CI workflow
3+
* after `prepublishOnly`:
4+
*
5+
* npm run prepublishOnly && npm run ci:smoke-test-built-package
6+
*
7+
* First, every emitted file is parsed at the ECMAScript version the README
8+
* states the package is compiled to, so syntax newer than that floor fails the
9+
* build. Node is far newer than the floor and will happily run output the
10+
* stated environment could not, so this is checked statically rather than by
11+
* executing under an older engine — no ESM-capable runtime is that old.
12+
*
13+
* Second, every entry point declared in package.json "exports" is loaded under
14+
* both `import` and `require`, and asserted to behave. The unit tests run
15+
* against `src/`, so they cannot see defects introduced by the build itself —
16+
* v1.1.1 shipped a `lib/` that threw `SyntaxError: 'super' keyword unexpected
17+
* here` on load while all 408 tests passed. Specifiers are resolved by name
18+
* rather than by path so that Node applies the real "exports" map, exactly as
19+
* a consumer would.
20+
*
21+
* Adding an entry point to "exports" without adding a case to SMOKE_TESTS
22+
* fails this script.
23+
*/
24+
25+
import assert from "node:assert/strict";
26+
import { readdir, readFile } from "node:fs/promises";
27+
import { createRequire } from "node:module";
28+
import { Linter } from "eslint";
29+
30+
const SUPPORTED_ECMA_VERSION = 2015;
31+
32+
const BUILT_OUTPUT = new URL("../../lib/", import.meta.url);
33+
34+
type LoadedModule = Record<string, unknown>;
35+
36+
type PartialMatchRegExpConstructor = new (
37+
pattern: RegExp | string,
38+
flags?: string
39+
) => RegExp;
40+
41+
const SMOKE_TESTS: Record<string, (loaded: LoadedModule) => void> = {
42+
".": (loaded) => {
43+
const PartialMatchRegExp =
44+
loaded.default as PartialMatchRegExpConstructor | undefined;
45+
assert.ok(PartialMatchRegExp, "no default export");
46+
47+
const partial = new PartialMatchRegExp(/^(\w+) \1 end$/);
48+
assert.equal(partial.test("abc ab"), true, "rejects a prefix");
49+
assert.equal(partial.test("abc abc end"), true, "rejects a full match");
50+
assert.equal(partial.test("abc xyz end"), false, "accepts an impossible input");
51+
},
52+
53+
"./extend": () => {
54+
assert.equal(
55+
typeof RegExp.prototype.toPartialMatchRegex,
56+
"function",
57+
"toPartialMatchRegex was not added to RegExp.prototype"
58+
);
59+
assert.equal(
60+
/^hello world$/.toPartialMatchRegex().test("hel"),
61+
true,
62+
"extended regex rejects a prefix"
63+
);
64+
}
65+
};
66+
67+
async function assertBuiltOutputParsesAtSupportedEcmaVersion(): Promise<void> {
68+
const linter = new Linter();
69+
const entries = await readdir(BUILT_OUTPUT, { recursive: true });
70+
const emitted = entries.filter((entry) => entry.endsWith(".js"));
71+
assert.ok(
72+
emitted.length > 0,
73+
"lib/ holds no JavaScript — was prepublishOnly run?"
74+
);
75+
76+
for (const file of emitted) {
77+
const source = await readFile(new URL(file, BUILT_OUTPUT), "utf8");
78+
const parseError = linter
79+
.verify(source, {
80+
languageOptions: {
81+
ecmaVersion: SUPPORTED_ECMA_VERSION,
82+
sourceType: "module"
83+
}
84+
})
85+
.find((message) => message.fatal);
86+
87+
if (parseError) {
88+
assert.fail(
89+
`lib/${file} uses syntax newer than ES${String(SUPPORTED_ECMA_VERSION)}${parseError.message} (line ${String(parseError.line)})`
90+
);
91+
}
92+
console.log(` ✓ lib/${file} parses as ES${String(SUPPORTED_ECMA_VERSION)}`);
93+
}
94+
}
95+
96+
async function readExportedSubpaths(): Promise<string[]> {
97+
const manifest = await readFile(
98+
new URL("../../package.json", import.meta.url),
99+
"utf8"
100+
);
101+
const { exports } = JSON.parse(manifest) as {
102+
exports: Record<string, unknown>;
103+
};
104+
return Object.keys(exports);
105+
}
106+
107+
function toSpecifier(packageName: string, subpath: string): string {
108+
return subpath === "." ? packageName : `${packageName}${subpath.slice(1)}`;
109+
}
110+
111+
async function main(): Promise<void> {
112+
const packageName = "regex-partial-match";
113+
const require = createRequire(import.meta.url);
114+
115+
await assertBuiltOutputParsesAtSupportedEcmaVersion();
116+
117+
const subpaths = await readExportedSubpaths();
118+
119+
for (const subpath of subpaths) {
120+
const specifier = toSpecifier(packageName, subpath);
121+
const smokeTest = SMOKE_TESTS[subpath];
122+
assert.ok(
123+
smokeTest,
124+
`"exports" declares ${subpath} but SMOKE_TESTS has no case for it`
125+
);
126+
127+
smokeTest((await import(specifier)) as LoadedModule);
128+
console.log(` ✓ import("${specifier}")`);
129+
130+
smokeTest(require(specifier) as LoadedModule);
131+
console.log(` ✓ require("${specifier}")`);
132+
}
133+
134+
console.log(`Smoke tested ${String(subpaths.length)} entry points from lib/`);
135+
}
136+
137+
await main();

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ jobs:
3737
- name: Run tests
3838
run: corepack npm test
3939

40+
- name: Build publishable output
41+
run: corepack npm run prepublishOnly
42+
43+
- name: Smoke test built output
44+
run: corepack npm run ci:smoke-test-built-package
45+
4046
changelog-and-semver-check:
4147
name: CHANGELOG and Semver Check
4248
runs-on: ubuntu-latest

docs/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Fixed the published v[1.1.1](#111---2026-08-02) package failing to load under both `import` and `require` with `SyntaxError: 'super' keyword unexpected here`, caused by the `ES2015` build target moving the `#`-private `#execDynamic()`, and its `super.exec()` call, outside the class body
13+
14+
### Added
15+
16+
- CI now smoke tests the built output, parsing every emitted file at the `ES2015` floor the README states, then loading every `exports` entry point under both `import` and `require`, so defects present only in emitted code are caught before publishing
17+
1018
## [1.1.1] - 2026-08-02
1119

1220
### Fixed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"lint:fix": "eslint . --fix",
6363
"prepare": "simple-git-hooks",
6464
"prepublishOnly": "tsc -p tsconfig.build.json",
65+
"ci:smoke-test-built-package": "node .github/scripts/smoke-test-built-package.ts",
6566
"test": "vitest run --config ./test/vitest.config.ts",
6667
"test:watch": "vitest --config ./test/vitest.config.ts"
6768
},

src/partialMatchRegExp.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,18 @@ class PartialMatchRegExp extends RegExp {
4747
override exec(input: string): RegExpExecArray | null {
4848
const compiled = this.#compiledPartial;
4949
if (compiled.kind === "dynamic")
50-
return this.#execDynamic(compiled.dynamic, input);
50+
return this._execDynamic(compiled.dynamic, input);
5151

5252
const { regex } = compiled;
5353
const match = execFrom(regex, input, this.lastIndex);
5454
this.lastIndex = regex.lastIndex;
5555
return match;
5656
}
5757

58-
#execDynamic(dynamic: DynamicPath, input: string): RegExpExecArray | null {
58+
private _execDynamic(
59+
dynamic: DynamicPath,
60+
input: string
61+
): RegExpExecArray | null {
5962
const { originalCaptureScan, preScan, expand } = dynamic;
6063

6164
const honoursLastIndex = this.global || this.sticky;

0 commit comments

Comments
 (0)