@zenithbuild/core 1.2.2 → 1.2.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.
- package/README.md +20 -19
- package/cli/commands/add.ts +2 -2
- package/cli/commands/build.ts +2 -3
- package/cli/commands/dev.ts +94 -74
- package/cli/commands/index.ts +1 -1
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/remove.ts +2 -2
- package/cli/index.ts +1 -1
- package/cli/main.ts +1 -1
- package/cli/utils/logger.ts +1 -1
- package/cli/utils/plugin-manager.ts +1 -1
- package/cli/utils/project.ts +4 -4
- package/core/components/ErrorPage.zen +218 -0
- package/core/components/index.ts +15 -0
- package/core/config.ts +1 -0
- package/core/index.ts +29 -0
- package/dist/compiler-native-frej59m4.node +0 -0
- package/dist/core/compiler-native-frej59m4.node +0 -0
- package/dist/core/index.js +6293 -0
- package/dist/runtime/lifecycle/index.js +1 -0
- package/dist/runtime/reactivity/index.js +1 -0
- package/dist/zen-build.js +1 -20118
- package/dist/zen-dev.js +1 -20118
- package/dist/zen-preview.js +1 -20118
- package/dist/zenith.js +1 -20118
- package/package.json +11 -20
- package/compiler/README.md +0 -380
- package/compiler/build-analyzer.ts +0 -122
- package/compiler/css/index.ts +0 -317
- package/compiler/discovery/componentDiscovery.ts +0 -242
- package/compiler/discovery/layouts.ts +0 -70
- package/compiler/errors/compilerError.ts +0 -56
- package/compiler/finalize/finalizeOutput.ts +0 -192
- package/compiler/finalize/generateFinalBundle.ts +0 -82
- package/compiler/index.ts +0 -83
- package/compiler/ir/types.ts +0 -174
- package/compiler/output/types.ts +0 -48
- package/compiler/parse/detectMapExpressions.ts +0 -102
- package/compiler/parse/importTypes.ts +0 -78
- package/compiler/parse/parseImports.ts +0 -309
- package/compiler/parse/parseScript.ts +0 -46
- package/compiler/parse/parseTemplate.ts +0 -628
- package/compiler/parse/parseZenFile.ts +0 -66
- package/compiler/parse/scriptAnalysis.ts +0 -91
- package/compiler/parse/trackLoopContext.ts +0 -82
- package/compiler/runtime/dataExposure.ts +0 -332
- package/compiler/runtime/generateDOM.ts +0 -255
- package/compiler/runtime/generateHydrationBundle.ts +0 -407
- package/compiler/runtime/hydration.ts +0 -309
- package/compiler/runtime/navigation.ts +0 -432
- package/compiler/runtime/thinRuntime.ts +0 -160
- package/compiler/runtime/transformIR.ts +0 -406
- package/compiler/runtime/wrapExpression.ts +0 -114
- package/compiler/runtime/wrapExpressionWithLoop.ts +0 -97
- package/compiler/spa-build.ts +0 -917
- package/compiler/ssg-build.ts +0 -486
- package/compiler/test/component-stacking.test.ts +0 -365
- package/compiler/test/map-lowering.test.ts +0 -130
- package/compiler/test/validate-test.ts +0 -104
- package/compiler/transform/classifyExpression.ts +0 -444
- package/compiler/transform/componentResolver.ts +0 -350
- package/compiler/transform/componentScriptTransformer.ts +0 -303
- package/compiler/transform/expressionTransformer.ts +0 -385
- package/compiler/transform/fragmentLowering.ts +0 -819
- package/compiler/transform/generateBindings.ts +0 -68
- package/compiler/transform/generateHTML.ts +0 -28
- package/compiler/transform/layoutProcessor.ts +0 -132
- package/compiler/transform/slotResolver.ts +0 -292
- package/compiler/transform/transformNode.ts +0 -314
- package/compiler/transform/transformTemplate.ts +0 -38
- package/compiler/validate/invariants.ts +0 -292
- package/compiler/validate/validateExpressions.ts +0 -168
- package/core/config/index.ts +0 -18
- package/core/config/loader.ts +0 -69
- package/core/config/types.ts +0 -119
- package/core/plugins/bridge.ts +0 -193
- package/core/plugins/index.ts +0 -7
- package/core/plugins/registry.ts +0 -126
- package/dist/cli.js +0 -11675
- package/runtime/build.ts +0 -17
- package/runtime/bundle-generator.ts +0 -1266
- package/runtime/client-runtime.ts +0 -891
- package/runtime/serve.ts +0 -93
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generate Bindings
|
|
3
|
-
*
|
|
4
|
-
* This module is handled by transformNode, but kept here for future extensibility
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { Binding } from '../output/types'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Valid binding types
|
|
11
|
-
*/
|
|
12
|
-
const VALID_BINDING_TYPES = new Set(['text', 'attribute', 'loop', 'conditional', 'optional'])
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Validate bindings structure
|
|
16
|
-
*/
|
|
17
|
-
export function validateBindings(bindings: Binding[]): void {
|
|
18
|
-
for (const binding of bindings) {
|
|
19
|
-
if (!binding.id || !binding.type || !binding.target || !binding.expression) {
|
|
20
|
-
throw new Error(`Invalid binding: ${JSON.stringify(binding)}`)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (!VALID_BINDING_TYPES.has(binding.type)) {
|
|
24
|
-
throw new Error(`Invalid binding type: ${binding.type}`)
|
|
25
|
-
}
|
|
26
|
-
|
|
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
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Sort bindings by location for deterministic output
|
|
58
|
-
*/
|
|
59
|
-
export function sortBindings(bindings: Binding[]): Binding[] {
|
|
60
|
-
return [...bindings].sort((a, b) => {
|
|
61
|
-
if (!a.location || !b.location) return 0
|
|
62
|
-
if (a.location.line !== b.location.line) {
|
|
63
|
-
return a.location.line - b.location.line
|
|
64
|
-
}
|
|
65
|
-
return a.location.column - b.location.column
|
|
66
|
-
})
|
|
67
|
-
}
|
|
68
|
-
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generate Static HTML from Transformed Nodes
|
|
3
|
-
*
|
|
4
|
-
* This generates pure HTML with no expressions or runtime code
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { TemplateNode } from '../ir/types'
|
|
8
|
-
import { transformNode } from './transformNode'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Generate HTML string from template nodes
|
|
12
|
-
*/
|
|
13
|
-
export function generateHTML(
|
|
14
|
-
nodes: TemplateNode[],
|
|
15
|
-
expressions: any[]
|
|
16
|
-
): { html: string; bindings: any[] } {
|
|
17
|
-
let html = ''
|
|
18
|
-
const allBindings: any[] = []
|
|
19
|
-
|
|
20
|
-
for (const node of nodes) {
|
|
21
|
-
const { html: nodeHtml, bindings } = transformNode(node, expressions)
|
|
22
|
-
html += nodeHtml
|
|
23
|
-
allBindings.push(...bindings)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return { html, bindings: allBindings }
|
|
27
|
-
}
|
|
28
|
-
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import type { LayoutMetadata } from '../discovery/layouts'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Process a page by inlining a layout
|
|
5
|
-
*/
|
|
6
|
-
export function processLayout(
|
|
7
|
-
source: string,
|
|
8
|
-
layout: LayoutMetadata,
|
|
9
|
-
props: Record<string, any> = {}
|
|
10
|
-
): string {
|
|
11
|
-
// 1. Extract scripts and styles from the page source
|
|
12
|
-
const pageScripts: string[] = []
|
|
13
|
-
const pageStyles: string[] = []
|
|
14
|
-
let isTypeScript = false
|
|
15
|
-
|
|
16
|
-
// Extract script blocks
|
|
17
|
-
const scriptRegex = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi
|
|
18
|
-
let scriptMatch
|
|
19
|
-
while ((scriptMatch = scriptRegex.exec(source)) !== null) {
|
|
20
|
-
const attrString = scriptMatch[1] || ''
|
|
21
|
-
const content = scriptMatch[2] || ''
|
|
22
|
-
if (attrString.includes('lang="ts"') || attrString.includes('setup="ts"')) {
|
|
23
|
-
isTypeScript = true
|
|
24
|
-
}
|
|
25
|
-
if (content) pageScripts.push(content.trim())
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Extract style blocks
|
|
29
|
-
const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi
|
|
30
|
-
let styleMatch
|
|
31
|
-
while ((styleMatch = styleRegex.exec(source)) !== null) {
|
|
32
|
-
if (styleMatch[1]) pageStyles.push(styleMatch[1].trim())
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// 2. Extract content from page source and parse props
|
|
36
|
-
const layoutTag = layout.name
|
|
37
|
-
// Support both <DefaultLayout ...> and <DefaultLayout>...</DefaultLayout>
|
|
38
|
-
const layoutRegex = new RegExp(`<${layoutTag}\\b([^>]*)>(?:([\\s\\S]*?)</${layoutTag}>)?`, 'i')
|
|
39
|
-
const match = source.match(layoutRegex)
|
|
40
|
-
|
|
41
|
-
let pageHtml = ''
|
|
42
|
-
let layoutPropsStr = ''
|
|
43
|
-
|
|
44
|
-
if (match) {
|
|
45
|
-
layoutPropsStr = match[1] || ''
|
|
46
|
-
pageHtml = match[2] || ''
|
|
47
|
-
|
|
48
|
-
// If it's a self-closing tag or empty, it might not have captured content correctly if regex failed
|
|
49
|
-
if (!pageHtml && !source.includes(`</${layoutTag}>`)) {
|
|
50
|
-
// Self-closing check? No, Zenith usually expects explicit tags or the layout to wrap everything.
|
|
51
|
-
}
|
|
52
|
-
} else {
|
|
53
|
-
// If layout tag not found as root, assume everything minus script/style is content
|
|
54
|
-
pageHtml = source.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
55
|
-
pageHtml = pageHtml.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '').trim()
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// 3. Parse props from the tag
|
|
59
|
-
const mergedProps = { ...props }
|
|
60
|
-
if (layoutPropsStr) {
|
|
61
|
-
// Support legacy props={{...}}
|
|
62
|
-
const legacyMatch = layoutPropsStr.match(/props=\{\{([^}]+)\}\}/)
|
|
63
|
-
if (legacyMatch && legacyMatch[1]) {
|
|
64
|
-
const propsBody = legacyMatch[1]
|
|
65
|
-
const pairs = propsBody.split(/,(?![^[]*\])(?![^{]*\})/)
|
|
66
|
-
for (const pair of pairs) {
|
|
67
|
-
const [key, ...valParts] = pair.split(':')
|
|
68
|
-
if (key && valParts.length > 0) {
|
|
69
|
-
mergedProps[key.trim()] = valParts.join(':').trim()
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Support natural props: title={"Home"} or title="Home" or title={title}
|
|
75
|
-
const attrRegex = /([a-zA-Z0-9-]+)=(?:\{([^}]+)\}|"([^"]*)"|'([^']*)')/g
|
|
76
|
-
let attrMatch
|
|
77
|
-
while ((attrMatch = attrRegex.exec(layoutPropsStr)) !== null) {
|
|
78
|
-
const name = attrMatch[1]
|
|
79
|
-
const value = attrMatch[2] || attrMatch[3] || attrMatch[4]
|
|
80
|
-
if (name && name !== 'props') {
|
|
81
|
-
mergedProps[name] = value
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// 4. Merge Scripts with Prop Injection
|
|
87
|
-
// Layout scripts come first, then page scripts. Props are injected at the very top.
|
|
88
|
-
const propDeclarations = Object.entries(mergedProps)
|
|
89
|
-
.map(([key, value]) => {
|
|
90
|
-
// If value looks like a string literal, keep it as is, otherwise wrap if needed
|
|
91
|
-
// Actually, if it came from {expression}, we should treat it as code.
|
|
92
|
-
// If it came from "string", we treat it as a string.
|
|
93
|
-
const isExpression = layoutPropsStr.includes(`${key}={${value}}`)
|
|
94
|
-
if (isExpression) {
|
|
95
|
-
return `const ${key} = ${value};`
|
|
96
|
-
}
|
|
97
|
-
return `const ${key} = ${typeof value === 'string' && !value.startsWith("'") && !value.startsWith('"') ? `'${value}'` : value};`
|
|
98
|
-
})
|
|
99
|
-
.join('\n')
|
|
100
|
-
|
|
101
|
-
const mergedScripts = [
|
|
102
|
-
propDeclarations,
|
|
103
|
-
...layout.scripts,
|
|
104
|
-
...pageScripts
|
|
105
|
-
].filter(Boolean).join('\n\n')
|
|
106
|
-
|
|
107
|
-
// 5. Merge Styles
|
|
108
|
-
const mergedStyles = [
|
|
109
|
-
...layout.styles,
|
|
110
|
-
...pageStyles
|
|
111
|
-
].filter(Boolean).join('\n\n')
|
|
112
|
-
|
|
113
|
-
// 6. Inline HTML into layout slot
|
|
114
|
-
let finalizedHtml = layout.html.replace(/<Slot\s*\/>/gi, pageHtml)
|
|
115
|
-
finalizedHtml = finalizedHtml.replace(/<slot\s*>[\s\S]*?<\/slot>/gi, pageHtml)
|
|
116
|
-
|
|
117
|
-
// 7. Reconstruct the full .zen source
|
|
118
|
-
const propNames = Object.keys(mergedProps).join(',')
|
|
119
|
-
const scriptTag = `<script setup${isTypeScript ? '="ts"' : ''}${propNames ? ` props="${propNames}"` : ''}>`
|
|
120
|
-
|
|
121
|
-
return `
|
|
122
|
-
${scriptTag}
|
|
123
|
-
${mergedScripts}
|
|
124
|
-
</script>
|
|
125
|
-
|
|
126
|
-
${finalizedHtml}
|
|
127
|
-
|
|
128
|
-
<style>
|
|
129
|
-
${mergedStyles}
|
|
130
|
-
</style>
|
|
131
|
-
`.trim()
|
|
132
|
-
}
|
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Slot Resolution - Compound Component Model
|
|
3
|
-
*
|
|
4
|
-
* Resolves slots using compound component pattern (Card.Header, Card.Body)
|
|
5
|
-
* NOT template tags. This matches React/Astro semantics.
|
|
6
|
-
*
|
|
7
|
-
* IMPORTANT: Slot content must preserve the parent reactive scope.
|
|
8
|
-
* Components are purely structural transforms - they don't create new reactive boundaries.
|
|
9
|
-
*
|
|
10
|
-
* Example usage:
|
|
11
|
-
* <Card>
|
|
12
|
-
* <Card.Header><h3>Title</h3></Card.Header>
|
|
13
|
-
* <p>Body content goes to default slot</p>
|
|
14
|
-
* <Card.Footer><Button>OK</Button></Card.Footer>
|
|
15
|
-
* </Card>
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type { TemplateNode, ComponentNode, ElementNode, LoopContext } from '../ir/types'
|
|
19
|
-
|
|
20
|
-
export interface ResolvedSlots {
|
|
21
|
-
default: TemplateNode[]
|
|
22
|
-
named: Map<string, TemplateNode[]>
|
|
23
|
-
// Preserve the parent's reactive scope for slot content
|
|
24
|
-
parentLoopContext?: LoopContext
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Extract slots from component children using compound component pattern
|
|
29
|
-
*
|
|
30
|
-
* Children named `ParentComponent.SlotName` become named slots.
|
|
31
|
-
* All other children go to the default slot.
|
|
32
|
-
* Preserves the parent's reactive scope (loopContext) for all slot content.
|
|
33
|
-
*
|
|
34
|
-
* @param parentName - Name of the parent component (e.g., "Card")
|
|
35
|
-
* @param children - Child nodes from component usage
|
|
36
|
-
* @param parentLoopContext - The reactive scope from the parent (must be preserved)
|
|
37
|
-
*/
|
|
38
|
-
export function extractSlotsFromChildren(
|
|
39
|
-
parentName: string,
|
|
40
|
-
children: TemplateNode[],
|
|
41
|
-
parentLoopContext?: LoopContext
|
|
42
|
-
): ResolvedSlots {
|
|
43
|
-
const defaultSlot: TemplateNode[] = []
|
|
44
|
-
const namedSlots = new Map<string, TemplateNode[]>()
|
|
45
|
-
|
|
46
|
-
for (const child of children) {
|
|
47
|
-
// Check if this is a compound component (e.g., Card.Header)
|
|
48
|
-
if (child.type === 'component') {
|
|
49
|
-
const compoundMatch = parseCompoundName(child.name, parentName)
|
|
50
|
-
|
|
51
|
-
if (compoundMatch) {
|
|
52
|
-
// This is a named slot (e.g., Card.Header -> "header")
|
|
53
|
-
const slotName = compoundMatch.toLowerCase()
|
|
54
|
-
|
|
55
|
-
if (!namedSlots.has(slotName)) {
|
|
56
|
-
namedSlots.set(slotName, [])
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// The compound component's children become the slot content
|
|
60
|
-
// Preserve parent's loopContext on each child
|
|
61
|
-
const scopedChildren = child.children.map(c =>
|
|
62
|
-
rebindNodeToScope(c, parentLoopContext)
|
|
63
|
-
)
|
|
64
|
-
namedSlots.get(slotName)!.push(...scopedChildren)
|
|
65
|
-
} else {
|
|
66
|
-
// Regular component, goes to default slot
|
|
67
|
-
// Preserve parent's loopContext
|
|
68
|
-
defaultSlot.push(rebindNodeToScope(child, parentLoopContext))
|
|
69
|
-
}
|
|
70
|
-
} else {
|
|
71
|
-
// Elements, text, expressions go to default slot
|
|
72
|
-
// Preserve parent's loopContext
|
|
73
|
-
defaultSlot.push(rebindNodeToScope(child, parentLoopContext))
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
default: defaultSlot,
|
|
79
|
-
named: namedSlots,
|
|
80
|
-
parentLoopContext
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Rebind a node to the parent's reactive scope
|
|
86
|
-
*
|
|
87
|
-
* This ensures that expressions and event bindings in slot content
|
|
88
|
-
* remain connected to the parent component's reactive graph.
|
|
89
|
-
* Components must be purely structural - they don't create new reactive boundaries.
|
|
90
|
-
*/
|
|
91
|
-
function rebindNodeToScope(node: TemplateNode, loopContext?: LoopContext): TemplateNode {
|
|
92
|
-
// If no parent scope to preserve, return as-is
|
|
93
|
-
if (!loopContext) {
|
|
94
|
-
return node
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Merge the parent's loopContext with existing loopContext
|
|
98
|
-
// Parent scope takes precedence to ensure reactivity flows through
|
|
99
|
-
switch (node.type) {
|
|
100
|
-
case 'expression':
|
|
101
|
-
return {
|
|
102
|
-
...node,
|
|
103
|
-
loopContext: mergeLoopContext(node.loopContext, loopContext)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
case 'element':
|
|
107
|
-
return {
|
|
108
|
-
...node,
|
|
109
|
-
loopContext: mergeLoopContext(node.loopContext, loopContext),
|
|
110
|
-
attributes: node.attributes.map(attr => ({
|
|
111
|
-
...attr,
|
|
112
|
-
loopContext: attr.loopContext
|
|
113
|
-
? mergeLoopContext(attr.loopContext, loopContext)
|
|
114
|
-
: loopContext
|
|
115
|
-
})),
|
|
116
|
-
children: node.children.map(c => rebindNodeToScope(c, loopContext))
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
case 'component':
|
|
120
|
-
return {
|
|
121
|
-
...node,
|
|
122
|
-
loopContext: mergeLoopContext(node.loopContext, loopContext),
|
|
123
|
-
children: node.children.map(c => rebindNodeToScope(c, loopContext))
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
case 'text':
|
|
127
|
-
// Text nodes don't have reactive bindings
|
|
128
|
-
return node
|
|
129
|
-
|
|
130
|
-
default:
|
|
131
|
-
return node
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Merge two loop contexts, combining their variables
|
|
137
|
-
* Parent context variables take precedence (added last so they shadow)
|
|
138
|
-
*/
|
|
139
|
-
function mergeLoopContext(existing?: LoopContext, parent?: LoopContext): LoopContext | undefined {
|
|
140
|
-
if (!existing && !parent) return undefined
|
|
141
|
-
if (!existing) return parent
|
|
142
|
-
if (!parent) return existing
|
|
143
|
-
|
|
144
|
-
// Combine variables, parent variables shadow existing
|
|
145
|
-
const allVars = new Set([...existing.variables, ...parent.variables])
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
variables: Array.from(allVars),
|
|
149
|
-
mapSource: parent.mapSource || existing.mapSource
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Parse compound component name
|
|
155
|
-
*
|
|
156
|
-
* Given "Card.Header" and parent "Card", returns "Header"
|
|
157
|
-
* Given "Card.Footer" and parent "Card", returns "Footer"
|
|
158
|
-
* Given "Button" and parent "Card", returns null (not a compound)
|
|
159
|
-
*
|
|
160
|
-
* @param componentName - Full component name (e.g., "Card.Header")
|
|
161
|
-
* @param parentName - Parent component name (e.g., "Card")
|
|
162
|
-
* @returns Slot name or null if not a compound of this parent
|
|
163
|
-
*/
|
|
164
|
-
function parseCompoundName(componentName: string, parentName: string): string | null {
|
|
165
|
-
const prefix = `${parentName}.`
|
|
166
|
-
|
|
167
|
-
if (componentName.startsWith(prefix)) {
|
|
168
|
-
return componentName.slice(prefix.length)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return null
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Resolve slots in component template nodes
|
|
176
|
-
*
|
|
177
|
-
* Replaces <slot /> and <slot name="X" /> with children from resolved slots.
|
|
178
|
-
* All slot content is rebound to the parent's reactive scope.
|
|
179
|
-
*/
|
|
180
|
-
export function resolveSlots(
|
|
181
|
-
componentNodes: TemplateNode[],
|
|
182
|
-
slots: ResolvedSlots
|
|
183
|
-
): TemplateNode[] {
|
|
184
|
-
const resolved: TemplateNode[] = []
|
|
185
|
-
|
|
186
|
-
for (const node of componentNodes) {
|
|
187
|
-
const result = resolveNode(node, slots)
|
|
188
|
-
if (Array.isArray(result)) {
|
|
189
|
-
resolved.push(...result)
|
|
190
|
-
} else {
|
|
191
|
-
resolved.push(result)
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return resolved
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Resolve a single node, replacing slot tags with content
|
|
200
|
-
* Ensures all slot content maintains the parent's reactive scope
|
|
201
|
-
*/
|
|
202
|
-
function resolveNode(
|
|
203
|
-
node: TemplateNode,
|
|
204
|
-
slots: ResolvedSlots
|
|
205
|
-
): TemplateNode | TemplateNode[] {
|
|
206
|
-
if (node.type === 'element' && node.tag === 'slot') {
|
|
207
|
-
// This is a slot tag - replace it with children
|
|
208
|
-
const nameAttr = node.attributes.find(attr => attr.name === 'name')
|
|
209
|
-
const slotName = typeof nameAttr?.value === 'string' ? nameAttr.value : null
|
|
210
|
-
|
|
211
|
-
if (slotName) {
|
|
212
|
-
// Named slot
|
|
213
|
-
const namedChildren = slots.named.get(slotName.toLowerCase()) || []
|
|
214
|
-
|
|
215
|
-
// If no children provided and slot has fallback content, use fallback
|
|
216
|
-
if (namedChildren.length === 0 && node.children.length > 0) {
|
|
217
|
-
return node.children
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Return slot content (already scoped during extraction)
|
|
221
|
-
return namedChildren.length > 0 ? namedChildren : []
|
|
222
|
-
} else {
|
|
223
|
-
// Default slot
|
|
224
|
-
// If no children provided and slot has fallback content, use fallback
|
|
225
|
-
if (slots.default.length === 0 && node.children.length > 0) {
|
|
226
|
-
return node.children
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Return slot content (already scoped during extraction)
|
|
230
|
-
return slots.default
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (node.type === 'element') {
|
|
235
|
-
// Recursively resolve slots in children
|
|
236
|
-
const resolvedChildren: TemplateNode[] = []
|
|
237
|
-
for (const child of node.children) {
|
|
238
|
-
const result = resolveNode(child, slots)
|
|
239
|
-
if (Array.isArray(result)) {
|
|
240
|
-
resolvedChildren.push(...result)
|
|
241
|
-
} else {
|
|
242
|
-
resolvedChildren.push(result)
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return {
|
|
247
|
-
...node,
|
|
248
|
-
children: resolvedChildren
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (node.type === 'component') {
|
|
253
|
-
// Recursively resolve slots in component children
|
|
254
|
-
const resolvedChildren: TemplateNode[] = []
|
|
255
|
-
for (const child of node.children) {
|
|
256
|
-
const result = resolveNode(child, slots)
|
|
257
|
-
if (Array.isArray(result)) {
|
|
258
|
-
resolvedChildren.push(...result)
|
|
259
|
-
} else {
|
|
260
|
-
resolvedChildren.push(result)
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return {
|
|
265
|
-
...node,
|
|
266
|
-
children: resolvedChildren
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Text and expression nodes pass through unchanged
|
|
271
|
-
return node
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Check if a node tree contains any slots
|
|
276
|
-
*/
|
|
277
|
-
export function hasSlots(nodes: TemplateNode[]): boolean {
|
|
278
|
-
function checkNode(node: TemplateNode): boolean {
|
|
279
|
-
if (node.type === 'element') {
|
|
280
|
-
if (node.tag === 'slot') {
|
|
281
|
-
return true
|
|
282
|
-
}
|
|
283
|
-
return node.children.some(checkNode)
|
|
284
|
-
}
|
|
285
|
-
if (node.type === 'component') {
|
|
286
|
-
return node.children.some(checkNode)
|
|
287
|
-
}
|
|
288
|
-
return false
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return nodes.some(checkNode)
|
|
292
|
-
}
|