Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a52327f

Browse files
committedNov 24, 2021
npm esm tag
1 parent cce8a85 commit a52327f

20 files changed

+269
-21
lines changed
 

‎.babelrc-npm.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222
"plugins": [
2323
["./resources/add-extension-to-import-paths", { "extension": "mjs" }]
2424
]
25+
},
26+
"esm": {
27+
"presets": [
28+
["@babel/preset-env", { "modules": false, "targets": { "node": "12" } }]
29+
],
30+
"plugins": [
31+
["./resources/add-extension-to-import-paths", { "extension": "js" }]
32+
]
2533
}
2634
}
2735
}

‎.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
/node_modules
44
/coverage
55
/npmDist
6+
/npmEsmDist
67
/denoDist
78
/npm
89
/deno

‎.eslintrc.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,10 @@ rules:
442442
yield-star-spacing: off
443443

444444
overrides:
445+
- files:
446+
- 'integrationTests/node-esm/**/*.js'
447+
parserOptions:
448+
sourceType: module
445449
- files: '**/*.ts'
446450
parser: '@typescript-eslint/parser'
447451
parserOptions:

‎.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
/node_modules
1212
/coverage
1313
/npmDist
14+
/npmEsmDist
1415
/denoDist
1516
/npm
1617
/deno

‎.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
/node_modules
55
/coverage
66
/npmDist
7+
/npmEsmDist
78
/denoDist
89
/npm
910
/deno

‎integrationTests/integration-test.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ describe('Integration Tests', () => {
2727
path.join(tmpDir, 'graphql.tgz'),
2828
);
2929

30+
const esmDistDir = path.resolve('./npmEsmDist');
31+
const esmArchiveName = exec(`npm --quiet pack ${esmDistDir}`, {
32+
cwd: tmpDir,
33+
});
34+
35+
fs.renameSync(
36+
path.join(tmpDir, esmArchiveName),
37+
path.join(tmpDir, 'graphql-esm.tgz'),
38+
);
39+
3040
function testOnNodeProject(projectName) {
3141
const projectPath = path.join(__dirname, projectName);
3242

@@ -44,5 +54,6 @@ describe('Integration Tests', () => {
4454

4555
testOnNodeProject('ts');
4656
testOnNodeProject('node');
57+
testOnNodeProject('node-esm');
4758
testOnNodeProject('webpack');
4859
});

‎integrationTests/node-esm/index.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/* eslint-disable node/no-missing-import, import/no-unresolved, node/no-unsupported-features/es-syntax */
2+
3+
import { deepStrictEqual, strictEqual } from 'assert';
4+
5+
import { version } from 'version';
6+
import { schema } from 'schema';
7+
8+
import { graphqlSync } from 'graphql';
9+
10+
// Import without explicit extension
11+
import { isPromise } from 'graphql/jsutils/isPromise';
12+
13+
// Import package.json
14+
import pkg from 'graphql/package.json';
15+
16+
deepStrictEqual(`${version}-esm`, pkg.version);
17+
18+
const result = graphqlSync({
19+
schema,
20+
source: '{ hello }',
21+
rootValue: { hello: 'world' },
22+
});
23+
24+
deepStrictEqual(result, {
25+
data: {
26+
__proto__: null,
27+
hello: 'world',
28+
},
29+
});
30+
31+
strictEqual(isPromise(Promise.resolve()), true);
32+
33+
// The possible promise rejection is handled by "--unhandled-rejections=strict"
34+
import('graphql/jsutils/isPromise').then((isPromisePkg) => {
35+
strictEqual(isPromisePkg.isPromise(Promise.resolve()), true);
36+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"type": "module",
3+
"description": "graphql-js ESM should work on all supported node versions",
4+
"scripts": {
5+
"test": "node test.js"
6+
},
7+
"dependencies": {
8+
"graphql": "file:../graphql-esm.tgz",
9+
"node-12": "npm:node@12.x.x",
10+
"node-14": "npm:node@14.x.x",
11+
"node-16": "npm:node@16.x.x",
12+
"schema": "file:./schema",
13+
"version": "file:./version"
14+
}
15+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "schema",
3+
"exports": {
4+
".": {
5+
"import": "./schema.mjs"
6+
}
7+
},
8+
"peerDependencies": {
9+
"graphql": "*"
10+
}
11+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { buildSchema } from 'graphql/utilities';
2+
3+
export const schema = buildSchema('type Query { hello: String }');

‎integrationTests/node-esm/test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { execSync } from 'child_process';
2+
import { readFileSync } from 'fs';
3+
import { resolve } from 'path';
4+
5+
const { dependencies } = JSON.parse(
6+
readFileSync(resolve('package.json'), 'utf-8'),
7+
);
8+
9+
const nodeVersions = Object.keys(dependencies)
10+
.filter((pkg) => pkg.startsWith('node-'))
11+
.sort((a, b) => b.localeCompare(a));
12+
13+
for (const version of nodeVersions) {
14+
console.log(`Testing on ${version} ...`);
15+
16+
const nodePath = resolve('node_modules', version, 'bin/node');
17+
execSync(
18+
nodePath +
19+
' --experimental-json-modules --unhandled-rejections=strict index.js',
20+
{ stdio: 'inherit' },
21+
);
22+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "bar",
3+
"type": "module",
4+
"main": "./version.js",
5+
"peerDependencies": {
6+
"graphql": "*"
7+
}
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/* eslint-disable import/no-unresolved, node/no-missing-import */
2+
3+
import { version } from 'graphql';
4+
5+
export { version };

‎integrationTests/ts/esm.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { ExecutionResult } from 'graphql-esm/execution';
2+
3+
import { graphqlSync } from 'graphql-esm';
4+
import {
5+
GraphQLString,
6+
GraphQLSchema,
7+
GraphQLObjectType,
8+
} from 'graphql-esm/type';
9+
10+
const queryType: GraphQLObjectType = new GraphQLObjectType({
11+
name: 'Query',
12+
fields: () => ({
13+
sayHi: {
14+
type: GraphQLString,
15+
args: {
16+
who: {
17+
type: GraphQLString,
18+
defaultValue: 'World',
19+
},
20+
},
21+
resolve(_root, args: { who: string }) {
22+
return 'Hello ' + args.who;
23+
},
24+
},
25+
}),
26+
});
27+
28+
const schema: GraphQLSchema = new GraphQLSchema({ query: queryType });
29+
30+
const result: ExecutionResult = graphqlSync({
31+
schema,
32+
source: `
33+
query helloWho($who: String){
34+
test(who: $who)
35+
}
36+
`,
37+
variableValues: { who: 'Dolly' },
38+
});

‎integrationTests/ts/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
},
77
"dependencies": {
88
"graphql": "file:../graphql.tgz",
9+
"graphql-esm": "file:../graphql-esm.tgz",
910
"typescript-4.1": "npm:typescript@4.1.x",
1011
"typescript-4.2": "npm:typescript@4.2.x",
1112
"typescript-4.3": "npm:typescript@4.3.x",
12-
"typescript-4.4": "npm:typescript@4.4.x"
13+
"typescript-4.4": "npm:typescript@4.4.x",
14+
"typescript-4.5": "npm:typescript@4.5.x"
1315
}
1416
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// eslint-disable-next-line node/no-missing-import, import/no-unresolved
2+
import { graphqlSync } from 'graphql-esm';
3+
4+
// eslint-disable-next-line node/no-missing-import, import/no-unresolved
5+
import { buildSchema } from 'graphql-esm/utilities/buildASTSchema';
6+
7+
const schema = buildSchema('type Query { hello: String }');
8+
9+
export const result = graphqlSync({
10+
schema,
11+
source: '{ hello }',
12+
rootValue: { hello: 'world' },
13+
});

‎integrationTests/webpack/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
},
77
"dependencies": {
88
"graphql": "file:../graphql.tgz",
9+
"graphql-esm": "file:../graphql-esm.tgz",
910
"webpack": "5.x.x",
1011
"webpack-cli": "4.x.x"
1112
}

‎integrationTests/webpack/test.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,23 @@
33
const assert = require('assert');
44

55
// eslint-disable-next-line node/no-missing-require
6-
const { result } = require('./dist/main.js');
6+
const { result: cjs } = require('./dist/cjs.js');
77

8-
assert.deepStrictEqual(result, {
8+
assert.deepStrictEqual(cjs, {
99
data: {
1010
__proto__: null,
1111
hello: 'world',
1212
},
1313
});
14+
15+
// eslint-disable-next-line node/no-missing-require
16+
const { result: esm } = require('./dist/esm.js');
17+
18+
assert.deepStrictEqual(esm, {
19+
data: {
20+
__proto__: null,
21+
hello: 'world',
22+
},
23+
});
24+
1425
console.log('Test script: Got correct result from Webpack bundle!');

‎integrationTests/webpack/webpack.config.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{
22
"mode": "production",
3-
"entry": "./entry.js",
3+
"entry": {
4+
"cjs": "./entry.js",
5+
"esm": "./entry-esm.mjs"
6+
},
47
"output": {
58
"libraryTarget": "commonjs2"
69
}

‎resources/build-npm.js

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,40 @@ const prettierConfig = JSON.parse(
1515
);
1616

1717
if (require.main === module) {
18-
fs.rmSync('./npmDist', { recursive: true, force: true });
19-
fs.mkdirSync('./npmDist');
18+
buildNPM();
19+
buildNPM({
20+
isESMOnly: true,
21+
});
22+
}
23+
24+
exports.buildNPM = buildNPM;
25+
26+
function buildNPM({ isESMOnly = false } = {}) {
27+
const distDirectory = isESMOnly ? './npmEsmDist' : './npmDist';
2028

21-
const packageJSON = buildPackageJSON();
29+
fs.rmSync(distDirectory, { recursive: true, force: true });
30+
fs.mkdirSync(distDirectory);
2231

2332
const srcFiles = readdirRecursive('./src', { ignoreDir: /^__.*__$/ });
33+
34+
const packageJSON = buildPackageJSON({ srcFiles, isESMOnly });
35+
2436
for (const filepath of srcFiles) {
2537
const srcPath = path.join('./src', filepath);
26-
const destPath = path.join('./npmDist', filepath);
38+
const destPath = path.join(distDirectory, filepath);
2739

2840
fs.mkdirSync(path.dirname(destPath), { recursive: true });
2941
if (filepath.endsWith('.ts')) {
30-
const cjs = babelBuild(srcPath, { envName: 'cjs' });
31-
writeGeneratedFile(destPath.replace(/\.ts$/, '.js'), cjs);
32-
33-
const mjs = babelBuild(srcPath, { envName: 'mjs' });
34-
writeGeneratedFile(destPath.replace(/\.ts$/, '.mjs'), mjs);
42+
if (isESMOnly) {
43+
const js = babelBuild(srcPath, { envName: 'esm' });
44+
writeGeneratedFile(destPath.replace(/\.ts$/, '.js'), js);
45+
} else {
46+
const cjs = babelBuild(srcPath, { envName: 'cjs' });
47+
writeGeneratedFile(destPath.replace(/\.ts$/, '.js'), cjs);
48+
49+
const mjs = babelBuild(srcPath, { envName: 'mjs' });
50+
writeGeneratedFile(destPath.replace(/\.ts$/, '.mjs'), mjs);
51+
}
3552
}
3653
}
3754

@@ -47,7 +64,7 @@ if (require.main === module) {
4764
...tsConfig.compilerOptions,
4865
noEmit: false,
4966
declaration: true,
50-
declarationDir: './npmDist',
67+
declarationDir: distDirectory,
5168
emitDeclarationOnly: true,
5269
};
5370

@@ -72,7 +89,7 @@ if (require.main === module) {
7289
// TODO: revisit once TS implements https://github.com/microsoft/TypeScript/issues/32166
7390
const notSupportedTSVersionFile = 'NotSupportedTSVersion.d.ts';
7491
fs.writeFileSync(
75-
path.join('./npmDist', notSupportedTSVersionFile),
92+
path.join(distDirectory, notSupportedTSVersionFile),
7693
// Provoke syntax error to show this message
7794
`"Package 'graphql' support only TS versions that are ${supportedTSVersions[0]}".`,
7895
);
@@ -81,13 +98,16 @@ if (require.main === module) {
8198
'*': { '*': [notSupportedTSVersionFile] },
8299
};
83100

84-
fs.copyFileSync('./LICENSE', './npmDist/LICENSE');
85-
fs.copyFileSync('./README.md', './npmDist/README.md');
101+
fs.copyFileSync('./LICENSE', distDirectory + '/LICENSE');
102+
fs.copyFileSync('./README.md', distDirectory + '/README.md');
86103

87104
// Should be done as the last step so only valid packages can be published
88-
writeGeneratedFile('./npmDist/package.json', JSON.stringify(packageJSON));
105+
writeGeneratedFile(
106+
distDirectory + '/package.json',
107+
JSON.stringify(packageJSON),
108+
);
89109

90-
showDirStats('./npmDist');
110+
showDirStats(distDirectory);
91111
}
92112

93113
function writeGeneratedFile(filepath, body) {
@@ -104,7 +124,10 @@ function babelBuild(srcPath, options) {
104124
return code + '\n';
105125
}
106126

107-
function buildPackageJSON() {
127+
function buildPackageJSON({ srcFiles, isESMOnly = false }) {
128+
/**
129+
* @type {Record<string,unknown>}
130+
*/
108131
const packageJSON = JSON.parse(
109132
fs.readFileSync(require.resolve('../package.json'), 'utf-8'),
110133
);
@@ -113,6 +136,19 @@ function buildPackageJSON() {
113136
delete packageJSON.scripts;
114137
delete packageJSON.devDependencies;
115138

139+
if (isESMOnly) {
140+
delete packageJSON.module;
141+
packageJSON.version = `${packageJSON.version}-esm`;
142+
143+
packageJSON.type = 'module';
144+
145+
packageJSON.exports = {
146+
'.': './index.js',
147+
'./*': './*',
148+
'./package.json': './package.json',
149+
};
150+
}
151+
116152
const { version } = packageJSON;
117153
const versionMatch = /^\d+\.\d+\.\d+-?(?<preReleaseTag>.*)?$/.exec(version);
118154
if (!versionMatch) {
@@ -126,7 +162,7 @@ function buildPackageJSON() {
126162
// Note: `experimental-*` take precedence over `alpha`, `beta` or `rc`.
127163
const publishTag = splittedTag[2] ?? splittedTag[0];
128164
assert(
129-
['alpha', 'beta', 'rc'].includes(publishTag) ||
165+
['alpha', 'beta', 'rc', 'esm'].includes(publishTag) ||
130166
publishTag.startsWith('experimental-'),
131167
`"${publishTag}" tag is not supported.`,
132168
);
@@ -135,5 +171,23 @@ function buildPackageJSON() {
135171
packageJSON.publishConfig = { tag: publishTag };
136172
}
137173

174+
if (isESMOnly) {
175+
/**
176+
* This allows imports without explicit extensions and index imports
177+
* Like `import("graphql/language/parser")` and `import("graphql/utilities")`
178+
*/
179+
for (const srcFile of srcFiles.map((v) => v.replace(/\\/g, '/'))) {
180+
if (srcFile.endsWith('.ts')) {
181+
const srcFilePath = srcFile.slice(0, srcFile.length - 3);
182+
packageJSON.exports[`./${srcFilePath}`] = `./${srcFilePath}.js`;
183+
184+
const indexMatch = /^(.+)\/index\.ts$/.exec(srcFile);
185+
if (indexMatch && indexMatch[1]) {
186+
packageJSON.exports['./' + indexMatch[1]] = `./${srcFilePath}.js`;
187+
}
188+
}
189+
}
190+
}
191+
138192
return packageJSON;
139193
}

0 commit comments

Comments
 (0)
Please sign in to comment.