mirror of https://github.com/sveltejs/svelte
feat: async SSR (#16748)
* feat: First pass at payload
* first crack
* snapshots
* checkpoint
* fix: cloning
* add test option
* big dumb
* today's hard work; few tests left to fix
* improve
* tests passing no wayyyyy yo
* lots of progress, couple of failing tests around selects
* meh
* solve async tree stuff
* fix select/option stuff
* whoop, tests
* simplify
* feat: hoisting
* fix: `$effect.pending` sends updates to incorrect boundary
* changeset
* stuff from upstream
* feat: first hydrationgaa
* remove docs
* snapshots
* silly fix
* checkpoint
* meh
* ALKASJDFALSKDFJ the test passes
* chore: Update a bunch of tests for hydration markers
* chore: remove snippet and is_async
* naming
* better errors for sync-in-async
* test improvements
* idk man
* merge local branches (#16757)
* use fragment as async hoist boundary
* remove async_hoist_boundary
* only dewaterfall when necessary
* unused
* simplify/fix
* de-waterfall awaits in separate elements
* update snapshots
* remove unnecessary wrapper
* fix
* fix
* remove suspends_without_fallback
---------
Co-authored-by: Rich Harris <rich.harris@vercel.com>
* Update payload.js
Co-authored-by: Rich Harris <rich.harris@vercel.com>
* checkpoint
* got the extra children to go away
* just gonna go ahead and merge this as the review comments take up too much space
* chore: remove hoisted_promises (#16766)
* chore: remove hoisted_promises
* WIP optimise promises
* WIP
* fix <slot> with await in prop
* tweak
* fix type error
* Update packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js
* chore: fix hydration treeshaking (#16767)
* chore: fix hydration treeshaking
* fix
* remove await_outside_boundary error (#16762)
* chore: remove unused analysis.boundary (#16763)
* chore: simplify slots (#16765)
* chore: simplify slots
* unused
* Apply suggestions from code review
* chore: remove metadata.pending (#16764)
* Update packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js
* put this back where it was, keep the diff small
* Update packages/svelte/src/compiler/phases/types.d.ts
Co-authored-by: Rich Harris <rich.harris@vercel.com>
* chore: remove analysis.state.title (#16771)
* chore: remove analysis.state.title
* unused
* chore: remove is_async (#16769)
* chore: remove is_async
* unused
* Apply suggestions from code review
Co-authored-by: Rich Harris <rich.harris@vercel.com>
* cleanup
* lint
* clean up payload a bit
* compiler work
* run ssr on sync and async
* prettier
* inline capture
* Update packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js
* chore: simplify build_template (#16780)
* small tweak to aid greppability
* chore: fix SSR context (#16781)
* at least passing
* cleanup
* fix
* remove push/pop from exports, not needed with payload
* I think this is better but tbh not sure
* async SSR
* qualification
* errors:
* I have lost the plot
* finally
* ugh
* tweak error codes to better align with existing conventions, such as they are
* tweak messages
* remove unused args
* DRY out a bit
* unused
* unused
* unused
* simplify - we can enforce readonly at a type level
* unused
* simplify
* avoid magical accessors
* simplify algorithm
* unused
* unused
* reduce indirection
* TreeState -> SSRState
* mark deprecated methods
* grab this.local from parent directly
* rename render -> fn per conventions (fn indicates 'arbitrary code')
* reduce indirection
* Revert "reduce indirection"
This reverts commit 3ec461baad
.
* tweak
* okay works this time
* no way chat, it works
* fix context stuff
* tweak
* make it chainable
* lint
* clean up
* lint
* Update packages/svelte/src/internal/server/types.d.ts
Co-authored-by: Rich Harris <rich.harris@vercel.com>
* sunset html for async
* types
* we use 'deprecated' in other messages
* oops
---------
Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/16774/head
parent
8c982f6101
commit
b8fd326d96
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': minor
|
||||
---
|
||||
|
||||
feat: experimental async SSR
|
@ -0,0 +1,9 @@
|
||||
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
|
||||
|
||||
### experimental_async_ssr
|
||||
|
||||
```
|
||||
Attempted to use asynchronous rendering without `experimental.async` enabled
|
||||
```
|
||||
|
||||
Set `experimental.async: true` in your compiler options (usually in `svelte.config.js`) to use async server rendering. This render ran synchronously.
|
@ -0,0 +1,15 @@
|
||||
## await_invalid
|
||||
|
||||
> Encountered asynchronous work while rendering synchronously.
|
||||
|
||||
You (or the framework you're using) called [`render(...)`](svelte-server#render) with a component containing an `await` expression. Either `await` the result of `render` or wrap the `await` (or the component containing it) in a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet.
|
||||
|
||||
## html_deprecated
|
||||
|
||||
> The `html` property of server render results has been deprecated. Use `body` instead.
|
||||
|
||||
## lifecycle_function_unavailable
|
||||
|
||||
> `%name%(...)` is not available on the server
|
||||
|
||||
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.
|
@ -1,5 +0,0 @@
|
||||
## lifecycle_function_unavailable
|
||||
|
||||
> `%name%(...)` is not available on the server
|
||||
|
||||
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.
|
@ -0,0 +1,5 @@
|
||||
## experimental_async_ssr
|
||||
|
||||
> Attempted to use asynchronous rendering without `experimental.async` enabled
|
||||
|
||||
Set `experimental.async: true` in your compiler options (usually in `svelte.config.js`) to use async server rendering. This render ran synchronously.
|
@ -0,0 +1,20 @@
|
||||
import { DEV } from 'esm-env';
|
||||
|
||||
var bold = 'font-weight: bold';
|
||||
var normal = 'font-weight: normal';
|
||||
|
||||
/**
|
||||
* MESSAGE
|
||||
* @param {string} PARAMETER
|
||||
*/
|
||||
export function CODE(PARAMETER) {
|
||||
if (DEV) {
|
||||
console.warn(
|
||||
`%c[svelte] ${'CODE'}\n%c${MESSAGE}\nhttps://svelte.dev/e/${'CODE'}`,
|
||||
bold,
|
||||
normal
|
||||
);
|
||||
} else {
|
||||
console.warn(`https://svelte.dev/e/${'CODE'}`);
|
||||
}
|
||||
}
|
@ -1,29 +1,33 @@
|
||||
/** @import { BlockStatement, Expression, Pattern } from 'estree' */
|
||||
/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
|
||||
/** @import { AST } from '#compiler' */
|
||||
/** @import { ComponentContext } from '../types.js' */
|
||||
import * as b from '#compiler/builders';
|
||||
import { block_close } from './shared/utils.js';
|
||||
import { block_close, call_child_payload } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* @param {AST.AwaitBlock} node
|
||||
* @param {ComponentContext} context
|
||||
*/
|
||||
export function AwaitBlock(node, context) {
|
||||
context.state.template.push(
|
||||
b.stmt(
|
||||
b.call(
|
||||
'$.await',
|
||||
b.id('$$payload'),
|
||||
/** @type {Expression} */ (context.visit(node.expression)),
|
||||
b.thunk(
|
||||
node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([])
|
||||
),
|
||||
b.arrow(
|
||||
node.value ? [/** @type {Pattern} */ (context.visit(node.value))] : [],
|
||||
node.then ? /** @type {BlockStatement} */ (context.visit(node.then)) : b.block([])
|
||||
)
|
||||
/** @type {Statement} */
|
||||
let statement = b.stmt(
|
||||
b.call(
|
||||
'$.await',
|
||||
b.id('$$payload'),
|
||||
/** @type {Expression} */ (context.visit(node.expression)),
|
||||
b.thunk(
|
||||
node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([])
|
||||
),
|
||||
b.arrow(
|
||||
node.value ? [/** @type {Pattern} */ (context.visit(node.value))] : [],
|
||||
node.then ? /** @type {BlockStatement} */ (context.visit(node.then)) : b.block([])
|
||||
)
|
||||
),
|
||||
block_close
|
||||
)
|
||||
);
|
||||
|
||||
if (node.metadata.expression.has_await) {
|
||||
statement = call_child_payload(b.block([statement]), true);
|
||||
}
|
||||
|
||||
context.state.template.push(statement, block_close);
|
||||
}
|
||||
|
@ -1,80 +1,573 @@
|
||||
export class HeadPayload {
|
||||
/** @type {Set<{ hash: string; code: string }>} */
|
||||
css = new Set();
|
||||
/** @type {string[]} */
|
||||
out = [];
|
||||
uid = () => '';
|
||||
title = '';
|
||||
|
||||
constructor(
|
||||
/** @type {Set<{ hash: string; code: string }>} */ css = new Set(),
|
||||
/** @type {string[]} */ out = [],
|
||||
title = '',
|
||||
uid = () => ''
|
||||
) {
|
||||
this.css = css;
|
||||
this.out = out;
|
||||
this.title = title;
|
||||
this.uid = uid;
|
||||
/** @import { Component } from 'svelte' */
|
||||
/** @import { RenderOutput, SSRContext, SyncRenderOutput } from './types.js' */
|
||||
import { async_mode_flag } from '../flags/index.js';
|
||||
import { abort } from './abort-signal.js';
|
||||
import { pop, push, set_ssr_context, ssr_context } from './context.js';
|
||||
import * as e from './errors.js';
|
||||
import * as w from './warnings.js';
|
||||
import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
|
||||
|
||||
/** @typedef {'head' | 'body'} PayloadType */
|
||||
/** @typedef {{ [key in PayloadType]: string }} AccumulatedContent */
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {T | Promise<T>} MaybePromise<T>
|
||||
*/
|
||||
/**
|
||||
* @typedef {string | Payload} PayloadItem
|
||||
*/
|
||||
|
||||
/**
|
||||
* Payloads are basically a tree of `string | Payload`s, where each `Payload` in the tree represents
|
||||
* work that may or may not have completed. A payload can be {@link collect}ed to aggregate the
|
||||
* content from itself and all of its children, but this will throw if any of the children are
|
||||
* performing asynchronous work. To asynchronously collect a payload, just `await` it.
|
||||
*
|
||||
* The `string` values within a payload are always associated with the {@link type} of that payload. To switch types,
|
||||
* call {@link child} with a different `type` argument.
|
||||
*/
|
||||
export class Payload {
|
||||
/**
|
||||
* The contents of the payload.
|
||||
* @type {PayloadItem[]}
|
||||
*/
|
||||
#out = [];
|
||||
|
||||
/**
|
||||
* Any `onDestroy` callbacks registered during execution of this payload.
|
||||
* @type {(() => void)[] | undefined}
|
||||
*/
|
||||
#on_destroy = undefined;
|
||||
|
||||
/**
|
||||
* Whether this payload is a component body.
|
||||
* @type {boolean}
|
||||
*/
|
||||
#is_component_body = false;
|
||||
|
||||
/**
|
||||
* The type of string content that this payload is accumulating.
|
||||
* @type {PayloadType}
|
||||
*/
|
||||
type;
|
||||
|
||||
/** @type {Payload | undefined} */
|
||||
#parent;
|
||||
|
||||
/**
|
||||
* Asynchronous work associated with this payload. `initial` is the promise from the function
|
||||
* this payload was passed to (if that function was async), and `followup` is any any additional
|
||||
* work from `compact` calls that needs to complete prior to collecting this payload's content.
|
||||
* @type {{ initial: Promise<void> | undefined, followup: Promise<void>[] }}
|
||||
*/
|
||||
promises = { initial: undefined, followup: [] };
|
||||
|
||||
/**
|
||||
* State which is associated with the content tree as a whole.
|
||||
* It will be re-exposed, uncopied, on all children.
|
||||
* @type {SSRState}
|
||||
* @readonly
|
||||
*/
|
||||
global;
|
||||
|
||||
/**
|
||||
* State that is local to the branch it is declared in.
|
||||
* It will be shallow-copied to all children.
|
||||
*
|
||||
* @type {{ select_value: string | undefined }}
|
||||
*/
|
||||
local;
|
||||
|
||||
/**
|
||||
* @param {SSRState} global
|
||||
* @param {Payload | undefined} [parent]
|
||||
* @param {PayloadType} [type]
|
||||
*/
|
||||
constructor(global, parent, type) {
|
||||
this.global = global;
|
||||
this.local = parent ? { ...parent.local } : { select_value: undefined };
|
||||
this.#parent = parent;
|
||||
this.type = type ?? parent?.type ?? 'body';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a child payload. The child payload inherits the state from the parent,
|
||||
* but has its own content.
|
||||
* @param {(payload: Payload) => MaybePromise<void>} fn
|
||||
* @param {PayloadType} [type]
|
||||
*/
|
||||
child(fn, type) {
|
||||
const child = new Payload(this.global, this, type);
|
||||
this.#out.push(child);
|
||||
|
||||
const parent = ssr_context;
|
||||
|
||||
set_ssr_context({
|
||||
...ssr_context,
|
||||
p: parent,
|
||||
c: null,
|
||||
r: child
|
||||
});
|
||||
|
||||
const result = fn(child);
|
||||
|
||||
set_ssr_context(parent);
|
||||
|
||||
if (result instanceof Promise) {
|
||||
if (child.global.mode === 'sync') {
|
||||
e.await_invalid();
|
||||
}
|
||||
// just to avoid unhandled promise rejections -- we'll end up throwing in `collect_async` if something fails
|
||||
result.catch(() => {});
|
||||
child.promises.initial = result;
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a component payload. The component payload inherits the state from the parent,
|
||||
* but has its own content. It is treated as an ordering boundary for ondestroy callbacks.
|
||||
* @param {(payload: Payload) => MaybePromise<void>} fn
|
||||
* @param {Function} [component_fn]
|
||||
* @returns {void}
|
||||
*/
|
||||
component(fn, component_fn) {
|
||||
push(component_fn);
|
||||
const child = this.child(fn);
|
||||
child.#is_component_body = true;
|
||||
pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | (() => Promise<string>)} content
|
||||
*/
|
||||
push(content) {
|
||||
if (typeof content === 'function') {
|
||||
this.child(async (payload) => payload.push(await content()));
|
||||
} else {
|
||||
this.#out.push(content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact everything between `start` and `end` into a single payload, then call `fn` with the result of that payload.
|
||||
* The compacted payload will be sync if all of the children are sync and {@link fn} is sync, otherwise it will be async.
|
||||
* @param {{ start: number, end?: number, fn: (content: AccumulatedContent) => AccumulatedContent | Promise<AccumulatedContent> }} args
|
||||
*/
|
||||
compact({ start, end = this.#out.length, fn }) {
|
||||
const child = new Payload(this.global, this);
|
||||
const to_compact = this.#out.splice(start, end - start, child);
|
||||
|
||||
if (this.global.mode === 'sync') {
|
||||
Payload.#compact(fn, child, to_compact, this.type);
|
||||
} else {
|
||||
this.promises.followup.push(Payload.#compact_async(fn, child, to_compact, this.type));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {() => void} fn
|
||||
*/
|
||||
on_destroy(fn) {
|
||||
(this.#on_destroy ??= []).push(fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number[]}
|
||||
*/
|
||||
get_path() {
|
||||
return this.#parent ? [...this.#parent.get_path(), this.#parent.#out.indexOf(this)] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated this is needed for legacy component bindings
|
||||
*/
|
||||
copy() {
|
||||
const copy = new Payload(this.global, this.#parent, this.type);
|
||||
copy.#out = this.#out.map((item) => (item instanceof Payload ? item.copy() : item));
|
||||
copy.promises = this.promises;
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Payload} other
|
||||
* @deprecated this is needed for legacy component bindings
|
||||
*/
|
||||
subsume(other) {
|
||||
if (this.global.mode !== other.global.mode) {
|
||||
throw new Error(
|
||||
"invariant: A payload cannot switch modes. If you're seeing this, there's a compiler bug. File an issue!"
|
||||
);
|
||||
}
|
||||
|
||||
this.local = other.local;
|
||||
this.#out = other.#out.map((item) => {
|
||||
if (item instanceof Payload) {
|
||||
item.subsume(item);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
this.promises = other.promises;
|
||||
this.type = other.type;
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.#out.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available on the server and when compiling with the `server` option.
|
||||
* Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app.
|
||||
* @template {Record<string, any>} Props
|
||||
* @param {Component<Props>} component
|
||||
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} [options]
|
||||
* @returns {RenderOutput}
|
||||
*/
|
||||
static render(component, options = {}) {
|
||||
/** @type {AccumulatedContent | undefined} */
|
||||
let sync;
|
||||
/** @type {Promise<AccumulatedContent> | undefined} */
|
||||
let async;
|
||||
|
||||
const result = /** @type {RenderOutput} */ ({});
|
||||
// making these properties non-enumerable so that console.logging
|
||||
// doesn't trigger a sync render
|
||||
Object.defineProperties(result, {
|
||||
html: {
|
||||
get: () => {
|
||||
return (sync ??= Payload.#render(component, options)).body;
|
||||
}
|
||||
},
|
||||
head: {
|
||||
get: () => {
|
||||
return (sync ??= Payload.#render(component, options)).head;
|
||||
}
|
||||
},
|
||||
body: {
|
||||
get: () => {
|
||||
return (sync ??= Payload.#render(component, options)).body;
|
||||
}
|
||||
},
|
||||
then: {
|
||||
value:
|
||||
/**
|
||||
* this is not type-safe, but honestly it's the best I can do right now, and it's a straightforward function.
|
||||
*
|
||||
* @template TResult1
|
||||
* @template [TResult2=never]
|
||||
* @param { (value: SyncRenderOutput) => TResult1 } onfulfilled
|
||||
* @param { (reason: unknown) => TResult2 } onrejected
|
||||
*/
|
||||
(onfulfilled, onrejected) => {
|
||||
if (!async_mode_flag) {
|
||||
w.experimental_async_ssr();
|
||||
const result = (sync ??= Payload.#render(component, options));
|
||||
const user_result = onfulfilled({
|
||||
head: result.head,
|
||||
body: result.body,
|
||||
html: result.body
|
||||
});
|
||||
return Promise.resolve(user_result);
|
||||
}
|
||||
async ??= Payload.#render_async(component, options);
|
||||
return async.then((result) => {
|
||||
Object.defineProperty(result, 'html', {
|
||||
// eslint-disable-next-line getter-return
|
||||
get: () => {
|
||||
e.html_deprecated();
|
||||
}
|
||||
});
|
||||
return onfulfilled(/** @type {SyncRenderOutput} */ (result));
|
||||
}, onrejected);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all of the `onDestroy` callbacks regsitered during rendering. In an async context, this is only safe to call
|
||||
* after awaiting `collect_async`.
|
||||
*
|
||||
* Child payloads are "porous" and don't affect execution order, but component body payloads
|
||||
* create ordering boundaries. Within a payload, callbacks run in order until hitting a component boundary.
|
||||
* @returns {Iterable<() => void>}
|
||||
*/
|
||||
*#collect_on_destroy() {
|
||||
for (const component of this.#traverse_components()) {
|
||||
yield* component.#collect_ondestroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a depth-first search of payloads, yielding the deepest components first, then additional components as we backtrack up the tree.
|
||||
* @returns {Iterable<Payload>}
|
||||
*/
|
||||
*#traverse_components() {
|
||||
for (const child of this.#out) {
|
||||
if (typeof child !== 'string') {
|
||||
yield* child.#traverse_components();
|
||||
}
|
||||
}
|
||||
if (this.#is_component_body) {
|
||||
yield this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Iterable<() => void>}
|
||||
*/
|
||||
*#collect_ondestroy() {
|
||||
if (this.#on_destroy) {
|
||||
for (const fn of this.#on_destroy) {
|
||||
yield fn;
|
||||
}
|
||||
}
|
||||
for (const child of this.#out) {
|
||||
if (child instanceof Payload && !child.#is_component_body) {
|
||||
yield* child.#collect_ondestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a component. Throws if any of the children are performing asynchronous work.
|
||||
*
|
||||
* @template {Record<string, any>} Props
|
||||
* @param {Component<Props>} component
|
||||
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} options
|
||||
* @returns {AccumulatedContent}
|
||||
*/
|
||||
static #render(component, options) {
|
||||
var previous_context = ssr_context;
|
||||
try {
|
||||
const payload = Payload.#open_render('sync', component, options);
|
||||
|
||||
const content = Payload.#collect_content([payload], payload.type);
|
||||
return Payload.#close_render(content, payload);
|
||||
} finally {
|
||||
abort();
|
||||
set_ssr_context(previous_context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a component.
|
||||
*
|
||||
* @template {Record<string, any>} Props
|
||||
* @param {Component<Props>} component
|
||||
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} options
|
||||
* @returns {Promise<AccumulatedContent>}
|
||||
*/
|
||||
static async #render_async(component, options) {
|
||||
var previous_context = ssr_context;
|
||||
try {
|
||||
const payload = Payload.#open_render('async', component, options);
|
||||
|
||||
const content = await Payload.#collect_content_async([payload], payload.type);
|
||||
return Payload.#close_render(content, payload);
|
||||
} finally {
|
||||
abort();
|
||||
set_ssr_context(previous_context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(content: AccumulatedContent) => AccumulatedContent | Promise<AccumulatedContent>} fn
|
||||
* @param {Payload} child
|
||||
* @param {PayloadItem[]} to_compact
|
||||
* @param {PayloadType} type
|
||||
*/
|
||||
static #compact(fn, child, to_compact, type) {
|
||||
const content = Payload.#collect_content(to_compact, type);
|
||||
const transformed_content = fn(content);
|
||||
if (transformed_content instanceof Promise) {
|
||||
throw new Error(
|
||||
"invariant: Somehow you've encountered asynchronous work while rendering synchronously. If you're seeing this, there's a compiler bug. File an issue!"
|
||||
);
|
||||
} else {
|
||||
Payload.#push_accumulated_content(child, transformed_content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(content: AccumulatedContent) => AccumulatedContent | Promise<AccumulatedContent>} fn
|
||||
* @param {Payload} child
|
||||
* @param {PayloadItem[]} to_compact
|
||||
* @param {PayloadType} type
|
||||
*/
|
||||
static async #compact_async(fn, child, to_compact, type) {
|
||||
const content = await Payload.#collect_content_async(to_compact, type);
|
||||
const transformed_content = await fn(content);
|
||||
Payload.#push_accumulated_content(child, transformed_content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all of the code from the `out` array and return it as a string, or a promise resolving to a string.
|
||||
* @param {PayloadItem[]} items
|
||||
* @param {PayloadType} current_type
|
||||
* @param {AccumulatedContent} content
|
||||
* @returns {AccumulatedContent}
|
||||
*/
|
||||
static #collect_content(items, current_type, content = { head: '', body: '' }) {
|
||||
for (const item of items) {
|
||||
if (typeof item === 'string') {
|
||||
content[current_type] += item;
|
||||
} else if (item instanceof Payload) {
|
||||
Payload.#collect_content(item.#out, item.type, content);
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all of the code from the `out` array and return it as a string.
|
||||
* @param {PayloadItem[]} items
|
||||
* @param {PayloadType} current_type
|
||||
* @param {AccumulatedContent} content
|
||||
* @returns {Promise<AccumulatedContent>}
|
||||
*/
|
||||
static async #collect_content_async(items, current_type, content = { head: '', body: '' }) {
|
||||
// no danger to sequentially awaiting stuff in here; all of the work is already kicked off
|
||||
for (const item of items) {
|
||||
if (typeof item === 'string') {
|
||||
content[current_type] += item;
|
||||
} else {
|
||||
if (item.promises.initial) {
|
||||
// this represents the async function that's modifying this payload.
|
||||
// we can't do anything until it's done and we know our `out` array is complete.
|
||||
await item.promises.initial;
|
||||
}
|
||||
for (const followup of item.promises.followup) {
|
||||
// this is sequential because `compact` could synchronously queue up additional followup work
|
||||
await followup;
|
||||
}
|
||||
await Payload.#collect_content_async(item.#out, item.type, content);
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Payload} tree
|
||||
* @param {AccumulatedContent} accumulated_content
|
||||
*/
|
||||
static #push_accumulated_content(tree, accumulated_content) {
|
||||
for (const [type, content] of Object.entries(accumulated_content)) {
|
||||
if (!content) continue;
|
||||
const child = new Payload(tree.global, tree, /** @type {PayloadType} */ (type));
|
||||
child.push(content);
|
||||
tree.#out.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {Record<string, any>} Props
|
||||
* @param {'sync' | 'async'} mode
|
||||
* @param {import('svelte').Component<Props>} component
|
||||
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} options
|
||||
* @returns {Payload}
|
||||
*/
|
||||
static #open_render(mode, component, options) {
|
||||
const payload = new Payload(new SSRState(mode, options.idPrefix ? options.idPrefix + '-' : ''));
|
||||
|
||||
payload.push(BLOCK_OPEN);
|
||||
|
||||
if (options.context) {
|
||||
push();
|
||||
/** @type {SSRContext} */ (ssr_context).c = options.context;
|
||||
/** @type {SSRContext} */ (ssr_context).r = payload;
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
component(payload, options.props ?? {});
|
||||
|
||||
if (options.context) {
|
||||
pop();
|
||||
}
|
||||
|
||||
payload.push(BLOCK_CLOSE);
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AccumulatedContent} content
|
||||
* @param {Payload} payload
|
||||
*/
|
||||
static #close_render(content, payload) {
|
||||
for (const cleanup of payload.#collect_on_destroy()) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
let head = content.head + payload.global.get_title();
|
||||
|
||||
const body = BLOCK_OPEN + content.body + BLOCK_CLOSE; // this inserts a fake boundary so hydration matches
|
||||
|
||||
for (const { hash, code } of payload.global.css) {
|
||||
head += `<style id="${hash}">${code}</style>`;
|
||||
}
|
||||
|
||||
return {
|
||||
head,
|
||||
body
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class Payload {
|
||||
/** @type {Set<{ hash: string; code: string }>} */
|
||||
export class SSRState {
|
||||
/** @readonly @type {'sync' | 'async'} */
|
||||
mode;
|
||||
|
||||
/** @readonly @type {() => string} */
|
||||
uid;
|
||||
|
||||
/** @readonly @type {Set<{ hash: string; code: string }>} */
|
||||
css = new Set();
|
||||
/** @type {string[]} */
|
||||
out = [];
|
||||
uid = () => '';
|
||||
select_value = undefined;
|
||||
|
||||
head = new HeadPayload();
|
||||
/** @type {{ path: number[], value: string }} */
|
||||
#title = { path: [], value: '' };
|
||||
|
||||
constructor(id_prefix = '') {
|
||||
this.uid = props_id_generator(id_prefix);
|
||||
this.head.uid = this.uid;
|
||||
/**
|
||||
* @param {'sync' | 'async'} mode
|
||||
* @param {string} [id_prefix]
|
||||
*/
|
||||
constructor(mode, id_prefix = '') {
|
||||
this.mode = mode;
|
||||
|
||||
let uid = 1;
|
||||
this.uid = () => `${id_prefix}s${uid++}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in legacy mode to handle bindings
|
||||
* @param {Payload} to_copy
|
||||
* @returns {Payload}
|
||||
*/
|
||||
export function copy_payload({ out, css, head, uid }) {
|
||||
const payload = new Payload();
|
||||
get_title() {
|
||||
return this.#title.value;
|
||||
}
|
||||
|
||||
payload.out = [...out];
|
||||
payload.css = new Set(css);
|
||||
payload.uid = uid;
|
||||
/**
|
||||
* Performs a depth-first (lexicographic) comparison using the path. Rejects sets
|
||||
* from earlier than or equal to the current value.
|
||||
* @param {string} value
|
||||
* @param {number[]} path
|
||||
*/
|
||||
set_title(value, path) {
|
||||
const current = this.#title.path;
|
||||
|
||||
payload.head = new HeadPayload();
|
||||
payload.head.out = [...head.out];
|
||||
payload.head.css = new Set(head.css);
|
||||
payload.head.title = head.title;
|
||||
payload.head.uid = head.uid;
|
||||
let i = 0;
|
||||
let l = Math.min(path.length, current.length);
|
||||
|
||||
return payload;
|
||||
}
|
||||
// skip identical prefixes - [1, 2, 3, ...] === [1, 2, 3, ...]
|
||||
while (i < l && path[i] === current[i]) i += 1;
|
||||
|
||||
/**
|
||||
* Assigns second payload to first
|
||||
* @param {Payload} p1
|
||||
* @param {Payload} p2
|
||||
* @returns {void}
|
||||
*/
|
||||
export function assign_payload(p1, p2) {
|
||||
p1.out = [...p2.out];
|
||||
p1.css = p2.css;
|
||||
p1.head = p2.head;
|
||||
p1.uid = p2.uid;
|
||||
}
|
||||
if (path[i] === undefined) return;
|
||||
|
||||
/**
|
||||
* Creates an ID generator
|
||||
* @param {string} prefix
|
||||
* @returns {() => string}
|
||||
*/
|
||||
function props_id_generator(prefix) {
|
||||
let uid = 1;
|
||||
return () => `${prefix}s${uid++}`;
|
||||
// replace title if
|
||||
// - incoming path is longer - [7, 8, 9] > [7, 8]
|
||||
// - incoming path is later - [7, 8, 9] > [7, 8, 8]
|
||||
if (current[i] === undefined || path[i] > current[i]) {
|
||||
this.#title.path = path;
|
||||
this.#title.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,364 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'vitest';
|
||||
import { Payload, SSRState } from './payload.js';
|
||||
import type { Component } from 'svelte';
|
||||
import { disable_async_mode_flag, enable_async_mode_flag } from '../flags/index.js';
|
||||
|
||||
test('collects synchronous body content by default', () => {
|
||||
const component = (payload: Payload) => {
|
||||
payload.push('a');
|
||||
payload.child(($$payload) => {
|
||||
$$payload.push('b');
|
||||
});
|
||||
payload.push('c');
|
||||
};
|
||||
|
||||
const { head, body } = Payload.render(component as unknown as Component);
|
||||
expect(head).toBe('');
|
||||
expect(body).toBe('<!--[--><!--[-->abc<!--]--><!--]-->');
|
||||
});
|
||||
|
||||
test('child type switches content area (head vs body)', () => {
|
||||
const component = (payload: Payload) => {
|
||||
payload.push('a');
|
||||
payload.child(($$payload) => {
|
||||
$$payload.push('<title>T</title>');
|
||||
}, 'head');
|
||||
payload.push('b');
|
||||
};
|
||||
|
||||
const { head, body } = Payload.render(component as unknown as Component);
|
||||
expect(head).toBe('<title>T</title>');
|
||||
expect(body).toBe('<!--[--><!--[-->ab<!--]--><!--]-->');
|
||||
});
|
||||
|
||||
test('child inherits parent type when not specified', () => {
|
||||
const component = (payload: Payload) => {
|
||||
payload.child((payload) => {
|
||||
payload.push('<meta name="x"/>');
|
||||
payload.child((payload) => {
|
||||
payload.push('<style>/* css */</style>');
|
||||
});
|
||||
}, 'head');
|
||||
};
|
||||
|
||||
const { head, body } = Payload.render(component as unknown as Component);
|
||||
expect(body).toBe('<!--[--><!--[--><!--]--><!--]-->');
|
||||
expect(head).toBe('<meta name="x"/><style>/* css */</style>');
|
||||
});
|
||||
|
||||
test('get_path returns the path indexes to a payload', () => {
|
||||
const root = new Payload(new SSRState('sync'));
|
||||
let child_a: InstanceType<typeof Payload> | undefined;
|
||||
let child_b: InstanceType<typeof Payload> | undefined;
|
||||
let child_b_0: InstanceType<typeof Payload> | undefined;
|
||||
|
||||
root.child(($$payload) => {
|
||||
child_a = $$payload;
|
||||
$$payload.push('A');
|
||||
});
|
||||
root.child(($$payload) => {
|
||||
child_b = $$payload;
|
||||
$$payload.child(($$inner) => {
|
||||
child_b_0 = $$inner;
|
||||
$$inner.push('B0');
|
||||
});
|
||||
$$payload.push('B1');
|
||||
});
|
||||
|
||||
expect(child_a!.get_path()).toEqual([0]);
|
||||
expect(child_b!.get_path()).toEqual([1]);
|
||||
expect(child_b_0!.get_path()).toEqual([1, 0]);
|
||||
});
|
||||
|
||||
test('creating an async child in a sync context throws', () => {
|
||||
const component = (payload: Payload) => {
|
||||
payload.push('a');
|
||||
payload.child(async ($$payload) => {
|
||||
await Promise.resolve();
|
||||
$$payload.push('x');
|
||||
});
|
||||
};
|
||||
|
||||
expect(() => Payload.render(component as unknown as Component).head).toThrow('await_invalid');
|
||||
expect(() => Payload.render(component as unknown as Component).html).toThrow('await_invalid');
|
||||
expect(() => Payload.render(component as unknown as Component).body).toThrow('await_invalid');
|
||||
});
|
||||
|
||||
test('compact synchronously aggregates a range and can transform into head/body', () => {
|
||||
const component = (payload: Payload) => {
|
||||
const start = payload.length;
|
||||
payload.push('a');
|
||||
payload.push('b');
|
||||
payload.push('c');
|
||||
payload.compact({
|
||||
start,
|
||||
end: start + 2,
|
||||
fn: (content) => {
|
||||
return { head: '<h>H</h>', body: content.body + 'd' };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const { head, body } = Payload.render(component as unknown as Component);
|
||||
expect(head).toBe('<h>H</h>');
|
||||
expect(body).toBe('<!--[--><!--[-->abdc<!--]--><!--]-->');
|
||||
});
|
||||
|
||||
test('local state is shallow-copied to children', () => {
|
||||
const root = new Payload(new SSRState('sync'));
|
||||
root.local.select_value = 'A';
|
||||
let child: InstanceType<typeof Payload> | undefined;
|
||||
root.child(($$payload) => {
|
||||
child = $$payload;
|
||||
});
|
||||
|
||||
expect(child!.local.select_value).toBe('A');
|
||||
child!.local.select_value = 'B';
|
||||
expect(root.local.select_value).toBe('A');
|
||||
});
|
||||
|
||||
test('subsume replaces tree content and state from other', () => {
|
||||
const a = new Payload(new SSRState('async'), undefined, 'head');
|
||||
a.push('<meta />');
|
||||
a.local.select_value = 'A';
|
||||
|
||||
const b = new Payload(new SSRState('async'));
|
||||
b.child(async ($$payload) => {
|
||||
await Promise.resolve();
|
||||
$$payload.push('body');
|
||||
});
|
||||
b.global.css.add({ hash: 'h', code: 'c' });
|
||||
b.global.set_title('Title', [1]);
|
||||
b.local.select_value = 'B';
|
||||
b.promises.initial = Promise.resolve();
|
||||
|
||||
a.subsume(b);
|
||||
|
||||
expect(a.type).toBe('body');
|
||||
expect(a.local.select_value).toBe('B');
|
||||
expect(a.promises).toBe(b.promises);
|
||||
});
|
||||
|
||||
test('subsume refuses to switch modes', () => {
|
||||
const a = new Payload(new SSRState('sync'), undefined, 'head');
|
||||
a.push('<meta />');
|
||||
a.local.select_value = 'A';
|
||||
|
||||
const b = new Payload(new SSRState('async'));
|
||||
b.child(async ($$payload) => {
|
||||
await Promise.resolve();
|
||||
$$payload.push('body');
|
||||
});
|
||||
b.global.css.add({ hash: 'h', code: 'c' });
|
||||
b.global.set_title('Title', [1]);
|
||||
b.local.select_value = 'B';
|
||||
b.promises.initial = Promise.resolve();
|
||||
|
||||
expect(() => a.subsume(b)).toThrow(
|
||||
"invariant: A payload cannot switch modes. If you're seeing this, there's a compiler bug. File an issue!"
|
||||
);
|
||||
});
|
||||
|
||||
test('SSRState uid generator uses prefix', () => {
|
||||
const state = new SSRState('sync', 'id-');
|
||||
expect(state.uid()).toBe('id-s1');
|
||||
});
|
||||
|
||||
test('SSRState title ordering favors later lexicographic paths', () => {
|
||||
const state = new SSRState('sync');
|
||||
|
||||
state.set_title('A', [1]);
|
||||
expect(state.get_title()).toBe('A');
|
||||
|
||||
// equal path -> unchanged
|
||||
state.set_title('B', [1]);
|
||||
expect(state.get_title()).toBe('A');
|
||||
|
||||
// earlier -> unchanged
|
||||
state.set_title('C', [0, 9]);
|
||||
expect(state.get_title()).toBe('A');
|
||||
|
||||
// later -> update
|
||||
state.set_title('D', [2]);
|
||||
expect(state.get_title()).toBe('D');
|
||||
|
||||
// longer but same prefix -> update
|
||||
state.set_title('E', [2, 0]);
|
||||
expect(state.get_title()).toBe('E');
|
||||
|
||||
// shorter (earlier) than current with same prefix -> unchanged
|
||||
state.set_title('F', [2]);
|
||||
expect(state.get_title()).toBe('E');
|
||||
});
|
||||
|
||||
describe('async', () => {
|
||||
beforeAll(() => {
|
||||
enable_async_mode_flag();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
disable_async_mode_flag();
|
||||
});
|
||||
|
||||
test('awaiting payload gets async content', async () => {
|
||||
const component = (payload: Payload) => {
|
||||
payload.push('1');
|
||||
payload.child(async ($$payload) => {
|
||||
await Promise.resolve();
|
||||
$$payload.push('2');
|
||||
});
|
||||
payload.push('3');
|
||||
};
|
||||
|
||||
const result = await Payload.render(component as unknown as Component);
|
||||
expect(result.head).toBe('');
|
||||
expect(result.body).toBe('<!--[--><!--[-->123<!--]--><!--]-->');
|
||||
expect(() => result.html).toThrow('html_deprecated');
|
||||
});
|
||||
|
||||
test('compact schedules followup when compaction input is async', async () => {
|
||||
const component = (payload: Payload) => {
|
||||
payload.push('a');
|
||||
payload.child(async ($$payload) => {
|
||||
await Promise.resolve();
|
||||
$$payload.push('X');
|
||||
});
|
||||
payload.push('b');
|
||||
payload.compact({
|
||||
start: 0,
|
||||
fn: async (content) => ({
|
||||
body: content.body.toLowerCase(),
|
||||
head: await Promise.resolve('')
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
const { body, head } = await Payload.render(component as unknown as Component);
|
||||
expect(head).toBe('');
|
||||
expect(body).toBe('<!--[--><!--[-->axb<!--]--><!--]-->');
|
||||
});
|
||||
|
||||
test('push accepts async functions in async context', async () => {
|
||||
const component = (payload: Payload) => {
|
||||
payload.push('a');
|
||||
payload.push(async () => {
|
||||
await Promise.resolve();
|
||||
return 'b';
|
||||
});
|
||||
payload.push('c');
|
||||
};
|
||||
|
||||
const { head, body } = await Payload.render(component as unknown as Component);
|
||||
expect(head).toBe('');
|
||||
expect(body).toBe('<!--[--><!--[-->abc<!--]--><!--]-->');
|
||||
});
|
||||
|
||||
test('push handles async functions with different timing', async () => {
|
||||
const component = (payload: Payload) => {
|
||||
payload.push(async () => {
|
||||
await Promise.resolve();
|
||||
return 'fast';
|
||||
});
|
||||
payload.push(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
return 'slow';
|
||||
});
|
||||
payload.push('sync');
|
||||
};
|
||||
|
||||
const { head, body } = await Payload.render(component as unknown as Component);
|
||||
expect(head).toBe('');
|
||||
expect(body).toBe('<!--[--><!--[-->fastslowsync<!--]--><!--]-->');
|
||||
});
|
||||
|
||||
test('push async functions work with head content type', async () => {
|
||||
const component = (payload: Payload) => {
|
||||
payload.child(($$payload) => {
|
||||
$$payload.push(async () => {
|
||||
await Promise.resolve();
|
||||
return '<title>Async Title</title>';
|
||||
});
|
||||
}, 'head');
|
||||
};
|
||||
|
||||
const { head, body } = await Payload.render(component as unknown as Component);
|
||||
expect(body).toBe('<!--[--><!--[--><!--]--><!--]-->');
|
||||
expect(head).toBe('<title>Async Title</title>');
|
||||
});
|
||||
|
||||
test('push async functions can be mixed with child payloads', async () => {
|
||||
const component = (payload: Payload) => {
|
||||
payload.push('start-');
|
||||
payload.push(async () => {
|
||||
await Promise.resolve();
|
||||
return 'async-';
|
||||
});
|
||||
payload.child(($$payload) => {
|
||||
$$payload.push('child-');
|
||||
});
|
||||
payload.push('-end');
|
||||
};
|
||||
|
||||
const { head, body } = await Payload.render(component as unknown as Component);
|
||||
expect(head).toBe('');
|
||||
expect(body).toBe('<!--[--><!--[-->start-async-child--end<!--]--><!--]-->');
|
||||
});
|
||||
|
||||
test('push async functions work with compact operations', async () => {
|
||||
const component = (payload: Payload) => {
|
||||
payload.push('a');
|
||||
payload.push(async () => {
|
||||
await Promise.resolve();
|
||||
return 'b';
|
||||
});
|
||||
payload.push('c');
|
||||
payload.compact({
|
||||
start: 0,
|
||||
fn: (content) => ({ head: '', body: content.body.toUpperCase() })
|
||||
});
|
||||
};
|
||||
|
||||
const { head, body } = await Payload.render(component as unknown as Component);
|
||||
expect(head).toBe('');
|
||||
expect(body).toBe('<!--[--><!--[-->ABC<!--]--><!--]-->');
|
||||
});
|
||||
|
||||
test('push async functions are not supported in sync context', () => {
|
||||
const component = (payload: Payload) => {
|
||||
payload.push('a');
|
||||
payload.push(() => Promise.resolve('b'));
|
||||
};
|
||||
|
||||
expect(() => Payload.render(component as unknown as Component).body).toThrow('await_invalid');
|
||||
expect(() => Payload.render(component as unknown as Component).html).toThrow('await_invalid');
|
||||
expect(() => Payload.render(component as unknown as Component).head).toThrow('await_invalid');
|
||||
});
|
||||
|
||||
test('on_destroy yields callbacks in the correct order', async () => {
|
||||
const destroyed: string[] = [];
|
||||
const component = (payload: Payload) => {
|
||||
payload.component((payload) => {
|
||||
payload.on_destroy(() => destroyed.push('a'));
|
||||
// children should not alter relative order
|
||||
payload.child(async (payload) => {
|
||||
await Promise.resolve();
|
||||
payload.on_destroy(() => destroyed.push('b'));
|
||||
payload.on_destroy(() => destroyed.push('b*'));
|
||||
});
|
||||
// but child components should
|
||||
payload.component((payload) => {
|
||||
payload.on_destroy(() => destroyed.push('c'));
|
||||
});
|
||||
payload.child((payload) => {
|
||||
payload.on_destroy(() => destroyed.push('d'));
|
||||
});
|
||||
payload.component((payload) => {
|
||||
payload.on_destroy(() => destroyed.push('e'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
await Payload.render(component as unknown as Component);
|
||||
expect(destroyed).toEqual(['c', 'e', 'a', 'b', 'b*', 'd']);
|
||||
});
|
||||
});
|
@ -0,0 +1,17 @@
|
||||
/* This file is generated by scripts/process-messages/index.js. Do not edit! */
|
||||
|
||||
import { DEV } from 'esm-env';
|
||||
|
||||
var bold = 'font-weight: bold';
|
||||
var normal = 'font-weight: normal';
|
||||
|
||||
/**
|
||||
* Attempted to use asynchronous rendering without `experimental.async` enabled
|
||||
*/
|
||||
export function experimental_async_ssr() {
|
||||
if (DEV) {
|
||||
console.warn(`%c[svelte] experimental_async_ssr\n%cAttempted to use asynchronous rendering without \`experimental.async\` enabled\nhttps://svelte.dev/e/experimental_async_ssr`, bold, normal);
|
||||
} else {
|
||||
console.warn(`https://svelte.dev/e/experimental_async_ssr`);
|
||||
}
|
||||
}
|
@ -1 +1 @@
|
||||
<!--[--><input> <p>Hello world!</p><!--]-->
|
||||
<!--[--><!--[--><input> <p>Hello world!</p><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--> <p>start</p><!--[--><p>cond</p><!--]--><!--]-->
|
||||
<!--[--><!--[--> <p>start</p><!--[--><p>cond</p><!--]--><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><h1>Hello everybody!</h1><!--]-->
|
||||
<!--[--><!--[--><h1>Hello everybody!</h1><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><ul><!--[--><li>a</li><!--]--></ul> <ul><!--[--><li>a</li><!--]--></ul> <ul><!--[--><li>a</li><!--]--></ul> <!--[--><li>a</li> <li>a</li><!--]--> <!--[--><li>a</li> <li>a</li><!--]--> <!--[--><li>a</li> <li>a</li><!--]--><!--]-->
|
||||
<!--[--><!--[--><ul><!--[--><li>a</li><!--]--></ul> <ul><!--[--><li>a</li><!--]--></ul> <ul><!--[--><li>a</li><!--]--></ul> <!--[--><li>a</li> <li>a</li><!--]--> <!--[--><li>a</li> <li>a</li><!--]--> <!--[--><li>a</li> <li>a</li><!--]--><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><!--[!--><p>a</p><!--]--> <!--[--><p>empty</p><!--]--><!--]-->
|
||||
<!--[--><!--[--><!--[!--><p>a</p><!--]--> <!--[--><p>empty</p><!--]--><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><ul><!--[--><li>a</li><!--]--></ul> <ul><!--[--><li>a</li><!--]--></ul> <ul><!--[--><li>a</li><!--]--></ul> <!--[--><li>a</li> <li>a</li><!--]--> <!--[--><li>a</li> <li>a</li><!--]--> <!--[--><li>a</li> <li>a</li><!--]--><!--]-->
|
||||
<!--[--><!--[--><ul><!--[--><li>a</li><!--]--></ul> <ul><!--[--><li>a</li><!--]--></ul> <ul><!--[--><li>a</li><!--]--></ul> <!--[--><li>a</li> <li>a</li><!--]--> <!--[--><li>a</li> <li>a</li><!--]--> <!--[--><li>a</li> <li>a</li><!--]--><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><ul><!--[--><li>a</li><li>b</li><!--]--></ul> <ul><!--[--><li>a</li><li>b</li><!--]--></ul> <ul><!--[--><li>a</li><li>b</li><!--]--></ul> <!--[--><li>a</li> <li>a</li><li>b</li> <li>b</li><!--]--> <!--[--><li>a</li> <li>a</li><li>b</li> <li>b</li><!--]--> <!--[--><li>a</li> <li>a</li><li>b</li> <li>b</li><!--]--><!--]-->
|
||||
<!--[--><!--[--><ul><!--[--><li>a</li><li>b</li><!--]--></ul> <ul><!--[--><li>a</li><li>b</li><!--]--></ul> <ul><!--[--><li>a</li><li>b</li><!--]--></ul> <!--[--><li>a</li> <li>a</li><li>b</li> <li>b</li><!--]--> <!--[--><li>a</li> <li>a</li><li>b</li> <li>b</li><!--]--> <!--[--><li>a</li> <li>a</li><li>b</li> <li>b</li><!--]--><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><div class="bar"></div><!--]-->
|
||||
<!--[--><!--[--><div class="bar"></div><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><div class="bar"></div><!--]-->
|
||||
<!--[--><!--[--><div class="bar"></div><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><div></div> <!--[--><div></div> <div></div><!--]--><!--]-->
|
||||
<!--[--><!--[--><div></div> <!--[--><div></div> <div></div><!--]--><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><!--[--><!--]--> <div><!----><!----></div> hello<!--]-->
|
||||
<!--[--><!--[--><!--[--><!--]--> <div><!----><!----></div> hello<!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><!--[!--><p>foo</p><!--]--><!--]-->
|
||||
<!--[--><!--[--><!--[!--><p>foo</p><!--]--><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><!---->.<input><!--]-->
|
||||
<!--[--><!--[--><!---->.<input><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><input type="text"><!--]-->
|
||||
<!--[--><!--[--><input type="text"><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><noscript>JavaScript is required for this site.</noscript> <h1>Hello!</h1><p>Count: 1</p><!--]-->
|
||||
<!--[--><!--[--><noscript>JavaScript is required for this site.</noscript> <h1>Hello!</h1><p>Count: 1</p><!--]--><!--]-->
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!--[--><pre>static content no line</pre> <pre> static content ignored line
|
||||
<!--[--><!--[--><pre>static content no line</pre> <pre> static content ignored line
|
||||
</pre> <pre>
|
||||
static content relevant line
|
||||
</pre> <pre><div><span></span></div>
|
||||
</pre> <pre>
|
||||
<div><span></span></div>
|
||||
</pre><!--]-->
|
||||
</pre><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><div></div><!--]-->
|
||||
<!--[--><!--[--><div></div><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><a href="/foo">foo</a> <a href="/foo">foo</a><!--]-->
|
||||
<!--[--><!--[--><a href="/foo">foo</a> <a href="/foo">foo</a><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><h1>call <a href="tel:+636-555-3226">+636-555-3226</a> now<span>!</span></h1><!--]-->
|
||||
<!--[--><!--[--><h1>call <a href="tel:+636-555-3226">+636-555-3226</a> now<span>!</span></h1><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><!----><script>{}<!----></script><!----><!--]-->
|
||||
<!--[--><!--[--><!----><script>{}<!----></script><!----><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><p>hydrated</p><!--]-->
|
||||
<!--[--><!--[--><p>hydrated</p><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><!--[--><p>child</p><!--]--> <!--[--><p>child</p><p>child</p><p>child</p><!--]--><!--]-->
|
||||
<!--[--><!--[--><!--[--><p>child</p><!--]--> <!--[--><p>child</p><p>child</p><p>child</p><!--]--><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><!--[--><p>thing</p><!--]--> <!--[--><p>thing</p><p>thing</p><p>thing</p><!--]--><!--]-->
|
||||
<!--[--><!--[--><!--[--><p>thing</p><!--]--> <!--[--><p>thing</p><p>thing</p><p>thing</p><!--]--><!--]--><!--]-->
|
||||
|
@ -1,2 +1,2 @@
|
||||
<!-- unrelated comment -->
|
||||
<!--[--><!--[-->hello<!--]--><!--]-->
|
||||
<!--[--><!--[--><!--[-->hello<!--]--><!--]--><!--]-->
|
||||
|
@ -1,2 +1,2 @@
|
||||
<!-- unrelated comment -->
|
||||
<!--[--><!--[-->hello<!--]--><!--]-->
|
||||
<!--[--><!--[--><!--[-->hello<!--]--><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><span><span></span></span><!--]-->
|
||||
<!--[--><!--[--><span><span></span></span><!--]--><!--]-->
|
||||
|
@ -1 +1 @@
|
||||
<!--[--><!---->x<!--]-->
|
||||
<!--[--><!--[--><!---->x<!--]--><!--]-->
|
||||
|
@ -1,2 +1,2 @@
|
||||
<!--[-->
|
||||
<main><p>nested</p><!----></main><!--]-->
|
||||
<!--[--><!--[-->
|
||||
<main><p>nested</p><!----></main><!--]--><!--]-->
|
@ -0,0 +1,7 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
mode: ['server'],
|
||||
|
||||
error: 'await_invalid'
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue