@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.
- package/cli/commands/dev.ts +7 -6
- package/compiler/parse/importTypes.ts +78 -0
- package/compiler/parse/parseImports.ts +309 -0
- package/compiler/runtime/transformIR.ts +2 -2
- package/compiler/transform/componentScriptTransformer.ts +111 -91
- package/dist/cli.js +2 -0
- package/dist/zen-build.js +5665 -305
- package/dist/zen-dev.js +5665 -305
- package/dist/zen-preview.js +5665 -305
- package/dist/zenith.js +5665 -305
- package/package.json +3 -1
package/cli/commands/dev.ts
CHANGED
|
@@ -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
|
-
//
|
|
44
|
-
const tempDir =
|
|
45
|
-
|
|
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 (
|
|
75
|
-
const componentScriptResult =
|
|
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({
|