@zenithbuild/core 0.5.0 → 0.6.1

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.
@@ -5,24 +5,18 @@
5
5
  * Uses namespace binding pattern for cleaner output:
6
6
  * const { signal, effect, onMount, ... } = __inst;
7
7
  *
8
- * Uses es-module-lexer to parse imports:
9
- * - .zen imports are stripped (compile-time resolved)
10
- * - npm imports are extracted as structured metadata for bundling
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
- * IMPORTANT: No regex-based import parsing.
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
- // Initialize es-module-lexer (must be called before parsing)
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
- * Parse and extract imports from script content using es-module-lexer
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
- export async function parseAndExtractImports(scriptContent: string): Promise<{
67
- imports: ScriptImport[]
68
- strippedCode: string
69
- }> {
70
- await ensureLexerInit()
71
-
72
- const imports: ScriptImport[] = []
73
- const [parsedImports] = parse(scriptContent)
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
- // Sort imports by start position (descending) for safe removal
76
- const sortedImports = [...parsedImports].sort((a, b) => b.ss - a.ss)
74
+ return {
75
+ source: parsed.source,
76
+ specifiers,
77
+ typeOnly: parsed.isTypeOnly,
78
+ sideEffect: parsed.kind === 'side-effect'
79
+ }
80
+ }
77
81
 
78
- let strippedCode = scriptContent
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
- for (const imp of sortedImports) {
81
- const source = imp.n || '' // Module specifier
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
- // Skip .zen file imports (compile-time resolved) - just strip them
85
- if (source.endsWith('.zen')) {
86
- strippedCode = strippedCode.slice(0, imp.ss) + strippedCode.slice(imp.se)
87
- continue
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
- // Skip relative imports (compile-time resolved) - just strip them
91
- if (source.startsWith('./') || source.startsWith('../')) {
92
- strippedCode = strippedCode.slice(0, imp.ss) + strippedCode.slice(imp.se)
93
- continue
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
- // This is an npm/external import - extract as structured metadata
97
- const isTypeOnly = importStatement.startsWith('import type')
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
- imports.push({
112
- source,
113
- specifiers,
114
- typeOnly: isTypeOnly,
115
- sideEffect: isSideEffect
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
- // Strip the import from the code (it will be hoisted to bundle top)
119
- strippedCode = strippedCode.slice(0, imp.ss) + strippedCode.slice(imp.se)
128
+ if (!parseResult.success) {
129
+ console.warn(`[Zenith] Import parse warnings for ${componentName}:`, parseResult.errors)
120
130
  }
121
131
 
122
- // Clean up any leftover empty lines from stripped imports
123
- strippedCode = strippedCode.replace(/^\s*\n/gm, '')
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
- // Reverse imports array since we processed in reverse order
126
- imports.reverse()
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 { imports, strippedCode }
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 async function transformComponentScript(
159
+ export function transformComponentScript(
140
160
  componentName: string,
141
161
  scriptContent: string,
142
162
  props: string[]
143
- ): Promise<TransformResult> {
144
- // Parse and extract imports using es-module-lexer
145
- const { imports, strippedCode } = await parseAndExtractImports(scriptContent)
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 async function transformAllComponentScripts(
275
+ export function transformAllComponentScripts(
254
276
  componentScripts: ComponentScriptIR[]
255
- ): Promise<TransformAllResult> {
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 = await Promise.all(
263
- componentScripts
264
- .filter(comp => comp.script && comp.script.trim().length > 0)
265
- .map(async comp => {
266
- const result = await transformComponentScript(
267
- comp.name,
268
- comp.script,
269
- comp.props
270
- )
271
-
272
- // Collect imports
273
- allImports.push(...result.imports)
274
-
275
- return generateComponentFactory(comp.name, result.script, comp.props)
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'),
package/dist/cli.js CHANGED
@@ -17,6 +17,8 @@
17
17
  #!/usr/bin/env bun
18
18
  #!/usr/bin/env bun
19
19
  #!/usr/bin/env bun
20
+ #!/usr/bin/env bun
21
+ #!/usr/bin/env bun
20
22
  // @bun
21
23
  var __create = Object.create;
22
24
  var __getProtoOf = Object.getPrototypeOf;