@zenithbuild/core 0.4.6 → 0.5.0

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.
@@ -0,0 +1,283 @@
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 es-module-lexer to parse imports:
9
+ * - .zen imports are stripped (compile-time resolved)
10
+ * - npm imports are extracted as structured metadata for bundling
11
+ *
12
+ * IMPORTANT: No regex-based import parsing.
13
+ */
14
+
15
+ import { init, parse } from 'es-module-lexer'
16
+ import type { ComponentScriptIR, ScriptImport } from '../ir/types'
17
+
18
+ // Initialize es-module-lexer (must be called before parsing)
19
+ let lexerInitialized = false
20
+ async function ensureLexerInit(): Promise<void> {
21
+ if (!lexerInitialized) {
22
+ await init
23
+ lexerInitialized = true
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Namespace bindings - destructured from the instance
29
+ * This is added at the top of every component script
30
+ */
31
+ const NAMESPACE_BINDINGS = `const {
32
+ signal, state, memo, effect, ref,
33
+ batch, untrack, onMount, onUnmount
34
+ } = __inst;`
35
+
36
+ /**
37
+ * Mapping of zen* prefixed names to unprefixed names
38
+ * These get rewritten to use the destructured namespace
39
+ */
40
+ const ZEN_PREFIX_MAPPINGS: Record<string, string> = {
41
+ 'zenSignal': 'signal',
42
+ 'zenState': 'state',
43
+ 'zenMemo': 'memo',
44
+ 'zenEffect': 'effect',
45
+ 'zenRef': 'ref',
46
+ 'zenBatch': 'batch',
47
+ 'zenUntrack': 'untrack',
48
+ 'zenOnMount': 'onMount',
49
+ 'zenOnUnmount': 'onUnmount',
50
+ }
51
+
52
+ /**
53
+ * Result of script transformation including extracted imports
54
+ */
55
+ export interface TransformResult {
56
+ script: string // Transformed script (imports removed)
57
+ imports: ScriptImport[] // Structured npm imports to hoist
58
+ }
59
+
60
+ /**
61
+ * Parse and extract imports from script content using es-module-lexer
62
+ *
63
+ * @param scriptContent - Raw script content
64
+ * @returns Object with imports array and script with imports stripped
65
+ */
66
+ export async function parseAndExtractImports(scriptContent: string): Promise<{
67
+ imports: ScriptImport[]
68
+ strippedCode: string
69
+ }> {
70
+ await ensureLexerInit()
71
+
72
+ const imports: ScriptImport[] = []
73
+ const [parsedImports] = parse(scriptContent)
74
+
75
+ // Sort imports by start position (descending) for safe removal
76
+ const sortedImports = [...parsedImports].sort((a, b) => b.ss - a.ss)
77
+
78
+ let strippedCode = scriptContent
79
+
80
+ for (const imp of sortedImports) {
81
+ const source = imp.n || '' // Module specifier
82
+ const importStatement = scriptContent.slice(imp.ss, imp.se)
83
+
84
+ // Skip .zen file imports (compile-time resolved) - just strip them
85
+ if (source.endsWith('.zen')) {
86
+ strippedCode = strippedCode.slice(0, imp.ss) + strippedCode.slice(imp.se)
87
+ continue
88
+ }
89
+
90
+ // Skip relative imports (compile-time resolved) - just strip them
91
+ if (source.startsWith('./') || source.startsWith('../')) {
92
+ strippedCode = strippedCode.slice(0, imp.ss) + strippedCode.slice(imp.se)
93
+ continue
94
+ }
95
+
96
+ // This is an npm/external import - extract as structured metadata
97
+ const isTypeOnly = importStatement.startsWith('import type')
98
+ const isSideEffect = imp.ss === imp.se || !importStatement.includes(' from ')
99
+
100
+ // Extract specifiers from the import statement
101
+ let specifiers = ''
102
+ if (!isSideEffect) {
103
+ const fromIndex = importStatement.indexOf(' from ')
104
+ if (fromIndex !== -1) {
105
+ // Get everything between 'import' (or 'import type') and 'from'
106
+ const start = isTypeOnly ? 'import type '.length : 'import '.length
107
+ specifiers = importStatement.slice(start, fromIndex).trim()
108
+ }
109
+ }
110
+
111
+ imports.push({
112
+ source,
113
+ specifiers,
114
+ typeOnly: isTypeOnly,
115
+ sideEffect: isSideEffect
116
+ })
117
+
118
+ // Strip the import from the code (it will be hoisted to bundle top)
119
+ strippedCode = strippedCode.slice(0, imp.ss) + strippedCode.slice(imp.se)
120
+ }
121
+
122
+ // Clean up any leftover empty lines from stripped imports
123
+ strippedCode = strippedCode.replace(/^\s*\n/gm, '')
124
+
125
+ // Reverse imports array since we processed in reverse order
126
+ imports.reverse()
127
+
128
+ return { imports, strippedCode }
129
+ }
130
+
131
+ /**
132
+ * Transform a component's script content for instance-scoped execution
133
+ *
134
+ * @param componentName - Name of the component
135
+ * @param scriptContent - Raw script content from the component
136
+ * @param props - Declared prop names
137
+ * @returns TransformResult with transformed script and extracted imports
138
+ */
139
+ export async function transformComponentScript(
140
+ componentName: string,
141
+ scriptContent: string,
142
+ props: string[]
143
+ ): Promise<TransformResult> {
144
+ // Parse and extract imports using es-module-lexer
145
+ const { imports, strippedCode } = await parseAndExtractImports(scriptContent)
146
+
147
+ let transformed = strippedCode
148
+
149
+ // Rewrite zen* prefixed calls to unprefixed (uses namespace bindings)
150
+ for (const [zenName, unprefixedName] of Object.entries(ZEN_PREFIX_MAPPINGS)) {
151
+ // Match the zen* name as a standalone call
152
+ const regex = new RegExp(`(?<!\\w)${zenName}\\s*\\(`, 'g')
153
+ transformed = transformed.replace(regex, `${unprefixedName}(`)
154
+ }
155
+
156
+ return {
157
+ script: transformed.trim(),
158
+ imports
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Generate a component factory function
164
+ *
165
+ * IMPORTANT: Factories are PASSIVE - they are registered but NOT invoked here.
166
+ * Instantiation is driven by the hydrator when it discovers component markers.
167
+ *
168
+ * @param componentName - Name of the component
169
+ * @param transformedScript - Script content after hook rewriting
170
+ * @param propNames - Declared prop names for destructuring
171
+ * @returns Component factory registration code (NO eager instantiation)
172
+ */
173
+ export function generateComponentFactory(
174
+ componentName: string,
175
+ transformedScript: string,
176
+ propNames: string[]
177
+ ): string {
178
+ const propsDestructure = propNames.length > 0
179
+ ? `const { ${propNames.join(', ')} } = props || {};`
180
+ : ''
181
+
182
+ // Register factory only - NO instantiation
183
+ // Hydrator will call instantiate() when it finds data-zen-component markers
184
+ return `
185
+ // Component Factory: ${componentName}
186
+ // Instantiation is driven by hydrator, not by bundle load
187
+ __zenith.defineComponent('${componentName}', function(props, rootElement) {
188
+ const __inst = __zenith.createInstance('${componentName}', rootElement);
189
+
190
+ // Namespace bindings (instance-scoped primitives)
191
+ ${NAMESPACE_BINDINGS}
192
+
193
+ ${propsDestructure}
194
+
195
+ // Component script (instance-scoped)
196
+ ${transformedScript}
197
+
198
+ // Execute mount lifecycle (rootElement is already in DOM)
199
+ __inst.mount();
200
+
201
+ return __inst;
202
+ });
203
+ `
204
+ }
205
+
206
+ /**
207
+ * Result of transforming all component scripts
208
+ */
209
+ export interface TransformAllResult {
210
+ code: string // Combined factory code
211
+ imports: ScriptImport[] // All collected npm imports (deduplicated)
212
+ }
213
+
214
+ /**
215
+ * Deduplicate imports by (source + specifiers + typeOnly) tuple
216
+ * Returns deterministically sorted imports
217
+ */
218
+ function deduplicateImports(imports: ScriptImport[]): ScriptImport[] {
219
+ const seen = new Map<string, ScriptImport>()
220
+
221
+ for (const imp of imports) {
222
+ const key = `${imp.source}|${imp.specifiers}|${imp.typeOnly}`
223
+ if (!seen.has(key)) {
224
+ seen.set(key, imp)
225
+ }
226
+ }
227
+
228
+ // Sort by source for deterministic output
229
+ return Array.from(seen.values()).sort((a, b) => a.source.localeCompare(b.source))
230
+ }
231
+
232
+ /**
233
+ * Emit import statements from structured metadata
234
+ */
235
+ export function emitImports(imports: ScriptImport[]): string {
236
+ const deduplicated = deduplicateImports(imports)
237
+
238
+ return deduplicated.map(imp => {
239
+ if (imp.sideEffect) {
240
+ return `import '${imp.source}';`
241
+ }
242
+ const typePrefix = imp.typeOnly ? 'type ' : ''
243
+ return `import ${typePrefix}${imp.specifiers} from '${imp.source}';`
244
+ }).join('\n')
245
+ }
246
+
247
+ /**
248
+ * Transform all component scripts from collected ComponentScriptIR
249
+ *
250
+ * @param componentScripts - Array of component script IRs
251
+ * @returns TransformAllResult with combined code and deduplicated imports
252
+ */
253
+ export async function transformAllComponentScripts(
254
+ componentScripts: ComponentScriptIR[]
255
+ ): Promise<TransformAllResult> {
256
+ if (!componentScripts || componentScripts.length === 0) {
257
+ return { code: '', imports: [] }
258
+ }
259
+
260
+ const allImports: ScriptImport[] = []
261
+
262
+ const factories = await Promise.all(
263
+ componentScripts
264
+ .filter(comp => comp.script && comp.script.trim().length > 0)
265
+ .map(async comp => {
266
+ const result = await transformComponentScript(
267
+ comp.name,
268
+ comp.script,
269
+ comp.props
270
+ )
271
+
272
+ // Collect imports
273
+ allImports.push(...result.imports)
274
+
275
+ return generateComponentFactory(comp.name, result.script, comp.props)
276
+ })
277
+ )
278
+
279
+ return {
280
+ code: factories.join('\n'),
281
+ imports: deduplicateImports(allImports)
282
+ }
283
+ }
package/dist/cli.js CHANGED
@@ -7,6 +7,16 @@
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
16
+ #!/usr/bin/env bun
17
+ #!/usr/bin/env bun
18
+ #!/usr/bin/env bun
19
+ #!/usr/bin/env bun
10
20
  // @bun
11
21
  var __create = Object.create;
12
22
  var __getProtoOf = Object.getPrototypeOf;