@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.
- package/CHANGELOG.md +14 -0
- package/README.md +173 -259
- package/dist/esm.js +1 -1
- package/dist/umd.js +1 -1
- package/package.json +19 -13
- package/src/AGENTS.md +177 -0
- package/src/Config/NamespaceRegistry.mjs +210 -0
- package/src/Container/Instantiate/ExportSelector.mjs +39 -0
- package/src/Container/Instantiate/Instantiator.mjs +143 -0
- package/src/Container/Lifecycle/Registry.mjs +81 -0
- package/src/Container/Resolve/GraphResolver.mjs +119 -0
- package/src/Container/Resolver.mjs +175 -0
- package/src/Container/Wrapper/Executor.mjs +71 -0
- package/src/Container.mjs +380 -0
- package/src/Def/Parser.mjs +146 -0
- package/src/Dto/DepId.mjs +131 -0
- package/src/Dto/Resolver/Config/Namespace.mjs +48 -0
- package/src/Dto/Resolver/Config.mjs +58 -0
- package/src/Enum/Composition.mjs +11 -0
- package/src/Enum/Life.mjs +11 -0
- package/src/Enum/Platform.mjs +12 -0
- package/src/Internal/Logger.mjs +54 -0
- package/types.d.ts +24 -32
- package/src/Api/Container/Config.js +0 -73
- package/src/Api/Container/Parser/Chunk.js +0 -27
- package/src/Api/Container/Parser.js +0 -34
- package/src/Api/Container/PostProcessor/Chunk.js +0 -17
- package/src/Api/Container/PostProcessor.js +0 -29
- package/src/Api/Container/PreProcessor/Chunk.js +0 -19
- package/src/Api/Container/PreProcessor.js +0 -27
- package/src/Api/Container/Resolver.js +0 -18
- package/src/Api/Container.js +0 -19
- package/src/Container/A/Composer/A/SpecParser.js +0 -86
- package/src/Container/A/Composer.js +0 -69
- package/src/Container/A/Parser/Chunk/Def.js +0 -69
- package/src/Container/A/Parser/Chunk/V02X.js +0 -66
- package/src/Container/Config.js +0 -93
- package/src/Container/Parser.js +0 -48
- package/src/Container/PostProcessor.js +0 -32
- package/src/Container/PreProcessor.js +0 -34
- package/src/Container/Resolver.js +0 -80
- package/src/Container.js +0 -187
- package/src/Defs.js +0 -22
- package/src/DepId.js +0 -52
- package/src/Pre/Replace.js +0 -80
- package/teqfw.json +0 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teqfw/di",
|
|
3
|
-
"version": "
|
|
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
|
-
"
|
|
35
|
-
|
|
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
|
-
"./
|
|
38
|
-
|
|
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/**/*.
|
|
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": "^
|
|
53
|
-
"@rollup/plugin-node-resolve": "^16.0.
|
|
59
|
+
"@eslint/js": "^10.0.1",
|
|
60
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
54
61
|
"@rollup/plugin-terser": "^0.4.4",
|
|
55
|
-
"eslint": "^
|
|
56
|
-
"rollup": "^4.
|
|
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
|
+
}
|