@zenithbuild/core 0.4.6 → 0.4.7

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.
@@ -26,6 +26,8 @@ export interface ComponentMetadata {
26
26
  slots: SlotDefinition[]
27
27
  props: string[] // Declared props
28
28
  styles: string[] // Raw CSS from <style> blocks
29
+ script: string | null // Raw script content for bundling
30
+ scriptAttributes: Record<string, string> | null // Script attributes (setup, lang)
29
31
  hasScript: boolean
30
32
  hasStyles: boolean
31
33
  }
@@ -112,6 +114,8 @@ function parseComponentFile(filePath: string): ComponentMetadata | null {
112
114
  slots,
113
115
  props,
114
116
  styles,
117
+ script: ir.script?.raw || null, // Store raw script content
118
+ scriptAttributes: ir.script?.attributes || null, // Store script attributes
115
119
  hasScript: ir.script !== null,
116
120
  hasStyles: ir.styles.length > 0
117
121
  }
@@ -41,8 +41,17 @@ export function discoverLayouts(layoutsDir: string): Map<string, LayoutMetadata>
41
41
  if (match[1]) styles.push(match[1].trim())
42
42
  }
43
43
 
44
- // Extract HTML (everything except script/style)
45
- let html = source.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
44
+ // Extract HTML (everything except inline scripts/style)
45
+ // Preserve external script tags (<script src="...">) but remove inline <script setup> blocks
46
+ // Use a function-based replace to check for src attribute
47
+ let html = source.replace(/<script([^>]*)>([\s\S]*?)<\/script>/gi, (match, attrs, content) => {
48
+ // Keep script tags with src attribute (external scripts)
49
+ if (attrs.includes('src=')) {
50
+ return match;
51
+ }
52
+ // Remove inline scripts (those without src)
53
+ return '';
54
+ })
46
55
  html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '').trim()
47
56
 
48
57
  layouts.set(name, {
package/compiler/index.ts CHANGED
@@ -53,6 +53,7 @@ export function compileZenSource(
53
53
  filePath,
54
54
  template,
55
55
  script,
56
+ // componentScripts: [],
56
57
  styles
57
58
  }
58
59
 
@@ -6,11 +6,23 @@
6
6
  * without any runtime execution or transformation.
7
7
  */
8
8
 
9
+ /**
10
+ * Component Script IR - represents a component's script block
11
+ * Used for collecting and bundling component scripts
12
+ */
13
+ export type ComponentScriptIR = {
14
+ name: string // Component name (e.g., 'HeroSection')
15
+ script: string // Raw script content
16
+ props: string[] // Declared props
17
+ scriptAttributes: Record<string, string> // Script attributes (setup, lang)
18
+ }
19
+
9
20
  export type ZenIR = {
10
21
  filePath: string
11
22
  template: TemplateIR
12
23
  script: ScriptIR | null
13
24
  styles: StyleIR[]
25
+ componentScripts?: ComponentScriptIR[] // Scripts from used components
14
26
  }
15
27
 
16
28
  export type TemplateIR = {
@@ -22,10 +22,18 @@ function generateExpressionId(): string {
22
22
 
23
23
  /**
24
24
  * Strip script and style blocks from HTML before parsing
25
+ * Preserves external script tags (<script src="...">) but removes inline scripts
25
26
  */
26
27
  function stripBlocks(html: string): string {
27
- // Remove script blocks
28
- let stripped = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
28
+ // Remove only inline script blocks (those WITHOUT src attribute), preserve external scripts
29
+ let stripped = html.replace(/<script([^>]*)>([\s\S]*?)<\/script>/gi, (match, attrs, content) => {
30
+ // Keep script tags with src attribute (external scripts)
31
+ if (attrs.includes('src=')) {
32
+ return match;
33
+ }
34
+ // Remove inline scripts (those without src)
35
+ return '';
36
+ })
29
37
  // Remove style blocks
30
38
  stripped = stripped.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
31
39
  return stripped
@@ -45,6 +45,7 @@ export function extractProps(script: string): string[] {
45
45
 
46
46
  /**
47
47
  * Transform script by removing state and prop declarations
48
+ * Also strips .zen imports (resolved at compile time) and other compile-time-only imports
48
49
  */
49
50
  export function transformStateDeclarations(script: string): string {
50
51
  let transformed = script
@@ -62,6 +63,13 @@ export function transformStateDeclarations(script: string): string {
62
63
  // Remove zenith/runtime imports
63
64
  transformed = transformed.replace(/import\s+{[^}]+}\s+from\s+['"]zenith\/runtime['"]\s*;?[ \t]*/g, '')
64
65
 
66
+ // Remove .zen file imports (resolved at compile time)
67
+ // Matches: import Name from '.../file.zen'; or import Name from '.../file.zen'
68
+ transformed = transformed.replace(/import\s+\w+\s+from\s+['"][^'"]*\.zen['"];?\s*/g, '')
69
+
70
+ // Remove relative imports with destructuring (components are inlined)
71
+ transformed = transformed.replace(/import\s+{[^}]*}\s+from\s+['"][^'"]+\.zen['"];?\s*/g, '')
72
+
65
73
  // Transform zenith:content imports to global lookups
66
74
  transformed = transformed.replace(
67
75
  /import\s*{\s*([^}]+)\s*}\s*from\s*['"]zenith:content['"]\s*;?/g,
@@ -11,6 +11,7 @@ import { generateHydrationRuntime, generateExpressionRegistry } from './generate
11
11
  import { analyzeAllExpressions } from './dataExposure'
12
12
  import { generateNavigationRuntime } from './navigation'
13
13
  import { extractStateDeclarations, extractProps, transformStateDeclarations } from '../parse/scriptAnalysis'
14
+ import { transformAllComponentScripts } from '../transform/componentScriptTransformer'
14
15
 
15
16
  export interface RuntimeCode {
16
17
  expressions: string // Expression wrapper functions
@@ -70,6 +71,9 @@ export function transformIR(ir: ZenIR): RuntimeCode {
70
71
  // Transform script (remove state and prop declarations, they're handled by runtime)
71
72
  const scriptCode = transformStateDeclarations(scriptContent)
72
73
 
74
+ // Transform component scripts for instance-scoped execution
75
+ const componentScriptCode = transformAllComponentScripts(ir.componentScripts || [])
76
+
73
77
  // Generate complete runtime bundle
74
78
  const bundle = generateRuntimeBundle({
75
79
  expressions,
@@ -78,7 +82,8 @@ export function transformIR(ir: ZenIR): RuntimeCode {
78
82
  navigationRuntime,
79
83
  stylesCode,
80
84
  scriptCode,
81
- stateInitCode
85
+ stateInitCode,
86
+ componentScriptCode // Component factories
82
87
  })
83
88
 
84
89
  return {
@@ -103,6 +108,7 @@ function generateRuntimeBundle(parts: {
103
108
  stylesCode: string
104
109
  scriptCode: string
105
110
  stateInitCode: string
111
+ componentScriptCode: string // Component factories
106
112
  }): string {
107
113
  // Extract function declarations from script code to register on window
108
114
  const functionRegistrations = extractFunctionRegistrations(parts.scriptCode)
@@ -129,6 +135,9 @@ ${functionRegistrations}
129
135
  ${parts.stateInitCode ? `// State initialization
130
136
  ${parts.stateInitCode}` : ''}
131
137
 
138
+ ${parts.componentScriptCode ? `// Component factories (instance-scoped)
139
+ ${parts.componentScriptCode}` : ''}
140
+
132
141
  // Export hydration functions
133
142
  if (typeof window !== 'undefined') {
134
143
  window.zenithHydrate = window.__zenith_hydrate || function(state, container) {
@@ -187,10 +196,21 @@ if (typeof window !== 'undefined') {
187
196
  // Get the router outlet or body
188
197
  const container = document.querySelector('#app') || document.body;
189
198
 
190
- // Hydrate with state
199
+ // Hydrate with state (expressions, bindings)
191
200
  if (window.__zenith_hydrate) {
192
201
  window.__zenith_hydrate(state, {}, {}, {}, container);
193
202
  }
203
+
204
+ // Hydrate components by discovering data-zen-component markers
205
+ // This is the ONLY place component instantiation happens - driven by DOM markers
206
+ if (window.__zenith && window.__zenith.hydrateComponents) {
207
+ window.__zenith.hydrateComponents(container);
208
+ }
209
+
210
+ // Trigger page-level mount lifecycle
211
+ if (window.__zenith && window.__zenith.triggerMount) {
212
+ window.__zenith.triggerMount();
213
+ }
194
214
  }
195
215
 
196
216
  // Run on DOM ready
@@ -5,19 +5,19 @@
5
5
  * Uses compound component pattern for named slots (Card.Header, Card.Footer).
6
6
  */
7
7
 
8
- import type { TemplateNode, ComponentNode, ElementNode, ZenIR, LoopContext } from '../ir/types'
8
+ import type { TemplateNode, ComponentNode, ElementNode, ZenIR, LoopContext, ComponentScriptIR } from '../ir/types'
9
9
  import type { ComponentMetadata } from '../discovery/componentDiscovery'
10
10
  import { extractSlotsFromChildren, resolveSlots } from './slotResolver'
11
11
  import { throwOrphanCompoundError, throwUnresolvedComponentError } from '../validate/invariants'
12
12
 
13
- // Track which components have been used (for style collection)
13
+ // Track which components have been used (for style and script collection)
14
14
  const usedComponents = new Set<string>()
15
15
 
16
16
  /**
17
17
  * Resolve all component nodes in a template IR
18
18
  *
19
19
  * Recursively replaces ComponentNode instances with their resolved templates
20
- * Also collects styles from used components and adds them to the IR
20
+ * Also collects styles AND scripts from used components and adds them to the IR
21
21
  */
22
22
  export function resolveComponentsInIR(
23
23
  ir: ZenIR,
@@ -35,6 +35,17 @@ export function resolveComponentsInIR(
35
35
  .filter((meta): meta is ComponentMetadata => meta !== undefined && meta.styles.length > 0)
36
36
  .flatMap(meta => meta.styles.map(raw => ({ raw })))
37
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
+
38
49
  return {
39
50
  ...ir,
40
51
  template: {
@@ -42,7 +53,9 @@ export function resolveComponentsInIR(
42
53
  nodes: resolvedNodes
43
54
  },
44
55
  // Merge component styles with existing page styles
45
- styles: [...ir.styles, ...componentStyles]
56
+ styles: [...ir.styles, ...componentStyles],
57
+ // Add component scripts for bundling
58
+ componentScripts: [...(ir.componentScripts || []), ...componentScripts]
46
59
  }
47
60
  }
48
61
 
@@ -192,11 +205,12 @@ function resolveComponent(
192
205
  const resolvedTemplate = resolveSlots(templateNodes, resolvedSlots)
193
206
 
194
207
  // Forward attributes from component usage to the root element
195
- // This ensures onclick, class, and other attributes are preserved
208
+ // Also adds data-zen-component marker for hydration-driven instantiation
196
209
  const forwardedTemplate = forwardAttributesToRoot(
197
210
  resolvedTemplate,
198
211
  componentNode.attributes,
199
- componentNode.loopContext
212
+ componentNode.loopContext,
213
+ componentMeta.hasScript ? componentName : undefined // Only mark if component has script
200
214
  )
201
215
 
202
216
  // Recursively resolve any nested components in the resolved template
@@ -210,16 +224,16 @@ function resolveComponent(
210
224
  *
211
225
  * When using <Button onclick="increment">Text</Button>,
212
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.
213
230
  */
214
231
  function forwardAttributesToRoot(
215
232
  nodes: TemplateNode[],
216
233
  attributes: ComponentNode['attributes'],
217
- loopContext?: LoopContext
234
+ loopContext?: LoopContext,
235
+ componentName?: string // If provided, adds hydration marker
218
236
  ): TemplateNode[] {
219
- if (attributes.length === 0) {
220
- return nodes
221
- }
222
-
223
237
  // Find the first non-text element (the root element)
224
238
  const rootIndex = nodes.findIndex(n => n.type === 'element')
225
239
  if (rootIndex === -1) {
@@ -228,10 +242,19 @@ function forwardAttributesToRoot(
228
242
 
229
243
  const root = nodes[rootIndex] as ElementNode
230
244
 
231
- // Merge attributes: component usage attributes override template defaults
232
- // Also preserve the parent's loopContext on forwarded attributes
245
+ // Start with existing attributes
233
246
  const mergedAttributes = [...root.attributes]
234
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
235
258
  for (const attr of attributes) {
236
259
  const existingIndex = mergedAttributes.findIndex(a => a.name === attr.name)
237
260
 
@@ -0,0 +1,147 @@
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
+ * Then rewrites zen* prefixed calls to unprefixed:
9
+ * zenSignal(v) → signal(v)
10
+ * zenEffect(fn) → effect(fn)
11
+ * zenOnMount(fn) → onMount(fn)
12
+ */
13
+
14
+ import type { ComponentScriptIR } from '../ir/types'
15
+
16
+ /**
17
+ * Namespace bindings - destructured from the instance
18
+ * This is added at the top of every component script
19
+ */
20
+ const NAMESPACE_BINDINGS = `const {
21
+ signal, state, memo, effect, ref,
22
+ batch, untrack, onMount, onUnmount
23
+ } = __inst;`
24
+
25
+ /**
26
+ * Mapping of zen* prefixed names to unprefixed names
27
+ * These get rewritten to use the destructured namespace
28
+ */
29
+ const ZEN_PREFIX_MAPPINGS: Record<string, string> = {
30
+ 'zenSignal': 'signal',
31
+ 'zenState': 'state',
32
+ 'zenMemo': 'memo',
33
+ 'zenEffect': 'effect',
34
+ 'zenRef': 'ref',
35
+ 'zenBatch': 'batch',
36
+ 'zenUntrack': 'untrack',
37
+ 'zenOnMount': 'onMount',
38
+ 'zenOnUnmount': 'onUnmount',
39
+ }
40
+
41
+ /**
42
+ * Transform a component's script content for instance-scoped execution
43
+ *
44
+ * @param componentName - Name of the component
45
+ * @param scriptContent - Raw script content from the component
46
+ * @param props - Declared prop names
47
+ * @returns Transformed script ready for bundling
48
+ */
49
+ export function transformComponentScript(
50
+ componentName: string,
51
+ scriptContent: string,
52
+ props: string[]
53
+ ): string {
54
+ let transformed = scriptContent
55
+
56
+ // Strip import statements for .zen files (resolved at compile time)
57
+ transformed = transformed.replace(
58
+ /import\s+\w+\s+from\s+['"][^'"]*\.zen['"];?\s*/g,
59
+ ''
60
+ )
61
+
62
+ // Strip any other relative imports (components are inlined)
63
+ transformed = transformed.replace(
64
+ /import\s+{[^}]*}\s+from\s+['"][^'"]+['"];?\s*/g,
65
+ ''
66
+ )
67
+
68
+ // Rewrite zen* prefixed calls to unprefixed (uses namespace bindings)
69
+ for (const [zenName, unprefixedName] of Object.entries(ZEN_PREFIX_MAPPINGS)) {
70
+ // Match the zen* name as a standalone call
71
+ const regex = new RegExp(`(?<!\\w)${zenName}\\s*\\(`, 'g')
72
+ transformed = transformed.replace(regex, `${unprefixedName}(`)
73
+ }
74
+
75
+ return transformed.trim()
76
+ }
77
+
78
+ /**
79
+ * Generate a component factory function
80
+ *
81
+ * IMPORTANT: Factories are PASSIVE - they are registered but NOT invoked here.
82
+ * Instantiation is driven by the hydrator when it discovers component markers.
83
+ *
84
+ * @param componentName - Name of the component
85
+ * @param transformedScript - Script content after hook rewriting
86
+ * @param propNames - Declared prop names for destructuring
87
+ * @returns Component factory registration code (NO eager instantiation)
88
+ */
89
+ export function generateComponentFactory(
90
+ componentName: string,
91
+ transformedScript: string,
92
+ propNames: string[]
93
+ ): string {
94
+ const propsDestructure = propNames.length > 0
95
+ ? `const { ${propNames.join(', ')} } = props || {};`
96
+ : ''
97
+
98
+ // Register factory only - NO instantiation
99
+ // Hydrator will call instantiate() when it finds data-zen-component markers
100
+ return `
101
+ // Component Factory: ${componentName}
102
+ // Instantiation is driven by hydrator, not by bundle load
103
+ __zenith.defineComponent('${componentName}', function(props, rootElement) {
104
+ const __inst = __zenith.createInstance('${componentName}', rootElement);
105
+
106
+ // Namespace bindings (instance-scoped primitives)
107
+ ${NAMESPACE_BINDINGS}
108
+
109
+ ${propsDestructure}
110
+
111
+ // Component script (instance-scoped)
112
+ ${transformedScript}
113
+
114
+ // Execute mount lifecycle (rootElement is already in DOM)
115
+ __inst.mount();
116
+
117
+ return __inst;
118
+ });
119
+ `
120
+ }
121
+
122
+ /**
123
+ * Transform all component scripts from collected ComponentScriptIR
124
+ *
125
+ * @param componentScripts - Array of component script IRs
126
+ * @returns Combined JavaScript code for all component factories
127
+ */
128
+ export function transformAllComponentScripts(
129
+ componentScripts: ComponentScriptIR[]
130
+ ): string {
131
+ if (!componentScripts || componentScripts.length === 0) {
132
+ return ''
133
+ }
134
+
135
+ const factories = componentScripts
136
+ .filter(comp => comp.script && comp.script.trim().length > 0)
137
+ .map(comp => {
138
+ const transformed = transformComponentScript(
139
+ comp.name,
140
+ comp.script,
141
+ comp.props
142
+ )
143
+ return generateComponentFactory(comp.name, transformed, comp.props)
144
+ })
145
+
146
+ return factories.join('\n')
147
+ }
package/dist/cli.js CHANGED
@@ -7,6 +7,12 @@
7
7
  #!/usr/bin/env bun
8
8
  #!/usr/bin/env bun
9
9
  #!/usr/bin/env bun
10
+ #!/usr/bin/env bun
11
+ #!/usr/bin/env bun
12
+ #!/usr/bin/env bun
13
+ #!/usr/bin/env bun
14
+ #!/usr/bin/env bun
15
+ #!/usr/bin/env bun
10
16
  // @bun
11
17
  var __create = Object.create;
12
18
  var __getProtoOf = Object.getPrototypeOf;