@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.
@@ -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,18 @@
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 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
+ *
12
+ * Import handling:
13
+ * - .zen imports: Stripped (compile-time resolved)
14
+ * - npm imports: Stored as structured metadata for later bundling
12
15
  */
13
16
 
14
- import type { ComponentScriptIR } from '../ir/types'
17
+ import type { ComponentScriptIR, ScriptImport } from '../ir/types'
18
+ import { parseImports, categorizeImports } from '../parse/parseImports'
19
+ import type { ParsedImport } from '../parse/importTypes'
15
20
 
16
21
  /**
17
22
  * Namespace bindings - destructured from the instance
@@ -38,32 +43,128 @@ const ZEN_PREFIX_MAPPINGS: Record<string, string> = {
38
43
  'zenOnUnmount': 'onUnmount',
39
44
  }
40
45
 
46
+ /**
47
+ * Result of script transformation including extracted imports
48
+ */
49
+ export interface TransformResult {
50
+ script: string // Transformed script (imports removed)
51
+ imports: ScriptImport[] // Structured npm imports to hoist
52
+ }
53
+
54
+ /**
55
+ * Convert ParsedImport to ScriptImport for compatibility with existing IR
56
+ */
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
+ }
73
+
74
+ return {
75
+ source: parsed.source,
76
+ specifiers,
77
+ typeOnly: parsed.isTypeOnly,
78
+ sideEffect: parsed.kind === 'side-effect'
79
+ }
80
+ }
81
+
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
91
+
92
+ // Sort by start position descending for safe removal
93
+ const sorted = [...imports].sort((a, b) => b.location.start - a.location.start)
94
+
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)
100
+
101
+ // Also remove trailing newline if present
102
+ const trimmedAfter = after.startsWith('\n') ? after.slice(1) : after
103
+ result = before + trimmedAfter
104
+ }
105
+
106
+ return result
107
+ }
108
+
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)
127
+
128
+ if (!parseResult.success) {
129
+ console.warn(`[Zenith] Import parse warnings for ${componentName}:`, parseResult.errors)
130
+ }
131
+
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)
137
+
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)
144
+
145
+ return {
146
+ imports: scriptImports,
147
+ strippedCode
148
+ }
149
+ }
150
+
41
151
  /**
42
152
  * Transform a component's script content for instance-scoped execution
43
153
  *
44
154
  * @param componentName - Name of the component
45
155
  * @param scriptContent - Raw script content from the component
46
156
  * @param props - Declared prop names
47
- * @returns Transformed script ready for bundling
157
+ * @returns TransformResult with transformed script and extracted imports
48
158
  */
49
159
  export function transformComponentScript(
50
160
  componentName: string,
51
161
  scriptContent: string,
52
162
  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
- )
163
+ ): TransformResult {
164
+ // Parse and extract imports using Acorn AST
165
+ const { imports, strippedCode } = parseAndExtractImports(scriptContent, componentName)
61
166
 
62
- // Strip any other relative imports (components are inlined)
63
- transformed = transformed.replace(
64
- /import\s+{[^}]*}\s+from\s+['"][^'"]+['"];?\s*/g,
65
- ''
66
- )
167
+ let transformed = strippedCode
67
168
 
68
169
  // Rewrite zen* prefixed calls to unprefixed (uses namespace bindings)
69
170
  for (const [zenName, unprefixedName] of Object.entries(ZEN_PREFIX_MAPPINGS)) {
@@ -72,7 +173,10 @@ export function transformComponentScript(
72
173
  transformed = transformed.replace(regex, `${unprefixedName}(`)
73
174
  }
74
175
 
75
- return transformed.trim()
176
+ return {
177
+ script: transformed.trim(),
178
+ imports
179
+ }
76
180
  }
77
181
 
78
182
  /**
@@ -119,29 +223,81 @@ __zenith.defineComponent('${componentName}', function(props, rootElement) {
119
223
  `
120
224
  }
121
225
 
226
+ /**
227
+ * Result of transforming all component scripts
228
+ */
229
+ export interface TransformAllResult {
230
+ code: string // Combined factory code
231
+ imports: ScriptImport[] // All collected npm imports (deduplicated)
232
+ }
233
+
234
+ /**
235
+ * Deduplicate imports by (source + specifiers + typeOnly) tuple
236
+ * Returns deterministically sorted imports
237
+ */
238
+ function deduplicateImports(imports: ScriptImport[]): ScriptImport[] {
239
+ const seen = new Map<string, ScriptImport>()
240
+
241
+ for (const imp of imports) {
242
+ const key = `${imp.source}|${imp.specifiers}|${imp.typeOnly}`
243
+ if (!seen.has(key)) {
244
+ seen.set(key, imp)
245
+ }
246
+ }
247
+
248
+ // Sort by source for deterministic output
249
+ return Array.from(seen.values()).sort((a, b) => a.source.localeCompare(b.source))
250
+ }
251
+
252
+ /**
253
+ * Emit import statements from structured metadata
254
+ */
255
+ export function emitImports(imports: ScriptImport[]): string {
256
+ const deduplicated = deduplicateImports(imports)
257
+
258
+ return deduplicated.map(imp => {
259
+ if (imp.sideEffect) {
260
+ return `import '${imp.source}';`
261
+ }
262
+ const typePrefix = imp.typeOnly ? 'type ' : ''
263
+ return `import ${typePrefix}${imp.specifiers} from '${imp.source}';`
264
+ }).join('\n')
265
+ }
266
+
122
267
  /**
123
268
  * Transform all component scripts from collected ComponentScriptIR
124
269
  *
270
+ * Now synchronous since Acorn parsing is synchronous.
271
+ *
125
272
  * @param componentScripts - Array of component script IRs
126
- * @returns Combined JavaScript code for all component factories
273
+ * @returns TransformAllResult with combined code and deduplicated imports
127
274
  */
128
275
  export function transformAllComponentScripts(
129
276
  componentScripts: ComponentScriptIR[]
130
- ): string {
277
+ ): TransformAllResult {
131
278
  if (!componentScripts || componentScripts.length === 0) {
132
- return ''
279
+ return { code: '', imports: [] }
133
280
  }
134
281
 
282
+ const allImports: ScriptImport[] = []
283
+
135
284
  const factories = componentScripts
136
285
  .filter(comp => comp.script && comp.script.trim().length > 0)
137
286
  .map(comp => {
138
- const transformed = transformComponentScript(
287
+ const result = transformComponentScript(
139
288
  comp.name,
140
289
  comp.script,
141
290
  comp.props
142
291
  )
143
- return generateComponentFactory(comp.name, transformed, comp.props)
292
+
293
+ // Collect imports
294
+ allImports.push(...result.imports)
295
+
296
+ return generateComponentFactory(comp.name, result.script, comp.props)
144
297
  })
145
298
 
146
- return factories.join('\n')
299
+ return {
300
+ code: factories.join('\n'),
301
+ imports: deduplicateImports(allImports)
302
+ }
147
303
  }
package/dist/cli.js CHANGED
@@ -13,6 +13,11 @@
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
20
+ #!/usr/bin/env bun
16
21
  // @bun
17
22
  var __create = Object.create;
18
23
  var __getProtoOf = Object.getPrototypeOf;