fortiplugin-bundle-adapter 0.0.1

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 ADDED
@@ -0,0 +1,405 @@
1
+ # FortiPlugin Bundle Adapter
2
+
3
+ A **build-time bundle adapter** for the FortiPlugin system.
4
+
5
+ It transforms your plugin’s compiled entry chunk(s) into a **runtime factory** that receives host-provided dependencies (React, JSX runtime, Inertia, and host UI bundles like `@host/ui`).
6
+
7
+ This lets the host:
8
+
9
+ * enforce **one React/Inertia instance** across all plugins
10
+ * inject host components (`@host/ui`, `@host/icons`, etc.)
11
+ * avoid bundling/duplicating framework libs inside plugins
12
+ * keep plugin bundles **portable** and **sandbox-friendly**
13
+
14
+ ---
15
+
16
+ ## What it outputs
17
+
18
+ Your plugin entry ends up like this (conceptually):
19
+
20
+ ```ts
21
+ export default function factory(deps) {
22
+ // deps.imports["react"], deps.imports["@host/ui"], ...
23
+ // plugin module code (rewritten)
24
+ return DefaultExport;
25
+ }
26
+ ```
27
+
28
+ The host loads the bundle and calls the factory with the dependency map.
29
+
30
+ ---
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ npm i -D fortiplugin-bundle-adapter
36
+ # or
37
+ pnpm add -D fortiplugin-bundle-adapter
38
+ # or
39
+ yarn add -D fortiplugin-bundle-adapter
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Usage (Vite)
45
+
46
+ In your plugin project:
47
+
48
+ ```ts
49
+ // vite.config.ts
50
+ import { defineConfig } from "vite";
51
+ import fortiPrep from "fortiplugin-bundle-adapter";
52
+
53
+ export default defineConfig({
54
+ plugins: [
55
+ fortiPrep({
56
+ injectedIds: ["react", "react/jsx-runtime"],
57
+ injectedPrefixes: ["@inertiajs/", "@host/"],
58
+ runtimeKey: "imports",
59
+ depsParam: "deps",
60
+ }),
61
+ ],
62
+ });
63
+ ```
64
+
65
+ ### Why Rollup external matters
66
+
67
+ This adapter marks injected imports as **external** so that:
68
+
69
+ * your build doesn’t try to resolve `@host/ui` locally
70
+ * those imports survive long enough for the transform to detect/remove them
71
+
72
+ ---
73
+
74
+ ## Runtime contract (host side)
75
+
76
+ ### 1) Plugins import from “virtual host modules”
77
+
78
+ Plugin code can import from host-defined IDs (examples):
79
+
80
+ ```ts
81
+ import React, { useMemo } from "react";
82
+ import { jsx, Fragment } from "react/jsx-runtime";
83
+
84
+ import { router } from "@inertiajs/core";
85
+
86
+ import HostUI, { Button } from "@host/ui";
87
+ import * as Icons from "@host/icons";
88
+ ```
89
+
90
+ ### 2) Host injects the exports map
91
+
92
+ The host must provide the module exports for every injected import ID.
93
+
94
+ ```ts
95
+ import React from "react";
96
+ import * as JsxRuntime from "react/jsx-runtime";
97
+ import * as InertiaCore from "@inertiajs/core";
98
+
99
+ import * as HostUI from "./host-ui";
100
+ import * as HostIcons from "./host-icons";
101
+
102
+ const imports = {
103
+ "react": React,
104
+ "react/jsx-runtime": JsxRuntime,
105
+ "@inertiajs/core": InertiaCore,
106
+ "@host/ui": HostUI,
107
+ "@host/icons": HostIcons,
108
+ };
109
+
110
+ // pluginModule is the loaded plugin bundle (ESM/CJS)
111
+ const factory = pluginModule.default;
112
+ const pluginDefaultExport = factory({ imports });
113
+ ```
114
+
115
+ ### Default export interop
116
+
117
+ The adapter normalizes default imports so both of these work:
118
+
119
+ * ESM namespace objects (`{ default, named... }`)
120
+ * CommonJS-like values (no `.default`)
121
+
122
+ Meaning: if a plugin writes `import HostUI from "@host/ui"`, the host may inject either:
123
+
124
+ * `{ default: { ... } }` or
125
+ * `{ ... }` directly
126
+
127
+ and the plugin still gets the “default” value.
128
+
129
+ ---
130
+
131
+ ## Configuration
132
+
133
+ ### `injectedIds?: string[]`
134
+
135
+ Exact import IDs to inject.
136
+
137
+ Example:
138
+
139
+ ```ts
140
+ injectedIds: ["react", "react/jsx-runtime", "@host/ui"]
141
+ ```
142
+
143
+ ### `injectedPrefixes?: string[]`
144
+
145
+ Prefixes to inject.
146
+
147
+ Example:
148
+
149
+ ```ts
150
+ injectedPrefixes: ["@inertiajs/", "@host/"]
151
+ ```
152
+
153
+ ### `runtimeKey?: string` (default: `"imports"`)
154
+
155
+ Where the import map is stored.
156
+
157
+ The wrapper accepts either:
158
+
159
+ * `factory({ imports: map })` (recommended)
160
+ * `factory(map)` (shortcut)
161
+
162
+ If you set `runtimeKey: "bundle"`, then the recommended call becomes:
163
+
164
+ ```ts
165
+ factory({ bundle: imports })
166
+ ```
167
+
168
+ ### `depsParam?: string` (default: `"deps"`)
169
+
170
+ The wrapper function param name.
171
+
172
+ ---
173
+
174
+ ## Host UI bundles (`@host/*`)
175
+
176
+ This adapter enables a clean pattern:
177
+
178
+ 1. Host defines a virtual module like `@host/ui`
179
+ 2. Host exports UI components from a real file
180
+ 3. Host injects it into `imports["@host/ui"]`
181
+
182
+ ### Example host UI module
183
+
184
+ ```ts
185
+ // host-ui.ts
186
+ export { Button } from "./ui/Button";
187
+ export { Modal } from "./ui/Modal";
188
+ export { Badge } from "./ui/Badge";
189
+ ```
190
+
191
+ Then inject:
192
+
193
+ ```ts
194
+ import * as HostUI from "./host-ui";
195
+
196
+ factory({
197
+ imports: {
198
+ "@host/ui": HostUI,
199
+ "react": React,
200
+ "react/jsx-runtime": JsxRuntime,
201
+ },
202
+ });
203
+ ```
204
+
205
+ ---
206
+
207
+ ## Local development with host bundles (CORS-safe URLs)
208
+
209
+ When you import from virtual host modules like `@host/ui`, your plugin can compile fine (because the adapter treats them as external), but **during development** you still need a way to *actually render/test* those host components.
210
+
211
+ The recommended approach is:
212
+
213
+ * The **host** publishes read-only **ESM bundle URLs** for the modules it wants plugins to use (UI, icons, theme, inertia helpers, etc.).
214
+ * The host config/manifest provides a mapping from virtual IDs to URLs.
215
+ * The plugin developer’s local dev harness dynamically imports those URLs and passes them into the plugin factory as `imports`.
216
+
217
+ ### 1) Host provides a “dev exports map”
218
+
219
+ Example (shape only — you can store this wherever FortiPlugin keeps policy/handshake data):
220
+
221
+ ```json
222
+ {
223
+ "@host/ui": "https://host.example.com/forti/dev/exports/ui.mjs",
224
+ "@host/icons": "https://host.example.com/forti/dev/exports/icons.mjs",
225
+ "@host/inertia": "https://host.example.com/forti/dev/exports/inertia.mjs"
226
+ }
227
+ ```
228
+
229
+ ### 2) Host serves those bundles with permissive CORS
230
+
231
+ At minimum, the host should allow cross-origin **GET** requests for these assets.
232
+
233
+ Recommended response headers for the dev export endpoints:
234
+
235
+ * `Content-Type: text/javascript; charset=utf-8`
236
+ * `Access-Control-Allow-Origin: *`
237
+ * `Access-Control-Allow-Methods: GET`
238
+
239
+ Notes:
240
+
241
+ * Keep these endpoints **read-only**.
242
+ * Export only the public plugin-facing surface (avoid internal auth/config).
243
+ * Ideally enable this in **dev/staging** environments only.
244
+
245
+ ### 3) Plugin dev harness loads host bundles by URL
246
+
247
+ The cleanest dev experience is to keep plugin source code unchanged:
248
+
249
+ ```ts
250
+ import { Button } from "@host/ui";
251
+ ```
252
+
253
+ …and load the real host module at runtime in your dev harness:
254
+
255
+ ```ts
256
+ // dev-harness.ts (runs in the browser or a dev page)
257
+ import React from "react";
258
+ import * as JsxRuntime from "react/jsx-runtime";
259
+
260
+ // pluginModule is your built/served plugin entry bundle
261
+ import pluginModule from "/path/to/plugin-entry.mjs";
262
+
263
+ const hostExports = {
264
+ "@host/ui": "https://host.example.com/forti/dev/exports/ui.mjs",
265
+ "@host/icons": "https://host.example.com/forti/dev/exports/icons.mjs",
266
+ };
267
+
268
+ async function loadImportMap() {
269
+ const imports = {
270
+ "react": React,
271
+ "react/jsx-runtime": JsxRuntime,
272
+ };
273
+
274
+ for (const [id, url] of Object.entries(hostExports)) {
275
+ // Vite note: @vite-ignore prevents Vite from trying to pre-bundle the URL
276
+ const mod = await import(/* @vite-ignore */ url);
277
+ imports[id] = mod;
278
+ }
279
+
280
+ return imports;
281
+ }
282
+
283
+ const factory = pluginModule.default;
284
+ const pluginDefaultExport = factory({ imports: await loadImportMap() });
285
+ ```
286
+
287
+ This gives plugin developers real host components during testing without needing those modules locally.
288
+
289
+ ### 4) Optional: make local imports resolve to URLs (advanced)
290
+
291
+ If you want `@host/ui` to resolve in the browser *as a URL module* during dev, you can add a small Vite dev-only resolver plugin that rewrites `@host/*` imports to the host URLs. This is optional; the harness approach above is simpler and avoids bundler edge cases.
292
+
293
+ ---
294
+
295
+ ## Testing (transform-only)
296
+
297
+ If you want to test the Babel transform without Vite:
298
+
299
+ ```txt
300
+ tests/
301
+ fixture-input.js
302
+ run-transform.mjs
303
+ ```
304
+
305
+ ### Export the transform (recommended)
306
+
307
+ Expose the transform from your package entry so tests can import it from `dist/index.mjs`:
308
+
309
+ ```ts
310
+ // src/index.ts
311
+ export { default } from "./vite/prep";
312
+ export { default as fortiPrepTransform } from "./babel/transform";
313
+ ```
314
+
315
+ ### `tests/run-transform.mjs`
316
+
317
+ ```js
318
+ import { readFileSync, writeFileSync } from "node:fs";
319
+ import { resolve } from "node:path";
320
+ import { transformSync } from "@babel/core";
321
+
322
+ import { fortiPrepTransform } from "../dist/index.mjs";
323
+
324
+ const inputFile = resolve(process.cwd(), process.argv[2] ?? "tests/fixture-input.js");
325
+ const outFile = resolve(process.cwd(), process.argv[3] ?? "tests/fixture-output.js");
326
+
327
+ const input = readFileSync(inputFile, "utf-8");
328
+
329
+ const result = transformSync(input, {
330
+ filename: inputFile,
331
+ sourceType: "module",
332
+ plugins: [
333
+ [
334
+ fortiPrepTransform,
335
+ {
336
+ injectedIds: ["react", "react/jsx-runtime"],
337
+ injectedPrefixes: ["@inertiajs/", "@host/"],
338
+ runtimeKey: "imports",
339
+ depsParam: "deps",
340
+ },
341
+ ],
342
+ ],
343
+ generatorOpts: {
344
+ compact: false,
345
+ comments: true,
346
+ retainLines: false,
347
+ },
348
+ });
349
+
350
+ if (!result?.code) throw new Error("No output produced");
351
+ writeFileSync(outFile, result.code, "utf-8");
352
+ console.log("✅ wrote", outFile);
353
+ ```
354
+
355
+ Run:
356
+
357
+ ```bash
358
+ node tests/run-transform.mjs
359
+ ```
360
+
361
+ ---
362
+
363
+ ## Limitations / gotchas
364
+
365
+ ### 1) Named exports are preserved as-is
366
+
367
+ The current behavior keeps `export const x = ...` / `export { x }` statements and appends them after the wrapper.
368
+
369
+ If those exports reference symbols that were moved into the wrapper scope, they can break.
370
+
371
+ **Recommended convention:** plugin entry files should primarily export **default**.
372
+
373
+ If you want stricter enforcement (“default export only”), you can add it.
374
+
375
+ ### 2) Dynamic imports are not rewritten
376
+
377
+ `import("@host/ui")` is not handled.
378
+
379
+ If you need this, add a pass for `Import()` expressions.
380
+
381
+ ### 3) Side-effect-only injected imports
382
+
383
+ `import "@host/ui";` becomes a no-op. Model side effects as explicit exports instead.
384
+
385
+ ---
386
+
387
+ ## FAQ
388
+
389
+ ### Why not bundle React inside each plugin?
390
+
391
+ Because the host needs a single, controlled instance for consistency, security policy, and to avoid multiple React copies.
392
+
393
+ ### Do I need to install `@host/ui` in plugin projects?
394
+
395
+ No. It’s treated as external and injected at runtime.
396
+
397
+ ### Can I inject other libraries too?
398
+
399
+ Yes. Add them to `injectedIds` or `injectedPrefixes`, and inject them in the host `imports` map.
400
+
401
+ ---
402
+
403
+ ## License
404
+
405
+ MIT (or your chosen license)