From 498b8ff218e22d9a0ae7b25868f993fb0e43e389 Mon Sep 17 00:00:00 2001 From: Matt Anderson Date: Wed, 6 May 2026 11:57:18 -0400 Subject: [PATCH] feat(javascript_client): add ESM build output Generate an ESM copy of the JavaScript client during publish and expose it through conditional package exports while keeping the existing CommonJS output as the default require path. The ESM preparation step marks the generated output as module code and normalizes extensionless imports that Node's ESM resolver cannot load directly. --- .gitignore | 3 ++ javascript_client/esm/package.json | 3 ++ javascript_client/package.json | 68 +++++++++++++++++++++++- javascript_client/scripts/clean-esm.js | 7 +++ javascript_client/scripts/prepare-esm.js | 57 ++++++++++++++++++++ javascript_client/tsconfig.esm.json | 9 ++++ 6 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 javascript_client/esm/package.json create mode 100644 javascript_client/scripts/clean-esm.js create mode 100644 javascript_client/scripts/prepare-esm.js create mode 100644 javascript_client/tsconfig.esm.json diff --git a/.gitignore b/.gitignore index 203f2fb1356..093c2629632 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,9 @@ javascript_client/subscriptions javascript_client/sync javascript_client/cli* javascript_client/index* +javascript_client/esm/**/*.d.ts +javascript_client/esm/**/*.js +!javascript_client/esm/package.json # Don't commit compiled extension files: *.bundle *.so diff --git a/javascript_client/esm/package.json b/javascript_client/esm/package.json new file mode 100644 index 00000000000..3dbc1ca591c --- /dev/null +++ b/javascript_client/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/javascript_client/package.json b/javascript_client/package.json index c5da8bf73d4..c3b6246f180 100644 --- a/javascript_client/package.json +++ b/javascript_client/package.json @@ -3,7 +3,71 @@ "version": "1.14.9", "description": "JavaScript client for graphql-ruby", "main": "index.js", + "module": "esm/index.js", "types": "index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./esm/index.js", + "require": "./index.js", + "default": "./index.js" + }, + "./index.js": { + "types": "./index.d.ts", + "import": "./esm/index.js", + "require": "./index.js", + "default": "./index.js" + }, + "./cli.js": "./cli.js", + "./CHANGELOG.md": "./CHANGELOG.md", + "./DumpPayloadExample.json": "./DumpPayloadExample.json", + "./LICENSE.md": "./LICENSE.md", + "./readme.md": "./readme.md", + "./sync": { + "types": "./sync/index.d.ts", + "import": "./esm/sync/index.js", + "require": "./sync/index.js", + "default": "./sync/index.js" + }, + "./sync/*": { + "types": "./sync/*", + "import": "./esm/sync/*.js", + "require": "./sync/*.js", + "default": "./sync/*.js" + }, + "./sync/*.js": { + "types": "./sync/*.d.ts", + "import": "./esm/sync/*.js", + "require": "./sync/*.js", + "default": "./sync/*.js" + }, + "./subscriptions/*": { + "types": "./subscriptions/*", + "import": "./esm/subscriptions/*.js", + "require": "./subscriptions/*.js", + "default": "./subscriptions/*.js" + }, + "./subscriptions/*.js": { + "types": "./subscriptions/*.d.ts", + "import": "./esm/subscriptions/*.js", + "require": "./subscriptions/*.js", + "default": "./subscriptions/*.js" + }, + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "sync": [ + "sync/index.d.ts" + ], + "sync/*": [ + "sync/*" + ], + "subscriptions/*": [ + "subscriptions/*" + ] + } + }, "repository": "https://github.com/rmosolgo/graphql-ruby", "author": "Robert Mosolgo", "license": "LGPL-3.0", @@ -34,8 +98,10 @@ "urql": "^2.2.2" }, "scripts": { + "build": "tsc && node scripts/clean-esm.js && tsc -p tsconfig.esm.json && node scripts/prepare-esm.js", + "prepack": "npm run build", "test": "tsc && jest", - "prepublishOnly": "tsc" + "prepublishOnly": "npm test" }, "dependencies": { "glob": "^13.0.6", diff --git a/javascript_client/scripts/clean-esm.js b/javascript_client/scripts/clean-esm.js new file mode 100644 index 00000000000..09345e4e395 --- /dev/null +++ b/javascript_client/scripts/clean-esm.js @@ -0,0 +1,7 @@ +const fs = require("fs") +const path = require("path") + +const esmRoot = path.join(__dirname, "..", "esm") + +fs.rmSync(esmRoot, { recursive: true, force: true }) +fs.mkdirSync(esmRoot, { recursive: true }) diff --git a/javascript_client/scripts/prepare-esm.js b/javascript_client/scripts/prepare-esm.js new file mode 100644 index 00000000000..7ab80ba7a59 --- /dev/null +++ b/javascript_client/scripts/prepare-esm.js @@ -0,0 +1,57 @@ +const fs = require("fs") +const path = require("path") + +const esmRoot = path.join(__dirname, "..", "esm") + +fs.writeFileSync( + path.join(esmRoot, "package.json"), + JSON.stringify({ type: "module" }, null, 2) + "\n" +) + +for (const filePath of findJavaScriptFiles(esmRoot)) { + const source = fs.readFileSync(filePath, "utf8") + const nextSource = source + .replaceAll("@apollo/client/core\"", "@apollo/client/core/index.js\"") + .replaceAll("@apollo/client/core'", "@apollo/client/core/index.js'") + .replaceAll("graphql/language/printer\"", "graphql/language/printer.js\"") + .replaceAll("graphql/language/printer'", "graphql/language/printer.js'") + .replace(/(from\s+["'])(\.[^"']+)(["'])/g, function(match, prefix, specifier, suffix) { + return prefix + resolveRelativeSpecifier(filePath, specifier) + suffix + }) + + if (nextSource !== source) { + fs.writeFileSync(filePath, nextSource) + } +} + +function findJavaScriptFiles(directory) { + return fs.readdirSync(directory, { withFileTypes: true }).flatMap(function(entry) { + const entryPath = path.join(directory, entry.name) + + if (entry.isDirectory()) { + return findJavaScriptFiles(entryPath) + } + + return entry.isFile() && entry.name.endsWith(".js") ? [entryPath] : [] + }) +} + +function resolveRelativeSpecifier(fromPath, specifier) { + if (path.extname(specifier)) { + return specifier + } + + const fromDirectory = path.dirname(fromPath) + const filePath = path.resolve(fromDirectory, specifier + ".js") + const indexPath = path.resolve(fromDirectory, specifier, "index.js") + + if (fs.existsSync(filePath)) { + return specifier + ".js" + } + + if (fs.existsSync(indexPath)) { + return specifier + "/index.js" + } + + return specifier +} diff --git a/javascript_client/tsconfig.esm.json b/javascript_client/tsconfig.esm.json new file mode 100644 index 00000000000..6187a1e50bc --- /dev/null +++ b/javascript_client/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["**/*.js", "src/**/__tests__/**"], + "compilerOptions": { + "module": "ES2020", + "moduleResolution": "node", + "outDir": "./esm" + } +}