From 4bb6d73da789b353fe98e916465c07fbffbd62c7 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 9 Jun 2022 12:21:38 -0400 Subject: [PATCH 01/10] Remove redundant hydration scripts --- .../e2e/fixtures/pass-js/astro.config.mjs | 7 ++ .../astro/e2e/fixtures/pass-js/package.json | 13 ++++ .../fixtures/pass-js/src/components/React.tsx | 29 +++++++++ .../fixtures/pass-js/src/pages/index.astro | 28 ++++++++ .../astro/e2e/fixtures/pass-js/src/types.ts | 11 ++++ packages/astro/e2e/pass-js.test.js | 61 ++++++++++++++++++ packages/astro/src/@types/astro.ts | 1 + packages/astro/src/runtime/client/idle.ts | 13 ++-- packages/astro/src/runtime/client/load.ts | 13 ++-- packages/astro/src/runtime/client/media.ts | 13 ++-- packages/astro/src/runtime/client/only.ts | 13 ++-- packages/astro/src/runtime/client/visible.ts | 22 +++---- .../astro/src/runtime/server/astro-island.ts | 51 +++++++++++++++ .../astro/src/runtime/server/hydration.ts | 64 +++++++++---------- packages/astro/src/runtime/server/index.ts | 50 +++++++++++---- .../astro/src/runtime/server/serialize.ts | 60 +++++++++++++++++ pnpm-lock.yaml | 22 +++++++ 17 files changed, 381 insertions(+), 90 deletions(-) create mode 100644 packages/astro/e2e/fixtures/pass-js/astro.config.mjs create mode 100644 packages/astro/e2e/fixtures/pass-js/package.json create mode 100644 packages/astro/e2e/fixtures/pass-js/src/components/React.tsx create mode 100644 packages/astro/e2e/fixtures/pass-js/src/pages/index.astro create mode 100644 packages/astro/e2e/fixtures/pass-js/src/types.ts create mode 100644 packages/astro/e2e/pass-js.test.js create mode 100644 packages/astro/src/runtime/server/astro-island.ts create mode 100644 packages/astro/src/runtime/server/serialize.ts diff --git a/packages/astro/e2e/fixtures/pass-js/astro.config.mjs b/packages/astro/e2e/fixtures/pass-js/astro.config.mjs new file mode 100644 index 000000000000..8a6f1951c9c2 --- /dev/null +++ b/packages/astro/e2e/fixtures/pass-js/astro.config.mjs @@ -0,0 +1,7 @@ +import { defineConfig } from 'astro/config'; +import react from '@astrojs/react'; + +// https://astro.build/config +export default defineConfig({ + integrations: [react()], +}); diff --git a/packages/astro/e2e/fixtures/pass-js/package.json b/packages/astro/e2e/fixtures/pass-js/package.json new file mode 100644 index 000000000000..b01293b84058 --- /dev/null +++ b/packages/astro/e2e/fixtures/pass-js/package.json @@ -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" + } +} diff --git a/packages/astro/e2e/fixtures/pass-js/src/components/React.tsx b/packages/astro/e2e/fixtures/pass-js/src/components/React.tsx new file mode 100644 index 000000000000..7302c54438e7 --- /dev/null +++ b/packages/astro/e2e/fixtures/pass-js/src/components/React.tsx @@ -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
+ } + + return ( +
+ {obj.nested.date.toUTCString()} + {Object.prototype.toString.call(obj.more.another.exp)} + {obj.more.another.exp.source} + {Object.prototype.toString.call(num)} + {num.toString()} + {Object.prototype.toString.call(arr)} + {arr.join(',')} +
+ ); +} diff --git a/packages/astro/e2e/fixtures/pass-js/src/pages/index.astro b/packages/astro/e2e/fixtures/pass-js/src/pages/index.astro new file mode 100644 index 000000000000..dc029b6fb111 --- /dev/null +++ b/packages/astro/e2e/fixtures/pass-js/src/pages/index.astro @@ -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/ + } + } +}; +--- + + + + + + + + +
+ +
+ + diff --git a/packages/astro/e2e/fixtures/pass-js/src/types.ts b/packages/astro/e2e/fixtures/pass-js/src/types.ts new file mode 100644 index 000000000000..736c33e1ae44 --- /dev/null +++ b/packages/astro/e2e/fixtures/pass-js/src/types.ts @@ -0,0 +1,11 @@ + +export interface BigNestedObject { + nested: { + date: Date; + }; + more: { + another: { + exp: RegExp; + } + } +} diff --git a/packages/astro/e2e/pass-js.test.js b/packages/astro/e2e/pass-js.test.js new file mode 100644 index 000000000000..e19b2ec295d8 --- /dev/null +++ b/packages/astro/e2e/pass-js.test.js @@ -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'); + }); +}); diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 45963dbcf052..22af344bbc46 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -998,6 +998,7 @@ export interface SSRElement { export interface SSRMetadata { renderers: SSRLoadedRenderer[]; pathname: string; + needsHydrationStyles: boolean; } export interface SSRResult { diff --git a/packages/astro/src/runtime/client/idle.ts b/packages/astro/src/runtime/client/idle.ts index be8c1cb9a023..d6ad71d9fd06 100644 --- a/packages/astro/src/runtime/client/idle.ts +++ b/packages/astro/src/runtime/client/idle.ts @@ -6,7 +6,7 @@ import { listen, notify } from './events'; * (or after a short delay, if `requestIdleCallback`) isn't supported */ export default async function onIdle( - astroId: string, + root: HTMLElement, options: HydrateOptions, getHydrateCallback: GetHydrateCallback ) { @@ -16,14 +16,12 @@ export default async function onIdle( 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')) { + 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(); @@ -35,8 +33,7 @@ export default async function onIdle( if (!hydrate) { hydrate = await getHydrateCallback(); } - for (const root of roots) { - if (root.parentElement?.closest('astro-root[ssr]')) continue; + if (!root.parentElement?.closest('astro-root[ssr]')) { await hydrate(root, innerHTML); root.removeAttribute('ssr'); } diff --git a/packages/astro/src/runtime/client/load.ts b/packages/astro/src/runtime/client/load.ts index abdf2bfde036..f083a4577217 100644 --- a/packages/astro/src/runtime/client/load.ts +++ b/packages/astro/src/runtime/client/load.ts @@ -5,7 +5,7 @@ import { listen, notify } from './events'; * Hydrate this component immediately */ export default async function onLoad( - astroId: string, + root: HTMLElement, options: HydrateOptions, getHydrateCallback: GetHydrateCallback ) { @@ -14,14 +14,12 @@ export default async function onLoad( 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')) { + 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(); @@ -33,8 +31,7 @@ export default async function onLoad( if (!hydrate) { hydrate = await getHydrateCallback(); } - for (const root of roots) { - if (root.parentElement?.closest('astro-root[ssr]')) continue; + if (!root.parentElement?.closest('astro-root[ssr]')) { await hydrate(root, innerHTML); root.removeAttribute('ssr'); } diff --git a/packages/astro/src/runtime/client/media.ts b/packages/astro/src/runtime/client/media.ts index 146b09831712..ac3c249ab03b 100644 --- a/packages/astro/src/runtime/client/media.ts +++ b/packages/astro/src/runtime/client/media.ts @@ -5,7 +5,7 @@ import { listen, 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 ) { @@ -15,14 +15,12 @@ export default async function onMedia( 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')) { + 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(); @@ -34,8 +32,7 @@ export default async function onMedia( if (!hydrate) { hydrate = await getHydrateCallback(); } - for (const root of roots) { - if (root.parentElement?.closest('astro-root[ssr]')) continue; + if (!root.parentElement?.closest('astro-root[ssr]')) { await hydrate(root, innerHTML); root.removeAttribute('ssr'); } diff --git a/packages/astro/src/runtime/client/only.ts b/packages/astro/src/runtime/client/only.ts index 65ea02bd7649..a598f302c23b 100644 --- a/packages/astro/src/runtime/client/only.ts +++ b/packages/astro/src/runtime/client/only.ts @@ -5,7 +5,7 @@ import { listen, notify } from './events'; * Hydrate this component only on the client */ export default async function onOnly( - astroId: string, + root: HTMLElement, options: HydrateOptions, getHydrateCallback: GetHydrateCallback ) { @@ -14,14 +14,12 @@ export default async function onOnly( async function only() { listen(only); - 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')) { + 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(); @@ -33,8 +31,7 @@ export default async function onOnly( if (!hydrate) { hydrate = await getHydrateCallback(); } - for (const root of roots) { - if (root.parentElement?.closest('astro-root[ssr]')) continue; + if (!root.parentElement?.closest('astro-root[ssr]')) { await hydrate(root, innerHTML); root.removeAttribute('ssr'); } diff --git a/packages/astro/src/runtime/client/visible.ts b/packages/astro/src/runtime/client/visible.ts index ed4b64c719a2..332f2c3a6ec0 100644 --- a/packages/astro/src/runtime/client/visible.ts +++ b/packages/astro/src/runtime/client/visible.ts @@ -7,7 +7,7 @@ import { listen, notify } from './events'; * which doesn't work with IntersectionObserver */ export default async function onVisible( - astroId: string, + root: HTMLElement, options: HydrateOptions, getHydrateCallback: GetHydrateCallback ) { @@ -17,15 +17,13 @@ export default async function onVisible( async function visible() { listen(visible); - const roots = document.querySelectorAll(`astro-root[ssr][uid="${astroId}"]`); const cb = async () => { - if (roots.length === 0) return; if (typeof innerHTML !== 'string') { - 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(); @@ -37,8 +35,8 @@ export default async function onVisible( if (!hydrate) { hydrate = await getHydrateCallback(); } - for (const root of roots) { - if (root.parentElement?.closest('astro-root[ssr]')) continue; + + if (!root.parentElement?.closest('astro-island[ssr]')) { await hydrate(root, innerHTML); root.removeAttribute('ssr'); } @@ -59,11 +57,9 @@ export default async function onVisible( } }); - 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); } } diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts new file mode 100644 index 000000000000..80f0bfecb6ae --- /dev/null +++ b/packages/astro/src/runtime/server/astro-island.ts @@ -0,0 +1,51 @@ +/* +{ + const propTypes = { + 0: value => value, + 1: value => JSON.parse(value, reviver), + 2: value => new RegExp(value), + 3: value => new Date(value), + 4: value => new Map(JSON.parse(value, reviver)), + 5: value => new Set(JSON.parse(value, reviver)), + 6: value => BigInt(value), + 7: value => new URL(value), + }; + + const reviver = (propKey, raw) => { + if(propKey === '' || !Array.isArray(raw)) return raw; + const [type, value] = raw; + return (type in propTypes) ? propTypes[type](value) : undefined; + }; + + 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 props = this.hasAttribute('props') ? JSON.parse(this.getAttribute('props'), reviver) : {}; + 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, { client: this.getAttribute('client') }); + }); + } + }); +} +*/ + +/** + * 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 = `{const t={0:t=>t,1:t=>JSON.parse(t,e),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(JSON.parse(t,e)),5:t=>new Set(JSON.parse(t,e)),6:t=>BigInt(t),7:t=>new URL(t)},e=(e,r)=>{if(""===e||!Array.isArray(r))return r;const[i,s]=r;return i in t?t[i](s):void 0};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"))]);t(this,JSON.parse(this.getAttribute("opts")),(async()=>{const t=this.hasAttribute("props")?JSON.parse(this.getAttribute("props"),e):{},r=this.getAttribute("renderer-url"),[{default:i},{default:s}]=await Promise.all([import(this.getAttribute("component-url")),r?import(r):()=>()=>{}]);return(e,r)=>s(e)(i,t,r,{client:this.getAttribute("client")})}))}})}`; diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts index ec077adbec93..ad12fe00e3a5 100644 --- a/packages/astro/src/runtime/server/hydration.ts +++ b/packages/astro/src/runtime/server/hydration.ts @@ -5,13 +5,9 @@ import type { SSRLoadedRenderer, SSRResult, } from '../../@types/astro'; +import { escapeHTML } from './escape.js'; import { hydrationSpecifier, serializeListValue } from './util.js'; - -// Serializes props passed into a component so that they can be reused during hydration. -// The value is any -export function serializeProps(value: any) { - return serializeJavaScript(value); -} +import { serializeProps } from './serialize.js'; const HydrationDirectives = ['load', 'idle', 'media', 'visible', 'only']; @@ -100,6 +96,11 @@ interface HydrateScriptOptions { props: Record; } + + + + + /** For hydrated components, generate a `; + } + return markHTMLString( - `${ - html ?? '' - }${template}` + script + + renderElement('astro-island', island, false) ); } @@ -619,7 +643,7 @@ export async function renderHead(result: SSRResult): Promise { const styles = Array.from(result.styles) .filter(uniqueElements) .map((style) => renderElement('style', style)); - let needsHydrationStyles = false; + let needsHydrationStyles = result._metadata.needsHydrationStyles; const scripts = Array.from(result.scripts) .filter(uniqueElements) .map((script, i) => { @@ -632,7 +656,7 @@ export async function renderHead(result: SSRResult): Promise { styles.push( renderElement('style', { props: {}, - children: 'astro-root, astro-fragment { display: contents; }', + children: 'astro-island, astro-fragment { display: contents; }', }) ); } diff --git a/packages/astro/src/runtime/server/serialize.ts b/packages/astro/src/runtime/server/serialize.ts new file mode 100644 index 000000000000..d101e2f74b99 --- /dev/null +++ b/packages/astro/src/runtime/server/serialize.ts @@ -0,0 +1,60 @@ +type ValueOf = T[keyof T]; + +const PROP_TYPE = { + Value: 0, + JSON: 1, + RegExp: 2, + Date: 3, + Map: 4, + Set: 5, + BigInt: 6, + URL: 7, +}; + +function serializeArray(value: any[]): any[] { + return value.map((v) => convertToSerializedForm(v)); +} + +function serializeObject(value: Record): Record { + return Object.fromEntries(Object.entries(value).map(([k, v]) => { + return [k, convertToSerializedForm(v)]; + })); +} + +function convertToSerializedForm(value: any): [ValueOf, any] { + const tag = Object.prototype.toString.call(value); + switch(tag) { + case '[object Date]': { + return [PROP_TYPE.Date, (value as Date).toISOString()]; + } + case '[object RegExp]': { + return [PROP_TYPE.RegExp, (value as RegExp).source]; + } + case '[object Map]': { + return [PROP_TYPE.Map, Array.from(value as Map)]; + } + case '[object Set]': { + return [PROP_TYPE.Set, Array.from(value as Set)]; + } + case '[object BigInt]': { + return [PROP_TYPE.BigInt, (value as bigint).toString()]; + } + case '[object URL]': { + return [PROP_TYPE.URL, (value as URL).toString()]; + } + case '[object Array]': { + return [PROP_TYPE.JSON, JSON.stringify(serializeArray(value))]; + } + default: { + if(typeof value === 'object') { + return [PROP_TYPE.Value, serializeObject(value)]; + } else { + return [PROP_TYPE.Value, value]; + } + } + } +} + +export function serializeProps(props: any) { + return JSON.stringify(serializeObject(props)); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0016663a87d5..718380490688 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -894,6 +894,19 @@ importers: dependencies: astro: link:../../.. + packages/astro/e2e/fixtures/pass-js: + specifiers: + '@astrojs/react': workspace:* + astro: workspace:* + react: ^18.1.0 + react-dom: ^18.1.0 + dependencies: + react: 18.1.0 + react-dom: 18.1.0_react@18.1.0 + devDependencies: + '@astrojs/react': link:../../../../integrations/react + astro: link:../../.. + packages/astro/e2e/fixtures/preact-component: specifiers: '@astrojs/preact': workspace:* @@ -8119,6 +8132,11 @@ packages: /debug/3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true dependencies: ms: 2.1.3 dev: false @@ -11059,6 +11077,8 @@ packages: debug: 3.2.7 iconv-lite: 0.4.24 sax: 1.2.4 + transitivePeerDependencies: + - supports-color dev: false /netmask/2.0.2: @@ -11142,6 +11162,8 @@ packages: rimraf: 2.7.1 semver: 5.7.1 tar: 4.4.19 + transitivePeerDependencies: + - supports-color dev: false /node-releases/2.0.5: From 018527232eeda4b28a1a0a8cd8ea00cb8c780dcd Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 10 Jun 2022 14:54:04 -0400 Subject: [PATCH 02/10] Prebuild the island JS --- packages/astro/package.json | 2 +- .../src/@types/serialize-javascript.d.ts | 3 -- packages/astro/src/core/create-vite.ts | 1 - .../runtime/server/astro-island.prebuilt.ts | 7 +++ .../astro/src/runtime/server/astro-island.ts | 34 ++++++------- .../astro/src/runtime/server/hydration.ts | 1 - packages/astro/src/runtime/server/index.ts | 2 +- pnpm-lock.yaml | 4 +- scripts/cmd/prebuild.js | 49 +++++++++++++++++++ scripts/index.js | 5 ++ 10 files changed, 80 insertions(+), 28 deletions(-) delete mode 100644 packages/astro/src/@types/serialize-javascript.d.ts create mode 100644 packages/astro/src/runtime/server/astro-island.prebuilt.ts create mode 100644 scripts/cmd/prebuild.js diff --git a/packages/astro/package.json b/packages/astro/package.json index cdae23376b98..8af54403525b 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -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\"", @@ -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", diff --git a/packages/astro/src/@types/serialize-javascript.d.ts b/packages/astro/src/@types/serialize-javascript.d.ts deleted file mode 100644 index 35ee081b2f5b..000000000000 --- a/packages/astro/src/@types/serialize-javascript.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module 'serialize-javascript' { - export default function serialize(value: any): string; -} diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts index 0bdda98260f4..7c447aadfaf3 100644 --- a/packages/astro/src/core/create-vite.ts +++ b/packages/astro/src/core/create-vite.ts @@ -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', diff --git a/packages/astro/src/runtime/server/astro-island.prebuilt.ts b/packages/astro/src/runtime/server/astro-island.prebuilt.ts new file mode 100644 index 000000000000..f5fdaa28eb51 --- /dev/null +++ b/packages/astro/src/runtime/server/astro-island.prebuilt.ts @@ -0,0 +1,7 @@ +/** + * This file is prebuilt from packages/astro/src/runtime/server/astro-island.ts + * Do not edit this directly, but instead edit that file and rerun the prebuild + * to generate this file. + */ + +export default `{const i={0:t=>t,1:t=>JSON.parse(t,r),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(JSON.parse(t,r)),5:t=>new Set(JSON.parse(t,r)),6:t=>BigInt(t),7:t=>new URL(t)},r=(t,e)=>{if(t===""||!Array.isArray(e))return e;const[n,s]=e;return n in i?i[n](s):void 0};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"))]),e=JSON.parse(this.getAttribute("opts"));t(this,e,async()=>{const n=this.hasAttribute("props")?JSON.parse(this.getAttribute("props"),r):{},s=this.getAttribute("renderer-url"),[{default:o},{default:a}]=await Promise.all([import(this.getAttribute("component-url")),s?import(s):()=>()=>{}]);return(p,l)=>a(p)(o,n,l,{client:this.getAttribute("client")})})}})}`; \ No newline at end of file diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts index 80f0bfecb6ae..5ff60102ca79 100644 --- a/packages/astro/src/runtime/server/astro-island.ts +++ b/packages/astro/src/runtime/server/astro-island.ts @@ -1,6 +1,12 @@ -/* +// Note that this file is prebuilt to astro-island.prebuilt.ts +// Do not import this file directly, instead import the prebuilt one instead. + { - const propTypes = { + interface PropTypeSelector { + [k: string]: (value: any) => any; + } + + const propTypes: PropTypeSelector = { 0: value => value, 1: value => JSON.parse(value, reviver), 2: value => new RegExp(value), @@ -11,7 +17,7 @@ 7: value => new URL(value), }; - const reviver = (propKey, raw) => { + const reviver = (propKey: string, raw: string): any => { if(propKey === '' || !Array.isArray(raw)) return raw; const [type, value] = raw; return (type in propTypes) ? propTypes[type](value) : undefined; @@ -20,32 +26,22 @@ 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')) + import(this.getAttribute('directive-url')!), + import(this.getAttribute('before-hydration-url')!) ]); - const opts = JSON.parse(this.getAttribute('opts')); + const opts = JSON.parse(this.getAttribute('opts')!); setup(this, opts, async () => { - const props = this.hasAttribute('props') ? JSON.parse(this.getAttribute('props'), reviver) : {}; + const props = this.hasAttribute('props') ? JSON.parse(this.getAttribute('props')!, reviver) : {}; const rendererUrl = this.getAttribute('renderer-url'); const [ { default: Component }, { default: hydrate } ] = await Promise.all([ - import(this.getAttribute('component-url')), + import(this.getAttribute('component-url')!), rendererUrl ? import(rendererUrl) : () => () => {} ]); - return (el, children) => hydrate(el)(Component, props, children, { client: this.getAttribute('client') }); + return (el: HTMLElement, children: string) => hydrate(el)(Component, props, children, { client: this.getAttribute('client') }); }); } }); } -*/ - -/** - * 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 = `{const t={0:t=>t,1:t=>JSON.parse(t,e),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(JSON.parse(t,e)),5:t=>new Set(JSON.parse(t,e)),6:t=>BigInt(t),7:t=>new URL(t)},e=(e,r)=>{if(""===e||!Array.isArray(r))return r;const[i,s]=r;return i in t?t[i](s):void 0};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"))]);t(this,JSON.parse(this.getAttribute("opts")),(async()=>{const t=this.hasAttribute("props")?JSON.parse(this.getAttribute("props"),e):{},r=this.getAttribute("renderer-url"),[{default:i},{default:s}]=await Promise.all([import(this.getAttribute("component-url")),r?import(r):()=>()=>{}]);return(e,r)=>s(e)(i,t,r,{client:this.getAttribute("client")})}))}})}`; diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts index ad12fe00e3a5..557dea684228 100644 --- a/packages/astro/src/runtime/server/hydration.ts +++ b/packages/astro/src/runtime/server/hydration.ts @@ -1,4 +1,3 @@ -import serializeJavaScript from 'serialize-javascript'; import type { AstroComponentMetadata, SSRElement, diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts index a468ab193788..6a99a43929e8 100644 --- a/packages/astro/src/runtime/server/index.ts +++ b/packages/astro/src/runtime/server/index.ts @@ -12,7 +12,7 @@ import { escapeHTML, HTMLString, markHTMLString } from './escape.js'; import { extractDirectives, generateHydrateScript } from './hydration.js'; import { shorthash } from './shorthash.js'; import { serializeListValue } from './util.js'; -import { islandScript } from './astro-island.js'; +import islandScript from './astro-island.prebuilt.js'; import { serializeProps } from './serialize.js'; export { markHTMLString, markHTMLString as unescapeHTML } from './escape.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 718380490688..d5fc147bd70f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -532,7 +532,6 @@ importers: rollup: ^2.75.5 sass: ^1.52.2 semver: ^7.3.7 - serialize-javascript: ^6.0.0 shiki: ^0.10.1 sirv: ^2.0.2 slash: ^4.0.0 @@ -590,7 +589,6 @@ importers: 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 @@ -11843,6 +11841,7 @@ packages: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: safe-buffer: 5.2.1 + dev: true /raw-body/2.5.1: resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} @@ -12375,6 +12374,7 @@ packages: resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} dependencies: randombytes: 2.1.0 + dev: true /set-blocking/2.0.0: resolution: {integrity: sha1-BF+XgtARrppoA93TgrJDkrPYkPc=} diff --git a/scripts/cmd/prebuild.js b/scripts/cmd/prebuild.js new file mode 100644 index 000000000000..501a0314dcdf --- /dev/null +++ b/scripts/cmd/prebuild.js @@ -0,0 +1,49 @@ +import * as terser from 'terser'; +import esbuild from 'esbuild'; +import glob from 'tiny-glob'; +import fs from 'fs'; +import path from 'path'; +import { pathToFileURL, fileURLToPath } from 'url'; + +export default async function prebuild(...args) { + let buildToString = args.indexOf('--to-string'); + if(buildToString !== -1) { + args.splice(buildToString, 1); + buildToString = true; + } + + let patterns = args; + let entryPoints = [].concat( + ...(await Promise.all( + patterns.map((pattern) => glob(pattern, { filesOnly: true, absolute: true })) + )) + ); + + function getPrebuildURL(entryfilepath) { + const entryURL = pathToFileURL(entryfilepath); + const basename = path.basename(entryfilepath); + const ext = path.extname(entryfilepath); + const name = basename.slice(0, basename.indexOf(ext)); + const outname = `${name}.prebuilt${ext}`; + const outURL = new URL('./' + outname, entryURL); + return outURL; + } + + async function prebuildFile(filepath) { + const tscode = await fs.promises.readFile(filepath, 'utf-8'); + const esbuildresult = await esbuild.transform(tscode, { loader: 'ts', minify: true }); + const rootURL = new URL('../../', import.meta.url); + const rel = path.relative(fileURLToPath(rootURL), filepath) + const mod = `/** + * This file is prebuilt from ${rel} + * Do not edit this directly, but instead edit that file and rerun the prebuild + * to generate this file. + */ + +export default \`${esbuildresult.code.trim()}\`;`; + const url = getPrebuildURL(filepath); + await fs.promises.writeFile(url, mod, 'utf-8'); + } + + await Promise.all(entryPoints.map(prebuildFile)); +} diff --git a/scripts/index.js b/scripts/index.js index dd789f032ec2..249eac53d135 100755 --- a/scripts/index.js +++ b/scripts/index.js @@ -13,6 +13,11 @@ export default async function run() { await copy(...args); break; } + case 'prebuild': { + const { default: prebuild } = await import('./cmd/prebuild.js'); + await prebuild(...args); + break; + } } } From 7c92f6534aa67ec27814c87b485d8816ef462c87 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 10 Jun 2022 15:08:51 -0400 Subject: [PATCH 03/10] Fix build --- packages/astro/src/core/render/result.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index 457efe44a28c..05ec344b91a5 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -221,6 +221,7 @@ ${extra}` }, resolve, _metadata: { + needsHydrationStyles: false, renderers, pathname, }, From 1fe6bfacfb062c74d5448ac7072f1dfefe95927a Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 10 Jun 2022 15:20:08 -0400 Subject: [PATCH 04/10] Updates to tests --- packages/astro/test/0-css.test.js | 2 +- packages/astro/test/astro-client-only.test.js | 16 ++++------ packages/astro/test/astro-dynamic.test.js | 31 +++++++++---------- .../astro/test/astro-partial-html.test.js | 21 ------------- packages/astro/test/component-library.test.js | 8 ++--- packages/astro/test/react-component.test.js | 4 +-- packages/astro/test/vue-component.test.js | 10 +++--- 7 files changed, 32 insertions(+), 60 deletions(-) diff --git a/packages/astro/test/0-css.test.js b/packages/astro/test/0-css.test.js index acfa01b38cec..e1b317f32d28 100644 --- a/packages/astro/test/0-css.test.js +++ b/packages/astro/test/0-css.test.js @@ -63,7 +63,7 @@ describe('CSS', function () { expect($('#passed-in').attr('class')).to.match(/outer astro-[A-Z0-9]+ astro-[A-Z0-9]+/); }); - it('Using hydrated components adds astro-root styles', async () => { + it('Using hydrated components adds astro-island styles', async () => { const inline = $('style').html(); expect(inline).to.include('display: contents'); }); diff --git a/packages/astro/test/astro-client-only.test.js b/packages/astro/test/astro-client-only.test.js index c8b0ca793d5b..f3435c1d0ad2 100644 --- a/packages/astro/test/astro-client-only.test.js +++ b/packages/astro/test/astro-client-only.test.js @@ -16,13 +16,11 @@ describe('Client only components', () => { const html = await fixture.readFile('/index.html'); const $ = cheerioLoad(html); - // test 1: is empty - expect($('astro-root').html()).to.equal(''); - const $script = $('script'); - const script = $script.html(); + // test 1: is empty + expect($('astro-island').html()).to.equal(''); // test 2: svelte renderer is on the page - expect(/import\("\/entry.*/g.test(script)).to.be.ok; + expect($('astro-island').attr('renderer-url')).to.be.ok; }); it('Adds the CSS to the page', async () => { @@ -53,13 +51,11 @@ describe('Client only components subpath', () => { const html = await fixture.readFile('/index.html'); const $ = cheerioLoad(html); - // test 1: is empty - expect($('astro-root').html()).to.equal(''); - const $script = $('script'); - const script = $script.html(); + // test 1: is empty + expect($('astro-island').html()).to.equal(''); // test 2: svelte renderer is on the page - expect(/import\("\/blog\/entry.*/g.test(script)).to.be.ok; + expect($('astro-island').attr('renderer-url')).to.be.ok; }); it('Adds the CSS to the page', async () => { diff --git a/packages/astro/test/astro-dynamic.test.js b/packages/astro/test/astro-dynamic.test.js index 9e90f073af89..5fcc4596c14b 100644 --- a/packages/astro/test/astro-dynamic.test.js +++ b/packages/astro/test/astro-dynamic.test.js @@ -16,7 +16,7 @@ describe('Dynamic components', () => { const html = await fixture.readFile('/index.html'); const $ = cheerio.load(html); - expect($('script').length).to.eq(2); + expect($('script').length).to.eq(1); }); it('Loads pages using client:media hydrator', async () => { @@ -25,19 +25,18 @@ describe('Dynamic components', () => { const $ = cheerio.load(html); // test 1: static value rendered - expect($('script').length).to.equal(2); // One for each + expect($('script').length).to.equal(1); }); it('Loads pages using client:only hydrator', async () => { const html = await fixture.readFile('/client-only/index.html'); const $ = cheerio.load(html); - // test 1: is empty. - expect($('').html()).to.equal(''); - // test 2: correct script is being loaded. - // because of bundling, we don't have access to the source import, - // only the bundled import. - expect($('script').html()).to.include(`import setup from '/entry`); + // test 1: is empty. + expect($('astro-island').html()).to.equal(''); + // test 2: component url + const href = $('astro-island').attr('component-url'); + expect(href).to.include(`/entry`); }); }); @@ -57,27 +56,25 @@ describe('Dynamic components subpath', () => { const html = await fixture.readFile('/index.html'); const $ = cheerio.load(html); - expect($('script').length).to.eq(2); + expect($('script').length).to.eq(1); }); 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 $ = cheerio.load(html); // test 1: static value rendered - expect($('script').length).to.equal(2); // One for each + expect($('script').length).to.equal(1); }); it('Loads pages using client:only hydrator', async () => { const html = await fixture.readFile('/client-only/index.html'); const $ = cheerio.load(html); - // test 1: is empty. - expect($('').html()).to.equal(''); - // test 2: correct script is being loaded. - // because of bundling, we don't have access to the source import, - // only the bundled import. - expect($('script').html()).to.include(`import setup from '/blog/entry`); + // test 1: is empty. + expect($('astro-island').html()).to.equal(''); + // test 2: has component url + const attr = $('astro-island').attr('component-url'); + expect(attr).to.include(`blog/entry`); }); }); diff --git a/packages/astro/test/astro-partial-html.test.js b/packages/astro/test/astro-partial-html.test.js index d7828a596cf2..5ae2929ce9b7 100644 --- a/packages/astro/test/astro-partial-html.test.js +++ b/packages/astro/test/astro-partial-html.test.js @@ -41,24 +41,3 @@ describe('Partial HTML', async () => { expect(allInjectedStyles).to.match(/h1{color:red;}/); }); }); - -describe('Head Component', async () => { - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/astro-partial-html/', - }); - await fixture.build(); - }); - - it('injects Astro hydration scripts', async () => { - const html = await fixture.readFile('/head/index.html'); - const $ = cheerio.load(html); - - const hydrationId = $('astro-root').attr('uid'); - - const script = $('script').html(); - expect(script).to.match(new RegExp(hydrationId)); - }); -}); diff --git a/packages/astro/test/component-library.test.js b/packages/astro/test/component-library.test.js index 63a8dd46f30c..9158c5b2ea78 100644 --- a/packages/astro/test/component-library.test.js +++ b/packages/astro/test/component-library.test.js @@ -74,7 +74,7 @@ describe('Component Libraries', () => { 'Rendered the client hydrated component' ); - expect($('astro-root[uid]')).to.have.lengthOf(1, 'Included one hydration island'); + expect($('astro-island[uid]')).to.have.lengthOf(1, 'Included one hydration island'); }); it('Works with components hydrated internally', async () => { @@ -87,7 +87,7 @@ describe('Component Libraries', () => { "rendered the counter's slot" ); - expect($('astro-root[uid]')).to.have.lengthOf(1, 'Included one hydration island'); + expect($('astro-island[uid]')).to.have.lengthOf(1, 'Included one hydration island'); }); }); @@ -152,7 +152,7 @@ describe('Component Libraries', () => { 'Rendered the client hydrated component' ); - expect($('astro-root[uid]')).to.have.lengthOf(1, 'Included one hydration island'); + expect($('astro-island[uid]')).to.have.lengthOf(1, 'Included one hydration island'); }); it('Works with components hydrated internally', async () => { @@ -165,7 +165,7 @@ describe('Component Libraries', () => { "rendered the counter's slot" ); - expect($('astro-root[uid]')).to.have.lengthOf(1, 'Included one hydration island'); + expect($('astro-island[uid]')).to.have.lengthOf(1, 'Included one hydration island'); }); }); }); diff --git a/packages/astro/test/react-component.test.js b/packages/astro/test/react-component.test.js index 749fc0c16575..68624aed642c 100644 --- a/packages/astro/test/react-component.test.js +++ b/packages/astro/test/react-component.test.js @@ -42,10 +42,10 @@ describe('React Components', () => { expect($('#pure')).to.have.lengthOf(1); // test 8: Check number of islands - expect($('astro-root[uid]')).to.have.lengthOf(5); + expect($('astro-island[uid]')).to.have.lengthOf(5); // test 9: Check island deduplication - const uniqueRootUIDs = new Set($('astro-root').map((i, el) => $(el).attr('uid'))); + const uniqueRootUIDs = new Set($('astro-island').map((i, el) => $(el).attr('uid'))); expect(uniqueRootUIDs.size).to.equal(4); }); diff --git a/packages/astro/test/vue-component.test.js b/packages/astro/test/vue-component.test.js index 3c57c65447b9..5ee632a47e43 100644 --- a/packages/astro/test/vue-component.test.js +++ b/packages/astro/test/vue-component.test.js @@ -27,17 +27,17 @@ describe('Vue component', () => { // test 1: renders all components correctly expect(allPreValues).to.deep.equal(['0', '1', '1', '1', '10', '100', '1000']); - // test 2: renders 3 s - expect($('astro-root')).to.have.lengthOf(6); + // test 2: renders 3 s + expect($('astro-island')).to.have.lengthOf(6); - // test 3: all s have uid attributes - expect($('astro-root[uid]')).to.have.lengthOf(6); + // test 3: all s have uid attributes + expect($('astro-island[uid]')).to.have.lengthOf(6); // test 4: treats as a custom element 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')); + const uniqueRootUIDs = $('astro-island').map((i, el) => $(el).attr('uid')); expect(new Set(uniqueRootUIDs).size).to.equal(5); }); }); From adf42656130cf250ad602e651a8703c5697faf85 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 10 Jun 2022 15:31:05 -0400 Subject: [PATCH 05/10] Update more references --- packages/astro/src/runtime/client/events.ts | 2 +- packages/astro/src/runtime/client/hmr.ts | 6 +++--- packages/astro/src/runtime/client/idle.ts | 2 +- packages/astro/src/runtime/client/load.ts | 2 +- packages/astro/src/runtime/client/media.ts | 2 +- packages/astro/src/runtime/client/only.ts | 2 +- packages/astro/src/runtime/client/visible.ts | 4 ++-- packages/markdown/remark/src/rehype-islands.ts | 6 +++--- packages/markdown/remark/src/remark-unwrap.ts | 6 +++--- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/astro/src/runtime/client/events.ts b/packages/astro/src/runtime/client/events.ts index fd7a31f2c9ac..e8ab25c3934e 100644 --- a/packages/astro/src/runtime/client/events.ts +++ b/packages/astro/src/runtime/client/events.ts @@ -9,7 +9,7 @@ function debounce any>(cb: T, wait = 20) { } export const notify = debounce(() => { - if (document.querySelector('astro-root[ssr]')) { + if (document.querySelector('astro-island[ssr]')) { window.dispatchEvent(new CustomEvent(HYDRATE_KEY)); } }); diff --git a/packages/astro/src/runtime/client/hmr.ts b/packages/astro/src/runtime/client/hmr.ts index e9cf5bd829c0..4e1713b2e109 100644 --- a/packages/astro/src/runtime/client/hmr.ts +++ b/packages/astro/src/runtime/client/hmr.ts @@ -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); @@ -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]")) { diff --git a/packages/astro/src/runtime/client/idle.ts b/packages/astro/src/runtime/client/idle.ts index d6ad71d9fd06..44ad5e22eeb6 100644 --- a/packages/astro/src/runtime/client/idle.ts +++ b/packages/astro/src/runtime/client/idle.ts @@ -33,7 +33,7 @@ export default async function onIdle( if (!hydrate) { hydrate = await getHydrateCallback(); } - if (!root.parentElement?.closest('astro-root[ssr]')) { + if (!root.parentElement?.closest('astro-island[ssr]')) { await hydrate(root, innerHTML); root.removeAttribute('ssr'); } diff --git a/packages/astro/src/runtime/client/load.ts b/packages/astro/src/runtime/client/load.ts index f083a4577217..ec72929379c8 100644 --- a/packages/astro/src/runtime/client/load.ts +++ b/packages/astro/src/runtime/client/load.ts @@ -31,7 +31,7 @@ export default async function onLoad( if (!hydrate) { hydrate = await getHydrateCallback(); } - if (!root.parentElement?.closest('astro-root[ssr]')) { + if (!root.parentElement?.closest('astro-island[ssr]')) { await hydrate(root, innerHTML); root.removeAttribute('ssr'); } diff --git a/packages/astro/src/runtime/client/media.ts b/packages/astro/src/runtime/client/media.ts index ac3c249ab03b..b417b779abe5 100644 --- a/packages/astro/src/runtime/client/media.ts +++ b/packages/astro/src/runtime/client/media.ts @@ -32,7 +32,7 @@ export default async function onMedia( if (!hydrate) { hydrate = await getHydrateCallback(); } - if (!root.parentElement?.closest('astro-root[ssr]')) { + if (!root.parentElement?.closest('astro-island[ssr]')) { await hydrate(root, innerHTML); root.removeAttribute('ssr'); } diff --git a/packages/astro/src/runtime/client/only.ts b/packages/astro/src/runtime/client/only.ts index a598f302c23b..dab21be42927 100644 --- a/packages/astro/src/runtime/client/only.ts +++ b/packages/astro/src/runtime/client/only.ts @@ -31,7 +31,7 @@ export default async function onOnly( if (!hydrate) { hydrate = await getHydrateCallback(); } - if (!root.parentElement?.closest('astro-root[ssr]')) { + if (!root.parentElement?.closest('astro-island[ssr]')) { await hydrate(root, innerHTML); root.removeAttribute('ssr'); } diff --git a/packages/astro/src/runtime/client/visible.ts b/packages/astro/src/runtime/client/visible.ts index 332f2c3a6ec0..d83c3ced5b43 100644 --- a/packages/astro/src/runtime/client/visible.ts +++ b/packages/astro/src/runtime/client/visible.ts @@ -3,7 +3,7 @@ import { listen, notify } from './events'; /** * 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( @@ -50,7 +50,7 @@ export default async function onVisible( 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 diff --git a/packages/markdown/remark/src/rehype-islands.ts b/packages/markdown/remark/src/rehype-islands.ts index bbd5847924fa..a8b78848df89 100644 --- a/packages/markdown/remark/src/rehype-islands.ts +++ b/packages/markdown/remark/src/rehype-islands.ts @@ -9,14 +9,14 @@ const visit = _visit as ( ) => any; // This fixes some confusing bugs coming from somewhere inside of our Markdown pipeline. -// `unist`/`remark`/`rehype` (not sure) often generate malformed HTML inside of +// `unist`/`remark`/`rehype` (not sure) often generate malformed HTML inside of // 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! export default function rehypeIslands(): any { return function (node: any): any { return visit(node, 'element', (el) => { - // Bugs only happen inside of islands - if (el.tagName == 'astro-root') { + // Bugs only happen inside of islands + if (el.tagName == 'astro-island') { visit(el, 'text', (child, index, parent) => { if (child.type === 'text') { // Sometimes comments can be trapped as text, which causes them to be escaped diff --git a/packages/markdown/remark/src/remark-unwrap.ts b/packages/markdown/remark/src/remark-unwrap.ts index e54f01397fb8..399bd6cd603f 100644 --- a/packages/markdown/remark/src/remark-unwrap.ts +++ b/packages/markdown/remark/src/remark-unwrap.ts @@ -8,7 +8,7 @@ const visit = _visit as ( callback?: (node: any, index: number, parent: any) => any ) => any; -// Remove the wrapping paragraph for islands +// Remove the wrapping paragraph for islands export default function remarkUnwrap() { const astroRootNodes = new Set(); let insideAstroRoot = false; @@ -19,10 +19,10 @@ export default function remarkUnwrap() { astroRootNodes.clear(); visit(tree, 'html', (node) => { - if (node.value.indexOf(' -1 && !insideAstroRoot) { + if (node.value.indexOf(' -1 && !insideAstroRoot) { insideAstroRoot = true; } - if (node.value.indexOf(' -1 && insideAstroRoot) { + if (node.value.indexOf(' -1 && insideAstroRoot) { insideAstroRoot = false; } astroRootNodes.add(node); From f52864cb6e05e1c346874e3c97b9552e17dfbdc3 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 10 Jun 2022 15:44:18 -0400 Subject: [PATCH 06/10] Custom element test now has two classic scripts --- packages/astro/test/custom-elements.test.js | 2 +- .../test/fixtures/custom-elements/my-component-lib/index.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/astro/test/custom-elements.test.js b/packages/astro/test/custom-elements.test.js index a00ea6887323..0a380026ff6c 100644 --- a/packages/astro/test/custom-elements.test.js +++ b/packages/astro/test/custom-elements.test.js @@ -50,7 +50,7 @@ describe('Custom Elements', () => { // Hydration // test 3: Component and polyfill scripts bundled separately - expect($('script[type=module]')).to.have.lengthOf(1); + expect($('script')).to.have.lengthOf(2); }); it('Custom elements not claimed by renderer are rendered as regular HTML', async () => { diff --git a/packages/astro/test/fixtures/custom-elements/my-component-lib/index.js b/packages/astro/test/fixtures/custom-elements/my-component-lib/index.js index a550dfee2117..5b9bba7e62e6 100644 --- a/packages/astro/test/fixtures/custom-elements/my-component-lib/index.js +++ b/packages/astro/test/fixtures/custom-elements/my-component-lib/index.js @@ -13,9 +13,9 @@ export default function () { hooks: { 'astro:config:setup': ({ updateConfig, addRenderer, injectScript }) => { // Inject the necessary polyfills on every page - injectScript('head-inline', `import '@test/custom-element-renderer/polyfill.js';`); + injectScript('head-inline', `import('@test/custom-element-renderer/polyfill.js');`); // Inject the hydration code, before a component is hydrated. - injectScript('before-hydration', `import '@test/custom-element-renderer/hydration-polyfill.js';`); + injectScript('before-hydration', `import('@test/custom-element-renderer/hydration-polyfill.js');`); // Add the lit renderer so that Astro can understand lit components. addRenderer({ name: '@test/custom-element-renderer', From bf25ff14051435ff384d2859915d844a20acf67c Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 13 Jun 2022 08:38:26 -0400 Subject: [PATCH 07/10] Account for non-default exports --- packages/astro/src/runtime/server/astro-island.prebuilt.ts | 2 +- packages/astro/src/runtime/server/astro-island.ts | 3 ++- packages/astro/src/runtime/server/hydration.ts | 1 + scripts/cmd/prebuild.js | 4 +++- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/runtime/server/astro-island.prebuilt.ts b/packages/astro/src/runtime/server/astro-island.prebuilt.ts index f5fdaa28eb51..39b53c052e32 100644 --- a/packages/astro/src/runtime/server/astro-island.prebuilt.ts +++ b/packages/astro/src/runtime/server/astro-island.prebuilt.ts @@ -4,4 +4,4 @@ * to generate this file. */ -export default `{const i={0:t=>t,1:t=>JSON.parse(t,r),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(JSON.parse(t,r)),5:t=>new Set(JSON.parse(t,r)),6:t=>BigInt(t),7:t=>new URL(t)},r=(t,e)=>{if(t===""||!Array.isArray(e))return e;const[n,s]=e;return n in i?i[n](s):void 0};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"))]),e=JSON.parse(this.getAttribute("opts"));t(this,e,async()=>{const n=this.hasAttribute("props")?JSON.parse(this.getAttribute("props"),r):{},s=this.getAttribute("renderer-url"),[{default:o},{default:a}]=await Promise.all([import(this.getAttribute("component-url")),s?import(s):()=>()=>{}]);return(p,l)=>a(p)(o,n,l,{client:this.getAttribute("client")})})}})}`; \ No newline at end of file +export default `{const i={0:t=>t,1:t=>JSON.parse(t,r),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(JSON.parse(t,r)),5:t=>new Set(JSON.parse(t,r)),6:t=>BigInt(t),7:t=>new URL(t)},r=(t,e)=>{if(t===""||!Array.isArray(e))return e;const[n,s]=e;return n in i?i[n](s):void 0};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"))]),e=JSON.parse(this.getAttribute("opts"));t(this,e,async()=>{const n=this.hasAttribute("props")?JSON.parse(this.getAttribute("props"),r):{},s=this.getAttribute("renderer-url"),[o,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),s?import(s):()=>()=>{}]),a=o[this.getAttribute("component-export")||"default"];return(l,c)=>p(l)(a,n,c,{client:this.getAttribute("client")})})}})}`; \ No newline at end of file diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts index 5ff60102ca79..18e301146405 100644 --- a/packages/astro/src/runtime/server/astro-island.ts +++ b/packages/astro/src/runtime/server/astro-island.ts @@ -34,12 +34,13 @@ const props = this.hasAttribute('props') ? JSON.parse(this.getAttribute('props')!, reviver) : {}; const rendererUrl = this.getAttribute('renderer-url'); const [ - { default: Component }, + componentModule, { default: hydrate } ] = await Promise.all([ import(this.getAttribute('component-url')!), rendererUrl ? import(rendererUrl) : () => () => {} ]); + const Component = componentModule[this.getAttribute('component-export') || 'default']; return (el: HTMLElement, children: string) => hydrate(el)(Component, props, children, { client: this.getAttribute('client') }); }); } diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts index 557dea684228..90a9d7b7d6ad 100644 --- a/packages/astro/src/runtime/server/hydration.ts +++ b/packages/astro/src/runtime/server/hydration.ts @@ -127,6 +127,7 @@ export async function generateHydrateScript( // Add renderer url if(renderer.clientEntrypoint) { + island.props['component-export'] = componentExport.value; island.props['renderer-url'] = await result.resolve(renderer.clientEntrypoint); island.props['props'] = escapeHTML(serializeProps(props)); } diff --git a/scripts/cmd/prebuild.js b/scripts/cmd/prebuild.js index 501a0314dcdf..06917e43a4c6 100644 --- a/scripts/cmd/prebuild.js +++ b/scripts/cmd/prebuild.js @@ -31,7 +31,9 @@ export default async function prebuild(...args) { async function prebuildFile(filepath) { const tscode = await fs.promises.readFile(filepath, 'utf-8'); - const esbuildresult = await esbuild.transform(tscode, { loader: 'ts', minify: true }); + const esbuildresult = await esbuild.transform(tscode, { + loader: 'ts', minify: true + }); const rootURL = new URL('../../', import.meta.url); const rel = path.relative(fileURLToPath(rootURL), filepath) const mod = `/** From 3b30f1c08ddb2ad916565b73e73e1f4684cfd0de Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 14 Jun 2022 08:45:55 -0400 Subject: [PATCH 08/10] Restructure hydration directives --- packages/astro/src/@types/astro.ts | 2 +- packages/astro/src/runtime/client/events.ts | 7 +- packages/astro/src/runtime/client/idle.ts | 29 +------- packages/astro/src/runtime/client/load.ts | 29 +------- packages/astro/src/runtime/client/media.ts | 29 +------- packages/astro/src/runtime/client/only.ts | 29 +------- packages/astro/src/runtime/client/visible.ts | 29 +------- .../runtime/server/astro-island.prebuilt.ts | 2 +- .../astro/src/runtime/server/astro-island.ts | 74 +++++++++++++------ packages/astro/src/runtime/server/index.ts | 12 ++- scripts/cmd/prebuild.js | 3 +- 11 files changed, 83 insertions(+), 162 deletions(-) diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 52d554e33135..2aae329a7eaf 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -767,7 +767,7 @@ export interface MarkdownInstance> { } export type GetHydrateCallback = () => Promise< - (element: Element, innerHTML: string | null) => void | Promise + () => void | Promise >; /** diff --git a/packages/astro/src/runtime/client/events.ts b/packages/astro/src/runtime/client/events.ts index e8ab25c3934e..93a8c2600a21 100644 --- a/packages/astro/src/runtime/client/events.ts +++ b/packages/astro/src/runtime/client/events.ts @@ -9,14 +9,9 @@ function debounce any>(cb: T, wait = 20) { } export const notify = debounce(() => { - if (document.querySelector('astro-island[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 }); diff --git a/packages/astro/src/runtime/client/idle.ts b/packages/astro/src/runtime/client/idle.ts index 44ad5e22eeb6..a719cd7a7b2a 100644 --- a/packages/astro/src/runtime/client/idle.ts +++ b/packages/astro/src/runtime/client/idle.ts @@ -1,5 +1,5 @@ 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 @@ -10,33 +10,10 @@ export default async function onIdle( options: HydrateOptions, getHydrateCallback: GetHydrateCallback ) { - let innerHTML: string | null = null; - let hydrate: Awaited>; - async function idle() { - listen(idle); const cb = async () => { - if (typeof innerHTML !== 'string') { - 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 = root.querySelector(`template[data-astro-template]`); - if (template) { - innerHTML = template.innerHTML; - template.remove(); - } - } else if (fragment) { - innerHTML = fragment.innerHTML; - } - } - if (!hydrate) { - hydrate = await getHydrateCallback(); - } - if (!root.parentElement?.closest('astro-island[ssr]')) { - await hydrate(root, innerHTML); - root.removeAttribute('ssr'); - } + let hydrate = await getHydrateCallback(); + await hydrate(); notify(); }; diff --git a/packages/astro/src/runtime/client/load.ts b/packages/astro/src/runtime/client/load.ts index ec72929379c8..0301aba1c473 100644 --- a/packages/astro/src/runtime/client/load.ts +++ b/packages/astro/src/runtime/client/load.ts @@ -1,5 +1,5 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; -import { listen, notify } from './events'; +import { notify } from './events'; /** * Hydrate this component immediately @@ -9,32 +9,9 @@ export default async function onLoad( options: HydrateOptions, getHydrateCallback: GetHydrateCallback ) { - let innerHTML: string | null = null; - let hydrate: Awaited>; - async function load() { - listen(load); - if (typeof innerHTML !== 'string') { - 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 = root.querySelector(`template[data-astro-template]`); - if (template) { - innerHTML = template.innerHTML; - template.remove(); - } - } else if (fragment) { - innerHTML = fragment.innerHTML; - } - } - if (!hydrate) { - hydrate = await getHydrateCallback(); - } - if (!root.parentElement?.closest('astro-island[ssr]')) { - await hydrate(root, innerHTML); - root.removeAttribute('ssr'); - } + let hydrate = await getHydrateCallback(); + await hydrate(); notify(); } load(); diff --git a/packages/astro/src/runtime/client/media.ts b/packages/astro/src/runtime/client/media.ts index b417b779abe5..22fbd641e001 100644 --- a/packages/astro/src/runtime/client/media.ts +++ b/packages/astro/src/runtime/client/media.ts @@ -1,5 +1,5 @@ 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 @@ -9,33 +9,10 @@ export default async function onMedia( options: HydrateOptions, getHydrateCallback: GetHydrateCallback ) { - let innerHTML: string | null = null; - let hydrate: Awaited>; - async function media() { - listen(media); const cb = async () => { - if (typeof innerHTML !== 'string') { - 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 = root.querySelector(`template[data-astro-template]`); - if (template) { - innerHTML = template.innerHTML; - template.remove(); - } - } else if (fragment) { - innerHTML = fragment.innerHTML; - } - } - if (!hydrate) { - hydrate = await getHydrateCallback(); - } - if (!root.parentElement?.closest('astro-island[ssr]')) { - await hydrate(root, innerHTML); - root.removeAttribute('ssr'); - } + let hydrate = await getHydrateCallback(); + await hydrate(); notify(); }; diff --git a/packages/astro/src/runtime/client/only.ts b/packages/astro/src/runtime/client/only.ts index dab21be42927..2fa5a58931ae 100644 --- a/packages/astro/src/runtime/client/only.ts +++ b/packages/astro/src/runtime/client/only.ts @@ -1,5 +1,5 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; -import { listen, notify } from './events'; +import { notify } from './events'; /** * Hydrate this component only on the client @@ -9,32 +9,9 @@ export default async function onOnly( options: HydrateOptions, getHydrateCallback: GetHydrateCallback ) { - let innerHTML: string | null = null; - let hydrate: Awaited>; - async function only() { - listen(only); - if (typeof innerHTML !== 'string') { - 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 = root.querySelector(`template[data-astro-template]`); - if (template) { - innerHTML = template.innerHTML; - template.remove(); - } - } else if (fragment) { - innerHTML = fragment.innerHTML; - } - } - if (!hydrate) { - hydrate = await getHydrateCallback(); - } - if (!root.parentElement?.closest('astro-island[ssr]')) { - await hydrate(root, innerHTML); - root.removeAttribute('ssr'); - } + let hydrate = await getHydrateCallback(); + await hydrate(); notify(); } only(); diff --git a/packages/astro/src/runtime/client/visible.ts b/packages/astro/src/runtime/client/visible.ts index d83c3ced5b43..2d1a8f70ae90 100644 --- a/packages/astro/src/runtime/client/visible.ts +++ b/packages/astro/src/runtime/client/visible.ts @@ -1,5 +1,5 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; -import { listen, notify } from './events'; +import { notify } from './events'; /** * Hydrate this component when one of it's children becomes visible @@ -12,34 +12,11 @@ export default async function onVisible( getHydrateCallback: GetHydrateCallback ) { let io: IntersectionObserver; - let innerHTML: string | null = null; - let hydrate: Awaited>; async function visible() { - listen(visible); const cb = async () => { - if (typeof innerHTML !== 'string') { - 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 = root.querySelector(`template[data-astro-template]`); - if (template) { - innerHTML = template.innerHTML; - template.remove(); - } - } else if (fragment) { - innerHTML = fragment.innerHTML; - } - } - if (!hydrate) { - hydrate = await getHydrateCallback(); - } - - if (!root.parentElement?.closest('astro-island[ssr]')) { - await hydrate(root, innerHTML); - root.removeAttribute('ssr'); - } + let hydrate = await getHydrateCallback(); + await hydrate(); notify(); }; diff --git a/packages/astro/src/runtime/server/astro-island.prebuilt.ts b/packages/astro/src/runtime/server/astro-island.prebuilt.ts index 39b53c052e32..82b5427943d5 100644 --- a/packages/astro/src/runtime/server/astro-island.prebuilt.ts +++ b/packages/astro/src/runtime/server/astro-island.prebuilt.ts @@ -4,4 +4,4 @@ * to generate this file. */ -export default `{const i={0:t=>t,1:t=>JSON.parse(t,r),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(JSON.parse(t,r)),5:t=>new Set(JSON.parse(t,r)),6:t=>BigInt(t),7:t=>new URL(t)},r=(t,e)=>{if(t===""||!Array.isArray(e))return e;const[n,s]=e;return n in i?i[n](s):void 0};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"))]),e=JSON.parse(this.getAttribute("opts"));t(this,e,async()=>{const n=this.hasAttribute("props")?JSON.parse(this.getAttribute("props"),r):{},s=this.getAttribute("renderer-url"),[o,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),s?import(s):()=>()=>{}]),a=o[this.getAttribute("component-export")||"default"];return(l,c)=>p(l)(a,n,c,{client:this.getAttribute("client")})})}})}`; \ No newline at end of file +export default `var a;{const o={0:t=>t,1:t=>JSON.parse(t,n),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(JSON.parse(t,n)),5:t=>new Set(JSON.parse(t,n)),6:t=>BigInt(t),7:t=>new URL(t)},n=(t,e)=>{if(t===""||!Array.isArray(e))return e;const[r,s]=e;return r in o?o[r](s):void 0};customElements.get("astro-island")||customElements.define("astro-island",(a=class extends HTMLElement{constructor(){super(...arguments);this.hydrate=()=>{if(this.parentElement?.closest("astro-island[ssr]"))window.addEventListener("astro:hydrate",this.hydrate,{once:!0});else{let e=null,r=this.querySelector("astro-fragment");if(r==null&&this.hasAttribute("tmpl")){let i=this.querySelector("template[data-astro-template]");i&&(e=i.innerHTML,i.remove())}else r&&(e=r.innerHTML);const s=this.hasAttribute("props")?JSON.parse(this.getAttribute("props"),n):{};this.hydrator(this)(this.Component,s,e,{client:this.getAttribute("client")})}}}async connectedCallback(){const[{default:e}]=await Promise.all([import(this.getAttribute("directive-url")),import(this.getAttribute("before-hydration-url"))]),r=JSON.parse(this.getAttribute("opts"));e(this,r,async()=>{const s=this.getAttribute("renderer-url"),[i,{default:l}]=await Promise.all([import(this.getAttribute("component-url")),s?import(s):()=>()=>{}]);return this.Component=i[this.getAttribute("component-export")||"default"],this.hydrator=l,this.hydrate})}attributeChangedCallback(){this.hydrator&&this.hydrate()}},a.observedAttributes=["props"],a))}`; \ No newline at end of file diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts index 18e301146405..6643f1994629 100644 --- a/packages/astro/src/runtime/server/astro-island.ts +++ b/packages/astro/src/runtime/server/astro-island.ts @@ -1,5 +1,6 @@ // Note that this file is prebuilt to astro-island.prebuilt.ts // Do not import this file directly, instead import the prebuilt one instead. +// pnpm --filter astro run prebuild { interface PropTypeSelector { @@ -23,26 +24,57 @@ return (type in propTypes) ? propTypes[type](value) : undefined; }; - 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 props = this.hasAttribute('props') ? JSON.parse(this.getAttribute('props')!, reviver) : {}; - const rendererUrl = this.getAttribute('renderer-url'); - const [ - componentModule, - { default: hydrate } - ] = await Promise.all([ - import(this.getAttribute('component-url')!), - rendererUrl ? import(rendererUrl) : () => () => {} + if(!customElements.get('astro-island')) { + customElements.define('astro-island', class extends HTMLElement { + public Component: any; + 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')!) ]); - const Component = componentModule[this.getAttribute('component-export') || 'default']; - return (el: HTMLElement, children: string) => hydrate(el)(Component, props, children, { client: this.getAttribute('client') }); - }); - } - }); + const opts = JSON.parse(this.getAttribute('opts')!); + setup(this, opts, async () => { + const rendererUrl = this.getAttribute('renderer-url'); + const [ + componentModule, + { default: hydrator } + ] = await Promise.all([ + import(this.getAttribute('component-url')!), + rendererUrl ? import(rendererUrl) : () => () => {} + ]); + this.Component = componentModule[this.getAttribute('component-export') || 'default']; + this.hydrator = hydrator; + return this.hydrate; + }); + } + hydrate = () => { + if(this.parentElement?.closest('astro-island[ssr]')) { + window.addEventListener('astro:hydrate', this.hydrate, { once: true }); + } else { + let innerHTML: string | null = null; + let fragment = this.querySelector('astro-fragment'); + if (fragment == null && this.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 = this.querySelector('template[data-astro-template]'); + if (template) { + innerHTML = template.innerHTML; + template.remove(); + } + } else if (fragment) { + innerHTML = fragment.innerHTML; + } + const props = this.hasAttribute('props') ? JSON.parse(this.getAttribute('props')!, reviver) : {}; + this.hydrator(this)(this.Component, props, innerHTML, { + client: this.getAttribute('client') + }); + } + } + attributeChangedCallback() { + if(this.hydrator) this.hydrate(); + } + }); + } } diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts index 6a99a43929e8..e06b5fedf929 100644 --- a/packages/astro/src/runtime/server/index.ts +++ b/packages/astro/src/runtime/server/index.ts @@ -32,6 +32,14 @@ const svgEnumAttributes = /^(autoReverse|externalResourcesRequired|focusable|pre // object corresponds to one page request, we are using it as a key to know. const resultsWithHydrationScript = new WeakSet(); +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. @@ -182,6 +190,7 @@ export async function renderComponent( const { hydration, props } = extractDirectives(_props); let html = ''; + let needsHydrationScript = hydration && determineIfNeedsHydrationScript(result); if (hydration) { metadata.hydrate = hydration.directive as AstroComponentMetadata['hydrate']; @@ -345,8 +354,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr // 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); + 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 diff --git a/scripts/cmd/prebuild.js b/scripts/cmd/prebuild.js index 06917e43a4c6..7f8733cfe7a5 100644 --- a/scripts/cmd/prebuild.js +++ b/scripts/cmd/prebuild.js @@ -32,7 +32,8 @@ export default async function prebuild(...args) { async function prebuildFile(filepath) { const tscode = await fs.promises.readFile(filepath, 'utf-8'); const esbuildresult = await esbuild.transform(tscode, { - loader: 'ts', minify: true + loader: 'ts', + minify: true }); const rootURL = new URL('../../', import.meta.url); const rel = path.relative(fileURLToPath(rootURL), filepath) From 263f9188f7f7d1d665494b9aefe6826ea17a2616 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 14 Jun 2022 11:29:17 -0400 Subject: [PATCH 09/10] Move nested logic into the island component --- .../runtime/server/astro-island.prebuilt.ts | 2 +- .../astro/src/runtime/server/astro-island.ts | 51 +++++++++++-------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/packages/astro/src/runtime/server/astro-island.prebuilt.ts b/packages/astro/src/runtime/server/astro-island.prebuilt.ts index 82b5427943d5..11e650096338 100644 --- a/packages/astro/src/runtime/server/astro-island.prebuilt.ts +++ b/packages/astro/src/runtime/server/astro-island.prebuilt.ts @@ -4,4 +4,4 @@ * to generate this file. */ -export default `var a;{const o={0:t=>t,1:t=>JSON.parse(t,n),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(JSON.parse(t,n)),5:t=>new Set(JSON.parse(t,n)),6:t=>BigInt(t),7:t=>new URL(t)},n=(t,e)=>{if(t===""||!Array.isArray(e))return e;const[r,s]=e;return r in o?o[r](s):void 0};customElements.get("astro-island")||customElements.define("astro-island",(a=class extends HTMLElement{constructor(){super(...arguments);this.hydrate=()=>{if(this.parentElement?.closest("astro-island[ssr]"))window.addEventListener("astro:hydrate",this.hydrate,{once:!0});else{let e=null,r=this.querySelector("astro-fragment");if(r==null&&this.hasAttribute("tmpl")){let i=this.querySelector("template[data-astro-template]");i&&(e=i.innerHTML,i.remove())}else r&&(e=r.innerHTML);const s=this.hasAttribute("props")?JSON.parse(this.getAttribute("props"),n):{};this.hydrator(this)(this.Component,s,e,{client:this.getAttribute("client")})}}}async connectedCallback(){const[{default:e}]=await Promise.all([import(this.getAttribute("directive-url")),import(this.getAttribute("before-hydration-url"))]),r=JSON.parse(this.getAttribute("opts"));e(this,r,async()=>{const s=this.getAttribute("renderer-url"),[i,{default:l}]=await Promise.all([import(this.getAttribute("component-url")),s?import(s):()=>()=>{}]);return this.Component=i[this.getAttribute("component-export")||"default"],this.hydrator=l,this.hydrate})}attributeChangedCallback(){this.hydrator&&this.hydrate()}},a.observedAttributes=["props"],a))}`; \ No newline at end of file +export default `var a;{const o={0:t=>t,1:t=>JSON.parse(t,n),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(JSON.parse(t,n)),5:t=>new Set(JSON.parse(t,n)),6:t=>BigInt(t),7:t=>new URL(t)},n=(t,e)=>{if(t===""||!Array.isArray(e))return e;const[r,s]=e;return r in o?o[r](s):void 0};customElements.get("astro-island")||customElements.define("astro-island",(a=class extends HTMLElement{constructor(){super(...arguments);this.hydrate=()=>{if(!this.hydrator||this.parentElement?.closest("astro-island[ssr]"))return;let e=null,r=this.querySelector("astro-fragment");if(r==null&&this.hasAttribute("tmpl")){let i=this.querySelector("template[data-astro-template]");i&&(e=i.innerHTML,i.remove())}else r&&(e=r.innerHTML);const s=this.hasAttribute("props")?JSON.parse(this.getAttribute("props"),n):{};this.hydrator(this)(this.Component,s,e,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),window.removeEventListener("astro:hydrate",this.hydrate),window.dispatchEvent(new CustomEvent("astro:hydrate"))}}async connectedCallback(){const[{default:e}]=await Promise.all([import(this.getAttribute("directive-url")),import(this.getAttribute("before-hydration-url"))]);window.addEventListener("astro:hydrate",this.hydrate);const r=JSON.parse(this.getAttribute("opts"));e(this,r,async()=>{const s=this.getAttribute("renderer-url"),[i,{default:l}]=await Promise.all([import(this.getAttribute("component-url")),s?import(s):()=>()=>{}]);return this.Component=i[this.getAttribute("component-export")||"default"],this.hydrator=l,this.hydrate})}attributeChangedCallback(){try{this.hydrator&&this.hydrate()}catch{debugger}}},a.observedAttributes=["props"],a))}`; \ No newline at end of file diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts index 6643f1994629..9598be723bca 100644 --- a/packages/astro/src/runtime/server/astro-island.ts +++ b/packages/astro/src/runtime/server/astro-island.ts @@ -34,6 +34,8 @@ 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 () => { const rendererUrl = this.getAttribute('renderer-url'); @@ -49,31 +51,38 @@ return this.hydrate; }); } - hydrate = () => { - if(this.parentElement?.closest('astro-island[ssr]')) { - window.addEventListener('astro:hydrate', this.hydrate, { once: true }); - } else { - let innerHTML: string | null = null; - let fragment = this.querySelector('astro-fragment'); - if (fragment == null && this.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 = this.querySelector('template[data-astro-template]'); - if (template) { - innerHTML = template.innerHTML; - template.remove(); - } - } else if (fragment) { - innerHTML = fragment.innerHTML; + hydrate = () => { + if(!this.hydrator || this.parentElement?.closest('astro-island[ssr]')) { + return; + } + let innerHTML: string | null = null; + let fragment = this.querySelector('astro-fragment'); + if (fragment == null && this.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 = this.querySelector('template[data-astro-template]'); + if (template) { + innerHTML = template.innerHTML; + template.remove(); } - const props = this.hasAttribute('props') ? JSON.parse(this.getAttribute('props')!, reviver) : {}; - this.hydrator(this)(this.Component, props, innerHTML, { - client: this.getAttribute('client') - }); + } else if (fragment) { + innerHTML = fragment.innerHTML; } + const props = this.hasAttribute('props') ? JSON.parse(this.getAttribute('props')!, reviver) : {}; + this.hydrator(this)(this.Component, props, innerHTML, { + client: this.getAttribute('client') + }); + this.removeAttribute('ssr'); + window.removeEventListener('astro:hydrate', this.hydrate); + window.dispatchEvent(new CustomEvent('astro:hydrate')); } attributeChangedCallback() { - if(this.hydrator) this.hydrate(); + try { + if(this.hydrator) this.hydrate(); + } catch( err) { + debugger; + } + } }); } From 6aa1a5ff4e621783fdb45b2e22d12de72247426e Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 14 Jun 2022 11:49:05 -0400 Subject: [PATCH 10/10] Remove try/catch --- packages/astro/src/runtime/server/astro-island.prebuilt.ts | 2 +- packages/astro/src/runtime/server/astro-island.ts | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/astro/src/runtime/server/astro-island.prebuilt.ts b/packages/astro/src/runtime/server/astro-island.prebuilt.ts index 11e650096338..0ac881edf74a 100644 --- a/packages/astro/src/runtime/server/astro-island.prebuilt.ts +++ b/packages/astro/src/runtime/server/astro-island.prebuilt.ts @@ -4,4 +4,4 @@ * to generate this file. */ -export default `var a;{const o={0:t=>t,1:t=>JSON.parse(t,n),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(JSON.parse(t,n)),5:t=>new Set(JSON.parse(t,n)),6:t=>BigInt(t),7:t=>new URL(t)},n=(t,e)=>{if(t===""||!Array.isArray(e))return e;const[r,s]=e;return r in o?o[r](s):void 0};customElements.get("astro-island")||customElements.define("astro-island",(a=class extends HTMLElement{constructor(){super(...arguments);this.hydrate=()=>{if(!this.hydrator||this.parentElement?.closest("astro-island[ssr]"))return;let e=null,r=this.querySelector("astro-fragment");if(r==null&&this.hasAttribute("tmpl")){let i=this.querySelector("template[data-astro-template]");i&&(e=i.innerHTML,i.remove())}else r&&(e=r.innerHTML);const s=this.hasAttribute("props")?JSON.parse(this.getAttribute("props"),n):{};this.hydrator(this)(this.Component,s,e,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),window.removeEventListener("astro:hydrate",this.hydrate),window.dispatchEvent(new CustomEvent("astro:hydrate"))}}async connectedCallback(){const[{default:e}]=await Promise.all([import(this.getAttribute("directive-url")),import(this.getAttribute("before-hydration-url"))]);window.addEventListener("astro:hydrate",this.hydrate);const r=JSON.parse(this.getAttribute("opts"));e(this,r,async()=>{const s=this.getAttribute("renderer-url"),[i,{default:l}]=await Promise.all([import(this.getAttribute("component-url")),s?import(s):()=>()=>{}]);return this.Component=i[this.getAttribute("component-export")||"default"],this.hydrator=l,this.hydrate})}attributeChangedCallback(){try{this.hydrator&&this.hydrate()}catch{debugger}}},a.observedAttributes=["props"],a))}`; \ No newline at end of file +export default `var a;{const o={0:t=>t,1:t=>JSON.parse(t,n),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(JSON.parse(t,n)),5:t=>new Set(JSON.parse(t,n)),6:t=>BigInt(t),7:t=>new URL(t)},n=(t,e)=>{if(t===""||!Array.isArray(e))return e;const[r,s]=e;return r in o?o[r](s):void 0};customElements.get("astro-island")||customElements.define("astro-island",(a=class extends HTMLElement{constructor(){super(...arguments);this.hydrate=()=>{if(!this.hydrator||this.parentElement?.closest("astro-island[ssr]"))return;let e=null,r=this.querySelector("astro-fragment");if(r==null&&this.hasAttribute("tmpl")){let i=this.querySelector("template[data-astro-template]");i&&(e=i.innerHTML,i.remove())}else r&&(e=r.innerHTML);const s=this.hasAttribute("props")?JSON.parse(this.getAttribute("props"),n):{};this.hydrator(this)(this.Component,s,e,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),window.removeEventListener("astro:hydrate",this.hydrate),window.dispatchEvent(new CustomEvent("astro:hydrate"))}}async connectedCallback(){const[{default:e}]=await Promise.all([import(this.getAttribute("directive-url")),import(this.getAttribute("before-hydration-url"))]);window.addEventListener("astro:hydrate",this.hydrate);const r=JSON.parse(this.getAttribute("opts"));e(this,r,async()=>{const s=this.getAttribute("renderer-url"),[i,{default:l}]=await Promise.all([import(this.getAttribute("component-url")),s?import(s):()=>()=>{}]);return this.Component=i[this.getAttribute("component-export")||"default"],this.hydrator=l,this.hydrate})}attributeChangedCallback(){this.hydrator&&this.hydrate()}},a.observedAttributes=["props"],a))}`; \ No newline at end of file diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts index 9598be723bca..cd93854dcda7 100644 --- a/packages/astro/src/runtime/server/astro-island.ts +++ b/packages/astro/src/runtime/server/astro-island.ts @@ -77,12 +77,7 @@ window.dispatchEvent(new CustomEvent('astro:hydrate')); } attributeChangedCallback() { - try { - if(this.hydrator) this.hydrate(); - } catch( err) { - debugger; - } - + if(this.hydrator) this.hydrate(); } }); }