@decohere/flux 0.0.1-dev.97 → 0.0.1-dev.99

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.
package/README.md ADDED
@@ -0,0 +1,247 @@
1
+ # @decohere/flux
2
+
3
+ In-process TypeScript load for Node.js ESM apps — either one-shot (`flux run`) or hot-reload serve (`flux serve`).
4
+
5
+ Use Flux when you want to run source `.ts` directly. Flux transpiles with the **Forge** SWC compiler; that compiler uses path aliases Mesh builds dynamically from the workspace (`package.json` `name` → source `main`). Do not use Flux as a browser bundler, a worker-thread HMR, or a production process manager.
6
+
7
+ Point Flux at a tree of TypeScript packages (`package.json` at each project root). [Mesh](../mesh/README.md) is how that tree is deduced — you do not run Flux against Mesh. Layout and glob overrides: [Mesh — workspace layout](../mesh/README.md#workspace-layout).
8
+
9
+ This package is the Flux API. You use it through the `flux` CLI (`flux run` / `flux serve`), not by importing `@decohere/flux` from application code. The [Programmatic API](#programmatic-api) is only for embedding Flux in another tool.
10
+
11
+ Related: [Mesh](../mesh/README.md) (package discovery), [Forge](../forge/README.md) (SWC + debug aliases). The `flux` command ships in `@decohere/cli`.
12
+
13
+ ## When to use
14
+
15
+ | Use Flux | Use something else |
16
+ |---|---|
17
+ | Local debug of Node apps with HMR (`serve`) | Production (`node` on Forge release output, Docker, orchestrator) |
18
+ | One-shot TS CLI / script without a pre-build (`run`) | Published package `bin` entries (stay built JS) |
19
+ | Packages resolved by `package.json` name | Browser UI (Vite / `ng serve` / … via package scripts) |
20
+ | Clean `Symbol.asyncDispose` on reboot (`serve`) | Worker threads / `createRequire` |
21
+
22
+ ## `run` vs `serve`
23
+
24
+ | | `flux run <entry.ts>` | `flux serve` |
25
+ |---|---|---|
26
+ | Compiles `.ts` on load | ✅ | ✅ |
27
+ | File watch / HMR reboot | ❌ | ✅ |
28
+ | Entry | Any path you pass | Package `main` (`main.ts` / `main.tsx`) |
29
+ | Process | Exits when the entry finishes | Long-lived until Ctrl+C (SIGINT) or SIGTERM |
30
+
31
+ ## Install
32
+
33
+ The `flux` command is on `@decohere/cli` (this package is the API). Add the CLI with your package manager:
34
+
35
+ ```bash
36
+ pnpm add -D @decohere/cli
37
+ # npm install --save-dev @decohere/cli
38
+ ```
39
+
40
+ That installs the `flux`, `forge`, and `decohere` bins. Then:
41
+
42
+ ```bash
43
+ pnpm exec flux --help
44
+ flux run projects/greeter/src/cli.ts
45
+ flux serve
46
+ ```
47
+
48
+ Scaffolded apps already depend on `@decohere/cli` — use those scripts (`pnpm dev`) or `pnpm exec flux`.
49
+
50
+ ## CLI
51
+
52
+ Host options (`--config-file`, `--log-level`) are parsed before the subcommand. `--config-file` (default `.decohere.js` if present) is optional Mesh config for package discovery. Omit it to use [Mesh defaults](../mesh/README.md#workspace-layout).
53
+
54
+ ### `flux run` — one-shot script
55
+
56
+ Any TypeScript file. The process exits when that module finishes evaluating.
57
+
58
+ ```ts
59
+ // projects/greeter/src/cli.ts
60
+ const name = process.argv[2] ?? "world";
61
+ console.log(`hello ${name}`);
62
+ ```
63
+
64
+ ```bash
65
+ flux run projects/greeter/src/cli.ts
66
+ # hello world
67
+
68
+ flux run projects/greeter/src/cli.ts -- Alice
69
+ # hello Alice
70
+ ```
71
+
72
+ | Flag / arg | Default | Meaning |
73
+ |---|---|---|
74
+ | `<entry>` | (required) | Path to a TypeScript entry file |
75
+ | `[args…]` | — | Forwarded to the entry after argv rewrite (`process.argv.slice(2)`) |
76
+ | `--configuration <name>` | `debug` | Build configuration name |
77
+
78
+ Before import, `run` sets `process.argv` to `[node, absoluteEntry, …args]` so the file behaves like a real Node bin. Use `--` when the entry’s own flags would otherwise be parsed by Flux (`flux run ./src/cli.ts -- --verbose`).
79
+
80
+ ### `flux serve` — long-lived process (HMR)
81
+
82
+ Use `serve` when the program should stay up until you kill it (HTTP server, worker). Flux watches source, disposes the previous entry, and re-imports on change.
83
+
84
+ `serve` boots the file named in that package’s `package.json` `main`. It must be `main.ts` / `main.tsx` (see [Classification](#classification-convention)). Snippet from `projects/my-api/package.json`:
85
+
86
+ ```json
87
+ {
88
+ "name": "my-api",
89
+ "main": "main.ts"
90
+ }
91
+ ```
92
+
93
+ ```ts
94
+ // projects/my-api/main.ts
95
+ import { createServer } from "node:http";
96
+
97
+ const server = createServer((_req, res) => {
98
+ res.end("ok\n");
99
+ }).listen(3000);
100
+
101
+ console.log("listening on 3000");
102
+
103
+ export default {
104
+ async [Symbol.asyncDispose]() {
105
+ await new Promise<void>((resolve, reject) => {
106
+ server.close((err) => (err != null ? reject(err) : resolve()));
107
+ });
108
+ },
109
+ };
110
+ ```
111
+
112
+ These are three ways to start the same serve (flags combine; `flux serve --project my-api --reboot-debounce-ms 2000` is valid):
113
+
114
+ ```bash
115
+ flux serve
116
+ flux serve --project my-api
117
+ flux serve --reboot-debounce-ms 2000
118
+ ```
119
+
120
+ | | What it does |
121
+ |---|---|
122
+ | `flux serve` | Boots the first discovered project (Mesh lists packages by `package.json` `name`, A→Z). |
123
+ | `--project my-api` | Boots that package by name. |
124
+ | `--reboot-debounce-ms 2000` | Same serve, but waits 2s after the last save before rebooting (default 250ms). |
125
+
126
+ Use `--project` or [directory slices](#directory-slice-flux-serve-token) when the first package is not the app you want.
127
+
128
+ Ctrl+C (SIGINT) or SIGTERM (Docker, CI, process managers) disposes the entry, then the CLI exits with code 0.
129
+
130
+ | Flag / arg | Default | Meaning |
131
+ |---|---|---|
132
+ | `[slices…]` | — | One or more module tokens among discovered packages (union; see **Directory slice**) |
133
+ | `--configuration <name>` | `debug` | Build configuration name (not a file path) |
134
+ | `--project <name>` | first discovered package | Single package name to serve (not with slices) |
135
+ | `--reboot-debounce-ms <ms>` | `250` | Quiet window after the last affecting save before a reboot. Use ~2000 when an agent writes many files in sequence. |
136
+
137
+ On a watched change that reaches a served entry, Flux invalidates that subgraph, calls `asyncDispose` on the previous default export, and re-imports. Rapid saves are trailing-debounced and serialized (overlapping dispose cannot run). A change that never reaches an entry does nothing. Each reboot logs `flux reboot` / `flux ready` at info (project names, triggering files, elapsed ms) so the event stays visible when the app boot is chatty. File-watch detail stays at trace.
138
+
139
+ ### Dispose on reboot
140
+
141
+ `serve` loads the entry’s **default export**. If that object implements `Symbol.asyncDispose`, Flux calls it:
142
+
143
+ - before each HMR reboot (so the next boot can bind the same port / reopen the DB)
144
+ - on SIGINT / SIGTERM / CLI teardown
145
+
146
+ The `export default { async [Symbol.asyncDispose]() { … } }` in the server example above is that contract. Close servers, pools, and timers there — not in a `process.on("SIGINT")` handler Flux does not own.
147
+
148
+ **Cortex APIs** scaffold `export default app`. `App.dispose` disposes the Crucible root container, then Scribe. Constructed services that implement `Symbol.asyncDispose` (or `Symbol.dispose`) are torn down automatically — including the HTTP provider. Put teardown on the service; do not wrap the default export to close sockets or pools.
149
+
150
+ Flux clears its reference to the object *before* calling dispose, so a failed boot cannot dispose the same instance twice.
151
+
152
+ ## Directory slice (`flux serve <token>…`)
153
+
154
+ A slice is a path token. Flux keeps Mesh packages whose directory sits under that token, then [classifies](#classification-convention) each one. `flux serve shop` and `flux serve projects/shop` are the same token.
155
+
156
+ Example workspace (every leaf directory here has its own `package.json`):
157
+
158
+ ```
159
+ projects/
160
+ app.web/ # scripts.dev: vite → UI
161
+ shop/
162
+ api/ # main: main.ts → Node app
163
+ worker/ # main: main.ts → Node app
164
+ web/ # scripts.dev: vite → UI
165
+ domain/ # main: src/index.ts → library
166
+ shop-admin.api/ # main: main.ts → other module
167
+ billing/
168
+ api/ # main: main.ts → Node app
169
+ domain/ # main: src/index.ts → library
170
+ ```
171
+
172
+ `flux serve shop` matches `projects/shop` and children (and `projects/shop.*`). It does **not** match `shop-admin.api`.
173
+
174
+ | | In-process (HMR) | Spawn `scripts.dev` / `serve` | Skip |
175
+ |---|---|---|---|
176
+ | `flux serve shop` | `shop/api`, `shop/worker` | `shop/web` | `shop/domain` |
177
+ | `flux serve app` | — | `app.web` | — |
178
+ | `flux serve shop-admin` | `shop-admin.api` | — | — |
179
+ | `flux serve app shop billing` | `shop/api`, `shop/worker`, `billing/api` | `app.web`, `shop/web` | `shop/domain`, `billing/domain` |
180
+
181
+ Multiple tokens are a union. Use slices **or** `--project`, not both.
182
+
183
+ Matching is path-safe:
184
+
185
+ - Nested: `shop` → `projects/shop`, `projects/shop/api`
186
+ - Flat dotted packages: `shop` → `projects/shop.web`, `projects/shop.domain`; `app` → `projects/app.web`
187
+ - Does **not** match siblings that only share a string prefix (`shop` ≠ `projects/shop-admin.api` — pass `shop-admin` explicitly)
188
+
189
+ Named serve profiles in `.decohere.js` (e.g. `shop-stack: ["app", "shop", "billing"]`) are deferred.
190
+
191
+ ### Classification (convention)
192
+
193
+ | Kind | How Flux decides | Behavior |
194
+ |---|---|---|
195
+ | **Foreign / UI** | `scripts.dev` or `scripts.serve` that does **not** invoke `flux` | Spawn `pnpm`/`npm`/`yarn` `run <script>` in that package cwd. Flux does not HMR-boot it. |
196
+ | **Flux app** | `package.json` `main` ends in `main.ts` / `main.tsx`, and no foreign serve script | Boot in-process (one HMR graph per app). |
197
+ | **Library** | Everything else (e.g. `main`: `src/index.ts`) | Skip — imported via debug aliases. |
198
+
199
+ Foreign scripts win over `main.ts` so Vite (and similar) apps are never loaded in the Node HMR hook. The **package script** is the contract, not a hard-coded Vite check.
200
+
201
+ Logs name skipped libraries and deferred packages (script + cwd). Child processes are stopped when the CLI exits (SIGINT or SIGTERM → dispose).
202
+
203
+ ## Limits
204
+
205
+ Flux does not support `createRequire`, worker threads, or browser HMR. Reloads always dispose and re-import the entry — there is no `import.meta.hot.accept()` boundary yet.
206
+
207
+ **Restart `flux serve` after changing any of these** — they're read once at start, and saving them doesn't reboot:
208
+
209
+ - `tsconfig.json`, `.swcrc`, or `.decohere.js`
210
+ - a `package.json` (name, `main`, dependencies)
211
+ - adding or removing a package under `projects/`
212
+
213
+ Only source files (`.ts`) that a served entry imports trigger a reboot.
214
+
215
+ **Memory grows over a long session.** Each reboot imports changed modules under a new `?version=` URL, and Node's ES module cache never releases the old versions. That's fine for a working day; if a session runs for days with thousands of reboots, restart it.
216
+
217
+ Flux uses **Forge’s compiler**. Forge debug builds path aliases from Mesh (`name` → source `main`) and rewrites `import "@scope/pkg"` to that `.ts` file. That is a debug rule so you can edit a library and hit HMR without a pre-build or `node_modules` workspace symlink. Release still resolves through real `node_modules` (published `exports` / `main`).
218
+
219
+ Why aliases, and a tree of where a symlink misses `src/index.ts`: [Forge — debug path aliases](../forge/README.md#debug-path-aliases-not-node_modules-links).
220
+
221
+ ## Programmatic API
222
+
223
+ Use this only when you need to **embed** Flux (tests, a custom host). Day-to-day, invoke `flux run` / `flux serve` — do not import `@decohere/flux` from application code.
224
+
225
+ ```ts
226
+ import { flux } from "@decohere/flux";
227
+
228
+ // One-shot compile-on-load (no watch) — same idea as `flux run`
229
+ await flux.run("/abs/path/to/entry.ts");
230
+ await flux.run("./tool.ts", { configuration: "debug", args: ["--flag"] });
231
+
232
+ // Long-lived HMR — same idea as `flux serve` (in-process apps)
233
+ const server = await flux.serve({
234
+ configuration: "debug",
235
+ project: "my-api", // optional single package
236
+ // projects: ["api", "svc"], // optional multi-entry
237
+ rebootDebounceMs: 2000, // optional; default 250
238
+ });
239
+ server.on("reboot", () => {}); // after dispose + boot attempt for affected entries
240
+ await server.dispose();
241
+ ```
242
+
243
+ `configuration` is the Mesh/Forge configuration **name** (`debug`, `release`, …), matching `--configuration`. Defaults to `debug`. Pass `workspace` in tests to skip `WorkspaceFactory`.
244
+
245
+ `flux.run` keeps Node ESM hooks registered for the process so the imported graph can still load `.ts`. Do not dispose the loader after a successful run.
246
+
247
+ Slice helpers (`classifySliceProjects`, `normalizeSlicePrefix`, …) and the engine types (`SourceModuleLoader`, `HotReloadServer`) stay exported for tests and tooling; the CLI and `flux` facade are the primary surfaces.
@@ -8,7 +8,6 @@ function _ts_metadata(k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  }
10
10
  import { injectable } from "@decohere/crucible/decorators";
11
- // biome-ignore lint/style/useImportType: <DI>
12
11
  import { Workspace } from "@decohere/mesh";
13
12
  import { ActionResponse } from "@decohere/nexus/cli";
14
13
  import { scribe } from "@decohere/scribe";
@@ -9,13 +9,10 @@ function _ts_metadata(k, v) {
9
9
  }
10
10
  import path from "node:path";
11
11
  import { injectable } from "@decohere/crucible/decorators";
12
- // biome-ignore lint/style/useImportType: <DI>
13
12
  import { Workspace } from "@decohere/mesh";
14
13
  import { ActionResponse } from "@decohere/nexus/cli";
15
14
  import { scribe } from "@decohere/scribe";
16
- // biome-ignore lint/style/useImportType: <DI>
17
15
  import { DeferredScriptRunner } from "./deferred-script-runner.js";
18
- // biome-ignore lint/style/useImportType: <DI>
19
16
  import { HotReloadServer } from "./hot-reload-server.js";
20
17
  import { classifySliceProjects, resolveDeferScript } from "./slice-serve.js";
21
18
  export class ServeAction {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decohere/flux",
3
- "version": "0.0.1-dev.97",
3
+ "version": "0.0.1-dev.99",
4
4
  "description": "Hot module reload server for Node.js ESM applications",
5
5
  "keywords": [
6
6
  "typescript",
@@ -25,11 +25,11 @@
25
25
  "test": "echo \"Error: no test specified\" && exit 1"
26
26
  },
27
27
  "dependencies": {
28
- "@decohere/crucible": "0.0.1-dev.97",
29
- "@decohere/forge": "0.0.1-dev.97",
30
- "@decohere/mesh": "0.0.1-dev.97",
31
- "@decohere/nexus": "0.0.1-dev.97",
32
- "@decohere/scribe": "0.0.1-dev.97",
28
+ "@decohere/crucible": "0.0.1-dev.99",
29
+ "@decohere/forge": "0.0.1-dev.99",
30
+ "@decohere/mesh": "0.0.1-dev.99",
31
+ "@decohere/nexus": "0.0.1-dev.99",
32
+ "@decohere/scribe": "0.0.1-dev.99",
33
33
  "commander": "^13.1.0",
34
34
  "rxjs": "^7.8.2"
35
35
  },