@zenithbuild/core 0.4.6 → 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/discovery/componentDiscovery.ts +4 -0
- package/compiler/discovery/layouts.ts +11 -2
- package/compiler/finalize/finalizeOutput.ts +6 -6
- package/compiler/index.ts +9 -7
- package/compiler/ir/types.ts +24 -0
- package/compiler/parse/parseTemplate.ts +10 -2
- package/compiler/parse/scriptAnalysis.ts +8 -0
- package/compiler/runtime/transformIR.ts +32 -5
- package/compiler/spa-build.ts +5 -5
- package/compiler/ssg-build.ts +6 -6
- package/compiler/transform/componentResolver.ts +36 -13
- package/compiler/transform/componentScriptTransformer.ts +283 -0
- package/dist/cli.js +10 -0
- package/dist/zen-build.js +474 -50
- package/dist/zen-dev.js +474 -50
- package/dist/zen-preview.js +474 -50
- package/dist/zenith.js +474 -50
- package/package.json +4 -2
- package/runtime/bundle-generator.ts +144 -1
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Script Transformer
|
|
3
|
+
*
|
|
4
|
+
* Transforms component scripts for instance-scoped execution.
|
|
5
|
+
* Uses namespace binding pattern for cleaner output:
|
|
6
|
+
* const { signal, effect, onMount, ... } = __inst;
|
|
7
|
+
*
|
|
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.
|
|
13
|
+
*/
|
|
14
|
+
|
|
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
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Namespace bindings - destructured from the instance
|
|
29
|
+
* This is added at the top of every component script
|
|
30
|
+
*/
|
|
31
|
+
const NAMESPACE_BINDINGS = `const {
|
|
32
|
+
signal, state, memo, effect, ref,
|
|
33
|
+
batch, untrack, onMount, onUnmount
|
|
34
|
+
} = __inst;`
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Mapping of zen* prefixed names to unprefixed names
|
|
38
|
+
* These get rewritten to use the destructured namespace
|
|
39
|
+
*/
|
|
40
|
+
const ZEN_PREFIX_MAPPINGS: Record<string, string> = {
|
|
41
|
+
'zenSignal': 'signal',
|
|
42
|
+
'zenState': 'state',
|
|
43
|
+
'zenMemo': 'memo',
|
|
44
|
+
'zenEffect': 'effect',
|
|
45
|
+
'zenRef': 'ref',
|
|
46
|
+
'zenBatch': 'batch',
|
|
47
|
+
'zenUntrack': 'untrack',
|
|
48
|
+
'zenOnMount': 'onMount',
|
|
49
|
+
'zenOnUnmount': 'onUnmount',
|
|
50
|
+
}
|
|
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
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Transform a component's script content for instance-scoped execution
|
|
133
|
+
*
|
|
134
|
+
* @param componentName - Name of the component
|
|
135
|
+
* @param scriptContent - Raw script content from the component
|
|
136
|
+
* @param props - Declared prop names
|
|
137
|
+
* @returns TransformResult with transformed script and extracted imports
|
|
138
|
+
*/
|
|
139
|
+
export async function transformComponentScript(
|
|
140
|
+
componentName: string,
|
|
141
|
+
scriptContent: string,
|
|
142
|
+
props: string[]
|
|
143
|
+
): Promise<TransformResult> {
|
|
144
|
+
// Parse and extract imports using es-module-lexer
|
|
145
|
+
const { imports, strippedCode } = await parseAndExtractImports(scriptContent)
|
|
146
|
+
|
|
147
|
+
let transformed = strippedCode
|
|
148
|
+
|
|
149
|
+
// Rewrite zen* prefixed calls to unprefixed (uses namespace bindings)
|
|
150
|
+
for (const [zenName, unprefixedName] of Object.entries(ZEN_PREFIX_MAPPINGS)) {
|
|
151
|
+
// Match the zen* name as a standalone call
|
|
152
|
+
const regex = new RegExp(`(?<!\\w)${zenName}\\s*\\(`, 'g')
|
|
153
|
+
transformed = transformed.replace(regex, `${unprefixedName}(`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
script: transformed.trim(),
|
|
158
|
+
imports
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Generate a component factory function
|
|
164
|
+
*
|
|
165
|
+
* IMPORTANT: Factories are PASSIVE - they are registered but NOT invoked here.
|
|
166
|
+
* Instantiation is driven by the hydrator when it discovers component markers.
|
|
167
|
+
*
|
|
168
|
+
* @param componentName - Name of the component
|
|
169
|
+
* @param transformedScript - Script content after hook rewriting
|
|
170
|
+
* @param propNames - Declared prop names for destructuring
|
|
171
|
+
* @returns Component factory registration code (NO eager instantiation)
|
|
172
|
+
*/
|
|
173
|
+
export function generateComponentFactory(
|
|
174
|
+
componentName: string,
|
|
175
|
+
transformedScript: string,
|
|
176
|
+
propNames: string[]
|
|
177
|
+
): string {
|
|
178
|
+
const propsDestructure = propNames.length > 0
|
|
179
|
+
? `const { ${propNames.join(', ')} } = props || {};`
|
|
180
|
+
: ''
|
|
181
|
+
|
|
182
|
+
// Register factory only - NO instantiation
|
|
183
|
+
// Hydrator will call instantiate() when it finds data-zen-component markers
|
|
184
|
+
return `
|
|
185
|
+
// Component Factory: ${componentName}
|
|
186
|
+
// Instantiation is driven by hydrator, not by bundle load
|
|
187
|
+
__zenith.defineComponent('${componentName}', function(props, rootElement) {
|
|
188
|
+
const __inst = __zenith.createInstance('${componentName}', rootElement);
|
|
189
|
+
|
|
190
|
+
// Namespace bindings (instance-scoped primitives)
|
|
191
|
+
${NAMESPACE_BINDINGS}
|
|
192
|
+
|
|
193
|
+
${propsDestructure}
|
|
194
|
+
|
|
195
|
+
// Component script (instance-scoped)
|
|
196
|
+
${transformedScript}
|
|
197
|
+
|
|
198
|
+
// Execute mount lifecycle (rootElement is already in DOM)
|
|
199
|
+
__inst.mount();
|
|
200
|
+
|
|
201
|
+
return __inst;
|
|
202
|
+
});
|
|
203
|
+
`
|
|
204
|
+
}
|
|
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
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Transform all component scripts from collected ComponentScriptIR
|
|
249
|
+
*
|
|
250
|
+
* @param componentScripts - Array of component script IRs
|
|
251
|
+
* @returns TransformAllResult with combined code and deduplicated imports
|
|
252
|
+
*/
|
|
253
|
+
export async function transformAllComponentScripts(
|
|
254
|
+
componentScripts: ComponentScriptIR[]
|
|
255
|
+
): Promise<TransformAllResult> {
|
|
256
|
+
if (!componentScripts || componentScripts.length === 0) {
|
|
257
|
+
return { code: '', imports: [] }
|
|
258
|
+
}
|
|
259
|
+
|
|
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)
|
|
274
|
+
|
|
275
|
+
return generateComponentFactory(comp.name, result.script, comp.props)
|
|
276
|
+
})
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
code: factories.join('\n'),
|
|
281
|
+
imports: deduplicateImports(allImports)
|
|
282
|
+
}
|
|
283
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
#!/usr/bin/env bun
|
|
8
8
|
#!/usr/bin/env bun
|
|
9
9
|
#!/usr/bin/env bun
|
|
10
|
+
#!/usr/bin/env bun
|
|
11
|
+
#!/usr/bin/env bun
|
|
12
|
+
#!/usr/bin/env bun
|
|
13
|
+
#!/usr/bin/env bun
|
|
14
|
+
#!/usr/bin/env bun
|
|
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
|
|
10
20
|
// @bun
|
|
11
21
|
var __create = Object.create;
|
|
12
22
|
var __getProtoOf = Object.getPrototypeOf;
|