millas 0.1.0

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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/bin/millas.js +6 -0
  4. package/package.json +56 -0
  5. package/src/admin/Admin.js +617 -0
  6. package/src/admin/index.js +13 -0
  7. package/src/admin/resources/AdminResource.js +317 -0
  8. package/src/auth/Auth.js +254 -0
  9. package/src/auth/AuthController.js +188 -0
  10. package/src/auth/AuthMiddleware.js +67 -0
  11. package/src/auth/Hasher.js +51 -0
  12. package/src/auth/JwtDriver.js +74 -0
  13. package/src/auth/RoleMiddleware.js +44 -0
  14. package/src/cache/Cache.js +231 -0
  15. package/src/cache/drivers/FileDriver.js +152 -0
  16. package/src/cache/drivers/MemoryDriver.js +158 -0
  17. package/src/cache/drivers/NullDriver.js +27 -0
  18. package/src/cache/index.js +8 -0
  19. package/src/cli.js +27 -0
  20. package/src/commands/make.js +61 -0
  21. package/src/commands/migrate.js +174 -0
  22. package/src/commands/new.js +50 -0
  23. package/src/commands/queue.js +92 -0
  24. package/src/commands/route.js +93 -0
  25. package/src/commands/serve.js +50 -0
  26. package/src/container/Application.js +177 -0
  27. package/src/container/Container.js +281 -0
  28. package/src/container/index.js +13 -0
  29. package/src/controller/Controller.js +367 -0
  30. package/src/errors/HttpError.js +29 -0
  31. package/src/events/Event.js +39 -0
  32. package/src/events/EventEmitter.js +151 -0
  33. package/src/events/Listener.js +46 -0
  34. package/src/events/index.js +15 -0
  35. package/src/index.js +93 -0
  36. package/src/mail/Mail.js +210 -0
  37. package/src/mail/MailMessage.js +196 -0
  38. package/src/mail/TemplateEngine.js +150 -0
  39. package/src/mail/drivers/LogDriver.js +36 -0
  40. package/src/mail/drivers/MailgunDriver.js +84 -0
  41. package/src/mail/drivers/SendGridDriver.js +97 -0
  42. package/src/mail/drivers/SmtpDriver.js +67 -0
  43. package/src/mail/index.js +19 -0
  44. package/src/middleware/AuthMiddleware.js +46 -0
  45. package/src/middleware/CorsMiddleware.js +59 -0
  46. package/src/middleware/LogMiddleware.js +61 -0
  47. package/src/middleware/Middleware.js +36 -0
  48. package/src/middleware/MiddlewarePipeline.js +94 -0
  49. package/src/middleware/ThrottleMiddleware.js +61 -0
  50. package/src/orm/drivers/DatabaseManager.js +135 -0
  51. package/src/orm/fields/index.js +132 -0
  52. package/src/orm/index.js +19 -0
  53. package/src/orm/migration/MigrationRunner.js +216 -0
  54. package/src/orm/migration/ModelInspector.js +338 -0
  55. package/src/orm/migration/SchemaBuilder.js +173 -0
  56. package/src/orm/model/Model.js +371 -0
  57. package/src/orm/query/QueryBuilder.js +197 -0
  58. package/src/providers/AdminServiceProvider.js +40 -0
  59. package/src/providers/AuthServiceProvider.js +53 -0
  60. package/src/providers/CacheStorageServiceProvider.js +71 -0
  61. package/src/providers/DatabaseServiceProvider.js +45 -0
  62. package/src/providers/EventServiceProvider.js +34 -0
  63. package/src/providers/MailServiceProvider.js +51 -0
  64. package/src/providers/ProviderRegistry.js +82 -0
  65. package/src/providers/QueueServiceProvider.js +52 -0
  66. package/src/providers/ServiceProvider.js +45 -0
  67. package/src/queue/Job.js +135 -0
  68. package/src/queue/Queue.js +147 -0
  69. package/src/queue/drivers/DatabaseDriver.js +194 -0
  70. package/src/queue/drivers/SyncDriver.js +72 -0
  71. package/src/queue/index.js +16 -0
  72. package/src/queue/workers/QueueWorker.js +140 -0
  73. package/src/router/MiddlewareRegistry.js +82 -0
  74. package/src/router/Route.js +255 -0
  75. package/src/router/RouteGroup.js +19 -0
  76. package/src/router/RouteRegistry.js +55 -0
  77. package/src/router/Router.js +138 -0
  78. package/src/router/index.js +15 -0
  79. package/src/scaffold/generator.js +34 -0
  80. package/src/scaffold/maker.js +272 -0
  81. package/src/scaffold/templates.js +350 -0
  82. package/src/storage/Storage.js +170 -0
  83. package/src/storage/drivers/LocalDriver.js +215 -0
  84. package/src/storage/index.js +6 -0
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const path = require('path');
5
+ const fs = require('fs-extra');
6
+
7
+ module.exports = function (program) {
8
+
9
+ program
10
+ .command('queue:work')
11
+ .description('Start the queue worker process')
12
+ .option('-q, --queue <queues>', 'Comma-separated queue names to process', 'default')
13
+ .option('-s, --sleep <seconds>', 'Seconds to sleep between polls', '3')
14
+ .option('--once', 'Process only one job then exit')
15
+ .action(async (options) => {
16
+ const bootstrapPath = path.resolve(process.cwd(), 'bootstrap/app.js');
17
+ if (!fs.existsSync(bootstrapPath)) {
18
+ console.error(chalk.red('\n ✖ Not inside a Millas project.\n'));
19
+ process.exit(1);
20
+ }
21
+
22
+ const Queue = require('../queue/Queue');
23
+ const QueueWorker = require('../queue/workers/QueueWorker');
24
+
25
+ // Boot the app to get config + job registry
26
+ process.env.MILLAS_ROUTE_LIST = 'true'; // suppress server listen
27
+ let app;
28
+ try {
29
+ const bootstrap = require(bootstrapPath);
30
+ app = bootstrap.app;
31
+ } catch (e) {
32
+ console.error(chalk.red(`\n ✖ Failed to load app: ${e.message}\n`));
33
+ process.exit(1);
34
+ }
35
+
36
+ const queues = options.queue.split(',').map(q => q.trim());
37
+ const sleep = Number(options.sleep) || 3;
38
+ const maxJobs = options.once ? 1 : Infinity;
39
+
40
+ const worker = new QueueWorker(
41
+ Queue.getDriver(),
42
+ Queue.getRegistry(),
43
+ { queues, sleep, maxJobs }
44
+ );
45
+
46
+ await worker.start();
47
+ });
48
+
49
+ program
50
+ .command('queue:status')
51
+ .description('Show queue statistics')
52
+ .action(async () => {
53
+ const Queue = require('../queue/Queue');
54
+
55
+ let queueConfig;
56
+ try { queueConfig = require(path.resolve(process.cwd(), 'config/queue')); }
57
+ catch { queueConfig = { default: process.env.QUEUE_DRIVER || 'sync' }; }
58
+
59
+ Queue.configure(queueConfig);
60
+
61
+ console.log();
62
+ if (queueConfig.default === 'sync') {
63
+ console.log(chalk.yellow(' Queue driver: sync (jobs run immediately, no persistence)'));
64
+ } else {
65
+ const stats = await Queue.stats();
66
+ if (!stats || !stats.length) {
67
+ console.log(chalk.gray(' No jobs in queue.'));
68
+ } else {
69
+ console.log(chalk.bold(' Queue Statistics\n'));
70
+ for (const row of stats) {
71
+ const statusColor = row.status === 'completed' ? chalk.green
72
+ : row.status === 'failed' ? chalk.red
73
+ : chalk.yellow;
74
+ console.log(` ${chalk.cyan(row.queue.padEnd(20))} ${statusColor(row.status.padEnd(12))} ${row.count}`);
75
+ }
76
+ }
77
+ }
78
+ console.log();
79
+ process.exit(0);
80
+ });
81
+
82
+ program
83
+ .command('queue:clear')
84
+ .description('Clear all pending jobs from a queue')
85
+ .option('-q, --queue <name>', 'Queue name to clear', 'default')
86
+ .action(async (options) => {
87
+ const Queue = require('../queue/Queue');
88
+ const n = await Queue.clear(options.queue);
89
+ console.log(chalk.green(`\n ✔ Cleared ${n} job(s) from "${options.queue}" queue.\n`));
90
+ process.exit(0);
91
+ });
92
+ };
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const path = require('path');
5
+ const fs = require('fs-extra');
6
+
7
+ module.exports = function (program) {
8
+ program
9
+ .command('route:list')
10
+ .description('List all registered routes')
11
+ .action(async () => {
12
+ const bootstrapPath = path.resolve(process.cwd(), 'bootstrap/app.js');
13
+
14
+ if (!fs.existsSync(bootstrapPath)) {
15
+ console.error(chalk.red('\n ✖ Not inside a Millas project.\n'));
16
+ process.exit(1);
17
+ }
18
+
19
+ process.env.MILLAS_ROUTE_LIST = 'true';
20
+
21
+ let route;
22
+ try {
23
+ const bootstrap = require(bootstrapPath);
24
+ route = bootstrap.route;
25
+ } catch (err) {
26
+ console.error(chalk.red(`\n ✖ Failed to load routes: ${err.message}\n`));
27
+ process.exit(1);
28
+ }
29
+
30
+ if (!route) {
31
+ console.log(chalk.yellow('\n ⚠ Bootstrap did not export { route }.\n'));
32
+ process.exit(0);
33
+ }
34
+
35
+ const rows = route.list();
36
+
37
+ if (rows.length === 0) {
38
+ console.log(chalk.yellow('\n No routes registered.\n'));
39
+ return;
40
+ }
41
+
42
+ console.log();
43
+ console.log(chalk.bold(' Registered Routes\n'));
44
+
45
+ const col = {
46
+ verb: 8,
47
+ path: Math.max(6, ...rows.map(r => r.path.length)) + 2,
48
+ handler: Math.max(8, ...rows.map(r => formatHandler(r).length)) + 2,
49
+ mw: Math.max(10, ...rows.map(r => (r.middleware || []).join(', ').length || 1)) + 2,
50
+ };
51
+
52
+ const header =
53
+ ' ' +
54
+ chalk.bold(pad('METHOD', col.verb)) +
55
+ chalk.bold(pad('PATH', col.path)) +
56
+ chalk.bold(pad('HANDLER', col.handler)) +
57
+ chalk.bold(pad('MIDDLEWARE', col.mw)) +
58
+ chalk.bold('NAME');
59
+
60
+ console.log(header);
61
+ console.log(chalk.gray(' ' + '─'.repeat(col.verb + col.path + col.handler + col.mw + 10)));
62
+
63
+ for (const r of rows) {
64
+ const mw = (r.middleware || []).join(', ') || chalk.gray('—');
65
+ const name = r.name || chalk.gray('—');
66
+ console.log(
67
+ ' ' +
68
+ verbChalk(r.verb)(pad(r.verb, col.verb)) +
69
+ chalk.cyan(pad(r.path, col.path)) +
70
+ chalk.white(pad(formatHandler(r), col.handler)) +
71
+ chalk.yellow(pad(mw, col.mw)) +
72
+ chalk.gray(name)
73
+ );
74
+ }
75
+
76
+ console.log(chalk.gray(`\n ${rows.length} route(s) total.\n`));
77
+ process.exit(0);
78
+ });
79
+ };
80
+
81
+ function pad(str, len) { return String(str).padEnd(len); }
82
+
83
+ function formatHandler(r) {
84
+ if (!r.handler) return '<none>';
85
+ if (typeof r.handler === 'function' && !r.method) return r.handler.name || '<closure>';
86
+ const name = r.handler.name || 'Controller';
87
+ return r.method ? `${name}@${r.method}` : name;
88
+ }
89
+
90
+ function verbChalk(verb) {
91
+ return { GET: chalk.green, POST: chalk.blue, PUT: chalk.yellow,
92
+ PATCH: chalk.magenta, DELETE: chalk.red }[verb] || chalk.white;
93
+ }
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const path = require('path');
5
+ const fs = require('fs-extra');
6
+
7
+ module.exports = function (program) {
8
+ program
9
+ .command('serve')
10
+ .description('Start the development server')
11
+ .option('-p, --port <port>', 'Port to listen on', '3000')
12
+ .option('-h, --host <host>', 'Host to bind to', 'localhost')
13
+ .action(async (options) => {
14
+ const appBootstrap = path.resolve(process.cwd(), 'bootstrap/app.js');
15
+
16
+ if (!fs.existsSync(appBootstrap)) {
17
+ console.error(chalk.red('\n ✖ No Millas project found in current directory.\n'));
18
+ console.log(` Make sure you are inside a Millas project and bootstrap/app.js exists.\n`);
19
+ process.exit(1);
20
+ }
21
+
22
+ console.log();
23
+ console.log(chalk.cyan(' ⚡ Millas Dev Server'));
24
+ console.log(chalk.gray(` Starting on http://${options.host}:${options.port}\n`));
25
+
26
+ process.env.MILLAS_PORT = options.port;
27
+ process.env.MILLAS_HOST = options.host;
28
+ process.env.NODE_ENV = process.env.NODE_ENV || 'development';
29
+
30
+ try {
31
+ require(appBootstrap);
32
+ } catch (err) {
33
+ console.error(chalk.red(`\n ✖ Failed to start server: ${err.message}\n`));
34
+ console.error(err.stack);
35
+ process.exit(1);
36
+ }
37
+ });
38
+ };
39
+
40
+ // Exported so queue:work can import the pattern
41
+ module.exports.requireProject = function(command) {
42
+ const path = require('path');
43
+ const fs = require('fs-extra');
44
+ const bootstrapPath = path.resolve(process.cwd(), 'bootstrap/app.js');
45
+ if (!fs.existsSync(bootstrapPath)) {
46
+ const chalk = require('chalk');
47
+ console.error(chalk.red(`\n ✖ Not inside a Millas project (${command}).\n`));
48
+ process.exit(1);
49
+ }
50
+ };
@@ -0,0 +1,177 @@
1
+ 'use strict';
2
+
3
+ const Container = require('./Container');
4
+ const ProviderRegistry = require('../providers/ProviderRegistry');
5
+ const { Route, Router, MiddlewareRegistry } = require('../router');
6
+ const CorsMiddleware = require('../middleware/CorsMiddleware');
7
+ const ThrottleMiddleware = require('../middleware/ThrottleMiddleware');
8
+ const LogMiddleware = require('../middleware/LogMiddleware');
9
+ const AuthMiddleware = require('../auth/AuthMiddleware');
10
+
11
+ /**
12
+ * Application
13
+ *
14
+ * The central Millas kernel. Owns:
15
+ * - The DI container
16
+ * - The provider registry
17
+ * - The Express app
18
+ * - The route instance
19
+ * - The middleware registry
20
+ *
21
+ * Usage in bootstrap/app.js:
22
+ *
23
+ * const { Application } = require('millas/src/container');
24
+ * const app = new Application(expressApp);
25
+ * app.providers([AppServiceProvider, DatabaseServiceProvider]);
26
+ * await app.boot();
27
+ * app.routes(route => {
28
+ * require('../routes/web')(route);
29
+ * require('../routes/api')(route);
30
+ * });
31
+ * app.mount();
32
+ * app.listen();
33
+ */
34
+ class Application {
35
+ constructor(expressApp) {
36
+ this._express = expressApp;
37
+ this._container = new Container();
38
+ this._providers = new ProviderRegistry(this._container, expressApp);
39
+ this._mwRegistry = new MiddlewareRegistry();
40
+ this._route = new Route();
41
+ this._booted = false;
42
+
43
+ // Register core framework bindings
44
+ this._registerCoreBindings();
45
+ }
46
+
47
+ // ─── Configuration ───────────────────────────────────────────────────────────
48
+
49
+ /**
50
+ * Add service providers.
51
+ * @param {Array} providers
52
+ */
53
+ providers(providers = []) {
54
+ this._providers.addMany(providers);
55
+ return this;
56
+ }
57
+
58
+ /**
59
+ * Register a middleware alias.
60
+ *
61
+ * app.middleware('auth', AuthMiddleware)
62
+ * app.middleware('throttle', new ThrottleMiddleware({ max: 60 }))
63
+ */
64
+ middleware(alias, handler) {
65
+ this._mwRegistry.register(alias, handler);
66
+ return this;
67
+ }
68
+
69
+ /**
70
+ * Define all routes via a callback.
71
+ *
72
+ * app.routes(route => {
73
+ * require('../routes/web')(route);
74
+ * require('../routes/api')(route);
75
+ * });
76
+ */
77
+ routes(callback) {
78
+ callback(this._route);
79
+ return this;
80
+ }
81
+
82
+ // ─── Lifecycle ───────────────────────────────────────────────────────────────
83
+
84
+ /**
85
+ * Run the full provider register → boot lifecycle.
86
+ */
87
+ async boot() {
88
+ if (this._booted) return this;
89
+ await this._providers.boot();
90
+ this._booted = true;
91
+ return this;
92
+ }
93
+
94
+ /**
95
+ * Bind all routes onto the Express app + mount error handlers.
96
+ */
97
+ mount() {
98
+ const router = new Router(this._express, this._route.getRegistry(), this._mwRegistry);
99
+ router.mount();
100
+ return this;
101
+ }
102
+
103
+ /**
104
+ * Start the HTTP server.
105
+ */
106
+ listen(port, host, callback) {
107
+ const _port = port || parseInt(process.env.APP_PORT, 10) || 3000;
108
+ const _host = host || process.env.MILLAS_HOST || 'localhost';
109
+
110
+ this._express.listen(_port, _host, () => {
111
+ if (!process.env.MILLAS_ROUTE_LIST) {
112
+ const routeCount = this._route.list().length;
113
+ console.log(`\n ⚡ Millas running at http://${_host}:${_port}`);
114
+ console.log(` ${routeCount} route(s) registered\n`);
115
+ }
116
+ if (typeof callback === 'function') callback(_port, _host);
117
+ });
118
+
119
+ return this;
120
+ }
121
+
122
+ // ─── Container Proxy ─────────────────────────────────────────────────────────
123
+
124
+ /**
125
+ * Bind a transient into the container.
126
+ */
127
+ bind(abstract, concrete) {
128
+ this._container.bind(abstract, concrete);
129
+ return this;
130
+ }
131
+
132
+ /**
133
+ * Bind a singleton into the container.
134
+ */
135
+ singleton(abstract, concrete) {
136
+ this._container.singleton(abstract, concrete);
137
+ return this;
138
+ }
139
+
140
+ /**
141
+ * Register a pre-built instance.
142
+ */
143
+ instance(abstract, value) {
144
+ this._container.instance(abstract, value);
145
+ return this;
146
+ }
147
+
148
+ /**
149
+ * Resolve something from the container.
150
+ */
151
+ make(abstract, overrides) {
152
+ return this._container.make(abstract, overrides);
153
+ }
154
+
155
+ // ─── Accessors ───────────────────────────────────────────────────────────────
156
+
157
+ get container() { return this._container; }
158
+ get route() { return this._route; }
159
+ get express() { return this._express; }
160
+ get mwRegistry() { return this._mwRegistry; }
161
+
162
+ // ─── Internal ────────────────────────────────────────────────────────────────
163
+
164
+ _registerCoreBindings() {
165
+ // Register the application itself
166
+ this._container.instance('app', this);
167
+ this._container.instance('container', this._container);
168
+
169
+ // Register built-in middleware
170
+ this._mwRegistry.register('cors', new CorsMiddleware());
171
+ this._mwRegistry.register('throttle', new ThrottleMiddleware({ max: 60, window: 60 }));
172
+ this._mwRegistry.register('log', new LogMiddleware());
173
+ this._mwRegistry.register('auth', AuthMiddleware);
174
+ }
175
+ }
176
+
177
+ module.exports = Application;
@@ -0,0 +1,281 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Container
5
+ *
6
+ * Millas dependency injection container.
7
+ *
8
+ * Three binding types:
9
+ *
10
+ * container.bind('UserService', UserService)
11
+ * — fresh instance each time make() is called
12
+ *
13
+ * container.singleton('DB', DatabaseService)
14
+ * — same instance returned on every make()
15
+ *
16
+ * container.instance('Config', configObject)
17
+ * — register a pre-built value or object directly
18
+ *
19
+ * Auto-resolution:
20
+ * If a class constructor lists dependencies by name in a static
21
+ * `inject` array, the container resolves them automatically:
22
+ *
23
+ * class OrderService {
24
+ * static inject = ['UserService', 'PaymentService'];
25
+ * constructor(userService, paymentService) { ... }
26
+ * }
27
+ *
28
+ * container.bind('OrderService', OrderService);
29
+ * const svc = container.make('OrderService');
30
+ * // UserService and PaymentService injected automatically
31
+ */
32
+ class Container {
33
+ constructor() {
34
+ this._bindings = new Map(); // abstract → { concrete, type }
35
+ this._resolved = new Map(); // abstract → instance (singletons)
36
+ this._aliases = new Map(); // alias → abstract
37
+ this._tags = new Map(); // tag → [abstract, ...]
38
+ this._resolving = new Set(); // circular dependency guard
39
+ }
40
+
41
+ // ─── Registration ────────────────────────────────────────────────────────────
42
+
43
+ /**
44
+ * Bind an abstract name to a concrete class or factory.
45
+ * A new instance is created every time make() is called.
46
+ *
47
+ * container.bind('UserService', UserService)
48
+ * container.bind('Logger', () => new Logger({ level: 'debug' }))
49
+ */
50
+ bind(abstract, concrete) {
51
+ this._bindings.set(abstract, { concrete, type: 'transient' });
52
+ // Clear any resolved singleton if re-bound
53
+ this._resolved.delete(abstract);
54
+ return this;
55
+ }
56
+
57
+ /**
58
+ * Bind as a singleton — the same instance is returned on every make().
59
+ *
60
+ * container.singleton('DB', DatabaseService)
61
+ */
62
+ singleton(abstract, concrete) {
63
+ this._bindings.set(abstract, { concrete, type: 'singleton' });
64
+ this._resolved.delete(abstract);
65
+ return this;
66
+ }
67
+
68
+ /**
69
+ * Register a pre-built value directly.
70
+ * make() always returns this exact value.
71
+ *
72
+ * container.instance('Config', { port: 3000 })
73
+ * container.instance('App', expressApp)
74
+ */
75
+ instance(abstract, value) {
76
+ this._bindings.set(abstract, { concrete: null, type: 'instance' });
77
+ this._resolved.set(abstract, value);
78
+ return this;
79
+ }
80
+
81
+ /**
82
+ * Register an alias for an abstract name.
83
+ *
84
+ * container.alias('db', 'DatabaseService')
85
+ * container.make('db') // resolves DatabaseService
86
+ */
87
+ alias(alias, abstract) {
88
+ this._aliases.set(alias, abstract);
89
+ return this;
90
+ }
91
+
92
+ /**
93
+ * Tag multiple bindings under a group name.
94
+ *
95
+ * container.tag(['MySQLDriver', 'SQLiteDriver'], 'db.drivers')
96
+ * container.tagged('db.drivers') // → [MySQLDriver instance, SQLiteDriver instance]
97
+ */
98
+ tag(abstracts, tag) {
99
+ if (!this._tags.has(tag)) this._tags.set(tag, []);
100
+ for (const a of [].concat(abstracts)) {
101
+ this._tags.get(tag).push(a);
102
+ }
103
+ return this;
104
+ }
105
+
106
+ // ─── Resolution ──────────────────────────────────────────────────────────────
107
+
108
+ /**
109
+ * Resolve a binding by name, building and injecting dependencies.
110
+ *
111
+ * const svc = container.make('UserService')
112
+ * const svc = container.make(UserService) // class reference also works
113
+ */
114
+ make(abstract, overrides = {}) {
115
+ // Accept class references directly
116
+ if (typeof abstract === 'function') {
117
+ return this._build(abstract, overrides);
118
+ }
119
+
120
+ // Resolve alias
121
+ const resolved = this._aliases.get(abstract) || abstract;
122
+
123
+ // Guard against circular deps
124
+ if (this._resolving.has(resolved)) {
125
+ throw new Error(
126
+ `Circular dependency detected while resolving "${resolved}".`
127
+ );
128
+ }
129
+
130
+ const binding = this._bindings.get(resolved);
131
+
132
+ if (!binding) {
133
+ throw new Error(
134
+ `"${resolved}" is not bound in the container. ` +
135
+ `Did you forget to call container.bind('${resolved}', MyClass)?`
136
+ );
137
+ }
138
+
139
+ // Pre-built instance
140
+ if (binding.type === 'instance') {
141
+ return this._resolved.get(resolved);
142
+ }
143
+
144
+ // Return cached singleton
145
+ if (binding.type === 'singleton' && this._resolved.has(resolved)) {
146
+ return this._resolved.get(resolved);
147
+ }
148
+
149
+ this._resolving.add(resolved);
150
+ let instance;
151
+
152
+ try {
153
+ instance = this._build(binding.concrete, overrides);
154
+ } finally {
155
+ this._resolving.delete(resolved);
156
+ }
157
+
158
+ if (binding.type === 'singleton') {
159
+ this._resolved.set(resolved, instance);
160
+ }
161
+
162
+ return instance;
163
+ }
164
+
165
+ /**
166
+ * Resolve all bindings under a tag.
167
+ *
168
+ * const drivers = container.tagged('db.drivers')
169
+ */
170
+ tagged(tag) {
171
+ const abstracts = this._tags.get(tag);
172
+ if (!abstracts) return [];
173
+ return abstracts.map(a => this.make(a));
174
+ }
175
+
176
+ /**
177
+ * Check whether a name is bound.
178
+ */
179
+ has(abstract) {
180
+ const resolved = this._aliases.get(abstract) || abstract;
181
+ return this._bindings.has(resolved);
182
+ }
183
+
184
+ /**
185
+ * Call a method or function, auto-injecting its declared dependencies.
186
+ *
187
+ * container.call(myService, 'processOrder', { orderId: 123 })
188
+ * container.call(myFunction)
189
+ */
190
+ call(target, method, extras = {}) {
191
+ if (typeof target === 'function') {
192
+ return this._callFunction(target, extras);
193
+ }
194
+ if (typeof target === 'object' && typeof target[method] === 'function') {
195
+ return this._callFunction(target[method].bind(target), extras);
196
+ }
197
+ throw new Error(`Cannot call "${method}" on the given target.`);
198
+ }
199
+
200
+ /**
201
+ * Forget a resolved singleton (force re-instantiation on next make()).
202
+ */
203
+ forgetInstance(abstract) {
204
+ this._resolved.delete(abstract);
205
+ return this;
206
+ }
207
+
208
+ /**
209
+ * Remove a binding entirely.
210
+ */
211
+ unbind(abstract) {
212
+ this._bindings.delete(abstract);
213
+ this._resolved.delete(abstract);
214
+ return this;
215
+ }
216
+
217
+ /**
218
+ * List all registered abstract names.
219
+ */
220
+ bindings() {
221
+ return [...this._bindings.keys()];
222
+ }
223
+
224
+ // ─── Internal ────────────────────────────────────────────────────────────────
225
+
226
+ /**
227
+ * Build a concrete class or factory, resolving its inject[] dependencies.
228
+ */
229
+ _build(concrete, overrides = {}) {
230
+ if (concrete === null || concrete === undefined) {
231
+ throw new Error('Cannot build a null concrete.');
232
+ }
233
+
234
+ // Factory function (not a class constructor)
235
+ if (this._isFactory(concrete)) {
236
+ return concrete(this, overrides);
237
+ }
238
+
239
+ // Class — resolve static inject[] array
240
+ const deps = this._resolveDependencies(concrete, overrides);
241
+ return new concrete(...deps);
242
+ }
243
+
244
+ /**
245
+ * Resolve the inject[] static array on a class.
246
+ *
247
+ * class MyService {
248
+ * static inject = ['Logger', 'Config'];
249
+ * }
250
+ */
251
+ _resolveDependencies(Cls, overrides = {}) {
252
+ const inject = Cls.inject || [];
253
+ return inject.map(dep => {
254
+ if (dep in overrides) return overrides[dep];
255
+ return this.make(dep);
256
+ });
257
+ }
258
+
259
+ /**
260
+ * Detect whether `fn` is a plain factory function vs a class constructor.
261
+ * Heuristic: class constructors start with an uppercase letter.
262
+ */
263
+ _isFactory(fn) {
264
+ if (typeof fn !== 'function') return false;
265
+ const str = fn.toString();
266
+ // Arrow functions and non-class functions = factory
267
+ if (str.startsWith('class ')) return false;
268
+ if (/^function\s+[A-Z]/.test(str)) return false; // old-style class
269
+ // Check if it's a regular function (not a constructor-style)
270
+ const name = fn.name || '';
271
+ return !(name[0] && name[0] === name[0].toUpperCase() && name[0] !== name[0].toLowerCase());
272
+ }
273
+
274
+ _callFunction(fn, extras = {}) {
275
+ const inject = fn.inject || [];
276
+ const deps = inject.map(dep => dep in extras ? extras[dep] : this.make(dep));
277
+ return fn(...deps);
278
+ }
279
+ }
280
+
281
+ module.exports = Container;
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ const Container = require('./Container');
4
+ const Application = require('./Application');
5
+ const ServiceProvider = require('../providers/ServiceProvider');
6
+ const ProviderRegistry = require('../providers/ProviderRegistry');
7
+
8
+ module.exports = {
9
+ Container,
10
+ Application,
11
+ ServiceProvider,
12
+ ProviderRegistry,
13
+ };