@teqfw/di 1.3.0 → 2.0.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 (46) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +173 -259
  3. package/dist/esm.js +1 -1
  4. package/dist/umd.js +1 -1
  5. package/package.json +19 -13
  6. package/src/AGENTS.md +177 -0
  7. package/src/Config/NamespaceRegistry.mjs +210 -0
  8. package/src/Container/Instantiate/ExportSelector.mjs +39 -0
  9. package/src/Container/Instantiate/Instantiator.mjs +143 -0
  10. package/src/Container/Lifecycle/Registry.mjs +81 -0
  11. package/src/Container/Resolve/GraphResolver.mjs +119 -0
  12. package/src/Container/Resolver.mjs +175 -0
  13. package/src/Container/Wrapper/Executor.mjs +71 -0
  14. package/src/Container.mjs +380 -0
  15. package/src/Def/Parser.mjs +146 -0
  16. package/src/Dto/DepId.mjs +131 -0
  17. package/src/Dto/Resolver/Config/Namespace.mjs +48 -0
  18. package/src/Dto/Resolver/Config.mjs +58 -0
  19. package/src/Enum/Composition.mjs +11 -0
  20. package/src/Enum/Life.mjs +11 -0
  21. package/src/Enum/Platform.mjs +12 -0
  22. package/src/Internal/Logger.mjs +54 -0
  23. package/types.d.ts +24 -32
  24. package/src/Api/Container/Config.js +0 -73
  25. package/src/Api/Container/Parser/Chunk.js +0 -27
  26. package/src/Api/Container/Parser.js +0 -34
  27. package/src/Api/Container/PostProcessor/Chunk.js +0 -17
  28. package/src/Api/Container/PostProcessor.js +0 -29
  29. package/src/Api/Container/PreProcessor/Chunk.js +0 -19
  30. package/src/Api/Container/PreProcessor.js +0 -27
  31. package/src/Api/Container/Resolver.js +0 -18
  32. package/src/Api/Container.js +0 -19
  33. package/src/Container/A/Composer/A/SpecParser.js +0 -86
  34. package/src/Container/A/Composer.js +0 -69
  35. package/src/Container/A/Parser/Chunk/Def.js +0 -69
  36. package/src/Container/A/Parser/Chunk/V02X.js +0 -66
  37. package/src/Container/Config.js +0 -93
  38. package/src/Container/Parser.js +0 -48
  39. package/src/Container/PostProcessor.js +0 -32
  40. package/src/Container/PreProcessor.js +0 -34
  41. package/src/Container/Resolver.js +0 -80
  42. package/src/Container.js +0 -187
  43. package/src/Defs.js +0 -22
  44. package/src/DepId.js +0 -52
  45. package/src/Pre/Replace.js +0 -80
  46. package/teqfw.json +0 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teqfw/di",
3
- "version": "1.3.0",
3
+ "version": "2.0.1",
4
4
  "description": "Dependency Injection container for ES6 modules that works in both browser and Node.js apps.",
5
5
  "keywords": [
6
6
  "dependency injection",
@@ -23,6 +23,9 @@
23
23
  "files": [
24
24
  "dist/",
25
25
  "src/",
26
+ "CHANGELOG.md",
27
+ "LICENSE",
28
+ "README.md",
26
29
  "teqfw.json",
27
30
  "types.d.ts"
28
31
  ],
@@ -31,12 +34,15 @@
31
34
  "types": "types.d.ts",
32
35
  "exports": {
33
36
  ".": {
34
- "import": "./dist/esm.js",
35
- "require": "./dist/umd.js"
37
+ "browser": {
38
+ "import": "./dist/esm.js",
39
+ "require": "./dist/umd.js"
40
+ },
41
+ "import": "./src/Container.mjs",
42
+ "default": "./src/Container.mjs"
36
43
  },
37
- "./pre/replace": {
38
- "import": "./src/Pre/Replace.js"
39
- }
44
+ "./src/Container.mjs": "./src/Container.mjs",
45
+ "./src/Config/NamespaceRegistry.mjs": "./src/Config/NamespaceRegistry.mjs"
40
46
  },
41
47
  "type": "module",
42
48
  "repository": {
@@ -45,16 +51,16 @@
45
51
  },
46
52
  "scripts": {
47
53
  "rollup": "rollup -c",
48
- "eslint": "npx eslint './src/**/*.js'",
49
- "test:unit": "node --test"
54
+ "eslint": "npx eslint './src/**/*.mjs'",
55
+ "test:unit": "node --test test/unit",
56
+ "test:integration": "node --test test/integration"
50
57
  },
51
58
  "devDependencies": {
52
- "@eslint/js": "^9.33.0",
53
- "@rollup/plugin-node-resolve": "^16.0.1",
59
+ "@eslint/js": "^10.0.1",
60
+ "@rollup/plugin-node-resolve": "^16.0.3",
54
61
  "@rollup/plugin-terser": "^0.4.4",
55
- "eslint": "^9.33.0",
56
- "rollup": "^4.47.1",
57
- "typescript": "^5.9.3"
62
+ "eslint": "^10.0.2",
63
+ "rollup": "^4.59.0"
58
64
  },
59
65
  "engines": {
60
66
  "node": ">=20"
package/src/AGENTS.md ADDED
@@ -0,0 +1,177 @@
1
+ # AGENTS.md — `src/`
2
+
3
+ Path: `./src/AGENTS.md`
4
+
5
+ ## Scope
6
+
7
+ This file governs all implementation source code located under `./src/`.
8
+
9
+ All changes in this directory MUST comply with normative documents located in `ctx/docs/code/`.
10
+
11
+ This file defines only implementation-level obligations. It does not redefine architecture, product, or composition semantics.
12
+
13
+ ## Normative References
14
+
15
+ The following documents are mandatory:
16
+
17
+ - `ctx/docs/code/structure.md`
18
+ - `ctx/docs/code/container.md`
19
+ - `ctx/docs/code/resolver.md`
20
+ - `ctx/docs/code/parser.md`
21
+ - `ctx/docs/code/depid.md`
22
+ - `ctx/docs/code/testing.md`
23
+ - `ctx/docs/code/jsdoc-spec.md`
24
+ - `ctx/docs/code/conventions/es6-modules.md`
25
+ - `ctx/docs/code/conventions/teqfw/dto.md`
26
+ - `ctx/docs/code/conventions/teqfw/enum.md`
27
+
28
+ Agent MUST read and follow them before generating or modifying code.
29
+
30
+ Deviation from these documents constitutes non-compliance.
31
+
32
+ ## Structural Compliance
33
+
34
+ Directory layout, namespace mapping, file naming, dependency direction, and static import rules MUST strictly follow:
35
+
36
+ `ctx/docs/code/structure.md`
37
+
38
+ In particular:
39
+
40
+ - `Enum/` and `Dto/` directories are mandatory.
41
+ - File names MUST use PascalCase.
42
+ - Underscores in file names are prohibited.
43
+ - Namespace hierarchy MUST be reflected in directory hierarchy.
44
+ - Static dependency direction MUST follow the allowed layer graph.
45
+ - `Container.mjs` is the only public entry point.
46
+
47
+ Structural violations are execution errors.
48
+
49
+ ## JSDoc Compliance
50
+
51
+ JSDoc is mandatory and governed by:
52
+
53
+ `ctx/docs/code/jsdoc-spec.md`
54
+
55
+ All implementation files MUST:
56
+
57
+ - include `// @ts-check`;
58
+ - contain a module-level JSDoc block;
59
+ - annotate all exported classes, functions, and public methods;
60
+ - annotate private methods and private fields;
61
+ - provide explicit `@typedef` for constructor dependency descriptors;
62
+ - use existing public type aliases from `types.d.ts` when available;
63
+ - avoid introducing duplicate typedefs for already declared public DTO types.
64
+
65
+ Local variables with non-trivial types MUST use `@type` annotations.
66
+
67
+ TypeScript source files are prohibited.
68
+
69
+ Absence of required JSDoc constitutes non-compliance.
70
+
71
+ ## DTO and Enum Rules
72
+
73
+ All DTO and Enum implementations MUST strictly follow:
74
+
75
+ - `ctx/docs/code/conventions/teqfw/dto.md`
76
+ - `ctx/docs/code/conventions/teqfw/enum.md`
77
+
78
+ In particular:
79
+
80
+ - DTO structural classes MUST NOT participate in DI.
81
+ - DTO factories MUST expose exactly one public `create(...)` method.
82
+ - Enum modules MUST export a single flat literal object as `default`.
83
+ - No behavioral logic may be introduced into DTO or Enum modules.
84
+ - Semantic constants MUST be referenced via Enum codifiers.
85
+ - String literals MUST NOT be used where an Enum codifier exists.
86
+
87
+ ## Fail-Fast Semantics (No Defensive Programming)
88
+
89
+ Implementation under `src/` follows strict fail-fast architecture.
90
+
91
+ Agents MUST assume that:
92
+
93
+ - DepId DTO instances are structurally valid.
94
+ - Resolver configuration DTO is valid.
95
+ - Enum codifiers are valid.
96
+ - Constructor dependency descriptors are correct.
97
+ - Public API callers provide semantically correct inputs.
98
+
99
+ Unless explicitly required by a code-level contract document, implementation MUST NOT:
100
+
101
+ - perform defensive runtime validation of input types,
102
+ - duplicate parser or DTO validation logic,
103
+ - introduce early-guard checks for invariants guaranteed by higher layers,
104
+ - validate constructor dependency descriptor shapes,
105
+ - add “safe” fallback branches,
106
+ - attempt graceful degradation.
107
+
108
+ This rule applies equally to internal and public methods, including `Container.get`.
109
+
110
+ If an invariant is violated, the system MUST fail at the point of use.
111
+ Explicit pre-validation for the sole purpose of producing earlier or clearer error messages is prohibited.
112
+
113
+ Redundant validation logic constitutes architectural violation.
114
+
115
+ ## Testing Alignment
116
+
117
+ Unit and integration tests MUST follow:
118
+
119
+ `ctx/docs/code/testing.md`
120
+
121
+ For every testable source module, exactly one corresponding unit test MUST exist under `test/unit/`, mirroring directory structure.
122
+
123
+ Tests MUST use:
124
+
125
+ - `node:test`
126
+ - `node:assert/strict`
127
+
128
+ Isolation and determinism requirements are mandatory.
129
+
130
+ ## Type Declaration Discipline
131
+
132
+ The file `types.d.ts` defines exported structural type aliases corresponding to implementation modules.
133
+
134
+ When adding or renaming an exported implementation module under `src/`, agent MUST:
135
+
136
+ - ensure a corresponding type alias exists in `types.d.ts`;
137
+ - ensure alias mapping follows namespace-to-file mapping rules.
138
+
139
+ Only types intended for global availability MAY be placed inside `declare global {}`.
140
+
141
+ All other types MUST be exported normally and imported explicitly.
142
+
143
+ Implicit global types are prohibited.
144
+
145
+ ## Responsibility Boundary
146
+
147
+ Agents operating under `src/` are responsible only for:
148
+
149
+ - implementation-level correctness,
150
+ - structural compliance,
151
+ - deterministic behavior,
152
+ - strict fail-fast semantics,
153
+ - JSDoc typing discipline,
154
+ - alignment with code-level contracts.
155
+
156
+ Agents MUST NOT:
157
+
158
+ - redefine architecture-level invariants,
159
+ - weaken fail-fast guarantees,
160
+ - introduce defensive runtime validation,
161
+ - modify dependency direction rules,
162
+ - introduce new extension points not defined at code level.
163
+
164
+ ## Summary
165
+
166
+ `src/` is governed by strict structural, typing, determinism, and fail-fast invariants.
167
+
168
+ Implementation must remain:
169
+
170
+ - structurally deterministic,
171
+ - layer-consistent,
172
+ - JSDoc-typed,
173
+ - DTO/Enum-disciplined,
174
+ - free of defensive redundancy,
175
+ - compliant with namespace-to-file mapping rules.
176
+
177
+ Any deviation constitutes non-compliant implementation.
@@ -0,0 +1,210 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @typedef {object} TeqFw_Di_Config_NamespaceRegistry_Dependencies
5
+ * @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
6
+ * @property {{join(...paths: string[]): string, dirname(path: string): string, relative(from: string, to: string): string, resolve(...paths: string[]): string, isAbsolute(path: string): boolean}} path
7
+ * @property {string} appRoot
8
+ */
9
+
10
+ /**
11
+ * @typedef {object} TeqFw_Di_Config_NamespaceRegistry_Entry
12
+ * @property {string} prefix
13
+ * @property {string} dirAbs
14
+ * @property {string} ext
15
+ */
16
+
17
+ /**
18
+ * Builds deterministic immutable namespace registry from root package and installed dependencies.
19
+ */
20
+ export default class TeqFw_Di_Config_NamespaceRegistry {
21
+ /**
22
+ * @param {TeqFw_Di_Config_NamespaceRegistry_Dependencies} dependencies
23
+ */
24
+ constructor({fs, path, appRoot}) {
25
+ const appRootAbs = path.resolve(appRoot);
26
+
27
+ /**
28
+ * @param {string} fileAbs
29
+ * @returns {Promise<Record<string, unknown>>}
30
+ */
31
+ const readJson = async function (fileAbs) {
32
+ const content = await fs.readFile(fileAbs, 'utf8');
33
+ return /** @type {Record<string, unknown>} */ (JSON.parse(content));
34
+ };
35
+
36
+ /**
37
+ * @param {string} absPath
38
+ * @returns {Promise<boolean>}
39
+ */
40
+ const isDirectory = async function (absPath) {
41
+ try {
42
+ const stat = await fs.stat(absPath);
43
+ return stat.isDirectory();
44
+ } catch {
45
+ return false;
46
+ }
47
+ };
48
+
49
+ /**
50
+ * @param {string} rootAbs
51
+ * @param {string} candidateAbs
52
+ * @returns {boolean}
53
+ */
54
+ const isInside = function (rootAbs, candidateAbs) {
55
+ const rel = path.relative(rootAbs, candidateAbs);
56
+ return (rel === '') || (!rel.startsWith('..') && !path.isAbsolute(rel));
57
+ };
58
+
59
+ /**
60
+ * @param {string} packageRootAbs
61
+ * @param {string} dirAbs
62
+ * @returns {Promise<void>}
63
+ */
64
+ const assertInsidePackageRoot = async function (packageRootAbs, dirAbs) {
65
+ const packageRootReal = await fs.realpath(packageRootAbs);
66
+ const dirReal = await fs.realpath(dirAbs);
67
+ if (!isInside(packageRootReal, dirReal)) {
68
+ throw new Error(`Namespace path resolves outside package root: '${dirAbs}'.`);
69
+ }
70
+ };
71
+
72
+ /**
73
+ * @param {unknown} ext
74
+ * @returns {string}
75
+ */
76
+ const normalizeExt = function (ext) {
77
+ if (ext === undefined) return '.mjs';
78
+ if ((typeof ext !== 'string') || (ext.length === 0)) {
79
+ throw new Error('Namespace extension must be a non-empty string.');
80
+ }
81
+ const normalized = ext.startsWith('.') ? ext : `.${ext}`;
82
+ if ((normalized !== '.mjs') && (normalized !== '.js')) {
83
+ throw new Error(`Namespace extension is not ESM-compatible: '${normalized}'.`);
84
+ }
85
+ return normalized;
86
+ };
87
+
88
+ /**
89
+ * @param {unknown} raw
90
+ * @param {string} packageRootAbs
91
+ * @returns {Promise<TeqFw_Di_Config_NamespaceRegistry_Entry>}
92
+ */
93
+ const normalizeEntry = async function (raw, packageRootAbs) {
94
+ if ((raw === null) || (typeof raw !== 'object')) {
95
+ throw new Error('Namespace entry must be an object.');
96
+ }
97
+ const item = /** @type {Record<string, unknown>} */ (raw);
98
+
99
+ const prefix = item.prefix;
100
+ if ((typeof prefix !== 'string') || (prefix.length === 0) || !prefix.endsWith('_')) {
101
+ throw new Error(`Namespace prefix is invalid: '${String(prefix)}'.`);
102
+ }
103
+
104
+ const rawPath = item.path;
105
+ if ((typeof rawPath !== 'string') || (rawPath.length === 0) || path.isAbsolute(rawPath)) {
106
+ throw new Error(`Namespace path must be a non-empty relative path: '${String(rawPath)}'.`);
107
+ }
108
+ const dirAbs = path.resolve(packageRootAbs, rawPath);
109
+ if (!(await isDirectory(dirAbs))) {
110
+ throw new Error(`Namespace path does not point to existing directory: '${dirAbs}'.`);
111
+ }
112
+ await assertInsidePackageRoot(packageRootAbs, dirAbs);
113
+
114
+ const ext = normalizeExt(item.ext);
115
+
116
+ return {prefix, dirAbs, ext};
117
+ };
118
+
119
+ /**
120
+ * @param {string} packageRootAbs
121
+ * @returns {Promise<{namespaces: unknown[], dependencies: string[]}>}
122
+ */
123
+ const readPackageMetadata = async function (packageRootAbs) {
124
+ const packageJsonAbs = path.join(packageRootAbs, 'package.json');
125
+ const packageJson = await readJson(packageJsonAbs);
126
+ const teqfw = (packageJson.teqfw && typeof packageJson.teqfw === 'object')
127
+ ? /** @type {Record<string, unknown>} */ (packageJson.teqfw)
128
+ : {};
129
+ const namespaces = Array.isArray(teqfw.namespaces) ? teqfw.namespaces : [];
130
+ const dependencies = (packageJson.dependencies && typeof packageJson.dependencies === 'object')
131
+ ? Object.keys(/** @type {Record<string, unknown>} */ (packageJson.dependencies)).sort()
132
+ : [];
133
+ return {namespaces, dependencies};
134
+ };
135
+
136
+ /**
137
+ * @param {string} packageName
138
+ * @param {string} fromPackageRootAbs
139
+ * @returns {Promise<string>}
140
+ */
141
+ const resolveDependencyPackageRoot = async function (packageName, fromPackageRootAbs) {
142
+ let cursor = fromPackageRootAbs;
143
+ while (isInside(appRootAbs, cursor)) {
144
+ const candidate = path.join(cursor, 'node_modules', packageName);
145
+ if (await isDirectory(candidate)) {
146
+ const packageJsonAbs = path.join(candidate, 'package.json');
147
+ const hasPackageJson = await (async () => {
148
+ try {
149
+ await fs.stat(packageJsonAbs);
150
+ return true;
151
+ } catch {
152
+ return false;
153
+ }
154
+ })();
155
+ if (hasPackageJson) {
156
+ return path.resolve(candidate);
157
+ }
158
+ }
159
+ if (cursor === appRootAbs) break;
160
+ const parent = path.dirname(cursor);
161
+ if (parent === cursor) break;
162
+ cursor = parent;
163
+ }
164
+ throw new Error(`Installed dependency is not found: '${packageName}' from '${fromPackageRootAbs}'.`);
165
+ };
166
+
167
+ /**
168
+ * @returns {Promise<Readonly<TeqFw_Di_Config_NamespaceRegistry_Entry[]>>}
169
+ */
170
+ this.build = async function () {
171
+ /** @type {{name: string, rootAbs: string}[]} */
172
+ const queue = [{name: '<root>', rootAbs: appRootAbs}];
173
+ /** @type {Set<string>} */
174
+ const visitedRoots = new Set();
175
+ /** @type {Set<string>} */
176
+ const uniquePrefixes = new Set();
177
+ /** @type {TeqFw_Di_Config_NamespaceRegistry_Entry[]} */
178
+ const entries = [];
179
+
180
+ while (queue.length > 0) {
181
+ const current = queue.shift();
182
+ const packageRootAbs = current.rootAbs;
183
+ const packageRootReal = await fs.realpath(packageRootAbs);
184
+ if (visitedRoots.has(packageRootReal)) continue;
185
+ visitedRoots.add(packageRootReal);
186
+
187
+ const meta = await readPackageMetadata(packageRootAbs);
188
+ for (const raw of meta.namespaces) {
189
+ const normalized = await normalizeEntry(raw, packageRootAbs);
190
+ if (uniquePrefixes.has(normalized.prefix)) {
191
+ throw new Error(`Duplicate namespace prefix is not allowed: '${normalized.prefix}'.`);
192
+ }
193
+ uniquePrefixes.add(normalized.prefix);
194
+ entries.push(normalized);
195
+ }
196
+
197
+ for (const depName of meta.dependencies) {
198
+ const depRootAbs = await resolveDependencyPackageRoot(depName, packageRootAbs);
199
+ queue.push({name: depName, rootAbs: depRootAbs});
200
+ }
201
+ }
202
+
203
+ entries.sort((a, b) => b.prefix.length - a.prefix.length);
204
+ for (const entry of entries) {
205
+ Object.freeze(entry);
206
+ }
207
+ return Object.freeze(entries);
208
+ };
209
+ }
210
+ }
@@ -0,0 +1,39 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Instantiate-stage export selector.
5
+ *
6
+ * Selects exactly one export from an already loaded ES module namespace object
7
+ * using only `depId.exportName`.
8
+ */
9
+ export default class TeqFw_Di_Container_Instantiate_ExportSelector {
10
+ /**
11
+ * Creates export selector instance.
12
+ */
13
+ constructor() {
14
+ /**
15
+ * Selects a raw export value from module namespace.
16
+ *
17
+ * @param {object} namespace Loaded ES module namespace object.
18
+ * @param {TeqFw_Di_DepId$DTO} depId Dependency identity DTO.
19
+ * @returns {unknown} Raw selected export value.
20
+ */
21
+ this.select = function (namespace, depId) {
22
+ if (!namespace || (typeof namespace !== 'object')) {
23
+ throw new Error('Namespace must be an object.');
24
+ }
25
+
26
+ /** @type {string|null} */
27
+ const exportName = depId.exportName;
28
+ if (exportName === null) {
29
+ throw new Error('Export name must not be null for export selection.');
30
+ }
31
+
32
+ if (!(exportName in namespace)) {
33
+ throw new Error(`Export '${exportName}' is not found in module namespace.`);
34
+ }
35
+
36
+ return namespace[exportName];
37
+ };
38
+ }
39
+ }
@@ -0,0 +1,143 @@
1
+ // @ts-check
2
+
3
+ import TeqFw_Di_Enum_Composition from '../../Enum/Composition.mjs';
4
+
5
+ /**
6
+ * Instantiate-stage immutable core executor.
7
+ *
8
+ * Performs export selection and composition execution only,
9
+ * using already resolved dependency values.
10
+ */
11
+
12
+ /**
13
+ * @typedef {(deps: object) => unknown} CallableFactory
14
+ */
15
+
16
+ /**
17
+ * @typedef {new (deps: object) => unknown} ConstructableFactory
18
+ */
19
+
20
+ /**
21
+ * @typedef {CallableFactory | ConstructableFactory} Factory
22
+ */
23
+ export default class TeqFw_Di_Container_Instantiate_Instantiator {
24
+
25
+ /**
26
+ * Creates instantiator instance.
27
+ */
28
+ constructor() {
29
+
30
+ /**
31
+ * Selects the value used by composition.
32
+ *
33
+ * @param {TeqFw_Di_DepId$DTO} depId
34
+ * @param {object} moduleNamespace
35
+ * @returns {Factory}
36
+ */
37
+ const selectExport = function (depId, moduleNamespace) {
38
+ if (depId.exportName === null) {
39
+ return moduleNamespace;
40
+ }
41
+
42
+ if (!(depId.exportName in moduleNamespace)) {
43
+ throw new Error(
44
+ `Export '${depId.exportName}' is not found in module namespace.`
45
+ );
46
+ }
47
+
48
+ return moduleNamespace[depId.exportName];
49
+ };
50
+
51
+ /**
52
+ * Determines whether a callable supports construction with `new`.
53
+ *
54
+ * @param {Function} value
55
+ * @returns {value is ConstructableFactory}
56
+ */
57
+ const isConstructible = function (value) {
58
+ try {
59
+ Reflect.construct(String, [], value);
60
+ return true;
61
+ } catch {
62
+ return false;
63
+ }
64
+ };
65
+
66
+ /**
67
+ * Detects Promise-like asynchronous return values.
68
+ *
69
+ * @param {unknown} value
70
+ * @returns {boolean}
71
+ */
72
+ const isThenable = function (value) {
73
+ if ((value === null) || (value === undefined)) {
74
+ return false;
75
+ }
76
+
77
+ const type = typeof value;
78
+
79
+ if ((type !== 'object') && (type !== 'function')) {
80
+ return false;
81
+ }
82
+
83
+ /** @type {{ then?: unknown }} */
84
+ const maybeThenable = value;
85
+
86
+ return (typeof maybeThenable.then === 'function');
87
+ };
88
+
89
+ /**
90
+ * Produces a value from a resolved module namespace and dependency map.
91
+ *
92
+ * @param {TeqFw_Di_DepId$DTO} depId
93
+ * @param {object} moduleNamespace
94
+ * @param {Record<string, unknown>} resolvedDeps
95
+ * @returns {unknown}
96
+ */
97
+ this.instantiate = function (depId, moduleNamespace, resolvedDeps) {
98
+ /** @type {Factory} */
99
+ const selected = selectExport(depId, moduleNamespace);
100
+
101
+ if (depId.composition === TeqFw_Di_Enum_Composition.AS_IS) {
102
+ return selected;
103
+ }
104
+
105
+ if (depId.composition === TeqFw_Di_Enum_Composition.FACTORY) {
106
+
107
+ if (typeof selected !== 'function') {
108
+ throw new Error(
109
+ 'Factory composition requires a callable export.'
110
+ );
111
+ }
112
+
113
+ /** @type {Factory} */
114
+ const factory = selected;
115
+
116
+ /** @type {unknown} */
117
+ let result;
118
+
119
+ if (isConstructible(factory)) {
120
+ /** @type {ConstructableFactory} */
121
+ const Ctor = factory;
122
+ result = new Ctor(resolvedDeps);
123
+ } else {
124
+ /** @type {CallableFactory} */
125
+ const Fn = factory;
126
+ result = Fn(resolvedDeps);
127
+ }
128
+
129
+ if (isThenable(result)) {
130
+ throw new Error(
131
+ 'Factory composition must return synchronously (non-thenable).'
132
+ );
133
+ }
134
+
135
+ return result;
136
+ }
137
+
138
+ throw new Error(
139
+ `Unsupported composition mode: ${String(depId.composition)}.`
140
+ );
141
+ };
142
+ }
143
+ }