@zenithbuild/core 0.4.7 → 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.
@@ -1,5 +1,6 @@
1
1
  import path from 'path'
2
2
  import fs from 'fs'
3
+ import os from 'os'
3
4
  import { serve, type ServerWebSocket } from 'bun'
4
5
  import { requireProject } from '../utils/project'
5
6
  import * as logger from '../utils/logger'
@@ -29,6 +30,53 @@ interface CompiledPage {
29
30
 
30
31
  const pageCache = new Map<string, CompiledPage>()
31
32
 
33
+ /**
34
+ * Bundle page script using Bun's bundler to resolve npm imports at compile time.
35
+ * This allows ES module imports like `import { gsap } from 'gsap'` to work.
36
+ */
37
+ async function bundlePageScript(script: string, projectRoot: string): Promise<string> {
38
+ // If no import statements, return as-is
39
+ if (!script.includes('import ')) {
40
+ return script
41
+ }
42
+
43
+ // Create a temporary file for bundling
44
+ const tempDir = os.tmpdir()
45
+ const tempFile = path.join(tempDir, `zenith-bundle-${Date.now()}.js`)
46
+
47
+ try {
48
+ // Write script to temp file
49
+ fs.writeFileSync(tempFile, script, 'utf-8')
50
+
51
+ // Use Bun.build to bundle with npm resolution
52
+ const result = await Bun.build({
53
+ entrypoints: [tempFile],
54
+ target: 'browser',
55
+ format: 'esm',
56
+ minify: false,
57
+ // Resolve modules from the project's node_modules
58
+ external: [], // Bundle everything
59
+ })
60
+
61
+ if (!result.success || !result.outputs[0]) {
62
+ console.error('[Zenith] Bundle errors:', result.logs)
63
+ return script // Fall back to original
64
+ }
65
+
66
+ // Get the bundled output
67
+ const bundledCode = await result.outputs[0].text()
68
+ return bundledCode
69
+ } catch (error: any) {
70
+ console.error('[Zenith] Failed to bundle page script:', error.message)
71
+ return script // Fall back to original
72
+ } finally {
73
+ // Clean up temp file
74
+ try {
75
+ fs.unlinkSync(tempFile)
76
+ } catch { }
77
+ }
78
+ }
79
+
32
80
  export async function dev(options: DevOptions = {}): Promise<void> {
33
81
  const project = requireProject()
34
82
  const port = options.port || parseInt(process.env.PORT || '3000', 10)
@@ -105,7 +153,7 @@ export async function dev(options: DevOptions = {}): Promise<void> {
105
153
  /**
106
154
  * Compile a .zen page in memory
107
155
  */
108
- function compilePageInMemory(pagePath: string): CompiledPage | null {
156
+ async function compilePageInMemory(pagePath: string): Promise<CompiledPage | null> {
109
157
  try {
110
158
  const layoutsDir = path.join(pagesDir, '../layouts')
111
159
  const componentsDir = path.join(pagesDir, '../components')
@@ -117,16 +165,19 @@ export async function dev(options: DevOptions = {}): Promise<void> {
117
165
 
118
166
  if (layoutToUse) processedSource = processLayout(source, layoutToUse)
119
167
 
120
- const result = compileZenSource(processedSource, pagePath, {
168
+ const result = await compileZenSource(processedSource, pagePath, {
121
169
  componentsDir: fs.existsSync(componentsDir) ? componentsDir : undefined
122
170
  })
123
171
  if (!result.finalized) throw new Error('Compilation failed')
124
172
 
125
173
  const routeDef = generateRouteDefinition(pagePath, pagesDir)
126
174
 
175
+ // Bundle the script to resolve npm imports at compile time
176
+ const bundledScript = await bundlePageScript(result.finalized.js, rootDir)
177
+
127
178
  return {
128
179
  html: result.finalized.html,
129
- script: result.finalized.js,
180
+ script: bundledScript,
130
181
  styles: result.finalized.styles,
131
182
  route: routeDef.path,
132
183
  lastModified: Date.now()
@@ -189,7 +240,7 @@ export async function dev(options: DevOptions = {}): Promise<void> {
189
240
 
190
241
  const server = serve({
191
242
  port,
192
- fetch(req, server) {
243
+ async fetch(req, server) {
193
244
  const startTime = performance.now()
194
245
  const url = new URL(req.url)
195
246
  const pathname = url.pathname
@@ -246,7 +297,7 @@ export async function dev(options: DevOptions = {}): Promise<void> {
246
297
  const stat = fs.statSync(pagePath)
247
298
 
248
299
  if (!cached || stat.mtimeMs > cached.lastModified) {
249
- cached = compilePageInMemory(pagePath) || undefined
300
+ cached = await compilePageInMemory(pagePath) || undefined
250
301
  if (cached) pageCache.set(pagePath, cached)
251
302
  }
252
303
  const compileEnd = performance.now()
@@ -327,7 +378,8 @@ function generateDevHTML(page: CompiledPage, contentData: any = {}): string {
327
378
  // Escape </script> sequences in JSON content to prevent breaking the script tag
328
379
  const contentJson = JSON.stringify(contentData).replace(/<\//g, '<\\/')
329
380
  const contentTag = `<script>window.__ZENITH_CONTENT__ = ${contentJson};</script>`
330
- const scriptTag = `<script>\n${page.script}\n</script>`
381
+ // Use type="module" to support ES6 imports from npm packages
382
+ const scriptTag = `<script type="module">\n${page.script}\n</script>`
331
383
  const allScripts = `${runtimeTag}\n${contentTag}\n${scriptTag}`
332
384
  return page.html.includes('</body>')
333
385
  ? page.html.replace('</body>', `${allScripts}\n</body>`)
@@ -39,10 +39,10 @@ export interface FinalizedOutput {
39
39
  * @param compiled - Compiled template from Phase 2
40
40
  * @returns Finalized output
41
41
  */
42
- export function finalizeOutput(
42
+ export async function finalizeOutput(
43
43
  ir: ZenIR,
44
44
  compiled: CompiledTemplate
45
- ): FinalizedOutput {
45
+ ): Promise<FinalizedOutput> {
46
46
  const errors: string[] = []
47
47
 
48
48
  // 1. Validate all expressions (Phase 8/9/10 requirement)
@@ -77,7 +77,7 @@ export function finalizeOutput(
77
77
  // 3. Generate runtime code
78
78
  let runtimeCode: RuntimeCode
79
79
  try {
80
- runtimeCode = transformIR(ir)
80
+ runtimeCode = await transformIR(ir)
81
81
  } catch (error: any) {
82
82
  errors.push(`Runtime generation failed: ${error.message}`)
83
83
  return {
@@ -176,11 +176,11 @@ function verifyNoRawExpressions(html: string, filePath: string): string[] {
176
176
  *
177
177
  * Throws if validation fails (build must fail on errors)
178
178
  */
179
- export function finalizeOutputOrThrow(
179
+ export async function finalizeOutputOrThrow(
180
180
  ir: ZenIR,
181
181
  compiled: CompiledTemplate
182
- ): FinalizedOutput {
183
- const output = finalizeOutput(ir, compiled)
182
+ ): Promise<FinalizedOutput> {
183
+ const output = await finalizeOutput(ir, compiled)
184
184
 
185
185
  if (output.hasErrors) {
186
186
  const errorMessage = output.errors.join('\n\n')
package/compiler/index.ts CHANGED
@@ -12,11 +12,11 @@ import type { FinalizedOutput } from './finalize/finalizeOutput'
12
12
  /**
13
13
  * Compile a .zen file into IR and CompiledTemplate
14
14
  */
15
- export function compileZen(filePath: string): {
15
+ export async function compileZen(filePath: string): Promise<{
16
16
  ir: ZenIR
17
17
  compiled: CompiledTemplate
18
18
  finalized?: FinalizedOutput
19
- } {
19
+ }> {
20
20
  const source = readFileSync(filePath, 'utf-8')
21
21
  return compileZenSource(source, filePath)
22
22
  }
@@ -24,17 +24,17 @@ export function compileZen(filePath: string): {
24
24
  /**
25
25
  * Compile Zen source string into IR and CompiledTemplate
26
26
  */
27
- export function compileZenSource(
27
+ export async function compileZenSource(
28
28
  source: string,
29
29
  filePath: string,
30
30
  options?: {
31
31
  componentsDir?: string
32
32
  }
33
- ): {
33
+ ): Promise<{
34
34
  ir: ZenIR
35
35
  compiled: CompiledTemplate
36
36
  finalized?: FinalizedOutput
37
- } {
37
+ }> {
38
38
  // Parse template
39
39
  const template = parseTemplate(source, filePath)
40
40
 
@@ -42,7 +42,7 @@ export function compileZenSource(
42
42
  const script = parseScript(source)
43
43
 
44
44
  // Parse styles
45
- const styleRegex = /\u003cstyle[^\u003e]*\u003e([\s\S]*?)\u003c\/style\u003e/gi
45
+ const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi
46
46
  const styles: StyleIR[] = []
47
47
  let match
48
48
  while ((match = styleRegex.exec(source)) !== null) {
@@ -74,9 +74,10 @@ export function compileZenSource(
74
74
  const compiled = transformTemplate(ir)
75
75
 
76
76
  try {
77
- const finalized = finalizeOutputOrThrow(ir, compiled)
77
+ const finalized = await finalizeOutputOrThrow(ir, compiled)
78
78
  return { ir, compiled, finalized }
79
79
  } catch (error: any) {
80
80
  throw new Error(`Failed to finalize output for ${filePath}:\\n${error.message}`)
81
81
  }
82
82
  }
83
+
@@ -6,6 +6,17 @@
6
6
  * without any runtime execution or transformation.
7
7
  */
8
8
 
9
+ /**
10
+ * Structured ES module import metadata
11
+ * Parsed from component scripts, used for deterministic bundling
12
+ */
13
+ export interface ScriptImport {
14
+ source: string // Module specifier, e.g. 'gsap'
15
+ specifiers: string // Import clause, e.g. '{ gsap }' or 'gsap' or ''
16
+ typeOnly: boolean // TypeScript type-only import
17
+ sideEffect: boolean // Side-effect import (no specifiers)
18
+ }
19
+
9
20
  /**
10
21
  * Component Script IR - represents a component's script block
11
22
  * Used for collecting and bundling component scripts
@@ -15,6 +26,7 @@ export type ComponentScriptIR = {
15
26
  script: string // Raw script content
16
27
  props: string[] // Declared props
17
28
  scriptAttributes: Record<string, string> // Script attributes (setup, lang)
29
+ imports: ScriptImport[] // Parsed npm imports for bundling
18
30
  }
19
31
 
20
32
  export type ZenIR = {
@@ -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
+ }
@@ -4,14 +4,14 @@
4
4
  * Phase 4: Transform ZenIR into runtime-ready JavaScript code with full reactivity
5
5
  */
6
6
 
7
- import type { ZenIR } from '../ir/types'
7
+ import type { ZenIR, ScriptImport } from '../ir/types'
8
8
  import { generateExpressionWrappers } from './wrapExpression'
9
9
  import { generateDOMFunction } from './generateDOM'
10
10
  import { generateHydrationRuntime, generateExpressionRegistry } from './generateHydrationBundle'
11
11
  import { analyzeAllExpressions } from './dataExposure'
12
12
  import { generateNavigationRuntime } from './navigation'
13
13
  import { extractStateDeclarations, extractProps, transformStateDeclarations } from '../parse/scriptAnalysis'
14
- import { transformAllComponentScripts } from '../transform/componentScriptTransformer'
14
+ import { transformAllComponentScripts, emitImports } from '../transform/componentScriptTransformer'
15
15
 
16
16
  export interface RuntimeCode {
17
17
  expressions: string // Expression wrapper functions
@@ -26,7 +26,7 @@ export interface RuntimeCode {
26
26
  /**
27
27
  * Transform ZenIR into runtime JavaScript code
28
28
  */
29
- export function transformIR(ir: ZenIR): RuntimeCode {
29
+ export async function transformIR(ir: ZenIR): Promise<RuntimeCode> {
30
30
  // Phase 6: Analyze expression dependencies for explicit data exposure
31
31
  const expressionDependencies = analyzeAllExpressions(
32
32
  ir.template.expressions,
@@ -71,8 +71,8 @@ export function transformIR(ir: ZenIR): 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 componentScriptCode = 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({
@@ -83,7 +83,8 @@ export function transformIR(ir: ZenIR): RuntimeCode {
83
83
  stylesCode,
84
84
  scriptCode,
85
85
  stateInitCode,
86
- componentScriptCode // Component factories
86
+ componentScriptCode: componentScriptResult.code,
87
+ npmImports: componentScriptResult.imports
87
88
  })
88
89
 
89
90
  return {
@@ -109,14 +110,20 @@ function generateRuntimeBundle(parts: {
109
110
  scriptCode: string
110
111
  stateInitCode: string
111
112
  componentScriptCode: string // Component factories
113
+ npmImports: ScriptImport[] // Structured npm imports from component scripts
112
114
  }): string {
113
115
  // Extract function declarations from script code to register on window
114
116
  const functionRegistrations = extractFunctionRegistrations(parts.scriptCode)
115
117
 
118
+ // Generate npm imports header (hoisted, deduplicated, deterministic)
119
+ const npmImportsHeader = parts.npmImports.length > 0
120
+ ? `// NPM Imports (hoisted from component scripts)\n${emitImports(parts.npmImports)}\n\n`
121
+ : ''
122
+
116
123
  return `// Zenith Runtime Bundle (Phase 5)
117
124
  // Generated at compile time - no .zen parsing in browser
118
125
 
119
- ${parts.expressions}
126
+ ${npmImportsHeader}${parts.expressions}
120
127
 
121
128
  ${parts.expressionRegistry}
122
129