@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/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,18 @@
|
|
|
5
5
|
* Uses namespace binding pattern for cleaner output:
|
|
6
6
|
* const { signal, effect, onMount, ... } = __inst;
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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
|
-
):
|
|
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
|
-
)
|
|
163
|
+
): TransformResult {
|
|
164
|
+
// Parse and extract imports using Acorn AST
|
|
165
|
+
const { imports, strippedCode } = parseAndExtractImports(scriptContent, componentName)
|
|
61
166
|
|
|
62
|
-
|
|
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
|
|
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
|
|
273
|
+
* @returns TransformAllResult with combined code and deduplicated imports
|
|
127
274
|
*/
|
|
128
275
|
export function transformAllComponentScripts(
|
|
129
276
|
componentScripts: ComponentScriptIR[]
|
|
130
|
-
):
|
|
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
|
|
287
|
+
const result = transformComponentScript(
|
|
139
288
|
comp.name,
|
|
140
289
|
comp.script,
|
|
141
290
|
comp.props
|
|
142
291
|
)
|
|
143
|
-
|
|
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
|
|
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;
|