@kudzujs/core 0.8.16 → 0.8.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -195,6 +195,13 @@ This queue orders the next investigations by general migration value. Start only
195
195
  - Component specialization, effect analysis, and keyed-list ownership remain in the main transformer until their AST-identity side tables can be replaced by explicit analysis results rather than moved behind large context objects.
196
196
  - The continuation packet under `docs/next-architecture` fixes the Goal A patch sequence, output/performance gates, and explicit decision boundaries for deferred optimization, state/resource research, routing, and React compatibility.
197
197
 
198
+ ### Completed In 0.8.17
199
+
200
+ - Supported command handlers cross the first JSON-safe ModuleIR slice: analysis emits plain command data, ModuleIR owns numeric signal/handler slots and lexical/source metadata, and focused codegen restores the existing `__kBehavior()` build ABI.
201
+ - Primitive SET, numeric ADD/subtract, functional setters, state logging, unary-plus, and negative-zero semantics remain specialized; non-finite values safely retain generic handler lowering.
202
+ - Same-named state in separate owners no longer aliases in IR, shared setter environments reuse signal identity, and synthetic specialized callbacks receive no fabricated source position.
203
+ - For unchanged source input, Counter HTML, route plans, command runtime, and generated artifacts remain byte-identical to 0.8.16; no browser capability or source syntax changed.
204
+
198
205
  ## Cross-Cutting Performance Gates
199
206
 
200
207
  Every migration feature must preserve:
package/PERFORMANCE.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Performance Records
2
2
 
3
+ ## 0.8.17 Command ModuleIR
4
+
5
+ Measured UTC 2026-08-08 on Apple M3, 8 logical CPUs, 8 GiB RAM, macOS 26.5.2 / Darwin 25.5.0, Node 25.6.1, and npm 11.18.0. Baseline `0.8.16` tag `98a4ad9` and the `0.8.17` release candidate used detached worktrees on the same temporary volume with identical installed dependencies.
6
+
7
+ Both targets received two warm-ups followed by 21 clean `worker-effects` production builds in round-robin alternating order. Cleanup remained outside timing. The distributions overlap; the 1.76% candidate median difference remains below the 5% architecture gate and does not establish a material regression.
8
+
9
+ | Target | Build median | Worker raw / gzip | Window raw / gzip |
10
+ |---|---:|---:|---:|
11
+ | 0.8.16 baseline | 782.0 ms | 907 B / 475 B | 12,148 B / 5,427 B |
12
+ | 0.8.17 candidate | 795.8 ms | 907 B / 475 B | 12,148 B / 5,427 B |
13
+
14
+ ```text
15
+ 0.8.16: [775.9,787.9,846.8,702.3,782.0,712.5,665.1,702.9,835.0,743.2,780.6,931.7,894.1,1004.0,849.8,1031.1,829.7,768.8,718.5,842.9,763.0]
16
+ 0.8.17: [696.0,721.9,778.8,850.4,846.1,835.2,710.4,711.6,836.1,766.9,822.6,945.0,838.2,991.5,858.6,803.4,706.3,760.8,773.8,764.3,795.8]
17
+ ```
18
+
19
+ Before release-content updates, the unchanged source site, Counter build module, route plan, and command runtime were byte-identical. The final Worker and window graphs remain byte-identical. This measurement covers compiler clean-build startup and artifact size, not browser interaction latency or cross-framework performance.
20
+
3
21
  ## 0.8.16 Compiler Analysis Boundaries
4
22
 
5
23
  Measured UTC 2026-08-08 on Apple M3, 8 logical CPUs, 8 GiB RAM, macOS 26.5.2 / Darwin 25.5.0, Node 25.6.1, and npm 11.18.0. Baseline `0.8.15` commit `8405d29` and the `0.8.16` release candidate used detached worktrees on the same temporary volume with identical installed dependencies.
package/README.md CHANGED
@@ -14,7 +14,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
14
14
 
15
15
  > Experimental `0.8.x`: the compiler API and supported TSX surface may change.
16
16
 
17
- **Latest release: 0.8.16 - Compiler analysis boundaries.** Per-source descriptors, pure collection analysis, command specialization, Zustand migration, and route capability planning now have focused compiler ownership while preserving generated output and runtime behavior. Read the [release notes](./RELEASES.md#0816---compiler-analysis-boundaries), open the [release page](https://kudzujs.cloud/releases/0.8.16), or follow the [next architecture packet](./docs/next-architecture/README.md).
17
+ **Latest release: 0.8.17 - Command ModuleIR.** Supported command handlers now cross a real JSON-safe ModuleIR boundary before existing source codegen, preserving byte-identical HTML, route plans, and browser assets. Read the [release notes](./RELEASES.md#0817---command-moduleir), open the [release page](https://kudzujs.cloud/releases/0.8.17), or follow the [next architecture packet](./docs/next-architecture/README.md).
18
18
 
19
19
  - [Documentation](https://kudzujs.cloud/docs)
20
20
  - [Installation guide](https://kudzujs.cloud/docs#install)
package/RELEASES.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.17 - Command ModuleIR
4
+
5
+ Kudzu 0.8.17 lands the first real Goal A vertical slice: supported command handlers are recognized as plain JSON-safe data, registered in a sparse per-source ModuleIR, and lowered back through focused source codegen without changing deploy output.
6
+
7
+ ### Changed in 0.8.17
8
+
9
+ - Direct setter arithmetic, functional setter arithmetic, primitive setter values, and state logging now produce plain Command IR instead of generated TypeScript array nodes during analysis.
10
+ - ModuleIR assigns deterministic numeric signal and handler slots while retaining readable state names, lexical state-owner keys, and original source ranges when they exist.
11
+ - Command codegen consumes only ModuleIR signal and handler data to reconstruct the existing `__kBehavior()` call; it performs no source analysis.
12
+ - Signed literals preserve unary-plus and negative-zero source semantics through JSON-safe syntax metadata. Non-finite numeric forms stay on the existing generic handler path.
13
+ - Same-named states in independent component owners receive distinct IR slots, while nested handlers sharing one setter environment reuse the same signal identity.
14
+ - Synthetic specialized handlers omit invented source positions; imported/original handlers retain normalized repository-relative source provenance.
15
+
16
+ ### Goal A boundary
17
+
18
+ - The first active ModuleIR slice contains lexical signals and command handlers only. `core.mjs` remains authoritative for initial values, final route/layout state IDs, complete HTML, and the serializable route plan.
19
+ - State/props/ref/component specialization is the planned `0.8.18` seam. Bindings, derived values, keyed blocks, and effects remain on their existing paths until their own explicit results replace AST side tables.
20
+ - No VDOM, hydration, component rerender, runtime component tree, public API, or accepted syntax was added.
21
+
22
+ ### Validation
23
+
24
+ - The complete suite passes 166/166 tests, including JSON round-trip, numeric edge, lexical owner, command codegen, specialized-component, and existing generic-handler contracts.
25
+ - Before release-content updates, the unchanged source site `dist`, Counter build module, route plan, and command runtime were byte-identical to `v0.8.16`; the final release adds only its expected version, homepage, documentation, and release-page content.
26
+ - The tracked Worker and window graphs remain byte-identical. A same-volume 21-run comparison found no build regression; raw arrays and environment are recorded in `PERFORMANCE.md`.
27
+ - `create-kudzu` remains 0.1.101 because its unchanged template already accepts `@kudzujs/core@^0.8.15`.
28
+
29
+ ### Upgrade
30
+
31
+ ```bash
32
+ npm install @kudzujs/core@^0.8.17
33
+ ```
34
+
3
35
  ## 0.8.16 - Compiler analysis boundaries
4
36
 
5
37
  Kudzu 0.8.16 continues the behavior-preserving compiler decomposition: source-local descriptors, the pure collection language, command fast paths, reduced Zustand migration, and route capability projection now have explicit ownership outside build orchestration.
@@ -6,7 +6,7 @@ The top-level [`GOAL_A.md`](../../GOAL_A.md) and [`GOAL_B.md`](../../GOAL_B.md)
6
6
 
7
7
  | Goal | Decision | Start condition |
8
8
  |---|---|---|
9
- | A: compiler foundation | Approved for execution | Continue with `0.8.17` after the `0.8.16` analysis-boundary release |
9
+ | A: compiler foundation | `0.8.17` command slice complete | Continue with `0.8.18` state/props/component analysis after the release boundary. |
10
10
  | B: optimization benchmarks | Deferred | Goal A complete and its output baseline recorded |
11
11
  | C: state/resource model | Research only | Reduced fixtures expose a limitation |
12
12
  | D: routing compatibility | Current behavior preserved | Revisit only with migration evidence and invariant review |
@@ -1,6 +1,6 @@
1
1
  # Current Compiler Architecture
2
2
 
3
- This maps the `0.8.16` compiler-analysis boundary. File and function names are the stable references; line numbers are intentionally omitted because Goal A moves code.
3
+ This maps the completed `0.8.17` command ModuleIR boundary. File and function names are the stable references; line numbers are intentionally omitted because Goal A moves code.
4
4
 
5
5
  ## Responsibility Map
6
6
 
@@ -15,6 +15,7 @@ This maps the `0.8.16` compiler-analysis boundary. File and function names are t
15
15
  | Pure collection language | [`framework/compiler/collection-analysis.mjs`](../../framework/compiler/collection-analysis.mjs) | Analyzes collection roots/selectors and serializes the allowed pure expression language used by lists and derived dependencies. |
16
16
  | Main semantic analysis | `framework/build.mjs`, `createKudzuTransformer()` | Still owns component specialization, hooks/state, effects, keyed-list ownership, imports, reactive JSX, and many AST-identity side tables. This is the largest remaining coupling. |
17
17
  | Per-source descriptor registration | [`framework/compiler/descriptor-session.mjs`](../../framework/compiler/descriptor-session.mjs), `createSemanticArtifact()`, `createDescriptorSession()` | Registers deterministic native handler, effect handler, binding, list evaluator, and client-import descriptors into one source-local artifact. |
18
+ | Command IR and codegen | [`framework/compiler/optimize/command-specialization.mjs`](../../framework/compiler/optimize/command-specialization.mjs), [`framework/compiler/ir/module-ir.mjs`](../../framework/compiler/ir/module-ir.mjs), [`framework/compiler/codegen/command-codegen.mjs`](../../framework/compiler/codegen/command-codegen.mjs) | Supported command handlers specialize to JSON-safe ModuleIR, then emit the existing `__kBehavior` AST without changing route plans. |
18
19
  | Build module generation | `framework/build.mjs`, `compile()` | Runs TypeScript with the Kudzu transformer, writes build-executable modules to `.kudzu`, rejects surviving React/Router runtime references, and generates handler source when descriptors exist. |
19
20
  | Handler/evaluator codegen | [`framework/compiler/handler-codegen.mjs`](../../framework/compiler/handler-codegen.mjs) | Converts descriptor AST into browser ESM exports and rewrites state, setter, reducer, capture, and imported-helper reads. It does not discover features. |
20
21
  | Worker graph | [`framework/compiler/worker-compiler.mjs`](../../framework/compiler/worker-compiler.mjs) | Validates the exact effect-owned Worker form, validates its relative graph, emits content-hashed ESM, and resolves placeholders only for rendered effects. |
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Status
4
4
 
5
- Approved for execution. Goal A is an architecture-only continuation: preserve accepted React-shaped syntax, diagnostics, complete HTML, emitted capability behavior, and current public APIs while making analysis and generation boundaries explicit.
5
+ The `0.8.17` command vertical slice is complete. Goal A continues with the planned `0.8.18` state/props/component analysis result while preserving accepted React-shaped syntax, diagnostics, complete HTML, emitted capability behavior, and current public APIs.
6
6
 
7
7
  ## Target Boundaries
8
8
 
@@ -68,7 +68,7 @@ Analysis produces plain data:
68
68
 
69
69
  ```js
70
70
  {
71
- signals: [{ slot: 0, debugName: "count", initial: 0, scope: "local" }],
71
+ signals: [{ slot: 0, key: "state:0:count", debugName: "count" }],
72
72
  handlers: [{
73
73
  slot: 0,
74
74
  kind: "commands",
@@ -77,6 +77,8 @@ Analysis produces plain data:
77
77
  }
78
78
  ```
79
79
 
80
+ The `0.8.17` slice intentionally records only lexical signal identity and command handlers. Initial values, state scope, bindings, derived values, effects, and keyed blocks join ModuleIR in their later planned patches; `core.mjs` remains authoritative for those values today.
81
+
80
82
  Source codegen lowers that data through the existing build ABI:
81
83
 
82
84
  ```js
@@ -1,6 +1,6 @@
1
1
  # Planned Version Sequence
2
2
 
3
- This is an execution sequence, not release history. `0.8.16` is the completed analysis-boundary scope represented by the current package/release records; later rows remain planned until their package version and release record exist.
3
+ This is an execution sequence, not release history. `0.8.16` and `0.8.17` are completed scopes represented by package/release records; later rows remain planned until their package version and release record exist.
4
4
 
5
5
  Keep each patch behavior-preserving and independently reviewable. If a boundary proves inseparable, revise this plan before combining releases; do not silently broaden a patch.
6
6
 
@@ -35,12 +35,12 @@ Reduced Zustand migration stores lower to one ordinary layout-lifetime state slo
35
35
  - `compiler/animation-frame-pass.mjs`: effect-owned animation-frame ref validation and plain mutable effect-scope lowering.
36
36
  - `compiler/browser-signal-passes.mjs`: static media-query external-store and navigator capability-condition normalization.
37
37
  - `compiler/collection-analysis.mjs`: pure collection expression IR, selector pipeline, alias, and imported-transform analysis.
38
- - `compiler/descriptor-session.mjs`: per-source semantic artifact creation and deterministic handler, effect, binding, and list evaluator descriptor registration.
38
+ - `compiler/descriptor-session.mjs`: per-source semantic artifact creation and deterministic command, handler, effect, binding, and list evaluator descriptor registration.
39
39
  - `compiler/normalization-pipeline.mjs`: ordered source normalization with parent-pointer repair after every compiler pass.
40
40
  - `compiler/react-migration-pass.mjs`: React import, memo, callback, `forwardRef`, and `useId` migration validation and lowering.
41
41
  - `compiler/custom-hook-timer-pass.mjs`: private custom-hook timeout-ref validation and compiler-owned timer-state lowering.
42
42
  - `compiler/effect-codegen.mjs`: route-specific ordinary, owned, and navigable effect entry generation.
43
- - `compiler/event-command-pass.mjs`: direct setter and state-log fast-path recognition into compact command descriptors.
43
+ - `compiler/optimize/command-specialization.mjs`, `compiler/ir/module-ir.mjs`, and `compiler/codegen/command-codegen.mjs`: JSON-safe command analysis, sparse per-source ModuleIR, and source-analysis-free existing behavior-call generation.
44
44
  - `compiler/handler-codegen.mjs`: route-specific native handler, reactive binding, keyed-list evaluator, and client-import module source generation.
45
45
  - `compiler/render-control-pass.mjs`: render-function early-return and exhaustive adjacent-assignment normalization.
46
46
  - `compiler/router-pass.mjs`: React Router import validation and native Link, pathname parameter, search parameter, and imperative navigation lowering.
@@ -11,8 +11,8 @@ import { analyzeCollectionPipeline, collectionExpression, collectionParameters,
11
11
  import { normalizeCustomHookTimerRefs } from "./compiler/custom-hook-timer-pass.mjs"
12
12
  import { captureNames, createDescriptorSession, createSemanticArtifact, nativeCaptureNames, referencedReducerDispatches, referencedStateNames } from "./compiler/descriptor-session.mjs"
13
13
  import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
14
- import { createEventCommandCompiler } from "./compiler/event-command-pass.mjs"
15
14
  import { createHandlerCodegen } from "./compiler/handler-codegen.mjs"
15
+ import { createCommandSpecializer } from "./compiler/optimize/command-specialization.mjs"
16
16
  import { applyNormalizationPasses } from "./compiler/normalization-pipeline.mjs"
17
17
  import { createReactMigrationPass, reactMemoExpression } from "./compiler/react-migration-pass.mjs"
18
18
  import { normalizeRenderControlFlow } from "./compiler/render-control-pass.mjs"
@@ -31,7 +31,7 @@ const pagesDirectory = join(sourceDirectory, "pages")
31
31
  const workDirectory = join(root, ".kudzu")
32
32
  const outputDirectory = join(root, "dist")
33
33
  const staticAssetExtensions = new Set([".avif", ".gif", ".ico", ".jpeg", ".jpg", ".otf", ".png", ".svg", ".ttf", ".webp", ".woff", ".woff2"])
34
- const compileEventCommand = createEventCommandCompiler({ isPrimitiveLiteral: isPrimitiveDefaultLiteral, synthesizeSerializableStateLiteral })
34
+ const compileEventCommand = createCommandSpecializer({ isPrimitiveLiteral: isPrimitiveDefaultLiteral })
35
35
  const { analyzeZustandStores, normalizeZustandMigrationSyntax } = createZustandPass({ isSerializableStateLiteral, nativeCaptureNames, sourceDirectory })
36
36
 
37
37
  export async function build({ quiet = false, minify = true } = {}) {
@@ -677,7 +677,7 @@ function escapeAttribute(value) {
677
677
 
678
678
  async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences) {
679
679
  const source = sourceIndex.get(file)
680
- const semantic = createSemanticArtifact()
680
+ const semantic = createSemanticArtifact(relative(root, file).replaceAll(sep, "/"))
681
681
  const handlerPath = `handlers/${relative(sourceDirectory, file).replaceAll(sep, "/").replace(/\.(?:ts|tsx)$/, ".js")}`
682
682
  const result = ts.transpileModule(source, {
683
683
  fileName: file,
@@ -702,7 +702,7 @@ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAsse
702
702
  await mkdir(resolve(output, ".."), { recursive: true })
703
703
  await writeFile(output, result.outputText)
704
704
 
705
- const { nativeHandlers, effectHandlers, reactiveBindings, listExpressions, clientImports } = semantic
705
+ const { nativeHandlers, effectHandlers, reactiveBindings, listExpressions, clientImports } = semantic
706
706
  if (!nativeHandlers.length && !effectHandlers.length && !reactiveBindings.length && !listExpressions.length) return undefined
707
707
  const callbacks = [...nativeHandlers, ...effectHandlers]
708
708
  const moduleSource = printHandlerModule({ callbacks, reactiveBindings, listExpressions, handlerPath })
@@ -894,6 +894,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
894
894
  context,
895
895
  compileEventCommand,
896
896
  isPrimitiveLiteral: isPrimitiveDefaultLiteral,
897
+ sourceName: source => relative(root, source.fileName).replaceAll(sep, "/"),
897
898
  rejectWorkerConstructions: expression => workerCompiler.rejectConstructions(expression, expression.getSourceFile(), "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback")
898
899
  })
899
900
  const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
@@ -0,0 +1,20 @@
1
+ import ts from "typescript"
2
+
3
+ export function generateCommandBehavior(moduleIR, handler, factory = ts.factory) {
4
+ const commands = handler.commands.map(command => factory.createArrayLiteralExpression([
5
+ factory.createStringLiteral(command.operation),
6
+ factory.createIdentifier(moduleIR.signals[command.signal].debugName),
7
+ literal(factory, command.value, command.syntax)
8
+ ]))
9
+ return factory.createCallExpression(factory.createIdentifier("__kBehavior"), undefined, [factory.createArrayLiteralExpression(commands)])
10
+ }
11
+
12
+ function literal(factory, value, syntax) {
13
+ if (value === null) return factory.createNull()
14
+ if (typeof value === "string") return factory.createStringLiteral(value)
15
+ if (typeof value === "boolean") return value ? factory.createTrue() : factory.createFalse()
16
+ const number = factory.createNumericLiteral(Math.abs(value))
17
+ if (syntax === "positive") return factory.createPrefixUnaryExpression(ts.SyntaxKind.PlusToken, number)
18
+ if (syntax === "negative") return factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, number)
19
+ return value < 0 ? factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, number) : number
20
+ }
@@ -1,12 +1,16 @@
1
1
  import ts from "typescript"
2
2
  import { bindingNames, isFunctionLike, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, unwrapExpression } from "./ast-helpers.mjs"
3
+ import { generateCommandBehavior } from "./codegen/command-codegen.mjs"
4
+ import { createModuleIR, registerCommandHandler } from "./ir/module-ir.mjs"
3
5
 
4
- export function createSemanticArtifact() {
5
- return { nativeHandlers: [], effectHandlers: [], reactiveBindings: [], listExpressions: [], clientImports: new Set() }
6
+ export function createSemanticArtifact(file) {
7
+ return { moduleIR: createModuleIR(file), nativeHandlers: [], effectHandlers: [], reactiveBindings: [], listExpressions: [], clientImports: new Set() }
6
8
  }
7
9
 
8
- export function createDescriptorSession({ semantic, handlerUrl, factory, context, compileEventCommand, isPrimitiveLiteral, rejectWorkerConstructions }) {
9
- const { nativeHandlers, effectHandlers, reactiveBindings, listExpressions, clientImports } = semantic
10
+ export function createDescriptorSession({ semantic, handlerUrl, factory, context, compileEventCommand, isPrimitiveLiteral, rejectWorkerConstructions, sourceName = source => source.fileName }) {
11
+ const { moduleIR, nativeHandlers, effectHandlers, reactiveBindings, listExpressions, clientImports } = semantic
12
+ const stateScopes = new WeakMap()
13
+ let nextStateScope = 0
10
14
 
11
15
  function compileListExpression(read, expression, item, index, states = new Set()) {
12
16
  const exportName = `listExpression${listExpressions.length}`
@@ -130,9 +134,14 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
130
134
 
131
135
  function compileOptimizedEvent(expression, setters) {
132
136
  const statements = ts.isBlock(expression.body) ? expression.body.statements : [factory.createExpressionStatement(expression.body)]
133
- const commands = statements.map(statement => ts.isExpressionStatement(statement) ? compileEventCommand(statement.expression, setters, factory) : undefined)
137
+ const commands = statements.map(statement => ts.isExpressionStatement(statement) ? compileEventCommand(statement.expression, setters) : undefined)
134
138
  if (!commands.length || commands.some(command => !command)) return undefined
135
- return factory.createCallExpression(factory.createIdentifier("__kBehavior"), undefined, [factory.createArrayLiteralExpression(commands)])
139
+ const original = ts.getOriginalNode(expression)
140
+ const source = original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
141
+ if (!stateScopes.has(setters)) stateScopes.set(setters, `state:${nextStateScope++}`)
142
+ const scope = stateScopes.get(setters)
143
+ const handler = registerCommandHandler(moduleIR, commands, source, scope)
144
+ return generateCommandBehavior(moduleIR, handler, factory)
136
145
  }
137
146
 
138
147
  function registerClientImports(imports) {
@@ -0,0 +1,23 @@
1
+ export function createModuleIR(file) {
2
+ return { version: 1, file, signals: [], handlers: [] }
3
+ }
4
+
5
+ export function registerCommandHandler(moduleIR, commands, source, scope = "module") {
6
+ const slots = new Map(moduleIR.signals.map(signal => [signal.key, signal.slot]))
7
+ for (const { state } of commands) {
8
+ const key = `${scope}:${state}`
9
+ if (slots.has(key)) continue
10
+ const slot = moduleIR.signals.length
11
+ slots.set(key, slot)
12
+ moduleIR.signals.push({ slot, key, debugName: state })
13
+ }
14
+ const signal = state => slots.get(`${scope}:${state}`) ?? slots.get(state)
15
+ const handler = {
16
+ slot: moduleIR.handlers.length,
17
+ kind: "commands",
18
+ commands: commands.map(({ operation, state, value, syntax }) => ({ operation, signal: signal(state), value, ...(syntax ? { syntax } : {}) })),
19
+ ...(source ? { source } : {})
20
+ }
21
+ moduleIR.handlers.push(handler)
22
+ return handler
23
+ }
@@ -0,0 +1,46 @@
1
+ import ts from "typescript"
2
+
3
+ export function createCommandSpecializer({ isPrimitiveLiteral }) {
4
+ return function specializeCommand(expression, setters) {
5
+ if (ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression) && ts.isIdentifier(expression.expression.expression) && expression.expression.expression.text === "console" && expression.expression.name.text === "log" && expression.arguments.length === 2 && ts.isStringLiteral(expression.arguments[0]) && ts.isIdentifier(expression.arguments[1]) && [...setters.values()].includes(expression.arguments[1].text)) {
6
+ return { operation: "log", state: expression.arguments[1].text, value: expression.arguments[0].text }
7
+ }
8
+
9
+ if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression) || expression.arguments.length !== 1) return undefined
10
+ const state = setters.get(expression.expression.text)
11
+ if (!state) return undefined
12
+
13
+ const value = expression.arguments[0]
14
+ if (ts.isBinaryExpression(value) && ts.isIdentifier(value.left) && value.left.text === state && ts.isNumericLiteral(value.right)) {
15
+ if (value.operatorToken.kind !== ts.SyntaxKind.PlusToken && value.operatorToken.kind !== ts.SyntaxKind.MinusToken) return undefined
16
+ const operand = signedNumber(value.right, value.operatorToken.kind === ts.SyntaxKind.MinusToken ? "negative" : undefined)
17
+ return operand ? { operation: "add", state, ...operand } : undefined
18
+ }
19
+ if (ts.isArrowFunction(value) && value.parameters.length === 1 && ts.isIdentifier(value.parameters[0].name) && ts.isBinaryExpression(value.body) && ts.isIdentifier(value.body.left) && value.body.left.text === value.parameters[0].name.text && ts.isNumericLiteral(value.body.right)) {
20
+ if (value.body.operatorToken.kind !== ts.SyntaxKind.PlusToken && value.body.operatorToken.kind !== ts.SyntaxKind.MinusToken) return undefined
21
+ const operand = signedNumber(value.body.right, value.body.operatorToken.kind === ts.SyntaxKind.MinusToken ? "negative" : undefined)
22
+ return operand ? { operation: "add", state, ...operand } : undefined
23
+ }
24
+ if (isPrimitiveLiteral(value)) {
25
+ const literal = primitiveValue(value)
26
+ return literal ? { operation: "set", state, ...literal } : undefined
27
+ }
28
+ return undefined
29
+ }
30
+ }
31
+
32
+ function primitiveValue(node) {
33
+ if (ts.isStringLiteral(node)) return { value: node.text }
34
+ if (ts.isNumericLiteral(node)) return signedNumber(node)
35
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return { value: true }
36
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return { value: false }
37
+ if (node.kind === ts.SyntaxKind.NullKeyword) return { value: null }
38
+ return signedNumber(node.operand, node.operator === ts.SyntaxKind.MinusToken ? "negative" : "positive")
39
+ }
40
+
41
+ function signedNumber(node, syntax) {
42
+ const number = Number(node.text)
43
+ if (!Number.isFinite(number)) return undefined
44
+ if (syntax === "negative") return number === 0 ? { value: 0, syntax } : { value: -number }
45
+ return syntax ? { value: number, syntax } : { value: number }
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.16",
3
+ "version": "0.8.17",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,35 +0,0 @@
1
- import ts from "typescript"
2
-
3
- export function createEventCommandCompiler({ isPrimitiveLiteral, synthesizeSerializableStateLiteral }) {
4
- return function compileEventCommand(expression, setters, factory) {
5
- if (ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression) && ts.isIdentifier(expression.expression.expression) && expression.expression.expression.text === "console" && expression.expression.name.text === "log" && expression.arguments.length === 2 && ts.isStringLiteral(expression.arguments[0]) && ts.isIdentifier(expression.arguments[1]) && [...setters.values()].includes(expression.arguments[1].text)) {
6
- return command(factory, "log", expression.arguments[1], factory.createStringLiteral(expression.arguments[0].text))
7
- }
8
-
9
- if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression) || expression.arguments.length !== 1) return undefined
10
- const stateName = setters.get(expression.expression.text)
11
- if (!stateName) return undefined
12
-
13
- const state = factory.createIdentifier(stateName)
14
- const value = expression.arguments[0]
15
- if (ts.isBinaryExpression(value) && ts.isIdentifier(value.left) && value.left.text === stateName && ts.isNumericLiteral(value.right)) {
16
- if (value.operatorToken.kind !== ts.SyntaxKind.PlusToken && value.operatorToken.kind !== ts.SyntaxKind.MinusToken) return undefined
17
- return command(factory, "add", state, numericExpression(factory, Number(value.right.text), value.operatorToken.kind === ts.SyntaxKind.MinusToken))
18
- }
19
- if (ts.isArrowFunction(value) && value.parameters.length === 1 && ts.isIdentifier(value.parameters[0].name) && ts.isBinaryExpression(value.body) && ts.isIdentifier(value.body.left) && value.body.left.text === value.parameters[0].name.text && ts.isNumericLiteral(value.body.right)) {
20
- if (value.body.operatorToken.kind !== ts.SyntaxKind.PlusToken && value.body.operatorToken.kind !== ts.SyntaxKind.MinusToken) return undefined
21
- return command(factory, "add", state, numericExpression(factory, Number(value.body.right.text), value.body.operatorToken.kind === ts.SyntaxKind.MinusToken))
22
- }
23
- if (isPrimitiveLiteral(value)) return command(factory, "set", state, synthesizeSerializableStateLiteral(value, factory))
24
- return undefined
25
- }
26
- }
27
-
28
- function command(factory, operation, state, value) {
29
- return factory.createArrayLiteralExpression([factory.createStringLiteral(operation), state, value])
30
- }
31
-
32
- function numericExpression(factory, value, negative) {
33
- const literal = factory.createNumericLiteral(value)
34
- return negative ? factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, literal) : literal
35
- }