fix: interop closures w/ tagsapi. large refactor of html writer.

This commit is contained in:
Michael Rawlings 2023-11-28 12:43:37 -05:00
parent 8911c597e0
commit d7eb9027f6
14 changed files with 760 additions and 377 deletions

View File

@ -25,6 +25,7 @@ export {
markResumeControlEnd,
markResumeControlSingleNodeEnd,
createRenderFn,
$_streamData,
} from "./writer";
export { createTemplate } from "./template";

View File

@ -44,6 +44,7 @@ export default function (
refNode = (walker as any)[id + "/"];
while (
targetNode &&
((nextNode = targetNode!.nextSibling),
targetParent.removeChild(targetNode!) !== refNode)
) {

View File

@ -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<string | number, unknown> | 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<string | number, unknown> | unknown[];
let $_buffer: Buffer | null = null;
let $_stream: Writable | null = null;
let $_flush: typeof flushToStream | null = null;
let $_promises: Array<Promise<unknown> & { 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<string, PartialScope> | null;
onAsync?: (complete: boolean, isPlaceholder?: boolean) => void;
onReject?: (err: Error) => void;
}
let $_streamData: {
interface StreamData {
scopeId: number;
tagId: number;
placeholderId: number;
scopeLookup: Map<number, PartialScope>;
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<Renderer>[0];
return async (
return (
stream: Writable,
input: Input = {},
context: Record<string, unknown> = {},
streamData: Partial<NonNullable<typeof $_streamData>> = {}
streamState: Partial<StreamData> = {}
) => {
$_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 `<for>` 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 `<for>` 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>): 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<T>(
promise: Promise<T>,
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 = `<!${marker(id)}>` + 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<Promise<unknown>> = [];
const placeholderPromises: Array<
Promise<unknown> & { isPlaceholder: true }
> = [];
for (const promise of childPromises) {
if (promise.isPlaceholder) {
placeholderPromises.push(
promise as Promise<unknown> & {
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<T>(
promise: Promise<T>,
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 += `<!${marker(id)}/>`);
}
function renderReplacement<T>(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 += `<t id="${marker(id)}">`;
render(data);
$_buffer!.content += `</t><script>${runtimeCall}(${id})</script>`;
replacementStart.content =
`<t id="${marker(id)}">` + replacementStart.content;
replacementEnd.content += `</t><script>${runtimeCall}(${id})</script>`;
}
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 += `<script>${
return `<script>${
isFirstFlush
? `(${runtimeId + ResumeSymbols.VAR_RESUME}=[])`
: runtimeId + ResumeSymbols.VAR_RESUME
}.push(${serializer.stringify($_buffer!.scopes)},[${
$_buffer!.calls
}])</script>`;
}.push(${serializer.stringify(scopes)},[${calls}])</script>`;
}
}
interface Buffer {
content: string;
calls: string;
scopes: Record<string, PartialScope> | 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<T>(
promise: Promise<T>,
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 "";
}

View File

@ -0,0 +1,273 @@
# Render {}
```html
<html>
<head />
<body>
<!--M[1-->
<script>
(M$h=[]).push((b,s,h,j,k)=&gt;(k={1:h={_:j={count:0,"#text/0(":b("@marko/tags-compat-5-to-6")(b("packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/components/class-layout.marko"),!0)}},2:{m5c:"s0"}},j["#text/0!"]=h,k),[])
</script>
<button
id="class"
>
0
</button>
<div>
<button
id="tags"
>
0
<!--M*1 #text/1-->
</button>
<!--M*1 #button/0-->
<script>
M$h.push((b,s)=&gt;({1:s[1]}),[1,"packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/template.marko_1_count",1,"packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/template.marko_1_count/subscriber",])
</script>
</div>
<script>
$MC=(window.$MC||[]).concat({"g":{"componentIdToScopeId":{"s0-2":1}},"w":[["s0",0,{},{"f":3}]],"t":["packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/components/class-layout.marko"]})
</script>
<!--M]0 #text/0-->
<script>
M$h.push((b,s)=&gt;({0:s[1]._}),[])
</script>
</body>
</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
<html>
<head />
<body>
<!--M[1-->
<script>
(M$h=[]).push((b,s,h,j,k)=&gt;(k={1:h={_:j={count:0,"#text/0(":b("@marko/tags-compat-5-to-6")(b("packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/components/class-layout.marko"),!0)}},2:{m5c:"s0"}},j["#text/0!"]=h,k),[])
</script>
<button
id="class"
>
0
</button>
<div>
<button
id="tags"
>
1
<!--M*1 #text/1-->
</button>
<!--M*1 #button/0-->
<script>
M$h.push((b,s)=&gt;({1:s[1]}),[1,"packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/template.marko_1_count",1,"packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/template.marko_1_count/subscriber",])
</script>
</div>
<script>
$MC=(window.$MC||[]).concat({"g":{"componentIdToScopeId":{"s0-2":1}},"w":[["s0",0,{},{"f":3}]],"t":["packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/components/class-layout.marko"]})
</script>
<!--M]0 #text/0-->
<script>
M$h.push((b,s)=&gt;({0:s[1]._}),[])
</script>
</body>
</html>
```
# Mutations
```
#document/html0/body1/div4/button1/#text0: "0" => "1"
```
# Render
container.querySelector("#class").click()
```html
<html>
<head />
<body>
<!--M[1-->
<script>
(M$h=[]).push((b,s,h,j,k)=&gt;(k={1:h={_:j={count:0,"#text/0(":b("@marko/tags-compat-5-to-6")(b("packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/components/class-layout.marko"),!0)}},2:{m5c:"s0"}},j["#text/0!"]=h,k),[])
</script>
<button
id="class"
>
1
</button>
<div>
<button
id="tags"
>
1
<!--M*1 #text/1-->
</button>
<!--M*1 #button/0-->
<script>
M$h.push((b,s)=&gt;({1:s[1]}),[1,"packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/template.marko_1_count",1,"packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/template.marko_1_count/subscriber",])
</script>
</div>
<script>
$MC=(window.$MC||[]).concat({"g":{"componentIdToScopeId":{"s0-2":1}},"w":[["s0",0,{},{"f":3}]],"t":["packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/components/class-layout.marko"]})
</script>
<!--M]0 #text/0-->
<script>
M$h.push((b,s)=&gt;({0:s[1]._}),[])
</script>
</body>
</html>
```
# Mutations
```
#document/html0/body1/button3/#text0: "0" => "1"
```
# Render
container.querySelector("#tags").click()
```html
<html>
<head />
<body>
<!--M[1-->
<script>
(M$h=[]).push((b,s,h,j,k)=&gt;(k={1:h={_:j={count:0,"#text/0(":b("@marko/tags-compat-5-to-6")(b("packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/components/class-layout.marko"),!0)}},2:{m5c:"s0"}},j["#text/0!"]=h,k),[])
</script>
<button
id="class"
>
1
</button>
<div>
<button
id="tags"
>
2
<!--M*1 #text/1-->
</button>
<!--M*1 #button/0-->
<script>
M$h.push((b,s)=&gt;({1:s[1]}),[1,"packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/template.marko_1_count",1,"packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/template.marko_1_count/subscriber",])
</script>
</div>
<script>
$MC=(window.$MC||[]).concat({"g":{"componentIdToScopeId":{"s0-2":1}},"w":[["s0",0,{},{"f":3}]],"t":["packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/components/class-layout.marko"]})
</script>
<!--M]0 #text/0-->
<script>
M$h.push((b,s)=&gt;({0:s[1]._}),[])
</script>
</body>
</html>
```
# Mutations
```
#document/html0/body1/div4/button1/#text0: "1" => "2"
```
# Render
container.querySelector("#class").click()
```html
<html>
<head />
<body>
<!--M[1-->
<script>
(M$h=[]).push((b,s,h,j,k)=&gt;(k={1:h={_:j={count:0,"#text/0(":b("@marko/tags-compat-5-to-6")(b("packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/components/class-layout.marko"),!0)}},2:{m5c:"s0"}},j["#text/0!"]=h,k),[])
</script>
<button
id="class"
>
2
</button>
<div>
<button
id="tags"
>
2
<!--M*1 #text/1-->
</button>
<!--M*1 #button/0-->
<script>
M$h.push((b,s)=&gt;({1:s[1]}),[1,"packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/template.marko_1_count",1,"packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/template.marko_1_count/subscriber",])
</script>
</div>
<script>
$MC=(window.$MC||[]).concat({"g":{"componentIdToScopeId":{"s0-2":1}},"w":[["s0",0,{},{"f":3}]],"t":["packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/components/class-layout.marko"]})
</script>
<!--M]0 #text/0-->
<script>
M$h.push((b,s)=&gt;({0:s[1]._}),[])
</script>
</body>
</html>
```
# Mutations
```
#document/html0/body1/button3/#text0: "1" => "2"
```
# Render
container.querySelector("#tags").click()
```html
<html>
<head />
<body>
<!--M[1-->
<script>
(M$h=[]).push((b,s,h,j,k)=&gt;(k={1:h={_:j={count:0,"#text/0(":b("@marko/tags-compat-5-to-6")(b("packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/components/class-layout.marko"),!0)}},2:{m5c:"s0"}},j["#text/0!"]=h,k),[])
</script>
<button
id="class"
>
2
</button>
<div>
<button
id="tags"
>
3
<!--M*1 #text/1-->
</button>
<!--M*1 #button/0-->
<script>
M$h.push((b,s)=&gt;({1:s[1]}),[1,"packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/template.marko_1_count",1,"packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/template.marko_1_count/subscriber",])
</script>
</div>
<script>
$MC=(window.$MC||[]).concat({"g":{"componentIdToScopeId":{"s0-2":1}},"w":[["s0",0,{},{"f":3}]],"t":["packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/components/class-layout.marko"]})
</script>
<!--M]0 #text/0-->
<script>
M$h.push((b,s)=&gt;({0:s[1]._}),[])
</script>
</body>
</html>
```
# Mutations
```
#document/html0/body1/div4/button1/#text0: "2" => "3"
```

View File

@ -1,9 +1,9 @@
# Write
<!M[1><script>(M$h=[]).push((b,s)=>({1:{}}),[])</script>
<!M[1><script>(M$h=[]).push((b,s,h,j,k)=>(k={1:h={_:j={count:0,"#text/0(":b("@marko/tags-compat-5-to-6")(b("packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/components/class-layout.marko"),!0)}},2:{m5c:"s0"}},j["#text/0!"]=h,k),[])</script>
# Emit error
TypeError: Cannot read properties of null (reading 'isSync')
# Write
<!--M#s0--><button id=class>0</button><div><!--F#2--><button id=tags>0<!M*1 #text/1></button><!M*1 #button/0><script>M$h.push((b,s)=>({1:s[1]}),[1,"packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/template.marko_1_count",1,"packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/template.marko_1_count/subscriber",])</script><!--F/--></div><!--M/--><script>$MC=(window.$MC||[]).concat({"g":{"componentIdToScopeId":{"s0-2":1}},"w":[["s0",0,{},{"f":3}]],"t":["packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/components/class-layout.marko"]})</script><!M]0 #text/0><script>M$h.push((b,s)=>({0:s[1]._}),[])</script>
# Render "End"
@ -13,7 +13,35 @@
<body>
<!--M[1-->
<script>
(M$h=[]).push((b,s)=&gt;({1:{}}),[])
(M$h=[]).push((b,s,h,j,k)=&gt;(k={1:h={_:j={count:0,"#text/0(":b("@marko/tags-compat-5-to-6")(b("packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/components/class-layout.marko"),!0)}},2:{m5c:"s0"}},j["#text/0!"]=h,k),[])
</script>
<!--M#s0-->
<button
id="class"
>
0
</button>
<div>
<!--F#2-->
<button
id="tags"
>
0
<!--M*1 #text/1-->
</button>
<!--M*1 #button/0-->
<script>
M$h.push((b,s)=&gt;({1:s[1]}),[1,"packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/template.marko_1_count",1,"packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/template.marko_1_count/subscriber",])
</script>
<!--F/-->
</div>
<!--M/-->
<script>
$MC=(window.$MC||[]).concat({"g":{"componentIdToScopeId":{"s0-2":1}},"w":[["s0",0,{},{"f":3}]],"t":["packages/translator-interop/src/__tests__/fixtures/interop-nested-tags-to-class/components/class-layout.marko"]})
</script>
<!--M]0 #text/0-->
<script>
M$h.push((b,s)=&gt;({0:s[1]._}),[])
</script>
</body>
</html>
@ -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
```

View File

@ -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;

View File

@ -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<void>((resolve) =>
serverTemplate.writeTo(writable(resolve), input, config.context)
);
} else {
await serverTemplate.render(input, writable);
await serverTemplate.render(input, writable());
}
tracker.cleanup();

View File

@ -11,11 +11,7 @@
# Write
defghi
# Write
jkl
defghijkl
# Render "End"

View File

@ -3,11 +3,7 @@
# Write
d<!M$0/>efg
# Write
<t id="M$0">ERROR!</t><script>(M$r=REORDER_RUNTIME)(0)</script>
<t id="M$0">ERROR!</t><script>(M$r=REORDER_RUNTIME)(0)</script>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"
```

View File

@ -2,28 +2,5 @@
a
# Write
b
# Emit error
Error: ERROR!
# Render "End"
```html
<html>
<head />
<body>
ab
</body>
</html>
```
# Mutations
```
inserted #document/html0
inserted #document/html0/head0
inserted #document/html0/body1
inserted #document/html0/body1/#text0
```
Error: ERROR!

View File

@ -1,25 +1,2 @@
# Write
a
# Emit error
Error: ERROR!
# Render "End"
```html
<html>
<head />
<body>
a
</body>
</html>
```
# Mutations
```
inserted #document/html0
inserted #document/html0/head0
inserted #document/html0/body1
inserted #document/html0/body1/#text0
```
Error: ERROR!

View File

@ -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<void>((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();