@arcmantle/lit-jsx 1.0.6 → 1.0.8

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.
Files changed (55) hide show
  1. package/README.md +22 -16
  2. package/dist/compiler/babel-plugin.d.ts +4 -0
  3. package/dist/compiler/babel-plugin.d.ts.map +1 -0
  4. package/dist/compiler/babel-plugin.js +20 -0
  5. package/dist/compiler/babel-plugin.js.map +1 -0
  6. package/dist/compiler/babel-traverse.d.ts +3 -0
  7. package/dist/compiler/babel-traverse.d.ts.map +1 -0
  8. package/dist/compiler/babel-traverse.js +7 -0
  9. package/dist/compiler/babel-traverse.js.map +1 -0
  10. package/dist/compiler/compiler-utils.d.ts +3 -2
  11. package/dist/compiler/compiler-utils.d.ts.map +1 -1
  12. package/dist/compiler/compiler-utils.js +25 -23
  13. package/dist/compiler/compiler-utils.js.map +1 -1
  14. package/dist/compiler/config.d.ts +5 -1
  15. package/dist/compiler/config.d.ts.map +1 -1
  16. package/dist/compiler/config.js +2 -2
  17. package/dist/compiler/config.js.map +1 -1
  18. package/dist/compiler/create-logger.d.ts +3 -0
  19. package/dist/compiler/create-logger.d.ts.map +1 -0
  20. package/dist/compiler/create-logger.js +32 -0
  21. package/dist/compiler/create-logger.js.map +1 -0
  22. package/dist/compiler/import-discovery.d.ts +43 -4
  23. package/dist/compiler/import-discovery.d.ts.map +1 -1
  24. package/dist/compiler/import-discovery.js +306 -160
  25. package/dist/compiler/import-discovery.js.map +1 -1
  26. package/dist/compiler/preprocess.d.ts +1 -1
  27. package/dist/compiler/preprocess.d.ts.map +1 -1
  28. package/dist/compiler/preprocess.js +5 -4
  29. package/dist/compiler/preprocess.js.map +1 -1
  30. package/dist/compiler/transpiler.js +4 -4
  31. package/dist/compiler/transpiler.js.map +1 -1
  32. package/dist/compiler/vite-plugin.d.ts +1 -0
  33. package/dist/compiler/vite-plugin.d.ts.map +1 -1
  34. package/dist/compiler/vite-plugin.js +11 -15
  35. package/dist/compiler/vite-plugin.js.map +1 -1
  36. package/dist/runtime/type-helpers.d.ts +24 -36
  37. package/dist/runtime/type-helpers.d.ts.map +1 -1
  38. package/dist/runtime/type-helpers.js +22 -34
  39. package/dist/runtime/type-helpers.js.map +1 -1
  40. package/package.json +6 -3
  41. package/src/compiler/babel-plugin.ts +23 -0
  42. package/src/compiler/babel-traverse.ts +7 -0
  43. package/src/compiler/compiler-utils.ts +35 -25
  44. package/src/compiler/config.ts +7 -2
  45. package/src/compiler/create-logger.ts +35 -0
  46. package/src/compiler/import-discovery.ts +381 -202
  47. package/src/compiler/preprocess.ts +9 -7
  48. package/src/compiler/transpiler.ts +4 -4
  49. package/src/compiler/vite-plugin.ts +18 -24
  50. package/src/runtime/type-helpers.ts +24 -36
  51. package/dist/compiler/babel-preset.d.ts +0 -6
  52. package/dist/compiler/babel-preset.d.ts.map +0 -1
  53. package/dist/compiler/babel-preset.js +0 -27
  54. package/dist/compiler/babel-preset.js.map +0 -1
  55. package/src/compiler/babel-preset.ts +0 -34
@@ -1,281 +1,460 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
- import { dirname, resolve } from 'node:path';
2
+ import { dirname } from 'node:path';
3
3
 
4
4
  import * as babel from '@babel/core';
5
- import traverse, { Binding, Hub, NodePath, Scope } from '@babel/traverse';
5
+ import type { Binding, NodePath, Scope } from '@babel/traverse';
6
6
  import * as t from '@babel/types';
7
+ import oxcResolver from 'oxc-resolver';
8
+ import type { Logger } from 'pino';
7
9
 
10
+ import { traverse } from './babel-traverse.ts';
11
+ import { getPathFilename, isComponent } from './compiler-utils.ts';
12
+ import { babelPlugins, debugMode } from './config.ts';
13
+ import { createLogger } from './create-logger.ts';
8
14
 
9
- // Cache for parsed files to avoid re-parsing
10
- const fileCache: Map<string, t.File> = new Map();
11
15
 
12
-
13
- export type BabelPlugins = NonNullable<NonNullable<babel.TransformOptions['parserOpts']>['plugins']>;
14
-
15
-
16
- // Types for our discovery results
17
16
  export interface ElementDefinition {
18
- type: 'custom-element' | 'import' | 'local-variable' | 'unknown';
19
- source?: string; // file path for imports
20
- originalName?: string; // for imports/re-exports
21
- callExpression?: t.CallExpression; // for toJSX calls
17
+ type: 'custom-element' | 'import' | 'local-variable' | 'wildcard-export' | 'unknown';
18
+ source?: string;
19
+ originalName?: string;
20
+ localName?: string;
21
+ callExpression?: t.CallExpression;
22
+ resolvedPath?: string; // For lazy import resolution
23
+ referencedName?: string; // For lazy local reference resolution
22
24
  }
23
25
 
26
+ interface FileSystemAdapter {
27
+ existsSync: (path: string) => boolean;
28
+ readFileSync: (path: string, encoding: 'utf-8') => string;
29
+ dirname: (path: string) => string;
30
+ }
24
31
 
25
- // Helper function to find the definition of a JSX element
26
- export function findElementDefinition(path: NodePath<t.JSXOpeningElement>): ElementDefinition {
27
- const elementName = path.node.name;
32
+ interface ResolverAdapter {
33
+ sync: (basedir: string, module: string) => { path?: string; };
34
+ }
28
35
 
29
- const hub = path.hub as Hub & { file: { opts: { filename: string; }; }; };
30
36
 
31
- const currentFileName = hub.file.opts.filename;
37
+ export class ImportDiscovery {
32
38
 
33
- // Only handle JSXIdentifier (not JSXMemberExpression or JSXNamespacedName)
34
- if (!t.isJSXIdentifier(elementName))
35
- return { type: 'unknown' };
39
+ static readonly definitionCache: Map<string, Map<string, ElementDefinition>> = new Map();
40
+ static readonly fileBindingsCache: Map<string, ReadonlyMap<string, ElementDefinition>> = new Map();
41
+ static readonly fileDependencies: Map<string, Set<string>> = new Map();
42
+ static readonly EMPTY_BINDINGS: ReadonlyMap<string, ElementDefinition> = new Map();
36
43
 
37
- const elementNameString = elementName.name;
44
+ static clearCacheForFileAndDependents(changedFilePath: string): void {
45
+ // Clear the changed file itself
46
+ ImportDiscovery.definitionCache.delete(changedFilePath);
47
+ ImportDiscovery.fileBindingsCache.delete(changedFilePath);
48
+ ImportDiscovery.fileDependencies.delete(changedFilePath);
38
49
 
39
- //console.log('Tracing element:', elementNameString);
50
+ // Find and clear all files that depend on the changed file in one pass
51
+ for (const [ file, dependencies ] of ImportDiscovery.fileDependencies) {
52
+ if (!dependencies.has(changedFilePath))
53
+ continue;
40
54
 
41
- // Start tracing with the current file context
42
- return traceElementDefinition(elementNameString, path.scope, currentFileName);
43
- }
55
+ ImportDiscovery.definitionCache.delete(file);
56
+ ImportDiscovery.fileBindingsCache.delete(file);
57
+ ImportDiscovery.fileDependencies.delete(file);
58
+ }
59
+ }
44
60
 
45
- // Core recursive tracing function
46
- function traceElementDefinition(
47
- elementName: string,
48
- scope: Scope,
49
- currentFileName: string,
50
- visitedFiles: Set<string> = new Set(),
51
- ): ElementDefinition {
52
- // Prevent infinite recursion
53
- if (visitedFiles.has(`${ currentFileName }:${ elementName }`))
54
- return { type: 'unknown' };
61
+ protected readonly visitedFiles: Set<string> = new Set();
62
+ protected readonly resolver: ResolverAdapter;
63
+ protected readonly log: Logger<never, boolean>;
64
+ protected readonly fs: FileSystemAdapter;
55
65
 
56
- visitedFiles.add(`${ currentFileName }:${ elementName }`);
66
+ constructor() {
67
+ this.resolver = oxcResolver;
68
+ this.log = createLogger('import-discovery', debugMode.value);
69
+ this.fs = { existsSync, readFileSync, dirname };
70
+ }
57
71
 
58
- // Check if there's a binding for this identifier in the current scope
59
- const binding = scope.getBinding(elementName);
72
+ /**
73
+ * Finds the definition of a JSX element in the given path.
74
+ */
75
+ findElementDefinition(
76
+ path: NodePath<t.JSXOpeningElement>,
77
+ ): ElementDefinition {
78
+ this.visitedFiles.clear();
79
+
80
+ const filePath = getPathFilename(path);
81
+ const cacheKey = String(path.node.start);
82
+
83
+ const fileCache = ImportDiscovery.definitionCache.get(filePath);
84
+ if (fileCache) {
85
+ const cached = fileCache.get(cacheKey);
86
+ if (cached)
87
+ return cached;
88
+ }
60
89
 
61
- if (!binding) {
62
- //console.log('No binding found for:', elementName);
90
+ if (!t.isJSXIdentifier(path.node.name))
91
+ return { type: 'unknown' };
63
92
 
64
- return { type: 'unknown' };
65
- }
93
+ const elementName = path.node.name.name;
94
+ if (!isComponent(elementName))
95
+ return { type: 'unknown' };
66
96
 
67
- //console.log('Binding kind:', binding.kind, 'type:', binding.path.type);
97
+ const currentFileName = getPathFilename(path);
98
+ const result = this.traceElementDefinition(elementName, path.scope, currentFileName);
68
99
 
69
- // Handle imports
70
- if (binding.kind === 'module' && t.isImportSpecifier(binding.path.node))
71
- return traceImport(binding, currentFileName, elementName, visitedFiles);
100
+ // Store in file-specific cache
101
+ const definitionCache = ImportDiscovery.definitionCache.get(filePath)
102
+ ?? ImportDiscovery.definitionCache
103
+ .set(filePath, new Map())
104
+ .get(filePath)!;
72
105
 
106
+ definitionCache.set(cacheKey, result);
73
107
 
74
- // Handle local variables/constants
75
- if (binding.kind === 'const' || binding.kind === 'let' || binding.kind === 'var')
76
- return traceLocalVariable(binding, currentFileName, visitedFiles);
108
+ return result;
109
+ }
77
110
 
111
+ // Trace the element definition recursively
112
+ protected traceElementDefinition(
113
+ elementName: string,
114
+ scope: Scope,
115
+ currentFileName: string,
116
+ ): ElementDefinition {
117
+ const traceKey = `${ currentFileName }:${ elementName }`;
78
118
 
79
- return { type: 'unknown' };
80
- }
119
+ // Prevent infinite recursion
120
+ if (this.visitedFiles.has(traceKey))
121
+ return { type: 'unknown' };
81
122
 
82
- function traceImport(
83
- binding: Binding,
84
- currentFileName: string,
85
- elementName: string,
86
- visitedFiles: Set<string>,
87
- ): ElementDefinition {
88
- const importDeclaration = binding.path.parent;
123
+ this.visitedFiles.add(traceKey);
89
124
 
90
- if (!t.isImportDeclaration(importDeclaration))
91
- return { type: 'unknown' };
125
+ // Use batched file analysis
126
+ const fileBindings = this.analyzeFileBindings(currentFileName);
92
127
 
93
- const importSource = importDeclaration.source.value;
94
- const currentDir = dirname(currentFileName);
95
- const resolvedPath = resolve(currentDir, importSource);
128
+ // Check if we have this element in our batch analysis
129
+ if (fileBindings.has(elementName)) {
130
+ const definition = fileBindings.get(elementName)!;
96
131
 
97
- //console.log('Tracing import from:', importSource);
98
- //console.log('Resolved to:', resolvedPath);
132
+ // Resolve any lazy references
133
+ return this.resolveLazyDefinition(definition);
134
+ }
99
135
 
100
- // Use cached parsing
101
- const ast = getOrParseFile(resolvedPath);
102
- if (!ast)
103
- return { type: 'unknown' };
136
+ // Fallback to scope-based lookup for dynamic cases
137
+ const binding = scope.getBinding(elementName);
138
+ if (!binding)
139
+ return { type: 'unknown' };
104
140
 
141
+ // Use the fast analysis methods
142
+ const result = this.analyzeBindingFast(binding, currentFileName);
105
143
 
106
- let result: ElementDefinition = { type: 'unknown' };
144
+ // Resolve any lazy references (imports, local references)
145
+ return this.resolveLazyDefinition(result);
146
+ }
107
147
 
108
- traverse(ast, {
109
- Program(programPath) {
110
- // First try to find a local binding (normal export)
111
- const localBinding = programPath.scope.getBinding(elementName);
148
+ // Analyze all relevant bindings in a file at once
149
+ protected analyzeFileBindings(filePath: string): ReadonlyMap<string, ElementDefinition> {
150
+ const fileBinding = ImportDiscovery.fileBindingsCache.get(filePath);
151
+ if (fileBinding)
152
+ return fileBinding;
112
153
 
113
- if (localBinding) {
114
- //console.log('Found local binding in imported file');
115
- result = traceElementDefinition(elementName, programPath.scope, resolvedPath, visitedFiles);
154
+ if (!this.fs.existsSync(filePath)) {
155
+ ImportDiscovery.fileBindingsCache.set(filePath, ImportDiscovery.EMPTY_BINDINGS);
116
156
 
117
- return;
118
- }
157
+ return ImportDiscovery.EMPTY_BINDINGS;
158
+ }
119
159
 
120
- // If no local binding found, check for re-exports
121
- //console.log('No local binding, checking for re-exports...');
122
- result = checkForReExports(programPath, elementName, resolvedPath, visitedFiles);
123
- },
124
- });
160
+ const fileContent = this.fs.readFileSync(filePath, 'utf-8');
161
+ let ast: t.File;
125
162
 
126
- return result;
127
- }
163
+ try {
164
+ ast = babel.parseSync(fileContent, {
165
+ filename: filePath,
166
+ parserOpts: {
167
+ plugins: babelPlugins,
168
+ },
169
+ })!;
170
+ }
171
+ catch (error) {
172
+ // Failed to parse, cache empty result
173
+ ImportDiscovery.fileBindingsCache.set(filePath, ImportDiscovery.EMPTY_BINDINGS);
128
174
 
129
- function traceLocalVariable(
130
- binding: Binding,
131
- currentFileName: string,
132
- visitedFiles: Set<string>,
133
- ): ElementDefinition {
134
- //console.log('Tracing local variable:', binding.kind, binding.path.type);
175
+ return ImportDiscovery.EMPTY_BINDINGS;
176
+ }
135
177
 
136
- // Check if it's a variable declarator (const/let/var)
137
- if (t.isVariableDeclarator(binding.path.node)) {
138
- const declarator = binding.path.node;
178
+ let programPath: babel.NodePath<babel.types.Program> = undefined as any;
179
+ traverse(ast, { Program(path) { programPath = path; path.stop(); } });
139
180
 
140
- // Check if it's assigned to a call expression
141
- if (declarator.init && t.isCallExpression(declarator.init)) {
142
- const callExpr = declarator.init;
181
+ const bindings: Map<string, ElementDefinition> = new Map();
143
182
 
144
- // Check if it's a toJSX call
145
- if (t.isIdentifier(callExpr.callee) && callExpr.callee.name === 'toJSX') {
146
- return {
147
- type: 'custom-element',
148
- source: currentFileName,
149
- callExpression: callExpr,
150
- };
151
- }
183
+ // 1. Analyze all relevant local bindings at once
184
+ this.analyzeFileDeclarations(programPath, filePath, bindings);
152
185
 
153
- // Could be assigned to another function call - trace that too
154
- //console.log('Local variable assigned to call expression:', callExpr.callee);
155
- }
186
+ // 2. Analyze all exports at once
187
+ this.analyzeFileExports(programPath, filePath, bindings);
156
188
 
157
- // Check if it's assigned to an identifier (another variable)
158
- if (declarator.init && t.isIdentifier(declarator.init)) {
159
- const assignedIdentifier = declarator.init.name;
160
- //console.log('Local variable assigned to identifier:', assignedIdentifier);
189
+ const readonlyBindings = new Map(bindings) as ReadonlyMap<string, ElementDefinition>;
190
+ ImportDiscovery.fileBindingsCache.set(filePath, readonlyBindings);
161
191
 
162
- // Recursively trace this identifier in the same scope
163
- return traceElementDefinition(assignedIdentifier, binding.path.scope, currentFileName, visitedFiles);
164
- }
192
+ return readonlyBindings;
165
193
  }
166
194
 
167
- return { type: 'local-variable' };
168
- }
195
+ // Resolve lazy references in the definition
196
+ protected analyzeFileDeclarations(
197
+ programPath: babel.NodePath<babel.types.Program>,
198
+ filePath: string,
199
+ bindings: Map<string, ElementDefinition>,
200
+ ): void {
201
+ for (const [ name, binding ] of Object.entries(programPath.scope.bindings)) {
202
+ // Skip function/import bindings that are clearly not component-related
203
+ if (binding.kind === 'module' || binding.kind === 'hoisted') {
204
+ if (!isComponent(name))
205
+ continue;
206
+ }
169
207
 
170
- function checkForReExports(
171
- programPath: babel.NodePath<babel.types.Program>,
172
- elementName: string,
173
- currentFileName: string,
174
- visitedFiles: Set<string>,
175
- ): ElementDefinition {
176
- let result: ElementDefinition = { type: 'unknown' };
208
+ const definition = this.analyzeBindingFast(binding, filePath);
209
+ if (definition.type !== 'unknown')
210
+ bindings.set(name, definition);
211
+ }
212
+ }
177
213
 
178
- // Check all export declarations for re-exports
179
- programPath.traverse({
180
- ExportNamedDeclaration(exportPath) {
181
- const node = exportPath.node;
214
+ // Fast binding analysis without deep traversal
215
+ protected analyzeBindingFast(
216
+ binding: Binding,
217
+ filePath: string,
218
+ ): ElementDefinition {
219
+ // Handle imports
220
+ if (binding.kind === 'module' && t.isImportSpecifier(binding.path.node))
221
+ return this.analyzeImportBinding(binding, filePath);
182
222
 
183
- if (!node.source || !node.specifiers)
184
- return; // Skip if no source or specifiers
223
+ // Handle local variables
224
+ if (binding.kind === 'const' || binding.kind === 'let' || binding.kind === 'var')
225
+ return this.analyzeLocalBinding(binding, filePath);
185
226
 
186
- // Handle re-exports: export { X } from './file'
187
- //console.log('Found re-export from:', node.source.value);
227
+ return { type: 'unknown' };
228
+ }
188
229
 
189
- for (const specifier of node.specifiers) {
190
- if (!t.isExportSpecifier(specifier))
191
- continue; // Only handle export specifiers
230
+ // Analyze import without deep file traversal
231
+ protected analyzeImportBinding(binding: Binding, currentFileName: string): ElementDefinition {
232
+ const importDeclaration = binding.path.parent;
233
+ if (!t.isImportDeclaration(importDeclaration))
234
+ return { type: 'unknown' };
235
+
236
+ const importSpecifier = binding.path.node;
237
+ if (!t.isImportSpecifier(importSpecifier))
238
+ return { type: 'unknown' };
239
+
240
+ const originalExportedName = t.isIdentifier(importSpecifier.imported)
241
+ ? importSpecifier.imported.name
242
+ : importSpecifier.imported.value;
243
+
244
+ const importSource = importDeclaration.source.value;
245
+ const currentDir = dirname(currentFileName);
246
+
247
+ const resolvedResult = this.resolver.sync(currentDir, importSource);
248
+ const resolvedPath = resolvedResult.path;
249
+
250
+ if (!resolvedPath)
251
+ return { type: 'unknown' };
252
+
253
+ // Instead of deep traversal, just mark as import and resolve lazily
254
+ return {
255
+ type: 'import',
256
+ source: importSource,
257
+ originalName: originalExportedName,
258
+ localName: binding.identifier.name,
259
+ // Store resolved path for later lookup
260
+ resolvedPath: resolvedPath,
261
+ };
262
+ }
192
263
 
193
- const exportedName = t.isIdentifier(specifier.exported)
194
- ? specifier.exported.name
195
- : specifier.exported.value;
264
+ // Analyze local binding without recursion
265
+ protected analyzeLocalBinding(binding: Binding, filePath: string): ElementDefinition {
266
+ if (!t.isVariableDeclarator(binding.path.node))
267
+ return { type: 'local-variable' };
196
268
 
197
- // Check if this re-export matches our element name
198
- if (exportedName !== elementName)
199
- continue;
200
269
 
201
- const originalName = specifier.local.name;
202
- //console.log(`Found re-export: ${ originalName } as ${ exportedName }`);
270
+ const declarator = binding.path.node;
271
+ if (!declarator.init)
272
+ return { type: 'local-variable' };
203
273
 
204
- // Resolve and trace the re-exported file
205
- const reExportSource = node.source.value;
206
- const currentDir = dirname(currentFileName);
207
- const resolvedPath = resolve(currentDir, reExportSource);
208
274
 
209
- if (!existsSync(resolvedPath))
210
- continue; // Skip if file doesn't exist
275
+ // Check for toComponent/toTag calls
276
+ if (t.isCallExpression(declarator.init)) {
277
+ if (this.isToComponentOrTagCall(declarator.init, binding.path.scope)) {
278
+ return {
279
+ type: 'custom-element',
280
+ source: filePath,
281
+ callExpression: declarator.init,
282
+ };
283
+ }
284
+ }
211
285
 
212
- //console.log('Tracing re-export to:', resolvedPath);
213
- result = traceReExport(originalName, resolvedPath, visitedFiles);
286
+ // For identifier assignments, store reference for later resolution
287
+ if (t.isIdentifier(declarator.init)) {
288
+ return {
289
+ type: 'local-variable',
290
+ source: filePath,
291
+ referencedName: declarator.init.name,
292
+ };
293
+ }
214
294
 
215
- // Stop traversing once we find the match
216
- return exportPath.stop();
217
- }
218
- },
219
- });
295
+ return { type: 'local-variable' };
296
+ }
220
297
 
221
- return result;
222
- }
298
+ // Helper function to check if a call expression is a toComponent/toTag call
299
+ // even if the function has been renamed through imports
300
+ protected isToComponentOrTagCall(
301
+ callExpr: t.CallExpression,
302
+ scope: Scope,
303
+ ): boolean {
304
+ if (!t.isIdentifier(callExpr.callee))
305
+ return false;
223
306
 
224
- function traceReExport(
225
- elementName: string,
226
- filePath: string,
227
- visitedFiles: Set<string>,
228
- ): ElementDefinition {
229
- // Use cached parsing
230
- const ast = getOrParseFile(filePath);
231
- if (!ast)
232
- return { type: 'unknown' };
307
+ const functionName = callExpr.callee.name;
233
308
 
309
+ // Check direct names first (fast path)
310
+ if (functionName === 'toComponent' || functionName === 'toTag')
311
+ return true;
234
312
 
235
- let result: ElementDefinition = { type: 'unknown' };
313
+ // Check if this identifier is bound to an import that originally was toComponent/toTag
314
+ const binding = scope.getBinding(functionName);
315
+ if (!binding || binding.kind !== 'module')
316
+ return false;
236
317
 
237
- traverse(ast, {
238
- Program(programPath) {
239
- // Continue tracing in the re-exported file
240
- result = traceElementDefinition(elementName, programPath.scope, filePath, visitedFiles);
241
- },
242
- });
318
+ if (!t.isImportSpecifier(binding.path.node))
319
+ return false;
243
320
 
244
- return result;
245
- }
321
+ const importSpecifier = binding.path.node;
322
+ const originalImportedName = t.isIdentifier(importSpecifier.imported)
323
+ ? importSpecifier.imported.name
324
+ : importSpecifier.imported.value;
246
325
 
326
+ // Check if the original imported name was toComponent or toTag
327
+ const isOriginallyToComponentOrTag =
328
+ originalImportedName === 'toComponent'
329
+ || originalImportedName === 'toTag';
247
330
 
248
- // Helper function to get or parse a file with caching
249
- function getOrParseFile(filePath: string): t.File | undefined {
250
- // Check cache first
251
- if (fileCache.has(filePath)) {
252
- //console.log('Using cached AST for:', filePath);
331
+ return isOriginallyToComponentOrTag;
332
+ }
253
333
 
254
- return fileCache.get(filePath)!;
334
+ // Analyze all exports in one pass
335
+ protected analyzeFileExports(
336
+ programPath: babel.NodePath<babel.types.Program>,
337
+ filePath: string,
338
+ bindings: Map<string, ElementDefinition>,
339
+ ): void {
340
+ for (const statement of programPath.node.body) {
341
+ // Handle named exports: export { X } from './file' or export { X }
342
+ if (t.isExportNamedDeclaration(statement)) {
343
+ for (const specifier of statement.specifiers) {
344
+ if (!t.isExportSpecifier(specifier))
345
+ continue;
346
+
347
+ const exportedName = t.isIdentifier(specifier.exported)
348
+ ? specifier.exported.name
349
+ : specifier.exported.value;
350
+
351
+ const localName = specifier.local.name;
352
+
353
+ if (!isComponent(exportedName))
354
+ continue;
355
+
356
+ // For re-exports with source
357
+ if (statement.source) {
358
+ const definition = {
359
+ type: 'import' as const,
360
+ source: statement.source.value,
361
+ originalName: localName,
362
+ localName: exportedName,
363
+ };
364
+ bindings.set(exportedName, definition);
365
+ }
366
+ else {
367
+ // For local exports, reference the local binding
368
+ const definition = {
369
+ type: 'local-variable' as const,
370
+ source: filePath,
371
+ referencedName: localName,
372
+ localName: exportedName,
373
+ };
374
+ bindings.set(exportedName, definition);
375
+ }
376
+ }
377
+ }
378
+ // Handle wildcard exports: export * from './file'
379
+ else if (t.isExportAllDeclaration(statement) && statement.source) {
380
+ // For wildcard exports, we need to mark this as a wildcard re-export
381
+ // The actual resolution will happen in resolveLazyDefinition
382
+ const definition = {
383
+ type: 'wildcard-export' as const,
384
+ source: statement.source.value,
385
+ };
386
+ // Store with a special key to indicate wildcard export
387
+ bindings.set('*', definition);
388
+ }
389
+ }
255
390
  }
256
391
 
257
- // File not in cache, parse it
258
- if (!existsSync(filePath))
259
- return;
392
+ // Resolve lazy definitions (imports, references)
393
+ protected resolveLazyDefinition(definition: ElementDefinition): ElementDefinition {
394
+ if (definition.type === 'import' && definition.resolvedPath && definition.originalName) {
395
+ if (definition.source) {
396
+ const currentFile = definition.source;
397
+ const dependsOn = definition.resolvedPath;
260
398
 
261
- const fileContent = readFileSync(filePath, 'utf-8');
399
+ let fileDependencies = ImportDiscovery.fileDependencies.get(currentFile);
400
+ if (!fileDependencies) {
401
+ fileDependencies = new Set<string>();
402
+ ImportDiscovery.fileDependencies.set(currentFile, fileDependencies);
403
+ }
262
404
 
263
- try {
264
- const ast = babel.parseSync(fileContent, {
265
- filename: filePath,
266
- parserOpts: {
267
- plugins: [ 'jsx', 'typescript' ] satisfies BabelPlugins,
268
- },
269
- });
405
+ fileDependencies.add(dependsOn);
406
+ }
270
407
 
271
- if (ast) {
272
- //console.log('Parsed and cached:', filePath);
273
- fileCache.set(filePath, ast);
408
+ // Recursively analyze the imported file
409
+ const importedBindings = this.analyzeFileBindings(definition.resolvedPath);
410
+ const binding = importedBindings.get(definition.originalName);
411
+
412
+ if (binding) // Recursively resolve the found definition
413
+ return this.resolveLazyDefinition(binding);
414
+
415
+ // If specific export not found, check for wildcard exports
416
+ const wildcardExport = importedBindings.get('*');
417
+ if (wildcardExport && wildcardExport.type === 'wildcard-export') {
418
+ // Resolve the wildcard export by looking in the target file
419
+ const currentDir = this.fs.dirname(definition.resolvedPath);
420
+ const resolvedResult = this.resolver.sync(currentDir, wildcardExport.source!);
421
+ const resolvedPath = resolvedResult.path;
422
+
423
+ if (resolvedPath) {
424
+ // Create a new import definition for the wildcard target
425
+ const wildcardTargetDefinition: ElementDefinition = {
426
+ type: 'import',
427
+ source: wildcardExport.source,
428
+ originalName: definition.originalName,
429
+ localName: definition.localName,
430
+ resolvedPath: resolvedPath,
431
+ };
432
+
433
+ return this.resolveLazyDefinition(wildcardTargetDefinition);
434
+ }
435
+ }
436
+ }
437
+
438
+ if (definition.type === 'local-variable' && definition.referencedName && definition.source) {
439
+ // Resolve local references
440
+ const fileBindings = this.analyzeFileBindings(definition.source);
441
+ const binding = fileBindings.get(definition.referencedName);
274
442
 
275
- return ast;
443
+ if (binding)
444
+ return this.resolveLazyDefinition(binding);
276
445
  }
446
+
447
+ return definition;
277
448
  }
278
- catch (error) {
279
- console.log('Failed to parse file:', filePath, error);
280
- }
449
+
281
450
  }
451
+
452
+
453
+ let discovery: ImportDiscovery;
454
+ export const findElementDefinition = (
455
+ ...args: Parameters<ImportDiscovery['findElementDefinition']>
456
+ ): ReturnType<ImportDiscovery['findElementDefinition']> => {
457
+ discovery ??= new ImportDiscovery();
458
+
459
+ return discovery.findElementDefinition(...args);
460
+ };