@moostjs/vite 0.6.34 → 0.6.36

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
@@ -3,6 +3,7 @@
3
3
  Vite dev plugin for [Moost](https://moost.org). Enables hot module replacement for Moost HTTP applications during development, automatic adapter detection, and production build configuration.
4
4
 
5
5
  Supports two modes:
6
+
6
7
  - **Backend mode** (default) — Moost owns the server, Vite provides HMR and TypeScript transforms
7
8
  - **Middleware mode** — Vite serves the frontend (Vue, React, etc.), Moost handles API routes as middleware
8
9
 
@@ -217,6 +218,10 @@ ssr: {
217
218
 
218
219
  When you use an explicit `noExternal` list, the plugin automatically keeps the **moost/wooks runtime** (`moost`, `@moostjs/*`, `@wooksjs/*`, `wooks`) bundled alongside your listed packages, so there is always a single runtime instance. This avoids a subtle production-only failure: if any `noExternal` package imports `@wooksjs/*` (e.g. `@aooth/*` and other `.as`-shipping libs do), it would otherwise pull in a second copy while externalized `moost` uses the first — splitting the event context so `useRequest()` / `useHeaders()` / `useAuthorization()` read `undefined`. If you would rather externalize the runtime instead, list it under `ssr.external` (e.g. `['@wooksjs/event-http', '@wooksjs/event-core', 'wooks', ...]`) and add those packages as direct dependencies — the plugin detects an externalized runtime and leaves your all-external setup intact.
219
220
 
221
+ The guard works in one direction only, and the runtime is not the only package that keeps module-level state — the `@atscript/*` family (`core`, `typescript`, `db` and its adapters, moost-db, the UI packages) relies on registries and class identity too. The mirror case is on you: a bundled shared package plus an external package that depends on it loads a second copy from `node_modules` and fails the same way — a lib calling wooks composables (`useRequest()`, `useHeaders()`, …) that wasn't in your `noExternal` list (or was put in `ssr.external`), or `@atscript/db-mysql` / `@atscript/db-sqlite` externalized while `@atscript/db` is bundled, where the adapter's `instanceof` check fails and it silently creates an empty table instead of the declared view. **A shared-state package is either entirely bundled or entirely external, together with everything that depends on it** — whatever `pnpm why @wooksjs/event-http` (or `pnpm why @atscript/db`) lists sits on the same side as the package itself. Never split.
222
+
223
+ After the SSR build the plugin checks for exactly that: it compares the packages inlined into `dist/server` with the bare imports left in it and warns when any externalized package (or one of its transitive dependencies) depends on a bundled watched package — `moost` / `@moostjs/*` / `@wooksjs/*` / `wooks` / `@atscript/*` by default — naming the package and the fix. `ssrExternalCheck: { packages: ['some-registry-lib', '@acme/', /^my-lib-/] }` watches more (exact name, scope prefix, `RegExp`); `ssrExternalCheck: false` silences it. The symptoms of a split are `TypeError: Cannot read properties of undefined (reading 'headers')` (or any header name) from inside a composable, and an empty table where a managed view was declared — production only. Source-level tests share one module graph and cannot see it: verify on the installed build. Full explanation and a manual verification recipe: [SSR Bundle Size](https://moost.org/webapp/vite#ssr-bundle-size).
224
+
220
225
  ## SSR Local Fetch
221
226
 
222
227
  When `ssrFetch` is enabled (default: `true`), the plugin patches `globalThis.fetch` so that local paths are routed in-process through Moost instead of making a real HTTP request. This is useful for SSR where server-side code fetches from its own API:
@@ -244,25 +249,26 @@ The plugin injects a `__VITE_ID` decorator on `@Injectable` and `@Controller` cl
244
249
 
245
250
  ## Options
246
251
 
247
- | Option | Type | Default | Description |
248
- |---|---|---|---|
249
- | `entry` | `string` | — | Application entry file (required) |
250
- | `port` | `number` | `3000` | Dev server port (backend mode only) |
251
- | `host` | `string` | `'localhost'` | Dev server host (backend mode only) |
252
- | `outDir` | `string` | `'dist'` | Build output directory (backend mode only) |
253
- | `format` | `'cjs' \| 'esm'` | `'esm'` | Output module format (backend mode only) |
254
- | `sourcemap` | `boolean` | `true` | Generate source maps (both modes; in middleware mode applies to the `dist/server` SSR build) |
255
- | `externals` | `boolean \| object` | `true` | Configure external dependencies (backend mode only) |
256
- | `onEject` | `function` | — | Hook to control DI instance ejection during HMR |
257
- | `ssrFetch` | `boolean` | `true` | Enable local fetch interception for SSR |
258
- | `middleware` | `boolean` | `false` | Run Moost as Connect middleware (Vite serves frontend) |
259
- | `prefix` | `string` | `'/api'` (prod server) | URL prefix filter for middleware mode (e.g. `'/api'`); the production server defaults to `'/api'` when omitted |
260
- | `ssrEntry` | `string` | — | Vue/React SSR entry module (e.g. `'/src/entry-server.ts'`) |
261
- | `ssrOutlet` | `string` | `'<!--ssr-outlet-->'` | HTML placeholder for SSR-rendered content |
262
- | `ssrState` | `string` | `'<!--ssr-state-->'` | HTML placeholder for SSR state transfer script |
263
- | `ssrHead` | `string` | `'<!--ssr-head-->'` | HTML placeholder for SSR-rendered `<head>` tags (place inside `<head>`) |
264
- | `serverEntry` | `string` | — | Custom production server entry file (e.g. `'./server.ts'`) |
265
- | `ssrExternal` | `string[]` | — | Packages to keep external in the SSR build (middleware mode, `vite build` only). Concatenated with `cfg.ssr.external`. See [SSR Bundle Size](#ssr-bundle-size). |
252
+ | Option | Type | Default | Description |
253
+ | ------------------ | ------------------------------------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
254
+ | `entry` | `string` | — | Application entry file (required) |
255
+ | `port` | `number` | `3000` | Dev server port (backend mode only) |
256
+ | `host` | `string` | `'localhost'` | Dev server host (backend mode only) |
257
+ | `outDir` | `string` | `'dist'` | Build output directory (backend mode only) |
258
+ | `format` | `'cjs' \| 'esm'` | `'esm'` | Output module format (backend mode only) |
259
+ | `sourcemap` | `boolean` | `true` | Generate source maps (both modes; in middleware mode applies to the `dist/server` SSR build) |
260
+ | `externals` | `boolean \| object` | `true` | Configure external dependencies (backend mode only) |
261
+ | `onEject` | `function` | — | Hook to control DI instance ejection during HMR |
262
+ | `ssrFetch` | `boolean` | `true` | Enable local fetch interception for SSR |
263
+ | `middleware` | `boolean` | `false` | Run Moost as Connect middleware (Vite serves frontend) |
264
+ | `prefix` | `string` | `'/api'` (prod server) | URL prefix filter for middleware mode (e.g. `'/api'`); the production server defaults to `'/api'` when omitted |
265
+ | `ssrEntry` | `string` | — | Vue/React SSR entry module (e.g. `'/src/entry-server.ts'`) |
266
+ | `ssrOutlet` | `string` | `'<!--ssr-outlet-->'` | HTML placeholder for SSR-rendered content |
267
+ | `ssrState` | `string` | `'<!--ssr-state-->'` | HTML placeholder for SSR state transfer script |
268
+ | `ssrHead` | `string` | `'<!--ssr-head-->'` | HTML placeholder for SSR-rendered `<head>` tags (place inside `<head>`) |
269
+ | `serverEntry` | `string` | — | Custom production server entry file (e.g. `'./server.ts'`) |
270
+ | `ssrExternal` | `string[]` | — | Packages to keep external in the SSR build (middleware mode, `vite build` only). Concatenated with `cfg.ssr.external`. See [SSR Bundle Size](#ssr-bundle-size). |
271
+ | `ssrExternalCheck` | `boolean \| { packages?: (string \| RegExp)[] }` | `true` | Warn after the middleware-mode SSR build when an externalized package depends on a bundled shared-state package (`moost`, `@moostjs/*`, `wooks`, `@wooksjs/*`, `@atscript/*`); `packages` watches more. See [SSR Bundle Size](#ssr-bundle-size). |
266
272
 
267
273
  Options marked "backend mode only" are ignored when `middleware: true` — the user's `vite.config.ts` controls build/server configuration in middleware mode.
268
274
 
package/dist/index.cjs CHANGED
@@ -289,14 +289,23 @@ function _define_property(obj, key, value) {
289
289
  /**
290
290
  * Clean up Moost’s global containers and optionally remove specific instances from the registry.
291
291
  *
292
+ * Every instance actually removed from a registry is **disposed** before the
293
+ * caches are dropped: its `@MoostDispose` hooks (or `Symbol.asyncDispose` /
294
+ * `Symbol.dispose`) are awaited, so a singleton owning a connection, consumer,
295
+ * timer or file handle releases it instead of leaking one copy per reload. An
296
+ * instance kept by an `onEject` veto is never disposed. A failing hook is
297
+ * warned about and the reload continues.
298
+ *
292
299
  * @param {Set<string>} [cleanupInstances] A set of module IDs to remove from the registry.
293
- */ function moostRestartCleanup(adapters, onEject, cleanupInstances) {
300
+ * @returns the instances that were ejected (and therefore disposed)
301
+ */ async function moostRestartCleanup(adapters, onEject, cleanupInstances) {
294
302
  const logger = getLogger();
295
303
  const infact = (0, moost.getMoostInfact)();
296
304
  const { registry, scopes } = infact;
297
305
  const registries = [registry, ...Object.values(scopes)];
298
306
  infact._cleanup();
299
307
  const mate = (0, moost.getMoostMate)();
308
+ /** Instances removed from a registry by this run — disposed below, before the caches drop. */ const ejected = [];
300
309
  if (cleanupInstances) {
301
310
  for (const reg of registries) {
302
311
  for (const key of Object.getOwnPropertySymbols(reg)) {
@@ -305,6 +314,7 @@ function _define_property(obj, key, value) {
305
314
  if (viteId && cleanupInstances.has(viteId)) {
306
315
  logger.debug(`🔃 Replacing "${constructorName(instance)}"`);
307
316
  delete reg[key];
317
+ ejected.push(instance);
308
318
  }
309
319
  }
310
320
  for (const key of Object.getOwnPropertySymbols(reg)) {
@@ -312,25 +322,39 @@ function _define_property(obj, key, value) {
312
322
  scanParams(instance, (type) => {
313
323
  if ((type === moost.Moost || type instanceof moost.Moost || type.prototype instanceof moost.Moost) && (!onEject || onEject(instance, type))) {
314
324
  delete reg[key];
325
+ ejected.push(instance);
315
326
  logger.debug(`✖️ Ejecting "${constructorName(instance)}" (depends on re-instantiated "Moost")`);
316
327
  return true;
317
328
  }
318
329
  for (const adapter of adapters) if (adapter.compare(type) && (!onEject || onEject(instance, type))) {
319
330
  delete reg[key];
331
+ ejected.push(instance);
320
332
  logger.debug(`✖️ Ejecting "${constructorName(instance)}" (depends on re-instantiated "${adapter.constructor.name}")`);
321
333
  return true;
322
334
  }
323
335
  });
324
336
  }
325
- clearDependantRegistry(reg, onEject);
337
+ clearDependantRegistry(reg, onEject, ejected);
326
338
  }
327
339
  infact.registry = registry;
328
340
  infact.scopes = scopes;
329
341
  }
342
+ await disposeEjected(ejected);
330
343
  (0, moost.getMoostMate)()._cleanup();
331
344
  (0, moost.clearGlobalWooks)();
345
+ return ejected;
346
+ }
347
+ /** Awaits the `@MoostDispose` hooks of the ejected instances; never throws. */ async function disposeEjected(ejected) {
348
+ if (ejected.length === 0) return;
349
+ const logger = getLogger();
350
+ const { disposed, errors } = await (0, moost.disposeInstances)(ejected, {
351
+ logger,
352
+ onError: "warn"
353
+ });
354
+ for (const instance of disposed) logger.debug(`♻️ Disposed "${constructorName(instance)}"`);
355
+ for (const e of errors) logger.warn(`⚠️ Dispose hook "${constructorName(e.instance)}.${String(e.method)}" failed — the reload continues`);
332
356
  }
333
- function clearDependantRegistry(registry, onEject) {
357
+ function clearDependantRegistry(registry, onEject, ejected) {
334
358
  const logger = getLogger();
335
359
  const objSet = /* @__PURE__ */ new Set();
336
360
  let somethingIsDeleted = true;
@@ -342,7 +366,10 @@ function clearDependantRegistry(registry, onEject) {
342
366
  }
343
367
  for (const key of Object.getOwnPropertySymbols(registry)) {
344
368
  const instance = registry[key];
345
- if (checkAndEject(instance, objSet, onEject, registry, key, logger)) somethingIsDeleted = true;
369
+ if (checkAndEject(instance, objSet, onEject, registry, key, logger)) {
370
+ ejected.push(instance);
371
+ somethingIsDeleted = true;
372
+ }
346
373
  }
347
374
  }
348
375
  }
@@ -375,6 +402,173 @@ function constructorName(i) {
375
402
  return Object.getPrototypeOf(i).constructor.name;
376
403
  }
377
404
 
405
+ //#endregion
406
+ //#region packages/vite/src/ssr-externals-check.ts
407
+ /**
408
+ * Packages that make up the moost/wooks runtime. They mint per-module Symbol
409
+ * slot keys, so the whole set must resolve to a single module instance in the
410
+ * production SSR graph. This is the scope of the single-instance *guard*
411
+ * (force-bundling under an explicit `noExternal` list) — the build *check*
412
+ * watches the wider {@link SHARED_STATE_PACKAGE_PATTERNS}.
413
+ */ const RUNTIME_PACKAGE_PATTERNS = [
414
+ /^moost($|\/)/,
415
+ /^@moostjs\//,
416
+ /^@wooksjs\//,
417
+ /^wooks($|\/)/
418
+ ];
419
+ /**
420
+ * Packages the build check watches by default: the moost/wooks runtime plus the
421
+ * `@atscript/*` family. All of them keep module-level state — Symbol slot keys,
422
+ * DI/model registries, class identity used by `instanceof` — so a second copy
423
+ * loaded by Node breaks lookups that the bundled copy filled in.
424
+ *
425
+ * This is deliberately wider than {@link RUNTIME_PACKAGE_PATTERNS}: the guard
426
+ * that force-bundles packages covers the runtime only, while the check merely
427
+ * reports what the output shows.
428
+ */ const SHARED_STATE_PACKAGE_PATTERNS = [...RUNTIME_PACKAGE_PATTERNS, /^@atscript\//];
429
+ function escapeRegExp(input) {
430
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
431
+ }
432
+ /**
433
+ * Build the watched-package patterns for the check: the defaults plus the
434
+ * user's extras. A `RegExp` is used as is, a string ending with `/` matches a
435
+ * scope/prefix (`'@acme/'` → every `@acme/*` package), any other string is an
436
+ * exact package name.
437
+ */ function compilePackagePatterns(extra) {
438
+ const patterns = [...SHARED_STATE_PACKAGE_PATTERNS];
439
+ for (const item of extra ?? []) if (item instanceof RegExp) patterns.push(item);
440
+ else if (item.endsWith("/")) patterns.push(new RegExp(`^${escapeRegExp(item)}`));
441
+ else patterns.push(new RegExp(`^${escapeRegExp(item)}$`));
442
+ return patterns;
443
+ }
444
+ const BUILTINS = new Set(node_module.builtinModules);
445
+ /** Bare npm package name of an import specifier (`vue/server-renderer` → `vue`). */ function npmPackageName(id) {
446
+ if (!id || id.startsWith(".") || id.startsWith("/") || id.startsWith("\0")) return;
447
+ if (id.startsWith("node:") || id.startsWith("data:") || id.startsWith("virtual:")) return;
448
+ const parts = id.split("/");
449
+ const name = id.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
450
+ if (!name || id.startsWith("@") && parts.length < 2) return;
451
+ return BUILTINS.has(name) ? void 0 : name;
452
+ }
453
+ /**
454
+ * Package names the bundler inlined into the output, derived from the module
455
+ * ids of the emitted chunks. Ids under `node_modules` name their package after
456
+ * the LAST `node_modules/` segment, which also yields the right name for pnpm
457
+ * store paths (`…/node_modules/.pnpm/pkg@1.0.0/node_modules/pkg/index.mjs`).
458
+ * Virtual ids and app sources (no `node_modules`) are ignored.
459
+ */ function bundledPackagesFromModuleIds(moduleIds) {
460
+ const packages = /* @__PURE__ */ new Set();
461
+ const marker = "/node_modules/";
462
+ for (const raw of moduleIds) {
463
+ if (!raw || raw.startsWith("\0")) continue;
464
+ const id = raw.replaceAll("\\", "/");
465
+ const at = id.lastIndexOf(marker);
466
+ if (at === -1) continue;
467
+ const name = npmPackageName(id.slice(at + 14));
468
+ if (name) packages.add(name);
469
+ }
470
+ return packages;
471
+ }
472
+ function readPkgJson(dir) {
473
+ try {
474
+ return JSON.parse((0, node_fs.readFileSync)((0, node_path.join)(dir, "package.json"), "utf8"));
475
+ } catch {
476
+ return;
477
+ }
478
+ }
479
+ /**
480
+ * Locate `name`'s package directory the way Node would from `fromDir`:
481
+ * `<fromDir>/node_modules/<name>`, then each ancestor's `node_modules`.
482
+ * Returns the realpath so pnpm's symlinked layout resolves to the store entry,
483
+ * whose own `node_modules` sibling holds the package's dependencies.
484
+ */ function findPackageDir(name, fromDir) {
485
+ let dir = fromDir;
486
+ for (;;) {
487
+ const candidate = (0, node_path.join)(dir, "node_modules", name);
488
+ if ((0, node_fs.existsSync)((0, node_path.join)(candidate, "package.json"))) try {
489
+ return (0, node_fs.realpathSync)(candidate);
490
+ } catch {
491
+ return candidate;
492
+ }
493
+ const parent = (0, node_path.dirname)(dir);
494
+ if (parent === dir) return;
495
+ dir = parent;
496
+ }
497
+ }
498
+ /** Deps of `pkg` that are watched packages AND were bundled into the output. */ function splitDepsOf(pkg, patterns, bundledPackages) {
499
+ const names = /* @__PURE__ */ new Set();
500
+ for (const group of [
501
+ pkg.dependencies,
502
+ pkg.peerDependencies,
503
+ pkg.optionalDependencies
504
+ ]) for (const dep of Object.keys(group ?? {})) if (bundledPackages.has(dep) && patterns.some((re) => re.test(dep))) names.add(dep);
505
+ return [...names];
506
+ }
507
+ /** Upper bound on visited packages — keeps a pathological tree from stalling the build. */ const MAX_VISITED = 5e3;
508
+ /**
509
+ * Given the bare specifiers left in the SSR build output (i.e. the packages the
510
+ * bundler externalized) and the package names it bundled, find every package
511
+ * Node will load at runtime that depends on a bundled shared-state package.
512
+ * Such a package resolves its own copy from `node_modules` while the bundled
513
+ * copy lives inside `dist/server` — two instances, split module state.
514
+ *
515
+ * Walks the dependency tree beneath each external (Node loads a package's deps
516
+ * from its own resolution root, so transitive consumers matter too), but never
517
+ * descends into a bundled dep: the fix belongs to the external consumer.
518
+ * Packages that are not installed are skipped silently.
519
+ */ function findSplitPackages(opts) {
520
+ const patterns = opts.patterns ?? SHARED_STATE_PACKAGE_PATTERNS;
521
+ const found = [];
522
+ const visited = /* @__PURE__ */ new Set();
523
+ const queue = [];
524
+ for (const id of opts.externalIds) {
525
+ const name = npmPackageName(id);
526
+ if (name && !visited.has(`root:${name}`)) {
527
+ visited.add(`root:${name}`);
528
+ queue.push({
529
+ name,
530
+ fromDir: opts.root,
531
+ via: []
532
+ });
533
+ }
534
+ }
535
+ while (queue.length > 0 && visited.size < MAX_VISITED) {
536
+ const { name, fromDir, via } = queue.shift();
537
+ const dir = findPackageDir(name, fromDir);
538
+ if (!dir || visited.has(dir)) continue;
539
+ visited.add(dir);
540
+ const pkg = readPkgJson(dir);
541
+ if (!pkg) continue;
542
+ const splitDeps = splitDepsOf(pkg, patterns, opts.bundledPackages);
543
+ if (splitDeps.length > 0) found.push({
544
+ name,
545
+ via,
546
+ splitDeps
547
+ });
548
+ for (const dep of Object.keys(pkg.dependencies ?? {})) if (!opts.bundledPackages.has(dep)) queue.push({
549
+ name: dep,
550
+ fromDir: dir,
551
+ via: [...via, name]
552
+ });
553
+ }
554
+ return found;
555
+ }
556
+ /** Human-readable build warning for {@link findSplitPackages} results. */ function formatSplitPackagesWarning(splits) {
557
+ const lines = splits.map((s) => {
558
+ const chain = s.via.length > 0 ? ` (loaded via external ${s.via.join(" → ")})` : "";
559
+ const deps = s.splitDeps.map((d) => `${d} (bundled)`).join(", ");
560
+ return ` - ${s.name}${chain} depends on ${deps}`;
561
+ });
562
+ const direct = [...new Set(splits.map((s) => s.via.length > 0 ? s.via[0] : s.name))];
563
+ return [
564
+ "These externalized packages depend on packages that are bundled into dist/server, so Node will load a second copy of them from node_modules:",
565
+ ...lines,
566
+ "Two copies of a package split its module state — event-context slots, DI registries and class identity (instanceof) stop matching across the boundary, in production only. Symptoms: \"Cannot read properties of undefined (reading 'headers')\" inside a wooks composable, or a database adapter creating an empty table where a managed view was declared.",
567
+ `Fix: keep each shared package and everything that depends on it on the same side — add ${direct.map((d) => `'${d}'`).join(", ")} to ssr.noExternal (or drop them from ssr.external / ssrExternal), or externalize the whole family (for example every @atscript/* package, or the whole moost/wooks runtime) via ssr.external.`,
568
+ "Set ssrExternalCheck: false in moostVite() to silence this check, or ssrExternalCheck: { packages: [...] } to watch more packages."
569
+ ].join("\n");
570
+ }
571
+
378
572
  //#endregion
379
573
  //#region packages/vite/src/moost-vite.ts
380
574
  /** Regex checks */ const REG_HAS_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/m;
@@ -408,6 +602,22 @@ function generatedServerEntry(root) {
408
602
  (0, node_fs.writeFileSync)(entryPath, DEFAULT_SERVER_ENTRY_CODE);
409
603
  return entryPath;
410
604
  }
605
+ /**
606
+ * Read the emitted SSR bundle: the packages inlined into it (from every chunk's
607
+ * module ids) and the bare specifiers it still imports (the externalized ones).
608
+ */ function scanSsrBundle(bundle) {
609
+ const externalIds = /* @__PURE__ */ new Set();
610
+ const moduleIds = [];
611
+ for (const chunk of Object.values(bundle)) {
612
+ if (chunk.type !== "chunk") continue;
613
+ moduleIds.push(...chunk.moduleIds ?? []);
614
+ for (const id of [...chunk.imports ?? [], ...chunk.dynamicImports ?? []]) if (!(id in bundle) && npmPackageName(id)) externalIds.add(id);
615
+ }
616
+ return {
617
+ externalIds,
618
+ bundledPackages: bundledPackagesFromModuleIds(moduleIds)
619
+ };
620
+ }
411
621
  function moostVite(options) {
412
622
  const isTest = process.env.NODE_ENV === "test";
413
623
  const isProd = process.env.NODE_ENV === "production";
@@ -502,6 +712,8 @@ function moostVite(options) {
502
712
  reloadRequired = true;
503
713
  };
504
714
  patchMoostHandlerLogging();
715
+ /** Set by the middleware-mode `config` hook: the consumer externalized the runtime themselves. */ let runtimeExternalized = false;
716
+ let resolvedConfig;
505
717
  const pluginConfig = {
506
718
  name: PLUGIN_NAME,
507
719
  enforce: "pre",
@@ -534,14 +746,8 @@ function moostVite(options) {
534
746
  let ssrNoExternal = userNoExternal === true ? true : userNoExternal !== void 0 ? [...Array.isArray(userNoExternal) ? userNoExternal : [userNoExternal], ourPlugin] : isBuild ? true : [ourPlugin];
535
747
  const userExternal = cfg.ssr?.external;
536
748
  const ssrExternal = isBuild ? [...Array.isArray(userExternal) ? userExternal : [], ...options.ssrExternal ?? []] : void 0;
537
- const runtimeNoExternal = [
538
- /^moost($|\/)/,
539
- /^@moostjs\//,
540
- /^@wooksjs\//,
541
- /^wooks($|\/)/
542
- ];
543
- const runtimeExternalized = (ssrExternal ?? []).some((e) => typeof e === "string" && runtimeNoExternal.some((re) => re.test(e)));
544
- if (isBuild && Array.isArray(ssrNoExternal) && !runtimeExternalized) ssrNoExternal = [...ssrNoExternal, ...runtimeNoExternal];
749
+ runtimeExternalized = (ssrExternal ?? []).some((e) => typeof e === "string" && RUNTIME_PACKAGE_PATTERNS.some((re) => re.test(e)));
750
+ if (isBuild && Array.isArray(ssrNoExternal) && !runtimeExternalized) ssrNoExternal = [...ssrNoExternal, ...RUNTIME_PACKAGE_PATTERNS];
545
751
  return {
546
752
  ...options.ssrEntry && { appType: "custom" },
547
753
  ssr: {
@@ -619,6 +825,19 @@ function moostVite(options) {
619
825
  ssrFetchForwarding: options.ssrFetchForwarding
620
826
  };
621
827
  config.__moostViteHttpRef = moostHttpRef;
828
+ resolvedConfig = config;
829
+ },
830
+ generateBundle(_outputOptions, bundle) {
831
+ const cfg = resolvedConfig;
832
+ if (!cfg || cfg.command !== "build" || !options.middleware || options.ssrExternalCheck === false || this.environment?.name !== "ssr") return;
833
+ const { externalIds, bundledPackages } = scanSsrBundle(bundle);
834
+ const splits = findSplitPackages({
835
+ root: cfg.root,
836
+ externalIds,
837
+ bundledPackages,
838
+ patterns: compilePackagePatterns(typeof options.ssrExternalCheck === "object" ? options.ssrExternalCheck.packages : void 0)
839
+ });
840
+ if (splits.length > 0) cfg.logger.warn(`\n[${PLUGIN_NAME}] ${formatSplitPackagesWarning(splits)}\n`);
622
841
  },
623
842
  async transform(code, id) {
624
843
  if (!id.endsWith(".ts")) return null;
@@ -667,7 +886,7 @@ function moostVite(options) {
667
886
  bootingGeneration = bootGeneration;
668
887
  const cleanupInstances = pendingCleanup ?? void 0;
669
888
  pendingCleanup = null;
670
- moostRestartCleanup(adapters, options.onEject, cleanupInstances);
889
+ await moostRestartCleanup(adapters, options.onEject, cleanupInstances);
671
890
  for (const adapter of adapters) if (adapter.detected) await adapter.init();
672
891
  const entryModule = await getEntryModule(server.environments.ssr.moduleGraph);
673
892
  if (entryModule?.transformResult) server.environments.ssr.moduleGraph.invalidateModule(entryModule);
@@ -690,7 +909,7 @@ function moostVite(options) {
690
909
  }
691
910
  };
692
911
  for (const adapter of adapters) adapter.ssrLoadModule = ssrImport;
693
- moostRestartCleanup(adapters, options.onEject);
912
+ await moostRestartCleanup(adapters, options.onEject);
694
913
  await ssrImport(options.entry);
695
914
  server.middlewares.use(async (req, res, next) => {
696
915
  await drainReload();
package/dist/index.d.ts CHANGED
@@ -173,6 +173,33 @@ interface TMoostViteDevOptions {
173
173
  * instead of evaluated by Vite's ESM-only SSR module runner.
174
174
  */
175
175
  ssrExternal?: string[];
176
+ /**
177
+ * Verify the production SSR build for split shared-state packages.
178
+ *
179
+ * After the middleware-mode `vite build`, the plugin compares what ended up
180
+ * inside `dist/server` (the bundled packages) with the bare imports left in
181
+ * the output (the externalized ones) and walks the externals' dependency
182
+ * trees. When an external package depends on a bundled watched package, Node
183
+ * loads a second copy of it at runtime — event-context slots, DI registries
184
+ * and `instanceof` checks stop matching in production only — so the build
185
+ * prints a warning naming the package and the fix.
186
+ *
187
+ * Watched by default: `moost`, `@moostjs/*`, `wooks`, `@wooksjs/*` and
188
+ * `@atscript/*`. Pass `{ packages: [...] }` to watch more: an exact package
189
+ * name (`'lodash'`), a scope/prefix ending with `/` (`'@acme/'`), or a
190
+ * `RegExp`.
191
+ *
192
+ * Default: `true`. Set `false` to silence the check.
193
+ */
194
+ ssrExternalCheck?: boolean | TSSRExternalCheckOptions;
195
+ }
196
+ /** Extra packages the SSR split check should watch on top of its defaults. */
197
+ interface TSSRExternalCheckOptions {
198
+ /**
199
+ * Package names (`'lodash'`), scope/prefix strings ending with `/`
200
+ * (`'@acme/'`) or `RegExp`s, added to the watched set.
201
+ */
202
+ packages?: (string | RegExp)[];
176
203
  }
177
204
  declare function moostVite(options: TMoostViteDevOptions): PluginOption;
178
205
 
package/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
- import { resolve } from "node:path";
1
+ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
3
  import { createServerModuleRunner } from "vite";
4
4
  import MagicString from "magic-string";
5
5
  import { builtinModules } from "node:module";
6
- import { Moost, clearGlobalWooks, createLogger, getMoostInfact, getMoostMate } from "moost";
6
+ import { Moost, clearGlobalWooks, createLogger, disposeInstances, getMoostInfact, getMoostMate } from "moost";
7
7
 
8
8
  //#region packages/vite/src/utils.ts
9
9
  const PLUGIN_NAME = "moost-vite";
@@ -260,14 +260,23 @@ function _define_property(obj, key, value) {
260
260
  /**
261
261
  * Clean up Moost’s global containers and optionally remove specific instances from the registry.
262
262
  *
263
+ * Every instance actually removed from a registry is **disposed** before the
264
+ * caches are dropped: its `@MoostDispose` hooks (or `Symbol.asyncDispose` /
265
+ * `Symbol.dispose`) are awaited, so a singleton owning a connection, consumer,
266
+ * timer or file handle releases it instead of leaking one copy per reload. An
267
+ * instance kept by an `onEject` veto is never disposed. A failing hook is
268
+ * warned about and the reload continues.
269
+ *
263
270
  * @param {Set<string>} [cleanupInstances] A set of module IDs to remove from the registry.
264
- */ function moostRestartCleanup(adapters, onEject, cleanupInstances) {
271
+ * @returns the instances that were ejected (and therefore disposed)
272
+ */ async function moostRestartCleanup(adapters, onEject, cleanupInstances) {
265
273
  const logger = getLogger();
266
274
  const infact = getMoostInfact();
267
275
  const { registry, scopes } = infact;
268
276
  const registries = [registry, ...Object.values(scopes)];
269
277
  infact._cleanup();
270
278
  const mate = getMoostMate();
279
+ /** Instances removed from a registry by this run — disposed below, before the caches drop. */ const ejected = [];
271
280
  if (cleanupInstances) {
272
281
  for (const reg of registries) {
273
282
  for (const key of Object.getOwnPropertySymbols(reg)) {
@@ -276,6 +285,7 @@ function _define_property(obj, key, value) {
276
285
  if (viteId && cleanupInstances.has(viteId)) {
277
286
  logger.debug(`🔃 Replacing "${constructorName(instance)}"`);
278
287
  delete reg[key];
288
+ ejected.push(instance);
279
289
  }
280
290
  }
281
291
  for (const key of Object.getOwnPropertySymbols(reg)) {
@@ -283,25 +293,39 @@ function _define_property(obj, key, value) {
283
293
  scanParams(instance, (type) => {
284
294
  if ((type === Moost || type instanceof Moost || type.prototype instanceof Moost) && (!onEject || onEject(instance, type))) {
285
295
  delete reg[key];
296
+ ejected.push(instance);
286
297
  logger.debug(`✖️ Ejecting "${constructorName(instance)}" (depends on re-instantiated "Moost")`);
287
298
  return true;
288
299
  }
289
300
  for (const adapter of adapters) if (adapter.compare(type) && (!onEject || onEject(instance, type))) {
290
301
  delete reg[key];
302
+ ejected.push(instance);
291
303
  logger.debug(`✖️ Ejecting "${constructorName(instance)}" (depends on re-instantiated "${adapter.constructor.name}")`);
292
304
  return true;
293
305
  }
294
306
  });
295
307
  }
296
- clearDependantRegistry(reg, onEject);
308
+ clearDependantRegistry(reg, onEject, ejected);
297
309
  }
298
310
  infact.registry = registry;
299
311
  infact.scopes = scopes;
300
312
  }
313
+ await disposeEjected(ejected);
301
314
  getMoostMate()._cleanup();
302
315
  clearGlobalWooks();
316
+ return ejected;
317
+ }
318
+ /** Awaits the `@MoostDispose` hooks of the ejected instances; never throws. */ async function disposeEjected(ejected) {
319
+ if (ejected.length === 0) return;
320
+ const logger = getLogger();
321
+ const { disposed, errors } = await disposeInstances(ejected, {
322
+ logger,
323
+ onError: "warn"
324
+ });
325
+ for (const instance of disposed) logger.debug(`♻️ Disposed "${constructorName(instance)}"`);
326
+ for (const e of errors) logger.warn(`⚠️ Dispose hook "${constructorName(e.instance)}.${String(e.method)}" failed — the reload continues`);
303
327
  }
304
- function clearDependantRegistry(registry, onEject) {
328
+ function clearDependantRegistry(registry, onEject, ejected) {
305
329
  const logger = getLogger();
306
330
  const objSet = /* @__PURE__ */ new Set();
307
331
  let somethingIsDeleted = true;
@@ -313,7 +337,10 @@ function clearDependantRegistry(registry, onEject) {
313
337
  }
314
338
  for (const key of Object.getOwnPropertySymbols(registry)) {
315
339
  const instance = registry[key];
316
- if (checkAndEject(instance, objSet, onEject, registry, key, logger)) somethingIsDeleted = true;
340
+ if (checkAndEject(instance, objSet, onEject, registry, key, logger)) {
341
+ ejected.push(instance);
342
+ somethingIsDeleted = true;
343
+ }
317
344
  }
318
345
  }
319
346
  }
@@ -346,6 +373,173 @@ function constructorName(i) {
346
373
  return Object.getPrototypeOf(i).constructor.name;
347
374
  }
348
375
 
376
+ //#endregion
377
+ //#region packages/vite/src/ssr-externals-check.ts
378
+ /**
379
+ * Packages that make up the moost/wooks runtime. They mint per-module Symbol
380
+ * slot keys, so the whole set must resolve to a single module instance in the
381
+ * production SSR graph. This is the scope of the single-instance *guard*
382
+ * (force-bundling under an explicit `noExternal` list) — the build *check*
383
+ * watches the wider {@link SHARED_STATE_PACKAGE_PATTERNS}.
384
+ */ const RUNTIME_PACKAGE_PATTERNS = [
385
+ /^moost($|\/)/,
386
+ /^@moostjs\//,
387
+ /^@wooksjs\//,
388
+ /^wooks($|\/)/
389
+ ];
390
+ /**
391
+ * Packages the build check watches by default: the moost/wooks runtime plus the
392
+ * `@atscript/*` family. All of them keep module-level state — Symbol slot keys,
393
+ * DI/model registries, class identity used by `instanceof` — so a second copy
394
+ * loaded by Node breaks lookups that the bundled copy filled in.
395
+ *
396
+ * This is deliberately wider than {@link RUNTIME_PACKAGE_PATTERNS}: the guard
397
+ * that force-bundles packages covers the runtime only, while the check merely
398
+ * reports what the output shows.
399
+ */ const SHARED_STATE_PACKAGE_PATTERNS = [...RUNTIME_PACKAGE_PATTERNS, /^@atscript\//];
400
+ function escapeRegExp(input) {
401
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
402
+ }
403
+ /**
404
+ * Build the watched-package patterns for the check: the defaults plus the
405
+ * user's extras. A `RegExp` is used as is, a string ending with `/` matches a
406
+ * scope/prefix (`'@acme/'` → every `@acme/*` package), any other string is an
407
+ * exact package name.
408
+ */ function compilePackagePatterns(extra) {
409
+ const patterns = [...SHARED_STATE_PACKAGE_PATTERNS];
410
+ for (const item of extra ?? []) if (item instanceof RegExp) patterns.push(item);
411
+ else if (item.endsWith("/")) patterns.push(new RegExp(`^${escapeRegExp(item)}`));
412
+ else patterns.push(new RegExp(`^${escapeRegExp(item)}$`));
413
+ return patterns;
414
+ }
415
+ const BUILTINS = new Set(builtinModules);
416
+ /** Bare npm package name of an import specifier (`vue/server-renderer` → `vue`). */ function npmPackageName(id) {
417
+ if (!id || id.startsWith(".") || id.startsWith("/") || id.startsWith("\0")) return;
418
+ if (id.startsWith("node:") || id.startsWith("data:") || id.startsWith("virtual:")) return;
419
+ const parts = id.split("/");
420
+ const name = id.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
421
+ if (!name || id.startsWith("@") && parts.length < 2) return;
422
+ return BUILTINS.has(name) ? void 0 : name;
423
+ }
424
+ /**
425
+ * Package names the bundler inlined into the output, derived from the module
426
+ * ids of the emitted chunks. Ids under `node_modules` name their package after
427
+ * the LAST `node_modules/` segment, which also yields the right name for pnpm
428
+ * store paths (`…/node_modules/.pnpm/pkg@1.0.0/node_modules/pkg/index.mjs`).
429
+ * Virtual ids and app sources (no `node_modules`) are ignored.
430
+ */ function bundledPackagesFromModuleIds(moduleIds) {
431
+ const packages = /* @__PURE__ */ new Set();
432
+ const marker = "/node_modules/";
433
+ for (const raw of moduleIds) {
434
+ if (!raw || raw.startsWith("\0")) continue;
435
+ const id = raw.replaceAll("\\", "/");
436
+ const at = id.lastIndexOf(marker);
437
+ if (at === -1) continue;
438
+ const name = npmPackageName(id.slice(at + 14));
439
+ if (name) packages.add(name);
440
+ }
441
+ return packages;
442
+ }
443
+ function readPkgJson(dir) {
444
+ try {
445
+ return JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
446
+ } catch {
447
+ return;
448
+ }
449
+ }
450
+ /**
451
+ * Locate `name`'s package directory the way Node would from `fromDir`:
452
+ * `<fromDir>/node_modules/<name>`, then each ancestor's `node_modules`.
453
+ * Returns the realpath so pnpm's symlinked layout resolves to the store entry,
454
+ * whose own `node_modules` sibling holds the package's dependencies.
455
+ */ function findPackageDir(name, fromDir) {
456
+ let dir = fromDir;
457
+ for (;;) {
458
+ const candidate = join(dir, "node_modules", name);
459
+ if (existsSync(join(candidate, "package.json"))) try {
460
+ return realpathSync(candidate);
461
+ } catch {
462
+ return candidate;
463
+ }
464
+ const parent = dirname(dir);
465
+ if (parent === dir) return;
466
+ dir = parent;
467
+ }
468
+ }
469
+ /** Deps of `pkg` that are watched packages AND were bundled into the output. */ function splitDepsOf(pkg, patterns, bundledPackages) {
470
+ const names = /* @__PURE__ */ new Set();
471
+ for (const group of [
472
+ pkg.dependencies,
473
+ pkg.peerDependencies,
474
+ pkg.optionalDependencies
475
+ ]) for (const dep of Object.keys(group ?? {})) if (bundledPackages.has(dep) && patterns.some((re) => re.test(dep))) names.add(dep);
476
+ return [...names];
477
+ }
478
+ /** Upper bound on visited packages — keeps a pathological tree from stalling the build. */ const MAX_VISITED = 5e3;
479
+ /**
480
+ * Given the bare specifiers left in the SSR build output (i.e. the packages the
481
+ * bundler externalized) and the package names it bundled, find every package
482
+ * Node will load at runtime that depends on a bundled shared-state package.
483
+ * Such a package resolves its own copy from `node_modules` while the bundled
484
+ * copy lives inside `dist/server` — two instances, split module state.
485
+ *
486
+ * Walks the dependency tree beneath each external (Node loads a package's deps
487
+ * from its own resolution root, so transitive consumers matter too), but never
488
+ * descends into a bundled dep: the fix belongs to the external consumer.
489
+ * Packages that are not installed are skipped silently.
490
+ */ function findSplitPackages(opts) {
491
+ const patterns = opts.patterns ?? SHARED_STATE_PACKAGE_PATTERNS;
492
+ const found = [];
493
+ const visited = /* @__PURE__ */ new Set();
494
+ const queue = [];
495
+ for (const id of opts.externalIds) {
496
+ const name = npmPackageName(id);
497
+ if (name && !visited.has(`root:${name}`)) {
498
+ visited.add(`root:${name}`);
499
+ queue.push({
500
+ name,
501
+ fromDir: opts.root,
502
+ via: []
503
+ });
504
+ }
505
+ }
506
+ while (queue.length > 0 && visited.size < MAX_VISITED) {
507
+ const { name, fromDir, via } = queue.shift();
508
+ const dir = findPackageDir(name, fromDir);
509
+ if (!dir || visited.has(dir)) continue;
510
+ visited.add(dir);
511
+ const pkg = readPkgJson(dir);
512
+ if (!pkg) continue;
513
+ const splitDeps = splitDepsOf(pkg, patterns, opts.bundledPackages);
514
+ if (splitDeps.length > 0) found.push({
515
+ name,
516
+ via,
517
+ splitDeps
518
+ });
519
+ for (const dep of Object.keys(pkg.dependencies ?? {})) if (!opts.bundledPackages.has(dep)) queue.push({
520
+ name: dep,
521
+ fromDir: dir,
522
+ via: [...via, name]
523
+ });
524
+ }
525
+ return found;
526
+ }
527
+ /** Human-readable build warning for {@link findSplitPackages} results. */ function formatSplitPackagesWarning(splits) {
528
+ const lines = splits.map((s) => {
529
+ const chain = s.via.length > 0 ? ` (loaded via external ${s.via.join(" → ")})` : "";
530
+ const deps = s.splitDeps.map((d) => `${d} (bundled)`).join(", ");
531
+ return ` - ${s.name}${chain} depends on ${deps}`;
532
+ });
533
+ const direct = [...new Set(splits.map((s) => s.via.length > 0 ? s.via[0] : s.name))];
534
+ return [
535
+ "These externalized packages depend on packages that are bundled into dist/server, so Node will load a second copy of them from node_modules:",
536
+ ...lines,
537
+ "Two copies of a package split its module state — event-context slots, DI registries and class identity (instanceof) stop matching across the boundary, in production only. Symptoms: \"Cannot read properties of undefined (reading 'headers')\" inside a wooks composable, or a database adapter creating an empty table where a managed view was declared.",
538
+ `Fix: keep each shared package and everything that depends on it on the same side — add ${direct.map((d) => `'${d}'`).join(", ")} to ssr.noExternal (or drop them from ssr.external / ssrExternal), or externalize the whole family (for example every @atscript/* package, or the whole moost/wooks runtime) via ssr.external.`,
539
+ "Set ssrExternalCheck: false in moostVite() to silence this check, or ssrExternalCheck: { packages: [...] } to watch more packages."
540
+ ].join("\n");
541
+ }
542
+
349
543
  //#endregion
350
544
  //#region packages/vite/src/moost-vite.ts
351
545
  /** Regex checks */ const REG_HAS_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/m;
@@ -379,6 +573,22 @@ function generatedServerEntry(root) {
379
573
  writeFileSync(entryPath, DEFAULT_SERVER_ENTRY_CODE);
380
574
  return entryPath;
381
575
  }
576
+ /**
577
+ * Read the emitted SSR bundle: the packages inlined into it (from every chunk's
578
+ * module ids) and the bare specifiers it still imports (the externalized ones).
579
+ */ function scanSsrBundle(bundle) {
580
+ const externalIds = /* @__PURE__ */ new Set();
581
+ const moduleIds = [];
582
+ for (const chunk of Object.values(bundle)) {
583
+ if (chunk.type !== "chunk") continue;
584
+ moduleIds.push(...chunk.moduleIds ?? []);
585
+ for (const id of [...chunk.imports ?? [], ...chunk.dynamicImports ?? []]) if (!(id in bundle) && npmPackageName(id)) externalIds.add(id);
586
+ }
587
+ return {
588
+ externalIds,
589
+ bundledPackages: bundledPackagesFromModuleIds(moduleIds)
590
+ };
591
+ }
382
592
  function moostVite(options) {
383
593
  const externals = options.externals ?? true;
384
594
  const prefixes = normalizePrefixes(options.prefix);
@@ -471,6 +681,8 @@ function moostVite(options) {
471
681
  reloadRequired = true;
472
682
  };
473
683
  patchMoostHandlerLogging();
684
+ /** Set by the middleware-mode `config` hook: the consumer externalized the runtime themselves. */ let runtimeExternalized = false;
685
+ let resolvedConfig;
474
686
  return [{
475
687
  name: PLUGIN_NAME,
476
688
  enforce: "pre",
@@ -503,14 +715,8 @@ function moostVite(options) {
503
715
  let ssrNoExternal = userNoExternal === true ? true : userNoExternal !== void 0 ? [...Array.isArray(userNoExternal) ? userNoExternal : [userNoExternal], ourPlugin] : isBuild ? true : [ourPlugin];
504
716
  const userExternal = cfg.ssr?.external;
505
717
  const ssrExternal = isBuild ? [...Array.isArray(userExternal) ? userExternal : [], ...options.ssrExternal ?? []] : void 0;
506
- const runtimeNoExternal = [
507
- /^moost($|\/)/,
508
- /^@moostjs\//,
509
- /^@wooksjs\//,
510
- /^wooks($|\/)/
511
- ];
512
- const runtimeExternalized = (ssrExternal ?? []).some((e) => typeof e === "string" && runtimeNoExternal.some((re) => re.test(e)));
513
- if (isBuild && Array.isArray(ssrNoExternal) && !runtimeExternalized) ssrNoExternal = [...ssrNoExternal, ...runtimeNoExternal];
718
+ runtimeExternalized = (ssrExternal ?? []).some((e) => typeof e === "string" && RUNTIME_PACKAGE_PATTERNS.some((re) => re.test(e)));
719
+ if (isBuild && Array.isArray(ssrNoExternal) && !runtimeExternalized) ssrNoExternal = [...ssrNoExternal, ...RUNTIME_PACKAGE_PATTERNS];
514
720
  return {
515
721
  ...options.ssrEntry && { appType: "custom" },
516
722
  ssr: {
@@ -588,6 +794,19 @@ function moostVite(options) {
588
794
  ssrFetchForwarding: options.ssrFetchForwarding
589
795
  };
590
796
  config.__moostViteHttpRef = moostHttpRef;
797
+ resolvedConfig = config;
798
+ },
799
+ generateBundle(_outputOptions, bundle) {
800
+ const cfg = resolvedConfig;
801
+ if (!cfg || cfg.command !== "build" || !options.middleware || options.ssrExternalCheck === false || this.environment?.name !== "ssr") return;
802
+ const { externalIds, bundledPackages } = scanSsrBundle(bundle);
803
+ const splits = findSplitPackages({
804
+ root: cfg.root,
805
+ externalIds,
806
+ bundledPackages,
807
+ patterns: compilePackagePatterns(typeof options.ssrExternalCheck === "object" ? options.ssrExternalCheck.packages : void 0)
808
+ });
809
+ if (splits.length > 0) cfg.logger.warn(`\n[${PLUGIN_NAME}] ${formatSplitPackagesWarning(splits)}\n`);
591
810
  },
592
811
  async transform(code, id) {
593
812
  if (!id.endsWith(".ts")) return null;
@@ -636,7 +855,7 @@ function moostVite(options) {
636
855
  bootingGeneration = bootGeneration;
637
856
  const cleanupInstances = pendingCleanup ?? void 0;
638
857
  pendingCleanup = null;
639
- moostRestartCleanup(adapters, options.onEject, cleanupInstances);
858
+ await moostRestartCleanup(adapters, options.onEject, cleanupInstances);
640
859
  for (const adapter of adapters) if (adapter.detected) await adapter.init();
641
860
  const entryModule = await getEntryModule(server.environments.ssr.moduleGraph);
642
861
  if (entryModule?.transformResult) server.environments.ssr.moduleGraph.invalidateModule(entryModule);
@@ -659,7 +878,7 @@ function moostVite(options) {
659
878
  }
660
879
  };
661
880
  for (const adapter of adapters) adapter.ssrLoadModule = ssrImport;
662
- moostRestartCleanup(adapters, options.onEject);
881
+ await moostRestartCleanup(adapters, options.onEject);
663
882
  await ssrImport(options.entry);
664
883
  server.middlewares.use(async (req, res, next) => {
665
884
  await drainReload();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moostjs/vite",
3
- "version": "0.6.34",
3
+ "version": "0.6.36",
4
4
  "description": "Vite Dev plugin for moostjs",
5
5
  "keywords": [
6
6
  "composables",
@@ -53,13 +53,13 @@
53
53
  "magic-string": "^0.30.21"
54
54
  },
55
55
  "devDependencies": {
56
- "vitest": "3.2.4"
56
+ "vitest": "3.2.7"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "sirv": "^3.0.0",
60
60
  "vite": "^8.0.0",
61
- "@moostjs/event-http": "^0.6.34",
62
- "moost": "^0.6.34"
61
+ "@moostjs/event-http": "^0.6.36",
62
+ "moost": "^0.6.36"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "sirv": {