@teqfw/di 0.31.0 → 0.32.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/RELEASE.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @teqfw/di releases
2
2
 
3
+ ## 0.32.0 - added support for the `node:` prefix
4
+
5
+ - **Added ability** to manually register objects in the container (`register`).
6
+ - **Optimized dependency parsing**, added support for the `node:` prefix.
7
+ - **Updated singleton handling**, fixed issues with `Defs.LS`.
8
+ - **Added protection against object modification**, freezing objects when possible (`Object.freeze`).
9
+ - **Updated dependencies** (Mocha, Rollup, Rollup plugins).
10
+
3
11
  ## 0.31.0
4
12
 
5
13
  * Added optional `stack` parameter to the `get` method for improved dependency tracking and debugging.
@@ -70,6 +78,6 @@
70
78
  ## 0.8.0
71
79
 
72
80
  * docs for plugin's teq-descriptor (see in `main` branch);
73
- * use object notation instead of array notation in namespace replacement statements of
74
- teq-descriptor (`@teqfw/di.replace` node format is changed in `./teqfw.json`);
81
+ * use object notation instead of array notation in namespace replacement statements of teq-descriptor (
82
+ `@teqfw/di.replace` node format is changed in `./teqfw.json`);
75
83
  * array is used as a container for upline dependencies in the 'SpecProxy' (object was);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teqfw/di",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
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",
@@ -31,10 +31,10 @@
31
31
  "test": "mocha --recursive './test/**/*.test.mjs'"
32
32
  },
33
33
  "devDependencies": {
34
- "@rollup/plugin-node-resolve": "^15.2.3",
35
- "mocha": "^10.7.0",
36
- "rollup": "^2.79.1",
37
- "rollup-plugin-terser": "^7.0.2"
34
+ "@rollup/plugin-node-resolve": "^16.0.1",
35
+ "mocha": "^11.1.0",
36
+ "rollup": "^4.36.0",
37
+ "@rollup/plugin-terser": "^0.4.4"
38
38
  },
39
39
  "mocha": {
40
40
  "spec": "./test/**/*.test.mjs",
package/rollup.config.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import resolve from '@rollup/plugin-node-resolve';
2
- import {terser} from 'rollup-plugin-terser';
2
+ import terser from '@rollup/plugin-terser';
3
3
 
4
4
  export default {
5
5
  input: 'src/Container.js',
@@ -34,6 +34,13 @@ export default class TeqFw_Di_Api_Container {
34
34
  */
35
35
  getResolver() {};
36
36
 
37
+ /**
38
+ * Registers an object (module, singleton, factory, or prototype) by dependency ID.
39
+ * @param {string} depId
40
+ * @param {Object} obj
41
+ */
42
+ register(depId, obj) {};
43
+
37
44
  /**
38
45
  * Enable disable debug output for the object composition process.
39
46
  * @param {boolean} data
@@ -29,17 +29,17 @@ export default class TeqFw_Di_Container_A_Composer {
29
29
  * @returns {Promise<*>}
30
30
  */
31
31
  this.create = async function (depId, module, stack, container) {
32
- if (stack.includes(depId.value))
33
- throw new Error(`Circular dependency for '${depId.value}'. Parents are: ${JSON.stringify(stack)}`);
32
+ if (stack.includes(depId.origin))
33
+ throw new Error(`Circular dependency for '${depId.origin}'. Parents are: ${JSON.stringify(stack)}`);
34
34
  if (depId.exportName) {
35
35
  // use export from the es6-module
36
- const stackNew = [...stack, depId.value];
36
+ const stackNew = [...stack, depId.origin];
37
37
  const {[depId.exportName]: exp} = module;
38
- if (depId.composition === Defs.COMP_F) {
38
+ if (depId.composition === Defs.CF) {
39
39
  if (typeof exp === 'function') {
40
40
  // create deps for factory function
41
41
  const deps = specParser(exp);
42
- if (deps.length) log(`Deps for object '${depId.value}' are: ${JSON.stringify(deps)}`);
42
+ if (deps.length) log(`Deps for object '${depId.origin}' are: ${JSON.stringify(deps)}`);
43
43
  const spec = {};
44
44
  for (const dep of deps)
45
45
  spec[dep] = await container.compose(dep, stackNew);
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Default parser for object keys in format:
3
3
  * - Ns_Module.export$$(post)
4
+ * - node:package.export$$(post)
5
+ * - node:@scope/package.export$$(post)
4
6
  *
5
7
  * @namespace TeqFw_Di_Container_A_Parser_Chunk_Def
6
8
  */
@@ -8,8 +10,8 @@ import Dto from '../../../../DepId.js';
8
10
  import Defs from '../../../../Defs.js';
9
11
 
10
12
  // VARS
11
- /** @type {RegExp} expression for default object key (Ns_Module.export$$(post)) */
12
- const REGEXP = /^((([A-Z])[A-Za-z0-9_]*)((\.)?([A-Za-z0-9_]*)((\$)?(\$)?)?)?(\(([A-Za-z0-9_,]*)\))?)$/;
13
+ /** @type {RegExp} expression for default object key */
14
+ const REGEXP = /^(node:)?((@?[A-Za-z0-9_\-]+\/?[A-Za-z0-9_\-]*))((\.)?([A-Za-z0-9_]*)((\$)?(\$)?)?)?(\(([A-Za-z0-9_,]*)\))?$/;
13
15
 
14
16
  /**
15
17
  * @implements TeqFw_Di_Api_Container_Parser_Chunk
@@ -17,49 +19,51 @@ const REGEXP = /^((([A-Z])[A-Za-z0-9_]*)((\.)?([A-Za-z0-9_]*)((\$)?(\$)?)?)?(\((
17
19
  export default class TeqFw_Di_Container_A_Parser_Chunk_Def {
18
20
 
19
21
  canParse(depId) {
20
- // default parser always trys to parse the depId
22
+ // default parser always tries to parse the depId
21
23
  return true;
22
24
  }
23
25
 
24
26
  parse(objectKey) {
25
27
  const res = new Dto();
26
- res.value = objectKey;
28
+ res.origin = objectKey;
27
29
  const parts = REGEXP.exec(objectKey);
28
30
  if (parts) {
29
- res.moduleName = parts[2];
31
+ res.isNodeModule = Boolean(parts[1]); // Detect 'node:' prefix
32
+ res.moduleName = parts[2].replace(/^node:/, ''); // Remove 'node:' prefix
33
+
30
34
  if (parts[5] === '.') {
31
- // Ns_Module.export...
35
+ // Ns_Module.export or node:package.export
32
36
  if ((parts[7] === '$') || (parts[7] === '$$')) {
33
- // Ns_Module.export$...
34
- res.composition = Defs.COMP_F;
37
+ res.composition = Defs.CF;
35
38
  res.exportName = parts[6];
36
- res.life = (parts[7] === '$') ? Defs.LIFE_S : Defs.LIFE_I;
39
+ res.life = (parts[7] === '$') ? Defs.LS : Defs.LI;
37
40
  } else {
38
- res.composition = Defs.COMP_A;
39
- res.life = Defs.LIFE_S;
40
- // res.exportName = (parts[6]) ? parts[6] : 'default';
41
+ res.composition = Defs.CA;
42
+ res.life = Defs.LS;
41
43
  res.exportName = (parts[6] !== '') ? parts[6] : 'default';
42
44
  }
43
45
  } else if ((parts[7] === '$') || parts[7] === '$$') {
44
- // Ns_Module$$
45
- res.composition = Defs.COMP_F;
46
+ // Ns_Module$$ or node:package$$
47
+ res.composition = Defs.CF;
46
48
  res.exportName = 'default';
47
- res.life = (parts[7] === '$') ? Defs.LIFE_S : Defs.LIFE_I;
49
+ res.life = (parts[7] === '$') ? Defs.LS : Defs.LI;
48
50
  } else {
49
- // Ns_Module (es6 module)
51
+ // Ns_Module or node:package (ES6 module)
50
52
  res.composition = undefined;
51
53
  res.exportName = undefined;
52
54
  res.life = undefined;
53
55
  }
54
- // wrappers
56
+
57
+ // Wrappers handling
55
58
  if (parts[11]) {
56
59
  res.wrappers = parts[11].split(',');
57
60
  }
58
61
  }
59
62
 
60
- // we should always use singletons for as-is exports
61
- if ((res.composition === Defs.COMP_A) && (res.life === Defs.LIFE_I))
62
- throw new Error(`Export is not a function and should be used as a singleton only: '${res.value}'.`);
63
+ // Ensure singletons for non-factory exports
64
+ if ((res.composition === Defs.CA) && (res.life === Defs.LI))
65
+ throw new Error(`Export is not a function and should be used as a singleton only: '${res.origin}'.`);
66
+
63
67
  return res;
64
68
  }
65
- }
69
+ }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Default parser for object keys in format:
3
3
  * - Ns_Module[.|#]export$[F|A][S|I]
4
+ * - node:package[.|#]export$[F|A][S|I]
4
5
  *
5
6
  * @namespace TeqFw_Di_Container_A_Parser_Chunk_V02X
6
7
  */
@@ -8,8 +9,8 @@ import Dto from '../../../../DepId.js';
8
9
  import Defs from '../../../../Defs.js';
9
10
 
10
11
  // VARS
11
- /** @type {RegExp} expression for default object key (Ns_Module[.|#]export$[F|A][S|I]) */
12
- const REGEXP = /^((([A-Z])[A-Za-z0-9_]*)((#|\.)?([A-Za-z0-9_]*)((\$)([F|A])?([S|I])?)?)?)$/;
12
+ /** @type {RegExp} expression for default object key */
13
+ const REGEXP = /^(node:)?((([A-Z])[A-Za-z0-9_]*|[a-z][a-z0-9\-]*))((#|\.)?([A-Za-z0-9_]*)((\$)([F|A])?([S|I])?)?)?$/;
13
14
 
14
15
  /**
15
16
  * @implements TeqFw_Di_Api_Container_Parser_Chunk
@@ -17,54 +18,48 @@ const REGEXP = /^((([A-Z])[A-Za-z0-9_]*)((#|\.)?([A-Za-z0-9_]*)((\$)([F|A])?([S|
17
18
  export default class TeqFw_Di_Container_A_Parser_Chunk_V02X {
18
19
 
19
20
  canParse(depId) {
20
- // default parser always trys to parse the depId
21
+ // default parser always tries to parse the depId
21
22
  return true;
22
23
  }
23
24
 
24
25
  parse(objectKey) {
25
26
  const res = new Dto();
26
- res.value = objectKey;
27
+ res.origin = objectKey;
27
28
  const parts = REGEXP.exec(objectKey);
28
29
  if (parts) {
29
- res.moduleName = parts[2];
30
- if (parts[5] === '.') {
31
- // App_Service.export...
32
- if (parts[8] === '$') {
33
- // App_Service.export$...
34
- res.composition = Defs.COMP_F;
35
- res.exportName = parts[6];
36
- res.life = (parts[10] === Defs.LIFE_I)
37
- ? Defs.LIFE_I : Defs.LIFE_S;
38
- } else {
39
- res.composition = ((parts[8] === undefined) || (parts[8] === Defs.COMP_A))
40
- ? Defs.COMP_A : Defs.COMP_F;
41
- res.exportName = parts[6];
42
- res.life = ((parts[8] === undefined) || (parts[10] === Defs.LIFE_S))
43
- ? Defs.LIFE_S : Defs.LIFE_I;
44
- }
30
+ res.isNodeModule = Boolean(parts[1]); // Check if it starts with 'node:'
31
+ res.moduleName = parts[2].replace(/^node:/, ''); // Remove 'node:' if present
45
32
 
46
-
47
- } else if (parts[8] === '$') {
48
- // App_Logger$FS
49
- res.composition = ((parts[9] === undefined) || (parts[9] === Defs.COMP_F))
50
- ? Defs.COMP_F : Defs.COMP_A;
51
- res.exportName = 'default';
52
- if (parts[10]) {
53
- res.life = (parts[10] === Defs.LIFE_S) ? Defs.LIFE_S : Defs.LIFE_I;
33
+ if (parts[6] === '.') {
34
+ // App_Service.export or node:package.export
35
+ if (parts[9] === '$') {
36
+ // App_Service.export$ or node:package.export$
37
+ res.composition = Defs.CF;
38
+ res.exportName = parts[7];
39
+ res.life = (parts[11] === Defs.LI) ? Defs.LI : Defs.LS;
54
40
  } else {
55
- res.life = (res.composition === Defs.COMP_F) ? Defs.LIFE_S : Defs.LIFE_I;
41
+ res.composition = (!parts[9] || parts[9] === Defs.CA) ? Defs.CA : Defs.CF;
42
+ res.exportName = parts[7];
43
+ res.life = (!parts[9] || parts[11] === Defs.LS) ? Defs.LS : Defs.LI;
56
44
  }
45
+ } else if (parts[9] === '$') {
46
+ // App_Logger$FS or node:package$
47
+ res.composition = (!parts[10] || parts[10] === Defs.CF) ? Defs.CF : Defs.CA;
48
+ res.exportName = 'default';
49
+ res.life = parts[11] ? (parts[11] === Defs.LS ? Defs.LS : Defs.LI) : (res.composition === Defs.CF ? Defs.LS : Defs.LI);
57
50
  } else {
58
- // App_Service (es6 module)
51
+ // App_Service or node:package (ES6 module)
59
52
  res.composition = undefined;
60
53
  res.exportName = undefined;
61
54
  res.life = undefined;
62
55
  }
63
56
  }
64
57
 
65
- // we should always use singletons for as-is exports
66
- if ((res.composition === Defs.COMP_A) && (res.life === Defs.LIFE_I))
67
- throw new Error(`Export is not a function and should be used as a singleton only: '${res.value}'.`);
58
+ // Enforce singleton for non-factory exports
59
+ if (res.composition === Defs.CA && res.life === Defs.LI) {
60
+ throw new Error(`Export is not a function and should be used as a singleton only: '${res.origin}'.`);
61
+ }
62
+
68
63
  return res;
69
64
  }
70
- }
65
+ }
package/src/Container.js CHANGED
@@ -19,6 +19,21 @@ function getSingletonId(key) {
19
19
  return `${key.moduleName}#${key.exportName}`;
20
20
  }
21
21
 
22
+ /**
23
+ * Determines if an object, function, or primitive can be safely frozen.
24
+ * @param {*} value - The value to check.
25
+ * @returns {boolean} - Returns true if the value can be safely frozen.
26
+ */
27
+ function canBeFrozen(value) {
28
+ // Primitives (except objects and functions) cannot be frozen
29
+ if (value === null || typeof value !== 'object' && typeof value !== 'function') return false;
30
+ // // ES modules cannot be frozen
31
+ if (Object.prototype.toString.call(value) === '[object Module]') return false;
32
+ // check is Object is already frozen
33
+ return !Object.isFrozen(value);
34
+ }
35
+
36
+
22
37
  // MAIN
23
38
  /**
24
39
  * @implements TeqFw_Di_Api_Container
@@ -80,7 +95,7 @@ export default class TeqFw_Di_Container {
80
95
  // modify original key according to some rules (replacements, etc.)
81
96
  const key = _preProcessor.modify(parsed, stack);
82
97
  // return existing singleton
83
- if (key.life === Defs.LIFE_S) {
98
+ if (key.life === Defs.LS) {
84
99
  const singleId = getSingletonId(key);
85
100
  if (_regSingles[singleId]) {
86
101
  log(`Existing singleton '${singleId}' is returned.`);
@@ -111,11 +126,13 @@ export default class TeqFw_Di_Container {
111
126
  }
112
127
  // create object using the composer then modify it in post-processor
113
128
  let res = await _composer.create(key, module, stack, this);
129
+ // freeze the result to prevent modifications (TODO: should we have configuration for the feature?)
130
+ if (canBeFrozen(res)) Object.freeze(res);
114
131
  res = await _postProcessor.modify(res, key, stack);
115
132
  log(`Object '${depId}' is created.`);
116
133
 
117
134
  // save singletons
118
- if (key.life === Defs.LIFE_S) {
135
+ if (key.life === Defs.LS) {
119
136
  const singleId = getSingletonId(key);
120
137
  _regSingles[singleId] = res;
121
138
  log(`Object '${depId}' is saved as singleton.`);
@@ -128,8 +145,27 @@ export default class TeqFw_Di_Container {
128
145
  this.getPreProcessor = () => _preProcessor;
129
146
 
130
147
  this.getPostProcessor = () => _postProcessor;
148
+
131
149
  this.getResolver = () => _resolver;
132
150
 
151
+ /**
152
+ * Register new object in the Container.
153
+ * @param {string} depId
154
+ * @param {Object} obj
155
+ */
156
+ this.register = function (depId, obj) {
157
+ if (!depId || !obj) throw new Error('depId and object are required');
158
+ const key = _parser.parse(depId);
159
+ if (key.life === Defs.LS) {
160
+ const singleId = getSingletonId(key);
161
+ _regSingles[singleId] = obj;
162
+ log(`Object '${depId}' is registered manually as singleton.`);
163
+ } else {
164
+ // TODO: factory function also should be added manually
165
+ throw new Error(`Only singletons can be registered manually. Given: ${depId}`);
166
+ }
167
+ };
168
+
133
169
  this.setDebug = function (data) {
134
170
  _debug = data;
135
171
  _composer.setDebug(data);
package/src/Defs.js CHANGED
@@ -3,12 +3,13 @@
3
3
  * @namespace TeqFw_Di_Defs
4
4
  */
5
5
  export default {
6
- COMP_A: 'A', // composition: as-is
7
- COMP_F: 'F', // composition: factory
6
+ CA: 'A', // composition: as-is
7
+ CF: 'F', // composition: factory
8
+ // TODO: we don't need an access to the container itself.
8
9
  ID: 'container', // default ID for container itself
9
10
  ID_FQN: 'TeqFw_Di_Container$', // default Full Qualified Name for container itself
10
- LIFE_I: 'I', // lifestyle: instance
11
- LIFE_S: 'S', // lifestyle: singleton
11
+ LI: 'I', // lifestyle: instance
12
+ LS: 'S', // lifestyle: singleton
12
13
 
13
14
  /**
14
15
  * Return 'true' if function is a class definition.
package/src/DepId.js CHANGED
@@ -1,33 +1,44 @@
1
1
  /**
2
- * This is a DTO that represents the structure of an ID for a runtime dependency.
2
+ * DTO representing a dependency identifier in the container.
3
3
  * @namespace TeqFw_Di_DepId
4
4
  */
5
5
  export default class TeqFw_Di_DepId {
6
6
  /**
7
- * The name of an export of the module.
7
+ * The name of an export from the module.
8
+ * Example: 'default', 'logger', 'DbClient'.
8
9
  * @type {string}
9
10
  */
10
11
  exportName;
11
12
  /**
12
- * Composition type (see Defs.COMPOSE_): use the export as Factory (F) or return as-is (A).
13
+ * Defines how the export should be used:
14
+ * - 'F' (Factory): The export is a factory function, call it to get an instance.
15
+ * - 'A' (As-Is): The export is returned as-is, without calling.
16
+ * Example: 'F', 'A'.
13
17
  * @type {string}
14
18
  */
15
19
  composition;
16
20
  /**
17
- * Lifestyle type (see Defs.LIFE_): singleton (S) or instance (I).
21
+ * Defines the lifecycle of the resolved dependency:
22
+ * - 'S' (Singleton): A single instance is created and reused.
23
+ * - 'I' (Instance): A new instance is created on each request.
24
+ * Example: 'S', 'I'.
18
25
  * @type {string}
19
26
  */
20
27
  life;
21
28
  /**
22
- * The code for ES6 module that can be converted to the path to this es6 module.
29
+ * ES6 module identifier, which can be transformed into a file path.
30
+ * This value is processed by the Resolver to determine the module's location.
31
+ * Example: 'TeqFw_Core_Shared_Api_Logger'.
23
32
  * @type {string}
24
33
  */
25
34
  moduleName;
26
35
  /**
27
- * Object key value.
36
+ * The original identifier string provided to the container.
37
+ * This is the unprocessed dependency key.
38
+ * Example: 'TeqFw_Core_Shared_Api_Logger$$'.
28
39
  * @type {string}
29
40
  */
30
- value;
41
+ origin;
31
42
  /**
32
43
  * List of wrappers to decorate the result.
33
44
  * @type {string[]}