Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@
"@std/http": "jsr:@std/http@^1",
"@std/path": "jsr:@std/path@^1",
"@std/testing": "jsr:@std/testing@^1",
"jsdom": "npm:jsdom",
"lit": "npm:lit@^3.3.0",
"merkle-reference": "npm:merkle-reference@^2.2.0",
"multiformats": "npm:multiformats@^13.3.2",
Expand Down
413 changes: 127 additions & 286 deletions deno.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion packages/cli/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ async function initWorkspace(cwd: string) {
const types = {
"commontools": runtimeModuleTypes.commontools,
"turndown": runtimeModuleTypes.turndown,
"dom-parser": runtimeModuleTypes["dom-parser"],
"ct-env": ctEnv,
"react/jsx-runtime": jsxRuntime,
};
Expand Down
9 changes: 0 additions & 9 deletions packages/cli/fixtures/3p-modules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
str,
UI,
} from "commontools";
import { DOMParser, type Element } from "dom-parser";
import TurndownService from "turndown";

const Input = {
Expand Down Expand Up @@ -53,14 +52,6 @@ export default recipe(
</ul>
</div>
`;
// test dom-parser
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/xml");
const root = doc.querySelector("#root");
const ul = doc.getElementsByTagName("ul")[0];
assert(ul.getAttribute("foo") === "bar", "getAttribute() works");
const listitems = doc.getElementsByTagName("li");
assert(listitems.length === 3, "getElementsByTagName() selected 3 items");
const turndown = new TurndownService({
headingStyle: "atx",
codeBlockStyle: "fenced",
Expand Down
125 changes: 52 additions & 73 deletions packages/cli/lib/charm-render.ts
Original file line number Diff line number Diff line change
@@ -1,102 +1,81 @@
import { render, VNode } from "@commontools/html";
import { Cell, effect, UI } from "@commontools/runner";
import { inspectCharm, loadManager } from "./charm.ts";
import { Cell, UI } from "@commontools/runner";
import { loadManager } from "./charm.ts";
import { CharmsController } from "@commontools/charm/ops";
import type { CharmConfig } from "./charm.ts";
import { getLogger } from "@commontools/utils/logger";
import { MockDoc } from "@commontools/html/utils";

const logger = getLogger("charm-render", { level: "info", enabled: true });
const logger = getLogger("charm-render", { level: "info", enabled: false });

export interface RenderOptions {
watch?: boolean;
onUpdate?: (html: string) => void;
}

/**
* Renders a charm's UI to HTML using JSDOM.
* Renders a charm's UI to HTML using htmlparser2.
* Supports both static and reactive rendering with --watch mode.
*/
export async function renderCharm(
config: CharmConfig,
options: RenderOptions = {},
): Promise<string | (() => void)> {
// Dynamically import JSDOM to avoid top-level import issues
const { JSDOM } = await import("npm:jsdom");

// 1. Setup JSDOM environment
const dom = new JSDOM(
const mock = new MockDoc(
'<!DOCTYPE html><html><body><div id="root"></div></body></html>',
);
const { window } = dom;

// Set up global DOM objects needed by the render system
globalThis.document = window.document;
globalThis.Element = window.Element;
globalThis.Node = window.Node;
globalThis.Text = window.Text;
globalThis.HTMLElement = window.HTMLElement;
globalThis.Event = window.Event;
globalThis.CustomEvent = window.CustomEvent;
globalThis.MutationObserver = window.MutationObserver;
const { document, renderOptions } = mock;

try {
// 2. Get charm controller to access the Cell
const manager = await loadManager(config);
const charms = new CharmsController(manager);
const charm = await charms.get(config.charm);
const cell = charm.getCell();
// 2. Get charm controller to access the Cell
const manager = await loadManager(config);
const charms = new CharmsController(manager);
const charm = await charms.get(config.charm);
const cell = charm.getCell();

// Check if charm has UI
const staticValue = cell.get();
if (!staticValue?.[UI]) {
throw new Error(`Charm ${config.charm} has no UI`);
}
// Check if charm has UI
const staticValue = cell.get();
if (!staticValue?.[UI]) {
throw new Error(`Charm ${config.charm} has no UI`);
}

// 3. Get the root container
const container = window.document.getElementById("root");
if (!container) {
throw new Error("Could not find root container");
}
// 3. Get the root container
const container = document.getElementById("root");
if (!container) {
throw new Error("Could not find root container");
}

if (options.watch) {
// 4a. Reactive rendering - pass the Cell directly
const uiCell = cell.key(UI);
const cancel = render(container, uiCell as Cell<VNode>); // FIXME: types
if (options.watch) {
// 4a. Reactive rendering - pass the Cell directly
const uiCell = cell.key(UI);
const cancel = render(container, uiCell as Cell<VNode>, renderOptions); // FIXME: types

// 5a. Set up monitoring for changes
let updateCount = 0;
const unsubscribe = cell.sink((value) => {
if (value?.[UI]) {
updateCount++;
// Wait for all runtime computations to complete
manager.runtime.idle().then(() => {
const html = container.innerHTML;
logger.info(() => `[Update ${updateCount}] UI changed`);
if (options.onUpdate) {
options.onUpdate(html);
}
});
}
});
// 5a. Set up monitoring for changes
let updateCount = 0;
const unsubscribe = cell.sink((value) => {
if (value?.[UI]) {
updateCount++;
// Wait for all runtime computations to complete
manager.runtime.idle().then(() => {
const html = container.innerHTML;
logger.info(() => `[Update ${updateCount}] UI changed`);
if (options.onUpdate) {
options.onUpdate(html);
}
});
}
});

// Return cleanup function
return () => {
cancel();
unsubscribe();
window.close();
};
} else {
// 4b. Static rendering - render once with current value
const vnode = staticValue[UI];
render(container, vnode as VNode); // FIXME: types
// Return cleanup function
return () => {
cancel();
unsubscribe();
};
} else {
// 4b. Static rendering - render once with current value
const vnode = staticValue[UI];
render(container, vnode as VNode, renderOptions); // FIXME: types

// 5b. Return the rendered HTML
return container.innerHTML;
}
} finally {
// Clean up JSDOM only in static mode
if (!options.watch) {
window.close();
}
// 5b. Return the rendered HTML
return container.innerHTML;
}
}
14 changes: 11 additions & 3 deletions packages/html/deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
{
"name": "@commontools/html",
"exports": "./src/index.ts",
"tasks": {
"test": "deno test --allow-env --allow-ffi --allow-read"
"test": "deno test --allow-env --allow-ffi --allow-read test/*.test.ts"
},
"imports": {},
"exports": {
".": "./src/index.ts",
"./utils": "./src/utils.ts"
},
"imports": {
"htmlparser2": "npm:htmlparser2",
"domhandler": "npm:domhandler",
"dom-serializer": "npm:dom-serializer"
},

"compilerOptions": {
"jsx": "react",
"jsxFactory": "h",
Expand Down
2 changes: 2 additions & 0 deletions packages/html/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export {
render,
type RenderOptions,
setEventSanitizer,
setNodeSanitizer,
type SetPropHandler,
vdomSchema,
} from "./render.ts";
export { debug, setDebug } from "./logger.ts";
Expand Down
Loading
Loading