From ae21b7c862b7761580144a52cb97dc0e50602ffb Mon Sep 17 00:00:00 2001 From: isaacs Date: Wed, 27 Sep 2023 23:24:38 -0700 Subject: [PATCH] fix: Fix ESM distribution for node import usage When loading this module with import() in Node, the CommonJS module was being loaded, resulting in {default:{default:fn}} instead of just {default:fn} as intended. Because the types are only defined for CommonJS, this was further compounded, meaning that instead of doing this: ```ts import reactElementToJSXString from 'react-element-to-jsx-string' ``` TypeScript users would have to do this: ```ts const { default: { default: reactElementToJSXString }, } = (await import('react-element-to-jsx-string')) as unknown as { default: typeof import('react-element-to-jsx-string') } // wat. ``` - Add conditional exports for import and require, so that the ESM is served to import() users. - Add a build script to place a package.json file in dist/esm, setting it to ESM mode for Node's loader and TypeScript's module resolution. - Test that require() and import() receive matching objects, when loading from a mjs file in a folder outside this project. With this change, `import()` in Node loads the ESM version, properly typed, without any hassle. --- esm-dist.sh | 7 +++++++ package.json | 12 ++++++++++++ src/dist.spec.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100755 esm-dist.sh create mode 100644 src/dist.spec.js diff --git a/esm-dist.sh b/esm-dist.sh new file mode 100755 index 000000000..d55123427 --- /dev/null +++ b/esm-dist.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Place a package.json so Node know that the dist/esm is ESM. +echo '{ "type": "module" }' > dist/esm/package.json + +# Copy the types in there so TS knows it's ESM as well. +cp index.d.ts dist/esm/index.d.ts diff --git a/package.json b/package.json index a23f0dc4d..082647189 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,23 @@ "module": "dist/esm/index.js", "browser": "dist/cjs/index.js", "types": "index.d.ts", + "exports": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./index.d.ts", + "default": "./dist/cjs/index.js" + } + }, "scripts": { "build": "rollup -c", "build:flow": "flow-copy-source -v --ignore=*.spec.js src/ dist/cjs", "prebuild": "rm -rf dist/", "postbuild": "npm run build:flow", + "postbuild:flow": "npm run build:esm", + "build:esm": "./esm-dist.sh", "prepare": "npm run build", "lint": "eslint .", "lint:fix": "npm run lint -- --fix", diff --git a/src/dist.spec.js b/src/dist.spec.js new file mode 100644 index 000000000..a7c77a4da --- /dev/null +++ b/src/dist.spec.js @@ -0,0 +1,45 @@ +const { resolve } = require('path'); +const { writeFileSync, unlinkSync } = require('fs'); +const { spawnSync } = require('child_process'); +const { tmpdir } = require('os'); + +// escape from Jest's environment, and from this folder. +// This ensures that we are loading the distribution files +// from a completely separate location, as one will when using +// this module as a dependency either via import() or require(). +const cjsMod = resolve(__dirname, '../dist/cjs/index.js'); +const esmMod = resolve(__dirname, '../dist/esm/index.js'); +const testCode = ` +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); +const cjsDist = require(${JSON.stringify(cjsMod)}); +const esmDist = await import(${JSON.stringify(esmMod)}); +import assert from 'assert'; + +for (const [name, value] of Object.entries(cjsDist)) { + assert.equal(typeof esmDist[name], typeof value, 'esm has ' + name); +} +for (const [name, value] of Object.entries(esmDist)) { + assert.equal(typeof cjsDist[name], typeof value, 'cjs has ' + name); +} +assert.equal(typeof cjsDist.default, 'function', 'cjs default function'); +assert.equal(typeof esmDist.default, 'function', 'cjs default function'); + +console.log('ok'); +`; +const testFile = resolve(tmpdir(), 'reactElementToJSXString.test.mjs'); + +describe('cjs and esm distributions', () => { + it('writes the test file', () => writeFileSync(testFile, testCode)); + + it('exports matching cjs and esm', () => { + const result = spawnSync(process.execPath, [testFile], { + encoding: 'utf8', + }); + expect(result.error).toBe(undefined); + expect(result.stderr).toEqual(''); + expect(result.stdout).toEqual('ok\n'); + }); + + it('cleans up', () => unlinkSync(testFile)); +});