millas 0.2.12-beta → 0.2.12-beta-1

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.
Files changed (52) hide show
  1. package/package.json +3 -16
  2. package/src/auth/Auth.js +13 -8
  3. package/src/auth/AuthController.js +3 -1
  4. package/src/auth/AuthUser.js +98 -0
  5. package/src/cli.js +1 -1
  6. package/src/commands/serve.js +81 -110
  7. package/src/container/AppInitializer.js +158 -0
  8. package/src/container/Application.js +278 -253
  9. package/src/container/HttpServer.js +156 -0
  10. package/src/container/MillasApp.js +23 -280
  11. package/src/container/MillasConfig.js +163 -0
  12. package/src/core/auth.js +9 -0
  13. package/src/core/db.js +8 -0
  14. package/src/core/foundation.js +67 -0
  15. package/src/core/http.js +11 -0
  16. package/src/core/mail.js +6 -0
  17. package/src/core/queue.js +7 -0
  18. package/src/core/validation.js +29 -0
  19. package/src/facades/Admin.js +1 -1
  20. package/src/facades/Auth.js +22 -39
  21. package/src/facades/Cache.js +21 -10
  22. package/src/facades/Database.js +1 -1
  23. package/src/facades/Events.js +18 -17
  24. package/src/facades/Facade.js +197 -0
  25. package/src/facades/Http.js +42 -45
  26. package/src/facades/Log.js +25 -49
  27. package/src/facades/Mail.js +27 -32
  28. package/src/facades/Queue.js +22 -15
  29. package/src/facades/Storage.js +18 -10
  30. package/src/facades/Url.js +53 -0
  31. package/src/http/HttpClient.js +673 -0
  32. package/src/http/ResponseDispatcher.js +18 -111
  33. package/src/http/UrlGenerator.js +375 -0
  34. package/src/http/WelcomePage.js +273 -0
  35. package/src/http/adapters/ExpressAdapter.js +315 -0
  36. package/src/http/adapters/HttpAdapter.js +168 -0
  37. package/src/http/adapters/index.js +9 -0
  38. package/src/index.js +5 -144
  39. package/src/logger/formatters/PrettyFormatter.js +15 -5
  40. package/src/logger/internal.js +2 -2
  41. package/src/logger/patchConsole.js +91 -81
  42. package/src/middleware/MiddlewareRegistry.js +62 -82
  43. package/src/orm/migration/ModelInspector.js +339 -340
  44. package/src/providers/AuthServiceProvider.js +9 -5
  45. package/src/providers/CacheStorageServiceProvider.js +3 -1
  46. package/src/providers/EventServiceProvider.js +2 -1
  47. package/src/providers/LogServiceProvider.js +3 -2
  48. package/src/providers/MailServiceProvider.js +3 -2
  49. package/src/providers/QueueServiceProvider.js +3 -2
  50. package/src/router/Router.js +119 -152
  51. package/src/scaffold/templates.js +8 -7
  52. package/src/facades/Validation.js +0 -69
@@ -0,0 +1,168 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * HttpAdapter
5
+ *
6
+ * Abstract interface between the Millas kernel and any underlying HTTP engine.
7
+ * The kernel never imports Express (or Fastify, or Hono) directly — it only
8
+ * calls the methods defined here.
9
+ *
10
+ * To add a new HTTP engine, create a class that extends HttpAdapter and
11
+ * implements every method. The kernel works unchanged.
12
+ *
13
+ * ── Implemented by ───────────────────────────────────────────────────────────
14
+ *
15
+ * ExpressAdapter (default, ships with Millas)
16
+ * FastifyAdapter (future)
17
+ * HonoAdapter (future)
18
+ *
19
+ * ── Lifecycle ────────────────────────────────────────────────────────────────
20
+ *
21
+ * 1. adapter.applyBodyParsers() — JSON + urlencoded
22
+ * 2. adapter.applyMiddleware(fn) — any raw adapter-level middleware
23
+ * 3. adapter.mountRoute(verb, path, handlers) — register app routes
24
+ * 4. adapter.mountWelcome(handler) — optional dev welcome page
25
+ * 5. adapter.mountNotFound() — 404 handler
26
+ * 6. adapter.mountErrorHandler() — global error handler
27
+ * 7. await adapter.listen(port, host) — start accepting connections
28
+ * 8. adapter.close() — graceful shutdown
29
+ */
30
+ class HttpAdapter {
31
+
32
+ // ── Setup ──────────────────────────────────────────────────────────────────
33
+
34
+ /**
35
+ * Apply JSON + urlencoded body parsers.
36
+ * Called once during bootstrap, before any routes are mounted.
37
+ */
38
+ applyBodyParsers() {
39
+ throw new Error(`${this.constructor.name} must implement applyBodyParsers()`);
40
+ }
41
+
42
+ /**
43
+ * Apply a single raw middleware function at the adapter level.
44
+ * Used for things like helmet(), compression() that are engine-specific.
45
+ *
46
+ * @param {Function} fn — adapter-native middleware function
47
+ */
48
+ applyMiddleware(fn) {
49
+ throw new Error(`${this.constructor.name} must implement applyMiddleware(fn)`);
50
+ }
51
+
52
+ // ── Route mounting ─────────────────────────────────────────────────────────
53
+
54
+ /**
55
+ * Register a route handler.
56
+ *
57
+ * @param {string} verb — 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS'
58
+ * @param {string} path — e.g. '/users/:id'
59
+ * @param {Function[]} handlers — [middleware..., terminalHandler]
60
+ * Each handler is a Millas kernel handler
61
+ * (expressReq, expressRes, expressNext) already
62
+ * converted by the adapter's wrapKernelHandler().
63
+ */
64
+ mountRoute(verb, path, handlers) {
65
+ throw new Error(`${this.constructor.name} must implement mountRoute(verb, path, handlers)`);
66
+ }
67
+
68
+ /**
69
+ * Mount the dev welcome page for GET /.
70
+ * Only called when no user route covers GET /.
71
+ *
72
+ * @param {Function} handler — adapter-native handler function
73
+ */
74
+ mountWelcome(handler) {
75
+ throw new Error(`${this.constructor.name} must implement mountWelcome(handler)`);
76
+ }
77
+
78
+ /**
79
+ * Mount the 404 fallback handler.
80
+ * Must be called AFTER all routes and mountWelcome().
81
+ */
82
+ mountNotFound() {
83
+ throw new Error(`${this.constructor.name} must implement mountNotFound()`);
84
+ }
85
+
86
+ /**
87
+ * Mount the global error handler.
88
+ * Must be called LAST — after mountNotFound().
89
+ */
90
+ mountErrorHandler() {
91
+ throw new Error(`${this.constructor.name} must implement mountErrorHandler()`);
92
+ }
93
+
94
+ // ── Request / Response bridge ──────────────────────────────────────────────
95
+
96
+ /**
97
+ * Wrap a Millas kernel handler function into an adapter-native handler.
98
+ *
99
+ * The kernel handler signature is:
100
+ * (millaCtx: RequestContext, trackedNext: Function) => Promise<MillasResponse>
101
+ *
102
+ * The adapter wraps this into whatever its native handler signature is,
103
+ * e.g. (req, res, next) for Express.
104
+ *
105
+ * @param {Function} kernelFn
106
+ * @param {string} displayName — for error messages
107
+ * @param {object} container — DI container
108
+ * @returns {Function} adapter-native handler
109
+ */
110
+ wrapKernelHandler(kernelFn, displayName, container) {
111
+ throw new Error(`${this.constructor.name} must implement wrapKernelHandler()`);
112
+ }
113
+
114
+ /**
115
+ * Wrap a Millas middleware instance into an adapter-native handler.
116
+ *
117
+ * @param {object} instance — Millas Middleware instance with handle(ctx, next)
118
+ * @param {object} container — DI container
119
+ * @returns {Function} adapter-native handler
120
+ */
121
+ wrapMiddleware(instance, container) {
122
+ throw new Error(`${this.constructor.name} must implement wrapMiddleware()`);
123
+ }
124
+
125
+ /**
126
+ * Dispatch a MillasResponse to the underlying engine's response object.
127
+ *
128
+ * @param {MillasResponse} response
129
+ * @param {*} nativeRes — e.g. Express res
130
+ */
131
+ dispatch(response, nativeRes) {
132
+ throw new Error(`${this.constructor.name} must implement dispatch(response, nativeRes)`);
133
+ }
134
+
135
+ // ── Server lifecycle ───────────────────────────────────────────────────────
136
+
137
+ /**
138
+ * Start listening on port/host.
139
+ * Returns a Promise that resolves once the server is bound.
140
+ *
141
+ * @param {number} port
142
+ * @param {string} host
143
+ * @returns {Promise<void>}
144
+ */
145
+ listen(port, host) {
146
+ throw new Error(`${this.constructor.name} must implement listen(port, host)`);
147
+ }
148
+
149
+ /**
150
+ * Gracefully close the server.
151
+ * Returns a Promise that resolves once all connections are drained.
152
+ *
153
+ * @returns {Promise<void>}
154
+ */
155
+ close() {
156
+ throw new Error(`${this.constructor.name} must implement close()`);
157
+ }
158
+
159
+ /**
160
+ * The name of this adapter, used in logs and error messages.
161
+ * @returns {string}
162
+ */
163
+ get name() {
164
+ return this.constructor.name;
165
+ }
166
+ }
167
+
168
+ module.exports = HttpAdapter;
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ const HttpAdapter = require('./HttpAdapter');
4
+ const ExpressAdapter = require('./ExpressAdapter');
5
+
6
+ module.exports = {
7
+ HttpAdapter,
8
+ ExpressAdapter,
9
+ };
package/src/index.js CHANGED
@@ -1,147 +1,8 @@
1
1
  'use strict';
2
2
 
3
- // ── HTTP Layer ────────────────────────────────────────────────────
4
- const {
5
- MillasRequest,
6
- MillasResponse,
7
- ResponseDispatcher,
8
- jsonify,
9
- view,
10
- redirect,
11
- text,
12
- file,
13
- empty,
14
- abort,
15
- notFound,
16
- unauthorized,
17
- forbidden,
18
- } = require('./http/index');
19
- const RequestContext = require('./http/RequestContext');
3
+ const Millas = require('./container/MillasApp');
20
4
 
21
- // ── Validation ────────────────────────────────────────────────────
22
- const {
23
- Validator,
24
- BaseValidator,
25
- StringValidator,
26
- EmailValidator,
27
- NumberValidator,
28
- BooleanValidator,
29
- DateValidator,
30
- ArrayValidator,
31
- ObjectValidator,
32
- FileValidator,
33
- string,
34
- email,
35
- number,
36
- boolean,
37
- date,
38
- array,
39
- object: objectField,
40
- file: fileField,
41
- } = require('./validation/Validator');
42
- const MillasApp = require('./container/MillasApp');
43
- // ── HTTP Layer (old) ──────────────────────────────────────────────
44
- const Controller = require('./controller/Controller');
45
- const Middleware = require('./middleware/Middleware');
46
- const MiddlewarePipeline = require('./middleware/MiddlewarePipeline');
47
- const CorsMiddleware = require('./middleware/CorsMiddleware');
48
- const ThrottleMiddleware = require('./middleware/ThrottleMiddleware');
49
- const LogMiddleware = require('./middleware/LogMiddleware');
50
- const HttpError = require('./errors/HttpError');
51
-
52
- // ── DI Container ─────────────────────────────────────────────────
53
- const Container = require('./container/Container');
54
- const Application = require('./container/Application');
55
- const ServiceProvider = require('./providers/ServiceProvider');
56
- const ProviderRegistry = require('./providers/ProviderRegistry');
57
-
58
- // ── ORM ───────────────────────────────────────────────────────────
59
- const { Model, fields, QueryBuilder, DatabaseManager,
60
- SchemaBuilder, MigrationRunner, ModelInspector } = require('./orm');
61
- const DatabaseServiceProvider = require('./providers/DatabaseServiceProvider');
62
-
63
- // ── Auth ──────────────────────────────────────────────────────────
64
- const Auth = require('./auth/Auth');
65
- const Hasher = require('./auth/Hasher');
66
- const JwtDriver = require('./auth/JwtDriver');
67
- const AuthMiddleware = require('./auth/AuthMiddleware');
68
- const RoleMiddleware = require('./auth/RoleMiddleware');
69
- const AuthController = require('./auth/AuthController');
70
- const AuthServiceProvider = require('./providers/AuthServiceProvider');
71
-
72
- // ── Mail ──────────────────────────────────────────────────────────
73
- const { Mail, MailMessage, TemplateEngine,
74
- SmtpDriver, SendGridDriver, MailgunDriver, LogDriver } = require('./mail');
75
- const MailServiceProvider = require('./providers/MailServiceProvider');
76
-
77
- // ── Queue ─────────────────────────────────────────────────────────
78
- const Queue = require('./queue/Queue');
79
- const Job = require('./queue/Job');
80
- const QueueWorker = require('./queue/workers/QueueWorker');
81
- const { dispatch } = require('./queue/Queue');
82
- const QueueServiceProvider = require('./providers/QueueServiceProvider');
83
-
84
- // ── Events ────────────────────────────────────────────────────────
85
- const EventEmitter = require('./events/EventEmitter');
86
- const Event = require('./events/Event');
87
- const Listener = require('./events/Listener');
88
- const { emit } = require('./events/EventEmitter');
89
- const EventServiceProvider = require('./providers/EventServiceProvider');
90
-
91
- // ── Cache ─────────────────────────────────────────────────────────
92
- const Cache = require('./cache/Cache');
93
- const MemoryDriver = require('./cache/drivers/MemoryDriver');
94
- const FileDriver = require('./cache/drivers/FileDriver');
95
- const NullDriver = require('./cache/drivers/NullDriver');
96
- const { CacheServiceProvider, StorageServiceProvider } = require('./providers/CacheStorageServiceProvider');
97
-
98
- // ── Storage ───────────────────────────────────────────────────────
99
- const Storage = require('./storage/Storage');
100
- const LocalDriver = require('./storage/drivers/LocalDriver');
101
-
102
- module.exports = {
103
- // Millas App
104
- MillasApp,
105
- // ── Millas HTTP layer ──────────────────────────────────────────
106
- MillasRequest, MillasResponse, ResponseDispatcher, RequestContext,
107
- jsonify, view, redirect, text, send_file:file, empty,
108
- abort, notFound, unauthorized, forbidden,
109
- // ── Validation ────────────────────────────────────────────────
110
- Validator,
111
- BaseValidator,
112
- StringValidator, EmailValidator, NumberValidator, BooleanValidator,
113
- DateValidator, ArrayValidator, ObjectValidator, FileValidator,
114
- string, email, number, boolean, date, array,
115
- object: objectField,
116
- file: fileField,
117
- // HTTP
118
- Controller, Middleware, MiddlewarePipeline,
119
- CorsMiddleware, ThrottleMiddleware, LogMiddleware, HttpError,
120
- // DI
121
- Container, Application, ServiceProvider, ProviderRegistry,
122
- // ORM
123
- Model, fields, QueryBuilder, DatabaseManager, SchemaBuilder,
124
- MigrationRunner, ModelInspector, DatabaseServiceProvider,
125
- // Auth
126
- Auth, Hasher, JwtDriver, AuthMiddleware, RoleMiddleware,
127
- AuthController, AuthServiceProvider,
128
- // Mail
129
- Mail, MailMessage, TemplateEngine,
130
- SmtpDriver, SendGridDriver, MailgunDriver, LogDriver, MailServiceProvider,
131
- // Queue
132
- Queue, Job, QueueWorker, dispatch, QueueServiceProvider,
133
- // Events
134
- EventEmitter, Event, Listener, emit, EventServiceProvider,
135
- // Cache
136
- Cache, MemoryDriver, FileDriver, NullDriver, CacheServiceProvider,
137
- // Storage
138
- Storage, LocalDriver, StorageServiceProvider,
139
- };
140
-
141
- // ── Admin ─────────────────────────────────────────────────────────
142
- const { Admin, AdminResource, AdminField, AdminFilter } = require('./admin');
143
- const AdminServiceProvider = require('./providers/AdminServiceProvider');
144
-
145
- Object.assign(module.exports, {
146
- Admin, AdminResource, AdminField, AdminFilter, AdminServiceProvider,
147
- });
5
+ /**
6
+ * @module millas
7
+ */
8
+ module.exports = { Millas };
@@ -56,19 +56,29 @@ class PrettyFormatter {
56
56
  parts.push(`${b}${tagStr}${r}`);
57
57
  }
58
58
 
59
- // Message
60
- parts.push(`${c}${message}${r}`);
59
+ // Message (handle multi-line)
60
+ const lines = message.split('\n');
61
+ parts.push(`${c}${lines[0]}${r}`);
62
+
63
+ let output = parts.join(' ');
64
+
65
+ // Continuation lines (aligned with first line)
66
+ if (lines.length > 1) {
67
+ const prefix = parts.slice(0, -1).map(p => p.replace(/\x1b\[[0-9;]*m/g, '')).join(' ');
68
+ const indent = ' '.repeat(prefix.length + 2);
69
+ for (let i = 1; i < lines.length; i++) {
70
+ output += `\n${indent}${c}${lines[i]}${r}`;
71
+ }
72
+ }
61
73
 
62
74
  // Context object
63
75
  if (context !== undefined && context !== null) {
64
76
  const ctx = typeof context === 'object'
65
77
  ? JSON.stringify(context, null, 0)
66
78
  : String(context);
67
- parts.push(`${d}${ctx}${r}`);
79
+ output += ` ${d}${ctx}${r}`;
68
80
  }
69
81
 
70
- let output = parts.join(' ');
71
-
72
82
  // Error stack
73
83
  if (error instanceof Error) {
74
84
  output += `\n${d}${error.stack || error.message}${r}`;
@@ -64,12 +64,12 @@ const MillasLog = new Logger();
64
64
  // from the framework itself unless you opt in to lower levels.
65
65
  MillasLog.configure({
66
66
  defaultTag: 'Millas',
67
- minLevel: LEVELS.WARN,
67
+ minLevel: LEVELS.VERBOSE,
68
68
  channel: new ConsoleChannel({
69
69
  formatter: new PrettyFormatter({
70
70
  colour: process.stdout.isTTY !== false,
71
71
  }),
72
- minLevel: LEVELS.WARN,
72
+ minLevel: LEVELS.VERBOSE,
73
73
  }),
74
74
  });
75
75
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { LEVELS } = require('./levels');
3
+ const {LEVELS} = require('./levels');
4
4
 
5
5
  /**
6
6
  * patchConsole(Log, defaultTag)
@@ -25,44 +25,54 @@ const { LEVELS } = require('./levels');
25
25
  * console.dir → Log.d (DEBUG)
26
26
  */
27
27
  function patchConsole(Log, defaultTag = 'App') {
28
- // Save originals — restore() puts these back
29
- const originals = {
30
- log: console.log.bind(console),
31
- info: console.info.bind(console),
32
- warn: console.warn.bind(console),
33
- error: console.error.bind(console),
34
- debug: console.debug.bind(console),
35
- trace: console.trace.bind(console),
36
- dir: console.dir.bind(console),
37
- };
38
-
39
- // Build a dispatcher for a given level
40
- function make(level) {
41
- return function (...args) {
42
- const { message, context, error } = parse(args);
43
- Log._emit({
44
- level,
45
- tag: defaultTag,
46
- message: message || '',
47
- context,
48
- error,
28
+
29
+ // Save originals restore() puts these back
30
+ const originals = {
31
+ log: console.log.bind(console),
32
+ info: console.info.bind(console),
33
+ warn: console.warn.bind(console),
34
+ error: console.error.bind(console),
35
+ debug: console.debug.bind(console),
36
+ trace: console.trace.bind(console),
37
+ dir: console.dir.bind(console),
38
+ };
39
+
40
+ // Build a dispatcher for a given level
41
+ function make(level) {
42
+ return function (...args) {
43
+ const {message, context, error} = parse(args);
44
+ Log._emit({
45
+ level,
46
+ tag: defaultTag,
47
+ message: message || '',
48
+ context,
49
+ error,
50
+ timestamp: new Date().toISOString(),
51
+ pid: process.pid,
52
+ });
53
+ return true
54
+ };
55
+ }
56
+
57
+ console.log = make(LEVELS.INFO);
58
+ console.info = make(LEVELS.INFO);
59
+ console.warn = make(LEVELS.WARN);
60
+ console.error = make(LEVELS.ERROR);
61
+ console.debug = make(LEVELS.DEBUG);
62
+ console.trace = make(LEVELS.VERBOSE);
63
+ console.dir = (obj) => Log._emit({
64
+ level: LEVELS.DEBUG,
65
+ tag: defaultTag,
66
+ message: '',
67
+ context: obj,
68
+ error: undefined,
49
69
  timestamp: new Date().toISOString(),
50
- pid: process.pid,
51
- });
70
+ pid: process.pid
71
+ });
72
+
73
+ return function restore() {
74
+ Object.assign(console, originals);
52
75
  };
53
- }
54
-
55
- console.log = make(LEVELS.INFO);
56
- console.info = make(LEVELS.INFO);
57
- console.warn = make(LEVELS.WARN);
58
- console.error = make(LEVELS.ERROR);
59
- console.debug = make(LEVELS.DEBUG);
60
- console.trace = make(LEVELS.VERBOSE);
61
- console.dir = (obj) => Log._emit({ level: LEVELS.DEBUG, tag: defaultTag, message: '', context: obj, error: undefined, timestamp: new Date().toISOString(), pid: process.pid });
62
-
63
- return function restore() {
64
- Object.assign(console, originals);
65
- };
66
76
  }
67
77
 
68
78
  // ── Argument parser ───────────────────────────────────────────────────────────
@@ -78,58 +88,58 @@ function patchConsole(Log, defaultTag = 'App') {
78
88
  // console.log('a', 'b', 'c') → message: 'a b c'
79
89
 
80
90
  function parse(args) {
81
- if (args.length === 0) {
82
- return { message: '', context: undefined, error: undefined };
83
- }
84
-
85
- if (args.length === 1) {
86
- const a = args[0];
87
- if (a instanceof Error) return { message: a.message, context: undefined, error: a };
88
- if (typeof a === 'object' && a !== null) return { message: '', context: a, error: undefined };
89
- return { message: String(a), context: undefined, error: undefined };
90
- }
91
-
92
- const [first, ...rest] = args;
93
-
94
- // First arg is an Error
95
- if (first instanceof Error) {
96
- return { message: first.message, context: rest.length ? rest : undefined, error: first };
97
- }
98
-
99
- // First arg is a string message
100
- if (typeof first === 'string') {
101
- // Single extra arg
102
- if (rest.length === 1) {
103
- const r = rest[0];
104
- if (r instanceof Error) return { message: first, context: undefined, error: r };
105
- if (typeof r === 'object' && r !== null) return { message: first, context: r, error: undefined };
106
- // Scalar extra: append to message (console.log('count:', 42))
107
- return { message: first + ' ' + String(r), context: undefined, error: undefined };
91
+ if (args.length === 0) {
92
+ return {message: '', context: undefined, error: undefined};
108
93
  }
109
94
 
110
- // Multiple extra args find a trailing Error, collect the rest as context
111
- const lastArg = rest[rest.length - 1];
112
- if (lastArg instanceof Error) {
113
- const ctx = rest.slice(0, -1);
114
- return { message: first, context: ctx.length ? ctx : undefined, error: lastArg };
95
+ if (args.length === 1) {
96
+ const a = args[0];
97
+ if (a instanceof Error) return {message: a.message, context: undefined, error: a};
98
+ if (typeof a === 'object' && a !== null) return {message: '', context: a, error: undefined};
99
+ return {message: String(a), context: undefined, error: undefined};
115
100
  }
116
101
 
117
- // All strings/scalars join into message
118
- if (rest.every(r => typeof r !== 'object' || r === null)) {
119
- return { message: [first, ...rest].map(String).join(' '), context: undefined, error: undefined };
102
+ const [first, ...rest] = args;
103
+
104
+ // First arg is an Error
105
+ if (first instanceof Error) {
106
+ return {message: first.message, context: rest.length ? rest : undefined, error: first};
120
107
  }
121
108
 
122
- // Mixed put extras in context
123
- return { message: first, context: rest, error: undefined };
124
- }
109
+ // First arg is a string message
110
+ if (typeof first === 'string') {
111
+ // Single extra arg
112
+ if (rest.length === 1) {
113
+ const r = rest[0];
114
+ if (r instanceof Error) return {message: first, context: undefined, error: r};
115
+ if (typeof r === 'object' && r !== null) return {message: first, context: r, error: undefined};
116
+ // Scalar extra: append to message (console.log('count:', 42))
117
+ return {message: first + ' ' + String(r), context: undefined, error: undefined};
118
+ }
119
+
120
+ // Multiple extra args — find a trailing Error, collect the rest as context
121
+ const lastArg = rest[rest.length - 1];
122
+ if (lastArg instanceof Error) {
123
+ const ctx = rest.slice(0, -1);
124
+ return {message: first, context: ctx.length ? ctx : undefined, error: lastArg};
125
+ }
126
+
127
+ // All strings/scalars — join into message
128
+ if (rest.every(r => typeof r !== 'object' || r === null)) {
129
+ return {message: [first, ...rest].map(String).join(' '), context: undefined, error: undefined};
130
+ }
131
+
132
+ // Mixed — put extras in context
133
+ return {message: first, context: rest, error: undefined};
134
+ }
125
135
 
126
- // First arg is an object
127
- if (typeof first === 'object' && first !== null) {
128
- return { message: '', context: first, error: undefined };
129
- }
136
+ // First arg is an object
137
+ if (typeof first === 'object' && first !== null) {
138
+ return {message: '', context: first, error: undefined};
139
+ }
130
140
 
131
- // Fallback — join everything as a string
132
- return { message: args.map(String).join(' '), context: undefined, error: undefined };
141
+ // Fallback — join everything as a string
142
+ return {message: args.map(String).join(' '), context: undefined, error: undefined};
133
143
  }
134
144
 
135
145
  module.exports = patchConsole;