@moostjs/vite 0.6.35 → 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 +25 -19
- package/dist/index.cjs +233 -14
- package/dist/index.d.ts +27 -0
- package/dist/index.mjs +236 -17
- package/package.json +3 -3
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
|
|
248
|
-
|
|
249
|
-
| `entry`
|
|
250
|
-
| `port`
|
|
251
|
-
| `host`
|
|
252
|
-
| `outDir`
|
|
253
|
-
| `format`
|
|
254
|
-
| `sourcemap`
|
|
255
|
-
| `externals`
|
|
256
|
-
| `onEject`
|
|
257
|
-
| `ssrFetch`
|
|
258
|
-
| `middleware`
|
|
259
|
-
| `prefix`
|
|
260
|
-
| `ssrEntry`
|
|
261
|
-
| `ssrOutlet`
|
|
262
|
-
| `ssrState`
|
|
263
|
-
| `ssrHead`
|
|
264
|
-
| `serverEntry`
|
|
265
|
-
| `ssrExternal`
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
538
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
507
|
-
|
|
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.
|
|
3
|
+
"version": "0.6.36",
|
|
4
4
|
"description": "Vite Dev plugin for moostjs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"composables",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"sirv": "^3.0.0",
|
|
60
60
|
"vite": "^8.0.0",
|
|
61
|
-
"@moostjs/event-http": "^0.6.
|
|
62
|
-
"moost": "^0.6.
|
|
61
|
+
"@moostjs/event-http": "^0.6.36",
|
|
62
|
+
"moost": "^0.6.36"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
65
|
"sirv": {
|