@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.
- 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/runtime/transformIR.ts +14 -7
- package/compiler/spa-build.ts +5 -5
- package/compiler/ssg-build.ts +6 -6
- package/compiler/transform/componentScriptTransformer.ts +172 -36
- package/dist/cli.js +4 -0
- package/dist/zen-build.js +233 -54
- package/dist/zen-dev.js +233 -54
- package/dist/zen-preview.js +233 -54
- package/dist/zenith.js +233 -54
- package/package.json +4 -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 = {
|
|
@@ -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 (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
|
|
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
|
|
package/compiler/spa-build.ts
CHANGED
|
@@ -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)
|
package/compiler/ssg-build.ts
CHANGED
|
@@ -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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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
|
|
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
|
-
):
|
|
54
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
):
|
|
255
|
+
): Promise<TransformAllResult> {
|
|
131
256
|
if (!componentScripts || componentScripts.length === 0) {
|
|
132
|
-
return ''
|
|
257
|
+
return { code: '', imports: [] }
|
|
133
258
|
}
|
|
134
259
|
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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