* Revert "Consolidate inline hydration scripts into one (#3244)"
This reverts commit 48a35e6042
.
* Fix types
* Adds changeset
This commit is contained in:
parent
59f07e8dd5
commit
8f8f05c1b9
20 changed files with 156 additions and 144 deletions
6
.changeset/fresh-wasps-smash.md
Normal file
6
.changeset/fresh-wasps-smash.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
'@astrojs/markdown-remark': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixes regression in passing JS args to islands
|
|
@ -1002,7 +1002,6 @@ export interface SSRElement {
|
||||||
export interface SSRMetadata {
|
export interface SSRMetadata {
|
||||||
renderers: SSRLoadedRenderer[];
|
renderers: SSRLoadedRenderer[];
|
||||||
pathname: string;
|
pathname: string;
|
||||||
needsHydrationStyles: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SSRResult {
|
export interface SSRResult {
|
||||||
|
|
|
@ -201,7 +201,6 @@ ${extra}`
|
||||||
_metadata: {
|
_metadata: {
|
||||||
renderers,
|
renderers,
|
||||||
pathname,
|
pathname,
|
||||||
needsHydrationStyles: false,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,9 @@ if (import.meta.hot) {
|
||||||
const doc = parser.parseFromString(html, 'text/html');
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
|
||||||
// Match incoming islands to current state
|
// Match incoming islands to current state
|
||||||
for (const root of doc.querySelectorAll('astro-island')) {
|
for (const root of doc.querySelectorAll('astro-root')) {
|
||||||
const uid = root.getAttribute('uid');
|
const uid = root.getAttribute('uid');
|
||||||
const current = document.querySelector(`astro-island[uid="${uid}"]`);
|
const current = document.querySelector(`astro-root[uid="${uid}"]`);
|
||||||
if (current) {
|
if (current) {
|
||||||
root.innerHTML = current?.innerHTML;
|
root.innerHTML = current?.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,17 +5,22 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
|
||||||
* (or after a short delay, if `requestIdleCallback`) isn't supported
|
* (or after a short delay, if `requestIdleCallback`) isn't supported
|
||||||
*/
|
*/
|
||||||
export default async function onIdle(
|
export default async function onIdle(
|
||||||
root: HTMLElement,
|
astroId: string,
|
||||||
options: HydrateOptions,
|
options: HydrateOptions,
|
||||||
getHydrateCallback: GetHydrateCallback
|
getHydrateCallback: GetHydrateCallback
|
||||||
) {
|
) {
|
||||||
const cb = async () => {
|
const cb = async () => {
|
||||||
|
const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
|
||||||
|
if (roots.length === 0) {
|
||||||
|
throw new Error(`Unable to find the root for the component ${options.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
let innerHTML: string | null = null;
|
let innerHTML: string | null = null;
|
||||||
let fragment = root.querySelector(`astro-fragment`);
|
let fragment = roots[0].querySelector(`astro-fragment`);
|
||||||
if (fragment == null && root.hasAttribute('tmpl')) {
|
if (fragment == null && roots[0].hasAttribute('tmpl')) {
|
||||||
// If there is no child fragment, check to see if there is a template.
|
// If there is no child fragment, check to see if there is a template.
|
||||||
// This happens if children were passed but the client component did not render any.
|
// This happens if children were passed but the client component did not render any.
|
||||||
let template = root.querySelector(`template[data-astro-template]`);
|
let template = roots[0].querySelector(`template[data-astro-template]`);
|
||||||
if (template) {
|
if (template) {
|
||||||
innerHTML = template.innerHTML;
|
innerHTML = template.innerHTML;
|
||||||
template.remove();
|
template.remove();
|
||||||
|
@ -24,7 +29,10 @@ export default async function onIdle(
|
||||||
innerHTML = fragment.innerHTML;
|
innerHTML = fragment.innerHTML;
|
||||||
}
|
}
|
||||||
const hydrate = await getHydrateCallback();
|
const hydrate = await getHydrateCallback();
|
||||||
hydrate(root, innerHTML);
|
|
||||||
|
for (const root of roots) {
|
||||||
|
hydrate(root, innerHTML);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if ('requestIdleCallback' in window) {
|
if ('requestIdleCallback' in window) {
|
||||||
|
|
|
@ -4,16 +4,21 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
|
||||||
* Hydrate this component immediately
|
* Hydrate this component immediately
|
||||||
*/
|
*/
|
||||||
export default async function onLoad(
|
export default async function onLoad(
|
||||||
root: HTMLElement,
|
astroId: string,
|
||||||
options: HydrateOptions,
|
options: HydrateOptions,
|
||||||
getHydrateCallback: GetHydrateCallback
|
getHydrateCallback: GetHydrateCallback
|
||||||
) {
|
) {
|
||||||
|
const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
|
||||||
|
if (roots.length === 0) {
|
||||||
|
throw new Error(`Unable to find the root for the component ${options.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
let innerHTML: string | null = null;
|
let innerHTML: string | null = null;
|
||||||
let fragment = root.querySelector(`astro-fragment`);
|
let fragment = roots[0].querySelector(`astro-fragment`);
|
||||||
if (fragment == null && root.hasAttribute('tmpl')) {
|
if (fragment == null && roots[0].hasAttribute('tmpl')) {
|
||||||
// If there is no child fragment, check to see if there is a template.
|
// If there is no child fragment, check to see if there is a template.
|
||||||
// This happens if children were passed but the client component did not render any.
|
// This happens if children were passed but the client component did not render any.
|
||||||
let template = root.querySelector(`template[data-astro-template]`);
|
let template = roots[0].querySelector(`template[data-astro-template]`);
|
||||||
if (template) {
|
if (template) {
|
||||||
innerHTML = template.innerHTML;
|
innerHTML = template.innerHTML;
|
||||||
template.remove();
|
template.remove();
|
||||||
|
@ -22,7 +27,10 @@ export default async function onLoad(
|
||||||
innerHTML = fragment.innerHTML;
|
innerHTML = fragment.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
//const innerHTML = root.querySelector(`astro-fragment`)?.innerHTML ?? null;
|
//const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null;
|
||||||
const hydrate = await getHydrateCallback();
|
const hydrate = await getHydrateCallback();
|
||||||
hydrate(root, innerHTML);
|
|
||||||
|
for (const root of roots) {
|
||||||
|
hydrate(root, innerHTML);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,16 +4,21 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
|
||||||
* Hydrate this component when a matching media query is found
|
* Hydrate this component when a matching media query is found
|
||||||
*/
|
*/
|
||||||
export default async function onMedia(
|
export default async function onMedia(
|
||||||
root: HTMLElement,
|
astroId: string,
|
||||||
options: HydrateOptions,
|
options: HydrateOptions,
|
||||||
getHydrateCallback: GetHydrateCallback
|
getHydrateCallback: GetHydrateCallback
|
||||||
) {
|
) {
|
||||||
|
const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
|
||||||
|
if (roots.length === 0) {
|
||||||
|
throw new Error(`Unable to find the root for the component ${options.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
let innerHTML: string | null = null;
|
let innerHTML: string | null = null;
|
||||||
let fragment = root.querySelector(`astro-fragment`);
|
let fragment = roots[0].querySelector(`astro-fragment`);
|
||||||
if (fragment == null && root.hasAttribute('tmpl')) {
|
if (fragment == null && roots[0].hasAttribute('tmpl')) {
|
||||||
// If there is no child fragment, check to see if there is a template.
|
// If there is no child fragment, check to see if there is a template.
|
||||||
// This happens if children were passed but the client component did not render any.
|
// This happens if children were passed but the client component did not render any.
|
||||||
let template = root.querySelector(`template[data-astro-template]`);
|
let template = roots[0].querySelector(`template[data-astro-template]`);
|
||||||
if (template) {
|
if (template) {
|
||||||
innerHTML = template.innerHTML;
|
innerHTML = template.innerHTML;
|
||||||
template.remove();
|
template.remove();
|
||||||
|
@ -24,7 +29,9 @@ export default async function onMedia(
|
||||||
|
|
||||||
const cb = async () => {
|
const cb = async () => {
|
||||||
const hydrate = await getHydrateCallback();
|
const hydrate = await getHydrateCallback();
|
||||||
hydrate(root, innerHTML);
|
for (const root of roots) {
|
||||||
|
hydrate(root, innerHTML);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (options.value) {
|
if (options.value) {
|
||||||
|
|
|
@ -4,16 +4,21 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
|
||||||
* Hydrate this component immediately
|
* Hydrate this component immediately
|
||||||
*/
|
*/
|
||||||
export default async function onLoad(
|
export default async function onLoad(
|
||||||
root: HTMLElement,
|
astroId: string,
|
||||||
options: HydrateOptions,
|
options: HydrateOptions,
|
||||||
getHydrateCallback: GetHydrateCallback
|
getHydrateCallback: GetHydrateCallback
|
||||||
) {
|
) {
|
||||||
|
const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
|
||||||
|
if (roots.length === 0) {
|
||||||
|
throw new Error(`Unable to find the root for the component ${options.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
let innerHTML: string | null = null;
|
let innerHTML: string | null = null;
|
||||||
let fragment = root.querySelector(`astro-fragment`);
|
let fragment = roots[0].querySelector(`astro-fragment`);
|
||||||
if (fragment == null && root.hasAttribute('tmpl')) {
|
if (fragment == null && roots[0].hasAttribute('tmpl')) {
|
||||||
// If there is no child fragment, check to see if there is a template.
|
// If there is no child fragment, check to see if there is a template.
|
||||||
// This happens if children were passed but the client component did not render any.
|
// This happens if children were passed but the client component did not render any.
|
||||||
let template = root.querySelector(`template[data-astro-template]`);
|
let template = roots[0].querySelector(`template[data-astro-template]`);
|
||||||
if (template) {
|
if (template) {
|
||||||
innerHTML = template.innerHTML;
|
innerHTML = template.innerHTML;
|
||||||
template.remove();
|
template.remove();
|
||||||
|
@ -22,5 +27,8 @@ export default async function onLoad(
|
||||||
innerHTML = fragment.innerHTML;
|
innerHTML = fragment.innerHTML;
|
||||||
}
|
}
|
||||||
const hydrate = await getHydrateCallback();
|
const hydrate = await getHydrateCallback();
|
||||||
hydrate(root, innerHTML);
|
|
||||||
|
for (const root of roots) {
|
||||||
|
hydrate(root, innerHTML);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,20 +2,25 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hydrate this component when one of it's children becomes visible.
|
* Hydrate this component when one of it's children becomes visible.
|
||||||
* We target the children because `astro-island` is set to `display: contents`
|
* We target the children because `astro-root` is set to `display: contents`
|
||||||
* which doesn't work with IntersectionObserver
|
* which doesn't work with IntersectionObserver
|
||||||
*/
|
*/
|
||||||
export default async function onVisible(
|
export default async function onVisible(
|
||||||
root: HTMLElement,
|
astroId: string,
|
||||||
options: HydrateOptions,
|
options: HydrateOptions,
|
||||||
getHydrateCallback: GetHydrateCallback
|
getHydrateCallback: GetHydrateCallback
|
||||||
) {
|
) {
|
||||||
|
const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
|
||||||
|
if (roots.length === 0) {
|
||||||
|
throw new Error(`Unable to find the root for the component ${options.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
let innerHTML: string | null = null;
|
let innerHTML: string | null = null;
|
||||||
let fragment = root.querySelector(`astro-fragment`);
|
let fragment = roots[0].querySelector(`astro-fragment`);
|
||||||
if (fragment == null && root.hasAttribute('tmpl')) {
|
if (fragment == null && roots[0].hasAttribute('tmpl')) {
|
||||||
// If there is no child fragment, check to see if there is a template.
|
// If there is no child fragment, check to see if there is a template.
|
||||||
// This happens if children were passed but the client component did not render any.
|
// This happens if children were passed but the client component did not render any.
|
||||||
let template = root.querySelector(`template[data-astro-template]`);
|
let template = roots[0].querySelector(`template[data-astro-template]`);
|
||||||
if (template) {
|
if (template) {
|
||||||
innerHTML = template.innerHTML;
|
innerHTML = template.innerHTML;
|
||||||
template.remove();
|
template.remove();
|
||||||
|
@ -26,21 +31,25 @@ export default async function onVisible(
|
||||||
|
|
||||||
const cb = async () => {
|
const cb = async () => {
|
||||||
const hydrate = await getHydrateCallback();
|
const hydrate = await getHydrateCallback();
|
||||||
hydrate(root, innerHTML);
|
for (const root of roots) {
|
||||||
|
hydrate(root, innerHTML);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const io = new IntersectionObserver((entries) => {
|
const io = new IntersectionObserver((entries) => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (!entry.isIntersecting) continue;
|
if (!entry.isIntersecting) continue;
|
||||||
// As soon as we hydrate, disconnect this IntersectionObserver for every `astro-island`
|
// As soon as we hydrate, disconnect this IntersectionObserver for every `astro-root`
|
||||||
io.disconnect();
|
io.disconnect();
|
||||||
cb();
|
cb();
|
||||||
break; // break loop on first match
|
break; // break loop on first match
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 0; i < root.children.length; i++) {
|
for (const root of roots) {
|
||||||
const child = root.children[i];
|
for (let i = 0; i < root.children.length; i++) {
|
||||||
io.observe(child);
|
const child = root.children[i];
|
||||||
|
io.observe(child);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
customElements.define('astro-island', class extends HTMLElement {
|
|
||||||
async connectedCallback(){
|
|
||||||
const [ { default: setup } ] = await Promise.all([
|
|
||||||
import(this.getAttribute('directive-url')),
|
|
||||||
import(this.getAttribute('before-hydration-url'))
|
|
||||||
]);
|
|
||||||
|
|
||||||
const opts = JSON.parse(this.getAttribute('opts'));
|
|
||||||
setup(this, opts, async () => {
|
|
||||||
const propsStr = this.getAttribute('props');
|
|
||||||
const props = propsStr ? JSON.parse(propsStr) : {};
|
|
||||||
const rendererUrl = this.getAttribute('renderer-url');
|
|
||||||
const [
|
|
||||||
{ default: Component },
|
|
||||||
{ default: hydrate }
|
|
||||||
] = await Promise.all([
|
|
||||||
import(this.getAttribute('component-url')),
|
|
||||||
rendererUrl ? import(rendererUrl) : () => () => {}
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (el, children) => hydrate(el)(Component, props, children);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a minified version of the above. If you modify the above you need to
|
|
||||||
* copy/paste it into a .js file and then run:
|
|
||||||
* > node_modules/.bin/terser --mangle --compress -- file.js
|
|
||||||
*
|
|
||||||
* And copy/paste the result below
|
|
||||||
*/
|
|
||||||
export const islandScript = `customElements.define("astro-island",class extends HTMLElement{async connectedCallback(){const[{default:t}]=await Promise.all([import(this.getAttribute("directive-url")),import(this.getAttribute("before-hydration-url"))]);const e=JSON.parse(this.getAttribute("opts"));t(this,e,(async()=>{const t=this.getAttribute("props");const e=t?JSON.parse(t):{};const r=this.getAttribute("renderer-url");const[{default:s},{default:i}]=await Promise.all([import(this.getAttribute("component-url")),r?import(r):()=>()=>{}]);return(t,r)=>i(t)(s,e,r)}))}});`;
|
|
|
@ -1,7 +1,6 @@
|
||||||
import type { AstroComponentMetadata, SSRLoadedRenderer } from '../../@types/astro';
|
import type { AstroComponentMetadata, SSRLoadedRenderer } from '../../@types/astro';
|
||||||
import type { SSRElement, SSRResult } from '../../@types/astro';
|
import type { SSRElement, SSRResult } from '../../@types/astro';
|
||||||
import { hydrationSpecifier, serializeListValue } from './util.js';
|
import { hydrationSpecifier, serializeListValue } from './util.js';
|
||||||
import { escapeHTML } from './escape.js';
|
|
||||||
import serializeJavaScript from 'serialize-javascript';
|
import serializeJavaScript from 'serialize-javascript';
|
||||||
|
|
||||||
// Serializes props passed into a component so that they can be reused during hydration.
|
// Serializes props passed into a component so that they can be reused during hydration.
|
||||||
|
@ -111,31 +110,32 @@ export async function generateHydrateScript(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const island: SSRElement = {
|
let hydrationSource = ``;
|
||||||
children: '',
|
|
||||||
props: {
|
hydrationSource += renderer.clientEntrypoint
|
||||||
// This is for HMR, probably can avoid it in prod
|
? `const [{ ${
|
||||||
uid: astroId,
|
componentExport.value
|
||||||
},
|
}: Component }, { default: hydrate }] = await Promise.all([import("${await result.resolve(
|
||||||
|
componentUrl
|
||||||
|
)}"), import("${await result.resolve(renderer.clientEntrypoint)}")]);
|
||||||
|
return (el, children) => hydrate(el)(Component, ${serializeProps(props)}, children);
|
||||||
|
`
|
||||||
|
: `await import("${await result.resolve(componentUrl)}");
|
||||||
|
return () => {};
|
||||||
|
`;
|
||||||
|
// TODO: If we can figure out tree-shaking in the final SSR build, we could safely
|
||||||
|
// use BEFORE_HYDRATION_SCRIPT_ID instead of 'astro:scripts/before-hydration.js'.
|
||||||
|
const hydrationScript = {
|
||||||
|
props: { type: 'module', 'data-astro-component-hydration': true },
|
||||||
|
children: `import setup from '${await result.resolve(hydrationSpecifier(hydrate))}';
|
||||||
|
${`import '${await result.resolve('astro:scripts/before-hydration.js')}';`}
|
||||||
|
setup("${astroId}", {name:"${metadata.displayName}",${
|
||||||
|
metadata.hydrateArgs ? `value: ${JSON.stringify(metadata.hydrateArgs)}` : ''
|
||||||
|
}}, async () => {
|
||||||
|
${hydrationSource}
|
||||||
|
});
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add component url
|
return hydrationScript;
|
||||||
island.props['component-url'] = await result.resolve(componentUrl);
|
|
||||||
|
|
||||||
// Add renderer url
|
|
||||||
if (renderer.clientEntrypoint) {
|
|
||||||
island.props['renderer-url'] = await result.resolve(renderer.clientEntrypoint);
|
|
||||||
island.props['props'] = escapeHTML(serializeProps(props));
|
|
||||||
}
|
|
||||||
|
|
||||||
island.props['directive-url'] = await result.resolve(hydrationSpecifier(hydrate));
|
|
||||||
island.props['before-hydration-url'] = await result.resolve('astro:scripts/before-hydration.js');
|
|
||||||
island.props['opts'] = escapeHTML(
|
|
||||||
JSON.stringify({
|
|
||||||
name: metadata.displayName,
|
|
||||||
value: metadata.hydrateArgs || '',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return island;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import type {
|
||||||
import { escapeHTML, HTMLString, markHTMLString } from './escape.js';
|
import { escapeHTML, HTMLString, markHTMLString } from './escape.js';
|
||||||
import { extractDirectives, generateHydrateScript, serializeProps } from './hydration.js';
|
import { extractDirectives, generateHydrateScript, serializeProps } from './hydration.js';
|
||||||
import { serializeListValue } from './util.js';
|
import { serializeListValue } from './util.js';
|
||||||
import { islandScript } from './astro-island.js';
|
|
||||||
|
|
||||||
export { markHTMLString, markHTMLString as unescapeHTML } from './escape.js';
|
export { markHTMLString, markHTMLString as unescapeHTML } from './escape.js';
|
||||||
export type { Metadata } from './metadata';
|
export type { Metadata } from './metadata';
|
||||||
|
@ -25,8 +24,6 @@ const htmlEnumAttributes = /^(contenteditable|draggable|spellcheck|value)$/i;
|
||||||
// Note: SVG is case-sensitive!
|
// Note: SVG is case-sensitive!
|
||||||
const svgEnumAttributes = /^(autoReverse|externalResourcesRequired|focusable|preserveAlpha)$/i;
|
const svgEnumAttributes = /^(autoReverse|externalResourcesRequired|focusable|preserveAlpha)$/i;
|
||||||
|
|
||||||
const resultsWithHydrationScript = new WeakSet<SSRResult>();
|
|
||||||
|
|
||||||
// INVESTIGATE:
|
// INVESTIGATE:
|
||||||
// 2. Less anys when possible and make it well known when they are needed.
|
// 2. Less anys when possible and make it well known when they are needed.
|
||||||
|
|
||||||
|
@ -311,32 +308,22 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
|
|
||||||
// Rather than appending this inline in the page, puts this into the `result.scripts` set that will be appended to the head.
|
// Rather than appending this inline in the page, puts this into the `result.scripts` set that will be appended to the head.
|
||||||
// INVESTIGATE: This will likely be a problem in streaming because the `<head>` will be gone at this point.
|
// INVESTIGATE: This will likely be a problem in streaming because the `<head>` will be gone at this point.
|
||||||
const island = await generateHydrateScript(
|
result.scripts.add(
|
||||||
{ renderer: renderer!, result, astroId, props },
|
await generateHydrateScript(
|
||||||
metadata as Required<AstroComponentMetadata>
|
{ renderer: renderer!, result, astroId, props },
|
||||||
|
metadata as Required<AstroComponentMetadata>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
result._metadata.needsHydrationStyles = true;
|
|
||||||
|
|
||||||
// Render a template if no fragment is provided.
|
// Render a template if no fragment is provided.
|
||||||
const needsAstroTemplate = children && !/<\/?astro-fragment\>/.test(html);
|
const needsAstroTemplate = children && !/<\/?astro-fragment\>/.test(html);
|
||||||
const template = needsAstroTemplate ? `<template data-astro-template>${children}</template>` : '';
|
const template = needsAstroTemplate ? `<template data-astro-template>${children}</template>` : '';
|
||||||
|
|
||||||
if (needsAstroTemplate) {
|
return markHTMLString(
|
||||||
island.props.tmpl = '';
|
`<astro-root uid="${astroId}"${needsAstroTemplate ? ' tmpl' : ''}>${
|
||||||
}
|
html ?? ''
|
||||||
|
}${template}</astro-root>`
|
||||||
island.children = `${html ?? ''}${template}`;
|
);
|
||||||
|
|
||||||
// Add the astro-island definition only once. Since the SSRResult object
|
|
||||||
// is scoped to a page renderer we can use it as a key to know if the script
|
|
||||||
// has been rendered or not.
|
|
||||||
let script = '';
|
|
||||||
if (!resultsWithHydrationScript.has(result)) {
|
|
||||||
resultsWithHydrationScript.add(result);
|
|
||||||
script = `<script>${islandScript}</script>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return markHTMLString(script + renderElement('astro-island', island, false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create the Astro.fetchContent() runtime function. */
|
/** Create the Astro.fetchContent() runtime function. */
|
||||||
|
@ -560,10 +547,13 @@ export async function renderHead(result: SSRResult): Promise<string> {
|
||||||
const styles = Array.from(result.styles)
|
const styles = Array.from(result.styles)
|
||||||
.filter(uniqueElements)
|
.filter(uniqueElements)
|
||||||
.map((style) => renderElement('style', style));
|
.map((style) => renderElement('style', style));
|
||||||
let needsHydrationStyles = result._metadata.needsHydrationStyles;
|
let needsHydrationStyles = false;
|
||||||
const scripts = Array.from(result.scripts)
|
const scripts = Array.from(result.scripts)
|
||||||
.filter(uniqueElements)
|
.filter(uniqueElements)
|
||||||
.map((script, i) => {
|
.map((script, i) => {
|
||||||
|
if ('data-astro-component-hydration' in script.props) {
|
||||||
|
needsHydrationStyles = true;
|
||||||
|
}
|
||||||
return renderElement('script', {
|
return renderElement('script', {
|
||||||
...script,
|
...script,
|
||||||
props: { ...script.props, 'astro-script': result._metadata.pathname + '/script-' + i },
|
props: { ...script.props, 'astro-script': result._metadata.pathname + '/script-' + i },
|
||||||
|
@ -573,7 +563,7 @@ export async function renderHead(result: SSRResult): Promise<string> {
|
||||||
styles.push(
|
styles.push(
|
||||||
renderElement('style', {
|
renderElement('style', {
|
||||||
props: {},
|
props: {},
|
||||||
children: 'astro-island, astro-fragment { display: contents; }',
|
children: 'astro-root, astro-fragment { display: contents; }',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,7 @@ describe('CSS', function () {
|
||||||
expect($('#passed-in').attr('class')).to.match(/outer astro-[A-Z0-9]+ astro-[A-Z0-9]+/);
|
expect($('#passed-in').attr('class')).to.match(/outer astro-[A-Z0-9]+ astro-[A-Z0-9]+/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Using hydrated components adds astro-island styles', async () => {
|
it('Using hydrated components adds astro-root styles', async () => {
|
||||||
const inline = $('style').html();
|
const inline = $('style').html();
|
||||||
expect(inline).to.include('display: contents');
|
expect(inline).to.include('display: contents');
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,13 +16,13 @@ describe('Client only components', () => {
|
||||||
const html = await fixture.readFile('/index.html');
|
const html = await fixture.readFile('/index.html');
|
||||||
const $ = cheerioLoad(html);
|
const $ = cheerioLoad(html);
|
||||||
|
|
||||||
// test 1: <astro-island> is empty
|
// test 1: <astro-root> is empty
|
||||||
expect($('astro-island').html()).to.equal('');
|
expect($('astro-root').html()).to.equal('');
|
||||||
const $script = $('script');
|
const $script = $('script');
|
||||||
const script = $script.html();
|
const script = $script.html();
|
||||||
|
|
||||||
// Has the renderer URL for svelte
|
// test 2: svelte renderer is on the page
|
||||||
expect($('astro-island').attr('renderer-url').length).to.be.greaterThan(0);
|
expect(/import\(".\/entry.*/g.test(script)).to.be.ok;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds the CSS to the page', async () => {
|
it('Adds the CSS to the page', async () => {
|
||||||
|
|
|
@ -16,25 +16,27 @@ describe('Dynamic components', () => {
|
||||||
const html = await fixture.readFile('/index.html');
|
const html = await fixture.readFile('/index.html');
|
||||||
|
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
expect($('script').length).to.eq(1);
|
expect($('script').length).to.eq(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Loads pages using client:media hydrator', async () => {
|
it('Loads pages using client:media hydrator', async () => {
|
||||||
|
const root = new URL('http://example.com/media/index.html');
|
||||||
const html = await fixture.readFile('/media/index.html');
|
const html = await fixture.readFile('/media/index.html');
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
// test 1: static value rendered
|
// test 1: static value rendered
|
||||||
expect($('script').length).to.equal(1); // One overall
|
expect($('script').length).to.equal(2); // One for each
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Loads pages using client:only hydrator', async () => {
|
it('Loads pages using client:only hydrator', async () => {
|
||||||
const html = await fixture.readFile('/client-only/index.html');
|
const html = await fixture.readFile('/client-only/index.html');
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
// test 1: <astro-island> is empty.
|
// test 1: <astro-root> is empty.
|
||||||
expect($('astro-island').html()).to.equal('');
|
expect($('<astro-root>').html()).to.equal('');
|
||||||
|
// test 2: correct script is being loaded.
|
||||||
// Has the directive URL
|
// because of bundling, we don't have access to the source import,
|
||||||
expect($('astro-island').attr('directive-url').length).to.be.greaterThan(0);
|
// only the bundled import.
|
||||||
|
expect($('script').html()).to.include(`import setup from '../entry`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -50,7 +50,7 @@ describe('Custom Elements', () => {
|
||||||
|
|
||||||
// Hydration
|
// Hydration
|
||||||
// test 3: Component and polyfill scripts bundled separately
|
// test 3: Component and polyfill scripts bundled separately
|
||||||
expect($('script')).to.have.lengthOf(2);
|
expect($('script[type=module]')).to.have.lengthOf(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Custom elements not claimed by renderer are rendered as regular HTML', async () => {
|
it('Custom elements not claimed by renderer are rendered as regular HTML', async () => {
|
||||||
|
|
|
@ -42,7 +42,11 @@ describe('React Components', () => {
|
||||||
expect($('#pure')).to.have.lengthOf(1);
|
expect($('#pure')).to.have.lengthOf(1);
|
||||||
|
|
||||||
// test 8: Check number of islands
|
// test 8: Check number of islands
|
||||||
expect($('astro-island')).to.have.lengthOf(5);
|
expect($('astro-root[uid]')).to.have.lengthOf(5);
|
||||||
|
|
||||||
|
// test 9: Check island deduplication
|
||||||
|
const uniqueRootUIDs = new Set($('astro-root').map((i, el) => $(el).attr('uid')));
|
||||||
|
expect(uniqueRootUIDs.size).to.equal(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Can load Vue', async () => {
|
it('Can load Vue', async () => {
|
||||||
|
|
|
@ -27,11 +27,18 @@ describe('Vue component', () => {
|
||||||
// test 1: renders all components correctly
|
// test 1: renders all components correctly
|
||||||
expect(allPreValues).to.deep.equal(['0', '1', '1', '1', '10', '100', '1000']);
|
expect(allPreValues).to.deep.equal(['0', '1', '1', '1', '10', '100', '1000']);
|
||||||
|
|
||||||
// test 2: renders 3 <astro-islands>s
|
// test 2: renders 3 <astro-root>s
|
||||||
expect($('astro-island')).to.have.lengthOf(6);
|
expect($('astro-root')).to.have.lengthOf(6);
|
||||||
|
|
||||||
// test 3: treats <my-button> as a custom element
|
// test 3: all <astro-root>s have uid attributes
|
||||||
|
expect($('astro-root[uid]')).to.have.lengthOf(6);
|
||||||
|
|
||||||
|
// test 4: treats <my-button> as a custom element
|
||||||
expect($('my-button')).to.have.lengthOf(7);
|
expect($('my-button')).to.have.lengthOf(7);
|
||||||
|
|
||||||
|
// test 5: components with identical render output and props have been deduplicated
|
||||||
|
const uniqueRootUIDs = $('astro-root').map((i, el) => $(el).attr('uid'));
|
||||||
|
expect(new Set(uniqueRootUIDs).size).to.equal(5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -9,14 +9,14 @@ const visit = _visit as (
|
||||||
) => any;
|
) => any;
|
||||||
|
|
||||||
// This fixes some confusing bugs coming from somewhere inside of our Markdown pipeline.
|
// This fixes some confusing bugs coming from somewhere inside of our Markdown pipeline.
|
||||||
// `unist`/`remark`/`rehype` (not sure) often generate malformed HTML inside of <astro-island>
|
// `unist`/`remark`/`rehype` (not sure) often generate malformed HTML inside of <astro-root>
|
||||||
// For hydration to work properly, frameworks need the DOM to be the exact same on server/client.
|
// For hydration to work properly, frameworks need the DOM to be the exact same on server/client.
|
||||||
// This reverts some "helpful corrections" that are applied to our perfectly valid HTML!
|
// This reverts some "helpful corrections" that are applied to our perfectly valid HTML!
|
||||||
export default function rehypeIslands(): any {
|
export default function rehypeIslands(): any {
|
||||||
return function (node: any): any {
|
return function (node: any): any {
|
||||||
return visit(node, 'element', (el) => {
|
return visit(node, 'element', (el) => {
|
||||||
// Bugs only happen inside of <astro-island> islands
|
// Bugs only happen inside of <astro-root> islands
|
||||||
if (el.tagName == 'astro-island') {
|
if (el.tagName == 'astro-root') {
|
||||||
visit(el, 'text', (child, index, parent) => {
|
visit(el, 'text', (child, index, parent) => {
|
||||||
if (child.type === 'text') {
|
if (child.type === 'text') {
|
||||||
// Sometimes comments can be trapped as text, which causes them to be escaped
|
// Sometimes comments can be trapped as text, which causes them to be escaped
|
||||||
|
|
|
@ -8,7 +8,7 @@ const visit = _visit as (
|
||||||
callback?: (node: any, index: number, parent: any) => any
|
callback?: (node: any, index: number, parent: any) => any
|
||||||
) => any;
|
) => any;
|
||||||
|
|
||||||
// Remove the wrapping paragraph for <astro-island> islands
|
// Remove the wrapping paragraph for <astro-root> islands
|
||||||
export default function remarkUnwrap() {
|
export default function remarkUnwrap() {
|
||||||
const astroRootNodes = new Set();
|
const astroRootNodes = new Set();
|
||||||
let insideAstroRoot = false;
|
let insideAstroRoot = false;
|
||||||
|
@ -19,10 +19,10 @@ export default function remarkUnwrap() {
|
||||||
astroRootNodes.clear();
|
astroRootNodes.clear();
|
||||||
|
|
||||||
visit(tree, 'html', (node) => {
|
visit(tree, 'html', (node) => {
|
||||||
if (node.value.indexOf('<astro-island') > -1 && !insideAstroRoot) {
|
if (node.value.indexOf('<astro-root') > -1 && !insideAstroRoot) {
|
||||||
insideAstroRoot = true;
|
insideAstroRoot = true;
|
||||||
}
|
}
|
||||||
if (node.value.indexOf('</astro-island') > -1 && insideAstroRoot) {
|
if (node.value.indexOf('</astro-root') > -1 && insideAstroRoot) {
|
||||||
insideAstroRoot = false;
|
insideAstroRoot = false;
|
||||||
}
|
}
|
||||||
astroRootNodes.add(node);
|
astroRootNodes.add(node);
|
||||||
|
|
Loading…
Reference in a new issue