@decohere/flux 0.0.1-dev.119 → 0.0.1-dev.12

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.
Files changed (47) hide show
  1. package/commonjs/src/flux.command.js +3 -3
  2. package/commonjs/src/flux.module.js +8 -9
  3. package/commonjs/src/hot-reload-server.js +219 -307
  4. package/commonjs/src/index.js +2 -34
  5. package/commonjs/src/serve.action.js +7 -78
  6. package/commonjs/src/serve.command.js +4 -35
  7. package/es6/src/flux.command.js +3 -3
  8. package/es6/src/flux.module.js +8 -9
  9. package/es6/src/hot-reload-server.js +175 -287
  10. package/es6/src/index.js +3 -5
  11. package/es6/src/serve.action.js +9 -73
  12. package/es6/src/serve.command.js +4 -35
  13. package/package.json +6 -8
  14. package/types/src/flux.command.d.ts +2 -2
  15. package/types/src/flux.module.d.ts +4 -6
  16. package/types/src/hot-reload-server.d.ts +23 -57
  17. package/types/src/index.d.ts +1 -8
  18. package/types/src/serve.action.d.ts +3 -13
  19. package/types/src/serve.command.d.ts +1 -7
  20. package/README.md +0 -273
  21. package/commonjs/src/deferred-script-runner.js +0 -56
  22. package/commonjs/src/flux.js +0 -56
  23. package/commonjs/src/rewrite-serve-specifier.js +0 -61
  24. package/commonjs/src/run.action.js +0 -47
  25. package/commonjs/src/run.command.js +0 -65
  26. package/commonjs/src/slice-serve.js +0 -130
  27. package/commonjs/src/source-module-loader.js +0 -378
  28. package/commonjs/src/static-imports.js +0 -37
  29. package/commonjs/src/transpile-disk-cache.js +0 -148
  30. package/es6/src/deferred-script-runner.js +0 -46
  31. package/es6/src/flux.js +0 -41
  32. package/es6/src/rewrite-serve-specifier.js +0 -51
  33. package/es6/src/run.action.js +0 -37
  34. package/es6/src/run.command.js +0 -55
  35. package/es6/src/slice-serve.js +0 -105
  36. package/es6/src/source-module-loader.js +0 -322
  37. package/es6/src/static-imports.js +0 -27
  38. package/es6/src/transpile-disk-cache.js +0 -142
  39. package/types/src/deferred-script-runner.d.ts +0 -18
  40. package/types/src/flux.d.ts +0 -43
  41. package/types/src/rewrite-serve-specifier.d.ts +0 -14
  42. package/types/src/run.action.d.ts +0 -15
  43. package/types/src/run.command.d.ts +0 -11
  44. package/types/src/slice-serve.d.ts +0 -52
  45. package/types/src/source-module-loader.d.ts +0 -96
  46. package/types/src/static-imports.d.ts +0 -5
  47. package/types/src/transpile-disk-cache.d.ts +0 -38
@@ -1,27 +0,0 @@
1
- /**
2
- * The module specifiers a compiled ES module names in `import … from`, `export … from`, bare
3
- * `import "x"` and literal `import("x")`.
4
- *
5
- * Only a hint for compiling ahead (#661): a specifier it misses is compiled when Node loads it,
6
- * and one it finds by mistake (an import written inside a string) costs one extra compile. So a
7
- * scan of SWC's output is enough; it never decides what runs.
8
- */ const STATIC = /\b(?:import|export)\s*(?:[\w$*{},\s]+?\s*from\s*)?["']([^"'\n]+)["']/g;
9
- const DYNAMIC = /\bimport\s*\(\s*["']([^"'\n]+)["']\s*\)/g;
10
- function scan(code) {
11
- const found = new Set();
12
- for (const pattern of [
13
- STATIC,
14
- DYNAMIC
15
- ]){
16
- for (const match of code.matchAll(pattern)){
17
- const specifier = match[1];
18
- if (specifier != null) found.add(specifier);
19
- }
20
- }
21
- return [
22
- ...found
23
- ];
24
- }
25
- export const staticImports = {
26
- scan
27
- };
@@ -1,142 +0,0 @@
1
- import { createHash, randomBytes } from "node:crypto";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { transpileCache } from "@decohere/forge";
5
- import { scribe } from "@decohere/scribe";
6
- /** Bump when what flux stores changes, so older entries stop matching. */ const FORMAT = 1;
7
- /** Entries untouched for this long are pruned. */ const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
8
- /** Pruning runs at most this often, recorded by the mtime of a marker file. */ const PRUNE_EVERY_MS = 24 * 60 * 60 * 1000;
9
- const PRUNE_MARKER = ".last-prune";
10
- /**
11
- * Compiled code on disk, so a cold start doesn't compile files that haven't changed (#514).
12
- * Content-addressed: the key hashes the file's path and source, the SWC options it compiles
13
- * with, SWC's version and this cache's format, so any change to those is a miss, never a stale
14
- * hit. Entries are written to a temp file and renamed into place, so a process reading an entry
15
- * another is writing sees all of it or none.
16
- *
17
- * The cache is an optimisation: if it can't read or write its directory, it says so once and
18
- * turns itself off for the process, and flux compiles as it would without it.
19
- */ export class TranspileDiskCache {
20
- dir;
21
- logger = scribe.logger(TranspileDiskCache.name);
22
- disabled = false;
23
- constructor(dir){
24
- this.dir = dir;
25
- }
26
- /** The cache for a workspace, or null when caching is off (`DECOHERE_CACHE=0`). */ static forWorkspace(rootDir) {
27
- const base = transpileCache.directory(rootDir);
28
- return base == null ? null : new TranspileDiskCache(path.join(base, "flux"));
29
- }
30
- get directory() {
31
- return this.dir;
32
- }
33
- /**
34
- * The key for a file compiled with `baseOptions`: the per-file options are those plus the
35
- * file's path (forge's `createFileConfig`), so hashing the base once per project and adding
36
- * the path covers them without serialising options for every file.
37
- */ keyFor(fullPath, source, baseOptions) {
38
- return createHash("sha256").update(`${FORMAT}\0${transpileCache.swcVersion}\0${this.optionsHash(baseOptions)}\0`).update(`${fullPath}\0`).update(source).digest("hex");
39
- }
40
- optionsHashes = new WeakMap();
41
- optionsHash(options) {
42
- const cached = this.optionsHashes.get(options);
43
- if (cached != null) return cached;
44
- const hash = createHash("sha256").update(JSON.stringify(options)).digest("hex");
45
- this.optionsHashes.set(options, hash);
46
- return hash;
47
- }
48
- readSync(key) {
49
- if (this.disabled) return undefined;
50
- try {
51
- return fs.readFileSync(this.entryPath(key), "utf8");
52
- } catch (error) {
53
- return this.missOrDisable(error);
54
- }
55
- }
56
- async read(key) {
57
- if (this.disabled) return undefined;
58
- try {
59
- return await fs.promises.readFile(this.entryPath(key), "utf8");
60
- } catch (error) {
61
- return this.missOrDisable(error);
62
- }
63
- }
64
- writeSync(key, code) {
65
- if (this.disabled) return;
66
- const file = this.entryPath(key);
67
- const temp = this.tempPath(file);
68
- try {
69
- fs.mkdirSync(path.dirname(file), {
70
- recursive: true
71
- });
72
- fs.writeFileSync(temp, code);
73
- fs.renameSync(temp, file);
74
- } catch (error) {
75
- this.disable("write", error);
76
- }
77
- }
78
- async write(key, code) {
79
- if (this.disabled) return;
80
- const file = this.entryPath(key);
81
- const temp = this.tempPath(file);
82
- try {
83
- await fs.promises.mkdir(path.dirname(file), {
84
- recursive: true
85
- });
86
- await fs.promises.writeFile(temp, code);
87
- await fs.promises.rename(temp, file);
88
- } catch (error) {
89
- this.disable("write", error);
90
- }
91
- }
92
- /** Deletes entries older than 30 days, if it hasn't done so in the last day. */ async pruneIfDue(now = Date.now()) {
93
- if (this.disabled) return;
94
- const marker = path.join(this.dir, PRUNE_MARKER);
95
- try {
96
- const last = await fs.promises.stat(marker).then((stat)=>stat.mtimeMs, (error)=>{
97
- if (error.code === "ENOENT") return 0;
98
- throw error;
99
- });
100
- if (now - last < PRUNE_EVERY_MS) return;
101
- await fs.promises.mkdir(this.dir, {
102
- recursive: true
103
- });
104
- await fs.promises.writeFile(marker, "");
105
- for (const shard of (await fs.promises.readdir(this.dir, {
106
- withFileTypes: true
107
- }))){
108
- if (!shard.isDirectory()) continue;
109
- const shardDir = path.join(this.dir, shard.name);
110
- for (const entry of (await fs.promises.readdir(shardDir))){
111
- // Old entries, and temp files a crashed process left mid-write.
112
- const file = path.join(shardDir, entry);
113
- const mtime = await fs.promises.stat(file).then((stat)=>stat.mtimeMs, (error)=>{
114
- // Renamed or removed by another process since the listing: nothing to prune.
115
- if (error.code === "ENOENT") return undefined;
116
- throw error;
117
- });
118
- if (mtime != null && now - mtime > MAX_AGE_MS) await fs.promises.rm(file, {
119
- force: true
120
- });
121
- }
122
- }
123
- } catch (error) {
124
- this.disable("prune", error);
125
- }
126
- }
127
- entryPath(key) {
128
- return path.join(this.dir, key.slice(0, 2), `${key}.js`);
129
- }
130
- tempPath(file) {
131
- return `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
132
- }
133
- /** A missing entry is a miss; any other read error turns the cache off. */ missOrDisable(error) {
134
- if (error.code !== "ENOENT") this.disable("read", error);
135
- return undefined;
136
- }
137
- disable(operation, error) {
138
- if (this.disabled) return;
139
- this.disabled = true;
140
- this.logger.warn(`Transpile cache turned off for this run: could not ${operation} ${this.dir} (${String(error)}). ` + "Flux compiles without it. Set DECOHERE_CACHE_DIR to a writable directory, or DECOHERE_CACHE=0 to silence this.");
141
- }
142
- }
@@ -1,18 +0,0 @@
1
- import { type ChildProcess } from "node:child_process";
2
- export type DeferredScriptStart = {
3
- readonly label: string;
4
- readonly cwd: string;
5
- readonly script: string;
6
- /** Workspace root used to detect pnpm/npm/yarn. */
7
- readonly workspaceRoot: string;
8
- };
9
- /**
10
- * Nixpacks-style child processes for foreign packages in a flux serve slice.
11
- * Killed when the Flux container disposes (SIGINT → RootModule teardown).
12
- */
13
- export declare class DeferredScriptRunner implements AsyncDisposable {
14
- private readonly logger;
15
- private readonly children;
16
- start(options: DeferredScriptStart): ChildProcess;
17
- [Symbol.asyncDispose](): Promise<void>;
18
- }
@@ -1,43 +0,0 @@
1
- import type { Workspace } from "@decohere/workspace";
2
- import { HotReloadServer } from "./hot-reload-server.js";
3
- export type FluxRunOptions = {
4
- /** Workspace configuration name. Defaults to `debug` (`--configuration`). */
5
- configuration?: string;
6
- /** Arguments forwarded to the entry (bin-style `process.argv.slice(2)`). */
7
- args?: string[];
8
- /**
9
- * Pre-built Workspace instead of `WorkspaceFactory`.
10
- * Useful in tests to skip filesystem discovery.
11
- */
12
- workspace?: Workspace;
13
- };
14
- export type FluxServeOptions = {
15
- /** Workspace configuration name. Defaults to `debug` (`--configuration`). */
16
- configuration?: string;
17
- /** Name of the project to serve. Defaults to the first project in the workspace. */
18
- project?: string;
19
- /**
20
- * Multiple Flux app package names to boot in one process (directory slice).
21
- * When set, takes precedence over {@link project}.
22
- */
23
- projects?: readonly string[];
24
- /**
25
- * Pre-built Workspace instead of `WorkspaceFactory`.
26
- * Useful in tests to skip filesystem discovery.
27
- */
28
- workspace?: Workspace;
29
- /**
30
- * Quiet window in milliseconds after the last affecting save before a reboot.
31
- * Raise this for agent save trains.
32
- */
33
- rebootDebounceMs?: number;
34
- };
35
- declare function run(entry: string, options?: FluxRunOptions): Promise<unknown>;
36
- declare function serve(options?: FluxServeOptions): Promise<HotReloadServer>;
37
- declare function dispose(): void;
38
- export declare const flux: {
39
- run: typeof run;
40
- serve: typeof serve;
41
- dispose: typeof dispose;
42
- };
43
- export {};
@@ -1,14 +0,0 @@
1
- export type RewriteServeSpecifierOptions = {
2
- specifier: string;
3
- parentUrl: string;
4
- isWatchedTs: (absolutePath: string) => boolean;
5
- resolveBare: (specifier: string) => string | null;
6
- versionFileUrl: (fileUrl: string) => string;
7
- };
8
- /**
9
- * Map a resolve-hook specifier to a versioned watched `.ts` file URL.
10
- * Covers relative `.js` / `.ts` / extensionless imports, directory imports (`./models` is
11
- * `./models/index.ts`), and bare workspace package names and subpaths. Flux compiles without
12
- * SWC's own path resolution, so this is the only place imports are mapped to sources (#513).
13
- */
14
- export declare function rewriteServeSpecifier(options: RewriteServeSpecifierOptions): string | null;
@@ -1,15 +0,0 @@
1
- import { Workspace } from "@decohere/workspace";
2
- import { type Action, ActionResponse } from "@decohere/devkit/cli";
3
- export type RunOptions = {
4
- /** Absolute or cwd-relative path to a `.ts` entry file. */
5
- entry: string;
6
- configuration: string;
7
- /** Arguments forwarded to the entry (bin-style `process.argv.slice(2)`). */
8
- args?: string[];
9
- };
10
- export declare class RunAction implements Action<RunOptions> {
11
- private readonly workspace;
12
- private readonly logger;
13
- constructor(workspace: Workspace);
14
- run(options: RunOptions): Promise<ActionResponse>;
15
- }
@@ -1,11 +0,0 @@
1
- import { DecohereCommand } from "@decohere/devkit/cli";
2
- import { RunAction } from "./run.action.js";
3
- export declare class RunCommand extends DecohereCommand {
4
- get actions(): (typeof RunAction)[];
5
- constructor(name: string);
6
- /**
7
- * Commander passes positionals before options when `<entry> [args...]` is declared.
8
- * The CLI host ModuleFactory expects `(opts, cmd)` — normalize and stash entry/args on opts.
9
- */
10
- action(fn: (...args: any[]) => void | Promise<void>): this;
11
- }
@@ -1,52 +0,0 @@
1
- /**
2
- * Directory-slice helpers for `flux serve <dir>…` — classify workspace packages as
3
- * Flux apps, Nixpacks-style script deferrals, or libraries to skip.
4
- */
5
- export type SliceProjectView = {
6
- readonly name: string;
7
- /** Relative project dir from workspace root (e.g. `projects/funding/api`). */
8
- readonly projectDirectory: string;
9
- readonly main: string;
10
- readonly scripts: Record<string, string>;
11
- };
12
- export type SliceKind = "flux" | "defer" | "skip";
13
- export type ClassifiedSlice = {
14
- readonly flux: SliceProjectView[];
15
- readonly defer: SliceProjectView[];
16
- readonly skip: SliceProjectView[];
17
- };
18
- /** Preferred package scripts for foreign toolchains (Nixpacks-style). */
19
- export declare const DEFER_SCRIPT_CANDIDATES: readonly ["dev", "serve"];
20
- /**
21
- * Normalize a CLI slice token to a `projects/…` prefix without a trailing slash.
22
- * `funding` and `projects/funding` both become `projects/funding`.
23
- */
24
- export declare function normalizeSlicePrefix(slice: string): string;
25
- /**
26
- * True when `projectDirectory` belongs to the slice token.
27
- *
28
- * Matches:
29
- * - nested: `projects/funding`, `projects/funding/api`
30
- * - flat dotted package dirs: `projects/funding.web` for token `funding`
31
- *
32
- * Does not match siblings that only share a string prefix (`funding` vs `funding-fee.api`).
33
- */
34
- export declare function isUnderSlice(projectDirectory: string, slicePrefix: string): boolean;
35
- export declare function projectsInSlice(projects: readonly SliceProjectView[], slice: string): SliceProjectView[];
36
- /** Union of packages under any of the slice tokens (deduped by package name). */
37
- export declare function projectsInSlices(projects: readonly SliceProjectView[], slices: readonly string[]): SliceProjectView[];
38
- /** Script invokes Flux (in-process serve stays on Flux, not deferred). */
39
- export declare function isFluxOwnedScript(script: string): boolean;
40
- /**
41
- * First Nixpacks-style serve script that is not Flux-owned.
42
- * Prefers `dev`, then `serve`.
43
- */
44
- export declare function resolveDeferScript(scripts: Record<string, string>): string | undefined;
45
- /** Convention: Flux Node apps use `main` ending in `main.ts` / `main.tsx`. */
46
- export declare function isFluxAppMain(main: string): boolean;
47
- /**
48
- * Classify one package. Foreign serve scripts win over `main.ts` (Vite apps
49
- * often have a main entry but must not be HMR-booted by Flux).
50
- */
51
- export declare function classifySliceProject(project: SliceProjectView): SliceKind;
52
- export declare function classifySliceProjects(projects: readonly SliceProjectView[], slices: string | readonly string[]): ClassifiedSlice;
@@ -1,96 +0,0 @@
1
- import type { ResolveFnOutput } from "node:module";
2
- import type { Project, Workspace } from "@decohere/workspace";
3
- export interface SourceModuleLoaderOptions {
4
- /** Build configuration name. */
5
- buildConfig: string;
6
- /**
7
- * Pre-built Workspace instance to use instead of running WorkspaceFactory.
8
- * Useful in tests to bypass filesystem-based workspace discovery.
9
- */
10
- workspace?: Workspace;
11
- /**
12
- * Map a watched `.ts` file URL for ESM resolution.
13
- * Hot reload passes a `?version=` bust; one-shot run uses identity.
14
- */
15
- versionFileUrl?: (fileUrl: string) => string;
16
- /** Optional post-resolve hook (e.g. dependency tracking for HMR). */
17
- onResolved?: (result: ResolveFnOutput, parentUrl: string) => void;
18
- }
19
- /**
20
- * Registers Node ESM resolve/load hooks and SWC-transpiles workspace `.ts`
21
- * on the fly. No file watching and no entry reboot — that belongs to
22
- * {@link HotReloadServer}.
23
- */
24
- export declare class SourceModuleLoader {
25
- private readonly logger;
26
- private readonly transpiler;
27
- private disposed;
28
- private hooksRegistered;
29
- private workspace?;
30
- private buildConfig;
31
- private fileInfoMap;
32
- private transpileCache;
33
- /** Bumped when a file's cached code is dropped, so an up-front compile already running can't put it back. */
34
- private transpileVersions;
35
- /** The source files each entry's walk reached, so a reboot only walks out from what changed. */
36
- private reachedFrom;
37
- /** Compiled code on disk across runs (#514); null when caching is off. */
38
- private diskCache;
39
- /** Up-front compiles in flight, shared by entries booting at the same time. */
40
- private compilingAhead;
41
- /** SWC options per project, built on first use and kept until the next `configure()`. */
42
- private swcConfigs;
43
- private commonJsConfigs;
44
- private aliasPaths;
45
- private aliasBaseUrl;
46
- private versionFileUrl;
47
- private onResolved?;
48
- getWorkspace(): Workspace | undefined;
49
- hasSourceFile(absolutePath: string): boolean;
50
- sourceFilePaths(): IterableIterator<string>;
51
- setSourceFile(absolutePath: string, project: Project): void;
52
- configure(options: SourceModuleLoaderOptions): Promise<void>;
53
- registerHooks(): void;
54
- importEntry(absolutePath: string): Promise<unknown>;
55
- invalidateTranspile(...absolutePaths: string[]): void;
56
- dispose(): void;
57
- private buildFileInfoMap;
58
- /**
59
- * Compiles the entry and the source files it imports, before Node imports it (#661). The load
60
- * hook is synchronous, so it would compile one file at a time on the main thread; async SWC
61
- * runs on the thread pool. Walks out from the entry, compiling each file's imports in parallel,
62
- * so files the app never imports (tests, other apps) are not compiled. A reboot walks out only
63
- * from the files it reached before that are no longer cached (the saved ones), so their new
64
- * imports are found without visiting the rest of the graph again.
65
- *
66
- * A file that fails here is left out of the cache instead of failing the boot: the load hook
67
- * then compiles it again and throws as before, naming the file. The walk is a hint; whatever
68
- * it misses is compiled by the load hook when Node gets to it.
69
- */
70
- private compileAhead;
71
- /** The file's compiled code, from the cache or compiled now; undefined if it isn't a source. */
72
- private compiledAhead;
73
- /** The source file a specifier names, mapped the way the resolve hook maps it. */
74
- private sourcePathOf;
75
- private loadTs;
76
- /**
77
- * Compiles a file for the load hook, through the disk cache (#514). Throws, naming the file,
78
- * when it can't be read or doesn't compile.
79
- */
80
- private compileSync;
81
- /**
82
- * Compiles a file ahead, through the disk cache; undefined when it doesn't compile, which the
83
- * load hook then reports (see {@link compileAhead}). The source and the cache entry are read
84
- * synchronously: for small files that is far cheaper than a trip through libuv's thread pool,
85
- * which async SWC needs for the compiles that miss (#514).
86
- */
87
- private compileAsync;
88
- /**
89
- * The project's SWC options, built once: rebuilding them for every file was about a quarter
90
- * of load time (#513). Import specifiers are left as written, without `jsc.paths` or
91
- * `resolveFully`: the resolve hook maps them to source files (aliases, `.js` to `.ts`,
92
- * directory indexes), so SWC does not resolve them a second time against the disk.
93
- */
94
- private swcConfigFor;
95
- private esModuleConfigFor;
96
- }
@@ -1,5 +0,0 @@
1
- declare function scan(code: string): string[];
2
- export declare const staticImports: {
3
- scan: typeof scan;
4
- };
5
- export {};
@@ -1,38 +0,0 @@
1
- /**
2
- * Compiled code on disk, so a cold start doesn't compile files that haven't changed (#514).
3
- * Content-addressed: the key hashes the file's path and source, the SWC options it compiles
4
- * with, SWC's version and this cache's format, so any change to those is a miss, never a stale
5
- * hit. Entries are written to a temp file and renamed into place, so a process reading an entry
6
- * another is writing sees all of it or none.
7
- *
8
- * The cache is an optimisation: if it can't read or write its directory, it says so once and
9
- * turns itself off for the process, and flux compiles as it would without it.
10
- */
11
- export declare class TranspileDiskCache {
12
- private readonly dir;
13
- private readonly logger;
14
- private disabled;
15
- private constructor();
16
- /** The cache for a workspace, or null when caching is off (`DECOHERE_CACHE=0`). */
17
- static forWorkspace(rootDir: string): TranspileDiskCache | null;
18
- get directory(): string;
19
- /**
20
- * The key for a file compiled with `baseOptions`: the per-file options are those plus the
21
- * file's path (forge's `createFileConfig`), so hashing the base once per project and adding
22
- * the path covers them without serialising options for every file.
23
- */
24
- keyFor(fullPath: string, source: string, baseOptions: object): string;
25
- private readonly optionsHashes;
26
- private optionsHash;
27
- readSync(key: string): string | undefined;
28
- read(key: string): Promise<string | undefined>;
29
- writeSync(key: string, code: string): void;
30
- write(key: string, code: string): Promise<void>;
31
- /** Deletes entries older than 30 days, if it hasn't done so in the last day. */
32
- pruneIfDue(now?: number): Promise<void>;
33
- private entryPath;
34
- private tempPath;
35
- /** A missing entry is a miss; any other read error turns the cache off. */
36
- private missOrDisable;
37
- private disable;
38
- }