@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.
- package/README.md +79 -0
- package/dist/index.cjs +915 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +91 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +91 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +913 -0
- package/dist/index.mjs.map +1 -0
- package/dist/native.cjs +254 -0
- package/dist/native.cjs.map +1 -0
- package/dist/native.d.cts +46 -0
- package/dist/native.d.cts.map +1 -0
- package/dist/native.d.mts +46 -0
- package/dist/native.d.mts.map +1 -0
- package/dist/native.mjs +256 -0
- package/dist/native.mjs.map +1 -0
- package/package.json +81 -0
- package/src/index.ts +290 -0
- package/src/lib.rs +87 -0
- package/src/native/index.d.ts +42 -0
- package/src/native/index.js +316 -0
- package/src/native/swc-transformer.linux-x64-gnu.node +0 -0
- package/src/transform/analysis.rs +240 -0
- package/src/transform/imports.rs +285 -0
- package/src/transform/metadata.rs +371 -0
- package/src/transform/mod.rs +7 -0
- package/src/transform/runtime.rs +197 -0
- package/src/transform/transformer.rs +438 -0
- package/src/types/artifact.rs +107 -0
- package/src/types/config.rs +72 -0
- package/src/types/error.rs +132 -0
- package/src/types/mod.rs +12 -0
|
@@ -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
|
+
}
|