@zenithbuild/core 0.6.2 → 0.6.4

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 (112) hide show
  1. package/CORE_CONTRACT.md +145 -0
  2. package/README.md +14 -29
  3. package/bin/zenith.js +89 -0
  4. package/package.json +39 -56
  5. package/src/config.js +136 -0
  6. package/src/core-template.js +30 -0
  7. package/src/errors.js +54 -0
  8. package/src/guards.js +61 -0
  9. package/src/hash.js +52 -0
  10. package/src/index.js +26 -0
  11. package/src/ir/index.js +1 -0
  12. package/src/order.js +69 -0
  13. package/src/path.js +131 -0
  14. package/src/schema.js +28 -0
  15. package/src/version.js +67 -0
  16. package/bin/zen-build.ts +0 -2
  17. package/bin/zen-dev.ts +0 -2
  18. package/bin/zen-preview.ts +0 -2
  19. package/bin/zenith.ts +0 -2
  20. package/cli/commands/add.ts +0 -37
  21. package/cli/commands/build.ts +0 -37
  22. package/cli/commands/create.ts +0 -702
  23. package/cli/commands/dev.ts +0 -388
  24. package/cli/commands/index.ts +0 -112
  25. package/cli/commands/preview.ts +0 -62
  26. package/cli/commands/remove.ts +0 -33
  27. package/cli/index.ts +0 -10
  28. package/cli/main.ts +0 -101
  29. package/cli/utils/branding.ts +0 -178
  30. package/cli/utils/content.ts +0 -112
  31. package/cli/utils/logger.ts +0 -46
  32. package/cli/utils/plugin-manager.ts +0 -114
  33. package/cli/utils/project.ts +0 -77
  34. package/compiler/README.md +0 -380
  35. package/compiler/build-analyzer.ts +0 -122
  36. package/compiler/css/index.ts +0 -317
  37. package/compiler/discovery/componentDiscovery.ts +0 -178
  38. package/compiler/discovery/layouts.ts +0 -70
  39. package/compiler/errors/compilerError.ts +0 -56
  40. package/compiler/finalize/finalizeOutput.ts +0 -192
  41. package/compiler/finalize/generateFinalBundle.ts +0 -82
  42. package/compiler/index.ts +0 -83
  43. package/compiler/ir/types.ts +0 -174
  44. package/compiler/output/types.ts +0 -34
  45. package/compiler/parse/detectMapExpressions.ts +0 -102
  46. package/compiler/parse/importTypes.ts +0 -78
  47. package/compiler/parse/parseImports.ts +0 -309
  48. package/compiler/parse/parseScript.ts +0 -46
  49. package/compiler/parse/parseTemplate.ts +0 -599
  50. package/compiler/parse/parseZenFile.ts +0 -66
  51. package/compiler/parse/scriptAnalysis.ts +0 -91
  52. package/compiler/parse/trackLoopContext.ts +0 -82
  53. package/compiler/runtime/dataExposure.ts +0 -317
  54. package/compiler/runtime/generateDOM.ts +0 -246
  55. package/compiler/runtime/generateHydrationBundle.ts +0 -407
  56. package/compiler/runtime/hydration.ts +0 -309
  57. package/compiler/runtime/navigation.ts +0 -432
  58. package/compiler/runtime/thinRuntime.ts +0 -160
  59. package/compiler/runtime/transformIR.ts +0 -370
  60. package/compiler/runtime/wrapExpression.ts +0 -95
  61. package/compiler/runtime/wrapExpressionWithLoop.ts +0 -83
  62. package/compiler/spa-build.ts +0 -917
  63. package/compiler/ssg-build.ts +0 -422
  64. package/compiler/test/validate-test.ts +0 -104
  65. package/compiler/transform/classifyExpression.ts +0 -444
  66. package/compiler/transform/componentResolver.ts +0 -312
  67. package/compiler/transform/componentScriptTransformer.ts +0 -303
  68. package/compiler/transform/expressionTransformer.ts +0 -385
  69. package/compiler/transform/fragmentLowering.ts +0 -634
  70. package/compiler/transform/generateBindings.ts +0 -47
  71. package/compiler/transform/generateHTML.ts +0 -28
  72. package/compiler/transform/layoutProcessor.ts +0 -132
  73. package/compiler/transform/slotResolver.ts +0 -292
  74. package/compiler/transform/transformNode.ts +0 -126
  75. package/compiler/transform/transformTemplate.ts +0 -38
  76. package/compiler/validate/invariants.ts +0 -292
  77. package/compiler/validate/validateExpressions.ts +0 -168
  78. package/core/config/index.ts +0 -16
  79. package/core/config/loader.ts +0 -69
  80. package/core/config/types.ts +0 -89
  81. package/core/index.ts +0 -135
  82. package/core/lifecycle/index.ts +0 -49
  83. package/core/lifecycle/zen-mount.ts +0 -182
  84. package/core/lifecycle/zen-unmount.ts +0 -88
  85. package/core/plugins/index.ts +0 -7
  86. package/core/plugins/registry.ts +0 -81
  87. package/core/reactivity/index.ts +0 -54
  88. package/core/reactivity/tracking.ts +0 -167
  89. package/core/reactivity/zen-batch.ts +0 -57
  90. package/core/reactivity/zen-effect.ts +0 -139
  91. package/core/reactivity/zen-memo.ts +0 -146
  92. package/core/reactivity/zen-ref.ts +0 -52
  93. package/core/reactivity/zen-signal.ts +0 -121
  94. package/core/reactivity/zen-state.ts +0 -180
  95. package/core/reactivity/zen-untrack.ts +0 -44
  96. package/dist/cli.js +0 -11665
  97. package/dist/zen-build.js +0 -21172
  98. package/dist/zen-dev.js +0 -21172
  99. package/dist/zen-preview.js +0 -21172
  100. package/dist/zenith.js +0 -21172
  101. package/router/index.ts +0 -28
  102. package/router/manifest.ts +0 -314
  103. package/router/navigation/ZenLink.zen +0 -231
  104. package/router/navigation/index.ts +0 -78
  105. package/router/navigation/zen-link.ts +0 -584
  106. package/router/runtime.ts +0 -458
  107. package/router/types.ts +0 -168
  108. package/runtime/build.ts +0 -17
  109. package/runtime/bundle-generator.ts +0 -1247
  110. package/runtime/client-runtime.ts +0 -549
  111. package/runtime/serve.ts +0 -93
  112. package/tsconfig.json +0 -28
@@ -1,312 +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 } 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 and script 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 AND scripts 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
- return {
50
- ...ir,
51
- template: {
52
- ...ir.template,
53
- nodes: resolvedNodes
54
- },
55
- // Merge component styles with existing page styles
56
- styles: [...ir.styles, ...componentStyles],
57
- // Add component scripts for bundling
58
- componentScripts: [...(ir.componentScripts || []), ...componentScripts]
59
- }
60
- }
61
-
62
- /**
63
- * Resolve component nodes in a list of template nodes
64
- */
65
- function resolveComponentsInNodes(
66
- nodes: TemplateNode[],
67
- components: Map<string, ComponentMetadata>,
68
- depth: number = 0
69
- ): TemplateNode[] {
70
- const resolved: TemplateNode[] = []
71
-
72
- for (const node of nodes) {
73
- const resolvedNode = resolveComponentNode(node, components, depth)
74
-
75
- if (Array.isArray(resolvedNode)) {
76
- resolved.push(...resolvedNode)
77
- } else {
78
- resolved.push(resolvedNode)
79
- }
80
- }
81
-
82
- return resolved
83
- }
84
-
85
- /**
86
- * Resolve a single component node
87
- *
88
- * If the node is a component, look up its definition and inline it with slot resolution.
89
- * Otherwise, recursively process children.
90
- */
91
- function resolveComponentNode(
92
- node: TemplateNode,
93
- components: Map<string, ComponentMetadata>,
94
- depth: number = 0
95
- ): TemplateNode | TemplateNode[] {
96
- // Handle component nodes
97
- if (node.type === 'component') {
98
- return resolveComponent(node, components, depth)
99
- }
100
-
101
- // Handle element nodes - recursively resolve children
102
- if (node.type === 'element') {
103
- const resolvedChildren = resolveComponentsInNodes(node.children, components, depth + 1)
104
-
105
- return {
106
- ...node,
107
- children: resolvedChildren
108
- }
109
- }
110
-
111
- // Text and expression nodes pass through unchanged
112
- return node
113
- }
114
-
115
- /**
116
- * Get base component name from compound name
117
- *
118
- * "Card.Header" -> "Card"
119
- * "Button" -> "Button"
120
- */
121
- function getBaseComponentName(name: string): string {
122
- const dotIndex = name.indexOf('.')
123
- return dotIndex > 0 ? name.slice(0, dotIndex) : name
124
- }
125
-
126
- /**
127
- * Check if a component name is a compound slot marker
128
- *
129
- * "Card.Header" -> true (if Card exists)
130
- * "Card" -> false
131
- * "Button" -> false
132
- */
133
- function isCompoundSlotMarker(name: string, components: Map<string, ComponentMetadata>): boolean {
134
- const dotIndex = name.indexOf('.')
135
- if (dotIndex <= 0) return false
136
-
137
- const baseName = name.slice(0, dotIndex)
138
- return components.has(baseName)
139
- }
140
-
141
- /**
142
- * Resolve a component by inlining its template with slot substitution
143
- */
144
- function resolveComponent(
145
- componentNode: ComponentNode,
146
- components: Map<string, ComponentMetadata>,
147
- depth: number = 0
148
- ): TemplateNode | TemplateNode[] {
149
- const componentName = componentNode.name
150
-
151
- // Check if this is a compound slot marker (Card.Header, Card.Footer)
152
- // These are handled by the parent component, not resolved directly
153
- // INV007: Orphan compound slot markers are a compile-time error
154
- if (isCompoundSlotMarker(componentName, components)) {
155
- throwOrphanCompoundError(
156
- componentName,
157
- getBaseComponentName(componentName),
158
- 'component', // filePath not available here, will be caught by caller
159
- componentNode.location.line,
160
- componentNode.location.column
161
- )
162
- }
163
-
164
- // Look up component metadata
165
- const componentMeta = components.get(componentName)
166
-
167
- // INV003: Unresolved components are a compile-time error
168
- if (!componentMeta) {
169
- throwUnresolvedComponentError(
170
- componentName,
171
- 'component', // filePath not available here, will be caught by caller
172
- componentNode.location.line,
173
- componentNode.location.column
174
- )
175
- }
176
-
177
- // Track this component as used (for style collection)
178
- usedComponents.add(componentName)
179
-
180
- // Extract slots from component children FIRST (before resolving nested components)
181
- // This preserves compound component structure (Card.Header, Card.Footer)
182
- // IMPORTANT: Pass parent's loopContext to preserve reactive scope
183
- // Components are purely structural - they don't create new reactive boundaries
184
- const slots = extractSlotsFromChildren(
185
- componentName,
186
- componentNode.children,
187
- componentNode.loopContext // Preserve parent's reactive scope
188
- )
189
-
190
- // Now resolve nested components within the extracted slot content
191
- const resolvedSlots = {
192
- default: resolveComponentsInNodes(slots.default, components, depth + 1),
193
- named: new Map<string, TemplateNode[]>(),
194
- parentLoopContext: slots.parentLoopContext // Carry through the parent scope
195
- }
196
-
197
- for (const [slotName, slotContent] of slots.named) {
198
- resolvedSlots.named.set(slotName, resolveComponentsInNodes(slotContent, components, depth + 1))
199
- }
200
-
201
- // Deep clone the component template nodes to avoid mutation
202
- const templateNodes = JSON.parse(JSON.stringify(componentMeta.nodes)) as TemplateNode[]
203
-
204
- // Resolve slots in component template
205
- const resolvedTemplate = resolveSlots(templateNodes, resolvedSlots)
206
-
207
- // Forward attributes from component usage to the root element
208
- // Also adds data-zen-component marker for hydration-driven instantiation
209
- const forwardedTemplate = forwardAttributesToRoot(
210
- resolvedTemplate,
211
- componentNode.attributes,
212
- componentNode.loopContext,
213
- componentMeta.hasScript ? componentName : undefined // Only mark if component has script
214
- )
215
-
216
- // Recursively resolve any nested components in the resolved template
217
- const fullyResolved = resolveComponentsInNodes(forwardedTemplate, components, depth + 1)
218
-
219
- return fullyResolved
220
- }
221
-
222
- /**
223
- * Forward attributes from component usage to the template's root element
224
- *
225
- * When using <Button onclick="increment">Text</Button>,
226
- * the onclick should be applied to the <button> element in Button.zen template.
227
- *
228
- * Also adds data-zen-component marker if componentName is provided,
229
- * enabling hydration-driven instantiation.
230
- */
231
- function forwardAttributesToRoot(
232
- nodes: TemplateNode[],
233
- attributes: ComponentNode['attributes'],
234
- loopContext?: LoopContext,
235
- componentName?: string // If provided, adds hydration marker
236
- ): TemplateNode[] {
237
- // Find the first non-text element (the root element)
238
- const rootIndex = nodes.findIndex(n => n.type === 'element')
239
- if (rootIndex === -1) {
240
- return nodes
241
- }
242
-
243
- const root = nodes[rootIndex] as ElementNode
244
-
245
- // Start with existing attributes
246
- const mergedAttributes = [...root.attributes]
247
-
248
- // Add component hydration marker if this component has a script
249
- if (componentName) {
250
- mergedAttributes.push({
251
- name: 'data-zen-component',
252
- value: componentName,
253
- location: { line: 0, column: 0 }
254
- })
255
- }
256
-
257
- // Forward attributes from component usage
258
- for (const attr of attributes) {
259
- const existingIndex = mergedAttributes.findIndex(a => a.name === attr.name)
260
-
261
- // Attach parent's loopContext to forwarded attributes to preserve reactivity
262
- const forwardedAttr = {
263
- ...attr,
264
- loopContext: attr.loopContext || loopContext
265
- }
266
-
267
- if (existingIndex >= 0) {
268
- const existingAttr = mergedAttributes[existingIndex]!
269
- // Special handling for class: merge classes
270
- if (attr.name === 'class' && typeof attr.value === 'string' && typeof existingAttr.value === 'string') {
271
- mergedAttributes[existingIndex] = {
272
- ...existingAttr,
273
- value: `${existingAttr.value} ${attr.value}`
274
- }
275
- } else {
276
- // Override other attributes
277
- mergedAttributes[existingIndex] = forwardedAttr
278
- }
279
- } else {
280
- // Add new attribute
281
- mergedAttributes.push(forwardedAttr)
282
- }
283
- }
284
-
285
- // Return updated nodes with root element having merged attributes
286
- return [
287
- ...nodes.slice(0, rootIndex),
288
- {
289
- ...root,
290
- attributes: mergedAttributes,
291
- loopContext: root.loopContext || loopContext
292
- },
293
- ...nodes.slice(rootIndex + 1)
294
- ]
295
- }
296
-
297
- /**
298
- * Check if an IR contains any component nodes
299
- */
300
- export function hasComponents(nodes: TemplateNode[]): boolean {
301
- function checkNode(node: TemplateNode): boolean {
302
- if (node.type === 'component') {
303
- return true
304
- }
305
- if (node.type === 'element') {
306
- return node.children.some(checkNode)
307
- }
308
- return false
309
- }
310
-
311
- return nodes.some(checkNode)
312
- }
@@ -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
- }