@decohere/flux 0.0.1-dev.11 → 0.0.1-dev.110

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 (40) hide show
  1. package/README.md +247 -0
  2. package/commonjs/src/deferred-script-runner.js +56 -0
  3. package/commonjs/src/flux.command.js +2 -2
  4. package/commonjs/src/flux.js +56 -0
  5. package/commonjs/src/flux.module.js +8 -7
  6. package/commonjs/src/hot-reload-server.js +302 -215
  7. package/commonjs/src/index.js +34 -2
  8. package/commonjs/src/rewrite-serve-specifier.js +59 -0
  9. package/commonjs/src/run.action.js +47 -0
  10. package/commonjs/src/run.command.js +65 -0
  11. package/commonjs/src/serve.action.js +76 -5
  12. package/commonjs/src/serve.command.js +31 -3
  13. package/commonjs/src/slice-serve.js +130 -0
  14. package/commonjs/src/source-module-loader.js +237 -0
  15. package/es6/src/deferred-script-runner.js +46 -0
  16. package/es6/src/flux.command.js +2 -2
  17. package/es6/src/flux.js +41 -0
  18. package/es6/src/flux.module.js +8 -7
  19. package/es6/src/hot-reload-server.js +283 -172
  20. package/es6/src/index.js +5 -3
  21. package/es6/src/rewrite-serve-specifier.js +49 -0
  22. package/es6/src/run.action.js +37 -0
  23. package/es6/src/run.command.js +55 -0
  24. package/es6/src/serve.action.js +71 -7
  25. package/es6/src/serve.command.js +31 -3
  26. package/es6/src/slice-serve.js +105 -0
  27. package/es6/src/source-module-loader.js +181 -0
  28. package/package.json +7 -6
  29. package/types/src/deferred-script-runner.d.ts +18 -0
  30. package/types/src/flux.d.ts +43 -0
  31. package/types/src/flux.module.d.ts +2 -0
  32. package/types/src/hot-reload-server.d.ts +55 -22
  33. package/types/src/index.d.ts +8 -1
  34. package/types/src/rewrite-serve-specifier.d.ts +14 -0
  35. package/types/src/run.action.d.ts +15 -0
  36. package/types/src/run.command.d.ts +11 -0
  37. package/types/src/serve.action.d.ts +11 -1
  38. package/types/src/serve.command.d.ts +5 -0
  39. package/types/src/slice-serve.d.ts +52 -0
  40. package/types/src/source-module-loader.d.ts +58 -0
@@ -0,0 +1,49 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath, pathToFileURL } from "node:url";
3
+ import { packageAliases } from "@decohere/mesh";
4
+ /**
5
+ * Map a resolve-hook specifier to a versioned watched `.ts` file URL.
6
+ * Covers relative `.js` / `.ts` / extensionless imports, directory imports (`./models` is
7
+ * `./models/index.ts`), and bare workspace package names and subpaths. Flux compiles without
8
+ * SWC's own path resolution, so this is the only place imports are mapped to sources (#513).
9
+ */ export function rewriteServeSpecifier(options) {
10
+ const { specifier, parentUrl, isWatchedTs, resolveBare, versionFileUrl } = options;
11
+ const relativeTs = resolveRelativeWatchedTs(specifier, parentUrl, isWatchedTs);
12
+ if (relativeTs != null) {
13
+ return versionFileUrl(pathToFileURL(relativeTs).toString());
14
+ }
15
+ const bareTarget = resolveBare(specifier);
16
+ const bareTs = bareTarget == null ? null : watchedTs(bareTarget, isWatchedTs);
17
+ if (bareTs != null) {
18
+ return versionFileUrl(pathToFileURL(bareTs).toString());
19
+ }
20
+ return null;
21
+ }
22
+ /**
23
+ * The watched `.ts` file a path names: itself (with `.js` read as `.ts`, or `.ts` added), else
24
+ * the `index.ts` of the directory it names. A file wins over a directory of the same name.
25
+ */ function watchedTs(filePath, isWatchedTs) {
26
+ const withoutSlash = filePath.replace(/[/\\]+$/, "");
27
+ const stem = withoutSlash.replace(/\.(js|ts)$/, "");
28
+ // From the stem: Mesh's alias lookup has already added `.ts` to a subpath that names a
29
+ // directory (`@lib/models` arrives as `…/models.ts`).
30
+ const candidates = [
31
+ `${stem}.ts`,
32
+ path.join(stem, "index.ts")
33
+ ];
34
+ return candidates.find((candidate)=>isWatchedTs(candidate)) ?? null;
35
+ }
36
+ function resolveRelativeWatchedTs(specifier, parentUrl, isWatchedTs) {
37
+ if (packageAliases.isBareSpecifier(specifier)) return null;
38
+ if (!(specifier.startsWith(".") || specifier.startsWith("/"))) return null;
39
+ const parentClean = parentUrl.split("?")[0];
40
+ if (parentClean == null || parentClean === "") return null;
41
+ let resolved;
42
+ try {
43
+ resolved = new URL(specifier, parentClean);
44
+ } catch {
45
+ return null;
46
+ }
47
+ if (resolved.protocol !== "file:") return null;
48
+ return watchedTs(fileURLToPath(resolved), isWatchedTs);
49
+ }
@@ -0,0 +1,37 @@
1
+ function _ts_decorate(decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ }
7
+ function _ts_metadata(k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ }
10
+ import { injectable } from "@decohere/crucible/decorators";
11
+ import { Workspace } from "@decohere/mesh";
12
+ import { ActionResponse } from "@decohere/nexus/cli";
13
+ import { scribe } from "@decohere/scribe";
14
+ import { flux } from "./flux.js";
15
+ export class RunAction {
16
+ workspace;
17
+ logger = scribe.logger(RunAction.name);
18
+ constructor(workspace){
19
+ this.workspace = workspace;
20
+ }
21
+ async run(options) {
22
+ this.logger.trace(`Running ${options.entry}`);
23
+ await flux.run(options.entry, {
24
+ configuration: options.configuration,
25
+ args: options.args,
26
+ workspace: this.workspace
27
+ });
28
+ return ActionResponse.Success(RunAction.name);
29
+ }
30
+ }
31
+ RunAction = _ts_decorate([
32
+ injectable(),
33
+ _ts_metadata("design:type", Function),
34
+ _ts_metadata("design:paramtypes", [
35
+ typeof Workspace === "undefined" ? Object : Workspace
36
+ ])
37
+ ], RunAction);
@@ -0,0 +1,55 @@
1
+ import { ConfigurationOption, DecohereCommand } from "@decohere/nexus/cli";
2
+ import { RunAction } from "./run.action.js";
3
+ export class RunCommand extends DecohereCommand {
4
+ get actions() {
5
+ return [
6
+ RunAction
7
+ ];
8
+ }
9
+ constructor(name){
10
+ super(name);
11
+ this.description("Compile and run a TypeScript entry on the fly (no watch / HMR)");
12
+ this.argument("<entry>", "Path to the TypeScript entry file");
13
+ this.argument("[args...]", "Arguments forwarded to the entry after rewriting argv");
14
+ this.addOption(new ConfigurationOption("debug"));
15
+ }
16
+ /**
17
+ * Commander passes positionals before options when `<entry> [args...]` is declared.
18
+ * The CLI host ModuleFactory expects `(opts, cmd)` — normalize and stash entry/args on opts.
19
+ */ action(fn) {
20
+ return super.action((...args)=>{
21
+ const cmd = args.at(-1);
22
+ const opts = args.at(-2);
23
+ const { entry, entryArgs } = normalizeRunPositionals(args.slice(0, -2));
24
+ cmd.setOptionValue("entry", entry);
25
+ cmd.setOptionValue("args", entryArgs);
26
+ return fn(opts, cmd);
27
+ });
28
+ }
29
+ }
30
+ /** Commander yields `[entry, args[]]` or sometimes a flat list depending on arity. */ function normalizeRunPositionals(positionals) {
31
+ if (positionals.length === 0) {
32
+ return {
33
+ entry: "",
34
+ entryArgs: []
35
+ };
36
+ }
37
+ const first = positionals[0];
38
+ if (typeof first !== "string" || first === "") {
39
+ return {
40
+ entry: "",
41
+ entryArgs: []
42
+ };
43
+ }
44
+ const second = positionals[1];
45
+ if (Array.isArray(second)) {
46
+ return {
47
+ entry: first,
48
+ entryArgs: second.filter((s)=>typeof s === "string")
49
+ };
50
+ }
51
+ return {
52
+ entry: first,
53
+ entryArgs: positionals.slice(1).filter((s)=>typeof s === "string" && s !== "")
54
+ };
55
+ }
@@ -7,35 +7,99 @@ function _ts_decorate(decorators, target, key, desc) {
7
7
  function _ts_metadata(k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  }
10
+ import path from "node:path";
10
11
  import { injectable } from "@decohere/crucible/decorators";
11
- // biome-ignore lint/style/useImportType: <DI>
12
12
  import { Workspace } from "@decohere/mesh";
13
13
  import { ActionResponse } from "@decohere/nexus/cli";
14
- // biome-ignore lint/style/useImportType: <DI>
14
+ import { scribe } from "@decohere/scribe";
15
+ import { DeferredScriptRunner } from "./deferred-script-runner.js";
15
16
  import { HotReloadServer } from "./hot-reload-server.js";
17
+ import { classifySliceProjects, resolveDeferScript } from "./slice-serve.js";
16
18
  export class ServeAction {
17
19
  server;
18
20
  workspace;
19
- constructor(server, workspace){
21
+ deferredScripts;
22
+ logger = scribe.logger(ServeAction.name);
23
+ constructor(server, workspace, deferredScripts){
20
24
  this.server = server;
21
25
  this.workspace = workspace;
26
+ this.deferredScripts = deferredScripts;
22
27
  }
23
28
  async run(options) {
29
+ const slices = (options.slices ?? []).filter((s)=>s !== "");
30
+ if (slices.length > 0 && options.project != null) {
31
+ throw new Error("Use either directory slices or --project, not both");
32
+ }
33
+ if (slices.length > 0) {
34
+ return this.runSlices(options, slices);
35
+ }
24
36
  await this.server.start({
25
37
  workspace: this.workspace,
26
38
  buildConfig: options.configuration,
27
- project: options.project
39
+ project: options.project,
40
+ rebootDebounceMs: options.rebootDebounceMs
28
41
  });
29
42
  return ActionResponse.Blocking(ServeAction.name);
30
43
  }
44
+ async runSlices(options, slices) {
45
+ const views = await this.toSliceViews(options.configuration);
46
+ const classified = classifySliceProjects(views, slices);
47
+ const label = slices.join(" ");
48
+ for (const skipped of classified.skip){
49
+ this.logger.info(`skipped ${skipped.projectDirectory} — library (no Flux entry / scripts)`);
50
+ }
51
+ if (classified.flux.length === 0 && classified.defer.length === 0) {
52
+ throw new Error(`No runnable projects in slice(s) "${label}" (need Flux main.ts apps and/or package scripts.dev/serve)`);
53
+ }
54
+ const workspaceRoot = path.resolve(this.workspace.rootDir ?? process.cwd());
55
+ for (const deferred of classified.defer){
56
+ const script = resolveDeferScript(deferred.scripts);
57
+ if (script == null) continue;
58
+ const project = this.findProject(options.configuration, deferred.name);
59
+ this.deferredScripts.start({
60
+ label: deferred.projectDirectory,
61
+ cwd: project.projectRoot,
62
+ script,
63
+ workspaceRoot
64
+ });
65
+ }
66
+ if (classified.flux.length > 0) {
67
+ await this.server.start({
68
+ workspace: this.workspace,
69
+ buildConfig: options.configuration,
70
+ projects: classified.flux.map((p)=>p.name),
71
+ rebootDebounceMs: options.rebootDebounceMs
72
+ });
73
+ }
74
+ return ActionResponse.Blocking(ServeAction.name);
75
+ }
76
+ async toSliceViews(configuration) {
77
+ const projects = this.workspace.getProjects(configuration);
78
+ return Promise.all(projects.map((project)=>this.toSliceView(project)));
79
+ }
80
+ async toSliceView(project) {
81
+ const pkg = await project.getPackageJson();
82
+ return {
83
+ name: project.name,
84
+ projectDirectory: project.projectDirectory.replaceAll("\\", "/"),
85
+ main: pkg.main ?? "src/index.ts",
86
+ scripts: pkg.scripts ?? {}
87
+ };
88
+ }
89
+ findProject(configuration, name) {
90
+ const found = this.workspace.getProjects(configuration).find((p)=>p.name === name);
91
+ if (found == null) {
92
+ throw new Error(`Project "${name}" not found in workspace`);
93
+ }
94
+ return found;
95
+ }
31
96
  }
32
97
  ServeAction = _ts_decorate([
33
98
  injectable(),
34
99
  _ts_metadata("design:type", Function),
35
100
  _ts_metadata("design:paramtypes", [
36
101
  typeof HotReloadServer === "undefined" ? Object : HotReloadServer,
37
- typeof Workspace === "undefined" ? Object : Workspace
102
+ typeof Workspace === "undefined" ? Object : Workspace,
103
+ typeof DeferredScriptRunner === "undefined" ? Object : DeferredScriptRunner
38
104
  ])
39
105
  ], ServeAction);
40
-
41
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9ob21lL3J1bm5lci93b3JrL2RlY29oZXJlL2RlY29oZXJlL3Byb2plY3RzL2ZsdXgvc3JjL3NlcnZlLmFjdGlvbi50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBpbmplY3RhYmxlIH0gZnJvbSBcIkBkZWNvaGVyZS9jcnVjaWJsZS9kZWNvcmF0b3JzXCI7XG4vLyBiaW9tZS1pZ25vcmUgbGludC9zdHlsZS91c2VJbXBvcnRUeXBlOiA8REk+XG5pbXBvcnQgeyBXb3Jrc3BhY2UgfSBmcm9tIFwiQGRlY29oZXJlL21lc2hcIjtcbmltcG9ydCB7IHR5cGUgQWN0aW9uLCBBY3Rpb25SZXNwb25zZSB9IGZyb20gXCJAZGVjb2hlcmUvbmV4dXMvY2xpXCI7XG4vLyBiaW9tZS1pZ25vcmUgbGludC9zdHlsZS91c2VJbXBvcnRUeXBlOiA8REk+XG5pbXBvcnQgeyBIb3RSZWxvYWRTZXJ2ZXIgfSBmcm9tIFwiLi9ob3QtcmVsb2FkLXNlcnZlci5qc1wiO1xuXG5leHBvcnQgdHlwZSBTZXJ2ZU9wdGlvbnMgPSB7XG5cdGNvbmZpZ3VyYXRpb246IHN0cmluZztcblx0cHJvamVjdD86IHN0cmluZztcbn07XG5cbkBpbmplY3RhYmxlKClcbmV4cG9ydCBjbGFzcyBTZXJ2ZUFjdGlvbiBpbXBsZW1lbnRzIEFjdGlvbjxTZXJ2ZU9wdGlvbnM+IHtcblx0cHVibGljIGNvbnN0cnVjdG9yKFxuXHRcdHByaXZhdGUgcmVhZG9ubHkgc2VydmVyOiBIb3RSZWxvYWRTZXJ2ZXIsXG5cdFx0cHJpdmF0ZSByZWFkb25seSB3b3Jrc3BhY2U6IFdvcmtzcGFjZSxcblx0KSB7fVxuXG5cdHB1YmxpYyBhc3luYyBydW4ob3B0aW9uczogU2VydmVPcHRpb25zKTogUHJvbWlzZTxBY3Rpb25SZXNwb25zZT4ge1xuXHRcdGF3YWl0IHRoaXMuc2VydmVyLnN0YXJ0KHtcblx0XHRcdHdvcmtzcGFjZTogdGhpcy53b3Jrc3BhY2UsXG5cdFx0XHRidWlsZENvbmZpZzogb3B0aW9ucy5jb25maWd1cmF0aW9uLFxuXHRcdFx0cHJvamVjdDogb3B0aW9ucy5wcm9qZWN0LFxuXHRcdH0pO1xuXHRcdHJldHVybiBBY3Rpb25SZXNwb25zZS5CbG9ja2luZyhTZXJ2ZUFjdGlvbi5uYW1lKTtcblx0fVxufVxuIl0sIm5hbWVzIjpbImluamVjdGFibGUiLCJXb3Jrc3BhY2UiLCJBY3Rpb25SZXNwb25zZSIsIkhvdFJlbG9hZFNlcnZlciIsIlNlcnZlQWN0aW9uIiwic2VydmVyIiwid29ya3NwYWNlIiwicnVuIiwib3B0aW9ucyIsInN0YXJ0IiwiYnVpbGRDb25maWciLCJjb25maWd1cmF0aW9uIiwicHJvamVjdCIsIkJsb2NraW5nIiwibmFtZSJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7O0FBQUEsU0FBU0EsVUFBVSxRQUFRLGdDQUFnQztBQUMzRCw4Q0FBOEM7QUFDOUMsU0FBU0MsU0FBUyxRQUFRLGlCQUFpQjtBQUMzQyxTQUFzQkMsY0FBYyxRQUFRLHNCQUFzQjtBQUNsRSw4Q0FBOEM7QUFDOUMsU0FBU0MsZUFBZSxRQUFRLHlCQUF5QjtBQVF6RCxPQUFPLE1BQU1DOzs7SUFDWixZQUNDLEFBQWlCQyxNQUF1QixFQUN4QyxBQUFpQkMsU0FBb0IsQ0FDcEM7YUFGZ0JELFNBQUFBO2FBQ0FDLFlBQUFBO0lBQ2Y7SUFFSCxNQUFhQyxJQUFJQyxPQUFxQixFQUEyQjtRQUNoRSxNQUFNLElBQUksQ0FBQ0gsTUFBTSxDQUFDSSxLQUFLLENBQUM7WUFDdkJILFdBQVcsSUFBSSxDQUFDQSxTQUFTO1lBQ3pCSSxhQUFhRixRQUFRRyxhQUFhO1lBQ2xDQyxTQUFTSixRQUFRSSxPQUFPO1FBQ3pCO1FBQ0EsT0FBT1YsZUFBZVcsUUFBUSxDQUFDVCxZQUFZVSxJQUFJO0lBQ2hEO0FBQ0QifQ==
@@ -1,4 +1,5 @@
1
1
  import { ConfigurationOption, DecohereCommand, ProjectOption } from "@decohere/nexus/cli";
2
+ import { DEFAULT_REBOOT_DEBOUNCE_MS, resolveRebootDebounceMs } from "./hot-reload-server.js";
2
3
  import { ServeAction } from "./serve.action.js";
3
4
  export class ServeCommand extends DecohereCommand {
4
5
  get actions() {
@@ -8,10 +9,37 @@ export class ServeCommand extends DecohereCommand {
8
9
  }
9
10
  constructor(name){
10
11
  super(name);
11
- this.description("Serve a project in a workspace configuration");
12
+ this.description("Serve a project or directory slices (Flux apps in-process; foreign packages via package scripts)");
13
+ this.argument("[slices...]", "Module directories to serve (e.g. funding shell secmaster, or projects/funding)");
12
14
  this.addOption(new ConfigurationOption("debug"));
13
15
  this.addOption(new ProjectOption());
16
+ this.option("--reboot-debounce-ms <ms>", "Quiet window in milliseconds after the last affecting save before a reboot", (value)=>resolveRebootDebounceMs(Number(value)), DEFAULT_REBOOT_DEBOUNCE_MS);
14
17
  }
18
+ /**
19
+ * Commander passes positional args before options when `[slices...]` is declared.
20
+ * The CLI host ModuleFactory expects `(opts, cmd)` — normalize here.
21
+ */ action(fn) {
22
+ return super.action((...args)=>{
23
+ const cmd = args.at(-1);
24
+ const first = args[0];
25
+ const hasPositionalSlices = Array.isArray(first) || typeof first === "string" || first === undefined;
26
+ const opts = hasPositionalSlices ? args[1] : args[0];
27
+ const slices = normalizePositionalSlices(first);
28
+ if (slices.length > 0) {
29
+ cmd.setOptionValue("slices", slices);
30
+ }
31
+ return fn(opts, cmd);
32
+ });
33
+ }
34
+ }
35
+ function normalizePositionalSlices(first) {
36
+ if (Array.isArray(first)) {
37
+ return first.filter((s)=>typeof s === "string" && s !== "");
38
+ }
39
+ if (typeof first === "string" && first !== "") {
40
+ return [
41
+ first
42
+ ];
43
+ }
44
+ return [];
15
45
  }
16
-
17
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9ob21lL3J1bm5lci93b3JrL2RlY29oZXJlL2RlY29oZXJlL3Byb2plY3RzL2ZsdXgvc3JjL3NlcnZlLmNvbW1hbmQudHMiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgQ29uZmlndXJhdGlvbk9wdGlvbiwgRGVjb2hlcmVDb21tYW5kLCBQcm9qZWN0T3B0aW9uIH0gZnJvbSBcIkBkZWNvaGVyZS9uZXh1cy9jbGlcIjtcbmltcG9ydCB7IFNlcnZlQWN0aW9uIH0gZnJvbSBcIi4vc2VydmUuYWN0aW9uLmpzXCI7XG5cbmV4cG9ydCBjbGFzcyBTZXJ2ZUNvbW1hbmQgZXh0ZW5kcyBEZWNvaGVyZUNvbW1hbmQge1xuXHRwdWJsaWMgb3ZlcnJpZGUgZ2V0IGFjdGlvbnMoKSB7XG5cdFx0cmV0dXJuIFtTZXJ2ZUFjdGlvbl07XG5cdH1cblxuXHRwdWJsaWMgY29uc3RydWN0b3IobmFtZTogc3RyaW5nKSB7XG5cdFx0c3VwZXIobmFtZSk7XG5cdFx0dGhpcy5kZXNjcmlwdGlvbihcIlNlcnZlIGEgcHJvamVjdCBpbiBhIHdvcmtzcGFjZSBjb25maWd1cmF0aW9uXCIpO1xuXHRcdHRoaXMuYWRkT3B0aW9uKG5ldyBDb25maWd1cmF0aW9uT3B0aW9uKFwiZGVidWdcIikpO1xuXHRcdHRoaXMuYWRkT3B0aW9uKG5ldyBQcm9qZWN0T3B0aW9uKCkpO1xuXHR9XG59XG4iXSwibmFtZXMiOlsiQ29uZmlndXJhdGlvbk9wdGlvbiIsIkRlY29oZXJlQ29tbWFuZCIsIlByb2plY3RPcHRpb24iLCJTZXJ2ZUFjdGlvbiIsIlNlcnZlQ29tbWFuZCIsImFjdGlvbnMiLCJuYW1lIiwiZGVzY3JpcHRpb24iLCJhZGRPcHRpb24iXSwibWFwcGluZ3MiOiJBQUFBLFNBQVNBLG1CQUFtQixFQUFFQyxlQUFlLEVBQUVDLGFBQWEsUUFBUSxzQkFBc0I7QUFDMUYsU0FBU0MsV0FBVyxRQUFRLG9CQUFvQjtBQUVoRCxPQUFPLE1BQU1DLHFCQUFxQkg7SUFDakMsSUFBb0JJLFVBQVU7UUFDN0IsT0FBTztZQUFDRjtTQUFZO0lBQ3JCO0lBRUEsWUFBbUJHLElBQVksQ0FBRTtRQUNoQyxLQUFLLENBQUNBO1FBQ04sSUFBSSxDQUFDQyxXQUFXLENBQUM7UUFDakIsSUFBSSxDQUFDQyxTQUFTLENBQUMsSUFBSVIsb0JBQW9CO1FBQ3ZDLElBQUksQ0FBQ1EsU0FBUyxDQUFDLElBQUlOO0lBQ3BCO0FBQ0QifQ==
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Directory-slice helpers for `flux serve <dir>…` — classify Mesh packages as
3
+ * Flux apps, Nixpacks-style script deferrals, or libraries to skip.
4
+ */ /** Preferred package scripts for foreign toolchains (Nixpacks-style). */ export const DEFER_SCRIPT_CANDIDATES = [
5
+ "dev",
6
+ "serve"
7
+ ];
8
+ /**
9
+ * Normalize a CLI slice token to a `projects/…` prefix without a trailing slash.
10
+ * `funding` and `projects/funding` both become `projects/funding`.
11
+ */ export function normalizeSlicePrefix(slice) {
12
+ const trimmed = slice.replaceAll("\\", "/").replace(/\/+$/, "");
13
+ if (trimmed === "" || trimmed === ".") {
14
+ throw new Error("Slice directory must be a non-empty path");
15
+ }
16
+ if (trimmed === "projects" || trimmed.startsWith("projects/")) {
17
+ return trimmed;
18
+ }
19
+ return `projects/${trimmed}`;
20
+ }
21
+ /**
22
+ * True when `projectDirectory` belongs to the slice token.
23
+ *
24
+ * Matches:
25
+ * - nested: `projects/funding`, `projects/funding/api`
26
+ * - flat dotted package dirs: `projects/funding.web` for token `funding`
27
+ *
28
+ * Does not match siblings that only share a string prefix (`funding` vs `funding-fee.api`).
29
+ */ export function isUnderSlice(projectDirectory, slicePrefix) {
30
+ const dir = projectDirectory.replaceAll("\\", "/").replace(/\/+$/, "");
31
+ const prefix = slicePrefix.replaceAll("\\", "/").replace(/\/+$/, "");
32
+ if (dir === prefix || dir.startsWith(`${prefix}/`)) {
33
+ return true;
34
+ }
35
+ // Flat monorepo packages: projects/<module>.<role>
36
+ return dir.startsWith(`${prefix}.`);
37
+ }
38
+ export function projectsInSlice(projects, slice) {
39
+ const prefix = normalizeSlicePrefix(slice);
40
+ return projects.filter((p)=>isUnderSlice(p.projectDirectory, prefix));
41
+ }
42
+ /** Union of packages under any of the slice tokens (deduped by package name). */ export function projectsInSlices(projects, slices) {
43
+ const byName = new Map();
44
+ for (const slice of slices){
45
+ for (const project of projectsInSlice(projects, slice)){
46
+ byName.set(project.name, project);
47
+ }
48
+ }
49
+ return [
50
+ ...byName.values()
51
+ ];
52
+ }
53
+ /** Script invokes Flux (in-process serve stays on Flux, not deferred). */ export function isFluxOwnedScript(script) {
54
+ return /\bflux\b/i.test(script);
55
+ }
56
+ /**
57
+ * First Nixpacks-style serve script that is not Flux-owned.
58
+ * Prefers `dev`, then `serve`.
59
+ */ export function resolveDeferScript(scripts) {
60
+ for (const name of DEFER_SCRIPT_CANDIDATES){
61
+ const body = scripts[name];
62
+ if (body != null && body !== "" && !isFluxOwnedScript(body)) {
63
+ return name;
64
+ }
65
+ }
66
+ return undefined;
67
+ }
68
+ /** Convention: Flux Node apps use `main` ending in `main.ts` / `main.tsx`. */ export function isFluxAppMain(main) {
69
+ const normalized = main.replaceAll("\\", "/");
70
+ return /(^|\/)main\.tsx?$/.test(normalized);
71
+ }
72
+ /**
73
+ * Classify one package. Foreign serve scripts win over `main.ts` (Vite apps
74
+ * often have a main entry but must not be HMR-booted by Flux).
75
+ */ export function classifySliceProject(project) {
76
+ if (resolveDeferScript(project.scripts) != null) {
77
+ return "defer";
78
+ }
79
+ if (isFluxAppMain(project.main)) {
80
+ return "flux";
81
+ }
82
+ return "skip";
83
+ }
84
+ export function classifySliceProjects(projects, slices) {
85
+ const tokens = typeof slices === "string" ? [
86
+ slices
87
+ ] : [
88
+ ...slices
89
+ ];
90
+ const inSlice = projectsInSlices(projects, tokens);
91
+ const flux = [];
92
+ const defer = [];
93
+ const skip = [];
94
+ for (const project of inSlice){
95
+ const kind = classifySliceProject(project);
96
+ if (kind === "flux") flux.push(project);
97
+ else if (kind === "defer") defer.push(project);
98
+ else skip.push(project);
99
+ }
100
+ return {
101
+ flux,
102
+ defer,
103
+ skip
104
+ };
105
+ }
@@ -0,0 +1,181 @@
1
+ function _ts_decorate(decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ }
7
+ import { registerHooks } from "node:module";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { injectable } from "@decohere/crucible/decorators";
11
+ import { BuildTargets, createWorkspace, SwcTranspiler } from "@decohere/forge";
12
+ import { packageAliases } from "@decohere/mesh";
13
+ import { FileEx, io } from "@decohere/nexus/io";
14
+ import { scribe } from "@decohere/scribe";
15
+ import { rewriteServeSpecifier } from "./rewrite-serve-specifier.js";
16
+ export class SourceModuleLoader {
17
+ logger = scribe.logger(SourceModuleLoader.name);
18
+ transpiler = new SwcTranspiler();
19
+ disposed = false;
20
+ hooksRegistered = false;
21
+ workspace;
22
+ buildConfig = "debug";
23
+ fileInfoMap = new Map();
24
+ transpileCache = new Map();
25
+ /** SWC options per project, built on first use and kept until the next `configure()`. */ swcConfigs = new Map();
26
+ aliasPaths = {};
27
+ aliasBaseUrl = path.resolve(".");
28
+ versionFileUrl = (fileUrl)=>fileUrl;
29
+ onResolved;
30
+ getWorkspace() {
31
+ return this.workspace;
32
+ }
33
+ getBuildConfig() {
34
+ return this.buildConfig;
35
+ }
36
+ hasSourceFile(absolutePath) {
37
+ return this.fileInfoMap.has(absolutePath);
38
+ }
39
+ sourceFilePaths() {
40
+ return this.fileInfoMap.keys();
41
+ }
42
+ setSourceFile(absolutePath, project) {
43
+ this.fileInfoMap.set(absolutePath, {
44
+ project
45
+ });
46
+ }
47
+ async configure(options) {
48
+ this.disposed = false;
49
+ this.transpileCache.clear();
50
+ this.swcConfigs.clear();
51
+ this.buildConfig = options.buildConfig;
52
+ this.workspace = options.workspace ?? await createWorkspace();
53
+ this.versionFileUrl = options.versionFileUrl ?? ((fileUrl)=>fileUrl);
54
+ this.onResolved = options.onResolved;
55
+ this.fileInfoMap = await this.buildFileInfoMap(this.buildConfig);
56
+ const aliases = this.workspace.getPackageAliases(this.buildConfig);
57
+ this.aliasBaseUrl = aliases.baseUrl;
58
+ this.aliasPaths = aliases.paths;
59
+ }
60
+ registerHooks() {
61
+ if (this.hooksRegistered) return;
62
+ this.hooksRegistered = true;
63
+ // Compiled code carries inline source maps; Node only applies them to stack traces when
64
+ // this is on. Without it, errors point at compiled line numbers (#506).
65
+ process.setSourceMapsEnabled(true);
66
+ registerHooks({
67
+ resolve: (specifier, context, next)=>{
68
+ if (this.disposed) return next(specifier, context);
69
+ const parentUrl = context.parentURL ?? "";
70
+ const rewritten = rewriteServeSpecifier({
71
+ specifier,
72
+ parentUrl,
73
+ isWatchedTs: (filePath)=>this.fileInfoMap.has(filePath),
74
+ resolveBare: (bare)=>packageAliases.resolvePath(bare, this.aliasPaths, this.aliasBaseUrl),
75
+ versionFileUrl: this.versionFileUrl
76
+ }) ?? specifier;
77
+ const result = next(rewritten, context);
78
+ this.onResolved?.(result, parentUrl);
79
+ return result;
80
+ },
81
+ load: (url, context, next)=>{
82
+ if (this.disposed) return next(url, context);
83
+ const result = this.loadTs(url);
84
+ if (result != null) {
85
+ return {
86
+ format: "module",
87
+ source: result.source,
88
+ shortCircuit: true
89
+ };
90
+ }
91
+ return next(url, context);
92
+ }
93
+ });
94
+ }
95
+ async importEntry(absolutePath) {
96
+ const url = this.versionFileUrl(io.pathToFileUrl(path.resolve(absolutePath)));
97
+ return import(url);
98
+ }
99
+ invalidateTranspile(...absolutePaths) {
100
+ for (const fullPath of absolutePaths){
101
+ this.transpileCache.delete(fullPath);
102
+ this.transpiler.invalidateContentCache(new FileEx({
103
+ fullPath
104
+ }));
105
+ }
106
+ }
107
+ dispose() {
108
+ this.disposed = true;
109
+ this.transpileCache.clear();
110
+ this.swcConfigs.clear();
111
+ this.fileInfoMap.clear();
112
+ this.workspace?.[Symbol.dispose]();
113
+ this.workspace = undefined;
114
+ }
115
+ async buildFileInfoMap(buildConfig) {
116
+ const projects = this.workspace?.getProjects(buildConfig) ?? [];
117
+ const entries = await Promise.all(projects.map(async (project)=>{
118
+ const files = await project.getAllFiles();
119
+ return files.map((file)=>[
120
+ file.fullPath,
121
+ {
122
+ project
123
+ }
124
+ ]);
125
+ }));
126
+ return new Map(entries.flat());
127
+ }
128
+ loadTs(url) {
129
+ if (!url.startsWith("file:")) return null;
130
+ // fileURLToPath, not URL.pathname: decodes %20 and gives Windows paths, so files
131
+ // whose path has a space still match Mesh's list (#511).
132
+ const cleanPath = fileURLToPath(url);
133
+ const fileInfo = this.fileInfoMap.get(cleanPath);
134
+ if (fileInfo == null) return null;
135
+ const cached = this.transpileCache.get(cleanPath);
136
+ if (cached != null) return {
137
+ source: cached
138
+ };
139
+ const file = new FileEx({
140
+ fullPath: cleanPath
141
+ });
142
+ const response = this.transpiler.transformFileSync(file, BuildTargets.es6, {}, this.swcConfigFor(fileInfo.project));
143
+ if (response.outcome !== "success") {
144
+ this.logger.error(`Failed to transpile ${cleanPath}`, response);
145
+ throw new Error(`Failed to transpile ${cleanPath}`);
146
+ }
147
+ this.transpileCache.set(cleanPath, response.code);
148
+ return {
149
+ source: response.code
150
+ };
151
+ }
152
+ /**
153
+ * The project's SWC options, built once: rebuilding them for every file was about a quarter
154
+ * of load time (#513). Import specifiers are left as written, without `jsc.paths` or
155
+ * `resolveFully`: the resolve hook maps them to source files (aliases, `.js` to `.ts`,
156
+ * directory indexes), so SWC does not resolve them a second time against the disk.
157
+ */ swcConfigFor(project) {
158
+ const cached = this.swcConfigs.get(project);
159
+ if (cached != null) return cached;
160
+ const base = this.transpiler.createBaseConfig(BuildTargets.es6, {
161
+ tsConfig: project.getTsConfigSync(),
162
+ userConfig: project.getSwcConfigSync(),
163
+ aliasPaths: false,
164
+ workspace: this.workspace
165
+ });
166
+ // `module` is the ES module config (`type: "es6"` above); the spread loses the narrowing.
167
+ const module = {
168
+ ...base.module,
169
+ resolveFully: false
170
+ };
171
+ const config = {
172
+ ...base,
173
+ module
174
+ };
175
+ this.swcConfigs.set(project, config);
176
+ return config;
177
+ }
178
+ }
179
+ SourceModuleLoader = _ts_decorate([
180
+ injectable()
181
+ ], SourceModuleLoader);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decohere/flux",
3
- "version": "0.0.1-dev.11",
3
+ "version": "0.0.1-dev.110",
4
4
  "description": "Hot module reload server for Node.js ESM applications",
5
5
  "keywords": [
6
6
  "typescript",
@@ -25,11 +25,12 @@
25
25
  "test": "echo \"Error: no test specified\" && exit 1"
26
26
  },
27
27
  "dependencies": {
28
- "@decohere/crucible": "0.0.1-dev.11",
29
- "@decohere/forge": "0.0.1-dev.11",
30
- "@decohere/mesh": "0.0.1-dev.11",
31
- "@decohere/nexus": "0.0.1-dev.11",
32
- "@decohere/scribe": "0.0.1-dev.11",
28
+ "@decohere/crucible": "0.0.1-dev.110",
29
+ "@decohere/forge": "0.0.1-dev.110",
30
+ "@decohere/mesh": "0.0.1-dev.110",
31
+ "@decohere/nexus": "0.0.1-dev.110",
32
+ "@decohere/scribe": "0.0.1-dev.110",
33
+ "commander": "^13.1.0",
33
34
  "rxjs": "^7.8.2"
34
35
  },
35
36
  "devDependencies": {}
@@ -0,0 +1,18 @@
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
+ }