@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.
- package/cli/commands/dev.ts +58 -6
- package/compiler/finalize/finalizeOutput.ts +6 -6
- package/compiler/index.ts +8 -7
- package/compiler/ir/types.ts +12 -0
- package/compiler/parse/importTypes.ts +78 -0
- package/compiler/parse/parseImports.ts +309 -0
- package/compiler/runtime/transformIR.ts +14 -7
- package/compiler/spa-build.ts +5 -5
- package/compiler/ssg-build.ts +6 -6
- package/compiler/transform/componentScriptTransformer.ts +182 -26
- package/dist/cli.js +5 -0
- package/dist/zen-build.js +5736 -199
- package/dist/zen-dev.js +5736 -199
- package/dist/zen-preview.js +5736 -199
- package/dist/zenith.js +5736 -199
- package/package.json +6 -2
package/cli/commands/dev.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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 =
|
|
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
|
+
|
package/compiler/ir/types.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|