From d802855f12e049c432ce7a91216715d7025e05e1 Mon Sep 17 00:00:00 2001 From: Brian M Hunt Date: Wed, 15 Apr 2026 09:33:50 -0400 Subject: [PATCH 01/10] Replace Make + lerna with Bun scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete Makefile, tools/build.mk, lerna.json, 28 per-package Makefiles, and dead electron/saucelabs workflows. Create tools/build.ts — shared esbuild build script that reads package.json for name/version/config. Each package gets a "build": "bun ../../tools/build.ts" script. Root package.json scripts replace all Make targets: bun run build — build all packages via bun --filter bun run test — vitest bun run lint — eslint bun run format — prettier bun run tsc — typecheck bun run clean — rm dist/ Eliminates make and lerna as dependencies. All commands are now `bun run - + ``` -## Start Here +## Agent Docs -- Verified Behaviors Index: /agents/verified-behaviors/index.md - Unit-test-backed behavior contract. Prefer this when behavior questions matter. -- Agent Contract: /agents/contract.md - Preferred state/binding/DOM split for TKO examples and prototypes. -- Agent Guide: /agents/guide.md - API reference, gotchas, and examples. -- Agent Testing: /agents/testing.md - How to run and verify TKO code. -- Agent TSX Scaffold: /agents/sample-tsx.html - Minimal in-browser TSX + esbuild scaffold for rapid prototype work. -- Glossary: /agents/glossary.md - Domain-specific terms, concepts, and package reference. -- Examples: /examples/ - Interactive self-contained HTML examples that show update locality and TKO's observable model. +- /agents/guide.md — API reference, gotchas, examples +- /agents/glossary.md — domain terms, concepts, packages +- /agents/testing.md — how to run and verify TKO code +- /agents/contract.md — state/binding/DOM architecture +- /agents/verified-behaviors/index.md — test-backed behavior contracts +- /agents/sample-tsx.html — minimal browser TSX scaffold +- /examples/ — interactive self-contained HTML examples -## Use This First +## Builds -- Behavior question or edge case: /agents/verified-behaviors/index.md -- State vs DOM architecture choice: /agents/contract.md -- API usage or authoring pattern: /agents/guide.md -- Verification or test flow: /agents/testing.md -- Rapid prototype in-browser: /agents/sample-tsx.html -- Conceptual interactive examples: /examples/ - The examples are self-contained HTML files. +- `@tko/build.reference` — modern TKO (TSX, ko-*, native provider) +- `@tko/build.knockout` — Knockout.js compatibility -## Build Choice +## Key Concepts -- Use `@tko/build.reference` for modern TKO, including TSX, `ko-*`, and the native provider path. -- Use `@tko/build.knockout` for compatibility-oriented Knockout-style work. +- Observables, observableArrays, computeds = state layer +- Bindings (`data-bind="text: msg"`) = DOM integration layer +- `ko.applyBindings(viewModel, element)` connects state to DOM +- TSX: `ko-text={msg}` with esbuild + `tko.jsx.render()` -## Common Gotchas +## Packages -- Derived `ko-*` values must stay observable or computed. `ko-text={price() > 50 ? 'expensive' : 'cheap'}` freezes; use a computed. -- `ko.applyBindings(...)` returns a `Promise`. -- TKO connects observable state to the DOM; `bindingHandlers` are the DOM/state bridge. -- Looking up a mount element with `document.getElementById(...)` (or similar) in order to call `ko.applyBindings(viewModel, element)` is normal and expected. What to avoid is driving reactive UI state through ad-hoc DOM mutation once bindings are active. +- Bindings: `@tko/binding.*` +- Observables: `@tko/observable`, `@tko/computed` +- Providers: `@tko/provider.*`, `@tko/utils.parser` +- TSX/JSX: `@tko/utils.jsx`, `@tko/provider.native` -## Package Routing +## Links -- Bindings and DOM behavior: `@tko/binding.*` -- Observables, computed, rate limiting: `@tko/observable`, `@tko/computed` -- Binding parsing and provider selection: `@tko/provider.*`, `@tko/utils.parser` -- TSX and JSX rendering: `@tko/utils.jsx`, `@tko/provider.native` - -## URLs - -- Verified Behaviors: /agents/verified-behaviors/index.md (package-scoped index) -- Agent Guide: /agents/guide.md (API reference, gotchas, examples) -- Agent Contract: /agents/contract.md (preferred state/binding/DOM split for examples and prototypes) -- Agent Testing: /agents/testing.md (how to run and verify TKO code) -- Agent TSX Scaffold: /agents/sample-tsx.html (minimal browser TSX + esbuild scaffold for rapid prototype work) -- Examples: /examples/ (interactive self-contained HTML examples showing update locality and reactive behavior) +- Docs: /observables/ · /computed/ · /bindings/ · /components/ - Playground: /playground - GitHub: https://github.com/knockout/tko - -## Core Contract - -- TKO is responsible for connecting state to the DOM. -- Observables, observableArrays, and computeds are the state layer. -- Bindings are the DOM integration layer: they read state, update DOM, and write user-driven changes back to state. -- `bindingHandlers` are the bridge between the DOM and the observable state layer. -- `ko.applyBindings(viewModel, element)` activates that bridge on an existing DOM subtree. -- It is acceptable to locate that mount subtree with `document.getElementById(...)`, `querySelector(...)`, or a framework-provided element reference before calling `ko.applyBindings(...)`. -- In TSX, `tko.jsx.render()` creates DOM nodes and `ko.applyBindings({}, root)` then activates the `ko-*` bindings on that rendered DOM. - -## Two Binding Syntaxes - -HTML: `data-bind="text: msg"` — runtime strings, works with `ko.applyBindings(vm, el)` -TSX: `ko-text={msg}` — compile-time JSX expressions, needs esbuild + `tko.jsx.render()` - -Inside `ko-foreach` children, binding-context vars use strings: `ko-text="$data"` (not `{$data}`) - -See /agents/guide.md for usage patterns. -See /agents/contract.md for preferred state vs DOM architecture. -Use /agents/verified-behaviors/index.md for test-backed behavior contracts. - -## Docs - -/observables/ · /computed/ · /bindings/ · /components/ · /binding-context/ · /advanced/ - -## Browser JSX (esbuild-wasm) - -```js -import * as esbuild from 'https://cdn.jsdelivr.net/npm/esbuild-wasm@0.27.4/esm/browser.min.js' -await esbuild.initialize({ wasmURL: 'https://cdn.jsdelivr.net/npm/esbuild-wasm@0.27.4/esbuild.wasm' }) -const result = await esbuild.transform(tsxCode, { - loader: 'tsx', - jsxFactory: 'tko.jsx.createElement', - jsxFragment: 'tko.jsx.Fragment' -}) -``` diff --git a/tools/build.ts b/tools/build.ts index e7629686..fcdef928 100644 --- a/tools/build.ts +++ b/tools/build.ts @@ -1,70 +1,50 @@ +#!/usr/bin/env bun /** * Shared build script for all TKO packages. - * - * Reads package.json from cwd() for name, version, and optional tko config. - * Runs esbuild to produce ESM, CJS, MJS, and/or browser bundles. - * + * Reads package.json for name, version, and optional tko config. * Usage: bun ../../tools/build.ts (from a package directory) */ -import { execSync } from 'node:child_process' -import { readFileSync, readdirSync, mkdirSync, existsSync } from 'node:fs' -import { join } from 'node:path' +import { $, Glob } from 'bun' -const pkg = JSON.parse(readFileSync('package.json', 'utf8')) -const name = pkg.name as string -const version = pkg.version as string +const pkg = await Bun.file('package.json').json() +const { name, version } = pkg const banner = `// ${name} 🥊 ${version}` -const tko = pkg.tko || {} -const buildMode = tko.buildMode || 'default' -const iifeGlobalName = tko.iifeGlobalName || 'tko' - -function run(cmd: string) { - console.log(`[build] ${name} → ${cmd.split('--outfile=')[1] || cmd.split('--outdir=')[1] || ''}`) - execSync(`bunx esbuild ${cmd}`, { stdio: 'inherit' }) -} - -function findSources(): string[] { - const sources: string[] = [] - function walk(dir: string) { - for (const entry of readdirSync(dir, { withFileTypes: true })) { - if (entry.isDirectory()) walk(join(dir, entry.name)) - else if (entry.name.endsWith('.ts')) sources.push(join(dir, entry.name)) - } - } - walk('src') - return sources -} - -if (!existsSync('dist')) mkdirSync('dist', { recursive: true }) +const { buildMode = 'default', iifeGlobalName = 'tko' } = pkg.tko ?? {} const common = `--log-level=warning --define:BUILD_VERSION='"${version}"' --sourcemap=external` -if (buildMode === 'default') { - // ESM — all source files to dist/ - const sources = findSources().join(' ') - run(`${sources} --platform=neutral --banner:js="${banner} ESM" ${common} --outdir=dist/`) +async function esbuild(args: string) { + console.log(`[build] ${name} → ${args.match(/--out(?:file|dir)=(\S+)/)?.[1] ?? ''}`) + const proc = Bun.spawn(['sh', '-c', `bunx esbuild ${args}`], { stdio: ['inherit', 'inherit', 'inherit'] }) + const code = await proc.exited + if (code !== 0) process.exit(code) +} - // MJS — single entry point - run(`src/index.ts --platform=neutral --banner:js="${banner} MJS" ${common} --outfile=dist/index.mjs`) +async function sources(): Promise { + const glob = new Glob('src/**/*.ts') + const files: string[] = [] + for await (const file of glob.scan('.')) files.push(file) + return files.join(' ') +} - // CJS — bundled, @tko/* external - run(`./index.ts --platform=neutral --target=es6 --format=cjs --bundle --banner:js="${banner} CommonJS" ${common} --outfile=dist/index.cjs --external:@tko/*`) -} else if (buildMode === 'browser') { - // ESM + CJS + MJS (same as default) - const sources = findSources().join(' ') - run(`${sources} --platform=neutral --banner:js="${banner} ESM" ${common} --outdir=dist/`) - run(`src/index.ts --platform=neutral --banner:js="${banner} MJS" ${common} --outfile=dist/index.mjs`) - run(`./index.ts --platform=neutral --target=es6 --format=cjs --bundle --banner:js="${banner} CommonJS" ${common} --outfile=dist/index.cjs --external:@tko/*`) +await $`mkdir -p dist`.quiet() - // Browser IIFE bundles - if (!existsSync('meta')) mkdirSync('meta', { recursive: true }) +if (buildMode === 'default' || buildMode === 'browser') { + const src = await sources() + await esbuild(`${src} --platform=neutral --banner:js="${banner} ESM" ${common} --outdir=dist/`) + await esbuild(`src/index.ts --platform=neutral --banner:js="${banner} MJS" ${common} --outfile=dist/index.mjs`) + await esbuild(`./index.ts --platform=neutral --target=es6 --format=cjs --bundle --banner:js="${banner} CommonJS" ${common} --outfile=dist/index.cjs --external:@tko/*`) +} +if (buildMode === 'browser') { + await $`mkdir -p meta`.quiet() const footer = `(typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : global).${iifeGlobalName} = ${iifeGlobalName}.default` - const iifeCommon = `--platform=browser --target=es6 --format=iife --global-name=${iifeGlobalName} --bundle --banner:js="${banner} IIFE" --footer:js="${footer}" ${common}` + const iife = `--platform=browser --target=es6 --format=iife --global-name=${iifeGlobalName} --bundle --banner:js="${banner} IIFE" --footer:js="${footer}" ${common}` + await esbuild(`./src/index.ts ${iife} --minify --outfile=dist/browser.min.js --metafile=meta/browser_min_meta.json`) + await esbuild(`./src/index.ts ${iife} --outfile=dist/browser.js --metafile=meta/browser_meta.json`) +} - run(`./src/index.ts ${iifeCommon} --minify --outfile=dist/browser.min.js --metafile=meta/browser_min_meta.json`) - run(`./src/index.ts ${iifeCommon} --outfile=dist/browser.js --metafile=meta/browser_meta.json`) -} else { +if (buildMode !== 'default' && buildMode !== 'browser') { console.error(`Unknown buildMode: ${buildMode}`) process.exit(1) } From c3b7a44e4f31d21c51ee2ab98868668d22eca894 Mon Sep 17 00:00:00 2001 From: Brian M Hunt Date: Wed, 15 Apr 2026 10:32:24 -0400 Subject: [PATCH 08/10] Use Bun.spawn, parallelize builds, restore llms.txt content - Use Bun.spawn for esbuild invocation (Bun.$ raw doesn't work) - Parallelize ESM/MJS/CJS builds with Promise.all - Parallelize IIFE minified/unminified builds - Restore gotchas and esbuild-wasm snippet to llms.txt Co-Authored-By: Claude Opus 4.6 (1M context) --- tko.io/public/llms.txt | 16 ++++++++++++++++ tools/build.ts | 17 ++++++++++------- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/tko.io/public/llms.txt b/tko.io/public/llms.txt index c14158cb..7184ec8b 100644 --- a/tko.io/public/llms.txt +++ b/tko.io/public/llms.txt @@ -39,6 +39,22 @@ - Providers: `@tko/provider.*`, `@tko/utils.parser` - TSX/JSX: `@tko/utils.jsx`, `@tko/provider.native` +## Gotchas + +- Derived `ko-*` values must stay observable/computed — inline expressions freeze +- `ko.applyBindings(...)` returns a Promise +- Inside `ko-foreach`, binding-context vars use strings: `ko-text="$data"` (not `{$data}`) + +## Browser JSX (esbuild-wasm) + +```js +import * as esbuild from 'https://cdn.jsdelivr.net/npm/esbuild-wasm@0.27.4/esm/browser.min.js' +await esbuild.initialize({ wasmURL: 'https://cdn.jsdelivr.net/npm/esbuild-wasm@0.27.4/esbuild.wasm' }) +const result = await esbuild.transform(tsxCode, { + loader: 'tsx', jsxFactory: 'tko.jsx.createElement', jsxFragment: 'tko.jsx.Fragment' +}) +``` + ## Links - Docs: /observables/ · /computed/ · /bindings/ · /components/ diff --git a/tools/build.ts b/tools/build.ts index fcdef928..e4b324aa 100644 --- a/tools/build.ts +++ b/tools/build.ts @@ -16,8 +16,7 @@ const common = `--log-level=warning --define:BUILD_VERSION='"${version}"' --sour async function esbuild(args: string) { console.log(`[build] ${name} → ${args.match(/--out(?:file|dir)=(\S+)/)?.[1] ?? ''}`) const proc = Bun.spawn(['sh', '-c', `bunx esbuild ${args}`], { stdio: ['inherit', 'inherit', 'inherit'] }) - const code = await proc.exited - if (code !== 0) process.exit(code) + if (await proc.exited) process.exit(1) } async function sources(): Promise { @@ -31,17 +30,21 @@ await $`mkdir -p dist`.quiet() if (buildMode === 'default' || buildMode === 'browser') { const src = await sources() - await esbuild(`${src} --platform=neutral --banner:js="${banner} ESM" ${common} --outdir=dist/`) - await esbuild(`src/index.ts --platform=neutral --banner:js="${banner} MJS" ${common} --outfile=dist/index.mjs`) - await esbuild(`./index.ts --platform=neutral --target=es6 --format=cjs --bundle --banner:js="${banner} CommonJS" ${common} --outfile=dist/index.cjs --external:@tko/*`) + await Promise.all([ + esbuild(`${src} --platform=neutral --banner:js="${banner} ESM" ${common} --outdir=dist/`), + esbuild(`src/index.ts --platform=neutral --banner:js="${banner} MJS" ${common} --outfile=dist/index.mjs`), + esbuild(`./index.ts --platform=neutral --target=es6 --format=cjs --bundle --banner:js="${banner} CommonJS" ${common} --outfile=dist/index.cjs --external:@tko/*`) + ]) } if (buildMode === 'browser') { await $`mkdir -p meta`.quiet() const footer = `(typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : global).${iifeGlobalName} = ${iifeGlobalName}.default` const iife = `--platform=browser --target=es6 --format=iife --global-name=${iifeGlobalName} --bundle --banner:js="${banner} IIFE" --footer:js="${footer}" ${common}` - await esbuild(`./src/index.ts ${iife} --minify --outfile=dist/browser.min.js --metafile=meta/browser_min_meta.json`) - await esbuild(`./src/index.ts ${iife} --outfile=dist/browser.js --metafile=meta/browser_meta.json`) + await Promise.all([ + esbuild(`./src/index.ts ${iife} --minify --outfile=dist/browser.min.js --metafile=meta/browser_min_meta.json`), + esbuild(`./src/index.ts ${iife} --outfile=dist/browser.js --metafile=meta/browser_meta.json`) + ]) } if (buildMode !== 'default' && buildMode !== 'browser') { From 4b46b4ea514e5ed4bc766317d1d642234df5a1df Mon Sep 17 00:00:00 2001 From: Brian M Hunt Date: Wed, 15 Apr 2026 10:40:45 -0400 Subject: [PATCH 09/10] Queue all esbuild invocations for full parallel execution All 5 builds (ESM, MJS, CJS + IIFE min/unmin) now run concurrently via a single Promise.all instead of two sequential groups. Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/build.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tools/build.ts b/tools/build.ts index e4b324aa..9d2801be 100644 --- a/tools/build.ts +++ b/tools/build.ts @@ -13,10 +13,10 @@ const { buildMode = 'default', iifeGlobalName = 'tko' } = pkg.tko ?? {} const common = `--log-level=warning --define:BUILD_VERSION='"${version}"' --sourcemap=external` -async function esbuild(args: string) { +function esbuild(args: string) { console.log(`[build] ${name} → ${args.match(/--out(?:file|dir)=(\S+)/)?.[1] ?? ''}`) const proc = Bun.spawn(['sh', '-c', `bunx esbuild ${args}`], { stdio: ['inherit', 'inherit', 'inherit'] }) - if (await proc.exited) process.exit(1) + return proc.exited.then(code => { if (code) process.exit(code) }) } async function sources(): Promise { @@ -28,26 +28,30 @@ async function sources(): Promise { await $`mkdir -p dist`.quiet() +const queued: Promise[] = [] + if (buildMode === 'default' || buildMode === 'browser') { const src = await sources() - await Promise.all([ + queued.push( esbuild(`${src} --platform=neutral --banner:js="${banner} ESM" ${common} --outdir=dist/`), esbuild(`src/index.ts --platform=neutral --banner:js="${banner} MJS" ${common} --outfile=dist/index.mjs`), esbuild(`./index.ts --platform=neutral --target=es6 --format=cjs --bundle --banner:js="${banner} CommonJS" ${common} --outfile=dist/index.cjs --external:@tko/*`) - ]) + ) } if (buildMode === 'browser') { await $`mkdir -p meta`.quiet() const footer = `(typeof globalThis !== 'undefined' ? globalThis : typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : global).${iifeGlobalName} = ${iifeGlobalName}.default` const iife = `--platform=browser --target=es6 --format=iife --global-name=${iifeGlobalName} --bundle --banner:js="${banner} IIFE" --footer:js="${footer}" ${common}` - await Promise.all([ + queued.push( esbuild(`./src/index.ts ${iife} --minify --outfile=dist/browser.min.js --metafile=meta/browser_min_meta.json`), esbuild(`./src/index.ts ${iife} --outfile=dist/browser.js --metafile=meta/browser_meta.json`) - ]) + ) } if (buildMode !== 'default' && buildMode !== 'browser') { console.error(`Unknown buildMode: ${buildMode}`) process.exit(1) } + +await Promise.all(queued) From c4760bfb7037bb5f6cd53869ba67775a1d33e6fb Mon Sep 17 00:00:00 2001 From: Brian M Hunt Date: Wed, 15 Apr 2026 12:53:52 -0400 Subject: [PATCH 10/10] Add language identifier to AGENTS.md code block Fixes markdownlint MD040 warning. Co-Authored-By: Claude Opus 4.6 (1M context) --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 2ff861fb..5d4eab70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ License: MIT Monorepo with Bun workspaces. -``` +```text packages/ # 26 modular @tko/* packages (all TypeScript) builds/ # 2 bundled distributions (knockout, reference) tools/ # Shared build script (build.ts)