@zenithbuild/core 1.2.2 → 1.2.3
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/README.md +20 -19
- package/cli/commands/add.ts +2 -2
- package/cli/commands/build.ts +2 -3
- package/cli/commands/dev.ts +93 -73
- package/cli/commands/index.ts +1 -1
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/remove.ts +2 -2
- package/cli/index.ts +1 -1
- package/cli/main.ts +1 -1
- package/cli/utils/logger.ts +1 -1
- package/cli/utils/plugin-manager.ts +1 -1
- package/cli/utils/project.ts +4 -4
- package/core/components/ErrorPage.zen +218 -0
- package/core/components/index.ts +15 -0
- package/core/config.ts +1 -0
- package/core/index.ts +29 -0
- package/dist/compiler-native-frej59m4.node +0 -0
- package/dist/core/compiler-native-frej59m4.node +0 -0
- package/dist/core/index.js +6293 -0
- package/dist/runtime/lifecycle/index.js +1 -0
- package/dist/runtime/reactivity/index.js +1 -0
- package/dist/zen-build.js +1 -20118
- package/dist/zen-dev.js +1 -20118
- package/dist/zen-preview.js +1 -20118
- package/dist/zenith.js +1 -20118
- package/package.json +11 -20
- package/compiler/README.md +0 -380
- package/compiler/build-analyzer.ts +0 -122
- package/compiler/css/index.ts +0 -317
- package/compiler/discovery/componentDiscovery.ts +0 -242
- package/compiler/discovery/layouts.ts +0 -70
- package/compiler/errors/compilerError.ts +0 -56
- package/compiler/finalize/finalizeOutput.ts +0 -192
- package/compiler/finalize/generateFinalBundle.ts +0 -82
- package/compiler/index.ts +0 -83
- package/compiler/ir/types.ts +0 -174
- package/compiler/output/types.ts +0 -48
- package/compiler/parse/detectMapExpressions.ts +0 -102
- package/compiler/parse/importTypes.ts +0 -78
- package/compiler/parse/parseImports.ts +0 -309
- package/compiler/parse/parseScript.ts +0 -46
- package/compiler/parse/parseTemplate.ts +0 -628
- package/compiler/parse/parseZenFile.ts +0 -66
- package/compiler/parse/scriptAnalysis.ts +0 -91
- package/compiler/parse/trackLoopContext.ts +0 -82
- package/compiler/runtime/dataExposure.ts +0 -332
- package/compiler/runtime/generateDOM.ts +0 -255
- package/compiler/runtime/generateHydrationBundle.ts +0 -407
- package/compiler/runtime/hydration.ts +0 -309
- package/compiler/runtime/navigation.ts +0 -432
- package/compiler/runtime/thinRuntime.ts +0 -160
- package/compiler/runtime/transformIR.ts +0 -406
- package/compiler/runtime/wrapExpression.ts +0 -114
- package/compiler/runtime/wrapExpressionWithLoop.ts +0 -97
- package/compiler/spa-build.ts +0 -917
- package/compiler/ssg-build.ts +0 -486
- package/compiler/test/component-stacking.test.ts +0 -365
- package/compiler/test/map-lowering.test.ts +0 -130
- package/compiler/test/validate-test.ts +0 -104
- package/compiler/transform/classifyExpression.ts +0 -444
- package/compiler/transform/componentResolver.ts +0 -350
- package/compiler/transform/componentScriptTransformer.ts +0 -303
- package/compiler/transform/expressionTransformer.ts +0 -385
- package/compiler/transform/fragmentLowering.ts +0 -819
- package/compiler/transform/generateBindings.ts +0 -68
- package/compiler/transform/generateHTML.ts +0 -28
- package/compiler/transform/layoutProcessor.ts +0 -132
- package/compiler/transform/slotResolver.ts +0 -292
- package/compiler/transform/transformNode.ts +0 -314
- package/compiler/transform/transformTemplate.ts +0 -38
- package/compiler/validate/invariants.ts +0 -292
- package/compiler/validate/validateExpressions.ts +0 -168
- package/core/config/index.ts +0 -18
- package/core/config/loader.ts +0 -69
- package/core/config/types.ts +0 -119
- package/core/plugins/bridge.ts +0 -193
- package/core/plugins/index.ts +0 -7
- package/core/plugins/registry.ts +0 -126
- package/dist/cli.js +0 -11675
- package/runtime/build.ts +0 -17
- package/runtime/bundle-generator.ts +0 -1266
- package/runtime/client-runtime.ts +0 -891
- package/runtime/serve.ts +0 -93
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Component Resolution
|
|
3
|
-
*
|
|
4
|
-
* Resolves component nodes in IR by inlining component templates with slot substitution.
|
|
5
|
-
* Uses compound component pattern for named slots (Card.Header, Card.Footer).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { TemplateNode, ComponentNode, ElementNode, ZenIR, LoopContext, ComponentScriptIR, ExpressionIR } from '../ir/types'
|
|
9
|
-
import type { ComponentMetadata } from '../discovery/componentDiscovery'
|
|
10
|
-
import { extractSlotsFromChildren, resolveSlots } from './slotResolver'
|
|
11
|
-
import { throwOrphanCompoundError, throwUnresolvedComponentError } from '../validate/invariants'
|
|
12
|
-
|
|
13
|
-
// Track which components have been used (for style, script, and expression collection)
|
|
14
|
-
const usedComponents = new Set<string>()
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Resolve all component nodes in a template IR
|
|
18
|
-
*
|
|
19
|
-
* Recursively replaces ComponentNode instances with their resolved templates
|
|
20
|
-
* Also collects styles, scripts, AND expressions from used components and adds them to the IR
|
|
21
|
-
*/
|
|
22
|
-
export function resolveComponentsInIR(
|
|
23
|
-
ir: ZenIR,
|
|
24
|
-
components: Map<string, ComponentMetadata>
|
|
25
|
-
): ZenIR {
|
|
26
|
-
// Clear used components tracking for this compilation
|
|
27
|
-
usedComponents.clear()
|
|
28
|
-
|
|
29
|
-
// Resolve components in template nodes
|
|
30
|
-
const resolvedNodes = resolveComponentsInNodes(ir.template.nodes, components)
|
|
31
|
-
|
|
32
|
-
// Collect styles from all used components
|
|
33
|
-
const componentStyles = Array.from(usedComponents)
|
|
34
|
-
.map(name => components.get(name))
|
|
35
|
-
.filter((meta): meta is ComponentMetadata => meta !== undefined && meta.styles.length > 0)
|
|
36
|
-
.flatMap(meta => meta.styles.map(raw => ({ raw })))
|
|
37
|
-
|
|
38
|
-
// Collect scripts from all used components (for bundling)
|
|
39
|
-
const componentScripts: ComponentScriptIR[] = Array.from(usedComponents)
|
|
40
|
-
.map(name => components.get(name))
|
|
41
|
-
.filter((meta): meta is ComponentMetadata => meta !== undefined && meta.script !== null)
|
|
42
|
-
.map(meta => ({
|
|
43
|
-
name: meta.name,
|
|
44
|
-
script: meta.script!,
|
|
45
|
-
props: meta.props,
|
|
46
|
-
scriptAttributes: meta.scriptAttributes || {}
|
|
47
|
-
}))
|
|
48
|
-
|
|
49
|
-
// Collect expressions from all used components (critical for rendering)
|
|
50
|
-
// Component templates may contain expression nodes that reference expression IDs
|
|
51
|
-
// These IDs must be present in the IR's expressions array for transformation to work
|
|
52
|
-
const componentExpressions: ExpressionIR[] = Array.from(usedComponents)
|
|
53
|
-
.map(name => components.get(name))
|
|
54
|
-
.filter((meta): meta is ComponentMetadata => meta !== undefined && meta.expressions?.length > 0)
|
|
55
|
-
.flatMap(meta => meta.expressions)
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
...ir,
|
|
59
|
-
template: {
|
|
60
|
-
...ir.template,
|
|
61
|
-
nodes: resolvedNodes,
|
|
62
|
-
// Merge component expressions with existing page expressions
|
|
63
|
-
expressions: [...ir.template.expressions, ...componentExpressions]
|
|
64
|
-
},
|
|
65
|
-
// Merge component styles with existing page styles
|
|
66
|
-
styles: [...ir.styles, ...componentStyles],
|
|
67
|
-
// Add component scripts for bundling
|
|
68
|
-
componentScripts: [...(ir.componentScripts || []), ...componentScripts]
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Resolve component nodes in a list of template nodes
|
|
74
|
-
*/
|
|
75
|
-
function resolveComponentsInNodes(
|
|
76
|
-
nodes: TemplateNode[],
|
|
77
|
-
components: Map<string, ComponentMetadata>,
|
|
78
|
-
depth: number = 0
|
|
79
|
-
): TemplateNode[] {
|
|
80
|
-
const resolved: TemplateNode[] = []
|
|
81
|
-
|
|
82
|
-
for (const node of nodes) {
|
|
83
|
-
const resolvedNode = resolveComponentNode(node, components, depth)
|
|
84
|
-
|
|
85
|
-
if (Array.isArray(resolvedNode)) {
|
|
86
|
-
resolved.push(...resolvedNode)
|
|
87
|
-
} else {
|
|
88
|
-
resolved.push(resolvedNode)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return resolved
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Resolve a single component node
|
|
97
|
-
*
|
|
98
|
-
* If the node is a component, look up its definition and inline it with slot resolution.
|
|
99
|
-
* Otherwise, recursively process children.
|
|
100
|
-
*/
|
|
101
|
-
function resolveComponentNode(
|
|
102
|
-
node: TemplateNode,
|
|
103
|
-
components: Map<string, ComponentMetadata>,
|
|
104
|
-
depth: number = 0
|
|
105
|
-
): TemplateNode | TemplateNode[] {
|
|
106
|
-
// Handle component nodes
|
|
107
|
-
if (node.type === 'component') {
|
|
108
|
-
return resolveComponent(node, components, depth)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Handle element nodes - recursively resolve children
|
|
112
|
-
if (node.type === 'element') {
|
|
113
|
-
const resolvedChildren = resolveComponentsInNodes(node.children, components, depth + 1)
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
...node,
|
|
117
|
-
children: resolvedChildren
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Handle loop-fragment nodes - recursively resolve body
|
|
122
|
-
if (node.type === 'loop-fragment') {
|
|
123
|
-
const loopNode = node as import('../ir/types').LoopFragmentNode
|
|
124
|
-
return {
|
|
125
|
-
...loopNode,
|
|
126
|
-
body: resolveComponentsInNodes(loopNode.body, components, depth + 1)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Handle conditional-fragment nodes - recursively resolve both branches
|
|
131
|
-
if (node.type === 'conditional-fragment') {
|
|
132
|
-
const condNode = node as import('../ir/types').ConditionalFragmentNode
|
|
133
|
-
return {
|
|
134
|
-
...condNode,
|
|
135
|
-
consequent: resolveComponentsInNodes(condNode.consequent, components, depth + 1),
|
|
136
|
-
alternate: resolveComponentsInNodes(condNode.alternate, components, depth + 1)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Handle optional-fragment nodes - recursively resolve fragment
|
|
141
|
-
if (node.type === 'optional-fragment') {
|
|
142
|
-
const optNode = node as import('../ir/types').OptionalFragmentNode
|
|
143
|
-
return {
|
|
144
|
-
...optNode,
|
|
145
|
-
fragment: resolveComponentsInNodes(optNode.fragment, components, depth + 1)
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Text and expression nodes pass through unchanged
|
|
150
|
-
return node
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Get base component name from compound name
|
|
155
|
-
*
|
|
156
|
-
* "Card.Header" -> "Card"
|
|
157
|
-
* "Button" -> "Button"
|
|
158
|
-
*/
|
|
159
|
-
function getBaseComponentName(name: string): string {
|
|
160
|
-
const dotIndex = name.indexOf('.')
|
|
161
|
-
return dotIndex > 0 ? name.slice(0, dotIndex) : name
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Check if a component name is a compound slot marker
|
|
166
|
-
*
|
|
167
|
-
* "Card.Header" -> true (if Card exists)
|
|
168
|
-
* "Card" -> false
|
|
169
|
-
* "Button" -> false
|
|
170
|
-
*/
|
|
171
|
-
function isCompoundSlotMarker(name: string, components: Map<string, ComponentMetadata>): boolean {
|
|
172
|
-
const dotIndex = name.indexOf('.')
|
|
173
|
-
if (dotIndex <= 0) return false
|
|
174
|
-
|
|
175
|
-
const baseName = name.slice(0, dotIndex)
|
|
176
|
-
return components.has(baseName)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Resolve a component by inlining its template with slot substitution
|
|
181
|
-
*/
|
|
182
|
-
function resolveComponent(
|
|
183
|
-
componentNode: ComponentNode,
|
|
184
|
-
components: Map<string, ComponentMetadata>,
|
|
185
|
-
depth: number = 0
|
|
186
|
-
): TemplateNode | TemplateNode[] {
|
|
187
|
-
const componentName = componentNode.name
|
|
188
|
-
|
|
189
|
-
// Check if this is a compound slot marker (Card.Header, Card.Footer)
|
|
190
|
-
// These are handled by the parent component, not resolved directly
|
|
191
|
-
// INV007: Orphan compound slot markers are a compile-time error
|
|
192
|
-
if (isCompoundSlotMarker(componentName, components)) {
|
|
193
|
-
throwOrphanCompoundError(
|
|
194
|
-
componentName,
|
|
195
|
-
getBaseComponentName(componentName),
|
|
196
|
-
'component', // filePath not available here, will be caught by caller
|
|
197
|
-
componentNode.location.line,
|
|
198
|
-
componentNode.location.column
|
|
199
|
-
)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Look up component metadata
|
|
203
|
-
const componentMeta = components.get(componentName)
|
|
204
|
-
|
|
205
|
-
// INV003: Unresolved components are a compile-time error
|
|
206
|
-
if (!componentMeta) {
|
|
207
|
-
throwUnresolvedComponentError(
|
|
208
|
-
componentName,
|
|
209
|
-
'component', // filePath not available here, will be caught by caller
|
|
210
|
-
componentNode.location.line,
|
|
211
|
-
componentNode.location.column
|
|
212
|
-
)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Track this component as used (for style collection)
|
|
216
|
-
usedComponents.add(componentName)
|
|
217
|
-
|
|
218
|
-
// Extract slots from component children FIRST (before resolving nested components)
|
|
219
|
-
// This preserves compound component structure (Card.Header, Card.Footer)
|
|
220
|
-
// IMPORTANT: Pass parent's loopContext to preserve reactive scope
|
|
221
|
-
// Components are purely structural - they don't create new reactive boundaries
|
|
222
|
-
const slots = extractSlotsFromChildren(
|
|
223
|
-
componentName,
|
|
224
|
-
componentNode.children,
|
|
225
|
-
componentNode.loopContext // Preserve parent's reactive scope
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
// Now resolve nested components within the extracted slot content
|
|
229
|
-
const resolvedSlots = {
|
|
230
|
-
default: resolveComponentsInNodes(slots.default, components, depth + 1),
|
|
231
|
-
named: new Map<string, TemplateNode[]>(),
|
|
232
|
-
parentLoopContext: slots.parentLoopContext // Carry through the parent scope
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
for (const [slotName, slotContent] of slots.named) {
|
|
236
|
-
resolvedSlots.named.set(slotName, resolveComponentsInNodes(slotContent, components, depth + 1))
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Deep clone the component template nodes to avoid mutation
|
|
240
|
-
const templateNodes = JSON.parse(JSON.stringify(componentMeta.nodes)) as TemplateNode[]
|
|
241
|
-
|
|
242
|
-
// Resolve slots in component template
|
|
243
|
-
const resolvedTemplate = resolveSlots(templateNodes, resolvedSlots)
|
|
244
|
-
|
|
245
|
-
// Forward attributes from component usage to the root element
|
|
246
|
-
// Also adds data-zen-component marker for hydration-driven instantiation
|
|
247
|
-
const forwardedTemplate = forwardAttributesToRoot(
|
|
248
|
-
resolvedTemplate,
|
|
249
|
-
componentNode.attributes,
|
|
250
|
-
componentNode.loopContext,
|
|
251
|
-
componentMeta.hasScript ? componentName : undefined // Only mark if component has script
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
// Recursively resolve any nested components in the resolved template
|
|
255
|
-
const fullyResolved = resolveComponentsInNodes(forwardedTemplate, components, depth + 1)
|
|
256
|
-
|
|
257
|
-
return fullyResolved
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Forward attributes from component usage to the template's root element
|
|
262
|
-
*
|
|
263
|
-
* When using <Button onclick="increment">Text</Button>,
|
|
264
|
-
* the onclick should be applied to the <button> element in Button.zen template.
|
|
265
|
-
*
|
|
266
|
-
* Also adds data-zen-component marker if componentName is provided,
|
|
267
|
-
* enabling hydration-driven instantiation.
|
|
268
|
-
*/
|
|
269
|
-
function forwardAttributesToRoot(
|
|
270
|
-
nodes: TemplateNode[],
|
|
271
|
-
attributes: ComponentNode['attributes'],
|
|
272
|
-
loopContext?: LoopContext,
|
|
273
|
-
componentName?: string // If provided, adds hydration marker
|
|
274
|
-
): TemplateNode[] {
|
|
275
|
-
// Find the first non-text element (the root element)
|
|
276
|
-
const rootIndex = nodes.findIndex(n => n.type === 'element')
|
|
277
|
-
if (rootIndex === -1) {
|
|
278
|
-
return nodes
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const root = nodes[rootIndex] as ElementNode
|
|
282
|
-
|
|
283
|
-
// Start with existing attributes
|
|
284
|
-
const mergedAttributes = [...root.attributes]
|
|
285
|
-
|
|
286
|
-
// Add component hydration marker if this component has a script
|
|
287
|
-
if (componentName) {
|
|
288
|
-
mergedAttributes.push({
|
|
289
|
-
name: 'data-zen-component',
|
|
290
|
-
value: componentName,
|
|
291
|
-
location: { line: 0, column: 0 }
|
|
292
|
-
})
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Forward attributes from component usage
|
|
296
|
-
for (const attr of attributes) {
|
|
297
|
-
const existingIndex = mergedAttributes.findIndex(a => a.name === attr.name)
|
|
298
|
-
|
|
299
|
-
// Attach parent's loopContext to forwarded attributes to preserve reactivity
|
|
300
|
-
const forwardedAttr = {
|
|
301
|
-
...attr,
|
|
302
|
-
loopContext: attr.loopContext || loopContext
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (existingIndex >= 0) {
|
|
306
|
-
const existingAttr = mergedAttributes[existingIndex]!
|
|
307
|
-
// Special handling for class: merge classes
|
|
308
|
-
if (attr.name === 'class' && typeof attr.value === 'string' && typeof existingAttr.value === 'string') {
|
|
309
|
-
mergedAttributes[existingIndex] = {
|
|
310
|
-
...existingAttr,
|
|
311
|
-
value: `${existingAttr.value} ${attr.value}`
|
|
312
|
-
}
|
|
313
|
-
} else {
|
|
314
|
-
// Override other attributes
|
|
315
|
-
mergedAttributes[existingIndex] = forwardedAttr
|
|
316
|
-
}
|
|
317
|
-
} else {
|
|
318
|
-
// Add new attribute
|
|
319
|
-
mergedAttributes.push(forwardedAttr)
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Return updated nodes with root element having merged attributes
|
|
324
|
-
return [
|
|
325
|
-
...nodes.slice(0, rootIndex),
|
|
326
|
-
{
|
|
327
|
-
...root,
|
|
328
|
-
attributes: mergedAttributes,
|
|
329
|
-
loopContext: root.loopContext || loopContext
|
|
330
|
-
},
|
|
331
|
-
...nodes.slice(rootIndex + 1)
|
|
332
|
-
]
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Check if an IR contains any component nodes
|
|
337
|
-
*/
|
|
338
|
-
export function hasComponents(nodes: TemplateNode[]): boolean {
|
|
339
|
-
function checkNode(node: TemplateNode): boolean {
|
|
340
|
-
if (node.type === 'component') {
|
|
341
|
-
return true
|
|
342
|
-
}
|
|
343
|
-
if (node.type === 'element') {
|
|
344
|
-
return node.children.some(checkNode)
|
|
345
|
-
}
|
|
346
|
-
return false
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return nodes.some(checkNode)
|
|
350
|
-
}
|
|
@@ -1,303 +0,0 @@
|
|
|
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 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
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type { ComponentScriptIR, ScriptImport } from '../ir/types'
|
|
18
|
-
import { parseImports, categorizeImports } from '../parse/parseImports'
|
|
19
|
-
import type { ParsedImport } from '../parse/importTypes'
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Namespace bindings - destructured from the instance
|
|
23
|
-
* This is added at the top of every component script
|
|
24
|
-
*/
|
|
25
|
-
const NAMESPACE_BINDINGS = `const {
|
|
26
|
-
signal, state, memo, effect, ref,
|
|
27
|
-
batch, untrack, onMount, onUnmount
|
|
28
|
-
} = __inst;`
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Mapping of zen* prefixed names to unprefixed names
|
|
32
|
-
* These get rewritten to use the destructured namespace
|
|
33
|
-
*/
|
|
34
|
-
const ZEN_PREFIX_MAPPINGS: Record<string, string> = {
|
|
35
|
-
'zenSignal': 'signal',
|
|
36
|
-
'zenState': 'state',
|
|
37
|
-
'zenMemo': 'memo',
|
|
38
|
-
'zenEffect': 'effect',
|
|
39
|
-
'zenRef': 'ref',
|
|
40
|
-
'zenBatch': 'batch',
|
|
41
|
-
'zenUntrack': 'untrack',
|
|
42
|
-
'zenOnMount': 'onMount',
|
|
43
|
-
'zenOnUnmount': 'onUnmount',
|
|
44
|
-
}
|
|
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
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Transform a component's script content for instance-scoped execution
|
|
153
|
-
*
|
|
154
|
-
* @param componentName - Name of the component
|
|
155
|
-
* @param scriptContent - Raw script content from the component
|
|
156
|
-
* @param props - Declared prop names
|
|
157
|
-
* @returns TransformResult with transformed script and extracted imports
|
|
158
|
-
*/
|
|
159
|
-
export function transformComponentScript(
|
|
160
|
-
componentName: string,
|
|
161
|
-
scriptContent: string,
|
|
162
|
-
props: string[]
|
|
163
|
-
): TransformResult {
|
|
164
|
-
// Parse and extract imports using Acorn AST
|
|
165
|
-
const { imports, strippedCode } = parseAndExtractImports(scriptContent, componentName)
|
|
166
|
-
|
|
167
|
-
let transformed = strippedCode
|
|
168
|
-
|
|
169
|
-
// Rewrite zen* prefixed calls to unprefixed (uses namespace bindings)
|
|
170
|
-
for (const [zenName, unprefixedName] of Object.entries(ZEN_PREFIX_MAPPINGS)) {
|
|
171
|
-
// Match the zen* name as a standalone call
|
|
172
|
-
const regex = new RegExp(`(?<!\\w)${zenName}\\s*\\(`, 'g')
|
|
173
|
-
transformed = transformed.replace(regex, `${unprefixedName}(`)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
script: transformed.trim(),
|
|
178
|
-
imports
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Generate a component factory function
|
|
184
|
-
*
|
|
185
|
-
* IMPORTANT: Factories are PASSIVE - they are registered but NOT invoked here.
|
|
186
|
-
* Instantiation is driven by the hydrator when it discovers component markers.
|
|
187
|
-
*
|
|
188
|
-
* @param componentName - Name of the component
|
|
189
|
-
* @param transformedScript - Script content after hook rewriting
|
|
190
|
-
* @param propNames - Declared prop names for destructuring
|
|
191
|
-
* @returns Component factory registration code (NO eager instantiation)
|
|
192
|
-
*/
|
|
193
|
-
export function generateComponentFactory(
|
|
194
|
-
componentName: string,
|
|
195
|
-
transformedScript: string,
|
|
196
|
-
propNames: string[]
|
|
197
|
-
): string {
|
|
198
|
-
const propsDestructure = propNames.length > 0
|
|
199
|
-
? `const { ${propNames.join(', ')} } = props || {};`
|
|
200
|
-
: ''
|
|
201
|
-
|
|
202
|
-
// Register factory only - NO instantiation
|
|
203
|
-
// Hydrator will call instantiate() when it finds data-zen-component markers
|
|
204
|
-
return `
|
|
205
|
-
// Component Factory: ${componentName}
|
|
206
|
-
// Instantiation is driven by hydrator, not by bundle load
|
|
207
|
-
__zenith.defineComponent('${componentName}', function(props, rootElement) {
|
|
208
|
-
const __inst = __zenith.createInstance('${componentName}', rootElement);
|
|
209
|
-
|
|
210
|
-
// Namespace bindings (instance-scoped primitives)
|
|
211
|
-
${NAMESPACE_BINDINGS}
|
|
212
|
-
|
|
213
|
-
${propsDestructure}
|
|
214
|
-
|
|
215
|
-
// Component script (instance-scoped)
|
|
216
|
-
${transformedScript}
|
|
217
|
-
|
|
218
|
-
// Execute mount lifecycle (rootElement is already in DOM)
|
|
219
|
-
__inst.mount();
|
|
220
|
-
|
|
221
|
-
return __inst;
|
|
222
|
-
});
|
|
223
|
-
`
|
|
224
|
-
}
|
|
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
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Transform all component scripts from collected ComponentScriptIR
|
|
269
|
-
*
|
|
270
|
-
* Now synchronous since Acorn parsing is synchronous.
|
|
271
|
-
*
|
|
272
|
-
* @param componentScripts - Array of component script IRs
|
|
273
|
-
* @returns TransformAllResult with combined code and deduplicated imports
|
|
274
|
-
*/
|
|
275
|
-
export function transformAllComponentScripts(
|
|
276
|
-
componentScripts: ComponentScriptIR[]
|
|
277
|
-
): TransformAllResult {
|
|
278
|
-
if (!componentScripts || componentScripts.length === 0) {
|
|
279
|
-
return { code: '', imports: [] }
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const allImports: ScriptImport[] = []
|
|
283
|
-
|
|
284
|
-
const factories = componentScripts
|
|
285
|
-
.filter(comp => comp.script && comp.script.trim().length > 0)
|
|
286
|
-
.map(comp => {
|
|
287
|
-
const result = transformComponentScript(
|
|
288
|
-
comp.name,
|
|
289
|
-
comp.script,
|
|
290
|
-
comp.props
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
// Collect imports
|
|
294
|
-
allImports.push(...result.imports)
|
|
295
|
-
|
|
296
|
-
return generateComponentFactory(comp.name, result.script, comp.props)
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
return {
|
|
300
|
-
code: factories.join('\n'),
|
|
301
|
-
imports: deduplicateImports(allImports)
|
|
302
|
-
}
|
|
303
|
-
}
|