@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 +21 -0
- package/README.md +84 -0
- package/dist/graph-channel.cjs +196 -0
- package/dist/graph-channel.d.ts +7 -0
- package/dist/graph-channel.d.ts.map +1 -0
- package/dist/graph-channel.js +243 -0
- package/dist/graph-channel.mjs +164 -0
- package/dist/index.cjs +285 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.mjs +254 -0
- package/dist/share.cjs +77 -0
- package/dist/share.d.ts +46 -0
- package/dist/share.d.ts.map +1 -0
- package/dist/share.js +77 -0
- package/dist/share.mjs +56 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +64 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/*! @fluixi/vite-plugin v1.0.0-alpha.100 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { createVitePlugin } from "@fluixi/compiler/integrations";
|
|
5
|
+
|
|
6
|
+
// src/graph-channel.ts
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
var VIRTUAL = "virtual:fluixi/devtools-agent";
|
|
12
|
+
var RESOLVED = `\0${VIRTUAL}`;
|
|
13
|
+
var ENDPOINT = "/__fluixi/graph";
|
|
14
|
+
var EVENT = "fluixi:graph";
|
|
15
|
+
var MISSING = "the plugin could not load @fluixi/devtools from its own dependencies";
|
|
16
|
+
var WAITING = "no page has reported yet — open the application in a browser";
|
|
17
|
+
var INTERVAL = 100;
|
|
18
|
+
var HISTORY = 500;
|
|
19
|
+
var agentSource = (devtools, plugins, signal, reactive) => `
|
|
20
|
+
import { installDevtoolsHook, observeGraph } from ${JSON.stringify(devtools)};
|
|
21
|
+
import ${JSON.stringify(plugins)};
|
|
22
|
+
import { observeReactiveNodes } from ${JSON.stringify(signal)};
|
|
23
|
+
import { VERSION } from ${JSON.stringify(reactive)};
|
|
24
|
+
|
|
25
|
+
// The runtime this module imports is the application's own, same module graph, same nodes.
|
|
26
|
+
// Nothing waits for the runtime to offer itself; that path is for a tool loaded from outside.
|
|
27
|
+
//
|
|
28
|
+
// \`watch\` is this copy of devtools, handed over with the runtime. A browser extension
|
|
29
|
+
// installs a hook at document_start and so owns it; without this the graph would be built
|
|
30
|
+
// by that extension's build rather than the one shipped alongside this application.
|
|
31
|
+
const hook = installDevtoolsHook();
|
|
32
|
+
hook.inject({ observeReactiveNodes, version: VERSION, watch: observeGraph });
|
|
33
|
+
|
|
34
|
+
const send = (kind, snapshot) => import.meta.hot?.send(${JSON.stringify(EVENT)}, { kind, snapshot });
|
|
35
|
+
|
|
36
|
+
const first = hook.snapshot();
|
|
37
|
+
// Kept and handed back, so a browser panel polling the same hook cannot consume what this
|
|
38
|
+
// one has not been told about yet.
|
|
39
|
+
let cursor = first?.cursor;
|
|
40
|
+
send('full', first);
|
|
41
|
+
const timer = setInterval(() => {
|
|
42
|
+
const delta = hook.snapshot({ partial: true, since: cursor });
|
|
43
|
+
if (!delta) return;
|
|
44
|
+
cursor = delta.cursor;
|
|
45
|
+
// Events count on their own: an effect that ran without changing a value moves no node.
|
|
46
|
+
if (delta.nodes.length || delta.removed?.length || delta.events?.length) send('delta', delta);
|
|
47
|
+
}, ${INTERVAL});
|
|
48
|
+
|
|
49
|
+
import.meta.hot?.dispose(() => clearInterval(timer));
|
|
50
|
+
`;
|
|
51
|
+
function ownDevtools() {
|
|
52
|
+
return resolveForBrowser("@fluixi/devtools");
|
|
53
|
+
}
|
|
54
|
+
function ownPlugins() {
|
|
55
|
+
return resolveForBrowser("@fluixi/devtools/plugins");
|
|
56
|
+
}
|
|
57
|
+
function resolveForBrowser(spec) {
|
|
58
|
+
try {
|
|
59
|
+
const resolved = import.meta.resolve?.(spec);
|
|
60
|
+
if (resolved) return fileURLToPath(resolved);
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const required = createRequire(import.meta.url).resolve(spec);
|
|
65
|
+
if (!required.endsWith(".cjs")) return required;
|
|
66
|
+
for (const ext of [".mjs", ".js"]) {
|
|
67
|
+
const sibling = required.slice(0, -".cjs".length) + ext;
|
|
68
|
+
if (existsSync(sibling)) return sibling;
|
|
69
|
+
}
|
|
70
|
+
return required;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function withGraphChannel(base, enabled, start) {
|
|
76
|
+
let wire = null;
|
|
77
|
+
let available = false;
|
|
78
|
+
let reason = MISSING;
|
|
79
|
+
let root = process.cwd();
|
|
80
|
+
let log;
|
|
81
|
+
let graph = /* @__PURE__ */ new Map();
|
|
82
|
+
let stamp = 0;
|
|
83
|
+
let events = [];
|
|
84
|
+
const baseResolveId = base.resolveId;
|
|
85
|
+
const baseTransformIndexHtml = base.transformIndexHtml;
|
|
86
|
+
const baseLoad = base.load;
|
|
87
|
+
const baseConfigureServer = base.configureServer;
|
|
88
|
+
return {
|
|
89
|
+
...base,
|
|
90
|
+
resolveId(id, importer) {
|
|
91
|
+
if (id === VIRTUAL) return RESOLVED;
|
|
92
|
+
return baseResolveId?.call(this, id, importer) ?? null;
|
|
93
|
+
},
|
|
94
|
+
async load(id) {
|
|
95
|
+
if (id !== RESOLVED) return await baseLoad?.call(this, id) ?? null;
|
|
96
|
+
const from = join(root, "fluixi-devtools-agent.js");
|
|
97
|
+
const specs = ["@fluixi/reactive/signal", "@fluixi/reactive"];
|
|
98
|
+
const ids = await Promise.all(specs.map((spec) => this.resolve?.(spec, from, { skipSelf: true })));
|
|
99
|
+
const missing = specs.filter((_, i) => !ids[i]?.id);
|
|
100
|
+
const devtools = ownDevtools();
|
|
101
|
+
const plugins = ownPlugins();
|
|
102
|
+
if (missing.length || !devtools || !plugins) {
|
|
103
|
+
reason = missing.length ? `${missing.join(", ")} did not resolve from ${root}` : MISSING;
|
|
104
|
+
log?.(`[fluixi] reactive graph off — ${reason}`);
|
|
105
|
+
return "export {};";
|
|
106
|
+
}
|
|
107
|
+
return agentSource(`/@fs/${devtools}`, `/@fs/${plugins}`, ids[0].id, ids[1].id);
|
|
108
|
+
},
|
|
109
|
+
async configureServer(server) {
|
|
110
|
+
await baseConfigureServer?.call(this, server);
|
|
111
|
+
if (!enabled()) return;
|
|
112
|
+
root = server.config?.root ?? root;
|
|
113
|
+
log = server.config?.logger?.info.bind(server.config.logger);
|
|
114
|
+
wire = await import("@fluixi/devtools/wire").catch(() => null);
|
|
115
|
+
available = wire !== null;
|
|
116
|
+
if (wire) start();
|
|
117
|
+
if (wire) reason = WAITING;
|
|
118
|
+
else {
|
|
119
|
+
log?.(`[fluixi] reactive graph off — ${MISSING}`);
|
|
120
|
+
}
|
|
121
|
+
server.middlewares.use(ENDPOINT, (_req, res) => {
|
|
122
|
+
const snapshot = wire ? { ...wire.serializeGraph(graph, stamp || Date.now()), events } : { t: Date.now(), nodes: [] };
|
|
123
|
+
const body = JSON.stringify(reason ? { ...snapshot, reason } : snapshot);
|
|
124
|
+
res.setHeader("Content-Type", "application/json");
|
|
125
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
126
|
+
res.setHeader("Cache-Control", "no-store");
|
|
127
|
+
res.end(body);
|
|
128
|
+
});
|
|
129
|
+
if (!wire) return;
|
|
130
|
+
server.ws.on(EVENT, (payload) => {
|
|
131
|
+
const message = payload;
|
|
132
|
+
if (!message?.snapshot || !wire) return;
|
|
133
|
+
const full = message.kind === "full";
|
|
134
|
+
graph = wire.applySnapshot(full ? /* @__PURE__ */ new Map() : graph, message.snapshot);
|
|
135
|
+
if (full) events = [];
|
|
136
|
+
const arriving = message.snapshot.events;
|
|
137
|
+
if (arriving?.length) events = [...events, ...arriving].slice(-HISTORY);
|
|
138
|
+
stamp = Date.now();
|
|
139
|
+
reason = null;
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
transformIndexHtml: {
|
|
143
|
+
order: "pre",
|
|
144
|
+
handler(html, ctx) {
|
|
145
|
+
const inherited = callInheritedHtml(baseTransformIndexHtml, html, ctx);
|
|
146
|
+
if (!enabled() || !available || !wire) return inherited;
|
|
147
|
+
return {
|
|
148
|
+
html: typeof inherited === "string" ? inherited : html,
|
|
149
|
+
tags: [
|
|
150
|
+
{
|
|
151
|
+
tag: "script",
|
|
152
|
+
// First in the head, so the entry cannot create a node before the agent runs.
|
|
153
|
+
injectTo: "head-prepend",
|
|
154
|
+
attrs: { type: "module", src: `/@id/${VIRTUAL}` }
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function callInheritedHtml(hook, html, ctx) {
|
|
163
|
+
if (typeof hook === "function") return hook(html, ctx);
|
|
164
|
+
const handler = hook?.handler;
|
|
165
|
+
return handler?.(html, ctx);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/share.ts
|
|
169
|
+
var PREFIX = "share/";
|
|
170
|
+
var chunkName = (id) => PREFIX + id.replace(/^@/, "").replace(/[^a-zA-Z0-9]+/g, "-");
|
|
171
|
+
function share(options) {
|
|
172
|
+
const modules = [...new Set(options.modules)];
|
|
173
|
+
const names = new Map(modules.map((id) => [chunkName(id), id]));
|
|
174
|
+
if (names.size !== modules.length) {
|
|
175
|
+
throw new Error("[fluixi] share: two modules map to the same chunk name; list each id once.");
|
|
176
|
+
}
|
|
177
|
+
let base = "/";
|
|
178
|
+
let browser = true;
|
|
179
|
+
const importMap = (bundle) => {
|
|
180
|
+
const imports = {};
|
|
181
|
+
for (const file of Object.values(bundle)) {
|
|
182
|
+
if (file.type !== "chunk" || !file.name) continue;
|
|
183
|
+
const id = names.get(file.name);
|
|
184
|
+
if (id) imports[id] = base + file.fileName;
|
|
185
|
+
}
|
|
186
|
+
const missing = modules.filter((id) => !imports[id]);
|
|
187
|
+
if (missing.length) throw new Error(`[fluixi] share: no chunk was written for ${missing.join(", ")}.`);
|
|
188
|
+
return { imports };
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
name: "fluixi:share",
|
|
192
|
+
apply: "build",
|
|
193
|
+
configResolved(config) {
|
|
194
|
+
browser = !config.build.ssr;
|
|
195
|
+
base = config.base.startsWith("/") ? config.base : "/";
|
|
196
|
+
},
|
|
197
|
+
buildStart() {
|
|
198
|
+
if (!browser) return;
|
|
199
|
+
for (const id of modules) {
|
|
200
|
+
this.emitFile({ type: "chunk", id, name: chunkName(id), preserveSignature: "strict" });
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
generateBundle(_, bundle) {
|
|
204
|
+
if (!browser) return;
|
|
205
|
+
this.emitFile({
|
|
206
|
+
type: "asset",
|
|
207
|
+
fileName: options.manifest ?? "share.json",
|
|
208
|
+
source: `${JSON.stringify(importMap(bundle), null, 2)}
|
|
209
|
+
`
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
transformIndexHtml: {
|
|
213
|
+
order: "post",
|
|
214
|
+
handler(_html, ctx) {
|
|
215
|
+
if (!browser || !ctx.bundle) return;
|
|
216
|
+
return [{ tag: "script", attrs: { type: "importmap" }, children: JSON.stringify(importMap(ctx.bundle)), injectTo: "head-prepend" }];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/index.ts
|
|
223
|
+
function fluixi(options = {}) {
|
|
224
|
+
const settings = options;
|
|
225
|
+
let serving = false;
|
|
226
|
+
const enabled = () => serving && settings.devtools !== false;
|
|
227
|
+
const base = createVitePlugin(settings);
|
|
228
|
+
const inherited = base.configResolved;
|
|
229
|
+
const start = () => {
|
|
230
|
+
settings.sourceLocations = true;
|
|
231
|
+
settings.sourceLocationsExternal = typeof settings.devtools === "object" && settings.devtools.external === true;
|
|
232
|
+
};
|
|
233
|
+
return withGraphChannel(
|
|
234
|
+
{
|
|
235
|
+
...base,
|
|
236
|
+
// Awaited: the compiler's own hook loads the transform it needs for component styles, and
|
|
237
|
+
// Vite starts transforming as soon as every configResolved has settled.
|
|
238
|
+
async configResolved(config) {
|
|
239
|
+
await inherited?.(config);
|
|
240
|
+
serving = config?.command === "serve";
|
|
241
|
+
settings.sourceRoot = config.root;
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
enabled,
|
|
245
|
+
start
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
var index_default = fluixi;
|
|
249
|
+
export {
|
|
250
|
+
createVitePlugin,
|
|
251
|
+
index_default as default,
|
|
252
|
+
fluixi,
|
|
253
|
+
share
|
|
254
|
+
};
|
package/dist/share.cjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/share.ts
|
|
21
|
+
var share_exports = {};
|
|
22
|
+
__export(share_exports, {
|
|
23
|
+
share: () => share
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(share_exports);
|
|
26
|
+
var PREFIX = "share/";
|
|
27
|
+
var chunkName = (id) => PREFIX + id.replace(/^@/, "").replace(/[^a-zA-Z0-9]+/g, "-");
|
|
28
|
+
function share(options) {
|
|
29
|
+
const modules = [...new Set(options.modules)];
|
|
30
|
+
const names = new Map(modules.map((id) => [chunkName(id), id]));
|
|
31
|
+
if (names.size !== modules.length) {
|
|
32
|
+
throw new Error("[fluixi] share: two modules map to the same chunk name; list each id once.");
|
|
33
|
+
}
|
|
34
|
+
let base = "/";
|
|
35
|
+
let browser = true;
|
|
36
|
+
const importMap = (bundle) => {
|
|
37
|
+
const imports = {};
|
|
38
|
+
for (const file of Object.values(bundle)) {
|
|
39
|
+
if (file.type !== "chunk" || !file.name) continue;
|
|
40
|
+
const id = names.get(file.name);
|
|
41
|
+
if (id) imports[id] = base + file.fileName;
|
|
42
|
+
}
|
|
43
|
+
const missing = modules.filter((id) => !imports[id]);
|
|
44
|
+
if (missing.length) throw new Error(`[fluixi] share: no chunk was written for ${missing.join(", ")}.`);
|
|
45
|
+
return { imports };
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
name: "fluixi:share",
|
|
49
|
+
apply: "build",
|
|
50
|
+
configResolved(config) {
|
|
51
|
+
browser = !config.build.ssr;
|
|
52
|
+
base = config.base.startsWith("/") ? config.base : "/";
|
|
53
|
+
},
|
|
54
|
+
buildStart() {
|
|
55
|
+
if (!browser) return;
|
|
56
|
+
for (const id of modules) {
|
|
57
|
+
this.emitFile({ type: "chunk", id, name: chunkName(id), preserveSignature: "strict" });
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
generateBundle(_, bundle) {
|
|
61
|
+
if (!browser) return;
|
|
62
|
+
this.emitFile({
|
|
63
|
+
type: "asset",
|
|
64
|
+
fileName: options.manifest ?? "share.json",
|
|
65
|
+
source: `${JSON.stringify(importMap(bundle), null, 2)}
|
|
66
|
+
`
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
transformIndexHtml: {
|
|
70
|
+
order: "post",
|
|
71
|
+
handler(_html, ctx) {
|
|
72
|
+
if (!browser || !ctx.bundle) return;
|
|
73
|
+
return [{ tag: "script", attrs: { type: "importmap" }, children: JSON.stringify(importMap(ctx.bundle)), injectTo: "head-prepend" }];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
package/dist/share.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modules an app shares with code it loads at run time: plugins installed into a deployment,
|
|
3
|
+
* not built with the app. Such code imports `@fluixi/core` by name and must get the app's
|
|
4
|
+
* copy, or it drives a second reactive runtime, a second DI container and a second element
|
|
5
|
+
* registry.
|
|
6
|
+
*
|
|
7
|
+
* In a browser build each shared module becomes its own chunk, keeping every export, and an
|
|
8
|
+
* import map names it: written as `share.json` next to the assets, and placed first in the
|
|
9
|
+
* `<head>` of an `index.html` the build processes. A server build is left alone; there the
|
|
10
|
+
* modules must stay external so loaded code resolves them from `node_modules` (`@fluixi/start`
|
|
11
|
+
* does that from its own `share` option).
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import type { Plugin } from 'vite';
|
|
16
|
+
/** Options of {@link share}. */
|
|
17
|
+
export interface ShareOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Module ids to share, each as code will import it: `@fluixi/core`, `@fluixi/core/router`.
|
|
20
|
+
* A subpath is a module of its own and is listed on its own.
|
|
21
|
+
*/
|
|
22
|
+
modules: readonly string[];
|
|
23
|
+
/** File name of the import map written next to the assets. Default `share.json`. */
|
|
24
|
+
manifest?: string;
|
|
25
|
+
}
|
|
26
|
+
/** What `share.json` holds: an import map. */
|
|
27
|
+
export interface ShareManifest {
|
|
28
|
+
imports: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Share `modules` with code loaded at run time, through an import map.
|
|
32
|
+
*
|
|
33
|
+
* Shared modules are kept whole: their unused exports are not shaken out, since the app cannot
|
|
34
|
+
* know which ones a plugin will use.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* import { fluixi, share } from '@fluixi/vite-plugin';
|
|
39
|
+
*
|
|
40
|
+
* export default defineConfig({
|
|
41
|
+
* plugins: [fluixi(), share({ modules: ['@fluixi/core', '@fluixi/reactive/signal'] })],
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function share(options: ShareOptions): Plugin;
|
|
46
|
+
//# sourceMappingURL=share.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"share.d.ts","sourceRoot":"","sources":["../src/share.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,gCAAgC;AAChC,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,8CAA8C;AAC9C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAOD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAsDnD"}
|
package/dist/share.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const PREFIX = 'share/';
|
|
2
|
+
/** `@buntubi/sdk/plugins` becomes `buntubi-sdk-plugins`: a chunk name, unique per id. */
|
|
3
|
+
const chunkName = (id) => PREFIX + id.replace(/^@/, '').replace(/[^a-zA-Z0-9]+/g, '-');
|
|
4
|
+
/**
|
|
5
|
+
* Share `modules` with code loaded at run time, through an import map.
|
|
6
|
+
*
|
|
7
|
+
* Shared modules are kept whole: their unused exports are not shaken out, since the app cannot
|
|
8
|
+
* know which ones a plugin will use.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { fluixi, share } from '@fluixi/vite-plugin';
|
|
13
|
+
*
|
|
14
|
+
* export default defineConfig({
|
|
15
|
+
* plugins: [fluixi(), share({ modules: ['@fluixi/core', '@fluixi/reactive/signal'] })],
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function share(options) {
|
|
20
|
+
const modules = [...new Set(options.modules)];
|
|
21
|
+
const names = new Map(modules.map((id) => [chunkName(id), id]));
|
|
22
|
+
if (names.size !== modules.length) {
|
|
23
|
+
throw new Error('[fluixi] share: two modules map to the same chunk name; list each id once.');
|
|
24
|
+
}
|
|
25
|
+
let base = '/';
|
|
26
|
+
let browser = true;
|
|
27
|
+
/** The import map, read from the chunks this build wrote. */
|
|
28
|
+
const importMap = (bundle) => {
|
|
29
|
+
const imports = {};
|
|
30
|
+
for (const file of Object.values(bundle)) {
|
|
31
|
+
if (file.type !== 'chunk' || !file.name)
|
|
32
|
+
continue;
|
|
33
|
+
const id = names.get(file.name);
|
|
34
|
+
if (id)
|
|
35
|
+
imports[id] = base + file.fileName;
|
|
36
|
+
}
|
|
37
|
+
const missing = modules.filter((id) => !imports[id]);
|
|
38
|
+
if (missing.length)
|
|
39
|
+
throw new Error(`[fluixi] share: no chunk was written for ${missing.join(', ')}.`);
|
|
40
|
+
return { imports };
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
name: 'fluixi:share',
|
|
44
|
+
apply: 'build',
|
|
45
|
+
configResolved(config) {
|
|
46
|
+
browser = !config.build.ssr;
|
|
47
|
+
// A relative base cannot address a chunk from an import map, which resolves against the
|
|
48
|
+
// page: the deployment's absolute base is what it needs.
|
|
49
|
+
base = config.base.startsWith('/') ? config.base : '/';
|
|
50
|
+
},
|
|
51
|
+
buildStart() {
|
|
52
|
+
if (!browser)
|
|
53
|
+
return;
|
|
54
|
+
for (const id of modules) {
|
|
55
|
+
this.emitFile({ type: 'chunk', id, name: chunkName(id), preserveSignature: 'strict' });
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
generateBundle(_, bundle) {
|
|
59
|
+
if (!browser)
|
|
60
|
+
return;
|
|
61
|
+
this.emitFile({
|
|
62
|
+
type: 'asset',
|
|
63
|
+
fileName: options.manifest ?? 'share.json',
|
|
64
|
+
source: `${JSON.stringify(importMap(bundle), null, 2)}\n`,
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
transformIndexHtml: {
|
|
68
|
+
order: 'post',
|
|
69
|
+
handler(_html, ctx) {
|
|
70
|
+
if (!browser || !ctx.bundle)
|
|
71
|
+
return;
|
|
72
|
+
// First in <head>: a module script or preload before it would resolve without the map.
|
|
73
|
+
return [{ tag: 'script', attrs: { type: 'importmap' }, children: JSON.stringify(importMap(ctx.bundle)), injectTo: 'head-prepend' }];
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
package/dist/share.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/share.ts
|
|
2
|
+
var PREFIX = "share/";
|
|
3
|
+
var chunkName = (id) => PREFIX + id.replace(/^@/, "").replace(/[^a-zA-Z0-9]+/g, "-");
|
|
4
|
+
function share(options) {
|
|
5
|
+
const modules = [...new Set(options.modules)];
|
|
6
|
+
const names = new Map(modules.map((id) => [chunkName(id), id]));
|
|
7
|
+
if (names.size !== modules.length) {
|
|
8
|
+
throw new Error("[fluixi] share: two modules map to the same chunk name; list each id once.");
|
|
9
|
+
}
|
|
10
|
+
let base = "/";
|
|
11
|
+
let browser = true;
|
|
12
|
+
const importMap = (bundle) => {
|
|
13
|
+
const imports = {};
|
|
14
|
+
for (const file of Object.values(bundle)) {
|
|
15
|
+
if (file.type !== "chunk" || !file.name) continue;
|
|
16
|
+
const id = names.get(file.name);
|
|
17
|
+
if (id) imports[id] = base + file.fileName;
|
|
18
|
+
}
|
|
19
|
+
const missing = modules.filter((id) => !imports[id]);
|
|
20
|
+
if (missing.length) throw new Error(`[fluixi] share: no chunk was written for ${missing.join(", ")}.`);
|
|
21
|
+
return { imports };
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
name: "fluixi:share",
|
|
25
|
+
apply: "build",
|
|
26
|
+
configResolved(config) {
|
|
27
|
+
browser = !config.build.ssr;
|
|
28
|
+
base = config.base.startsWith("/") ? config.base : "/";
|
|
29
|
+
},
|
|
30
|
+
buildStart() {
|
|
31
|
+
if (!browser) return;
|
|
32
|
+
for (const id of modules) {
|
|
33
|
+
this.emitFile({ type: "chunk", id, name: chunkName(id), preserveSignature: "strict" });
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
generateBundle(_, bundle) {
|
|
37
|
+
if (!browser) return;
|
|
38
|
+
this.emitFile({
|
|
39
|
+
type: "asset",
|
|
40
|
+
fileName: options.manifest ?? "share.json",
|
|
41
|
+
source: `${JSON.stringify(importMap(bundle), null, 2)}
|
|
42
|
+
`
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
transformIndexHtml: {
|
|
46
|
+
order: "post",
|
|
47
|
+
handler(_html, ctx) {
|
|
48
|
+
if (!browser || !ctx.bundle) return;
|
|
49
|
+
return [{ tag: "script", attrs: { type: "importmap" }, children: JSON.stringify(importMap(ctx.bundle)), injectTo: "head-prepend" }];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
share
|
|
56
|
+
};
|