From f9596218124c97fd3313b61603d5642e14cd10d8 Mon Sep 17 00:00:00 2001 From: Seth Trowbridge Date: Mon, 3 Feb 2025 14:13:41 -0500 Subject: [PATCH] init --- .vscode/settings.json | 3 + app.js | 57 +++++++++++++++++ apptest.js | 7 +++ appx.js | 26 ++++++++ index.html | 9 +++ lib/gale/gale.js | 72 +++++++++++++++++++++ lib/test.d.ts | 6 ++ lib/test.js | 4 ++ lib/van/hmr.js | 62 ++++++++++++++++++ lib/van/van.d.ts | 48 ++++++++++++++ lib/van/van.js | 140 +++++++++++++++++++++++++++++++++++++++++ lib/van/van.members.js | 8 +++ lib/vanx/hmr.js | 8 +++ lib/vanx/vanx.d.ts | 24 +++++++ lib/vanx/vanx.js | 5 ++ tsconfig.json | 6 ++ 16 files changed, 485 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 app.js create mode 100644 apptest.js create mode 100644 appx.js create mode 100644 index.html create mode 100644 lib/gale/gale.js create mode 100644 lib/test.d.ts create mode 100644 lib/test.js create mode 100644 lib/van/hmr.js create mode 100644 lib/van/van.d.ts create mode 100644 lib/van/van.js create mode 100644 lib/van/van.members.js create mode 100644 lib/vanx/hmr.js create mode 100644 lib/vanx/vanx.d.ts create mode 100644 lib/vanx/vanx.js create mode 100644 tsconfig.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6f3a291 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.port": 5501 +} \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..636e6e7 --- /dev/null +++ b/app.js @@ -0,0 +1,57 @@ +//@ts-check +import Gale from "./lib/gale/gale.js"; +import Van from "./lib/van/van.js"; +import HMR from "./lib/van/hmr.js"; + +const css = Gale({ + Board:{ + padding: "1rem", + background: "blue", + color: "white" + } +}) + + +const Components = { + Title() + { + const titleString = HMR("title", "default!") + return ()=> div({class:css("Board"), onclick(){ + titleString.val = Math.random(); + }}, titleString.val); + } +} + +const {div} = Van.tags; +Van.add(document.body, Components.Title()); + + +const World = { + Round: Van.state(0), + Turn: Van.state(0), + Energy: Van.state([4, 4, 4]) +}; + +/** @typedef {[From:number, To:number]} Transfer */ + +/** @typedef {{Transfers:Transfer[], Power:number, Vector:number[], Charge:number, Effect:string}} Ability */ + +/** @type Ability */ +const Ability = { + Transfers:[], + Power:1, + Vector:[], + Charge:0, + Effect:"damage" +}; + + +/** @typedef {{}} Character */ + +function shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + let j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +} \ No newline at end of file diff --git a/apptest.js b/apptest.js new file mode 100644 index 0000000..12271df --- /dev/null +++ b/apptest.js @@ -0,0 +1,7 @@ +import Test from "./lib/test.js"; + + +/** @type {Test.de} */ +const thing =()=>{} + +console.log(Test.default("a", "b")); \ No newline at end of file diff --git a/appx.js b/appx.js new file mode 100644 index 0000000..611a498 --- /dev/null +++ b/appx.js @@ -0,0 +1,26 @@ +import vanX from "./lib/vanx/vanx.js"; +import Van from "./lib/van/van.js"; + + +const {span, input, button} = Van.tags; + + +const data = vanX.reactive({name: {first: "Tao", last: "Xin"}}); +const flatDerived = vanX.calc(() => `${data.name.first} ${data.name.last}`); + +const Name = () => { + + return span( + "First name: ", + input({type: "text", value: () => data.name.first, + oninput: e => data.name.first = e.target.value}), " ", + "Last name: ", + input({type: "text", value: () => data.name.last, + oninput: e => data.name.last = e.target.value}), " ", + //"Full name: ", () => derived.fullName, " ", + "CALC:", flatDerived, + button({onclick: () => data.name = {first: "Tao", last: "Xin"}}, "Reset"), + ) + } + + Van.add(document.body, Name()); \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..903178d --- /dev/null +++ b/index.html @@ -0,0 +1,9 @@ + + + + +
+ + + + \ No newline at end of file diff --git a/lib/gale/gale.js b/lib/gale/gale.js new file mode 100644 index 0000000..4646e3a --- /dev/null +++ b/lib/gale/gale.js @@ -0,0 +1,72 @@ +// @ts-check +const KeyQuery = "@"; +const KeyPseudo = ":"; +const KeyChild = "."; +const KeyGroup = "^"; + +/** @typedef { Partial & {[key: `${KeyQuery|KeyPseudo|KeyChild|KeyGroup}${string}`]: UserStyles } } UserStyles */ +/** @typedef {Record} UserSheet */ + + +/** + * @template Obj + * @typedef { { [Key in keyof Obj]: Obj[Key] extends object ? Key | CollectKeys : Key }[keyof Obj] } CollectKeys + */ + +/** + * @template Keys + * @typedef { Keys extends `${KeyChild|KeyGroup}${infer Rest}` ? Keys : never } FilterKeys + */ +/** + * @template A + * @template B + * @typedef {A extends string ? B extends string ? `${A}${B}` : never : never } CrossMultiply + */ + +/** + * @template Rec + * @typedef { keyof Rec | { [K in keyof Rec]: K extends string ? CrossMultiply>> : never }[keyof Rec] } CrossMultiplyRecord + */ + + +/** @type {(selector:string, obj:UserStyles)=>string} */ +const Tier=(selector, obj)=> +{ + const styles = Object.keys(obj).map((key)=> + { + const value = obj[key]; + switch(key[0]) + { + case KeyQuery : + return Tier(`@media(max-width:${key.substring(KeyQuery.length)})`, value); + case KeyPseudo : + return Tier(`&${key}`, value); + case KeyChild : + return Tier(`${key}`, value); + case KeyGroup : + return Tier(`&:hover .${key.substring(KeyGroup.length)}`, value); + } + return `${ key.replace(/([a-z])([A-Z])/g, '$1-$2') }: ${value};` + }); + return `${selector}{${styles.join("\n")}}`; +} + +let i = 0; +/** @type {(sheet:UserSheet&T)=> ((...args:CrossMultiplyRecord[])=>string)&{css:string}} */ +export default (sheet)=> +{ + const id = i ? "_"+i : ""; + i++; + const css = Object.keys(sheet).map(key=>Tier("."+key, sheet[key])).join(`\n`); + globalThis.document?.head.insertAdjacentHTML("beforeend", ``); + const classes =(...args)=>{ + /** @type {(needle:string, str:string)=>string} */ + const extractLast =(needle, str)=>{ + const ind = str.lastIndexOf(needle)+needle.length; + return ind ? str.substring(ind) : str; + } + return args.map((arg)=>extractLast(KeyGroup, extractLast(KeyChild, arg))).join(id+" ")+id; + } + classes.css = css; + return classes; +} \ No newline at end of file diff --git a/lib/test.d.ts b/lib/test.d.ts new file mode 100644 index 0000000..4144124 --- /dev/null +++ b/lib/test.d.ts @@ -0,0 +1,6 @@ + +export type defaultType = (a:string, b:string)=>string; + +declare const members:defaultType + +export default members \ No newline at end of file diff --git a/lib/test.js b/lib/test.js new file mode 100644 index 0000000..a6ea423 --- /dev/null +++ b/lib/test.js @@ -0,0 +1,4 @@ +export default function(a, b) +{ + return a + "---" + b; +} \ No newline at end of file diff --git a/lib/van/hmr.js b/lib/van/hmr.js new file mode 100644 index 0000000..31867ce --- /dev/null +++ b/lib/van/hmr.js @@ -0,0 +1,62 @@ +//@ts-check +import * as Van from "./van.members.js"; + +const Gateway = { + Time: 0, + Temp:{}, + Tick() + { + for(let k in Gateway.Temp) + { + localStorage.setItem(k, Gateway.Temp[k]); + } + Gateway.Temp = {}; + Gateway.Time = 0; + }, + Save(key, value) + { + Gateway.Temp[key] = value; + if(!Gateway.Time) + { + Gateway.Time = setTimeout(Gateway.Tick, 500); + } + }, + Load(key) + { + return localStorage.getItem(key); + } +} + +/** + * HMR Wrapper for Van.state + * @template T + * @param {T} value - initial value + * @param {string} key - Storage ID + * @returns {Van.State} + */ +export default function(value, key) +{ + const type = typeof value; + let reader =(data)=>data; + let writer =(data)=> data.toString(); + + if(type === "object") + { + reader = JSON.parse; + writer = JSON.stringify; + } + else if(type === "number") + { + reader = parseFloat; + } + else if(type === "boolean") + { + reader =(data)=> data === "true"; + } + + const stringValue = Gateway.Load(key); + const signal = Van.state(/**@type{T}*/(stringValue ? reader(stringValue) : value)); + Van.derive(()=>Gateway.Save(key, writer(signal.val))); + + return signal; +} \ No newline at end of file diff --git a/lib/van/van.d.ts b/lib/van/van.d.ts new file mode 100644 index 0000000..969f3e0 --- /dev/null +++ b/lib/van/van.d.ts @@ -0,0 +1,48 @@ +export interface State { + val: T + readonly oldVal: T + readonly rawVal: T +} + +// Defining readonly view of State for covariance. +// Basically we want StateView to implement StateView +export type StateView = Readonly> + +export type Val = State | T + +export type Primitive = string | number | boolean | bigint + +export type PropValue = Primitive | ((e: any) => void) | null + +export type PropValueOrDerived = PropValue | StateView | (() => PropValue) + +export type Props = Record & { class?: PropValueOrDerived; is?: string } + +export type PropsWithKnownKeys = Partial<{[K in keyof ElementType]: PropValueOrDerived}> + +export type ValidChildDomValue = Primitive | Node | null | undefined + +export type BindingFunc = ((dom?: Node) => ValidChildDomValue) | ((dom?: Element) => Element) + +export type ChildDom = ValidChildDomValue | StateView | BindingFunc | readonly ChildDom[] + +export type TagFunc = (first?: Props & PropsWithKnownKeys | ChildDom, ...rest: readonly ChildDom[]) => Result + +type Tags = Readonly>> & { + [K in keyof HTMLElementTagNameMap]: TagFunc +} + +declare function state(): State +declare function state(initVal: T): State + +export interface Van { + readonly state: typeof state + readonly derive: (f: () => T) => State + readonly add: (dom: Element, ...children: readonly ChildDom[]) => Element + readonly tags: Tags & ((namespaceURI: string) => Readonly>>) + readonly hydrate: (dom: T, f: (dom: T) => T | null | undefined) => T +} + +declare const van: Van + +export default van diff --git a/lib/van/van.js b/lib/van/van.js new file mode 100644 index 0000000..cd5ab53 --- /dev/null +++ b/lib/van/van.js @@ -0,0 +1,140 @@ +// This file consistently uses `let` keyword instead of `const` for reducing the bundle size. + +// Global variables - aliasing some builtin symbols to reduce the bundle size. +let protoOf = Object.getPrototypeOf +let changedStates, derivedStates, curDeps, curNewDerives, alwaysConnectedDom = {isConnected: 1} +let gcCycleInMs = 1000, statesToGc, propSetterCache = {} +let objProto = protoOf(alwaysConnectedDom), funcProto = protoOf(protoOf), _undefined + +let addAndScheduleOnFirst = (set, s, f, waitMs) => + (set ?? (setTimeout(f, waitMs), new Set)).add(s) + +let runAndCaptureDeps = (f, deps, arg) => { + let prevDeps = curDeps + curDeps = deps + try { + return f(arg) + } catch (e) { + console.error(e) + return arg + } finally { + curDeps = prevDeps + } +} + +let keepConnected = l => l.filter(b => b._dom?.isConnected) + +let addStatesToGc = d => statesToGc = addAndScheduleOnFirst(statesToGc, d, () => { + for (let s of statesToGc) + s._bindings = keepConnected(s._bindings), + s._listeners = keepConnected(s._listeners) + statesToGc = _undefined +}, gcCycleInMs) + +let stateProto = { + get val() { + curDeps?._getters?.add(this) + return this.rawVal + }, + + get oldVal() { + curDeps?._getters?.add(this) + return this._oldVal + }, + + set val(v) { + curDeps?._setters?.add(this) + if (v !== this.rawVal) { + this.rawVal = v + this._bindings.length + this._listeners.length ? + (derivedStates?.add(this), changedStates = addAndScheduleOnFirst(changedStates, this, updateDoms)) : + this._oldVal = v + } + }, +} + +let state = initVal => ({ + __proto__: stateProto, + rawVal: initVal, + _oldVal: initVal, + _bindings: [], + _listeners: [], +}) + +let bind = (f, dom) => { + let deps = {_getters: new Set, _setters: new Set}, binding = {f}, prevNewDerives = curNewDerives + curNewDerives = [] + let newDom = runAndCaptureDeps(f, deps, dom) + newDom = (newDom ?? document).nodeType ? newDom : new Text(newDom) + for (let d of deps._getters) + deps._setters.has(d) || (addStatesToGc(d), d._bindings.push(binding)) + for (let l of curNewDerives) l._dom = newDom + curNewDerives = prevNewDerives + return binding._dom = newDom +} + +let derive = (f, s = state(), dom) => { + let deps = {_getters: new Set, _setters: new Set}, listener = {f, s} + listener._dom = dom ?? curNewDerives?.push(listener) ?? alwaysConnectedDom + s.val = runAndCaptureDeps(f, deps, s.rawVal) + for (let d of deps._getters) + deps._setters.has(d) || (addStatesToGc(d), d._listeners.push(listener)) + return s +} + +let add = (dom, ...children) => { + for (let c of children.flat(Infinity)) { + let protoOfC = protoOf(c ?? 0) + let child = protoOfC === stateProto ? bind(() => c.val) : + protoOfC === funcProto ? bind(c) : c + child != _undefined && dom.append(child) + } + return dom +} + +let tag = (ns, name, ...args) => { + let [{is, ...props}, ...children] = protoOf(args[0] ?? 0) === objProto ? args : [{}, ...args] + let dom = ns ? document.createElementNS(ns, name, {is}) : document.createElement(name, {is}) + for (let [k, v] of Object.entries(props)) { + let getPropDescriptor = proto => proto ? + Object.getOwnPropertyDescriptor(proto, k) ?? getPropDescriptor(protoOf(proto)) : + _undefined + let cacheKey = name + "," + k + let propSetter = propSetterCache[cacheKey] ??= getPropDescriptor(protoOf(dom))?.set ?? 0 + let setter = k.startsWith("on") ? + (v, oldV) => { + let event = k.slice(2) + dom.removeEventListener(event, oldV) + dom.addEventListener(event, v) + } : + propSetter ? propSetter.bind(dom) : dom.setAttribute.bind(dom, k) + let protoOfV = protoOf(v ?? 0) + k.startsWith("on") || protoOfV === funcProto && (v = derive(v), protoOfV = stateProto) + protoOfV === stateProto ? bind(() => (setter(v.val, v._oldVal), dom)) : setter(v) + } + return add(dom, children) +} + +let handler = ns => ({get: (_, name) => tag.bind(_undefined, ns, name)}) + +let update = (dom, newDom) => newDom ? newDom !== dom && dom.replaceWith(newDom) : dom.remove() + +let updateDoms = () => { + let iter = 0, derivedStatesArray = [...changedStates].filter(s => s.rawVal !== s._oldVal) + do { + derivedStates = new Set + for (let l of new Set(derivedStatesArray.flatMap(s => s._listeners = keepConnected(s._listeners)))) + derive(l.f, l.s, l._dom), l._dom = _undefined + } while (++iter < 100 && (derivedStatesArray = [...derivedStates]).length) + let changedStatesArray = [...changedStates].filter(s => s.rawVal !== s._oldVal) + changedStates = _undefined + for (let b of new Set(changedStatesArray.flatMap(s => s._bindings = keepConnected(s._bindings)))) + update(b._dom, bind(b.f, b._dom)), b._dom = _undefined + for (let s of changedStatesArray) s._oldVal = s.rawVal +} + +export default { + tags: new Proxy(ns => new Proxy(tag, handler(ns)), handler()), + hydrate: (dom, f) => update(dom, bind(f, dom)), + add, state, derive, +} \ No newline at end of file diff --git a/lib/van/van.members.js b/lib/van/van.members.js new file mode 100644 index 0000000..86a03a4 --- /dev/null +++ b/lib/van/van.members.js @@ -0,0 +1,8 @@ +export * from "./van.js"; +import Van from "./van.js"; + +export const add = Van.add; +export const derive = Van.derive; +export const state = Van.state; +export const hydrate = Van.hydrate; +export const tags = Van.tags; \ No newline at end of file diff --git a/lib/vanx/hmr.js b/lib/vanx/hmr.js new file mode 100644 index 0000000..3827085 --- /dev/null +++ b/lib/vanx/hmr.js @@ -0,0 +1,8 @@ +import * as VanX from "./vanx.js"; + +const HMR =()=> +{ + +} + +VanX.default.reactive \ No newline at end of file diff --git a/lib/vanx/vanx.d.ts b/lib/vanx/vanx.d.ts new file mode 100644 index 0000000..373a3f8 --- /dev/null +++ b/lib/vanx/vanx.d.ts @@ -0,0 +1,24 @@ +import * as Van from "../van/van.members.js"; + +export type StateOf = { readonly [K in keyof T]: Van.State } +export type ValueType = T extends (infer V)[] ? V : T[keyof T] +export type KeyType = T extends unknown[] ? number : string +export type ReplacementFunc = + T extends (infer V)[] ? (items: V[]) => readonly V[] : + (items: [string, T[keyof T]][]) => readonly [string, T[keyof T]][] + +declare const vanX:{ + calc: (f: () => R) => R + reactive: (obj: T) => T + noreactive: (obj: T) => T, + stateFields: (obj: T) => StateOf + raw: (obj: T) => T, + list: ( + container: (() => ElementType) | ElementType, items: T, + itemFunc: (v: Van.State>, + deleter: () => void, k: KeyType) => Node + ) => ElementType, + replace: (obj: T, replacement: ReplacementFunc | T) => T, + compact: (obj: T) => T, +} +export default vanX; diff --git a/lib/vanx/vanx.js b/lib/vanx/vanx.js new file mode 100644 index 0000000..6387bb5 --- /dev/null +++ b/lib/vanx/vanx.js @@ -0,0 +1,5 @@ +import van from "../van/van.js"; +let e,t,r,{fromEntries:o,entries:l,keys:n,hasOwn:f,getPrototypeOf:a}=Object,{get:i,set:y,deleteProperty:c,ownKeys:s}=Reflect, +{state:m,derive:d,add:u}=van, +b=1e3,w=Symbol(),A=Symbol(),S=Symbol(),_=Symbol(),g=Symbol(),p=Symbol(),P=e=>(e[A]=1,e),v=e=>e instanceof Object&&!(e instanceof Function)&&!e[p],h=e=>{if(e?.[A]){let t=m();return d(()=>{let r=e();v(t.rawVal)&&v(r)?B(t.rawVal,r):t.val=x(r)}),t}return m(x(e))},F=e=>{let t=Array.isArray(e)?[]:{__proto__:a(e)};for(let[r,o]of l(e))t[r]=h(o);return t[S]=[],t[_]=m(1),t},O={get:(e,t,r)=>t===w?e:f(e,t)?Array.isArray(e)&&"length"===t?(e[_].val,e.length):e[t].val:i(e,t,r),set:(e,o,l,n)=>f(e,o)?Array.isArray(e)&&"length"===o?(l!==e.length&&++e[_].val,e.length=l,1):(e[o].val=x(l),1):o in e?y(e,o,l,n):y(e,o,h(l))&&(++e[_].val,C(e).forEach(E.bind(t,n,o,e[o],r)),1),deleteProperty:(e,t)=>(c(e,t)&&R(e,t),++e[_].val),ownKeys:e=>(e[_].val,s(e))},x=e=>!v(e)||e[w]?e:new Proxy(F(e),O),D=e=>(e[p]=1,e),j=e=>e[w],K=a(m()),N=e=>new Proxy(e,{get:(e,t,r)=>a(e[t]??0)===K?{val:k(e[t].rawVal)}:i(e,t,r)}),k=e=>e?.[w]?new Proxy(N(e[w]),O):e,C=e=>e[S]=e[S].filter(e=>e.t.isConnected),E=(e,t,r,o,{t:l,f:f})=>{let a=Array.isArray(e),i=a?Number(t):t;u(l,()=>l[g][t]=f(r,()=>delete e[t],i)),a&&!o&&i!==e.length-1&&l.insertBefore(l.lastChild,l[g][n(e).find(e=>Number(e)>i)])},R=(e,t)=>{for(let r of C(e)){let e=r.t[g];e[t]?.remove(),delete e[t]}},T=r=>(e??(setTimeout(()=>(e.forEach(C),e=t),b),e=new Set)).add(r),q=(e,t,r)=>{let o={t:e instanceof Function?e():e,f:r},n=t[w];o.t[g]={},n[S].push(o),T(n);for(let[e,r]of l(n))E(t,e,r,1,o);return o.t},z=(e,t)=>{for(let[r,o]of l(t)){let t=e[r];v(t)&&v(o)?z(t,o):e[r]=o}for(let r in e)f(t,r)||delete e[r];let r=n(t),o=Array.isArray(e);if(o||n(e).some((e,t)=>e!==r[t])){let l=e[w];if(o)e.length=t.length;else{++l[_].val;let e={...l};for(let e of r)delete l[e];for(let t of r)l[t]=e[t]}for(let{t:e}of C(l)){let{firstChild:t,[g]:o}=e;for(let l of r)t===o[l]?t=t.nextSibling:e.insertBefore(o[l],t)}}return e},B=(e,n)=>{r=1;try{return z(e,n instanceof Function?Array.isArray(e)?n(e.filter(e=>1)):o(n(l(e))):n)}finally{r=t}},G=e=>Array.isArray(e)?e.filter(e=>1).map(G):v(e)?o(l(e).map(([e,t])=>[e,G(t)])):e; +export default {calc:P,reactive:x,noreactive:D,stateFields:j,raw:k,list:q,replace:B,compact:G} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8942f42 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "checkJs": true, + "lib": ["ES2024", "DOM"] + } +} \ No newline at end of file