@teqfw/di 2.7.0 → 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.
- package/CHANGELOG.md +16 -0
- package/README.md +63 -561
- package/ai/AGENTS.md +10 -54
- package/dist/esm.js +1 -1
- package/dist/umd.js +1 -1
- package/package.json +23 -16
- package/skills/teqfw-di/SKILL.md +36 -0
- package/skills/teqfw-di/agents/openai.yaml +4 -0
- package/skills/teqfw-di/references/compatibility.md +35 -0
- package/skills/teqfw-di/references/concepts.md +43 -0
- package/{ai → skills/teqfw-di/references}/container.md +12 -6
- package/{ai → skills/teqfw-di/references}/dependency-id.md +19 -8
- package/skills/teqfw-di/references/distribution.md +24 -0
- package/{ai → skills/teqfw-di/references}/extensions.md +18 -7
- package/{ai → skills/teqfw-di/references}/package-api.ts +54 -31
- package/skills/teqfw-di/references/usage.md +283 -0
- package/src/AGENTS.md +9 -1
- package/src/Config/NamespaceRegistry.mjs +3 -213
- package/src/Container/GraphResolver.mjs +17 -6
- package/src/Container/Pipeline.mjs +16 -18
- package/src/Container.mjs +56 -24
- package/src/Dto/DepId.mjs +1 -1
- package/src/Internal/DepsDecl.mjs +2 -2
- package/src/Node/Registry/Namespace.mjs +208 -0
- package/src/Node/Registry/Package.mjs +215 -0
- package/src/Parser.mjs +14 -14
- package/types.d.ts +5 -0
- package/ai/concepts.md +0 -25
- package/ai/usage.md +0 -201
|
@@ -2,218 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @namespace TeqFw_Di_Config_NamespaceRegistry
|
|
5
|
-
* @description
|
|
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";
|
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* @typedef {object} TeqFw_Di_Container_GraphResolver_Dependencies
|
|
17
|
-
* @property {
|
|
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_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';
|
|
@@ -31,7 +32,8 @@ export default class TeqFw_Di_Container_GraphResolver {
|
|
|
31
32
|
/**
|
|
32
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
|
|
|
@@ -61,19 +63,28 @@ export default class TeqFw_Di_Container_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
|
-
|
|
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
|
|
84
|
+
for (const [name, nextSpecifier] of Object.entries(depsMap)) {
|
|
75
85
|
/** @type {TeqFw_Di_DepId__DTO} */
|
|
76
|
-
const nextDepId =
|
|
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
|
}
|
|
@@ -6,12 +6,10 @@
|
|
|
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
14
|
* @property {TeqFw_Di_Container_GraphResolver|undefined} graphResolver
|
|
17
15
|
* @property {TeqFw_Di_Container_Lifecycle|undefined} lifecycle
|
|
@@ -21,20 +19,19 @@ import {makePromiseSafe} from '../Internal/PromiseSafe.mjs';
|
|
|
21
19
|
* @property {boolean} testMode
|
|
22
20
|
* @property {Map<string, unknown>} mockRegistry
|
|
23
21
|
* @property {(value: unknown) => unknown} freeze
|
|
24
|
-
* @property {(
|
|
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
|
|
27
|
+
* Executes the full container pipeline for a Dependency Specifier.
|
|
30
28
|
*
|
|
31
29
|
* @param {TeqFw_Di_Container_Pipeline_Context} ctx
|
|
32
|
-
* @param {string}
|
|
30
|
+
* @param {string} specifier
|
|
33
31
|
* @returns {Promise<any>}
|
|
34
32
|
*/
|
|
35
|
-
export async function executeContainerPipeline(ctx,
|
|
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
|
-
|
|
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:
|
|
58
|
+
logger.log(`Container.get: specifier='${specifier}'.`);
|
|
62
59
|
stage = 'parse';
|
|
63
60
|
logger.log('Container.pipeline: parse:entry.');
|
|
64
|
-
const
|
|
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
|
-
|
|
106
|
-
|
|
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';
|
package/src/Container.mjs
CHANGED
|
@@ -32,7 +32,7 @@ export default class TeqFw_Di_Container {
|
|
|
32
32
|
let state = 'notConfigured';
|
|
33
33
|
/** @type {((depId: TeqFw_Di_DepId__DTO) => TeqFw_Di_DepId__DTO)[]} */
|
|
34
34
|
const preprocess = [];
|
|
35
|
-
/** @type {((value: unknown) => unknown)[]} */
|
|
35
|
+
/** @type {((value: unknown, context: TeqFw_Di_Container_Postprocess_Context) => unknown)[]} */
|
|
36
36
|
const postprocess = [];
|
|
37
37
|
/** @type {TeqFw_Di_Dto_Resolver_Config_Namespace__DTO[]} */
|
|
38
38
|
const namespaceRoots = [];
|
|
@@ -60,18 +60,13 @@ export default class TeqFw_Di_Container {
|
|
|
60
60
|
const getKey = buildDependencyKey;
|
|
61
61
|
const getMockKey = buildDependencyKey;
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
let freeze = function (value) {
|
|
64
64
|
if ((value === null) || (value === undefined)) return value;
|
|
65
65
|
const type = typeof value;
|
|
66
66
|
if ((type !== 'object') && (type !== 'function')) return value;
|
|
67
67
|
if (Object.prototype.toString.call(value) === '[object Module]') return value;
|
|
68
68
|
if (Object.isFrozen(value)) return value;
|
|
69
|
-
|
|
70
|
-
Object.freeze(value);
|
|
71
|
-
} catch (error) {
|
|
72
|
-
logger.log(`Container.freeze: skipped (${String(error)}).`);
|
|
73
|
-
}
|
|
74
|
-
return value;
|
|
69
|
+
return Object.freeze(value);
|
|
75
70
|
};
|
|
76
71
|
|
|
77
72
|
const applyPreprocess = function (depId) {
|
|
@@ -81,10 +76,26 @@ export default class TeqFw_Di_Container {
|
|
|
81
76
|
return current;
|
|
82
77
|
};
|
|
83
78
|
|
|
84
|
-
const
|
|
79
|
+
const canonicalize = function (specifier) {
|
|
80
|
+
return applyPreprocess(parser.parse(specifier));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const findMock = function (depId) {
|
|
84
|
+
const key = getMockKey(depId);
|
|
85
|
+
return {found: testMode === true && mockRegistry.has(key), value: mockRegistry.get(key)};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const applyPostprocess = function (value, depId) {
|
|
85
89
|
/** @type {unknown} */
|
|
86
90
|
let current = value;
|
|
87
|
-
|
|
91
|
+
/** @type {TeqFw_Di_Container_Postprocess_Context} */
|
|
92
|
+
const context = Object.freeze({depId});
|
|
93
|
+
for (const fn of postprocess) {
|
|
94
|
+
current = fn(current, context);
|
|
95
|
+
if (current instanceof Promise) {
|
|
96
|
+
throw new Error('Postprocess callback must return synchronously (non-Promise).');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
88
99
|
return current;
|
|
89
100
|
};
|
|
90
101
|
|
|
@@ -104,7 +115,7 @@ export default class TeqFw_Di_Container {
|
|
|
104
115
|
const resolverConfig = configFactory.create({namespaces: namespaceRoots});
|
|
105
116
|
if (typeof parser.setLogger === 'function') parser.setLogger(logger);
|
|
106
117
|
resolver = new TeqFw_Di_Resolver({config: resolverConfig, logger});
|
|
107
|
-
graphResolver = new TeqFw_Di_Container_GraphResolver({
|
|
118
|
+
graphResolver = new TeqFw_Di_Container_GraphResolver({canonicalize, findMock, resolver, logger});
|
|
108
119
|
lifecycle = new TeqFw_Di_Container_Lifecycle(logger);
|
|
109
120
|
};
|
|
110
121
|
|
|
@@ -123,7 +134,7 @@ export default class TeqFw_Di_Container {
|
|
|
123
134
|
/**
|
|
124
135
|
* Adds a postprocessing hook.
|
|
125
136
|
*
|
|
126
|
-
* @param {(value: unknown) => unknown} fn
|
|
137
|
+
* @param {(value: unknown, context: TeqFw_Di_Container_Postprocess_Context) => unknown} fn
|
|
127
138
|
* @returns {void}
|
|
128
139
|
*/
|
|
129
140
|
this.addPostprocess = function (fn) {
|
|
@@ -132,6 +143,19 @@ export default class TeqFw_Di_Container {
|
|
|
132
143
|
postprocess.push(fn);
|
|
133
144
|
};
|
|
134
145
|
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Sets the host hardening policy for every resolved value and mock.
|
|
149
|
+
*
|
|
150
|
+
* @param {(value: unknown) => unknown} fn
|
|
151
|
+
* @returns {void}
|
|
152
|
+
*/
|
|
153
|
+
this.setHardener = function (fn) {
|
|
154
|
+
assertBuilderStage();
|
|
155
|
+
logBuilder('setHardener().');
|
|
156
|
+
freeze = fn;
|
|
157
|
+
};
|
|
158
|
+
|
|
135
159
|
/**
|
|
136
160
|
* Registers namespace root mapping.
|
|
137
161
|
*
|
|
@@ -172,29 +196,29 @@ export default class TeqFw_Di_Container {
|
|
|
172
196
|
};
|
|
173
197
|
|
|
174
198
|
/**
|
|
175
|
-
* Registers a mock value for a
|
|
199
|
+
* Registers a mock value for a Dependency Specifier.
|
|
176
200
|
*
|
|
177
|
-
* @param {string}
|
|
201
|
+
* @param {string} specifier
|
|
178
202
|
* @param {any} mock
|
|
179
203
|
* @returns {void}
|
|
180
204
|
*/
|
|
181
|
-
this.register = function (
|
|
205
|
+
this.register = function (specifier, mock) {
|
|
182
206
|
assertBuilderStage();
|
|
183
|
-
logBuilder(`register('${
|
|
207
|
+
logBuilder(`register('${specifier}').`);
|
|
184
208
|
if (testMode !== true) throw new Error('Container test mode is disabled.');
|
|
185
|
-
const depId =
|
|
209
|
+
const depId = canonicalize(specifier);
|
|
186
210
|
mockRegistry.set(getMockKey(depId), mock);
|
|
187
211
|
};
|
|
188
212
|
|
|
189
213
|
/**
|
|
190
|
-
* Resolves a
|
|
214
|
+
* Resolves a Dependency Specifier into a frozen Resolved Value.
|
|
191
215
|
*
|
|
192
|
-
* @param {string}
|
|
216
|
+
* @param {string} specifier
|
|
193
217
|
* @returns {Promise<any>}
|
|
194
218
|
*/
|
|
195
|
-
this.get = async function (
|
|
219
|
+
this.get = async function (specifier) {
|
|
196
220
|
if (state === 'failed') {
|
|
197
|
-
logger.error(`Container.get: rejected in failed state
|
|
221
|
+
logger.error(`Container.get: rejected in failed state specifier='${specifier}'.`);
|
|
198
222
|
throw new Error('Container is in failed state.');
|
|
199
223
|
}
|
|
200
224
|
|
|
@@ -202,7 +226,6 @@ export default class TeqFw_Di_Container {
|
|
|
202
226
|
initializeInfrastructure();
|
|
203
227
|
logger.log(`Container.state: '${state}'.`);
|
|
204
228
|
return await executeContainerPipeline({
|
|
205
|
-
parser,
|
|
206
229
|
resolver,
|
|
207
230
|
graphResolver,
|
|
208
231
|
lifecycle,
|
|
@@ -212,9 +235,9 @@ export default class TeqFw_Di_Container {
|
|
|
212
235
|
testMode,
|
|
213
236
|
mockRegistry,
|
|
214
237
|
freeze,
|
|
215
|
-
|
|
238
|
+
canonicalize,
|
|
216
239
|
applyPostprocess,
|
|
217
|
-
},
|
|
240
|
+
}, specifier);
|
|
218
241
|
} catch (error) {
|
|
219
242
|
logger.error(`Container.transition: operational -> failed.`, error);
|
|
220
243
|
state = 'failed';
|
|
@@ -223,3 +246,12 @@ export default class TeqFw_Di_Container {
|
|
|
223
246
|
};
|
|
224
247
|
}
|
|
225
248
|
}
|
|
249
|
+
|
|
250
|
+
const canonicalize = function (specifier) {
|
|
251
|
+
return applyPreprocess(parser.parse(specifier));
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const findMock = function (depId) {
|
|
255
|
+
const key = getMockKey(depId);
|
|
256
|
+
return {found: testMode === true && mockRegistry.has(key), value: mockRegistry.get(key)};
|
|
257
|
+
};
|
package/src/Dto/DepId.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Reads dependency declaration map for the selected export of a module namespace.
|
|
10
10
|
*
|
|
11
11
|
* @param {object} namespace Loaded module namespace.
|
|
12
|
-
* @param {TeqFw_Di_DepId__DTO} depId
|
|
12
|
+
* @param {TeqFw_Di_DepId__DTO} depId Parsed dependency identity.
|
|
13
13
|
* @returns {Record<string, unknown>} Dependency declaration map.
|
|
14
14
|
*/
|
|
15
15
|
export function readDepsDecl(namespace, depId) {
|
|
@@ -24,7 +24,7 @@ export function readDepsDecl(namespace, depId) {
|
|
|
24
24
|
if ((exportScoped !== undefined) && (exportScoped !== null) && (typeof exportScoped === 'object') && !Array.isArray(exportScoped)) {
|
|
25
25
|
const values = Object.values(/** @type {Record<string, unknown>} */ (exportScoped));
|
|
26
26
|
if (!values.every((value) => typeof value === 'string')) {
|
|
27
|
-
throw new Error('__deps__ export entries must map dependency names to
|
|
27
|
+
throw new Error('__deps__ export entries must map dependency names to Dependency Specifier strings.');
|
|
28
28
|
}
|
|
29
29
|
return /** @type {Record<string, unknown>} */ (exportScoped);
|
|
30
30
|
}
|