Compare commits
21 commits
Author | SHA1 | Date | |
---|---|---|---|
|
e4b4871b20 | ||
|
5c0f464df3 | ||
|
acb9d446c8 | ||
|
d98c6a322b | ||
|
671fc28b93 | ||
|
57a6f5b3b4 | ||
|
9d48d35db0 | ||
|
1ba5e40a2c | ||
|
57bac129cc | ||
|
8cb00a73fa | ||
|
4912cc600b | ||
|
5628e62194 | ||
|
c70396e72d | ||
|
9ee36d8ff9 | ||
|
896ca32d53 | ||
|
f744d2bb76 | ||
|
a037015a5a | ||
|
e5aaea52b3 | ||
|
8ff81c7e20 | ||
|
f177cf4f38 | ||
|
1c719ff4fb |
22 changed files with 175 additions and 394 deletions
9
.changeset/grumpy-chairs-leave.md
Normal file
9
.changeset/grumpy-chairs-leave.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
'astro': major
|
||||||
|
---
|
||||||
|
|
||||||
|
Adjust `astro-island` serialization approach.
|
||||||
|
|
||||||
|
Previously, Astro had custom serialization logic that created a large HTML attribute and came with a deserialization cost on the client. Now, component props are serialized using [`seroval`](https://github.com/lxsmnsyc/seroval) and injected as a `<script>`.
|
||||||
|
|
||||||
|
This should significantly reduce the size and runtime cost of large data objects.
|
|
@ -1,26 +0,0 @@
|
||||||
import { expect } from '@playwright/test';
|
|
||||||
import { testFactory, getErrorOverlayContent } from './test-utils.js';
|
|
||||||
|
|
||||||
const test = testFactory({
|
|
||||||
root: './fixtures/error-cyclic/',
|
|
||||||
});
|
|
||||||
|
|
||||||
let devServer;
|
|
||||||
|
|
||||||
test.beforeEach(async ({ astro }) => {
|
|
||||||
devServer = await astro.startDevServer();
|
|
||||||
});
|
|
||||||
|
|
||||||
test.afterEach(async ({ astro }) => {
|
|
||||||
await devServer.stop();
|
|
||||||
astro.resetAllFiles();
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Error: Cyclic Reference', () => {
|
|
||||||
test('overlay', async ({ page, astro }) => {
|
|
||||||
await page.goto(astro.resolveUrl('/'));
|
|
||||||
|
|
||||||
const message = (await getErrorOverlayContent(page)).message;
|
|
||||||
expect(message).toMatch('Cyclic reference');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -160,6 +160,7 @@
|
||||||
"prompts": "^2.4.2",
|
"prompts": "^2.4.2",
|
||||||
"rehype": "^12.0.1",
|
"rehype": "^12.0.1",
|
||||||
"semver": "^7.5.3",
|
"semver": "^7.5.3",
|
||||||
|
"seroval": "^0.9.0",
|
||||||
"server-destroy": "^1.0.1",
|
"server-destroy": "^1.0.1",
|
||||||
"sharp": "^0.32.1",
|
"sharp": "^0.32.1",
|
||||||
"shiki": "^0.14.1",
|
"shiki": "^0.14.1",
|
||||||
|
|
|
@ -10,39 +10,27 @@ declare const Astro: {
|
||||||
opts: Record<string, any>,
|
opts: Record<string, any>,
|
||||||
root: HTMLElement
|
root: HTMLElement
|
||||||
) => void;
|
) => void;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
interface IslandMeta {
|
||||||
|
componentUrl: string;
|
||||||
|
rendererUrl: string;
|
||||||
|
beforeHydrationUrl?: string;
|
||||||
|
componentExport?: string;
|
||||||
|
props: Record<string, any>;
|
||||||
|
opts: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
interface PropTypeSelector {
|
// island meta info (for all islands) is stored here
|
||||||
[k: string]: (value: any) => any;
|
let $meta: Record<string, IslandMeta> = {};
|
||||||
|
// Astro.assign is called in injected scripts
|
||||||
|
(self as any).Astro = Object.assign((self as any).Astro || {}, {
|
||||||
|
assign(script: HTMLElement, value: any) {
|
||||||
|
$meta[(script.parentElement as any).getAttribute('uid')] = value;
|
||||||
|
script.remove();
|
||||||
}
|
}
|
||||||
|
})
|
||||||
const propTypes: PropTypeSelector = {
|
|
||||||
0: (value) => reviveObject(value),
|
|
||||||
1: (value) => reviveArray(value),
|
|
||||||
2: (value) => new RegExp(value),
|
|
||||||
3: (value) => new Date(value),
|
|
||||||
4: (value) => new Map(reviveArray(value)),
|
|
||||||
5: (value) => new Set(reviveArray(value)),
|
|
||||||
6: (value) => BigInt(value),
|
|
||||||
7: (value) => new URL(value),
|
|
||||||
8: (value) => new Uint8Array(value),
|
|
||||||
9: (value) => new Uint16Array(value),
|
|
||||||
10: (value) => new Uint32Array(value),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Not using JSON.parse reviver because it's bottom-up but we want top-down
|
|
||||||
const reviveTuple = (raw: any): any => {
|
|
||||||
const [type, value] = raw;
|
|
||||||
return type in propTypes ? propTypes[type](value) : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const reviveArray = (raw: any): any => (raw as Array<any>).map(reviveTuple);
|
|
||||||
|
|
||||||
const reviveObject = (raw: any): any => {
|
|
||||||
if (typeof raw !== 'object' || raw === null) return raw;
|
|
||||||
return Object.fromEntries(Object.entries(raw).map(([key, value]) => [key, reviveTuple(value)]));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!customElements.get('astro-island')) {
|
if (!customElements.get('astro-island')) {
|
||||||
customElements.define(
|
customElements.define(
|
||||||
|
@ -50,8 +38,11 @@ declare const Astro: {
|
||||||
class extends HTMLElement {
|
class extends HTMLElement {
|
||||||
public Component: any;
|
public Component: any;
|
||||||
public hydrator: any;
|
public hydrator: any;
|
||||||
static observedAttributes = ['props'];
|
public uid: any;
|
||||||
|
static get observedAttributes() { return ['ssr'] }
|
||||||
|
get meta() { return $meta[this.uid]; }
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
this.uid = this.getAttribute('uid')!;
|
||||||
if (!this.hasAttribute('await-children') || this.firstChild) {
|
if (!this.hasAttribute('await-children') || this.firstChild) {
|
||||||
this.childrenConnectedCallback();
|
this.childrenConnectedCallback();
|
||||||
} else {
|
} else {
|
||||||
|
@ -66,27 +57,27 @@ declare const Astro: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async childrenConnectedCallback() {
|
async childrenConnectedCallback() {
|
||||||
let beforeHydrationUrl = this.getAttribute('before-hydration-url');
|
let { beforeHydrationUrl } = this.meta;
|
||||||
if (beforeHydrationUrl) {
|
if (beforeHydrationUrl) {
|
||||||
await import(beforeHydrationUrl);
|
await import(beforeHydrationUrl);
|
||||||
}
|
}
|
||||||
this.start();
|
this.start();
|
||||||
|
this.removeAttribute('await-children');
|
||||||
}
|
}
|
||||||
start() {
|
start() {
|
||||||
const opts = JSON.parse(this.getAttribute('opts')!) as Record<string, any>;
|
const { opts = {} } = this.meta;
|
||||||
const directive = this.getAttribute('client') as directiveAstroKeys;
|
const client = this.getAttribute('client') as directiveAstroKeys;
|
||||||
if (Astro[directive] === undefined) {
|
if (Astro[client] === undefined) {
|
||||||
window.addEventListener(`astro:${directive}`, () => this.start(), { once: true });
|
window.addEventListener(`astro:${client}`, () => this.start(), { once: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Astro[directive]!(
|
Astro[client]!(
|
||||||
async () => {
|
async () => {
|
||||||
const rendererUrl = this.getAttribute('renderer-url');
|
const { rendererUrl, componentUrl, componentExport = 'default' } = this.meta;
|
||||||
const [componentModule, { default: hydrator }] = await Promise.all([
|
const [componentModule, { default: hydrator }] = await Promise.all([
|
||||||
import(this.getAttribute('component-url')!),
|
import(componentUrl),
|
||||||
rendererUrl ? import(rendererUrl) : () => () => {},
|
rendererUrl ? import(rendererUrl) : () => () => {},
|
||||||
]);
|
]);
|
||||||
const componentExport = this.getAttribute('component-export') || 'default';
|
|
||||||
if (!componentExport.includes('.')) {
|
if (!componentExport.includes('.')) {
|
||||||
this.Component = componentModule[componentExport];
|
this.Component = componentModule[componentExport];
|
||||||
} else {
|
} else {
|
||||||
|
@ -134,29 +125,7 @@ declare const Astro: {
|
||||||
if (!closest?.isSameNode(this)) continue;
|
if (!closest?.isSameNode(this)) continue;
|
||||||
slots[slot.getAttribute('name') || 'default'] = slot.innerHTML;
|
slots[slot.getAttribute('name') || 'default'] = slot.innerHTML;
|
||||||
}
|
}
|
||||||
|
const { props = {} } = this.meta;
|
||||||
let props: Record<string, unknown>;
|
|
||||||
|
|
||||||
try {
|
|
||||||
props = this.hasAttribute('props')
|
|
||||||
? reviveObject(JSON.parse(this.getAttribute('props')!))
|
|
||||||
: {};
|
|
||||||
} catch (e) {
|
|
||||||
let componentName: string = this.getAttribute('component-url') || '<unknown>';
|
|
||||||
const componentExport = this.getAttribute('component-export');
|
|
||||||
|
|
||||||
if (componentExport) {
|
|
||||||
componentName += ` (export ${componentExport})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error(
|
|
||||||
`[hydrate] Error parsing props for component ${componentName}`,
|
|
||||||
this.getAttribute('props'),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
await this.hydrator(this)(this.Component, props, slots, {
|
await this.hydrator(this)(this.Component, props, slots, {
|
||||||
client: this.getAttribute('client'),
|
client: this.getAttribute('client'),
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,9 +4,10 @@ import type {
|
||||||
SSRLoadedRenderer,
|
SSRLoadedRenderer,
|
||||||
SSRResult,
|
SSRResult,
|
||||||
} from '../../@types/astro';
|
} from '../../@types/astro';
|
||||||
|
|
||||||
|
import { serialize } from 'seroval';
|
||||||
import { AstroError, AstroErrorData } from '../../core/errors/index.js';
|
import { AstroError, AstroErrorData } from '../../core/errors/index.js';
|
||||||
import { escapeHTML } from './escape.js';
|
import { escapeHTML } from './escape.js';
|
||||||
import { serializeProps } from './serialize.js';
|
|
||||||
import { serializeListValue } from './util.js';
|
import { serializeListValue } from './util.js';
|
||||||
|
|
||||||
export interface HydrationMetadata {
|
export interface HydrationMetadata {
|
||||||
|
@ -123,7 +124,7 @@ interface HydrateScriptOptions {
|
||||||
export async function generateHydrateScript(
|
export async function generateHydrateScript(
|
||||||
scriptOptions: HydrateScriptOptions,
|
scriptOptions: HydrateScriptOptions,
|
||||||
metadata: Required<AstroComponentMetadata>
|
metadata: Required<AstroComponentMetadata>
|
||||||
): Promise<SSRElement> {
|
): Promise<[SSRElement, SSRElement]> {
|
||||||
const { renderer, result, astroId, props, attrs } = scriptOptions;
|
const { renderer, result, astroId, props, attrs } = scriptOptions;
|
||||||
const { hydrate, componentUrl, componentExport } = metadata;
|
const { hydrate, componentUrl, componentExport } = metadata;
|
||||||
|
|
||||||
|
@ -140,6 +141,7 @@ export async function generateHydrateScript(
|
||||||
uid: astroId,
|
uid: astroId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const scriptProps: Record<string, any> = {}
|
||||||
|
|
||||||
// Attach renderer-provided attributes
|
// Attach renderer-provided attributes
|
||||||
if (attrs) {
|
if (attrs) {
|
||||||
|
@ -149,27 +151,31 @@ export async function generateHydrateScript(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add component url
|
// Add component url
|
||||||
island.props['component-url'] = await result.resolve(decodeURI(componentUrl));
|
scriptProps['componentUrl'] = await result.resolve(decodeURI(componentUrl));
|
||||||
|
|
||||||
// Add renderer url
|
// Add renderer url
|
||||||
if (renderer.clientEntrypoint) {
|
if (renderer.clientEntrypoint) {
|
||||||
island.props['component-export'] = componentExport.value;
|
if (componentExport.value !== 'default') scriptProps['componentExport'] = componentExport.value;
|
||||||
island.props['renderer-url'] = await result.resolve(decodeURI(renderer.clientEntrypoint));
|
scriptProps['rendererUrl'] = await result.resolve(decodeURI(renderer.clientEntrypoint));
|
||||||
island.props['props'] = escapeHTML(serializeProps(props, metadata));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
island.props['ssr'] = '';
|
island.props['ssr'] = '';
|
||||||
island.props['client'] = hydrate;
|
island.props['client'] = hydrate;
|
||||||
let beforeHydrationUrl = await result.resolve('astro:scripts/before-hydration.js');
|
let beforeHydrationUrl = await result.resolve('astro:scripts/before-hydration.js');
|
||||||
if (beforeHydrationUrl.length) {
|
if (beforeHydrationUrl.length) {
|
||||||
island.props['before-hydration-url'] = beforeHydrationUrl;
|
scriptProps['beforeHydrationUrl'] = beforeHydrationUrl;
|
||||||
|
}
|
||||||
|
scriptProps['opts'] = {};
|
||||||
|
if (metadata.displayName) {
|
||||||
|
scriptProps.opts.name = metadata.displayName;
|
||||||
|
}
|
||||||
|
if (metadata.hydrateArgs !== true) {
|
||||||
|
scriptProps.opts.value = metadata.hydrateArgs;
|
||||||
|
}
|
||||||
|
if (Object.keys(scriptProps.opts).length === 0) delete scriptProps.opts;
|
||||||
|
if (typeof props === 'object' && Object.keys(props).length > 0) {
|
||||||
|
scriptProps['props'] = props;
|
||||||
}
|
}
|
||||||
island.props['opts'] = escapeHTML(
|
|
||||||
JSON.stringify({
|
|
||||||
name: metadata.displayName,
|
|
||||||
value: metadata.hydrateArgs || '',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
transitionDirectivesToCopyOnIsland.forEach((name) => {
|
transitionDirectivesToCopyOnIsland.forEach((name) => {
|
||||||
if (props[name]) {
|
if (props[name]) {
|
||||||
|
@ -177,5 +183,11 @@ export async function generateHydrateScript(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return island;
|
const serializedProps = serialize(scriptProps)
|
||||||
|
const script: SSRElement = {
|
||||||
|
children: `Astro.assign(document.currentScript,${serializedProps})`,
|
||||||
|
props: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [island, script];
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,9 +50,11 @@ export async function renderJSX(result: SSRResult, vnode: any): Promise<any> {
|
||||||
case !vnode && vnode !== 0:
|
case !vnode && vnode !== 0:
|
||||||
return '';
|
return '';
|
||||||
case Array.isArray(vnode):
|
case Array.isArray(vnode):
|
||||||
return markHTMLString(
|
let dest = '';
|
||||||
(await Promise.all(vnode.map((v: any) => renderJSX(result, v)))).join('')
|
for (const child of vnode) {
|
||||||
);
|
dest += await renderJSX(result, child);
|
||||||
|
}
|
||||||
|
return markHTMLString(dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the skip from the props, if we've already attempted a previous render
|
// Extract the skip from the props, if we've already attempted a previous render
|
||||||
|
|
|
@ -6,10 +6,10 @@ import type {
|
||||||
} from '../../../@types/astro';
|
} from '../../../@types/astro';
|
||||||
import type { RenderInstruction } from './types.js';
|
import type { RenderInstruction } from './types.js';
|
||||||
|
|
||||||
|
import { serialize } from 'seroval';
|
||||||
import { AstroError, AstroErrorData } from '../../../core/errors/index.js';
|
import { AstroError, AstroErrorData } from '../../../core/errors/index.js';
|
||||||
import { HTMLBytes, markHTMLString } from '../escape.js';
|
import { HTMLBytes, markHTMLString } from '../escape.js';
|
||||||
import { extractDirectives, generateHydrateScript } from '../hydration.js';
|
import { extractDirectives, generateHydrateScript } from '../hydration.js';
|
||||||
import { serializeProps } from '../serialize.js';
|
|
||||||
import { shorthash } from '../shorthash.js';
|
import { shorthash } from '../shorthash.js';
|
||||||
import { isPromise } from '../util.js';
|
import { isPromise } from '../util.js';
|
||||||
import {
|
import {
|
||||||
|
@ -89,7 +89,7 @@ async function renderFrameworkComponent(
|
||||||
const { renderers, clientDirectives } = result;
|
const { renderers, clientDirectives } = result;
|
||||||
const metadata: AstroComponentMetadata = {
|
const metadata: AstroComponentMetadata = {
|
||||||
astroStaticSlot: true,
|
astroStaticSlot: true,
|
||||||
displayName,
|
displayName: typeof displayName === 'string' ? displayName : '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { hydration, isPage, props } = extractDirectives(_props, clientDirectives);
|
const { hydration, isPage, props } = extractDirectives(_props, clientDirectives);
|
||||||
|
@ -314,13 +314,10 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
|
|
||||||
// Include componentExport name, componentUrl, and props in hash to dedupe identical islands
|
// Include componentExport name, componentUrl, and props in hash to dedupe identical islands
|
||||||
const astroId = shorthash(
|
const astroId = shorthash(
|
||||||
`<!--${metadata.componentExport!.value}:${metadata.componentUrl}-->\n${html}\n${serializeProps(
|
`<!--${metadata.componentExport!.value}:${metadata.componentUrl}-->\n${html}\n${serialize(props)}`
|
||||||
props,
|
|
||||||
metadata
|
|
||||||
)}`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const island = await generateHydrateScript(
|
const [island, script] = await generateHydrateScript(
|
||||||
{ renderer: renderer!, result, astroId, props, attrs },
|
{ renderer: renderer!, result, astroId, props, attrs },
|
||||||
metadata as Required<AstroComponentMetadata>
|
metadata as Required<AstroComponentMetadata>
|
||||||
);
|
);
|
||||||
|
@ -357,10 +354,10 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
island.children = `${html ?? ''}${template}`;
|
island.children = `${html ?? ''}${template}`;
|
||||||
|
|
||||||
if (island.children) {
|
if (island.children) {
|
||||||
island.props['await-children'] = '';
|
island.props['await-children'] = '';
|
||||||
}
|
}
|
||||||
|
island.children = `${renderElement('script', script, false)}${island.children}`
|
||||||
|
|
||||||
return {
|
return {
|
||||||
render(destination) {
|
render(destination) {
|
||||||
|
|
|
@ -1,109 +0,0 @@
|
||||||
import type { AstroComponentMetadata } from '../../@types/astro';
|
|
||||||
|
|
||||||
type ValueOf<T> = T[keyof T];
|
|
||||||
|
|
||||||
const PROP_TYPE = {
|
|
||||||
Value: 0,
|
|
||||||
JSON: 1, // Actually means Array
|
|
||||||
RegExp: 2,
|
|
||||||
Date: 3,
|
|
||||||
Map: 4,
|
|
||||||
Set: 5,
|
|
||||||
BigInt: 6,
|
|
||||||
URL: 7,
|
|
||||||
Uint8Array: 8,
|
|
||||||
Uint16Array: 9,
|
|
||||||
Uint32Array: 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
function serializeArray(
|
|
||||||
value: any[],
|
|
||||||
metadata: AstroComponentMetadata | Record<string, any> = {},
|
|
||||||
parents = new WeakSet<any>()
|
|
||||||
): any[] {
|
|
||||||
if (parents.has(value)) {
|
|
||||||
throw new Error(`Cyclic reference detected while serializing props for <${metadata.displayName} client:${metadata.hydrate}>!
|
|
||||||
|
|
||||||
Cyclic references cannot be safely serialized for client-side usage. Please remove the cyclic reference.`);
|
|
||||||
}
|
|
||||||
parents.add(value);
|
|
||||||
const serialized = value.map((v) => {
|
|
||||||
return convertToSerializedForm(v, metadata, parents);
|
|
||||||
});
|
|
||||||
parents.delete(value);
|
|
||||||
return serialized;
|
|
||||||
}
|
|
||||||
|
|
||||||
function serializeObject(
|
|
||||||
value: Record<any, any>,
|
|
||||||
metadata: AstroComponentMetadata | Record<string, any> = {},
|
|
||||||
parents = new WeakSet<any>()
|
|
||||||
): Record<any, any> {
|
|
||||||
if (parents.has(value)) {
|
|
||||||
throw new Error(`Cyclic reference detected while serializing props for <${metadata.displayName} client:${metadata.hydrate}>!
|
|
||||||
|
|
||||||
Cyclic references cannot be safely serialized for client-side usage. Please remove the cyclic reference.`);
|
|
||||||
}
|
|
||||||
parents.add(value);
|
|
||||||
const serialized = Object.fromEntries(
|
|
||||||
Object.entries(value).map(([k, v]) => {
|
|
||||||
return [k, convertToSerializedForm(v, metadata, parents)];
|
|
||||||
})
|
|
||||||
);
|
|
||||||
parents.delete(value);
|
|
||||||
return serialized;
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertToSerializedForm(
|
|
||||||
value: any,
|
|
||||||
metadata: AstroComponentMetadata | Record<string, any> = {},
|
|
||||||
parents = new WeakSet<any>()
|
|
||||||
): [ValueOf<typeof PROP_TYPE>, any] | [ValueOf<typeof PROP_TYPE>] {
|
|
||||||
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, serializeArray(Array.from(value as Map<any, any>), metadata, parents)];
|
|
||||||
}
|
|
||||||
case '[object Set]': {
|
|
||||||
return [PROP_TYPE.Set, serializeArray(Array.from(value as Set<any>), metadata, parents)];
|
|
||||||
}
|
|
||||||
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, serializeArray(value, metadata, parents)];
|
|
||||||
}
|
|
||||||
case '[object Uint8Array]': {
|
|
||||||
return [PROP_TYPE.Uint8Array, Array.from(value as Uint8Array)];
|
|
||||||
}
|
|
||||||
case '[object Uint16Array]': {
|
|
||||||
return [PROP_TYPE.Uint16Array, Array.from(value as Uint16Array)];
|
|
||||||
}
|
|
||||||
case '[object Uint32Array]': {
|
|
||||||
return [PROP_TYPE.Uint32Array, Array.from(value as Uint32Array)];
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
if (value !== null && typeof value === 'object') {
|
|
||||||
return [PROP_TYPE.Value, serializeObject(value, metadata, parents)];
|
|
||||||
} else if (value === undefined) {
|
|
||||||
return [PROP_TYPE.Value];
|
|
||||||
} else {
|
|
||||||
return [PROP_TYPE.Value, value];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function serializeProps(props: any, metadata: AstroComponentMetadata) {
|
|
||||||
const serialized = JSON.stringify(serializeObject(props, metadata));
|
|
||||||
return serialized;
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import testAdapter from './test-adapter.js';
|
import testAdapter from './test-adapter.js';
|
||||||
import { loadFixture } from './test-utils.js';
|
import { loadFixture, getIslandDataFromScript } from './test-utils.js';
|
||||||
|
|
||||||
const assetsPrefix = 'http://localhost:4321';
|
const assetsPrefix = 'http://localhost:4321';
|
||||||
const assetsPrefixRegex = /^http:\/\/localhost:4321\/_astro\/.*/;
|
const assetsPrefixRegex = /^http:\/\/localhost:4321\/_astro\/.*/;
|
||||||
|
@ -36,9 +36,11 @@ describe('Assets Prefix - Static', () => {
|
||||||
it('react component astro-island should import from assetsPrefix', async () => {
|
it('react component astro-island should import from assetsPrefix', async () => {
|
||||||
const html = await fixture.readFile('/index.html');
|
const html = await fixture.readFile('/index.html');
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
const island = $('astro-island');
|
const script = $('astro-island > script').first();
|
||||||
expect(island.attr('component-url')).to.match(assetsPrefixRegex);
|
const data = getIslandDataFromScript(script.text());
|
||||||
expect(island.attr('renderer-url')).to.match(assetsPrefixRegex);
|
|
||||||
|
expect(data.componentUrl).to.match(assetsPrefixRegex);
|
||||||
|
expect(data.rendererUrl).to.match(assetsPrefixRegex);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('import.meta.env.ASSETS_PREFIX works', async () => {
|
it('import.meta.env.ASSETS_PREFIX works', async () => {
|
||||||
|
@ -127,9 +129,11 @@ describe('Assets Prefix, server', () => {
|
||||||
expect(response.status).to.equal(200);
|
expect(response.status).to.equal(200);
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
const island = $('astro-island');
|
const script = $('astro-island > script');
|
||||||
expect(island.attr('component-url')).to.match(assetsPrefixRegex);
|
const data = getIslandDataFromScript(script.text());
|
||||||
expect(island.attr('renderer-url')).to.match(assetsPrefixRegex);
|
|
||||||
|
expect(data.componentUrl).to.match(assetsPrefixRegex);
|
||||||
|
expect(data.rendererUrl).to.match(assetsPrefixRegex);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('markdown optimized image src does not start with assetsPrefix in SSR', async () => {
|
it('markdown optimized image src does not start with assetsPrefix in SSR', async () => {
|
||||||
|
|
|
@ -76,7 +76,7 @@ describe('Component children', () => {
|
||||||
|
|
||||||
// test 2: If client, and no children are rendered, a template is.
|
// test 2: If client, and no children are rendered, a template is.
|
||||||
expect($('#client').parent().children()).to.have.lengthOf(
|
expect($('#client').parent().children()).to.have.lengthOf(
|
||||||
2,
|
3,
|
||||||
'rendered the client component and a template'
|
'rendered the client component and a template'
|
||||||
);
|
);
|
||||||
expect($('#client').parent().find('template[data-astro-template]')).to.have.lengthOf(
|
expect($('#client').parent().find('template[data-astro-template]')).to.have.lengthOf(
|
||||||
|
@ -85,11 +85,11 @@ describe('Component children', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// test 3: If client, and children are rendered, no template is.
|
// test 3: If client, and children are rendered, no template is.
|
||||||
expect($('#client-render').parent().children()).to.have.lengthOf(1);
|
expect($('#client-render').parent().children()).to.have.lengthOf(2);
|
||||||
expect($('#client-render').parent().find('template')).to.have.lengthOf(0);
|
expect($('#client-render').parent().find('template')).to.have.lengthOf(0);
|
||||||
|
|
||||||
// test 4: If client and no children are provided, no template is.
|
// test 4: If client and no children are provided, no template is.
|
||||||
expect($('#client-no-children').parent().children()).to.have.lengthOf(1);
|
expect($('#client-no-children').parent().children()).to.have.lengthOf(2);
|
||||||
expect($('#client-no-children').parent().find('template')).to.have.lengthOf(0);
|
expect($('#client-no-children').parent().find('template')).to.have.lengthOf(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { load as cheerioLoad } from 'cheerio';
|
import { load as cheerioLoad } from 'cheerio';
|
||||||
import { loadFixture } from './test-utils.js';
|
import { loadFixture, getIslandDataFromScript } from './test-utils.js';
|
||||||
|
|
||||||
describe('Client only components', () => {
|
describe('Client only components', () => {
|
||||||
/** @type {import('./test-utils').Fixture} */
|
/** @type {import('./test-utils').Fixture} */
|
||||||
|
@ -18,12 +18,14 @@ describe('Client only components', () => {
|
||||||
it('Loads pages using client:only hydrator', async () => {
|
it('Loads pages using client:only hydrator', async () => {
|
||||||
const html = await fixture.readFile('/index.html');
|
const html = await fixture.readFile('/index.html');
|
||||||
const $ = cheerioLoad(html);
|
const $ = cheerioLoad(html);
|
||||||
|
const script = $('astro-island > script').first();
|
||||||
|
const data = getIslandDataFromScript(script.text());
|
||||||
|
|
||||||
// test 1: <astro-island> is empty
|
// test 1: <astro-island> is empty
|
||||||
expect($('astro-island').html()).to.equal('');
|
expect($('astro-island').first().children().length).to.equal(1);
|
||||||
|
|
||||||
// test 2: svelte renderer is on the page
|
// test 2: svelte renderer is on the page
|
||||||
expect($('astro-island').attr('renderer-url')).to.be.ok;
|
expect(data.rendererUrl).to.be.ok;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds the CSS to the page', async () => {
|
it('Adds the CSS to the page', async () => {
|
||||||
|
@ -83,12 +85,14 @@ describe('Client only components subpath', () => {
|
||||||
it('Loads pages using client:only hydrator', async () => {
|
it('Loads pages using client:only hydrator', async () => {
|
||||||
const html = await fixture.readFile('/index.html');
|
const html = await fixture.readFile('/index.html');
|
||||||
const $ = cheerioLoad(html);
|
const $ = cheerioLoad(html);
|
||||||
|
const script = $('astro-island > script').first();
|
||||||
|
const data = getIslandDataFromScript(script.text());
|
||||||
|
|
||||||
// test 1: <astro-island> is empty
|
// test 1: <astro-island> is empty
|
||||||
expect($('astro-island').html()).to.equal('');
|
expect($('astro-island').first().children.length).to.equal(1);
|
||||||
|
|
||||||
// test 2: svelte renderer is on the page
|
// test 2: svelte renderer is on the page
|
||||||
expect($('astro-island').attr('renderer-url')).to.be.ok;
|
expect(data.rendererUrl).to.be.ok;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Adds the CSS to the page', async () => {
|
it('Adds the CSS to the page', async () => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { loadFixture } from './test-utils.js';
|
import { loadFixture, getIslandDataFromScript } from './test-utils.js';
|
||||||
|
|
||||||
describe('Dynamic components', () => {
|
describe('Dynamic components', () => {
|
||||||
let fixture;
|
let fixture;
|
||||||
|
@ -16,7 +16,7 @@ describe('Dynamic components', () => {
|
||||||
const html = await fixture.readFile('/index.html');
|
const html = await fixture.readFile('/index.html');
|
||||||
|
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
expect($('script').length).to.eq(1);
|
expect($('script').length).to.eq(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Loads pages using client:media hydrator', async () => {
|
it('Loads pages using client:media hydrator', async () => {
|
||||||
|
@ -24,18 +24,19 @@ describe('Dynamic components', () => {
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
// test 1: static value rendered
|
// test 1: static value rendered
|
||||||
expect($('script').length).to.equal(1);
|
expect($('script').length).to.equal(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Loads pages using client:only hydrator', async () => {
|
it('Loads pages using client:only hydrator', async () => {
|
||||||
const html = await fixture.readFile('/client-only/index.html');
|
const html = await fixture.readFile('/client-only/index.html');
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
const script = $('astro-island > script').first();
|
||||||
|
const data = getIslandDataFromScript(script.text());
|
||||||
|
|
||||||
// test 1: <astro-island> is empty.
|
// test 1: <astro-island> is empty.
|
||||||
expect($('astro-island').html()).to.equal('');
|
expect($('astro-island').first().children().length).to.equal(1);
|
||||||
// test 2: component url
|
// test 2: component url
|
||||||
const href = $('astro-island').attr('component-url');
|
expect(data.componentUrl).to.include(`/PersistentCounter`);
|
||||||
expect(href).to.include(`/PersistentCounter`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -55,7 +56,7 @@ describe('Dynamic components subpath', () => {
|
||||||
const html = await fixture.readFile('/index.html');
|
const html = await fixture.readFile('/index.html');
|
||||||
|
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
expect($('script').length).to.eq(1);
|
expect($('script').length).to.eq(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Loads pages using client:media hydrator', async () => {
|
it('Loads pages using client:media hydrator', async () => {
|
||||||
|
@ -63,17 +64,18 @@ describe('Dynamic components subpath', () => {
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
// test 1: static value rendered
|
// test 1: static value rendered
|
||||||
expect($('script').length).to.equal(1);
|
expect($('script').length).to.equal(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Loads pages using client:only hydrator', async () => {
|
it('Loads pages using client:only hydrator', async () => {
|
||||||
const html = await fixture.readFile('/client-only/index.html');
|
const html = await fixture.readFile('/client-only/index.html');
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
const script = $('astro-island > script').first();
|
||||||
|
const data = getIslandDataFromScript(script.text());
|
||||||
|
|
||||||
// test 1: <astro-island> is empty.
|
// test 1: <astro-island> is empty.
|
||||||
expect($('astro-island').html()).to.equal('');
|
expect($('astro-island').first().children().length).to.equal(1);
|
||||||
// test 2: has component url
|
// test 2: component url
|
||||||
const attr = $('astro-island').attr('component-url');
|
expect(data.componentUrl).to.include(`blog/_astro/PersistentCounter`);
|
||||||
expect(attr).to.include(`blog/_astro/PersistentCounter`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,7 @@ describe('Slots with client: directives', () => {
|
||||||
it('Tags of dynamic tags works', async () => {
|
it('Tags of dynamic tags works', async () => {
|
||||||
const html = await fixture.readFile('/index.html');
|
const html = await fixture.readFile('/index.html');
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
expect($('script')).to.have.a.lengthOf(1);
|
expect($('script')).to.have.a.lengthOf(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Astro slot tags are cleaned', async () => {
|
it('Astro slot tags are cleaned', async () => {
|
||||||
|
|
|
@ -14,15 +14,16 @@ describe('Nested Slots', () => {
|
||||||
it('Hidden nested slots see their hydration scripts hoisted', async () => {
|
it('Hidden nested slots see their hydration scripts hoisted', async () => {
|
||||||
const html = await fixture.readFile('/hidden-nested/index.html');
|
const html = await fixture.readFile('/hidden-nested/index.html');
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
expect($('script')).to.have.a.lengthOf(1, 'script rendered');
|
expect($('script')).to.have.a.lengthOf(3, 'script rendered');
|
||||||
|
// Astro Island scripts are, so this will only find other types of scripts
|
||||||
const scriptInTemplate = $($('template')[0].children[0]).find('script');
|
const scriptInTemplate = $($('template')[0].children[0]).find('script');
|
||||||
expect(scriptInTemplate).to.have.a.lengthOf(0, 'script defined outside of the inner template');
|
expect(scriptInTemplate).to.have.a.lengthOf(1, 'script defined outside of the inner template');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Slots rendered via Astro.slots.render have the hydration script', async () => {
|
it('Slots rendered via Astro.slots.render have the hydration script', async () => {
|
||||||
const html = await fixture.readFile('/component-slot/index.html');
|
const html = await fixture.readFile('/component-slot/index.html');
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
expect($('script')).to.have.a.lengthOf(1, 'script rendered');
|
expect($('script')).to.have.a.lengthOf(2, 'script rendered');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Client components nested inside server-only framework components', () => {
|
describe('Client components nested inside server-only framework components', () => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { loadFixture } from './test-utils.js';
|
import { loadFixture, getIslandDataFromScript } from './test-utils.js';
|
||||||
import { preact } from './fixtures/before-hydration/deps.mjs';
|
import { preact } from './fixtures/before-hydration/deps.mjs';
|
||||||
import testAdapter from './test-adapter.js';
|
import testAdapter from './test-adapter.js';
|
||||||
|
|
||||||
|
@ -43,7 +43,8 @@ describe('Astro Scripts before-hydration', () => {
|
||||||
let res = await fixture.fetch('/');
|
let res = await fixture.fetch('/');
|
||||||
let html = await res.text();
|
let html = await res.text();
|
||||||
let $ = cheerio.load(html);
|
let $ = cheerio.load(html);
|
||||||
expect($('astro-island[before-hydration-url]')).has.a.lengthOf(1);
|
const data = getIslandDataFromScript($('astro-island > script').first().text())
|
||||||
|
expect(data.beforeHydrationUrl).to.be.ok;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -55,7 +56,8 @@ describe('Astro Scripts before-hydration', () => {
|
||||||
it('Is included in the astro-island', async () => {
|
it('Is included in the astro-island', async () => {
|
||||||
let html = await fixture.readFile('/index.html');
|
let html = await fixture.readFile('/index.html');
|
||||||
let $ = cheerio.load(html);
|
let $ = cheerio.load(html);
|
||||||
expect($('astro-island[before-hydration-url]')).has.a.lengthOf(1);
|
const data = getIslandDataFromScript($('astro-island > script').first().text())
|
||||||
|
expect(data.beforeHydrationUrl).to.be.ok;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -86,7 +88,8 @@ describe('Astro Scripts before-hydration', () => {
|
||||||
let res = await fixture.fetch('/');
|
let res = await fixture.fetch('/');
|
||||||
let html = await res.text();
|
let html = await res.text();
|
||||||
let $ = cheerio.load(html);
|
let $ = cheerio.load(html);
|
||||||
expect($('astro-island[before-hydration-url]')).has.a.lengthOf(1);
|
const data = getIslandDataFromScript($('astro-island > script').first().text())
|
||||||
|
expect(data.beforeHydrationUrl).to.be.ok;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -98,7 +101,8 @@ describe('Astro Scripts before-hydration', () => {
|
||||||
it('Does not include before-hydration-url on the astro-island', async () => {
|
it('Does not include before-hydration-url on the astro-island', async () => {
|
||||||
let html = await fixture.readFile('/index.html');
|
let html = await fixture.readFile('/index.html');
|
||||||
let $ = cheerio.load(html);
|
let $ = cheerio.load(html);
|
||||||
expect($('astro-island[before-hydration-url]')).has.a.lengthOf(0);
|
const data = getIslandDataFromScript($('astro-island > script').first().text())
|
||||||
|
expect(data.beforeHydrationUrl).not.to.be.ok;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -139,7 +143,8 @@ describe('Astro Scripts before-hydration', () => {
|
||||||
let response = await app.render(request);
|
let response = await app.render(request);
|
||||||
let html = await response.text();
|
let html = await response.text();
|
||||||
let $ = cheerio.load(html);
|
let $ = cheerio.load(html);
|
||||||
expect($('astro-island[before-hydration-url]')).has.a.lengthOf(1);
|
const data = getIslandDataFromScript($('astro-island > script').first().text())
|
||||||
|
expect(data.beforeHydrationUrl).to.be.ok;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -167,7 +172,8 @@ describe('Astro Scripts before-hydration', () => {
|
||||||
let response = await app.render(request);
|
let response = await app.render(request);
|
||||||
let html = await response.text();
|
let html = await response.text();
|
||||||
let $ = cheerio.load(html);
|
let $ = cheerio.load(html);
|
||||||
expect($('astro-island[before-hydration-url]')).has.a.lengthOf(0);
|
const data = getIslandDataFromScript($('astro-island > script').first().text())
|
||||||
|
expect(data.beforeHydrationUrl).not.to.be.ok;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { loadFixture } from './test-utils.js';
|
import { loadFixture, getIslandDataFromScript } from './test-utils.js';
|
||||||
import { preact } from './fixtures/before-hydration/deps.mjs';
|
import { preact } from './fixtures/before-hydration/deps.mjs';
|
||||||
import testAdapter from './test-adapter.js';
|
import testAdapter from './test-adapter.js';
|
||||||
|
|
||||||
|
@ -44,8 +44,9 @@ describe('build assets (static)', () => {
|
||||||
|
|
||||||
const island = $('astro-island');
|
const island = $('astro-island');
|
||||||
expect(island.length).to.eq(1);
|
expect(island.length).to.eq(1);
|
||||||
expect(island.attr('component-url')).to.match(/^\/_astro\//);
|
const data = getIslandDataFromScript(island.children('script').text())
|
||||||
expect(island.attr('renderer-url')).to.match(/^\/_astro\//);
|
expect(data.componentUrl).to.match(/^\/_astro\//);
|
||||||
|
expect(data.rendererUrl).to.match(/^\/_astro\//);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -83,8 +84,9 @@ describe('build assets (static)', () => {
|
||||||
|
|
||||||
const island = $('astro-island');
|
const island = $('astro-island');
|
||||||
expect(island.length).to.eq(1);
|
expect(island.length).to.eq(1);
|
||||||
expect(island.attr('component-url')).to.match(/^\/custom-assets\//);
|
const data = getIslandDataFromScript(island.children('script').text())
|
||||||
expect(island.attr('renderer-url')).to.match(/^\/custom-assets\//);
|
expect(data.componentUrl).to.match(/^\/custom-assets\//);
|
||||||
|
expect(data.rendererUrl).to.match(/^\/custom-assets\//);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -130,8 +132,9 @@ describe('build assets (server)', () => {
|
||||||
|
|
||||||
const island = $('astro-island');
|
const island = $('astro-island');
|
||||||
expect(island.length).to.eq(1);
|
expect(island.length).to.eq(1);
|
||||||
expect(island.attr('component-url')).to.match(/^\/_astro\//);
|
const data = getIslandDataFromScript(island.children('script').text())
|
||||||
expect(island.attr('renderer-url')).to.match(/^\/_astro\//);
|
expect(data.componentUrl).to.match(/^\/_astro\//);
|
||||||
|
expect(data.rendererUrl).to.match(/^\/_astro\//);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -170,8 +173,9 @@ describe('build assets (server)', () => {
|
||||||
|
|
||||||
const island = $('astro-island');
|
const island = $('astro-island');
|
||||||
expect(island.length).to.eq(1);
|
expect(island.length).to.eq(1);
|
||||||
expect(island.attr('component-url')).to.match(/^\/custom-assets\//);
|
const data = getIslandDataFromScript(island.children('script').text())
|
||||||
expect(island.attr('renderer-url')).to.match(/^\/custom-assets\//);
|
expect(data.componentUrl).to.match(/^\/custom-assets\//);
|
||||||
|
expect(data.rendererUrl).to.match(/^\/custom-assets\//);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,8 +6,6 @@ import SvelteCounter from '../components/SvelteCounter.svelte';
|
||||||
<head><title>Dynamic pages</title></head>
|
<head><title>Dynamic pages</title></head>
|
||||||
<body>
|
<body>
|
||||||
<Counter client:load />
|
<Counter client:load />
|
||||||
|
|
||||||
<!-- Including the original hydration syntax to test backwards compatibility -->
|
|
||||||
<SvelteCounter client:load />
|
<SvelteCounter client:load />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -24,6 +24,6 @@ describe('Hydration script ordering', async () => {
|
||||||
|
|
||||||
// Sanity check that we're only rendering them once.
|
// Sanity check that we're only rendering them once.
|
||||||
expect($('style')).to.have.a.lengthOf(1, 'hydration style added once');
|
expect($('style')).to.have.a.lengthOf(1, 'hydration style added once');
|
||||||
expect($('script')).to.have.a.lengthOf(1, 'only one hydration script needed');
|
expect($('script')).to.have.a.lengthOf(4, 'only one hydration script needed');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { loadFixture } from './test-utils.js';
|
import { loadFixture, getIslandDataFromScript } from './test-utils.js';
|
||||||
|
|
||||||
describe('Re-exported astro components with client components', () => {
|
describe('Re-exported astro components with client components', () => {
|
||||||
let fixture;
|
let fixture;
|
||||||
|
@ -14,6 +14,7 @@ describe('Re-exported astro components with client components', () => {
|
||||||
const html = await fixture.readFile('/index.html');
|
const html = await fixture.readFile('/index.html');
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
expect($('astro-island').length).to.equal(1);
|
expect($('astro-island').length).to.equal(1);
|
||||||
expect($('astro-island').attr('component-export')).to.equal('One');
|
const data = getIslandDataFromScript($('astro-island > script').text())
|
||||||
|
expect(data.componentExport).to.equal('One');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,113 +0,0 @@
|
||||||
import { expect } from 'chai';
|
|
||||||
import { serializeProps } from '../dist/runtime/server/serialize.js';
|
|
||||||
|
|
||||||
describe('serialize', () => {
|
|
||||||
it('serializes undefined', () => {
|
|
||||||
const input = { a: undefined };
|
|
||||||
const output = `{"a":[0]}`;
|
|
||||||
expect(serializeProps(input)).to.equal(output);
|
|
||||||
});
|
|
||||||
it('serializes null', () => {
|
|
||||||
const input = { a: null };
|
|
||||||
const output = `{"a":[0,null]}`;
|
|
||||||
expect(serializeProps(input)).to.equal(output);
|
|
||||||
});
|
|
||||||
it('serializes a boolean', () => {
|
|
||||||
const input = { a: false };
|
|
||||||
const output = `{"a":[0,false]}`;
|
|
||||||
expect(serializeProps(input)).to.equal(output);
|
|
||||||
});
|
|
||||||
it('serializes a number', () => {
|
|
||||||
const input = { a: 1 };
|
|
||||||
const output = `{"a":[0,1]}`;
|
|
||||||
expect(serializeProps(input)).to.equal(output);
|
|
||||||
});
|
|
||||||
it('serializes a string', () => {
|
|
||||||
const input = { a: 'b' };
|
|
||||||
const output = `{"a":[0,"b"]}`;
|
|
||||||
expect(serializeProps(input)).to.equal(output);
|
|
||||||
});
|
|
||||||
it('serializes an object', () => {
|
|
||||||
const input = { a: { b: 'c' } };
|
|
||||||
const output = `{"a":[0,{"b":[0,"c"]}]}`;
|
|
||||||
expect(serializeProps(input)).to.equal(output);
|
|
||||||
});
|
|
||||||
it('serializes an array', () => {
|
|
||||||
const input = { a: [0] };
|
|
||||||
const output = `{"a":[1,[[0,0]]]}`;
|
|
||||||
expect(serializeProps(input)).to.equal(output);
|
|
||||||
});
|
|
||||||
it('can serialize deeply nested data without quadratic quote escaping', () => {
|
|
||||||
const input = { a: [{ b: [{ c: [{ d: [{ e: [{ f: [{ g: ['leaf'] }] }] }] }] }] }] };
|
|
||||||
const output =
|
|
||||||
'{"a":[1,[[0,{"b":[1,[[0,{"c":[1,[[0,{"d":[1,[[0,{"e":[1,[[0,{"f":[1,[[0,{"g":[1,[[0,"leaf"]]]}]]]}]]]}]]]}]]]}]]]}]]]}';
|
|
||||||
expect(serializeProps(input)).to.equal(output);
|
|
||||||
});
|
|
||||||
it('serializes a regular expression', () => {
|
|
||||||
const input = { a: /b/ };
|
|
||||||
const output = `{"a":[2,"b"]}`;
|
|
||||||
expect(serializeProps(input)).to.equal(output);
|
|
||||||
});
|
|
||||||
it('serializes a Date', () => {
|
|
||||||
const input = { a: new Date(0) };
|
|
||||||
const output = `{"a":[3,"1970-01-01T00:00:00.000Z"]}`;
|
|
||||||
expect(serializeProps(input)).to.equal(output);
|
|
||||||
});
|
|
||||||
it('serializes a Map', () => {
|
|
||||||
const input = { a: new Map([[0, 1]]) };
|
|
||||||
const output = `{"a":[4,[[1,[[0,0],[0,1]]]]]}`;
|
|
||||||
expect(serializeProps(input)).to.equal(output);
|
|
||||||
});
|
|
||||||
it('serializes a Set', () => {
|
|
||||||
const input = { a: new Set([0, 1, 2, 3]) };
|
|
||||||
const output = `{"a":[5,[[0,0],[0,1],[0,2],[0,3]]]}`;
|
|
||||||
expect(serializeProps(input)).to.equal(output);
|
|
||||||
});
|
|
||||||
it('serializes a BigInt', () => {
|
|
||||||
const input = { a: BigInt('1') };
|
|
||||||
const output = `{"a":[6,"1"]}`;
|
|
||||||
expect(serializeProps(input)).to.equal(output);
|
|
||||||
});
|
|
||||||
it('serializes a URL', () => {
|
|
||||||
const input = { a: new URL('https://example.com/') };
|
|
||||||
const output = `{"a":[7,"https://example.com/"]}`;
|
|
||||||
expect(serializeProps(input)).to.equal(output);
|
|
||||||
});
|
|
||||||
it('serializes a Uint8Array', () => {
|
|
||||||
const input = { a: new Uint8Array([1, 2, 3]) };
|
|
||||||
const output = `{"a":[8,[1,2,3]]}`;
|
|
||||||
expect(serializeProps(input)).to.equal(output);
|
|
||||||
});
|
|
||||||
it('serializes a Uint16Array', () => {
|
|
||||||
const input = { a: new Uint16Array([1, 2, 3]) };
|
|
||||||
const output = `{"a":[9,[1,2,3]]}`;
|
|
||||||
expect(serializeProps(input)).to.equal(output);
|
|
||||||
});
|
|
||||||
it('serializes a Uint32Array', () => {
|
|
||||||
const input = { a: new Uint32Array([1, 2, 3]) };
|
|
||||||
const output = `{"a":[10,[1,2,3]]}`;
|
|
||||||
expect(serializeProps(input)).to.equal(output);
|
|
||||||
});
|
|
||||||
it('cannot serialize a cyclic reference', () => {
|
|
||||||
const a = {};
|
|
||||||
a.b = a;
|
|
||||||
const input = { a };
|
|
||||||
expect(() => serializeProps(input)).to.throw(/cyclic/);
|
|
||||||
});
|
|
||||||
it('cannot serialize a cyclic array', () => {
|
|
||||||
const input = { foo: ['bar'] };
|
|
||||||
input.foo.push(input);
|
|
||||||
expect(() => serializeProps(input)).to.throw(/cyclic/);
|
|
||||||
});
|
|
||||||
it('cannot serialize a deep cyclic reference', () => {
|
|
||||||
const a = { b: {} };
|
|
||||||
a.b.c = a;
|
|
||||||
const input = { a };
|
|
||||||
expect(() => serializeProps(input)).to.throw(/cyclic/);
|
|
||||||
});
|
|
||||||
it('can serialize shared references that are not cyclic', () => {
|
|
||||||
const b = {};
|
|
||||||
const input = { a: { b, b }, b };
|
|
||||||
expect(() => serializeProps(input)).not.to.throw();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -242,6 +242,21 @@ export async function loadFixture(inlineConfig) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {{ componentUrl: string, componentExport: string, rendererUrl: string, beforeHydrationUrl?: string, opts: Record<string, unknown>, props: Record<string, unknown> }}
|
||||||
|
*/
|
||||||
|
export function getIslandDataFromScript(text) {
|
||||||
|
const Astro = { assign: (_, props) => props };
|
||||||
|
const document = {};
|
||||||
|
const args = { Astro, document }
|
||||||
|
|
||||||
|
// Yes, I know, `eval` is bad! But our data is embedded as JS on purpose.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
||||||
|
return new Function(Object.keys(args), `return ${text}`)(...Object.values(args));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} [address]
|
* @param {string} [address]
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -605,6 +605,9 @@ importers:
|
||||||
semver:
|
semver:
|
||||||
specifier: ^7.5.3
|
specifier: ^7.5.3
|
||||||
version: 7.5.3
|
version: 7.5.3
|
||||||
|
seroval:
|
||||||
|
specifier: ^0.9.0
|
||||||
|
version: 0.9.0
|
||||||
server-destroy:
|
server-destroy:
|
||||||
specifier: ^1.0.1
|
specifier: ^1.0.1
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
|
@ -15869,6 +15872,11 @@ packages:
|
||||||
resolution: {integrity: sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g==}
|
resolution: {integrity: sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
/seroval@0.9.0:
|
||||||
|
resolution: {integrity: sha512-Ttr96/8czi3SXjbFFzpRc2Xpp1wvBufmaNuTviUL8eGQhUr1mdeiQ6YYSaLnMwMc4YWSeBggq72bKEBVu6/IFA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/server-destroy@1.0.1:
|
/server-destroy@1.0.1:
|
||||||
resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==}
|
resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -18158,25 +18166,21 @@ packages:
|
||||||
file:packages/astro/test/fixtures/css-assets/packages/font-awesome:
|
file:packages/astro/test/fixtures/css-assets/packages/font-awesome:
|
||||||
resolution: {directory: packages/astro/test/fixtures/css-assets/packages/font-awesome, type: directory}
|
resolution: {directory: packages/astro/test/fixtures/css-assets/packages/font-awesome, type: directory}
|
||||||
name: '@test/astro-font-awesome-package'
|
name: '@test/astro-font-awesome-package'
|
||||||
version: 0.0.1
|
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
file:packages/astro/test/fixtures/multiple-renderers/renderers/one:
|
file:packages/astro/test/fixtures/multiple-renderers/renderers/one:
|
||||||
resolution: {directory: packages/astro/test/fixtures/multiple-renderers/renderers/one, type: directory}
|
resolution: {directory: packages/astro/test/fixtures/multiple-renderers/renderers/one, type: directory}
|
||||||
name: '@test/astro-renderer-one'
|
name: '@test/astro-renderer-one'
|
||||||
version: 1.0.0
|
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
file:packages/astro/test/fixtures/multiple-renderers/renderers/two:
|
file:packages/astro/test/fixtures/multiple-renderers/renderers/two:
|
||||||
resolution: {directory: packages/astro/test/fixtures/multiple-renderers/renderers/two, type: directory}
|
resolution: {directory: packages/astro/test/fixtures/multiple-renderers/renderers/two, type: directory}
|
||||||
name: '@test/astro-renderer-two'
|
name: '@test/astro-renderer-two'
|
||||||
version: 1.0.0
|
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
file:packages/astro/test/fixtures/solid-component/deps/solid-jsx-component:
|
file:packages/astro/test/fixtures/solid-component/deps/solid-jsx-component:
|
||||||
resolution: {directory: packages/astro/test/fixtures/solid-component/deps/solid-jsx-component, type: directory}
|
resolution: {directory: packages/astro/test/fixtures/solid-component/deps/solid-jsx-component, type: directory}
|
||||||
name: '@test/solid-jsx-component'
|
name: '@test/solid-jsx-component'
|
||||||
version: 0.0.0
|
|
||||||
dependencies:
|
dependencies:
|
||||||
solid-js: 1.7.6
|
solid-js: 1.7.6
|
||||||
dev: false
|
dev: false
|
||||||
|
|
Loading…
Reference in a new issue