adonisjs-server-stats 1.11.1 → 1.11.2

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.
@@ -18,6 +18,16 @@ export async function initDashboardStore(opts) {
18
18
  log.info('dashboard: SQLite store ready');
19
19
  }
20
20
  catch (err) {
21
+ const errMsg = err?.message ?? String(err);
22
+ const errStack = err?.stack ?? '(no stack)';
23
+ // Write to file since console.error may be swallowed by AdonisJS watcher
24
+ try {
25
+ const { writeFileSync } = await import('node:fs');
26
+ writeFileSync(app.makePath('server-stats-dashboard-error.log'), `${new Date().toISOString()}\nError: ${errMsg}\nStack: ${errStack}\n`);
27
+ }
28
+ catch { }
29
+ console.error('[server-stats] dashboard start error:', errMsg);
30
+ console.error('[server-stats] dashboard start stack:', errStack);
21
31
  logDashboardStartError(err);
22
32
  onResult({
23
33
  dashboardStore: null,
@@ -107,6 +117,7 @@ function pipeDashRequests(debugStore, dashboardStore) {
107
117
  });
108
118
  }
109
119
  function logDashboardStartError(err) {
120
+ const errMsg = err?.message ?? String(err);
110
121
  const c = classifyDashboardError(err);
111
122
  if (c === 'missing-dep') {
112
123
  log.block('Dashboard could not start — missing dependencies. Install with:', [
@@ -115,7 +126,11 @@ function logDashboardStartError(err) {
115
126
  '',
116
127
  dim('Dashboard has been disabled for this session.'),
117
128
  dim('Everything else (stats bar, debug panel) works without it.'),
129
+ '',
130
+ dim(`Error: ${errMsg}`),
118
131
  ]);
132
+ if (err?.stack)
133
+ console.error(err.stack);
119
134
  return;
120
135
  }
121
136
  if (c === 'timeout') {
@@ -30,7 +30,7 @@ export declare function setupLogStreamBroadcast(transmit: {
30
30
  broadcast(ch: string, d: unknown): void;
31
31
  }, channelName: string, pinoHookActive: boolean, makePath: (...parts: string[]) => string): LogStreamService | null;
32
32
  /** Check if dashboard dependencies are available. Returns true if available, false if missing. */
33
- export declare function checkDashboardDepsHelper(config: ResolvedServerStatsConfig, _app: ApplicationService): Promise<boolean>;
33
+ export declare function checkDashboardDepsHelper(config: ResolvedServerStatsConfig, app: ApplicationService): Promise<boolean>;
34
34
  /** Register the Edge.js plugin if Edge is available. Returns true if registered. */
35
35
  export declare function registerEdgePluginHelper(app: ApplicationService, config: ResolvedServerStatsConfig): Promise<boolean>;
36
36
  /** Set up the publisher-only email bridge for non-web environments. */
@@ -67,31 +67,58 @@ export function setupLogStreamBroadcast(transmit, channelName, pinoHookActive, m
67
67
  }
68
68
  // ── checkDashboardDepsHelper ────────────────────────────────────
69
69
  /** Check if dashboard dependencies are available. Returns true if available, false if missing. */
70
- export async function checkDashboardDepsHelper(config, _app) {
70
+ export async function checkDashboardDepsHelper(config, app) {
71
71
  if (!config.devToolbar?.enabled || !config.devToolbar.dashboard)
72
72
  return true;
73
- const { appImport } = await import('../utils/app_import.js');
73
+ const { createRequire } = await import('node:module');
74
+ const { join } = await import('node:path');
75
+ const { pathToFileURL } = await import('node:url');
74
76
  const missing = [];
75
- try {
76
- await appImport('knex');
77
- }
78
- catch {
77
+ const errors = [];
78
+ // Try multiple resolution strategies for monorepo compatibility
79
+ const tryImport = async (specifier) => {
80
+ // Strategy 1: resolve from app root (handles monorepo hoisting)
81
+ const appRoot = app.makePath('');
82
+ try {
83
+ const appRequire = createRequire(join(appRoot, 'package.json'));
84
+ const resolved = appRequire.resolve(specifier);
85
+ await import(pathToFileURL(resolved).href);
86
+ return true;
87
+ }
88
+ catch { }
89
+ // Strategy 2: resolve from cwd (original approach)
90
+ try {
91
+ const cwdRequire = createRequire(join(process.cwd(), 'package.json'));
92
+ const resolved = cwdRequire.resolve(specifier);
93
+ await import(pathToFileURL(resolved).href);
94
+ return true;
95
+ }
96
+ catch { }
97
+ // Strategy 3: bare import (works when installed normally)
98
+ try {
99
+ await import(specifier);
100
+ return true;
101
+ }
102
+ catch (err) {
103
+ errors.push(`${specifier}: ${err?.message ?? err}`);
104
+ return false;
105
+ }
106
+ };
107
+ if (!(await tryImport('knex')))
79
108
  missing.push('knex');
80
- }
81
- try {
82
- await appImport('better-sqlite3');
83
- }
84
- catch {
109
+ if (!(await tryImport('better-sqlite3')))
85
110
  missing.push('better-sqlite3');
86
- }
87
111
  if (missing.length === 0)
88
112
  return true;
89
113
  log.block(`Dashboard requires ${missing.join(' and ')}. Install with:`, [
90
114
  '',
91
115
  bold(`npm install ${missing.join(' ')}`),
92
116
  '',
93
- dim('Dashboard routes have been skipped for now.'),
117
+ dim('Dashboard has been disabled for this session.'),
94
118
  dim('Everything else (stats bar, debug panel) works without it.'),
119
+ ...(errors.length > 0
120
+ ? ['', dim('Resolution errors:'), ...errors.map((e) => dim(` ${e}`))]
121
+ : []),
95
122
  ]);
96
123
  return false;
97
124
  }
@@ -56,6 +56,9 @@ export default class ServerStatsProvider {
56
56
  }
57
57
  setVerbose(config.verbose);
58
58
  log.info('booting...');
59
+ // Register app root for module resolution (critical in monorepos)
60
+ const { setAppRoot } = await import('../utils/app_import.js');
61
+ setAppRoot(this.app.makePath(''));
59
62
  if (config.shouldShow)
60
63
  setShouldShow(config.shouldShow);
61
64
  await this.registerRoutes(config);
@@ -155,11 +158,14 @@ export default class ServerStatsProvider {
155
158
  const pfx = buildExcludedPrefixes(tc, config.endpoint);
156
159
  if (pfx.length > 0)
157
160
  setExcludedPrefixes(pfx);
158
- if (!this.debugStore)
161
+ if (!this.debugStore) {
162
+ log.warn('debugStore is null after toolbar setup — apiController will not be created');
159
163
  return;
164
+ }
160
165
  const { DataAccess: DA } = await import('../data/data_access.js');
161
166
  const { ApiController: AC } = await import('../controller/api_controller.js');
162
167
  this.apiController = new AC(new DA(this.debugStore, () => this.dashboardStore, this.app.makePath('logs', 'adonisjs.log')));
168
+ log.info('apiController created');
163
169
  }
164
170
  async setupLogBroadcast() {
165
171
  const t = (await this.resolve('transmit'));
@@ -172,7 +178,8 @@ export default class ServerStatsProvider {
172
178
  try {
173
179
  return await this.app.container.make(binding);
174
180
  }
175
- catch {
181
+ catch (err) {
182
+ log.info(`resolve('${binding}') failed: ${err?.message ?? err}`);
176
183
  return null;
177
184
  }
178
185
  }
@@ -117,8 +117,15 @@ export function applyToolbarResult(result, tc, provider) {
117
117
  provider.transmitChannels.push(ch);
118
118
  }
119
119
  provider.emailBridgeRedis = result.emailBridgeRedis;
120
- if (!tc.dashboard || !provider.dashboardDepsAvailable)
120
+ if (!tc.dashboard) {
121
+ log.info('dashboard: skipped — dashboard is disabled in config');
121
122
  return;
123
+ }
124
+ if (!provider.dashboardDepsAvailable) {
125
+ log.info('dashboard: skipped — dependencies (knex/better-sqlite3) not available');
126
+ return;
127
+ }
128
+ log.info('dashboard: scheduling async initialization...');
122
129
  setImmediate(() => {
123
130
  initDashboardStore({
124
131
  tc,
@@ -133,9 +140,17 @@ export function applyToolbarResult(result, tc, provider) {
133
140
  provider.dashboardController = r.dashboardController;
134
141
  provider.dashboardLogStream = r.dashboardLogStream;
135
142
  provider.dashboardBroadcastTimer = r.dashboardBroadcastTimer;
143
+ if (r.dashboardController) {
144
+ log.info('dashboard: controller ready — API endpoints are now live');
145
+ }
146
+ else {
147
+ log.warn('dashboard: init completed but controller is null — dashboard API will return 503');
148
+ }
136
149
  },
137
150
  }).catch((e) => {
138
- log.warn(`dashboard setup failed: ${e?.message ?? e}`);
151
+ log.warn(`dashboard: setup failed: ${e?.message ?? e}`);
152
+ if (e?.stack)
153
+ log.warn(e.stack);
139
154
  });
140
155
  });
141
156
  }
@@ -1,16 +1,27 @@
1
+ import { log } from '../utils/logger.js';
1
2
  function bindDash(getController, method) {
2
3
  return async (ctx) => {
3
4
  const controller = getController();
4
- if (!controller)
5
- return ctx.response.serviceUnavailable({ error: 'Dashboard is starting up, please retry' });
5
+ if (!controller) {
6
+ log.warn(`503 on ${ctx.request.url()} dashboardController is null (method: ${method}). ` +
7
+ 'Dashboard may still be initializing or failed to start. Check for earlier errors.');
8
+ return ctx.response.serviceUnavailable({
9
+ error: 'Dashboard is not available. Check server logs for initialization errors.',
10
+ });
11
+ }
6
12
  return controller[method].call(controller, ctx);
7
13
  };
8
14
  }
9
15
  function bindApi(getApi, fn) {
10
16
  return async (ctx) => {
11
17
  const api = getApi();
12
- if (!api)
13
- return ctx.response.serviceUnavailable({ error: 'Dashboard is starting up, please retry' });
18
+ if (!api) {
19
+ log.warn(`503 on ${ctx.request.url()} apiController is null. ` +
20
+ 'Dashboard may still be initializing or failed to start. Check for earlier errors.');
21
+ return ctx.response.serviceUnavailable({
22
+ error: 'Dashboard is not available. Check server logs for initialization errors.',
23
+ });
24
+ }
14
25
  return fn(api, ctx);
15
26
  };
16
27
  }
@@ -1,20 +1,21 @@
1
1
  /**
2
- * Dynamically import a module, resolving from the app root (`process.cwd()`)
2
+ * Set the AdonisJS application root for module resolution.
3
+ * In monorepos, cwd may be the workspace root while the app lives
4
+ * in a sub-directory — this ensures we also look there.
5
+ */
6
+ export declare function setAppRoot(appRoot: string): void;
7
+ /**
8
+ * Dynamically import a module, resolving from the app root
3
9
  * instead of from this package's location.
4
10
  *
5
- * This is critical when `adonisjs-server-stats` is symlinked into the app
6
- * (e.g. via `file:../../adonisjs-server-stats` in package.json). Without
7
- * this, Node.js dereferences the symlink and resolves bare specifiers from
8
- * the package's *real* directory tree — which may contain devDependency
9
- * stubs with different module identity than the app's actual packages.
10
- *
11
- * Falls back to a normal `import()` when `createRequire` resolution fails
12
- * (e.g. when the package is installed normally, not symlinked).
11
+ * Tries multiple strategies for monorepo/symlink compatibility:
12
+ * 1. createRequire from each resolution root + ESM import
13
+ * 2. createRequire from each resolution root + CJS require (for native modules)
14
+ * 3. Bare import() fallback
13
15
  */
14
16
  export declare function appImport<T = unknown>(specifier: string): Promise<T>;
15
17
  /**
16
18
  * Same as {@link appImport} but also returns the resolved file path.
17
- * Useful for diagnostic logging.
18
19
  */
19
20
  export declare function appImportWithPath<T = unknown>(specifier: string): Promise<{
20
21
  module: T;
@@ -2,43 +2,93 @@ import { createRequire } from 'node:module';
2
2
  import { join } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  /**
5
- * Dynamically import a module, resolving from the app root (`process.cwd()`)
5
+ * Resolution roots used by appImport, in priority order.
6
+ * By default uses process.cwd(). Call {@link setAppRoot} to prepend
7
+ * the AdonisJS app root (important in monorepos where cwd != app root).
8
+ */
9
+ const resolutionRoots = [process.cwd()];
10
+ /**
11
+ * Set the AdonisJS application root for module resolution.
12
+ * In monorepos, cwd may be the workspace root while the app lives
13
+ * in a sub-directory — this ensures we also look there.
14
+ */
15
+ export function setAppRoot(appRoot) {
16
+ if (!resolutionRoots.includes(appRoot)) {
17
+ resolutionRoots.unshift(appRoot);
18
+ }
19
+ }
20
+ /**
21
+ * Dynamically import a module, resolving from the app root
6
22
  * instead of from this package's location.
7
23
  *
8
- * This is critical when `adonisjs-server-stats` is symlinked into the app
9
- * (e.g. via `file:../../adonisjs-server-stats` in package.json). Without
10
- * this, Node.js dereferences the symlink and resolves bare specifiers from
11
- * the package's *real* directory tree — which may contain devDependency
12
- * stubs with different module identity than the app's actual packages.
13
- *
14
- * Falls back to a normal `import()` when `createRequire` resolution fails
15
- * (e.g. when the package is installed normally, not symlinked).
24
+ * Tries multiple strategies for monorepo/symlink compatibility:
25
+ * 1. createRequire from each resolution root + ESM import
26
+ * 2. createRequire from each resolution root + CJS require (for native modules)
27
+ * 3. Bare import() fallback
16
28
  */
17
29
  export async function appImport(specifier) {
18
- try {
19
- const appRequire = createRequire(join(process.cwd(), 'package.json'));
20
- const resolved = appRequire.resolve(specifier);
21
- return await import(pathToFileURL(resolved).href);
30
+ const errors = [];
31
+ for (const root of resolutionRoots) {
32
+ try {
33
+ const appRequire = createRequire(join(root, 'package.json'));
34
+ const resolved = appRequire.resolve(specifier);
35
+ // Strategy A: ESM import via file URL
36
+ try {
37
+ return await import(pathToFileURL(resolved).href);
38
+ }
39
+ catch { }
40
+ // Strategy B: CJS require (works for native addons like better-sqlite3)
41
+ try {
42
+ return appRequire(specifier);
43
+ }
44
+ catch { }
45
+ }
46
+ catch (err) {
47
+ errors.push(err);
48
+ }
22
49
  }
23
- catch {
24
- // Fallback: normal import (works when not symlinked)
50
+ // Strategy C: bare import (works when installed normally, not symlinked)
51
+ try {
25
52
  return await import(specifier);
26
53
  }
54
+ catch (err) {
55
+ errors.push(err);
56
+ }
57
+ throw errors[0] ?? new Error(`Could not import '${specifier}'`);
27
58
  }
28
59
  /**
29
60
  * Same as {@link appImport} but also returns the resolved file path.
30
- * Useful for diagnostic logging.
31
61
  */
32
62
  export async function appImportWithPath(specifier) {
33
- try {
34
- const appRequire = createRequire(join(process.cwd(), 'package.json'));
35
- const resolved = appRequire.resolve(specifier);
36
- const module = await import(pathToFileURL(resolved).href);
37
- return { module: module, resolvedPath: resolved };
63
+ const errors = [];
64
+ for (const root of resolutionRoots) {
65
+ try {
66
+ const appRequire = createRequire(join(root, 'package.json'));
67
+ const resolved = appRequire.resolve(specifier);
68
+ // Strategy A: ESM import via file URL
69
+ try {
70
+ const module = await import(pathToFileURL(resolved).href);
71
+ return { module: module, resolvedPath: resolved };
72
+ }
73
+ catch { }
74
+ // Strategy B: CJS require (works for native addons like better-sqlite3)
75
+ try {
76
+ const module = appRequire(specifier);
77
+ return { module, resolvedPath: resolved };
78
+ }
79
+ catch { }
80
+ }
81
+ catch (err) {
82
+ errors.push(err);
83
+ }
38
84
  }
39
- catch {
40
- // Fallback: normal import (works when not symlinked)
85
+ // Strategy C: bare import
86
+ try {
41
87
  const module = await import(specifier);
42
88
  return { module: module, resolvedPath: `(bare import: ${specifier})` };
43
89
  }
90
+ catch (err) {
91
+ errors.push(err);
92
+ }
93
+ throw errors[0] ?? new Error(`Could not import '${specifier}'`);
44
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adonisjs-server-stats",
3
- "version": "1.11.1",
3
+ "version": "1.11.2",
4
4
  "description": "Real-time server monitoring for AdonisJS v6 applications",
5
5
  "keywords": [
6
6
  "adonisjs",