Skip to content

Commit ef98bf8

Browse files
committed
fix: keep sourcemaps intact when injecting fragments
Signed-off-by: Frederik Bußmann <frederik@bussmann.io>
1 parent 042a067 commit ef98bf8

4 files changed

Lines changed: 64 additions & 18 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
"graphql": "^16.14.2",
105105
"h3": "^1.15.11",
106106
"lru-cache": "^11.5.2",
107+
"magic-string": "^0.30.21",
107108
"minimatch": "^10.2.5",
108109
"nitropack": "^2.13.4",
109110
"ohash": "^2.0.11",

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/utils/graphql/transform.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { FragmentRegistry } from './registry'
22

3+
import MagicString from 'magic-string'
4+
35
import { collectSpreads, scanDefinitions } from '../../runtime/utils/graphql/scanner'
46
import { findGraphqlLiterals } from './literals'
57
import { resolveFragments } from './registry'
@@ -16,23 +18,13 @@ export interface GraphqlTransformOptions {
1618
resolveRegistry: (file: string) => FragmentRegistry | undefined
1719
}
1820

19-
function applyInsertions(source: string, insertions: Insertion[]) {
20-
let result = source
21-
22-
for (const insertion of [...insertions].sort((left, right) => right.at - left.at)) {
23-
result = result.slice(0, insertion.at) + insertion.text + result.slice(insertion.at)
24-
}
25-
26-
return result
27-
}
28-
29-
export function transformGraphqlLiterals(code: string, file: string, options: GraphqlTransformOptions) {
21+
function collectFragmentInsertions(code: string, file: string, options: GraphqlTransformOptions) {
3022
const registry = options.resolveRegistry(file)
3123

32-
if (!registry) return
33-
3424
const insertions: Insertion[] = []
3525

26+
if (!registry) return insertions
27+
3628
for (const literal of findGraphqlLiterals(code)) {
3729
const definitions = scanDefinitions(literal.content)
3830
const definesOperation = definitions.some(definition => definition.kind === 'operation')
@@ -62,25 +54,42 @@ export function transformGraphqlLiterals(code: string, file: string, options: Gr
6254
})
6355
}
6456

57+
return insertions
58+
}
59+
60+
function rewrite(code: string, file: string, options: GraphqlTransformOptions) {
61+
const insertions = collectFragmentInsertions(code, file, options)
62+
6563
if (!insertions.length) return
6664

67-
return applyInsertions(code, insertions)
65+
const source = new MagicString(code)
66+
67+
for (const insertion of insertions) source.appendLeft(insertion.at, insertion.text)
68+
69+
return source
70+
}
71+
72+
export function transformGraphqlLiterals(code: string, file: string, options: GraphqlTransformOptions) {
73+
return rewrite(code, file, options)?.toString()
6874
}
6975

7076
export function createGraphqlTransformPlugin(options: GraphqlTransformOptions) {
7177
return {
7278
name: 'nuxt-shopify:graphql',
73-
enforce: 'pre' as const,
79+
enforce: 'post' as const,
7480

7581
transform(code: string, id: string) {
7682
if (!TRANSFORMABLE.test(id) || !code.includes('#graphql')) return
7783

7884
const file = id.split('?')[0]!
79-
const transformed = transformGraphqlLiterals(code, file, options)
85+
const transformed = rewrite(code, file, options)
8086

8187
if (!transformed) return
8288

83-
return { code: transformed, map: null }
89+
return {
90+
code: transformed.toString(),
91+
map: transformed.generateMap({ source: file, includeContent: true, hires: true }),
92+
}
8493
},
8594
}
8695
}

test/unit/graphql-transform.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { findGraphqlLiterals } from '../../src/utils/graphql/literals'
66
import { normalizeOperation } from '../../src/runtime/utils/graphql/normalize'
77
import { scanDefinitions } from '../../src/runtime/utils/graphql/scanner'
88
import { touchesFragments } from '../../src/setup/graphql-transform'
9-
import { transformGraphqlLiterals } from '../../src/utils/graphql/transform'
9+
import { createGraphqlTransformPlugin, transformGraphqlLiterals } from '../../src/utils/graphql/transform'
1010

1111
const IMAGE = `fragment ImageFields on Image {
1212
url
@@ -171,6 +171,39 @@ describe('fragment injection', () => {
171171
})
172172
})
173173

174+
describe('transform plugin', () => {
175+
const plugin = createGraphqlTransformPlugin({ resolveRegistry: () => registry() })
176+
177+
const code = 'const q = `#graphql\n query FetchProduct {\n product { ...ProductFields }\n }\n`'
178+
179+
const run = (id: string) => plugin.transform(code, id)
180+
181+
it('transforms a plain script module', () => {
182+
expect(run('/project/app/composables/product.ts')?.code).toContain('fragment ProductFields')
183+
})
184+
185+
it('transforms a single file component', () => {
186+
expect(run('/project/app/pages/product.vue')?.code).toContain('fragment ProductFields')
187+
})
188+
189+
it('transforms the script block of a single file component', () => {
190+
const id = '/project/app/pages/product.vue?vue&type=script&setup=true&lang.ts'
191+
192+
expect(run(id)?.code).toContain('fragment ProductFields')
193+
})
194+
195+
it('runs after the vue plugin so it never rewrites the source content of a component', () => {
196+
expect(plugin.enforce).toBe('post')
197+
})
198+
199+
it('emits a source map pointing back at the untransformed file', () => {
200+
const result = run('/project/app/composables/product.ts')!
201+
202+
expect(result.map?.sources).toStrictEqual(['/project/app/composables/product.ts'])
203+
expect(result.map?.sourcesContent).toStrictEqual([code])
204+
})
205+
})
206+
174207
describe('fragment watching', () => {
175208
const dirs = ['/project/graphql']
176209
const srcDir = '/project/app'

0 commit comments

Comments
 (0)