Skip to content

Commit

Permalink
Consolidate inline hydration scripts into one (#3244)
Browse files Browse the repository at this point in the history
* Consolidate inline hydration scripts into one

* Adds changeset

* Update custom element test

* Provide a better name for tracking if we have added a hydration script
  • Loading branch information
matthewp committed May 3, 2022
1 parent 4599f1f commit 48a35e6
Show file tree
Hide file tree
Showing 20 changed files with 152 additions and 148 deletions.
5 changes: 5 additions & 0 deletions .changeset/eleven-toes-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Consolidates hydration scripts into one
1 change: 1 addition & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,7 @@ export interface SSRElement {
export interface SSRMetadata {
renderers: SSRLoadedRenderer[];
pathname: string;
needsHydrationStyles: boolean;
}

export interface SSRResult {
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/render/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ ${extra}`
_metadata: {
renderers,
pathname,
needsHydrationStyles: false
},
};

Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/runtime/client/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ if (import.meta.hot) {
const doc = parser.parseFromString(html, 'text/html');

// Match incoming islands to current state
for (const root of doc.querySelectorAll('astro-root')) {
for (const root of doc.querySelectorAll('astro-island')) {
const uid = root.getAttribute('uid');
const current = document.querySelector(`astro-root[uid="${uid}"]`);
const current = document.querySelector(`astro-island[uid="${uid}"]`);
if (current) {
root.innerHTML = current?.innerHTML;
}
Expand Down
18 changes: 5 additions & 13 deletions packages/astro/src/runtime/client/idle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,17 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
* (or after a short delay, if `requestIdleCallback`) isn't supported
*/
export default async function onIdle(
astroId: string,
root: HTMLElement,
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
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 fragment = roots[0].querySelector(`astro-fragment`);
if (fragment == null && roots[0].hasAttribute('tmpl')) {
let fragment = root.querySelector(`astro-fragment`);
if (fragment == null && root.hasAttribute('tmpl')) {
// 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.
let template = roots[0].querySelector(`template[data-astro-template]`);
let template = root.querySelector(`template[data-astro-template]`);
if (template) {
innerHTML = template.innerHTML;
template.remove();
Expand All @@ -29,10 +24,7 @@ export default async function onIdle(
innerHTML = fragment.innerHTML;
}
const hydrate = await getHydrateCallback();

for (const root of roots) {
hydrate(root, innerHTML);
}
hydrate(root, innerHTML);
};

if ('requestIdleCallback' in window) {
Expand Down
20 changes: 6 additions & 14 deletions packages/astro/src/runtime/client/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,16 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
* Hydrate this component immediately
*/
export default async function onLoad(
astroId: string,
root: HTMLElement,
options: HydrateOptions,
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 fragment = roots[0].querySelector(`astro-fragment`);
if (fragment == null && roots[0].hasAttribute('tmpl')) {
let fragment = root.querySelector(`astro-fragment`);
if (fragment == null && root.hasAttribute('tmpl')) {
// 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.
let template = roots[0].querySelector(`template[data-astro-template]`);
let template = root.querySelector(`template[data-astro-template]`);
if (template) {
innerHTML = template.innerHTML;
template.remove();
Expand All @@ -27,10 +22,7 @@ export default async function onLoad(
innerHTML = fragment.innerHTML;
}

//const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null;
//const innerHTML = root.querySelector(`astro-fragment`)?.innerHTML ?? null;
const hydrate = await getHydrateCallback();

for (const root of roots) {
hydrate(root, innerHTML);
}
hydrate(root, innerHTML);
}
17 changes: 5 additions & 12 deletions packages/astro/src/runtime/client/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,16 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
* Hydrate this component when a matching media query is found
*/
export default async function onMedia(
astroId: string,
root: HTMLElement,
options: HydrateOptions,
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 fragment = roots[0].querySelector(`astro-fragment`);
if (fragment == null && roots[0].hasAttribute('tmpl')) {
let fragment = root.querySelector(`astro-fragment`);
if (fragment == null && root.hasAttribute('tmpl')) {
// 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.
let template = roots[0].querySelector(`template[data-astro-template]`);
let template = root.querySelector(`template[data-astro-template]`);
if (template) {
innerHTML = template.innerHTML;
template.remove();
Expand All @@ -29,9 +24,7 @@ export default async function onMedia(

const cb = async () => {
const hydrate = await getHydrateCallback();
for (const root of roots) {
hydrate(root, innerHTML);
}
hydrate(root, innerHTML);
};

if (options.value) {
Expand Down
18 changes: 5 additions & 13 deletions packages/astro/src/runtime/client/only.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,16 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
* Hydrate this component immediately
*/
export default async function onLoad(
astroId: string,
root: HTMLElement,
options: HydrateOptions,
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 fragment = roots[0].querySelector(`astro-fragment`);
if (fragment == null && roots[0].hasAttribute('tmpl')) {
let fragment = root.querySelector(`astro-fragment`);
if (fragment == null && root.hasAttribute('tmpl')) {
// 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.
let template = roots[0].querySelector(`template[data-astro-template]`);
let template = root.querySelector(`template[data-astro-template]`);
if (template) {
innerHTML = template.innerHTML;
template.remove();
Expand All @@ -27,8 +22,5 @@ export default async function onLoad(
innerHTML = fragment.innerHTML;
}
const hydrate = await getHydrateCallback();

for (const root of roots) {
hydrate(root, innerHTML);
}
hydrate(root, innerHTML);
}
29 changes: 10 additions & 19 deletions packages/astro/src/runtime/client/visible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,20 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';

/**
* Hydrate this component when one of it's children becomes visible.
* We target the children because `astro-root` is set to `display: contents`
* We target the children because `astro-island` is set to `display: contents`
* which doesn't work with IntersectionObserver
*/
export default async function onVisible(
astroId: string,
root: HTMLElement,
options: HydrateOptions,
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 fragment = roots[0].querySelector(`astro-fragment`);
if (fragment == null && roots[0].hasAttribute('tmpl')) {
let fragment = root.querySelector(`astro-fragment`);
if (fragment == null && root.hasAttribute('tmpl')) {
// 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.
let template = roots[0].querySelector(`template[data-astro-template]`);
let template = root.querySelector(`template[data-astro-template]`);
if (template) {
innerHTML = template.innerHTML;
template.remove();
Expand All @@ -31,25 +26,21 @@ export default async function onVisible(

const cb = async () => {
const hydrate = await getHydrateCallback();
for (const root of roots) {
hydrate(root, innerHTML);
}
hydrate(root, innerHTML);
};

const io = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
// As soon as we hydrate, disconnect this IntersectionObserver for every `astro-root`
// As soon as we hydrate, disconnect this IntersectionObserver for every `astro-island`
io.disconnect();
cb();
break; // break loop on first match
}
});

for (const root of roots) {
for (let i = 0; i < root.children.length; i++) {
const child = root.children[i];
io.observe(child);
}
for (let i = 0; i < root.children.length; i++) {
const child = root.children[i];
io.observe(child);
}
}
35 changes: 35 additions & 0 deletions packages/astro/src/runtime/server/astro-island.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
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)}))}});`;
51 changes: 25 additions & 26 deletions packages/astro/src/runtime/server/hydration.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { AstroComponentMetadata, SSRLoadedRenderer } from '../../@types/astro';
import type { SSRElement, SSRResult } from '../../@types/astro';
import { hydrationSpecifier, serializeListValue } from './util.js';
import { escapeHTML } from './escape.js';
import serializeJavaScript from 'serialize-javascript';


// Serializes props passed into a component so that they can be reused during hydration.
// The value is any
export function serializeProps(value: any) {
Expand Down Expand Up @@ -110,32 +112,29 @@ export async function generateHydrateScript(
);
}

let hydrationSource = ``;

hydrationSource += renderer.clientEntrypoint
? `const [{ ${
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}
});
`,
const island: SSRElement = {
children: '',
props: {
// This is for HMR, probably can avoid it in prod
uid: astroId
}
};

return hydrationScript;
// Add component url
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;
}
Loading

0 comments on commit 48a35e6

Please sign in to comment.