feat: pull in custom serializer

This commit is contained in:
Michael Rawlings 2021-11-05 15:55:27 -07:00
parent 2905a7abcf
commit d2e723b5df
No known key found for this signature in database
GPG Key ID: B9088328804D407C
7 changed files with 473 additions and 26 deletions

View File

@ -6,9 +6,9 @@
{
"name": "*",
"individual": {
"min": 11965,
"gzip": 5131,
"brotli": 4621
"min": 11962,
"gzip": 5140,
"brotli": 4628
}
}
]

View File

@ -1,5 +1,5 @@
# Write
<!M^ROOT><body><!M#6 ROOT 6><button><!M#1 ROOT 7>0</button></body><!M/ROOT><script>(M$h=[]).push({"ROOT":["ROOT",null,null,null,null,null,null,null,0]},["counter",6,"ROOT",])</script>
<!M^ROOT><body><!M#6 ROOT 6><button><!M#1 ROOT 7>0</button></body><!M/ROOT><script>(M$h=[]).push((b,s)=>({ROOT:["ROOT",,,,,,,,0]}),["counter",6,"ROOT",])</script>
# Render "End"
@ -14,7 +14,7 @@
0
</button>
<script>
(M$h=[]).push({"ROOT":["ROOT",null,null,null,null,null,null,null,0]},["counter",6,"ROOT",])
(M$h=[]).push((b,s)=&gt;({ROOT:["ROOT",,,,,,,,0]}),["counter",6,"ROOT",])
</script>
</body>
<!--M/ROOT-->
@ -49,7 +49,7 @@ inserted #document/html1/body1/script2/#text0
0
</button>
<script>
(M$h=[]).push({"ROOT":["ROOT",null,null,null,null,null,null,null,0]},["counter",6,"ROOT",])
(M$h=[]).push((b,s)=&gt;({ROOT:["ROOT",,,,,,,,0]}),["counter",6,"ROOT",])
</script>
</body>
<!--M/ROOT-->
@ -76,7 +76,7 @@ container.querySelector("button").click();
1
</button>
<script>
(M$h=[]).push({"ROOT":["ROOT",null,null,null,null,null,null,null,0]},["counter",6,"ROOT",])
(M$h=[]).push((b,s)=&gt;({ROOT:["ROOT",,,,,,,,0]}),["counter",6,"ROOT",])
</script>
</body>
<!--M/ROOT-->
@ -103,7 +103,7 @@ container.querySelector("button").click();
2
</button>
<script>
(M$h=[]).push({"ROOT":["ROOT",null,null,null,null,null,null,null,0]},["counter",6,"ROOT",])
(M$h=[]).push((b,s)=&gt;({ROOT:["ROOT",,,,,,,,0]}),["counter",6,"ROOT",])
</script>
</body>
<!--M/ROOT-->
@ -130,7 +130,7 @@ container.querySelector("button").click();
3
</button>
<script>
(M$h=[]).push({"ROOT":["ROOT",null,null,null,null,null,null,null,0]},["counter",6,"ROOT",])
(M$h=[]).push((b,s)=&gt;({ROOT:["ROOT",,,,,,,,0]}),["counter",6,"ROOT",])
</script>
</body>
<!--M/ROOT-->

View File

@ -1,5 +1,5 @@
import { Scope, ScopeOffsets, HydrateSymbols } from "../common/types";
import { runWithScope } from "./scope";
import { bind, runWithScope } from "./scope";
type HydrateFn = () => void;
@ -23,9 +23,14 @@ export function init(runtimeId = "M" /* [a-zA-Z0-9]+ */) {
let currentScope: Scope;
let currentOffset: number;
let currentNode: Node;
const scopeLookup: Map<string, Scope> = new Map();
const scopeLookup: Record<string, Scope> = {};
const stack: Array<string | number> = [];
const fakeArray = { push: hydrate };
const bindFunction = (fnId: string, offset: number, scopeId: string) => {
const fn = fnsById[fnId];
const scope = scopeLookup[scopeId];
return bind(fn, offset, scope);
};
Object.defineProperty(window, hydrateVar, {
get() {
@ -40,13 +45,15 @@ export function init(runtimeId = "M" /* [a-zA-Z0-9]+ */) {
}
function hydrate(
scopes: Record<string, Scope>,
scopesFn: (b, s, ...rest: unknown[]) => Record<string, Scope>,
calls: Array<string | number>
) {
if (doc.readyState !== "loading") {
walker.currentNode = doc;
}
const scopes = scopesFn(bindFunction, scopeLookup);
/**
* Loop over all the new hydration scopes and see if a previous walk
* had to create a dummy scope to store Nodes of interest.
@ -54,13 +61,13 @@ export function init(runtimeId = "M" /* [a-zA-Z0-9]+ */) {
*/
for (const scopeId in scopes) {
const scope = scopes[scopeId];
const storedScope = scopeLookup.get(scope[ScopeOffsets.ID]);
const storedScope = scopeLookup[scopeId];
if (storedScope !== scope) {
if (storedScope) {
Object.assign(scope, storedScope);
}
scopeLookup.set(scope[ScopeOffsets.ID], scope);
scopeLookup[scopeId] = scope;
if (currentScope === storedScope) {
currentScope = scope;
}
@ -91,10 +98,10 @@ export function init(runtimeId = "M" /* [a-zA-Z0-9]+ */) {
if (currentScope) {
stack.push(currentScope[ScopeOffsets.ID] as string, currentOffset);
}
currentScope = scopeLookup.get(data)!;
currentScope = scopeLookup[data]!;
currentOffset = 0;
if (!currentScope) {
scopeLookup.set(data, (currentScope = [data] as unknown as Scope));
scopeLookup[data] = currentScope = [data] as unknown as Scope;
}
currentScope[ScopeOffsets.START_NODE] = currentNode;
} else if (token === HydrateSymbols.SCOPE_END) {
@ -106,7 +113,7 @@ export function init(runtimeId = "M" /* [a-zA-Z0-9]+ */) {
}
currentScope[ScopeOffsets.END_NODE] = currentNode;
currentOffset = stack.pop() as number;
currentScope = scopeLookup.get(stack.pop() as string)!;
currentScope = scopeLookup[stack.pop() as string]!;
// eslint-disable-next-line no-constant-condition
} else if ("MARKO_DEBUG") {
throw new Error("MALFORMED MARKER: " + nodeValue);
@ -118,7 +125,7 @@ export function init(runtimeId = "M" /* [a-zA-Z0-9]+ */) {
runWithScope(
fnsById[calls[i]]!,
calls[i + 1] as number,
scopeLookup.get(calls[i + 2] as string)
scopeLookup[calls[i + 2]]
);
}
}

View File

@ -68,9 +68,11 @@ export function getOwnerScope(ownerLevel = 1) {
return scope;
}
export function bind(fn: (...args: unknown[]) => unknown) {
const boundScope = currentScope;
const boundOffset = currentOffset;
export function bind(
fn: (...args: unknown[]) => unknown,
boundOffset = currentOffset,
boundScope = currentScope
) {
return fn.length
? (...args: unknown[]) => runWithScope(fn, boundOffset, boundScope, args)
: () => runWithScope(fn, boundOffset, boundScope);

View File

@ -21,4 +21,6 @@ export {
writeScope,
} from "./writer";
export { register } from "./serializer";
export { pushContext, popContext, getInContext } from "../common/context";

View File

@ -0,0 +1,428 @@
/* eslint "@typescript-eslint/ban-types": ["error", { "types": { "object": false }, "extendDefaults": true }] */
const { hasOwnProperty } = Object.prototype;
const PARAM_BIND = "b";
const PARAM_SCOPE = "s";
const REF_START_CHARS = "hjkmoquxzABCDEFGHIJKLNPQRTUVWXYZ$_"; // Avoids chars that could evaluate to a reserved word.
const REF_START_CHARS_LEN = REF_START_CHARS.length;
const REF_CHARS =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$_";
const REF_CHARS_LEN = REF_CHARS.length;
const SYMBOL_FN_ID = Symbol("FN_ID");
const SYMBOL_SCOPE_ID = Symbol("SCOPE_ID");
const SYMBOL_OFFSET = Symbol("OFFSET");
type SerializableFn = ((...args: unknown[]) => unknown) & {
[SYMBOL_FN_ID]: string;
[SYMBOL_SCOPE_ID]: string;
[SYMBOL_OFFSET]: number;
};
export function register(
fn: (...args: unknown[]) => unknown,
fnId: string,
scopeId: string,
offset: number
): SerializableFn {
(fn as SerializableFn)[SYMBOL_FN_ID] = fnId;
(fn as SerializableFn)[SYMBOL_SCOPE_ID] = scopeId;
(fn as SerializableFn)[SYMBOL_OFFSET] = offset;
return fn as SerializableFn;
}
export function stringify(root: unknown) {
return new Serializer().stringify(root);
}
export class Serializer {
// TODO: hoist these back out?
STACK: object[] = [];
BUFFER: string[] = [""];
ASSIGNMENTS: Map<string, string> = new Map();
INDEX_OR_REF: WeakMap<object, number | string> = new WeakMap();
REF_COUNT = 0;
// These stay
PARENTS: WeakMap<object, object> = new WeakMap();
KEYS: WeakMap<object, number | string> = new WeakMap();
constructor() {
this.BUFFER.pop();
}
stringify(root: unknown) {
if (this.writeProp(root, "", undefined)) {
const { BUFFER, REF_COUNT, ASSIGNMENTS, INDEX_OR_REF } = this;
let result = BUFFER[0];
for (let i = 1, len = BUFFER.length; i < len; i++) {
result += BUFFER[i];
}
if (REF_COUNT) {
if (ASSIGNMENTS.size) {
let ref = INDEX_OR_REF.get(root as object);
if (typeof ref === "number") {
ref = toRefParam(this.REF_COUNT++);
result = ref + "=" + result;
}
for (const [assignmentRef, assignments] of ASSIGNMENTS) {
result += "," + assignments + assignmentRef;
}
result += "," + ref;
this.ASSIGNMENTS = new Map();
}
result =
"(" +
PARAM_BIND +
"," +
PARAM_SCOPE +
"," +
this.refParamsString() +
")=>(" +
result +
")";
} else if (root && (root as object).constructor === Object) {
result = "(" + PARAM_BIND + "," + PARAM_SCOPE + ")=>(" + result + ")";
}
BUFFER.length = 0;
this.INDEX_OR_REF = new WeakMap();
return result;
}
return "void 0";
}
writeProp(
cur: unknown,
accessor: string | number,
parent: object | undefined
): boolean {
const { BUFFER } = this;
switch (typeof cur) {
case "string":
BUFFER.push(quote(cur, 0));
break;
case "number":
BUFFER.push(cur + "");
break;
case "boolean":
BUFFER.push(cur ? "!0" : "!1");
break;
case "object":
if (cur === null) {
BUFFER.push("null");
} else {
const ref = this.getRef(cur, accessor, parent);
switch (ref) {
case true:
return false;
case false:
switch (cur.constructor) {
case Object:
this.writeObject(cur as Record<string, unknown>);
break;
case Array:
this.writeArray(cur as unknown[]);
break;
case Date:
BUFFER.push(
'new Date("' + (cur as Date).toISOString() + '")'
);
break;
case RegExp:
BUFFER.push(cur + "");
break;
case Map:
BUFFER.push("new Map(");
this.writeArray(
Array.from(cur as Map<unknown, unknown> | Set<unknown>)
);
BUFFER.push(")");
break;
case Set:
BUFFER.push("new Set(");
this.writeArray(
Array.from(cur as Map<unknown, unknown> | Set<unknown>)
);
BUFFER.push(")");
break;
case undefined:
BUFFER.push("Object.assign(Object.create(null),");
this.writeObject(cur as Record<string, unknown>);
BUFFER.push("))");
break;
default:
return false;
}
break;
default:
BUFFER.push(ref);
break;
}
}
break;
case "function": {
return this.writeFunction(cur as SerializableFn);
}
default:
return false;
}
return true;
}
writeFunction(fn: SerializableFn) {
const {
[SYMBOL_FN_ID]: fnId,
[SYMBOL_SCOPE_ID]: scopeId,
[SYMBOL_OFFSET]: offset,
} = fn;
if (fnId && scopeId && offset != null) {
// ASSERT: fnId and scopeId don't need `quote` escaping
this.BUFFER.push(`${PARAM_BIND}("${fnId}",${offset},"${scopeId}")`);
return true;
}
return false;
}
writeObject(obj: Record<string, unknown>) {
const { STACK, BUFFER } = this;
let sep = "{";
STACK.push(obj);
for (const key in obj) {
if (hasOwnProperty.call(obj, key)) {
const val = obj[key];
const escapedKey = toObjectKey(key);
BUFFER.push(sep + escapedKey + ":");
if (this.writeProp(val, escapedKey, obj)) {
sep = ",";
} else {
BUFFER.pop();
}
}
}
if (sep === "{") {
BUFFER.push("{}");
} else {
BUFFER.push("}");
}
STACK.pop();
}
writeArray(arr: unknown[]) {
const { STACK, BUFFER } = this;
BUFFER.push("[");
STACK.push(arr);
this.writeProp(arr[0], 0, arr);
for (let i = 1, len = arr.length; i < len; i++) {
BUFFER.push(",");
this.writeProp(arr[i], i, arr);
}
STACK.pop();
BUFFER.push("]");
}
getRef(cur: object, accessor: string | number, parent: object | undefined) {
const { STACK, BUFFER, INDEX_OR_REF, ASSIGNMENTS, PARENTS, KEYS } = this;
let ref = INDEX_OR_REF.get(cur);
if (ref === undefined) {
INDEX_OR_REF.set(cur, BUFFER.length);
let knownParent = PARENTS.get(cur);
if (knownParent === undefined) {
PARENTS.set(cur, parent!);
KEYS.set(cur, accessor);
return false;
} else {
let ref = "";
while (knownParent) {
ref = toPropertyAccess(KEYS.get(cur)!) + ref;
knownParent = PARENTS.get((cur = knownParent));
}
return PARAM_SCOPE + ref;
}
}
if (typeof ref === "number") {
ref = this.insertAndGetRef(cur, ref);
}
if (STACK.includes(cur)) {
const parent = STACK[STACK.length - 1];
let parentRef = INDEX_OR_REF.get(parent) as string | number;
if (typeof parentRef === "number") {
parentRef = this.insertAndGetRef(parent, parentRef);
}
ASSIGNMENTS.set(
ref,
(ASSIGNMENTS.get(ref) || "") + toAssignment(parentRef, accessor) + "="
);
return true;
}
return ref;
}
insertAndGetRef(obj: object, pos: number) {
const ref = toRefParam(this.REF_COUNT++);
this.INDEX_OR_REF.set(obj, ref);
if (pos) {
this.BUFFER[pos - 1] += ref + "=";
} else {
this.BUFFER[pos] = ref + "=" + this.BUFFER[pos];
}
return ref;
}
refParamsString() {
let result = REF_START_CHARS[0];
for (let i = 1; i < this.REF_COUNT; i++) {
result += "," + toRefParam(i);
}
this.REF_COUNT = 0;
return result;
}
}
function toObjectKey(name: string) {
const invalidIdentifierPos = getInvalidIdentifierPos(name);
return invalidIdentifierPos === -1 ? name : quote(name, invalidIdentifierPos);
}
function toAssignment(parent: string, key: string | number) {
return parent + toPropertyAccess(key);
}
function toPropertyAccess(key: string | number) {
return typeof key === "number" || key[0] === '"'
? "[" + key + "]"
: "." + key;
}
function getInvalidIdentifierPos(name: string) {
let char = name[0];
if (
!(
(char >= "a" && char <= "z") ||
(char >= "A" && char <= "Z") ||
char === "$" ||
char === "_"
)
) {
return 0;
}
for (let i = 1, len = name.length; i < len; i++) {
char = name[i];
if (
!(
(char >= "a" && char <= "z") ||
(char >= "A" && char <= "Z") ||
(char >= "0" && char <= "9") ||
char === "$" ||
char === "_"
)
) {
return i;
}
}
return -1;
}
// Creates a JavaScript double quoted string and escapes all characters not listed as DoubleStringCharacters on
// Also includes "<" to escape "</script>" and "\" to avoid invalid escapes in the output.
// http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4
function quote(str: string, startPos: number): string {
let result = "";
let lastPos = 0;
for (let i = startPos, len = str.length; i < len; i++) {
let replacement: string;
switch (str[i]) {
case '"':
replacement = '\\"';
break;
case "\\":
replacement = "\\\\";
break;
case "<":
replacement = "\\x3C";
break;
case "\n":
replacement = "\\n";
break;
case "\r":
replacement = "\\r";
break;
case "\u2028":
replacement = "\\u2028";
break;
case "\u2029":
replacement = "\\u2029";
break;
default:
continue;
}
result += str.slice(lastPos, i) + replacement;
lastPos = i + 1;
}
if (lastPos === startPos) {
result = str;
} else {
result += str.slice(lastPos);
}
return '"' + result + '"';
}
function toRefParam(index: number) {
let mod = index % REF_START_CHARS_LEN;
let ref = REF_START_CHARS[mod];
index = (index - mod) / REF_START_CHARS_LEN;
while (index > 0) {
mod = index % REF_CHARS_LEN;
ref += REF_CHARS[mod];
index = (index - mod) / REF_CHARS_LEN;
}
return ref;
}

View File

@ -2,6 +2,7 @@ import type { Writable } from "stream";
import { Context, setContext } from "../common/context";
import { Renderer, Scope, ScopeOffsets, HydrateSymbols } from "../common/types";
import reorderRuntime from "./reorder-runtime";
import { Serializer } from "./serializer";
const runtimeId = "M";
const reorderRuntimeString = String(reorderRuntime).replace(
@ -18,7 +19,7 @@ let $_promises: Array<Promise<unknown> & { isPlaceholder?: true }> | null =
const uids: WeakMap<MaybeFlushable, number> = new WeakMap();
const runtimeFlushed: WeakSet<MaybeFlushable> = new WeakSet();
const hydrateFlushed: WeakSet<MaybeFlushable> = new WeakSet();
const streamSerializers: WeakMap<MaybeFlushable, Serializer> = new WeakMap();
export function nextId() {
const id = uids.get($_stream!)! + 1 || 0;
@ -286,11 +287,18 @@ export function markReplaceEnd(id: number) {
function flushToStream() {
if ($_buffer!.calls || $_buffer!.scopes) {
let isFirstFlush;
let serializer = streamSerializers.get($_stream!);
if ((isFirstFlush = !serializer)) {
streamSerializers.set($_stream!, (serializer = new Serializer()));
}
$_buffer!.content += `<script>${
hydrateFlushed.has($_stream!)
? runtimeId + HydrateSymbols.VAR_HYDRATE
: `(${runtimeId + HydrateSymbols.VAR_HYDRATE}=[])`
}.push(${JSON.stringify($_buffer!.scopes)},[${$_buffer!.calls}])</script>`;
isFirstFlush
? `(${runtimeId + HydrateSymbols.VAR_HYDRATE}=[])`
: runtimeId + HydrateSymbols.VAR_HYDRATE
}.push(${serializer.stringify($_buffer!.scopes)},[${
$_buffer!.calls
}])</script>`;
}
$_stream!.write($_buffer!.content);
if ($_stream!.flush) {