Repository: Gozala/reflex Branch: main Commit: 1a2460253cad Files: 18 Total size: 86.9 KB Directory structure: gitextract_p4d61kwm/ ├── .github/ │ └── workflows/ │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Readme.md ├── package.json ├── src/ │ ├── Application.js │ ├── Attribute.js │ ├── Basics.js │ ├── Document.js │ ├── Effect.js │ ├── Element.js │ ├── Program.ts │ ├── Task.ts │ ├── VirtualDOM.d.ts │ ├── VirtualDOM.js │ ├── Widget.js │ └── lib.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - main paths: - "src/**" pull_request: branches: - main paths: - "src**" - ".github/workflows/ci.yml" workflow_dispatch: jobs: check: name: Typecheck runs-on: ubuntu-latest strategy: matrix: node-version: - 16 steps: - name: Checkout uses: actions/checkout@v2 - name: Setup node ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - name: Install dependencies uses: bahmutov/npm-install@v1 - name: Typecheck uses: gozala/typescript-error-reporter-action@v1.0.8 with: project: ./tsconfig.json test-node: name: Test Node runs-on: ${{ matrix.os }} strategy: matrix: node-version: - 16 os: - ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Setup Node uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - name: Install dependencies uses: bahmutov/npm-install@v1 - name: Test run: yarn test:node test-web: name: Test Web runs-on: ubuntu-latest strategy: matrix: node-version: - 16 steps: - name: Checkout uses: actions/checkout@v2 - name: Setup Node uses: actions/setup-node@v2 with: node-version: 16 - name: Install dependencies uses: bahmutov/npm-install@v1 - name: Test run: yarn test:web ================================================ FILE: .github/workflows/release.yml ================================================ on: push: branches: - main workflow_dispatch: name: release jobs: release-please: runs-on: ubuntu-latest steps: - uses: google-github-actions/release-please-action@v3 id: release with: token: ${{ secrets.GITHUB_TOKEN }} release-type: node bump-minor-pre-major: true # The logic below handles the npm publication: - uses: actions/checkout@v2 # these if statements ensure that a publication only occurs when # a new release is created: if: ${{ steps.release.outputs.release_created }} - uses: actions/setup-node@v2 with: node-version: "16" registry-url: https://registry.npmjs.org/ if: ${{ steps.release.outputs.release_created }} - name: Install uses: bahmutov/npm-install@v1 if: ${{ steps.release.outputs.release_created }} - name: Publish run: npm publish --access=public env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} if: ${{ steps.release.outputs.release_created }} ================================================ FILE: .gitignore ================================================ dist .nyc_output tmp node_modules ================================================ FILE: Readme.md ================================================ # reflex [![Gitter][gitter.icon]][gitter.url] [![styled with prettier][prettier.icon]][prettier.url] Reflex is a reactive UI library that is heavily inspired by (pretty much is a port of) [elm][] and it's amazingly simple yet powerful [architecture][elm architecture] where "[flux][]" (in [react][] terms) is simply a byproduct of a pattern. In order to keep a major attraction of [elm][] — [algebraic data types][] & type safety — the library uses [flow][], a static type checker, that being said it's your call whether you want to use [flow][] yourself or just happer a pure JS either way this library has things to offer. Library is authored as pure ES modules and can be used with [import syntax][]. ```js import * as Reflex from "//reflex.hashbase.io/lib.js" ``` [elm]: http://elm-lang.org [elm architecture]: http://elm-lang.org/guide/architecture [react]: http://facebook.github.io/react/ [immutable.js]: https://facebook.github.io/immutable-js/ [flux]: https://facebook.github.io/flux/ [algebraic data types]: https://en.wikipedia.org/wiki/Algebraic_data_type [flow]: http://flowtype.org [gitter.url]: https://gitter.im/mozilla/reflex?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge [gitter.icon]: https://badges.gitter.im/Join%20Chat.svg [prettier.url]: https://github.com/prettier/prettier [prettier.icon]: https://img.shields.io/badge/styled_with-prettier-ff69b4.svg [import syntax]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import ================================================ FILE: package.json ================================================ { "name": "reflex", "version": "2.0.0", "description": "Functional reactive UI library", "keywords": [ "reflex", "reactive", "functional", "UI" ], "author": "Irakli Gozalishvili ", "scripts": { "prepare": "tsc --build", "test:web": "playwright-test test/**/*.spec.js --cov && nyc report", "test:node": "c8 --check-coverage --branches 85 --functions 70 --lines 80 mocha test/**/*.spec.js", "test": "npm run test:node", "typecheck": "tsc --build", "precommit": "lint-staged" }, "type": "module", "main": "src/lib.js", "types": "./dist/src/lib.d.ts", "devDependencies": { "typescript": "^4.6.3", "@types/mocha": "^9.1.0", "@types/chai": "^4.3.0", "mocha": "^9.2.2", "chai": "^4.3.6", "husky": "^7.0.4", "lint-staged": "^12.4.0", "prettier": "^2.6.2" }, "typesVersions": { "*": { "*": [ "dist/*" ], "dist/src/lib.d.ts": [ "dist/src/lib.d.ts" ] } }, "exports": { ".": { "types": "./dist/src/lib.d.ts", "import": "./src/lib.js" }, "./src/lib.js": { "types": "./dist/src/lib.d.ts", "import": "./src/lib.js" } }, "files": [ "src", "dist/src" ], "repository": { "type": "git", "url": "https://github.com/gozala/reflex.git" }, "homepage": "https://github.com/gozala/reflex", "c8": { "exclude": [ "test/**", "dist/**" ] }, "lint-staged": { "*.js": [ "prettier --parser flow --no-semi --write", "git add" ] }, "license": "MIT" } ================================================ FILE: src/Application.js ================================================ import { DocumentWidget } from "./Document.js" /** * @template T * @typedef {import('./Program').DocumentView} DocumentView */ /** * @template Message, State * @extends {DocumentWidget} */ class ApplicationWidget extends DocumentWidget { /** * * @param {import('./Program').Router} router */ constructor(router) { super() this.onExternalURLRequest = router.onExternalURLRequest this.onInternalURLRequest = router.onInternalURLRequest this.onURLChange = router.onURLChange } getURL() { return new URL(this.root.location.href) } /** * @param {MouseEvent} event */ handleEvent(event) { switch (event.type) { case "navigate": // manually notify when we do pushState replaceState case "popstate": case "hashchange": return this.thread.send(this.onURLChange(this.getURL())) case "click": { if ( !event.ctrlKey && !event.metaKey && !event.shiftKey && event.button < 1 && // @ts-ignore !event.target.target && // @ts-ignore !event.target.download ) { event.preventDefault() const current = this.getURL() // @ts-ignore const next = new URL(event.currentTarget.href, current.href) const isInternal = current.protocol === next.protocol && current.host === next.host && current.port === next.port const message = isInternal ? this.onInternalURLRequest(next) : this.onExternalURLRequest(next) return this.thread.send(message) } } } return undefined } /** * @param {Document} document */ addListeners(document) { const top = document.defaultView if (top) { top.addEventListener("popstate", this) top.addEventListener("hashchange", this) // @ts-ignore top.onnavigate = this } else { throw new Error("document.defaultView is not defined") } } } /** * @template {{url?:URL}} Options * @template Message, State * @param {import('./Program').Application, Options>} application * @param {Options} options * @param {Document} document * @returns {ApplicationWidget} */ export const spawn = ( application, options, document ) /*: ApplicationWidget */ => { const self = new ApplicationWidget(application) const root = DocumentWidget.root(document) self.update = application.update self.view = application.view self.root = root self.node = root.widget ? root.widget.node : self.mount(root) self.thread = self.fork(root) root.widget = self self.addListeners(document) options.url = self.getURL() self.transact(application.init(options)) return self } ================================================ FILE: src/Attribute.js ================================================ // @flow strict import { attribute, property, style, on } from "./VirtualDOM.js" export { style, attribute, property, on } // // TODO: defaultValue, defaultChecked, innerHTML, suppressContentEditableWarning, suppressHydrationWarning, style /** * @template {string|number|boolean} T * @param {T} value */ export const defaultValue = value => property("defaultValue", value) /** * @template {string|number|boolean} T * @param {T} value */ export const value = value => property("value", value) /** * @template {string} T * @param {T} value */ export const acceptCharset = value => property("accept-charset", value) /** * @template {string} T * @param {T} value */ export const className = value => attribute("class", value) /** * @param {string[]} values */ export const classList = (...values) => attribute("class", values.join(" ")) /** * * @param {string} value */ export const textContent = value => property("textContent", value) /** * @param {string} value */ export const For = value => attribute("for", value) /** * @param {string} value */ export const Equiv = value => attribute("equiv", value) /** * @param {string} name * @param {string} value */ export const data = (name, value) => attribute(`data-${name}`, value) /** * @param {string} name */ const setHTMLAttribute = name => /** * @param {string} [value] */ (value = "") => attribute(name, value) export const src = setHTMLAttribute("src") export const srcset = setHTMLAttribute("srcset") export const alt = setHTMLAttribute("alt") export const href = setHTMLAttribute("href") export const id = setHTMLAttribute("id") export const accept = setHTMLAttribute("accept") export const type = setHTMLAttribute("type") export const placeholder = setHTMLAttribute("placeholder") export const title = setHTMLAttribute("title") /** * @param {string} name */ const setBooleanHTMLAttribute = name => /** * @param {boolean} value */ value => attribute(name, value ? "true" : "false") export const contentEditable = setBooleanHTMLAttribute("contenteditable") export const draggable = setBooleanHTMLAttribute("draggable") export const spellCheck = setBooleanHTMLAttribute("spellcheck") /** * @param {string} name */ const setBooleanSVGAttribute = name => /** * @param {boolean} value */ value => attribute(name, value ? "true" : "false") export const autoReverse = setBooleanSVGAttribute("autoReverse") export const externalResourcesRequired = setBooleanSVGAttribute( "externalResourcesRequired" ) export const preserveAlpha = setBooleanSVGAttribute("preserveAlpha") /** * * @param {string} name */ const setModalHTMLAttribute = name => /** * * @param {boolean} [value] */ (value = true) => attribute(name, value ? "" : null) export const allowFullScreen = setModalHTMLAttribute("allowfullscreen") export const async = setModalHTMLAttribute("async") export const autoFocus = setModalHTMLAttribute("autofocus") export const autoPlay = setModalHTMLAttribute("autoplay") export const controls = setModalHTMLAttribute("controls") export const htmlDefault = setModalHTMLAttribute("default") export const defer = setModalHTMLAttribute("defer") export const disabled = setModalHTMLAttribute("disabled") export const formNoValidate = setModalHTMLAttribute("formnovalidate") export const hidden = setModalHTMLAttribute("hidden") export const loop = setModalHTMLAttribute("loop") export const noValidate = setModalHTMLAttribute("novalidate") export const open = setModalHTMLAttribute("open") export const playsInline = setModalHTMLAttribute("playsinline") export const readOnly = setModalHTMLAttribute("readonly") export const required = setModalHTMLAttribute("required") export const reversed = setModalHTMLAttribute("reversed") export const scoped = setModalHTMLAttribute("scoped") export const seamless = setModalHTMLAttribute("seamless") export const itemScope = setModalHTMLAttribute("itemscope") /** * @param {string} name */ const setBooleanProperty = name => /** * * @param {boolean} value */ value => property(name, value) export const checked = setBooleanProperty("checked") export const multiple = setBooleanProperty("multiple") export const muted = setBooleanProperty("muted") export const selected = setBooleanProperty("selected") /** * @param {string} name */ const setOptionalStringAttribute = name => /** * * @param {string} [value] */ (value = "") => attribute(name, value) export const capture = setOptionalStringAttribute("capture") export const download = setOptionalStringAttribute("download") /** * @param {string} name */ const setNumberAttribute = name => /** * @param {number} value */ value => attribute(name, `${value}`) export const cols = setNumberAttribute("cols") export const rows = setNumberAttribute("rows") export const size = setNumberAttribute("size") export const span = setNumberAttribute("span") export const tabIndex = setNumberAttribute("tabindex") export const rowSpan = setNumberAttribute("rowSpan") export const start = setNumberAttribute("start") ================================================ FILE: src/Basics.js ================================================ /** * @template T * @param {T} value * @returns {T} */ export const identity = value => value /** * @template T * @param {T} value * @returns {() => T} */ export const always = value => () => value export const True = always(/** @type {true}*/ true) export const False = always(/** @type {false}*/ false) export const Null = always(null) export const Void = always(/** @type {undefined} */ undefined) export const EmptyString = always(/** @type {""} */ "") export const EmptyObject = always(Object.freeze({})) /** @type {readonly any[]} */ const anyArray = Object.freeze(/** @type {any[]}*/ []) export const EmptyArray /*: <$, a>($) => a[] */ = always(anyArray) /** @type {Record} */ const table = Object.freeze(Object.create(null)) export const EmptyTable = always(table) /** * @param {never} value * @returns {any} */ export const unreachable = value => { console.error(`value passed to never`, value) throw TypeError( `unreachable was supposed to be unreachable but it was called with ${value}` ) } /** * @template T * @param {T} [value] */ export const nothing = value => void value const defaultReason = "Typesystem established invariant was broken at runtime, likely due to incorrect call from untyped JS." /** * @param {Error} reason * @returns {never} */ export const panic = /*:: */(reason = new Error(defaultReason)) => { throw reason } ================================================ FILE: src/Document.js ================================================ // @flow strict import { virtualize, diff, patch, doc } from "./VirtualDOM.js" import { Widget, MainThread } from "./Widget.js" /** * @template Message * @typedef {import('./Program').DocumentView} DocumentView */ /** * @template Message * @typedef {{ * body: Element * title: string * location: Location * widget?: { * node: DocumentView * thread: MainThread * } * }} RenderedDocument */ /** * @template Message, State * @extends {Widget, RenderedDocument>} */ export class DocumentWidget extends Widget { /** * @template Message * @param {Document} document * @returns {RenderedDocument} */ static root(document) { const root = document if (!document.body) { document.appendChild(document.createElement("body")) } return root } /** * @param {RenderedDocument} root * @returns {DocumentView} */ mount(root) { return root.widget ? root.widget.node : doc(root.title, virtualize(root.body)) } /** * * @param {RenderedDocument} root * @returns {MainThread} */ fork(root) { const thread = root.widget ? root.widget.thread : Widget.fork(this) thread.root = this return thread } /** * * @param {State} state */ render(state) { const newDocument = this.view(state) const renderedDocument = this.node const delta = diff(renderedDocument.body, newDocument.body) patch(this.root.body, renderedDocument.body, delta, this.thread) this.node = newDocument if (renderedDocument.title !== newDocument.title) { this.root.title = newDocument.title } } } /** * @template Message, State, Options * @param {import('./Program').Program, Options>} program * @param {Options} options * @param {Document} document * @returns {Widget, RenderedDocument>} */ export const spawn = ({ init, update, view }, options, document) => { const self = new DocumentWidget() const root = DocumentWidget.root(document) self.update = update self.view = view self.root = root self.node = self.mount(root) self.thread = self.fork(root) root.widget = self self.transact(init(options)) return self } ================================================ FILE: src/Effect.js ================================================ import { nothing } from "./Basics.js" /** * @template T * @typedef {import('./Task').Effect} Effect */ /** * @implements {Effect} */ class None { perform() {} map() { return this } } const none = new None() /** * @template T * @typedef {import('./Task').Main} Main */ /** * @template T * @implements {Effect} */ class Send { /** * * @param {T} message */ constructor(message) { /** * @private */ this.message = message } /** * @param {Main} main */ perform(main) { main.send(this.message) } /** * @template U * @param {(value:T) => U} tag * @returns {Effect} */ map(tag) { return new Tagged(this, tag) } } /** * @template X, I, O */ class FX { /** * @param {import('./Task').Task} task * @param {(value:I) => void|O} success * @param {(error:X) => void|O} failure */ constructor(task, success, failure) { /** * @private */ this.task = task /** * @private */ this.success = success /** * @private */ this.failure = failure } /** * @private * @param {Main} main */ async execute(main) { try { const value = await this.task() const message = this.success(value) if (message != null) { main.send(message) } } catch (error) { const message = this.failure(/** @type {X} */(error)) if (message != null) { main.send(message) } } } /** * @param {Main} main */ perform(main) { this.execute(main) } /** * @template U * @param {(value:O) => U} tag * @returns {Effect} */ map(tag) { return new Tagged(this, tag) } } /** * @template T * @implements {Effect} */ class Batch { /** * * @param {Effect[]} effects */ constructor(effects) { /** * @private */ this.effects = effects } /** * @param {Main} main */ perform(main) { for (const fx of this.effects) { fx.perform(main) } } /** * @template U * @param {(value:T) => U} tag * @returns {Effect} */ map(tag) { return new Tagged(this, tag) } } /** * @typedef {import('./Task').Thread} Thread * @typedef {import('./Task').ThreadID} ThreadID */ /** * @template T, U * @implements {Effect} * @implements {Main} */ export class Tagged { /** * @param {Effect} fx * @param {(value:T) => U} tag */ constructor(fx, tag) { /** * @private */ this.fx = fx /** * @private */ this.tag = tag /** * @private * @type {Main} */ this.port } /** * * @param {Main} main */ perform(main) { this.port = main this.fx.perform(this) } /** * @param {Thread} thread */ link(thread) { return this.port.link(thread) } /** * @param {Thread} thread */ unlink(thread) { return this.port.unlink(thread) } /** * @param {ThreadID} id */ linked(id) { return this.port.linked(id) } /** * @param {T} message */ send(message) { return this.port.send(this.tag(message)) } /** * @template E * @param {(value:U) => E} tag * @returns {Effect} */ map(tag) { return new Tagged(this, tag) } } export const nofx = none /** * Creates an effect that will execute a given task and send * a message back to program. Provided handlers are used to turn task * result into message * * @template X, I, O * @param {import('./Task').Task} task * @param {(value:I) => O|void} [ok] * @param {(error:X) => O|void} [error] */ export const fx = (task, ok = nothing, error = warn) /*: Effect */ => new FX(task, ok, error) /** * Sends a given message to the program * * @template T * @param {T} message */ export const send = message => new Send(message) /** * @template T * @param {Effect[]} fx */ export const batch = (...fx) => new Batch(fx) /** * @param {unknown} error */ const warn = error => { console.warn("Task failed but error was not handled", error) } ================================================ FILE: src/Element.js ================================================ import { node, text, doc, keyedNode, customElement } from "./VirtualDOM.js" /** * @param {string} tag */ const factory = tag => /** * @template T * @param {import('./VirtualDOM').Attribute[]} [settings] * @param {import('./VirtualDOM').Node[]} [children] */ (settings, children) => node(tag, settings, children) export { text, node, doc, customElement, keyedNode } export const html = factory("html") export const link = factory("link") export const meta = factory("meta") export const style = factory("style") export const body = factory("body") export const address = factory("address") export const article = factory("article") export const aside = factory("aside") export const footer = factory("footer") export const header = factory("header") export const h1 = factory("h1") export const h2 = factory("h2") export const h3 = factory("h3") export const h4 = factory("h4") export const h5 = factory("h5") export const h6 = factory("h6") export const hgroup = factory("hgroup") export const nav = factory("nav") export const section = factory("section") export const blockquote = factory("blockquote") export const dd = factory("dd") export const dir = factory("dir") export const div = factory("div") export const dl = factory("dl") export const dt = factory("dt") export const figcaption = factory("figcaption") export const figure = factory("figure") export const hr = factory("hr") export const li = factory("li") export const main = factory("main") export const ol = factory("ol") export const p = factory("p") export const pre = factory("pre") export const ul = factory("ul") export const a = factory("a") export const abbr = factory("abbr") export const b = factory("b") export const bdi = factory("bdi") export const bdo = factory("bdo") export const br = factory("br") export const cite = factory("cite") export const code = factory("code") export const data = factory("data") export const dfn = factory("dfn") export const em = factory("em") export const i = factory("i") export const kbd = factory("kbd") export const mark = factory("mark") export const nobr = factory("nobr") export const q = factory("q") export const rp = factory("rp") export const rt = factory("rt") export const rtc = factory("rtc") export const ruby = factory("ruby") export const s = factory("s") export const samp = factory("samp") export const small = factory("small") export const span = factory("span") export const strong = factory("strong") export const sub = factory("sub") export const sup = factory("sup") export const time = factory("time") export const u = factory("u") export const Var = factory("var") export const wbr = factory("wbr") export const area = factory("area") export const audio = factory("audio") export const img = factory("img") export const map = factory("map") export const track = factory("track") export const video = factory("video") export const applet = factory("applet") export const embed = factory("embed") export const noembed = factory("noembed") export const object = factory("object") export const param = factory("param") export const picture = factory("picture") export const source = factory("source") export const canvas = factory("canvas") export const noscript = factory("noscript") export const script = factory("script") export const del = factory("del") export const ins = factory("ins") export const caption = factory("caption") export const col = factory("col") export const colgroup = factory("colgroup") export const table = factory("table") export const tbody = factory("tbody") export const td = factory("td") export const tfoot = factory("tfoot") export const th = factory("th") export const thead = factory("thead") export const tr = factory("tr") export const button = factory("button") export const datalist = factory("datalist") export const fieldset = factory("fieldset") export const form = factory("form") export const input = factory("input") export const label = factory("label") export const legend = factory("legend") export const meter = factory("meter") export const optgroup = factory("optgroup") export const option = factory("option") export const output = factory("output") export const progress = factory("progress") export const select = factory("select") export const textarea = factory("textarea") export const details = factory("details") export const dialog = factory("dialog") export const menu = factory("menu") export const menuitem = factory("menuitem") export const summary = factory("summary") export const content = factory("content") export const element = factory("element") export const shadow = factory("shadow") export const slot = factory("slot") export const template = factory("template") export const acronym = factory("acronym") export const basefont = factory("basefont") export const bgsound = factory("bgsound") export const big = factory("big") export const blink = factory("blink") export const center = factory("center") export const command = factory("command") export const font = factory("font") export const frame = factory("frame") export const frameset = factory("frameset") export const image = factory("image") export const isindex = factory("isindex") export const keygen = factory("keygen") export const listing = factory("listing") export const marquee = factory("marquee") export const multicol = factory("multicol") export const nextid = factory("nextid") export const noframes = factory("noframes") export const plaintext = factory("plaintext") export const spacer = factory("spacer") export const strike = factory("strike") export const tt = factory("tt") export const xmp = factory("xmp") ================================================ FILE: src/Program.ts ================================================ import { Transaction, Main } from "./Task" import { Node } from "./VirtualDOM" export interface Program { init(options: Options): Transaction update(message: Message, state: State): Transaction view(state: State): View } export interface Router { onExternalURLRequest(url: URL): Message onInternalURLRequest(url: URL): Message onURLChange(url: URL): Message } export interface Application extends Router, Program {} export interface DocumentView { title: string body: Node } export interface RenderedDocument { body: Element title: string location: Location widget?: { node: DocumentView thread: Main } } ================================================ FILE: src/Task.ts ================================================ export type Phantom = T & { readonly kind: unique symbol } export type ThreadID = Phantom export interface Thread { exit(reason?: Error): void } export interface Port { send(message: T): unknown } export interface Main extends Port { link(thread: Thread): ThreadID unlink(thread: Thread): void linked(thread: ThreadID): Thread | undefined | null } export interface IO { perform(thread: Main): unknown } export type Transaction = [state, IO] export interface Sync { sync(value: T): void } export interface Effect extends IO { map(tag: (value: T) => U): Effect } export interface AsyncResult { then(succeed: (value: T) => void, fail: (error: X) => void): unknown } export interface Task { (): AsyncResult } ================================================ FILE: src/VirtualDOM.d.ts ================================================ import { Port } from "./Task" export interface Doc { title: string body: Node map(f: (inn: T) => U): Doc } export declare class Node { settings(): Attribute[] children(): Node[] map(f: (inn: T) => U): Node } export declare class Attribute { map(f: (inn: T) => U): Attribute } export type Keyed = [string, T] export declare function node< A extends Attribute, C extends Node >(localName: string, attributes?: A[], children?: C[]): EventNode type EventSource = Attribute | Node type EventNode> = E extends EventSource ? Node : never export declare function customElement>( localName: string, constructor: { new (): HTMLElement }, attributes?: A[] ): EventNode export declare function keyedNode< A extends Attribute, C extends Node >(localName: string, attributes?: A[], children?: Keyed[]): EventNode export declare function keyedNodeNS< A extends Attribute, C extends Node >( namespace: string, localName: string, attributes?: A[], children?: Keyed[] ): EventNode export declare function nodeNS< A extends Attribute, C extends Node >( namespace: string, localName: string, attributes?: A[], children?: C[] ): EventNode export declare function text(value: string): Node export declare function doc(title: string, body: Node): Doc export declare function property(name: string, value: T): Attribute export declare function attribute( name: string, value: null | string ): Attribute export declare function attributeNS( namespace: string, name: string, value: string | boolean | number | null | void ): Attribute export declare function style(string, string): Attribute export interface Decoder { decode(inn): Out | Error } export type EncodedEvent = | Event | DragEvent | MouseEvent | KeyboardEvent | UIEvent export interface DecodedEvent { message: T preventDefault?: boolean stopPropagation?: boolean } export interface EventDecoder extends Decoder> {} export declare function on( type: string, decoder: EventDecoder ): Attribute declare class Delta {} export type { Delta } export declare function diff(before: Node, after: Node): Delta export declare function patch( root: EventTarget, current: Node, delta: Delta, port: Port ): void export declare function virtualize(EventTarget): Node ================================================ FILE: src/VirtualDOM.js ================================================ // @ts-nocheck const TEXT = "VirtualDOM.Text" const NODE = "VirtualDOM.Node" const KEYED_NODE = "VirtualDOM.Keyed.Node" const CUSTOM_NODE = "VirtualDOM.Custom" const CUSTOM_ELEMENT = "VirtualDOM.CustomElement" const TAGGED_NODE = "VirtualDOM.Tagger" const THUNK_NODE = "VirtualDOM.Thunk" const OP_REDRAW = "VirtualDOM.OP.Redraw" const OP_THUNK = "VirtualDOM.OP.Thunk" const OP_TAGGER = "VirtualDOM.OP.Tagger" const OP_TEXT = "VirtualDOM.OP.Text" const OP_FACTS = "VirtualDOM.OP.Facts" const OP_CUSTOM = "VirtualDOM.OP.Custom" const OP_REMOVE_LAST = "VirtualDOM.OP.RemoveLast" const OP_APPEND = "VirtualDOM.OP.Append" const OP_REORDER = "VirtualDOM.OP.Reorder" const OP_REMOVE = "VirtualDOM.OP.Remove" const OP_DELETE = "VirtualDOM.OP.Delete" const OP_INSERT = "VirtualDOM.OP.Insert" const OP_MOVE = "VirtualDOM.OP.Move" const SETTING_EVENT = "VirtualDOM.Setting.Event" const SETTING_STYLE = "VirtualDOM.Setting.Style" const SETTING_PROPERTY = "VirtualDOM.Setting.Property" const SETTING_ATTRIBUTE = "VirtualDOM.Setting.Attribute" const SETTING_ATTRIBUTE_NS = "VirtualDOM.Setting.AttributeNS" // HELPERS function appendChild(parent, child) { parent.appendChild(child) } var init = function(virtualNode, flagDecoder, debugMetadata, args) { const { node } = args node.parentNode.replaceChild( render(node.ownerDocument, virtualNode, function() {}), node ) return {} } // TEXT class Text { constructor(content) { this.nodeType = TEXT this.text = content } map(tagger) { return this } toInnerHTML() { return "" } toOuterHTML(indent) { return `${indent}${this.text}` } get innerHTML() { return this.toInnerHTML() } get outerHTML() { return this.toOuterHTML() } } export const text = content => new Text(content) // NODE const noKids = Object.freeze([]) const noFacts = Object.freeze([]) class CustomElement { constructor(localName, elementConstructor, options, settings) { this.nodeType = CUSTOM_ELEMENT this.localName = localName this.elementConstructor = elementConstructor this.options = options this.settings = settings this.children = noKids } map(tagger) { return new TaggerNode(tagger, this) } serializeSettings(key = "") { return Node.prototype.serializeSettings.call(this, key) } toOuterHTML(indent = "", key = "") { return Node.prototype.toOuterHTML.call(this, indent, key) } toInnerHTML(indent = "") { return Node.prototype.toInnerHTML.call(this, indent) } get outerHTML() { return this.toOuterHTML() } get innerHTML() { return this.toInnerHTML() } } export const customElement = (localName, constructor, settings) => new CustomElement(localName, constructor, undefined, organizeFacts(settings)) class Node { constructor(localName, settings, children, namespace, descendantsCount) { this.nodeType = NODE this.localName = localName this.settings = settings this.children = children this.namespace = namespace this.descendantsCount = descendantsCount } map(tagger) { return new TaggerNode(tagger, this) } toInnerHTML(indent = "") { const { children } = this let html = "" if (children.length > 0) { const childIndent = `${indent} ` for (const child of children) { html += `\n${child.toOuterHTML(indent, "")}` } } return html } serializeSettings(key) { let buffer = "" if (key !== "") { buffer += ` key=${key}` } const { settings } = this for (const type in settings) { const group = settings[type] for (const name in group) { const value = group[name] switch (type) { case SETTING_ATTRIBUTE: { buffer += ` ${name}="${value}"` break } case SETTING_EVENT: { buffer += ` on${name}` break } case SETTING_PROPERTY: { buffer += ` ${name}=${value}` } } } } if (this.namespace) { buffer += ` xmlns="${namespace}"` } return buffer } toOuterHTML(indent = "", key = "") { const { localName } = this const open = `<${localName}${this.serializeSettings(key)}>` const close = `` const innerHTML = this.toInnerHTML(`${indent} `) if (innerHTML === "") { return `${indent}${open}${close}` } else { return `${indent}${open}${innerHTML}\n${indent}${close}` } } get innerHTML() { return this.toInnerHTML() } get outerHTML() { return this.toOuterHTML() } } export const nodeNS = ( namespace, localName, factList = noFacts, kidList = noKids ) => { for ( var kids = [], descendantsCount = 0, index = 0; kidList.length > index; index++ ) { var kid = kidList[index] descendantsCount += kid.descendantsCount || 0 kids.push(kid) } descendantsCount += kids.length return new Node( localName, organizeFacts(factList), kids, namespace, descendantsCount ) } export var node = nodeNS.bind(null, null) // KEYED NODE class KeyedNode { constructor(localName, facts, kids, namespace, descendantsCount) { this.nodeType = KEYED_NODE this.localName = localName this.settings = facts this.children = kids this.namespace = namespace this.descendantsCount = descendantsCount } map(tagger) { return new TaggerNode(tagger, this) } serializeSettings(key = "") { return Node.prototype.serializeSettings.call(this, key) } toOuterHTML(indent = "", key = "") { return Node.prototype.toOuterHTML.call(this, indent, key) } toInnerHTML(indent = "") { let html = "" const { children } = this if (children.length > 0) { for (const [key, child] of children) { html += `\n${child.toOuterHTML(indent, key)}` } } return html } get outerHTML() { return this.toOuterHTML() } get innerHTML() { return this.toInnerHTML() } } export const keyedNodeNS = (namespace, localName, factList, kidList) => { for ( var kids = [], descendantsCount = 0, index = 0; kidList.length > index; index++ ) { var kid = kidList[index] descendantsCount += kid[1].descendantsCount || 0 kids.push(kid) } descendantsCount += kids.length return new KeyedNode( localName, organizeFacts(factList), kids, namespace, descendantsCount ) } export var keyedNode = keyedNodeNS.bind(null, null) // CUSTOM class CustomNode { constructor(facts, model, render, diff) { this.nodeType = CUSTOM_NODE this.settings = facts this.model = model this.render = render this.diff = diff } map(tagger) { return new TaggerNode(tagger, this) } serializeSettings(key = "") { return Node.prototype.serializeSettings.call(this, key) } toOuterHTML(indent = "", key = "") { const settings = key === "" ? ` key=${key}` : "" return `${indent}` } toInnerHTML(indent = "") { return "" } get outerHTML() { return this.toOuterHTML() } get innerHTML() { return this.toInnerHTML() } } export const custom = (factList, model, render, diff) => new CustomNode(organizeFacts(factList), model, render, diff) class Doc { constructor(title, body) { this.title = title this.body = body } map(tagger) { return new Doc(this.title, this.body.map(tagger)) } toOuterHTML(indent = "", key = "") { const innerHTML = this.toInnerHTML(`${indent} `) return `${indent}\n${innerHTML}\n${indent}<\html>` } toInnerHTML(indent = "") { const head = `${this.title}` const body = this.body.toOuterHTML(indent) if (body === "") { return `${indent}${head}` } else { return `${indent}${head}\n${body}` } } get outerHTML() { return this.toOuterHTML() } get innerHTML() { return this.toInnerHTML() } } export const doc = (title, body) => new Doc(title, body) // MAP class TaggerNode { constructor(tagger, node) { this.nodeType = TAGGED_NODE this.tagger = tagger this.node = node this.descendantsCount = 1 + (node.descendantsCount || 0) } map(tagger) { return new TaggerNode(tagger, this) } toOuterHTML(indent = "", key = "") { return this.node.toOuterHTML(indent, key) } toInnerHTML(indent = "") { return this.node.toInnerHTML(indent) } get outerHTML() { return this.toOuterHTML() } get innerHTML() { return this.toInnerHTML() } } export const map = (tagger, node) => new TaggerNode(tagger, node) // LAZY class Thunk { constructor(refs, thunk) { this.nodeType = THUNK_NODE this.refs = refs this.force = thunk this.node = undefined } map(tagger) { return new TaggerNode(tagger, this) } toNode() { const { node } = this if (node) { return node } else { const node = this.force() this.node = node return node } } toOuterHTML(indent = "", key = "") { return this.toNode().toOuterHTML(indent, key) } toInnerHTML(indent = "") { const node = (vNode.node = vNode.force()) return this.toNode().toInnerHTML(indent) } get outerHTML() { return this.toOuterHTML() } get innerHTML() { return this.toInnerHTML() } } const thunk = (refs, thunk) => new Thunk(refs, thunk) export var lazy = function(func, a) { return thunk([func, a], function() { return func(a) }) } export var lazy2 = function(func, a, b) { return thunk([func, a, b], function() { return func(a, b) }) } export var lazy3 = function(func, a, b, c) { return thunk([func, a, b, c], function() { return func(a, b, c) }) } export var lazy4 = function(func, a, b, c, d) { return thunk([func, a, b, c, d], function() { return func(a, b, c, d) }) } export var lazy5 = function(func, a, b, c, d, e) { return thunk([func, a, b, c, d, e], function() { return func(a, b, c, d, e) }) } export var lazy6 = function(func, a, b, c, d, e, f) { return thunk([func, a, b, c, d, e, f], function() { return func(a, b, c, d, e, f) }) } export var lazy7 = function(func, a, b, c, d, e, f, g) { return thunk([func, a, b, c, d, e, f, g], function() { return func(a, b, c, d, e, f, g) }) } export var lazy8 = function(func, a, b, c, d, e, f, g, h) { return thunk([func, a, b, c, d, e, f, g, h], function() { return func(a, b, c, d, e, f, g, h) }) } // FACTS const notaggers = Object.freeze([]) export const on = function(key, handler) { return new EventHandler(key, handler, Normal, notaggers) } class VirtualDOMStyle { constructor(key, value) { this.nodeType = SETTING_STYLE this.key = key this.value = value } map(tag) { return this } } export var style = function(key, value) { return new VirtualDOMStyle(key, value) } class VirtualDOMProperty { constructor(key, value) { this.nodeType = SETTING_PROPERTY this.key = key this.value = value } map(tag) { return this } } export var property = function(key, value) { return new VirtualDOMProperty(key, value) } class VirtualDOMAttribute { constructor(key, value) { this.nodeType = SETTING_ATTRIBUTE this.key = key this.value = value } map(tag) { return this } } export const attribute = (key, value) => new VirtualDOMAttribute(key, value) class VirtualDOMAttributeNS { constructor(namespace, key, value) { this.nodeType = SETTING_ATTRIBUTE_NS this.key = key this.value = { namespace: namespace, value: value } } map(tag) { return this } } export const attributeNS = (namespace, key, value) => new VirtualDOMAttributeNS(namespace, key, value) // XSS ATTACK VECTOR CHECKS export function noScript(localName) { return localName == "script" ? "p" : localName } export function noOnOrFormAction(key) { return /^(on|formAction$)/i.test(key) ? "data-" + key : key } export function noInnerHtmlOrFormAction(key) { return key == "innerHTML" || key == "formAction" ? "data-" + key : key } export function noJavaScriptUri__PROD(value) { return /^javascript:/i.test(value.replace(/\s/g, "")) ? "" : value } export function noJavaScriptUri__DEBUG(value) { return /^javascript:/i.test(value.replace(/\s/g, "")) ? 'javascript:alert("This is an XSS vector. Please use ports or web components instead.")' : value } export function noJavaScriptOrHtmlUri__PROD(value) { return /^\s*(javascript:|data:text\/html)/i.test(value) ? "" : value } export function noJavaScriptOrHtmlUri__DEBUG(value) { return /^\s*(javascript:|data:text\/html)/i.test(value) ? 'javascript:alert("This is an XSS vector. Please use ports or web components instead.")' : value } // MAP FACTS class EventHandler { constructor(type, decoder, eventPhase, taggers) { this.nodeType = SETTING_EVENT this.type = type this.decoder = decoder this.eventPhase = eventPhase this.taggers = taggers } map(tag) { const { type, decoder, eventPhase, taggers } = this return new EventHandler(type, decoder, eventPhase, [tag, ...taggers]) } forwardEvent(event, eventTarget) { const { eventPhase, taggers } = this const result = this.decoder.decode(event) if (result instanceof Error) { return } const tag = this.eventPhase // 0 = Normal // 1 = MayStopPropagation // 2 = MayPreventDefault // 3 = Custom const { stopPropagation, preventDefault } = result if (stopPropagation) { event.stopPropagation() } if (preventDefault) { event.preventDefault() } let message = result.message for (let tag of this.taggers) { message = tag(message) } let currentTarget = eventTarget while (currentTarget.tagger) { const { tagger } = currentTarget if (typeof tagger == "function") { message = tagger(message) } else { for (let i = tagger.length; i--; ) { message = tagger[i](message) } } currentTarget = currentTarget.parent } if (stopPropagation) { currentTarget.sync(message) // stopPropagation implies isSync } else { currentTarget.send(message) } } equal(other) { return ( this.type === other.type && this.eventPhase === other.eventPhase && this.decoder === other.decoder && pairwiseRefEqual(this.taggers, other.taggers) ) } } // ORGANIZE FACTS function organizeFacts(factList) { for ( var facts = {}, index = 0; factList.length > index; index++ // WHILE_CONS ) { var fact = factList[index] var nodeType = fact.nodeType switch (nodeType) { case SETTING_PROPERTY: { const { key, value } = fact if (key === "className") { addClass(facts, key, value) } else { facts[key] = value } break } case SETTING_STYLE: { const { key, value } = fact var subFacts = facts[nodeType] || (facts[nodeType] = {}) subFacts[key] = value break } case SETTING_EVENT: { const { type, handler } = fact var subFacts = facts[nodeType] || (facts[nodeType] = {}) subFacts[type] = fact break } case SETTING_ATTRIBUTE: { const { key, value } = fact var subFacts = facts[nodeType] || (facts[nodeType] = {}) if (key === "class") { addClass(subFacts, key, value) } else { subFacts[key] = value } break } case SETTING_ATTRIBUTE_NS: { const { key, value } = fact var subFacts = facts[nodeType] || (facts[nodeType] = {}) if (key === "class") { addClass(subFacts, key, value) } else { subFacts[key] = value } break } } } return facts } function addClass(object, key, newClass) { var classes = object[key] object[key] = classes ? classes + " " + newClass : newClass } // RENDER function render(doc, vNode, eventNode) { var nodeType = vNode.nodeType if (nodeType === THUNK_NODE) { return render(doc, vNode.node || (vNode.node = vNode.force()), eventNode) } if (nodeType === TEXT) { return doc.createTextNode(vNode.text) } if (nodeType === TAGGED_NODE) { var subNode = vNode.node var tagger = vNode.tagger while (subNode.nodeType === TAGGED_NODE) { typeof tagger !== "object" ? (tagger = [tagger, subNode.tagger]) : tagger.push(subNode.tagger) subNode = subNode.node } var subEventRoot = { tagger: tagger, parent: eventNode } var domNode = render(doc, subNode, subEventRoot) domNode.elm_event_node_ref = subEventRoot return domNode } if (nodeType === CUSTOM_NODE) { var domNode = vNode.render(doc, vNode.model) applyFacts(domNode, eventNode, vNode.settings) return domNode } if (nodeType === CUSTOM_ELEMENT) { const { elementConstructor, localName, options } = vNode const { customElements } = document.defaultView const registration = customElements.get(localName) if (registration) { if (registration !== elementConstructor.registration) { Object.setPrototypeOf( registration.prototype, elementConstructor.prototype ) elementConstructor.registration = registration registration.proto = elementConstructor.prototype } } else { const registration = class extends vNode.elementConstructor { constructor(...args) { super(...args) } connectedCallback(...args) { if (registration.proto.connectedCallback) { super.connectedCallback(...args) } } disconnectedCallback(...args) { if (registration.proto.disconnectedCallback) { super.disconnectedCallback(...args) } } adoptedCallback(...args) { if (registration.proto.adoptedCallback) { super.adoptedCallback(...args) } } attributeChangedCallback(...args) { if (registration.proto.attributeChangedCallback) { super.attributeChangedCallback(...args) } } } registration.proto = vNode.elementConstructor.prototype elementConstructor.registration = registration customElements.define(localName, registration, options) } } // at this point `nodeType` must be NODE or KEYED_NODE var domNode = vNode.namespace ? doc.createElementNS(vNode.namespace, vNode.localName) : doc.createElement(vNode.localName) const { onnavigate } = doc.defaultView if (onnavigate && vNode.localName == "a") { domNode.addEventListener("click", onnavigate) } applyFacts(domNode, eventNode, vNode.settings) for (var kids = vNode.children, i = 0; i < kids.length; i++) { appendChild( domNode, render(doc, nodeType === NODE ? kids[i] : kids[i][1], eventNode) ) } return domNode } // APPLY FACTS function applyFacts(domNode, eventNode, facts) { for (var key in facts) { var value = facts[key] switch (key) { case SETTING_STYLE: { applyStyles(domNode, value) break } case SETTING_EVENT: { applyEvents(domNode, eventNode, value) break } case SETTING_ATTRIBUTE: { applyAttrs(domNode, value) break } case SETTING_ATTRIBUTE_NS: { applyAttrsNS(domNode, value) break } default: { switch (key) { case "value": { if (domNode.value !== value) { domNode.value = value } break } case "checked": break case domNode[key]: break default: { domNode[key] = value break } } } } } } // APPLY STYLES function applyStyles(domNode, styles) { var domNodeStyle = domNode.style for (var key in styles) { domNodeStyle[key] = styles[key] } } // APPLY ATTRS function applyAttrs(domNode, attrs) { for (var key in attrs) { var value = attrs[key] value != null ? domNode.setAttribute(key, value) : domNode.removeAttribute(key) } } // APPLY NAMESPACED ATTRS function applyAttrsNS(domNode, nsAttrs) { for (var key in nsAttrs) { var pair = nsAttrs[key] var namespace = pair.namespace var value = pair.value value ? domNode.setAttributeNS(namespace, key, value) : domNode.removeAttributeNS(namespace, key) } } // APPLY EVENTS class EventRouter { constructor() { this.handlers = {} this.handleEvent = this.handleEvent.bind(this) this.target = null } handleEvent(event) { this.handlers[event.type].forwardEvent(event, this.target) } } function applyEvents(domNode, eventNode, events) { const router = domNode.eventRouter || (domNode.eventRouter = new EventRouter()) router.target = eventNode const { handlers, handleEvent } = router for (var key in events) { const oldHandler = handlers[key] const newHandler = events[key] if (!newHandler) { domNode.removeEventListener(key, handleEvent) handlers[key] = null continue } if (oldHandler) { if (oldHandler.type === newHandler.type) { handlers[key] = newHandler continue } else { domNode.removeEventListener(key, handleEvent) } } const { eventPhase } = newHandler handlers[key] = newHandler const passive = eventPhase == Normal || eventPhase == MayStopPropagation domNode.addEventListener(key, handleEvent, passiveSupported && { passive }) } } // PASSIVE EVENTS var passiveSupported try { window.addEventListener( "t", null, Object.defineProperty({}, "passive", { get: function() { passiveSupported = true } }) ) } catch (e) {} // EVENT HANDLERS const Normal = 0 const MayStopPropagation = 1 const MayPreventDefault = 2 const Custom = 3 // DIFF // TODO: Should we do patches like in iOS? // // type Patch // = At Int Patch // | Batch (List Patch) // | Change ... // // How could it not be better? // export function diff(x, y) { var patches = [] diffHelp(x, y, patches, 0) return patches } function pushPatch(patches, op, index, data) { var patch = { op: op, index: index, changes: data, domNode: undefined, eventNode: undefined } patches.push(patch) return patch } function diffHelp(x, y, patches, index) { if (x === y) { return } var xType = x.nodeType var yType = y.nodeType // Bail if you run into different types of nodes. Implies that the // structure has changed significantly and it's not worth a diff. if (xType !== yType) { if (xType === NODE && yType === KEYED_NODE) { y = dekey(y) yType = NODE } else { pushPatch(patches, OP_REDRAW, index, y) return } } // Now we know that both nodes are the same type. switch (yType) { case THUNK_NODE: { var xRefs = x.refs var yRefs = y.refs var i = xRefs.length var same = i === yRefs.length while (same && i--) { same = xRefs[i] === yRefs[i] } if (same) { y.node = x.node return } y.node = y.force() var subPatches = [] diffHelp(x.node, y.node, subPatches, 0) subPatches.length > 0 && pushPatch(patches, OP_THUNK, index, subPatches) return } case TAGGED_NODE: { // gather nested taggers var xTaggers = x.tagger var yTaggers = y.tagger var nesting = false var xSubNode = x.node while (xSubNode.nodeType === TAGGED_NODE) { nesting = true typeof xTaggers !== "object" ? (xTaggers = [xTaggers, xSubNode.tagger]) : xTaggers.push(xSubNode.tagger) xSubNode = xSubNode.node } var ySubNode = y.node while (ySubNode.nodeType === TAGGED_NODE) { nesting = true typeof yTaggers !== "object" ? (yTaggers = [yTaggers, ySubNode.tagger]) : yTaggers.push(ySubNode.tagger) ySubNode = ySubNode.node } // Just bail if different numbers of taggers. This implies the // structure of the virtual DOM has changed. if (nesting && xTaggers.length !== yTaggers.length) { pushPatch(patches, OP_REDRAW, index, y) return } // check if taggers are "the same" if ( nesting ? !pairwiseRefEqual(xTaggers, yTaggers) : xTaggers !== yTaggers ) { pushPatch(patches, OP_TAGGER, index, yTaggers) } // diff everything below the taggers diffHelp(xSubNode, ySubNode, patches, index + 1) return } case TEXT: { if (x.text !== y.text) { pushPatch(patches, OP_TEXT, index, y.text) } return } case NODE: { diffNodes(x, y, patches, index, diffKids) return } case KEYED_NODE: { diffNodes(x, y, patches, index, diffKeyedKids) return } case CUSTOM_ELEMENT: { if (x.elementConstructor !== y.elementConstructor) { pushPatch(patches, OP_REDRAW, index, y) return } else { diffNodes(x, y, patches, index, diffKids) return } } case CUSTOM_NODE: if (x.render !== y.render) { pushPatch(patches, OP_REDRAW, index, y) return } var factsDiff = diffFacts(x.settings, y.settings) factsDiff && pushPatch(patches, OP_FACTS, index, factsDiff) var patch = y.diff(x.model, y.model) patch && pushPatch(patches, OP_CUSTOM, index, patch) return } } // assumes the incoming arrays are the same length function pairwiseRefEqual(as, bs) { for (var i = 0; i < as.length; i++) { if (as[i] !== bs[i]) { return false } } return true } function diffNodes(x, y, patches, index, diffKids) { // Bail if obvious indicators have changed. Implies more serious // structural changes such that it's not worth it to diff. if (x.localName !== y.localName || x.namespace !== y.namespace) { pushPatch(patches, OP_REDRAW, index, y) return } var factsDiff = diffFacts(x.settings, y.settings) factsDiff && pushPatch(patches, OP_FACTS, index, factsDiff) diffKids(x, y, patches, index) } // DIFF FACTS // TODO Instead of creating a new diff object, it's possible to just test if // there *is* a diff. During the actual patch, do the diff again and make the // modifications directly. This way, there's no new allocations. Worth it? function diffFacts(x, y, category) { var diff // look for changes and removals for (var xKey in x) { if ( xKey === SETTING_STYLE || xKey === SETTING_EVENT || xKey === SETTING_ATTRIBUTE || xKey === SETTING_ATTRIBUTE_NS ) { var subDiff = diffFacts(x[xKey], y[xKey] || {}, xKey) if (subDiff) { diff = diff || {} diff[xKey] = subDiff } continue } // remove if not in the new facts if (!(xKey in y)) { diff = diff || {} diff[xKey] = !category ? typeof x[xKey] === "string" ? "" : null : category === SETTING_STYLE ? "" : category === SETTING_EVENT || category === SETTING_ATTRIBUTE ? undefined : { namespace: x[xKey].namespace, value: undefined } continue } var xValue = x[xKey] var yValue = y[xKey] // reference equal, so don't worry about it if ( (xValue === yValue && xKey !== "value" && xKey !== "checked") || (category === SETTING_EVENT && xValue.equal(yValue)) ) { continue } diff = diff || {} diff[xKey] = yValue } // add new stuff for (var yKey in y) { if (!(yKey in x)) { diff = diff || {} diff[yKey] = y[yKey] } } return diff } // DIFF KIDS function diffKids(xParent, yParent, patches, index) { var xKids = xParent.children var yKids = yParent.children var xLen = xKids.length var yLen = yKids.length // FIGURE OUT IF THERE ARE INSERTS OR REMOVALS if (xLen > yLen) { pushPatch(patches, OP_REMOVE_LAST, index, { offset: yLen, diff: xLen - yLen }) } else if (xLen < yLen) { pushPatch(patches, OP_APPEND, index, { offset: xLen, children: yKids }) } // PAIRWISE DIFF EVERYTHING ELSE for (var minLen = xLen < yLen ? xLen : yLen, i = 0; i < minLen; i++) { var xKid = xKids[i] diffHelp(xKid, yKids[i], patches, ++index) index += xKid.descendantsCount || 0 } } // KEYED DIFF function diffKeyedKids(xParent, yParent, patches, rootIndex) { var localPatches = [] var changes = {} // Dict String Entry var inserts = [] // Array { index : Int, entry : Entry } // type Entry = { tag : String, vnode : VNode, index : Int, data : _ } var xKids = xParent.children var yKids = yParent.children var xLen = xKids.length var yLen = yKids.length var xIndex = 0 var yIndex = 0 var index = rootIndex while (xIndex < xLen && yIndex < yLen) { var x = xKids[xIndex] var y = yKids[yIndex] var [xKey, xNode] = x var [yKey, yNode] = y // check if keys match if (xKey === yKey) { index++ diffHelp(xNode, yNode, localPatches, index) index += xNode.descendantsCount || 0 xIndex++ yIndex++ continue } // look ahead 1 to detect insertions and removals. var xNext = xKids[xIndex + 1] var yNext = yKids[yIndex + 1] if (xNext) { var xNextKey = xNext[0] var xNextNode = xNext[1] var oldMatch = yKey === xNextKey } if (yNext) { var yNextKey = yNext[0] var yNextNode = yNext[1] var newMatch = xKey === yNextKey } // swap x and y if (newMatch && oldMatch) { index++ diffHelp(xNode, yNextNode, localPatches, index) insertNode(changes, localPatches, xKey, yNode, yIndex, inserts) index += xNode.descendantsCount || 0 index++ removeNode(changes, localPatches, xKey, xNextNode, index) index += xNextNode.descendantsCount || 0 xIndex += 2 yIndex += 2 continue } // insert y if (newMatch) { index++ insertNode(changes, localPatches, yKey, yNode, yIndex, inserts) diffHelp(xNode, yNextNode, localPatches, index) index += xNode.descendantsCount || 0 xIndex += 1 yIndex += 2 continue } // remove x if (oldMatch) { index++ removeNode(changes, localPatches, xKey, xNode, index) index += xNode.descendantsCount || 0 index++ diffHelp(xNextNode, yNode, localPatches, index) index += xNextNode.descendantsCount || 0 xIndex += 2 yIndex += 1 continue } // remove x, insert y if (xNext && xNextKey === yNextKey) { index++ removeNode(changes, localPatches, xKey, xNode, index) insertNode(changes, localPatches, yKey, yNode, yIndex, inserts) index += xNode.descendantsCount || 0 index++ diffHelp(xNextNode, yNextNode, localPatches, index) index += xNextNode.descendantsCount || 0 xIndex += 2 yIndex += 2 continue } break } // eat up any remaining nodes with removeNode and insertNode while (xIndex < xLen) { index++ var x = xKids[xIndex] var xNode = x[1] removeNode(changes, localPatches, x[0], xNode, index) index += xNode.descendantsCount || 0 xIndex++ } while (yIndex < yLen) { var endInserts = endInserts || [] var y = yKids[yIndex] insertNode(changes, localPatches, y[0], y[1], undefined, endInserts) yIndex++ } if (localPatches.length > 0 || inserts.length > 0 || endInserts) { pushPatch(patches, OP_REORDER, rootIndex, { subPatches: localPatches, inserts: inserts, endInserts: endInserts }) } } // CHANGES FROM KEYED DIFF var POSTFIX = "_elmW6BL" function insertNode(changes, localPatches, key, vnode, yIndex, inserts) { var entry = changes[key] // never seen this key before if (!entry) { entry = { op: OP_INSERT, vnode: vnode, index: yIndex, changes: undefined } inserts.push({ index: yIndex, entry: entry }) changes[key] = entry return } // this key was removed earlier, a match! if (entry.op === OP_DELETE) { inserts.push({ index: yIndex, entry: entry }) entry.op = OP_MOVE var subPatches = [] diffHelp(entry.vnode, vnode, subPatches, entry.index) entry.index = yIndex entry.changes.changes = { subPatches: subPatches, entry: entry } return } // this key has already been inserted or moved, a duplicate! insertNode(changes, localPatches, key + POSTFIX, vnode, yIndex, inserts) } function removeNode(changes, localPatches, key, vnode, index) { var entry = changes[key] // never seen this key before if (!entry) { var patch = pushPatch(localPatches, OP_REMOVE, index, undefined) changes[key] = { op: OP_DELETE, vnode: vnode, index: index, changes: patch } return } // this key was inserted earlier, a match! if (entry.op === OP_INSERT) { entry.op = OP_MOVE var subPatches = [] diffHelp(vnode, entry.vnode, subPatches, index) pushPatch(localPatches, OP_REMOVE, index, { subPatches: subPatches, entry: entry }) return } // this key has already been removed or moved, a duplicate! removeNode(changes, localPatches, key + POSTFIX, vnode, index) } // ADD DOM NODES // // Each DOM node has an "index" assigned in order of traversal. It is important // to minimize our crawl over the actual DOM, so these indexes (along with the // descendantsCount of virtual nodes) let us skip touching entire subtrees of // the DOM if we know there are no patches there. function addDomNodes(domNode, vNode, patches, eventNode) { addDomNodesHelp( domNode, vNode, patches, 0, 0, vNode.descendantsCount, eventNode ) } // assumes `patches` is non-empty and indexes increase monotonically. function addDomNodesHelp(domNode, vNode, patches, i, low, high, eventNode) { var patch = patches[i] var index = patch.index while (index === low) { var patchType = patch.op if (patchType === OP_THUNK) { addDomNodes(domNode, vNode.node, patch.changes, eventNode) } else if (patchType === OP_REORDER) { patch.domNode = domNode patch.eventNode = eventNode var subPatches = patch.changes.subPatches if (subPatches.length > 0) { addDomNodesHelp(domNode, vNode, subPatches, 0, low, high, eventNode) } } else if (patchType === OP_REMOVE) { patch.domNode = domNode patch.eventNode = eventNode var data = patch.changes if (data) { data.entry.changes = domNode var subPatches = data.subPatches if (subPatches.length > 0) { addDomNodesHelp(domNode, vNode, subPatches, 0, low, high, eventNode) } } } else { patch.domNode = domNode patch.eventNode = eventNode } i++ if (!(patch = patches[i]) || (index = patch.index) > high) { return i } } var nodeType = vNode.nodeType if (nodeType === TAGGED_NODE) { var subNode = vNode.node while (subNode.nodeType === TAGGED_NODE) { subNode = subNode.node } return addDomNodesHelp( domNode, subNode, patches, i, low + 1, high, domNode.elm_event_node_ref ) } // tag must be NODE or KEYED_NODE at this point var vKids = vNode.children var childNodes = domNode.childNodes for (var j = 0; j < vKids.length; j++) { low++ var vKid = nodeType === NODE ? vKids[j] : vKids[j][1] var nextLow = low + (vKid.descendantsCount || 0) if (low <= index && index <= nextLow) { i = addDomNodesHelp( childNodes[j], vKid, patches, i, low, nextLow, eventNode ) if (!(patch = patches[i]) || (index = patch.index) > high) { return i } } low = nextLow } return i } // APPLY PATCHES export const patch = (rootDomNode, oldVirtualNode, patches, eventNode) => { if (patches.length === 0) { return rootDomNode } addDomNodes(rootDomNode, oldVirtualNode, patches, eventNode) return applyPatchesHelp(rootDomNode, patches) } function applyPatchesHelp(rootDomNode, patches) { for (var i = 0; i < patches.length; i++) { var patch = patches[i] var localDomNode = patch.domNode var newNode = applyPatch(localDomNode, patch) if (localDomNode === rootDomNode) { rootDomNode = newNode } } return rootDomNode } function applyPatch(domNode, patch) { const doc = domNode.ownerDocument switch (patch.op) { case OP_REDRAW: { return applyPatchRedraw(domNode, patch.changes, patch.eventNode) } case OP_FACTS: { applyFacts(domNode, patch.eventNode, patch.changes) return domNode } case OP_TEXT: { domNode.replaceData(0, domNode.length, patch.changes) return domNode } case OP_THUNK: { return applyPatchesHelp(domNode, patch.changes) } case OP_TAGGER: { if (domNode.elm_event_node_ref) { domNode.elm_event_node_ref.tagger = patch.changes } else { domNode.elm_event_node_ref = { tagger: patch.changes, parent: patch.eventNode } } return domNode } case OP_REMOVE_LAST: { var data = patch.changes for (var i = 0; i < data.diff; i++) { domNode.removeChild(domNode.childNodes[data.offset]) } return domNode } case OP_APPEND: { var data = patch.changes var kids = data.children var i = data.offset var theEnd = domNode.childNodes[i] for (; i < kids.length; i++) { domNode.insertBefore(render(doc, kids[i], patch.eventNode), theEnd) } return domNode } case OP_REMOVE: { var data = patch.changes if (!data) { domNode.parentNode.removeChild(domNode) return domNode } var entry = data.entry if (typeof entry.index !== "undefined") { domNode.parentNode.removeChild(domNode) } entry.changes = applyPatchesHelp(domNode, data.subPatches) return domNode } case OP_REORDER: { return applyPatchReorder(domNode, patch) } case OP_CUSTOM: { return patch.changes(domNode) } default: { throw TypeError("Unknown operation") } } } function applyPatchRedraw(domNode, vNode, eventNode) { const doc = domNode.ownerDocument var parentNode = domNode.parentNode var newNode = render(doc, vNode, eventNode) if (!newNode.elm_event_node_ref) { newNode.elm_event_node_ref = domNode.elm_event_node_ref } if (parentNode && newNode !== domNode) { parentNode.replaceChild(newNode, domNode) } return newNode } function applyPatchReorder(domNode, patch) { const doc = domNode.ownerDocument var data = patch.changes // remove end inserts var frag = applyPatchReorderEndInsertsHelp(doc, data.endInserts, patch) // removals domNode = applyPatchesHelp(domNode, data.subPatches) // inserts var inserts = data.inserts for (var i = 0; i < inserts.length; i++) { var insert = inserts[i] var entry = insert.entry var node = entry.op === OP_MOVE ? entry.changes : render(doc, entry.vnode, patch.eventNode) domNode.insertBefore(node, domNode.childNodes[insert.index]) } // add end inserts if (frag) { appendChild(domNode, frag) } return domNode } function applyPatchReorderEndInsertsHelp(doc, endInserts, patch) { if (!endInserts) { return } var frag = doc.createDocumentFragment() for (var i = 0; i < endInserts.length; i++) { var insert = endInserts[i] var entry = insert.entry appendChild( frag, entry.op === OP_MOVE ? entry.changes : render(doc, entry.vnode, patch.eventNode) ) } return frag } export function virtualize(root) { // TEXT NODES if (root.nodeType === 3) { return text(root.textContent) } // WEIRD NODES if (root.nodeType !== 1) { return text("") } // ELEMENT NODES var factList = [] var attrs = root.attributes for (var i = attrs.length; i--; ) { var attr = attrs[i] var name = attr.name var value = attr.value switch (name) { case "style": break default: factList.push(attribute(name, value)) } } const rules = root.style for (var i = rules.length; i--; ) { var name = rules[i] var value = rules[name] factList.push(style(name, value)) } var localName = root.localName var kidList = [] var kids = root.childNodes for (var i = kids.length; i--; ) { kidList.unshift(virtualize(kids[i])) } return node(localName, factList, kidList) } function dekey(keyedNode) { var keyedKids = keyedNode.children var len = keyedKids.length var kids = new Array(len) for (var i = 0; i < len; i++) { kids[i] = keyedKids[i][1] } return { nodeType: NODE, localName: keyedNode.localName, settings: keyedNode.settings, children: kids, namespace: keyedNode.namespace, descendantsCount: keyedNode.descendantsCount } } ================================================ FILE: src/Widget.js ================================================ import { diff, patch, virtualize } from "./VirtualDOM.js" /** * @typedef {import('./Task').ThreadID} ThreadID * @typedef {import('./Task').Thread} Thread */ /** * @template T * @typedef {import('./Task').Main} Main */ /** * @template T * @typedef {import('./Task').Sync} Sync */ /** * @template T * @implements {Main} */ export class MainThread { /** * @param {import('./Task').Sync} root */ constructor(root) { this.root = root /** * @private * @type {Record} */ this.threads /** * @private * @type {number} */ this.threadID } /** * Sends a message to this program * * @param {T} message */ async send(message) { await 0 this.root.sync(message) } /** * @param {T} message */ sync(message) { this.root.sync(message) } /** * @param {Thread} thread * @returns {ThreadID} */ link(thread) { if (this.threadID == null) { this.threadID = 0 this.threads = {} } const id = `@${++this.threadID}` this.threads[id] = thread // @ts-expect-error - string isn't ThreadID return id } /** * @param {Thread} thread */ unlink(thread) { const { threads } = this if (threads) { for (const id in threads) { if (thread === threads[id]) { delete threads[id] break } } } } /** * @param {ThreadID} threadID */ linked(threadID) { const { threads } = this if (threads) { return threads[threadID] } return undefined } } /** * @template Message, State * @typedef {import('./Task').Transaction} Transaction */ /** * @template T, State, View, Target */ export class Widget { constructor() { /** @type {MainThread} */ this.thread /** @type {State} */ this.state /** @type {(message:T, state:State) => Transaction} */ this.update /** @type {(state:State) => View} */ this.view /** @type {Target} */ this.root /** @type {View} */ this.node } get version() { const stack = new Error().stack || "" const start = stack.indexOf("+") const end = stack.indexOf("/", start) return stack.slice(start, end) } /** * @template T * @param {Sync} self * @returns {MainThread} */ static fork(self /*: Sync */) /*: MainThread */ { return new MainThread(self) } /** * * @param {T} message */ sync(message) { this.transact(this.update(message, this.state)) } /** * @param {Transaction} transaction */ transact([state, fx] /*: Transaction */) { this.state = state this.render(state) fx.perform(this.thread) } /** * @param {State} _state */ render(_state) {} /** * @param {Target} _root * @returns {View} */ mount(_root) { return this.node } toJSON() { return this.state } } /** * @template T * @typedef {import('./VirtualDOM').Node} Node */ /** * @template T, State * @extends {Widget, Element>} */ class ElementWidget extends Widget { /** * @param {Element} root * @returns {Node} */ mount(root) { return virtualize(root) } /** * @param {State} state */ render(state) { const newNode = this.view(state) const renderedNode = this.node const delta = diff(renderedNode, newNode) patch(this.root, renderedNode, delta, this.thread) this.node = newNode } } /** * @template Message, State, View, Options * @typedef {import('./Program').Program} Program */ /** * @template Message, State, Options * @param {Program, Options>} program * @param {Options} options * @param {Element} root * @returns {Widget, Element>} */ export const spawn = ({ init, update, view }, options, root) => { const self = new ElementWidget() self.thread = Widget.fork(self) self.update = update self.view = view self.root = root self.node = self.mount(root) self.transact(init(options)) return self } ================================================ FILE: src/lib.js ================================================ import * as DOM from "./VirtualDOM.js" import * as Element from "./Element.js" import * as Attribute from "./Attribute.js" import * as Widget from "./Widget.js" import * as Document from "./Document.js" import * as Application from "./Application.js" import * as Effect from "./Effect.js" import { identity, unreachable, always, nothing } from "./Basics.js" export { // DOM DOM, Element, Attribute, // Spawnining VirtualDOM Widget, Document, Application, // Effect System Effect, // Utilities identity, unreachable, always, nothing } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Projects */ "incremental": true, /* Enable incremental compilation */ "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./dist", /* Specify the folder for .tsbuildinfo incremental compilation files. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ /* Modules */ "module": "ES2020", /* Specify what module code is generated. */ // "rootDir": "./", /* Specify the root folder within your source files. */ "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "resolveJsonModule": true, /* Enable importing .json files */ // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ /* Emit */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ "declarationMap": true, /* Create sourcemaps for d.ts files. */ "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ "outDir": "./dist/", /* Specify an output folder for all emitted files. */ // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ // "newLine": "crlf", /* Set the newline character for emitting files. */ // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ /* Interop Constraints */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ /* Type Checking */ "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "include": [ "src", "test" ] }