@fluixi/vite-plugin 1.0.0-alpha.82 β†’ 1.0.0-alpha.84

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 CHANGED
@@ -60,6 +60,24 @@ Options are [`@fluixi/compiler`](../compiler)'s `CompilerOptions`:
60
60
  | `delegateEvents` | `true` | event delegation |
61
61
  | `sourceMaps` | `true` | emit source maps |
62
62
  | `babel` | `false` | compile through the babel front-end instead, for a project whose own babel plugins have to see the output. Needs `@babel/core`, which is an optional peer |
63
+ | `devtools` | on in dev | serve the running app's reactive graph, and mark where each node was created. `{ external: true }` marks dependencies too |
64
+
65
+ ## πŸ•ΈοΈ Reactive graph
66
+
67
+ The plugin adds a small agent to the page in development mode, which reports the reactive graph at `GET /__fluixi/graph`. This graph is available as plain JSON, making it easy to read and use in tools like editor extensions or panels. The agent comes with the plugin, so a project needs nothing else installed; only the framework itself is resolved from the project, since watching a second copy of the runtime would watch a graph nothing is using.
68
+
69
+ Here is how you can configure the plugin:
70
+
71
+ ```ts
72
+ // vite.config.ts
73
+ plugins: [fluixi()], // graph on in dev
74
+ plugins: [fluixi({ devtools: false })], // off
75
+ plugins: [fluixi({ devtools: { external: true } })], // dependencies too
76
+ ```
77
+
78
+ Each node in the graph carries the file, line, and column it was created at, named relative to the project root. By default, nodes from dependencies are not marked, which keeps the graph focused on the code you are editing.
79
+
80
+ The `external` option is useful for working on the framework itself. It marks nodes from `node_modules` and outside the project root as `external`, allowing you to see which nodes were created by libraries and which by your application. The application's call site takes precedence over the wrapper that created the node, so `count` appears as the line you wrote it on, not the line inside `signal()`.
63
81
 
64
82
  ## πŸ“„ License
65
83
 
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/graph-channel.ts
31
+ var graph_channel_exports = {};
32
+ __export(graph_channel_exports, {
33
+ withGraphChannel: () => withGraphChannel
34
+ });
35
+ module.exports = __toCommonJS(graph_channel_exports);
36
+ var import_node_module = require("node:module");
37
+ var import_node_path = require("node:path");
38
+ var import_meta = {};
39
+ var VIRTUAL = "virtual:fluixi/devtools-agent";
40
+ var RESOLVED = `\0${VIRTUAL}`;
41
+ var ENDPOINT = "/__fluixi/graph";
42
+ var EVENT = "fluixi:graph";
43
+ var MISSING = "the plugin could not load @fluixi/devtools from its own dependencies";
44
+ var WAITING = "no page has reported yet β€” open the application in a browser";
45
+ var INTERVAL = 100;
46
+ var HISTORY = 500;
47
+ var agentSource = (devtools, plugins, signal, reactive) => `
48
+ import { installDevtoolsHook, observeGraph } from ${JSON.stringify(devtools)};
49
+ import ${JSON.stringify(plugins)};
50
+ import { observeReactiveNodes } from ${JSON.stringify(signal)};
51
+ import { VERSION } from ${JSON.stringify(reactive)};
52
+
53
+ // The runtime this module imports is the application's own β€” same module graph, same nodes.
54
+ // Nothing waits for the runtime to offer itself; that path is for a tool loaded from outside.
55
+ //
56
+ // \`watch\` is this copy of devtools, handed over with the runtime. A browser extension
57
+ // installs a hook at document_start and so owns it; without this the graph would be built
58
+ // by that extension's build rather than the one shipped alongside this application.
59
+ const hook = installDevtoolsHook();
60
+ hook.inject({ observeReactiveNodes, version: VERSION, watch: observeGraph });
61
+
62
+ const send = (kind, snapshot) => import.meta.hot?.send(${JSON.stringify(EVENT)}, { kind, snapshot });
63
+
64
+ const first = hook.snapshot();
65
+ // Kept and handed back, so a browser panel polling the same hook cannot consume what this
66
+ // one has not been told about yet.
67
+ let cursor = first?.cursor;
68
+ send('full', first);
69
+ const timer = setInterval(() => {
70
+ const delta = hook.snapshot({ partial: true, since: cursor });
71
+ if (!delta) return;
72
+ cursor = delta.cursor;
73
+ // Events count on their own: an effect that ran without changing a value moves no node.
74
+ if (delta.nodes.length || delta.removed?.length || delta.events?.length) send('delta', delta);
75
+ }, ${INTERVAL});
76
+
77
+ import.meta.hot?.dispose(() => clearInterval(timer));
78
+ `;
79
+ function ownDevtools() {
80
+ try {
81
+ return (0, import_node_module.createRequire)(import_meta.url).resolve("@fluixi/devtools");
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+ function ownPlugins() {
87
+ try {
88
+ return (0, import_node_module.createRequire)(import_meta.url).resolve("@fluixi/devtools/plugins");
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+ function withGraphChannel(base, enabled, start) {
94
+ let wire = null;
95
+ let available = false;
96
+ let reason = MISSING;
97
+ let root = process.cwd();
98
+ let log;
99
+ let graph = /* @__PURE__ */ new Map();
100
+ let stamp = 0;
101
+ let events = [];
102
+ const baseResolveId = base.resolveId;
103
+ const baseTransformIndexHtml = base.transformIndexHtml;
104
+ return {
105
+ ...base,
106
+ resolveId(id, importer) {
107
+ if (id === VIRTUAL) return RESOLVED;
108
+ return baseResolveId?.call(this, id, importer) ?? null;
109
+ },
110
+ async load(id) {
111
+ if (id !== RESOLVED) return null;
112
+ const from = (0, import_node_path.join)(root, "fluixi-devtools-agent.js");
113
+ const specs = ["@fluixi/reactive/signal", "@fluixi/reactive"];
114
+ const ids = await Promise.all(specs.map((spec) => this.resolve?.(spec, from, { skipSelf: true })));
115
+ const missing = specs.filter((_, i) => !ids[i]?.id);
116
+ const devtools = ownDevtools();
117
+ const plugins = ownPlugins();
118
+ if (missing.length || !devtools || !plugins) {
119
+ reason = missing.length ? `${missing.join(", ")} did not resolve from ${root}` : MISSING;
120
+ log?.(`[fluixi] reactive graph off β€” ${reason}`);
121
+ return "export {};";
122
+ }
123
+ return agentSource(`/@fs/${devtools}`, `/@fs/${plugins}`, ids[0].id, ids[1].id);
124
+ },
125
+ async configureServer(server) {
126
+ if (!enabled()) return;
127
+ root = server.config?.root ?? root;
128
+ log = server.config?.logger?.info.bind(server.config.logger);
129
+ wire = await import("@fluixi/devtools/wire").catch(() => null);
130
+ available = wire !== null;
131
+ if (wire) start();
132
+ if (wire) reason = WAITING;
133
+ else {
134
+ log?.(`[fluixi] reactive graph off β€” ${MISSING}`);
135
+ }
136
+ server.middlewares.use(ENDPOINT, (_req, res) => {
137
+ const snapshot = wire ? { ...wire.serializeGraph(graph, stamp || Date.now()), events } : { t: Date.now(), nodes: [] };
138
+ const body = JSON.stringify(reason ? { ...snapshot, reason } : snapshot);
139
+ res.setHeader("Content-Type", "application/json");
140
+ res.setHeader("Access-Control-Allow-Origin", "*");
141
+ res.setHeader("Cache-Control", "no-store");
142
+ res.end(body);
143
+ });
144
+ if (!wire) return;
145
+ server.ws.on(EVENT, (payload) => {
146
+ const message = payload;
147
+ if (!message?.snapshot || !wire) return;
148
+ const full = message.kind === "full";
149
+ graph = wire.applySnapshot(full ? /* @__PURE__ */ new Map() : graph, message.snapshot);
150
+ if (full) events = [];
151
+ const arriving = message.snapshot.events;
152
+ if (arriving?.length) events = [...events, ...arriving].slice(-HISTORY);
153
+ stamp = Date.now();
154
+ reason = null;
155
+ });
156
+ },
157
+ transformIndexHtml: {
158
+ order: "pre",
159
+ handler(html, ctx) {
160
+ const inherited = callInheritedHtml(baseTransformIndexHtml, html, ctx);
161
+ if (!enabled() || !available || !wire) return inherited;
162
+ return {
163
+ html: typeof inherited === "string" ? inherited : html,
164
+ tags: [
165
+ {
166
+ tag: "script",
167
+ // First in the head, so the entry cannot create a node before the agent runs.
168
+ injectTo: "head-prepend",
169
+ attrs: { type: "module", src: `/@id/${VIRTUAL}` }
170
+ }
171
+ ]
172
+ };
173
+ }
174
+ }
175
+ };
176
+ }
177
+ function callInheritedHtml(hook, html, ctx) {
178
+ if (typeof hook === "function") return hook(html, ctx);
179
+ const handler = hook?.handler;
180
+ return handler?.(html, ctx);
181
+ }
@@ -0,0 +1,7 @@
1
+ import type { Plugin } from 'vite';
2
+ /**
3
+ * Add the graph channel to a plugin, keeping whatever hooks it already has. Composed rather
4
+ * than returned alongside so `fluixi()` stays one plugin.
5
+ */
6
+ export declare function withGraphChannel(base: Plugin, enabled: () => boolean, start: () => void): Plugin;
7
+ //# sourceMappingURL=graph-channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-channel.d.ts","sourceRoot":"","sources":["../src/graph-channel.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AA4EnC;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,OAAO,EAAE,KAAK,EAAE,MAAM,IAAI,GAAG,MAAM,CAyHhG"}
@@ -0,0 +1,210 @@
1
+ // The running app's reactive graph, offered to whatever wants to draw it.
2
+ //
3
+ // A dev-only agent module goes into the page ahead of the entry, so no node is created
4
+ // before something is watching. It pushes the graph over the channel Vite already keeps open
5
+ // for HMR, and the dev server holds the latest state and answers a plain GET β€” so an editor
6
+ // extension, a panel, or curl all read the same thing without speaking HMR.
7
+ //
8
+ // GET /__fluixi/graph β†’ { t, nodes: [...] }
9
+ //
10
+ // Off in a build. The agent's own half comes from this package's dependency rather than from
11
+ // the application, so a project gets the graph by having the plugin and nothing else; only
12
+ // the framework itself is resolved from the project, because watching a second copy of the
13
+ // runtime would watch a graph nobody is using.
14
+ import { createRequire } from 'node:module';
15
+ import { join } from 'node:path';
16
+ const VIRTUAL = 'virtual:fluixi/devtools-agent';
17
+ const RESOLVED = `\0${VIRTUAL}`;
18
+ const ENDPOINT = '/__fluixi/graph';
19
+ const EVENT = 'fluixi:graph';
20
+ /** Why a project that runs the plugin might still have nothing to show. */
21
+ const MISSING = 'the plugin could not load @fluixi/devtools from its own dependencies';
22
+ const WAITING = 'no page has reported yet β€” open the application in a browser';
23
+ /** How often the agent looks for movement. Fast enough to watch, slow enough to ignore. */
24
+ const INTERVAL = 100;
25
+ /** A few hundred interactions' worth. */
26
+ const HISTORY = 500;
27
+ /** Written with the ids resolved: a virtual module has no directory to resolve a bare one from. */
28
+ const agentSource = (devtools, plugins, signal, reactive) => `
29
+ import { installDevtoolsHook, observeGraph } from ${JSON.stringify(devtools)};
30
+ import ${JSON.stringify(plugins)};
31
+ import { observeReactiveNodes } from ${JSON.stringify(signal)};
32
+ import { VERSION } from ${JSON.stringify(reactive)};
33
+
34
+ // The runtime this module imports is the application's own β€” same module graph, same nodes.
35
+ // Nothing waits for the runtime to offer itself; that path is for a tool loaded from outside.
36
+ //
37
+ // \`watch\` is this copy of devtools, handed over with the runtime. A browser extension
38
+ // installs a hook at document_start and so owns it; without this the graph would be built
39
+ // by that extension's build rather than the one shipped alongside this application.
40
+ const hook = installDevtoolsHook();
41
+ hook.inject({ observeReactiveNodes, version: VERSION, watch: observeGraph });
42
+
43
+ const send = (kind, snapshot) => import.meta.hot?.send(${JSON.stringify(EVENT)}, { kind, snapshot });
44
+
45
+ const first = hook.snapshot();
46
+ // Kept and handed back, so a browser panel polling the same hook cannot consume what this
47
+ // one has not been told about yet.
48
+ let cursor = first?.cursor;
49
+ send('full', first);
50
+ const timer = setInterval(() => {
51
+ const delta = hook.snapshot({ partial: true, since: cursor });
52
+ if (!delta) return;
53
+ cursor = delta.cursor;
54
+ // Events count on their own: an effect that ran without changing a value moves no node.
55
+ if (delta.nodes.length || delta.removed?.length || delta.events?.length) send('delta', delta);
56
+ }, ${INTERVAL});
57
+
58
+ import.meta.hot?.dispose(() => clearInterval(timer));
59
+ `;
60
+ /**
61
+ * The devtools bundle this plugin depends on, as a path the dev server can serve.
62
+ *
63
+ * Asking the application to install it as well made the graph depend on a devDependency
64
+ * nobody knew to add, and in a workspace it resolved for one project and not the one beside
65
+ * it β€” which reads as the feature being broken rather than absent.
66
+ */
67
+ function ownDevtools() {
68
+ try {
69
+ return createRequire(import.meta.url).resolve('@fluixi/devtools');
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ /** The built-in plugins, beside the bundle they extend. Collecting happens in the agent. */
76
+ function ownPlugins() {
77
+ try {
78
+ return createRequire(import.meta.url).resolve('@fluixi/devtools/plugins');
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ /**
85
+ * Add the graph channel to a plugin, keeping whatever hooks it already has. Composed rather
86
+ * than returned alongside so `fluixi()` stays one plugin.
87
+ */
88
+ export function withGraphChannel(base, enabled, start) {
89
+ let wire = null;
90
+ let available = false;
91
+ /** What to say when the graph is empty. Cleared once a page has reported. */
92
+ let reason = MISSING;
93
+ let root = process.cwd();
94
+ let log;
95
+ let graph = new Map();
96
+ let stamp = 0;
97
+ /**
98
+ * Kept, not drained: this endpoint has no single reader, and an editor panel draining it
99
+ * would blank a browser panel's timeline. Every event carries a seq, so consumers dedupe.
100
+ */
101
+ let events = [];
102
+ const baseResolveId = base.resolveId;
103
+ const baseTransformIndexHtml = base.transformIndexHtml;
104
+ return {
105
+ ...base,
106
+ resolveId(id, importer) {
107
+ if (id === VIRTUAL)
108
+ return RESOLVED;
109
+ return baseResolveId?.call(this, id, importer) ?? null;
110
+ },
111
+ async load(id) {
112
+ if (id !== RESOLVED)
113
+ return null;
114
+ const from = join(root, 'fluixi-devtools-agent.js');
115
+ // Only the framework comes from the application: the hook hands over that runtime's
116
+ // own registration, and a second copy would report a graph nothing is using.
117
+ const specs = ['@fluixi/reactive/signal', '@fluixi/reactive'];
118
+ const ids = await Promise.all(specs.map((spec) => this.resolve?.(spec, from, { skipSelf: true })));
119
+ const missing = specs.filter((_, i) => !ids[i]?.id);
120
+ const devtools = ownDevtools();
121
+ const plugins = ownPlugins();
122
+ // Nothing to watch with is not an error worth breaking the page over.
123
+ if (missing.length || !devtools || !plugins) {
124
+ reason = missing.length
125
+ ? `${missing.join(', ')} did not resolve from ${root}`
126
+ : MISSING;
127
+ log?.(`[fluixi] reactive graph off β€” ${reason}`);
128
+ return 'export {};';
129
+ }
130
+ // Served by path: it is this package's copy, not one the application imports.
131
+ return agentSource(`/@fs/${devtools}`, `/@fs/${plugins}`, ids[0].id, ids[1].id);
132
+ },
133
+ async configureServer(server) {
134
+ if (!enabled())
135
+ return;
136
+ root = server.config?.root ?? root;
137
+ log = server.config?.logger?.info.bind(server.config.logger);
138
+ wire = await import('@fluixi/devtools/wire').catch(() => null);
139
+ available = wire !== null;
140
+ // Source marks cost something, so they wait until there is a server watching for them.
141
+ if (wire)
142
+ start();
143
+ if (wire)
144
+ reason = WAITING;
145
+ else {
146
+ // Said once, not per request: without the package there is no agent to inject and
147
+ // nothing to report, and that is a missing devDependency rather than a failure.
148
+ log?.(`[fluixi] reactive graph off β€” ${MISSING}`);
149
+ }
150
+ // The endpoint answers either way. A tool asking for the graph of a project that has
151
+ // not turned it on deserves to be told which of the two it is, and 404 says neither β€”
152
+ // it reads the same as a dev server that is not a fluixi project at all.
153
+ server.middlewares.use(ENDPOINT, (_req, res) => {
154
+ const snapshot = wire
155
+ ? { ...wire.serializeGraph(graph, stamp || Date.now()), events }
156
+ : { t: Date.now(), nodes: [] };
157
+ const body = JSON.stringify(reason ? { ...snapshot, reason } : snapshot);
158
+ res.setHeader('Content-Type', 'application/json');
159
+ // Read by an editor extension and by panels served from another origin.
160
+ res.setHeader('Access-Control-Allow-Origin', '*');
161
+ res.setHeader('Cache-Control', 'no-store');
162
+ res.end(body);
163
+ });
164
+ if (!wire)
165
+ return;
166
+ server.ws.on(EVENT, (payload) => {
167
+ const message = payload;
168
+ if (!message?.snapshot || !wire)
169
+ return;
170
+ // A reload sends a full snapshot, which replaces whatever the last page left behind.
171
+ const full = message.kind === 'full';
172
+ graph = wire.applySnapshot(full ? new Map() : graph, message.snapshot);
173
+ // The history goes with it: its ids name nodes the previous page had.
174
+ if (full)
175
+ events = [];
176
+ const arriving = message.snapshot.events;
177
+ if (arriving?.length)
178
+ events = [...events, ...arriving].slice(-HISTORY);
179
+ stamp = Date.now();
180
+ reason = null;
181
+ });
182
+ },
183
+ transformIndexHtml: {
184
+ order: 'pre',
185
+ handler(html, ctx) {
186
+ const inherited = callInheritedHtml(baseTransformIndexHtml, html, ctx);
187
+ if (!enabled() || !available || !wire)
188
+ return inherited;
189
+ return {
190
+ html: typeof inherited === 'string' ? inherited : html,
191
+ tags: [
192
+ {
193
+ tag: 'script',
194
+ // First in the head, so the entry cannot create a node before the agent runs.
195
+ injectTo: 'head-prepend',
196
+ attrs: { type: 'module', src: `/@id/${VIRTUAL}` },
197
+ },
198
+ ],
199
+ };
200
+ },
201
+ },
202
+ };
203
+ }
204
+ /** A plugin may declare transformIndexHtml as a function or as an object with a handler. */
205
+ function callInheritedHtml(hook, html, ctx) {
206
+ if (typeof hook === 'function')
207
+ return hook(html, ctx);
208
+ const handler = hook?.handler;
209
+ return handler?.(html, ctx);
210
+ }
@@ -0,0 +1,149 @@
1
+ // src/graph-channel.ts
2
+ import { createRequire } from "node:module";
3
+ import { join } from "node:path";
4
+ var VIRTUAL = "virtual:fluixi/devtools-agent";
5
+ var RESOLVED = `\0${VIRTUAL}`;
6
+ var ENDPOINT = "/__fluixi/graph";
7
+ var EVENT = "fluixi:graph";
8
+ var MISSING = "the plugin could not load @fluixi/devtools from its own dependencies";
9
+ var WAITING = "no page has reported yet β€” open the application in a browser";
10
+ var INTERVAL = 100;
11
+ var HISTORY = 500;
12
+ var agentSource = (devtools, plugins, signal, reactive) => `
13
+ import { installDevtoolsHook, observeGraph } from ${JSON.stringify(devtools)};
14
+ import ${JSON.stringify(plugins)};
15
+ import { observeReactiveNodes } from ${JSON.stringify(signal)};
16
+ import { VERSION } from ${JSON.stringify(reactive)};
17
+
18
+ // The runtime this module imports is the application's own β€” same module graph, same nodes.
19
+ // Nothing waits for the runtime to offer itself; that path is for a tool loaded from outside.
20
+ //
21
+ // \`watch\` is this copy of devtools, handed over with the runtime. A browser extension
22
+ // installs a hook at document_start and so owns it; without this the graph would be built
23
+ // by that extension's build rather than the one shipped alongside this application.
24
+ const hook = installDevtoolsHook();
25
+ hook.inject({ observeReactiveNodes, version: VERSION, watch: observeGraph });
26
+
27
+ const send = (kind, snapshot) => import.meta.hot?.send(${JSON.stringify(EVENT)}, { kind, snapshot });
28
+
29
+ const first = hook.snapshot();
30
+ // Kept and handed back, so a browser panel polling the same hook cannot consume what this
31
+ // one has not been told about yet.
32
+ let cursor = first?.cursor;
33
+ send('full', first);
34
+ const timer = setInterval(() => {
35
+ const delta = hook.snapshot({ partial: true, since: cursor });
36
+ if (!delta) return;
37
+ cursor = delta.cursor;
38
+ // Events count on their own: an effect that ran without changing a value moves no node.
39
+ if (delta.nodes.length || delta.removed?.length || delta.events?.length) send('delta', delta);
40
+ }, ${INTERVAL});
41
+
42
+ import.meta.hot?.dispose(() => clearInterval(timer));
43
+ `;
44
+ function ownDevtools() {
45
+ try {
46
+ return createRequire(import.meta.url).resolve("@fluixi/devtools");
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+ function ownPlugins() {
52
+ try {
53
+ return createRequire(import.meta.url).resolve("@fluixi/devtools/plugins");
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+ function withGraphChannel(base, enabled, start) {
59
+ let wire = null;
60
+ let available = false;
61
+ let reason = MISSING;
62
+ let root = process.cwd();
63
+ let log;
64
+ let graph = /* @__PURE__ */ new Map();
65
+ let stamp = 0;
66
+ let events = [];
67
+ const baseResolveId = base.resolveId;
68
+ const baseTransformIndexHtml = base.transformIndexHtml;
69
+ return {
70
+ ...base,
71
+ resolveId(id, importer) {
72
+ if (id === VIRTUAL) return RESOLVED;
73
+ return baseResolveId?.call(this, id, importer) ?? null;
74
+ },
75
+ async load(id) {
76
+ if (id !== RESOLVED) return null;
77
+ const from = join(root, "fluixi-devtools-agent.js");
78
+ const specs = ["@fluixi/reactive/signal", "@fluixi/reactive"];
79
+ const ids = await Promise.all(specs.map((spec) => this.resolve?.(spec, from, { skipSelf: true })));
80
+ const missing = specs.filter((_, i) => !ids[i]?.id);
81
+ const devtools = ownDevtools();
82
+ const plugins = ownPlugins();
83
+ if (missing.length || !devtools || !plugins) {
84
+ reason = missing.length ? `${missing.join(", ")} did not resolve from ${root}` : MISSING;
85
+ log?.(`[fluixi] reactive graph off β€” ${reason}`);
86
+ return "export {};";
87
+ }
88
+ return agentSource(`/@fs/${devtools}`, `/@fs/${plugins}`, ids[0].id, ids[1].id);
89
+ },
90
+ async configureServer(server) {
91
+ if (!enabled()) return;
92
+ root = server.config?.root ?? root;
93
+ log = server.config?.logger?.info.bind(server.config.logger);
94
+ wire = await import("@fluixi/devtools/wire").catch(() => null);
95
+ available = wire !== null;
96
+ if (wire) start();
97
+ if (wire) reason = WAITING;
98
+ else {
99
+ log?.(`[fluixi] reactive graph off β€” ${MISSING}`);
100
+ }
101
+ server.middlewares.use(ENDPOINT, (_req, res) => {
102
+ const snapshot = wire ? { ...wire.serializeGraph(graph, stamp || Date.now()), events } : { t: Date.now(), nodes: [] };
103
+ const body = JSON.stringify(reason ? { ...snapshot, reason } : snapshot);
104
+ res.setHeader("Content-Type", "application/json");
105
+ res.setHeader("Access-Control-Allow-Origin", "*");
106
+ res.setHeader("Cache-Control", "no-store");
107
+ res.end(body);
108
+ });
109
+ if (!wire) return;
110
+ server.ws.on(EVENT, (payload) => {
111
+ const message = payload;
112
+ if (!message?.snapshot || !wire) return;
113
+ const full = message.kind === "full";
114
+ graph = wire.applySnapshot(full ? /* @__PURE__ */ new Map() : graph, message.snapshot);
115
+ if (full) events = [];
116
+ const arriving = message.snapshot.events;
117
+ if (arriving?.length) events = [...events, ...arriving].slice(-HISTORY);
118
+ stamp = Date.now();
119
+ reason = null;
120
+ });
121
+ },
122
+ transformIndexHtml: {
123
+ order: "pre",
124
+ handler(html, ctx) {
125
+ const inherited = callInheritedHtml(baseTransformIndexHtml, html, ctx);
126
+ if (!enabled() || !available || !wire) return inherited;
127
+ return {
128
+ html: typeof inherited === "string" ? inherited : html,
129
+ tags: [
130
+ {
131
+ tag: "script",
132
+ // First in the head, so the entry cannot create a node before the agent runs.
133
+ injectTo: "head-prepend",
134
+ attrs: { type: "module", src: `/@id/${VIRTUAL}` }
135
+ }
136
+ ]
137
+ };
138
+ }
139
+ }
140
+ };
141
+ }
142
+ function callInheritedHtml(hook, html, ctx) {
143
+ if (typeof hook === "function") return hook(html, ctx);
144
+ const handler = hook?.handler;
145
+ return handler?.(html, ctx);
146
+ }
147
+ export {
148
+ withGraphChannel
149
+ };