@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.
@@ -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 (async)
75
- const componentScriptResult = await transformAllComponentScripts(ir.componentScripts || [])
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 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,7 @@
17
17
  #!/usr/bin/env bun
18
18
  #!/usr/bin/env bun
19
19
  #!/usr/bin/env bun
20
+ #!/usr/bin/env bun
20
21
  // @bun
21
22
  var __create = Object.create;
22
23
  var __getProtoOf = Object.getPrototypeOf;