From d7eb9027f638c1ebc4872f2832d5de400bacdca3 Mon Sep 17 00:00:00 2001 From: Michael Rawlings Date: Tue, 28 Nov 2023 12:43:37 -0500 Subject: [PATCH] fix: interop closures w/ tagsapi. large refactor of html writer. --- packages/runtime/src/html/index.ts | 1 + packages/runtime/src/html/reorder-runtime.ts | 1 + packages/runtime/src/html/writer.ts | 653 ++++++++++-------- .../__snapshots__/resume.expected.md | 273 ++++++++ .../__snapshots__/ssr.expected.md | 54 +- .../interop-nested-tags-to-class/test.ts | 3 - .../src/__tests__/main.test.ts | 15 +- .../__snapshots__/ssr.expected.md | 6 +- .../__snapshots__/ssr.expected.md | 12 +- .../__snapshots__/ssr-sanitized.expected.md | 2 +- .../error-async/__snapshots__/ssr.expected.md | 25 +- .../__snapshots__/ssr-sanitized.expected.md | 4 - .../error-sync/__snapshots__/ssr.expected.md | 25 +- .../translator/src/__tests__/main.test.ts | 63 +- 14 files changed, 760 insertions(+), 377 deletions(-) create mode 100644 packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/__snapshots__/resume.expected.md diff --git a/packages/runtime/src/html/index.ts b/packages/runtime/src/html/index.ts index 999b8db29..ac9ac57ea 100644 --- a/packages/runtime/src/html/index.ts +++ b/packages/runtime/src/html/index.ts @@ -25,6 +25,7 @@ export { markResumeControlEnd, markResumeControlSingleNodeEnd, createRenderFn, + $_streamData, } from "./writer"; export { createTemplate } from "./template"; diff --git a/packages/runtime/src/html/reorder-runtime.ts b/packages/runtime/src/html/reorder-runtime.ts index 683b5d7f2..1f1fdf2b5 100644 --- a/packages/runtime/src/html/reorder-runtime.ts +++ b/packages/runtime/src/html/reorder-runtime.ts @@ -44,6 +44,7 @@ export default function ( refNode = (walker as any)[id + "/"]; while ( + targetNode && ((nextNode = targetNode!.nextSibling), targetParent.removeChild(targetNode!) !== refNode) ) { diff --git a/packages/runtime/src/html/writer.ts b/packages/runtime/src/html/writer.ts index ce9a65229..bad10d479 100644 --- a/packages/runtime/src/html/writer.ts +++ b/packages/runtime/src/html/writer.ts @@ -1,4 +1,9 @@ -import { Context, pushContext, setContext } from "../common/context"; +import { + Context, + popContext, + pushContext, + setContext, +} from "../common/context"; import { type Accessor, type Renderer, ResumeSymbols } from "../common/types"; import reorderRuntime from "./reorder-runtime"; import { Serializer } from "./serializer"; @@ -9,6 +14,8 @@ const reorderRuntimeString = String(reorderRuntime).replace( runtimeId ); +type PartialScope = Record | unknown[]; + export interface Writable { write(data: string): void; end(): void; @@ -16,67 +23,71 @@ export interface Writable { emit(name: string, data: unknown): void; } -type PartialScope = Record | unknown[]; -let $_buffer: Buffer | null = null; -let $_stream: Writable | null = null; -let $_flush: typeof flushToStream | null = null; -let $_promises: Array & { isPlaceholder?: true }> | null = - null; +interface Buffer { + stream?: Writable; + pending: boolean; + flushed: boolean; + disabled: boolean; + next: Buffer | null; + prev: Buffer | null; + content: string; + calls: string; + scopes: Record | null; + onAsync?: (complete: boolean, isPlaceholder?: boolean) => void; + onReject?: (err: Error) => void; +} -let $_streamData: { +interface StreamData { scopeId: number; tagId: number; placeholderId: number; scopeLookup: Map; runtimeFlushed: boolean; serializer?: Serializer; -} | null = null; - -export function nextTagId() { - return "s" + $_streamData!.tagId++; } -export function nextPlaceholderId() { - return $_streamData!.placeholderId++; -} +let $_buffer: Buffer | null = null; +export let $_streamData: StreamData | null = null; export function createRenderFn(renderer: Renderer) { type Input = Parameters[0]; - return async ( + return ( stream: Writable, input: Input = {}, context: Record = {}, - streamData: Partial> = {} + streamState: Partial = {} ) => { - $_buffer = createBuffer(); - $_stream = stream; - $_flush = flushToStream; - $_streamData = streamData as typeof $_streamData; - streamData.scopeId ??= 0; - streamData.tagId ??= 0; - streamData.placeholderId ??= 0; - streamData.scopeLookup ??= new Map(); - streamData.runtimeFlushed ??= false; - streamData.serializer ??= undefined; + let remainingChildren = 1; + + const originalBuffer = $_buffer; + const originalStreamState = $_streamData; + const reject = (err: Error) => { + stream.emit("error", err); + }; + const async = (complete: boolean) => { + remainingChildren += complete ? -1 : 1; + if (!remainingChildren) { + setImmediate(() => stream.end()); + } + }; + + $_buffer = createInitialBuffer(stream); + $_streamData = createStreamState(streamState); pushContext("$", context); - try { - let renderedPromises: typeof $_promises; - try { - renderer(input); - } finally { - renderedPromises = $_promises; - $_flush(); - clearScope(); - } + $_buffer.onReject = reject; + $_buffer.onAsync = async; - if (renderedPromises) { - await Promise.all(renderedPromises); - } + try { + scheduleFlush(); + renderer(input); + async(true); } catch (err) { - stream.emit("error", err); + reject(err as Error); } finally { - stream.end(); + $_buffer = originalBuffer; + $_streamData = originalStreamState; + popContext(); } }; } @@ -87,68 +98,157 @@ export function write(data: string) { const TARGET_BUFFER_SIZE = 64000; export function maybeFlush() { - if ( - $_flush === flushToStream && - $_buffer!.content.length > TARGET_BUFFER_SIZE - ) { - flushToStream(); + if (!$_buffer!.prev && $_buffer!.content.length > TARGET_BUFFER_SIZE) { + // TODO: figure out if we can do this + // The idea is to flush in a `` if the buffer gets too large. + // + // However, a synchronous flush will break the owner scope reference + // as things are currently implemented: the owner scope object will + // not have been created if you flush in a scope that closes over it + // However, a scheduled flush will be too late: + // the entire contents of the `` will have been written + // by the time the flush occurs defeating the purpose + // Additionally, because we aren't eagerly merging buffers, + // buffer.content.length isn't necessarily 100% accurate } } -function flushToStream() { - writeResumeScript(); - $_stream!.write($_buffer!.content); - if ($_stream!.flush) { - $_stream!.flush(); +export function scheduleFlush() { + const buffer = $_buffer!; + const streamState = $_streamData!; + if (!buffer.prev) { + setImmediate(() => flushToStream(buffer, streamState)); } - clearBuffer($_buffer!); +} + +function flushToStream(buffer: Buffer, streamState: StreamData) { + while (buffer.prev) buffer = buffer.prev; + if (buffer.disabled) return; + + const stream = buffer.stream!; + + let { content, calls, scopes } = buffer; + buffer.flushed = true; + while (!buffer.pending && buffer.next) { + // TODO: we shouldn't need to clear here + clearBuffer(buffer); + buffer = buffer.next; + buffer.prev = null; + buffer.flushed = true; + content += buffer.content; + calls += buffer.calls; + if (buffer.scopes) { + if (scopes) { + Object.assign(scopes, buffer.scopes); + } else { + scopes = buffer.scopes; + } + } + } + const data = content + getResumeScript(calls, scopes, streamState); + + if (data) { + stream.write(data); + stream.flush?.(); + } + + // TODO: we should only have to call clearBuffer if the buffer is pending + // (which means it will flush again in the future). Otherwise, it can just + // be garbage collected. + clearBuffer(buffer); +} + +function createStreamState(state: Partial): StreamData { + state.scopeId ??= 0; + state.tagId ??= 0; + state.placeholderId ??= 0; + state.scopeLookup ??= new Map(); + state.runtimeFlushed ??= false; + return state as StreamData; +} + +function createNextBuffer(prevBuffer: Buffer): Buffer { + const newBuffer = { + stream: prevBuffer.stream, + pending: false, + flushed: false, + disabled: false, + prev: prevBuffer, + next: prevBuffer?.next ?? null, + content: "", + calls: "", + scopes: null, + onReject: prevBuffer.onReject, + onAsync: prevBuffer.onAsync, + }; + if (prevBuffer.next) { + prevBuffer.next.prev = prevBuffer; + } + prevBuffer.next = newBuffer; + return newBuffer; +} + +function createDetatchedBuffer(parentBuffer: Buffer): Buffer { + return { + stream: parentBuffer.stream, + pending: false, + flushed: false, + disabled: true, + prev: null, + next: null, + content: "", + calls: "", + scopes: null, + onReject: parentBuffer.onReject, + onAsync: parentBuffer.onAsync, + }; +} + +function createInitialBuffer(stream: Writable): Buffer { + return { + stream, + pending: false, + flushed: false, + disabled: false, + prev: null, + next: null, + content: "", + calls: "", + scopes: null, + onReject: undefined, + onAsync: undefined, + }; } export function fork( promise: Promise, renderResult: (result: T) => void ) { - $_flush!(); - - let resolved = false; - let targetFlush = $_flush!; - const forkedBuffer = createBuffer(); - - $_promises = $_promises || []; - $_promises.push( - resolveWithScope( - promise, - (result) => { - resolved = true; - try { - renderResult(result); - } finally { - mergeBuffers(forkedBuffer, $_buffer!); - if ($_promises) { - const originalTargetFlush = targetFlush; - targetFlush = $_flush!; - Promise.all($_promises).then( - () => (targetFlush = originalTargetFlush) - ); - } - } - }, - (err) => { - resolved = true; - $_buffer = forkedBuffer; - $_flush = targetFlush; - throw err; + resolveWithScope( + promise, + (result) => { + try { + $_buffer!.pending = false; + renderResult(result); + } catch (err) { + $_buffer!.onReject?.(err as Error); + } finally { + $_buffer!.onAsync?.(true); } - ) + }, + (err) => { + try { + $_buffer!.onReject?.(err); + } finally { + $_buffer!.onAsync?.(true); + } + } ); - $_flush = () => { - if (resolved) { - targetFlush(); - } else { - mergeBuffers($_buffer!, forkedBuffer); - } - }; + scheduleFlush(); + $_buffer!.pending = true; + $_buffer!.onAsync?.(false); + $_buffer = createNextBuffer($_buffer!); } export function tryCatch( @@ -158,45 +258,44 @@ export function tryCatch( const id = nextPlaceholderId(); let err: Error | null = null; - const originalPromises = $_promises; const originalBuffer = $_buffer!; - const originalFlush = $_flush!; - const tryBuffer = createBuffer(); + const tryBuffer = createDetatchedBuffer(originalBuffer); + let finalTryBuffer: Buffer; - $_flush = () => { - $_buffer = originalBuffer; - $_flush = originalFlush; - markReplaceStart(id); - mergeBuffers(tryBuffer, $_buffer); - $_flush(); + tryBuffer.onReject = (asyncErr) => { + const errorBuffer = createDetatchedBuffer(originalBuffer); + $_buffer = errorBuffer; + renderError(asyncErr); + const finalErrorBuffer = $_buffer; + replaceBuffers( + id, + tryBuffer, + finalTryBuffer, + errorBuffer, + finalErrorBuffer + ); }; try { $_buffer = tryBuffer; - $_promises = null; renderBody(); } catch (_err) { err = _err as Error; } finally { - const childPromises = $_promises; - $_promises = originalPromises; - if (err) { $_buffer = originalBuffer; - $_flush = originalFlush; renderError(err); - } else if (!childPromises) { - $_buffer = originalBuffer; - $_flush = originalFlush; - mergeBuffers(tryBuffer, $_buffer); } else { - markReplaceEnd(id); - $_promises = $_promises || []; - $_promises.push( - resolveWithScope(Promise.all(childPromises), null, (asyncErr) => { - renderReplacement(renderError, asyncErr, id); - }) - ); + tryBuffer.disabled = false; + originalBuffer.next = tryBuffer; + tryBuffer.prev = originalBuffer; + if ($_buffer !== tryBuffer) { + tryBuffer.content = `` + tryBuffer.content; + markReplaceEnd(id); + finalTryBuffer = $_buffer!; + $_buffer = createNextBuffer(finalTryBuffer); + } + $_buffer.onReject = originalBuffer.onReject; } } } @@ -206,73 +305,110 @@ export function tryPlaceholder( renderPlaceholder: () => void ) { const originalBuffer = $_buffer!; - const originalPromises = $_promises; - const originalFlush = $_flush!; - const asyncBuffer = createBuffer(); + const asyncBuffer = createDetatchedBuffer(originalBuffer); + let id: number, + placeholderBuffer: Buffer, + finalPlaceholderBuffer: Buffer, + finalAsyncBuffer: Buffer; + let remainingChildren = 0; + let remainingPlaceholders = 0; - let resolved = false; - const targetFlush = originalFlush; - $_flush = () => { - if (resolved) { - targetFlush(); + asyncBuffer.onAsync = (complete: boolean, isPlaceholder?: boolean) => { + const delta = complete ? -1 : 1; + if (isPlaceholder) { + remainingPlaceholders += delta; } else { - mergeBuffers($_buffer!, asyncBuffer); + remainingChildren += delta; } - }; - $_buffer = createBuffer(); - $_promises = null; - - renderBody(); - $_flush(); - - const childPromises = $_promises!; - $_buffer = originalBuffer; - $_promises = originalPromises; - $_flush = originalFlush; - - if (childPromises) { - const contentPromises: Array> = []; - const placeholderPromises: Array< - Promise & { isPlaceholder: true } - > = []; - for (const promise of childPromises) { - if (promise.isPlaceholder) { - placeholderPromises.push( - promise as Promise & { - isPlaceholder: true; - } + if (!remainingChildren) { + if (!isPlaceholder) { + // last child has finished, replace the placeholder + // however, the replacement content may contain its own placeholder(s) + replaceBuffers( + id, + placeholderBuffer, + finalPlaceholderBuffer, + asyncBuffer, + finalAsyncBuffer ); - } else { - contentPromises.push(promise); + } + if (!remainingPlaceholders) { + // all async content under this placeholder is complete + originalBuffer.onAsync?.(true, true); } } + }; - if (placeholderPromises.length) { - ($_promises = originalPromises || []).push(...placeholderPromises); - } else { - $_promises = originalPromises; - } + $_buffer = asyncBuffer; + renderBody(); - if (contentPromises.length) { - const id = nextPlaceholderId(); - $_promises = $_promises || []; - $_promises.push( - Object.assign( - resolveWithScope(Promise.all(contentPromises), () => { - resolved = true; - renderReplacement(mergeBuffers, asyncBuffer, id); - }), - { isPlaceholder: true } as const - ) - ); - markReplaceStart(id); - renderPlaceholder(); - markReplaceEnd(id); - return; - } + if ($_buffer === asyncBuffer) { + originalBuffer.next = asyncBuffer; + asyncBuffer.prev = originalBuffer; + asyncBuffer.disabled = false; + asyncBuffer.onAsync = originalBuffer.onAsync; + } else { + id = nextPlaceholderId(); + placeholderBuffer = createNextBuffer(originalBuffer); + finalAsyncBuffer = $_buffer; + $_buffer = placeholderBuffer; + markReplaceStart(id); + renderPlaceholder(); + markReplaceEnd(id); + finalPlaceholderBuffer = $_buffer; + $_buffer = createNextBuffer(finalPlaceholderBuffer); + originalBuffer.onAsync?.(false, true); } +} - mergeBuffers(asyncBuffer, originalBuffer); +function resolveWithScope( + promise: Promise, + onResolve: null | ((r: T) => unknown), + onReject?: (e: Error) => unknown +) { + const originalBuffer = $_buffer; + const originalStreamState = $_streamData; + const originalContext = Context; + + return promise.then( + onResolve && + ((result) => { + $_streamData = originalStreamState; + $_buffer = originalBuffer; + scheduleFlush(); + + try { + setContext(originalContext); + onResolve(result); + } finally { + clearScope(); + } + }), + onReject && + ((error) => { + $_streamData = originalStreamState; + $_buffer = originalBuffer; + scheduleFlush(); + + try { + setContext(originalContext); + onReject(error); + } finally { + clearScope(); + } + }) + ); +} + +function clearBuffer(buffer: Buffer) { + buffer.content = ""; + buffer.calls = ""; + buffer.scopes = null; +} + +function clearScope() { + $_buffer = $_streamData = null; + setContext(null); } /* Async */ @@ -285,15 +421,61 @@ export function markReplaceEnd(id: number) { return ($_buffer!.content += ``); } -function renderReplacement(render: (data: T) => void, data: T, id: number) { +function replaceBuffers( + id: number, + placeholderStart: Buffer, + placeholderEnd: Buffer, + replacementStart: Buffer, + replacementEnd: Buffer +) { + if (placeholderStart.flushed) { + addReplacementWrapper(id, replacementStart, replacementEnd); + + let next: Buffer | null = placeholderEnd.next; + if (placeholderEnd.flushed) { + while (next && !next.pending && next.flushed) { + next = next.next; + } + } else { + // TODO: ensure the remaining original content cannot flush + } + + if (next) { + replacementStart.next = next; + next.prev = replacementEnd; + } + + $_buffer = replacementStart; + scheduleFlush(); + } else { + const prev = placeholderStart.prev; + const next = placeholderEnd.next; + if (prev) { + prev.next = replacementStart; + replacementStart.prev = prev; + } + if (next) { + next.prev = replacementEnd; + replacementEnd.next = next; + } + } + + replacementStart.disabled = false; +} + +function addReplacementWrapper( + id: number, + replacementStart: Buffer, + replacementEnd: Buffer +) { let runtimeCall = runtimeId + ResumeSymbols.VAR_REORDER_RUNTIME; if (!$_streamData!.runtimeFlushed) { runtimeCall = `(${runtimeCall}=${reorderRuntimeString})`; $_streamData!.runtimeFlushed = true; } - $_buffer!.content += ``; - render(data); - $_buffer!.content += ``; + replacementStart.content = + `` + replacementStart.content; + replacementEnd.content += ``; } function marker(id: number) { @@ -302,6 +484,14 @@ function marker(id: number) { /* Hydration */ +export function nextTagId() { + return "s" + $_streamData!.tagId++; +} + +export function nextPlaceholderId() { + return $_streamData!.placeholderId++; +} + export function nextScopeId() { return $_streamData!.scopeId++; } @@ -317,7 +507,10 @@ export function writeEffect(scopeId: number, fnId: string) { export function writeScope( scopeId: number, scope: PartialScope, - assignTo?: PartialScope | PartialScope[] + assignTo: + | PartialScope + | PartialScope[] + | undefined = $_streamData!.scopeLookup.get(scopeId) ) { if (assignTo !== undefined) { if (Array.isArray(assignTo)) { @@ -356,106 +549,24 @@ export function markResumeControlSingleNodeEnd( }${scopeId} ${index} ${childScopeIds ?? ""}>`; } -function writeResumeScript() { - if ($_buffer!.calls || $_buffer!.scopes) { +function getResumeScript( + calls: string, + scopes: Buffer["scopes"], + streamState: StreamData +) { + if (calls || scopes) { let isFirstFlush; - let serializer = $_streamData!.serializer; + let serializer = streamState.serializer; if ((isFirstFlush = !serializer)) { - serializer = $_streamData!.serializer = new Serializer( - $_streamData!.scopeLookup + serializer = streamState.serializer = new Serializer( + streamState.scopeLookup ); } - $_buffer!.content += ``; + }.push(${serializer.stringify(scopes)},[${calls}])`; } -} - -interface Buffer { - content: string; - calls: string; - scopes: Record | null; -} - -function createBuffer() { - return { - content: "", - calls: "", - scopes: null, - } as Buffer; -} - -function mergeBuffers(source: Buffer, target: Buffer = $_buffer!) { - target.content += source.content; - target.calls += source.calls; - if (source.scopes) { - if (target.scopes) { - Object.assign(target.scopes, source.scopes); - } else { - target.scopes = source.scopes; - } - } - clearBuffer(source); -} - -function clearBuffer(buffer: Buffer) { - buffer.content = ""; - buffer.calls = ""; - buffer.scopes = null; -} - -function clearScope() { - $_buffer = $_promises = $_stream = $_flush = $_streamData = null; - setContext(null); -} - -function resolveWithScope( - promise: Promise, - onResolve: null | ((r: T) => unknown), - onReject?: (e: Error) => unknown -) { - const originalStream = $_stream; - const originalBuffer = $_buffer; - const originalFlush = $_flush; - const originalIds = $_streamData; - const originalContext = Context; - - return promise.then( - onResolve && - ((result) => { - $_stream = originalStream; - $_buffer = originalBuffer; - $_flush = originalFlush; - $_streamData = originalIds; - - try { - setContext(originalContext); - onResolve(result); - return $_promises && Promise.all($_promises); - } finally { - $_flush!(); - clearScope(); - } - }), - onReject && - ((error) => { - $_stream = originalStream; - $_buffer = originalBuffer; - $_flush = originalFlush; - $_streamData = originalIds; - - try { - setContext(originalContext); - onReject(error); - return $_promises && Promise.all($_promises); - } finally { - $_flush!(); - clearScope(); - } - }) - ); + return ""; } diff --git a/packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/__snapshots__/resume.expected.md b/packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/__snapshots__/resume.expected.md new file mode 100644 index 000000000..12599ee9d --- /dev/null +++ b/packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/__snapshots__/resume.expected.md @@ -0,0 +1,273 @@ +# Render {} +```html + + + + + + +
+ + + +
+ + + + + +``` + +# Mutations +``` +inserted #document/html0/body1/#text2 +inserted #document/html0/body1/#text5 +removed #comment after #document/html0/body1/script1 +removed #comment after #document/html0/body1/#text5 +inserted #document/html0/body1/div4/#text0 +inserted #document/html0/body1/div4/#text4 +removed #comment after #document/html0/body1/div4/#text0 +removed #comment after #document/html0/body1/div4/script3 +``` + + +# Render +container.querySelector("#tags").click() + +```html + + + + + + +
+ + + +
+ + + + + +``` + +# Mutations +``` +#document/html0/body1/div4/button1/#text0: "0" => "1" +``` + + +# Render +container.querySelector("#class").click() + +```html + + + + + + +
+ + + +
+ + + + + +``` + +# Mutations +``` +#document/html0/body1/button3/#text0: "0" => "1" +``` + + +# Render +container.querySelector("#tags").click() + +```html + + + + + + +
+ + + +
+ + + + + +``` + +# Mutations +``` +#document/html0/body1/div4/button1/#text0: "1" => "2" +``` + + +# Render +container.querySelector("#class").click() + +```html + + + + + + +
+ + + +
+ + + + + +``` + +# Mutations +``` +#document/html0/body1/button3/#text0: "1" => "2" +``` + + +# Render +container.querySelector("#tags").click() + +```html + + + + + + +
+ + + +
+ + + + + +``` + +# Mutations +``` +#document/html0/body1/div4/button1/#text0: "2" => "3" +``` \ No newline at end of file diff --git a/packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/__snapshots__/ssr.expected.md b/packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/__snapshots__/ssr.expected.md index 8cdd0b863..b96a81587 100644 --- a/packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/__snapshots__/ssr.expected.md +++ b/packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/__snapshots__/ssr.expected.md @@ -1,9 +1,9 @@ # Write - + -# Emit error - TypeError: Cannot read properties of null (reading 'isSync') +# Write +
# Render "End" @@ -13,7 +13,35 @@ + + +
+ + + + + +
+ + + + @@ -27,4 +55,22 @@ inserted #document/html0/body1 inserted #document/html0/body1/#comment0 inserted #document/html0/body1/script1 inserted #document/html0/body1/script1/#text0 +inserted #document/html0/body1/#comment2 +inserted #document/html0/body1/button3 +inserted #document/html0/body1/button3/#text0 +inserted #document/html0/body1/div4 +inserted #document/html0/body1/div4/#comment0 +inserted #document/html0/body1/div4/button1 +inserted #document/html0/body1/div4/button1/#text0 +inserted #document/html0/body1/div4/button1/#comment1 +inserted #document/html0/body1/div4/#comment2 +inserted #document/html0/body1/div4/script3 +inserted #document/html0/body1/div4/script3/#text0 +inserted #document/html0/body1/div4/#comment4 +inserted #document/html0/body1/#comment5 +inserted #document/html0/body1/script6 +inserted #document/html0/body1/script6/#text0 +inserted #document/html0/body1/#comment7 +inserted #document/html0/body1/script8 +inserted #document/html0/body1/script8/#text0 ``` \ No newline at end of file diff --git a/packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/test.ts b/packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/test.ts index 7e74a16c9..3939a580e 100644 --- a/packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/test.ts +++ b/packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/test.ts @@ -14,6 +14,3 @@ function clickClass(container: Element) { function clickTags(container: Element) { (container.querySelector("#tags") as HTMLButtonElement).click(); } - -export const skip_ssr = true; -export const skip_resume = true; diff --git a/packages/translator-interop/src/__tests__/main.test.ts b/packages/translator-interop/src/__tests__/main.test.ts index ac96ba8f7..91d378429 100644 --- a/packages/translator-interop/src/__tests__/main.test.ts +++ b/packages/translator-interop/src/__tests__/main.test.ts @@ -141,7 +141,7 @@ describe("translator-interop", () => { document.open(); const tracker = createMutationTracker(browser.window, document); - const writable = { + const writable = (resolve?: () => void) => ({ write(data: string) { buffer += data; tracker.log( @@ -161,19 +161,26 @@ describe("translator-interop", () => { ); document.close(); tracker.logUpdate("End"); + resolve?.(); }, emit(type: string, ...args: unknown[]) { console.log(...args); tracker.log( `# Emit ${type}${args.map((arg) => `\n${indent(arg)}`)}` ); + if (type === "error") { + document.close(); + resolve?.(); + } }, - }; + }); if (serverTemplate.writeTo) { - await serverTemplate.writeTo(writable, input, config.context); + await new Promise((resolve) => + serverTemplate.writeTo(writable(resolve), input, config.context) + ); } else { - await serverTemplate.render(input, writable); + await serverTemplate.render(input, writable()); } tracker.cleanup(); diff --git a/packages/translator/src/__tests__/fixtures/async-nested-resolve-in-order/__snapshots__/ssr.expected.md b/packages/translator/src/__tests__/fixtures/async-nested-resolve-in-order/__snapshots__/ssr.expected.md index f3459dfe9..b0e3eeefe 100644 --- a/packages/translator/src/__tests__/fixtures/async-nested-resolve-in-order/__snapshots__/ssr.expected.md +++ b/packages/translator/src/__tests__/fixtures/async-nested-resolve-in-order/__snapshots__/ssr.expected.md @@ -11,11 +11,7 @@ # Write - defghi - - -# Write - jkl + defghijkl # Render "End" diff --git a/packages/translator/src/__tests__/fixtures/catch-single-reject-async/__snapshots__/ssr.expected.md b/packages/translator/src/__tests__/fixtures/catch-single-reject-async/__snapshots__/ssr.expected.md index 5c0f7bd93..a73951d00 100644 --- a/packages/translator/src/__tests__/fixtures/catch-single-reject-async/__snapshots__/ssr.expected.md +++ b/packages/translator/src/__tests__/fixtures/catch-single-reject-async/__snapshots__/ssr.expected.md @@ -3,11 +3,7 @@ # Write - defg - - -# Write - ERROR! + ERROR!efg # Render "End" @@ -28,8 +24,6 @@ inserted #document/html0/body1 inserted #document/html0/body1/#text0 inserted #comment inserted #text -inserted #comment -inserted #document/html0/body1/#text2 inserted t inserted #document/html0/body1/#text1 inserted script @@ -37,8 +31,8 @@ inserted script/#text0 removed #document/html0/body1/#text1 in t inserted #document/html0/body1/#text1 removed script after t -removed t after #document/html0/body1/#text2 +removed t after #text removed #comment after #document/html0/body1/#text1 removed #text after #document/html0/body1/#text1 -removed #comment after #document/html0/body1/#text1 +#document/html0/body1/#text1: "ERROR!" => "ERROR!efg" ``` \ No newline at end of file diff --git a/packages/translator/src/__tests__/fixtures/error-async/__snapshots__/ssr-sanitized.expected.md b/packages/translator/src/__tests__/fixtures/error-async/__snapshots__/ssr-sanitized.expected.md index 5dfd08704..76d6f9a67 100644 --- a/packages/translator/src/__tests__/fixtures/error-async/__snapshots__/ssr-sanitized.expected.md +++ b/packages/translator/src/__tests__/fixtures/error-async/__snapshots__/ssr-sanitized.expected.md @@ -1,4 +1,4 @@ # Render "End" ```html -ab +a ``` \ No newline at end of file diff --git a/packages/translator/src/__tests__/fixtures/error-async/__snapshots__/ssr.expected.md b/packages/translator/src/__tests__/fixtures/error-async/__snapshots__/ssr.expected.md index fb00ce372..f902f3c71 100644 --- a/packages/translator/src/__tests__/fixtures/error-async/__snapshots__/ssr.expected.md +++ b/packages/translator/src/__tests__/fixtures/error-async/__snapshots__/ssr.expected.md @@ -2,28 +2,5 @@ a -# Write - b - - # Emit error - Error: ERROR! - - -# Render "End" -```html - - - - ab - - -``` - -# Mutations -``` -inserted #document/html0 -inserted #document/html0/head0 -inserted #document/html0/body1 -inserted #document/html0/body1/#text0 -``` \ No newline at end of file + Error: ERROR! \ No newline at end of file diff --git a/packages/translator/src/__tests__/fixtures/error-sync/__snapshots__/ssr-sanitized.expected.md b/packages/translator/src/__tests__/fixtures/error-sync/__snapshots__/ssr-sanitized.expected.md index 76d6f9a67..e69de29bb 100644 --- a/packages/translator/src/__tests__/fixtures/error-sync/__snapshots__/ssr-sanitized.expected.md +++ b/packages/translator/src/__tests__/fixtures/error-sync/__snapshots__/ssr-sanitized.expected.md @@ -1,4 +0,0 @@ -# Render "End" -```html -a -``` \ No newline at end of file diff --git a/packages/translator/src/__tests__/fixtures/error-sync/__snapshots__/ssr.expected.md b/packages/translator/src/__tests__/fixtures/error-sync/__snapshots__/ssr.expected.md index dd2e3b509..71f74fe2f 100644 --- a/packages/translator/src/__tests__/fixtures/error-sync/__snapshots__/ssr.expected.md +++ b/packages/translator/src/__tests__/fixtures/error-sync/__snapshots__/ssr.expected.md @@ -1,25 +1,2 @@ -# Write - a - - # Emit error - Error: ERROR! - - -# Render "End" -```html - - - - a - - -``` - -# Mutations -``` -inserted #document/html0 -inserted #document/html0/head0 -inserted #document/html0/body1 -inserted #document/html0/body1/#text0 -``` \ No newline at end of file + Error: ERROR! \ No newline at end of file diff --git a/packages/translator/src/__tests__/main.test.ts b/packages/translator/src/__tests__/main.test.ts index e5ad07bf4..d66afdbc4 100644 --- a/packages/translator/src/__tests__/main.test.ts +++ b/packages/translator/src/__tests__/main.test.ts @@ -169,35 +169,42 @@ describe("translator-tags", () => { const tracker = createMutationTracker(browser.window, document); - await serverTemplate.writeTo( - { - write(data: string) { - buffer += data; - tracker.log( - `# Write\n${indent( - data.replace(reorderRuntimeString, "REORDER_RUNTIME") - )}` - ); + await new Promise((resolve) => + serverTemplate.writeTo( + { + write(data: string) { + buffer += data; + tracker.log( + `# Write\n${indent( + data.replace(reorderRuntimeString, "REORDER_RUNTIME") + )}` + ); + }, + flush() { + // tracker.logUpdate("Flush"); + // document.write(buffer); + // buffer = ""; + }, + end(data?: string) { + document.write(buffer + (data || "")); + document.close(); + tracker.logUpdate("End"); + resolve(); + }, + emit(type: string, ...args: unknown[]) { + // console.log(...args); + tracker.log( + `# Emit ${type}${args.map((arg) => `\n${indent(arg)}`)}` + ); + if (type === "error") { + document.close(); + resolve(); + } + }, }, - flush() { - // tracker.logUpdate("Flush"); - // document.write(buffer); - // buffer = ""; - }, - end(data?: string) { - document.write(buffer + (data || "")); - document.close(); - tracker.logUpdate("End"); - }, - emit(type: string, ...args: unknown[]) { - // console.log(...args); - tracker.log( - `# Emit ${type}${args.map((arg) => `\n${indent(arg)}`)}` - ); - }, - }, - input, - config.context + input, + config.context + ) ); tracker.cleanup();