@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.
@@ -1,6 +1,5 @@
1
1
  import path from 'path'
2
2
  import fs from 'fs'
3
- import os from 'os'
4
3
  import { serve, type ServerWebSocket } from 'bun'
5
4
  import { requireProject } from '../utils/project'
6
5
  import * as logger from '../utils/logger'
@@ -40,21 +39,23 @@ async function bundlePageScript(script: string, projectRoot: string): Promise<st
40
39
  return script
41
40
  }
42
41
 
43
- // Create a temporary file for bundling
44
- const tempDir = os.tmpdir()
45
- const tempFile = path.join(tempDir, `zenith-bundle-${Date.now()}.js`)
42
+ // Write temp file in PROJECT directory so Bun can find node_modules
43
+ const tempDir = path.join(projectRoot, '.zenith-cache')
44
+ if (!fs.existsSync(tempDir)) {
45
+ fs.mkdirSync(tempDir, { recursive: true })
46
+ }
47
+ const tempFile = path.join(tempDir, `bundle-${Date.now()}.js`)
46
48
 
47
49
  try {
48
50
  // Write script to temp file
49
51
  fs.writeFileSync(tempFile, script, 'utf-8')
50
52
 
51
- // Use Bun.build to bundle with npm resolution
53
+ // Use Bun.build to bundle with npm resolution from project's node_modules
52
54
  const result = await Bun.build({
53
55
  entrypoints: [tempFile],
54
56
  target: 'browser',
55
57
  format: 'esm',
56
58
  minify: false,
57
- // Resolve modules from the project's node_modules
58
59
  external: [], // Bundle everything
59
60
  })
60
61
 
@@ -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({