Skip to content

Commit e0c7331

Browse files
committed
starting to extract charm logic
moving to common-charm for now allows llhl and jumble to share logic
1 parent f2913c0 commit e0c7331

File tree

21 files changed

+291
-128
lines changed

21 files changed

+291
-128
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "@commontools/charm",
3+
"version": "0.1.0",
4+
"description": "charm",
5+
"main": "src/index.js",
6+
"type": "module",
7+
"packageManager": "pnpm@10.0.0",
8+
"engines": {
9+
"npm": "please-use-pnpm",
10+
"yarn": "please-use-pnpm",
11+
"pnpm": ">= 10.0.0",
12+
"node": "20.11.0"
13+
},
14+
"scripts": {
15+
"test": "vitest --exclude=lib/**/*.test.js",
16+
"format": "prettier --write . --ignore-path ../../../.prettierignore",
17+
"lint": "eslint .",
18+
"build": "tsc && vite build"
19+
},
20+
"author": "",
21+
"license": "UNLICENSED",
22+
"dependencies": {
23+
"@commontools/builder": "workspace:*",
24+
"@commontools/html": "workspace:*",
25+
"@commontools/memory": "workspace:*",
26+
"@commontools/runner": "workspace:*",
27+
"merkle-reference": "^2.0.1",
28+
"zod": "^3.24.1",
29+
"zod-to-json-schema": "^3.24.1"
30+
},
31+
"devDependencies": {
32+
"@eslint/js": "^9.19.0",
33+
"@types/node": "^22.10.10",
34+
"eslint": "^9.19.0",
35+
"eslint-config-prettier": "^10.0.1",
36+
"eslint-plugin-prettier": "^5.2.3",
37+
"globals": "^15.14.0",
38+
"prettier": "^3.4.2",
39+
"typescript": "^5.7.3",
40+
"vite": "^6.0.11",
41+
"vite-plugin-dts": "^4.5.0",
42+
"vitest": "^3.0.4"
43+
}
44+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { Module, NAME, Recipe, TYPE, UI } from "@commontools/builder";
2+
import { getDoc, type DocLink, DocImpl, EntityId, idle, createRef, getRecipe, isDoc, isDocLink, run } from "@commontools/runner";
3+
import { createStorage } from "./storage.js";
4+
5+
export type Charm = {
6+
[NAME]?: string;
7+
[UI]?: any;
8+
[TYPE]?: string;
9+
[key: string]: any;
10+
};
11+
12+
export const storage = createStorage((import.meta as any).env.VITE_STORAGE_TYPE ?? "memory");
13+
export const charms = getDoc<DocLink[]>([], "charms");
14+
(window as any).charms = charms;
15+
16+
export async function addCharms(newCharms: DocImpl<any>[]) {
17+
await storage.syncCell(charms);
18+
19+
await idle();
20+
21+
const currentCharmsIds = charms.get().map(({ cell }) => JSON.stringify(cell.entityId));
22+
const charmsToAdd = newCharms.filter(
23+
(cell) => !currentCharmsIds.includes(JSON.stringify(cell.entityId)),
24+
);
25+
26+
if (charmsToAdd.length > 0) {
27+
charms.send([
28+
...charms.get(),
29+
...charmsToAdd.map((cell) => ({ cell, path: [] }) satisfies DocLink),
30+
]);
31+
}
32+
}
33+
34+
export function removeCharm(id: EntityId) {
35+
const newCharms = charms.get().filter(({ cell }) => cell.entityId !== id);
36+
if (newCharms.length !== charms.get().length) charms.send(newCharms);
37+
}
38+
39+
export async function runPersistent(
40+
recipe: Recipe | Module,
41+
inputs?: any,
42+
cause?: any,
43+
): Promise<DocImpl<any>> {
44+
await idle();
45+
46+
// Fill in missing parameters from other charms. It's a simple match on
47+
// hashtags: For each top-level argument prop that has a hashtag in the
48+
// description, look for a charm that has a top-level output prop with the
49+
// same hashtag in the description, or has the hashtag in its own description.
50+
// If there is a match, assign the first one to the input property.
51+
52+
// TODO: This should really be extracted into a full-fledged query builder.
53+
if (
54+
!isDoc(inputs) && // Adding to a cell input is not supported yet
55+
!isDocLink(inputs) && // Neither for cell reference
56+
recipe.argumentSchema &&
57+
(recipe.argumentSchema as any).type === "object"
58+
) {
59+
const properties = (recipe.argumentSchema as any).properties;
60+
const inputProperties =
61+
typeof inputs === "object" && inputs !== null ? Object.keys(inputs) : [];
62+
for (const key in properties) {
63+
if (!(key in inputProperties) && properties[key].description?.includes("#")) {
64+
const hashtag = properties[key].description.match(/#(\w+)/)?.[1];
65+
if (hashtag) {
66+
charms.get().forEach(({ cell }) => {
67+
const type = cell.sourceCell?.get()?.[TYPE];
68+
const recipe = getRecipe(type);
69+
const charmProperties = (recipe?.resultSchema as any)?.properties as any;
70+
const matchingProperty = Object.keys(charmProperties ?? {}).find((property) =>
71+
charmProperties[property].description?.includes(`#${hashtag}`),
72+
);
73+
if (matchingProperty) {
74+
inputs = {
75+
...inputs,
76+
[key]: { $alias: { cell, path: [matchingProperty] } },
77+
};
78+
}
79+
});
80+
}
81+
}
82+
}
83+
}
84+
85+
return run(recipe, inputs, await storage.syncCell(createRef({ recipe, inputs }, cause)));
86+
}
87+
88+
export async function syncCharm(
89+
entityId: string | EntityId | DocImpl<any>,
90+
waitForStorage: boolean = false,
91+
): Promise<DocImpl<Charm>> {
92+
return storage.syncCell(entityId, waitForStorage);
93+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { runPersistent, type Charm, addCharms, removeCharm, storage, syncCharm, charms } from "./charm.js";

typescript/packages/lookslike-high-level/src/storage-providers.ts renamed to typescript/packages/common-charm/src/storage-providers.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { EntityId, Cancel } from "@commontools/runner";
22
import { log } from "./storage.js";
3-
import { fromJSON, fromBytes, refer, type Reference } from "merkle-reference";
3+
import { fromJSON, refer, type Reference } from "merkle-reference";
44
import z from "zod";
55
import type {
66
State,
@@ -9,7 +9,6 @@ import type {
99
Entity,
1010
Unclaimed,
1111
Selector,
12-
Claim,
1312
Fact,
1413
ConflictError,
1514
TransactionError,

typescript/packages/lookslike-high-level/src/storage.ts renamed to typescript/packages/common-charm/src/storage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
InMemoryStorageProvider,
2121
RemoteStorageProvider,
2222
} from "./storage-providers.js";
23-
import { debug } from "@commontools/html";
23+
import { debug } from "@commontools/html"; // FIXME(ja): can we move debug to somewhere else?
2424

2525
export function log(...args: any[]) {
2626
// Get absolute time in milliseconds since Unix epoch
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"extends": "../../tsconfig.base.json",
3+
"moduleResolution": "nodenext",
4+
"compilerOptions": {
5+
"lib": ["es2022", "esnext.array", "esnext", "dom"],
6+
"outDir": "./lib",
7+
"declaration": true,
8+
"jsx": "preserve",
9+
"jsxFactory": "h",
10+
"jsxFragmentFactory": "Fragment",
11+
"experimentalDecorators": true,
12+
"useDefineForClassFields": false
13+
},
14+
"include": ["src/**/*.ts", "test/**/*.ts", "src/**/*.tsx", "vite-env.d.ts"],
15+
"exclude": []
16+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineConfig } from "vite";
2+
import { resolve } from "path";
3+
import dts from "vite-plugin-dts";
4+
5+
// https://vitejs.dev/config/
6+
export default defineConfig({
7+
build: {
8+
lib: { entry: resolve(__dirname, "src/index.ts"), formats: ["es"] },
9+
},
10+
resolve: { alias: { src: resolve("src/") } },
11+
plugins: [dts()],
12+
});

typescript/packages/jumble/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"@commontools/builder": "workspace:*",
2929
"@commontools/runner": "workspace:*",
3030
"@commontools/html": "workspace:*",
31-
"@commontools/lookslike-high-level": "workspace:*",
31+
"@commontools/charm": "workspace:*",
32+
"@commontools/iframe-sandbox": "workspace:*",
3233
"@react-spring/web": "^9.7.5",
3334
"@tailwindcss/typography": "^0.5.16",
3435
"@tailwindcss/vite": "^4.0.1",

typescript/packages/jumble/src/views/Shell.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,31 @@
11
// This is all you need to import/register the @commontools/ui web components
22
import "@commontools/ui";
33
import React, { useRef } from "react";
4-
import { type Charm, runPersistent } from "@commontools/lookslike-high-level";
4+
import { type Charm, runPersistent, addCharms } from "@commontools/charm";
55
import { effect, idle, run } from "@commontools/runner";
66
import { render } from "@commontools/html";
7+
import { setIframeContextHandler } from "@commontools/iframe-sandbox";
8+
import { Action, ReactivityLog, addAction, removeAction } from "@commontools/runner";
9+
10+
// FIXME(ja): perhaps this could be in common-charm? needed to enable iframe with sandboxing
11+
setIframeContextHandler({
12+
read(context: any, key: string): any {
13+
return context?.getAsQueryResult ? context?.getAsQueryResult([key]) : context?.[key];
14+
},
15+
write(context: any, key: string, value: any) {
16+
context.getAsQueryResult()[key] = value;
17+
},
18+
subscribe(context: any, key: string, callback: (key: string, value: any) => void): any {
19+
const action: Action = (log: ReactivityLog) =>
20+
callback(key, context.getAsQueryResult([key], log));
21+
22+
addAction(action);
23+
return action;
24+
},
25+
unsubscribe(_context: any, receipt: any) {
26+
removeAction(receipt);
27+
},
28+
});
729

830
export default function Shell() {
931
const containerRef = useRef<HTMLDivElement>(null);
@@ -19,15 +41,20 @@ export default function Shell() {
1941
// run the charm (this makes the logic go, cells, etc)
2042
// but nothing about rendering...
2143
const charm = await runPersistent(smolFactory);
44+
addCharms([charm]);
45+
2246
await idle();
2347
run(undefined, undefined, charm);
2448
await idle();
2549

2650
// connect the cells of the charm (reactive docs) and the
2751
// view (recipe VDOM) to be rendered using common/html
2852
// into a specific DOM element (created in react)
53+
console.log("charm", JSON.stringify(charm, null, 2));
2954
effect(charm.asCell<Charm>(), (charm) => {
55+
console.log("charm", JSON.stringify(charm, null, 2));
3056
effect(charm['$UI'], (view) => {
57+
console.log("view", JSON.stringify(view, null, 2));
3158
render(containerRef.current as HTMLElement, view);
3259
});
3360
});

typescript/packages/lookslike-high-level/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@commontools/os-ui": "workspace:*",
2929
"@commontools/runner": "workspace:*",
3030
"@commontools/iframe-sandbox": "workspace:*",
31+
"@commontools/charm": "workspace:*",
3132
"@commontools/ui": "workspace:*",
3233
"@commontools/memory": "workspace:*",
3334
"lit": "^3.2.1",

0 commit comments

Comments
 (0)