v1.0.0 incompatible with Node.js at runtime
The recent v1.0.0 release has made this package incompatible with the Node.js runtime, because of this change from a valid fully specified import path to an invalid non fully specified import path missing the file extension:
https://github.com/slicknode/graphql-query-complexity/pull/91/files#diff-720574529a576948113b0f865801527a139a756ce25203d58f98f714bfd59b76R11
With Node.js v22.3.0, here is the runtime error:
node:internal/modules/run_main:115
triggerUncaughtException(
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '[redacted]/node_modules/graphql/execution/values' imported from [redacted]/node_modules/graphql-query-complexity/dist/esm/QueryComplexity.js
Did you mean to import "graphql/execution/values.js"?
at finalizeResolution (node:internal/modules/esm/resolve:260:11)
at moduleResolve (node:internal/modules/esm/resolve:920:10)
at defaultResolve (node:internal/modules/esm/resolve:1119:11)
at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:541:12)
at ModuleLoader.resolve (node:internal/modules/esm/loader:510:25)
at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:240:38)
at ModuleJob._link (node:internal/modules/esm/module_job:126:49) {
code: 'ERR_MODULE_NOT_FOUND',
url: 'file://[redacted]/node_modules/graphql/execution/values'
}
We do not have a bundler in our Node.js GraphQL API project, and even if we did, we would be enforcing modern ESM module resolution rules which require fully specified import paths.
We will have to keep our dependency on graphql-query-complexity at ^0.12.0 until this issue is resolved.
Because the ecosystem can't be importing both ESM and CJS versions of graphql modules, so far it has settled on only importing the CJS because that's what came first and took root. The people best positioned to effect a transition to the ESM is the graphql maintainers, who could stop publishing CJS in a new major version and only publish pure ESM and force an ecosystem migration to align to ESM instead. That was actually planned for a major graphql release, but they chickened out on it in the end.
As a package author, you have to pick either importing ESM or CJS from graphql; you can't avoid making a decision.
I see you're publishing both CJS and ESM modules in graphql-query-complexity:
https://unpkg.com/browse/[email protected]/dist/
Something you could consider, is having a build step when creating those artifacts that inserts .mjs file extensions in the graphql import paths when generating ESM modules, and .js when generating CJS. In the past I published a Babel plugin to help with this sort of thing:
https://github.com/jaydenseric/babel-plugin-transform-require-extensions
Nowadays in my own packages I militantly publish pure ESM and haven't used it for some time.
The catch with the above approach is that people (like me) like to consume your package as ESM, but expect it to pull in the CJS version of graphql. For us to continue using the CJS version of graphql (only to prevent duel package hazards with other ecosystem packages; our codebase is actually pure ESM) we would have to deep import the CJS version of graphql-query-complexity. Your package exports field would need to be updated to allow this, because currently it prevents any deep imports:
https://github.com/slicknode/graphql-query-complexity/blob/455978d4ed2934d7160a457965ab104998a3c515/package.json#L46-L51
For optimal JavaScript module design you should have package exports allowing deep imports of only the things people need anyway, and ideally remove the ability for consumers to import any other way. Force them to fall into the pit of success.
+1 for this
Has anyone found a workaround for using this plugin in an ESM project? I'm assuming this is the PR that introduced the bug: https://github.com/slicknode/graphql-query-complexity/pull/91
CC @ivome
In dist/esm/QueryComplexity.js:
import { getArgumentValues, getDirectiveValues, getVariableValues, } from 'graphql/execution/values';
It should be:
import { getArgumentValues, getDirectiveValues, getVariableValues, } from 'graphql/execution/values.mjs';
…
Btw, @narthur we still are able to use the commonjs version:
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const { getComplexity } = require("graphql-query-complexity");
This should be fixed in v1.1.0 that I just published.
I just tried to upgrade to 1.1.0, and the .mjs extension in the transpiled code is causing a bit of trouble in my app:
import { ValidationContext, isCompositeType, TypeInfo, visit, visitWithTypeInfo, isAbstractType, GraphQLObjectType, GraphQLInterfaceType, Kind, getNamedType, GraphQLError, SchemaMetaFieldDef, TypeMetaFieldDef, TypeNameMetaFieldDef, } from 'graphql/index.mjs';
It's causing these instanceof checks to fail because I'm using import ... from 'graphql'; elsewhere in my app. I've made sure that there's only one version of the graphql module in the node_modules folder.
To illustrate:
import * as graphql from 'graphql';
import * as graphqlMjs from 'graphql/index.mjs';
console.log(graphql.GraphQLObjectType === graphqlMjs.GraphQLObjectType); // false
console.log(
new graphql.GraphQLObjectType({ name: 'hey' }) instanceof
graphqlMjs.GraphQLObjectType,
); // false
I'm not sure if we should consider if this a problem with the graphql package, if importing graphql/index.mjs is an anti-pattern, or if instanceof checks should be avoided 🤔
This should be fixed in
v1.1.0that I just published.
Thanks for the new version. I tried and I have another error now:
GraphQL error: Error: Cannot use GraphQLObjectType "Query" 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
relied on modules, use "resolutions" to ensure only one version is installed.
https://yarnpkg.com/en/docs/selective-version-resolutions
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.
at instanceOf (file:///path/to/project/node_modules/graphql/jsutils/instanceOf.mjs:41:19)
at isObjectType (file:///path/to/project/node_modules/graphql/type/definition.mjs:51:10)
at TypeInfo.enter (file:///path/to/project/node_modules/graphql/utilities/TypeInfo.mjs:164:30)
at Object.enter (file:///path/to/project/node_modules/graphql/utilities/TypeInfo.mjs:364:16)
at visit (file:///path/to/project/node_modules/graphql/language/visitor.mjs:181:21)
at getComplexity (file:///path/to/project/node_modules/graphql-query-complexity/dist/esm/QueryComplexity.js:25:5)
at Object.didResolveOperation (file:///path/to/project/app/server/src/graphql/complexity-plugin.ts:28:30)
at file:///path/to/project/node_modules/@apollo/server/src/requestPipeline.ts:326:32
at Array.map (<anonymous>)
at processGraphQLRequest (file:///path/to/project/node_modules/@apollo/server/src/requestPipeline.ts:325:24)
Not sure why. I'm using graphql v16.10.0, I can't find another version in npm ls graphql. But it still works using createRequire so I will keep it with require for now.
What you guys are encountering is known as the "dual package hazard":
https://github.com/nodejs/package-examples/blob/52c84dfbc5603273264d08a481075b693f542b8a/README.md#dual-package-hazard
This can happen when everything is functioning as intended, and while there are very complicated ways to explain how a package can publish CJS modules that are consumable via ESM imports to avoid the hazard and support both ESM and CJS consumers, by far the better practice is to simply publish pure standard ESM modules (and no CJS at all) so every dependency and your application code is consuming the same thing: ESM.
As an application dev you have to decide, do I want everything in my app's module graph to consume the CJS, or the ESM, version of GraphQL and ecosystem dependencies (like graphql-query-complexity). Unfortunately, because CJS was the starting point years ago, most things currently consume the CJS version. As more and more packages switch to pure ESM, the default assumption will invert over time to consuming the ESM version. The community should identify if a particular GraphQL consuming package is forcing CJS, and apply pressure in issues and PRs to ensure it's converted to either pure ESM, or dual ESM/CJS so it's not holding up progress. Eventually CJS will be purged from the ecosystem, and we won't have to worry about dual package hazards ever again. Until then, sometimes if you are forced to use CJS dependencies that require the CJS version of graphql, you will have to configure a bundler to forcibly resolve the ESM versions whenever require paths resolve CJS modules.
I personally don't have much problems with this in my projects, because I avoid packages that don't properly support ESM and even publish my own alternatives for some things to bypass hacky and bloated areas of the ecosystem.
@papandreou
I'm not sure if we should consider if this a problem with the
graphqlpackage, if importinggraphql/index.mjsis an anti-pattern
Importing from graphql/index.mjs and graphql are both an anti pattern, because you should always use deep imports for optimal module design:
https://jaydenseric.com/blog/optimal-javascript-module-design
The new published version provides access to a graphql-query-complexity for both CJS and ESM. If the default import does not work because your other modules import a different GraphQL version, you can specify which one you want to use via explicit imports:
import { getComplexity } from "graphql-query-complexity/cjs";
Or for ESM
import { getComplexity } from "graphql-query-complexity/esm";
One of those should work in your project.
@ivome, but don't you agree that something is wrong with the builds? Right now when I use vanilla imports in my node.js project:
import { GraphQLObjectType } from 'graphql';
import { createComplexityRule } from 'graphql-query-complexity';
... I end up with a copy of the graphql-query-complexity library that doesn't recognize the types from that come out of the graphql library 🤔
The new published version provides access to a
graphql-query-complexityfor both CJS and ESM. If the default import does not work because your other modules import a different GraphQL version, you can specify which one you want to use via explicit imports:import { getComplexity } from "graphql-query-complexity/cjs"; Or for ESM
import { getComplexity } from "graphql-query-complexity/esm"; One of those should work in your project.
Unfortunately this won't work for an ESM project. The problem is that graphqljs doesn't publish "real" ESM, (they're using the "module" field). So a Node ESM project will resolve to the CJS version of the files when importing exports via "graphql", eg import { GraphQLObjectType } from "graphql". This package however deep imports ESM from graphqljs. Instead it should not deep import so shared dependencies resolve correctly.