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

Inline hydration directive scripts #3605

Merged
merged 3 commits into from
Jun 16, 2022
Merged
Show file tree
Hide file tree
Changes from 2 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/many-seas-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Inlines hydration scripts
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package-lock.json
# do not commit .env files or any files that end with `.env`
*.env

packages/astro/src/**/*.prebuilt.ts
!packages/astro/vendor/vite/dist
packages/integrations/**/.netlify/

Expand Down
8 changes: 4 additions & 4 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@
"vendor"
],
"scripts": {
"prebuild": "astro-scripts prebuild --to-string \"src/runtime/server/astro-island.ts\"",
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
"build:ci": "astro-scripts build \"src/**/*.ts\"",
"dev": "astro-scripts dev \"src/**/*.ts\"",
"prebuild": "astro-scripts prebuild --to-string \"src/runtime/server/astro-island.ts\" \"src/runtime/client/{idle,load,media,only,visible}.ts\"",
"build": "pnpm run prebuild && astro-scripts build \"src/**/*.ts\" && tsc",
"build:ci": "pnpm run prebuild && astro-scripts build \"src/**/*.ts\"",
"dev": "astro-scripts dev --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.ts\"",
"postbuild": "astro-scripts copy \"src/**/*.astro\"",
"benchmark": "node test/benchmark/dev.bench.js && node test/benchmark/build.bench.js",
"test": "mocha --exit --timeout 20000 --ignore **/lit-element.test.js --ignore **/errors.test.js && mocha --timeout 20000 **/lit-element.test.js && mocha --timeout 20000 **/errors.test.js",
Expand Down
2 changes: 0 additions & 2 deletions packages/astro/src/core/build/static-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,6 @@ export async function staticBuild(opts: StaticBuildOptions) {
...metadata.hydratedComponentPaths(),
// Client-only components
...clientOnlys,
// Any hydration directive like astro/client/idle.js
...metadata.hydrationDirectiveSpecifiers(),
// The client path for each renderer
...renderers
.filter((renderer) => !!renderer.clientEntrypoint)
Expand Down
17 changes: 17 additions & 0 deletions packages/astro/src/runtime/client/hydration-directives.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';

type DirectiveLoader = (get: GetHydrateCallback, opts: HydrateOptions, root: HTMLElement) => void;

declare global {
interface Window {
Astro: {
idle: DirectiveLoader;
load: DirectiveLoader;
media: DirectiveLoader;
only: DirectiveLoader;
visible: DirectiveLoader;
}
}
}

export {};
33 changes: 9 additions & 24 deletions packages/astro/src/runtime/client/idle.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,12 @@
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
import { notify } from './events';
(self.Astro = self.Astro || {}).idle = (getHydrateCallback) => {
const cb = async () => {
let hydrate = await getHydrateCallback();
await hydrate();
};

/**
* Hydrate this component as soon as the main thread is free
* (or after a short delay, if `requestIdleCallback`) isn't supported
*/
export default async function onIdle(
root: HTMLElement,
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
async function idle() {
const cb = async () => {
let hydrate = await getHydrateCallback();
await hydrate();
notify();
};

if ('requestIdleCallback' in window) {
(window as any).requestIdleCallback(cb);
} else {
setTimeout(cb, 200);
}
if ('requestIdleCallback' in window) {
(window as any).requestIdleCallback(cb);
} else {
setTimeout(cb, 200);
}
idle();
}
22 changes: 6 additions & 16 deletions packages/astro/src/runtime/client/load.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
import { notify } from './events';

/**
* Hydrate this component immediately
*/
export default async function onLoad(
root: HTMLElement,
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
async function load() {
(self.Astro = self.Astro || {}).load = (getHydrateCallback) => {
(async () => {
let hydrate = await getHydrateCallback();
await hydrate();
notify();
}
load();
}
})();
};


39 changes: 16 additions & 23 deletions packages/astro/src/runtime/client/media.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
import { notify } from './events';

/**
* Hydrate this component when a matching media query is found
*/
export default async function onMedia(
root: HTMLElement,
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
async function media() {
const cb = async () => {
let hydrate = await getHydrateCallback();
await hydrate();
notify();
};
(self.Astro = self.Astro || {}).media = (getHydrateCallback, options) => {
const cb = async () => {
let hydrate = await getHydrateCallback();
await hydrate();
};

if (options.value) {
const mql = matchMedia(options.value);
if (mql.matches) {
cb();
} else {
mql.addEventListener('change', cb, { once: true });
}
if (options.value) {
const mql = matchMedia(options.value);
if (mql.matches) {
cb();
} else {
mql.addEventListener('change', cb, { once: true });
}
}
media();
}
};




20 changes: 7 additions & 13 deletions packages/astro/src/runtime/client/only.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
import { notify } from './events';

/**
* Hydrate this component only on the client
*/
export default async function onOnly(
root: HTMLElement,
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
async function only() {
(self.Astro = self.Astro || {}).only = (getHydrateCallback) => {
(async () => {
let hydrate = await getHydrateCallback();
await hydrate();
notify();
}
only();
}
})();
};



26 changes: 4 additions & 22 deletions packages/astro/src/runtime/client/visible.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,15 @@
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
import { notify } from './events';

/**
* Hydrate this component when one of it's children becomes visible
* We target the children because `astro-island` is set to `display: contents`
* which doesn't work with IntersectionObserver
*/
export default async function onVisible(
root: HTMLElement,
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
let io: IntersectionObserver;

async function visible() {
const cb = async () => {
(self.Astro = self.Astro || {}).visible = (getHydrateCallback, _opts, root) => {
const cb = async () => {
let hydrate = await getHydrateCallback();
await hydrate();
notify();
};

if (io) {
io.disconnect();
}

io = new IntersectionObserver((entries) => {
let io = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
// As soon as we hydrate, disconnect this IntersectionObserver for every `astro-island`
Expand All @@ -38,7 +23,4 @@ export default async function onVisible(
const child = root.children[i];
io.observe(child);
}
}

visible();
}
};
7 changes: 0 additions & 7 deletions packages/astro/src/runtime/server/astro-island.prebuilt.ts

This file was deleted.

22 changes: 14 additions & 8 deletions packages/astro/src/runtime/server/astro-island.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@
// Do not import this file directly, instead import the prebuilt one instead.
// pnpm --filter astro run prebuild

type directiveAstroKeys = string;
Copy link
Member

@natemoo-re natemoo-re Jun 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this supposed to be the specific hydration method names rather than string?

Copy link
Contributor Author

@matthewp matthewp Jun 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking here was that we might support custom directives in the future and then this might actually be any string. But I didn't really think this all the way through so this might be too confusing as is, will change it.


declare const Astro: {
[k in directiveAstroKeys]: (
fn: () => Promise<() => void>,
opts: Record<string, any>,
root: HTMLElement
) => void;
}

{
interface PropTypeSelector {
[k: string]: (value: any) => any;
Expand Down Expand Up @@ -32,14 +42,10 @@
public hydrator: any;
static observedAttributes = ['props'];
async connectedCallback() {
const [{ default: setup }] = await Promise.all([
import(this.getAttribute('directive-url')!),
import(this.getAttribute('before-hydration-url')!),
]);
window.addEventListener('astro:hydrate', this.hydrate);

const opts = JSON.parse(this.getAttribute('opts')!);
setup(this, opts, async () => {
await import(this.getAttribute('before-hydration-url')!);
const opts = JSON.parse(this.getAttribute('opts')!) as Record<string, any>;
Astro[this.getAttribute('client') as directiveAstroKeys](async () => {
const rendererUrl = this.getAttribute('renderer-url');
const [componentModule, { default: hydrator }] = await Promise.all([
import(this.getAttribute('component-url')!),
Expand All @@ -48,7 +54,7 @@
this.Component = componentModule[this.getAttribute('component-export') || 'default'];
this.hydrator = hydrator;
return this.hydrate;
});
}, opts, this);
}
hydrate = () => {
if (!this.hydrator || this.parentElement?.closest('astro-island[ssr]')) {
Expand Down
3 changes: 1 addition & 2 deletions packages/astro/src/runtime/server/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
} from '../../@types/astro';
import { escapeHTML } from './escape.js';
import { serializeProps } from './serialize.js';
import { hydrationSpecifier, serializeListValue } from './util.js';
import { serializeListValue } from './util.js';

const HydrationDirectives = ['load', 'idle', 'media', 'visible', 'only'];

Expand Down Expand Up @@ -129,7 +129,6 @@ export async function generateHydrateScript(

island.props['ssr'] = '';
island.props['client'] = hydrate;
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({
Expand Down
40 changes: 14 additions & 26 deletions packages/astro/src/runtime/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@ import type {
SSRLoadedRenderer,
SSRResult,
} from '../../@types/astro';
import islandScript from './astro-island.prebuilt.js';

import { escapeHTML, HTMLString, markHTMLString } from './escape.js';
import { extractDirectives, generateHydrateScript } from './hydration.js';
import { serializeProps } from './serialize.js';
import { shorthash } from './shorthash.js';
import {
determineIfNeedsHydrationScript,
determinesIfNeedsDirectiveScript,
PrescriptType,
getPrescripts
} from './scripts.js';
import { serializeListValue } from './util.js';

export { markHTMLString, markHTMLString as unescapeHTML } from './escape.js';
Expand All @@ -27,18 +33,6 @@ const htmlEnumAttributes = /^(contenteditable|draggable|spellcheck|value)$/i;
// Note: SVG is case-sensitive!
const svgEnumAttributes = /^(autoReverse|externalResourcesRequired|focusable|preserveAlpha)$/i;

// This is used to keep track of which requests (pages) have had the hydration script
// appended. We only add the hydration script once per page, and since the SSRResult
// object corresponds to one page request, we are using it as a key to know.
const resultsWithHydrationScript = new WeakSet<SSRResult>();

function determineIfNeedsHydrationScript(result: SSRResult): boolean {
if (resultsWithHydrationScript.has(result)) {
return false;
}
resultsWithHydrationScript.add(result);
return true;
}

// INVESTIGATE:
// 2. Less anys when possible and make it well known when they are needed.
Expand Down Expand Up @@ -158,6 +152,7 @@ function formatList(values: string[]): string {
return `${values.slice(0, -1).join(', ')} or ${values[values.length - 1]}`;
}


export async function renderComponent(
result: SSRResult,
displayName: string,
Expand Down Expand Up @@ -191,6 +186,7 @@ export async function renderComponent(
const { hydration, props } = extractDirectives(_props);
let html = '';
let needsHydrationScript = hydration && determineIfNeedsHydrationScript(result);
let needsDirectiveScript = hydration && determinesIfNeedsDirectiveScript(result, hydration.directive);

if (hydration) {
metadata.hydrate = hydration.directive as AstroComponentMetadata['hydrate'];
Expand Down Expand Up @@ -348,19 +344,11 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr

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 (needsHydrationScript) {
// Note that this is a class script, not a module script.
// This is so that it executes immediate, and when the browser encounters
// an astro-island element the callbacks will fire immediately, causing the JS
// deps to be loaded immediately.
script = `<script>${islandScript}</script>`;
}

return markHTMLString(script + renderElement('astro-island', island, false));
let prescriptType: PrescriptType = needsHydrationScript ? 'both' : needsDirectiveScript ?
'directive' : null;
let prescripts = getPrescripts(prescriptType, hydration.directive);

return markHTMLString(prescripts + renderElement('astro-island', island, false));
}

/** Create the Astro.fetchContent() runtime function. */
Expand Down
Loading