@zenithbuild/core 0.4.7 → 0.5.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 = {
@@ -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 (async)
75
+ const componentScriptResult = await 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
 
@@ -43,11 +43,11 @@ interface SPABuildOptions {
43
43
  /**
44
44
  * Compile a single page file
45
45
  */
46
- function compilePage(
46
+ async function compilePage(
47
47
  pagePath: string,
48
48
  pagesDir: string,
49
49
  baseDir: string = process.cwd()
50
- ): CompiledPage {
50
+ ): Promise<CompiledPage> {
51
51
  try {
52
52
  const layoutsDir = path.join(baseDir, 'app', 'layouts')
53
53
  const layouts = discoverLayouts(layoutsDir)
@@ -63,7 +63,7 @@ function compilePage(
63
63
  }
64
64
 
65
65
  // Use new compiler pipeline on the processed source
66
- const result = compileZenSource(processedSource, pagePath)
66
+ const result = await compileZenSource(processedSource, pagePath)
67
67
 
68
68
  if (!result.finalized) {
69
69
  throw new Error(`Compilation failed: No finalized output`)
@@ -843,7 +843,7 @@ function generateHTMLShell(
843
843
  /**
844
844
  * Build SPA from pages directory
845
845
  */
846
- export function buildSPA(options: SPABuildOptions): void {
846
+ export async function buildSPA(options: SPABuildOptions): Promise<void> {
847
847
  const { pagesDir, outDir, baseDir } = options
848
848
 
849
849
  // Clean output directory
@@ -870,7 +870,7 @@ export function buildSPA(options: SPABuildOptions): void {
870
870
  console.log(`[Zenith Build] Compiling: ${path.relative(pagesDir, pageFile)}`)
871
871
 
872
872
  try {
873
- const compiled = compilePage(pageFile, pagesDir)
873
+ const compiled = await compilePage(pageFile, pagesDir)
874
874
  compiledPages.push(compiled)
875
875
  } catch (error) {
876
876
  console.error(`[Zenith Build] Error compiling ${pageFile}:`, error)
@@ -65,11 +65,11 @@ export interface SSGBuildOptions {
65
65
  /**
66
66
  * Compile a single page file for SSG output
67
67
  */
68
- function compilePage(
68
+ async function compilePage(
69
69
  pagePath: string,
70
70
  pagesDir: string,
71
71
  baseDir: string = process.cwd()
72
- ): CompiledPage {
72
+ ): Promise<CompiledPage> {
73
73
  const source = fs.readFileSync(pagePath, 'utf-8')
74
74
 
75
75
  // Analyze page requirements
@@ -88,7 +88,7 @@ function compilePage(
88
88
  }
89
89
 
90
90
  // Compile with new pipeline
91
- const result = compileZenSource(processedSource, pagePath)
91
+ const result = await compileZenSource(processedSource, pagePath)
92
92
 
93
93
  if (!result.finalized) {
94
94
  throw new Error(`Compilation failed for ${pagePath}: No finalized output`)
@@ -239,7 +239,7 @@ ${page.pageScript}
239
239
  /**
240
240
  * Build all pages using SSG approach
241
241
  */
242
- export function buildSSG(options: SSGBuildOptions): void {
242
+ export async function buildSSG(options: SSGBuildOptions): Promise<void> {
243
243
  const { pagesDir, outDir, baseDir = path.dirname(pagesDir) } = options
244
244
  const contentDir = path.join(baseDir, 'content')
245
245
  const contentData = loadContent(contentDir)
@@ -275,7 +275,7 @@ export function buildSSG(options: SSGBuildOptions): void {
275
275
  console.log(` Compiling: ${relativePath}`)
276
276
 
277
277
  try {
278
- const compiled = compilePage(pageFile, pagesDir, baseDir)
278
+ const compiled = await compilePage(pageFile, pagesDir, baseDir)
279
279
  compiledPages.push(compiled)
280
280
 
281
281
  if (compiled.analysis.needsHydration) {
@@ -356,7 +356,7 @@ export function buildSSG(options: SSGBuildOptions): void {
356
356
  const custom404Path = path.join(pagesDir, candidate)
357
357
  if (fs.existsSync(custom404Path)) {
358
358
  try {
359
- const compiled = compilePage(custom404Path, pagesDir, baseDir)
359
+ const compiled = await compilePage(custom404Path, pagesDir, baseDir)
360
360
  const html = generatePageHTML(compiled, globalStyles, contentData)
361
361
  fs.writeFileSync(path.join(outDir, '404.html'), html)
362
362
  console.log('📦 Generated 404.html (custom)')
@@ -5,13 +5,24 @@
5
5
  * Uses namespace binding pattern for cleaner output:
6
6
  * const { signal, effect, onMount, ... } = __inst;
7
7
  *
8
- * Then rewrites zen* prefixed calls to unprefixed:
9
- * zenSignal(v) → signal(v)
10
- * zenEffect(fn) effect(fn)
11
- * zenOnMount(fn) → onMount(fn)
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
11
+ *
12
+ * IMPORTANT: No regex-based import parsing.
12
13
  */
13
14
 
14
- import type { ComponentScriptIR } from '../ir/types'
15
+ import { init, parse } from 'es-module-lexer'
16
+ 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
+ }
15
26
 
16
27
  /**
17
28
  * Namespace bindings - destructured from the instance
@@ -38,32 +49,102 @@ const ZEN_PREFIX_MAPPINGS: Record<string, string> = {
38
49
  'zenOnUnmount': 'onUnmount',
39
50
  }
40
51
 
52
+ /**
53
+ * Result of script transformation including extracted imports
54
+ */
55
+ export interface TransformResult {
56
+ script: string // Transformed script (imports removed)
57
+ imports: ScriptImport[] // Structured npm imports to hoist
58
+ }
59
+
60
+ /**
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
65
+ */
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)
74
+
75
+ // Sort imports by start position (descending) for safe removal
76
+ const sortedImports = [...parsedImports].sort((a, b) => b.ss - a.ss)
77
+
78
+ let strippedCode = scriptContent
79
+
80
+ for (const imp of sortedImports) {
81
+ const source = imp.n || '' // Module specifier
82
+ const importStatement = scriptContent.slice(imp.ss, imp.se)
83
+
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
+ }
89
+
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
+ }
95
+
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
+ }
110
+
111
+ imports.push({
112
+ source,
113
+ specifiers,
114
+ typeOnly: isTypeOnly,
115
+ sideEffect: isSideEffect
116
+ })
117
+
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)
120
+ }
121
+
122
+ // Clean up any leftover empty lines from stripped imports
123
+ strippedCode = strippedCode.replace(/^\s*\n/gm, '')
124
+
125
+ // Reverse imports array since we processed in reverse order
126
+ imports.reverse()
127
+
128
+ return { imports, strippedCode }
129
+ }
130
+
41
131
  /**
42
132
  * Transform a component's script content for instance-scoped execution
43
133
  *
44
134
  * @param componentName - Name of the component
45
135
  * @param scriptContent - Raw script content from the component
46
136
  * @param props - Declared prop names
47
- * @returns Transformed script ready for bundling
137
+ * @returns TransformResult with transformed script and extracted imports
48
138
  */
49
- export function transformComponentScript(
139
+ export async function transformComponentScript(
50
140
  componentName: string,
51
141
  scriptContent: string,
52
142
  props: string[]
53
- ): string {
54
- let transformed = scriptContent
55
-
56
- // Strip import statements for .zen files (resolved at compile time)
57
- transformed = transformed.replace(
58
- /import\s+\w+\s+from\s+['"][^'"]*\.zen['"];?\s*/g,
59
- ''
60
- )
143
+ ): Promise<TransformResult> {
144
+ // Parse and extract imports using es-module-lexer
145
+ const { imports, strippedCode } = await parseAndExtractImports(scriptContent)
61
146
 
62
- // Strip any other relative imports (components are inlined)
63
- transformed = transformed.replace(
64
- /import\s+{[^}]*}\s+from\s+['"][^'"]+['"];?\s*/g,
65
- ''
66
- )
147
+ let transformed = strippedCode
67
148
 
68
149
  // Rewrite zen* prefixed calls to unprefixed (uses namespace bindings)
69
150
  for (const [zenName, unprefixedName] of Object.entries(ZEN_PREFIX_MAPPINGS)) {
@@ -72,7 +153,10 @@ export function transformComponentScript(
72
153
  transformed = transformed.replace(regex, `${unprefixedName}(`)
73
154
  }
74
155
 
75
- return transformed.trim()
156
+ return {
157
+ script: transformed.trim(),
158
+ imports
159
+ }
76
160
  }
77
161
 
78
162
  /**
@@ -119,29 +203,81 @@ __zenith.defineComponent('${componentName}', function(props, rootElement) {
119
203
  `
120
204
  }
121
205
 
206
+ /**
207
+ * Result of transforming all component scripts
208
+ */
209
+ export interface TransformAllResult {
210
+ code: string // Combined factory code
211
+ imports: ScriptImport[] // All collected npm imports (deduplicated)
212
+ }
213
+
214
+ /**
215
+ * Deduplicate imports by (source + specifiers + typeOnly) tuple
216
+ * Returns deterministically sorted imports
217
+ */
218
+ function deduplicateImports(imports: ScriptImport[]): ScriptImport[] {
219
+ const seen = new Map<string, ScriptImport>()
220
+
221
+ for (const imp of imports) {
222
+ const key = `${imp.source}|${imp.specifiers}|${imp.typeOnly}`
223
+ if (!seen.has(key)) {
224
+ seen.set(key, imp)
225
+ }
226
+ }
227
+
228
+ // Sort by source for deterministic output
229
+ return Array.from(seen.values()).sort((a, b) => a.source.localeCompare(b.source))
230
+ }
231
+
232
+ /**
233
+ * Emit import statements from structured metadata
234
+ */
235
+ export function emitImports(imports: ScriptImport[]): string {
236
+ const deduplicated = deduplicateImports(imports)
237
+
238
+ return deduplicated.map(imp => {
239
+ if (imp.sideEffect) {
240
+ return `import '${imp.source}';`
241
+ }
242
+ const typePrefix = imp.typeOnly ? 'type ' : ''
243
+ return `import ${typePrefix}${imp.specifiers} from '${imp.source}';`
244
+ }).join('\n')
245
+ }
246
+
122
247
  /**
123
248
  * Transform all component scripts from collected ComponentScriptIR
124
249
  *
125
250
  * @param componentScripts - Array of component script IRs
126
- * @returns Combined JavaScript code for all component factories
251
+ * @returns TransformAllResult with combined code and deduplicated imports
127
252
  */
128
- export function transformAllComponentScripts(
253
+ export async function transformAllComponentScripts(
129
254
  componentScripts: ComponentScriptIR[]
130
- ): string {
255
+ ): Promise<TransformAllResult> {
131
256
  if (!componentScripts || componentScripts.length === 0) {
132
- return ''
257
+ return { code: '', imports: [] }
133
258
  }
134
259
 
135
- const factories = componentScripts
136
- .filter(comp => comp.script && comp.script.trim().length > 0)
137
- .map(comp => {
138
- const transformed = transformComponentScript(
139
- comp.name,
140
- comp.script,
141
- comp.props
142
- )
143
- return generateComponentFactory(comp.name, transformed, comp.props)
144
- })
260
+ const allImports: ScriptImport[] = []
261
+
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)
145
274
 
146
- return factories.join('\n')
275
+ return generateComponentFactory(comp.name, result.script, comp.props)
276
+ })
277
+ )
278
+
279
+ return {
280
+ code: factories.join('\n'),
281
+ imports: deduplicateImports(allImports)
282
+ }
147
283
  }
package/dist/cli.js CHANGED
@@ -13,6 +13,10 @@
13
13
  #!/usr/bin/env bun
14
14
  #!/usr/bin/env bun
15
15
  #!/usr/bin/env bun
16
+ #!/usr/bin/env bun
17
+ #!/usr/bin/env bun
18
+ #!/usr/bin/env bun
19
+ #!/usr/bin/env bun
16
20
  // @bun
17
21
  var __create = Object.create;
18
22
  var __getProtoOf = Object.getPrototypeOf;