@teqfw/di 2.6.2 → 2.8.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.
@@ -2,218 +2,8 @@
2
2
 
3
3
  /**
4
4
  * @namespace TeqFw_Di_Config_NamespaceRegistry
5
- * @description Deterministic loader for package namespace rules.
5
+ * @description Deprecated compatibility re-export for the Node.js namespace registry.
6
+ * @deprecated Use @teqfw/di/node/registry/namespace in new integrations.
6
7
  */
7
8
 
8
- /**
9
- * @typedef {object} TeqFw_Di_Config_NamespaceRegistry_Dependencies
10
- * @property {{readFile(path: string, encoding: string): Promise<string>, readdir(path: string): Promise<string[]>, realpath(path: string): Promise<string>, stat(path: string): Promise<{isDirectory(): boolean}>}} fs
11
- * @property {{join(...paths: string[]): string, dirname(path: string): string, relative(from: string, to: string): string, resolve(...paths: string[]): string, isAbsolute(path: string): boolean}} path
12
- * @property {string} appRoot
13
- */
14
-
15
- /**
16
- * @typedef {object} TeqFw_Di_Config_NamespaceRegistry_Entry
17
- * @property {string} prefix
18
- * @property {string} dirAbs
19
- * @property {string} ext
20
- */
21
-
22
- /**
23
- * Builds deterministic immutable namespace registry from root package and installed dependencies.
24
- */
25
- export default class TeqFw_Di_Config_NamespaceRegistry {
26
- /**
27
- * @param {TeqFw_Di_Config_NamespaceRegistry_Dependencies} deps
28
- */
29
- constructor({fs, path, appRoot}) {
30
- const appRootAbs = path.resolve(appRoot);
31
-
32
- /**
33
- * @param {string} fileAbs
34
- * @returns {Promise<Record<string, unknown>>}
35
- */
36
- const readJson = async function (fileAbs) {
37
- const content = await fs.readFile(fileAbs, 'utf8');
38
- return /** @type {Record<string, unknown>} */ (JSON.parse(content));
39
- };
40
-
41
- /**
42
- * @param {string} absPath
43
- * @returns {Promise<boolean>}
44
- */
45
- const isDirectory = async function (absPath) {
46
- try {
47
- const stat = await fs.stat(absPath);
48
- return stat.isDirectory();
49
- } catch {
50
- return false;
51
- }
52
- };
53
-
54
- /**
55
- * @param {string} rootAbs
56
- * @param {string} candidateAbs
57
- * @returns {boolean}
58
- */
59
- const isInside = function (rootAbs, candidateAbs) {
60
- const rel = path.relative(rootAbs, candidateAbs);
61
- return (rel === '') || (!rel.startsWith('..') && !path.isAbsolute(rel));
62
- };
63
-
64
- /**
65
- * @param {string} packageRootAbs
66
- * @param {string} dirAbs
67
- * @returns {Promise<void>}
68
- */
69
- const assertInsidePackageRoot = async function (packageRootAbs, dirAbs) {
70
- const packageRootReal = await fs.realpath(packageRootAbs);
71
- const dirReal = await fs.realpath(dirAbs);
72
- if (!isInside(packageRootReal, dirReal)) {
73
- throw new Error(`Namespace path resolves outside package root: '${dirAbs}'.`);
74
- }
75
- };
76
-
77
- /**
78
- * @param {unknown} ext
79
- * @returns {string}
80
- */
81
- const normalizeExt = function (ext) {
82
- if (ext === undefined) return '.mjs';
83
- if ((typeof ext !== 'string') || (ext.length === 0)) {
84
- throw new Error('Namespace extension must be a non-empty string.');
85
- }
86
- const normalized = ext.startsWith('.') ? ext : `.${ext}`;
87
- if ((normalized !== '.mjs') && (normalized !== '.js')) {
88
- throw new Error(`Namespace extension is not ESM-compatible: '${normalized}'.`);
89
- }
90
- return normalized;
91
- };
92
-
93
- /**
94
- * @param {unknown} raw
95
- * @param {string} packageRootAbs
96
- * @returns {Promise<TeqFw_Di_Config_NamespaceRegistry_Entry>}
97
- */
98
- const normalizeEntry = async function (raw, packageRootAbs) {
99
- if ((raw === null) || (typeof raw !== 'object')) {
100
- throw new Error('Namespace entry must be an object.');
101
- }
102
- const item = /** @type {Record<string, unknown>} */ (raw);
103
-
104
- const prefix = item.prefix;
105
- if ((typeof prefix !== 'string') || (prefix.length === 0) || !prefix.endsWith('_')) {
106
- throw new Error(`Namespace prefix is invalid: '${String(prefix)}'.`);
107
- }
108
-
109
- const rawPath = item.path;
110
- if ((typeof rawPath !== 'string') || (rawPath.length === 0) || path.isAbsolute(rawPath)) {
111
- throw new Error(`Namespace path must be a non-empty relative path: '${String(rawPath)}'.`);
112
- }
113
- const dirAbs = path.resolve(packageRootAbs, rawPath);
114
- if (!(await isDirectory(dirAbs))) {
115
- throw new Error(`Namespace path does not point to existing directory: '${dirAbs}'.`);
116
- }
117
- await assertInsidePackageRoot(packageRootAbs, dirAbs);
118
-
119
- const ext = normalizeExt(item.ext);
120
-
121
- return {prefix, dirAbs, ext};
122
- };
123
-
124
- /**
125
- * @param {string} packageRootAbs
126
- * @returns {Promise<{namespaces: unknown[], dependencies: string[]}>}
127
- */
128
- const readPackageMetadata = async function (packageRootAbs) {
129
- const packageJsonAbs = path.join(packageRootAbs, 'package.json');
130
- const packageJson = await readJson(packageJsonAbs);
131
- const teqfw = (packageJson.teqfw && typeof packageJson.teqfw === 'object')
132
- ? /** @type {Record<string, unknown>} */ (packageJson.teqfw)
133
- : {};
134
- const namespaces = Array.isArray(teqfw.namespaces) ? teqfw.namespaces : [];
135
- const dependencies = (packageJson.dependencies && typeof packageJson.dependencies === 'object')
136
- ? Object.keys(/** @type {Record<string, unknown>} */ (packageJson.dependencies)).sort()
137
- : [];
138
- return {namespaces, dependencies};
139
- };
140
-
141
- /**
142
- * Checks whether package.json exists inside a dependency directory.
143
- *
144
- * @param {string} candidateAbs
145
- * @returns {Promise<boolean>}
146
- */
147
- const hasPackageJson = async function (candidateAbs) {
148
- try {
149
- await fs.stat(path.join(candidateAbs, 'package.json'));
150
- return true;
151
- } catch {
152
- return false;
153
- }
154
- };
155
-
156
- /**
157
- * @param {string} packageName
158
- * @param {string} fromPackageRootAbs
159
- * @returns {Promise<string>}
160
- */
161
- const resolveDependencyPackageRoot = async function (packageName, fromPackageRootAbs) {
162
- let cursor = fromPackageRootAbs;
163
- while (isInside(appRootAbs, cursor)) {
164
- const candidate = path.join(cursor, 'node_modules', packageName);
165
- if (await isDirectory(candidate) && (await hasPackageJson(candidate))) {
166
- return path.resolve(candidate);
167
- }
168
- if (cursor === appRootAbs) break;
169
- const parent = path.dirname(cursor);
170
- if (parent === cursor) break;
171
- cursor = parent;
172
- }
173
- throw new Error(`Installed dependency is not found: '${packageName}' from '${fromPackageRootAbs}'.`);
174
- };
175
-
176
- /**
177
- * @returns {Promise<Readonly<TeqFw_Di_Config_NamespaceRegistry_Entry[]>>}
178
- */
179
- this.build = async function () {
180
- /** @type {{name: string, rootAbs: string}[]} */
181
- const queue = [{rootAbs: appRootAbs}];
182
- /** @type {Set<string>} */
183
- const visitedRoots = new Set();
184
- /** @type {Set<string>} */
185
- const uniquePrefixes = new Set();
186
- /** @type {TeqFw_Di_Config_NamespaceRegistry_Entry[]} */
187
- const entries = [];
188
-
189
- while (queue.length > 0) {
190
- const current = queue.shift();
191
- const packageRootAbs = current.rootAbs;
192
- const packageRootReal = await fs.realpath(packageRootAbs);
193
- if (visitedRoots.has(packageRootReal)) continue;
194
- visitedRoots.add(packageRootReal);
195
-
196
- const meta = await readPackageMetadata(packageRootAbs);
197
- for (const raw of meta.namespaces) {
198
- const normalized = await normalizeEntry(raw, packageRootAbs);
199
- if (uniquePrefixes.has(normalized.prefix)) {
200
- throw new Error(`Duplicate namespace prefix is not allowed: '${normalized.prefix}'.`);
201
- }
202
- uniquePrefixes.add(normalized.prefix);
203
- entries.push(normalized);
204
- }
205
-
206
- for (const depName of meta.dependencies) {
207
- const depRootAbs = await resolveDependencyPackageRoot(depName, packageRootAbs);
208
- queue.push({rootAbs: depRootAbs});
209
- }
210
- }
211
-
212
- entries.sort((a, b) => b.prefix.length - a.prefix.length);
213
- for (const entry of entries) {
214
- Object.freeze(entry);
215
- }
216
- return Object.freeze(entries);
217
- };
218
- }
219
- }
9
+ export {default} from "../Node/Registry/Namespace.mjs";
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  /**
4
- * @namespace TeqFw_Di_Container_Wrapper_Executor
4
+ * @namespace TeqFw_Di_Container_Executor
5
5
  * @description Applies wrapper pipeline to resolved values.
6
6
  */
7
7
 
@@ -11,7 +11,7 @@
11
11
  * Executes wrapper pipeline declared in `depId.wrappers` using functions
12
12
  * exported by the resolved module namespace.
13
13
  */
14
- export default class TeqFw_Di_Container_Wrapper_Executor {
14
+ export default class TeqFw_Di_Container_Executor {
15
15
 
16
16
  /**
17
17
  * Creates wrapper executor instance.
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  /**
4
- * @namespace TeqFw_Di_Container_Resolve_GraphResolver
4
+ * @namespace TeqFw_Di_Container_GraphResolver
5
5
  * @description Dependency graph resolver for container preloading.
6
6
  */
7
7
 
@@ -13,25 +13,27 @@
13
13
  */
14
14
 
15
15
  /**
16
- * @typedef {object} TeqFw_Di_Container_Resolve_GraphResolver_Dependencies
17
- * @property {TeqFw_Di_Parser} parser
16
+ * @typedef {object} TeqFw_Di_Container_GraphResolver_Dependencies
17
+ * @property {(specifier: string) => TeqFw_Di_DepId__DTO} canonicalize
18
+ * @property {(depId: TeqFw_Di_DepId__DTO) => {found: boolean, value: unknown}} findMock
18
19
  * @property {TeqFw_Di_Resolver} resolver
19
20
  * @property {{log(message: string): void}|null} [logger]
20
21
  */
21
22
 
22
23
  /**
23
- * @typedef {{depId: TeqFw_Di_DepId__DTO, namespace: object}} TeqFw_Di_Container_Resolve_GraphResolver_Node
24
+ * @typedef {{depId: TeqFw_Di_DepId__DTO, namespace: object|null, dependencies: Map<string, string>, mock: {found: boolean, value: unknown}}} TeqFw_Di_Container_GraphResolver_Node
24
25
  */
25
26
 
26
27
  import {buildDependencyKey} from '../Internal/DependencyKey.mjs';
27
28
  import {readDepsDecl} from '../Internal/DepsDecl.mjs';
28
29
 
29
- export default class TeqFw_Di_Container_Resolve_GraphResolver {
30
+ export default class TeqFw_Di_Container_GraphResolver {
30
31
 
31
32
  /**
32
- * @param {TeqFw_Di_Container_Resolve_GraphResolver_Dependencies} deps
33
+ * @param {TeqFw_Di_Container_GraphResolver_Dependencies} deps
33
34
  */
34
- constructor({parser, resolver, logger = null}) {
35
+ constructor({canonicalize, parser, findMock = () => ({found: false, value: undefined}), resolver, logger = null}) {
36
+ canonicalize ??= parser.parse.bind(parser);
35
37
  /** @type {{log(message: string): void}|null} */
36
38
  const log = logger;
37
39
 
@@ -43,7 +45,7 @@ export default class TeqFw_Di_Container_Resolve_GraphResolver {
43
45
 
44
46
  /**
45
47
  * @param {TeqFw_Di_DepId__DTO} depId
46
- * @param {Map<string, TeqFw_Di_Container_Resolve_GraphResolver_Node>} out
48
+ * @param {Map<string, TeqFw_Di_Container_GraphResolver_Node>} out
47
49
  * @param {Set<string>} stack
48
50
  * @param {string[]} chain
49
51
  * @returns {Promise<void>}
@@ -61,19 +63,28 @@ export default class TeqFw_Di_Container_Resolve_GraphResolver {
61
63
  const key = makeNodeKey(depId);
62
64
  if (out.has(key)) return;
63
65
 
66
+ const mock = findMock(depId);
67
+ if (mock.found) {
68
+ out.set(key, {depId, namespace: null, dependencies: new Map(), mock});
69
+ return;
70
+ }
71
+
64
72
  stack.add(identity);
65
73
  chain.push(identity);
66
74
  try {
67
75
  /** @type {object} */
68
76
  const namespace = await resolver.resolve(depId);
69
77
  if (log) log.log(`GraphResolver.walk: resolved '${key}'.`);
70
- out.set(key, {depId, namespace});
78
+ /** @type {Map<string, string>} */
79
+ const dependencies = new Map();
80
+ out.set(key, {depId, namespace, dependencies, mock});
71
81
 
72
82
  /** @type {Record<string, unknown>} */
73
83
  const depsMap = readDepsDecl(namespace, depId);
74
- for (const nextCdc of Object.values(depsMap)) {
84
+ for (const [name, nextSpecifier] of Object.entries(depsMap)) {
75
85
  /** @type {TeqFw_Di_DepId__DTO} */
76
- const nextDepId = parser.parse(/** @type {string} */ (nextCdc));
86
+ const nextDepId = canonicalize(/** @type {string} */ (nextSpecifier));
87
+ dependencies.set(name, makeNodeKey(nextDepId));
77
88
  if (log) log.log(`GraphResolver.walk: edge '${key}' -> '${nextDepId.platform}::${nextDepId.moduleName}'.`);
78
89
  await walk(nextDepId, out, stack, chain);
79
90
  }
@@ -87,10 +98,10 @@ export default class TeqFw_Di_Container_Resolve_GraphResolver {
87
98
  * Resolves full dependency graph for a root depId.
88
99
  *
89
100
  * @param {TeqFw_Di_DepId__DTO} depId
90
- * @returns {Promise<Map<string, TeqFw_Di_Container_Resolve_GraphResolver_Node>>}
101
+ * @returns {Promise<Map<string, TeqFw_Di_Container_GraphResolver_Node>>}
91
102
  */
92
103
  this.resolve = async function (depId) {
93
- /** @type {Map<string, TeqFw_Di_Container_Resolve_GraphResolver_Node>} */
104
+ /** @type {Map<string, TeqFw_Di_Container_GraphResolver_Node>} */
94
105
  const out = new Map();
95
106
  /** @type {Set<string>} */
96
107
  const stack = new Set();
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  /**
4
- * @namespace TeqFw_Di_Container_Instantiate_Instantiator
4
+ * @namespace TeqFw_Di_Container_Instantiate
5
5
  * @description Instantiates selected exports using composition rules.
6
6
  */
7
7
 
@@ -25,7 +25,7 @@ import TeqFw_Di_Enum_Composition from '../Enum/Composition.mjs';
25
25
  /**
26
26
  * @typedef {CallableFactory | ConstructableFactory} Factory
27
27
  */
28
- export default class TeqFw_Di_Container_Instantiate_Instantiator {
28
+ export default class TeqFw_Di_Container_Instantiate {
29
29
 
30
30
  /**
31
31
  * Creates instantiator instance.
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  /**
4
- * @namespace TeqFw_Di_Container_Lifecycle_Registry
4
+ * @namespace TeqFw_Di_Container_Lifecycle
5
5
  * @description Lifecycle policy cache for produced values.
6
6
  */
7
7
 
@@ -17,7 +17,7 @@ import {buildDependencyKey} from '../Internal/DependencyKey.mjs';
17
17
  * - transient values are never cached;
18
18
  * - as-is composition is returned as produced without lifecycle caching.
19
19
  */
20
- export default class TeqFw_Di_Container_Lifecycle_Registry {
20
+ export default class TeqFw_Di_Container_Lifecycle {
21
21
 
22
22
  /**
23
23
  * Creates lifecycle registry instance.
@@ -6,35 +6,32 @@
6
6
  */
7
7
 
8
8
  import {buildDependencyKey} from '../Internal/DependencyKey.mjs';
9
- import {readDepsDecl} from '../Internal/DepsDecl.mjs';
10
9
  import {makePromiseSafe} from '../Internal/PromiseSafe.mjs';
11
10
 
12
11
  /**
13
12
  * @typedef {object} TeqFw_Di_Container_Pipeline_Context
14
- * @property {TeqFw_Di_Parser} parser
15
13
  * @property {TeqFw_Di_Resolver|undefined} resolver
16
- * @property {TeqFw_Di_Container_Resolve_GraphResolver|undefined} graphResolver
17
- * @property {TeqFw_Di_Container_Lifecycle_Registry|undefined} lifecycle
18
- * @property {TeqFw_Di_Container_Instantiate_Instantiator} instantiator
19
- * @property {TeqFw_Di_Container_Wrapper_Executor} wrapperExecutor
14
+ * @property {TeqFw_Di_Container_GraphResolver|undefined} graphResolver
15
+ * @property {TeqFw_Di_Container_Lifecycle|undefined} lifecycle
16
+ * @property {TeqFw_Di_Container_Instantiate} instantiator
17
+ * @property {TeqFw_Di_Container_Executor} wrapperExecutor
20
18
  * @property {{log(message: string): void, error(message: string, error?: unknown): void}} logger
21
19
  * @property {boolean} testMode
22
20
  * @property {Map<string, unknown>} mockRegistry
23
21
  * @property {(value: unknown) => unknown} freeze
24
- * @property {(depId: TeqFw_Di_DepId__DTO) => TeqFw_Di_DepId__DTO} applyPreprocess
25
- * @property {(value: unknown) => unknown} applyPostprocess
22
+ * @property {(specifier: string) => TeqFw_Di_DepId__DTO} canonicalize
23
+ * @property {(value: unknown, depId: TeqFw_Di_DepId__DTO) => unknown} applyPostprocess
26
24
  */
27
25
 
28
26
  /**
29
- * Executes full container get pipeline for a CDC.
27
+ * Executes the full container pipeline for a Dependency Specifier.
30
28
  *
31
29
  * @param {TeqFw_Di_Container_Pipeline_Context} ctx
32
- * @param {string} cdc
30
+ * @param {string} specifier
33
31
  * @returns {Promise<any>}
34
32
  */
35
- export async function executeContainerPipeline(ctx, cdc) {
33
+ export async function executeContainerPipeline(ctx, specifier) {
36
34
  const {
37
- parser,
38
35
  resolver,
39
36
  graphResolver,
40
37
  lifecycle,
@@ -44,7 +41,7 @@ export async function executeContainerPipeline(ctx, cdc) {
44
41
  testMode,
45
42
  mockRegistry,
46
43
  freeze,
47
- applyPreprocess,
44
+ canonicalize,
48
45
  applyPostprocess,
49
46
  } = ctx;
50
47
 
@@ -58,14 +55,14 @@ export async function executeContainerPipeline(ctx, cdc) {
58
55
  /** @type {string} */
59
56
  let stage = 'start';
60
57
  try {
61
- logger.log(`Container.get: cdc='${cdc}'.`);
58
+ logger.log(`Container.get: specifier='${specifier}'.`);
62
59
  stage = 'parse';
63
60
  logger.log('Container.pipeline: parse:entry.');
64
- const parsed = parser.parse(cdc);
61
+ const root = canonicalize(specifier);
62
+ const parsed = root;
65
63
  logger.log(`Container.pipeline: parse:exit '${parsed.platform}::${parsed.moduleName}'.`);
66
64
  stage = 'preprocess';
67
65
  logger.log('Container.pipeline: preprocess:entry.');
68
- const root = applyPreprocess(parsed);
69
66
  logger.log(`Container.pipeline: preprocess:exit '${root.platform}::${root.moduleName}'.`);
70
67
  if (testMode === true) {
71
68
  stage = 'mock';
@@ -95,6 +92,10 @@ export async function executeContainerPipeline(ctx, cdc) {
95
92
  if (built.has(key)) return built.get(key);
96
93
  if (!graph.has(key)) throw new Error(`Resolved graph node is missing for '${key}'.`);
97
94
  const node = graph.get(key);
95
+ if (node.mock?.found) {
96
+ stage = 'freeze';
97
+ return freeze(node.mock.value);
98
+ }
98
99
  stage = 'lifecycle';
99
100
  logger.log(`Container.pipeline: lifecycle:entry '${node.depId.platform}::${node.depId.moduleName}'.`);
100
101
  const value = lifecycle.apply(node.depId, function () {
@@ -102,17 +103,14 @@ export async function executeContainerPipeline(ctx, cdc) {
102
103
  logger.log(`Container.pipeline: instantiate:entry '${node.depId.platform}::${node.depId.moduleName}'.`);
103
104
  /** @type {Record<string, unknown>} */
104
105
  const deps = {};
105
- /** @type {Record<string, unknown>} */
106
- const depsDecl = readDepsDecl(node.namespace, node.depId);
107
- for (const [name, cdcValue] of Object.entries(depsDecl)) {
108
- const childDepId = parser.parse(/** @type {string} */ (cdcValue));
109
- deps[name] = build(getKey(childDepId));
106
+ for (const [name, childKey] of (node.dependencies ?? [])) {
107
+ deps[name] = build(childKey);
110
108
  }
111
109
  const instantiated = instantiator.instantiate(node.depId, node.namespace, deps);
112
110
  logger.log(`Container.pipeline: instantiate:exit '${node.depId.platform}::${node.depId.moduleName}'.`);
113
111
  stage = 'postprocess';
114
112
  logger.log(`Container.pipeline: postprocess:entry '${node.depId.platform}::${node.depId.moduleName}'.`);
115
- const postprocessed = applyPostprocess(instantiated);
113
+ const postprocessed = applyPostprocess(instantiated, node.depId);
116
114
  logger.log(`Container.pipeline: postprocess:exit '${node.depId.platform}::${node.depId.moduleName}'.`);
117
115
  const wrapped = wrapperExecutor.execute(node.depId, postprocessed, node.namespace);
118
116
  stage = 'freeze';