@graphox/babel-plugin-plugin 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # @graphox/babel-plugin
2
+
3
+ ## Overview
4
+
5
+ Ensures GraphQL AST files are properly codesplit, preventing them from all ending up in the initial chunk.
6
+
7
+ ## Why Use This Plugin?
8
+
9
+ Codegen already generates AST files at compile time. The problem is bundler behavior:
10
+
11
+ **Without plugin:**
12
+ ```
13
+ Initial chunk: ALL generated AST files (~50KB+)
14
+ Lazy chunks: empty or minimal
15
+ ```
16
+
17
+ **With plugin:**
18
+ ```
19
+ Initial chunk: ~1KB (just imports)
20
+ Lazy chunks: each operation in its chunk
21
+ ```
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pnpm add --save-dev @graphox/babel-plugin
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ### 1. Configure graphox
32
+
33
+ ```yaml
34
+ # graphox.yaml
35
+ output_dir: "__generated__"
36
+ projects:
37
+ - schema: "schema.graphql"
38
+ include: "src/**/*.{ts,tsx}"
39
+ ```
40
+
41
+ ### 2. Run Codegen
42
+
43
+ ```bash
44
+ pnpm graphox codegen
45
+ ```
46
+
47
+ ### 3. Configure Babel
48
+
49
+ ```javascript
50
+ // babel.config.js
51
+ const path = require('path');
52
+
53
+ module.exports = {
54
+ presets: ['@babel/preset-typescript'],
55
+ plugins: [
56
+ ['@graphox/babel-plugin', {
57
+ manifestPath: path.resolve(__dirname, '__generated__/manifest.json'),
58
+ outputDir: path.resolve(__dirname, '__generated__'),
59
+ graphqlImportPaths: ['@/graphql']
60
+ }]
61
+ ]
62
+ };
63
+ ```
64
+
65
+ ## Metro (React Native)
66
+
67
+ Metro uses Babel transformers under the hood. Configure in `metro.config.js`:
68
+
69
+ ```javascript
70
+ // metro.config.js
71
+ module.exports = {
72
+ transformer: {
73
+ babelTransformerPath: require.resolve('@graphox/babel-plugin'),
74
+ },
75
+ };
76
+ ```
77
+
78
+ For full compatibility, also configure in `babel.config.js`.
79
+
80
+ ## Codesplitting Impact
81
+
82
+ | Configuration | Initial Chunk | Per-Lazy-Chunk |
83
+ |--------------|--------------|----------------|
84
+ | Without plugin | ~50KB+ (all AST) | ~1KB |
85
+ | With plugin | ~1KB | ~1KB |
86
+
87
+ ## Configuration Options
88
+
89
+ | Option | Type | Required | Description |
90
+ |--------|------|----------|-------------|
91
+ | `manifestPath` | `string` | Yes* | Path to `manifest.json` generated by codegen |
92
+ | `manifestData` | `object[]` | Yes* | Inline manifest data (alternative to `manifestPath`) |
93
+ | `outputDir` | `string` | Yes | Directory containing generated files |
94
+ | `graphqlImportPaths` | `string[]` | No | Explicit import paths to treat as GraphQL entrypoints |
95
+ | `emitExtensions` | `string` | No | File extension for generated imports: `"none"` (default), `"ts"`, `"js"`, `"dts"` |
96
+
97
+ *Either `manifestPath` or `manifestData` is required.
98
+
99
+ ### emitExtensions
100
+
101
+ Controls the file extension appended to generated import paths. Should match the `emit_extensions` setting in your `graphox.yaml`:
102
+
103
+ | Value | Result |
104
+ |-------|--------|
105
+ | `"none"` (default) | `import { X } from "./file.codegen"` |
106
+ | `"ts"` | `import { X } from "./file.codegen.ts"` |
107
+ | `"js"` | `import { X } from "./file.codegen.js"` |
108
+ | `"dts"` | `import { X } from "./file.codegen.d.ts"` |
109
+
110
+ ## Fragment Documents
111
+
112
+ When `generate_ast_for_fragments: true` is enabled in your config, fragment documents are also included in the manifest and will be properly rewritten by the plugin.
113
+
114
+ ## When to Use
115
+
116
+ | Build Tool | Recommended Plugin |
117
+ |------------|-------------------|
118
+ | React Native (Metro) | Babel |
119
+ | Webpack | Babel |
120
+ | Create React App | Babel |
121
+ | Storybook | Babel |
122
+ | rsbuild | Use SWC plugin |
123
+ | Turbopack/Next.js | Use SWC plugin |
124
+
125
+ ## See Also
126
+
127
+ - [SWC Plugin](../swc/README.md)
128
+ - [graphox CLI](../../README.md)
129
+ - [Configuration Guide](../../docs/configurations.md)
package/index.js ADDED
@@ -0,0 +1,227 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ function normalize(s) {
5
+ return s.replace(/\s+/g, '');
6
+ }
7
+
8
+ /**
9
+ * Convert emitExtensions config to file extension string
10
+ * @param {string|undefined} emitExtensions - One of: "none", "ts", "dts", "js"
11
+ * @returns {string} The file extension to append (e.g., ".ts", ".js", or "")
12
+ */
13
+ function getExtension(emitExtensions) {
14
+ switch (emitExtensions) {
15
+ case 'ts':
16
+ return '.ts';
17
+ case 'dts':
18
+ return '.d.ts';
19
+ case 'js':
20
+ return '.js';
21
+ case 'none':
22
+ case undefined:
23
+ case null:
24
+ default:
25
+ return '';
26
+ }
27
+ }
28
+
29
+ module.exports = function (babel) {
30
+ const { types: t } = babel;
31
+
32
+ return {
33
+ name: '@graphox/babel-plugin',
34
+ visitor: {
35
+ Program: {
36
+ enter(programPath, state) {
37
+ const {
38
+ manifestPath,
39
+ manifestData,
40
+ outputDir,
41
+ graphqlImportPaths = [],
42
+ emitExtensions,
43
+ } = state.opts;
44
+
45
+ if (!outputDir) {
46
+ throw new Error('outputDir is required for @graphox/babel-plugin');
47
+ }
48
+
49
+ const extension = getExtension(emitExtensions);
50
+
51
+ let entries = [];
52
+ if (manifestData) {
53
+ entries = manifestData;
54
+ } else if (manifestPath) {
55
+ try {
56
+ const content = fs.readFileSync(manifestPath, 'utf8');
57
+ entries = JSON.parse(content);
58
+ } catch (e) {
59
+ // Ignore missing manifest during build if necessary
60
+ }
61
+ }
62
+
63
+ const manifest = new Map();
64
+ const documentNameToEntry = new Map();
65
+ for (const entry of entries) {
66
+ manifest.set(normalize(entry.source), entry);
67
+ if (entry.name) {
68
+ documentNameToEntry.set(entry.name, entry);
69
+ }
70
+ }
71
+
72
+ const currentFile = state.file.opts.filename;
73
+ const absoluteOutputDir = path.resolve(outputDir);
74
+ const absoluteIndexPath = path.join(absoluteOutputDir, 'index');
75
+ const absoluteEntrypointPath = path.join(absoluteOutputDir, 'graphql');
76
+
77
+ // If processing the entrypoint itself, clear it
78
+ if (currentFile) {
79
+ const currentFileNoExt = currentFile.replace(/\.(js|ts)x?$/, '');
80
+ if (currentFileNoExt === absoluteEntrypointPath) {
81
+ programPath.node.body = [
82
+ t.exportNamedDeclaration(
83
+ t.variableDeclaration('const', [
84
+ t.variableDeclarator(
85
+ t.identifier('graphql'),
86
+ t.arrowFunctionExpression([], t.nullLiteral())
87
+ ),
88
+ ])
89
+ ),
90
+ t.exportNamedDeclaration(
91
+ t.variableDeclaration('const', [
92
+ t.variableDeclarator(t.identifier('gql'), t.identifier('graphql')),
93
+ ])
94
+ ),
95
+ ];
96
+ return;
97
+ }
98
+ }
99
+
100
+ const isOurGraphqlPath = (src) => {
101
+ if (graphqlImportPaths.includes(src)) return true;
102
+ const srcNoExt = src.replace(/\.(js|ts)x?$/, '');
103
+ if (graphqlImportPaths.includes(srcNoExt)) return true;
104
+
105
+ if (src.startsWith('#') && src.includes('graphql')) return true;
106
+
107
+ if (currentFile && (src.startsWith('.') || src.startsWith('/'))) {
108
+ const absoluteSrc = path.resolve(path.dirname(currentFile), srcNoExt);
109
+ if (absoluteSrc === absoluteEntrypointPath || absoluteSrc === absoluteIndexPath) {
110
+
111
+ return true;
112
+ }
113
+ // Handle directory import resolving to index
114
+ const absoluteSrcIndex = path.join(absoluteSrc, 'index');
115
+ if (absoluteSrcIndex === absoluteIndexPath) {
116
+ return true;
117
+ }
118
+ }
119
+
120
+ return false;
121
+ };
122
+
123
+ const graphqlIds = new Set();
124
+ // newImports stores: localName -> { sourcePath, importedName }
125
+ // importedName is needed for aliased imports like { GetUserDocument as MyDoc }
126
+ const newImports = new Map();
127
+
128
+ // First pass: identify imports from our graphql.ts or index.ts
129
+ programPath.traverse({
130
+ ImportDeclaration(importPath) {
131
+ const src = importPath.node.source.value;
132
+ if (isOurGraphqlPath(src)) {
133
+ const importIsTypeOnly = importPath.node.importKind === 'type';
134
+ importPath.get('specifiers').forEach((specifier) => {
135
+ if (specifier.isImportSpecifier()) {
136
+ // Handle both Identifier and StringLiteral for imported name
137
+ const importedName = t.isIdentifier(specifier.node.imported)
138
+ ? specifier.node.imported.name
139
+ : specifier.node.imported.value;
140
+ const localName = specifier.node.local.name;
141
+ const specifierIsTypeOnly = specifier.node.importKind === 'type' || importIsTypeOnly;
142
+
143
+ if (importedName === 'graphql' || importedName === 'gql') {
144
+ graphqlIds.add(specifier.scope.getBinding(localName));
145
+ } else if (documentNameToEntry.has(importedName) && !specifierIsTypeOnly) {
146
+ // Only rewrite non-type-only imports of document names
147
+ const entry = documentNameToEntry.get(importedName);
148
+ const codegenAbsPath = path.join(absoluteOutputDir, entry.path);
149
+ let relPath = path.relative(path.dirname(currentFile), codegenAbsPath);
150
+ if (!relPath.startsWith('.') && !relPath.startsWith('/')) {
151
+ relPath = './' + relPath;
152
+ }
153
+ // Append the emit extension
154
+ relPath += extension;
155
+ newImports.set(localName, { sourcePath: relPath, importedName });
156
+ }
157
+ }
158
+ });
159
+ }
160
+ },
161
+ });
162
+
163
+ // Second pass: transform graphql() calls
164
+ programPath.traverse({
165
+ CallExpression(callPath) {
166
+ const callee = callPath.get('callee');
167
+ if (callee.isIdentifier()) {
168
+ const binding = callPath.scope.getBinding(callee.node.name);
169
+ if (graphqlIds.has(binding)) {
170
+ const arg = callPath.node.arguments[0];
171
+ let source = null;
172
+
173
+ if (t.isTemplateLiteral(arg)) {
174
+ if (arg.quasis.length === 1) {
175
+ source = arg.quasis[0].value.cooked || arg.quasis[0].value.raw;
176
+ }
177
+ } else if (t.isStringLiteral(arg)) {
178
+ source = arg.value;
179
+ }
180
+
181
+ if (source) {
182
+ const normalizedSource = normalize(source);
183
+ const entry = manifest.get(normalizedSource);
184
+ if (entry) {
185
+ const codegenAbsPath = path.join(absoluteOutputDir, entry.path);
186
+ let relPath = path.relative(path.dirname(currentFile), codegenAbsPath);
187
+ if (!relPath.startsWith('.') && !relPath.startsWith('/')) {
188
+ relPath = './' + relPath;
189
+ }
190
+ // Append the emit extension
191
+ relPath += extension;
192
+
193
+ newImports.set(entry.name, { sourcePath: relPath, importedName: entry.name });
194
+ callPath.replaceWith(t.identifier(entry.name));
195
+ }
196
+ }
197
+ }
198
+ }
199
+ },
200
+ });
201
+
202
+ // Third pass: remove ALL imports from graphql.ts/index.ts
203
+ programPath.traverse({
204
+ ImportDeclaration(importPath) {
205
+ const src = importPath.node.source.value;
206
+ if (isOurGraphqlPath(src)) {
207
+ importPath.remove();
208
+ }
209
+ },
210
+ });
211
+
212
+ // Add new imports at the top (sorted alphabetically by local name)
213
+ const sortedNewImports = Array.from(newImports.entries()).sort((a, b) => a[0].localeCompare(b[0]));
214
+ for (const [localName, { sourcePath, importedName }] of sortedNewImports) {
215
+ const specifier = t.importSpecifier(
216
+ t.identifier(localName),
217
+ t.identifier(importedName)
218
+ );
219
+ programPath.node.body.unshift(
220
+ t.importDeclaration([specifier], t.stringLiteral(sourcePath))
221
+ );
222
+ }
223
+ },
224
+ },
225
+ },
226
+ };
227
+ };
package/index.test.js ADDED
@@ -0,0 +1,337 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as babel from '@babel/core';
3
+ import path from 'path';
4
+ import plugin from './index.js';
5
+
6
+ function transform(code, options, filename = 'test.ts') {
7
+ const result = babel.transformSync(code, {
8
+ plugins: [[plugin, options]],
9
+ presets: ['@babel/preset-typescript'],
10
+ filename: path.resolve(filename),
11
+ babelrc: false,
12
+ configFile: false,
13
+ });
14
+ return result.code;
15
+ }
16
+
17
+ describe('@graphox/babel-plugin', () => {
18
+ const defaultManifest = [
19
+ {
20
+ source: 'query { me { id } }',
21
+ path: './query.codegen',
22
+ name: 'MyQueryDocument',
23
+ },
24
+ ];
25
+
26
+ const defaultOptions = {
27
+ manifestData: defaultManifest,
28
+ outputDir: './gen',
29
+ };
30
+
31
+ it('transforms basic graphql call and removes import', () => {
32
+ const code = "import { graphql } from './gen/graphql'; const q = graphql(`query { me { id } }`);";
33
+ const output = transform(code, defaultOptions);
34
+
35
+ expect(output).toContain('import { MyQueryDocument } from "./gen/query.codegen";');
36
+ expect(output).toContain('const q = MyQueryDocument;');
37
+ expect(output).not.toContain('from "./gen/graphql"');
38
+ });
39
+
40
+ it('transforms multiple calls', () => {
41
+ const manifest = [
42
+ {
43
+ source: 'query GetMe { me { id } }',
44
+ path: './me.codegen',
45
+ name: 'GetMeDocument',
46
+ },
47
+ {
48
+ source: 'query GetOther { other { id } }',
49
+ path: './other.codegen',
50
+ name: 'GetOtherDocument',
51
+ },
52
+ ];
53
+ const options = { manifestData: manifest, outputDir: './gen' };
54
+ const code = `
55
+ import { graphql } from './gen/graphql';
56
+ const q1 = graphql(\`query GetMe { me { id } }\`);
57
+ const q2 = graphql(\`query GetOther { other { id } }\`);
58
+ `;
59
+ const output = transform(code, options);
60
+
61
+ expect(output).toContain('import { GetMeDocument } from "./gen/me.codegen";');
62
+ expect(output).toContain('import { GetOtherDocument } from "./gen/other.codegen";');
63
+ expect(output).toContain('const q1 = GetMeDocument;');
64
+ expect(output).toContain('const q2 = GetOtherDocument;');
65
+ });
66
+
67
+ it('removes all imports from graphql.ts including unknown specifiers', () => {
68
+ const code = "import { graphql, other } from './gen/graphql'; const q = graphql(`query { me { id } }`);";
69
+ const output = transform(code, defaultOptions);
70
+
71
+ expect(output).toContain('import { MyQueryDocument } from "./gen/query.codegen";');
72
+ expect(output).not.toContain('from "./gen/graphql"');
73
+ expect(output).not.toContain('graphql,');
74
+ expect(output).not.toContain(', graphql');
75
+ expect(output).not.toContain('other');
76
+ });
77
+
78
+ it('resolves relative paths correctly', () => {
79
+ const manifest = [
80
+ {
81
+ source: 'query { me { id } }',
82
+ path: './src/query.codegen',
83
+ name: 'MyQueryDocument',
84
+ },
85
+ ];
86
+ const outputDir = path.resolve('/root/gen');
87
+ const options = { manifestData: manifest, outputDir };
88
+ const filename = '/root/app/test.ts';
89
+
90
+ const code = "import { graphql } from '../gen/graphql'; const q = graphql(`query { me { id } }`);";
91
+ const output = transform(code, options, filename);
92
+
93
+ expect(output).toContain('import { MyQueryDocument } from "../gen/src/query.codegen";');
94
+ });
95
+
96
+ it('supports gql tag', () => {
97
+ const code = "import { gql } from './gen/graphql'; const q = gql(`query { me { id } }`);";
98
+ const output = transform(code, defaultOptions);
99
+
100
+ expect(output).toContain('import { MyQueryDocument } from "./gen/query.codegen";');
101
+ expect(output).toContain('const q = MyQueryDocument;');
102
+ expect(output).not.toContain('from "./gen/graphql"');
103
+ });
104
+
105
+ it('is whitespace insensitive (normalization)', () => {
106
+ const code = `
107
+ import { graphql } from './gen/graphql';
108
+ const q = graphql(\`query {
109
+ me {
110
+ id
111
+ }
112
+ }\`);
113
+ `;
114
+ const output = transform(code, defaultOptions);
115
+ expect(output).toContain('const q = MyQueryDocument;');
116
+ });
117
+
118
+ it('does not transform identifiers from other libraries', () => {
119
+ const code = "import { graphql } from 'other-lib'; const q = graphql(`query { me { id } }`);";
120
+ const output = transform(code, defaultOptions);
121
+
122
+ expect(output).toContain("import { graphql } from 'other-lib';");
123
+ expect(output).toContain('graphql(');
124
+ expect(output).not.toContain('MyQueryDocument');
125
+ });
126
+
127
+ it('supports subpath imports (#graphql)', () => {
128
+ const code = "import { graphql } from '#graphql/graphql'; const q = graphql(`query { me { id } }`);";
129
+ const output = transform(code, defaultOptions);
130
+
131
+ expect(output).toContain('import { MyQueryDocument } from "./gen/query.codegen";');
132
+ expect(output).toContain('const q = MyQueryDocument;');
133
+ });
134
+
135
+ it('supports explicit import paths', () => {
136
+ const options = {
137
+ ...defaultOptions,
138
+ graphqlImportPaths: ['@app/gql-entrypoint'],
139
+ };
140
+ const code = "import { graphql } from '@app/gql-entrypoint'; const q = graphql(`query { me { id } }`);";
141
+ const output = transform(code, options);
142
+
143
+ expect(output).toContain('import { MyQueryDocument } from "./gen/query.codegen";');
144
+ expect(output).toContain('const q = MyQueryDocument;');
145
+ });
146
+
147
+ it('clears the entrypoint file', () => {
148
+ const outputDir = path.resolve('./gen');
149
+ const filename = path.join(outputDir, 'graphql.ts');
150
+ const code = "export const graphql = () => { /* big map */ }; export const gql = graphql;";
151
+ const output = transform(code, { outputDir }, filename);
152
+
153
+ expect(output).toContain('export const graphql = () => null;');
154
+ expect(output).toContain('export const gql = graphql;');
155
+ expect(output).not.toContain('big map');
156
+ });
157
+
158
+ describe('emit extensions', () => {
159
+ it('appends .ts extension when emitExtensions is "ts"', () => {
160
+ const code = "import { graphql } from './gen/graphql'; const q = graphql(`query { me { id } }`);";
161
+ const output = transform(code, { ...defaultOptions, emitExtensions: 'ts' });
162
+
163
+ expect(output).toContain('import { MyQueryDocument } from "./gen/query.codegen.ts";');
164
+ });
165
+
166
+ it('appends .js extension when emitExtensions is "js"', () => {
167
+ const code = "import { graphql } from './gen/graphql'; const q = graphql(`query { me { id } }`);";
168
+ const output = transform(code, { ...defaultOptions, emitExtensions: 'js' });
169
+
170
+ expect(output).toContain('import { MyQueryDocument } from "./gen/query.codegen.js";');
171
+ });
172
+
173
+ it('appends .d.ts extension when emitExtensions is "dts"', () => {
174
+ const code = "import { graphql } from './gen/graphql'; const q = graphql(`query { me { id } }`);";
175
+ const output = transform(code, { ...defaultOptions, emitExtensions: 'dts' });
176
+
177
+ expect(output).toContain('import { MyQueryDocument } from "./gen/query.codegen.d.ts";');
178
+ });
179
+
180
+ it('does not append extension when emitExtensions is "none" or omitted', () => {
181
+ const code = "import { graphql } from './gen/graphql'; const q = graphql(`query { me { id } }`);";
182
+ const output1 = transform(code, { ...defaultOptions, emitExtensions: 'none' });
183
+ const output2 = transform(code, defaultOptions);
184
+
185
+ expect(output1).toContain('import { MyQueryDocument } from "./gen/query.codegen";');
186
+ expect(output2).toContain('import { MyQueryDocument } from "./gen/query.codegen";');
187
+ });
188
+
189
+ it('appends extension to relative paths correctly', () => {
190
+ const manifest = [
191
+ {
192
+ source: 'query { me { id } }',
193
+ path: './src/query.codegen',
194
+ name: 'MyQueryDocument',
195
+ },
196
+ ];
197
+ const outputDir = path.resolve('/root/gen');
198
+ const options = { manifestData: manifest, outputDir, emitExtensions: 'js' };
199
+ const filename = '/root/app/test.ts';
200
+
201
+ const code = "import { graphql } from '../gen/graphql'; const q = graphql(`query { me { id } }`);";
202
+ const output = transform(code, options, filename);
203
+
204
+ expect(output).toContain('import { MyQueryDocument } from "../gen/src/query.codegen.js";');
205
+ });
206
+ });
207
+
208
+ describe('re-exported document imports', () => {
209
+ const reExportManifest = [
210
+ {
211
+ source: 'query GetUser { user { id } }',
212
+ path: './user.codegen',
213
+ name: 'GetUserDocument',
214
+ },
215
+ {
216
+ source: 'query GetPost { post { id } }',
217
+ path: './post.codegen',
218
+ name: 'GetPostDocument',
219
+ },
220
+ ];
221
+
222
+ const reExportOptions = {
223
+ manifestData: reExportManifest,
224
+ outputDir: './gen',
225
+ };
226
+
227
+ it('rewrites single document name import from graphql entrypoint', () => {
228
+ const code = "import { GetUserDocument } from './gen/graphql'; console.log(GetUserDocument);";
229
+ const output = transform(code, reExportOptions);
230
+
231
+ expect(output).toContain("import { GetUserDocument } from \"./gen/user.codegen\";");
232
+ expect(output).not.toContain("from './gen/graphql'");
233
+ expect(output).toContain('console.log(GetUserDocument);');
234
+ });
235
+
236
+ it('rewrites multiple document name imports to correct codegen files', () => {
237
+ const code = "import { GetUserDocument, GetPostDocument } from './gen/graphql';";
238
+ const output = transform(code, reExportOptions);
239
+
240
+ expect(output).toContain("import { GetUserDocument } from \"./gen/user.codegen\";");
241
+ expect(output).toContain("import { GetPostDocument } from \"./gen/post.codegen\";");
242
+ expect(output).not.toContain("from './gen/graphql'");
243
+ });
244
+
245
+ it('removes graphql import alongside document imports', () => {
246
+ const code = "import { GetUserDocument, graphql } from './gen/graphql';";
247
+ const output = transform(code, reExportOptions);
248
+
249
+ expect(output).toContain("import { GetUserDocument } from \"./gen/user.codegen\";");
250
+ expect(output).not.toContain("from './gen/graphql'");
251
+ expect(output).not.toContain('graphql');
252
+ });
253
+
254
+ it('removes non-document imports while rewriting document imports', () => {
255
+ const code = "import { GetUserDocument, SomeOtherType } from './gen/graphql';";
256
+ const output = transform(code, reExportOptions);
257
+
258
+ expect(output).toContain("import { GetUserDocument } from \"./gen/user.codegen\";");
259
+ expect(output).not.toContain("from './gen/graphql'");
260
+ expect(output).not.toContain('SomeOtherType');
261
+ });
262
+
263
+ it('removes type-only imports (they dont exist in minified JS)', () => {
264
+ const code = "import type { GetUserDocument } from './gen/graphql';";
265
+ const output = transform(code, reExportOptions);
266
+
267
+ // Type-only imports are removed entirely since they don't exist in minified JS
268
+ expect(output).not.toContain("from './gen/graphql'");
269
+ expect(output).not.toContain('GetUserDocument');
270
+ });
271
+
272
+ it('removes inline type specifiers from mixed imports', () => {
273
+ const code = "import { GetUserDocument, type GetPostDocument } from './gen/graphql';";
274
+ const output = transform(code, reExportOptions);
275
+
276
+ // Only GetUserDocument (non-type) is kept and rewritten
277
+ expect(output).toContain("import { GetUserDocument } from \"./gen/user.codegen\";");
278
+ // GetPostDocument with inline type is removed
279
+ expect(output).not.toContain("GetPostDocument");
280
+ expect(output).not.toContain("from './gen/graphql'");
281
+ });
282
+
283
+ it('rewrites imports from index.ts barrel file', () => {
284
+ const code = "import { GetUserDocument } from './gen/index';";
285
+ const output = transform(code, reExportOptions);
286
+
287
+ expect(output).toContain("import { GetUserDocument } from \"./gen/user.codegen\";");
288
+ expect(output).not.toContain("from './gen/index'");
289
+ });
290
+
291
+ it('rewrites imports from index.ts with extension', () => {
292
+ const code = "import { GetUserDocument } from './gen/index.ts';";
293
+ const output = transform(code, reExportOptions);
294
+
295
+ expect(output).toContain("import { GetUserDocument } from \"./gen/user.codegen\";");
296
+ expect(output).not.toContain("from './gen/index");
297
+ });
298
+
299
+ it('handles aliased document imports', () => {
300
+ const code = "import { GetUserDocument as MyUserDoc } from './gen/graphql'; console.log(MyUserDoc);";
301
+ const output = transform(code, reExportOptions);
302
+
303
+ expect(output).toContain("import { GetUserDocument as MyUserDoc } from \"./gen/user.codegen\";");
304
+ expect(output).toContain('console.log(MyUserDoc);');
305
+ });
306
+
307
+ it('rewrites fragment document imports when generate_ast_for_fragments is enabled', () => {
308
+ const manifest = [
309
+ {
310
+ source: 'query GetUser { user { id } }',
311
+ path: './user.codegen',
312
+ name: 'GetUserDocument',
313
+ },
314
+ {
315
+ source: 'fragment UserFields on User { id name }',
316
+ path: './userFields.codegen',
317
+ name: 'UserFieldsFragmentDocument',
318
+ },
319
+ ];
320
+ const options = { manifestData: manifest, outputDir: './gen' };
321
+ const code = "import { GetUserDocument, UserFieldsFragmentDocument } from './gen/graphql';";
322
+ const output = transform(code, options);
323
+
324
+ expect(output).toContain("import { GetUserDocument } from \"./gen/user.codegen\";");
325
+ expect(output).toContain("import { UserFieldsFragmentDocument } from \"./gen/userFields.codegen\";");
326
+ expect(output).not.toContain("from './gen/graphql'");
327
+ });
328
+
329
+ it('rewrites imports from directory resolving to index', () => {
330
+ const code = "import { GetUserDocument } from './gen';";
331
+ const output = transform(code, reExportOptions);
332
+
333
+ expect(output).toContain("import { GetUserDocument } from \"./gen/user.codegen\";");
334
+ expect(output).not.toContain("from './gen'");
335
+ });
336
+ });
337
+ });
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@graphox/babel-plugin-plugin",
3
+ "version": "0.1.13",
4
+ "description": "Babel plugin for Graphox codesplitting",
5
+ "main": "index.js",
6
+ "license": "UNLICENSED",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/soundtrackyourbrand/graphox",
13
+ "directory": "plugins/babel"
14
+ },
15
+ "devDependencies": {
16
+ "@babel/core": "^7.23.0",
17
+ "@babel/preset-typescript": "^7.28.5",
18
+ "vitest": "^1.0.0"
19
+ },
20
+ "scripts": {
21
+ "test": "vitest run"
22
+ }
23
+ }
@@ -0,0 +1,3 @@
1
+ import { GetUserDocument } from './graphql';
2
+
3
+ export const query = GetUserDocument;
@@ -0,0 +1,4 @@
1
+ output_dir: "__generated__"
2
+ projects:
3
+ - schema: "schema.graphql"
4
+ include: "src/**/*.{ts,tsx}"
@@ -0,0 +1,8 @@
1
+ type User {
2
+ id: ID!
3
+ name: String!
4
+ }
5
+
6
+ type Query {
7
+ user(id: ID!): User
8
+ }
@@ -0,0 +1,8 @@
1
+ import { graphql } from './graphql';
2
+
3
+ export const query = graphql(`query GetUser {
4
+ user(id: "1") {
5
+ id
6
+ name
7
+ }
8
+ }`);
@@ -0,0 +1,3 @@
1
+ import { GetMeDocument } from './graphql';
2
+
3
+ export const query = GetMeDocument;
@@ -0,0 +1,4 @@
1
+ output_dir: "__generated__"
2
+ projects:
3
+ - schema: "schema.graphql"
4
+ include: "src/**/*.{ts,tsx}"
@@ -0,0 +1,8 @@
1
+ type User {
2
+ id: ID!
3
+ name: String!
4
+ }
5
+
6
+ type Query {
7
+ me: User
8
+ }
@@ -0,0 +1,8 @@
1
+ import { gql } from './graphql';
2
+
3
+ export const query = gql(`query GetMe {
4
+ me {
5
+ id
6
+ name
7
+ }
8
+ }`);
@@ -0,0 +1,3 @@
1
+ import { GetMeDocument } from './graphql';
2
+
3
+ export const meQuery = GetMeDocument;
@@ -0,0 +1,3 @@
1
+ import { GetSettingsDocument } from './graphql';
2
+
3
+ export const settingsQuery = GetSettingsDocument;
@@ -0,0 +1,4 @@
1
+ output_dir: "__generated__"
2
+ projects:
3
+ - schema: "schema.graphql"
4
+ include: "src/**/*.{ts,tsx}"
@@ -0,0 +1,10 @@
1
+ type User {
2
+ id: ID!
3
+ name: String!
4
+ email: String!
5
+ }
6
+
7
+ type Query {
8
+ me: User
9
+ user(id: ID!): User
10
+ }
@@ -0,0 +1,16 @@
1
+ import { graphql } from './graphql';
2
+
3
+ export const meQuery = graphql(`query GetMe {
4
+ me {
5
+ id
6
+ name
7
+ }
8
+ }`);
9
+
10
+ export const userQuery = graphql(`query GetUser($id: ID!) {
11
+ user(id: $id) {
12
+ id
13
+ name
14
+ email
15
+ }
16
+ }`);
@@ -0,0 +1,8 @@
1
+ import { graphql } from './graphql';
2
+
3
+ export const settingsQuery = graphql(`query GetSettings {
4
+ settings {
5
+ theme
6
+ notifications
7
+ }
8
+ }`);