From eee3f00554c41c928cf46acbcb84d466c06a7d66 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 25 Oct 2026 03:24:47 +0000 Subject: [PATCH] fix(parser): ship ESM entry points as .mjs so Node 18 can import them --- parser/package.json | 44 +++++++++++++++--------------- parser/scripts/prepare.js | 14 ++++++---- parser/templates/index.js.template | 2 +- parser/test/esm.test.mjs | 26 ++++++++++++++++++ 4 files changed, 57 insertions(+), 29 deletions(-) diff --git a/parser/package.json b/parser/package.json index 76f62ae4..c4ff7beb 100644 --- a/parser/package.json +++ b/parser/package.json @@ -4,43 +4,43 @@ "author": "Constructive ", "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": [ diff --git a/parser/scripts/prepare.js b/parser/scripts/prepare.js index 9c1ac240..e767e3fe 100644 --- a/parser/scripts/prepare.js +++ b/parser/scripts/prepare.js @@ -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'); @@ -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}...`); @@ -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' } ]; @@ -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 diff --git a/parser/templates/index.js.template b/parser/templates/index.js.template index 6716d1eb..96276df1 100644 --- a/parser/templates/index.js.template +++ b/parser/templates/index.js.template @@ -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) { diff --git a/parser/test/esm.test.mjs b/parser/test/esm.test.mjs index c5241dfe..67b953c9 100644 --- a/parser/test/esm.test.mjs +++ b/parser/test/esm.test.mjs @@ -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')); @@ -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); + }); + } +});