@vlynk-studios/nodulus-core 1.2.0 → 1.2.5

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/dist/index.d.ts CHANGED
@@ -1,21 +1,5 @@
1
- import { RequestHandler, Router, Application } from 'express';
1
+ import { RequestHandler, Application } from 'express';
2
2
 
3
- interface ControllerEntry {
4
- name: string;
5
- path: string;
6
- prefix: string;
7
- middlewares: RequestHandler[];
8
- router?: Router;
9
- enabled: boolean;
10
- }
11
- interface ModuleEntry {
12
- name: string;
13
- path: string;
14
- indexPath: string;
15
- imports: string[];
16
- exports: string[];
17
- controllers: ControllerEntry[];
18
- }
19
3
  type LogLevel = 'debug' | 'info' | 'warn' | 'error';
20
4
  /**
21
5
  * Function that receives a log event from Nodulus.
@@ -93,9 +77,24 @@ interface SchemaEntry {
93
77
  }
94
78
  /** Discriminated union for all file-level identifier entries. */
95
79
  type FileEntry = ServiceEntry | RepositoryEntry | SchemaEntry;
80
+ interface NitsConfig {
81
+ /**
82
+ * Custom similarity threshold (0.0 to 1.0).
83
+ * If omitted, a dynamic threshold based on module size is used.
84
+ */
85
+ similarityThreshold?: number;
86
+ /** Whether to enable NITS identity tracking. Default: true. */
87
+ enabled?: boolean;
88
+ /** Custom path to the NITS registry file. Default: '.nodulus/registry.json'. */
89
+ registryPath?: string;
90
+ }
96
91
  interface CreateAppOptions {
97
92
  /** Glob pointing to module folders. Default: 'src/modules/*'. */
98
93
  modules?: string;
94
+ /** Glob pointing to domain folders (v2.0.0+). Default: undefined. */
95
+ domains?: string;
96
+ /** Glob pointing to shared global folders (v2.0.0+). Default: undefined. */
97
+ shared?: string;
99
98
  /** Global route prefix. Example: '/api/v1'. Default: ''. */
100
99
  prefix?: string;
101
100
  /** Folder aliases beyond the auto-generated @modules/* entries. Default: {}. */
@@ -122,16 +121,8 @@ interface CreateAppOptions {
122
121
  * Default: 'info' (debug is off unless explicitly set).
123
122
  */
124
123
  logLevel?: LogLevel;
125
- }
126
- /** Resolved configuration used internally (defaults applied). */
127
- interface ResolvedConfig {
128
- modules: string;
129
- prefix: string;
130
- aliases: Record<string, string>;
131
- strict: boolean;
132
- resolveAliases: boolean;
133
- logger: LogHandler;
134
- logLevel: LogLevel;
124
+ /** NITS (Nodulus Integrated Tracking System) configuration. */
125
+ nits?: NitsConfig;
135
126
  }
136
127
  /** A module as it appears in the NodularApp result after bootstrap. */
137
128
  interface RegisteredModule {
@@ -184,6 +175,8 @@ interface GetAliasesOptions {
184
175
  absolute?: boolean;
185
176
  }
186
177
 
178
+ type ModuleRegistration = RegisteredModule;
179
+ type FeatureRegistration = FileEntry;
187
180
  /**
188
181
  * Returns a read-only view of the registry active in the current async context.
189
182
  *
@@ -193,14 +186,12 @@ interface GetAliasesOptions {
193
186
  */
194
187
  declare const getRegistry: () => NodulusRegistryAdvanced;
195
188
 
196
- type NodulusErrorCode = "MODULE_NOT_FOUND" | "DUPLICATE_MODULE" | "MISSING_IMPORT" | "UNDECLARED_IMPORT" | "CIRCULAR_DEPENDENCY" | "EXPORT_MISMATCH" | "INVALID_CONTROLLER" | "ALIAS_NOT_FOUND" | "DUPLICATE_ALIAS" | "DUPLICATE_BOOTSTRAP" | "REGISTRY_MISSING_CONTEXT" | "INVALID_MODULE_DECLARATION" | "DUPLICATE_SERVICE" | "DUPLICATE_REPOSITORY" | "DUPLICATE_SCHEMA" | "INVALID_ESM_ENV";
189
+ type NodulusErrorCode = "MODULE_NOT_FOUND" | "DUPLICATE_MODULE" | "MISSING_IMPORT" | "UNDECLARED_IMPORT" | "CIRCULAR_DEPENDENCY" | "EXPORT_MISMATCH" | "INVALID_CONTROLLER" | "ALIAS_NOT_FOUND" | "DUPLICATE_ALIAS" | "DUPLICATE_BOOTSTRAP" | "REGISTRY_MISSING_CONTEXT" | "INVALID_MODULE_DECLARATION" | "DUPLICATE_SERVICE" | "DUPLICATE_REPOSITORY" | "DUPLICATE_SCHEMA" | "INVALID_ESM_ENV" | "CLI_ERROR";
197
190
  declare class NodulusError extends Error {
198
191
  readonly code: NodulusErrorCode;
199
192
  readonly details?: string;
200
193
  constructor(code: NodulusErrorCode, message: string, details?: string);
201
194
  }
202
- /** @deprecated — not used internally. Messages are defined at each throw site. */
203
- declare const ERROR_MESSAGES: Record<NodulusErrorCode, string>;
204
195
 
205
196
  declare function Module(name: string, options?: ModuleOptions): void;
206
197
 
@@ -264,4 +255,4 @@ declare function createApp(app: Application, options?: CreateAppOptions): Promis
264
255
 
265
256
  declare function getAliases(options?: GetAliasesOptions): Promise<Record<string, string>>;
266
257
 
267
- export { Controller, type ControllerEntry, type ControllerOptions, type CreateAppOptions, ERROR_MESSAGES, type FileEntry, type GetAliasesOptions, type LogHandler, type LogLevel, Module, type ModuleEntry, type ModuleOptions, type MountedRoute, type NodulusApp, type NodulusConfig, NodulusError, type NodulusErrorCode, type NodulusRegistry, type NodulusRegistryAdvanced, type RegisteredModule, Repository, type RepositoryEntry, type RepositoryOptions, type ResolvedConfig, Schema, type SchemaEntry, type SchemaOptions, Service, type ServiceEntry, type ServiceOptions, createApp, getAliases, getRegistry };
258
+ export { Controller, type ControllerOptions, type CreateAppOptions, type FeatureRegistration, type GetAliasesOptions, type LogHandler, type LogLevel, Module, type ModuleOptions, type ModuleRegistration, type MountedRoute, type NodulusApp, type NodulusConfig, NodulusError, type NodulusErrorCode, type NodulusRegistry, type NodulusRegistryAdvanced, type RegisteredModule, Repository, type RepositoryOptions, Schema, type SchemaOptions, Service, type ServiceOptions, createApp, getAliases, getRegistry };
package/dist/index.js CHANGED
@@ -12,24 +12,36 @@ var NodulusError = class extends Error {
12
12
  this.details = details;
13
13
  }
14
14
  };
15
- var ERROR_MESSAGES = {
16
- MODULE_NOT_FOUND: "This folder was discovered but index.ts does not call Module(). Add Module('name') to the top of index.ts.",
17
- DUPLICATE_MODULE: "A module with this name or path already exists. Ensure every module name is unique across the app.",
18
- MISSING_IMPORT: "A module listed in 'imports' does not exist in the registry. Verify the module name exists and its index.ts calls Module().",
19
- UNDECLARED_IMPORT: "Attempted to import a module not listed in this module's 'imports' field. Add the missing dependency to Module() options.",
20
- CIRCULAR_DEPENDENCY: "A circular dependency chain was detected. Extract shared logic into a third module to break the cycle.",
21
- EXPORT_MISMATCH: "A name declared in 'exports' is not a real export of index.ts. Ensure you 'export { ... }' the matching member.",
22
- INVALID_CONTROLLER: "Controller has no default export of an Express Router. Add 'export default router;' to the controller file.",
23
- ALIAS_NOT_FOUND: "An alias points to a target directory that does not exist. Verify the path in nodulus.config.ts or createApp() options.",
24
- DUPLICATE_ALIAS: "An alias with this name is already registered to a different path. Check for naming collisions in your config.",
25
- DUPLICATE_BOOTSTRAP: "createApp() was called more than once with the same Express instance. Reuse the existing NodulusApp instead.",
26
- REGISTRY_MISSING_CONTEXT: "No active registry found in the current async context. Ensure Nodulus API calls run within a createApp() scope.",
27
- INVALID_MODULE_DECLARATION: "The Module() call violates architectural rules. Ensure it's called at the top level of index.ts.",
28
- DUPLICATE_SERVICE: "A service with this name is already registered. Ensure every Service() name is unique within the same module.",
29
- DUPLICATE_REPOSITORY: "A repository with this name is already registered. Ensure every Repository() name is unique within the same module.",
30
- DUPLICATE_SCHEMA: "A schema with this name is already registered. Ensure every Schema() name is unique within the same module.",
31
- INVALID_ESM_ENV: `Nodulus requires an ESM environment. Please ensure '"type": "module"' is present heavily in your root package.json file.`
32
- };
15
+
16
+ // src/core/utils/cycle-detector.ts
17
+ function findCircularDependencies(dependencyMap) {
18
+ const cycles = [];
19
+ const visited = /* @__PURE__ */ new Set();
20
+ const recStack = /* @__PURE__ */ new Set();
21
+ const path10 = [];
22
+ const dfs = (node) => {
23
+ visited.add(node);
24
+ recStack.add(node);
25
+ path10.push(node);
26
+ const deps = dependencyMap.get(node) || [];
27
+ for (const neighbor of deps) {
28
+ if (!visited.has(neighbor)) {
29
+ dfs(neighbor);
30
+ } else if (recStack.has(neighbor)) {
31
+ const cycleStart = path10.indexOf(neighbor);
32
+ cycles.push([...path10.slice(cycleStart), neighbor]);
33
+ }
34
+ }
35
+ recStack.delete(node);
36
+ path10.pop();
37
+ };
38
+ for (const node of dependencyMap.keys()) {
39
+ if (!visited.has(node)) {
40
+ dfs(node);
41
+ }
42
+ }
43
+ return cycles;
44
+ }
33
45
 
34
46
  // src/core/registry.ts
35
47
  var toRegisteredModule = (entry) => ({
@@ -71,32 +83,11 @@ function createRegistry() {
71
83
  return graph;
72
84
  },
73
85
  findCircularDependencies() {
74
- const cycles = [];
75
- const visited = /* @__PURE__ */ new Set();
76
- const recStack = /* @__PURE__ */ new Set();
77
- const path10 = [];
78
- const dfs = (node) => {
79
- visited.add(node);
80
- recStack.add(node);
81
- path10.push(node);
82
- const deps = modules.get(node)?.imports || [];
83
- for (const neighbor of deps) {
84
- if (!visited.has(neighbor)) {
85
- dfs(neighbor);
86
- } else if (recStack.has(neighbor)) {
87
- const cycleStart = path10.indexOf(neighbor);
88
- cycles.push([...path10.slice(cycleStart), neighbor]);
89
- }
90
- }
91
- recStack.delete(node);
92
- path10.pop();
93
- };
94
- for (const node of modules.keys()) {
95
- if (!visited.has(node)) {
96
- dfs(node);
97
- }
86
+ const dependencyMap = /* @__PURE__ */ new Map();
87
+ for (const [name, entry] of modules.entries()) {
88
+ dependencyMap.set(name, entry.imports);
98
89
  }
99
- return cycles;
90
+ return findCircularDependencies(dependencyMap);
100
91
  },
101
92
  registerModule(name, options, dirPath, indexPath) {
102
93
  if (modules.has(name)) {
@@ -366,7 +357,7 @@ function Schema(name, options = {}) {
366
357
  // src/bootstrap/createApp.ts
367
358
  import fs2 from "fs";
368
359
  import path8 from "path";
369
- import { pathToFileURL as pathToFileURL3 } from "url";
360
+ import { pathToFileURL as pathToFileURL2 } from "url";
370
361
  import fg from "fast-glob";
371
362
 
372
363
  // src/core/config.ts
@@ -432,25 +423,27 @@ function createLogger(handler, minLevel) {
432
423
  var defaultStrict = typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
433
424
  var DEFAULTS = {
434
425
  modules: "src/modules/*",
426
+ domains: void 0,
427
+ shared: void 0,
435
428
  prefix: "",
436
429
  aliases: {},
437
430
  strict: defaultStrict,
438
431
  resolveAliases: true,
439
432
  logger: defaultLogHandler,
440
- logLevel: resolveLogLevel()
433
+ logLevel: resolveLogLevel(),
434
+ nits: {
435
+ enabled: true,
436
+ similarityThreshold: void 0,
437
+ // Use dynamic by default
438
+ registryPath: ".nodulus/registry.json"
439
+ }
441
440
  };
442
441
  var loadConfig = async (options = {}) => {
443
442
  const cwd = process.cwd();
444
443
  let fileConfig = {};
445
444
  const tsPath = path7.join(cwd, "nodulus.config.ts");
446
445
  const jsPath = path7.join(cwd, "nodulus.config.js");
447
- const isProduction = process.env.NODE_ENV === "production";
448
- const hasTsLoader = process.execArgv.some((arg) => arg.includes("ts-node") || arg.includes("tsx")) || process._preload_modules?.some((m) => m.includes("ts-node") || m.includes("tsx"));
449
- const candidates = [];
450
- if (!isProduction || hasTsLoader) {
451
- candidates.push(tsPath);
452
- }
453
- candidates.push(jsPath);
446
+ const candidates = [tsPath, jsPath];
454
447
  let configPathToLoad = null;
455
448
  for (const candidate of candidates) {
456
449
  if (fs.existsSync(candidate)) {
@@ -477,6 +470,8 @@ var loadConfig = async (options = {}) => {
477
470
  }
478
471
  return {
479
472
  modules: options.modules ?? fileConfig.modules ?? DEFAULTS.modules,
473
+ domains: options.domains ?? fileConfig.domains ?? DEFAULTS.domains,
474
+ shared: options.shared ?? fileConfig.shared ?? DEFAULTS.shared,
480
475
  prefix: options.prefix ?? fileConfig.prefix ?? DEFAULTS.prefix,
481
476
  aliases: {
482
477
  ...DEFAULTS.aliases,
@@ -487,25 +482,33 @@ var loadConfig = async (options = {}) => {
487
482
  strict: options.strict ?? fileConfig.strict ?? DEFAULTS.strict,
488
483
  resolveAliases: options.resolveAliases ?? fileConfig.resolveAliases ?? DEFAULTS.resolveAliases,
489
484
  logger: options.logger ?? fileConfig.logger ?? DEFAULTS.logger,
490
- logLevel: resolveLogLevel(options.logLevel ?? fileConfig.logLevel)
485
+ logLevel: resolveLogLevel(options.logLevel ?? fileConfig.logLevel),
486
+ nits: {
487
+ enabled: options.nits?.enabled ?? fileConfig.nits?.enabled ?? DEFAULTS.nits.enabled,
488
+ similarityThreshold: options.nits?.similarityThreshold ?? fileConfig.nits?.similarityThreshold ?? DEFAULTS.nits.similarityThreshold,
489
+ registryPath: options.nits?.registryPath ?? fileConfig.nits?.registryPath ?? DEFAULTS.nits.registryPath
490
+ }
491
491
  };
492
492
  };
493
493
 
494
494
  // src/aliases/resolver.ts
495
495
  import { register } from "module";
496
- import { pathToFileURL as pathToFileURL2 } from "url";
497
496
  var isHookRegistered = false;
498
- function activateAliasResolver(moduleAliases, folderAliases, log) {
497
+ var registrationPromise = null;
498
+ async function activateAliasResolver(moduleAliases, folderAliases, log) {
499
499
  if (isHookRegistered) return;
500
- const combinedAliases = { ...moduleAliases, ...folderAliases };
501
- for (const [alias, target] of Object.entries(folderAliases)) {
502
- log.debug(`Alias registered: ${alias} \u2192 ${target}`, { alias, target, source: "config" });
503
- }
504
- for (const [alias, target] of Object.entries(moduleAliases)) {
505
- log.debug(`Alias registered: ${alias} \u2192 ${target}`, { alias, target, source: "module" });
506
- }
507
- const serialisedAliases = JSON.stringify(combinedAliases);
508
- const loaderCode = `
500
+ if (registrationPromise) return registrationPromise;
501
+ registrationPromise = (async () => {
502
+ try {
503
+ const combinedAliases = { ...moduleAliases, ...folderAliases };
504
+ for (const [alias, target] of Object.entries(folderAliases)) {
505
+ log.debug(`Alias registered: ${alias} \u2192 ${target}`, { alias, target, source: "config" });
506
+ }
507
+ for (const [alias, target] of Object.entries(moduleAliases)) {
508
+ log.debug(`Alias registered: ${alias} \u2192 ${target}`, { alias, target, source: "module" });
509
+ }
510
+ const serialisedAliases = JSON.stringify(combinedAliases);
511
+ const loaderCode = `
509
512
  import { pathToFileURL } from 'node:url';
510
513
  import path from 'node:path';
511
514
 
@@ -513,43 +516,57 @@ const aliases = ${serialisedAliases};
513
516
 
514
517
  export async function resolve(specifier, context, nextResolve) {
515
518
  for (const alias of Object.keys(aliases)) {
516
- if (specifier === alias || specifier.startsWith(alias + '/')) {
519
+ if (alias.endsWith('/*')) {
520
+ const baseAlias = alias.slice(0, -2);
521
+ if (specifier.startsWith(baseAlias + '/')) {
522
+ const baseTarget = aliases[alias].slice(0, -2);
523
+ const subPath = specifier.slice(baseAlias.length + 1);
524
+ const resolvedPath = path.resolve(baseTarget, subPath);
525
+ return nextResolve(pathToFileURL(resolvedPath).href, context);
526
+ }
527
+ } else if (specifier === alias) {
517
528
  const target = aliases[alias];
518
- const resolvedPath = specifier.replace(alias, target);
519
- return nextResolve(pathToFileURL(path.resolve(resolvedPath)).href, context);
529
+ return nextResolve(pathToFileURL(path.resolve(target)).href, context);
520
530
  }
521
531
  }
522
532
  return nextResolve(specifier, context);
523
533
  }
524
534
  `;
525
- try {
526
- const dataUrl = `data:text/javascript,${encodeURIComponent(loaderCode)}`;
527
- const parentUrl = typeof __filename === "undefined" ? import.meta.url : pathToFileURL2(__filename).href;
528
- if (typeof register === "function") {
529
- register(dataUrl, { parentURL: parentUrl });
530
- isHookRegistered = true;
531
- log.info(`ESM alias hook activated (${Object.keys(combinedAliases).length} alias(es))`, {
532
- aliasCount: Object.keys(combinedAliases).length
533
- });
534
- } else {
535
- log.warn("ESM alias hook could not be registered \u2014 upgrade to Node.js >= 20.6.0 for runtime alias support", {
536
- nodeVersion: process.version
535
+ const dataUrl = `data:text/javascript,${encodeURIComponent(loaderCode)}`;
536
+ const parentUrl = import.meta.url;
537
+ if (typeof register === "function") {
538
+ register(dataUrl, { parentURL: parentUrl });
539
+ isHookRegistered = true;
540
+ log.info(`ESM alias hook activated (${Object.keys(combinedAliases).length} alias(es))`, {
541
+ aliasCount: Object.keys(combinedAliases).length
542
+ });
543
+ } else {
544
+ log.warn("ESM alias hook could not be registered \u2014 upgrade to Node.js >= 20.6.0 for runtime alias support", {
545
+ nodeVersion: process.version
546
+ });
547
+ }
548
+ } catch (err) {
549
+ log.warn("ESM alias hook registration threw an unexpected error \u2014 aliases may not resolve at runtime", {
550
+ error: err?.message ?? String(err)
537
551
  });
552
+ } finally {
553
+ registrationPromise = null;
538
554
  }
539
- } catch (err) {
540
- log.warn("ESM alias hook registration threw an unexpected error \u2014 aliases may not resolve at runtime", {
541
- error: err?.message ?? String(err)
542
- });
543
- }
555
+ })();
556
+ return registrationPromise;
544
557
  }
545
558
 
546
559
  // src/aliases/cache.ts
547
- var aliasCache = {};
560
+ var globalAliasCache = {};
548
561
  function updateAliasCache(aliases) {
549
- aliasCache = { ...aliases };
562
+ globalAliasCache = { ...aliases };
550
563
  }
551
564
  function getAliasCache() {
552
- return aliasCache;
565
+ const activeRegistry = registryContext.getStore();
566
+ if (activeRegistry) {
567
+ return activeRegistry.getAllAliases();
568
+ }
569
+ return globalAliasCache;
553
570
  }
554
571
 
555
572
  // src/bootstrap/createApp.ts
@@ -585,6 +602,9 @@ async function createApp(app, options = {}) {
585
602
  try {
586
603
  const config = await loadConfig(options);
587
604
  const log = createLogger(config.logger, config.logLevel);
605
+ if (config.domains || config.shared) {
606
+ log.warn("Infrastructure (domains/shared) is not yet supported in v1.2.x. These keys in configuration will be ignored until v2.0.0.");
607
+ }
588
608
  log.info("Bootstrap started", {
589
609
  modules: pc2.cyan(config.modules),
590
610
  prefix: pc2.cyan(config.prefix || "(none)"),
@@ -623,14 +643,16 @@ async function createApp(app, options = {}) {
623
643
  for (const mod of resolvedModules) {
624
644
  const modName = path8.basename(mod.dirPath);
625
645
  const aliasKey = `@modules/${modName}`;
626
- pureModuleAliases[aliasKey] = mod.dirPath;
627
- registry.registerAlias(aliasKey, mod.dirPath);
646
+ pureModuleAliases[aliasKey] = mod.indexPath;
647
+ pureModuleAliases[`${aliasKey}/*`] = `${mod.dirPath}/*`;
648
+ registry.registerAlias(aliasKey, mod.indexPath);
649
+ registry.registerAlias(`${aliasKey}/*`, `${mod.dirPath}/*`);
628
650
  }
629
- activateAliasResolver(pureModuleAliases, config.aliases, log);
651
+ await activateAliasResolver(pureModuleAliases, config.aliases, log);
630
652
  updateAliasCache(registry.getAllAliases());
631
653
  }
632
654
  for (const mod of resolvedModules) {
633
- const imported = await import(pathToFileURL3(mod.indexPath).href);
655
+ const imported = await import(pathToFileURL2(mod.indexPath).href);
634
656
  const allRegistered = registry.getAllModules();
635
657
  const registeredMod = allRegistered.find((m) => path8.normalize(m.path) === path8.normalize(mod.dirPath));
636
658
  if (!registeredMod) {
@@ -712,7 +734,7 @@ async function createApp(app, options = {}) {
712
734
  file = path8.normalize(file);
713
735
  let imported;
714
736
  try {
715
- imported = await import(pathToFileURL3(file).href);
737
+ imported = await import(pathToFileURL2(file).href);
716
738
  } catch (err) {
717
739
  throw new NodulusError(
718
740
  "INVALID_CONTROLLER",
@@ -867,7 +889,6 @@ async function getAliases(options = {}) {
867
889
  }
868
890
  export {
869
891
  Controller,
870
- ERROR_MESSAGES,
871
892
  Module,
872
893
  NodulusError,
873
894
  Repository,