@zenithbuild/core 0.6.3 → 1.1.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.
- package/cli/commands/dev.ts +107 -48
- package/compiler/discovery/componentDiscovery.ts +75 -11
- package/compiler/output/types.ts +15 -1
- package/compiler/parse/parseTemplate.ts +29 -0
- package/compiler/runtime/dataExposure.ts +27 -12
- package/compiler/runtime/generateDOM.ts +12 -3
- package/compiler/runtime/transformIR.ts +36 -0
- package/compiler/runtime/wrapExpression.ts +32 -13
- package/compiler/runtime/wrapExpressionWithLoop.ts +24 -10
- package/compiler/ssg-build.ts +71 -7
- package/compiler/test/component-stacking.test.ts +365 -0
- package/compiler/transform/componentResolver.ts +42 -4
- package/compiler/transform/fragmentLowering.ts +153 -1
- package/compiler/transform/generateBindings.ts +31 -10
- package/compiler/transform/transformNode.ts +114 -1
- package/core/config/index.ts +5 -3
- package/core/config/types.ts +67 -37
- package/core/plugins/bridge.ts +193 -0
- package/core/plugins/registry.ts +51 -6
- package/dist/cli.js +8 -0
- package/dist/zen-build.js +482 -1802
- package/dist/zen-dev.js +482 -1802
- package/dist/zen-preview.js +482 -1802
- package/dist/zenith.js +482 -1802
- package/package.json +10 -2
- package/runtime/bundle-generator.ts +10 -1
- package/runtime/client-runtime.ts +21 -1
- package/cli/utils/content.ts +0 -112
- package/router/manifest.ts +0 -314
- package/router/navigation/ZenLink.zen +0 -231
- package/router/navigation/index.ts +0 -78
- package/router/navigation/zen-link.ts +0 -584
- package/router/runtime.ts +0 -458
- package/router/types.ts +0 -168
|
@@ -56,11 +56,22 @@ function lowerNode(
|
|
|
56
56
|
case 'expression':
|
|
57
57
|
return lowerExpressionNode(node, filePath, expressions)
|
|
58
58
|
|
|
59
|
-
case 'element':
|
|
59
|
+
case 'element': {
|
|
60
|
+
// Check if this is a <for> element directive
|
|
61
|
+
if (node.tag === 'for') {
|
|
62
|
+
return lowerForElement(node, filePath, expressions)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check if this is an <html-content> element directive
|
|
66
|
+
if (node.tag === 'html-content') {
|
|
67
|
+
return lowerHtmlContentElement(node, filePath, expressions)
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
return {
|
|
61
71
|
...node,
|
|
62
72
|
children: lowerFragments(node.children, filePath, expressions)
|
|
63
73
|
}
|
|
74
|
+
}
|
|
64
75
|
|
|
65
76
|
case 'component':
|
|
66
77
|
return {
|
|
@@ -276,6 +287,147 @@ function lowerInlineFragment(
|
|
|
276
287
|
return node
|
|
277
288
|
}
|
|
278
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Lower <for> element directive to LoopFragmentNode
|
|
292
|
+
*
|
|
293
|
+
* Syntax: <for each="item" in="items">...body...</for>
|
|
294
|
+
* Or: <for each="item, index" in="items">...body...</for>
|
|
295
|
+
*
|
|
296
|
+
* This is compile-time sugar for {items.map(item => ...)}
|
|
297
|
+
*/
|
|
298
|
+
function lowerForElement(
|
|
299
|
+
node: import('../ir/types').ElementNode,
|
|
300
|
+
filePath: string,
|
|
301
|
+
expressions: ExpressionIR[]
|
|
302
|
+
): LoopFragmentNode {
|
|
303
|
+
// Extract 'each' and 'in' attributes
|
|
304
|
+
const eachAttr = node.attributes.find(a => a.name === 'each')
|
|
305
|
+
const inAttr = node.attributes.find(a => a.name === 'in')
|
|
306
|
+
|
|
307
|
+
if (!eachAttr || typeof eachAttr.value !== 'string') {
|
|
308
|
+
throw new InvariantError(
|
|
309
|
+
'ZEN001',
|
|
310
|
+
`<for> element requires an 'each' attribute specifying the item variable`,
|
|
311
|
+
'Usage: <for each="item" in="items">...body...</for>',
|
|
312
|
+
filePath,
|
|
313
|
+
node.location.line,
|
|
314
|
+
node.location.column
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!inAttr || typeof inAttr.value !== 'string') {
|
|
319
|
+
throw new InvariantError(
|
|
320
|
+
'ZEN001',
|
|
321
|
+
`<for> element requires an 'in' attribute specifying the source array`,
|
|
322
|
+
'Usage: <for each="item" in="items">...body...</for>',
|
|
323
|
+
filePath,
|
|
324
|
+
node.location.line,
|
|
325
|
+
node.location.column
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Parse item variable (may include index: "item, index" or "item, i")
|
|
330
|
+
const eachValue = eachAttr.value.trim()
|
|
331
|
+
let itemVar: string
|
|
332
|
+
let indexVar: string | undefined
|
|
333
|
+
|
|
334
|
+
if (eachValue.includes(',')) {
|
|
335
|
+
const parts = eachValue.split(',').map(p => p.trim())
|
|
336
|
+
itemVar = parts[0]!
|
|
337
|
+
indexVar = parts[1]
|
|
338
|
+
} else {
|
|
339
|
+
itemVar = eachValue
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const source = inAttr.value.trim()
|
|
343
|
+
|
|
344
|
+
// Create loop context for the body
|
|
345
|
+
const loopVariables = [itemVar]
|
|
346
|
+
if (indexVar) {
|
|
347
|
+
loopVariables.push(indexVar)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const bodyLoopContext: LoopContext = {
|
|
351
|
+
variables: node.loopContext
|
|
352
|
+
? [...node.loopContext.variables, ...loopVariables]
|
|
353
|
+
: loopVariables,
|
|
354
|
+
mapSource: source
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Lower children with loop context
|
|
358
|
+
const body = node.children.map(child => {
|
|
359
|
+
// Recursively lower children
|
|
360
|
+
const lowered = lowerNode(child, filePath, expressions)
|
|
361
|
+
// Attach loop context to children that need it
|
|
362
|
+
if ('loopContext' in lowered) {
|
|
363
|
+
return { ...lowered, loopContext: bodyLoopContext }
|
|
364
|
+
}
|
|
365
|
+
return lowered
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
type: 'loop-fragment',
|
|
370
|
+
source,
|
|
371
|
+
itemVar,
|
|
372
|
+
indexVar,
|
|
373
|
+
body,
|
|
374
|
+
location: node.location,
|
|
375
|
+
loopContext: bodyLoopContext
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Lower <html-content> element directive
|
|
381
|
+
*
|
|
382
|
+
* Syntax: <html-content content="expr" />
|
|
383
|
+
*
|
|
384
|
+
* This renders the expression value as raw HTML (innerHTML).
|
|
385
|
+
* Use with caution - only for trusted content like icons defined in code.
|
|
386
|
+
*/
|
|
387
|
+
function lowerHtmlContentElement(
|
|
388
|
+
node: import('../ir/types').ElementNode,
|
|
389
|
+
filePath: string,
|
|
390
|
+
expressions: ExpressionIR[]
|
|
391
|
+
): TemplateNode {
|
|
392
|
+
// Extract 'content' attribute
|
|
393
|
+
const contentAttr = node.attributes.find(a => a.name === 'content')
|
|
394
|
+
|
|
395
|
+
if (!contentAttr || typeof contentAttr.value !== 'string') {
|
|
396
|
+
throw new InvariantError(
|
|
397
|
+
'ZEN001',
|
|
398
|
+
`<html-content> element requires a 'content' attribute specifying the expression`,
|
|
399
|
+
'Usage: <html-content content="item.html" />',
|
|
400
|
+
filePath,
|
|
401
|
+
node.location.line,
|
|
402
|
+
node.location.column
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const exprCode = contentAttr.value.trim()
|
|
407
|
+
|
|
408
|
+
// Generate expression ID and register the expression
|
|
409
|
+
const exprId = `expr_${expressions.length}`
|
|
410
|
+
const exprIR: ExpressionIR = {
|
|
411
|
+
id: exprId,
|
|
412
|
+
code: exprCode,
|
|
413
|
+
location: node.location
|
|
414
|
+
}
|
|
415
|
+
expressions.push(exprIR)
|
|
416
|
+
|
|
417
|
+
// Create a span element with data-zen-html attribute for raw HTML binding
|
|
418
|
+
return {
|
|
419
|
+
type: 'element',
|
|
420
|
+
tag: 'span',
|
|
421
|
+
attributes: [
|
|
422
|
+
{ name: 'data-zen-html', value: exprIR, location: node.location, loopContext: node.loopContext },
|
|
423
|
+
{ name: 'style', value: 'display: contents;', location: node.location }
|
|
424
|
+
],
|
|
425
|
+
children: [],
|
|
426
|
+
location: node.location,
|
|
427
|
+
loopContext: node.loopContext
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
279
431
|
/**
|
|
280
432
|
* Parse JSX code string into TemplateNode[]
|
|
281
433
|
*
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
import type { Binding } from '../output/types'
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Valid binding types
|
|
11
|
+
*/
|
|
12
|
+
const VALID_BINDING_TYPES = new Set(['text', 'attribute', 'loop', 'conditional', 'optional'])
|
|
13
|
+
|
|
9
14
|
/**
|
|
10
15
|
* Validate bindings structure
|
|
11
16
|
*/
|
|
@@ -15,19 +20,35 @@ export function validateBindings(bindings: Binding[]): void {
|
|
|
15
20
|
throw new Error(`Invalid binding: ${JSON.stringify(binding)}`)
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
if (
|
|
23
|
+
if (!VALID_BINDING_TYPES.has(binding.type)) {
|
|
19
24
|
throw new Error(`Invalid binding type: ${binding.type}`)
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
// Validate specific binding types
|
|
28
|
+
switch (binding.type) {
|
|
29
|
+
case 'text':
|
|
30
|
+
if (binding.target !== 'data-zen-text') {
|
|
31
|
+
throw new Error(`Text binding must have target 'data-zen-text', got: ${binding.target}`)
|
|
32
|
+
}
|
|
33
|
+
break
|
|
34
|
+
case 'loop':
|
|
35
|
+
if (binding.target !== 'data-zen-loop') {
|
|
36
|
+
throw new Error(`Loop binding must have target 'data-zen-loop', got: ${binding.target}`)
|
|
37
|
+
}
|
|
38
|
+
break
|
|
39
|
+
case 'conditional':
|
|
40
|
+
if (binding.target !== 'data-zen-cond') {
|
|
41
|
+
throw new Error(`Conditional binding must have target 'data-zen-cond', got: ${binding.target}`)
|
|
42
|
+
}
|
|
43
|
+
break
|
|
44
|
+
case 'optional':
|
|
45
|
+
if (binding.target !== 'data-zen-opt') {
|
|
46
|
+
throw new Error(`Optional binding must have target 'data-zen-opt', got: ${binding.target}`)
|
|
47
|
+
}
|
|
48
|
+
break
|
|
49
|
+
case 'attribute':
|
|
50
|
+
// Attribute bindings can have various targets
|
|
51
|
+
break
|
|
31
52
|
}
|
|
32
53
|
}
|
|
33
54
|
}
|
|
@@ -2,11 +2,30 @@
|
|
|
2
2
|
* Transform Template Nodes
|
|
3
3
|
*
|
|
4
4
|
* Transforms IR nodes into HTML strings and collects bindings
|
|
5
|
+
*
|
|
6
|
+
* Phase 8: Supports fragment node types (loop-fragment, conditional-fragment, optional-fragment)
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
TemplateNode,
|
|
11
|
+
ElementNode,
|
|
12
|
+
TextNode,
|
|
13
|
+
ExpressionNode,
|
|
14
|
+
ExpressionIR,
|
|
15
|
+
LoopContext,
|
|
16
|
+
LoopFragmentNode,
|
|
17
|
+
ConditionalFragmentNode,
|
|
18
|
+
OptionalFragmentNode,
|
|
19
|
+
ComponentNode
|
|
20
|
+
} from '../ir/types'
|
|
8
21
|
import type { Binding } from '../output/types'
|
|
9
22
|
|
|
23
|
+
let loopIdCounter = 0
|
|
24
|
+
|
|
25
|
+
function generateLoopId(): string {
|
|
26
|
+
return `loop_${loopIdCounter++}`
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
let bindingIdCounter = 0
|
|
11
30
|
|
|
12
31
|
function generateBindingId(): string {
|
|
@@ -105,6 +124,100 @@ export function transformNode(
|
|
|
105
124
|
|
|
106
125
|
return `<${tag}${attrStr}>${childrenHtml}</${tag}>`
|
|
107
126
|
}
|
|
127
|
+
|
|
128
|
+
case 'loop-fragment': {
|
|
129
|
+
// Loop fragment: {items.map(item => <li>...</li>)} or <for each="item" in="items">
|
|
130
|
+
// For SSR/SSG, we render one instance of the body as a template
|
|
131
|
+
// The runtime will hydrate and expand this for each actual item
|
|
132
|
+
const loopNode = node as LoopFragmentNode
|
|
133
|
+
const loopId = generateLoopId()
|
|
134
|
+
const activeLoopContext = loopNode.loopContext || loopContext
|
|
135
|
+
|
|
136
|
+
// Create a binding for the loop expression
|
|
137
|
+
bindings.push({
|
|
138
|
+
id: loopId,
|
|
139
|
+
type: 'loop',
|
|
140
|
+
target: 'data-zen-loop',
|
|
141
|
+
expression: loopNode.source,
|
|
142
|
+
location: loopNode.location,
|
|
143
|
+
loopContext: activeLoopContext,
|
|
144
|
+
loopMeta: {
|
|
145
|
+
itemVar: loopNode.itemVar,
|
|
146
|
+
indexVar: loopNode.indexVar,
|
|
147
|
+
bodyTemplate: loopNode.body
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// Generate the loop body template HTML
|
|
152
|
+
// For SSR, we render ONE visible instance of the body as a template/placeholder
|
|
153
|
+
// The runtime will clone this for each item in the array
|
|
154
|
+
const bodyHtml = loopNode.body.map(child => transform(child, activeLoopContext)).join('')
|
|
155
|
+
|
|
156
|
+
// Render container with body visible for SSR (not in hidden <template>)
|
|
157
|
+
// Runtime will clear and re-render with actual data
|
|
158
|
+
return `<div data-zen-loop="${loopId}" data-zen-source="${escapeHtml(loopNode.source)}" data-zen-item="${loopNode.itemVar}"${loopNode.indexVar ? ` data-zen-index="${loopNode.indexVar}"` : ''} style="display: contents;">${bodyHtml}</div>`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
case 'conditional-fragment': {
|
|
162
|
+
// Conditional fragment: {cond ? <A /> : <B />}
|
|
163
|
+
// Both branches are pre-rendered, runtime toggles visibility
|
|
164
|
+
const condNode = node as ConditionalFragmentNode
|
|
165
|
+
const condId = generateBindingId()
|
|
166
|
+
const activeLoopContext = condNode.loopContext || loopContext
|
|
167
|
+
|
|
168
|
+
bindings.push({
|
|
169
|
+
id: condId,
|
|
170
|
+
type: 'conditional',
|
|
171
|
+
target: 'data-zen-cond',
|
|
172
|
+
expression: condNode.condition,
|
|
173
|
+
location: condNode.location,
|
|
174
|
+
loopContext: activeLoopContext
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Render both branches
|
|
178
|
+
const consequentHtml = condNode.consequent.map(child => transform(child, activeLoopContext)).join('')
|
|
179
|
+
const alternateHtml = condNode.alternate.map(child => transform(child, activeLoopContext)).join('')
|
|
180
|
+
|
|
181
|
+
return `<div data-zen-cond="${condId}" data-zen-cond-true style="display: contents;">${consequentHtml}</div><div data-zen-cond="${condId}" data-zen-cond-false style="display: none;">${alternateHtml}</div>`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case 'optional-fragment': {
|
|
185
|
+
// Optional fragment: {cond && <A />}
|
|
186
|
+
// Fragment is pre-rendered, runtime toggles mount/unmount
|
|
187
|
+
const optNode = node as OptionalFragmentNode
|
|
188
|
+
const optId = generateBindingId()
|
|
189
|
+
const activeLoopContext = optNode.loopContext || loopContext
|
|
190
|
+
|
|
191
|
+
bindings.push({
|
|
192
|
+
id: optId,
|
|
193
|
+
type: 'optional',
|
|
194
|
+
target: 'data-zen-opt',
|
|
195
|
+
expression: optNode.condition,
|
|
196
|
+
location: optNode.location,
|
|
197
|
+
loopContext: activeLoopContext
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const fragmentHtml = optNode.fragment.map(child => transform(child, activeLoopContext)).join('')
|
|
201
|
+
|
|
202
|
+
return `<div data-zen-opt="${optId}" style="display: contents;">${fragmentHtml}</div>`
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
case 'component': {
|
|
206
|
+
// Component node - should have been resolved before reaching here
|
|
207
|
+
// This is a fallback for unresolved components
|
|
208
|
+
const compNode = node as ComponentNode
|
|
209
|
+
console.warn(`[Zenith] Unresolved component in transformNode: ${compNode.name}`)
|
|
210
|
+
|
|
211
|
+
// Render children as a fragment
|
|
212
|
+
const childrenHtml = compNode.children.map(child => transform(child, loopContext)).join('')
|
|
213
|
+
return `<!-- unresolved: ${compNode.name} -->${childrenHtml}`
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
default: {
|
|
217
|
+
// Handle any unknown node types
|
|
218
|
+
console.warn(`[Zenith] Unknown node type in transformNode: ${(node as any).type}`)
|
|
219
|
+
return ''
|
|
220
|
+
}
|
|
108
221
|
}
|
|
109
222
|
}
|
|
110
223
|
|
package/core/config/index.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Zenith Config
|
|
3
3
|
*
|
|
4
4
|
* Public exports for zenith/config
|
|
5
|
+
*
|
|
6
|
+
* Core exports ONLY generic plugin infrastructure.
|
|
7
|
+
* Plugin-specific types are owned by their respective plugins.
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
10
|
export { defineConfig } from './types';
|
|
@@ -9,8 +12,7 @@ export type {
|
|
|
9
12
|
ZenithConfig,
|
|
10
13
|
ZenithPlugin,
|
|
11
14
|
PluginContext,
|
|
12
|
-
|
|
13
|
-
ContentPluginOptions,
|
|
14
|
-
ContentItem
|
|
15
|
+
PluginData
|
|
15
16
|
} from './types';
|
|
16
17
|
export { loadZenithConfig, hasZenithConfig } from './loader';
|
|
18
|
+
|
package/core/config/types.ts
CHANGED
|
@@ -2,71 +2,101 @@
|
|
|
2
2
|
* Zenith Config Types
|
|
3
3
|
*
|
|
4
4
|
* Configuration interfaces for zenith.config.ts
|
|
5
|
+
*
|
|
6
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
7
|
+
* HOOK OWNERSHIP RULE (CANONICAL)
|
|
8
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
9
|
+
*
|
|
10
|
+
* Core may ONLY define types that are universally valid in all Zenith applications.
|
|
11
|
+
* Plugin-specific types MUST be owned by their respective plugins.
|
|
12
|
+
*
|
|
13
|
+
* ✅ ALLOWED in Core:
|
|
14
|
+
* - ZenithConfig, ZenithPlugin, PluginContext (generic plugin infrastructure)
|
|
15
|
+
* - Universal lifecycle hooks (onMount, onUnmount)
|
|
16
|
+
* - Reactivity primitives (signal, effect, etc.)
|
|
17
|
+
*
|
|
18
|
+
* ❌ PROHIBITED in Core:
|
|
19
|
+
* - Content plugin types (ContentItem, ContentSourceConfig, etc.)
|
|
20
|
+
* - Router plugin types (RouteState, NavigationGuard, etc.)
|
|
21
|
+
* - Documentation plugin types
|
|
22
|
+
* - Any type that exists only because a plugin exists
|
|
23
|
+
*
|
|
24
|
+
* If removing a plugin would make a type meaningless, that type belongs to the plugin.
|
|
25
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
5
26
|
*/
|
|
6
27
|
|
|
28
|
+
import type { CLIBridgeAPI } from '../plugins/bridge';
|
|
29
|
+
|
|
7
30
|
// ============================================
|
|
8
|
-
//
|
|
31
|
+
// Core Plugin Types (Generic Infrastructure)
|
|
9
32
|
// ============================================
|
|
10
33
|
|
|
11
34
|
/**
|
|
12
|
-
*
|
|
35
|
+
* Generic data record for plugin data exchange
|
|
36
|
+
* Plugins define their own specific types internally
|
|
13
37
|
*/
|
|
14
|
-
export
|
|
15
|
-
/** Root directory relative to project root (e.g., "../zenith-docs" or "content") */
|
|
16
|
-
root: string;
|
|
17
|
-
/** Folders to include from the root (e.g., ["documentation"]). Defaults to all. */
|
|
18
|
-
include?: string[];
|
|
19
|
-
/** Folders to exclude from the root (e.g., ["changelog"]) */
|
|
20
|
-
exclude?: string[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Options for the content plugin
|
|
25
|
-
*/
|
|
26
|
-
export interface ContentPluginOptions {
|
|
27
|
-
/** Named content sources mapped to their configuration */
|
|
28
|
-
sources?: Record<string, ContentSourceConfig>;
|
|
29
|
-
/** Legacy: Single content directory (deprecated, use sources instead) */
|
|
30
|
-
contentDir?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ============================================
|
|
34
|
-
// Core Plugin Types
|
|
35
|
-
// ============================================
|
|
38
|
+
export type PluginData = Record<string, unknown[]>;
|
|
36
39
|
|
|
37
40
|
/**
|
|
38
41
|
* Context passed to plugins during setup
|
|
42
|
+
*
|
|
43
|
+
* This is intentionally generic - plugins define their own data shapes.
|
|
44
|
+
* Core provides the stage, plugins bring the actors.
|
|
39
45
|
*/
|
|
40
46
|
export interface PluginContext {
|
|
41
47
|
/** Absolute path to project root */
|
|
42
48
|
projectRoot: string;
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Set plugin data for the runtime
|
|
52
|
+
*
|
|
53
|
+
* Generic setter - plugins define their own data structures.
|
|
54
|
+
* The runtime stores this data and makes it available to components.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // Content plugin uses it for content items
|
|
58
|
+
* ctx.setPluginData('content', contentItems);
|
|
59
|
+
*
|
|
60
|
+
* // Analytics plugin uses it for tracking config
|
|
61
|
+
* ctx.setPluginData('analytics', analyticsConfig);
|
|
62
|
+
*/
|
|
63
|
+
setPluginData: (namespace: string, data: unknown[]) => void;
|
|
64
|
+
|
|
45
65
|
/** Additional options passed from config */
|
|
46
66
|
options?: Record<string, unknown>;
|
|
47
67
|
}
|
|
48
68
|
|
|
49
|
-
/**
|
|
50
|
-
* A content item loaded from a source
|
|
51
|
-
*/
|
|
52
|
-
export interface ContentItem {
|
|
53
|
-
id?: string | number;
|
|
54
|
-
slug?: string | null;
|
|
55
|
-
collection?: string | null;
|
|
56
|
-
content?: string | null;
|
|
57
|
-
[key: string]: unknown;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
69
|
/**
|
|
61
70
|
* A Zenith plugin definition
|
|
71
|
+
*
|
|
72
|
+
* Plugins are self-contained, removable extensions.
|
|
73
|
+
* Core must build and run identically with or without any plugin installed.
|
|
62
74
|
*/
|
|
63
75
|
export interface ZenithPlugin {
|
|
64
76
|
/** Unique plugin name */
|
|
65
77
|
name: string;
|
|
78
|
+
|
|
66
79
|
/** Setup function called during initialization */
|
|
67
80
|
setup: (ctx: PluginContext) => void | Promise<void>;
|
|
81
|
+
|
|
68
82
|
/** Plugin-specific configuration (preserved for reference) */
|
|
69
83
|
config?: unknown;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Optional CLI registration
|
|
87
|
+
*
|
|
88
|
+
* Plugin receives the CLI bridge API to register namespaced hooks.
|
|
89
|
+
* CLI lifecycle hooks: 'cli:*' (owned by CLI)
|
|
90
|
+
* Plugin hooks: '<namespace>:*' (owned by plugin)
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* registerCLI(api) {
|
|
94
|
+
* api.on('cli:runtime:collect', (ctx) => {
|
|
95
|
+
* return { namespace: 'myPlugin', payload: ctx.getPluginData('myPlugin') }
|
|
96
|
+
* })
|
|
97
|
+
* }
|
|
98
|
+
*/
|
|
99
|
+
registerCLI?: (api: CLIBridgeAPI) => void;
|
|
70
100
|
}
|
|
71
101
|
|
|
72
102
|
// ============================================
|