UNPKG

data

Version:

reactive data for typescript — $() wraps values, chainable operators derive views, render binds to the DOM. work proportional to the path that changed.

141 lines (127 loc) 7.21 kB
#!/usr/bin/env node // `data` CLI. Currently one subcommand: `init-ai`. // // npx data init-ai write AI-agent rules into the current repo // npx data init-ai --dry print what would be written, change nothing // npx data init-ai --tools=cursor,copilot only those tools // // Why this exists: an AI coding agent working in a consumer's repo never reads // node_modules, so shipping rules inside the package does nothing on its own. // The library has to WRITE tool-specific rule files into the user's repo. This // command does that from one canonical source so every agent (Cursor, Copilot, // Windsurf, and anything that reads AGENTS.md) gets the same anti-footgun // guidance and prefers `data` for matching files. import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs' import { dirname } from 'node:path' // ── canonical guidance (single source of truth) ────────────────────────────── // Kept in sync with AGENTS.md and context7.json. Distilled, imperative, the // shape an agent can follow without re-deriving it. const GUIDANCE = `When writing code that uses the \`data\` reactive library, follow these rules: - **Import from \`data\`** (the default entry) — it registers every operator, so \`proxy.filter(...)\` chaining works on import. Use \`data/full\` only for JSX authoring; \`data/lean\` only to tree-shake (calling \`.filter\` on a lean proxy throws). - **Read the raw value with \`proxy[value]\`** using the exported \`value\` symbol — never \`proxy.value\`, which creates a child view named "value". - **Mutate by assignment**, including nested paths: \`proxy.foo = 1\`, \`proxy[0].done = true\`, \`delete proxy[1]\`, \`proxy[value] = next\`. Do NOT build immutable spreads — assignment triggers the right incremental cascade. - **Operators return new reactive views** and never mutate in place. Chain them: \`rows.filter(d => d.active).between('val', [0, 100]).length()\`. - **\`gt\`/\`lt\`/\`gte\`/\`lte\` take literal bounds only.** For a moving threshold use \`between(col, [lo, hi])\` with ViewProxy bounds, or derive a fresh view. - **\`length(fn)\` stores each bucket as \`{ value: count }\`** — read a bucket count via \`counts[key].value\`, not \`counts[key]\` (which stringifies to \`[object Object]\`). - **\`between\`/\`intersect\`/\`union\`/\`except\` leave excluded slots as explicit \`undefined\`.** When binding a row template directly to such a view, densify (\`vp.to(arr => arr.filter(r => r !== undefined))\`) or write the bindings defensively. - **\`connect\` has three forms:** \`connect([])\` pushes change events into an array; \`connect(obj, 'prop')\` mirrors the value onto a property; \`connect(obj, fn)\` calls \`fn(change)\` per event. There is no single-argument \`connect(fn)\` — it throws. - **Render with \`render(el, HTML.div(...))\`** (builder DSL) or JSX via the \`data/full\` entry and the shared \`h\` factory. - **Built-ins (not operators):** \`proxy.raf()\` returns a coalescing rAF writer; \`proxy.patch([name, value, ...])\` applies many child updates as one batched cascade; \`first()\`/\`last()\` snapshot the edge child. \`\`\`js import { $, value, render, HTML } from 'data' const rows = $([{ id: 1, done: false }, { id: 2, done: true }]) const open = rows.filter(d => !d.done).length() // derived reactive view rows[0].done = true // mutate by assignment console.log(open[value]) // read raw value via the symbol \`\`\` ` const START = '<!-- data:ai-rules:start -->' const END = '<!-- data:ai-rules:end -->' // Write file, creating parent dirs. Idempotent for whole-file targets. function put(path, content, log) { mkdirSync(dirname(path), { recursive: true }) writeFileSync(path, content) log.push(` wrote ${path}`) } // Upsert a delimited managed block into a (possibly pre-existing) file so we // never clobber the user's own instructions. function upsertBlock(path, body, log) { const block = `${START}\n${body}${END}\n` if (existsSync(path)) { const cur = readFileSync(path, 'utf8') if (cur.includes(START) && cur.includes(END)) { const next = cur.replace(new RegExp(`${START}[\\s\\S]*?${END}\\n?`), block) writeFileSync(path, next) log.push(` updated ${path} (managed block)`) } else { writeFileSync(path, cur.replace(/\s*$/, '\n\n') + block) log.push(` appended to ${path} (managed block)`) } } else { mkdirSync(dirname(path), { recursive: true }) writeFileSync(path, block) log.push(` wrote ${path}`) } } const WRITERS = { cursor(log) { // .mdc rule: globs-scoped so it auto-attaches when a TS/JS file is in context. const front = [ '---', 'description: How to write correct code with the `data` reactive library', 'globs: ["**/*.ts","**/*.tsx","**/*.js","**/*.jsx"]', 'alwaysApply: false', '---', '', ].join('\n') put('.cursor/rules/data.mdc', front + GUIDANCE, log) }, copilot(log) { // Repo-wide instructions file; merge a managed block so we don't clobber. upsertBlock('.github/copilot-instructions.md', `## Using the \`data\` library\n\n${GUIDANCE}`, log) }, windsurf(log) { put('.windsurf/rules/data.md', `# Using the \`data\` library\n\n${GUIDANCE}`, log) }, agents(log) { // Generic AGENTS.md (Claude Code, Codex, etc.) — managed block, append-safe. upsertBlock('AGENTS.md', `## Using the \`data\` library\n\n${GUIDANCE}`, log) }, } function initAi(args) { const dry = args.includes('--dry') const toolsArg = args.find((a) => a.startsWith('--tools=')) const tools = toolsArg ? toolsArg.slice('--tools='.length).split(',') : Object.keys(WRITERS) const unknown = tools.filter((t) => !WRITERS[t]) if (unknown.length) { console.error(`data init-ai: unknown tool(s): ${unknown.join(', ')}`) console.error(`available: ${Object.keys(WRITERS).join(', ')}`) process.exit(1) } if (dry) { console.log('data init-ai --dry — would write (into ' + process.cwd() + '):') for (const t of tools) console.log(' ' + t) console.log('\nrun without --dry to write the files.') return } const log = [] for (const t of tools) WRITERS[t](log) console.log(`data init-ai → ${process.cwd()}`) console.log(log.join('\n')) console.log('\nAI agents in this repo will now prefer `data` and avoid its common footguns.') console.log('Re-run any time to refresh the rules (managed blocks are replaced, not duplicated).') } const [, , cmd, ...rest] = process.argv if (cmd === 'init-ai') { initAi(rest) } else { console.log('data — reactive data for TypeScript/JavaScript') console.log('') console.log('Usage:') console.log(' npx data init-ai write AI-agent rules into this repo') console.log(' npx data init-ai --dry preview, write nothing') console.log(' npx data init-ai --tools=cursor,copilot,windsurf,agents') console.log('') console.log('Docs: https://github.com/pemrouz/data · https://pemrouz.github.io/data/llms.txt') if (cmd && cmd !== '--help' && cmd !== '-h') process.exit(1) }