Skip to content

Commit 03d4440

Browse files
committed
add support for conditional exports
this commits adds support for conditional exports bun, module, module-sync => ESM deno, node and require => CJS default => ESM. deno and node (in versions that do not support module-sync) point to CJS even when using import so as to avoid the dual-package hazard. the require condition points to CJS just because our default is otherwise now ESM.
1 parent f2890ca commit 03d4440

File tree

9 files changed

+161
-18
lines changed

9 files changed

+161
-18
lines changed

cspell.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ words:
6666

6767
# TODO: contribute upstream
6868
- deno
69+
- denoland
6970
- hashbang
7071
- Rspack
7172
- Rollup

integrationTests/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ Each subdirectory represents a different environment/bundler:
1414
- `ts` - tests for supported Typescript versions
1515
- `webpack` - tests for Webpack
1616

17+
### Verifying Conditional Exports
18+
19+
The `conditions` subdirectory contains tests that verify the conditional exports of GraphQL.js. These tests ensure that the correct files are imported based on the environment being used.
20+
1721
### Verifying Development Mode Tests
1822

1923
Each subdirectory represents a different environment/bundler demonstrating enabling development mode by setting the environment variable `NODE_ENV` to `development`.

integrationTests/conditions/check.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import assert from 'node:assert';
2+
3+
import { GraphQLObjectType as ESMGraphQLObjectType } from 'graphql';
4+
5+
import { CJSGraphQLObjectType, cjsPath } from './cjs-importer.cjs';
6+
7+
const moduleSync = process.env.MODULE_SYNC === 'true';
8+
const expectedExtension = moduleSync ? '.mjs' : '.js';
9+
assert.ok(
10+
cjsPath.endsWith(expectedExtension),
11+
`require('graphql') should resolve to a file with extension "${expectedExtension}", but got "${cjsPath}".`,
12+
);
13+
14+
const isSameModule = ESMGraphQLObjectType === CJSGraphQLObjectType;
15+
assert.strictEqual(
16+
isSameModule,
17+
true,
18+
'ESM and CJS imports should be the same module instances.',
19+
);
20+
21+
console.log('Module identity and path checks passed.');
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use strict';
2+
3+
const { GraphQLObjectType } = require('graphql');
4+
5+
const cjsPath = require.resolve('graphql');
6+
7+
// eslint-disable-next-line import/no-commonjs
8+
module.exports = {
9+
CJSGraphQLObjectType: GraphQLObjectType,
10+
cjsPath,
11+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"description": "graphql-js should be loaded correctly on different versions of Node.js, Deno and Bun",
3+
"private": true,
4+
"scripts": {
5+
"test": "node test.js"
6+
},
7+
"dependencies": {
8+
"graphql": "file:../graphql.tgz"
9+
}
10+
}

integrationTests/conditions/test.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import childProcess from 'node:child_process';
2+
3+
const nodeTests = [
4+
// Old node versions, require => CJS
5+
{ version: '20.18.0', moduleSync: false },
6+
{ version: '22.11.0', moduleSync: false },
7+
// New node versions, module-sync => ESM
8+
{ version: '20.19.0', moduleSync: true },
9+
{ version: '22.12.0', moduleSync: true },
10+
{ version: '24.0.0', moduleSync: true },
11+
];
12+
13+
for (const { version, moduleSync } of nodeTests) {
14+
console.log(`Testing on node@${version} (moduleSync: ${moduleSync}) ...`);
15+
childProcess.execSync(
16+
`docker run --rm --volume "$PWD":/usr/src/app -w /usr/src/app --env MODULE_SYNC=${moduleSync} node:${version}-slim node ./check.mjs`,
17+
{ stdio: 'inherit' },
18+
);
19+
}
20+
21+
console.log('Testing on bun (moduleSync: true) ...');
22+
childProcess.execSync(
23+
`docker run --rm --volume "$PWD":/usr/src/app -w /usr/src/app --env MODULE_SYNC=true oven/bun:alpine bun ./check.mjs`,
24+
{ stdio: 'inherit' },
25+
);
26+
27+
console.log('Testing on deno (moduleSync: false) ...');
28+
childProcess.execSync(
29+
`docker run --rm --volume "$PWD":/usr/src/app -w /usr/src/app --env MODULE_SYNC=false denoland/deno:2.4.0 deno run --allow-read --allow-env ./check.mjs`,
30+
{ stdio: 'inherit' },
31+
);

resources/build-npm.ts

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import ts from 'typescript';
66

77
import { changeExtensionInImportPaths } from './change-extension-in-import-paths.js';
88
import { inlineInvariant } from './inline-invariant.js';
9+
import type { ConditionalExports } from './utils.js';
910
import {
1011
prettify,
1112
readPackageJSON,
@@ -98,18 +99,35 @@ async function buildPackage(outDir: string, isESMOnly: boolean): Promise<void> {
9899
}
99100
}
100101

101-
// Temporary workaround to allow "internal" imports, no grantees provided
102102
packageJSON.exports['./*.js'] = './*.js';
103103
packageJSON.exports['./*'] = './*.js';
104104

105105
packageJSON.publishConfig.tag += '-esm';
106106
packageJSON.version += '+esm';
107107
} else {
108108
delete packageJSON.type;
109-
packageJSON.main = 'index';
109+
packageJSON.main = 'index.js';
110110
packageJSON.module = 'index.mjs';
111-
emitTSFiles({ outDir, module: 'commonjs', extension: '.js' });
111+
packageJSON.types = 'index.d.ts';
112+
113+
const { emittedTSFiles } = emitTSFiles({
114+
outDir,
115+
module: 'commonjs',
116+
extension: '.js',
117+
});
112118
emitTSFiles({ outDir, module: 'es2020', extension: '.mjs' });
119+
120+
packageJSON.exports = {};
121+
for (const filepath of emittedTSFiles) {
122+
if (path.basename(filepath) === 'index.js') {
123+
const relativePath = './' + path.relative('./npmDist', filepath);
124+
packageJSON.exports[path.dirname(relativePath)] =
125+
buildExports(relativePath);
126+
}
127+
}
128+
129+
packageJSON.exports['./*.js'] = buildExports('./*.js');
130+
packageJSON.exports['./*'] = buildExports('./*.js');
113131
}
114132

115133
const packageJsonPath = `./${outDir}/package.json`;
@@ -141,21 +159,31 @@ function emitTSFiles(options: {
141159

142160
const tsHost = ts.createCompilerHost(tsOptions);
143161
tsHost.writeFile = (filepath, body) => {
144-
if (filepath.match(/.js$/) && extension === '.mjs') {
145-
let bodyToWrite = body;
146-
bodyToWrite = bodyToWrite.replace(
147-
'//# sourceMappingURL=graphql.js.map',
148-
'//# sourceMappingURL=graphql.mjs.map',
149-
);
150-
writeGeneratedFile(filepath.replace(/.js$/, extension), bodyToWrite);
151-
} else if (filepath.match(/.js.map$/) && extension === '.mjs') {
152-
writeGeneratedFile(
153-
filepath.replace(/.js.map$/, extension + '.map'),
154-
body,
155-
);
156-
} else {
157-
writeGeneratedFile(filepath, body);
162+
if (extension === '.mjs') {
163+
if (filepath.match(/.js$/)) {
164+
let bodyToWrite = body;
165+
bodyToWrite = bodyToWrite.replace(
166+
'//# sourceMappingURL=graphql.js.map',
167+
'//# sourceMappingURL=graphql.mjs.map',
168+
);
169+
writeGeneratedFile(filepath.replace(/.js$/, extension), bodyToWrite);
170+
return;
171+
}
172+
173+
if (filepath.match(/.js.map$/)) {
174+
writeGeneratedFile(
175+
filepath.replace(/.js.map$/, extension + '.map'),
176+
body,
177+
);
178+
return;
179+
}
180+
181+
if (filepath.match(/.d.ts$/)) {
182+
writeGeneratedFile(filepath.replace(/.d.ts$/, '.d.mts'), body);
183+
return;
184+
}
158185
}
186+
writeGeneratedFile(filepath, body);
159187
};
160188

161189
const tsProgram = ts.createProgram(['src/index.ts'], tsOptions, tsHost);
@@ -172,3 +200,24 @@ function emitTSFiles(options: {
172200
emittedTSFiles: tsResult.emittedFiles.sort((a, b) => a.localeCompare(b)),
173201
};
174202
}
203+
204+
function buildExports(filepath: string): ConditionalExports {
205+
const { dir, name } = path.parse(filepath);
206+
const base = `./${path.join(dir, name)}`;
207+
return {
208+
types: {
209+
module: `${base}.d.mts`,
210+
'module-sync': `${base}.d.mts`,
211+
bun: `${base}.d.mts`,
212+
node: `${base}.d.ts`,
213+
require: `${base}.d.ts`,
214+
default: `${base}.d.mts`,
215+
},
216+
module: `${base}.mjs`,
217+
bun: `${base}.mjs`,
218+
'module-sync': `${base}.mjs`,
219+
node: `${base}.js`,
220+
require: `${base}.js`,
221+
default: `${base}.mjs`,
222+
};
223+
}

resources/integration-test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ describe('Integration Tests', () => {
3939
testOnNodeProject('node');
4040
testOnNodeProject('webpack');
4141

42+
// Conditional export tests
43+
testOnNodeProject('conditions');
44+
4245
// Development mode tests
4346
testOnNodeProject('dev-node');
4447
testOnNodeProject('dev-deno');

resources/utils.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ interface PackageJSON {
234234
repository?: { url?: string };
235235
scripts?: { [name: string]: string };
236236
type?: string;
237-
exports: { [path: string]: string };
237+
exports: { [path: string]: string | ConditionalExports };
238238
types?: string;
239239
typesVersions: { [ranges: string]: { [path: string]: Array<string> } };
240240
devDependencies?: { [name: string]: string };
@@ -245,6 +245,19 @@ interface PackageJSON {
245245
module?: string;
246246
}
247247

248+
export interface ConditionalExports extends BaseExports {
249+
types: BaseExports;
250+
}
251+
252+
interface BaseExports {
253+
module: string;
254+
bun: string;
255+
'module-sync': string;
256+
node: string;
257+
require: string;
258+
default: string;
259+
}
260+
248261
export function readPackageJSON(
249262
dirPath: string = localRepoPath(),
250263
): PackageJSON {

0 commit comments

Comments
 (0)