@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,285 @@
|
|
|
1
|
+
//! Import management module.
|
|
2
|
+
//!
|
|
3
|
+
//! This module handles:
|
|
4
|
+
//! - Adding the `@soda-gql/runtime` import/require
|
|
5
|
+
//! - Removing the `graphql-system` imports
|
|
6
|
+
|
|
7
|
+
use swc_core::common::{SyntaxContext, DUMMY_SP};
|
|
8
|
+
use swc_core::ecma::ast::*;
|
|
9
|
+
use swc_core::ecma::visit::{VisitMut, VisitMutWith};
|
|
10
|
+
|
|
11
|
+
const RUNTIME_MODULE: &str = "@soda-gql/runtime";
|
|
12
|
+
const RUNTIME_IMPORT_NAME: &str = "gqlRuntime";
|
|
13
|
+
const CJS_RUNTIME_NAME: &str = "__soda_gql_runtime";
|
|
14
|
+
|
|
15
|
+
/// Manages imports for the transformation.
|
|
16
|
+
pub struct ImportManager {
|
|
17
|
+
needs_runtime_import: bool,
|
|
18
|
+
is_cjs: bool,
|
|
19
|
+
graphql_system_aliases: Vec<String>,
|
|
20
|
+
has_added_import: bool,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
impl ImportManager {
|
|
24
|
+
pub fn new(needs_runtime_import: bool, is_cjs: bool, graphql_system_aliases: &[String]) -> Self {
|
|
25
|
+
Self {
|
|
26
|
+
needs_runtime_import,
|
|
27
|
+
is_cjs,
|
|
28
|
+
graphql_system_aliases: graphql_system_aliases.to_vec(),
|
|
29
|
+
has_added_import: false,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Check if a specifier is a graphql-system import.
|
|
34
|
+
fn is_graphql_system_import(&self, specifier: &str) -> bool {
|
|
35
|
+
self.graphql_system_aliases.iter().any(|alias| {
|
|
36
|
+
specifier == alias || specifier.starts_with(&format!("{}/", alias))
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Create the ESM runtime import.
|
|
41
|
+
fn create_esm_import(&self) -> ModuleItem {
|
|
42
|
+
// import { gqlRuntime } from "@soda-gql/runtime";
|
|
43
|
+
ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
|
|
44
|
+
span: DUMMY_SP,
|
|
45
|
+
specifiers: vec![ImportSpecifier::Named(ImportNamedSpecifier {
|
|
46
|
+
span: DUMMY_SP,
|
|
47
|
+
local: Ident::new(RUNTIME_IMPORT_NAME.into(), DUMMY_SP, Default::default()),
|
|
48
|
+
imported: None,
|
|
49
|
+
is_type_only: false,
|
|
50
|
+
})],
|
|
51
|
+
src: Box::new(Str {
|
|
52
|
+
span: DUMMY_SP,
|
|
53
|
+
value: RUNTIME_MODULE.into(),
|
|
54
|
+
raw: None,
|
|
55
|
+
}),
|
|
56
|
+
type_only: false,
|
|
57
|
+
with: None,
|
|
58
|
+
phase: ImportPhase::Evaluation,
|
|
59
|
+
}))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Create the CJS runtime require.
|
|
63
|
+
fn create_cjs_require(&self) -> ModuleItem {
|
|
64
|
+
// const __soda_gql_runtime = require("@soda-gql/runtime");
|
|
65
|
+
ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl {
|
|
66
|
+
span: DUMMY_SP,
|
|
67
|
+
ctxt: SyntaxContext::empty(),
|
|
68
|
+
kind: VarDeclKind::Const,
|
|
69
|
+
declare: false,
|
|
70
|
+
decls: vec![VarDeclarator {
|
|
71
|
+
span: DUMMY_SP,
|
|
72
|
+
name: Pat::Ident(BindingIdent {
|
|
73
|
+
id: Ident::new(CJS_RUNTIME_NAME.into(), DUMMY_SP, Default::default()),
|
|
74
|
+
type_ann: None,
|
|
75
|
+
}),
|
|
76
|
+
init: Some(Box::new(Expr::Call(CallExpr {
|
|
77
|
+
span: DUMMY_SP,
|
|
78
|
+
ctxt: SyntaxContext::empty(),
|
|
79
|
+
callee: Callee::Expr(Box::new(Expr::Ident(Ident::new(
|
|
80
|
+
"require".into(),
|
|
81
|
+
DUMMY_SP,
|
|
82
|
+
Default::default(),
|
|
83
|
+
)))),
|
|
84
|
+
args: vec![ExprOrSpread {
|
|
85
|
+
spread: None,
|
|
86
|
+
expr: Box::new(Expr::Lit(Lit::Str(Str {
|
|
87
|
+
span: DUMMY_SP,
|
|
88
|
+
value: RUNTIME_MODULE.into(),
|
|
89
|
+
raw: None,
|
|
90
|
+
}))),
|
|
91
|
+
}],
|
|
92
|
+
type_args: None,
|
|
93
|
+
}))),
|
|
94
|
+
definite: false,
|
|
95
|
+
}],
|
|
96
|
+
}))))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Check if a variable declaration is a require for graphql-system.
|
|
100
|
+
fn is_graphql_system_require(&self, decl: &VarDeclarator) -> bool {
|
|
101
|
+
if let Some(init) = &decl.init {
|
|
102
|
+
if let Some(specifier) = extract_require_specifier(init) {
|
|
103
|
+
return self.is_graphql_system_import(&specifier);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
false
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Check if an import already has the runtime import.
|
|
110
|
+
fn has_runtime_import(&self, import: &ImportDecl) -> bool {
|
|
111
|
+
if !wtf8_eq(&import.src.value, RUNTIME_MODULE) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
import.specifiers.iter().any(|spec| {
|
|
116
|
+
if let ImportSpecifier::Named(named) = spec {
|
|
117
|
+
atom_eq(&named.local.sym, RUNTIME_IMPORT_NAME)
|
|
118
|
+
} else {
|
|
119
|
+
false
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
impl VisitMut for ImportManager {
|
|
126
|
+
fn visit_mut_module(&mut self, module: &mut Module) {
|
|
127
|
+
// First, visit children to handle nested transformations
|
|
128
|
+
module.visit_mut_children_with(self);
|
|
129
|
+
|
|
130
|
+
// Collect new body items
|
|
131
|
+
let mut new_body: Vec<ModuleItem> = Vec::new();
|
|
132
|
+
let mut import_insert_pos = 0;
|
|
133
|
+
let mut found_non_import = false;
|
|
134
|
+
let mut existing_runtime_import_idx: Option<usize> = None;
|
|
135
|
+
|
|
136
|
+
for (_idx, item) in module.body.iter().enumerate() {
|
|
137
|
+
match item {
|
|
138
|
+
// Handle ESM imports
|
|
139
|
+
ModuleItem::ModuleDecl(ModuleDecl::Import(import)) => {
|
|
140
|
+
let specifier = wtf8_to_string(&import.src.value);
|
|
141
|
+
|
|
142
|
+
// Skip graphql-system imports
|
|
143
|
+
if self.is_graphql_system_import(&specifier) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check if this is already the runtime import
|
|
148
|
+
if specifier == RUNTIME_MODULE {
|
|
149
|
+
existing_runtime_import_idx = Some(new_body.len());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
import_insert_pos = new_body.len() + 1;
|
|
153
|
+
new_body.push(item.clone());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Handle CJS require statements
|
|
157
|
+
ModuleItem::Stmt(Stmt::Decl(Decl::Var(var_decl))) => {
|
|
158
|
+
// Filter out graphql-system requires
|
|
159
|
+
let filtered_decls: Vec<VarDeclarator> = var_decl
|
|
160
|
+
.decls
|
|
161
|
+
.iter()
|
|
162
|
+
.filter(|decl| !self.is_graphql_system_require(decl))
|
|
163
|
+
.cloned()
|
|
164
|
+
.collect();
|
|
165
|
+
|
|
166
|
+
if filtered_decls.is_empty() {
|
|
167
|
+
// All declarations were graphql-system requires, skip
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if filtered_decls.len() < var_decl.decls.len() {
|
|
172
|
+
// Some declarations were filtered
|
|
173
|
+
new_body.push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(
|
|
174
|
+
VarDecl {
|
|
175
|
+
span: var_decl.span,
|
|
176
|
+
ctxt: var_decl.ctxt,
|
|
177
|
+
kind: var_decl.kind,
|
|
178
|
+
declare: var_decl.declare,
|
|
179
|
+
decls: filtered_decls,
|
|
180
|
+
},
|
|
181
|
+
)))));
|
|
182
|
+
} else {
|
|
183
|
+
new_body.push(item.clone());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if !found_non_import {
|
|
187
|
+
import_insert_pos = new_body.len();
|
|
188
|
+
}
|
|
189
|
+
found_non_import = true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
_ => {
|
|
193
|
+
if !found_non_import {
|
|
194
|
+
import_insert_pos = new_body.len();
|
|
195
|
+
}
|
|
196
|
+
found_non_import = true;
|
|
197
|
+
new_body.push(item.clone());
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Add runtime import if needed
|
|
203
|
+
if self.needs_runtime_import && !self.has_added_import {
|
|
204
|
+
// Check if we already have the runtime import
|
|
205
|
+
let already_has_import = existing_runtime_import_idx.map_or(false, |idx| {
|
|
206
|
+
if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = &new_body[idx] {
|
|
207
|
+
self.has_runtime_import(import)
|
|
208
|
+
} else {
|
|
209
|
+
false
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if !already_has_import {
|
|
214
|
+
if let Some(idx) = existing_runtime_import_idx {
|
|
215
|
+
// Merge with existing import
|
|
216
|
+
if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = &mut new_body[idx] {
|
|
217
|
+
let mut specifiers = import.specifiers.clone();
|
|
218
|
+
specifiers.push(ImportSpecifier::Named(ImportNamedSpecifier {
|
|
219
|
+
span: DUMMY_SP,
|
|
220
|
+
local: Ident::new(RUNTIME_IMPORT_NAME.into(), DUMMY_SP, Default::default()),
|
|
221
|
+
imported: None,
|
|
222
|
+
is_type_only: false,
|
|
223
|
+
}));
|
|
224
|
+
import.specifiers = specifiers;
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
// Add new import
|
|
228
|
+
let runtime_import = if self.is_cjs {
|
|
229
|
+
self.create_cjs_require()
|
|
230
|
+
} else {
|
|
231
|
+
self.create_esm_import()
|
|
232
|
+
};
|
|
233
|
+
new_body.insert(import_insert_pos, runtime_import);
|
|
234
|
+
}
|
|
235
|
+
self.has_added_import = true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.body = new_body;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// Extract the module specifier from a require() call.
|
|
244
|
+
fn extract_require_specifier(expr: &Expr) -> Option<String> {
|
|
245
|
+
match expr {
|
|
246
|
+
Expr::Call(call) => {
|
|
247
|
+
// Direct require("...")
|
|
248
|
+
if let Callee::Expr(callee) = &call.callee {
|
|
249
|
+
if let Expr::Ident(ident) = &**callee {
|
|
250
|
+
if atom_eq(&ident.sym, "require") {
|
|
251
|
+
if let Some(arg) = call.args.first() {
|
|
252
|
+
if let Expr::Lit(Lit::Str(s)) = &*arg.expr {
|
|
253
|
+
return Some(wtf8_to_string(&s.value));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// __importDefault(require("...")) or __importStar(require("..."))
|
|
259
|
+
if atom_eq(&ident.sym, "__importDefault") || atom_eq(&ident.sym, "__importStar") {
|
|
260
|
+
if let Some(arg) = call.args.first() {
|
|
261
|
+
return extract_require_specifier(&arg.expr);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
None
|
|
267
|
+
}
|
|
268
|
+
_ => None,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// Helper to compare an Atom with a string.
|
|
273
|
+
fn atom_eq<T: AsRef<str>>(atom: &T, s: &str) -> bool {
|
|
274
|
+
atom.as_ref() == s
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/// Helper to compare a Wtf8Atom (string literal value) with a string.
|
|
278
|
+
fn wtf8_eq(atom: &swc_core::atoms::Wtf8Atom, s: &str) -> bool {
|
|
279
|
+
atom.to_string_lossy() == s
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/// Helper to convert a Wtf8Atom to String.
|
|
283
|
+
fn wtf8_to_string(atom: &swc_core::atoms::Wtf8Atom) -> String {
|
|
284
|
+
atom.to_string_lossy().into_owned()
|
|
285
|
+
}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
//! Metadata collection module.
|
|
2
|
+
//!
|
|
3
|
+
//! This module collects metadata about GQL definitions in the source code:
|
|
4
|
+
//! - AST path (canonical path)
|
|
5
|
+
//! - Export bindings
|
|
6
|
+
//! - Scope tracking
|
|
7
|
+
|
|
8
|
+
use std::collections::HashMap;
|
|
9
|
+
use swc_core::common::Span;
|
|
10
|
+
use swc_core::ecma::ast::*;
|
|
11
|
+
use swc_core::ecma::visit::{Visit, VisitWith};
|
|
12
|
+
|
|
13
|
+
/// Metadata about a GQL definition.
|
|
14
|
+
#[derive(Debug, Clone)]
|
|
15
|
+
pub struct GqlDefinitionMetadata {
|
|
16
|
+
/// The AST path for canonical ID resolution.
|
|
17
|
+
pub ast_path: String,
|
|
18
|
+
/// Whether this is a top-level definition.
|
|
19
|
+
#[allow(dead_code)]
|
|
20
|
+
pub is_top_level: bool,
|
|
21
|
+
/// Whether this definition is exported.
|
|
22
|
+
#[allow(dead_code)]
|
|
23
|
+
pub is_exported: bool,
|
|
24
|
+
/// The export binding name, if exported.
|
|
25
|
+
#[allow(dead_code)]
|
|
26
|
+
pub export_binding: Option<String>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Map from call expression span to metadata.
|
|
30
|
+
pub type MetadataMap = HashMap<Span, GqlDefinitionMetadata>;
|
|
31
|
+
|
|
32
|
+
/// Map from local name to export name.
|
|
33
|
+
type ExportBindingMap = HashMap<String, String>;
|
|
34
|
+
|
|
35
|
+
/// Collects metadata about GQL definitions in a module.
|
|
36
|
+
pub struct MetadataCollector {
|
|
37
|
+
#[allow(dead_code)]
|
|
38
|
+
source_path: String,
|
|
39
|
+
export_bindings: ExportBindingMap,
|
|
40
|
+
scope_stack: Vec<ScopeFrame>,
|
|
41
|
+
metadata: MetadataMap,
|
|
42
|
+
anonymous_counters: HashMap<String, usize>,
|
|
43
|
+
#[allow(dead_code)]
|
|
44
|
+
definition_counter: usize,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
struct ScopeFrame {
|
|
48
|
+
segment: String,
|
|
49
|
+
#[allow(dead_code)]
|
|
50
|
+
kind: String,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
impl MetadataCollector {
|
|
54
|
+
/// Collect metadata from a module.
|
|
55
|
+
pub fn collect(module: &Module, source_path: &str) -> MetadataMap {
|
|
56
|
+
let export_bindings = Self::collect_export_bindings(module);
|
|
57
|
+
|
|
58
|
+
let mut collector = Self {
|
|
59
|
+
source_path: source_path.to_string(),
|
|
60
|
+
export_bindings,
|
|
61
|
+
scope_stack: Vec::new(),
|
|
62
|
+
metadata: HashMap::new(),
|
|
63
|
+
anonymous_counters: HashMap::new(),
|
|
64
|
+
definition_counter: 0,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
module.visit_with(&mut collector);
|
|
68
|
+
collector.metadata
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Collect export bindings from the module.
|
|
72
|
+
fn collect_export_bindings(module: &Module) -> ExportBindingMap {
|
|
73
|
+
let mut bindings = HashMap::new();
|
|
74
|
+
|
|
75
|
+
for item in &module.body {
|
|
76
|
+
match item {
|
|
77
|
+
// ESM: export { foo }
|
|
78
|
+
ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
|
|
79
|
+
if export.src.is_none() {
|
|
80
|
+
for spec in &export.specifiers {
|
|
81
|
+
if let ExportSpecifier::Named(named) = spec {
|
|
82
|
+
let local = match &named.orig {
|
|
83
|
+
ModuleExportName::Ident(id) => atom_to_string(&id.sym),
|
|
84
|
+
ModuleExportName::Str(s) => wtf8_to_string(&s.value),
|
|
85
|
+
};
|
|
86
|
+
let exported = match &named.exported {
|
|
87
|
+
Some(ModuleExportName::Ident(id)) => atom_to_string(&id.sym),
|
|
88
|
+
Some(ModuleExportName::Str(s)) => wtf8_to_string(&s.value),
|
|
89
|
+
None => local.clone(),
|
|
90
|
+
};
|
|
91
|
+
bindings.insert(local, exported);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ESM: export const foo = ...
|
|
98
|
+
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => {
|
|
99
|
+
if let Decl::Var(var_decl) = &export.decl {
|
|
100
|
+
for decl in &var_decl.decls {
|
|
101
|
+
if let Pat::Ident(ident) = &decl.name {
|
|
102
|
+
let name = atom_to_string(&ident.id.sym);
|
|
103
|
+
bindings.insert(name.clone(), name);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else if let Decl::Fn(fn_decl) = &export.decl {
|
|
107
|
+
let name = atom_to_string(&fn_decl.ident.sym);
|
|
108
|
+
bindings.insert(name.clone(), name);
|
|
109
|
+
} else if let Decl::Class(class_decl) = &export.decl {
|
|
110
|
+
let name = atom_to_string(&class_decl.ident.sym);
|
|
111
|
+
bindings.insert(name.clone(), name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// CommonJS: exports.foo = ... or module.exports.foo = ...
|
|
116
|
+
ModuleItem::Stmt(Stmt::Expr(expr_stmt)) => {
|
|
117
|
+
if let Expr::Assign(assign) = &*expr_stmt.expr {
|
|
118
|
+
if let Some(name) = get_commonjs_export_name(&assign.left) {
|
|
119
|
+
bindings.insert(name.clone(), name);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_ => {}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
bindings
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Get the current AST path.
|
|
132
|
+
fn get_ast_path(&self) -> String {
|
|
133
|
+
self.scope_stack
|
|
134
|
+
.iter()
|
|
135
|
+
.map(|f| f.segment.clone())
|
|
136
|
+
.collect::<Vec<_>>()
|
|
137
|
+
.join(".")
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Get an anonymous name for a scope kind (not currently used but kept for future).
|
|
141
|
+
#[allow(dead_code)]
|
|
142
|
+
fn get_anonymous_name(&mut self, kind: &str) -> String {
|
|
143
|
+
let count = self.anonymous_counters.entry(kind.to_string()).or_insert(0);
|
|
144
|
+
let name = format!("{}#{}", kind, count);
|
|
145
|
+
*count += 1;
|
|
146
|
+
name
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// Register a definition and get its AST path.
|
|
150
|
+
/// The AST path is the scope segments joined by `.`, with `$N` suffix for duplicates.
|
|
151
|
+
fn register_definition(&mut self) -> String {
|
|
152
|
+
let base_path = self.get_ast_path();
|
|
153
|
+
|
|
154
|
+
// Track occurrences for uniqueness
|
|
155
|
+
let count = self.anonymous_counters.entry(base_path.clone()).or_insert(0);
|
|
156
|
+
let path = if *count == 0 {
|
|
157
|
+
base_path.clone()
|
|
158
|
+
} else {
|
|
159
|
+
format!("{}${}", base_path, count)
|
|
160
|
+
};
|
|
161
|
+
*count += 1;
|
|
162
|
+
path
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Enter a scope.
|
|
166
|
+
fn enter_scope(&mut self, segment: String, kind: &str) {
|
|
167
|
+
self.scope_stack.push(ScopeFrame {
|
|
168
|
+
segment,
|
|
169
|
+
kind: kind.to_string(),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Exit a scope.
|
|
174
|
+
fn exit_scope(&mut self) {
|
|
175
|
+
self.scope_stack.pop();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// Check if a call expression is a GQL definition call.
|
|
179
|
+
fn is_gql_definition_call(&self, call: &CallExpr) -> bool {
|
|
180
|
+
// Check if callee is gql.* pattern
|
|
181
|
+
if let Callee::Expr(expr) = &call.callee {
|
|
182
|
+
if let Expr::Member(member) = &**expr {
|
|
183
|
+
if is_gql_reference(&member.obj) {
|
|
184
|
+
// Check if first argument is an arrow function or function expression
|
|
185
|
+
if let Some(first_arg) = call.args.first() {
|
|
186
|
+
return matches!(&*first_arg.expr, Expr::Arrow(_) | Expr::Fn(_));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
false
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// Resolve top-level export info for a call.
|
|
195
|
+
fn resolve_export_info(&self, _call: &CallExpr) -> Option<String> {
|
|
196
|
+
// This is a simplified version - in practice, you'd need to track
|
|
197
|
+
// parent nodes to find the variable declaration or assignment
|
|
198
|
+
// For now, we'll look at the scope stack
|
|
199
|
+
if self.scope_stack.len() == 1 {
|
|
200
|
+
let binding_name = &self.scope_stack[0].segment;
|
|
201
|
+
self.export_bindings.get(binding_name).cloned()
|
|
202
|
+
} else {
|
|
203
|
+
None
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
impl Visit for MetadataCollector {
|
|
209
|
+
fn visit_var_declarator(&mut self, decl: &VarDeclarator) {
|
|
210
|
+
if let Pat::Ident(ident) = &decl.name {
|
|
211
|
+
let name = atom_to_string(&ident.id.sym);
|
|
212
|
+
self.enter_scope(name, "variable");
|
|
213
|
+
decl.visit_children_with(self);
|
|
214
|
+
self.exit_scope();
|
|
215
|
+
} else {
|
|
216
|
+
decl.visit_children_with(self);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fn visit_fn_decl(&mut self, decl: &FnDecl) {
|
|
221
|
+
let name = atom_to_string(&decl.ident.sym);
|
|
222
|
+
self.enter_scope(name, "function");
|
|
223
|
+
decl.visit_children_with(self);
|
|
224
|
+
self.exit_scope();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fn visit_fn_expr(&mut self, expr: &FnExpr) {
|
|
228
|
+
let name = expr
|
|
229
|
+
.ident
|
|
230
|
+
.as_ref()
|
|
231
|
+
.map(|i| atom_to_string(&i.sym))
|
|
232
|
+
.unwrap_or_else(|| self.get_anonymous_name("function"));
|
|
233
|
+
self.enter_scope(name, "function");
|
|
234
|
+
expr.visit_children_with(self);
|
|
235
|
+
self.exit_scope();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fn visit_arrow_expr(&mut self, expr: &ArrowExpr) {
|
|
239
|
+
let name = self.get_anonymous_name("arrow");
|
|
240
|
+
self.enter_scope(name, "function");
|
|
241
|
+
expr.visit_children_with(self);
|
|
242
|
+
self.exit_scope();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
fn visit_class_decl(&mut self, decl: &ClassDecl) {
|
|
246
|
+
let name = atom_to_string(&decl.ident.sym);
|
|
247
|
+
self.enter_scope(name, "class");
|
|
248
|
+
decl.visit_children_with(self);
|
|
249
|
+
self.exit_scope();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fn visit_class_method(&mut self, method: &ClassMethod) {
|
|
253
|
+
if let PropName::Ident(ident) = &method.key {
|
|
254
|
+
let name = atom_to_string(&ident.sym);
|
|
255
|
+
self.enter_scope(name, "method");
|
|
256
|
+
method.visit_children_with(self);
|
|
257
|
+
self.exit_scope();
|
|
258
|
+
} else {
|
|
259
|
+
method.visit_children_with(self);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fn visit_key_value_prop(&mut self, prop: &KeyValueProp) {
|
|
264
|
+
let name = match &prop.key {
|
|
265
|
+
PropName::Ident(ident) => Some(atom_to_string(&ident.sym)),
|
|
266
|
+
PropName::Str(s) => Some(wtf8_to_string(&s.value)),
|
|
267
|
+
_ => None,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
if let Some(name) = name {
|
|
271
|
+
self.enter_scope(name, "property");
|
|
272
|
+
prop.visit_children_with(self);
|
|
273
|
+
self.exit_scope();
|
|
274
|
+
} else {
|
|
275
|
+
prop.visit_children_with(self);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
fn visit_assign_expr(&mut self, expr: &AssignExpr) {
|
|
280
|
+
// Handle CommonJS exports: exports.foo = ...
|
|
281
|
+
if let Some(name) = get_commonjs_export_name(&expr.left) {
|
|
282
|
+
self.enter_scope(name, "variable");
|
|
283
|
+
expr.visit_children_with(self);
|
|
284
|
+
self.exit_scope();
|
|
285
|
+
} else {
|
|
286
|
+
expr.visit_children_with(self);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
fn visit_call_expr(&mut self, call: &CallExpr) {
|
|
291
|
+
if self.is_gql_definition_call(call) {
|
|
292
|
+
let ast_path = self.register_definition();
|
|
293
|
+
let is_top_level = self.scope_stack.len() <= 1;
|
|
294
|
+
let export_binding = self.resolve_export_info(call);
|
|
295
|
+
|
|
296
|
+
self.metadata.insert(
|
|
297
|
+
call.span,
|
|
298
|
+
GqlDefinitionMetadata {
|
|
299
|
+
ast_path,
|
|
300
|
+
is_top_level,
|
|
301
|
+
is_exported: export_binding.is_some(),
|
|
302
|
+
export_binding,
|
|
303
|
+
},
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
// Don't visit children of GQL calls
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
call.visit_children_with(self);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/// Check if an expression is a reference to `gql`.
|
|
315
|
+
fn is_gql_reference(expr: &Expr) -> bool {
|
|
316
|
+
match expr {
|
|
317
|
+
Expr::Ident(ident) => atom_eq(&ident.sym, "gql"),
|
|
318
|
+
Expr::Member(member) => {
|
|
319
|
+
if let MemberProp::Ident(ident) = &member.prop {
|
|
320
|
+
if atom_eq(&ident.sym, "gql") {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
is_gql_reference(&member.obj)
|
|
325
|
+
}
|
|
326
|
+
_ => false,
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/// Get the export name from a CommonJS export pattern.
|
|
331
|
+
fn get_commonjs_export_name(target: &AssignTarget) -> Option<String> {
|
|
332
|
+
match target {
|
|
333
|
+
AssignTarget::Simple(SimpleAssignTarget::Member(member)) => {
|
|
334
|
+
// Check for exports.foo or module.exports.foo
|
|
335
|
+
let is_exports = matches!(&*member.obj, Expr::Ident(ident) if atom_eq(&ident.sym, "exports"));
|
|
336
|
+
let is_module_exports = if let Expr::Member(inner) = &*member.obj {
|
|
337
|
+
matches!(&*inner.obj, Expr::Ident(ident) if atom_eq(&ident.sym, "module"))
|
|
338
|
+
&& matches!(&inner.prop, MemberProp::Ident(ident) if atom_eq(&ident.sym, "exports"))
|
|
339
|
+
} else {
|
|
340
|
+
false
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
if !is_exports && !is_module_exports {
|
|
344
|
+
return None;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Extract property name
|
|
348
|
+
if let MemberProp::Ident(ident) = &member.prop {
|
|
349
|
+
Some(atom_to_string(&ident.sym))
|
|
350
|
+
} else {
|
|
351
|
+
None
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
_ => None,
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/// Helper to compare an Atom with a string.
|
|
359
|
+
fn atom_eq<T: AsRef<str>>(atom: &T, s: &str) -> bool {
|
|
360
|
+
atom.as_ref() == s
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/// Helper to convert an Atom to String.
|
|
364
|
+
fn atom_to_string<T: AsRef<str>>(atom: &T) -> String {
|
|
365
|
+
atom.as_ref().to_string()
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/// Helper to convert a Wtf8Atom (string literal value) to String.
|
|
369
|
+
fn wtf8_to_string(atom: &swc_core::atoms::Wtf8Atom) -> String {
|
|
370
|
+
atom.to_string_lossy().into_owned()
|
|
371
|
+
}
|