@soda-gql/swc-transformer 0.2.0

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.
@@ -0,0 +1,197 @@
1
+ //! Runtime call generation module.
2
+ //!
3
+ //! This module generates the `gqlRuntime.*` calls that replace `gql.default()` calls.
4
+
5
+ use swc_core::common::{SyntaxContext, DUMMY_SP};
6
+ use swc_core::ecma::ast::*;
7
+
8
+ use crate::types::{BuilderArtifactElement, FragmentPrebuild, OperationPrebuild};
9
+
10
+ use super::analysis::GqlReplacement;
11
+
12
+ const RUNTIME_IMPORT_NAME: &str = "gqlRuntime";
13
+ const CJS_RUNTIME_NAME: &str = "__soda_gql_runtime";
14
+
15
+ /// Builds runtime calls for GQL transformations.
16
+ pub struct RuntimeCallBuilder {
17
+ is_cjs: bool,
18
+ }
19
+
20
+ impl RuntimeCallBuilder {
21
+ pub fn new(is_cjs: bool) -> Self {
22
+ Self { is_cjs }
23
+ }
24
+
25
+ /// Build replacement expression and optional runtime statement.
26
+ ///
27
+ /// For fragments: returns just the replacement expression.
28
+ /// For operations: returns both a reference expression and a runtime setup statement.
29
+ pub fn build_replacement(&self, replacement: &GqlReplacement) -> Option<(Expr, Option<Stmt>)> {
30
+ let result = match &replacement.artifact {
31
+ BuilderArtifactElement::Fragment { prebuild, .. } => self
32
+ .build_fragment_call(prebuild, &replacement.builder_args)
33
+ .map(|expr| (expr, None)),
34
+ BuilderArtifactElement::Operation { prebuild, .. } => {
35
+ self.build_operation_calls(prebuild)
36
+ }
37
+ };
38
+
39
+ if result.is_none() {
40
+ let artifact_type = match &replacement.artifact {
41
+ BuilderArtifactElement::Fragment { .. } => "Fragment",
42
+ BuilderArtifactElement::Operation { .. } => "Operation",
43
+ };
44
+ eprintln!(
45
+ "[swc-transformer] Warning: Failed to build replacement for {} artifact (canonical ID: '{}'). \
46
+ This may indicate missing or mismatched builder arguments.",
47
+ artifact_type, replacement.canonical_id
48
+ );
49
+ }
50
+
51
+ result
52
+ }
53
+
54
+ /// Create the runtime accessor expression.
55
+ fn create_runtime_accessor(&self) -> Expr {
56
+ if self.is_cjs {
57
+ // __soda_gql_runtime.gqlRuntime
58
+ Expr::Member(MemberExpr {
59
+ span: DUMMY_SP,
60
+ obj: Box::new(Expr::Ident(Ident::new(
61
+ CJS_RUNTIME_NAME.into(),
62
+ DUMMY_SP,
63
+ Default::default(),
64
+ ))),
65
+ prop: MemberProp::Ident(IdentName::new(RUNTIME_IMPORT_NAME.into(), DUMMY_SP)),
66
+ })
67
+ } else {
68
+ Expr::Ident(Ident::new(
69
+ RUNTIME_IMPORT_NAME.into(),
70
+ DUMMY_SP,
71
+ Default::default(),
72
+ ))
73
+ }
74
+ }
75
+
76
+ /// Create a runtime method call.
77
+ fn create_runtime_call(&self, method: &str, args: Vec<ExprOrSpread>) -> Expr {
78
+ Expr::Call(CallExpr {
79
+ span: DUMMY_SP,
80
+ ctxt: SyntaxContext::empty(),
81
+ callee: Callee::Expr(Box::new(Expr::Member(MemberExpr {
82
+ span: DUMMY_SP,
83
+ obj: Box::new(self.create_runtime_accessor()),
84
+ prop: MemberProp::Ident(IdentName::new(method.into(), DUMMY_SP)),
85
+ }))),
86
+ args,
87
+ type_args: None,
88
+ })
89
+ }
90
+
91
+ /// Build a fragment runtime call.
92
+ ///
93
+ /// Input: `fragment.User({}, fields)`
94
+ /// Output: `gqlRuntime.fragment({ prebuild: { typename: "User" } })`
95
+ fn build_fragment_call(
96
+ &self,
97
+ prebuild: &FragmentPrebuild,
98
+ _builder_args: &[ExprOrSpread],
99
+ ) -> Option<Expr> {
100
+ let arg = self.create_object_lit(vec![(
101
+ "prebuild",
102
+ self.create_object_lit(vec![("typename", self.create_string_lit(&prebuild.typename))]),
103
+ )]);
104
+
105
+ Some(self.create_runtime_call(
106
+ "fragment",
107
+ vec![ExprOrSpread {
108
+ spread: None,
109
+ expr: Box::new(arg),
110
+ }],
111
+ ))
112
+ }
113
+
114
+ /// Build operation runtime calls.
115
+ ///
116
+ /// Returns (reference_call, runtime_call) where:
117
+ /// - runtime_call: `gqlRuntime.operation({ prebuild: JSON.parse(...), runtime: {} })`
118
+ /// - reference_call: `gqlRuntime.getOperation("OperationName")`
119
+ fn build_operation_calls(&self, prebuild: &OperationPrebuild) -> Option<(Expr, Option<Stmt>)> {
120
+ // Build the runtime call
121
+ let prebuild_json = serde_json::to_string(prebuild).ok()?;
122
+ let runtime_call_expr = self.create_runtime_call(
123
+ "operation",
124
+ vec![ExprOrSpread {
125
+ spread: None,
126
+ expr: Box::new(self.create_object_lit(vec![
127
+ ("prebuild", self.create_json_parse(&prebuild_json)),
128
+ ("runtime", self.create_object_lit(vec![])),
129
+ ])),
130
+ }],
131
+ );
132
+
133
+ // Wrap in an expression statement
134
+ let runtime_stmt = Stmt::Expr(ExprStmt {
135
+ span: DUMMY_SP,
136
+ expr: Box::new(runtime_call_expr),
137
+ });
138
+
139
+ // Build the reference call
140
+ let reference_call = self.create_runtime_call(
141
+ "getOperation",
142
+ vec![ExprOrSpread {
143
+ spread: None,
144
+ expr: Box::new(self.create_string_lit(&prebuild.operation_name)),
145
+ }],
146
+ );
147
+
148
+ Some((reference_call, Some(runtime_stmt)))
149
+ }
150
+
151
+ /// Create an object literal expression.
152
+ fn create_object_lit(&self, props: Vec<(&str, Expr)>) -> Expr {
153
+ Expr::Object(ObjectLit {
154
+ span: DUMMY_SP,
155
+ props: props
156
+ .into_iter()
157
+ .map(|(key, value)| {
158
+ PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
159
+ key: PropName::Ident(IdentName::new(key.into(), DUMMY_SP)),
160
+ value: Box::new(value),
161
+ })))
162
+ })
163
+ .collect(),
164
+ })
165
+ }
166
+
167
+ /// Create a string literal expression.
168
+ fn create_string_lit(&self, value: &str) -> Expr {
169
+ Expr::Lit(Lit::Str(Str {
170
+ span: DUMMY_SP,
171
+ value: value.into(),
172
+ raw: None,
173
+ }))
174
+ }
175
+
176
+ /// Create a JSON.parse() call expression.
177
+ fn create_json_parse(&self, json: &str) -> Expr {
178
+ Expr::Call(CallExpr {
179
+ span: DUMMY_SP,
180
+ ctxt: SyntaxContext::empty(),
181
+ callee: Callee::Expr(Box::new(Expr::Member(MemberExpr {
182
+ span: DUMMY_SP,
183
+ obj: Box::new(Expr::Ident(Ident::new(
184
+ "JSON".into(),
185
+ DUMMY_SP,
186
+ Default::default(),
187
+ ))),
188
+ prop: MemberProp::Ident(IdentName::new("parse".into(), DUMMY_SP)),
189
+ }))),
190
+ args: vec![ExprOrSpread {
191
+ spread: None,
192
+ expr: Box::new(self.create_string_lit(json)),
193
+ }],
194
+ type_args: None,
195
+ })
196
+ }
197
+ }
@@ -0,0 +1,438 @@
1
+ //! Main transformation orchestration.
2
+ //!
3
+ //! This module coordinates the transformation process:
4
+ //! 1. Parse source with SWC parser
5
+ //! 2. Collect metadata (canonical IDs, export bindings)
6
+ //! 3. Visit AST and transform gql.default() calls
7
+ //! 4. Manage imports (add runtime, remove graphql-system)
8
+ //! 5. Insert runtime calls after imports
9
+ //! 6. Emit code with SWC codegen
10
+
11
+ use serde::{Deserialize, Serialize};
12
+ use swc_core::common::comments::SingleThreadedComments;
13
+ use swc_core::common::source_map::SourceMapGenConfig;
14
+ use swc_core::common::sync::Lrc;
15
+ use swc_core::common::{BytePos, FileName, SourceMap};
16
+ use swc_core::ecma::ast::*;
17
+ use swc_core::ecma::codegen::{text_writer::JsWriter, Emitter};
18
+ use swc_core::ecma::parser::{lexer::Lexer, Parser, Syntax, TsSyntax};
19
+ use swc_core::ecma::visit::{VisitMut, VisitMutWith, VisitWith};
20
+
21
+ use crate::types::{BuilderArtifact, TransformInput, TransformInputRef};
22
+
23
+ use super::analysis::GqlCallFinder;
24
+ use super::imports::ImportManager;
25
+ use super::metadata::MetadataCollector;
26
+ use super::runtime::RuntimeCallBuilder;
27
+
28
+ use crate::types::PluginError;
29
+
30
+ /// Result of a transformation.
31
+ #[derive(Debug, Clone, Serialize, Deserialize)]
32
+ #[serde(rename_all = "camelCase")]
33
+ pub struct TransformResult {
34
+ /// The transformed source code.
35
+ pub output_code: String,
36
+
37
+ /// Whether any transformation was performed.
38
+ pub transformed: bool,
39
+
40
+ /// Errors encountered during transformation.
41
+ /// These are non-fatal - transformation continues but logs issues.
42
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
43
+ pub errors: Vec<PluginError>,
44
+
45
+ /// Source map JSON, if source map generation was enabled.
46
+ #[serde(default, skip_serializing_if = "Option::is_none")]
47
+ pub source_map: Option<String>,
48
+ }
49
+
50
+ /// Transform a source file.
51
+ ///
52
+ /// # Arguments
53
+ /// * `input` - The transformation input containing source, path, artifact, and config
54
+ ///
55
+ /// # Returns
56
+ /// Result containing the transformed code, or an error message
57
+ pub fn transform_source(input: &TransformInput) -> Result<TransformResult, String> {
58
+ // Check if this is the graphql-system file - if so, stub it out
59
+ if is_graphql_system_file(&input.source_path, &input.config.graphql_system_path) {
60
+ return Ok(TransformResult {
61
+ output_code: "export {};".to_string(),
62
+ transformed: true,
63
+ errors: Vec::new(),
64
+ source_map: None,
65
+ });
66
+ }
67
+
68
+ // Parse the artifact
69
+ let artifact: BuilderArtifact = serde_json::from_str(&input.artifact_json)
70
+ .map_err(|e| format!("Failed to parse artifact: {}", e))?;
71
+
72
+ // Create source map
73
+ let cm: Lrc<SourceMap> = Default::default();
74
+ let fm = cm.new_source_file(
75
+ Lrc::new(FileName::Custom(input.source_path.clone())),
76
+ input.source_code.clone(),
77
+ );
78
+
79
+ // Determine if this is a TSX file
80
+ let is_tsx = input.source_path.ends_with(".tsx");
81
+
82
+ // Create comments storage for preservation
83
+ let comments = SingleThreadedComments::default();
84
+
85
+ // Create parser with comments collection
86
+ let lexer = Lexer::new(
87
+ Syntax::Typescript(TsSyntax {
88
+ tsx: is_tsx,
89
+ ..Default::default()
90
+ }),
91
+ EsVersion::Es2022,
92
+ (&*fm).into(),
93
+ Some(&comments),
94
+ );
95
+
96
+ let mut parser = Parser::new_from(lexer);
97
+ let mut module = parser
98
+ .parse_module()
99
+ .map_err(|e| format!("Parse error: {:?}", e))?;
100
+
101
+ // Collect metadata about GQL definitions
102
+ let metadata = MetadataCollector::collect(&module, &input.source_path);
103
+
104
+ // Find and analyze GQL calls
105
+ let mut finder = GqlCallFinder::new(&artifact, &metadata, &input.source_path);
106
+ module.visit_with(&mut finder);
107
+
108
+ // If no GQL calls found, return unchanged (but may have errors)
109
+ if !finder.has_transformations() {
110
+ return Ok(TransformResult {
111
+ output_code: input.source_code.clone(),
112
+ transformed: false,
113
+ errors: finder.take_errors(),
114
+ source_map: None,
115
+ });
116
+ }
117
+
118
+ // Build runtime calls and transform
119
+ let runtime_builder = RuntimeCallBuilder::new(input.config.is_cjs);
120
+ let mut transformer = GqlTransformer::new(&finder, &runtime_builder, &input.source_path);
121
+ module.visit_mut_with(&mut transformer);
122
+
123
+ // Manage imports
124
+ let mut import_manager = ImportManager::new(
125
+ transformer.needs_runtime_import(),
126
+ input.config.is_cjs,
127
+ &input.config.graphql_system_aliases,
128
+ );
129
+ module.visit_mut_with(&mut import_manager);
130
+
131
+ // Insert runtime calls after imports
132
+ if !transformer.runtime_calls.is_empty() {
133
+ insert_runtime_calls(&mut module, std::mem::take(&mut transformer.runtime_calls));
134
+ }
135
+
136
+ // Emit the transformed code with preserved comments and optional source map
137
+ let emit_output = emit_module(&cm, &module, &comments, input.config.source_map)?;
138
+
139
+ // Collect errors from both phases
140
+ // Take transformer errors first, then drop to release borrow of finder
141
+ let transformer_errors = transformer.take_errors();
142
+ drop(transformer);
143
+ // Now we can mutably borrow finder
144
+ let mut errors = finder.take_errors();
145
+ errors.extend(transformer_errors);
146
+
147
+ Ok(TransformResult {
148
+ output_code: emit_output.code,
149
+ transformed: true,
150
+ errors,
151
+ source_map: emit_output.source_map,
152
+ })
153
+ }
154
+
155
+ /// Transform a source file with a pre-parsed artifact reference.
156
+ ///
157
+ /// This is more efficient than `transform_source` when transforming multiple files
158
+ /// with the same artifact, as it avoids repeated JSON parsing.
159
+ ///
160
+ /// # Arguments
161
+ /// * `input` - The transformation input containing source, path, artifact reference, and config
162
+ ///
163
+ /// # Returns
164
+ /// Result containing the transformed code, or an error message
165
+ pub fn transform_source_ref(input: &TransformInputRef<'_>) -> Result<TransformResult, String> {
166
+ // Check if this is the graphql-system file - if so, stub it out
167
+ if is_graphql_system_file(&input.source_path, &input.config.graphql_system_path) {
168
+ return Ok(TransformResult {
169
+ output_code: "export {};".to_string(),
170
+ transformed: true,
171
+ errors: Vec::new(),
172
+ source_map: None,
173
+ });
174
+ }
175
+
176
+ // Create source map
177
+ let cm: Lrc<SourceMap> = Default::default();
178
+ let fm = cm.new_source_file(
179
+ Lrc::new(FileName::Custom(input.source_path.clone())),
180
+ input.source_code.clone(),
181
+ );
182
+
183
+ // Determine if this is a TSX file
184
+ let is_tsx = input.source_path.ends_with(".tsx");
185
+
186
+ // Create comments storage for preservation
187
+ let comments = SingleThreadedComments::default();
188
+
189
+ // Create parser with comments collection
190
+ let lexer = Lexer::new(
191
+ Syntax::Typescript(TsSyntax {
192
+ tsx: is_tsx,
193
+ ..Default::default()
194
+ }),
195
+ EsVersion::Es2022,
196
+ (&*fm).into(),
197
+ Some(&comments),
198
+ );
199
+
200
+ let mut parser = Parser::new_from(lexer);
201
+ let mut module = parser
202
+ .parse_module()
203
+ .map_err(|e| format!("Parse error: {:?}", e))?;
204
+
205
+ // Collect metadata about GQL definitions
206
+ let metadata = MetadataCollector::collect(&module, &input.source_path);
207
+
208
+ // Find and analyze GQL calls (use pre-parsed artifact reference)
209
+ let mut finder = GqlCallFinder::new(input.artifact, &metadata, &input.source_path);
210
+ module.visit_with(&mut finder);
211
+
212
+ // If no GQL calls found, return unchanged (but may have errors)
213
+ if !finder.has_transformations() {
214
+ return Ok(TransformResult {
215
+ output_code: input.source_code.clone(),
216
+ transformed: false,
217
+ errors: finder.take_errors(),
218
+ source_map: None,
219
+ });
220
+ }
221
+
222
+ // Build runtime calls and transform
223
+ let runtime_builder = RuntimeCallBuilder::new(input.config.is_cjs);
224
+ let mut transformer = GqlTransformer::new(&finder, &runtime_builder, &input.source_path);
225
+ module.visit_mut_with(&mut transformer);
226
+
227
+ // Manage imports
228
+ let mut import_manager = ImportManager::new(
229
+ transformer.needs_runtime_import(),
230
+ input.config.is_cjs,
231
+ &input.config.graphql_system_aliases,
232
+ );
233
+ module.visit_mut_with(&mut import_manager);
234
+
235
+ // Insert runtime calls after imports
236
+ if !transformer.runtime_calls.is_empty() {
237
+ insert_runtime_calls(&mut module, std::mem::take(&mut transformer.runtime_calls));
238
+ }
239
+
240
+ // Emit the transformed code with preserved comments and optional source map
241
+ let emit_output = emit_module(&cm, &module, &comments, input.config.source_map)?;
242
+
243
+ // Collect errors from both phases
244
+ let transformer_errors = transformer.take_errors();
245
+ drop(transformer);
246
+ let mut errors = finder.take_errors();
247
+ errors.extend(transformer_errors);
248
+
249
+ Ok(TransformResult {
250
+ output_code: emit_output.code,
251
+ transformed: true,
252
+ errors,
253
+ source_map: emit_output.source_map,
254
+ })
255
+ }
256
+
257
+ /// Main AST transformer that replaces gql.default() calls with runtime calls.
258
+ struct GqlTransformer<'a> {
259
+ finder: &'a GqlCallFinder<'a>,
260
+ runtime_builder: &'a RuntimeCallBuilder,
261
+ needs_runtime: bool,
262
+ pub runtime_calls: Vec<Stmt>,
263
+ errors: Vec<PluginError>,
264
+ source_path: String,
265
+ }
266
+
267
+ impl<'a> GqlTransformer<'a> {
268
+ fn new(finder: &'a GqlCallFinder<'a>, runtime_builder: &'a RuntimeCallBuilder, source_path: &str) -> Self {
269
+ Self {
270
+ finder,
271
+ runtime_builder,
272
+ needs_runtime: false,
273
+ runtime_calls: Vec::new(),
274
+ errors: Vec::new(),
275
+ source_path: source_path.to_string(),
276
+ }
277
+ }
278
+
279
+ fn needs_runtime_import(&self) -> bool {
280
+ self.needs_runtime
281
+ }
282
+
283
+ fn take_errors(&mut self) -> Vec<PluginError> {
284
+ std::mem::take(&mut self.errors)
285
+ }
286
+ }
287
+
288
+ impl VisitMut for GqlTransformer<'_> {
289
+ fn visit_mut_expr(&mut self, expr: &mut Expr) {
290
+ // First visit children
291
+ expr.visit_mut_children_with(self);
292
+
293
+ // Check if this is a GQL call that should be transformed
294
+ if let Expr::Call(call) = expr {
295
+ if let Some(replacement) = self.finder.get_replacement(call) {
296
+ // Mark that we need the runtime import
297
+ self.needs_runtime = true;
298
+
299
+ // Build the replacement expression
300
+ if let Some((reference_expr, runtime_stmt)) =
301
+ self.runtime_builder.build_replacement(replacement)
302
+ {
303
+ // Store the runtime statement to be inserted later
304
+ if let Some(stmt) = runtime_stmt {
305
+ self.runtime_calls.push(stmt);
306
+ }
307
+
308
+ // Replace the expression
309
+ *expr = reference_expr;
310
+ } else {
311
+ // Record structured error when replacement build fails
312
+ let artifact_type = match &replacement.artifact {
313
+ crate::types::BuilderArtifactElement::Fragment { .. } => "fragment",
314
+ crate::types::BuilderArtifactElement::Operation { .. } => "operation",
315
+ };
316
+ let error = PluginError::missing_builder_arg(
317
+ &self.source_path,
318
+ artifact_type,
319
+ "builder callback",
320
+ );
321
+ eprintln!("[swc-transformer] {}", error.format());
322
+ self.errors.push(error);
323
+ }
324
+ }
325
+ }
326
+ }
327
+ }
328
+
329
+ /// Insert runtime calls after the last import statement.
330
+ fn insert_runtime_calls(module: &mut Module, calls: Vec<Stmt>) {
331
+ if calls.is_empty() {
332
+ return;
333
+ }
334
+
335
+ // Find the position after the last import
336
+ let mut insert_pos = 0;
337
+ for (i, item) in module.body.iter().enumerate() {
338
+ if matches!(item, ModuleItem::ModuleDecl(ModuleDecl::Import(_))) {
339
+ insert_pos = i + 1;
340
+ }
341
+ }
342
+
343
+ // Insert runtime calls
344
+ let items: Vec<ModuleItem> = calls.into_iter().map(ModuleItem::Stmt).collect();
345
+ module.body.splice(insert_pos..insert_pos, items);
346
+ }
347
+
348
+ /// Output from code emission.
349
+ struct EmitOutput {
350
+ code: String,
351
+ source_map: Option<String>,
352
+ }
353
+
354
+ /// Configuration for source map generation.
355
+ struct SimpleSourceMapConfig;
356
+
357
+ impl SourceMapGenConfig for SimpleSourceMapConfig {
358
+ fn file_name_to_source(&self, f: &FileName) -> String {
359
+ match f {
360
+ FileName::Real(path) => path.to_string_lossy().to_string(),
361
+ FileName::Custom(name) => name.clone(),
362
+ FileName::Url(url) => url.to_string(),
363
+ _ => "unknown".to_string(),
364
+ }
365
+ }
366
+
367
+ fn name_for_bytepos(&self, _bpos: BytePos) -> Option<&str> {
368
+ None
369
+ }
370
+
371
+ fn inline_sources_content(&self, _f: &FileName) -> bool {
372
+ true // Include source content in the source map
373
+ }
374
+ }
375
+
376
+ /// Emit the module as JavaScript code with preserved comments.
377
+ fn emit_module(
378
+ cm: &Lrc<SourceMap>,
379
+ module: &Module,
380
+ comments: &SingleThreadedComments,
381
+ generate_source_map: bool,
382
+ ) -> Result<EmitOutput, String> {
383
+ let mut buf = vec![];
384
+ let mut srcmap_buf = if generate_source_map {
385
+ Some(vec![])
386
+ } else {
387
+ None
388
+ };
389
+
390
+ {
391
+ let writer = JsWriter::new(
392
+ cm.clone(),
393
+ "\n",
394
+ &mut buf,
395
+ srcmap_buf.as_mut(),
396
+ );
397
+ let mut emitter = Emitter {
398
+ cfg: swc_core::ecma::codegen::Config::default().with_minify(false),
399
+ cm: cm.clone(),
400
+ comments: Some(comments),
401
+ wr: writer,
402
+ };
403
+
404
+ emitter
405
+ .emit_module(module)
406
+ .map_err(|e| format!("Emit error: {:?}", e))?;
407
+ }
408
+
409
+ let code = String::from_utf8(buf).map_err(|e| format!("UTF-8 error: {}", e))?;
410
+
411
+ let source_map = if let Some(srcmap) = srcmap_buf {
412
+ // Build source map from collected entries
413
+ let config = SimpleSourceMapConfig;
414
+ let map = cm.build_source_map(&srcmap, None, config);
415
+ let mut map_buf = vec![];
416
+ map.to_writer(&mut map_buf)
417
+ .map_err(|e| format!("Source map error: {:?}", e))?;
418
+ Some(String::from_utf8(map_buf).map_err(|e| format!("Source map UTF-8 error: {}", e))?)
419
+ } else {
420
+ None
421
+ };
422
+
423
+ Ok(EmitOutput { code, source_map })
424
+ }
425
+
426
+ /// Check if the source file is the graphql-system file.
427
+ /// Both paths should be normalized (forward slashes) before comparison.
428
+ fn is_graphql_system_file(source_path: &str, graphql_system_path: &Option<String>) -> bool {
429
+ match graphql_system_path {
430
+ Some(gql_path) => {
431
+ // Normalize both paths for comparison (remove trailing slashes, normalize separators)
432
+ let normalized_source = source_path.replace('\\', "/");
433
+ let normalized_gql = gql_path.replace('\\', "/");
434
+ normalized_source == normalized_gql
435
+ }
436
+ None => false,
437
+ }
438
+ }