summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Matthew Phillips <matthew@skypack.dev> 2022-05-03 15:18:17 -0400
committerGravatar GitHub <noreply@github.com> 2022-05-03 15:18:17 -0400
commit8f8f05c1b99d073a43af3020ba3922ea2d5b466d (patch)
treeb88696399800926e9dc64eec62db18f0dc8e7dde
parent59f07e8dd5736037162f7c839f5701f42e69f10c (diff)
downloadastro-8f8f05c1b99d073a43af3020ba3922ea2d5b466d.tar.gz
astro-8f8f05c1b99d073a43af3020ba3922ea2d5b466d.tar.zst
astro-8f8f05c1b99d073a43af3020ba3922ea2d5b466d.zip
Revert "Consolidate inline hydration scripts into one (#3244)" (#3275)
* Revert "Consolidate inline hydration scripts into one (#3244)" This reverts commit 48a35e6042a6634c836ec333d18801e9d603b328. * Fix types * Adds changeset
-rw-r--r--.changeset/fresh-wasps-smash.md6
-rw-r--r--packages/astro/src/@types/astro.ts1
-rw-r--r--packages/astro/src/core/render/result.ts1
-rw-r--r--packages/astro/src/runtime/client/hmr.ts4
-rw-r--r--packages/astro/src/runtime/client/idle.ts18
-rw-r--r--packages/astro/src/runtime/client/load.ts20
-rw-r--r--packages/astro/src/runtime/client/media.ts17
-rw-r--r--packages/astro/src/runtime/client/only.ts18
-rw-r--r--packages/astro/src/runtime/client/visible.ts29
-rw-r--r--packages/astro/src/runtime/server/astro-island.ts35
-rw-r--r--packages/astro/src/runtime/server/hydration.ts52
-rw-r--r--packages/astro/src/runtime/server/index.ts40
-rw-r--r--packages/astro/test/0-css.test.js2
-rw-r--r--packages/astro/test/astro-client-only.test.js8
-rw-r--r--packages/astro/test/astro-dynamic.test.js16
-rw-r--r--packages/astro/test/custom-elements.test.js2
-rw-r--r--packages/astro/test/react-component.test.js6
-rw-r--r--packages/astro/test/vue-component.test.js13
-rw-r--r--packages/markdown/remark/src/rehype-islands.ts6
-rw-r--r--packages/markdown/remark/src/remark-unwrap.ts6
20 files changed, 156 insertions, 144 deletions
diff --git a/.changeset/fresh-wasps-smash.md b/.changeset/fresh-wasps-smash.md
new file mode 100644
index 000000000..3c6aef9b3
--- /dev/null
+++ b/.changeset/fresh-wasps-smash.md
@@ -0,0 +1,6 @@
+---
+'astro': patch
+'@astrojs/markdown-remark': patch
+---
+
+Fixes regression in passing JS args to islands
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 0ecc7e414..4a19d0443 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -1002,7 +1002,6 @@ 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 1bee4ab77..3a8a71235 100644
--- a/packages/astro/src/core/render/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -201,7 +201,6 @@ ${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 cefb3e068..7cd773256 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-island')) {
+ for (const root of doc.querySelectorAll('astro-root')) {
const uid = root.getAttribute('uid');
- const current = document.querySelector(`astro-island[uid="${uid}"]`);
+ const current = document.querySelector(`astro-root[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 d0b3f29a9..627d896db 100644
--- a/packages/astro/src/runtime/client/idle.ts
+++ b/packages/astro/src/runtime/client/idle.ts
@@ -5,17 +5,22 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
* (or after a short delay, if `requestIdleCallback`) isn't supported
*/
export default async function onIdle(
- root: HTMLElement,
+ astroId: string,
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 = root.querySelector(`astro-fragment`);
- if (fragment == null && root.hasAttribute('tmpl')) {
+ let fragment = roots[0].querySelector(`astro-fragment`);
+ if (fragment == null && roots[0].hasAttribute('tmpl')) {
// If there is no child fragment, check to see if there is a template.
// This happens if children were passed but the client component did not render any.
- let template = root.querySelector(`template[data-astro-template]`);
+ let template = roots[0].querySelector(`template[data-astro-template]`);
if (template) {
innerHTML = template.innerHTML;
template.remove();
@@ -24,7 +29,10 @@ export default async function onIdle(
innerHTML = fragment.innerHTML;
}
const hydrate = await getHydrateCallback();
- hydrate(root, innerHTML);
+
+ for (const root of roots) {
+ 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 cba255e1d..cf4cd83af 100644
--- a/packages/astro/src/runtime/client/load.ts
+++ b/packages/astro/src/runtime/client/load.ts
@@ -4,16 +4,21 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
* Hydrate this component immediately
*/
export default async function onLoad(
- root: HTMLElement,
+ astroId: string,
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 = root.querySelector(`astro-fragment`);
- if (fragment == null && root.hasAttribute('tmpl')) {
+ let fragment = roots[0].querySelector(`astro-fragment`);
+ if (fragment == null && roots[0].hasAttribute('tmpl')) {
// If there is no child fragment, check to see if there is a template.
// This happens if children were passed but the client component did not render any.
- let template = root.querySelector(`template[data-astro-template]`);
+ let template = roots[0].querySelector(`template[data-astro-template]`);
if (template) {
innerHTML = template.innerHTML;
template.remove();
@@ -22,7 +27,10 @@ export default async function onLoad(
innerHTML = fragment.innerHTML;
}
- //const innerHTML = root.querySelector(`astro-fragment`)?.innerHTML ?? null;
+ //const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null;
const hydrate = await getHydrateCallback();
- hydrate(root, innerHTML);
+
+ for (const root of roots) {
+ hydrate(root, innerHTML);
+ }
}
diff --git a/packages/astro/src/runtime/client/media.ts b/packages/astro/src/runtime/client/media.ts
index 56b8dedf3..32e883908 100644
--- a/packages/astro/src/runtime/client/media.ts
+++ b/packages/astro/src/runtime/client/media.ts
@@ -4,16 +4,21 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
* Hydrate this component when a matching media query is found
*/
export default async function onMedia(
- root: HTMLElement,
+ astroId: string,
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 = root.querySelector(`astro-fragment`);
- if (fragment == null && root.hasAttribute('tmpl')) {
+ let fragment = roots[0].querySelector(`astro-fragment`);
+ if (fragment == null && roots[0].hasAttribute('tmpl')) {
// If there is no child fragment, check to see if there is a template.
// This happens if children were passed but the client component did not render any.
- let template = root.querySelector(`template[data-astro-template]`);
+ let template = roots[0].querySelector(`template[data-astro-template]`);
if (template) {
innerHTML = template.innerHTML;
template.remove();
@@ -24,7 +29,9 @@ export default async function onMedia(
const cb = async () => {
const hydrate = await getHydrateCallback();
- hydrate(root, innerHTML);
+ for (const root of roots) {
+ 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 8fe3ac726..6400d44b8 100644
--- a/packages/astro/src/runtime/client/only.ts
+++ b/packages/astro/src/runtime/client/only.ts
@@ -4,16 +4,21 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
* Hydrate this component immediately
*/
export default async function onLoad(
- root: HTMLElement,
+ astroId: string,
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 = root.querySelector(`astro-fragment`);
- if (fragment == null && root.hasAttribute('tmpl')) {
+ let fragment = roots[0].querySelector(`astro-fragment`);
+ if (fragment == null && roots[0].hasAttribute('tmpl')) {
// If there is no child fragment, check to see if there is a template.
// This happens if children were passed but the client component did not render any.
- let template = root.querySelector(`template[data-astro-template]`);
+ let template = roots[0].querySelector(`template[data-astro-template]`);
if (template) {
innerHTML = template.innerHTML;
template.remove();
@@ -22,5 +27,8 @@ export default async function onLoad(
innerHTML = fragment.innerHTML;
}
const hydrate = await getHydrateCallback();
- hydrate(root, innerHTML);
+
+ for (const root of roots) {
+ hydrate(root, innerHTML);
+ }
}
diff --git a/packages/astro/src/runtime/client/visible.ts b/packages/astro/src/runtime/client/visible.ts
index 70570865f..e0c1fdc73 100644
--- a/packages/astro/src/runtime/client/visible.ts
+++ b/packages/astro/src/runtime/client/visible.ts
@@ -2,20 +2,25 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
/**
* Hydrate this component when one of it's children becomes visible.
- * We target the children because `astro-island` is set to `display: contents`
+ * We target the children because `astro-root` is set to `display: contents`
* which doesn't work with IntersectionObserver
*/
export default async function onVisible(
- root: HTMLElement,
+ astroId: string,
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 = root.querySelector(`astro-fragment`);
- if (fragment == null && root.hasAttribute('tmpl')) {
+ let fragment = roots[0].querySelector(`astro-fragment`);
+ if (fragment == null && roots[0].hasAttribute('tmpl')) {
// If there is no child fragment, check to see if there is a template.
// This happens if children were passed but the client component did not render any.
- let template = root.querySelector(`template[data-astro-template]`);
+ let template = roots[0].querySelector(`template[data-astro-template]`);
if (template) {
innerHTML = template.innerHTML;
template.remove();
@@ -26,21 +31,25 @@ export default async function onVisible(
const cb = async () => {
const hydrate = await getHydrateCallback();
- hydrate(root, innerHTML);
+ for (const root of roots) {
+ 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-island`
+ // As soon as we hydrate, disconnect this IntersectionObserver for every `astro-root`
io.disconnect();
cb();
break; // break loop on first match
}
});
- for (let i = 0; i < root.children.length; i++) {
- const child = root.children[i];
- io.observe(child);
+ for (const root of roots) {
+ 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
deleted file mode 100644
index 8cb573cf8..000000000
--- a/packages/astro/src/runtime/server/astro-island.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
-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 04d678c3f..e7267fe16 100644
--- a/packages/astro/src/runtime/server/hydration.ts
+++ b/packages/astro/src/runtime/server/hydration.ts
@@ -1,7 +1,6 @@
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.
@@ -111,31 +110,32 @@ export async function generateHydrateScript(
);
}
- const island: SSRElement = {
- children: '',
- props: {
- // This is for HMR, probably can avoid it in prod
- uid: astroId,
- },
+ 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}
+});
+`,
};
- // 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;
+ return hydrationScript;
}
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index aa2d1d574..f888a4852 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -11,7 +11,6 @@ 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';
@@ -25,8 +24,6 @@ const htmlEnumAttributes = /^(contenteditable|draggable|spellcheck|value)$/i;
// Note: SVG is case-sensitive!
const svgEnumAttributes = /^(autoReverse|externalResourcesRequired|focusable|preserveAlpha)$/i;
-const resultsWithHydrationScript = new WeakSet<SSRResult>();
-
// INVESTIGATE:
// 2. Less anys when possible and make it well known when they are needed.
@@ -311,32 +308,22 @@ 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 `<head>` will be gone at this point.
- const island = await generateHydrateScript(
- { renderer: renderer!, result, astroId, props },
- metadata as Required<AstroComponentMetadata>
+ result.scripts.add(
+ await generateHydrateScript(
+ { renderer: renderer!, result, astroId, props },
+ metadata as Required<AstroComponentMetadata>
+ )
);
- result._metadata.needsHydrationStyles = true;
// Render a template if no fragment is provided.
const needsAstroTemplate = children && !/<\/?astro-fragment\>/.test(html);
const template = needsAstroTemplate ? `<template data-astro-template>${children}</template>` : '';
- 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 (!resultsWithHydrationScript.has(result)) {
- resultsWithHydrationScript.add(result);
- script = `<script>${islandScript}</script>`;
- }
-
- return markHTMLString(script + renderElement('astro-island', island, false));
+ return markHTMLString(
+ `<astro-root uid="${astroId}"${needsAstroTemplate ? ' tmpl' : ''}>${
+ html ?? ''
+ }${template}</astro-root>`
+ );
}
/** Create the Astro.fetchContent() runtime function. */
@@ -560,10 +547,13 @@ export async function renderHead(result: SSRResult): Promise<string> {
const styles = Array.from(result.styles)
.filter(uniqueElements)
.map((style) => renderElement('style', style));
- let needsHydrationStyles = result._metadata.needsHydrationStyles;
+ let needsHydrationStyles = false;
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 },
@@ -573,7 +563,7 @@ export async function renderHead(result: SSRResult): Promise<string> {
styles.push(
renderElement('style', {
props: {},
- children: 'astro-island, astro-fragment { display: contents; }',
+ children: 'astro-root, astro-fragment { display: contents; }',
})
);
}
diff --git a/packages/astro/test/0-css.test.js b/packages/astro/test/0-css.test.js
index 8a6130429..65efdeba2 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-island styles', async () => {
+ it('Using hydrated components adds astro-root 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 7a096453f..5e2cc6ce8 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: <astro-island> is empty
- expect($('astro-island').html()).to.equal('');
+ // test 1: <astro-root> is empty
+ expect($('astro-root').html()).to.equal('');
const $script = $('script');
const script = $script.html();
- // Has the renderer URL for svelte
- expect($('astro-island').attr('renderer-url').length).to.be.greaterThan(0);
+ // test 2: svelte renderer is on the page
+ expect(/import\(".\/entry.*/g.test(script)).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 ed1e9df08..1b8b323ee 100644
--- a/packages/astro/test/astro-dynamic.test.js
+++ b/packages/astro/test/astro-dynamic.test.js
@@ -16,25 +16,27 @@ describe('Dynamic components', () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
- expect($('script').length).to.eq(1);
+ expect($('script').length).to.eq(2);
});
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(1); // One overall
+ expect($('script').length).to.equal(2); // One for each
});
it('Loads pages using client:only hydrator', async () => {
const html = await fixture.readFile('/client-only/index.html');
const $ = cheerio.load(html);
- // test 1: <astro-island> is empty.
- expect($('astro-island').html()).to.equal('');
-
- // Has the directive URL
- expect($('astro-island').attr('directive-url').length).to.be.greaterThan(0);
+ // test 1: <astro-root> is empty.
+ expect($('<astro-root>').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`);
});
});
diff --git a/packages/astro/test/custom-elements.test.js b/packages/astro/test/custom-elements.test.js
index 0a380026f..a00ea6887 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')).to.have.lengthOf(2);
+ expect($('script[type=module]')).to.have.lengthOf(1);
});
it('Custom elements not claimed by renderer are rendered as regular HTML', async () => {
diff --git a/packages/astro/test/react-component.test.js b/packages/astro/test/react-component.test.js
index ee984e756..749fc0c16 100644
--- a/packages/astro/test/react-component.test.js
+++ b/packages/astro/test/react-component.test.js
@@ -42,7 +42,11 @@ describe('React Components', () => {
expect($('#pure')).to.have.lengthOf(1);
// test 8: Check number of islands
- expect($('astro-island')).to.have.lengthOf(5);
+ 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);
});
it('Can load Vue', async () => {
diff --git a/packages/astro/test/vue-component.test.js b/packages/astro/test/vue-component.test.js
index 5fd3885ba..3c57c6544 100644
--- a/packages/astro/test/vue-component.test.js
+++ b/packages/astro/test/vue-component.test.js
@@ -27,11 +27,18 @@ 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 <astro-islands>s
- expect($('astro-island')).to.have.lengthOf(6);
+ // test 2: renders 3 <astro-root>s
+ expect($('astro-root')).to.have.lengthOf(6);
- // test 3: treats <my-button> as a custom element
+ // test 3: all <astro-root>s have uid attributes
+ expect($('astro-root[uid]')).to.have.lengthOf(6);
+
+ // test 4: treats <my-button> 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 a8b78848d..bbd584792 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 <astro-island>
+// `unist`/`remark`/`rehype` (not sure) often generate malformed HTML inside of <astro-root>
// 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 <astro-island> islands
- if (el.tagName == 'astro-island') {
+ // Bugs only happen inside of <astro-root> islands
+ if (el.tagName == 'astro-root') {
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 05f16fbee..3ce7a72c0 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 <astro-island> islands
+// Remove the wrapping paragraph for <astro-root> 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('<astro-island') > -1 && !insideAstroRoot) {
+ if (node.value.indexOf('<astro-root') > -1 && !insideAstroRoot) {
insideAstroRoot = true;
}
- if (node.value.indexOf('</astro-island') > -1 && insideAstroRoot) {
+ if (node.value.indexOf('</astro-root') > -1 && insideAstroRoot) {
insideAstroRoot = false;
}
astroRootNodes.add(node);