Skip to content

Commit

Permalink
Consolidate hydration scripts into just one (#3571)
Browse files Browse the repository at this point in the history
* Remove redundant hydration scripts

* Prebuild the island JS

* Fix build

* Updates to tests

* Update more references

* Custom element test now has two classic scripts

* Account for non-default exports

* Restructure hydration directives

* Move nested logic into the island component

* Remove try/catch
  • Loading branch information
matthewp authored Jun 15, 2022
1 parent a7637e6 commit fc52321
Show file tree
Hide file tree
Showing 37 changed files with 533 additions and 288 deletions.
7 changes: 7 additions & 0 deletions packages/astro/e2e/fixtures/pass-js/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';

// https://astro.build/config
export default defineConfig({
integrations: [react()],
});
13 changes: 13 additions & 0 deletions packages/astro/e2e/fixtures/pass-js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@e2e/pass-js",
"version": "0.0.0",
"private": true,
"devDependencies": {
"@astrojs/react": "workspace:*",
"astro": "workspace:*"
},
"dependencies": {
"react": "^18.1.0",
"react-dom": "^18.1.0"
}
}
29 changes: 29 additions & 0 deletions packages/astro/e2e/fixtures/pass-js/src/components/React.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { BigNestedObject } from '../types';
import { useState } from 'react';

interface Props {
obj: BigNestedObject;
num: bigint;
}

const isNode = typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]';

/** a counter written in React */
export default function Component({ obj, num, arr }: Props) {
// We are testing hydration, so don't return anything in the server.
if(isNode) {
return <div></div>
}

return (
<div>
<span id="nested-date">{obj.nested.date.toUTCString()}</span>
<span id="regexp-type">{Object.prototype.toString.call(obj.more.another.exp)}</span>
<span id="regexp-value">{obj.more.another.exp.source}</span>
<span id="bigint-type">{Object.prototype.toString.call(num)}</span>
<span id="bigint-value">{num.toString()}</span>
<span id="arr-type">{Object.prototype.toString.call(arr)}</span>
<span id="arr-value">{arr.join(',')}</span>
</div>
);
}
28 changes: 28 additions & 0 deletions packages/astro/e2e/fixtures/pass-js/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
import Component from '../components/React';
import { BigNestedObject } from '../types';
const obj: BigNestedObject = {
nested: {
date: new Date('Thu, 09 Jun 2022 14:18:27 GMT')
},
more: {
another: {
exp: /ok/
}
}
};
---

<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
</head>
<body>
<main>
<Component client:load obj={obj} num={11n} arr={[0, "foo"]} />
</main>
</body>
</html>
11 changes: 11 additions & 0 deletions packages/astro/e2e/fixtures/pass-js/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

export interface BigNestedObject {
nested: {
date: Date;
};
more: {
another: {
exp: RegExp;
}
}
}
61 changes: 61 additions & 0 deletions packages/astro/e2e/pass-js.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { test as base, expect } from '@playwright/test';
import { loadFixture } from './test-utils.js';

const test = base.extend({
astro: async ({}, use) => {
const fixture = await loadFixture({ root: './fixtures/pass-js/' });
await use(fixture);
},
});

let devServer;

test.beforeEach(async ({ astro }) => {
devServer = await astro.startDevServer();
});

test.afterEach(async () => {
await devServer.stop();
});

test.describe('Passing JS into client components', () => {
test('Complex nested objects', async ({ astro, page }) => {
await page.goto('/');

const nestedDate = await page.locator('#nested-date');
await expect(nestedDate, 'component is visible').toBeVisible();
await expect(nestedDate).toHaveText('Thu, 09 Jun 2022 14:18:27 GMT');

const regeExpType = await page.locator('#regexp-type');
await expect(regeExpType, 'is visible').toBeVisible();
await expect(regeExpType).toHaveText('[object RegExp]');

const regExpValue = await page.locator('#regexp-value');
await expect(regExpValue, 'is visible').toBeVisible();
await expect(regExpValue).toHaveText('ok');
});

test('BigInts', async ({ page }) => {
await page.goto('/');

const bigIntType = await page.locator('#bigint-type');
await expect(bigIntType, 'is visible').toBeVisible();
await expect(bigIntType).toHaveText('[object BigInt]');

const bigIntValue = await page.locator('#bigint-value');
await expect(bigIntValue, 'is visible').toBeVisible();
await expect(bigIntValue).toHaveText('11');
});

test('Arrays that look like the serialization format', async ({ page }) => {
await page.goto('/');

const arrType = await page.locator('#arr-type');
await expect(arrType, 'is visible').toBeVisible();
await expect(arrType).toHaveText('[object Array]');

const arrValue = await page.locator('#arr-value');
await expect(arrValue, 'is visible').toBeVisible();
await expect(arrValue).toHaveText('0,foo');
});
});
2 changes: 1 addition & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"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\"",
Expand Down Expand Up @@ -121,7 +122,6 @@
"resolve": "^1.22.0",
"rollup": "^2.75.5",
"semver": "^7.3.7",
"serialize-javascript": "^6.0.0",
"shiki": "^0.10.1",
"sirv": "^2.0.2",
"slash": "^4.0.0",
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,7 @@ export interface MarkdownInstance<T extends Record<string, any>> {
}

export type GetHydrateCallback = () => Promise<
(element: Element, innerHTML: string | null) => void | Promise<void>
() => void | Promise<void>
>;

/**
Expand Down Expand Up @@ -1005,6 +1005,7 @@ export interface SSRElement {
export interface SSRMetadata {
renderers: SSRLoadedRenderer[];
pathname: string;
needsHydrationStyles: boolean;
}

export interface SSRResult {
Expand Down
3 changes: 0 additions & 3 deletions packages/astro/src/@types/serialize-javascript.d.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/astro/src/core/create-vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ const ALWAYS_EXTERNAL = new Set([
'@sveltejs/vite-plugin-svelte',
'micromark-util-events-to-acorn',
'@astrojs/markdown-remark',
'serialize-javascript',
'node-fetch',
'prismjs',
'shiki',
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 @@ -221,6 +221,7 @@ ${extra}`
},
resolve,
_metadata: {
needsHydrationStyles: false,
renderers,
pathname,
},
Expand Down
7 changes: 1 addition & 6 deletions packages/astro/src/runtime/client/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,9 @@ function debounce<T extends (...args: any[]) => any>(cb: T, wait = 20) {
}

export const notify = debounce(() => {
if (document.querySelector('astro-root[ssr]')) {
window.dispatchEvent(new CustomEvent(HYDRATE_KEY));
}
window.dispatchEvent(new CustomEvent(HYDRATE_KEY));
});

export const listen = (cb: (...args: any[]) => any) =>
window.addEventListener(HYDRATE_KEY, cb, { once: true });

if (!(window as any)[HYDRATE_KEY]) {
if ('MutationObserver' in window) {
new MutationObserver(notify).observe(document.body, { subtree: true, childList: true });
Expand Down
6 changes: 3 additions & 3 deletions packages/astro/src/runtime/client/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ if (import.meta.hot) {
doc.head.appendChild(style);
}
// 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) {
current.setAttribute('data-persist', '');
root.replaceWith(current);
Expand All @@ -26,7 +26,7 @@ if (import.meta.hot) {
}
return diff(document, doc).then(() => {
// clean up data-persist attributes added before diffing
for (const root of document.querySelectorAll('astro-root[data-persist]')) {
for (const root of document.querySelectorAll('astro-island[data-persist]')) {
root.removeAttribute('data-persist');
}
for (const style of document.querySelectorAll("style[type='text/css'][data-persist]")) {
Expand Down
34 changes: 4 additions & 30 deletions packages/astro/src/runtime/client/idle.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,19 @@
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
import { listen, notify } from './events';
import { notify } from './events';

/**
* 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(
astroId: string,
root: HTMLElement,
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
let innerHTML: string | null = null;
let hydrate: Awaited<ReturnType<GetHydrateCallback>>;

async function idle() {
listen(idle);
const cb = async () => {
const roots = document.querySelectorAll(`astro-root[ssr][uid="${astroId}"]`);
if (roots.length === 0) return;
if (typeof innerHTML !== 'string') {
let fragment = roots[0].querySelector(`astro-fragment`);
if (fragment == null && roots[0].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]`);
if (template) {
innerHTML = template.innerHTML;
template.remove();
}
} else if (fragment) {
innerHTML = fragment.innerHTML;
}
}
if (!hydrate) {
hydrate = await getHydrateCallback();
}
for (const root of roots) {
if (root.parentElement?.closest('astro-root[ssr]')) continue;
await hydrate(root, innerHTML);
root.removeAttribute('ssr');
}
let hydrate = await getHydrateCallback();
await hydrate();
notify();
};

Expand Down
34 changes: 4 additions & 30 deletions packages/astro/src/runtime/client/load.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,17 @@
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
import { listen, notify } from './events';
import { notify } from './events';

/**
* Hydrate this component immediately
*/
export default async function onLoad(
astroId: string,
root: HTMLElement,
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
let innerHTML: string | null = null;
let hydrate: Awaited<ReturnType<GetHydrateCallback>>;

async function load() {
listen(load);
const roots = document.querySelectorAll(`astro-root[ssr][uid="${astroId}"]`);
if (roots.length === 0) return;
if (typeof innerHTML !== 'string') {
let fragment = roots[0].querySelector(`astro-fragment`);
if (fragment == null && roots[0].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]`);
if (template) {
innerHTML = template.innerHTML;
template.remove();
}
} else if (fragment) {
innerHTML = fragment.innerHTML;
}
}
if (!hydrate) {
hydrate = await getHydrateCallback();
}
for (const root of roots) {
if (root.parentElement?.closest('astro-root[ssr]')) continue;
await hydrate(root, innerHTML);
root.removeAttribute('ssr');
}
let hydrate = await getHydrateCallback();
await hydrate();
notify();
}
load();
Expand Down
34 changes: 4 additions & 30 deletions packages/astro/src/runtime/client/media.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,18 @@
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
import { listen, notify } from './events';
import { notify } from './events';

/**
* Hydrate this component when a matching media query is found
*/
export default async function onMedia(
astroId: string,
root: HTMLElement,
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
let innerHTML: string | null = null;
let hydrate: Awaited<ReturnType<GetHydrateCallback>>;

async function media() {
listen(media);
const cb = async () => {
const roots = document.querySelectorAll(`astro-root[ssr][uid="${astroId}"]`);
if (roots.length === 0) return;
if (typeof innerHTML !== 'string') {
let fragment = roots[0].querySelector(`astro-fragment`);
if (fragment == null && roots[0].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]`);
if (template) {
innerHTML = template.innerHTML;
template.remove();
}
} else if (fragment) {
innerHTML = fragment.innerHTML;
}
}
if (!hydrate) {
hydrate = await getHydrateCallback();
}
for (const root of roots) {
if (root.parentElement?.closest('astro-root[ssr]')) continue;
await hydrate(root, innerHTML);
root.removeAttribute('ssr');
}
let hydrate = await getHydrateCallback();
await hydrate();
notify();
};

Expand Down
Loading

0 comments on commit fc52321

Please sign in to comment.