@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
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import TeqFw_Di_Node_Registry_Package from './Package.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @namespace TeqFw_Di_Node_Registry_Namespace
|
|
7
|
+
* @description Deterministic Node.js namespace registry derived from the runtime package graph.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} TeqFw_Di_Node_Registry_Namespace_Dependencies
|
|
12
|
+
* @property {{readFile(path: string, encoding: string): Promise<string>, realpath(path: string): Promise<string>, stat(path: string): Promise<{isDirectory(): boolean}>}} fs
|
|
13
|
+
* @property {{join(...paths: string[]): string, dirname(path: string): string, relative(from: string, to: string): string, resolve(...paths: string[]): string, isAbsolute(path: string): boolean}} path
|
|
14
|
+
* @property {string} appRoot
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {object} TeqFw_Di_Node_Registry_Namespace_Entry
|
|
19
|
+
* @property {string} prefix
|
|
20
|
+
* @property {string} dirAbs
|
|
21
|
+
* @property {string} ext
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Builds a deterministic immutable Node.js namespace registry from runtime package records.
|
|
26
|
+
*/
|
|
27
|
+
export default class TeqFw_Di_Node_Registry_Namespace {
|
|
28
|
+
/**
|
|
29
|
+
* @param {TeqFw_Di_Node_Registry_Namespace_Dependencies} deps
|
|
30
|
+
*/
|
|
31
|
+
constructor({fs, path, appRoot}) {
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} rootAbs
|
|
34
|
+
* @param {string} candidateAbs
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
const isInside = function (rootAbs, candidateAbs) {
|
|
38
|
+
const rel = path.relative(rootAbs, candidateAbs);
|
|
39
|
+
return (rel === '') || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {unknown} value
|
|
44
|
+
* @returns {value is Record<string, unknown>}
|
|
45
|
+
*/
|
|
46
|
+
const isRecord = function (value) {
|
|
47
|
+
return (value !== null) && (typeof value === 'object') && !Array.isArray(value);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {{name: string, rootAbs: string}} publisher
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
const publisherLabel = function (publisher) {
|
|
55
|
+
return `package '${publisher.name}' at '${publisher.rootAbs}'`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} absPath
|
|
60
|
+
* @returns {Promise<boolean>}
|
|
61
|
+
*/
|
|
62
|
+
const isDirectory = async function (absPath) {
|
|
63
|
+
try {
|
|
64
|
+
return (await fs.stat(absPath)).isDirectory();
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} packageRootAbs
|
|
72
|
+
* @param {string} dirAbs
|
|
73
|
+
* @returns {Promise<void>}
|
|
74
|
+
*/
|
|
75
|
+
const assertInsidePackageRoot = async function (packageRootAbs, dirAbs) {
|
|
76
|
+
const packageRootReal = await fs.realpath(packageRootAbs);
|
|
77
|
+
const dirReal = await fs.realpath(dirAbs);
|
|
78
|
+
if (!isInside(packageRootReal, dirReal)) {
|
|
79
|
+
throw new Error('Namespace path resolves outside package root: ' + dirAbs + '.');
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {unknown} ext
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
const normalizeExt = function (ext) {
|
|
88
|
+
if (ext === undefined) return '.mjs';
|
|
89
|
+
if ((typeof ext !== 'string') || (ext.length === 0)) {
|
|
90
|
+
throw new Error('Namespace extension must be a non-empty string.');
|
|
91
|
+
}
|
|
92
|
+
const normalized = ext.startsWith('.') ? ext : '.' + ext;
|
|
93
|
+
if ((normalized !== '.mjs') && (normalized !== '.js')) {
|
|
94
|
+
throw new Error('Namespace extension is not ESM-compatible: ' + normalized + '.');
|
|
95
|
+
}
|
|
96
|
+
return normalized;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {unknown} raw
|
|
101
|
+
* @param {{name: string, rootAbs: string}} publisher
|
|
102
|
+
* @param {{source: 'canonical'|'legacy', index: number}} origin
|
|
103
|
+
* @returns {Promise<{entry: TeqFw_Di_Node_Registry_Namespace_Entry, origin: {source: 'canonical'|'legacy', index: number}}>}
|
|
104
|
+
*/
|
|
105
|
+
const normalizeEntry = async function (raw, publisher, origin) {
|
|
106
|
+
const packageRootAbs = publisher.rootAbs;
|
|
107
|
+
const source = `${publisherLabel(publisher)} ${origin.source} namespace declaration at index ${origin.index}`;
|
|
108
|
+
if (!isRecord(raw)) {
|
|
109
|
+
throw new Error(`DI namespace declared by ${source} must be an object.`);
|
|
110
|
+
}
|
|
111
|
+
const item = raw;
|
|
112
|
+
const prefix = item.prefix;
|
|
113
|
+
if ((typeof prefix !== 'string') || (prefix.length === 0) || !prefix.endsWith('_')) {
|
|
114
|
+
throw new Error(`DI namespace prefix declared by ${source} is invalid: ${String(prefix)}.`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const rawPath = item.path;
|
|
118
|
+
if ((typeof rawPath !== 'string') || (rawPath.length === 0) || path.isAbsolute(rawPath)) {
|
|
119
|
+
throw new Error(`DI namespace path declared by ${source} must be a non-empty relative path: ${String(rawPath)}.`);
|
|
120
|
+
}
|
|
121
|
+
const dirAbs = path.resolve(packageRootAbs, rawPath);
|
|
122
|
+
if (!isInside(packageRootAbs, dirAbs)) {
|
|
123
|
+
throw new Error(`DI namespace path declared by ${source} lexically escapes its package root: ${rawPath}.`);
|
|
124
|
+
}
|
|
125
|
+
if (!(await isDirectory(dirAbs))) {
|
|
126
|
+
throw new Error(`DI namespace path declared by ${source} does not point to an existing directory: ${dirAbs}.`);
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
await assertInsidePackageRoot(packageRootAbs, dirAbs);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw new Error(`DI namespace path declared by ${source} resolves outside its package root: ${dirAbs}.`, {cause: error});
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
return {entry: {prefix, dirAbs, ext: normalizeExt(item.ext)}, origin};
|
|
135
|
+
} catch (error) {
|
|
136
|
+
throw new Error(`DI namespace extension declared by ${source} is invalid: ${String(error)}.`, {cause: error});
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Selects one supported declaration set without merging schemas.
|
|
142
|
+
*
|
|
143
|
+
* @param {Readonly<Record<string, unknown>>} packageJson
|
|
144
|
+
* @param {{name: string, rootAbs: string}} publisher
|
|
145
|
+
* @returns {{source: 'canonical'|'legacy', entries: unknown[]}|undefined}
|
|
146
|
+
*/
|
|
147
|
+
const selectDeclarationSet = function (packageJson, publisher) {
|
|
148
|
+
const teqfw = packageJson.teqfw;
|
|
149
|
+
if (teqfw === undefined) return undefined;
|
|
150
|
+
if (!isRecord(teqfw)) throw new Error(`TeqFW metadata for ${publisherLabel(publisher)} must be an object.`);
|
|
151
|
+
const fw = teqfw.fw;
|
|
152
|
+
if (fw !== undefined) {
|
|
153
|
+
if (!isRecord(fw)) throw new Error(`TeqFW framework metadata for ${publisherLabel(publisher)} must be an object.`);
|
|
154
|
+
const di = fw.di;
|
|
155
|
+
if (di !== undefined) {
|
|
156
|
+
if (!isRecord(di)) throw new Error(`TeqFW DI metadata for ${publisherLabel(publisher)} must be an object.`);
|
|
157
|
+
if (Object.hasOwn(di, 'namespaces')) {
|
|
158
|
+
if (!Array.isArray(di.namespaces)) {
|
|
159
|
+
throw new Error(`Canonical DI namespaces declared by ${publisherLabel(publisher)} must be an array.`);
|
|
160
|
+
}
|
|
161
|
+
return {source: 'canonical', entries: di.namespaces};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (Object.hasOwn(teqfw, 'namespaces')) {
|
|
166
|
+
if (!Array.isArray(teqfw.namespaces)) {
|
|
167
|
+
throw new Error(`Legacy DI namespaces declared by ${publisherLabel(publisher)} must be an array.`);
|
|
168
|
+
}
|
|
169
|
+
return {source: 'legacy', entries: teqfw.namespaces};
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @returns {Promise<ReadonlyArray<TeqFw_Di_Node_Registry_Namespace_Entry>>}
|
|
176
|
+
*/
|
|
177
|
+
this.build = async function () {
|
|
178
|
+
const packages = await new TeqFw_Di_Node_Registry_Package({fs, path, appRoot}).build();
|
|
179
|
+
/** @type {Map<string, {publisher: {name: string, rootAbs: string}, origin: {source: 'canonical'|'legacy', index: number}}>} */
|
|
180
|
+
const publisherByPrefix = new Map();
|
|
181
|
+
/** @type {TeqFw_Di_Node_Registry_Namespace_Entry[]} */
|
|
182
|
+
const entries = [];
|
|
183
|
+
|
|
184
|
+
for (const onePackage of packages) {
|
|
185
|
+
const packageJson = onePackage.packageJson;
|
|
186
|
+
const publisher = {name: onePackage.name, rootAbs: onePackage.rootAbs};
|
|
187
|
+
const declarationSet = selectDeclarationSet(packageJson, publisher);
|
|
188
|
+
if (declarationSet === undefined) continue;
|
|
189
|
+
for (const [index, raw] of declarationSet.entries.entries()) {
|
|
190
|
+
const origin = {source: declarationSet.source, index};
|
|
191
|
+
const normalized = await normalizeEntry(raw, publisher, origin);
|
|
192
|
+
const existing = publisherByPrefix.get(normalized.entry.prefix);
|
|
193
|
+
if (existing) {
|
|
194
|
+
throw new Error(`Duplicate DI namespace prefix '${normalized.entry.prefix}' declared by ${publisherLabel(publisher)} ${origin.source} namespace declaration at index ${origin.index} conflicts with ${publisherLabel(existing.publisher)} ${existing.origin.source} namespace declaration at index ${existing.origin.index}.`);
|
|
195
|
+
}
|
|
196
|
+
publisherByPrefix.set(normalized.entry.prefix, {publisher, origin: normalized.origin});
|
|
197
|
+
entries.push(normalized.entry);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
entries.sort((a, b) => (b.prefix.length - a.prefix.length) || a.prefix.localeCompare(b.prefix));
|
|
202
|
+
for (const entry of entries) {
|
|
203
|
+
Object.freeze(entry);
|
|
204
|
+
}
|
|
205
|
+
return Object.freeze(entries);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @namespace TeqFw_Di_Node_Registry_Package
|
|
5
|
+
* @description Deterministic Node.js composition-stage graph of installed runtime packages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {object} TeqFw_Di_Node_Registry_Package_Dependencies
|
|
10
|
+
* @property {{readFile(path: string, encoding: 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
|
+
* Builds a deterministic immutable dependency-first list of root and installed runtime packages.
|
|
17
|
+
*/
|
|
18
|
+
export default class TeqFw_Di_Node_Registry_Package {
|
|
19
|
+
/**
|
|
20
|
+
* @param {TeqFw_Di_Node_Registry_Package_Dependencies} deps
|
|
21
|
+
*/
|
|
22
|
+
constructor({fs, path, appRoot}) {
|
|
23
|
+
const appRootAbs = path.resolve(appRoot);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {unknown} value
|
|
27
|
+
* @returns {value is Record<string, unknown>}
|
|
28
|
+
*/
|
|
29
|
+
const isRecord = function (value) {
|
|
30
|
+
return (value !== null) && (typeof value === 'object') && !Array.isArray(value);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {object} value
|
|
35
|
+
* @returns {object}
|
|
36
|
+
*/
|
|
37
|
+
const freezeDeep = function (value) {
|
|
38
|
+
for (const nested of Object.values(value)) {
|
|
39
|
+
if ((nested !== null) && (typeof nested === 'object') && !Object.isFrozen(nested)) {
|
|
40
|
+
freezeDeep(nested);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return Object.freeze(value);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} fileAbs
|
|
48
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
49
|
+
*/
|
|
50
|
+
const readJson = async function (fileAbs) {
|
|
51
|
+
let content;
|
|
52
|
+
try {
|
|
53
|
+
content = await fs.readFile(fileAbs, 'utf8');
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new Error('Unable to read package metadata ' + fileAbs + ': ' + String(error) + '.');
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const value = JSON.parse(content);
|
|
59
|
+
if (!isRecord(value)) {
|
|
60
|
+
throw new Error('root must be an object');
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error('Invalid package metadata ' + fileAbs + ': ' + String(error) + '.');
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {string} packageRootAbs
|
|
70
|
+
* @returns {Promise<{name: string, packageJson: Record<string, unknown>, dependencies: string[]}>}
|
|
71
|
+
*/
|
|
72
|
+
const readPackageMetadata = async function (packageRootAbs) {
|
|
73
|
+
const packageJsonAbs = path.join(packageRootAbs, 'package.json');
|
|
74
|
+
const packageJson = await readJson(packageJsonAbs);
|
|
75
|
+
const name = packageJson.name;
|
|
76
|
+
if ((typeof name !== 'string') || (name.length === 0)) {
|
|
77
|
+
throw new Error('Invalid package metadata ' + packageJsonAbs + ': name must be a non-empty string.');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const rawDependencies = packageJson.dependencies;
|
|
81
|
+
if (rawDependencies === undefined) {
|
|
82
|
+
return {name, packageJson, dependencies: []};
|
|
83
|
+
}
|
|
84
|
+
if (!isRecord(rawDependencies)) {
|
|
85
|
+
throw new Error('Invalid dependency metadata ' + packageJsonAbs + ': dependencies must be an object.');
|
|
86
|
+
}
|
|
87
|
+
const dependencies = Object.keys(rawDependencies).sort();
|
|
88
|
+
for (const dependencyName of dependencies) {
|
|
89
|
+
const dependencyRange = rawDependencies[dependencyName];
|
|
90
|
+
if ((dependencyName.length === 0) || (typeof dependencyRange !== 'string') || (dependencyRange.length === 0)) {
|
|
91
|
+
throw new Error('Invalid dependency metadata ' + packageJsonAbs + ': ' + dependencyName + ' must have a non-empty string declaration.');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {name, packageJson, dependencies};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {string} absPath
|
|
99
|
+
* @returns {Promise<boolean>}
|
|
100
|
+
*/
|
|
101
|
+
const isDirectory = async function (absPath) {
|
|
102
|
+
try {
|
|
103
|
+
return (await fs.stat(absPath)).isDirectory();
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} rootAbs
|
|
111
|
+
* @param {string} candidateAbs
|
|
112
|
+
* @returns {boolean}
|
|
113
|
+
*/
|
|
114
|
+
const isInside = function (rootAbs, candidateAbs) {
|
|
115
|
+
const rel = path.relative(rootAbs, candidateAbs);
|
|
116
|
+
return (rel === '') || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {string} candidateAbs
|
|
121
|
+
* @returns {Promise<boolean>}
|
|
122
|
+
*/
|
|
123
|
+
const hasPackageJson = async function (candidateAbs) {
|
|
124
|
+
try {
|
|
125
|
+
await fs.stat(path.join(candidateAbs, 'package.json'));
|
|
126
|
+
return true;
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {string} packageName
|
|
134
|
+
* @param {string} fromPackageRootAbs
|
|
135
|
+
* @returns {Promise<string>}
|
|
136
|
+
*/
|
|
137
|
+
const resolveDependencyPackageRoot = async function (packageName, fromPackageRootAbs) {
|
|
138
|
+
let cursor = fromPackageRootAbs;
|
|
139
|
+
while (isInside(appRootAbs, cursor)) {
|
|
140
|
+
const candidate = path.join(cursor, 'node_modules', packageName);
|
|
141
|
+
if ((await isDirectory(candidate)) && (await hasPackageJson(candidate))) {
|
|
142
|
+
return path.resolve(candidate);
|
|
143
|
+
}
|
|
144
|
+
if (cursor === appRootAbs) break;
|
|
145
|
+
const parent = path.dirname(cursor);
|
|
146
|
+
if (parent === cursor) break;
|
|
147
|
+
cursor = parent;
|
|
148
|
+
}
|
|
149
|
+
throw new Error('Installed dependency is not found: ' + packageName + ' from ' + fromPackageRootAbs + '.');
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @returns {Promise<ReadonlyArray<TeqFw_Di_Node_Registry_Package_Record>>}
|
|
154
|
+
*/
|
|
155
|
+
this.build = async function () {
|
|
156
|
+
/** @type {Map<string, TeqFw_Di_Node_Registry_Package_Record>} */
|
|
157
|
+
const recordsByRoot = new Map();
|
|
158
|
+
/** @type {Set<string>} */
|
|
159
|
+
const visitingRoots = new Set();
|
|
160
|
+
/** @type {TeqFw_Di_Node_Registry_Package_Record[]} */
|
|
161
|
+
const records = [];
|
|
162
|
+
/** @type {string[]} */
|
|
163
|
+
const pathStack = [];
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Visits a package after all of its dependencies.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} rootAbs
|
|
169
|
+
* @returns {Promise<string>}
|
|
170
|
+
*/
|
|
171
|
+
const visit = async function (rootAbs) {
|
|
172
|
+
let rootReal;
|
|
173
|
+
try {
|
|
174
|
+
rootReal = await fs.realpath(rootAbs);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
throw new Error('Unable to resolve package root ' + rootAbs + ': ' + String(error) + '.');
|
|
177
|
+
}
|
|
178
|
+
if (recordsByRoot.has(rootReal)) return rootReal;
|
|
179
|
+
if (visitingRoots.has(rootReal)) {
|
|
180
|
+
const cycleStart = pathStack.indexOf(rootReal);
|
|
181
|
+
const cycle = [...pathStack.slice(cycleStart), rootReal].join(' -> ');
|
|
182
|
+
throw new Error('Cyclic package dependency detected: ' + cycle + '.');
|
|
183
|
+
}
|
|
184
|
+
visitingRoots.add(rootReal);
|
|
185
|
+
pathStack.push(rootReal);
|
|
186
|
+
|
|
187
|
+
const metadata = await readPackageMetadata(rootAbs);
|
|
188
|
+
const packageJson = /** @type {Readonly<Record<string, unknown>>} */ (freezeDeep(metadata.packageJson));
|
|
189
|
+
/** @type {string[]} */
|
|
190
|
+
const dependencies = [];
|
|
191
|
+
|
|
192
|
+
for (const dependencyName of metadata.dependencies) {
|
|
193
|
+
const dependencyRootAbs = await resolveDependencyPackageRoot(dependencyName, rootAbs);
|
|
194
|
+
dependencies.push(await visit(dependencyRootAbs));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
pathStack.pop();
|
|
198
|
+
visitingRoots.delete(rootReal);
|
|
199
|
+
const record = Object.freeze({
|
|
200
|
+
name: metadata.name,
|
|
201
|
+
rootAbs,
|
|
202
|
+
rootReal,
|
|
203
|
+
packageJson,
|
|
204
|
+
dependencies: Object.freeze([...new Set(dependencies)]),
|
|
205
|
+
});
|
|
206
|
+
recordsByRoot.set(rootReal, record);
|
|
207
|
+
records.push(record);
|
|
208
|
+
return rootReal;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
await visit(appRootAbs);
|
|
212
|
+
return Object.freeze(records);
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
package/src/Parser.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @namespace TeqFw_Di_Parser
|
|
5
|
-
* @description
|
|
5
|
+
* @description Dependency Specifier parser that builds dependency identity DTOs.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import TeqFw_Di_Enum_Composition from './Enum/Composition.mjs';
|
|
@@ -11,7 +11,7 @@ import TeqFw_Di_Enum_Platform from './Enum/Platform.mjs';
|
|
|
11
11
|
import {Factory as TeqFw_Di_Dto_DepId_Factory} from './Dto/DepId.mjs';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Parser for
|
|
14
|
+
* Parser for Dependency Specifiers into frozen dependency identity DTOs.
|
|
15
15
|
*/
|
|
16
16
|
export default class TeqFw_Di_Parser {
|
|
17
17
|
/**
|
|
@@ -26,7 +26,7 @@ export default class TeqFw_Di_Parser {
|
|
|
26
26
|
/**
|
|
27
27
|
* Detects platform prefix and strips it from the source string.
|
|
28
28
|
*
|
|
29
|
-
* @param {string} source
|
|
29
|
+
* @param {string} source Dependency Specifier source without validation.
|
|
30
30
|
* @returns {{platform: typeof TeqFw_Di_Enum_Platform[keyof typeof TeqFw_Di_Enum_Platform], source: string}}
|
|
31
31
|
*/
|
|
32
32
|
const detectPlatform = function (source) {
|
|
@@ -49,7 +49,7 @@ export default class TeqFw_Di_Parser {
|
|
|
49
49
|
/**
|
|
50
50
|
* Parses lifecycle and wrapper suffix from the source string.
|
|
51
51
|
*
|
|
52
|
-
* @param {string} source
|
|
52
|
+
* @param {string} source Dependency Specifier source without platform prefix.
|
|
53
53
|
* @param {typeof TeqFw_Di_Enum_Platform[keyof typeof TeqFw_Di_Enum_Platform]} platform
|
|
54
54
|
* @returns {{core: string, life: typeof TeqFw_Di_Enum_Life[keyof typeof TeqFw_Di_Enum_Life] | null, lifecycleDeclared: boolean, wrappers: string[]}}
|
|
55
55
|
*/
|
|
@@ -89,7 +89,7 @@ export default class TeqFw_Di_Parser {
|
|
|
89
89
|
/**
|
|
90
90
|
* Splits module and export names from canonical core string.
|
|
91
91
|
*
|
|
92
|
-
* @param {string} core
|
|
92
|
+
* @param {string} core Dependency Specifier core without lifecycle suffix.
|
|
93
93
|
* @returns {{moduleName: string, exportName: string|null}}
|
|
94
94
|
*/
|
|
95
95
|
const parseModuleExport = function (core) {
|
|
@@ -139,20 +139,20 @@ export default class TeqFw_Di_Parser {
|
|
|
139
139
|
};
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
|
-
* Parses one
|
|
142
|
+
* Parses one Dependency Specifier and returns a normalized frozen dependency DTO.
|
|
143
143
|
*
|
|
144
|
-
* @param {string}
|
|
144
|
+
* @param {string} specifier Dependency Specifier string.
|
|
145
145
|
* @returns {TeqFw_Di_DepId__DTO}
|
|
146
146
|
*/
|
|
147
|
-
this.parse = function (
|
|
148
|
-
if (logger) logger.log(`Parser.parse: input='${
|
|
149
|
-
if (typeof
|
|
150
|
-
if (
|
|
151
|
-
if (!/^[\x00-\x7F]+$/.test(
|
|
147
|
+
this.parse = function (specifier) {
|
|
148
|
+
if (logger) logger.log(`Parser.parse: input='${specifier}'.`);
|
|
149
|
+
if (typeof specifier !== 'string') throw new Error('Dependency Specifier must be a string.');
|
|
150
|
+
if (specifier.length === 0) throw new Error('Dependency Specifier must be non-empty.');
|
|
151
|
+
if (!/^[\x00-\x7F]+$/.test(specifier)) throw new Error('Dependency Specifier must be ASCII.');
|
|
152
152
|
|
|
153
153
|
/** @type {string} */
|
|
154
|
-
const origin =
|
|
155
|
-
const detected = detectPlatform(
|
|
154
|
+
const origin = specifier;
|
|
155
|
+
const detected = detectPlatform(specifier);
|
|
156
156
|
const platform = detected.platform;
|
|
157
157
|
const source = detected.source;
|
|
158
158
|
if (source.length === 0) throw new Error('moduleName must be non-empty.');
|
package/types.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
declare global {
|
|
2
|
+
type TeqFw_Di_Node_Registry_Namespace = import("./src/Node/Registry/Namespace.mjs").default;
|
|
2
3
|
type TeqFw_Di_Config_NamespaceRegistry = import("./src/Config/NamespaceRegistry.mjs").default;
|
|
4
|
+
type TeqFw_Di_Node_Registry_Namespace_Entry = Readonly<{prefix: string, dirAbs: string, ext: string}>;
|
|
5
|
+
type TeqFw_Di_Node_Registry_Package = import("./src/Node/Registry/Package.mjs").default;
|
|
6
|
+
type TeqFw_Di_Node_Registry_Package_Record = Readonly<{name: string, rootAbs: string, rootReal: string, packageJson: Readonly<Record<string, unknown>>, dependencies: ReadonlyArray<string>}>;
|
|
3
7
|
type TeqFw_Di_Container = import("./src/Container.mjs").default;
|
|
4
8
|
type TeqFw_Di_Container_Instantiate = import("./src/Container/Instantiate.mjs").default;
|
|
5
9
|
type TeqFw_Di_Container_Lifecycle = import("./src/Container/Lifecycle.mjs").default;
|
|
10
|
+
type TeqFw_Di_Container_Postprocess_Context = Readonly<{depId: TeqFw_Di_DepId__DTO}>;
|
|
6
11
|
type TeqFw_Di_Container_GraphResolver = import("./src/Container/GraphResolver.mjs").default;
|
|
7
12
|
type TeqFw_Di_Container_Executor = import("./src/Container/Executor.mjs").default;
|
|
8
13
|
type TeqFw_Di_Parser = import("./src/Parser.mjs").default;
|
package/ai/concepts.md
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# concepts.md
|
|
2
|
-
|
|
3
|
-
Version: 20260606
|
|
4
|
-
|
|
5
|
-
## Late Binding
|
|
6
|
-
|
|
7
|
-
Dependencies are resolved at runtime rather than through direct static imports between application modules. This keeps modules independent of concrete implementations and moves dependency binding into the container.
|
|
8
|
-
|
|
9
|
-
## Runtime Linker
|
|
10
|
-
|
|
11
|
-
The container acts as a runtime linker for ES modules. It interprets CDC identifiers, resolves modules, selects exports, and produces linked values for callers.
|
|
12
|
-
|
|
13
|
-
## Dependency Contracts
|
|
14
|
-
|
|
15
|
-
Dependencies are declared through CDC strings and module-level `__deps__` descriptors. Together they form the contract between module code and runtime composition.
|
|
16
|
-
|
|
17
|
-
The canonical `__deps__` form is hierarchical and keyed by export name.
|
|
18
|
-
|
|
19
|
-
## Namespace Mapping
|
|
20
|
-
|
|
21
|
-
Logical module identifiers are translated into module-specifier bases through namespace roots. This keeps dependency addressing independent from concrete filesystem paths or URL locations.
|
|
22
|
-
|
|
23
|
-
## Immutable Linked Values
|
|
24
|
-
|
|
25
|
-
Values returned by the container are frozen after linking. Consumers should treat them as stable resolved values rather than mutable construction targets.
|