@zenithbuild/core 0.6.2 → 1.0.1

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 (87) 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 +182 -103
  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 +7465 -19128
  23. package/dist/zen-dev.js +7465 -19128
  24. package/dist/zen-preview.js +7465 -19128
  25. package/dist/zenith.js +7465 -19128
  26. package/package.json +21 -22
  27. package/cli/utils/content.ts +0 -112
  28. package/compiler/README.md +0 -380
  29. package/compiler/build-analyzer.ts +0 -122
  30. package/compiler/css/index.ts +0 -317
  31. package/compiler/discovery/componentDiscovery.ts +0 -178
  32. package/compiler/discovery/layouts.ts +0 -70
  33. package/compiler/errors/compilerError.ts +0 -56
  34. package/compiler/finalize/finalizeOutput.ts +0 -192
  35. package/compiler/finalize/generateFinalBundle.ts +0 -82
  36. package/compiler/index.ts +0 -83
  37. package/compiler/ir/types.ts +0 -174
  38. package/compiler/output/types.ts +0 -34
  39. package/compiler/parse/detectMapExpressions.ts +0 -102
  40. package/compiler/parse/importTypes.ts +0 -78
  41. package/compiler/parse/parseImports.ts +0 -309
  42. package/compiler/parse/parseScript.ts +0 -46
  43. package/compiler/parse/parseTemplate.ts +0 -599
  44. package/compiler/parse/parseZenFile.ts +0 -66
  45. package/compiler/parse/scriptAnalysis.ts +0 -91
  46. package/compiler/parse/trackLoopContext.ts +0 -82
  47. package/compiler/runtime/dataExposure.ts +0 -317
  48. package/compiler/runtime/generateDOM.ts +0 -246
  49. package/compiler/runtime/generateHydrationBundle.ts +0 -407
  50. package/compiler/runtime/hydration.ts +0 -309
  51. package/compiler/runtime/navigation.ts +0 -432
  52. package/compiler/runtime/thinRuntime.ts +0 -160
  53. package/compiler/runtime/transformIR.ts +0 -370
  54. package/compiler/runtime/wrapExpression.ts +0 -95
  55. package/compiler/runtime/wrapExpressionWithLoop.ts +0 -83
  56. package/compiler/spa-build.ts +0 -917
  57. package/compiler/ssg-build.ts +0 -422
  58. package/compiler/test/validate-test.ts +0 -104
  59. package/compiler/transform/classifyExpression.ts +0 -444
  60. package/compiler/transform/componentResolver.ts +0 -312
  61. package/compiler/transform/componentScriptTransformer.ts +0 -303
  62. package/compiler/transform/expressionTransformer.ts +0 -385
  63. package/compiler/transform/fragmentLowering.ts +0 -634
  64. package/compiler/transform/generateBindings.ts +0 -47
  65. package/compiler/transform/generateHTML.ts +0 -28
  66. package/compiler/transform/layoutProcessor.ts +0 -132
  67. package/compiler/transform/slotResolver.ts +0 -292
  68. package/compiler/transform/transformNode.ts +0 -126
  69. package/compiler/transform/transformTemplate.ts +0 -38
  70. package/compiler/validate/invariants.ts +0 -292
  71. package/compiler/validate/validateExpressions.ts +0 -168
  72. package/core/config/index.ts +0 -16
  73. package/core/config/loader.ts +0 -69
  74. package/core/config/types.ts +0 -89
  75. package/core/plugins/index.ts +0 -7
  76. package/core/plugins/registry.ts +0 -81
  77. package/dist/cli.js +0 -11665
  78. package/router/manifest.ts +0 -314
  79. package/router/navigation/ZenLink.zen +0 -231
  80. package/router/navigation/index.ts +0 -78
  81. package/router/navigation/zen-link.ts +0 -584
  82. package/router/runtime.ts +0 -458
  83. package/router/types.ts +0 -168
  84. package/runtime/build.ts +0 -17
  85. package/runtime/bundle-generator.ts +0 -1247
  86. package/runtime/client-runtime.ts +0 -549
  87. package/runtime/serve.ts +0 -93
@@ -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
- }