From 5ced5ac73a3fb2861d4ed0c3aff758d16046260f Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 29 Apr 2022 14:15:46 -0400 Subject: [PATCH 1/4] Consolidate inline hydration scripts into one --- packages/astro/src/@types/astro.ts | 1 + packages/astro/src/core/render/result.ts | 1 + packages/astro/src/runtime/client/hmr.ts | 4 +- packages/astro/src/runtime/client/idle.ts | 18 ++----- packages/astro/src/runtime/client/load.ts | 20 +++----- packages/astro/src/runtime/client/media.ts | 17 ++----- packages/astro/src/runtime/client/only.ts | 18 ++----- packages/astro/src/runtime/client/visible.ts | 29 ++++------- .../astro/src/runtime/server/astro-island.ts | 35 +++++++++++++ .../astro/src/runtime/server/hydration.ts | 51 +++++++++---------- packages/astro/src/runtime/server/index.ts | 42 ++++++++++----- packages/astro/test/0-css.test.js | 2 +- packages/astro/test/astro-client-only.test.js | 8 +-- packages/astro/test/astro-dynamic.test.js | 16 +++--- packages/astro/test/react-component.test.js | 6 +-- packages/astro/test/vue-component.test.js | 13 ++--- .../markdown/remark/src/rehype-islands.ts | 6 +-- packages/markdown/remark/src/remark-unwrap.ts | 6 +-- 18 files changed, 146 insertions(+), 147 deletions(-) create mode 100644 packages/astro/src/runtime/server/astro-island.ts diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index f2c5577de0ca..e0edc5d67a07 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1007,6 +1007,7 @@ export interface SSRElement { export interface SSRMetadata { renderers: SSRLoadedRenderer[]; pathname: string; + needsHydrationStyles: boolean; } export interface SSRResult { diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index 3a8a712357d4..27fc652d8143 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -201,6 +201,7 @@ ${extra}` _metadata: { renderers, pathname, + needsHydrationStyles: false }, }; diff --git a/packages/astro/src/runtime/client/hmr.ts b/packages/astro/src/runtime/client/hmr.ts index 7cd7732569df..cefb3e06891c 100644 --- a/packages/astro/src/runtime/client/hmr.ts +++ b/packages/astro/src/runtime/client/hmr.ts @@ -7,9 +7,9 @@ if (import.meta.hot) { const doc = parser.parseFromString(html, 'text/html'); // Match incoming islands to current state - for (const root of doc.querySelectorAll('astro-root')) { + for (const root of doc.querySelectorAll('astro-island')) { const uid = root.getAttribute('uid'); - const current = document.querySelector(`astro-root[uid="${uid}"]`); + const current = document.querySelector(`astro-island[uid="${uid}"]`); if (current) { root.innerHTML = current?.innerHTML; } diff --git a/packages/astro/src/runtime/client/idle.ts b/packages/astro/src/runtime/client/idle.ts index 627d896db75f..d0b3f29a9e77 100644 --- a/packages/astro/src/runtime/client/idle.ts +++ b/packages/astro/src/runtime/client/idle.ts @@ -5,22 +5,17 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; * (or after a short delay, if `requestIdleCallback`) isn't supported */ export default async function onIdle( - astroId: string, + root: HTMLElement, options: HydrateOptions, getHydrateCallback: GetHydrateCallback ) { const cb = async () => { - const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`); - if (roots.length === 0) { - throw new Error(`Unable to find the root for the component ${options.name}`); - } - let innerHTML: string | null = null; - let fragment = roots[0].querySelector(`astro-fragment`); - if (fragment == null && roots[0].hasAttribute('tmpl')) { + let fragment = root.querySelector(`astro-fragment`); + if (fragment == null && root.hasAttribute('tmpl')) { // If there is no child fragment, check to see if there is a template. // This happens if children were passed but the client component did not render any. - let template = roots[0].querySelector(`template[data-astro-template]`); + let template = root.querySelector(`template[data-astro-template]`); if (template) { innerHTML = template.innerHTML; template.remove(); @@ -29,10 +24,7 @@ export default async function onIdle( innerHTML = fragment.innerHTML; } const hydrate = await getHydrateCallback(); - - for (const root of roots) { - hydrate(root, innerHTML); - } + hydrate(root, innerHTML); }; if ('requestIdleCallback' in window) { diff --git a/packages/astro/src/runtime/client/load.ts b/packages/astro/src/runtime/client/load.ts index cf4cd83af430..cba255e1d05d 100644 --- a/packages/astro/src/runtime/client/load.ts +++ b/packages/astro/src/runtime/client/load.ts @@ -4,21 +4,16 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; * Hydrate this component immediately */ export default async function onLoad( - astroId: string, + root: HTMLElement, options: HydrateOptions, getHydrateCallback: GetHydrateCallback ) { - const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`); - if (roots.length === 0) { - throw new Error(`Unable to find the root for the component ${options.name}`); - } - let innerHTML: string | null = null; - let fragment = roots[0].querySelector(`astro-fragment`); - if (fragment == null && roots[0].hasAttribute('tmpl')) { + let fragment = root.querySelector(`astro-fragment`); + if (fragment == null && root.hasAttribute('tmpl')) { // If there is no child fragment, check to see if there is a template. // This happens if children were passed but the client component did not render any. - let template = roots[0].querySelector(`template[data-astro-template]`); + let template = root.querySelector(`template[data-astro-template]`); if (template) { innerHTML = template.innerHTML; template.remove(); @@ -27,10 +22,7 @@ export default async function onLoad( innerHTML = fragment.innerHTML; } - //const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null; + //const innerHTML = root.querySelector(`astro-fragment`)?.innerHTML ?? null; const hydrate = await getHydrateCallback(); - - for (const root of roots) { - hydrate(root, innerHTML); - } + hydrate(root, innerHTML); } diff --git a/packages/astro/src/runtime/client/media.ts b/packages/astro/src/runtime/client/media.ts index 32e8839089e2..56b8dedf3eb5 100644 --- a/packages/astro/src/runtime/client/media.ts +++ b/packages/astro/src/runtime/client/media.ts @@ -4,21 +4,16 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; * Hydrate this component when a matching media query is found */ export default async function onMedia( - astroId: string, + root: HTMLElement, options: HydrateOptions, getHydrateCallback: GetHydrateCallback ) { - const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`); - if (roots.length === 0) { - throw new Error(`Unable to find the root for the component ${options.name}`); - } - let innerHTML: string | null = null; - let fragment = roots[0].querySelector(`astro-fragment`); - if (fragment == null && roots[0].hasAttribute('tmpl')) { + let fragment = root.querySelector(`astro-fragment`); + if (fragment == null && root.hasAttribute('tmpl')) { // If there is no child fragment, check to see if there is a template. // This happens if children were passed but the client component did not render any. - let template = roots[0].querySelector(`template[data-astro-template]`); + let template = root.querySelector(`template[data-astro-template]`); if (template) { innerHTML = template.innerHTML; template.remove(); @@ -29,9 +24,7 @@ export default async function onMedia( const cb = async () => { const hydrate = await getHydrateCallback(); - for (const root of roots) { - hydrate(root, innerHTML); - } + hydrate(root, innerHTML); }; if (options.value) { diff --git a/packages/astro/src/runtime/client/only.ts b/packages/astro/src/runtime/client/only.ts index 6400d44b855c..8fe3ac726032 100644 --- a/packages/astro/src/runtime/client/only.ts +++ b/packages/astro/src/runtime/client/only.ts @@ -4,21 +4,16 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; * Hydrate this component immediately */ export default async function onLoad( - astroId: string, + root: HTMLElement, options: HydrateOptions, getHydrateCallback: GetHydrateCallback ) { - const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`); - if (roots.length === 0) { - throw new Error(`Unable to find the root for the component ${options.name}`); - } - let innerHTML: string | null = null; - let fragment = roots[0].querySelector(`astro-fragment`); - if (fragment == null && roots[0].hasAttribute('tmpl')) { + let fragment = root.querySelector(`astro-fragment`); + if (fragment == null && root.hasAttribute('tmpl')) { // If there is no child fragment, check to see if there is a template. // This happens if children were passed but the client component did not render any. - let template = roots[0].querySelector(`template[data-astro-template]`); + let template = root.querySelector(`template[data-astro-template]`); if (template) { innerHTML = template.innerHTML; template.remove(); @@ -27,8 +22,5 @@ export default async function onLoad( innerHTML = fragment.innerHTML; } const hydrate = await getHydrateCallback(); - - for (const root of roots) { - hydrate(root, innerHTML); - } + hydrate(root, innerHTML); } diff --git a/packages/astro/src/runtime/client/visible.ts b/packages/astro/src/runtime/client/visible.ts index e0c1fdc73ad1..70570865fe40 100644 --- a/packages/astro/src/runtime/client/visible.ts +++ b/packages/astro/src/runtime/client/visible.ts @@ -2,25 +2,20 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; /** * Hydrate this component when one of it's children becomes visible. - * We target the children because `astro-root` is set to `display: contents` + * We target the children because `astro-island` is set to `display: contents` * which doesn't work with IntersectionObserver */ export default async function onVisible( - astroId: string, + root: HTMLElement, options: HydrateOptions, getHydrateCallback: GetHydrateCallback ) { - const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`); - if (roots.length === 0) { - throw new Error(`Unable to find the root for the component ${options.name}`); - } - let innerHTML: string | null = null; - let fragment = roots[0].querySelector(`astro-fragment`); - if (fragment == null && roots[0].hasAttribute('tmpl')) { + let fragment = root.querySelector(`astro-fragment`); + if (fragment == null && root.hasAttribute('tmpl')) { // If there is no child fragment, check to see if there is a template. // This happens if children were passed but the client component did not render any. - let template = roots[0].querySelector(`template[data-astro-template]`); + let template = root.querySelector(`template[data-astro-template]`); if (template) { innerHTML = template.innerHTML; template.remove(); @@ -31,25 +26,21 @@ export default async function onVisible( const cb = async () => { const hydrate = await getHydrateCallback(); - for (const root of roots) { - hydrate(root, innerHTML); - } + hydrate(root, innerHTML); }; const io = new IntersectionObserver((entries) => { for (const entry of entries) { if (!entry.isIntersecting) continue; - // As soon as we hydrate, disconnect this IntersectionObserver for every `astro-root` + // As soon as we hydrate, disconnect this IntersectionObserver for every `astro-island` io.disconnect(); cb(); break; // break loop on first match } }); - for (const root of roots) { - for (let i = 0; i < root.children.length; i++) { - const child = root.children[i]; - io.observe(child); - } + for (let i = 0; i < root.children.length; i++) { + const child = root.children[i]; + io.observe(child); } } 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..3eadeb1f5a1b --- /dev/null +++ b/packages/astro/src/runtime/server/astro-island.ts @@ -0,0 +1,35 @@ +/* +customElements.define('astro-island', class extends HTMLElement { + async connectedCallback(){ + const [ { default: setup } ] = await Promise.all([ + import(this.getAttribute('directive-url')), + import(this.getAttribute('before-hydration-url')) + ]); + + const opts = JSON.parse(this.getAttribute('opts')); + setup(this, opts, async () => { + const propsStr = this.getAttribute('props'); + const props = propsStr ? JSON.parse(propsStr) : {}; + const rendererUrl = this.getAttribute('renderer-url'); + const [ + { default: Component }, + { default: hydrate } + ] = await Promise.all([ + import(this.getAttribute('component-url')), + rendererUrl ? import(rendererUrl) : () => () => {} + ]); + + return (el, children) => hydrate(el)(Component, props, children); + }); + } +}); +*/ + +/** + * This is a minified version of the above. If you modify the above you need to + * copy/paste it into a .js file and then run: + * > node_modules/.bin/terser --mangle --compress -- file.js + * + * And copy/paste the result below + */ +export const islandScript = `customElements.define("astro-island",class extends HTMLElement{async connectedCallback(){const[{default:t}]=await Promise.all([import(this.getAttribute("directive-url")),import(this.getAttribute("before-hydration-url"))]);const e=JSON.parse(this.getAttribute("opts"));t(this,e,(async()=>{const t=this.getAttribute("props");const e=t?JSON.parse(t):{};const r=this.getAttribute("renderer-url");const[{default:s},{default:i}]=await Promise.all([import(this.getAttribute("component-url")),r?import(r):()=>()=>{}]);return(t,r)=>i(t)(s,e,r)}))}});`; diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts index e7267fe16e3c..f29379b10679 100644 --- a/packages/astro/src/runtime/server/hydration.ts +++ b/packages/astro/src/runtime/server/hydration.ts @@ -1,8 +1,10 @@ import type { AstroComponentMetadata, SSRLoadedRenderer } from '../../@types/astro'; import type { SSRElement, SSRResult } from '../../@types/astro'; import { hydrationSpecifier, serializeListValue } from './util.js'; +import { escapeHTML } from './escape.js'; import serializeJavaScript from 'serialize-javascript'; + // Serializes props passed into a component so that they can be reused during hydration. // The value is any export function serializeProps(value: any) { @@ -110,32 +112,29 @@ export async function generateHydrateScript( ); } - let hydrationSource = ``; - - hydrationSource += renderer.clientEntrypoint - ? `const [{ ${ - componentExport.value - }: Component }, { default: hydrate }] = await Promise.all([import("${await result.resolve( - componentUrl - )}"), import("${await result.resolve(renderer.clientEntrypoint)}")]); - return (el, children) => hydrate(el)(Component, ${serializeProps(props)}, children); -` - : `await import("${await result.resolve(componentUrl)}"); - return () => {}; -`; - // TODO: If we can figure out tree-shaking in the final SSR build, we could safely - // use BEFORE_HYDRATION_SCRIPT_ID instead of 'astro:scripts/before-hydration.js'. - const hydrationScript = { - props: { type: 'module', 'data-astro-component-hydration': true }, - children: `import setup from '${await result.resolve(hydrationSpecifier(hydrate))}'; -${`import '${await result.resolve('astro:scripts/before-hydration.js')}';`} -setup("${astroId}", {name:"${metadata.displayName}",${ - metadata.hydrateArgs ? `value: ${JSON.stringify(metadata.hydrateArgs)}` : '' - }}, async () => { - ${hydrationSource} -}); -`, + const island: SSRElement = { + children: '', + props: { + // This is for HMR, probably can avoid it in prod + uid: astroId + } }; - return hydrationScript; + // Add component url + island.props['component-url'] = await result.resolve(componentUrl); + + // Add renderer url + if(renderer.clientEntrypoint) { + island.props['renderer-url'] = await result.resolve(renderer.clientEntrypoint); + island.props['props'] = escapeHTML(serializeProps(props)); + } + + island.props['directive-url'] = await result.resolve(hydrationSpecifier(hydrate)); + island.props['before-hydration-url'] = await result.resolve('astro:scripts/before-hydration.js'); + island.props['opts'] = escapeHTML(JSON.stringify({ + name: metadata.displayName, + value: metadata.hydrateArgs || '' + })) + + return island; } diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts index bc80e5cdbed2..435ba1227e77 100644 --- a/packages/astro/src/runtime/server/index.ts +++ b/packages/astro/src/runtime/server/index.ts @@ -11,6 +11,7 @@ import type { import { escapeHTML, HTMLString, markHTMLString } from './escape.js'; import { extractDirectives, generateHydrateScript, serializeProps } from './hydration.js'; import { serializeListValue } from './util.js'; +import { islandScript } from './astro-island.js'; export { markHTMLString, markHTMLString as unescapeHTML } from './escape.js'; export type { Metadata } from './metadata'; @@ -24,6 +25,8 @@ const htmlEnumAttributes = /^(contenteditable|draggable|spellcheck|value)$/i; // Note: SVG is case-sensitive! const svgEnumAttributes = /^(autoReverse|externalResourcesRequired|focusable|preserveAlpha)$/i; +const hydrationedRenders = new WeakSet(); + // INVESTIGATE: // 2. Less anys when possible and make it well known when they are needed. @@ -308,20 +311,36 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr // Rather than appending this inline in the page, puts this into the `result.scripts` set that will be appended to the head. // INVESTIGATE: This will likely be a problem in streaming because the `` will be gone at this point. - result.scripts.add( - await generateHydrateScript( - { renderer: renderer!, result, astroId, props }, - metadata as Required - ) + const island = await generateHydrateScript( + { renderer: renderer!, result, astroId, props }, + metadata as Required ); + result._metadata.needsHydrationStyles = true; // Render a template if no fragment is provided. const needsAstroTemplate = children && !/<\/?astro-fragment\>/.test(html); const template = needsAstroTemplate ? `` : ''; + + if(needsAstroTemplate) { + island.props.tmpl = ''; + } + + island.children = `${ + html ?? '' + }${template}`; + + // Add the astro-island definition only once. Since the SSRResult object + // is scoped to a page renderer we can use it as a key to know if the script + // has been rendered or not. + let script = ''; + if(!hydrationedRenders.has(result)) { + hydrationedRenders.add(result); + script = ``; + } + return markHTMLString( - `${ - html ?? '' - }${template}` + script + + renderElement('astro-island', island, false) ); } @@ -544,13 +563,10 @@ const uniqueElements = (item: any, index: number, all: any[]) => { // styles and scripts into the head. export async function renderHead(result: SSRResult): Promise { const styles = []; - let needsHydrationStyles = false; + let needsHydrationStyles = result._metadata.needsHydrationStyles; const scripts = Array.from(result.scripts) .filter(uniqueElements) .map((script, i) => { - if ('data-astro-component-hydration' in script.props) { - needsHydrationStyles = true; - } return renderElement('script', { ...script, props: { ...script.props, 'astro-script': result._metadata.pathname + '/script-' + i }, @@ -560,7 +576,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/test/0-css.test.js b/packages/astro/test/0-css.test.js index 65efdeba298a..8a6130429f99 100644 --- a/packages/astro/test/0-css.test.js +++ b/packages/astro/test/0-css.test.js @@ -56,7 +56,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 5e2cc6ce85a3..7a096453f7dd 100644 --- a/packages/astro/test/astro-client-only.test.js +++ b/packages/astro/test/astro-client-only.test.js @@ -16,13 +16,13 @@ describe('Client only components', () => { const html = await fixture.readFile('/index.html'); const $ = cheerioLoad(html); - // test 1: is empty - expect($('astro-root').html()).to.equal(''); + // test 1: is empty + expect($('astro-island').html()).to.equal(''); const $script = $('script'); const script = $script.html(); - // test 2: svelte renderer is on the page - expect(/import\(".\/entry.*/g.test(script)).to.be.ok; + // Has the renderer URL for svelte + expect($('astro-island').attr('renderer-url').length).to.be.greaterThan(0); }); 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 1b8b323ee6a9..ed1e9df08b4e 100644 --- a/packages/astro/test/astro-dynamic.test.js +++ b/packages/astro/test/astro-dynamic.test.js @@ -16,27 +16,25 @@ 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 () => { - 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); // One overall }); 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(''); + + // Has the directive URL + expect($('astro-island').attr('directive-url').length).to.be.greaterThan(0); }); }); diff --git a/packages/astro/test/react-component.test.js b/packages/astro/test/react-component.test.js index 749fc0c16575..ee984e756f80 100644 --- a/packages/astro/test/react-component.test.js +++ b/packages/astro/test/react-component.test.js @@ -42,11 +42,7 @@ describe('React Components', () => { expect($('#pure')).to.have.lengthOf(1); // test 8: Check number of islands - expect($('astro-root[uid]')).to.have.lengthOf(5); - - // test 9: Check island deduplication - const uniqueRootUIDs = new Set($('astro-root').map((i, el) => $(el).attr('uid'))); - expect(uniqueRootUIDs.size).to.equal(4); + expect($('astro-island')).to.have.lengthOf(5); }); it('Can load Vue', async () => { diff --git a/packages/astro/test/vue-component.test.js b/packages/astro/test/vue-component.test.js index 3c57c65447b9..5fd3885baa39 100644 --- a/packages/astro/test/vue-component.test.js +++ b/packages/astro/test/vue-component.test.js @@ -27,18 +27,11 @@ 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 4: treats as a custom element + // test 3: 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')); - expect(new Set(uniqueRootUIDs).size).to.equal(5); }); }); 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 3ce7a72c0ca2..05f16fbee60c 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 2646cbfc43960dd7122795671cd757aaad4b0d0d Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sat, 30 Apr 2022 07:30:52 -0400 Subject: [PATCH 2/4] Adds changeset --- .changeset/eleven-toes-attack.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eleven-toes-attack.md diff --git a/.changeset/eleven-toes-attack.md b/.changeset/eleven-toes-attack.md new file mode 100644 index 000000000000..f17f30474ff6 --- /dev/null +++ b/.changeset/eleven-toes-attack.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Consolidates hydration scripts into one From 6af3399f50d6e6d20c525102b7cfba6dc8b9eb51 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sat, 30 Apr 2022 07:40:05 -0400 Subject: [PATCH 3/4] Update custom element test --- packages/astro/test/custom-elements.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 () => { From 71f3694f6cc17a64deadb24fa5503296d381fd9a Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 3 May 2022 08:47:54 -0400 Subject: [PATCH 4/4] Provide a better name for tracking if we have added a hydration script --- packages/astro/src/runtime/server/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts index 786ce63af7ae..69c97144b6ce 100644 --- a/packages/astro/src/runtime/server/index.ts +++ b/packages/astro/src/runtime/server/index.ts @@ -25,7 +25,7 @@ const htmlEnumAttributes = /^(contenteditable|draggable|spellcheck|value)$/i; // Note: SVG is case-sensitive! const svgEnumAttributes = /^(autoReverse|externalResourcesRequired|focusable|preserveAlpha)$/i; -const hydrationedRenders = new WeakSet(); +const resultsWithHydrationScript = new WeakSet(); // INVESTIGATE: // 2. Less anys when possible and make it well known when they are needed. @@ -333,8 +333,8 @@ 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(!hydrationedRenders.has(result)) { - hydrationedRenders.add(result); + if(!resultsWithHydrationScript.has(result)) { + resultsWithHydrationScript.add(result); script = ``; }