@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.
Files changed (83) hide show
  1. package/README.md +20 -19
  2. package/cli/commands/add.ts +2 -2
  3. package/cli/commands/build.ts +2 -3
  4. package/cli/commands/dev.ts +93 -73
  5. package/cli/commands/index.ts +1 -1
  6. package/cli/commands/preview.ts +1 -1
  7. package/cli/commands/remove.ts +2 -2
  8. package/cli/index.ts +1 -1
  9. package/cli/main.ts +1 -1
  10. package/cli/utils/logger.ts +1 -1
  11. package/cli/utils/plugin-manager.ts +1 -1
  12. package/cli/utils/project.ts +4 -4
  13. package/core/components/ErrorPage.zen +218 -0
  14. package/core/components/index.ts +15 -0
  15. package/core/config.ts +1 -0
  16. package/core/index.ts +29 -0
  17. package/dist/compiler-native-frej59m4.node +0 -0
  18. package/dist/core/compiler-native-frej59m4.node +0 -0
  19. package/dist/core/index.js +6293 -0
  20. package/dist/runtime/lifecycle/index.js +1 -0
  21. package/dist/runtime/reactivity/index.js +1 -0
  22. package/dist/zen-build.js +1 -20118
  23. package/dist/zen-dev.js +1 -20118
  24. package/dist/zen-preview.js +1 -20118
  25. package/dist/zenith.js +1 -20118
  26. package/package.json +11 -20
  27. package/compiler/README.md +0 -380
  28. package/compiler/build-analyzer.ts +0 -122
  29. package/compiler/css/index.ts +0 -317
  30. package/compiler/discovery/componentDiscovery.ts +0 -242
  31. package/compiler/discovery/layouts.ts +0 -70
  32. package/compiler/errors/compilerError.ts +0 -56
  33. package/compiler/finalize/finalizeOutput.ts +0 -192
  34. package/compiler/finalize/generateFinalBundle.ts +0 -82
  35. package/compiler/index.ts +0 -83
  36. package/compiler/ir/types.ts +0 -174
  37. package/compiler/output/types.ts +0 -48
  38. package/compiler/parse/detectMapExpressions.ts +0 -102
  39. package/compiler/parse/importTypes.ts +0 -78
  40. package/compiler/parse/parseImports.ts +0 -309
  41. package/compiler/parse/parseScript.ts +0 -46
  42. package/compiler/parse/parseTemplate.ts +0 -628
  43. package/compiler/parse/parseZenFile.ts +0 -66
  44. package/compiler/parse/scriptAnalysis.ts +0 -91
  45. package/compiler/parse/trackLoopContext.ts +0 -82
  46. package/compiler/runtime/dataExposure.ts +0 -332
  47. package/compiler/runtime/generateDOM.ts +0 -255
  48. package/compiler/runtime/generateHydrationBundle.ts +0 -407
  49. package/compiler/runtime/hydration.ts +0 -309
  50. package/compiler/runtime/navigation.ts +0 -432
  51. package/compiler/runtime/thinRuntime.ts +0 -160
  52. package/compiler/runtime/transformIR.ts +0 -406
  53. package/compiler/runtime/wrapExpression.ts +0 -114
  54. package/compiler/runtime/wrapExpressionWithLoop.ts +0 -97
  55. package/compiler/spa-build.ts +0 -917
  56. package/compiler/ssg-build.ts +0 -486
  57. package/compiler/test/component-stacking.test.ts +0 -365
  58. package/compiler/test/map-lowering.test.ts +0 -130
  59. package/compiler/test/validate-test.ts +0 -104
  60. package/compiler/transform/classifyExpression.ts +0 -444
  61. package/compiler/transform/componentResolver.ts +0 -350
  62. package/compiler/transform/componentScriptTransformer.ts +0 -303
  63. package/compiler/transform/expressionTransformer.ts +0 -385
  64. package/compiler/transform/fragmentLowering.ts +0 -819
  65. package/compiler/transform/generateBindings.ts +0 -68
  66. package/compiler/transform/generateHTML.ts +0 -28
  67. package/compiler/transform/layoutProcessor.ts +0 -132
  68. package/compiler/transform/slotResolver.ts +0 -292
  69. package/compiler/transform/transformNode.ts +0 -314
  70. package/compiler/transform/transformTemplate.ts +0 -38
  71. package/compiler/validate/invariants.ts +0 -292
  72. package/compiler/validate/validateExpressions.ts +0 -168
  73. package/core/config/index.ts +0 -18
  74. package/core/config/loader.ts +0 -69
  75. package/core/config/types.ts +0 -119
  76. package/core/plugins/bridge.ts +0 -193
  77. package/core/plugins/index.ts +0 -7
  78. package/core/plugins/registry.ts +0 -126
  79. package/dist/cli.js +0 -11675
  80. package/runtime/build.ts +0 -17
  81. package/runtime/bundle-generator.ts +0 -1266
  82. package/runtime/client-runtime.ts +0 -891
  83. 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
- }