meno-core 1.0.52 → 1.0.53
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/build-astro.ts +183 -13
- package/build-next.ts +1361 -0
- package/build-static.ts +7 -5
- package/dist/bin/cli.js +2 -2
- package/dist/build-static.js +6 -6
- package/dist/chunks/{chunk-HNLUO36W.js → chunk-GZHGVVW3.js} +2 -2
- package/dist/chunks/chunk-GZHGVVW3.js.map +7 -0
- package/dist/chunks/{chunk-LPVETICS.js → chunk-H3GJ4H2U.js} +185 -1
- package/dist/chunks/chunk-H3GJ4H2U.js.map +7 -0
- package/dist/chunks/{chunk-CXCBV2M7.js → chunk-IGYR22T6.js} +76 -270
- package/dist/chunks/chunk-IGYR22T6.js.map +7 -0
- package/dist/chunks/{chunk-LHLHPYSP.js → chunk-JGP5A3Y5.js} +12 -11
- package/dist/chunks/chunk-JGP5A3Y5.js.map +7 -0
- package/dist/chunks/{chunk-7NIC4I3V.js → chunk-JGWFTO6P.js} +167 -21
- package/dist/chunks/chunk-JGWFTO6P.js.map +7 -0
- package/dist/chunks/{chunk-EDQSMAMP.js → chunk-O3NAGJP4.js} +85 -4
- package/dist/chunks/chunk-O3NAGJP4.js.map +7 -0
- package/dist/chunks/{chunk-H4JSCDNW.js → chunk-QB2LNO4W.js} +24 -1
- package/dist/chunks/chunk-QB2LNO4W.js.map +7 -0
- package/dist/chunks/{chunk-A725KYFK.js → chunk-R6XHAFBF.js} +561 -112
- package/dist/chunks/chunk-R6XHAFBF.js.map +7 -0
- package/dist/chunks/{chunk-J23ZX5AP.js → chunk-X754AHS5.js} +277 -1
- package/dist/chunks/chunk-X754AHS5.js.map +7 -0
- package/dist/chunks/{chunk-2QK6U5UK.js → chunk-YBLHKYFF.js} +12 -2
- package/dist/chunks/chunk-YBLHKYFF.js.map +7 -0
- package/dist/chunks/{constants-GWBAD66U.js → constants-STK2YBIW.js} +2 -2
- package/dist/entries/server-router.js +7 -7
- package/dist/lib/client/index.js +354 -59
- package/dist/lib/client/index.js.map +4 -4
- package/dist/lib/server/index.js +1458 -190
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +202 -34
- package/dist/lib/shared/index.js.map +4 -4
- package/dist/lib/test-utils/index.js +1 -1
- package/entries/client-router.tsx +5 -165
- package/lib/client/ErrorBoundary.test.tsx +27 -25
- package/lib/client/ErrorBoundary.tsx +34 -19
- package/lib/client/core/ComponentBuilder.ts +19 -2
- package/lib/client/core/builders/embedBuilder.ts +8 -4
- package/lib/client/core/builders/listBuilder.ts +23 -4
- package/lib/client/fontFamiliesService.test.ts +76 -0
- package/lib/client/fontFamiliesService.ts +69 -0
- package/lib/client/hmrCssReload.ts +160 -0
- package/lib/client/hooks/useColorVariables.ts +2 -0
- package/lib/client/index.ts +4 -0
- package/lib/client/meno-filter/ui.ts +2 -0
- package/lib/client/routing/RouteLoader.test.ts +2 -2
- package/lib/client/routing/RouteLoader.ts +8 -2
- package/lib/client/routing/Router.tsx +81 -15
- package/lib/client/scripts/ScriptExecutor.test.ts +143 -0
- package/lib/client/scripts/ScriptExecutor.ts +56 -2
- package/lib/client/styles/StyleInjector.ts +20 -5
- package/lib/client/styles/UtilityClassCollector.ts +7 -1
- package/lib/client/styles/cspNonce.test.ts +67 -0
- package/lib/client/styles/cspNonce.ts +63 -0
- package/lib/client/templateEngine.test.ts +80 -0
- package/lib/client/templateEngine.ts +5 -0
- package/lib/server/astro/cmsPageEmitter.ts +35 -5
- package/lib/server/astro/componentEmitter.ts +61 -5
- package/lib/server/astro/nodeToAstro.ts +149 -11
- package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +264 -0
- package/lib/server/astro/normalizeOrphanTemplateProps.ts +184 -0
- package/lib/server/createServer.ts +11 -0
- package/lib/server/draftPageStore.ts +49 -0
- package/lib/server/fileWatcher.ts +62 -2
- package/lib/server/index.ts +13 -1
- package/lib/server/providers/fileSystemPageProvider.ts +8 -0
- package/lib/server/routes/api/components.ts +9 -4
- package/lib/server/routes/api/core-routes.ts +2 -2
- package/lib/server/routes/api/pages.ts +14 -22
- package/lib/server/routes/api/shared.ts +56 -0
- package/lib/server/routes/index.ts +90 -0
- package/lib/server/routes/pages.ts +13 -6
- package/lib/server/services/componentService.test.ts +199 -2
- package/lib/server/services/componentService.ts +354 -49
- package/lib/server/services/fileWatcherService.ts +4 -24
- package/lib/server/services/pageService.test.ts +23 -0
- package/lib/server/services/pageService.ts +124 -6
- package/lib/server/ssr/attributeBuilder.ts +8 -2
- package/lib/server/ssr/buildErrorOverlay.ts +1 -1
- package/lib/server/ssr/errorOverlay.test.ts +21 -2
- package/lib/server/ssr/errorOverlay.ts +38 -11
- package/lib/server/ssr/htmlGenerator.test.ts +53 -13
- package/lib/server/ssr/htmlGenerator.ts +71 -27
- package/lib/server/ssr/liveReloadIntegration.test.ts +123 -2
- package/lib/server/ssr/ssrRenderer.test.ts +67 -0
- package/lib/server/ssr/ssrRenderer.ts +94 -9
- package/lib/server/websocketManager.ts +0 -1
- package/lib/shared/componentRefs.ts +45 -0
- package/lib/shared/constants.ts +8 -0
- package/lib/shared/cssGeneration.ts +2 -0
- package/lib/shared/cssProperties.ts +184 -0
- package/lib/shared/expressionEvaluator.ts +54 -0
- package/lib/shared/fontCss.ts +101 -0
- package/lib/shared/fontLoader.ts +8 -86
- package/lib/shared/friendlyError.test.ts +87 -0
- package/lib/shared/friendlyError.ts +121 -0
- package/lib/shared/hrefRefs.test.ts +130 -0
- package/lib/shared/hrefRefs.ts +100 -0
- package/lib/shared/index.ts +52 -0
- package/lib/shared/inlineSvgStyleRules.test.ts +108 -0
- package/lib/shared/inlineSvgStyleRules.ts +134 -0
- package/lib/shared/interfaces/contentProvider.ts +13 -0
- package/lib/shared/itemTemplateUtils.test.ts +14 -0
- package/lib/shared/itemTemplateUtils.ts +4 -1
- package/lib/shared/registry/NodeTypeDefinition.ts +1 -1
- package/lib/shared/registry/nodeTypes/LinkNodeType.ts +1 -1
- package/lib/shared/slugTranslator.test.ts +24 -0
- package/lib/shared/slugTranslator.ts +24 -0
- package/lib/shared/styleNodeUtils.ts +4 -1
- package/lib/shared/tree/PathBuilder.test.ts +128 -1
- package/lib/shared/tree/PathBuilder.ts +83 -31
- package/lib/shared/types/comment.ts +99 -0
- package/lib/shared/types/index.ts +12 -0
- package/lib/shared/types/rendering.ts +8 -0
- package/lib/shared/utilityClassConfig.ts +4 -2
- package/lib/shared/utilityClassMapper.test.ts +24 -0
- package/lib/shared/validation/commentValidators.ts +69 -0
- package/lib/shared/validation/index.ts +1 -0
- package/lib/shared/viewportUnits.integration.test.ts +42 -0
- package/lib/shared/viewportUnits.test.ts +103 -0
- package/lib/shared/viewportUnits.ts +63 -0
- package/lib/test-utils/dom-setup.ts +6 -0
- package/package.json +1 -1
- package/dist/chunks/chunk-2QK6U5UK.js.map +0 -7
- package/dist/chunks/chunk-7NIC4I3V.js.map +0 -7
- package/dist/chunks/chunk-A725KYFK.js.map +0 -7
- package/dist/chunks/chunk-CXCBV2M7.js.map +0 -7
- package/dist/chunks/chunk-EDQSMAMP.js.map +0 -7
- package/dist/chunks/chunk-H4JSCDNW.js.map +0 -7
- package/dist/chunks/chunk-HNLUO36W.js.map +0 -7
- package/dist/chunks/chunk-J23ZX5AP.js.map +0 -7
- package/dist/chunks/chunk-LHLHPYSP.js.map +0 -7
- package/dist/chunks/chunk-LPVETICS.js.map +0 -7
- /package/dist/chunks/{constants-GWBAD66U.js.map → constants-STK2YBIW.js.map} +0 -0
|
@@ -32,6 +32,7 @@ import type { BreakpointConfig } from '../../shared/breakpoints';
|
|
|
32
32
|
import type { ResponsiveScales } from '../../shared/responsiveScaling';
|
|
33
33
|
import { generateElementClassName, type ElementClassContext } from '../../shared/elementClassName';
|
|
34
34
|
import { isVoidElement, hasIf, hasChildren, isSlotContent as isSlotContentNode } from '../../shared/nodeUtils';
|
|
35
|
+
import { deepMergeStyles } from '../../shared/styleNodeUtils';
|
|
35
36
|
import { NODE_TYPE, RAW_HTML_PREFIX } from '../../shared/constants';
|
|
36
37
|
import { extractInteractiveStyleMappings, hasInteractiveStyleMappings } from '../../shared/interactiveStyleMappings';
|
|
37
38
|
import type { ImageMetadataMap } from '../ssr/imageMetadata';
|
|
@@ -94,6 +95,18 @@ export interface AstroEmitContext {
|
|
|
94
95
|
cmsSchema?: CMSSchema;
|
|
95
96
|
/** Wrap function name for i18n resolution (e.g., 'r') */
|
|
96
97
|
cmsWrapFn?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Names of components that consume the CMS entry. When emitting an instance
|
|
100
|
+
* of one of these inside a CMS context, thread `cms={<entry binding>}` so the
|
|
101
|
+
* component (which has an isolated Astro scope) can resolve `{{cms.*}}`.
|
|
102
|
+
*/
|
|
103
|
+
cmsConsumers?: Set<string>;
|
|
104
|
+
/**
|
|
105
|
+
* collectionId → JS template literal (over entry `e`) for an item's `_url`,
|
|
106
|
+
* used when flattening `sourceType: 'collection'` lists so list items expose
|
|
107
|
+
* a usable link (`{{item._url}}`).
|
|
108
|
+
*/
|
|
109
|
+
collectionUrlExpr?: Map<string, string>;
|
|
97
110
|
/** Default locale for i18n resolver (used in component defs) */
|
|
98
111
|
defaultLocale?: string;
|
|
99
112
|
/** Set to true during emission when an i18n object is encountered in a component def */
|
|
@@ -139,6 +152,16 @@ function ind(ctx: AstroEmitContext): string {
|
|
|
139
152
|
return ' '.repeat(ctx.indent);
|
|
140
153
|
}
|
|
141
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Build the runtime href expression for a link-typed value. A link value may be
|
|
157
|
+
* an object `{ href, target? }` or a bare string URL — the prop validator
|
|
158
|
+
* coerces strings to `{ href }`, so collection lists and CMS fields legitimately
|
|
159
|
+
* pass a string (e.g. `post._url`). Mirror that coercion at render time.
|
|
160
|
+
*/
|
|
161
|
+
function linkHrefExpr(expr: string): string {
|
|
162
|
+
return `(typeof ${expr} === 'string' ? ${expr} : ${expr}?.href) ?? "#"`;
|
|
163
|
+
}
|
|
164
|
+
|
|
142
165
|
function localizeHref(href: string, ctx: AstroEmitContext): string {
|
|
143
166
|
if (!href.startsWith('/') || href.startsWith('//')) return href;
|
|
144
167
|
const { locale, i18nDefaultLocale, slugMappings } = ctx;
|
|
@@ -505,6 +528,70 @@ function buildElementClass(
|
|
|
505
528
|
});
|
|
506
529
|
}
|
|
507
530
|
|
|
531
|
+
function isResponsiveStyleObject(
|
|
532
|
+
style: StyleObject | ResponsiveStyleObject
|
|
533
|
+
): style is ResponsiveStyleObject {
|
|
534
|
+
return 'base' in style || 'tablet' in style || 'mobile' in style;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* The style on a component's root element — `node.style` for HTML/embed/list/
|
|
539
|
+
* link roots, or `props.style` for a component/link-instance root. Used to
|
|
540
|
+
* replicate SSR's per-breakpoint merge of an instance override into the root.
|
|
541
|
+
*/
|
|
542
|
+
function getComponentRootStyle(
|
|
543
|
+
def: ComponentDefinition | undefined
|
|
544
|
+
): StyleObject | ResponsiveStyleObject | undefined {
|
|
545
|
+
const root = def?.component?.structure as
|
|
546
|
+
| { style?: unknown; props?: { style?: unknown } }
|
|
547
|
+
| undefined;
|
|
548
|
+
if (!root || typeof root !== 'object') return undefined;
|
|
549
|
+
const s = (root.style ?? root.props?.style);
|
|
550
|
+
return s && typeof s === 'object' ? (s as StyleObject | ResponsiveStyleObject) : undefined;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* True when a flat style has any property, or any responsive branch
|
|
555
|
+
* (base/tablet/mobile/custom breakpoint) is non-empty. Guards against emitting
|
|
556
|
+
* an empty `.class {}` rule / dangling `class=""` for the common empty override
|
|
557
|
+
* shape `{ base:{}, tablet:{}, mobile:{} }`.
|
|
558
|
+
*/
|
|
559
|
+
function hasNonEmptyStyle(style: StyleObject | ResponsiveStyleObject): boolean {
|
|
560
|
+
if (isResponsiveStyleObject(style)) {
|
|
561
|
+
return Object.values(style).some(
|
|
562
|
+
(branch) => !!branch && typeof branch === 'object' && Object.keys(branch as object).length > 0
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
return Object.keys(style).length > 0;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Drop properties whose value is a `{{...}}` template expression — these can't
|
|
570
|
+
* be serialized to static CSS. Recurses into responsive branches. Template-
|
|
571
|
+
* expression instance overrides are a pre-existing unsupported case; this just
|
|
572
|
+
* avoids emitting invalid CSS like `padding: {{x}}`.
|
|
573
|
+
*/
|
|
574
|
+
function stripTemplateExpressionStyle<T extends StyleObject | ResponsiveStyleObject>(style: T): T {
|
|
575
|
+
const cleanFlat = (s: Record<string, unknown>): Record<string, unknown> => {
|
|
576
|
+
const out: Record<string, unknown> = {};
|
|
577
|
+
for (const [k, v] of Object.entries(s)) {
|
|
578
|
+
if (typeof v === 'string' && hasTemplates(v)) continue;
|
|
579
|
+
out[k] = v;
|
|
580
|
+
}
|
|
581
|
+
return out;
|
|
582
|
+
};
|
|
583
|
+
if (isResponsiveStyleObject(style)) {
|
|
584
|
+
const out: Record<string, unknown> = {};
|
|
585
|
+
for (const [bp, branch] of Object.entries(style as Record<string, unknown>)) {
|
|
586
|
+
out[bp] = branch && typeof branch === 'object'
|
|
587
|
+
? cleanFlat(branch as Record<string, unknown>)
|
|
588
|
+
: branch;
|
|
589
|
+
}
|
|
590
|
+
return out as T;
|
|
591
|
+
}
|
|
592
|
+
return cleanFlat(style as Record<string, unknown>) as T;
|
|
593
|
+
}
|
|
594
|
+
|
|
508
595
|
|
|
509
596
|
/**
|
|
510
597
|
* Build HTML attributes string from node attributes
|
|
@@ -529,7 +616,7 @@ function buildAttributesString(
|
|
|
529
616
|
if (ctx.listIndexVar) expr = replaceItemMetaVars(expr, ctx.listIndexVar, ctx.listSourceVar);
|
|
530
617
|
const propDef = ctx.componentProps[expr];
|
|
531
618
|
if (propDef && propDef.type === 'link') {
|
|
532
|
-
parts.push(`${key}={${expr}
|
|
619
|
+
parts.push(`${key}={${linkHrefExpr(expr)}}`);
|
|
533
620
|
} else {
|
|
534
621
|
parts.push(`${key}={${expr} || undefined}`);
|
|
535
622
|
}
|
|
@@ -540,7 +627,7 @@ function buildAttributesString(
|
|
|
540
627
|
if (ctx.listItemBinding) trimmed = rewriteItemVar(trimmed, ctx.listItemBinding);
|
|
541
628
|
if (ctx.listIndexVar) trimmed = replaceItemMetaVars(trimmed, ctx.listIndexVar, ctx.listSourceVar);
|
|
542
629
|
const pd = ctx.componentProps[trimmed];
|
|
543
|
-
return pd?.type === 'link' ? `\${${trimmed}
|
|
630
|
+
return pd?.type === 'link' ? `\${${linkHrefExpr(trimmed)}}` : `\${${trimmed}}`;
|
|
544
631
|
});
|
|
545
632
|
parts.push(`${key}={\`${resolved}\`}`);
|
|
546
633
|
}
|
|
@@ -1310,11 +1397,54 @@ function emitComponentInstance(node: ComponentInstanceNode, ctx: AstroEmitContex
|
|
|
1310
1397
|
}
|
|
1311
1398
|
}
|
|
1312
1399
|
|
|
1313
|
-
//
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1400
|
+
// Thread the CMS entry into consumer components. Astro components have
|
|
1401
|
+
// isolated scopes, so a component referencing `{{cms.*}}` can't see the
|
|
1402
|
+
// page's `entry`/`cms` binding unless we pass it explicitly. The binding is
|
|
1403
|
+
// `entry` at the CMS page level and `cms` inside a consumer component, so
|
|
1404
|
+
// this also forwards correctly through nested consumers.
|
|
1405
|
+
if (ctx.cmsMode && ctx.cmsConsumers?.has(node.component) && !('cms' in (node.props ?? {}))) {
|
|
1406
|
+
propParts.push(`cms={${ctx.cmsEntryBinding || 'entry'}}`);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// Instance-level style overrides (+ interactive styles) via element-class CSS.
|
|
1410
|
+
// Route them through the same machinery the export uses for hover styles: an
|
|
1411
|
+
// UNLAYERED `<style is:global>` rule, which beats Tailwind v4's `@layer
|
|
1412
|
+
// utilities` deterministically — so the instance override wins over the
|
|
1413
|
+
// component's base utilities. This mirrors live SSR, which deep-merges the
|
|
1414
|
+
// instance style into the component root. (The previous approach appended the
|
|
1415
|
+
// override as more utility classes at equal specificity, so CSS output order
|
|
1416
|
+
// decided the winner and the override frequently lost.)
|
|
1417
|
+
{
|
|
1418
|
+
const instanceStyle = node.style as StyleObject | ResponsiveStyleObject | undefined;
|
|
1419
|
+
const instanceInteractive = node.interactiveStyles as InteractiveStyles | undefined;
|
|
1420
|
+
const hasStyle = !!instanceStyle && hasNonEmptyStyle(instanceStyle);
|
|
1421
|
+
const hasInteractive = Array.isArray(instanceInteractive) && instanceInteractive.length > 0;
|
|
1422
|
+
|
|
1423
|
+
if ((hasStyle || hasInteractive) && ctx.collectedInteractiveStyles) {
|
|
1424
|
+
const elementClass = buildElementClass(ctx, node.label);
|
|
1425
|
+
const rules: InteractiveStyles = [];
|
|
1426
|
+
if (hasStyle) {
|
|
1427
|
+
// Per-breakpoint merge the component's root style UNDER the instance
|
|
1428
|
+
// override (mirrors SSR's mergeNodeStyles). This keeps the component's
|
|
1429
|
+
// own responsive breakpoint values for properties the instance only sets
|
|
1430
|
+
// at `base`, so a base override doesn't leak past tablet/mobile. The
|
|
1431
|
+
// merged rule is unlayered, so it fully shadows the component's own
|
|
1432
|
+
// (layered) root utilities. `_mapping`/`{{…}}` values carried in from the
|
|
1433
|
+
// root are dropped downstream (styleObjectToCSS skips mappings) /
|
|
1434
|
+
// stripped here, falling back to the component's own emission.
|
|
1435
|
+
const rootStyle = getComponentRootStyle(ctx.globalComponents[node.component]);
|
|
1436
|
+
const merged = (rootStyle
|
|
1437
|
+
? deepMergeStyles(rootStyle, instanceStyle!)
|
|
1438
|
+
: instanceStyle!) as StyleObject | ResponsiveStyleObject;
|
|
1439
|
+
const cssSafe = stripTemplateExpressionStyle(merged);
|
|
1440
|
+
if (hasNonEmptyStyle(cssSafe)) rules.push({ style: cssSafe }); // no prefix/postfix → `.cls {…}`
|
|
1441
|
+
}
|
|
1442
|
+
if (hasInteractive) rules.push(...instanceInteractive!); // fold hover/etc. into the same class
|
|
1443
|
+
if (rules.length > 0) {
|
|
1444
|
+
const existing = ctx.collectedInteractiveStyles.get(elementClass);
|
|
1445
|
+
ctx.collectedInteractiveStyles.set(elementClass, existing ? [...existing, ...rules] : rules);
|
|
1446
|
+
propParts.push(`class="${elementClass}"`);
|
|
1447
|
+
}
|
|
1318
1448
|
}
|
|
1319
1449
|
}
|
|
1320
1450
|
|
|
@@ -1461,8 +1591,8 @@ function emitLinkNode(node: LinkNode, ctx: AstroEmitContext): string {
|
|
|
1461
1591
|
if (isLinkMapping(nodeHref)) {
|
|
1462
1592
|
if (ctx.isComponentDef) {
|
|
1463
1593
|
const propRef = (nodeHref as LinkMapping).prop;
|
|
1464
|
-
// Link props are objects with {href, target?}
|
|
1465
|
-
hrefAttr = ` href={${propRef}
|
|
1594
|
+
// Link props are objects with {href, target?} — or a coerced string URL
|
|
1595
|
+
hrefAttr = ` href={${linkHrefExpr(propRef)}}`;
|
|
1466
1596
|
} else {
|
|
1467
1597
|
hrefAttr = ' href="#"';
|
|
1468
1598
|
}
|
|
@@ -1476,7 +1606,7 @@ function emitLinkNode(node: LinkNode, ctx: AstroEmitContext): string {
|
|
|
1476
1606
|
if (ctx.listIndexVar) expr = replaceItemMetaVars(expr, ctx.listIndexVar, ctx.listSourceVar);
|
|
1477
1607
|
const propDef = ctx.componentProps[expr];
|
|
1478
1608
|
if (propDef && propDef.type === 'link') {
|
|
1479
|
-
hrefAttr = ` href={${expr}
|
|
1609
|
+
hrefAttr = ` href={${linkHrefExpr(expr)}}`;
|
|
1480
1610
|
} else {
|
|
1481
1611
|
hrefAttr = ` href={${expr}}`;
|
|
1482
1612
|
}
|
|
@@ -1486,7 +1616,7 @@ function emitLinkNode(node: LinkNode, ctx: AstroEmitContext): string {
|
|
|
1486
1616
|
if (ctx.listItemBinding) trimmed = rewriteItemVar(trimmed, ctx.listItemBinding);
|
|
1487
1617
|
if (ctx.listIndexVar) trimmed = replaceItemMetaVars(trimmed, ctx.listIndexVar, ctx.listSourceVar);
|
|
1488
1618
|
const pd = ctx.componentProps[trimmed];
|
|
1489
|
-
return pd?.type === 'link' ? `\${${trimmed}
|
|
1619
|
+
return pd?.type === 'link' ? `\${${linkHrefExpr(trimmed)}}` : `\${${trimmed}}`;
|
|
1490
1620
|
});
|
|
1491
1621
|
hrefAttr = ` href={\`${resolved}\`}`;
|
|
1492
1622
|
}
|
|
@@ -1742,6 +1872,14 @@ function emitCollectionListNode(node: ListNode, ctx: AstroEmitContext): string {
|
|
|
1742
1872
|
queryChain += `.then(items => items.slice(${start}${end !== undefined ? `, ${end}` : ''}))`;
|
|
1743
1873
|
}
|
|
1744
1874
|
|
|
1875
|
+
// Flatten content-collection entries ({ id, data: {...} }) into the flat item
|
|
1876
|
+
// shape the templates expect ({{item.field}} → item.field), and synthesize the
|
|
1877
|
+
// `_url`/`_id` meta the CMS data model exposes. Filter/sort above operate on the
|
|
1878
|
+
// raw entries (e.data.*); this map runs last so it sees the final set.
|
|
1879
|
+
const urlExpr = ctx.collectionUrlExpr?.get(source)
|
|
1880
|
+
?? `\`/${source}/\${e.data.slug ?? e.id}\``;
|
|
1881
|
+
queryChain += `.then(items => items.map((e) => ({ ...e.data, _id: e.id, _url: ${urlExpr} })))`;
|
|
1882
|
+
|
|
1745
1883
|
ctx.frontmatterLines.push(`const ${collectionVar} = ${queryChain};`);
|
|
1746
1884
|
|
|
1747
1885
|
// Build children with item binding
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { test, expect, describe } from "bun:test";
|
|
2
|
+
import { normalizeOrphanTemplateProps } from "./normalizeOrphanTemplateProps";
|
|
3
|
+
import type { ComponentDefinition } from "../../shared/types";
|
|
4
|
+
|
|
5
|
+
describe("normalizeOrphanTemplateProps", () => {
|
|
6
|
+
test("lifts an orphan {{x}} onto the component's interface as a string default", () => {
|
|
7
|
+
const components: Record<string, ComponentDefinition> = {
|
|
8
|
+
ArrowLink: {
|
|
9
|
+
component: {
|
|
10
|
+
interface: {},
|
|
11
|
+
structure: {
|
|
12
|
+
type: "node",
|
|
13
|
+
tag: "span",
|
|
14
|
+
children: ["{{ctaText}}"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const out = normalizeOrphanTemplateProps(components);
|
|
21
|
+
const arrowIface = out.ArrowLink.component!.interface!;
|
|
22
|
+
expect(arrowIface.ctaText).toBeDefined();
|
|
23
|
+
expect((arrowIface.ctaText as any).type).toBe("string");
|
|
24
|
+
expect((arrowIface.ctaText as any).default).toBe("");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("forwards lifted props on an instance ref when the host declares the prop", () => {
|
|
28
|
+
const components: Record<string, ComponentDefinition> = {
|
|
29
|
+
ArrowLink: {
|
|
30
|
+
component: {
|
|
31
|
+
interface: {},
|
|
32
|
+
structure: {
|
|
33
|
+
type: "node",
|
|
34
|
+
tag: "span",
|
|
35
|
+
children: ["{{ctaText}}"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
Card: {
|
|
40
|
+
component: {
|
|
41
|
+
interface: {
|
|
42
|
+
ctaText: { type: "string", default: "Click" } as any
|
|
43
|
+
},
|
|
44
|
+
structure: {
|
|
45
|
+
type: "node",
|
|
46
|
+
tag: "div",
|
|
47
|
+
children: [
|
|
48
|
+
{ type: "component", component: "ArrowLink" } as any
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const out = normalizeOrphanTemplateProps(components);
|
|
56
|
+
const cardChildren = (out.Card.component!.structure as any).children;
|
|
57
|
+
expect(cardChildren[0].props).toBeDefined();
|
|
58
|
+
expect(cardChildren[0].props.ctaText).toBe("{{ctaText}}");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("does NOT forward when the host doesn't declare the same prop", () => {
|
|
62
|
+
const components: Record<string, ComponentDefinition> = {
|
|
63
|
+
ArrowLink: {
|
|
64
|
+
component: {
|
|
65
|
+
interface: {},
|
|
66
|
+
structure: { type: "node", tag: "span", children: ["{{ctaText}}"] }
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
// Page-level wrapper that doesn't know about ctaText.
|
|
70
|
+
Wrapper: {
|
|
71
|
+
component: {
|
|
72
|
+
interface: {},
|
|
73
|
+
structure: {
|
|
74
|
+
type: "node",
|
|
75
|
+
tag: "div",
|
|
76
|
+
children: [{ type: "component", component: "ArrowLink" } as any]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const out = normalizeOrphanTemplateProps(components);
|
|
83
|
+
const wrapperChildren = (out.Wrapper.component!.structure as any).children;
|
|
84
|
+
// No forward — would have made Wrapper produce a new orphan ref.
|
|
85
|
+
expect(wrapperChildren[0].props).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("does not override an instance prop the host already passes", () => {
|
|
89
|
+
const components: Record<string, ComponentDefinition> = {
|
|
90
|
+
ArrowLink: {
|
|
91
|
+
component: {
|
|
92
|
+
interface: {},
|
|
93
|
+
structure: { type: "node", tag: "span", children: ["{{ctaText}}"] }
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
Card: {
|
|
97
|
+
component: {
|
|
98
|
+
interface: {
|
|
99
|
+
ctaText: { type: "string", default: "Click" } as any
|
|
100
|
+
},
|
|
101
|
+
structure: {
|
|
102
|
+
type: "node",
|
|
103
|
+
tag: "div",
|
|
104
|
+
children: [
|
|
105
|
+
{
|
|
106
|
+
type: "component",
|
|
107
|
+
component: "ArrowLink",
|
|
108
|
+
props: { ctaText: "Explicit value" }
|
|
109
|
+
} as any
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const out = normalizeOrphanTemplateProps(components);
|
|
117
|
+
const cardChildren = (out.Card.component!.structure as any).children;
|
|
118
|
+
expect(cardChildren[0].props.ctaText).toBe("Explicit value");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("ignores cms.*, item.*, page.* refs (already namespaced)", () => {
|
|
122
|
+
const components: Record<string, ComponentDefinition> = {
|
|
123
|
+
Title: {
|
|
124
|
+
component: {
|
|
125
|
+
interface: {},
|
|
126
|
+
structure: {
|
|
127
|
+
type: "node",
|
|
128
|
+
tag: "h1",
|
|
129
|
+
children: ["{{cms.title}} - {{page.slug}}"]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const out = normalizeOrphanTemplateProps(components);
|
|
136
|
+
expect(Object.keys(out.Title.component!.interface!)).toHaveLength(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("ignores list loop variables", () => {
|
|
140
|
+
const components: Record<string, ComponentDefinition> = {
|
|
141
|
+
List: {
|
|
142
|
+
component: {
|
|
143
|
+
interface: {
|
|
144
|
+
items: { type: "list", default: [], itemSchema: {} } as any
|
|
145
|
+
},
|
|
146
|
+
structure: {
|
|
147
|
+
type: "list",
|
|
148
|
+
sourceType: "prop",
|
|
149
|
+
source: "items",
|
|
150
|
+
itemAs: "row",
|
|
151
|
+
children: [
|
|
152
|
+
{ type: "node", tag: "div", children: ["{{row.label}}"] }
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const out = normalizeOrphanTemplateProps(components);
|
|
160
|
+
// `row` is the loop var — must NOT be auto-lifted as a string prop.
|
|
161
|
+
expect((out.List.component!.interface as any).row).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("ignores expressions with operators (not a bare ident)", () => {
|
|
165
|
+
const components: Record<string, ComponentDefinition> = {
|
|
166
|
+
Cmp: {
|
|
167
|
+
component: {
|
|
168
|
+
interface: {},
|
|
169
|
+
structure: {
|
|
170
|
+
type: "node",
|
|
171
|
+
tag: "div",
|
|
172
|
+
children: ["{{a + b}} {{x ? y : z}}"]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const out = normalizeOrphanTemplateProps(components);
|
|
179
|
+
// Don't lift `a`, `b`, `x`, `y`, `z` from compound expressions — too
|
|
180
|
+
// risky to guess types or what's actually intended.
|
|
181
|
+
expect(Object.keys(out.Cmp.component!.interface!)).toHaveLength(0);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("does not mutate the input component definitions", () => {
|
|
185
|
+
const components: Record<string, ComponentDefinition> = {
|
|
186
|
+
ArrowLink: {
|
|
187
|
+
component: {
|
|
188
|
+
interface: {},
|
|
189
|
+
structure: { type: "node", tag: "span", children: ["{{ctaText}}"] }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const before = JSON.stringify(components);
|
|
194
|
+
normalizeOrphanTemplateProps(components);
|
|
195
|
+
expect(JSON.stringify(components)).toBe(before);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("lifts a prop ref from a style _mapping object", () => {
|
|
199
|
+
const components: Record<string, ComponentDefinition> = {
|
|
200
|
+
Cmp: {
|
|
201
|
+
component: {
|
|
202
|
+
interface: {},
|
|
203
|
+
structure: {
|
|
204
|
+
type: "node",
|
|
205
|
+
tag: "div",
|
|
206
|
+
style: {
|
|
207
|
+
base: {
|
|
208
|
+
color: {
|
|
209
|
+
_mapping: true,
|
|
210
|
+
prop: "variant",
|
|
211
|
+
values: { default: "black", featured: "white" }
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} as any
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const out = normalizeOrphanTemplateProps(components);
|
|
221
|
+
expect((out.Cmp.component!.interface as any).variant).toBeDefined();
|
|
222
|
+
expect((out.Cmp.component!.interface as any).variant.type).toBe("string");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("lifts a prop ref from an if boolean mapping", () => {
|
|
226
|
+
const components: Record<string, ComponentDefinition> = {
|
|
227
|
+
Cmp: {
|
|
228
|
+
component: {
|
|
229
|
+
interface: {},
|
|
230
|
+
structure: {
|
|
231
|
+
type: "node",
|
|
232
|
+
tag: "span",
|
|
233
|
+
if: {
|
|
234
|
+
_mapping: true,
|
|
235
|
+
prop: "showBadge",
|
|
236
|
+
values: { true: true, false: false }
|
|
237
|
+
}
|
|
238
|
+
} as any
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const out = normalizeOrphanTemplateProps(components);
|
|
244
|
+
expect((out.Cmp.component!.interface as any).showBadge).toBeDefined();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("dotted ref {{link.href}} lifts the base name", () => {
|
|
248
|
+
const components: Record<string, ComponentDefinition> = {
|
|
249
|
+
Anchor: {
|
|
250
|
+
component: {
|
|
251
|
+
interface: {},
|
|
252
|
+
structure: {
|
|
253
|
+
type: "node",
|
|
254
|
+
tag: "a",
|
|
255
|
+
attributes: { href: "{{link.href}}" }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const out = normalizeOrphanTemplateProps(components);
|
|
262
|
+
expect((out.Anchor.component!.interface as any).link).toBeDefined();
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize orphan template props for Astro emission.
|
|
3
|
+
*
|
|
4
|
+
* Each Meno component becomes its own `.astro` file. Templates like
|
|
5
|
+
* `{{ctaText}}` are emitted as `{ctaText}` — a JSX reference to a destructured
|
|
6
|
+
* `Astro.props` variable. If the component's `interface` doesn't declare the
|
|
7
|
+
* prop, the variable isn't in scope and Astro errors at build time.
|
|
8
|
+
*
|
|
9
|
+
* SSR/Next.js handles this at runtime via `processStructure`'s `parentProps`
|
|
10
|
+
* fallback (see templateEngine.ts). Astro can't fall back at runtime — each
|
|
11
|
+
* .astro file is its own scope — so we normalize the component graph before
|
|
12
|
+
* emission:
|
|
13
|
+
*
|
|
14
|
+
* 1. For each component, find orphan `{{x}}` refs in its structure (not
|
|
15
|
+
* declared in the interface, not item/cms/page namespaced, not a list
|
|
16
|
+
* loop var) and add them as optional string props with `default: ""`.
|
|
17
|
+
*
|
|
18
|
+
* 2. For each component-instance ref `<X />` inside any host's structure,
|
|
19
|
+
* forward each of X's auto-lifted props by writing
|
|
20
|
+
* `props.x = "{{x}}"` on the instance — but ONLY when the host itself
|
|
21
|
+
* already declares `x`. This mirrors the SSR cascade's one-level scope:
|
|
22
|
+
* the cascade fixes the single-step host→child case, never the leap to
|
|
23
|
+
* a grandparent.
|
|
24
|
+
*
|
|
25
|
+
* Multi-level chains (host doesn't have the prop either) gracefully degrade
|
|
26
|
+
* to the lifted default ("") instead of failing the build — same outcome as
|
|
27
|
+
* the SSR cascade for that shape.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { ComponentDefinition, PropDefinition } from '../../shared/types';
|
|
31
|
+
|
|
32
|
+
// Bare or dotted identifier inside `{{...}}`, with whitespace allowed around
|
|
33
|
+
// the body. Skips anything containing operators, calls, ternaries, etc. —
|
|
34
|
+
// liftable refs are *only* simple prop reads.
|
|
35
|
+
const BARE_IDENT_BODY = /^\s*([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\s*\.\s*[a-zA-Z_$][a-zA-Z0-9_$]*)*\s*$/;
|
|
36
|
+
|
|
37
|
+
// Namespaces and meta-vars that are never component-instance props.
|
|
38
|
+
const RESERVED_BASES = new Set([
|
|
39
|
+
'cms', 'item', 'page',
|
|
40
|
+
'itemIndex', 'itemFirst', 'itemLast',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
function recordRef(
|
|
44
|
+
base: string,
|
|
45
|
+
declared: Set<string>,
|
|
46
|
+
listVars: Set<string>,
|
|
47
|
+
out: Set<string>
|
|
48
|
+
): void {
|
|
49
|
+
if (RESERVED_BASES.has(base)) return;
|
|
50
|
+
if (listVars.has(base)) return;
|
|
51
|
+
if (declared.has(base)) return;
|
|
52
|
+
out.add(base);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function findOrphanRefs(value: unknown, declared: Set<string>, listVars: Set<string>, out: Set<string>): void {
|
|
56
|
+
if (value == null) return;
|
|
57
|
+
if (typeof value === 'string') {
|
|
58
|
+
const re = /\{\{([^}]+)\}\}/g;
|
|
59
|
+
let m: RegExpExecArray | null;
|
|
60
|
+
while ((m = re.exec(value)) !== null) {
|
|
61
|
+
const body = m[1];
|
|
62
|
+
const idMatch = body.match(BARE_IDENT_BODY);
|
|
63
|
+
if (!idMatch) continue; // expressions, operators, calls — skip
|
|
64
|
+
recordRef(idMatch[1], declared, listVars, out);
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
for (const v of value) findOrphanRefs(v, declared, listVars, out);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (typeof value !== 'object') return;
|
|
73
|
+
const obj = value as Record<string, unknown>;
|
|
74
|
+
|
|
75
|
+
// Mapping objects (`{ _mapping: true, prop: "variant", values: {...} }`)
|
|
76
|
+
// reference a component prop by name. The Astro emitter turns these into
|
|
77
|
+
// bare identifier references like `String(variant) === "default"`, so the
|
|
78
|
+
// `prop` field is just as much a binding site as a `{{variant}}` template.
|
|
79
|
+
// Used for style mappings, link mappings, HTML mappings, and `if` boolean
|
|
80
|
+
// mappings — all share the same shape.
|
|
81
|
+
if (obj._mapping === true && typeof obj.prop === 'string') {
|
|
82
|
+
recordRef(obj.prop, declared, listVars, out);
|
|
83
|
+
// Keep walking — `values` may contain nested templates too (rare, but
|
|
84
|
+
// not impossible).
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A `list` node introduces a new loop variable into the scope of its
|
|
88
|
+
// children (the template instance). Default name is `item` when
|
|
89
|
+
// `itemAs` is omitted.
|
|
90
|
+
let nextListVars = listVars;
|
|
91
|
+
if (obj.type === 'list') {
|
|
92
|
+
const itemAs = typeof obj.itemAs === 'string' && obj.itemAs ? obj.itemAs : 'item';
|
|
93
|
+
nextListVars = new Set(listVars);
|
|
94
|
+
nextListVars.add(itemAs);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const key of Object.keys(obj)) {
|
|
98
|
+
// Don't peek into a nested component instance's `props` — those refs
|
|
99
|
+
// are resolved in the host's scope, not the target's. We handle them
|
|
100
|
+
// when walking the host itself.
|
|
101
|
+
if (obj.type === 'component' && key === 'props') continue;
|
|
102
|
+
findOrphanRefs(obj[key], declared, nextListVars, out);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Walk a host's structure and forward each target component's lifted props
|
|
108
|
+
* from the host's scope. Only forwards props the host itself declares — see
|
|
109
|
+
* the module docstring for why.
|
|
110
|
+
*/
|
|
111
|
+
function forwardLiftedPropsOnInstances(
|
|
112
|
+
structure: unknown,
|
|
113
|
+
hostProps: Set<string>,
|
|
114
|
+
liftedByComp: ReadonlyMap<string, ReadonlySet<string>>
|
|
115
|
+
): void {
|
|
116
|
+
if (structure == null || typeof structure !== 'object') return;
|
|
117
|
+
if (Array.isArray(structure)) {
|
|
118
|
+
for (const s of structure) forwardLiftedPropsOnInstances(s, hostProps, liftedByComp);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const obj = structure as Record<string, unknown>;
|
|
122
|
+
if (obj.type === 'component' && typeof obj.component === 'string') {
|
|
123
|
+
const lifted = liftedByComp.get(obj.component);
|
|
124
|
+
if (lifted && lifted.size > 0) {
|
|
125
|
+
const props = (obj.props as Record<string, unknown> | undefined) ?? {};
|
|
126
|
+
let changed = false;
|
|
127
|
+
for (const propName of lifted) {
|
|
128
|
+
if (propName in props) continue;
|
|
129
|
+
if (!hostProps.has(propName)) continue;
|
|
130
|
+
props[propName] = `{{${propName}}}`;
|
|
131
|
+
changed = true;
|
|
132
|
+
}
|
|
133
|
+
if (changed) obj.props = props;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const key of Object.keys(obj)) {
|
|
137
|
+
forwardLiftedPropsOnInstances(obj[key], hostProps, liftedByComp);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Produce a normalized copy of the component map suitable for Astro emission.
|
|
143
|
+
* The input map is not mutated.
|
|
144
|
+
*/
|
|
145
|
+
export function normalizeOrphanTemplateProps(
|
|
146
|
+
components: Record<string, ComponentDefinition>
|
|
147
|
+
): Record<string, ComponentDefinition> {
|
|
148
|
+
const next: Record<string, ComponentDefinition> = {};
|
|
149
|
+
const liftedByComp = new Map<string, Set<string>>();
|
|
150
|
+
|
|
151
|
+
// Pass 1: clone every component and lift orphan refs onto its interface.
|
|
152
|
+
for (const [name, def] of Object.entries(components)) {
|
|
153
|
+
// structuredClone preserves Map/Set/Date but plain JSON deep-clone is
|
|
154
|
+
// enough here — component defs are JSON-serializable by construction.
|
|
155
|
+
const cloned = structuredClone(def) as ComponentDefinition;
|
|
156
|
+
next[name] = cloned;
|
|
157
|
+
|
|
158
|
+
const comp = cloned.component;
|
|
159
|
+
if (!comp || !comp.structure) continue;
|
|
160
|
+
const iface = (comp.interface ?? {}) as Record<string, PropDefinition>;
|
|
161
|
+
const declared = new Set(Object.keys(iface));
|
|
162
|
+
const orphans = new Set<string>();
|
|
163
|
+
findOrphanRefs(comp.structure, declared, new Set(), orphans);
|
|
164
|
+
if (orphans.size === 0) continue;
|
|
165
|
+
|
|
166
|
+
const augmented: Record<string, PropDefinition> = { ...iface };
|
|
167
|
+
for (const propName of orphans) {
|
|
168
|
+
augmented[propName] = { type: 'string', default: '' } as PropDefinition;
|
|
169
|
+
}
|
|
170
|
+
comp.interface = augmented;
|
|
171
|
+
liftedByComp.set(name, orphans);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Pass 2: forward lifted props from each host where the host declares the
|
|
175
|
+
// same prop. Mutates the cloned structures in `next`.
|
|
176
|
+
for (const [, hostDef] of Object.entries(next)) {
|
|
177
|
+
const comp = hostDef.component;
|
|
178
|
+
if (!comp || !comp.structure) continue;
|
|
179
|
+
const hostProps = new Set(Object.keys(comp.interface ?? {}));
|
|
180
|
+
forwardLiftedPropsOnInstances(comp.structure, hostProps, liftedByComp);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return next;
|
|
184
|
+
}
|