@fluixi/vite-plugin 1.0.0-alpha.100

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ibrahima Touré
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/fluixi/assets/main/logos/128x128.png" alt="Fluixi" width="120" height="120" />
3
+ </p>
4
+
5
+ # @fluixi/vite-plugin
6
+
7
+ **The Vite plugin for Fluixi — compiles JSX and `` html`` `` templates.**
8
+
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-22c55e.svg)](./LICENSE)
10
+ ![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript&logoColor=white)
11
+ [![npm](https://img.shields.io/npm/v/@fluixi/vite-plugin?logo=npm)](https://www.npmjs.com/package/@fluixi/vite-plugin)
12
+ ![Vite](https://img.shields.io/badge/built%20on-Vite-646cff?logo=vite&logoColor=white)
13
+
14
+ ---
15
+
16
+ ## ✨ Overview
17
+
18
+ `@fluixi/vite-plugin` sets the esbuild `jsxImportSource` and runs the Fluixi compile
19
+ (via [`@fluixi/compiler`](../compiler)) — JSX and `` html`...` `` both, straight into
20
+ `@fluixi/dom` runtime calls. It rewrites the source in place, so output carries a source
21
+ map and holes keep the text the author wrote. Nothing from babel is installed for it.
22
+ `@fluixi/core`
23
+ re-exports `fluixi`/`createVitePlugin` from here, and [`@fluixi/start`](../start) wires it
24
+ in automatically — use this plugin directly for a plain (non-meta-framework) Vite app.
25
+
26
+ ## 📦 Installation
27
+
28
+ ```sh
29
+ pnpm add -D @fluixi/vite-plugin
30
+ ```
31
+
32
+ ## 🚀 Usage
33
+
34
+ ```ts
35
+ import { defineConfig } from 'vite';
36
+ import { fluixi } from '@fluixi/vite-plugin';
37
+
38
+ export default defineConfig({
39
+ plugins: [
40
+ // JSX and html`` templates both compile straight into @fluixi/dom runtime
41
+ // calls (createNativeElement/spread/insert/…). No options required.
42
+ fluixi(),
43
+ ],
44
+ });
45
+ ```
46
+
47
+ ## ⚙️ Options
48
+
49
+ Options are [`@fluixi/compiler`](../compiler)'s `CompilerOptions`:
50
+
51
+ | Option | Default | What |
52
+ | --- | --- | --- |
53
+ | `importSource` | `@fluixi/jsx` | where the JSX *types* come from; also the `jsxImportSource` given to esbuild |
54
+ | `format` | `both` | which syntaxes to compile — `jsx`, `lit`, or `both` |
55
+ | `signalModule` / `controlFlowModule` | `@fluixi/dom` | where emitted runtime symbols come from |
56
+ | `resolve` | – | rules that supply a component import from a bare tag |
57
+ | `globals` | `fluixi.d.ts` | where to write the generated ambient types (the resolved components and the `$signal`-style intrinsics). `false` skips it |
58
+ | `propsDestructure` | `true` | rewrite a component's destructured props into property reads, so `function Card({ title })` keeps updating. A pattern it can't prove is left as written. Turn off for a package that ships its source |
59
+ | `templateClone` / `partialTemplates` | `true` | clone static subtrees instead of building them |
60
+ | `delegateEvents` | `true` | event delegation |
61
+ | `sourceMaps` | `true` | emit source maps |
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()`.
81
+
82
+ ## 📄 License
83
+
84
+ [MIT](./LICENSE) © [Fluixi](https://fluixi.dev)
@@ -0,0 +1,196 @@
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_fs = require("node:fs");
38
+ var import_node_path = require("node:path");
39
+ var import_node_url = require("node:url");
40
+ var import_meta = {};
41
+ var VIRTUAL = "virtual:fluixi/devtools-agent";
42
+ var RESOLVED = `\0${VIRTUAL}`;
43
+ var ENDPOINT = "/__fluixi/graph";
44
+ var EVENT = "fluixi:graph";
45
+ var MISSING = "the plugin could not load @fluixi/devtools from its own dependencies";
46
+ var WAITING = "no page has reported yet — open the application in a browser";
47
+ var INTERVAL = 100;
48
+ var HISTORY = 500;
49
+ var agentSource = (devtools, plugins, signal, reactive) => `
50
+ import { installDevtoolsHook, observeGraph } from ${JSON.stringify(devtools)};
51
+ import ${JSON.stringify(plugins)};
52
+ import { observeReactiveNodes } from ${JSON.stringify(signal)};
53
+ import { VERSION } from ${JSON.stringify(reactive)};
54
+
55
+ // The runtime this module imports is the application's own, same module graph, same nodes.
56
+ // Nothing waits for the runtime to offer itself; that path is for a tool loaded from outside.
57
+ //
58
+ // \`watch\` is this copy of devtools, handed over with the runtime. A browser extension
59
+ // installs a hook at document_start and so owns it; without this the graph would be built
60
+ // by that extension's build rather than the one shipped alongside this application.
61
+ const hook = installDevtoolsHook();
62
+ hook.inject({ observeReactiveNodes, version: VERSION, watch: observeGraph });
63
+
64
+ const send = (kind, snapshot) => import.meta.hot?.send(${JSON.stringify(EVENT)}, { kind, snapshot });
65
+
66
+ const first = hook.snapshot();
67
+ // Kept and handed back, so a browser panel polling the same hook cannot consume what this
68
+ // one has not been told about yet.
69
+ let cursor = first?.cursor;
70
+ send('full', first);
71
+ const timer = setInterval(() => {
72
+ const delta = hook.snapshot({ partial: true, since: cursor });
73
+ if (!delta) return;
74
+ cursor = delta.cursor;
75
+ // Events count on their own: an effect that ran without changing a value moves no node.
76
+ if (delta.nodes.length || delta.removed?.length || delta.events?.length) send('delta', delta);
77
+ }, ${INTERVAL});
78
+
79
+ import.meta.hot?.dispose(() => clearInterval(timer));
80
+ `;
81
+ function ownDevtools() {
82
+ return resolveForBrowser("@fluixi/devtools");
83
+ }
84
+ function ownPlugins() {
85
+ return resolveForBrowser("@fluixi/devtools/plugins");
86
+ }
87
+ function resolveForBrowser(spec) {
88
+ try {
89
+ const resolved = import_meta.resolve?.(spec);
90
+ if (resolved) return (0, import_node_url.fileURLToPath)(resolved);
91
+ } catch {
92
+ }
93
+ try {
94
+ const required = (0, import_node_module.createRequire)(import_meta.url).resolve(spec);
95
+ if (!required.endsWith(".cjs")) return required;
96
+ for (const ext of [".mjs", ".js"]) {
97
+ const sibling = required.slice(0, -".cjs".length) + ext;
98
+ if ((0, import_node_fs.existsSync)(sibling)) return sibling;
99
+ }
100
+ return required;
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+ function withGraphChannel(base, enabled, start) {
106
+ let wire = null;
107
+ let available = false;
108
+ let reason = MISSING;
109
+ let root = process.cwd();
110
+ let log;
111
+ let graph = /* @__PURE__ */ new Map();
112
+ let stamp = 0;
113
+ let events = [];
114
+ const baseResolveId = base.resolveId;
115
+ const baseTransformIndexHtml = base.transformIndexHtml;
116
+ const baseLoad = base.load;
117
+ const baseConfigureServer = base.configureServer;
118
+ return {
119
+ ...base,
120
+ resolveId(id, importer) {
121
+ if (id === VIRTUAL) return RESOLVED;
122
+ return baseResolveId?.call(this, id, importer) ?? null;
123
+ },
124
+ async load(id) {
125
+ if (id !== RESOLVED) return await baseLoad?.call(this, id) ?? null;
126
+ const from = (0, import_node_path.join)(root, "fluixi-devtools-agent.js");
127
+ const specs = ["@fluixi/reactive/signal", "@fluixi/reactive"];
128
+ const ids = await Promise.all(specs.map((spec) => this.resolve?.(spec, from, { skipSelf: true })));
129
+ const missing = specs.filter((_, i) => !ids[i]?.id);
130
+ const devtools = ownDevtools();
131
+ const plugins = ownPlugins();
132
+ if (missing.length || !devtools || !plugins) {
133
+ reason = missing.length ? `${missing.join(", ")} did not resolve from ${root}` : MISSING;
134
+ log?.(`[fluixi] reactive graph off — ${reason}`);
135
+ return "export {};";
136
+ }
137
+ return agentSource(`/@fs/${devtools}`, `/@fs/${plugins}`, ids[0].id, ids[1].id);
138
+ },
139
+ async configureServer(server) {
140
+ await baseConfigureServer?.call(this, server);
141
+ if (!enabled()) return;
142
+ root = server.config?.root ?? root;
143
+ log = server.config?.logger?.info.bind(server.config.logger);
144
+ wire = await import("@fluixi/devtools/wire").catch(() => null);
145
+ available = wire !== null;
146
+ if (wire) start();
147
+ if (wire) reason = WAITING;
148
+ else {
149
+ log?.(`[fluixi] reactive graph off — ${MISSING}`);
150
+ }
151
+ server.middlewares.use(ENDPOINT, (_req, res) => {
152
+ const snapshot = wire ? { ...wire.serializeGraph(graph, stamp || Date.now()), events } : { t: Date.now(), nodes: [] };
153
+ const body = JSON.stringify(reason ? { ...snapshot, reason } : snapshot);
154
+ res.setHeader("Content-Type", "application/json");
155
+ res.setHeader("Access-Control-Allow-Origin", "*");
156
+ res.setHeader("Cache-Control", "no-store");
157
+ res.end(body);
158
+ });
159
+ if (!wire) return;
160
+ server.ws.on(EVENT, (payload) => {
161
+ const message = payload;
162
+ if (!message?.snapshot || !wire) return;
163
+ const full = message.kind === "full";
164
+ graph = wire.applySnapshot(full ? /* @__PURE__ */ new Map() : graph, message.snapshot);
165
+ if (full) events = [];
166
+ const arriving = message.snapshot.events;
167
+ if (arriving?.length) events = [...events, ...arriving].slice(-HISTORY);
168
+ stamp = Date.now();
169
+ reason = null;
170
+ });
171
+ },
172
+ transformIndexHtml: {
173
+ order: "pre",
174
+ handler(html, ctx) {
175
+ const inherited = callInheritedHtml(baseTransformIndexHtml, html, ctx);
176
+ if (!enabled() || !available || !wire) return inherited;
177
+ return {
178
+ html: typeof inherited === "string" ? inherited : html,
179
+ tags: [
180
+ {
181
+ tag: "script",
182
+ // First in the head, so the entry cannot create a node before the agent runs.
183
+ injectTo: "head-prepend",
184
+ attrs: { type: "module", src: `/@id/${VIRTUAL}` }
185
+ }
186
+ ]
187
+ };
188
+ }
189
+ }
190
+ };
191
+ }
192
+ function callInheritedHtml(hook, html, ctx) {
193
+ if (typeof hook === "function") return hook(html, ctx);
194
+ const handler = hook?.handler;
195
+ return handler?.(html, ctx);
196
+ }
@@ -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":"AAiBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAqGnC;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,OAAO,EAAE,KAAK,EAAE,MAAM,IAAI,GAAG,MAAM,CA8HhG"}
@@ -0,0 +1,243 @@
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 { existsSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ const VIRTUAL = 'virtual:fluixi/devtools-agent';
19
+ const RESOLVED = `\0${VIRTUAL}`;
20
+ const ENDPOINT = '/__fluixi/graph';
21
+ const EVENT = 'fluixi:graph';
22
+ /** Why a project that runs the plugin might still have nothing to show. */
23
+ const MISSING = 'the plugin could not load @fluixi/devtools from its own dependencies';
24
+ const WAITING = 'no page has reported yet — open the application in a browser';
25
+ /** How often the agent looks for movement. Fast enough to watch, slow enough to ignore. */
26
+ const INTERVAL = 100;
27
+ /** A few hundred interactions' worth. */
28
+ const HISTORY = 500;
29
+ /** Written with the ids resolved: a virtual module has no directory to resolve a bare one from. */
30
+ const agentSource = (devtools, plugins, signal, reactive) => `
31
+ import { installDevtoolsHook, observeGraph } from ${JSON.stringify(devtools)};
32
+ import ${JSON.stringify(plugins)};
33
+ import { observeReactiveNodes } from ${JSON.stringify(signal)};
34
+ import { VERSION } from ${JSON.stringify(reactive)};
35
+
36
+ // The runtime this module imports is the application's own, same module graph, same nodes.
37
+ // Nothing waits for the runtime to offer itself; that path is for a tool loaded from outside.
38
+ //
39
+ // \`watch\` is this copy of devtools, handed over with the runtime. A browser extension
40
+ // installs a hook at document_start and so owns it; without this the graph would be built
41
+ // by that extension's build rather than the one shipped alongside this application.
42
+ const hook = installDevtoolsHook();
43
+ hook.inject({ observeReactiveNodes, version: VERSION, watch: observeGraph });
44
+
45
+ const send = (kind, snapshot) => import.meta.hot?.send(${JSON.stringify(EVENT)}, { kind, snapshot });
46
+
47
+ const first = hook.snapshot();
48
+ // Kept and handed back, so a browser panel polling the same hook cannot consume what this
49
+ // one has not been told about yet.
50
+ let cursor = first?.cursor;
51
+ send('full', first);
52
+ const timer = setInterval(() => {
53
+ const delta = hook.snapshot({ partial: true, since: cursor });
54
+ if (!delta) return;
55
+ cursor = delta.cursor;
56
+ // Events count on their own: an effect that ran without changing a value moves no node.
57
+ if (delta.nodes.length || delta.removed?.length || delta.events?.length) send('delta', delta);
58
+ }, ${INTERVAL});
59
+
60
+ import.meta.hot?.dispose(() => clearInterval(timer));
61
+ `;
62
+ /**
63
+ * The devtools bundle this plugin depends on, as a path the dev server can serve.
64
+ *
65
+ * Asking the application to install it as well made the graph depend on a devDependency
66
+ * nobody knew to add, and in a workspace it resolved for one project and not the one beside
67
+ * it: which reads as the feature being broken rather than absent.
68
+ */
69
+ function ownDevtools() {
70
+ return resolveForBrowser('@fluixi/devtools');
71
+ }
72
+ /** The built-in plugins, beside the bundle they extend. Collecting happens in the agent. */
73
+ function ownPlugins() {
74
+ return resolveForBrowser('@fluixi/devtools/plugins');
75
+ }
76
+ /**
77
+ * A package's ESM entry, by path.
78
+ *
79
+ * The agent below is served to a browser as an ES module and imports these by `/@fs/`
80
+ * path, so the file has to be the one with the named exports. `createRequire().resolve`
81
+ * answers with the `require` condition instead: the CJS build, and a `.cjs` file
82
+ * offers nothing to import, which surfaces as "does not provide an export named
83
+ * 'installDevtoolsHook'". It only started happening when devtools gained a CJS build;
84
+ * before that there was one file and either resolver found it.
85
+ */
86
+ function resolveForBrowser(spec) {
87
+ // Honours the `import` condition, which is the one that matches how this is loaded.
88
+ try {
89
+ const resolved = import.meta.resolve?.(spec);
90
+ if (resolved)
91
+ return fileURLToPath(resolved);
92
+ }
93
+ catch {
94
+ // Fall through to the require-based resolution below.
95
+ }
96
+ try {
97
+ const required = createRequire(import.meta.url).resolve(spec);
98
+ if (!required.endsWith('.cjs'))
99
+ return required;
100
+ // Landed on the CJS build. Prefer its ESM sibling when the package ships one.
101
+ for (const ext of ['.mjs', '.js']) {
102
+ const sibling = required.slice(0, -'.cjs'.length) + ext;
103
+ if (existsSync(sibling))
104
+ return sibling;
105
+ }
106
+ return required;
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ }
112
+ /**
113
+ * Add the graph channel to a plugin, keeping whatever hooks it already has. Composed rather
114
+ * than returned alongside so `fluixi()` stays one plugin.
115
+ */
116
+ export function withGraphChannel(base, enabled, start) {
117
+ let wire = null;
118
+ let available = false;
119
+ /** What to say when the graph is empty. Cleared once a page has reported. */
120
+ let reason = MISSING;
121
+ let root = process.cwd();
122
+ let log;
123
+ let graph = new Map();
124
+ let stamp = 0;
125
+ /**
126
+ * Kept, not drained: this endpoint has no single reader, and an editor panel draining it
127
+ * would blank a browser panel's timeline. Every event carries a seq, so consumers dedupe.
128
+ */
129
+ let events = [];
130
+ const baseResolveId = base.resolveId;
131
+ const baseTransformIndexHtml = base.transformIndexHtml;
132
+ // The compiler plugin serves component styles through these hooks: the channel's own
133
+ // handling comes on top, never instead.
134
+ const baseLoad = base.load;
135
+ const baseConfigureServer = base.configureServer;
136
+ return {
137
+ ...base,
138
+ resolveId(id, importer) {
139
+ if (id === VIRTUAL)
140
+ return RESOLVED;
141
+ return baseResolveId?.call(this, id, importer) ?? null;
142
+ },
143
+ async load(id) {
144
+ if (id !== RESOLVED)
145
+ return (await baseLoad?.call(this, id)) ?? null;
146
+ const from = join(root, 'fluixi-devtools-agent.js');
147
+ // Only the framework comes from the application: the hook hands over that runtime's
148
+ // own registration, and a second copy would report a graph nothing is using.
149
+ const specs = ['@fluixi/reactive/signal', '@fluixi/reactive'];
150
+ const ids = await Promise.all(specs.map((spec) => this.resolve?.(spec, from, { skipSelf: true })));
151
+ const missing = specs.filter((_, i) => !ids[i]?.id);
152
+ const devtools = ownDevtools();
153
+ const plugins = ownPlugins();
154
+ // Nothing to watch with is not an error worth breaking the page over.
155
+ if (missing.length || !devtools || !plugins) {
156
+ reason = missing.length
157
+ ? `${missing.join(', ')} did not resolve from ${root}`
158
+ : MISSING;
159
+ log?.(`[fluixi] reactive graph off — ${reason}`);
160
+ return 'export {};';
161
+ }
162
+ // Served by path: it is this package's copy, not one the application imports.
163
+ return agentSource(`/@fs/${devtools}`, `/@fs/${plugins}`, ids[0].id, ids[1].id);
164
+ },
165
+ async configureServer(server) {
166
+ await baseConfigureServer?.call(this, server);
167
+ if (!enabled())
168
+ return;
169
+ root = server.config?.root ?? root;
170
+ log = server.config?.logger?.info.bind(server.config.logger);
171
+ wire = await import('@fluixi/devtools/wire').catch(() => null);
172
+ available = wire !== null;
173
+ // Source marks cost something, so they wait until there is a server watching for them.
174
+ if (wire)
175
+ start();
176
+ if (wire)
177
+ reason = WAITING;
178
+ else {
179
+ // Said once, not per request: without the package there is no agent to inject and
180
+ // nothing to report, and that is a missing devDependency rather than a failure.
181
+ log?.(`[fluixi] reactive graph off — ${MISSING}`);
182
+ }
183
+ // The endpoint answers either way. A tool asking for the graph of a project that has
184
+ // not turned it on deserves to be told which of the two it is, and 404 says neither ,
185
+ // it reads the same as a dev server that is not a fluixi project at all.
186
+ server.middlewares.use(ENDPOINT, (_req, res) => {
187
+ const snapshot = wire
188
+ ? { ...wire.serializeGraph(graph, stamp || Date.now()), events }
189
+ : { t: Date.now(), nodes: [] };
190
+ const body = JSON.stringify(reason ? { ...snapshot, reason } : snapshot);
191
+ res.setHeader('Content-Type', 'application/json');
192
+ // Read by an editor extension and by panels served from another origin.
193
+ res.setHeader('Access-Control-Allow-Origin', '*');
194
+ res.setHeader('Cache-Control', 'no-store');
195
+ res.end(body);
196
+ });
197
+ if (!wire)
198
+ return;
199
+ server.ws.on(EVENT, (payload) => {
200
+ const message = payload;
201
+ if (!message?.snapshot || !wire)
202
+ return;
203
+ // A reload sends a full snapshot, which replaces whatever the last page left behind.
204
+ const full = message.kind === 'full';
205
+ graph = wire.applySnapshot(full ? new Map() : graph, message.snapshot);
206
+ // The history goes with it: its ids name nodes the previous page had.
207
+ if (full)
208
+ events = [];
209
+ const arriving = message.snapshot.events;
210
+ if (arriving?.length)
211
+ events = [...events, ...arriving].slice(-HISTORY);
212
+ stamp = Date.now();
213
+ reason = null;
214
+ });
215
+ },
216
+ transformIndexHtml: {
217
+ order: 'pre',
218
+ handler(html, ctx) {
219
+ const inherited = callInheritedHtml(baseTransformIndexHtml, html, ctx);
220
+ if (!enabled() || !available || !wire)
221
+ return inherited;
222
+ return {
223
+ html: typeof inherited === 'string' ? inherited : html,
224
+ tags: [
225
+ {
226
+ tag: 'script',
227
+ // First in the head, so the entry cannot create a node before the agent runs.
228
+ injectTo: 'head-prepend',
229
+ attrs: { type: 'module', src: `/@id/${VIRTUAL}` },
230
+ },
231
+ ],
232
+ };
233
+ },
234
+ },
235
+ };
236
+ }
237
+ /** A plugin may declare transformIndexHtml as a function or as an object with a handler. */
238
+ function callInheritedHtml(hook, html, ctx) {
239
+ if (typeof hook === 'function')
240
+ return hook(html, ctx);
241
+ const handler = hook?.handler;
242
+ return handler?.(html, ctx);
243
+ }