Prevent removal of nested slots within islands (#7093)
* Prevent removal of nested slots within islands * Fix build errors
This commit is contained in:
parent
e9fc2c2213
commit
3d525efc95
24 changed files with 288 additions and 26 deletions
24
.changeset/unlucky-lamps-remember.md
Normal file
24
.changeset/unlucky-lamps-remember.md
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
'@astrojs/preact': minor
|
||||||
|
'@astrojs/svelte': minor
|
||||||
|
'@astrojs/react': minor
|
||||||
|
'@astrojs/solid-js': minor
|
||||||
|
'@astrojs/vue': minor
|
||||||
|
'astro': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Prevent removal of nested slots within islands
|
||||||
|
|
||||||
|
This change introduces a new flag that renderers can add called `supportsAstroStaticSlot`. What this does is let Astro know that the render is sending `<astro-static-slot>` as placeholder values for static (non-hydrated) slots which Astro will then remove.
|
||||||
|
|
||||||
|
This change is completely backwards compatible, but fixes bugs caused by combining ssr-only and client-side framework components like so:
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<Component>
|
||||||
|
<div>
|
||||||
|
<Component client:load>
|
||||||
|
<span>Nested</span>
|
||||||
|
</Component>
|
||||||
|
</div>
|
||||||
|
</Component>
|
||||||
|
```
|
|
@ -94,6 +94,7 @@ export interface AstroComponentMetadata {
|
||||||
hydrateArgs?: any;
|
hydrateArgs?: any;
|
||||||
componentUrl?: string;
|
componentUrl?: string;
|
||||||
componentExport?: { value: string; namespace?: boolean };
|
componentExport?: { value: string; namespace?: boolean };
|
||||||
|
astroStaticSlot: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The flags supported by the Astro CLI */
|
/** The flags supported by the Astro CLI */
|
||||||
|
@ -1718,6 +1719,7 @@ export interface SSRLoadedRenderer extends AstroRenderer {
|
||||||
html: string;
|
html: string;
|
||||||
attrs?: Record<string, string>;
|
attrs?: Record<string, string>;
|
||||||
}>;
|
}>;
|
||||||
|
supportsAstroStaticSlot?: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,13 @@ function isHTMLComponent(Component: unknown) {
|
||||||
return Component && typeof Component === 'object' && (Component as any)['astro:html'];
|
return Component && typeof Component === 'object' && (Component as any)['astro:html'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ASTRO_SLOT_EXP = /\<\/?astro-slot\b[^>]*>/g;
|
||||||
|
const ASTRO_STATIC_SLOT_EXP = /\<\/?astro-static-slot\b[^>]*>/g;
|
||||||
|
function removeStaticAstroSlot(html: string, supportsAstroStaticSlot: boolean) {
|
||||||
|
const exp = supportsAstroStaticSlot ? ASTRO_STATIC_SLOT_EXP : ASTRO_SLOT_EXP;
|
||||||
|
return html.replace(exp, '');
|
||||||
|
}
|
||||||
|
|
||||||
async function renderFrameworkComponent(
|
async function renderFrameworkComponent(
|
||||||
result: SSRResult,
|
result: SSRResult,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
|
@ -68,7 +75,10 @@ async function renderFrameworkComponent(
|
||||||
}
|
}
|
||||||
|
|
||||||
const { renderers, clientDirectives } = result._metadata;
|
const { renderers, clientDirectives } = result._metadata;
|
||||||
const metadata: AstroComponentMetadata = { displayName };
|
const metadata: AstroComponentMetadata = {
|
||||||
|
astroStaticSlot: true,
|
||||||
|
displayName
|
||||||
|
};
|
||||||
|
|
||||||
const { hydration, isPage, props } = extractDirectives(_props, clientDirectives);
|
const { hydration, isPage, props } = extractDirectives(_props, clientDirectives);
|
||||||
let html = '';
|
let html = '';
|
||||||
|
@ -263,7 +273,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
if (isPage || renderer?.name === 'astro:jsx') {
|
if (isPage || renderer?.name === 'astro:jsx') {
|
||||||
yield html;
|
yield html;
|
||||||
} else if (html && html.length > 0) {
|
} else if (html && html.length > 0) {
|
||||||
yield markHTMLString(html.replace(/\<\/?astro-slot\b[^>]*>/g, ''));
|
yield markHTMLString(removeStaticAstroSlot(html, renderer?.ssr?.supportsAstroStaticSlot ?? false));
|
||||||
} else {
|
} else {
|
||||||
yield '';
|
yield '';
|
||||||
}
|
}
|
||||||
|
@ -288,7 +298,11 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
if (html) {
|
if (html) {
|
||||||
if (Object.keys(children).length > 0) {
|
if (Object.keys(children).length > 0) {
|
||||||
for (const key of Object.keys(children)) {
|
for (const key of Object.keys(children)) {
|
||||||
if (!html.includes(key === 'default' ? `<astro-slot>` : `<astro-slot name="${key}">`)) {
|
let tagName = renderer?.ssr?.supportsAstroStaticSlot ?
|
||||||
|
!!metadata.hydrate ? 'astro-slot' : 'astro-static-slot'
|
||||||
|
: 'astro-slot';
|
||||||
|
let expectedHTML = key === 'default' ? `<${tagName}>` : `<${tagName} name="${key}">`;
|
||||||
|
if (!html.includes(expectedHTML)) {
|
||||||
unrenderedSlots.push(key);
|
unrenderedSlots.push(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -202,7 +202,7 @@ export default function jsx({ settings, logging }: AstroPluginJSXOptions): Plugi
|
||||||
Unable to resolve a renderer that handles this file! With more than one renderer enabled, you should include an import or use a pragma comment.
|
Unable to resolve a renderer that handles this file! With more than one renderer enabled, you should include an import or use a pragma comment.
|
||||||
Add ${colors.cyan(
|
Add ${colors.cyan(
|
||||||
IMPORT_STATEMENTS[defaultRendererName] || `import '${defaultRendererName}';`
|
IMPORT_STATEMENTS[defaultRendererName] || `import '${defaultRendererName}';`
|
||||||
)} or ${colors.cyan(`/* jsxImportSource: ${defaultRendererName} */`)} to this file.
|
)} or ${colors.cyan(`/** @jsxImportSource: ${defaultRendererName} */`)} to this file.
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -3,6 +3,7 @@ import * as cheerio from 'cheerio';
|
||||||
import { loadFixture } from './test-utils.js';
|
import { loadFixture } from './test-utils.js';
|
||||||
|
|
||||||
describe('Nested Slots', () => {
|
describe('Nested Slots', () => {
|
||||||
|
/** @type {import('./test-utils').Fixture} */
|
||||||
let fixture;
|
let fixture;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
@ -23,4 +24,38 @@ describe('Nested Slots', () => {
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
expect($('script')).to.have.a.lengthOf(1, 'script rendered');
|
expect($('script')).to.have.a.lengthOf(1, 'script rendered');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Client components nested inside server-only framework components', () => {
|
||||||
|
/** @type {cheerio.CheerioAPI} */
|
||||||
|
let $;
|
||||||
|
before(async () => {
|
||||||
|
const html = await fixture.readFile('/server-component-nested/index.html');
|
||||||
|
$ = cheerio.load(html);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('react', () => {
|
||||||
|
expect($('#react astro-slot')).to.have.a.lengthOf(1);
|
||||||
|
expect($('#react astro-static-slot')).to.have.a.lengthOf(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('vue', () => {
|
||||||
|
expect($('#vue astro-slot')).to.have.a.lengthOf(1);
|
||||||
|
expect($('#vue astro-static-slot')).to.have.a.lengthOf(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preact', () => {
|
||||||
|
expect($('#preact astro-slot')).to.have.a.lengthOf(1);
|
||||||
|
expect($('#preact astro-static-slot')).to.have.a.lengthOf(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('solid', () => {
|
||||||
|
expect($('#solid astro-slot')).to.have.a.lengthOf(1);
|
||||||
|
expect($('#solid astro-static-slot')).to.have.a.lengthOf(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('svelte', () => {
|
||||||
|
expect($('#svelte astro-slot')).to.have.a.lengthOf(1);
|
||||||
|
expect($('#svelte astro-static-slot')).to.have.a.lengthOf(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
import react from '@astrojs/react';
|
import react from '@astrojs/react';
|
||||||
|
import preact from '@astrojs/preact';
|
||||||
|
import solid from '@astrojs/solid-js';
|
||||||
|
import svelte from '@astrojs/svelte';
|
||||||
|
import vue from '@astrojs/vue';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
integrations: [react()]
|
integrations: [
|
||||||
|
react(),
|
||||||
|
preact(),
|
||||||
|
solid(),
|
||||||
|
svelte(),
|
||||||
|
vue()
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,9 +3,17 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@astrojs/preact": "workspace:*",
|
||||||
"@astrojs/react": "workspace:*",
|
"@astrojs/react": "workspace:*",
|
||||||
|
"@astrojs/vue": "workspace:*",
|
||||||
|
"@astrojs/solid-js": "workspace:*",
|
||||||
|
"@astrojs/svelte": "workspace:*",
|
||||||
"astro": "workspace:*",
|
"astro": "workspace:*",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"solid-js": "^1.7.4",
|
||||||
|
"svelte": "^3.58.0",
|
||||||
|
"vue": "^3.2.47",
|
||||||
|
"preact": "^10.13.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
export default function Inner() {
|
export default function Inner() {
|
||||||
return <span>Inner</span>;
|
return <span>Inner</span>;
|
||||||
}
|
}
|
||||||
|
|
5
packages/astro/test/fixtures/astro-slots-nested/src/components/PassesChildren.tsx
vendored
Normal file
5
packages/astro/test/fixtures/astro-slots-nested/src/components/PassesChildren.tsx
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import React, { Fragment } from 'react';
|
||||||
|
|
||||||
|
export default function PassesChildren({ children }) {
|
||||||
|
return <Fragment>{ children }</Fragment>;
|
||||||
|
}
|
5
packages/astro/test/fixtures/astro-slots-nested/src/components/PassesChildrenP.tsx
vendored
Normal file
5
packages/astro/test/fixtures/astro-slots-nested/src/components/PassesChildrenP.tsx
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { h, Fragment } from 'preact';
|
||||||
|
|
||||||
|
export default function PassesChildren({ children }) {
|
||||||
|
return <Fragment>{ children }</Fragment>;
|
||||||
|
}
|
5
packages/astro/test/fixtures/astro-slots-nested/src/components/PassesChildrenS.tsx
vendored
Normal file
5
packages/astro/test/fixtures/astro-slots-nested/src/components/PassesChildrenS.tsx
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/** @jsxImportSource solid-js */
|
||||||
|
|
||||||
|
export default function PassesChildren({ children }) {
|
||||||
|
return <>{ children }</>;
|
||||||
|
}
|
3
packages/astro/test/fixtures/astro-slots-nested/src/components/PassesChildrenSv.svelte
vendored
Normal file
3
packages/astro/test/fixtures/astro-slots-nested/src/components/PassesChildrenSv.svelte
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="svelte-children">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
5
packages/astro/test/fixtures/astro-slots-nested/src/components/PassesChildrenV.vue
vendored
Normal file
5
packages/astro/test/fixtures/astro-slots-nested/src/components/PassesChildrenV.vue
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<div class="vue-wrapper">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
62
packages/astro/test/fixtures/astro-slots-nested/src/pages/server-component-nested.astro
vendored
Normal file
62
packages/astro/test/fixtures/astro-slots-nested/src/pages/server-component-nested.astro
vendored
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
---
|
||||||
|
import PassesChildren from '../components/PassesChildren.jsx';
|
||||||
|
import PassesChildrenP from '../components/PassesChildrenP.jsx';
|
||||||
|
import PassesChildrenS from '../components/PassesChildrenS.jsx';
|
||||||
|
import PassesChildrenSv from '../components/PassesChildrenSv.svelte';
|
||||||
|
import PassesChildrenV from '../components/PassesChildrenV.vue';
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Testing</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div id="react">
|
||||||
|
<PassesChildren>
|
||||||
|
<div>
|
||||||
|
<PassesChildren client:load>
|
||||||
|
<span>Inner children</span>
|
||||||
|
</PassesChildren>
|
||||||
|
</div>
|
||||||
|
</PassesChildren>
|
||||||
|
</div>
|
||||||
|
<div id="preact">
|
||||||
|
<PassesChildrenP>
|
||||||
|
<div>
|
||||||
|
<PassesChildrenP client:load>
|
||||||
|
<span>Inner children</span>
|
||||||
|
</PassesChildrenP>
|
||||||
|
</div>
|
||||||
|
</PassesChildrenP>
|
||||||
|
</div>
|
||||||
|
<div id="solid">
|
||||||
|
<PassesChildrenS>
|
||||||
|
<div>
|
||||||
|
<PassesChildrenS client:load>
|
||||||
|
<span>Inner children</span>
|
||||||
|
</PassesChildrenS>
|
||||||
|
</div>
|
||||||
|
</PassesChildrenS>
|
||||||
|
</div>
|
||||||
|
<div id="svelte">
|
||||||
|
<PassesChildrenSv>
|
||||||
|
<div>
|
||||||
|
<PassesChildrenSv client:load>
|
||||||
|
<span>Inner children</span>
|
||||||
|
</PassesChildrenSv>
|
||||||
|
</div>
|
||||||
|
</PassesChildrenSv>
|
||||||
|
</div>
|
||||||
|
<div id="vue">
|
||||||
|
<PassesChildrenV>
|
||||||
|
<div>
|
||||||
|
<PassesChildrenV client:load>
|
||||||
|
<span>Inner children</span>
|
||||||
|
</PassesChildrenV>
|
||||||
|
</div>
|
||||||
|
</PassesChildrenV>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { AstroComponentMetadata } from 'astro';
|
||||||
import { Component as BaseComponent, h } from 'preact';
|
import { Component as BaseComponent, h } from 'preact';
|
||||||
import render from 'preact-render-to-string';
|
import render from 'preact-render-to-string';
|
||||||
import { getContext } from './context.js';
|
import { getContext } from './context.js';
|
||||||
|
@ -10,7 +11,7 @@ const slotName = (str: string) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w
|
||||||
let originalConsoleError: typeof console.error;
|
let originalConsoleError: typeof console.error;
|
||||||
let consoleFilterRefs = 0;
|
let consoleFilterRefs = 0;
|
||||||
|
|
||||||
function check(this: RendererContext, Component: any, props: Record<string, any>, children: any) {
|
function check(this: RendererContext, Component: any, props: Record<string, any>, children: any, ) {
|
||||||
if (typeof Component !== 'function') return false;
|
if (typeof Component !== 'function') return false;
|
||||||
|
|
||||||
if (Component.prototype != null && typeof Component.prototype.render === 'function') {
|
if (Component.prototype != null && typeof Component.prototype.render === 'function') {
|
||||||
|
@ -21,7 +22,7 @@ function check(this: RendererContext, Component: any, props: Record<string, any>
|
||||||
|
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
const { html } = renderToStaticMarkup.call(this, Component, props, children);
|
const { html } = renderToStaticMarkup.call(this, Component, props, children, undefined);
|
||||||
if (typeof html !== 'string') {
|
if (typeof html !== 'string') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -38,18 +39,28 @@ function check(this: RendererContext, Component: any, props: Record<string, any>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldHydrate(metadata: AstroComponentMetadata | undefined) {
|
||||||
|
// Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot`
|
||||||
|
return metadata?.astroStaticSlot ? !!metadata.hydrate : true;
|
||||||
|
}
|
||||||
|
|
||||||
function renderToStaticMarkup(
|
function renderToStaticMarkup(
|
||||||
this: RendererContext,
|
this: RendererContext,
|
||||||
Component: any,
|
Component: any,
|
||||||
props: Record<string, any>,
|
props: Record<string, any>,
|
||||||
{ default: children, ...slotted }: Record<string, any>
|
{ default: children, ...slotted }: Record<string, any>,
|
||||||
|
metadata: AstroComponentMetadata | undefined,
|
||||||
) {
|
) {
|
||||||
const ctx = getContext(this.result);
|
const ctx = getContext(this.result);
|
||||||
|
|
||||||
const slots: Record<string, ReturnType<typeof h>> = {};
|
const slots: Record<string, ReturnType<typeof h>> = {};
|
||||||
for (const [key, value] of Object.entries(slotted)) {
|
for (const [key, value] of Object.entries(slotted)) {
|
||||||
const name = slotName(key);
|
const name = slotName(key);
|
||||||
slots[name] = h(StaticHtml, { value, name });
|
slots[name] = h(StaticHtml, {
|
||||||
|
hydrate: shouldHydrate(metadata),
|
||||||
|
value,
|
||||||
|
name
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore signals back onto props so that they will be passed as-is to components
|
// Restore signals back onto props so that they will be passed as-is to components
|
||||||
|
@ -61,7 +72,9 @@ function renderToStaticMarkup(
|
||||||
serializeSignals(ctx, props, attrs, propsMap);
|
serializeSignals(ctx, props, attrs, propsMap);
|
||||||
|
|
||||||
const html = render(
|
const html = render(
|
||||||
h(Component, newProps, children != null ? h(StaticHtml, { value: children }) : children)
|
h(Component, newProps, children != null ? h(StaticHtml, {
|
||||||
|
hydrate: shouldHydrate(metadata),
|
||||||
|
value: children}) : children)
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
attrs,
|
attrs,
|
||||||
|
@ -127,4 +140,5 @@ function filteredConsoleError(msg: string, ...rest: any[]) {
|
||||||
export default {
|
export default {
|
||||||
check,
|
check,
|
||||||
renderToStaticMarkup,
|
renderToStaticMarkup,
|
||||||
|
supportsAstroStaticSlot: true,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: string;
|
||||||
|
name?: string;
|
||||||
|
hydrate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Astro passes `children` as a string of HTML, so we need
|
* Astro passes `children` as a string of HTML, so we need
|
||||||
* a wrapper `div` to render that content as VNodes.
|
* a wrapper `div` to render that content as VNodes.
|
||||||
|
@ -7,9 +13,10 @@ import { h } from 'preact';
|
||||||
* As a bonus, we can signal to Preact that this subtree is
|
* As a bonus, we can signal to Preact that this subtree is
|
||||||
* entirely static and will never change via `shouldComponentUpdate`.
|
* entirely static and will never change via `shouldComponentUpdate`.
|
||||||
*/
|
*/
|
||||||
const StaticHtml = ({ value, name }: { value: string; name?: string }) => {
|
const StaticHtml = ({ value, name, hydrate }: Props) => {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
return h('astro-slot', { name, dangerouslySetInnerHTML: { __html: value } });
|
const tagName = hydrate === false ? 'astro-static-slot' : 'astro-slot';
|
||||||
|
return h(tagName, { name, dangerouslySetInnerHTML: { __html: value } });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -65,7 +65,11 @@ function renderToStaticMarkup(Component, props, { default: children, ...slotted
|
||||||
};
|
};
|
||||||
const newChildren = children ?? props.children;
|
const newChildren = children ?? props.children;
|
||||||
if (newChildren != null) {
|
if (newChildren != null) {
|
||||||
newProps.children = React.createElement(StaticHtml, { value: newChildren });
|
newProps.children = React.createElement(StaticHtml, {
|
||||||
|
// Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot`
|
||||||
|
hydrate: metadata.astroStaticSlot ? !!metadata.hydrate : true,
|
||||||
|
value: newChildren
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const vnode = React.createElement(Component, newProps);
|
const vnode = React.createElement(Component, newProps);
|
||||||
let html;
|
let html;
|
||||||
|
@ -80,4 +84,5 @@ function renderToStaticMarkup(Component, props, { default: children, ...slotted
|
||||||
export default {
|
export default {
|
||||||
check,
|
check,
|
||||||
renderToStaticMarkup,
|
renderToStaticMarkup,
|
||||||
|
supportsAstroStaticSlot: true,
|
||||||
};
|
};
|
||||||
|
|
|
@ -58,6 +58,11 @@ async function getNodeWritable() {
|
||||||
return Writable;
|
return Writable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function needsHydration(metadata) {
|
||||||
|
// Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot`
|
||||||
|
return metadata.astroStaticSlot ? !!metadata.hydrate : true;
|
||||||
|
}
|
||||||
|
|
||||||
async function renderToStaticMarkup(Component, props, { default: children, ...slotted }, metadata) {
|
async function renderToStaticMarkup(Component, props, { default: children, ...slotted }, metadata) {
|
||||||
let prefix;
|
let prefix;
|
||||||
if (this && this.result) {
|
if (this && this.result) {
|
||||||
|
@ -69,7 +74,11 @@ async function renderToStaticMarkup(Component, props, { default: children, ...sl
|
||||||
const slots = {};
|
const slots = {};
|
||||||
for (const [key, value] of Object.entries(slotted)) {
|
for (const [key, value] of Object.entries(slotted)) {
|
||||||
const name = slotName(key);
|
const name = slotName(key);
|
||||||
slots[name] = React.createElement(StaticHtml, { value, name });
|
slots[name] = React.createElement(StaticHtml, {
|
||||||
|
hydrate: needsHydration(metadata),
|
||||||
|
value,
|
||||||
|
name
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// Note: create newProps to avoid mutating `props` before they are serialized
|
// Note: create newProps to avoid mutating `props` before they are serialized
|
||||||
const newProps = {
|
const newProps = {
|
||||||
|
@ -78,7 +87,10 @@ async function renderToStaticMarkup(Component, props, { default: children, ...sl
|
||||||
};
|
};
|
||||||
const newChildren = children ?? props.children;
|
const newChildren = children ?? props.children;
|
||||||
if (newChildren != null) {
|
if (newChildren != null) {
|
||||||
newProps.children = React.createElement(StaticHtml, { value: newChildren });
|
newProps.children = React.createElement(StaticHtml, {
|
||||||
|
hydrate: needsHydration(metadata),
|
||||||
|
value: newChildren
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const vnode = React.createElement(Component, newProps);
|
const vnode = React.createElement(Component, newProps);
|
||||||
const renderOptions = {
|
const renderOptions = {
|
||||||
|
@ -182,4 +194,5 @@ async function renderToReadableStreamAsync(vnode, options) {
|
||||||
export default {
|
export default {
|
||||||
check,
|
check,
|
||||||
renderToStaticMarkup,
|
renderToStaticMarkup,
|
||||||
|
supportsAstroStaticSlot: true,
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,9 +7,10 @@ import { createElement as h } from 'react';
|
||||||
* As a bonus, we can signal to React that this subtree is
|
* As a bonus, we can signal to React that this subtree is
|
||||||
* entirely static and will never change via `shouldComponentUpdate`.
|
* entirely static and will never change via `shouldComponentUpdate`.
|
||||||
*/
|
*/
|
||||||
const StaticHtml = ({ value, name }) => {
|
const StaticHtml = ({ value, name, hydrate }) => {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
return h('astro-slot', {
|
const tagName = hydrate ? 'astro-slot' : 'astro-static-slot';
|
||||||
|
return h(tagName, {
|
||||||
name,
|
name,
|
||||||
suppressHydrationWarning: true,
|
suppressHydrationWarning: true,
|
||||||
dangerouslySetInnerHTML: { __html: value },
|
dangerouslySetInnerHTML: { __html: value },
|
||||||
|
|
|
@ -18,20 +18,22 @@ function renderToStaticMarkup(
|
||||||
metadata?: undefined | Record<string, any>
|
metadata?: undefined | Record<string, any>
|
||||||
) {
|
) {
|
||||||
const renderId = metadata?.hydrate ? incrementId(getContext(this.result)) : '';
|
const renderId = metadata?.hydrate ? incrementId(getContext(this.result)) : '';
|
||||||
|
const needsHydrate = metadata?.astroStaticSlot ? !!metadata.hydrate : true;
|
||||||
|
const tagName = needsHydrate ? 'astro-slot' : 'astro-static-slot';
|
||||||
|
|
||||||
const html = renderToString(
|
const html = renderToString(
|
||||||
() => {
|
() => {
|
||||||
const slots: Record<string, any> = {};
|
const slots: Record<string, any> = {};
|
||||||
for (const [key, value] of Object.entries(slotted)) {
|
for (const [key, value] of Object.entries(slotted)) {
|
||||||
const name = slotName(key);
|
const name = slotName(key);
|
||||||
slots[name] = ssr(`<astro-slot name="${name}">${value}</astro-slot>`);
|
slots[name] = ssr(`<${tagName} name="${name}">${value}</${tagName}>`);
|
||||||
}
|
}
|
||||||
// Note: create newProps to avoid mutating `props` before they are serialized
|
// Note: create newProps to avoid mutating `props` before they are serialized
|
||||||
const newProps = {
|
const newProps = {
|
||||||
...props,
|
...props,
|
||||||
...slots,
|
...slots,
|
||||||
// In Solid SSR mode, `ssr` creates the expected structure for `children`.
|
// In Solid SSR mode, `ssr` creates the expected structure for `children`.
|
||||||
children: children != null ? ssr(`<astro-slot>${children}</astro-slot>`) : children,
|
children: children != null ? ssr(`<${tagName}>${children}</${tagName}>`) : children,
|
||||||
};
|
};
|
||||||
|
|
||||||
return createComponent(Component, newProps);
|
return createComponent(Component, newProps);
|
||||||
|
@ -51,4 +53,5 @@ function renderToStaticMarkup(
|
||||||
export default {
|
export default {
|
||||||
check,
|
check,
|
||||||
renderToStaticMarkup,
|
renderToStaticMarkup,
|
||||||
|
supportsAstroStaticSlot: true,
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,11 +2,17 @@ function check(Component) {
|
||||||
return Component['render'] && Component['$$render'];
|
return Component['render'] && Component['$$render'];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderToStaticMarkup(Component, props, slotted) {
|
function needsHydration(metadata) {
|
||||||
|
// Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot`
|
||||||
|
return metadata.astroStaticSlot ? !!metadata.hydrate : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderToStaticMarkup(Component, props, slotted, metadata) {
|
||||||
|
const tagName = needsHydration(metadata) ? 'astro-slot' : 'astro-static-slot';
|
||||||
const slots = {};
|
const slots = {};
|
||||||
for (const [key, value] of Object.entries(slotted)) {
|
for (const [key, value] of Object.entries(slotted)) {
|
||||||
slots[key] = () =>
|
slots[key] = () =>
|
||||||
`<astro-slot${key === 'default' ? '' : ` name="${key}"`}>${value}</astro-slot>`;
|
`<${tagName}${key === 'default' ? '' : ` name="${key}"`}>${value}</${tagName}>`;
|
||||||
}
|
}
|
||||||
const { html } = Component.render(props, { $$slots: slots });
|
const { html } = Component.render(props, { $$slots: slots });
|
||||||
return { html };
|
return { html };
|
||||||
|
@ -15,4 +21,5 @@ async function renderToStaticMarkup(Component, props, slotted) {
|
||||||
export default {
|
export default {
|
||||||
check,
|
check,
|
||||||
renderToStaticMarkup,
|
renderToStaticMarkup,
|
||||||
|
supportsAstroStaticSlot: true,
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,10 +7,15 @@ function check(Component) {
|
||||||
return !!Component['ssrRender'] || !!Component['__ssrInlineRender'];
|
return !!Component['ssrRender'] || !!Component['__ssrInlineRender'];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderToStaticMarkup(Component, props, slotted) {
|
async function renderToStaticMarkup(Component, props, slotted, metadata) {
|
||||||
const slots = {};
|
const slots = {};
|
||||||
for (const [key, value] of Object.entries(slotted)) {
|
for (const [key, value] of Object.entries(slotted)) {
|
||||||
slots[key] = () => h(StaticHtml, { value, name: key === 'default' ? undefined : key });
|
slots[key] = () => h(StaticHtml, {
|
||||||
|
value,
|
||||||
|
name: key === 'default' ? undefined : key,
|
||||||
|
// Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot`
|
||||||
|
hydrate: metadata.astroStaticSlot ? !!metadata.hydrate : true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const app = createSSRApp({ render: () => h(Component, props, slots) });
|
const app = createSSRApp({ render: () => h(Component, props, slots) });
|
||||||
await setup(app);
|
await setup(app);
|
||||||
|
@ -21,4 +26,5 @@ async function renderToStaticMarkup(Component, props, slotted) {
|
||||||
export default {
|
export default {
|
||||||
check,
|
check,
|
||||||
renderToStaticMarkup,
|
renderToStaticMarkup,
|
||||||
|
supportsAstroStaticSlot: true,
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,10 +10,12 @@ const StaticHtml = defineComponent({
|
||||||
props: {
|
props: {
|
||||||
value: String,
|
value: String,
|
||||||
name: String,
|
name: String,
|
||||||
|
hydrate: Boolean,
|
||||||
},
|
},
|
||||||
setup({ name, value }) {
|
setup({ name, value, hydrate }) {
|
||||||
if (!value) return () => null;
|
if (!value) return () => null;
|
||||||
return () => h('astro-slot', { name, innerHTML: value });
|
let tagName = hydrate ? 'astro-slot' : 'astro-static-slot';
|
||||||
|
return () => h(tagName, { name, innerHTML: value });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2194,18 +2194,42 @@ importers:
|
||||||
|
|
||||||
packages/astro/test/fixtures/astro-slots-nested:
|
packages/astro/test/fixtures/astro-slots-nested:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@astrojs/preact':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../../../integrations/preact
|
||||||
'@astrojs/react':
|
'@astrojs/react':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../../integrations/react
|
version: link:../../../../integrations/react
|
||||||
|
'@astrojs/solid-js':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../../../integrations/solid
|
||||||
|
'@astrojs/svelte':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../../../integrations/svelte
|
||||||
|
'@astrojs/vue':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../../../integrations/vue
|
||||||
astro:
|
astro:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../..
|
version: link:../../..
|
||||||
|
preact:
|
||||||
|
specifier: ^10.13.2
|
||||||
|
version: 10.13.2
|
||||||
react:
|
react:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0
|
version: 18.2.0
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
|
solid-js:
|
||||||
|
specifier: ^1.7.4
|
||||||
|
version: 1.7.4
|
||||||
|
svelte:
|
||||||
|
specifier: ^3.58.0
|
||||||
|
version: 3.58.0
|
||||||
|
vue:
|
||||||
|
specifier: ^3.2.47
|
||||||
|
version: 3.2.47
|
||||||
|
|
||||||
packages/astro/test/fixtures/before-hydration:
|
packages/astro/test/fixtures/before-hydration:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
Loading…
Reference in a new issue