From 5b13f37acf5e50d6f11d11ff0b2cfa8a3ae2cd43 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 21 Apr 2023 17:10:50 -0400 Subject: [PATCH 1/4] Use resources in the vanilla renderer --- .../universal/resource/src/resource-list.ts | 3 + packages/x/vanilla/index.ts | 2 +- packages/x/vanilla/src/dom.ts | 139 ++++++++++-------- packages/x/vanilla/tests/package.json | 3 + packages/x/vanilla/tests/vanilla.spec.ts | 27 +++- pnpm-lock.yaml | 4 + 6 files changed, 117 insertions(+), 61 deletions(-) diff --git a/packages/universal/resource/src/resource-list.ts b/packages/universal/resource/src/resource-list.ts index 5edd770b..92a29570 100644 --- a/packages/universal/resource/src/resource-list.ts +++ b/packages/universal/resource/src/resource-list.ts @@ -9,6 +9,9 @@ import { type UseFnOptions, } from "./api.js"; +/** + * TODO: handle static + */ export function ResourceList( list: Iterable, { diff --git a/packages/x/vanilla/index.ts b/packages/x/vanilla/index.ts index 198c6961..2d1901e2 100644 --- a/packages/x/vanilla/index.ts +++ b/packages/x/vanilla/index.ts @@ -1,2 +1,2 @@ export { Cursor } from "./src/cursor.js"; -export { Attr, Element as El, Fragment, Text } from "./src/dom.js"; +export { Attr, Comment,Element as El, Fragment, Text } from "./src/dom.js"; diff --git a/packages/x/vanilla/src/dom.ts b/packages/x/vanilla/src/dom.ts index c7fdd26e..0fd4d61b 100644 --- a/packages/x/vanilla/src/dom.ts +++ b/packages/x/vanilla/src/dom.ts @@ -1,6 +1,23 @@ +/** + * TODO: + * - DynamicFragment + * - Comment + * - Namespaces + * - SVG + * - Modifier + * - Portal + * - SSR + * + * Goals: + * - Implement Glimmer compatibility + * - Write compiler Glimmer -> whatever this DSL ends up being + * + * Stretch Goals: + * - other compilers (html``) + */ import type { Description, Reactive } from "@starbeam/interfaces"; -import { CachedFormula, DEBUG, type FormulaFn } from "@starbeam/reactive"; -import { RUNTIME } from "@starbeam/runtime"; +import { CachedFormula, DEBUG } from "@starbeam/reactive"; +import { Resource, type ResourceBlueprint,RUNTIME,use } from "@starbeam/universal"; import { Cursor } from "./cursor.js"; @@ -8,27 +25,30 @@ export function Text( text: Reactive, description?: string | Description ): ContentNode { - return ContentNode(({ into }) => { - const node = into.insert(into.document.createTextNode(text.read())); + return Content( + ({ into, owner }) => { + const node = into.insert(into.document.createTextNode(text.read())); + + return Resource(({ on }) => { + on.cleanup(() => void node.remove()); + node.textContent = text.read(); + }) + } - return { - cleanup: () => void node.remove(), - update: () => (node.textContent = text.read()), - }; - }, DEBUG?.Desc("resource", description, "Text")); + , DEBUG?.Desc('resource', description, 'Text')); } export function Comment( text: Reactive, description?: string | Description ): ContentNode { - return ContentNode(({ into }) => { + return Content(({ into }) => { const node = into.insert(into.document.createComment(text.read())); - return { - cleanup: () => void node.remove(), - update: () => (node.textContent = text.read()), - }; + return Resource(({ on }) => { + on.cleanup(() => void node.remove()); + node.textContent = text.read(); + }); }, DEBUG?.Desc("resource", description, "Comment")); } @@ -36,22 +56,23 @@ export function Fragment( nodes: ContentNode[], description?: string | Description ): ContentNode { - return ContentNode(({ into, owner }) => { + return Content(({ into, owner }) => { const start = placeholder(into.document); into.insert(start); - const renderedNodes = nodes.map((nodeConstructor) => - nodeConstructor(into).create({ owner }) + const renderedNodes = + nodes.map( + (nodeConstructor) => nodeConstructor(into).create({ owner }) ); const end = placeholder(into.document); into.insert(end); const range = FragmentRange.create(start, end); - return { - cleanup: () => void range.clear(), - update: () => void poll(renderedNodes), - }; + return Resource(({ on }) => { + on.cleanup(() => void range.clear()); + poll(renderedNodes); + }) }, DEBUG?.Desc("resource", description, "Fragment")); } @@ -60,7 +81,7 @@ export function Attr( value: Reactive, description?: string | Description ): AttrNode { - return ContentNode(({ into }) => { + return Content(({ into }) => { const current = value.read(); if (typeof current === "string") { @@ -69,22 +90,18 @@ export function Attr( into.setAttribute(name, ""); } - return { - cleanup: () => { + return Resource(({ on }) => { + on.cleanup(() => void into.removeAttribute(name)); + const next = value.read(); + + if (typeof next === "string") { + into.setAttribute(name, next); + } else if (next === true) { + into.setAttribute(name, ""); + } else if (next === false) { into.removeAttribute(name); - }, - update: () => { - const next = value.read(); - - if (typeof next === "string") { - into.setAttribute(name, next); - } else if (next === true) { - into.setAttribute(name, ""); - } else if (next === false) { - into.removeAttribute(name); - } - }, - }; + } + }); }, DEBUG?.Desc("resource", description, "Attr")); } @@ -100,7 +117,7 @@ export function Element( }, description?: Description | string ): ContentNode { - return ContentNode(({ into, owner }) => { + return Content(({ into, owner }) => { const element = into.document.createElement(tag); const elementCursor = Cursor.appendTo(element); @@ -113,13 +130,16 @@ export function Element( into.insert(element); - return { - cleanup: () => void element.remove(), - update: () => { - poll(renderAttributes); - poll(renderBody); - }, - }; + return Resource(({on}, meta) => { + on.cleanup(() => void element.remove()); + + return { + update: () => { + renderAttributes.forEach(a => a.read()); + poll(renderBody); + }, + } + }); }, DEBUG?.Desc("resource", description, "Element")); } @@ -129,14 +149,16 @@ function placeholder(document: Document): Text { return document.createTextNode(""); } -type Rendered = FormulaFn; +type Rendered = Resource; interface OutputConstructor { - create: (options: { owner: object }) => Rendered; + create: (options: { owner: object }) => { + read: () => void; + update: () => void + }; } - type ContentNode = (into: Cursor) => OutputConstructor; -type AttrNode = (into: E) => OutputConstructor; +type AttrNode = (into: E) => Resource; function poll(rendered: Rendered[] | Rendered): void { if (Array.isArray(rendered)) { @@ -146,21 +168,22 @@ function poll(rendered: Rendered[] | Rendered): void { } } -function ContentNode( - create: (options: { into: T; owner: object }) => { - cleanup: () => void; - update: () => void; - }, +type ContentConstructor = (options: { into: T, owner: object }) => ResourceBlueprint<{ + read: () => void, + update: () => void + }>; + +function Content( + create: ContentConstructor, description: Description | undefined ): (into: T) => OutputConstructor { return (into: T) => { return { create({ owner }) { - const { cleanup, update } = create({ into, owner }); - - const formula = CachedFormula(update, description); + const blueprint = create({ into, owner }); + const formula = CachedFormula(() => (use(blueprint, { lifetime: owner })).current, description); - RUNTIME.onFinalize(owner, cleanup); + RUNTIME.onFinalize(owner, () => void RUNTIME.finalize(formula)); return formula; }, diff --git a/packages/x/vanilla/tests/package.json b/packages/x/vanilla/tests/package.json index 13a2aa64..294e5f59 100644 --- a/packages/x/vanilla/tests/package.json +++ b/packages/x/vanilla/tests/package.json @@ -11,5 +11,8 @@ "dependencies": { "@starbeam/universal": "workspace:^", "@starbeamx/vanilla": "workspace:^" + }, + "devDependencies": { + "vitest": "^0.30.1" } } diff --git a/packages/x/vanilla/tests/vanilla.spec.ts b/packages/x/vanilla/tests/vanilla.spec.ts index 70d53753..2f75c148 100644 --- a/packages/x/vanilla/tests/vanilla.spec.ts +++ b/packages/x/vanilla/tests/vanilla.spec.ts @@ -1,7 +1,7 @@ // @vitest-environment happy-dom import { Cell, RUNTIME } from "@starbeam/universal"; -import { Cursor, El, Fragment, Text } from "@starbeamx/vanilla"; +import { Comment, Cursor, El, Fragment, Text } from "@starbeamx/vanilla"; import { describe, expect, test } from "vitest"; import { env } from "./env"; @@ -11,7 +11,9 @@ describe("Vanilla Renderer", () => { const { body, owner } = env(); const cell = Cell("Hello World"); - const render = Text(cell)(body.cursor).create({ owner }); + const text = Text(cell); + const renderer = text(body.cursor); + const render = renderer.create({ owner }); expect(body.innerHTML).toBe("Hello World"); @@ -26,6 +28,27 @@ describe("Vanilla Renderer", () => { expect(body.innerHTML).toBe(""); }); + test("it can render a comment", () => { + const { body, owner } = env(); + + const cell = Cell("Hello World"); + const text = Comment(cell); + const renderer = text(body.cursor); + const render = renderer.create({ owner }); + + expect(body.innerHTML).toBe(""); + + cell.set("Goodbye world"); + render.read(); + + expect(body.innerHTML).toBe(""); + + RUNTIME.finalize(owner); + render.read(); + + expect(body.innerHTML).toBe(""); + }); + test("it can render fragments", () => { const { body, owner } = env(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9bf8f86..bd8f1edb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1542,6 +1542,10 @@ importers: '@starbeamx/vanilla': specifier: workspace:^ version: link:.. + devDependencies: + vitest: + specifier: ^0.30.1 + version: 0.30.1(@vitest/ui@0.30.1)(happy-dom@9.8.1)(jsdom@21.1.1) workspace/@domtree/any: dependencies: From ee4d928bfcd353a8cee2a8f3a363882cbe6301ce Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 22 Apr 2023 00:48:42 -0400 Subject: [PATCH 2/4] Needed to add typescript to the local packages for linting / lsp / type checking to work (in both terminal (cd'd, and editor)) --- packages/x/vanilla/package.json | 3 ++- packages/x/vanilla/src/dom.ts | 21 +++++++-------------- packages/x/vanilla/tests/package.json | 1 + pnpm-lock.yaml | 6 ++++++ 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/x/vanilla/package.json b/packages/x/vanilla/package.json index 87e2498f..e9aa043c 100644 --- a/packages/x/vanilla/package.json +++ b/packages/x/vanilla/package.json @@ -34,6 +34,7 @@ }, "devDependencies": { "@starbeam-dev/build-support": "workspace:*", - "rollup": "^3.20.6" + "rollup": "^3.20.6", + "typescript": "^5.0.4" } } diff --git a/packages/x/vanilla/src/dom.ts b/packages/x/vanilla/src/dom.ts index 0fd4d61b..ca19572f 100644 --- a/packages/x/vanilla/src/dom.ts +++ b/packages/x/vanilla/src/dom.ts @@ -1,7 +1,6 @@ /** * TODO: * - DynamicFragment - * - Comment * - Namespaces * - SVG * - Modifier @@ -16,8 +15,8 @@ * - other compilers (html``) */ import type { Description, Reactive } from "@starbeam/interfaces"; -import { CachedFormula, DEBUG } from "@starbeam/reactive"; -import { Resource, type ResourceBlueprint,RUNTIME,use } from "@starbeam/universal"; +import { CachedFormula, DEBUG, type FormulaFn } from "@starbeam/reactive"; +import { Resource,type ResourceBlueprint,RUNTIME,use } from "@starbeam/universal"; import { Cursor } from "./cursor.js"; @@ -149,16 +148,13 @@ function placeholder(document: Document): Text { return document.createTextNode(""); } -type Rendered = Resource; +type Rendered = FormulaFn; interface OutputConstructor { - create: (options: { owner: object }) => { - read: () => void; - update: () => void - }; + create: (options: { owner: object }) => FormulaFn; } type ContentNode = (into: Cursor) => OutputConstructor; -type AttrNode = (into: E) => Resource; +type AttrNode = (into: E) => OutputConstructor; function poll(rendered: Rendered[] | Rendered): void { if (Array.isArray(rendered)) { @@ -168,10 +164,7 @@ function poll(rendered: Rendered[] | Rendered): void { } } -type ContentConstructor = (options: { into: T, owner: object }) => ResourceBlueprint<{ - read: () => void, - update: () => void - }>; +type ContentConstructor = (options: { into: T, owner: object }) => ResourceBlueprint; function Content( create: ContentConstructor, @@ -181,7 +174,7 @@ function Content( return { create({ owner }) { const blueprint = create({ into, owner }); - const formula = CachedFormula(() => (use(blueprint, { lifetime: owner })).current, description); + const formula = CachedFormula(() => (use(blueprint, { lifetime: owner, metadata: { owner } })).current, description); RUNTIME.onFinalize(owner, () => void RUNTIME.finalize(formula)); diff --git a/packages/x/vanilla/tests/package.json b/packages/x/vanilla/tests/package.json index 294e5f59..42887391 100644 --- a/packages/x/vanilla/tests/package.json +++ b/packages/x/vanilla/tests/package.json @@ -13,6 +13,7 @@ "@starbeamx/vanilla": "workspace:^" }, "devDependencies": { + "typescript": "^5.0.4", "vitest": "^0.30.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd8f1edb..5c136199 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1533,6 +1533,9 @@ importers: rollup: specifier: ^3.20.6 version: 3.20.6 + typescript: + specifier: ^5.0.4 + version: 5.0.4 packages/x/vanilla/tests: dependencies: @@ -1543,6 +1546,9 @@ importers: specifier: workspace:^ version: link:.. devDependencies: + typescript: + specifier: ^5.0.4 + version: 5.0.4 vitest: specifier: ^0.30.1 version: 0.30.1(@vitest/ui@0.30.1)(happy-dom@9.8.1)(jsdom@21.1.1) From 3ecda8ab5ed1692cba1d6147911c8317f00e7d00 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 22 Apr 2023 01:11:11 -0400 Subject: [PATCH 3/4] Add slow test to benchmark against, roughly, the old implementation --- packages/x/vanilla/tests/vanilla.spec.ts | 32 +++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/x/vanilla/tests/vanilla.spec.ts b/packages/x/vanilla/tests/vanilla.spec.ts index 2f75c148..c110f941 100644 --- a/packages/x/vanilla/tests/vanilla.spec.ts +++ b/packages/x/vanilla/tests/vanilla.spec.ts @@ -82,7 +82,7 @@ describe("Vanilla Renderer", () => { expect(body.innerHTML).toBe(""); }); - test("it can render elements", () => { + test('it can render elements', () => { const { body, owner } = env(); const cursor = body.cursor; @@ -103,6 +103,32 @@ describe("Vanilla Renderer", () => { `
Hello World - Goodbye World
` ); }); + + // This is currently *very* slow + test("it can render many elements", () => { + const { body, owner } = env(); + const cursor = body.cursor; + const fragments = []; + + for (let i = 0; i < 1000; i++) { + const a = Cell("Hello World"); + const b = Cell(" - "); + const c = Cell("Goodbye World"); + + const title = El.Attr("title", a); + + const el = El({ + tag: "div", + attributes: [title], + body: [Text(a), Text(b), Text(c)], + }); + fragments.push(el); + } + + Fragment(fragments)(cursor).create({ owner }); + + expect(body.snapshot().length).toBe(fragments.length); + }); }); export class Body { @@ -122,8 +148,8 @@ export class Body { return this.#body.innerHTML; } - snapshot(): void { - this.#snapshot = [...this.#body.childNodes]; + snapshot() : ChildNode[] { + return this.#snapshot = [...this.#body.childNodes]; } expectStable(): void { From 6a11f911d926a63cadcda4e5df410821f03450a4 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 22 Apr 2023 01:24:17 -0400 Subject: [PATCH 4/4] Separate timing collection ofr setup, and rendering --- packages/x/vanilla/tests/vanilla.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/x/vanilla/tests/vanilla.spec.ts b/packages/x/vanilla/tests/vanilla.spec.ts index c110f941..cd7cf61a 100644 --- a/packages/x/vanilla/tests/vanilla.spec.ts +++ b/packages/x/vanilla/tests/vanilla.spec.ts @@ -125,7 +125,9 @@ describe("Vanilla Renderer", () => { fragments.push(el); } + console.time('render'); Fragment(fragments)(cursor).create({ owner }); + console.timeEnd('render'); expect(body.snapshot().length).toBe(fragments.length); });