@moostjs/vite 0.6.35 → 0.6.37
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 +340 -28
- package/dist/index.d.ts +27 -0
- package/dist/index.mjs +343 -31
- 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
|
@@ -249,19 +249,63 @@ function _define_property(obj, key, value) {
|
|
|
249
249
|
* A reference to a shared LogsStorage instance that persists
|
|
250
250
|
* across init cycles in dev mode.
|
|
251
251
|
*/ let logsStorage;
|
|
252
|
+
/** Whether {@link patchMoostHandlerLogging} already installed its wrappers in this process. */ let installed = false;
|
|
252
253
|
/**
|
|
253
|
-
*
|
|
254
|
+
* The slot {@link captureMoostInit} fills: receives every `init()` promise the
|
|
255
|
+
* wrapper below produces. Unset in prod/test, where nothing consumes a capture.
|
|
256
|
+
*/ let onInitCaptured;
|
|
257
|
+
/**
|
|
258
|
+
* Hands every `Moost.prototype.init()` promise to `onCapture`, letting a boot
|
|
259
|
+
* await the app's **initialization** instead of only its module evaluation.
|
|
260
|
+
*
|
|
261
|
+
* The documented entry shape is `app.adapter(http).listen(port)` followed by an
|
|
262
|
+
* **un-awaited** `app.init()`: the HTTP middleware is therefore captured *before*
|
|
263
|
+
* init finishes, and nothing else observes a rejecting init — a bind error, a DI
|
|
264
|
+
* audit error or a throwing `@MoostInit` hook would leave the dev server serving
|
|
265
|
+
* a half-booted app (the routes bound before the failure answer 200, the rest
|
|
266
|
+
* fall through to the SPA fallback) instead of the plugin's 502. That is also
|
|
267
|
+
* why the plugin's `bootError` gate runs BEFORE the captured middleware, and why
|
|
268
|
+
* the SSR fallback checks it too: `listen()` ran first and left both a middleware
|
|
269
|
+
* and the local-fetch hook behind.
|
|
270
|
+
*
|
|
271
|
+
* Patches the plugin's **own** `moost` import, exactly like the handler logging
|
|
272
|
+
* does: in dev the runtime is externalized, so the plugin and the app share one
|
|
273
|
+
* native `moost` instance (the premise the DI eject in `restart-cleanup` rests on
|
|
274
|
+
* too, and what `ssrExternalCheck` warns about when it stops holding). Unlike the
|
|
275
|
+
* adapters there is nothing to re-establish per reload — a static import is never
|
|
276
|
+
* re-evaluated — and unlike them it must NOT be pulled through the SSR module
|
|
277
|
+
* runner: importing `moost` there registers it as the runner's first module and
|
|
278
|
+
* makes subsequent hot updates re-evaluate the entry only, serving every one of
|
|
279
|
+
* its dependencies (controllers included) from the stale cache.
|
|
280
|
+
*
|
|
281
|
+
* There is exactly ONE `init` wrapper per process (installed by
|
|
282
|
+
* {@link patchMoostHandlerLogging}) and this only fills its slot, so the newest
|
|
283
|
+
* plugin instance wins: Vite re-imports the config on every config-file
|
|
284
|
+
* `server.restart()`, re-running the plugin factory, and a per-instance wrapper
|
|
285
|
+
* would stack on the previous one and keep the dead instance's scope alive.
|
|
286
|
+
*
|
|
287
|
+
* @param onCapture receives each captured init promise (already marked handled).
|
|
288
|
+
*/ function captureMoostInit(onCapture) {
|
|
289
|
+
onInitCaptured = onCapture;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Patches Moost’s .init() and .logMappedHandler() methods — once per process —
|
|
293
|
+
* so that, during dev:
|
|
254
294
|
* - Only newly added handlers are logged normally.
|
|
255
295
|
* - Removed handlers are logged/stroked out to indicate removal.
|
|
256
296
|
* - Prevents repeated logs from flooding the console across multiple hot reloads.
|
|
297
|
+
* - Every init() promise reaches the {@link captureMoostInit} slot, if filled.
|
|
257
298
|
*/ function patchMoostHandlerLogging() {
|
|
299
|
+
if (installed) return;
|
|
300
|
+
installed = true;
|
|
258
301
|
const origInit = moost.Moost.prototype.init;
|
|
302
|
+
const origLogMappedHandler = moost.Moost.prototype.logMappedHandler;
|
|
259
303
|
/**
|
|
260
|
-
*
|
|
304
|
+
* The logging body of the patched Moost.init:
|
|
261
305
|
* 1. Start a new recording cycle in LogsStorage.
|
|
262
306
|
* 2. Call the real .init().
|
|
263
307
|
* 3. End the recording, logging any removed handlers.
|
|
264
|
-
*/
|
|
308
|
+
*/ const initWithLogging = async function() {
|
|
265
309
|
if (!logsStorage) logsStorage = new LogsStorage();
|
|
266
310
|
logsStorage.startRecording();
|
|
267
311
|
await origInit.call(this);
|
|
@@ -269,7 +313,14 @@ function _define_property(obj, key, value) {
|
|
|
269
313
|
origLogMappedHandler.call(this, item.eventName, item.classConstructor, item.method, true, "❌ ");
|
|
270
314
|
});
|
|
271
315
|
};
|
|
272
|
-
|
|
316
|
+
moost.Moost.prototype.init = function init() {
|
|
317
|
+
const promise = initWithLogging.call(this);
|
|
318
|
+
if (onInitCaptured) {
|
|
319
|
+
promise.catch(() => {});
|
|
320
|
+
onInitCaptured(promise);
|
|
321
|
+
}
|
|
322
|
+
return promise;
|
|
323
|
+
};
|
|
273
324
|
/**
|
|
274
325
|
* Monkey-patch Moost.logMappedHandler:
|
|
275
326
|
* If we haven’t seen this particular route+class+method combination
|
|
@@ -289,14 +340,23 @@ function _define_property(obj, key, value) {
|
|
|
289
340
|
/**
|
|
290
341
|
* Clean up Moost’s global containers and optionally remove specific instances from the registry.
|
|
291
342
|
*
|
|
343
|
+
* Every instance actually removed from a registry is **disposed** before the
|
|
344
|
+
* caches are dropped: its `@MoostDispose` hooks (or `Symbol.asyncDispose` /
|
|
345
|
+
* `Symbol.dispose`) are awaited, so a singleton owning a connection, consumer,
|
|
346
|
+
* timer or file handle releases it instead of leaking one copy per reload. An
|
|
347
|
+
* instance kept by an `onEject` veto is never disposed. A failing hook is
|
|
348
|
+
* warned about and the reload continues.
|
|
349
|
+
*
|
|
292
350
|
* @param {Set<string>} [cleanupInstances] A set of module IDs to remove from the registry.
|
|
293
|
-
|
|
351
|
+
* @returns the instances that were ejected (and therefore disposed)
|
|
352
|
+
*/ async function moostRestartCleanup(adapters, onEject, cleanupInstances) {
|
|
294
353
|
const logger = getLogger();
|
|
295
354
|
const infact = (0, moost.getMoostInfact)();
|
|
296
355
|
const { registry, scopes } = infact;
|
|
297
356
|
const registries = [registry, ...Object.values(scopes)];
|
|
298
357
|
infact._cleanup();
|
|
299
358
|
const mate = (0, moost.getMoostMate)();
|
|
359
|
+
/** Instances removed from a registry by this run — disposed below, before the caches drop. */ const ejected = [];
|
|
300
360
|
if (cleanupInstances) {
|
|
301
361
|
for (const reg of registries) {
|
|
302
362
|
for (const key of Object.getOwnPropertySymbols(reg)) {
|
|
@@ -305,6 +365,7 @@ function _define_property(obj, key, value) {
|
|
|
305
365
|
if (viteId && cleanupInstances.has(viteId)) {
|
|
306
366
|
logger.debug(`🔃 Replacing "${constructorName(instance)}"`);
|
|
307
367
|
delete reg[key];
|
|
368
|
+
ejected.push(instance);
|
|
308
369
|
}
|
|
309
370
|
}
|
|
310
371
|
for (const key of Object.getOwnPropertySymbols(reg)) {
|
|
@@ -312,25 +373,39 @@ function _define_property(obj, key, value) {
|
|
|
312
373
|
scanParams(instance, (type) => {
|
|
313
374
|
if ((type === moost.Moost || type instanceof moost.Moost || type.prototype instanceof moost.Moost) && (!onEject || onEject(instance, type))) {
|
|
314
375
|
delete reg[key];
|
|
376
|
+
ejected.push(instance);
|
|
315
377
|
logger.debug(`✖️ Ejecting "${constructorName(instance)}" (depends on re-instantiated "Moost")`);
|
|
316
378
|
return true;
|
|
317
379
|
}
|
|
318
380
|
for (const adapter of adapters) if (adapter.compare(type) && (!onEject || onEject(instance, type))) {
|
|
319
381
|
delete reg[key];
|
|
382
|
+
ejected.push(instance);
|
|
320
383
|
logger.debug(`✖️ Ejecting "${constructorName(instance)}" (depends on re-instantiated "${adapter.constructor.name}")`);
|
|
321
384
|
return true;
|
|
322
385
|
}
|
|
323
386
|
});
|
|
324
387
|
}
|
|
325
|
-
clearDependantRegistry(reg, onEject);
|
|
388
|
+
clearDependantRegistry(reg, onEject, ejected);
|
|
326
389
|
}
|
|
327
390
|
infact.registry = registry;
|
|
328
391
|
infact.scopes = scopes;
|
|
329
392
|
}
|
|
393
|
+
await disposeEjected(ejected);
|
|
330
394
|
(0, moost.getMoostMate)()._cleanup();
|
|
331
395
|
(0, moost.clearGlobalWooks)();
|
|
396
|
+
return ejected;
|
|
332
397
|
}
|
|
333
|
-
function
|
|
398
|
+
/** Awaits the `@MoostDispose` hooks of the ejected instances; never throws. */ async function disposeEjected(ejected) {
|
|
399
|
+
if (ejected.length === 0) return;
|
|
400
|
+
const logger = getLogger();
|
|
401
|
+
const { disposed, errors } = await (0, moost.disposeInstances)(ejected, {
|
|
402
|
+
logger,
|
|
403
|
+
onError: "warn"
|
|
404
|
+
});
|
|
405
|
+
for (const instance of disposed) logger.debug(`♻️ Disposed "${constructorName(instance)}"`);
|
|
406
|
+
for (const e of errors) logger.warn(`⚠️ Dispose hook "${constructorName(e.instance)}.${String(e.method)}" failed — the reload continues`);
|
|
407
|
+
}
|
|
408
|
+
function clearDependantRegistry(registry, onEject, ejected) {
|
|
334
409
|
const logger = getLogger();
|
|
335
410
|
const objSet = /* @__PURE__ */ new Set();
|
|
336
411
|
let somethingIsDeleted = true;
|
|
@@ -342,7 +417,10 @@ function clearDependantRegistry(registry, onEject) {
|
|
|
342
417
|
}
|
|
343
418
|
for (const key of Object.getOwnPropertySymbols(registry)) {
|
|
344
419
|
const instance = registry[key];
|
|
345
|
-
if (checkAndEject(instance, objSet, onEject, registry, key, logger))
|
|
420
|
+
if (checkAndEject(instance, objSet, onEject, registry, key, logger)) {
|
|
421
|
+
ejected.push(instance);
|
|
422
|
+
somethingIsDeleted = true;
|
|
423
|
+
}
|
|
346
424
|
}
|
|
347
425
|
}
|
|
348
426
|
}
|
|
@@ -375,6 +453,173 @@ function constructorName(i) {
|
|
|
375
453
|
return Object.getPrototypeOf(i).constructor.name;
|
|
376
454
|
}
|
|
377
455
|
|
|
456
|
+
//#endregion
|
|
457
|
+
//#region packages/vite/src/ssr-externals-check.ts
|
|
458
|
+
/**
|
|
459
|
+
* Packages that make up the moost/wooks runtime. They mint per-module Symbol
|
|
460
|
+
* slot keys, so the whole set must resolve to a single module instance in the
|
|
461
|
+
* production SSR graph. This is the scope of the single-instance *guard*
|
|
462
|
+
* (force-bundling under an explicit `noExternal` list) — the build *check*
|
|
463
|
+
* watches the wider {@link SHARED_STATE_PACKAGE_PATTERNS}.
|
|
464
|
+
*/ const RUNTIME_PACKAGE_PATTERNS = [
|
|
465
|
+
/^moost($|\/)/,
|
|
466
|
+
/^@moostjs\//,
|
|
467
|
+
/^@wooksjs\//,
|
|
468
|
+
/^wooks($|\/)/
|
|
469
|
+
];
|
|
470
|
+
/**
|
|
471
|
+
* Packages the build check watches by default: the moost/wooks runtime plus the
|
|
472
|
+
* `@atscript/*` family. All of them keep module-level state — Symbol slot keys,
|
|
473
|
+
* DI/model registries, class identity used by `instanceof` — so a second copy
|
|
474
|
+
* loaded by Node breaks lookups that the bundled copy filled in.
|
|
475
|
+
*
|
|
476
|
+
* This is deliberately wider than {@link RUNTIME_PACKAGE_PATTERNS}: the guard
|
|
477
|
+
* that force-bundles packages covers the runtime only, while the check merely
|
|
478
|
+
* reports what the output shows.
|
|
479
|
+
*/ const SHARED_STATE_PACKAGE_PATTERNS = [...RUNTIME_PACKAGE_PATTERNS, /^@atscript\//];
|
|
480
|
+
function escapeRegExp(input) {
|
|
481
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Build the watched-package patterns for the check: the defaults plus the
|
|
485
|
+
* user's extras. A `RegExp` is used as is, a string ending with `/` matches a
|
|
486
|
+
* scope/prefix (`'@acme/'` → every `@acme/*` package), any other string is an
|
|
487
|
+
* exact package name.
|
|
488
|
+
*/ function compilePackagePatterns(extra) {
|
|
489
|
+
const patterns = [...SHARED_STATE_PACKAGE_PATTERNS];
|
|
490
|
+
for (const item of extra ?? []) if (item instanceof RegExp) patterns.push(item);
|
|
491
|
+
else if (item.endsWith("/")) patterns.push(new RegExp(`^${escapeRegExp(item)}`));
|
|
492
|
+
else patterns.push(new RegExp(`^${escapeRegExp(item)}$`));
|
|
493
|
+
return patterns;
|
|
494
|
+
}
|
|
495
|
+
const BUILTINS = new Set(node_module.builtinModules);
|
|
496
|
+
/** Bare npm package name of an import specifier (`vue/server-renderer` → `vue`). */ function npmPackageName(id) {
|
|
497
|
+
if (!id || id.startsWith(".") || id.startsWith("/") || id.startsWith("\0")) return;
|
|
498
|
+
if (id.startsWith("node:") || id.startsWith("data:") || id.startsWith("virtual:")) return;
|
|
499
|
+
const parts = id.split("/");
|
|
500
|
+
const name = id.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
501
|
+
if (!name || id.startsWith("@") && parts.length < 2) return;
|
|
502
|
+
return BUILTINS.has(name) ? void 0 : name;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Package names the bundler inlined into the output, derived from the module
|
|
506
|
+
* ids of the emitted chunks. Ids under `node_modules` name their package after
|
|
507
|
+
* the LAST `node_modules/` segment, which also yields the right name for pnpm
|
|
508
|
+
* store paths (`…/node_modules/.pnpm/pkg@1.0.0/node_modules/pkg/index.mjs`).
|
|
509
|
+
* Virtual ids and app sources (no `node_modules`) are ignored.
|
|
510
|
+
*/ function bundledPackagesFromModuleIds(moduleIds) {
|
|
511
|
+
const packages = /* @__PURE__ */ new Set();
|
|
512
|
+
const marker = "/node_modules/";
|
|
513
|
+
for (const raw of moduleIds) {
|
|
514
|
+
if (!raw || raw.startsWith("\0")) continue;
|
|
515
|
+
const id = raw.replaceAll("\\", "/");
|
|
516
|
+
const at = id.lastIndexOf(marker);
|
|
517
|
+
if (at === -1) continue;
|
|
518
|
+
const name = npmPackageName(id.slice(at + 14));
|
|
519
|
+
if (name) packages.add(name);
|
|
520
|
+
}
|
|
521
|
+
return packages;
|
|
522
|
+
}
|
|
523
|
+
function readPkgJson(dir) {
|
|
524
|
+
try {
|
|
525
|
+
return JSON.parse((0, node_fs.readFileSync)((0, node_path.join)(dir, "package.json"), "utf8"));
|
|
526
|
+
} catch {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Locate `name`'s package directory the way Node would from `fromDir`:
|
|
532
|
+
* `<fromDir>/node_modules/<name>`, then each ancestor's `node_modules`.
|
|
533
|
+
* Returns the realpath so pnpm's symlinked layout resolves to the store entry,
|
|
534
|
+
* whose own `node_modules` sibling holds the package's dependencies.
|
|
535
|
+
*/ function findPackageDir(name, fromDir) {
|
|
536
|
+
let dir = fromDir;
|
|
537
|
+
for (;;) {
|
|
538
|
+
const candidate = (0, node_path.join)(dir, "node_modules", name);
|
|
539
|
+
if ((0, node_fs.existsSync)((0, node_path.join)(candidate, "package.json"))) try {
|
|
540
|
+
return (0, node_fs.realpathSync)(candidate);
|
|
541
|
+
} catch {
|
|
542
|
+
return candidate;
|
|
543
|
+
}
|
|
544
|
+
const parent = (0, node_path.dirname)(dir);
|
|
545
|
+
if (parent === dir) return;
|
|
546
|
+
dir = parent;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/** Deps of `pkg` that are watched packages AND were bundled into the output. */ function splitDepsOf(pkg, patterns, bundledPackages) {
|
|
550
|
+
const names = /* @__PURE__ */ new Set();
|
|
551
|
+
for (const group of [
|
|
552
|
+
pkg.dependencies,
|
|
553
|
+
pkg.peerDependencies,
|
|
554
|
+
pkg.optionalDependencies
|
|
555
|
+
]) for (const dep of Object.keys(group ?? {})) if (bundledPackages.has(dep) && patterns.some((re) => re.test(dep))) names.add(dep);
|
|
556
|
+
return [...names];
|
|
557
|
+
}
|
|
558
|
+
/** Upper bound on visited packages — keeps a pathological tree from stalling the build. */ const MAX_VISITED = 5e3;
|
|
559
|
+
/**
|
|
560
|
+
* Given the bare specifiers left in the SSR build output (i.e. the packages the
|
|
561
|
+
* bundler externalized) and the package names it bundled, find every package
|
|
562
|
+
* Node will load at runtime that depends on a bundled shared-state package.
|
|
563
|
+
* Such a package resolves its own copy from `node_modules` while the bundled
|
|
564
|
+
* copy lives inside `dist/server` — two instances, split module state.
|
|
565
|
+
*
|
|
566
|
+
* Walks the dependency tree beneath each external (Node loads a package's deps
|
|
567
|
+
* from its own resolution root, so transitive consumers matter too), but never
|
|
568
|
+
* descends into a bundled dep: the fix belongs to the external consumer.
|
|
569
|
+
* Packages that are not installed are skipped silently.
|
|
570
|
+
*/ function findSplitPackages(opts) {
|
|
571
|
+
const patterns = opts.patterns ?? SHARED_STATE_PACKAGE_PATTERNS;
|
|
572
|
+
const found = [];
|
|
573
|
+
const visited = /* @__PURE__ */ new Set();
|
|
574
|
+
const queue = [];
|
|
575
|
+
for (const id of opts.externalIds) {
|
|
576
|
+
const name = npmPackageName(id);
|
|
577
|
+
if (name && !visited.has(`root:${name}`)) {
|
|
578
|
+
visited.add(`root:${name}`);
|
|
579
|
+
queue.push({
|
|
580
|
+
name,
|
|
581
|
+
fromDir: opts.root,
|
|
582
|
+
via: []
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
while (queue.length > 0 && visited.size < MAX_VISITED) {
|
|
587
|
+
const { name, fromDir, via } = queue.shift();
|
|
588
|
+
const dir = findPackageDir(name, fromDir);
|
|
589
|
+
if (!dir || visited.has(dir)) continue;
|
|
590
|
+
visited.add(dir);
|
|
591
|
+
const pkg = readPkgJson(dir);
|
|
592
|
+
if (!pkg) continue;
|
|
593
|
+
const splitDeps = splitDepsOf(pkg, patterns, opts.bundledPackages);
|
|
594
|
+
if (splitDeps.length > 0) found.push({
|
|
595
|
+
name,
|
|
596
|
+
via,
|
|
597
|
+
splitDeps
|
|
598
|
+
});
|
|
599
|
+
for (const dep of Object.keys(pkg.dependencies ?? {})) if (!opts.bundledPackages.has(dep)) queue.push({
|
|
600
|
+
name: dep,
|
|
601
|
+
fromDir: dir,
|
|
602
|
+
via: [...via, name]
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
return found;
|
|
606
|
+
}
|
|
607
|
+
/** Human-readable build warning for {@link findSplitPackages} results. */ function formatSplitPackagesWarning(splits) {
|
|
608
|
+
const lines = splits.map((s) => {
|
|
609
|
+
const chain = s.via.length > 0 ? ` (loaded via external ${s.via.join(" → ")})` : "";
|
|
610
|
+
const deps = s.splitDeps.map((d) => `${d} (bundled)`).join(", ");
|
|
611
|
+
return ` - ${s.name}${chain} depends on ${deps}`;
|
|
612
|
+
});
|
|
613
|
+
const direct = [...new Set(splits.map((s) => s.via.length > 0 ? s.via[0] : s.name))];
|
|
614
|
+
return [
|
|
615
|
+
"These externalized packages depend on packages that are bundled into dist/server, so Node will load a second copy of them from node_modules:",
|
|
616
|
+
...lines,
|
|
617
|
+
"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.",
|
|
618
|
+
`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.`,
|
|
619
|
+
"Set ssrExternalCheck: false in moostVite() to silence this check, or ssrExternalCheck: { packages: [...] } to watch more packages."
|
|
620
|
+
].join("\n");
|
|
621
|
+
}
|
|
622
|
+
|
|
378
623
|
//#endregion
|
|
379
624
|
//#region packages/vite/src/moost-vite.ts
|
|
380
625
|
/** Regex checks */ const REG_HAS_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/m;
|
|
@@ -408,6 +653,22 @@ function generatedServerEntry(root) {
|
|
|
408
653
|
(0, node_fs.writeFileSync)(entryPath, DEFAULT_SERVER_ENTRY_CODE);
|
|
409
654
|
return entryPath;
|
|
410
655
|
}
|
|
656
|
+
/**
|
|
657
|
+
* Read the emitted SSR bundle: the packages inlined into it (from every chunk's
|
|
658
|
+
* module ids) and the bare specifiers it still imports (the externalized ones).
|
|
659
|
+
*/ function scanSsrBundle(bundle) {
|
|
660
|
+
const externalIds = /* @__PURE__ */ new Set();
|
|
661
|
+
const moduleIds = [];
|
|
662
|
+
for (const chunk of Object.values(bundle)) {
|
|
663
|
+
if (chunk.type !== "chunk") continue;
|
|
664
|
+
moduleIds.push(...chunk.moduleIds ?? []);
|
|
665
|
+
for (const id of [...chunk.imports ?? [], ...chunk.dynamicImports ?? []]) if (!(id in bundle) && npmPackageName(id)) externalIds.add(id);
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
externalIds,
|
|
669
|
+
bundledPackages: bundledPackagesFromModuleIds(moduleIds)
|
|
670
|
+
};
|
|
671
|
+
}
|
|
411
672
|
function moostVite(options) {
|
|
412
673
|
const isTest = process.env.NODE_ENV === "test";
|
|
413
674
|
const isProd = process.env.NODE_ENV === "production";
|
|
@@ -433,6 +694,11 @@ function moostVite(options) {
|
|
|
433
694
|
*/ let bootGeneration = 0;
|
|
434
695
|
let bootingGeneration = 0;
|
|
435
696
|
/** Whether the HTTP listen() patch has ever captured a middleware (i.e. this is an HTTP app). */ let httpCaptured = false;
|
|
697
|
+
/**
|
|
698
|
+
* The `app.init()` promises the current boot handed us (see `captureMoostInit`) —
|
|
699
|
+
* several when one entry boots several apps, none for a boot that never calls
|
|
700
|
+
* `init()`. Reset by `beginBoot()`, consumed by `settleBootInit()`.
|
|
701
|
+
*/ let bootInitPromises = [];
|
|
436
702
|
/** Module IDs awaiting DI cleanup — consumed in runReload; see ejectApp. */ let pendingCleanup = null;
|
|
437
703
|
/** In middleware mode: maps req → next() for the onNoMatch callback */ const pendingNextMap = /* @__PURE__ */ new WeakMap();
|
|
438
704
|
const adapters = isTest ? [] : [
|
|
@@ -502,6 +768,43 @@ function moostVite(options) {
|
|
|
502
768
|
reloadRequired = true;
|
|
503
769
|
};
|
|
504
770
|
patchMoostHandlerLogging();
|
|
771
|
+
if (!isTest && !isProd) captureMoostInit((promise) => {
|
|
772
|
+
if (bootingGeneration !== bootGeneration) {
|
|
773
|
+
logger.error(`⚠️ A stale Moost boot called init() (boot generation ${bootingGeneration}, latest ${bootGeneration}) — an HMR reload race; a follow-up reload will replace it.`);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
bootInitPromises.push(promise);
|
|
777
|
+
});
|
|
778
|
+
/** Marks the boot about to run as authoritative for the current generation. */ const beginBoot = () => {
|
|
779
|
+
bootingGeneration = bootGeneration;
|
|
780
|
+
bootInitPromises = [];
|
|
781
|
+
};
|
|
782
|
+
/**
|
|
783
|
+
* Finishes a boot: awaits the `init()` promise(s) the entry that just executed
|
|
784
|
+
* handed us, so the boot is only "done" once the app is fully initialized —
|
|
785
|
+
* every controller bound, every `@MoostInit` hook run (see `captureMoostInit`).
|
|
786
|
+
* Owns `bootError`: cleared on success, set and logged when an init rejected;
|
|
787
|
+
* returns whether the boot succeeded.
|
|
788
|
+
*/ const settleBootInit = async () => {
|
|
789
|
+
const initPromises = bootInitPromises;
|
|
790
|
+
bootInitPromises = [];
|
|
791
|
+
try {
|
|
792
|
+
await Promise.all(initPromises);
|
|
793
|
+
bootError = null;
|
|
794
|
+
return true;
|
|
795
|
+
} catch (error) {
|
|
796
|
+
bootError = error;
|
|
797
|
+
logger.error(`✖️ Moost app init failed: ${error.message}`);
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
/** Answers a request while `bootError` is set (see the gate in configureServer). */ const answerBootError = (res) => {
|
|
802
|
+
res.statusCode = 502;
|
|
803
|
+
res.setHeader("Content-Type", "text/plain");
|
|
804
|
+
res.end(`Moost app failed to load: ${bootError.message}`);
|
|
805
|
+
};
|
|
806
|
+
/** Set by the middleware-mode `config` hook: the consumer externalized the runtime themselves. */ let runtimeExternalized = false;
|
|
807
|
+
let resolvedConfig;
|
|
505
808
|
const pluginConfig = {
|
|
506
809
|
name: PLUGIN_NAME,
|
|
507
810
|
enforce: "pre",
|
|
@@ -534,14 +837,8 @@ function moostVite(options) {
|
|
|
534
837
|
let ssrNoExternal = userNoExternal === true ? true : userNoExternal !== void 0 ? [...Array.isArray(userNoExternal) ? userNoExternal : [userNoExternal], ourPlugin] : isBuild ? true : [ourPlugin];
|
|
535
838
|
const userExternal = cfg.ssr?.external;
|
|
536
839
|
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];
|
|
840
|
+
runtimeExternalized = (ssrExternal ?? []).some((e) => typeof e === "string" && RUNTIME_PACKAGE_PATTERNS.some((re) => re.test(e)));
|
|
841
|
+
if (isBuild && Array.isArray(ssrNoExternal) && !runtimeExternalized) ssrNoExternal = [...ssrNoExternal, ...RUNTIME_PACKAGE_PATTERNS];
|
|
545
842
|
return {
|
|
546
843
|
...options.ssrEntry && { appType: "custom" },
|
|
547
844
|
ssr: {
|
|
@@ -619,6 +916,19 @@ function moostVite(options) {
|
|
|
619
916
|
ssrFetchForwarding: options.ssrFetchForwarding
|
|
620
917
|
};
|
|
621
918
|
config.__moostViteHttpRef = moostHttpRef;
|
|
919
|
+
resolvedConfig = config;
|
|
920
|
+
},
|
|
921
|
+
generateBundle(_outputOptions, bundle) {
|
|
922
|
+
const cfg = resolvedConfig;
|
|
923
|
+
if (!cfg || cfg.command !== "build" || !options.middleware || options.ssrExternalCheck === false || this.environment?.name !== "ssr") return;
|
|
924
|
+
const { externalIds, bundledPackages } = scanSsrBundle(bundle);
|
|
925
|
+
const splits = findSplitPackages({
|
|
926
|
+
root: cfg.root,
|
|
927
|
+
externalIds,
|
|
928
|
+
bundledPackages,
|
|
929
|
+
patterns: compilePackagePatterns(typeof options.ssrExternalCheck === "object" ? options.ssrExternalCheck.packages : void 0)
|
|
930
|
+
});
|
|
931
|
+
if (splits.length > 0) cfg.logger.warn(`\n[${PLUGIN_NAME}] ${formatSplitPackagesWarning(splits)}\n`);
|
|
622
932
|
},
|
|
623
933
|
async transform(code, id) {
|
|
624
934
|
if (!id.endsWith(".ts")) return null;
|
|
@@ -656,7 +966,7 @@ function moostVite(options) {
|
|
|
656
966
|
reloadRequired = false;
|
|
657
967
|
reloadPromise = null;
|
|
658
968
|
pendingCleanup = null;
|
|
659
|
-
|
|
969
|
+
beginBoot();
|
|
660
970
|
const runReload = () => {
|
|
661
971
|
reloadRequired = false;
|
|
662
972
|
console.log();
|
|
@@ -664,16 +974,15 @@ function moostVite(options) {
|
|
|
664
974
|
console.log();
|
|
665
975
|
return (async () => {
|
|
666
976
|
try {
|
|
667
|
-
|
|
977
|
+
beginBoot();
|
|
668
978
|
const cleanupInstances = pendingCleanup ?? void 0;
|
|
669
979
|
pendingCleanup = null;
|
|
670
|
-
moostRestartCleanup(adapters, options.onEject, cleanupInstances);
|
|
980
|
+
await moostRestartCleanup(adapters, options.onEject, cleanupInstances);
|
|
671
981
|
for (const adapter of adapters) if (adapter.detected) await adapter.init();
|
|
672
982
|
const entryModule = await getEntryModule(server.environments.ssr.moduleGraph);
|
|
673
983
|
if (entryModule?.transformResult) server.environments.ssr.moduleGraph.invalidateModule(entryModule);
|
|
674
984
|
await ssrImport(options.entry);
|
|
675
|
-
|
|
676
|
-
if (httpCaptured && !moostMiddleware) logger.error(`⚠️ Moost app reloaded but no HTTP middleware was captured — the entry did not re-run listen().`);
|
|
985
|
+
if (await settleBootInit() && httpCaptured && !moostMiddleware) logger.error(`⚠️ Moost app reloaded but no HTTP middleware was captured — the entry did not re-run listen().`);
|
|
677
986
|
} catch (error) {
|
|
678
987
|
bootError = error;
|
|
679
988
|
logger.error(`✖️ Failed to reload Moost App: ${error.message}`);
|
|
@@ -690,11 +999,16 @@ function moostVite(options) {
|
|
|
690
999
|
}
|
|
691
1000
|
};
|
|
692
1001
|
for (const adapter of adapters) adapter.ssrLoadModule = ssrImport;
|
|
693
|
-
moostRestartCleanup(adapters, options.onEject);
|
|
1002
|
+
await moostRestartCleanup(adapters, options.onEject);
|
|
694
1003
|
await ssrImport(options.entry);
|
|
1004
|
+
await settleBootInit();
|
|
695
1005
|
server.middlewares.use(async (req, res, next) => {
|
|
696
1006
|
await drainReload();
|
|
697
1007
|
if (options.middleware && prefixes && !matchesPrefix(req.url || "", prefixes)) return next();
|
|
1008
|
+
if (bootError) {
|
|
1009
|
+
answerBootError(res);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
698
1012
|
if (moostMiddleware) {
|
|
699
1013
|
if (options.middleware) {
|
|
700
1014
|
pendingNextMap.set(req, next);
|
|
@@ -703,12 +1017,6 @@ function moostVite(options) {
|
|
|
703
1017
|
}
|
|
704
1018
|
return moostMiddleware(req, res);
|
|
705
1019
|
}
|
|
706
|
-
if (bootError) {
|
|
707
|
-
res.statusCode = 502;
|
|
708
|
-
res.setHeader("Content-Type", "text/plain");
|
|
709
|
-
res.end(`Moost app failed to load: ${bootError.message}`);
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
1020
|
next();
|
|
713
1021
|
});
|
|
714
1022
|
},
|
|
@@ -748,6 +1056,10 @@ function moostVite(options) {
|
|
|
748
1056
|
const url = req.originalUrl || req.url || "/";
|
|
749
1057
|
try {
|
|
750
1058
|
await drainReload();
|
|
1059
|
+
if (bootError) {
|
|
1060
|
+
answerBootError(res);
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
751
1063
|
let template = await fs.readFile((0, node_path.resolve)(server.config.root, "index.html"), "utf8");
|
|
752
1064
|
template = await server.transformIndexHtml(url, template);
|
|
753
1065
|
const { render } = await server.ssrLoadModule(options.ssrEntry);
|
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";
|
|
@@ -220,19 +220,63 @@ function _define_property(obj, key, value) {
|
|
|
220
220
|
* A reference to a shared LogsStorage instance that persists
|
|
221
221
|
* across init cycles in dev mode.
|
|
222
222
|
*/ let logsStorage;
|
|
223
|
+
/** Whether {@link patchMoostHandlerLogging} already installed its wrappers in this process. */ let installed = false;
|
|
223
224
|
/**
|
|
224
|
-
*
|
|
225
|
+
* The slot {@link captureMoostInit} fills: receives every `init()` promise the
|
|
226
|
+
* wrapper below produces. Unset in prod/test, where nothing consumes a capture.
|
|
227
|
+
*/ let onInitCaptured;
|
|
228
|
+
/**
|
|
229
|
+
* Hands every `Moost.prototype.init()` promise to `onCapture`, letting a boot
|
|
230
|
+
* await the app's **initialization** instead of only its module evaluation.
|
|
231
|
+
*
|
|
232
|
+
* The documented entry shape is `app.adapter(http).listen(port)` followed by an
|
|
233
|
+
* **un-awaited** `app.init()`: the HTTP middleware is therefore captured *before*
|
|
234
|
+
* init finishes, and nothing else observes a rejecting init — a bind error, a DI
|
|
235
|
+
* audit error or a throwing `@MoostInit` hook would leave the dev server serving
|
|
236
|
+
* a half-booted app (the routes bound before the failure answer 200, the rest
|
|
237
|
+
* fall through to the SPA fallback) instead of the plugin's 502. That is also
|
|
238
|
+
* why the plugin's `bootError` gate runs BEFORE the captured middleware, and why
|
|
239
|
+
* the SSR fallback checks it too: `listen()` ran first and left both a middleware
|
|
240
|
+
* and the local-fetch hook behind.
|
|
241
|
+
*
|
|
242
|
+
* Patches the plugin's **own** `moost` import, exactly like the handler logging
|
|
243
|
+
* does: in dev the runtime is externalized, so the plugin and the app share one
|
|
244
|
+
* native `moost` instance (the premise the DI eject in `restart-cleanup` rests on
|
|
245
|
+
* too, and what `ssrExternalCheck` warns about when it stops holding). Unlike the
|
|
246
|
+
* adapters there is nothing to re-establish per reload — a static import is never
|
|
247
|
+
* re-evaluated — and unlike them it must NOT be pulled through the SSR module
|
|
248
|
+
* runner: importing `moost` there registers it as the runner's first module and
|
|
249
|
+
* makes subsequent hot updates re-evaluate the entry only, serving every one of
|
|
250
|
+
* its dependencies (controllers included) from the stale cache.
|
|
251
|
+
*
|
|
252
|
+
* There is exactly ONE `init` wrapper per process (installed by
|
|
253
|
+
* {@link patchMoostHandlerLogging}) and this only fills its slot, so the newest
|
|
254
|
+
* plugin instance wins: Vite re-imports the config on every config-file
|
|
255
|
+
* `server.restart()`, re-running the plugin factory, and a per-instance wrapper
|
|
256
|
+
* would stack on the previous one and keep the dead instance's scope alive.
|
|
257
|
+
*
|
|
258
|
+
* @param onCapture receives each captured init promise (already marked handled).
|
|
259
|
+
*/ function captureMoostInit(onCapture) {
|
|
260
|
+
onInitCaptured = onCapture;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Patches Moost’s .init() and .logMappedHandler() methods — once per process —
|
|
264
|
+
* so that, during dev:
|
|
225
265
|
* - Only newly added handlers are logged normally.
|
|
226
266
|
* - Removed handlers are logged/stroked out to indicate removal.
|
|
227
267
|
* - Prevents repeated logs from flooding the console across multiple hot reloads.
|
|
268
|
+
* - Every init() promise reaches the {@link captureMoostInit} slot, if filled.
|
|
228
269
|
*/ function patchMoostHandlerLogging() {
|
|
270
|
+
if (installed) return;
|
|
271
|
+
installed = true;
|
|
229
272
|
const origInit = Moost.prototype.init;
|
|
273
|
+
const origLogMappedHandler = Moost.prototype.logMappedHandler;
|
|
230
274
|
/**
|
|
231
|
-
*
|
|
275
|
+
* The logging body of the patched Moost.init:
|
|
232
276
|
* 1. Start a new recording cycle in LogsStorage.
|
|
233
277
|
* 2. Call the real .init().
|
|
234
278
|
* 3. End the recording, logging any removed handlers.
|
|
235
|
-
*/
|
|
279
|
+
*/ const initWithLogging = async function() {
|
|
236
280
|
if (!logsStorage) logsStorage = new LogsStorage();
|
|
237
281
|
logsStorage.startRecording();
|
|
238
282
|
await origInit.call(this);
|
|
@@ -240,7 +284,14 @@ function _define_property(obj, key, value) {
|
|
|
240
284
|
origLogMappedHandler.call(this, item.eventName, item.classConstructor, item.method, true, "❌ ");
|
|
241
285
|
});
|
|
242
286
|
};
|
|
243
|
-
|
|
287
|
+
Moost.prototype.init = function init() {
|
|
288
|
+
const promise = initWithLogging.call(this);
|
|
289
|
+
if (onInitCaptured) {
|
|
290
|
+
promise.catch(() => {});
|
|
291
|
+
onInitCaptured(promise);
|
|
292
|
+
}
|
|
293
|
+
return promise;
|
|
294
|
+
};
|
|
244
295
|
/**
|
|
245
296
|
* Monkey-patch Moost.logMappedHandler:
|
|
246
297
|
* If we haven’t seen this particular route+class+method combination
|
|
@@ -260,14 +311,23 @@ function _define_property(obj, key, value) {
|
|
|
260
311
|
/**
|
|
261
312
|
* Clean up Moost’s global containers and optionally remove specific instances from the registry.
|
|
262
313
|
*
|
|
314
|
+
* Every instance actually removed from a registry is **disposed** before the
|
|
315
|
+
* caches are dropped: its `@MoostDispose` hooks (or `Symbol.asyncDispose` /
|
|
316
|
+
* `Symbol.dispose`) are awaited, so a singleton owning a connection, consumer,
|
|
317
|
+
* timer or file handle releases it instead of leaking one copy per reload. An
|
|
318
|
+
* instance kept by an `onEject` veto is never disposed. A failing hook is
|
|
319
|
+
* warned about and the reload continues.
|
|
320
|
+
*
|
|
263
321
|
* @param {Set<string>} [cleanupInstances] A set of module IDs to remove from the registry.
|
|
264
|
-
|
|
322
|
+
* @returns the instances that were ejected (and therefore disposed)
|
|
323
|
+
*/ async function moostRestartCleanup(adapters, onEject, cleanupInstances) {
|
|
265
324
|
const logger = getLogger();
|
|
266
325
|
const infact = getMoostInfact();
|
|
267
326
|
const { registry, scopes } = infact;
|
|
268
327
|
const registries = [registry, ...Object.values(scopes)];
|
|
269
328
|
infact._cleanup();
|
|
270
329
|
const mate = getMoostMate();
|
|
330
|
+
/** Instances removed from a registry by this run — disposed below, before the caches drop. */ const ejected = [];
|
|
271
331
|
if (cleanupInstances) {
|
|
272
332
|
for (const reg of registries) {
|
|
273
333
|
for (const key of Object.getOwnPropertySymbols(reg)) {
|
|
@@ -276,6 +336,7 @@ function _define_property(obj, key, value) {
|
|
|
276
336
|
if (viteId && cleanupInstances.has(viteId)) {
|
|
277
337
|
logger.debug(`🔃 Replacing "${constructorName(instance)}"`);
|
|
278
338
|
delete reg[key];
|
|
339
|
+
ejected.push(instance);
|
|
279
340
|
}
|
|
280
341
|
}
|
|
281
342
|
for (const key of Object.getOwnPropertySymbols(reg)) {
|
|
@@ -283,25 +344,39 @@ function _define_property(obj, key, value) {
|
|
|
283
344
|
scanParams(instance, (type) => {
|
|
284
345
|
if ((type === Moost || type instanceof Moost || type.prototype instanceof Moost) && (!onEject || onEject(instance, type))) {
|
|
285
346
|
delete reg[key];
|
|
347
|
+
ejected.push(instance);
|
|
286
348
|
logger.debug(`✖️ Ejecting "${constructorName(instance)}" (depends on re-instantiated "Moost")`);
|
|
287
349
|
return true;
|
|
288
350
|
}
|
|
289
351
|
for (const adapter of adapters) if (adapter.compare(type) && (!onEject || onEject(instance, type))) {
|
|
290
352
|
delete reg[key];
|
|
353
|
+
ejected.push(instance);
|
|
291
354
|
logger.debug(`✖️ Ejecting "${constructorName(instance)}" (depends on re-instantiated "${adapter.constructor.name}")`);
|
|
292
355
|
return true;
|
|
293
356
|
}
|
|
294
357
|
});
|
|
295
358
|
}
|
|
296
|
-
clearDependantRegistry(reg, onEject);
|
|
359
|
+
clearDependantRegistry(reg, onEject, ejected);
|
|
297
360
|
}
|
|
298
361
|
infact.registry = registry;
|
|
299
362
|
infact.scopes = scopes;
|
|
300
363
|
}
|
|
364
|
+
await disposeEjected(ejected);
|
|
301
365
|
getMoostMate()._cleanup();
|
|
302
366
|
clearGlobalWooks();
|
|
367
|
+
return ejected;
|
|
303
368
|
}
|
|
304
|
-
function
|
|
369
|
+
/** Awaits the `@MoostDispose` hooks of the ejected instances; never throws. */ async function disposeEjected(ejected) {
|
|
370
|
+
if (ejected.length === 0) return;
|
|
371
|
+
const logger = getLogger();
|
|
372
|
+
const { disposed, errors } = await disposeInstances(ejected, {
|
|
373
|
+
logger,
|
|
374
|
+
onError: "warn"
|
|
375
|
+
});
|
|
376
|
+
for (const instance of disposed) logger.debug(`♻️ Disposed "${constructorName(instance)}"`);
|
|
377
|
+
for (const e of errors) logger.warn(`⚠️ Dispose hook "${constructorName(e.instance)}.${String(e.method)}" failed — the reload continues`);
|
|
378
|
+
}
|
|
379
|
+
function clearDependantRegistry(registry, onEject, ejected) {
|
|
305
380
|
const logger = getLogger();
|
|
306
381
|
const objSet = /* @__PURE__ */ new Set();
|
|
307
382
|
let somethingIsDeleted = true;
|
|
@@ -313,7 +388,10 @@ function clearDependantRegistry(registry, onEject) {
|
|
|
313
388
|
}
|
|
314
389
|
for (const key of Object.getOwnPropertySymbols(registry)) {
|
|
315
390
|
const instance = registry[key];
|
|
316
|
-
if (checkAndEject(instance, objSet, onEject, registry, key, logger))
|
|
391
|
+
if (checkAndEject(instance, objSet, onEject, registry, key, logger)) {
|
|
392
|
+
ejected.push(instance);
|
|
393
|
+
somethingIsDeleted = true;
|
|
394
|
+
}
|
|
317
395
|
}
|
|
318
396
|
}
|
|
319
397
|
}
|
|
@@ -346,6 +424,173 @@ function constructorName(i) {
|
|
|
346
424
|
return Object.getPrototypeOf(i).constructor.name;
|
|
347
425
|
}
|
|
348
426
|
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region packages/vite/src/ssr-externals-check.ts
|
|
429
|
+
/**
|
|
430
|
+
* Packages that make up the moost/wooks runtime. They mint per-module Symbol
|
|
431
|
+
* slot keys, so the whole set must resolve to a single module instance in the
|
|
432
|
+
* production SSR graph. This is the scope of the single-instance *guard*
|
|
433
|
+
* (force-bundling under an explicit `noExternal` list) — the build *check*
|
|
434
|
+
* watches the wider {@link SHARED_STATE_PACKAGE_PATTERNS}.
|
|
435
|
+
*/ const RUNTIME_PACKAGE_PATTERNS = [
|
|
436
|
+
/^moost($|\/)/,
|
|
437
|
+
/^@moostjs\//,
|
|
438
|
+
/^@wooksjs\//,
|
|
439
|
+
/^wooks($|\/)/
|
|
440
|
+
];
|
|
441
|
+
/**
|
|
442
|
+
* Packages the build check watches by default: the moost/wooks runtime plus the
|
|
443
|
+
* `@atscript/*` family. All of them keep module-level state — Symbol slot keys,
|
|
444
|
+
* DI/model registries, class identity used by `instanceof` — so a second copy
|
|
445
|
+
* loaded by Node breaks lookups that the bundled copy filled in.
|
|
446
|
+
*
|
|
447
|
+
* This is deliberately wider than {@link RUNTIME_PACKAGE_PATTERNS}: the guard
|
|
448
|
+
* that force-bundles packages covers the runtime only, while the check merely
|
|
449
|
+
* reports what the output shows.
|
|
450
|
+
*/ const SHARED_STATE_PACKAGE_PATTERNS = [...RUNTIME_PACKAGE_PATTERNS, /^@atscript\//];
|
|
451
|
+
function escapeRegExp(input) {
|
|
452
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Build the watched-package patterns for the check: the defaults plus the
|
|
456
|
+
* user's extras. A `RegExp` is used as is, a string ending with `/` matches a
|
|
457
|
+
* scope/prefix (`'@acme/'` → every `@acme/*` package), any other string is an
|
|
458
|
+
* exact package name.
|
|
459
|
+
*/ function compilePackagePatterns(extra) {
|
|
460
|
+
const patterns = [...SHARED_STATE_PACKAGE_PATTERNS];
|
|
461
|
+
for (const item of extra ?? []) if (item instanceof RegExp) patterns.push(item);
|
|
462
|
+
else if (item.endsWith("/")) patterns.push(new RegExp(`^${escapeRegExp(item)}`));
|
|
463
|
+
else patterns.push(new RegExp(`^${escapeRegExp(item)}$`));
|
|
464
|
+
return patterns;
|
|
465
|
+
}
|
|
466
|
+
const BUILTINS = new Set(builtinModules);
|
|
467
|
+
/** Bare npm package name of an import specifier (`vue/server-renderer` → `vue`). */ function npmPackageName(id) {
|
|
468
|
+
if (!id || id.startsWith(".") || id.startsWith("/") || id.startsWith("\0")) return;
|
|
469
|
+
if (id.startsWith("node:") || id.startsWith("data:") || id.startsWith("virtual:")) return;
|
|
470
|
+
const parts = id.split("/");
|
|
471
|
+
const name = id.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
472
|
+
if (!name || id.startsWith("@") && parts.length < 2) return;
|
|
473
|
+
return BUILTINS.has(name) ? void 0 : name;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Package names the bundler inlined into the output, derived from the module
|
|
477
|
+
* ids of the emitted chunks. Ids under `node_modules` name their package after
|
|
478
|
+
* the LAST `node_modules/` segment, which also yields the right name for pnpm
|
|
479
|
+
* store paths (`…/node_modules/.pnpm/pkg@1.0.0/node_modules/pkg/index.mjs`).
|
|
480
|
+
* Virtual ids and app sources (no `node_modules`) are ignored.
|
|
481
|
+
*/ function bundledPackagesFromModuleIds(moduleIds) {
|
|
482
|
+
const packages = /* @__PURE__ */ new Set();
|
|
483
|
+
const marker = "/node_modules/";
|
|
484
|
+
for (const raw of moduleIds) {
|
|
485
|
+
if (!raw || raw.startsWith("\0")) continue;
|
|
486
|
+
const id = raw.replaceAll("\\", "/");
|
|
487
|
+
const at = id.lastIndexOf(marker);
|
|
488
|
+
if (at === -1) continue;
|
|
489
|
+
const name = npmPackageName(id.slice(at + 14));
|
|
490
|
+
if (name) packages.add(name);
|
|
491
|
+
}
|
|
492
|
+
return packages;
|
|
493
|
+
}
|
|
494
|
+
function readPkgJson(dir) {
|
|
495
|
+
try {
|
|
496
|
+
return JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
497
|
+
} catch {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Locate `name`'s package directory the way Node would from `fromDir`:
|
|
503
|
+
* `<fromDir>/node_modules/<name>`, then each ancestor's `node_modules`.
|
|
504
|
+
* Returns the realpath so pnpm's symlinked layout resolves to the store entry,
|
|
505
|
+
* whose own `node_modules` sibling holds the package's dependencies.
|
|
506
|
+
*/ function findPackageDir(name, fromDir) {
|
|
507
|
+
let dir = fromDir;
|
|
508
|
+
for (;;) {
|
|
509
|
+
const candidate = join(dir, "node_modules", name);
|
|
510
|
+
if (existsSync(join(candidate, "package.json"))) try {
|
|
511
|
+
return realpathSync(candidate);
|
|
512
|
+
} catch {
|
|
513
|
+
return candidate;
|
|
514
|
+
}
|
|
515
|
+
const parent = dirname(dir);
|
|
516
|
+
if (parent === dir) return;
|
|
517
|
+
dir = parent;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/** Deps of `pkg` that are watched packages AND were bundled into the output. */ function splitDepsOf(pkg, patterns, bundledPackages) {
|
|
521
|
+
const names = /* @__PURE__ */ new Set();
|
|
522
|
+
for (const group of [
|
|
523
|
+
pkg.dependencies,
|
|
524
|
+
pkg.peerDependencies,
|
|
525
|
+
pkg.optionalDependencies
|
|
526
|
+
]) for (const dep of Object.keys(group ?? {})) if (bundledPackages.has(dep) && patterns.some((re) => re.test(dep))) names.add(dep);
|
|
527
|
+
return [...names];
|
|
528
|
+
}
|
|
529
|
+
/** Upper bound on visited packages — keeps a pathological tree from stalling the build. */ const MAX_VISITED = 5e3;
|
|
530
|
+
/**
|
|
531
|
+
* Given the bare specifiers left in the SSR build output (i.e. the packages the
|
|
532
|
+
* bundler externalized) and the package names it bundled, find every package
|
|
533
|
+
* Node will load at runtime that depends on a bundled shared-state package.
|
|
534
|
+
* Such a package resolves its own copy from `node_modules` while the bundled
|
|
535
|
+
* copy lives inside `dist/server` — two instances, split module state.
|
|
536
|
+
*
|
|
537
|
+
* Walks the dependency tree beneath each external (Node loads a package's deps
|
|
538
|
+
* from its own resolution root, so transitive consumers matter too), but never
|
|
539
|
+
* descends into a bundled dep: the fix belongs to the external consumer.
|
|
540
|
+
* Packages that are not installed are skipped silently.
|
|
541
|
+
*/ function findSplitPackages(opts) {
|
|
542
|
+
const patterns = opts.patterns ?? SHARED_STATE_PACKAGE_PATTERNS;
|
|
543
|
+
const found = [];
|
|
544
|
+
const visited = /* @__PURE__ */ new Set();
|
|
545
|
+
const queue = [];
|
|
546
|
+
for (const id of opts.externalIds) {
|
|
547
|
+
const name = npmPackageName(id);
|
|
548
|
+
if (name && !visited.has(`root:${name}`)) {
|
|
549
|
+
visited.add(`root:${name}`);
|
|
550
|
+
queue.push({
|
|
551
|
+
name,
|
|
552
|
+
fromDir: opts.root,
|
|
553
|
+
via: []
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
while (queue.length > 0 && visited.size < MAX_VISITED) {
|
|
558
|
+
const { name, fromDir, via } = queue.shift();
|
|
559
|
+
const dir = findPackageDir(name, fromDir);
|
|
560
|
+
if (!dir || visited.has(dir)) continue;
|
|
561
|
+
visited.add(dir);
|
|
562
|
+
const pkg = readPkgJson(dir);
|
|
563
|
+
if (!pkg) continue;
|
|
564
|
+
const splitDeps = splitDepsOf(pkg, patterns, opts.bundledPackages);
|
|
565
|
+
if (splitDeps.length > 0) found.push({
|
|
566
|
+
name,
|
|
567
|
+
via,
|
|
568
|
+
splitDeps
|
|
569
|
+
});
|
|
570
|
+
for (const dep of Object.keys(pkg.dependencies ?? {})) if (!opts.bundledPackages.has(dep)) queue.push({
|
|
571
|
+
name: dep,
|
|
572
|
+
fromDir: dir,
|
|
573
|
+
via: [...via, name]
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
return found;
|
|
577
|
+
}
|
|
578
|
+
/** Human-readable build warning for {@link findSplitPackages} results. */ function formatSplitPackagesWarning(splits) {
|
|
579
|
+
const lines = splits.map((s) => {
|
|
580
|
+
const chain = s.via.length > 0 ? ` (loaded via external ${s.via.join(" → ")})` : "";
|
|
581
|
+
const deps = s.splitDeps.map((d) => `${d} (bundled)`).join(", ");
|
|
582
|
+
return ` - ${s.name}${chain} depends on ${deps}`;
|
|
583
|
+
});
|
|
584
|
+
const direct = [...new Set(splits.map((s) => s.via.length > 0 ? s.via[0] : s.name))];
|
|
585
|
+
return [
|
|
586
|
+
"These externalized packages depend on packages that are bundled into dist/server, so Node will load a second copy of them from node_modules:",
|
|
587
|
+
...lines,
|
|
588
|
+
"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.",
|
|
589
|
+
`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.`,
|
|
590
|
+
"Set ssrExternalCheck: false in moostVite() to silence this check, or ssrExternalCheck: { packages: [...] } to watch more packages."
|
|
591
|
+
].join("\n");
|
|
592
|
+
}
|
|
593
|
+
|
|
349
594
|
//#endregion
|
|
350
595
|
//#region packages/vite/src/moost-vite.ts
|
|
351
596
|
/** Regex checks */ const REG_HAS_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/m;
|
|
@@ -379,6 +624,22 @@ function generatedServerEntry(root) {
|
|
|
379
624
|
writeFileSync(entryPath, DEFAULT_SERVER_ENTRY_CODE);
|
|
380
625
|
return entryPath;
|
|
381
626
|
}
|
|
627
|
+
/**
|
|
628
|
+
* Read the emitted SSR bundle: the packages inlined into it (from every chunk's
|
|
629
|
+
* module ids) and the bare specifiers it still imports (the externalized ones).
|
|
630
|
+
*/ function scanSsrBundle(bundle) {
|
|
631
|
+
const externalIds = /* @__PURE__ */ new Set();
|
|
632
|
+
const moduleIds = [];
|
|
633
|
+
for (const chunk of Object.values(bundle)) {
|
|
634
|
+
if (chunk.type !== "chunk") continue;
|
|
635
|
+
moduleIds.push(...chunk.moduleIds ?? []);
|
|
636
|
+
for (const id of [...chunk.imports ?? [], ...chunk.dynamicImports ?? []]) if (!(id in bundle) && npmPackageName(id)) externalIds.add(id);
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
externalIds,
|
|
640
|
+
bundledPackages: bundledPackagesFromModuleIds(moduleIds)
|
|
641
|
+
};
|
|
642
|
+
}
|
|
382
643
|
function moostVite(options) {
|
|
383
644
|
const externals = options.externals ?? true;
|
|
384
645
|
const prefixes = normalizePrefixes(options.prefix);
|
|
@@ -402,6 +663,11 @@ function moostVite(options) {
|
|
|
402
663
|
*/ let bootGeneration = 0;
|
|
403
664
|
let bootingGeneration = 0;
|
|
404
665
|
/** Whether the HTTP listen() patch has ever captured a middleware (i.e. this is an HTTP app). */ let httpCaptured = false;
|
|
666
|
+
/**
|
|
667
|
+
* The `app.init()` promises the current boot handed us (see `captureMoostInit`) —
|
|
668
|
+
* several when one entry boots several apps, none for a boot that never calls
|
|
669
|
+
* `init()`. Reset by `beginBoot()`, consumed by `settleBootInit()`.
|
|
670
|
+
*/ let bootInitPromises = [];
|
|
405
671
|
/** Module IDs awaiting DI cleanup — consumed in runReload; see ejectApp. */ let pendingCleanup = null;
|
|
406
672
|
/** In middleware mode: maps req → next() for the onNoMatch callback */ const pendingNextMap = /* @__PURE__ */ new WeakMap();
|
|
407
673
|
const adapters = [
|
|
@@ -471,6 +737,43 @@ function moostVite(options) {
|
|
|
471
737
|
reloadRequired = true;
|
|
472
738
|
};
|
|
473
739
|
patchMoostHandlerLogging();
|
|
740
|
+
captureMoostInit((promise) => {
|
|
741
|
+
if (bootingGeneration !== bootGeneration) {
|
|
742
|
+
logger.error(`⚠️ A stale Moost boot called init() (boot generation ${bootingGeneration}, latest ${bootGeneration}) — an HMR reload race; a follow-up reload will replace it.`);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
bootInitPromises.push(promise);
|
|
746
|
+
});
|
|
747
|
+
/** Marks the boot about to run as authoritative for the current generation. */ const beginBoot = () => {
|
|
748
|
+
bootingGeneration = bootGeneration;
|
|
749
|
+
bootInitPromises = [];
|
|
750
|
+
};
|
|
751
|
+
/**
|
|
752
|
+
* Finishes a boot: awaits the `init()` promise(s) the entry that just executed
|
|
753
|
+
* handed us, so the boot is only "done" once the app is fully initialized —
|
|
754
|
+
* every controller bound, every `@MoostInit` hook run (see `captureMoostInit`).
|
|
755
|
+
* Owns `bootError`: cleared on success, set and logged when an init rejected;
|
|
756
|
+
* returns whether the boot succeeded.
|
|
757
|
+
*/ const settleBootInit = async () => {
|
|
758
|
+
const initPromises = bootInitPromises;
|
|
759
|
+
bootInitPromises = [];
|
|
760
|
+
try {
|
|
761
|
+
await Promise.all(initPromises);
|
|
762
|
+
bootError = null;
|
|
763
|
+
return true;
|
|
764
|
+
} catch (error) {
|
|
765
|
+
bootError = error;
|
|
766
|
+
logger.error(`✖️ Moost app init failed: ${error.message}`);
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
/** Answers a request while `bootError` is set (see the gate in configureServer). */ const answerBootError = (res) => {
|
|
771
|
+
res.statusCode = 502;
|
|
772
|
+
res.setHeader("Content-Type", "text/plain");
|
|
773
|
+
res.end(`Moost app failed to load: ${bootError.message}`);
|
|
774
|
+
};
|
|
775
|
+
/** Set by the middleware-mode `config` hook: the consumer externalized the runtime themselves. */ let runtimeExternalized = false;
|
|
776
|
+
let resolvedConfig;
|
|
474
777
|
return [{
|
|
475
778
|
name: PLUGIN_NAME,
|
|
476
779
|
enforce: "pre",
|
|
@@ -503,14 +806,8 @@ function moostVite(options) {
|
|
|
503
806
|
let ssrNoExternal = userNoExternal === true ? true : userNoExternal !== void 0 ? [...Array.isArray(userNoExternal) ? userNoExternal : [userNoExternal], ourPlugin] : isBuild ? true : [ourPlugin];
|
|
504
807
|
const userExternal = cfg.ssr?.external;
|
|
505
808
|
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];
|
|
809
|
+
runtimeExternalized = (ssrExternal ?? []).some((e) => typeof e === "string" && RUNTIME_PACKAGE_PATTERNS.some((re) => re.test(e)));
|
|
810
|
+
if (isBuild && Array.isArray(ssrNoExternal) && !runtimeExternalized) ssrNoExternal = [...ssrNoExternal, ...RUNTIME_PACKAGE_PATTERNS];
|
|
514
811
|
return {
|
|
515
812
|
...options.ssrEntry && { appType: "custom" },
|
|
516
813
|
ssr: {
|
|
@@ -588,6 +885,19 @@ function moostVite(options) {
|
|
|
588
885
|
ssrFetchForwarding: options.ssrFetchForwarding
|
|
589
886
|
};
|
|
590
887
|
config.__moostViteHttpRef = moostHttpRef;
|
|
888
|
+
resolvedConfig = config;
|
|
889
|
+
},
|
|
890
|
+
generateBundle(_outputOptions, bundle) {
|
|
891
|
+
const cfg = resolvedConfig;
|
|
892
|
+
if (!cfg || cfg.command !== "build" || !options.middleware || options.ssrExternalCheck === false || this.environment?.name !== "ssr") return;
|
|
893
|
+
const { externalIds, bundledPackages } = scanSsrBundle(bundle);
|
|
894
|
+
const splits = findSplitPackages({
|
|
895
|
+
root: cfg.root,
|
|
896
|
+
externalIds,
|
|
897
|
+
bundledPackages,
|
|
898
|
+
patterns: compilePackagePatterns(typeof options.ssrExternalCheck === "object" ? options.ssrExternalCheck.packages : void 0)
|
|
899
|
+
});
|
|
900
|
+
if (splits.length > 0) cfg.logger.warn(`\n[${PLUGIN_NAME}] ${formatSplitPackagesWarning(splits)}\n`);
|
|
591
901
|
},
|
|
592
902
|
async transform(code, id) {
|
|
593
903
|
if (!id.endsWith(".ts")) return null;
|
|
@@ -625,7 +935,7 @@ function moostVite(options) {
|
|
|
625
935
|
reloadRequired = false;
|
|
626
936
|
reloadPromise = null;
|
|
627
937
|
pendingCleanup = null;
|
|
628
|
-
|
|
938
|
+
beginBoot();
|
|
629
939
|
const runReload = () => {
|
|
630
940
|
reloadRequired = false;
|
|
631
941
|
console.log();
|
|
@@ -633,16 +943,15 @@ function moostVite(options) {
|
|
|
633
943
|
console.log();
|
|
634
944
|
return (async () => {
|
|
635
945
|
try {
|
|
636
|
-
|
|
946
|
+
beginBoot();
|
|
637
947
|
const cleanupInstances = pendingCleanup ?? void 0;
|
|
638
948
|
pendingCleanup = null;
|
|
639
|
-
moostRestartCleanup(adapters, options.onEject, cleanupInstances);
|
|
949
|
+
await moostRestartCleanup(adapters, options.onEject, cleanupInstances);
|
|
640
950
|
for (const adapter of adapters) if (adapter.detected) await adapter.init();
|
|
641
951
|
const entryModule = await getEntryModule(server.environments.ssr.moduleGraph);
|
|
642
952
|
if (entryModule?.transformResult) server.environments.ssr.moduleGraph.invalidateModule(entryModule);
|
|
643
953
|
await ssrImport(options.entry);
|
|
644
|
-
|
|
645
|
-
if (httpCaptured && !moostMiddleware) logger.error(`⚠️ Moost app reloaded but no HTTP middleware was captured — the entry did not re-run listen().`);
|
|
954
|
+
if (await settleBootInit() && httpCaptured && !moostMiddleware) logger.error(`⚠️ Moost app reloaded but no HTTP middleware was captured — the entry did not re-run listen().`);
|
|
646
955
|
} catch (error) {
|
|
647
956
|
bootError = error;
|
|
648
957
|
logger.error(`✖️ Failed to reload Moost App: ${error.message}`);
|
|
@@ -659,11 +968,16 @@ function moostVite(options) {
|
|
|
659
968
|
}
|
|
660
969
|
};
|
|
661
970
|
for (const adapter of adapters) adapter.ssrLoadModule = ssrImport;
|
|
662
|
-
moostRestartCleanup(adapters, options.onEject);
|
|
971
|
+
await moostRestartCleanup(adapters, options.onEject);
|
|
663
972
|
await ssrImport(options.entry);
|
|
973
|
+
await settleBootInit();
|
|
664
974
|
server.middlewares.use(async (req, res, next) => {
|
|
665
975
|
await drainReload();
|
|
666
976
|
if (options.middleware && prefixes && !matchesPrefix(req.url || "", prefixes)) return next();
|
|
977
|
+
if (bootError) {
|
|
978
|
+
answerBootError(res);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
667
981
|
if (moostMiddleware) {
|
|
668
982
|
if (options.middleware) {
|
|
669
983
|
pendingNextMap.set(req, next);
|
|
@@ -672,12 +986,6 @@ function moostVite(options) {
|
|
|
672
986
|
}
|
|
673
987
|
return moostMiddleware(req, res);
|
|
674
988
|
}
|
|
675
|
-
if (bootError) {
|
|
676
|
-
res.statusCode = 502;
|
|
677
|
-
res.setHeader("Content-Type", "text/plain");
|
|
678
|
-
res.end(`Moost app failed to load: ${bootError.message}`);
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
989
|
next();
|
|
682
990
|
});
|
|
683
991
|
},
|
|
@@ -709,6 +1017,10 @@ function moostVite(options) {
|
|
|
709
1017
|
const url = req.originalUrl || req.url || "/";
|
|
710
1018
|
try {
|
|
711
1019
|
await drainReload();
|
|
1020
|
+
if (bootError) {
|
|
1021
|
+
answerBootError(res);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
712
1024
|
let template = await fs.readFile(resolve(server.config.root, "index.html"), "utf8");
|
|
713
1025
|
template = await server.transformIndexHtml(url, template);
|
|
714
1026
|
const { render } = await server.ssrLoadModule(options.ssrEntry);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moostjs/vite",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.37",
|
|
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.37",
|
|
62
|
+
"moost": "^0.6.37"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
65
|
"sirv": {
|