@arcmantle/lit-jsx 1.0.5 → 1.0.7
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 +22 -16
- package/dist/compiler/babel-plugin.d.ts +4 -0
- package/dist/compiler/babel-plugin.d.ts.map +1 -0
- package/dist/compiler/babel-plugin.js +20 -0
- package/dist/compiler/babel-plugin.js.map +1 -0
- package/dist/compiler/babel-traverse.d.ts +3 -0
- package/dist/compiler/babel-traverse.d.ts.map +1 -0
- package/dist/compiler/babel-traverse.js +7 -0
- package/dist/compiler/babel-traverse.js.map +1 -0
- package/dist/compiler/compiler-utils.d.ts +3 -2
- package/dist/compiler/compiler-utils.d.ts.map +1 -1
- package/dist/compiler/compiler-utils.js +25 -23
- package/dist/compiler/compiler-utils.js.map +1 -1
- package/dist/compiler/config.d.ts +5 -1
- package/dist/compiler/config.d.ts.map +1 -1
- package/dist/compiler/config.js +2 -2
- package/dist/compiler/config.js.map +1 -1
- package/dist/compiler/create-logger.d.ts +3 -0
- package/dist/compiler/create-logger.d.ts.map +1 -0
- package/dist/compiler/create-logger.js +32 -0
- package/dist/compiler/create-logger.js.map +1 -0
- package/dist/compiler/import-discovery.d.ts +51 -0
- package/dist/compiler/import-discovery.d.ts.map +1 -0
- package/dist/compiler/import-discovery.js +325 -0
- package/dist/compiler/import-discovery.js.map +1 -0
- package/dist/compiler/preprocess.d.ts +1 -1
- package/dist/compiler/preprocess.d.ts.map +1 -1
- package/dist/compiler/preprocess.js +5 -4
- package/dist/compiler/preprocess.js.map +1 -1
- package/dist/compiler/transpiler.js +4 -4
- package/dist/compiler/transpiler.js.map +1 -1
- package/dist/compiler/vite-plugin.d.ts +1 -0
- package/dist/compiler/vite-plugin.d.ts.map +1 -1
- package/dist/compiler/vite-plugin.js +9 -13
- package/dist/compiler/vite-plugin.js.map +1 -1
- package/dist/runtime/type-helpers.d.ts +24 -36
- package/dist/runtime/type-helpers.d.ts.map +1 -1
- package/dist/runtime/type-helpers.js +22 -34
- package/dist/runtime/type-helpers.js.map +1 -1
- package/package.json +5 -2
- package/src/compiler/babel-plugin.ts +23 -0
- package/src/compiler/babel-traverse.ts +7 -0
- package/src/compiler/compiler-utils.ts +35 -25
- package/src/compiler/config.ts +7 -2
- package/src/compiler/create-logger.ts +35 -0
- package/src/compiler/import-discovery.ts +460 -0
- package/src/compiler/preprocess.ts +9 -7
- package/src/compiler/transpiler.ts +4 -4
- package/src/compiler/vite-plugin.ts +16 -22
- package/src/runtime/type-helpers.ts +24 -36
- package/dist/compiler/babel-preset.d.ts +0 -6
- package/dist/compiler/babel-preset.d.ts.map +0 -1
- package/dist/compiler/babel-preset.js +0 -27
- package/dist/compiler/babel-preset.js.map +0 -1
- package/src/compiler/babel-preset.ts +0 -34
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import * as babel from '@babel/core';
|
|
5
|
+
import type { Binding, NodePath, Scope } from '@babel/traverse';
|
|
6
|
+
import * as t from '@babel/types';
|
|
7
|
+
import oxcResolver from 'oxc-resolver';
|
|
8
|
+
import type { Logger } from 'pino';
|
|
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';
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export interface ElementDefinition {
|
|
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
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface FileSystemAdapter {
|
|
27
|
+
existsSync: (path: string) => boolean;
|
|
28
|
+
readFileSync: (path: string, encoding: 'utf-8') => string;
|
|
29
|
+
dirname: (path: string) => string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ResolverAdapter {
|
|
33
|
+
sync: (basedir: string, module: string) => { path?: string; };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
export class ImportDiscovery {
|
|
38
|
+
|
|
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();
|
|
43
|
+
|
|
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);
|
|
49
|
+
|
|
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;
|
|
54
|
+
|
|
55
|
+
ImportDiscovery.definitionCache.delete(file);
|
|
56
|
+
ImportDiscovery.fileBindingsCache.delete(file);
|
|
57
|
+
ImportDiscovery.fileDependencies.delete(file);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
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;
|
|
65
|
+
|
|
66
|
+
constructor() {
|
|
67
|
+
this.resolver = oxcResolver;
|
|
68
|
+
this.log = createLogger('import-discovery', debugMode.value);
|
|
69
|
+
this.fs = { existsSync, readFileSync, dirname };
|
|
70
|
+
}
|
|
71
|
+
|
|
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
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!t.isJSXIdentifier(path.node.name))
|
|
91
|
+
return { type: 'unknown' };
|
|
92
|
+
|
|
93
|
+
const elementName = path.node.name.name;
|
|
94
|
+
if (!isComponent(elementName))
|
|
95
|
+
return { type: 'unknown' };
|
|
96
|
+
|
|
97
|
+
const currentFileName = getPathFilename(path);
|
|
98
|
+
const result = this.traceElementDefinition(elementName, path.scope, currentFileName);
|
|
99
|
+
|
|
100
|
+
// Store in file-specific cache
|
|
101
|
+
const definitionCache = ImportDiscovery.definitionCache.get(filePath)
|
|
102
|
+
?? ImportDiscovery.definitionCache
|
|
103
|
+
.set(filePath, new Map())
|
|
104
|
+
.get(filePath)!;
|
|
105
|
+
|
|
106
|
+
definitionCache.set(cacheKey, result);
|
|
107
|
+
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
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 }`;
|
|
118
|
+
|
|
119
|
+
// Prevent infinite recursion
|
|
120
|
+
if (this.visitedFiles.has(traceKey))
|
|
121
|
+
return { type: 'unknown' };
|
|
122
|
+
|
|
123
|
+
this.visitedFiles.add(traceKey);
|
|
124
|
+
|
|
125
|
+
// Use batched file analysis
|
|
126
|
+
const fileBindings = this.analyzeFileBindings(currentFileName);
|
|
127
|
+
|
|
128
|
+
// Check if we have this element in our batch analysis
|
|
129
|
+
if (fileBindings.has(elementName)) {
|
|
130
|
+
const definition = fileBindings.get(elementName)!;
|
|
131
|
+
|
|
132
|
+
// Resolve any lazy references
|
|
133
|
+
return this.resolveLazyDefinition(definition);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Fallback to scope-based lookup for dynamic cases
|
|
137
|
+
const binding = scope.getBinding(elementName);
|
|
138
|
+
if (!binding)
|
|
139
|
+
return { type: 'unknown' };
|
|
140
|
+
|
|
141
|
+
// Use the fast analysis methods
|
|
142
|
+
const result = this.analyzeBindingFast(binding, currentFileName);
|
|
143
|
+
|
|
144
|
+
// Resolve any lazy references (imports, local references)
|
|
145
|
+
return this.resolveLazyDefinition(result);
|
|
146
|
+
}
|
|
147
|
+
|
|
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;
|
|
153
|
+
|
|
154
|
+
if (!this.fs.existsSync(filePath)) {
|
|
155
|
+
ImportDiscovery.fileBindingsCache.set(filePath, ImportDiscovery.EMPTY_BINDINGS);
|
|
156
|
+
|
|
157
|
+
return ImportDiscovery.EMPTY_BINDINGS;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const fileContent = this.fs.readFileSync(filePath, 'utf-8');
|
|
161
|
+
let ast: t.File;
|
|
162
|
+
|
|
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);
|
|
174
|
+
|
|
175
|
+
return ImportDiscovery.EMPTY_BINDINGS;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let programPath: babel.NodePath<babel.types.Program> = undefined as any;
|
|
179
|
+
traverse(ast, { Program(path) { programPath = path; path.stop(); } });
|
|
180
|
+
|
|
181
|
+
const bindings: Map<string, ElementDefinition> = new Map();
|
|
182
|
+
|
|
183
|
+
// 1. Analyze all relevant local bindings at once
|
|
184
|
+
this.analyzeFileDeclarations(programPath, filePath, bindings);
|
|
185
|
+
|
|
186
|
+
// 2. Analyze all exports at once
|
|
187
|
+
this.analyzeFileExports(programPath, filePath, bindings);
|
|
188
|
+
|
|
189
|
+
const readonlyBindings = new Map(bindings) as ReadonlyMap<string, ElementDefinition>;
|
|
190
|
+
ImportDiscovery.fileBindingsCache.set(filePath, readonlyBindings);
|
|
191
|
+
|
|
192
|
+
return readonlyBindings;
|
|
193
|
+
}
|
|
194
|
+
|
|
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
|
+
}
|
|
207
|
+
|
|
208
|
+
const definition = this.analyzeBindingFast(binding, filePath);
|
|
209
|
+
if (definition.type !== 'unknown')
|
|
210
|
+
bindings.set(name, definition);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
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);
|
|
222
|
+
|
|
223
|
+
// Handle local variables
|
|
224
|
+
if (binding.kind === 'const' || binding.kind === 'let' || binding.kind === 'var')
|
|
225
|
+
return this.analyzeLocalBinding(binding, filePath);
|
|
226
|
+
|
|
227
|
+
return { type: 'unknown' };
|
|
228
|
+
}
|
|
229
|
+
|
|
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
|
+
}
|
|
263
|
+
|
|
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' };
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
const declarator = binding.path.node;
|
|
271
|
+
if (!declarator.init)
|
|
272
|
+
return { type: 'local-variable' };
|
|
273
|
+
|
|
274
|
+
|
|
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
|
+
}
|
|
285
|
+
|
|
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
|
+
}
|
|
294
|
+
|
|
295
|
+
return { type: 'local-variable' };
|
|
296
|
+
}
|
|
297
|
+
|
|
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;
|
|
306
|
+
|
|
307
|
+
const functionName = callExpr.callee.name;
|
|
308
|
+
|
|
309
|
+
// Check direct names first (fast path)
|
|
310
|
+
if (functionName === 'toComponent' || functionName === 'toTag')
|
|
311
|
+
return true;
|
|
312
|
+
|
|
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;
|
|
317
|
+
|
|
318
|
+
if (!t.isImportSpecifier(binding.path.node))
|
|
319
|
+
return false;
|
|
320
|
+
|
|
321
|
+
const importSpecifier = binding.path.node;
|
|
322
|
+
const originalImportedName = t.isIdentifier(importSpecifier.imported)
|
|
323
|
+
? importSpecifier.imported.name
|
|
324
|
+
: importSpecifier.imported.value;
|
|
325
|
+
|
|
326
|
+
// Check if the original imported name was toComponent or toTag
|
|
327
|
+
const isOriginallyToComponentOrTag =
|
|
328
|
+
originalImportedName === 'toComponent'
|
|
329
|
+
|| originalImportedName === 'toTag';
|
|
330
|
+
|
|
331
|
+
return isOriginallyToComponentOrTag;
|
|
332
|
+
}
|
|
333
|
+
|
|
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
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
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;
|
|
398
|
+
|
|
399
|
+
let fileDependencies = ImportDiscovery.fileDependencies.get(currentFile);
|
|
400
|
+
if (!fileDependencies) {
|
|
401
|
+
fileDependencies = new Set<string>();
|
|
402
|
+
ImportDiscovery.fileDependencies.set(currentFile, fileDependencies);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
fileDependencies.add(dependsOn);
|
|
406
|
+
}
|
|
407
|
+
|
|
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);
|
|
442
|
+
|
|
443
|
+
if (binding)
|
|
444
|
+
return this.resolveLazyDefinition(binding);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return definition;
|
|
448
|
+
}
|
|
449
|
+
|
|
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
|
+
};
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { PluginPass } from '@babel/core';
|
|
2
|
-
import type { VisitNodeFunction } from '@babel/traverse';
|
|
3
|
-
import {
|
|
2
|
+
import type { NodePath, VisitNodeFunction } from '@babel/traverse';
|
|
3
|
+
import type { JSXElement, Program } from '@babel/types';
|
|
4
|
+
import { isJSXElement, isJSXIdentifier } from '@babel/types';
|
|
4
5
|
import { isValidHTMLNesting } from 'validate-html-nesting';
|
|
5
6
|
|
|
6
7
|
import { isComponent } from './compiler-utils.js';
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const preprocessVisitors = {
|
|
11
|
+
// From https://github.com/MananTank/babel-plugin-validate-jsx-nesting/blob/main/src/index.js
|
|
12
|
+
// Validates JSX nesting based on HTML5 rules.
|
|
13
|
+
JSXElement(path: NodePath<JSXElement>) {
|
|
12
14
|
const elName = path.node.openingElement.name;
|
|
13
15
|
const parent = path.parent;
|
|
14
16
|
|
|
@@ -35,6 +37,6 @@ const JSXValidator = {
|
|
|
35
37
|
};
|
|
36
38
|
|
|
37
39
|
|
|
38
|
-
export const preprocess: VisitNodeFunction<PluginPass, Program> = (path
|
|
39
|
-
path.traverse(
|
|
40
|
+
export const preprocess: VisitNodeFunction<PluginPass, Program> = (path): void => {
|
|
41
|
+
path.traverse(preprocessVisitors);
|
|
40
42
|
};
|
|
@@ -92,7 +92,7 @@ export class TemplateTranspiler extends JSXTranspiler<TemplateContext> {
|
|
|
92
92
|
|
|
93
93
|
context.tagName = getJSXElementName(context.path.node);
|
|
94
94
|
|
|
95
|
-
if (isJSXCustomElementComponent(context.
|
|
95
|
+
if (isJSXCustomElementComponent(context.path)) {
|
|
96
96
|
this.openingTag(context);
|
|
97
97
|
this.attributes(context);
|
|
98
98
|
this.children(context);
|
|
@@ -101,7 +101,7 @@ export class TemplateTranspiler extends JSXTranspiler<TemplateContext> {
|
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
if (isJSXFunctionElementComponent(context.
|
|
104
|
+
if (isJSXFunctionElementComponent(context.path)) {
|
|
105
105
|
// Process attributes and children into a props object
|
|
106
106
|
if (!context.isInitialElement)
|
|
107
107
|
return this.functionalComponent(context);
|
|
@@ -118,7 +118,7 @@ export class TemplateTranspiler extends JSXTranspiler<TemplateContext> {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
override openingTag(context: TemplateContext): void {
|
|
121
|
-
if (isJSXCustomElementComponent(context.
|
|
121
|
+
if (isJSXCustomElementComponent(context.path)) {
|
|
122
122
|
const literalIdentifier = Ensure.componentLiteral(
|
|
123
123
|
context.tagName,
|
|
124
124
|
COMPONENT_LITERAL_PREFIX + context.tagName,
|
|
@@ -414,7 +414,7 @@ export class CompiledTranspiler extends JSXTranspiler<CompiledContext> {
|
|
|
414
414
|
|
|
415
415
|
context.tagName = getJSXElementName(context.path.node);
|
|
416
416
|
|
|
417
|
-
if (isJSXFunctionElementComponent(context.
|
|
417
|
+
if (isJSXFunctionElementComponent(context.path)) {
|
|
418
418
|
// Process attributes and children into a props object
|
|
419
419
|
if (!context.isInitialElement)
|
|
420
420
|
return this.functionalComponent(context);
|
|
@@ -24,10 +24,9 @@ import * as babel from '@babel/core';
|
|
|
24
24
|
import { mergeAndConcat } from 'merge-anything';
|
|
25
25
|
import type { PluginOption } from 'vite';
|
|
26
26
|
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
type BabelPlugins = NonNullable<NonNullable<babel.TransformOptions['parserOpts']>['plugins']>;
|
|
27
|
+
import { litJsxBabelPlugin } from './babel-plugin.js';
|
|
28
|
+
import { babelPlugins, debugMode } from './config.js';
|
|
29
|
+
import { ImportDiscovery } from './import-discovery.js';
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
/**
|
|
@@ -41,6 +40,7 @@ type BabelPlugins = NonNullable<NonNullable<babel.TransformOptions['parserOpts']
|
|
|
41
40
|
* @returns Vite plugin configuration
|
|
42
41
|
*/
|
|
43
42
|
export const litJsx = (options: {
|
|
43
|
+
debug?: boolean; // Enable debug mode for additional logging
|
|
44
44
|
/** Options for the Babel transform */
|
|
45
45
|
babel?:
|
|
46
46
|
| babel.TransformOptions
|
|
@@ -48,6 +48,8 @@ export const litJsx = (options: {
|
|
|
48
48
|
} = {}): PluginOption => {
|
|
49
49
|
let projectRoot: string;
|
|
50
50
|
|
|
51
|
+
debugMode.value = !!options.debug;
|
|
52
|
+
|
|
51
53
|
return {
|
|
52
54
|
name: 'lit-jsx-preserve',
|
|
53
55
|
config: {
|
|
@@ -63,10 +65,6 @@ export const litJsx = (options: {
|
|
|
63
65
|
},
|
|
64
66
|
order: 'pre',
|
|
65
67
|
async handler(source, id) {
|
|
66
|
-
const plugins: BabelPlugins = [ 'jsx', 'decorators', 'decoratorAutoAccessors' ];
|
|
67
|
-
if (id.endsWith('.tsx'))
|
|
68
|
-
plugins.push('typescript');
|
|
69
|
-
|
|
70
68
|
// Default value for babel user options
|
|
71
69
|
let babelUserOptions: babel.TransformOptions = {};
|
|
72
70
|
|
|
@@ -86,20 +84,13 @@ export const litJsx = (options: {
|
|
|
86
84
|
root: projectRoot,
|
|
87
85
|
filename: id,
|
|
88
86
|
sourceFileName: id,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
plugins: [],
|
|
97
|
-
ast: false,
|
|
98
|
-
sourceMaps: true,
|
|
99
|
-
configFile: false,
|
|
100
|
-
babelrc: false,
|
|
101
|
-
parserOpts: {
|
|
102
|
-
plugins,
|
|
87
|
+
plugins: [ litJsxBabelPlugin() ],
|
|
88
|
+
ast: false,
|
|
89
|
+
sourceMaps: true,
|
|
90
|
+
configFile: false,
|
|
91
|
+
babelrc: false,
|
|
92
|
+
parserOpts: {
|
|
93
|
+
plugins: babelPlugins,
|
|
103
94
|
},
|
|
104
95
|
};
|
|
105
96
|
|
|
@@ -110,5 +101,8 @@ export const litJsx = (options: {
|
|
|
110
101
|
return { code: result.code, map: result.map };
|
|
111
102
|
},
|
|
112
103
|
},
|
|
104
|
+
hotUpdate(options) {
|
|
105
|
+
ImportDiscovery.clearCacheForFileAndDependents(options.file);
|
|
106
|
+
},
|
|
113
107
|
};
|
|
114
108
|
};
|