foundry-vtt-react 0.1.4 → 0.1.5

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
@@ -1,14 +1,12 @@
1
1
  # Foundry VTT React
2
2
 
3
- This package provides extensions of various Foundry VTT classes to support building React applications inside Foundry VTT.
4
-
5
- It is a **library**, consumed by module developers — not a Foundry module itself.
6
-
7
- This is a bit experimental, but enables a modern JS workflow and if you know and enjoy React workflows with HMR/fast-refresh, this should have you flying in no time!
3
+ This package provides a development and build harness for creating React applications within Foundry. It also includes extensions of various Foundry VTT classes to support building React applications inside Foundry VTT. This is a bit experimental, but enables a modern JS workflow and if you know and enjoy React workflows with HMR/fast-refresh, this should have you flying in no time.
8
4
 
9
5
  **Disclaimers:**
10
- - ⚠️ Only tested against Foundry V13 at this point, v14 compatability testing coming soon (wanna try it out for me?)
11
- - ℹ️ Would love input if you're a Vite pro -- I have a suspicion devSetup could be a plugin, haven't had time to look into it.
6
+ - Only tested against Foundry v13
7
+ - Not a Foundry module, but a build dependency to add to your module's build workflow and dev setup
8
+ - Relies on Vitejs building.
9
+ - TypeScript friendly!
12
10
 
13
11
  ## Installation
14
12
 
@@ -159,143 +157,96 @@ const off = contextConnector.onUpdate(handleUpdate);
159
157
 
160
158
  ## Development setup with Vite
161
159
 
162
- For a fast dev loop with React Fast Refresh (HMR) inside Foundry, you split your module into **two entry points** and let a Vite dev server serve them:
163
-
164
- - `src/main.ts` — your **real app** entry (registers hooks, sheets, etc.). This is what gets bundled for production.
165
- - `src/main.js` — a tiny **dev-only shim** that calls `devSetup`, which injects the React Refresh runtime and then loads `src/main.ts` over HMR.
166
-
167
- The trick that ties it together: your manifest's `esmodules` points at `dist/main.js`. In a production build that's the bundled app. In development, the Vite dev server is configured so that the same `/modules/<id>/dist/main.js` URL is served from `src/main.js` (the shim) — so **the manifest entry resolves to the dev shim while you develop, and to the real bundle once built.**
168
-
169
- ### Directory structure
160
+ For a fast dev loop with React Fast Refresh inside Foundry, add the `foundry-vtt-react/vite` plugin. Your manifest's `esmodules` points at `dist/main.js`: in production that's your built bundle, and in dev the plugin's middleware serves that same URL with the Fast Refresh preamble + a dynamic import of your real entry — so there's no shim file and no hand-written Vite config to maintain.
170
161
 
171
162
  ```text
172
163
  my-module/
173
- ├─ module.json # manifest → esmodules: ["dist/main.js"]
174
- ├─ package.json
164
+ ├─ module.json # esmodules: ["dist/main.js"]
175
165
  ├─ vite.config.ts
176
- ├─ src/
177
- ├─ main.js # dev-only shim: calls devSetup() (served at dist/main.js in dev)
178
- │ ├─ main.ts # real app entry: Hooks, registerSheet, … (build input)
179
- │ └─ MySheetApp.tsx # your React component(s)
180
- └─ dist/ # build output (gitignored); created by `vite build`
166
+ └─ src/
167
+ ├─ main.ts # real entry: Hooks, registerSheet, (build input)
168
+ └─ MySheetApp.tsx # your React component(s)
181
169
  ```
182
170
 
183
- ### Steps
171
+ **1. Add the plugin** — it derives everything from your `module.json` `id`:
184
172
 
185
- **1. Manifest — point** `esmodules` **at the built path.**
173
+ ```ts
174
+ // vite.config.ts
175
+ import { defineConfig } from "vite";
176
+ import react from "@vitejs/plugin-react";
177
+ import foundryReact from "foundry-vtt-react/vite";
186
178
 
187
- ```jsonc
188
- // module.json
189
- {
190
- "id": "my-module",
191
- "esmodules": ["dist/main.js"],
192
- "styles": ["dist/main.css"],
193
- "compatibility": { "minimum": "13", "verified": "13" }
194
- }
179
+ export default defineConfig({ plugins: [react(), foundryReact()] });
195
180
  ```
196
181
 
197
- **2. Real app entry (**`src/main.ts`**)** — your normal module bootstrap:
182
+ **2. Add scripts** — `"dev": "vite"` for the HMR server, `"build": "vite build"` for production.
198
183
 
199
- ```ts
200
- import MyActorSheet from "./MyActorSheet";
184
+ **3. Make the module visible to Foundry** — symlink (or copy) your module folder into Foundry's `Data/modules/`. With Docker, mount it instead:
201
185
 
202
- foundry.helpers.Hooks.once("ready", () => {
203
- foundry.documents.collections.Actors.registerSheet("my-module", MyActorSheet, {
204
- types: ["character"],
205
- makeDefault: true,
206
- });
207
- });
208
- ```
186
+ > ```yaml
187
+ > foundry:
188
+ > image: felddy/foundryvtt:13
189
+ > volumes:
190
+ > - /path/to/my-module:/data/Data/modules/my-module
191
+ > ```
209
192
 
210
- **3. Dev-only shim (**`src/main.js`**)** — calls `devSetup` with your module id and the path to the real entry:
193
+ **4. Run it** — start Foundry (`:30000`), then `npm run dev` and open the Vite server (`:30001`). Edits to your components hot-reload. For production, `npm run build` ships `dist/`; the dev middleware isn't involved.
211
194
 
212
- ```js
213
- import { id as APP_ID } from "../module.json";
214
- import { devSetup } from "foundry-vtt-react";
195
+ `vite` and `@vitejs/plugin-react` are **optional peer dependencies** — needed only for this plugin, not the runtime classes (you already have them for any React + Vite setup).
215
196
 
216
- // Loads @react-refresh + src/main.ts (served at dist/main.ts) with HMR.
217
- devSetup(APP_ID, "dist/main.ts");
218
- ```
197
+ ### Plugin options
198
+
199
+ All options are optional:
200
+
201
+ | Option | Default | Description |
202
+ | --------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------ |
203
+ | `appId` | the `id` read from `./module.json` | Your module's `id`. Used to build served paths and the proxy rule. |
204
+ | `entry` | `"src/main.ts"` | Your real app entry / build input, relative to the project root. |
205
+ | `foundryUrl` | `"http://localhost:30000"` | The local Foundry server that non-bundle requests are proxied to. |
206
+ | `port` | `30001` | The Vite dev server port. |
207
+ | `manifestEntry` | basename of `module.json` `esmodules[0]`, else `"main.js"` | The bundle filename Foundry requests (where the dev preamble is served). |
219
208
 
220
- **4. Vite config** serve `src/` at the manifest's `dist` path and proxy everything else to Foundry:
209
+ ### What it expands to
210
+
211
+ For a module whose `id` is `my-module`, `foundryReact()` contributes the Vite config you'd otherwise hand-write — each value applied only when you haven't set it yourself, so your own config always wins:
221
212
 
222
213
  ```ts
223
- // vite.config.ts
224
- import { defineConfig } from "vite";
225
- import react from "@vitejs/plugin-react";
226
- import path from "path";
227
- import { id as APP_ID } from "./module.json";
228
-
229
- export default defineConfig({
230
- plugins: [react()],
231
- root: "src/",
232
- // Serve src/ under the path the manifest references, so
233
- // /modules/<id>/dist/main.js resolves to src/main.js (the shim),
234
- // and /modules/<id>/dist/main.ts resolves to src/main.ts (the app).
235
- base: `/modules/${APP_ID}/dist`,
214
+ {
215
+ base: "/modules/my-module/dist",
216
+ root: "src",
217
+ // react/react-dom forced to a single copy — without this, a linked or git-installed
218
+ // foundry-vtt-react pulls a second React and every hook throws "Invalid hook call".
219
+ resolve: { dedupe: ["react", "react-dom"] },
236
220
  server: {
237
221
  port: 30001,
238
222
  proxy: {
239
- // Everything that isn't your dev bundle goes to the Foundry server.
240
- [`^(?!/modules/${APP_ID}/dist)`]: "http://localhost:30000/",
223
+ "^(?!/modules/my-module/dist)": "http://localhost:30000", // non-bundle requests Foundry
241
224
  "/socket.io": { target: "ws://localhost:30000", ws: true },
242
225
  },
243
226
  },
244
227
  build: {
245
- outDir: path.resolve(__dirname, "dist"),
228
+ outDir: "<root>/dist",
246
229
  emptyOutDir: true,
247
230
  rollupOptions: {
248
- input: path.resolve(__dirname, "src/main.ts"), // build the real entry
231
+ input: "<root>/src/main.ts", // your `entry`
249
232
  output: { entryFileNames: "[name].js", assetFileNames: "[name].[ext]", format: "es" },
250
233
  },
251
234
  },
252
- });
253
- ```
254
-
255
- **5. Scripts (**`package.json`**):**
256
-
257
- ```jsonc
258
- {
259
- "scripts": {
260
- "dev": "vite", // dev server with HMR
261
- "build": "vite build" // produces dist/main.js for production
262
- }
263
235
  }
264
236
  ```
265
237
 
266
- **6. Make the module visible to Foundry** symlink (or copy) your module folder into your Foundry data `Data/modules/` directory so Foundry can read `module.json`.
267
-
268
- > If you run foundry in docker like me, you can simply mount your local module volume into the docker foundry's modules folder like:
269
- >
270
- > ```
271
- > foundry:
272
- > image: felddy/foundryvtt:13
273
- > hostname: localhost
274
- > volumes:
275
- > - /path/to/my-module:/data/Data/modules/my-module
276
- > ```
277
-
278
- **7. Run it.** Start Foundry (port `30000`), then `npm run dev` and open the Vite server (e.g. `http://localhost:30001`). Foundry loads your module; the manifest's `dist/main.js` is served from `src/main.js`, `devSetup` boots React Refresh, and edits to your components hot-reload.
238
+ In dev it also serves the manifest URL (`/modules/my-module/dist/main.js`) with the module that replaces the old `src/main.js` shim:
279
239
 
280
- > For production, run `npm run build` and ship `dist/` — `dist/main.js` is now the bundled app, and `devSetup` is not involved.
281
-
282
- ### `devSetup` reference
283
-
284
- ```ts
285
- devSetup(appId: string, entrypoint: string): void
240
+ ```js
241
+ // Fast Refresh preamble (reused from @vitejs/plugin-react; base derived from your config)
242
+ import { injectIntoGlobalHook } from "/modules/my-module/dist/@react-refresh";
243
+ injectIntoGlobalHook(window);
244
+ window.$RefreshReg$ = () => {};
245
+ window.$RefreshSig$ = () => (type) => type;
246
+ import("/modules/my-module/dist/main.ts"); // dynamic, so the preamble runs first
286
247
  ```
287
248
 
288
-
289
- | Parameter | Description |
290
- | ------------ | ---------------------------------------------------------------------------------------------------------------------- |
291
- | `appId` | Your module's `id` (from `module.json`). Used to build the served paths and to namespace the injected `<script>` tags. |
292
- | `entrypoint` | Path to your real app entry, relative to the module root (e.g. `dist/main.ts`), as served by the Vite dev server. |
293
-
294
-
295
- It injects (idempotently — safe to call more than once):
296
-
297
- - A `<script type="module">` preamble that wires up the React Refresh global hooks, loading `@react-refresh` from `/modules/<appId>/dist/@react-refresh`.
298
- - A `<script type="module" src="/modules/<appId>/<entrypoint>">` that loads your app.
249
+ > **Migrating from `devSetup`:** `devSetup` is **deprecated**. Delete your `src/main.js` shim and the hand-written `base`/`server`/`build` config, then add `foundryReact()` — it reproduces the same behavior, deriving the preamble and paths from your resolved Vite config.
299
250
 
300
251
  ## Exports
301
252
 
@@ -40,4 +40,4 @@ function devSetup(appId, entrypoint) {
40
40
  export {
41
41
  devSetup
42
42
  };
43
- //# sourceMappingURL=chunk-U6WX7YSY.mjs.map
43
+ //# sourceMappingURL=chunk-7ZJUEWDX.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../lib/util/logger.ts","../lib/util/dev-setup.ts"],"sourcesContent":["const logger =\n (namespace: string): ((message: string) => void) =>\n (message: string) => {\n console.log(`%c[foundry-vtt-react][${namespace}]`, \"color: tomato;\", message);\n };\n\nexport default logger;\n","import logger from \"../util/logger.js\";\nconst log = logger(\"dev-setup\");\n\n/**\n * devSetup.js\n *\n * Utility function to set up development environment for React applications in Foundry VTT. Used in conjunction\n * with a Vite development server, this function injects necessary scripts to enable React Fast Refresh and loads\n * the React application entrypoint.\n *\n * @deprecated Use the `foundry-vtt-react/vite` plugin instead. It derives the refresh preamble and paths\n * from your Vite config (no hand-maintained paths) and serves the dev entry via dev-server middleware, so\n * the `src/main.js` shim that calls `devSetup` is no longer needed. This function will be removed in a future major.\n *\n * @param {string} appId - The application ID used to construct module paths, typically the `id` value in `module.json`.\n * @param {string} entrypoint - The path to the React application entrypoint script relative to the module root. This should match\n * the entrypoint defined in the Vite configuration.\n */\nexport function devSetup(appId: string, entrypoint: string) {\n const refreshScriptId = `foundry-react-refresh-script-${appId}`;\n\n if (document.getElementById(refreshScriptId)) {\n log(\"Script tag already exists, not adding again\");\n return;\n } else {\n log(\"Adding script tag for react refresh\");\n\n const scriptInner = `\n import { injectIntoGlobalHook } from \"/modules/${appId}/dist/@react-refresh\";\n injectIntoGlobalHook(window);\n window.$RefreshReg$ = () => {};\n window.$RefreshSig$ = () => (type) => type;\n `;\n\n const tag = document.createElement(\"script\");\n tag.type = \"module\";\n tag.id = refreshScriptId;\n tag.innerHTML = scriptInner;\n document.head.prepend(tag);\n }\n\n const devEntrypointId = `foundry-react-dev-entrypoint-${appId}`;\n if (document.getElementById(devEntrypointId)) {\n log(\"Dev entrypoint script tag already exists, not adding again\");\n return;\n } else {\n const mainScript = document.createElement(\"script\");\n mainScript.type = \"module\";\n mainScript.src = `/modules/${appId}${entrypoint.startsWith(\"/\") ? \"\" : \"/\"}${entrypoint}`;\n document.body.appendChild(mainScript);\n }\n}\n"],"mappings":";AAAA,IAAM,SACJ,CAAC,cACD,CAAC,YAAoB;AACnB,UAAQ,IAAI,yBAAyB,SAAS,KAAK,kBAAkB,OAAO;AAC9E;AAEF,IAAO,iBAAQ;;;ACLf,IAAM,MAAM,eAAO,WAAW;AAiBvB,SAAS,SAAS,OAAe,YAAoB;AAC1D,QAAM,kBAAkB,gCAAgC,KAAK;AAE7D,MAAI,SAAS,eAAe,eAAe,GAAG;AAC5C,QAAI,6CAA6C;AACjD;AAAA,EACF,OAAO;AACL,QAAI,qCAAqC;AAEzC,UAAM,cAAc;AAAA,uDAC+B,KAAK;AAAA;AAAA;AAAA;AAAA;AAMxD,UAAM,MAAM,SAAS,cAAc,QAAQ;AAC3C,QAAI,OAAO;AACX,QAAI,KAAK;AACT,QAAI,YAAY;AAChB,aAAS,KAAK,QAAQ,GAAG;AAAA,EAC3B;AAEA,QAAM,kBAAkB,gCAAgC,KAAK;AAC7D,MAAI,SAAS,eAAe,eAAe,GAAG;AAC5C,QAAI,4DAA4D;AAChE;AAAA,EACF,OAAO;AACL,UAAM,aAAa,SAAS,cAAc,QAAQ;AAClD,eAAW,OAAO;AAClB,eAAW,MAAM,YAAY,KAAK,GAAG,WAAW,WAAW,GAAG,IAAI,KAAK,GAAG,GAAG,UAAU;AACvF,aAAS,KAAK,YAAY,UAAU;AAAA,EACtC;AACF;","names":[]}
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  devSetup
3
- } from "./chunk-U6WX7YSY.mjs";
3
+ } from "./chunk-7ZJUEWDX.mjs";
4
4
 
5
5
  // lib/context-connector.ts
6
6
  var ContextConnector = class _ContextConnector extends EventTarget {
@@ -5,6 +5,10 @@
5
5
  * with a Vite development server, this function injects necessary scripts to enable React Fast Refresh and loads
6
6
  * the React application entrypoint.
7
7
  *
8
+ * @deprecated Use the `foundry-vtt-react/vite` plugin instead. It derives the refresh preamble and paths
9
+ * from your Vite config (no hand-maintained paths) and serves the dev entry via dev-server middleware, so
10
+ * the `src/main.js` shim that calls `devSetup` is no longer needed. This function will be removed in a future major.
11
+ *
8
12
  * @param {string} appId - The application ID used to construct module paths, typically the `id` value in `module.json`.
9
13
  * @param {string} entrypoint - The path to the React application entrypoint script relative to the module root. This should match
10
14
  * the entrypoint defined in the Vite configuration.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  devSetup
3
- } from "../chunk-U6WX7YSY.mjs";
3
+ } from "../chunk-7ZJUEWDX.mjs";
4
4
  export {
5
5
  devSetup
6
6
  };
@@ -0,0 +1,38 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ /** Options for the `foundry-vtt-react` Vite plugin. All optional; Foundry-friendly defaults fill in. */
4
+ interface FoundryReactOptions {
5
+ /** Module `id`. Defaults to the `id` in `./module.json`. */
6
+ appId?: string;
7
+ /** App entry / build input, relative to the project root. Default `"src/main.ts"`. */
8
+ entry?: string;
9
+ /** Foundry server to proxy non-bundle requests to. Default `"http://localhost:30000"`. */
10
+ foundryUrl?: string;
11
+ /** Dev server port. Default `30001`. */
12
+ port?: number;
13
+ /** Bundle filename Foundry requests. Defaults to the basename of `esmodules[0]`, else `"main.js"`. */
14
+ manifestEntry?: string;
15
+ }
16
+ /**
17
+ * Read the Fast Refresh preamble template from `@vitejs/plugin-react` (its source of truth).
18
+ * Lazy import, never static: a static one couples us to plugin-react's internal-API version.
19
+ * plugin-react is required for the dev server, so a missing dep throws naturally here. `??` covers
20
+ * both export shapes (named pre-v6, default-export property since).
21
+ */
22
+ declare function loadPreambleTemplate(): Promise<string>;
23
+ /**
24
+ * Vite plugin wiring a React module into a local Foundry VTT instance: owns the Foundry-specific
25
+ * config (`base`, `root`, `server.proxy`, `build`, react dedupe — only where you haven't set it)
26
+ * and serves a Fast Refresh preamble at the manifest URL so Foundry boots with HMR, no shim file.
27
+ *
28
+ * @example
29
+ * export default defineConfig({ plugins: [react(), foundryReact()] });
30
+ */
31
+ declare function foundryReact(options?: FoundryReactOptions): Plugin;
32
+ /**
33
+ * Preamble (base-substituted) + a **dynamic** import of the real entry. Dynamic is required: a
34
+ * static `import` hoists above `injectIntoGlobalHook(window)` and breaks Fast Refresh.
35
+ */
36
+ declare function buildDevModule(base: string, entryUrl: string, preambleTemplate: string): string;
37
+
38
+ export { type FoundryReactOptions, buildDevModule, foundryReact as default, loadPreambleTemplate };
@@ -0,0 +1,93 @@
1
+ // lib/vite/index.ts
2
+ import { readFileSync } from "fs";
3
+ import { basename, resolve } from "path";
4
+ var PLUGIN_NAME = "vite-plugin-foundry-react";
5
+ async function loadPreambleTemplate() {
6
+ const mod = await import("@vitejs/plugin-react");
7
+ return mod.default?.preambleCode ?? mod.preambleCode;
8
+ }
9
+ function readManifest(root) {
10
+ try {
11
+ return JSON.parse(readFileSync(resolve(root, "module.json"), "utf-8"));
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+ function foundryReact(options = {}) {
17
+ let base = "";
18
+ let entryUrl = "";
19
+ let manifestEntry = "";
20
+ let foundryUrl = "";
21
+ return {
22
+ name: PLUGIN_NAME,
23
+ config(userConfig) {
24
+ const root = userConfig.root ? resolve(process.cwd(), userConfig.root) : process.cwd();
25
+ const manifest = readManifest(process.cwd());
26
+ const appId = options.appId ?? manifest?.id;
27
+ if (!appId) {
28
+ throw new Error(
29
+ `[${PLUGIN_NAME}] Could not determine appId. Pass { appId } to the plugin, or run from a directory containing a module.json with an "id" field.`
30
+ );
31
+ }
32
+ const entry = options.entry ?? "src/main.ts";
33
+ foundryUrl = options.foundryUrl ?? "http://localhost:30000";
34
+ const port = options.port ?? 30001;
35
+ manifestEntry = options.manifestEntry ?? (manifest?.esmodules?.[0] ? basename(manifest.esmodules[0]) : "main.js");
36
+ base = `/modules/${appId}/dist`;
37
+ entryUrl = `${base}/${basename(entry)}`;
38
+ const proxy = {
39
+ [`^(?!${base})`]: foundryUrl,
40
+ // anything but the dev bundle → Foundry
41
+ "/socket.io": { target: foundryUrl.replace(/^http/, "ws"), ws: true }
42
+ };
43
+ const next = {};
44
+ if (userConfig.base == null) next.base = base;
45
+ if (userConfig.root == null) next.root = "src";
46
+ next.server = {
47
+ port: userConfig.server?.port ?? port,
48
+ proxy: { ...proxy, ...userConfig.server?.proxy }
49
+ };
50
+ next.build = {
51
+ outDir: userConfig.build?.outDir ?? resolve(root, "dist"),
52
+ emptyOutDir: userConfig.build?.emptyOutDir ?? true,
53
+ rollupOptions: {
54
+ input: userConfig.build?.rollupOptions?.input ?? resolve(root, entry),
55
+ output: userConfig.build?.rollupOptions?.output ?? {
56
+ entryFileNames: "[name].js",
57
+ assetFileNames: "[name].[ext]",
58
+ format: "es"
59
+ }
60
+ }
61
+ };
62
+ const dedupe = ["react", "react-dom"].filter((d) => !userConfig.resolve?.dedupe?.includes(d));
63
+ if (dedupe.length) next.resolve = { dedupe };
64
+ return next;
65
+ },
66
+ async configureServer(server) {
67
+ const manifestUrl = `${base}/${manifestEntry}`;
68
+ const preambleTemplate = await loadPreambleTemplate();
69
+ const body = buildDevModule(base, entryUrl, preambleTemplate);
70
+ server.middlewares.use((req, res, next) => {
71
+ const url = req.url?.split("?")[0];
72
+ if (url !== manifestUrl) return next();
73
+ res.setHeader("Content-Type", "text/javascript");
74
+ res.end(body);
75
+ });
76
+ server.config.logger.info(
77
+ ` ${PLUGIN_NAME} serving dev entry at ${manifestUrl} (proxying to ${foundryUrl})`
78
+ );
79
+ }
80
+ };
81
+ }
82
+ function buildDevModule(base, entryUrl, preambleTemplate) {
83
+ const baseWithSlash = base.endsWith("/") ? base : `${base}/`;
84
+ return `${preambleTemplate.replace("__BASE__", baseWithSlash)}
85
+ import(${JSON.stringify(entryUrl)});
86
+ `;
87
+ }
88
+ export {
89
+ buildDevModule,
90
+ foundryReact as default,
91
+ loadPreambleTemplate
92
+ };
93
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../lib/vite/index.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { basename, resolve } from \"node:path\";\nimport type { Plugin, ProxyOptions, UserConfig } from \"vite\";\n\n/** Options for the `foundry-vtt-react` Vite plugin. All optional; Foundry-friendly defaults fill in. */\nexport interface FoundryReactOptions {\n /** Module `id`. Defaults to the `id` in `./module.json`. */\n appId?: string;\n /** App entry / build input, relative to the project root. Default `\"src/main.ts\"`. */\n entry?: string;\n /** Foundry server to proxy non-bundle requests to. Default `\"http://localhost:30000\"`. */\n foundryUrl?: string;\n /** Dev server port. Default `30001`. */\n port?: number;\n /** Bundle filename Foundry requests. Defaults to the basename of `esmodules[0]`, else `\"main.js\"`. */\n manifestEntry?: string;\n}\n\nconst PLUGIN_NAME = \"vite-plugin-foundry-react\";\n\n/**\n * Read the Fast Refresh preamble template from `@vitejs/plugin-react` (its source of truth).\n * Lazy import, never static: a static one couples us to plugin-react's internal-API version.\n * plugin-react is required for the dev server, so a missing dep throws naturally here. `??` covers\n * both export shapes (named pre-v6, default-export property since).\n */\nexport async function loadPreambleTemplate(): Promise<string> {\n const mod: any = await import(\"@vitejs/plugin-react\");\n return mod.default?.preambleCode ?? mod.preambleCode;\n}\n\nfunction readManifest(root: string): { id?: string; esmodules?: string[] } | null {\n try {\n return JSON.parse(readFileSync(resolve(root, \"module.json\"), \"utf-8\"));\n } catch {\n return null;\n }\n}\n\n/**\n * Vite plugin wiring a React module into a local Foundry VTT instance: owns the Foundry-specific\n * config (`base`, `root`, `server.proxy`, `build`, react dedupe — only where you haven't set it)\n * and serves a Fast Refresh preamble at the manifest URL so Foundry boots with HMR, no shim file.\n *\n * @example\n * export default defineConfig({ plugins: [react(), foundryReact()] });\n */\nexport default function foundryReact(options: FoundryReactOptions = {}): Plugin {\n let base = \"\";\n let entryUrl = \"\";\n let manifestEntry = \"\";\n let foundryUrl = \"\";\n\n return {\n name: PLUGIN_NAME,\n\n config(userConfig): UserConfig {\n const root = userConfig.root ? resolve(process.cwd(), userConfig.root) : process.cwd();\n const manifest = readManifest(process.cwd());\n\n const appId = options.appId ?? manifest?.id;\n if (!appId) {\n throw new Error(\n `[${PLUGIN_NAME}] Could not determine appId. Pass { appId } to the plugin, ` +\n `or run from a directory containing a module.json with an \"id\" field.`,\n );\n }\n\n const entry = options.entry ?? \"src/main.ts\";\n foundryUrl = options.foundryUrl ?? \"http://localhost:30000\";\n const port = options.port ?? 30001;\n manifestEntry =\n options.manifestEntry ?? (manifest?.esmodules?.[0] ? basename(manifest.esmodules[0]) : \"main.js\");\n\n base = `/modules/${appId}/dist`;\n entryUrl = `${base}/${basename(entry)}`;\n\n const proxy: Record<string, string | ProxyOptions> = {\n [`^(?!${base})`]: foundryUrl, // anything but the dev bundle → Foundry\n \"/socket.io\": { target: foundryUrl.replace(/^http/, \"ws\"), ws: true },\n };\n\n // Only fill what the user hasn't set, so their config always wins.\n const next: UserConfig = {};\n if (userConfig.base == null) next.base = base;\n if (userConfig.root == null) next.root = \"src\";\n next.server = {\n port: userConfig.server?.port ?? port,\n proxy: { ...proxy, ...userConfig.server?.proxy },\n };\n next.build = {\n outDir: userConfig.build?.outDir ?? resolve(root, \"dist\"),\n emptyOutDir: userConfig.build?.emptyOutDir ?? true,\n rollupOptions: {\n input: userConfig.build?.rollupOptions?.input ?? resolve(root, entry),\n output: userConfig.build?.rollupOptions?.output ?? {\n entryFileNames: \"[name].js\",\n assetFileNames: \"[name].[ext]\",\n format: \"es\",\n },\n },\n };\n\n // Force a single react/react-dom — a linked/git-installed copy otherwise duplicates React\n // (\"Invalid hook call\"). Append only what the user hasn't (Vite concatenates dedupe lists).\n const dedupe = [\"react\", \"react-dom\"].filter((d) => !userConfig.resolve?.dedupe?.includes(d));\n if (dedupe.length) next.resolve = { dedupe };\n\n return next;\n },\n\n async configureServer(server) {\n const manifestUrl = `${base}/${manifestEntry}`;\n const preambleTemplate = await loadPreambleTemplate();\n const body = buildDevModule(base, entryUrl, preambleTemplate);\n\n // Serve the preamble + dynamic entry import at the manifest URL (replaces the dev shim file).\n server.middlewares.use((req, res, next) => {\n const url = req.url?.split(\"?\")[0];\n if (url !== manifestUrl) return next();\n\n res.setHeader(\"Content-Type\", \"text/javascript\");\n res.end(body);\n });\n\n server.config.logger.info(\n ` ${PLUGIN_NAME} serving dev entry at ${manifestUrl} (proxying to ${foundryUrl})`,\n );\n },\n };\n}\n\n/**\n * Preamble (base-substituted) + a **dynamic** import of the real entry. Dynamic is required: a\n * static `import` hoists above `injectIntoGlobalHook(window)` and breaks Fast Refresh.\n */\nexport function buildDevModule(base: string, entryUrl: string, preambleTemplate: string): string {\n // plugin-react's `__BASE__` expects Vite's normalized base (trailing slash).\n const baseWithSlash = base.endsWith(\"/\") ? base : `${base}/`;\n return `${preambleTemplate.replace(\"__BASE__\", baseWithSlash)}\\nimport(${JSON.stringify(entryUrl)});\\n`;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foundry-vtt-react",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Extensions of various FoundryVTT classes to support React applications.",
5
5
  "packageManager": "pnpm@10.30.3",
6
6
  "main": "dist/index.mjs",
@@ -13,6 +13,10 @@
13
13
  "./dev-setup": {
14
14
  "types": "./dist/util/dev-setup.d.mts",
15
15
  "import": "./dist/util/dev-setup.mjs"
16
+ },
17
+ "./vite": {
18
+ "types": "./dist/vite/index.d.mts",
19
+ "import": "./dist/vite/index.mjs"
16
20
  }
17
21
  },
18
22
  "files": [
@@ -21,6 +25,7 @@
21
25
  "scripts": {
22
26
  "build": "tsup",
23
27
  "build:watch": "tsup --watch",
28
+ "prepare": "tsup",
24
29
  "prepublishOnly": "pnpm build"
25
30
  },
26
31
  "keywords": [],
@@ -35,13 +40,25 @@
35
40
  "url": "https://github.com/tasandberg/foundry-vtt-react/issues"
36
41
  },
37
42
  "devDependencies": {
43
+ "@vitejs/plugin-react": "^6.0.2",
38
44
  "fvtt-types": "^13",
39
45
  "tsup": "^8.5.1",
40
- "typescript": "^5.9.3"
46
+ "typescript": "^5.9.3",
47
+ "vite": "^8.0.16"
41
48
  },
42
49
  "peerDependencies": {
50
+ "@vitejs/plugin-react": ">=4",
43
51
  "react": "^19.2.0",
44
- "react-dom": "^19.2.0"
52
+ "react-dom": "^19.2.0",
53
+ "vite": ">=5"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "@vitejs/plugin-react": {
57
+ "optional": true
58
+ },
59
+ "vite": {
60
+ "optional": true
61
+ }
45
62
  },
46
63
  "dependencies": {
47
64
  "@types/react": "^19.2.5",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../lib/util/logger.ts","../lib/util/dev-setup.ts"],"sourcesContent":["const logger =\n (namespace: string): ((message: string) => void) =>\n (message: string) => {\n console.log(`%c[foundry-vtt-react][${namespace}]`, \"color: tomato;\", message);\n };\n\nexport default logger;\n","import logger from \"../util/logger.js\";\nconst log = logger(\"dev-setup\");\n\n/**\n * devSetup.js\n *\n * Utility function to set up development environment for React applications in Foundry VTT. Used in conjunction\n * with a Vite development server, this function injects necessary scripts to enable React Fast Refresh and loads\n * the React application entrypoint.\n *\n * @param {string} appId - The application ID used to construct module paths, typically the `id` value in `module.json`.\n * @param {string} entrypoint - The path to the React application entrypoint script relative to the module root. This should match\n * the entrypoint defined in the Vite configuration.\n */\nexport function devSetup(appId: string, entrypoint: string) {\n const refreshScriptId = `foundry-react-refresh-script-${appId}`;\n\n if (document.getElementById(refreshScriptId)) {\n log(\"Script tag already exists, not adding again\");\n return;\n } else {\n log(\"Adding script tag for react refresh\");\n\n const scriptInner = `\n import { injectIntoGlobalHook } from \"/modules/${appId}/dist/@react-refresh\";\n injectIntoGlobalHook(window);\n window.$RefreshReg$ = () => {};\n window.$RefreshSig$ = () => (type) => type;\n `;\n\n const tag = document.createElement(\"script\");\n tag.type = \"module\";\n tag.id = refreshScriptId;\n tag.innerHTML = scriptInner;\n document.head.prepend(tag);\n }\n\n const devEntrypointId = `foundry-react-dev-entrypoint-${appId}`;\n if (document.getElementById(devEntrypointId)) {\n log(\"Dev entrypoint script tag already exists, not adding again\");\n return;\n } else {\n const mainScript = document.createElement(\"script\");\n mainScript.type = \"module\";\n mainScript.src = `/modules/${appId}${entrypoint.startsWith(\"/\") ? \"\" : \"/\"}${entrypoint}`;\n document.body.appendChild(mainScript);\n }\n}\n"],"mappings":";AAAA,IAAM,SACJ,CAAC,cACD,CAAC,YAAoB;AACnB,UAAQ,IAAI,yBAAyB,SAAS,KAAK,kBAAkB,OAAO;AAC9E;AAEF,IAAO,iBAAQ;;;ACLf,IAAM,MAAM,eAAO,WAAW;AAavB,SAAS,SAAS,OAAe,YAAoB;AAC1D,QAAM,kBAAkB,gCAAgC,KAAK;AAE7D,MAAI,SAAS,eAAe,eAAe,GAAG;AAC5C,QAAI,6CAA6C;AACjD;AAAA,EACF,OAAO;AACL,QAAI,qCAAqC;AAEzC,UAAM,cAAc;AAAA,uDAC+B,KAAK;AAAA;AAAA;AAAA;AAAA;AAMxD,UAAM,MAAM,SAAS,cAAc,QAAQ;AAC3C,QAAI,OAAO;AACX,QAAI,KAAK;AACT,QAAI,YAAY;AAChB,aAAS,KAAK,QAAQ,GAAG;AAAA,EAC3B;AAEA,QAAM,kBAAkB,gCAAgC,KAAK;AAC7D,MAAI,SAAS,eAAe,eAAe,GAAG;AAC5C,QAAI,4DAA4D;AAChE;AAAA,EACF,OAAO;AACL,UAAM,aAAa,SAAS,cAAc,QAAQ;AAClD,eAAW,OAAO;AAClB,eAAW,MAAM,YAAY,KAAK,GAAG,WAAW,WAAW,GAAG,IAAI,KAAK,GAAG,GAAG,UAAU;AACvF,aAAS,KAAK,YAAY,UAAU;AAAA,EACtC;AACF;","names":[]}