Skip to content
Open
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
44 changes: 22 additions & 22 deletions parser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,43 @@
"author": "Constructive <[email protected]>",
"description": "Multi-version PostgreSQL parser with dynamic version selection",
"main": "./wasm/index.cjs",
"module": "./wasm/index.js",
"module": "./wasm/index.mjs",
"types": "./wasm/index.d.ts",
"exports": {
".": {
"import": "./wasm/index.js",
"require": "./wasm/index.cjs",
"types": "./wasm/index.d.ts"
"types": "./wasm/index.d.ts",
"import": "./wasm/index.mjs",
"require": "./wasm/index.cjs"
},
"./v13": {
"import": "./wasm/v13.js",
"require": "./wasm/v13.cjs",
"types": "./wasm/v13.d.ts"
"types": "./wasm/v13.d.ts",
"import": "./wasm/v13.mjs",
"require": "./wasm/v13.cjs"
},
"./v14": {
"import": "./wasm/v14.js",
"require": "./wasm/v14.cjs",
"types": "./wasm/v14.d.ts"
"types": "./wasm/v14.d.ts",
"import": "./wasm/v14.mjs",
"require": "./wasm/v14.cjs"
},
"./v15": {
"import": "./wasm/v15.js",
"require": "./wasm/v15.cjs",
"types": "./wasm/v15.d.ts"
"types": "./wasm/v15.d.ts",
"import": "./wasm/v15.mjs",
"require": "./wasm/v15.cjs"
},
"./v16": {
"import": "./wasm/v16.js",
"require": "./wasm/v16.cjs",
"types": "./wasm/v16.d.ts"
"types": "./wasm/v16.d.ts",
"import": "./wasm/v16.mjs",
"require": "./wasm/v16.cjs"
},
"./v17": {
"import": "./wasm/v17.js",
"require": "./wasm/v17.cjs",
"types": "./wasm/v17.d.ts"
"types": "./wasm/v17.d.ts",
"import": "./wasm/v17.mjs",
"require": "./wasm/v17.cjs"
},
"./v18": {
"import": "./wasm/v18.js",
"require": "./wasm/v18.cjs",
"types": "./wasm/v18.d.ts"
"types": "./wasm/v18.d.ts",
"import": "./wasm/v18.mjs",
"require": "./wasm/v18.cjs"
}
},
"files": [
Expand Down
14 changes: 8 additions & 6 deletions parser/scripts/prepare.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ function generateTemplateVars(versions) {
// For TypeScript union: 13 | 14 | 15 | 16 | 17
const versionUnion = versions.join(' | ');

// For ESM exports: export * as v13 from './v13/index.js';
const versionExports = versions.map(v => `export * as v${v} from './v${v}/index.js';`).join('\n');
// For ESM exports: export * as v13 from './v13/index.mjs';
const versionExports = versions.map(v => `export * as v${v} from './v${v}/index.mjs';`).join('\n');

// For CommonJS requires: v13: require('./v13/index.cjs'),
const versionRequires = versions.map(v => ` v${v}: require('./v${v}/index.cjs')`).join(',\n');
Expand Down Expand Up @@ -113,7 +113,9 @@ config.versions.forEach(version => {
const files = fs.readdirSync(sourceWasmDir);
files.forEach(file => {
const sourcePath = path.join(sourceWasmDir, file);
const destPath = path.join(versionWasmDir, file);
// ESM files must be .mjs: the package has no "type": "module", so Node
// before 20.19 (and without syntax detection) would load a .js as CommonJS.
const destPath = path.join(versionWasmDir, file === 'index.js' ? 'index.mjs' : file);

if (fs.statSync(sourcePath).isFile()) {
console.log(`Copying ${file} for v${version}...`);
Expand Down Expand Up @@ -162,7 +164,7 @@ const templateVars = generateTemplateVars(config.versions);

// Process each template
const templates = [
{ template: 'index.js.template', output: 'index.js' },
{ template: 'index.js.template', output: 'index.mjs' },
{ template: 'index.cjs.template', output: 'index.cjs' },
{ template: 'index.d.ts.template', output: 'index.d.ts' }
];
Expand All @@ -182,9 +184,9 @@ templates.forEach(({ template, output }) => {
config.versions.forEach(version => {
// ESM version
const esmContent = `// Re-export everything from the v${version} parser
export * from './v${version}/index.js';
export * from './v${version}/index.mjs';
`;
fs.writeFileSync(path.join(wasmDir, `v${version}.js`), esmContent);
fs.writeFileSync(path.join(wasmDir, `v${version}.mjs`), esmContent);

// CJS version
const cjsContent = `// Re-export everything from the v${version} parser
Expand Down
2 changes: 1 addition & 1 deletion parser/templates/index.js.template
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class Parser {
}

async _doLoad() {
const module = await import(`./v${this.version}/index.js`);
const module = await import(`./v${this.version}/index.mjs`);
this.parser = module;

if (this.parser.loadModule) {
Expand Down
26 changes: 26 additions & 0 deletions parser/test/esm.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';

const { versions } = JSON.parse(readFileSync(new URL('../wasm/build-info.json', import.meta.url), 'utf8'));

Expand All @@ -21,3 +23,27 @@ describe('ESM entry points', () => {
});
}
});

// Node 18 and Node 20 before 20.19 load a `.js` file in a package without
// "type": "module" as CommonJS. Turning off syntax detection reproduces that
// on current Node, so this fails if an ESM entry point is not `.mjs`.
describe('ESM entry points without module syntax detection', () => {
const flags = process.allowedNodeEnvironmentFlags.has('--experimental-detect-module')
? ['--no-experimental-detect-module']
: [];
const specifiers = ['@pgsql/parser', ...versions.map((v) => `@pgsql/parser/v${v}`)];
for (const specifier of specifiers) {
it(`imports ${specifier}`, () => {
const script = `const m = await import(${JSON.stringify(specifier)});
const lib = m.Parser ? new m.Parser() : m;
if (m.loadModule) await m.loadModule();
const result = m.Parser ? await lib.parse('SELECT 1') : lib.parseSync('SELECT 1');
if (result.stmts.length !== 1) throw new Error('bad parse');`;
const { status, stderr } = spawnSync(process.execPath, [...flags, '--input-type=module', '-e', script], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
encoding: 'utf8',
});
assert.equal(status, 0, stderr);
Comment on lines +42 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 bug · medium

spawnSync spawn failures surface as null == 0

spawnSync reports spawn failures (ENOENT, EACCES) and signal kills as status: null plus an error field, which the test destructures away (parser/test/esm.test.mjs:42). assert.equal(status, 0, stderr) at parser/test/esm.test.mjs:46 then fails as null == 0 with an empty or misleading message, turning an infrastructure failure into an opaque one. The child's stdout is also discarded and the inline script throws a bare 'bad parse', so a failing matrix entry may give no actionable output.

📋 Prompt for AI Agents

In parser/test/esm.test.mjs lines 42-46, destructure error and stdout from the spawnSync result; before assert.equal(status, 0, stderr) add assert.ifError(error), and change the assertion message to include the specifier and both streams, e.g. assert.equal(status, 0, import ${specifier} failed\nstdout: ${stdout}\nstderr: ${stderr}), so spawn failures (ENOENT, signal kills) and parse failures produce actionable CI output instead of null == 0 with an empty stderr.

});
}
});
Loading