From 1bac82e18a4c59f6938bc382dcb2f22ec80f653c Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sat, 4 Jul 2026 20:46:26 +0200 Subject: [PATCH] fix: make dev-build sourcemaps debuggable in VS Code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contributors attaching a debugger to tsserver could only bind breakpoints in dist/graphqlsp.js, never in packages/graphqlsp/src, because: 1. The emitted sourcemaps use relative sources (e.g. ../src/index.ts), which resolve against wherever pnpm materialized the built package (a symlink target or a hard-linked copy under node_modules/.pnpm), never matching the real files under packages/graphqlsp/src that breakpoints are set in. 2. The attach configuration in .vscode/launch.json relied on js-debug's default resolveSourceMapLocations, which excludes **/node_modules/** — exactly where tsserver loads the plugin from — so the sourcemaps were ignored outright. Development builds (NODE_ENV=development, i.e. `pnpm dev`) now rewrite sourcemap sources to absolute paths via sourcemapPathTransform and skip terser so mappings stay pristine. Production output is unchanged (verified byte-identical). The launch config now enables sourceMaps, widens resolveSourceMapLocations, and points outFiles at the plugin's dist output; README documents the debugging flow. Refs #304 Co-Authored-By: Claude Fable 5 --- .vscode/launch.json | 13 ++++++- README.md | 25 ++++++++++++++ scripts/rollup.config.mjs | 73 ++++++++++++++++++++++++--------------- 3 files changed, 83 insertions(+), 28 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c91e11ed..e6c4d4ea 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,18 @@ "type": "node", "request": "attach", "name": "Attach to VS Code TS Server via Port", - "port": 9559 + "port": 9559, + "sourceMaps": true, + // The plugin is loaded by tsserver from the example project's + // node_modules. The default value of "resolveSourceMapLocations" + // excludes anything inside node_modules, which prevents breakpoints in + // packages/graphqlsp/src from ever binding, so it's widened here. + // See: https://github.com/0no-co/GraphQLSP/issues/304 + "resolveSourceMapLocations": ["${workspaceFolder}/**"], + "outFiles": [ + "${workspaceFolder}/packages/graphqlsp/dist/**/*.js", + "${workspaceFolder}/**/node_modules/@0no-co/graphqlsp/dist/**/*.js" + ] } ] } diff --git a/README.md b/README.md index 574a7e28..95008fec 100644 --- a/README.md +++ b/README.md @@ -135,3 +135,28 @@ breakpoints do it with the `TSS_DEBUG_BRK=9559` prefix. When you make changes in to do is run `pnpm i` in your other editor and restart the `TypeScript server` for the changes to apply. > Ensure that both instances of your editor are using the Workspace Version of TypeScript + +### Debugging with breakpoints + +To set breakpoints in the TypeScript sources of `packages/graphqlsp` you need a +development build of the plugin, which emits sourcemaps that point back at the +files in `packages/graphqlsp/src`: + +1. Build the plugin with `pnpm --filter @0no-co/graphqlsp dev` (this watches for + changes; a one-off `NODE_ENV=development pnpm --filter @0no-co/graphqlsp build` + works too) +2. Open the example project with `TSS_DEBUG_BRK=9559 code packages/example` and + run `pnpm i` in it, so it picks up the freshly built plugin +3. In the VS Code window that has the repository root open, set your breakpoints + in `packages/graphqlsp/src` and start the "Attach to VS Code TS Server via + Port" launch configuration from the "Run and Debug" panel +4. Trigger the plugin from the example window (e.g. by typing inside a `graphql()` + document) and your breakpoints will be hit + +After making changes in `packages/graphqlsp`, restart the example window's +TypeScript server ("TypeScript: Restart TS Server" in the command palette) and +reattach the debugger for the changes to apply. + +> Production builds (`pnpm build` without `NODE_ENV=development`) also emit +> sourcemaps, but their `sources` are relative paths that don't resolve from the +> location pnpm installs the package to, so breakpoints won't bind with them. diff --git a/scripts/rollup.config.mjs b/scripts/rollup.config.mjs index 2ab5da84..ba9f268d 100644 --- a/scripts/rollup.config.mjs +++ b/scripts/rollup.config.mjs @@ -1,5 +1,6 @@ import fs from 'node:fs/promises'; import path from 'node:path/posix'; +import nodePath from 'node:path'; import { readFileSync } from 'node:fs'; import * as prettier from 'prettier'; @@ -28,6 +29,8 @@ const extension = name => { } }; +const isDevelopment = process.env.NODE_ENV === 'development'; + const meta = JSON.parse(readFileSync('package.json')); const name = normalize(meta.name); @@ -87,6 +90,19 @@ const commonOutput = { exports: 'auto', sourcemap: true, sourcemapExcludeSources: false, + // In development builds, sourcemap sources are rewritten to absolute paths. + // pnpm links or copies the built package into the example project's + // node_modules, from where relative sources never resolve back to the + // actual files in packages/graphqlsp/src, leaving debugger breakpoints + // unbound. See: https://github.com/0no-co/GraphQLSP/issues/304 + ...(isDevelopment && { + sourcemapPathTransform(relativeSourcePath, sourcemapPath) { + return nodePath.resolve( + nodePath.dirname(sourcemapPath), + relativeSourcePath + ); + }, + }), hoistTransitiveImports: false, indent: false, freeze: false, @@ -134,35 +150,38 @@ const outputPlugins = [ cjsCheck(), - terser({ - warnings: true, - ecma: 2015, - keep_fnames: true, - ie8: false, - compress: { - pure_getters: true, - toplevel: true, - booleans_as_integers: false, + // Minification is skipped in development builds to keep the emitted + // sourcemap mappings as close to the original sources as possible. + !isDevelopment && + terser({ + warnings: true, + ecma: 2015, keep_fnames: true, - keep_fargs: true, - if_return: false, ie8: false, - sequences: false, - loops: false, - conditionals: false, - join_vars: false, - }, - mangle: { - module: true, - keep_fnames: true, - }, - output: { - beautify: true, - braces: true, - indent_level: 2, - }, - }), -]; + compress: { + pure_getters: true, + toplevel: true, + booleans_as_integers: false, + keep_fnames: true, + keep_fargs: true, + if_return: false, + ie8: false, + sequences: false, + loops: false, + conditionals: false, + join_vars: false, + }, + mangle: { + module: true, + keep_fnames: true, + }, + output: { + beautify: true, + braces: true, + indent_level: 2, + }, + }), +].filter(Boolean); export default [ {