Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consolidate inline hydration scripts into one #3244

Merged
merged 5 commits into from
May 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}"]`);
matthewp marked this conversation as resolved.
Show resolved Hide resolved
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)}))}});`;
matthewp marked this conversation as resolved.
Show resolved Hide resolved
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