From d245e12fe6754c2630b5a6a93c9d010b13cc4c83 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 29 Jul 2025 22:07:09 +0300 Subject: [PATCH 1/2] use `enableDevMode()` or development condition instead of NODE_ENV --- cspell.yml | 4 +- integrationTests/README.md | 2 +- integrationTests/dev-bun/package.json | 2 +- integrationTests/dev-bun/test.js | 4 +- .../dev-deno-condition-based/package.json | 10 + .../test.js | 2 +- .../package.json | 2 +- integrationTests/dev-deno-explicit/test.js | 20 ++ integrationTests/dev-esbuild/package.json | 2 +- integrationTests/dev-jest/jest.config.js | 22 ++ integrationTests/dev-jest/package.json | 6 +- .../dev-node-condition-based/package.json | 11 + .../test.js | 2 +- .../package.json | 2 +- integrationTests/dev-node-explicit/test.js | 20 ++ integrationTests/dev-rollup/package.json | 3 +- integrationTests/dev-rollup/rollup.config.js | 8 + integrationTests/dev-rspack/rspack.config.js | 3 + integrationTests/dev-swc/index.js | 4 +- integrationTests/dev-vitest/vitest.config.js | 3 + .../dev-webpack/webpack.config.js | 3 + integrationTests/prod-bun/package.json | 2 +- integrationTests/prod-deno/env-shim.js | 4 - integrationTests/prod-deno/package.json | 2 +- integrationTests/prod-esbuild/package.json | 2 +- integrationTests/prod-node/package.json | 2 +- integrationTests/prod-rollup/package.json | 3 +- integrationTests/prod-rollup/rollup.config.js | 13 -- integrationTests/prod-swc/package.json | 2 +- resources/build-npm.ts | 101 ++++++++- resources/integration-test.ts | 6 +- resources/utils.ts | 15 +- src/__tests__/devMode-test.ts | 15 ++ src/devMode.ts | 12 ++ src/index.ts | 3 + src/jsutils/__tests__/instanceOf-test.ts | 8 +- src/jsutils/instanceOf.ts | 82 ++++---- website/pages/docs/_meta.ts | 1 + website/pages/docs/development-mode.mdx | 195 ++++++++++++++++++ website/pages/docs/going-to-production.mdx | 138 +------------ website/pages/upgrade-guides/v16-v17.mdx | 10 + 41 files changed, 527 insertions(+), 224 deletions(-) create mode 100644 integrationTests/dev-deno-condition-based/package.json rename integrationTests/{dev-deno => dev-deno-condition-based}/test.js (77%) rename integrationTests/{dev-deno => dev-deno-explicit}/package.json (76%) create mode 100644 integrationTests/dev-deno-explicit/test.js create mode 100644 integrationTests/dev-jest/jest.config.js create mode 100644 integrationTests/dev-node-condition-based/package.json rename integrationTests/{dev-node => dev-node-condition-based}/test.js (77%) rename integrationTests/{dev-node => dev-node-explicit}/package.json (65%) create mode 100644 integrationTests/dev-node-explicit/test.js delete mode 100644 integrationTests/prod-deno/env-shim.js create mode 100644 src/__tests__/devMode-test.ts create mode 100644 src/devMode.ts create mode 100644 website/pages/docs/development-mode.mdx diff --git a/cspell.yml b/cspell.yml index 9a713bca1d..0bf5d395a7 100644 --- a/cspell.yml +++ b/cspell.yml @@ -68,8 +68,10 @@ words: - deno - denoland - hashbang - - Rspack - Rollup + - Rspack + - Rsbuild + - Turbopack # Website tech - Nextra diff --git a/integrationTests/README.md b/integrationTests/README.md index c510a5f619..5695883501 100644 --- a/integrationTests/README.md +++ b/integrationTests/README.md @@ -20,7 +20,7 @@ The `conditions` subdirectory contains tests that verify the conditional exports ### Verifying Development Mode Tests -Each subdirectory represents a different environment/bundler demonstrating enabling development mode by setting the environment variable `NODE_ENV` to `development`. +Each subdirectory represents a different platform/bundler demonstrating enabling development mode by enabling the `development` condition or by calling `enableDevMode()`. ### Verifying Production Mode Tests diff --git a/integrationTests/dev-bun/package.json b/integrationTests/dev-bun/package.json index 4b955f6116..472ced00fa 100644 --- a/integrationTests/dev-bun/package.json +++ b/integrationTests/dev-bun/package.json @@ -2,7 +2,7 @@ "description": "graphql-js development mode should work with Bun", "private": true, "scripts": { - "test": "docker run --rm --volume \"$PWD\":/usr/src/app -w /usr/src/app oven/bun:\"$BUN_VERSION\"-slim bun test.js" + "test": "docker run --rm --volume \"$PWD\":/usr/src/app -w /usr/src/app oven/bun:\"$BUN_VERSION\"-slim bun --conditions=development test.js" }, "dependencies": { "graphql": "file:../graphql.tgz" diff --git a/integrationTests/dev-bun/test.js b/integrationTests/dev-bun/test.js index ebf7e79e4b..a235052f74 100644 --- a/integrationTests/dev-bun/test.js +++ b/integrationTests/dev-bun/test.js @@ -1,4 +1,6 @@ -import { isObjectType } from 'graphql'; +import { enableDevMode, isObjectType } from 'graphql'; + +enableDevMode(); class GraphQLObjectType { get [Symbol.toStringTag]() { diff --git a/integrationTests/dev-deno-condition-based/package.json b/integrationTests/dev-deno-condition-based/package.json new file mode 100644 index 0000000000..66c7618348 --- /dev/null +++ b/integrationTests/dev-deno-condition-based/package.json @@ -0,0 +1,10 @@ +{ + "description": "graphql-js condition-based development mode should work with Deno", + "private": true, + "scripts": { + "test": "docker run --rm --volume \"$PWD\":/usr/src/app -w /usr/src/app denoland/deno:latest deno run --conditions=development --allow-env=NODE_ENV test.js" + }, + "dependencies": { + "graphql": "file:../graphql.tgz" + } +} diff --git a/integrationTests/dev-deno/test.js b/integrationTests/dev-deno-condition-based/test.js similarity index 77% rename from integrationTests/dev-deno/test.js rename to integrationTests/dev-deno-condition-based/test.js index 3f2143191f..67bcb13460 100644 --- a/integrationTests/dev-deno/test.js +++ b/integrationTests/dev-deno-condition-based/test.js @@ -9,7 +9,7 @@ class GraphQLObjectType { try { isObjectType(new GraphQLObjectType()); throw new Error( - 'Expected isObjectType to throw an error in Deno implicit dev mode.', + 'Expected isObjectType to throw an error in Deno condition-based development mode.', ); } catch (error) { if (!error.message.includes('from another module or realm')) { diff --git a/integrationTests/dev-deno/package.json b/integrationTests/dev-deno-explicit/package.json similarity index 76% rename from integrationTests/dev-deno/package.json rename to integrationTests/dev-deno-explicit/package.json index f6c0525ec8..f282e0d41f 100644 --- a/integrationTests/dev-deno/package.json +++ b/integrationTests/dev-deno-explicit/package.json @@ -1,5 +1,5 @@ { - "description": "graphql-js development mode should work with Deno", + "description": "graphql-js explicit development mode should work with Deno", "private": true, "scripts": { "test": "docker run --rm --volume \"$PWD\":/usr/src/app -w /usr/src/app denoland/deno:alpine-\"$DENO_VERSION\" deno run --allow-env=NODE_ENV test.js" diff --git a/integrationTests/dev-deno-explicit/test.js b/integrationTests/dev-deno-explicit/test.js new file mode 100644 index 0000000000..a3e6adb1c2 --- /dev/null +++ b/integrationTests/dev-deno-explicit/test.js @@ -0,0 +1,20 @@ +import { enableDevMode, isObjectType } from 'graphql'; + +enableDevMode(); + +class GraphQLObjectType { + get [Symbol.toStringTag]() { + return 'GraphQLObjectType'; + } +} + +try { + isObjectType(new GraphQLObjectType()); + throw new Error( + 'Expected isObjectType to throw an error in Deno explicit development mode.', + ); +} catch (error) { + if (!error.message.includes('from another module or realm')) { + throw error; + } +} diff --git a/integrationTests/dev-esbuild/package.json b/integrationTests/dev-esbuild/package.json index df513566e4..1e51840f8f 100644 --- a/integrationTests/dev-esbuild/package.json +++ b/integrationTests/dev-esbuild/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "scripts": { - "build": "esbuild index.js --bundle --outfile=dist/bundle.js --format=esm", + "build": "esbuild index.js --bundle --outfile=dist/bundle.js --format=esm --conditions=development", "test": "npm run build && node dist/bundle.js" }, "dependencies": { diff --git a/integrationTests/dev-jest/jest.config.js b/integrationTests/dev-jest/jest.config.js new file mode 100644 index 0000000000..e69b1da716 --- /dev/null +++ b/integrationTests/dev-jest/jest.config.js @@ -0,0 +1,22 @@ +const jestConfig = { + testEnvironment: 'node', + + testEnvironmentOptions: { + customExportConditions: ['development'], + }, + + transform: { + '^.+\\.(t|j)sx?$': ['@swc/jest'], + }, + + transformIgnorePatterns: [ + // Allow 'graphql' to be transformed, ignore all other node_modules. + // This regex means: "match /node_modules/ unless it's followed by graphql/" + '/node_modules/(?!graphql/)', + // Keep Jest's default for .pnp.js files if using Yarn PnP + '\\.pnp\\.[^\\/]+$', + ], +}; + +// eslint-disable-next-line no-restricted-exports, import/no-default-export +export default jestConfig; diff --git a/integrationTests/dev-jest/package.json b/integrationTests/dev-jest/package.json index 7d8e9ccf19..a01045c53f 100644 --- a/integrationTests/dev-jest/package.json +++ b/integrationTests/dev-jest/package.json @@ -1,5 +1,5 @@ { - "description": "graphql-js development mode should work with Jest", + "description": "graphql-js development mode should work with Jest and SWC", "private": true, "type": "module", "scripts": { @@ -9,6 +9,8 @@ "graphql": "file:../graphql.tgz" }, "devDependencies": { - "jest": "^29.7.0" + "jest": "^29.7.0", + "@swc/core": "^1.6.0", + "@swc/jest": "^0.2.36" } } diff --git a/integrationTests/dev-node-condition-based/package.json b/integrationTests/dev-node-condition-based/package.json new file mode 100644 index 0000000000..d735c5ecb3 --- /dev/null +++ b/integrationTests/dev-node-condition-based/package.json @@ -0,0 +1,11 @@ +{ + "description": "graphql-js condition-based development mode should work with node", + "private": true, + "type": "module", + "scripts": { + "test": "node --conditions=development test.js" + }, + "dependencies": { + "graphql": "file:../graphql.tgz" + } +} diff --git a/integrationTests/dev-node/test.js b/integrationTests/dev-node-condition-based/test.js similarity index 77% rename from integrationTests/dev-node/test.js rename to integrationTests/dev-node-condition-based/test.js index 303ba7f590..a3c4ce4c45 100644 --- a/integrationTests/dev-node/test.js +++ b/integrationTests/dev-node-condition-based/test.js @@ -9,7 +9,7 @@ class GraphQLObjectType { try { isObjectType(new GraphQLObjectType()); throw new Error( - 'Expected isObjectType to throw an error in Node.js implicit dev mode.', + 'Expected isObjectType to throw an error in Node.js condition-based development mode.', ); } catch (error) { if (!error.message.includes('from another module or realm')) { diff --git a/integrationTests/dev-node/package.json b/integrationTests/dev-node-explicit/package.json similarity index 65% rename from integrationTests/dev-node/package.json rename to integrationTests/dev-node-explicit/package.json index ab7124ef6a..73d30603d1 100644 --- a/integrationTests/dev-node/package.json +++ b/integrationTests/dev-node-explicit/package.json @@ -1,5 +1,5 @@ { - "description": "graphql-js development mode should work with node", + "description": "graphql-js explicit development mode should work with node", "private": true, "type": "module", "scripts": { diff --git a/integrationTests/dev-node-explicit/test.js b/integrationTests/dev-node-explicit/test.js new file mode 100644 index 0000000000..cfbb97c0ce --- /dev/null +++ b/integrationTests/dev-node-explicit/test.js @@ -0,0 +1,20 @@ +import { enableDevMode, isObjectType } from 'graphql'; + +enableDevMode(); + +class GraphQLObjectType { + get [Symbol.toStringTag]() { + return 'GraphQLObjectType'; + } +} + +try { + isObjectType(new GraphQLObjectType()); + throw new Error( + 'Expected isObjectType to throw an error in Node.js development mode.', + ); +} catch (error) { + if (!error.message.includes('from another module or realm')) { + throw error; + } +} diff --git a/integrationTests/dev-rollup/package.json b/integrationTests/dev-rollup/package.json index 9f6cf1084b..b3bd3157bd 100644 --- a/integrationTests/dev-rollup/package.json +++ b/integrationTests/dev-rollup/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "graphql": "file:../graphql.tgz", - "rollup": "^4.0.0" + "rollup": "^4.0.0", + "@rollup/plugin-node-resolve": "^15.0.0" } } diff --git a/integrationTests/dev-rollup/rollup.config.js b/integrationTests/dev-rollup/rollup.config.js index d46fd8fedb..d3cbff3310 100644 --- a/integrationTests/dev-rollup/rollup.config.js +++ b/integrationTests/dev-rollup/rollup.config.js @@ -1,9 +1,17 @@ +// eslint-disable-next-line n/no-missing-import +import resolve from '@rollup/plugin-node-resolve'; + const rollupConfig = { input: 'index.js', output: { file: 'dist/bundle.js', format: 'es', }, + plugins: [ + resolve({ + exportConditions: ['development'], + }), + ], }; // eslint-disable-next-line no-restricted-exports, import/no-default-export diff --git a/integrationTests/dev-rspack/rspack.config.js b/integrationTests/dev-rspack/rspack.config.js index a39f3e5b4f..5ca0b715bc 100644 --- a/integrationTests/dev-rspack/rspack.config.js +++ b/integrationTests/dev-rspack/rspack.config.js @@ -11,6 +11,9 @@ const rspack = { }, mode: 'development', target: 'node', + resolve: { + conditionNames: ['development'], + }, }; // eslint-disable-next-line no-restricted-exports, import/no-default-export diff --git a/integrationTests/dev-swc/index.js b/integrationTests/dev-swc/index.js index 4db39b4ffb..8385819d25 100644 --- a/integrationTests/dev-swc/index.js +++ b/integrationTests/dev-swc/index.js @@ -1,4 +1,6 @@ -import { isObjectType } from 'graphql'; +import { enableDevMode, isObjectType } from 'graphql'; + +enableDevMode(); class GraphQLObjectType { get [Symbol.toStringTag]() { diff --git a/integrationTests/dev-vitest/vitest.config.js b/integrationTests/dev-vitest/vitest.config.js index e6d9de6ed7..4351ba7182 100644 --- a/integrationTests/dev-vitest/vitest.config.js +++ b/integrationTests/dev-vitest/vitest.config.js @@ -2,6 +2,9 @@ import { defineConfig } from 'vitest/config'; const vitestConfig = defineConfig({ + resolve: { + conditions: ['development'], + }, test: { include: ['**/*.test.js'], }, diff --git a/integrationTests/dev-webpack/webpack.config.js b/integrationTests/dev-webpack/webpack.config.js index 4715eee3a1..5802e9dc96 100644 --- a/integrationTests/dev-webpack/webpack.config.js +++ b/integrationTests/dev-webpack/webpack.config.js @@ -11,6 +11,9 @@ const webpackConfig = { }, mode: 'development', target: 'node', + resolve: { + conditionNames: ['development'], + }, }; // eslint-disable-next-line no-restricted-exports, import/no-default-export diff --git a/integrationTests/prod-bun/package.json b/integrationTests/prod-bun/package.json index 458560b6fa..8a85058ce8 100644 --- a/integrationTests/prod-bun/package.json +++ b/integrationTests/prod-bun/package.json @@ -2,7 +2,7 @@ "description": "graphql-js production mode should work with Bun", "private": true, "scripts": { - "test": "docker run --rm --volume \"$PWD\":/usr/src/app -w /usr/src/app oven/bun:\"$BUN_VERSION\"-slim bun --define process.env.NODE_ENV='\"production\"' test.js" + "test": "docker run --rm --volume \"$PWD\":/usr/src/app -w /usr/src/app oven/bun:\"$BUN_VERSION\"-slim bun test.js" }, "dependencies": { "graphql": "file:../graphql.tgz" diff --git a/integrationTests/prod-deno/env-shim.js b/integrationTests/prod-deno/env-shim.js deleted file mode 100644 index dbb9d9dace..0000000000 --- a/integrationTests/prod-deno/env-shim.js +++ /dev/null @@ -1,4 +0,0 @@ -// eslint-disable-next-line n/prefer-global/process -import process from 'node:process'; - -process.env.NODE_ENV = 'production'; diff --git a/integrationTests/prod-deno/package.json b/integrationTests/prod-deno/package.json index 8cede53be3..6f7d4fa953 100644 --- a/integrationTests/prod-deno/package.json +++ b/integrationTests/prod-deno/package.json @@ -2,7 +2,7 @@ "description": "graphql-js production mode should work with Deno", "private": true, "scripts": { - "test": "docker run --rm --volume \"$PWD\":/usr/src/app -w /usr/src/app denoland/deno:alpine-\"$DENO_VERSION\" deno run --allow-env --preload env-shim.js test.js" + "test": "docker run --rm --volume \"$PWD\":/usr/src/app -w /usr/src/app denoland/deno:alpine-\"$DENO_VERSION\" deno run test.js" }, "dependencies": { "graphql": "file:../graphql.tgz" diff --git a/integrationTests/prod-esbuild/package.json b/integrationTests/prod-esbuild/package.json index c6e9077d3a..12c8171b2c 100644 --- a/integrationTests/prod-esbuild/package.json +++ b/integrationTests/prod-esbuild/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "scripts": { - "build": "esbuild index.js --bundle --outfile=dist/bundle.js --format=esm --define:process.env.NODE_ENV='\"production\"'", + "build": "esbuild index.js --bundle --outfile=dist/bundle.js --format=esm", "test": "npm run build && node dist/bundle.js" }, "dependencies": { diff --git a/integrationTests/prod-node/package.json b/integrationTests/prod-node/package.json index aec74f094f..280e414e96 100644 --- a/integrationTests/prod-node/package.json +++ b/integrationTests/prod-node/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "scripts": { - "test": "NODE_ENV=production node test.js" + "test": "node test.js" }, "dependencies": { "graphql": "file:../graphql.tgz" diff --git a/integrationTests/prod-rollup/package.json b/integrationTests/prod-rollup/package.json index 07aedb43d3..582828543b 100644 --- a/integrationTests/prod-rollup/package.json +++ b/integrationTests/prod-rollup/package.json @@ -9,7 +9,6 @@ "dependencies": { "graphql": "file:../graphql.tgz", "rollup": "^4.0.0", - "@rollup/plugin-node-resolve": "^15.0.0", - "@rollup/plugin-replace": "^5.0.0" + "@rollup/plugin-node-resolve": "^15.0.0" } } diff --git a/integrationTests/prod-rollup/rollup.config.js b/integrationTests/prod-rollup/rollup.config.js index 5b4bdff379..d46fd8fedb 100644 --- a/integrationTests/prod-rollup/rollup.config.js +++ b/integrationTests/prod-rollup/rollup.config.js @@ -1,22 +1,9 @@ -// eslint-disable-next-line n/no-missing-import -import resolve from '@rollup/plugin-node-resolve'; -// eslint-disable-next-line n/no-missing-import -import replace from '@rollup/plugin-replace'; - const rollupConfig = { input: 'index.js', output: { file: 'dist/bundle.js', format: 'es', }, - plugins: [ - replace({ - preventAssignment: true, - 'process.env.NODE_ENV': JSON.stringify('production'), - include: ['node_modules/graphql/**'], - }), - resolve(), - ], }; // eslint-disable-next-line no-restricted-exports, import/no-default-export diff --git a/integrationTests/prod-swc/package.json b/integrationTests/prod-swc/package.json index 78f0f7f259..8ae43d3619 100644 --- a/integrationTests/prod-swc/package.json +++ b/integrationTests/prod-swc/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "build": "swc index.js -d dist", - "test": "npm run build && NODE_ENV=production node dist/index.js" + "test": "npm run build && node dist/index.js" }, "dependencies": { "graphql": "file:../graphql.tgz", diff --git a/resources/build-npm.ts b/resources/build-npm.ts index 01121a668b..607ece9eec 100644 --- a/resources/build-npm.ts +++ b/resources/build-npm.ts @@ -6,7 +6,7 @@ import ts from 'typescript'; import { changeExtensionInImportPaths } from './change-extension-in-import-paths.js'; import { inlineInvariant } from './inline-invariant.js'; -import type { ConditionalExports } from './utils.js'; +import type { PlatformConditionalExports } from './utils.js'; import { prettify, readPackageJSON, @@ -24,8 +24,11 @@ await buildPackage('./npmEsmDist', true); showDirStats('./npmEsmDist'); async function buildPackage(outDir: string, isESMOnly: boolean): Promise { + const devDir = path.join(outDir, '__dev__'); + fs.rmSync(outDir, { recursive: true, force: true }); fs.mkdirSync(outDir); + fs.mkdirSync(devDir); fs.copyFileSync('./LICENSE', `./${outDir}/LICENSE`); fs.copyFileSync('./README.md', `./${outDir}/README.md`); @@ -118,16 +121,86 @@ async function buildPackage(outDir: string, isESMOnly: boolean): Promise { emitTSFiles({ outDir, module: 'es2020', extension: '.mjs' }); packageJSON.exports = {}; - for (const filepath of emittedTSFiles) { - if (path.basename(filepath) === 'index.js') { - const relativePath = './' + path.relative('./npmDist', filepath); - packageJSON.exports[path.dirname(relativePath)] = - buildExports(relativePath); + for (const prodFile of emittedTSFiles) { + const { dir, base, name, ext } = path.parse(prodFile); + + if (ext === '.map') { + continue; + } else if (path.basename(dir) === 'dev') { + packageJSON.exports['./dev'] = buildPlatformConditionalExports( + './dev', + 'index', + ); + continue; + } + + const relativePathToProd = path.relative(prodFile, outDir); + + const { name: innerName, ext: innerExt } = path.parse(name); + + if (innerExt === '.d') { + const relativePathAndName = path.relative( + outDir, + `${dir}/${innerName}`, + ); + + const line = `export * from '${relativePathToProd}/${relativePathAndName}.mjs';`; + for (const typeExt of ['.ts', '.mts']) { + writeGeneratedFile( + path.join( + devDir, + path.relative(outDir, `${dir}/${name}${typeExt}`), + ), + line, + ); + } + continue; + } + + const relativePathAndName = path.relative(outDir, `${dir}/${name}`); + + let lines = [ + `const { enableDevMode } = require('${relativePathToProd}/devMode.js');`, + 'enableDevMode();', + `module.exports = require('${relativePathToProd}/${relativePathAndName}.js');`, + ]; + + writeGeneratedFile( + path.join(devDir, path.relative(outDir, `${dir}/${name}.js`)), + lines.join('\n'), + ); + + lines = [ + `import { enableDevMode } from '${relativePathToProd}/devMode.mjs';`, + 'enableDevMode();', + `export * from '${relativePathToProd}/${relativePathAndName}.mjs';`, + ]; + + writeGeneratedFile( + path.join(devDir, path.relative(outDir, `${dir}/${name}.mjs`)), + lines.join('\n'), + ); + + if (base === 'index.js') { + const dirname = path.dirname(relativePathAndName); + packageJSON.exports[dirname === '.' ? dirname : `./${dirname}`] = { + development: buildPlatformConditionalExports( + './__dev__', + relativePathAndName, + ), + default: buildPlatformConditionalExports('.', relativePathAndName), + }; } } - packageJSON.exports['./*.js'] = buildExports('./*.js'); - packageJSON.exports['./*'] = buildExports('./*.js'); + const globEntryPoints = { + development: buildPlatformConditionalExports('./__dev__', '*'), + default: buildPlatformConditionalExports('.', '*'), + }; + packageJSON.exports['./*.js'] = globEntryPoints; + packageJSON.exports['./*'] = globEntryPoints; + + packageJSON.sideEffects = ['__dev__/*']; } const packageJsonPath = `./${outDir}/package.json`; @@ -186,7 +259,11 @@ function emitTSFiles(options: { writeGeneratedFile(filepath, body); }; - const tsProgram = ts.createProgram(['src/index.ts'], tsOptions, tsHost); + const tsProgram = ts.createProgram( + ['src/index.ts', 'src/dev/index.ts'], + tsOptions, + tsHost, + ); const tsResult = tsProgram.emit(undefined, undefined, undefined, undefined, { after: [changeExtensionInImportPaths({ extension }), inlineInvariant], }); @@ -201,8 +278,10 @@ function emitTSFiles(options: { }; } -function buildExports(filepath: string): ConditionalExports { - const { dir, name } = path.parse(filepath); +function buildPlatformConditionalExports( + dir: string, + name: string, +): PlatformConditionalExports { const base = `./${path.join(dir, name)}`; return { module: `${base}.mjs`, diff --git a/resources/integration-test.ts b/resources/integration-test.ts index 33df9f9825..ab8abf4584 100644 --- a/resources/integration-test.ts +++ b/resources/integration-test.ts @@ -54,8 +54,10 @@ describe('Integration Tests', () => { testOnNodeProject('conditions'); // Development mode tests - testOnNodeProject('dev-node'); - testOnNodeProject('dev-deno'); + testOnNodeProject('dev-node-condition-based'); + testOnNodeProject('dev-node-explicit'); + testOnNodeProject('dev-deno-condition-based'); + testOnNodeProject('dev-deno-explicit'); testOnNodeProject('dev-bun'); testOnNodeProject('dev-webpack'); testOnNodeProject('dev-rspack'); diff --git a/resources/utils.ts b/resources/utils.ts index 08d26c2537..d80d486a7e 100644 --- a/resources/utils.ts +++ b/resources/utils.ts @@ -228,7 +228,8 @@ interface PackageJSON { repository?: { url?: string }; scripts?: { [name: string]: string }; type?: string; - exports: { [path: string]: string | ConditionalExports }; + sideEffects?: false | Array; + exports: ConditionalExports; types?: string; typesVersions: { [ranges: string]: { [path: string]: Array } }; devDependencies?: { [name: string]: string }; @@ -240,6 +241,18 @@ interface PackageJSON { } export interface ConditionalExports { + [path: string]: + | string + | PlatformConditionalExports + | EnvironmentConditionalExports; +} + +interface EnvironmentConditionalExports { + development: PlatformConditionalExports; + default: PlatformConditionalExports; +} + +export interface PlatformConditionalExports { module: string; bun: string; 'module-sync': string; diff --git a/src/__tests__/devMode-test.ts b/src/__tests__/devMode-test.ts new file mode 100644 index 0000000000..e869ea9a1a --- /dev/null +++ b/src/__tests__/devMode-test.ts @@ -0,0 +1,15 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { enableDevMode, isDevModeEnabled } from '../devMode.js'; + +describe('dev mode', () => { + it('should be disabled by default', () => { + expect(isDevModeEnabled()).to.equal(false); + }); + + it('should enable development mode', () => { + enableDevMode(); + expect(isDevModeEnabled()).to.equal(true); + }); +}); diff --git a/src/devMode.ts b/src/devMode.ts new file mode 100644 index 0000000000..3a07f30b2d --- /dev/null +++ b/src/devMode.ts @@ -0,0 +1,12 @@ +import { enableDevInstanceOf } from './jsutils/instanceOf.js'; + +let devMode = false; + +export function enableDevMode(): void { + devMode = true; + enableDevInstanceOf(); +} + +export function isDevModeEnabled(): boolean { + return devMode; +} diff --git a/src/index.ts b/src/index.ts index 7bba9d8eee..434411deb7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,9 @@ // The GraphQL.js version info. export { version, versionInfo } from './version.js'; +// Enable development mode for additional checks. +export { enableDevMode, isDevModeEnabled } from './devMode.js'; + // The primary entry point into fulfilling a GraphQL request. export type { GraphQLArgs } from './graphql.js'; export { graphql, graphqlSync } from './graphql.js'; diff --git a/src/jsutils/__tests__/instanceOf-test.ts b/src/jsutils/__tests__/instanceOf-test.ts index f0920c80bc..1d717f6afe 100644 --- a/src/jsutils/__tests__/instanceOf-test.ts +++ b/src/jsutils/__tests__/instanceOf-test.ts @@ -1,9 +1,13 @@ import { expect } from 'chai'; -import { describe, it } from 'mocha'; +import { before, describe, it } from 'mocha'; -import { instanceOf } from '../instanceOf.js'; +import { enableDevInstanceOf, instanceOf } from '../instanceOf.js'; describe('instanceOf', () => { + before(() => { + enableDevInstanceOf(); + }); + it('do not throw on values without prototype', () => { const fooSymbol: unique symbol = Symbol('Foo'); class Foo { diff --git a/src/jsutils/instanceOf.ts b/src/jsutils/instanceOf.ts index 2a483ce121..9577aac24b 100644 --- a/src/jsutils/instanceOf.ts +++ b/src/jsutils/instanceOf.ts @@ -1,11 +1,5 @@ import { inspect } from './inspect.js'; -/* c8 ignore next 3 */ -const isProduction = - globalThis.process != null && - // eslint-disable-next-line no-undef - process.env.NODE_ENV === 'production'; - /** * A replacement for instanceof relying on a symbol-driven type brand which in * development mode includes an error warning when multi-realm constructors are @@ -13,37 +7,26 @@ const isProduction = * See: https://expressjs.com/en/advanced/best-practice-performance.html#set-node_env-to-production * See: https://webpack.js.org/guides/production/ */ -export const instanceOf: ( +function devInstanceOf( value: unknown, symbol: symbol, constructor: Constructor, -) => boolean = - /* c8 ignore next 9 */ - // FIXME: https://github.com/graphql/graphql-js/issues/2317 - isProduction - ? function instanceOf(value: unknown, symbol: symbol): boolean { - return (value as any)?.__kind === symbol; - } - : function instanceOf( - value: unknown, - symbol: symbol, - constructor: Constructor, - ): boolean { - if ((value as any)?.__kind === symbol) { - return true; - } - if (typeof value === 'object' && value !== null) { - // Prefer Symbol.toStringTag since it is immune to minification. - const className = constructor.prototype[Symbol.toStringTag]; - const valueClassName = - // We still need to support constructor's name to detect conflicts with older versions of this library. - Symbol.toStringTag in value - ? value[Symbol.toStringTag] - : value.constructor?.name; - if (className === valueClassName) { - const stringifiedValue = inspect(value); - throw new Error( - `Cannot use ${className} "${stringifiedValue}" from another module or realm. +): boolean { + if ((value as any)?.__kind === symbol) { + return true; + } + if (typeof value === 'object' && value !== null) { + // Prefer Symbol.toStringTag since it is immune to minification. + const className = constructor.prototype[Symbol.toStringTag]; + const valueClassName = + // We still need to support constructor's name to detect conflicts with older versions of this library. + Symbol.toStringTag in value + ? value[Symbol.toStringTag] + : value.constructor?.name; + if (className === valueClassName) { + const stringifiedValue = inspect(value); + throw new Error( + `Cannot use ${className} "${stringifiedValue}" from another module or realm. Ensure that there is only one instance of "graphql" in the node_modules directory. If different versions of "graphql" are the dependencies of other @@ -55,11 +38,22 @@ Duplicate "graphql" modules cannot be used at the same time since different versions may have different capabilities and behavior. The data from one version used in the function from another could produce confusing and spurious results.`, - ); - } - } - return false; - }; + ); + } + } + return false; +} + +function prodInstanceOf(value: unknown, symbol: symbol): boolean { + return (value as any)?.__kind === symbol; +} + +interface Constructor { + prototype: { + [Symbol.toStringTag]: string; + }; + new (...args: Array): any; +} interface Constructor { prototype: { @@ -67,3 +61,13 @@ interface Constructor { }; new (...args: Array): any; } + +/* eslint-disable-next-line import/no-mutable-exports */ +export let instanceOf: ( + value: unknown, + symbol: symbol, + constructor: Constructor, +) => boolean = prodInstanceOf; +export function enableDevInstanceOf(): void { + instanceOf = devInstanceOf; +} diff --git a/website/pages/docs/_meta.ts b/website/pages/docs/_meta.ts index 2e6156976b..5006571650 100644 --- a/website/pages/docs/_meta.ts +++ b/website/pages/docs/_meta.ts @@ -41,6 +41,7 @@ const meta = { type: 'separator', title: 'Production & Scaling', }, + 'development-mode': '', 'going-to-production': '', 'scaling-graphql': '', }; diff --git a/website/pages/docs/development-mode.mdx b/website/pages/docs/development-mode.mdx new file mode 100644 index 0000000000..000935cf93 --- /dev/null +++ b/website/pages/docs/development-mode.mdx @@ -0,0 +1,195 @@ +--- +title: Development Mode +--- + +# Development Mode + +In development mode, GraphQL.JS can provides additional runtime check appropriate for +development-time errors, including primarily the erroneous inclusion of multiple +GraphQL.JS modules. + +Unlike earlier versions of GraphQL.JS, by default, development mode is disabled. +This is to best ensure that production builds do not incur the performance and bundle +size penalties associated with the additional checks. + +Also, unlike earlier versions, development mode is not configured by use of +environment variables, which are accessed in disparate ways on the various platforms. +In particular, the `NODE_ENV` environment variable now has no effect on triggering +development mode. Rather, development mode is either enabled: +1. explicitly, by calling `enableDevMode()` or +2. implicitly, by setting the 'development' condition, which is possible only on + platforms that support `package.json` conditional exports and custom conditions. + +Conditional exports are supported by: Node.js, Deno (canary), Bun, Webpack 5, Rspack, +Rollup (via the `node-resolve` plugin), esbuild, Vite, and Rsbuild. create-react-app and Next.js +support conditional exports when using Webpack 5 as their bundler. + +Conditional exports are not supported by Deno (current), Webpack 4, Rollup (without +the `node-resolve` plugin), or swc. create-react-app and Next.js do not support +conditional exports when using Webpack 4 as their bundler, nor does Next.js yet +support conditional exports when using Turbopack (see +https://github.com/vercel/next.js/discussions/78912). + +We encourage enabling development mode in a development environment, either +explicitly or implicitly. This allows for an additional check to ensure that only a +single GraphQL.JS module is used. Additional development-time checks may also be added in +the future. + +First, we will discuss the implications of using multiple GraphQL.JS modules, and then +we will share additional details for how to enable development mode on various platforms. + +## Multiple Graphql.JS Modules + +Only a single GraphQL.JS can be used within a project. Different GraphQL.JS versions cannot be +used at the same time since different versions may have different capabilities and behavior. +The data from one version used in the function from another could produce confusing and spurious +results. + +Duplicate modules of GraphQL.JS of the same version may also fail at runtime, sometimes in +unexpected ways. This is because GraphQL.JS relies on the identity of the module for +key features. Most significantly, `instanceof` checks are used throughout GraphQL.JS to +identify and distinguish between GraphQL schema elements, such as the particular GraphQL type. +Also, special exported constants like `BREAK` allow library users to manipulate visitor +behavior, also relying on the module identity. + +To ensure that only a single GraphQL.JS module is used, all libraries depending on GraphQL.JS +should use the appropriate peer dependency mechanism, as provided by their package manager, +bundler, build tool, or runtime. + +In development mode, GraphQL.JS provides a validation check triggered by any use of `instanceof` +that should catch most cases of multiple GraphQL.js modules being used in the same project. + +This additional validation is unnecessary in production, where the GraphQL.js library is +expected to be have been setup correctly as a single module. So as to avoid the performance +and bundle size overhead this check entails, it is only included when the `development` +exports condition is explicitly enabled or when the `enableDevMode()` module is +called. + +## Enabling Development Mode + +### A Catch-All Option: Explicit Enabling of Development Mode + +Development mode may be enabled explicitly by calling `enableDevMode()`: + +```js +// entrypoint.js +import { enableDevMode } from 'graphql'; +enableDevMode(); +``` + +A bootstrapping file can be used to enable development mode conditionally: + +```js +// bootstrap.js +import process from 'node:process'; +import { enableDevMode } from 'graphql'; +if (process.env.NODE_ENV === 'development') { + enableDevMode(); +} +import './path/to/my/entry.js'; +``` + +The above is compatible with Node.js; the exact environment variable and method +of accessing it depends on the individual platform. + +### Conditional Exports and Implicit Enabling of Development Mode + +Depending on your platform, you may be able to use the 'development' condition +to enable development mode without the need for an explicit import. + +### Node.js + +In Node.js, the development condition can be enabled by passing the `--conditions=development` +flag to the Node.js runtime. This can be done within `package.json` scripts: + +```json +{ + "scripts": { + "start": "node --conditions=development index.js" + } +} +``` + +Alternatively, this can be included within the `NODE_OPTIONS` environment variable: + +```bash +export NODE_OPTIONS=development +``` + +#### Deno + +In Deno, conditional exports are not yet released, but are available within the latest canary build as follows: + +```bash +deno run --conditions=development main.js +``` + +Alternatively, within the canary build, this can be included within the `DENO_CONDITIONS` environment variable: + +```bash +export DENO_CONDITIONS=development +``` + +#### Bun + +In Bun, you can also enable the development condition by passing the `--conditions=development` to the runtime: + +```bash +bun --conditions=development main.js +``` + +#### Webpack + +Webpack 5 supports the 'development' condition natively and requires no additional configuration. + +### Rollup + +Rollup supports the 'development' condition only when using the `@rollup/plugin-node-resolve` plugin. + +```ts +//rollup.config.js +import resolve from '@rollup/plugin-node-resolve'; + +export default { + plugins: [ + resolve({ + exportConditions: ['development'], + }) + ] +}; +``` + +### esbuild + +When using esbuild, you can enable the 'development' condition by setting the `--conditions=development` +flag in your build command: + +```bash +esbuild --conditions=development entrypoint.js +``` + +Note that setting any custom conditions will drop the default 'module' condition (used to avoid the dual +package hazard), so you may need to use: + +```bash +esbuild --conditions=development,module entrypoint.js +``` + +See further discussion within the [esbuild documentation](https://esbuild.github.io/api/#conditions) for +more details. + +### Vite + +Vite supports the 'development' condition natively and requires no additional configuration. + +### Next.js + +When using Webpack 5 as its bundler, Next.js supports the 'development' condition natively +and require no additional configuration. When using Webpack 4 or Turbopack, development mode +must be enabled explicitly. + +### create-react-app + +When using Webpack 5 as its bundler, create-react-app support the 'development' condition +natively and requires no additional configuration. When using Webpack 4, development mode +must be enabled explicitly. \ No newline at end of file diff --git a/website/pages/docs/going-to-production.mdx b/website/pages/docs/going-to-production.mdx index da69a36942..587b939b80 100644 --- a/website/pages/docs/going-to-production.mdx +++ b/website/pages/docs/going-to-production.mdx @@ -4,141 +4,15 @@ title: Going to Production # Going to Production -GraphQL.JS contains a few development checks which in production will cause slower performance and -an increase in bundle-size. Every bundler goes about these changes different, in here we'll list -out the most popular ones. - -GraphQL.js includes development-time checks that are useful during local testing but should -be disabled in production to reduce overhead. Additional concerns include caching, error handling, -schema management, and operational monitoring. - This guide covers key practices to prepare a server built with GraphQL.js for production use. -## Optimize your build for production - -In development, GraphQL.js includes validation checks to catch common mistakes like invalid schemas -or resolver returns. These checks are not needed in production and can increase runtime overhead. - -You can disable them by setting `process.env.NODE_ENV` to `'production'` during your build process. -GraphQL.js will automatically skip over development-only code paths. - -Bundlers are tools that compile and optimize JavaScript for deployment. Most can be configured to -replace environment variables such as `process.env.NODE_ENV` at build time, -allowing for unused code (such as development only code paths) to be elided by -minification tools. - -### Bundler configuration examples - -The following examples show how to configure common bundlers to set `process.env.NODE_ENV` -and remove development-only code: - -#### Vite - -```js -// vite.config.js -import { defineConfig } from 'vite'; - -export default defineConfig({ - define: { - 'process.env.NODE_ENV': '"production"', - }, -}); -``` - -#### Next.js - -When you build your application with `next build` and run it using `next start`, Next.js sets -`process.env.NODE_ENV` to `'production'` automatically. No additional configuration is required. - -```bash -next build -next start -``` - -If you run a custom server, make sure `NODE_ENV` is set manually. - -#### Create React App (CRA) - -To customize Webpack behavior in CRA, you can use a tool like [`craco`](https://craco.js.org/). -This example uses CommonJS syntax instead of ESM syntax, which is required by `craco.config.js`: - -```js -// craco.config.js -const webpack = require('webpack'); - -module.exports = { - webpack: { - plugins: [ - new webpack.DefinePlugin({ - 'globalThis.process': JSON.stringify(true), - 'process.env.NODE_ENV': JSON.stringify('production'), - }), - ], - }, -}; -``` - -#### esbuild - -```json -{ - "define": { - "globalThis.process": true, - "process.env.NODE_ENV": "production" - } -} -``` - -#### Webpack - -```js -// webpack.config.js -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +Concerns include caching, error handling, schema management, and operational monitoring. -export default { - mode: 'production', // Automatically sets NODE_ENV - context: __dirname, -}; -``` - -#### Rollup - -```js -// rollup.config.js -import replace from '@rollup/plugin-replace'; - -export default { - plugins: [ - replace({ - preventAssignment: true, - 'process.env.NODE_ENV': JSON.stringify('production'), - }), - ], -}; -``` - -#### SWC - -```json filename=".swcrc" -{ - "jsc": { - "transform": { - "optimizer": { - "globals": { - "vars": { - "globalThis.process": true, - "process.env.NODE_ENV": "production" - } - } - } - } - } -} -``` +Of note, GraphQL.JS provides a development mode which includes additional checks impacting +performance and bundle size, as further discussed within the +[Development Mode](./development-mode) section. Development mode is disabled by default, but +one can and should verify that it has not been enabled in production, especially if it is +enabled in your development environment. ## Secure your schema diff --git a/website/pages/upgrade-guides/v16-v17.mdx b/website/pages/upgrade-guides/v16-v17.mdx index bbb40f82e6..5e6629cd6b 100644 --- a/website/pages/upgrade-guides/v16-v17.mdx +++ b/website/pages/upgrade-guides/v16-v17.mdx @@ -35,6 +35,16 @@ does it seem to provide the `deno` condition when calling `require`, see https:/ Deno users can access a Typescript build for deno via git://github.com/graphql/graphql-js.git#deno as well as by specifically loading the index.mjs file, i.e. `import { graphql } from 'graphql/index.mjs'`, although this does not protect against the dual-package hazard. +## Development mode no longer enabled by default and no longer dependent on NODE_ENV value + +GraphQl.js development mode in v17 is disabled by default and can be enabled by the `development` condition on supporting platforms or +by explicitly enabling it within user code by calling `enableDevMode()`. Development mode may trigger permanent de-optimizations and +therefore cannot be disabled once enabled. The new `isDevModeEnabled()` function can be used to check whether development mode has +been enabled. + +GraphQL.js development mode no longer depends on the `NODE_ENV` environment variable; build tools other than Node.js no longer need +to replace this Node.js specific code. + ## Default values GraphQL schemas allow default values for input fields and arguments. Historically, GraphQL.js did not rigorously validate or coerce these From cfdce8935e44541904dfa90984a72cd748a42d16 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Wed, 13 Aug 2025 05:47:51 +0300 Subject: [PATCH 2/2] update docs --- website/pages/docs/development-mode.mdx | 26 +++++++++---------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/website/pages/docs/development-mode.mdx b/website/pages/docs/development-mode.mdx index 000935cf93..7326440862 100644 --- a/website/pages/docs/development-mode.mdx +++ b/website/pages/docs/development-mode.mdx @@ -47,7 +47,7 @@ results. Duplicate modules of GraphQL.JS of the same version may also fail at runtime, sometimes in unexpected ways. This is because GraphQL.JS relies on the identity of the module for -key features. Most significantly, `instanceof` checks are used throughout GraphQL.JS to +key features. Most significantly, unique symbols are used throughout GraphQL.JS to identify and distinguish between GraphQL schema elements, such as the particular GraphQL type. Also, special exported constants like `BREAK` allow library users to manipulate visitor behavior, also relying on the module identity. @@ -56,8 +56,8 @@ To ensure that only a single GraphQL.JS module is used, all libraries depending should use the appropriate peer dependency mechanism, as provided by their package manager, bundler, build tool, or runtime. -In development mode, GraphQL.JS provides a validation check triggered by any use of `instanceof` -that should catch most cases of multiple GraphQL.js modules being used in the same project. +In development mode, GraphQL.JS provides validation checks that should catch most cases of +multiple GraphQL.js modules being used within the same project. This additional validation is unnecessary in production, where the GraphQL.js library is expected to be have been setup correctly as a single module. So as to avoid the performance @@ -100,31 +100,23 @@ to enable development mode without the need for an explicit import. ### Node.js In Node.js, the development condition can be enabled by passing the `--conditions=development` -flag to the Node.js runtime. This can be done within `package.json` scripts: - -```json -{ - "scripts": { - "start": "node --conditions=development index.js" - } -} -``` +flag to the Node.js runtime. Alternatively, this can be included within the `NODE_OPTIONS` environment variable: ```bash -export NODE_OPTIONS=development +export NODE_OPTIONS=--conditions=development ``` #### Deno -In Deno, conditional exports are not yet released, but are available within the latest canary build as follows: +In Deno version 2.4.0 and later, you can enable the development condition by passing the `--conditions=development` flag to the runtime: ```bash deno run --conditions=development main.js ``` -Alternatively, within the canary build, this can be included within the `DENO_CONDITIONS` environment variable: +Alternatively, the `DENO_CONDITIONS` environment variable may be used: ```bash export DENO_CONDITIONS=development @@ -132,7 +124,7 @@ export DENO_CONDITIONS=development #### Bun -In Bun, you can also enable the development condition by passing the `--conditions=development` to the runtime: +In Bun version 1.0.30 and later, you can enable the development condition by passing the `--conditions=development` flag to the runtime: ```bash bun --conditions=development main.js @@ -190,6 +182,6 @@ must be enabled explicitly. ### create-react-app -When using Webpack 5 as its bundler, create-react-app support the 'development' condition +When using Webpack 5 as its bundler, create-react-app supports the 'development' condition natively and requires no additional configuration. When using Webpack 4, development mode must be enabled explicitly. \ No newline at end of file