@zenithbuild/core 0.5.0 → 0.6.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/compiler/parse/importTypes.ts +78 -0
- package/compiler/parse/parseImports.ts +309 -0
- package/compiler/runtime/transformIR.ts +2 -2
- package/compiler/transform/componentScriptTransformer.ts +111 -91
- package/dist/cli.js +1 -0
- package/dist/zen-build.js +5660 -302
- package/dist/zen-dev.js +5660 -302
- package/dist/zen-preview.js +5660 -302
- package/dist/zenith.js +5660 -302
- package/package.json +3 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import Metadata Types
|
|
3
|
+
*
|
|
4
|
+
* Structured types for deterministic import parsing.
|
|
5
|
+
* These types represent the parsed AST data for all ES module import forms.
|
|
6
|
+
*
|
|
7
|
+
* Phase 1: Analysis only - no bundling, no resolution, no emission.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Import kind classification covering all static import forms
|
|
12
|
+
*/
|
|
13
|
+
export type ImportKind =
|
|
14
|
+
| 'default' // import x from "mod"
|
|
15
|
+
| 'named' // import { a, b } from "mod"
|
|
16
|
+
| 'namespace' // import * as x from "mod"
|
|
17
|
+
| 'side-effect' // import "mod"
|
|
18
|
+
| 're-export' // export { x } from "mod"
|
|
19
|
+
| 're-export-all' // export * from "mod"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Individual specifier within an import declaration
|
|
23
|
+
*/
|
|
24
|
+
export interface ImportSpecifier {
|
|
25
|
+
/** Local binding name used in this module */
|
|
26
|
+
local: string
|
|
27
|
+
/** Original exported name (differs from local when aliased: `import { x as y }`) */
|
|
28
|
+
imported?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Structured import metadata - parsed from AST
|
|
33
|
+
*
|
|
34
|
+
* This is the canonical representation of an import declaration.
|
|
35
|
+
* All imports in source MUST appear as ParsedImport entries.
|
|
36
|
+
*/
|
|
37
|
+
export interface ParsedImport {
|
|
38
|
+
/** Classification of import type */
|
|
39
|
+
kind: ImportKind
|
|
40
|
+
/** Module specifier (e.g., 'gsap', './Button.zen', '../utils') */
|
|
41
|
+
source: string
|
|
42
|
+
/** Bound names and their aliases */
|
|
43
|
+
specifiers: ImportSpecifier[]
|
|
44
|
+
/** TypeScript type-only import (import type { ... }) */
|
|
45
|
+
isTypeOnly: boolean
|
|
46
|
+
/** Source location for error reporting */
|
|
47
|
+
location: {
|
|
48
|
+
start: number
|
|
49
|
+
end: number
|
|
50
|
+
line: number
|
|
51
|
+
column: number
|
|
52
|
+
}
|
|
53
|
+
/** Original source text of the import statement */
|
|
54
|
+
raw: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Result of parsing all imports from a source file
|
|
59
|
+
*/
|
|
60
|
+
export interface ImportParseResult {
|
|
61
|
+
/** All parsed imports */
|
|
62
|
+
imports: ParsedImport[]
|
|
63
|
+
/** Source file path for error context */
|
|
64
|
+
filePath: string
|
|
65
|
+
/** Whether parsing completed successfully */
|
|
66
|
+
success: boolean
|
|
67
|
+
/** Any errors encountered during parsing */
|
|
68
|
+
errors: ImportParseError[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Error encountered during import parsing
|
|
73
|
+
*/
|
|
74
|
+
export interface ImportParseError {
|
|
75
|
+
message: string
|
|
76
|
+
line?: number
|
|
77
|
+
column?: number
|
|
78
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import Parser Module
|
|
3
|
+
*
|
|
4
|
+
* Phase 1: Deterministic import parsing using Acorn AST.
|
|
5
|
+
*
|
|
6
|
+
* This module parses JavaScript/TypeScript source code and extracts
|
|
7
|
+
* structured metadata for all import declarations. It does NOT:
|
|
8
|
+
* - Resolve imports
|
|
9
|
+
* - Bundle dependencies
|
|
10
|
+
* - Emit any code
|
|
11
|
+
*
|
|
12
|
+
* All import analysis happens at compile time.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as acorn from 'acorn'
|
|
16
|
+
import type {
|
|
17
|
+
ParsedImport,
|
|
18
|
+
ImportSpecifier,
|
|
19
|
+
ImportKind,
|
|
20
|
+
ImportParseResult,
|
|
21
|
+
ImportParseError
|
|
22
|
+
} from './importTypes'
|
|
23
|
+
|
|
24
|
+
// Acorn AST node types (simplified for our use case)
|
|
25
|
+
interface AcornNode {
|
|
26
|
+
type: string
|
|
27
|
+
start: number
|
|
28
|
+
end: number
|
|
29
|
+
loc?: {
|
|
30
|
+
start: { line: number; column: number }
|
|
31
|
+
end: { line: number; column: number }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ImportDeclarationNode extends AcornNode {
|
|
36
|
+
type: 'ImportDeclaration'
|
|
37
|
+
source: { value: string; raw: string }
|
|
38
|
+
specifiers: Array<{
|
|
39
|
+
type: 'ImportDefaultSpecifier' | 'ImportSpecifier' | 'ImportNamespaceSpecifier'
|
|
40
|
+
local: { name: string }
|
|
41
|
+
imported?: { name: string }
|
|
42
|
+
}>
|
|
43
|
+
importKind?: 'type' | 'value'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ExportNamedDeclarationNode extends AcornNode {
|
|
47
|
+
type: 'ExportNamedDeclaration'
|
|
48
|
+
source?: { value: string; raw: string }
|
|
49
|
+
specifiers: Array<{
|
|
50
|
+
type: 'ExportSpecifier'
|
|
51
|
+
local: { name: string }
|
|
52
|
+
exported: { name: string }
|
|
53
|
+
}>
|
|
54
|
+
exportKind?: 'type' | 'value'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface ExportAllDeclarationNode extends AcornNode {
|
|
58
|
+
type: 'ExportAllDeclaration'
|
|
59
|
+
source: { value: string; raw: string }
|
|
60
|
+
exported?: { name: string }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ProgramNode extends AcornNode {
|
|
64
|
+
type: 'Program'
|
|
65
|
+
body: AcornNode[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse an ImportDeclaration AST node into structured metadata
|
|
70
|
+
*/
|
|
71
|
+
function parseImportDeclaration(
|
|
72
|
+
node: ImportDeclarationNode,
|
|
73
|
+
source: string
|
|
74
|
+
): ParsedImport {
|
|
75
|
+
const specifiers: ImportSpecifier[] = []
|
|
76
|
+
let kind: ImportKind = 'side-effect'
|
|
77
|
+
|
|
78
|
+
for (const spec of node.specifiers) {
|
|
79
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
80
|
+
kind = 'default'
|
|
81
|
+
specifiers.push({ local: spec.local.name })
|
|
82
|
+
} else if (spec.type === 'ImportNamespaceSpecifier') {
|
|
83
|
+
kind = 'namespace'
|
|
84
|
+
specifiers.push({ local: spec.local.name })
|
|
85
|
+
} else if (spec.type === 'ImportSpecifier') {
|
|
86
|
+
kind = 'named'
|
|
87
|
+
specifiers.push({
|
|
88
|
+
local: spec.local.name,
|
|
89
|
+
imported: spec.imported?.name !== spec.local.name
|
|
90
|
+
? spec.imported?.name
|
|
91
|
+
: undefined
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// If no specifiers, it's a side-effect import
|
|
97
|
+
if (node.specifiers.length === 0) {
|
|
98
|
+
kind = 'side-effect'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
kind,
|
|
103
|
+
source: node.source.value,
|
|
104
|
+
specifiers,
|
|
105
|
+
isTypeOnly: node.importKind === 'type',
|
|
106
|
+
location: {
|
|
107
|
+
start: node.start,
|
|
108
|
+
end: node.end,
|
|
109
|
+
line: node.loc?.start.line ?? 1,
|
|
110
|
+
column: node.loc?.start.column ?? 0
|
|
111
|
+
},
|
|
112
|
+
raw: source.slice(node.start, node.end)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse an ExportNamedDeclaration with source (re-export)
|
|
118
|
+
*/
|
|
119
|
+
function parseReExport(
|
|
120
|
+
node: ExportNamedDeclarationNode,
|
|
121
|
+
source: string
|
|
122
|
+
): ParsedImport {
|
|
123
|
+
const specifiers: ImportSpecifier[] = node.specifiers.map(spec => ({
|
|
124
|
+
local: spec.exported.name,
|
|
125
|
+
imported: spec.local.name !== spec.exported.name
|
|
126
|
+
? spec.local.name
|
|
127
|
+
: undefined
|
|
128
|
+
}))
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
kind: 're-export',
|
|
132
|
+
source: node.source!.value,
|
|
133
|
+
specifiers,
|
|
134
|
+
isTypeOnly: node.exportKind === 'type',
|
|
135
|
+
location: {
|
|
136
|
+
start: node.start,
|
|
137
|
+
end: node.end,
|
|
138
|
+
line: node.loc?.start.line ?? 1,
|
|
139
|
+
column: node.loc?.start.column ?? 0
|
|
140
|
+
},
|
|
141
|
+
raw: source.slice(node.start, node.end)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Parse an ExportAllDeclaration (export * from "mod")
|
|
147
|
+
*/
|
|
148
|
+
function parseExportAll(
|
|
149
|
+
node: ExportAllDeclarationNode,
|
|
150
|
+
source: string
|
|
151
|
+
): ParsedImport {
|
|
152
|
+
return {
|
|
153
|
+
kind: 're-export-all',
|
|
154
|
+
source: node.source.value,
|
|
155
|
+
specifiers: node.exported
|
|
156
|
+
? [{ local: node.exported.name }]
|
|
157
|
+
: [],
|
|
158
|
+
isTypeOnly: false,
|
|
159
|
+
location: {
|
|
160
|
+
start: node.start,
|
|
161
|
+
end: node.end,
|
|
162
|
+
line: node.loc?.start.line ?? 1,
|
|
163
|
+
column: node.loc?.start.column ?? 0
|
|
164
|
+
},
|
|
165
|
+
raw: source.slice(node.start, node.end)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Parse all imports from a source file using Acorn AST parser
|
|
171
|
+
*
|
|
172
|
+
* @param source - JavaScript/TypeScript source code
|
|
173
|
+
* @param filePath - Path to the source file (for error context)
|
|
174
|
+
* @returns ParsedImport[] - All import declarations found
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* const result = parseImports(`
|
|
178
|
+
* import { gsap } from 'gsap';
|
|
179
|
+
* import Button from './Button.zen';
|
|
180
|
+
* `, 'MyComponent.zen');
|
|
181
|
+
*
|
|
182
|
+
* // result.imports[0].kind === 'named'
|
|
183
|
+
* // result.imports[0].source === 'gsap'
|
|
184
|
+
*/
|
|
185
|
+
export function parseImports(
|
|
186
|
+
source: string,
|
|
187
|
+
filePath: string
|
|
188
|
+
): ImportParseResult {
|
|
189
|
+
const imports: ParsedImport[] = []
|
|
190
|
+
const errors: ImportParseError[] = []
|
|
191
|
+
|
|
192
|
+
// Strip TypeScript type annotations for parsing
|
|
193
|
+
// Acorn doesn't support TypeScript, so we handle type imports specially
|
|
194
|
+
const strippedSource = stripTypeAnnotations(source)
|
|
195
|
+
|
|
196
|
+
let ast: ProgramNode
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
ast = acorn.parse(strippedSource, {
|
|
200
|
+
ecmaVersion: 'latest',
|
|
201
|
+
sourceType: 'module',
|
|
202
|
+
locations: true
|
|
203
|
+
}) as unknown as ProgramNode
|
|
204
|
+
} catch (error: any) {
|
|
205
|
+
// Parse error - return with error info
|
|
206
|
+
return {
|
|
207
|
+
imports: [],
|
|
208
|
+
filePath,
|
|
209
|
+
success: false,
|
|
210
|
+
errors: [{
|
|
211
|
+
message: `Parse error: ${error.message}`,
|
|
212
|
+
line: error.loc?.line,
|
|
213
|
+
column: error.loc?.column
|
|
214
|
+
}]
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Walk the AST to find all import/export declarations
|
|
219
|
+
for (const node of ast.body) {
|
|
220
|
+
try {
|
|
221
|
+
if (node.type === 'ImportDeclaration') {
|
|
222
|
+
imports.push(parseImportDeclaration(
|
|
223
|
+
node as ImportDeclarationNode,
|
|
224
|
+
strippedSource
|
|
225
|
+
))
|
|
226
|
+
} else if (node.type === 'ExportNamedDeclaration') {
|
|
227
|
+
const exportNode = node as ExportNamedDeclarationNode
|
|
228
|
+
// Only process re-exports (exports with a source)
|
|
229
|
+
if (exportNode.source) {
|
|
230
|
+
imports.push(parseReExport(exportNode, strippedSource))
|
|
231
|
+
}
|
|
232
|
+
} else if (node.type === 'ExportAllDeclaration') {
|
|
233
|
+
imports.push(parseExportAll(
|
|
234
|
+
node as ExportAllDeclarationNode,
|
|
235
|
+
strippedSource
|
|
236
|
+
))
|
|
237
|
+
}
|
|
238
|
+
} catch (error: any) {
|
|
239
|
+
errors.push({
|
|
240
|
+
message: `Failed to parse node: ${error.message}`,
|
|
241
|
+
line: (node as any).loc?.start?.line
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
imports,
|
|
248
|
+
filePath,
|
|
249
|
+
success: errors.length === 0,
|
|
250
|
+
errors
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Strip TypeScript-specific syntax that Acorn can't parse
|
|
256
|
+
* This is a simple preprocessing step for common patterns
|
|
257
|
+
*/
|
|
258
|
+
function stripTypeAnnotations(source: string): string {
|
|
259
|
+
// Handle `import type` by converting to regular import
|
|
260
|
+
// The isTypeOnly flag will be set based on the original text
|
|
261
|
+
let result = source
|
|
262
|
+
|
|
263
|
+
// Track which imports are type-only before stripping
|
|
264
|
+
const typeImportPattern = /import\s+type\s+/g
|
|
265
|
+
result = result.replace(typeImportPattern, 'import ')
|
|
266
|
+
|
|
267
|
+
// Strip inline type annotations in destructuring
|
|
268
|
+
// e.g., `import { type Foo, Bar }` -> `import { Foo, Bar }`
|
|
269
|
+
result = result.replace(/,\s*type\s+(\w+)/g, ', $1')
|
|
270
|
+
result = result.replace(/{\s*type\s+(\w+)/g, '{ $1')
|
|
271
|
+
|
|
272
|
+
// Strip type-only exports
|
|
273
|
+
result = result.replace(/export\s+type\s+{/g, 'export {')
|
|
274
|
+
|
|
275
|
+
return result
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Check if the original source has a type import at the given position
|
|
280
|
+
*/
|
|
281
|
+
export function isTypeImportAtPosition(source: string, position: number): boolean {
|
|
282
|
+
const before = source.slice(Math.max(0, position - 20), position)
|
|
283
|
+
return /import\s+type\s*$/.test(before)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Categorize imports by their module source type
|
|
288
|
+
*/
|
|
289
|
+
export function categorizeImports(imports: ParsedImport[]): {
|
|
290
|
+
zenImports: ParsedImport[] // .zen file imports (compile-time)
|
|
291
|
+
npmImports: ParsedImport[] // Package imports (npm)
|
|
292
|
+
relativeImports: ParsedImport[] // Relative path imports
|
|
293
|
+
} {
|
|
294
|
+
const zenImports: ParsedImport[] = []
|
|
295
|
+
const npmImports: ParsedImport[] = []
|
|
296
|
+
const relativeImports: ParsedImport[] = []
|
|
297
|
+
|
|
298
|
+
for (const imp of imports) {
|
|
299
|
+
if (imp.source.endsWith('.zen')) {
|
|
300
|
+
zenImports.push(imp)
|
|
301
|
+
} else if (imp.source.startsWith('./') || imp.source.startsWith('../')) {
|
|
302
|
+
relativeImports.push(imp)
|
|
303
|
+
} else {
|
|
304
|
+
npmImports.push(imp)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { zenImports, npmImports, relativeImports }
|
|
309
|
+
}
|
|
@@ -71,8 +71,8 @@ export async function transformIR(ir: ZenIR): Promise<RuntimeCode> {
|
|
|
71
71
|
// Transform script (remove state and prop declarations, they're handled by runtime)
|
|
72
72
|
const scriptCode = transformStateDeclarations(scriptContent)
|
|
73
73
|
|
|
74
|
-
// Transform component scripts for instance-scoped execution (
|
|
75
|
-
const componentScriptResult =
|
|
74
|
+
// Transform component scripts for instance-scoped execution (synchronous - Acorn)
|
|
75
|
+
const componentScriptResult = transformAllComponentScripts(ir.componentScripts || [])
|
|
76
76
|
|
|
77
77
|
// Generate complete runtime bundle
|
|
78
78
|
const bundle = generateRuntimeBundle({
|
|
@@ -5,24 +5,18 @@
|
|
|
5
5
|
* Uses namespace binding pattern for cleaner output:
|
|
6
6
|
* const { signal, effect, onMount, ... } = __inst;
|
|
7
7
|
*
|
|
8
|
-
* Uses
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
8
|
+
* Uses Acorn AST parser for deterministic import parsing.
|
|
9
|
+
* Phase 1: Analysis only - imports are parsed and categorized.
|
|
10
|
+
* Phase 2 (bundling) happens in dev.ts.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* Import handling:
|
|
13
|
+
* - .zen imports: Stripped (compile-time resolved)
|
|
14
|
+
* - npm imports: Stored as structured metadata for later bundling
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
|
-
import { init, parse } from 'es-module-lexer'
|
|
16
17
|
import type { ComponentScriptIR, ScriptImport } from '../ir/types'
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
let lexerInitialized = false
|
|
20
|
-
async function ensureLexerInit(): Promise<void> {
|
|
21
|
-
if (!lexerInitialized) {
|
|
22
|
-
await init
|
|
23
|
-
lexerInitialized = true
|
|
24
|
-
}
|
|
25
|
-
}
|
|
18
|
+
import { parseImports, categorizeImports } from '../parse/parseImports'
|
|
19
|
+
import type { ParsedImport } from '../parse/importTypes'
|
|
26
20
|
|
|
27
21
|
/**
|
|
28
22
|
* Namespace bindings - destructured from the instance
|
|
@@ -58,74 +52,100 @@ export interface TransformResult {
|
|
|
58
52
|
}
|
|
59
53
|
|
|
60
54
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* @param scriptContent - Raw script content
|
|
64
|
-
* @returns Object with imports array and script with imports stripped
|
|
55
|
+
* Convert ParsedImport to ScriptImport for compatibility with existing IR
|
|
65
56
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
57
|
+
function toScriptImport(parsed: ParsedImport): ScriptImport {
|
|
58
|
+
// Build specifiers string from parsed specifiers
|
|
59
|
+
let specifiers = ''
|
|
60
|
+
|
|
61
|
+
if (parsed.kind === 'default') {
|
|
62
|
+
specifiers = parsed.specifiers[0]?.local || ''
|
|
63
|
+
} else if (parsed.kind === 'namespace') {
|
|
64
|
+
specifiers = `* as ${parsed.specifiers[0]?.local || ''}`
|
|
65
|
+
} else if (parsed.kind === 'named') {
|
|
66
|
+
const parts = parsed.specifiers.map(s =>
|
|
67
|
+
s.imported ? `${s.imported} as ${s.local}` : s.local
|
|
68
|
+
)
|
|
69
|
+
specifiers = `{ ${parts.join(', ')} }`
|
|
70
|
+
} else if (parsed.kind === 'side-effect') {
|
|
71
|
+
specifiers = ''
|
|
72
|
+
}
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
return {
|
|
75
|
+
source: parsed.source,
|
|
76
|
+
specifiers,
|
|
77
|
+
typeOnly: parsed.isTypeOnly,
|
|
78
|
+
sideEffect: parsed.kind === 'side-effect'
|
|
79
|
+
}
|
|
80
|
+
}
|
|
77
81
|
|
|
78
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Strip imports from source code based on parsed import locations
|
|
84
|
+
*
|
|
85
|
+
* @param source - Original source code
|
|
86
|
+
* @param imports - Parsed imports to strip
|
|
87
|
+
* @returns Source with imports removed
|
|
88
|
+
*/
|
|
89
|
+
function stripImportsFromSource(source: string, imports: ParsedImport[]): string {
|
|
90
|
+
if (imports.length === 0) return source
|
|
79
91
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const importStatement = scriptContent.slice(imp.ss, imp.se)
|
|
92
|
+
// Sort by start position descending for safe removal
|
|
93
|
+
const sorted = [...imports].sort((a, b) => b.location.start - a.location.start)
|
|
83
94
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
let result = source
|
|
96
|
+
for (const imp of sorted) {
|
|
97
|
+
// Remove the import statement
|
|
98
|
+
const before = result.slice(0, imp.location.start)
|
|
99
|
+
const after = result.slice(imp.location.end)
|
|
89
100
|
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
101
|
+
// Also remove trailing newline if present
|
|
102
|
+
const trimmedAfter = after.startsWith('\n') ? after.slice(1) : after
|
|
103
|
+
result = before + trimmedAfter
|
|
104
|
+
}
|
|
95
105
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const isSideEffect = imp.ss === imp.se || !importStatement.includes(' from ')
|
|
99
|
-
|
|
100
|
-
// Extract specifiers from the import statement
|
|
101
|
-
let specifiers = ''
|
|
102
|
-
if (!isSideEffect) {
|
|
103
|
-
const fromIndex = importStatement.indexOf(' from ')
|
|
104
|
-
if (fromIndex !== -1) {
|
|
105
|
-
// Get everything between 'import' (or 'import type') and 'from'
|
|
106
|
-
const start = isTypeOnly ? 'import type '.length : 'import '.length
|
|
107
|
-
specifiers = importStatement.slice(start, fromIndex).trim()
|
|
108
|
-
}
|
|
109
|
-
}
|
|
106
|
+
return result
|
|
107
|
+
}
|
|
110
108
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Parse and extract imports from script content using Acorn AST parser
|
|
111
|
+
*
|
|
112
|
+
* Phase 1: Deterministic parsing - no bundling or resolution
|
|
113
|
+
*
|
|
114
|
+
* @param scriptContent - Raw script content
|
|
115
|
+
* @param componentName - Name of the component (for error context)
|
|
116
|
+
* @returns Object with npm imports array and script with all imports stripped
|
|
117
|
+
*/
|
|
118
|
+
export function parseAndExtractImports(
|
|
119
|
+
scriptContent: string,
|
|
120
|
+
componentName: string = 'unknown'
|
|
121
|
+
): {
|
|
122
|
+
imports: ScriptImport[]
|
|
123
|
+
strippedCode: string
|
|
124
|
+
} {
|
|
125
|
+
// Parse imports using Acorn AST
|
|
126
|
+
const parseResult = parseImports(scriptContent, componentName)
|
|
117
127
|
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
if (!parseResult.success) {
|
|
129
|
+
console.warn(`[Zenith] Import parse warnings for ${componentName}:`, parseResult.errors)
|
|
120
130
|
}
|
|
121
131
|
|
|
122
|
-
//
|
|
123
|
-
|
|
132
|
+
// Categorize imports
|
|
133
|
+
const { zenImports, npmImports, relativeImports } = categorizeImports(parseResult.imports)
|
|
134
|
+
|
|
135
|
+
// Convert npm imports to ScriptImport format
|
|
136
|
+
const scriptImports = npmImports.map(toScriptImport)
|
|
124
137
|
|
|
125
|
-
//
|
|
126
|
-
imports
|
|
138
|
+
// Strip ALL imports from source (zen, npm, and relative)
|
|
139
|
+
// - .zen imports: resolved at compile time
|
|
140
|
+
// - npm imports: will be bundled separately
|
|
141
|
+
// - relative imports: resolved at compile time
|
|
142
|
+
const allImportsToStrip = [...zenImports, ...npmImports, ...relativeImports]
|
|
143
|
+
const strippedCode = stripImportsFromSource(scriptContent, allImportsToStrip)
|
|
127
144
|
|
|
128
|
-
return {
|
|
145
|
+
return {
|
|
146
|
+
imports: scriptImports,
|
|
147
|
+
strippedCode
|
|
148
|
+
}
|
|
129
149
|
}
|
|
130
150
|
|
|
131
151
|
/**
|
|
@@ -136,13 +156,13 @@ export async function parseAndExtractImports(scriptContent: string): Promise<{
|
|
|
136
156
|
* @param props - Declared prop names
|
|
137
157
|
* @returns TransformResult with transformed script and extracted imports
|
|
138
158
|
*/
|
|
139
|
-
export
|
|
159
|
+
export function transformComponentScript(
|
|
140
160
|
componentName: string,
|
|
141
161
|
scriptContent: string,
|
|
142
162
|
props: string[]
|
|
143
|
-
):
|
|
144
|
-
// Parse and extract imports using
|
|
145
|
-
const { imports, strippedCode } =
|
|
163
|
+
): TransformResult {
|
|
164
|
+
// Parse and extract imports using Acorn AST
|
|
165
|
+
const { imports, strippedCode } = parseAndExtractImports(scriptContent, componentName)
|
|
146
166
|
|
|
147
167
|
let transformed = strippedCode
|
|
148
168
|
|
|
@@ -247,34 +267,34 @@ export function emitImports(imports: ScriptImport[]): string {
|
|
|
247
267
|
/**
|
|
248
268
|
* Transform all component scripts from collected ComponentScriptIR
|
|
249
269
|
*
|
|
270
|
+
* Now synchronous since Acorn parsing is synchronous.
|
|
271
|
+
*
|
|
250
272
|
* @param componentScripts - Array of component script IRs
|
|
251
273
|
* @returns TransformAllResult with combined code and deduplicated imports
|
|
252
274
|
*/
|
|
253
|
-
export
|
|
275
|
+
export function transformAllComponentScripts(
|
|
254
276
|
componentScripts: ComponentScriptIR[]
|
|
255
|
-
):
|
|
277
|
+
): TransformAllResult {
|
|
256
278
|
if (!componentScripts || componentScripts.length === 0) {
|
|
257
279
|
return { code: '', imports: [] }
|
|
258
280
|
}
|
|
259
281
|
|
|
260
282
|
const allImports: ScriptImport[] = []
|
|
261
283
|
|
|
262
|
-
const factories =
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
})
|
|
277
|
-
)
|
|
284
|
+
const factories = componentScripts
|
|
285
|
+
.filter(comp => comp.script && comp.script.trim().length > 0)
|
|
286
|
+
.map(comp => {
|
|
287
|
+
const result = transformComponentScript(
|
|
288
|
+
comp.name,
|
|
289
|
+
comp.script,
|
|
290
|
+
comp.props
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
// Collect imports
|
|
294
|
+
allImports.push(...result.imports)
|
|
295
|
+
|
|
296
|
+
return generateComponentFactory(comp.name, result.script, comp.props)
|
|
297
|
+
})
|
|
278
298
|
|
|
279
299
|
return {
|
|
280
300
|
code: factories.join('\n'),
|