qtools-library-dot-d 1.0.13 → 1.1.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/README.md +88 -0
- package/package.json +8 -2
- package/qtools-library-dot-d.js +553 -2
package/README.md
CHANGED
|
@@ -227,8 +227,96 @@ The dotD pattern works particularly well in:
|
|
|
227
227
|
|
|
228
228
|
This pattern is used extensively in the qtools ecosystem for building scalable, maintainable Node.js applications.
|
|
229
229
|
|
|
230
|
+
## Loading from several directories
|
|
231
|
+
|
|
232
|
+
*New in 1.1.0. Opt-in: nothing changes for a library that uses `setLibraryPath`.*
|
|
233
|
+
|
|
234
|
+
A library can load from an ordered list of roots, so that one installation (an *instance*) can add its own modules beside the shared *framework* ones, and replace a framework module only by saying so.
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
const screenersDotD = qtLibDotD({ libraryName: 'corpusScreeners' });
|
|
238
|
+
|
|
239
|
+
screenersDotD.setInterface({ // optional; checks every element
|
|
240
|
+
describeFunctionName: 'describeCorpusScreener',
|
|
241
|
+
requiredFunctionList: ['screenCorpus'],
|
|
242
|
+
nameFieldName: 'corpusScreenerName',
|
|
243
|
+
versionFieldName: 'corpusScreenerVersion',
|
|
244
|
+
contractVersion: 1,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
screenersDotD.setForbiddenRequireCheck({ // optional; static scan of instance source
|
|
248
|
+
provenanceList: ['instance'],
|
|
249
|
+
allowedModuleList: require('module').builtinModules,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
screenersDotD.setLibraryPathList([
|
|
253
|
+
{ dirPath: instanceScreenersPath, provenance: 'instance', mayBeAbsent: true },
|
|
254
|
+
{ dirPath: frameworkScreenersPath, provenance: 'framework' },
|
|
255
|
+
]);
|
|
256
|
+
|
|
257
|
+
screenersDotD.loadModules({ passThroughParameters }, (err) => {
|
|
258
|
+
// err names every refusal, one per line; '' when all loaded
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### The rules
|
|
263
|
+
|
|
264
|
+
- **Entries.** `{ dirPath, provenance: 'instance' | 'framework', mayBeAbsent }`. A plain string is a framework root that must exist. `mayBeAbsent` is optional and false when omitted.
|
|
265
|
+
- **Order.** Framework roots load first, then instance roots, whatever order the list gives them in.
|
|
266
|
+
- **Missing roots.** Refused as `PLUGIN_ROOT_NOT_FOUND` unless `mayBeAbsent` is true, in which case the log says `ROOT_ABSENT_ALLOWED`.
|
|
267
|
+
- **What is a module.** A `.js`/`.cjs`/`.mjs` file or a directory (`require()` must resolve it: `package.json` `main` or `index.js`). Dotfiles and other files are skipped and logged as `SKIPPED_NON_MODULE`; in single-path mode they would be "ignoring invalid file" lines.
|
|
268
|
+
- **One name, one element.** A name added a second time is refused as `PLUGIN_NAME_COLLISION`, naming both files, unless an instance element replaces a framework element **and declares it**:
|
|
269
|
+
|
|
270
|
+
```javascript
|
|
271
|
+
overridesFramework: { replacesVersion: '1', reason: 'why this instance needs its own' }
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The declaration lives in the element's describe output when an interface is set, otherwise on the element itself (an object or function property). It needs both fields, or it is refused as `PLUGIN_OVERRIDE_DECLARATION_INVALID`. Two framework elements, or two instance elements, of one name always collide.
|
|
275
|
+
- **Stale overrides warn.** When `replacesVersion` is not strictly equal to the framework element's declared version (`versionFieldName`), the override is accepted and the log says `OVERRIDE_TARGET_CHANGED`. Without an interface there is no framework version to compare, and the log says `OVERRIDE_TARGET_UNVERSIONED`. A declaration with nothing to replace logs `OVERRIDE_TARGET_ABSENT`.
|
|
276
|
+
- **Interface check.** With `setInterface`, all five fields are required (`INTERFACE_SPEC_INCOMPLETE`). Each element must carry the describe function and every required function (`PLUGIN_INTERFACE_INCOMPLETE`), describe itself by the name it was added under (`PLUGIN_NAME_MISMATCH`), and declare the library's `contractVersion` (`PLUGIN_CONTRACT_MISMATCH`).
|
|
277
|
+
- **Load errors are refusals.** A module whose `require()` throws is refused as `PLUGIN_LOAD_FAILED`, naming the file. `loadModules({ passThroughParameters, tolerateLoadErrors: true })` lists it in the exception list instead, still as `PLUGIN_LOAD_FAILED`.
|
|
278
|
+
- **Forbidden requires.** With `setForbiddenRequireCheck`, the source of every module from a listed provenance is scanned before it runs. A refused module is never executed (`PLUGIN_FORBIDDEN_REQUIRE`).
|
|
279
|
+
- **What is allowed:**
|
|
280
|
+
- names in `allowedModuleList` (a `node:` prefix is ignored);
|
|
281
|
+
- relative or absolute paths whose resolved file, judged by its real path after symlinks, stays inside the plugin's own directory. For a single-file module, that is its root.
|
|
282
|
+
- **What is refused:**
|
|
283
|
+
- a path that resolves to a non-module file, such as `require('./payload.txt')`, which CommonJS would run as JavaScript;
|
|
284
|
+
- a path that does not resolve at all;
|
|
285
|
+
- a module whose own real path, or `package.json` `main`, lies outside its directory.
|
|
286
|
+
- **What the scan cannot see.** The scan reads every `.js`/`.cjs`/`.mjs` file in a directory module, recursively (this needs Node 20 or later). It is a regular expression over string-literal `require('…')` and static template calls. It does **not** catch:
|
|
287
|
+
- a require whose name is built at run time: a variable, string concatenation, or a `${}` template;
|
|
288
|
+
- `import()`;
|
|
289
|
+
- `module.createRequire` (`module` is a built-in, so allowed).
|
|
290
|
+
|
|
291
|
+
It also refuses a `require('…')` that appears only in a comment. That is a false refusal, not a missed one.
|
|
292
|
+
- **Why the check exists.** It serves portability (spec R6), not security: it is not a sandbox.
|
|
293
|
+
- **Reserved names.** An element may not take a library API name such as `toString` or `getProvenance`, nor a name `Object.prototype` carries (`constructor`, `hasOwnProperty`, `__proto__`, and the `qt…` methods qtools-functional-library installs there). Elements are properties of the library object itself, so such a name would shadow a method the library relies on: `qtSelectProperties` would break `seal()`. Refused as `PLUGIN_NAME_RESERVED`.
|
|
294
|
+
- **Adding late.** `library.add()` after loading has finished throws `PLUGIN_ADD_OUTSIDE_LOAD`: there is no file to attribute it to and no callback left to report it. A module must register synchronously, while it is being required. A registration from a timer or a promise throws there, uncaught, and stops the process.
|
|
295
|
+
- **Load once.** A second `loadModules` on a path-list library is refused (`LOAD_MODULES_REPEATED`); single-path libraries still simply re-add.
|
|
296
|
+
- **Directory modules need an entry point.** `require(<dir>)` resolves `package.json` `main` or `index.js`. A layout like `screeners.d/<name>/<name>.js` without one of them is `PLUGIN_LOAD_FAILED`.
|
|
297
|
+
- **A loadModules error is fatal.** Refusals are collected, and `loadModules` calls back with all of them. Elements that passed stay loaded, so after an error the library is partly populated. Treat the error as fatal and do not use the library.
|
|
298
|
+
- **Silent modules.** A module that loads but adds nothing is logged as `MODULE_ADDED_NOTHING`.
|
|
299
|
+
- **Not mixable.** One library uses either `setLibraryPath` or `setLibraryPathList` (`LIBRARY_PATH_MODE_CONFLICT`). `setInterface` and `setForbiddenRequireCheck` need a path list (`REQUIRES_PATH_LIST`), and so does the default add: `setCustomAddFunction` with a path list is refused (`CUSTOM_ADD_REQUIRES_SINGLE_PATH`).
|
|
300
|
+
- **Without a callback,** `setLibraryPathList` and `loadModules` throw the refusal string, as `setLibraryPath` throws a bad path.
|
|
301
|
+
- **Sealing.** `seal()` removes the setters, `library` and `logList`, as it always has. For a path-list library it also removes `setLibraryPathList`, `setInterface` and `setForbiddenRequireCheck`. The getters remain.
|
|
302
|
+
|
|
303
|
+
### Provenance
|
|
304
|
+
|
|
305
|
+
Every element records `{ provenance, filePath, overrides? }`:
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
screenersDotD.getProvenance('transcriptDetector');
|
|
309
|
+
// { provenance: 'instance', filePath: '…/corpusScreeners.d/transcriptDetector',
|
|
310
|
+
// overrides: { provenance: 'framework', filePath: '…', version: '1', reason: '…' } }
|
|
311
|
+
screenersDotD.getProvenanceList(); // [{ elementName, provenance, filePath, overrides? }, …]
|
|
312
|
+
screenersDotD.getRefusalList(); // [{ refusalCode, elementName, filePath, message }, …]
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`toString()` adds the same on each element line, e.g. `transcriptDetector: object [instance: …; overrides framework v1 (…) because …]`. `getProvenance` throws `PROVENANCE_NOT_RECORDED` for a single-path library and `PROVENANCE_UNKNOWN_NAME` for a name it does not have. Warnings (`OVERRIDE_TARGET_…`, `ROOT_ABSENT_ALLOWED`, `SKIPPED_NON_MODULE`, `MODULE_ADDED_NOTHING`) are in `qtdProcessLog()`.
|
|
316
|
+
|
|
230
317
|
## HISTORY
|
|
231
318
|
|
|
319
|
+
v1.1.0 - setLibraryPathList: framework and instance roots, declared overrides, opt-in interface and forbidden-require checks, provenance, named refusals; Object.prototype names reserved in path-list mode. setLibraryPath unchanged. Tests added (npm test); test/ not shipped in the package.
|
|
232
320
|
v1.13 - License changed to MIT (was ISC). No code changes.
|
|
233
321
|
v1.12 - clarified error message "Ignoring invalid file".
|
|
234
322
|
v1.10 - there was a reason I did not return the exception list. Undone.
|
package/package.json
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qtools-library-dot-d",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Scan a directory for JS modules that can execute and addMe() that attaches it to some form of name/value list, eg, a Map() with functions as values or an xPressJS route/endpoint list.",
|
|
5
5
|
"main": "qtools-library-dot-d.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"qtools-library-dot-d.js",
|
|
8
|
+
"README.md",
|
|
9
|
+
"README2.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
6
12
|
"scripts": {
|
|
7
|
-
"test": "
|
|
13
|
+
"test": "node --test test/*.test.js"
|
|
8
14
|
},
|
|
9
15
|
"author": "TQ White II",
|
|
10
16
|
"license": "MIT",
|
package/qtools-library-dot-d.js
CHANGED
|
@@ -12,6 +12,11 @@ const fs = require('fs');
|
|
|
12
12
|
const qt = require('qtools-functional-library');
|
|
13
13
|
|
|
14
14
|
//START OF moduleFunction() ============================================================
|
|
15
|
+
// Two ways to load. setLibraryPath(dirPath) is the original single directory, and behaves exactly as in 1.0.13,
|
|
16
|
+
// including swallowing a module's load error into the exception list. setLibraryPathList(pathList) loads framework
|
|
17
|
+
// roots, then instance roots, and refuses by name what single-path mode would let pass silently: name collisions,
|
|
18
|
+
// undeclared overrides, interface failures, load errors and (opt-in) forbidden requires. See README, "Loading from
|
|
19
|
+
// several directories".
|
|
15
20
|
const moduleFunction =
|
|
16
21
|
({ moduleName } = {}) =>
|
|
17
22
|
({ libraryName = 'libraryDotD' }) => {
|
|
@@ -22,14 +27,19 @@ const moduleFunction =
|
|
|
22
27
|
// SET LIBRARY PATH
|
|
23
28
|
|
|
24
29
|
let libraryPath;
|
|
30
|
+
let libraryPathMode = ''; // 'singlePath' (setLibraryPath) or 'pathList' (setLibraryPathList); never both
|
|
25
31
|
|
|
26
32
|
const setLibraryPath = (dirPath, callback) => {
|
|
33
|
+
if (libraryPathMode == 'pathList') {
|
|
34
|
+
throw `LIBRARY_PATH_MODE_CONFLICT: ${libraryName} already has a path list; setLibraryPath(${dirPath}) refused`;
|
|
35
|
+
}
|
|
27
36
|
try {
|
|
28
37
|
libraryPath = fs.realpathSync(dirPath);
|
|
29
38
|
} catch (err) {
|
|
30
39
|
throw err.toString();
|
|
31
40
|
return;
|
|
32
41
|
}
|
|
42
|
+
libraryPathMode = 'singlePath';
|
|
33
43
|
|
|
34
44
|
if (typeof callback == 'function') {
|
|
35
45
|
callback('', libraryPath);
|
|
@@ -38,6 +48,149 @@ const moduleFunction =
|
|
|
38
48
|
}
|
|
39
49
|
};
|
|
40
50
|
|
|
51
|
+
// --------------------------------------------------------------------------------
|
|
52
|
+
// SET LIBRARY PATH LIST (instance and framework roots)
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* One root in a path list. A plain string means { dirPath, provenance: 'framework', mayBeAbsent: false }.
|
|
56
|
+
* @typedef {Object} PathListEntry
|
|
57
|
+
* @property {string} dirPath
|
|
58
|
+
* @property {'instance'|'framework'} provenance - framework roots load first, then instance roots
|
|
59
|
+
* @property {boolean} [mayBeAbsent=false] - a missing root is refused (PLUGIN_ROOT_NOT_FOUND) unless true
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The contract every element of an interface-checked library must meet (setInterface).
|
|
64
|
+
* @interface InterfaceSpec
|
|
65
|
+
* @property {string} describeFunctionName - element[describeFunctionName]() returns its description object
|
|
66
|
+
* @property {string[]} requiredFunctionList - functions every element must carry
|
|
67
|
+
* @property {string} nameFieldName - description field that must equal the name passed to library.add
|
|
68
|
+
* @property {string} versionFieldName - description field holding the element's own version
|
|
69
|
+
* @property {*} contractVersion - description.contractVersion must equal this (strict ===)
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Where an element came from (getProvenance). overrides is present only for a declared instance override.
|
|
74
|
+
* @typedef {Object} ProvenanceRecord
|
|
75
|
+
* @property {'instance'|'framework'} provenance
|
|
76
|
+
* @property {string} filePath - the module (file or directory) whose require() added the element
|
|
77
|
+
* @property {{provenance: string, filePath: string, version: *, reason: string}} [overrides]
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
const provenanceNameList = ['instance', 'framework'];
|
|
81
|
+
let libraryPathList = [];
|
|
82
|
+
|
|
83
|
+
// Refusals in the setters are thrown when no callback is given, matching setLibraryPath.
|
|
84
|
+
const deliverResult = (errString, result, callback) => {
|
|
85
|
+
if (typeof callback == 'function') {
|
|
86
|
+
callback(errString, result);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (errString) {
|
|
90
|
+
throw errString;
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const normalizePathListEntry = (pathListEntry, entryIndex) => {
|
|
96
|
+
if (typeof pathListEntry == 'string') {
|
|
97
|
+
return { dirPath: pathListEntry, provenance: 'framework', mayBeAbsent: false };
|
|
98
|
+
}
|
|
99
|
+
const { dirPath, provenance, mayBeAbsent = false } = pathListEntry || {};
|
|
100
|
+
const problemList = [
|
|
101
|
+
typeof dirPath == 'string' && dirPath ? '' : 'dirPath must be a non-empty string',
|
|
102
|
+
provenanceNameList.includes(provenance) ? '' : `provenance must be one of ${provenanceNameList.join(', ')} (got ${JSON.stringify(provenance)})`,
|
|
103
|
+
typeof mayBeAbsent == 'boolean' ? '' : `mayBeAbsent must be boolean (got ${JSON.stringify(mayBeAbsent)})`,
|
|
104
|
+
].filter((problem) => problem);
|
|
105
|
+
if (problemList.length) {
|
|
106
|
+
return { entryProblem: `PATH_LIST_ENTRY_INVALID: ${libraryName} entry ${entryIndex}: ${problemList.join('; ')}` };
|
|
107
|
+
}
|
|
108
|
+
return { dirPath, provenance, mayBeAbsent };
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const setLibraryPathList = (pathList, callback) => {
|
|
112
|
+
if (libraryPathMode == 'singlePath') {
|
|
113
|
+
return deliverResult(`LIBRARY_PATH_MODE_CONFLICT: ${libraryName} already has a single library path; setLibraryPathList refused`, undefined, callback);
|
|
114
|
+
}
|
|
115
|
+
if (!Array.isArray(pathList) || !pathList.length) {
|
|
116
|
+
return deliverResult(`PATH_LIST_INVALID: ${libraryName} setLibraryPathList needs a non-empty array (got ${JSON.stringify(pathList)})`, undefined, callback);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const normalizedEntryList = pathList.map(normalizePathListEntry);
|
|
120
|
+
const problemList = normalizedEntryList
|
|
121
|
+
.filter(({ entryProblem }) => entryProblem)
|
|
122
|
+
.map(({ entryProblem }) => entryProblem);
|
|
123
|
+
|
|
124
|
+
const resolvedEntryList = normalizedEntryList
|
|
125
|
+
.filter(({ entryProblem }) => !entryProblem)
|
|
126
|
+
.map((pathListEntry) => {
|
|
127
|
+
const { dirPath, provenance, mayBeAbsent } = pathListEntry;
|
|
128
|
+
const rootExists = fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory();
|
|
129
|
+
if (rootExists) {
|
|
130
|
+
return { dirPath: fs.realpathSync(dirPath), provenance, mayBeAbsent, rootIsAbsent: false };
|
|
131
|
+
}
|
|
132
|
+
if (mayBeAbsent) {
|
|
133
|
+
logList.push(`ROOT_ABSENT_ALLOWED: ${provenance} root ${dirPath} is not an existing directory; nothing loaded from it`);
|
|
134
|
+
return { dirPath, provenance, mayBeAbsent, rootIsAbsent: true };
|
|
135
|
+
}
|
|
136
|
+
problemList.push(`PLUGIN_ROOT_NOT_FOUND: ${libraryName} ${provenance} root ${dirPath} is not an existing directory`);
|
|
137
|
+
return { dirPath, provenance, mayBeAbsent, rootIsAbsent: true };
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (problemList.length) {
|
|
141
|
+
return deliverResult(problemList.join('\n'), undefined, callback);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
libraryPathList = resolvedEntryList;
|
|
145
|
+
libraryPathMode = 'pathList';
|
|
146
|
+
return deliverResult('', libraryPathList.map((pathListEntry) => ({ ...pathListEntry })), callback);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// --------------------------------------------------------------------------------
|
|
150
|
+
// SET INTERFACE (opt-in check of every added item, path-list mode only)
|
|
151
|
+
|
|
152
|
+
let interfaceSpec;
|
|
153
|
+
|
|
154
|
+
const setInterface = (newInterfaceSpec) => {
|
|
155
|
+
const { describeFunctionName, requiredFunctionList, nameFieldName, versionFieldName, contractVersion } =
|
|
156
|
+
newInterfaceSpec || {};
|
|
157
|
+
const isNameString = (candidate) => typeof candidate == 'string' && candidate;
|
|
158
|
+
const problemList = [
|
|
159
|
+
isNameString(describeFunctionName) ? '' : 'describeFunctionName',
|
|
160
|
+
Array.isArray(requiredFunctionList) && requiredFunctionList.every(isNameString) ? '' : 'requiredFunctionList (array of function names)',
|
|
161
|
+
isNameString(nameFieldName) ? '' : 'nameFieldName',
|
|
162
|
+
isNameString(versionFieldName) ? '' : 'versionFieldName',
|
|
163
|
+
contractVersion !== undefined && contractVersion !== null ? '' : 'contractVersion',
|
|
164
|
+
].filter((problem) => problem);
|
|
165
|
+
if (problemList.length) {
|
|
166
|
+
throw `INTERFACE_SPEC_INCOMPLETE: ${libraryName} setInterface is missing or has invalid: ${problemList.join(', ')}`;
|
|
167
|
+
}
|
|
168
|
+
interfaceSpec = { describeFunctionName, requiredFunctionList: [...requiredFunctionList], nameFieldName, versionFieldName, contractVersion };
|
|
169
|
+
return { ...interfaceSpec };
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// --------------------------------------------------------------------------------
|
|
173
|
+
// SET FORBIDDEN REQUIRE CHECK (opt-in static scan, path-list mode only)
|
|
174
|
+
|
|
175
|
+
let forbiddenRequireSpec;
|
|
176
|
+
|
|
177
|
+
const setForbiddenRequireCheck = (newForbiddenRequireSpec) => {
|
|
178
|
+
const { provenanceList, allowedModuleList } = newForbiddenRequireSpec || {};
|
|
179
|
+
const problemList = [
|
|
180
|
+
Array.isArray(provenanceList) && provenanceList.length && provenanceList.every((provenance) => provenanceNameList.includes(provenance))
|
|
181
|
+
? ''
|
|
182
|
+
: `provenanceList must be a non-empty array of ${provenanceNameList.join(', ')}`,
|
|
183
|
+
Array.isArray(allowedModuleList) && allowedModuleList.every((allowedModuleName) => typeof allowedModuleName == 'string')
|
|
184
|
+
? ''
|
|
185
|
+
: 'allowedModuleList must be an array of module names',
|
|
186
|
+
].filter((problem) => problem);
|
|
187
|
+
if (problemList.length) {
|
|
188
|
+
throw `FORBIDDEN_REQUIRE_SPEC_INVALID: ${libraryName} setForbiddenRequireCheck: ${problemList.join('; ')}`;
|
|
189
|
+
}
|
|
190
|
+
forbiddenRequireSpec = { provenanceList: [...provenanceList], allowedModuleList: [...allowedModuleList] };
|
|
191
|
+
return { ...forbiddenRequireSpec };
|
|
192
|
+
};
|
|
193
|
+
|
|
41
194
|
// --------------------------------------------------------------------------------
|
|
42
195
|
// SET CUSTOM MODULE ADDING FUNCTION
|
|
43
196
|
|
|
@@ -86,12 +239,161 @@ const moduleFunction =
|
|
|
86
239
|
const exceptionList = [];
|
|
87
240
|
const library = (() => {
|
|
88
241
|
const add = (name, item) => {
|
|
242
|
+
if (libraryPathMode == 'pathList') {
|
|
243
|
+
addFromPathList(name, item);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
89
246
|
libraryObject[name] = item; //used only for producing display listings
|
|
90
247
|
externalElements[name] = item;
|
|
91
248
|
};
|
|
92
249
|
return { add };
|
|
93
250
|
})();
|
|
94
251
|
|
|
252
|
+
// ======================================================================================
|
|
253
|
+
// PATH LIST ADD (collision, declared override, interface check, provenance)
|
|
254
|
+
|
|
255
|
+
const refusalList = [];
|
|
256
|
+
// No prototype: an element named 'constructor' or 'valueOf' must not find Object.prototype's member.
|
|
257
|
+
const provenanceByName = Object.create(null); // { provenance, filePath, overrides? } per element
|
|
258
|
+
const declaredVersionByName = Object.create(null);
|
|
259
|
+
let loadContext; // { provenance, filePath, addedNameList } while one module is being required
|
|
260
|
+
let apiNameList = []; // the library's own API names; filled once externalElements exists
|
|
261
|
+
let reservedNameList = []; // apiNameList plus Object.prototype's members
|
|
262
|
+
|
|
263
|
+
const refuse = ({ refusalCode, elementName, filePath, message }) =>
|
|
264
|
+
refusalList.push({ refusalCode, elementName, filePath, message: `${refusalCode}: ${message}` });
|
|
265
|
+
|
|
266
|
+
const checkInterface = (elementName, item) => {
|
|
267
|
+
const { describeFunctionName, requiredFunctionList, nameFieldName, contractVersion } = interfaceSpec;
|
|
268
|
+
const missingFunctionList = [describeFunctionName, ...requiredFunctionList].filter(
|
|
269
|
+
(functionName) => item === null || item === undefined || typeof item[functionName] != 'function',
|
|
270
|
+
);
|
|
271
|
+
if (missingFunctionList.length) {
|
|
272
|
+
return { refusalCode: 'PLUGIN_INTERFACE_INCOMPLETE', message: `'${elementName}' lacks required function(s): ${missingFunctionList.join(', ')}` };
|
|
273
|
+
}
|
|
274
|
+
const describeOutput = item[describeFunctionName]();
|
|
275
|
+
if (describeOutput === null || typeof describeOutput != 'object') {
|
|
276
|
+
return { refusalCode: 'PLUGIN_INTERFACE_INCOMPLETE', message: `'${elementName}' ${describeFunctionName}() did not return an object` };
|
|
277
|
+
}
|
|
278
|
+
if (describeOutput[nameFieldName] !== elementName) {
|
|
279
|
+
return {
|
|
280
|
+
refusalCode: 'PLUGIN_NAME_MISMATCH',
|
|
281
|
+
message: `added as '${elementName}' but ${describeFunctionName}().${nameFieldName} is ${JSON.stringify(describeOutput[nameFieldName])}`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (describeOutput.contractVersion !== contractVersion) {
|
|
285
|
+
return {
|
|
286
|
+
refusalCode: 'PLUGIN_CONTRACT_MISMATCH',
|
|
287
|
+
message: `'${elementName}' declares contractVersion ${JSON.stringify(describeOutput.contractVersion)}; ${libraryName} requires ${JSON.stringify(contractVersion)}`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return { describeOutput };
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const findOverrideDeclarationProblem = (overrideDeclaration) => {
|
|
294
|
+
const { replacesVersion, reason } = overrideDeclaration;
|
|
295
|
+
const hasReplacesVersion = replacesVersion !== undefined && replacesVersion !== null && replacesVersion !== '';
|
|
296
|
+
const hasReason = typeof reason == 'string' && reason;
|
|
297
|
+
return hasReplacesVersion && hasReason ? '' : 'overridesFramework needs both replacesVersion and a reason';
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const addFromPathList = (elementName, item) => {
|
|
301
|
+
if (!loadContext) {
|
|
302
|
+
// Nothing is loading, so there is no file to attribute this to and no loadModules callback left to report it.
|
|
303
|
+
throw `PLUGIN_ADD_OUTSIDE_LOAD: ${libraryName} library.add('${elementName}') arrived while no module was loading`;
|
|
304
|
+
}
|
|
305
|
+
const { provenance, filePath } = loadContext;
|
|
306
|
+
const refuseHere = (refusalCode, message) => refuse({ refusalCode, elementName, filePath, message: `${message} (${filePath})` });
|
|
307
|
+
loadContext.addedNameList.push(elementName);
|
|
308
|
+
|
|
309
|
+
if (reservedNameList.includes(elementName)) {
|
|
310
|
+
const reservedBy = apiNameList.includes(elementName) ? `a ${libraryName} API name` : 'a member of Object.prototype';
|
|
311
|
+
refuseHere('PLUGIN_NAME_RESERVED', `'${elementName}' is ${reservedBy} and cannot be an element`);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let describeOutput = {};
|
|
316
|
+
if (interfaceSpec) {
|
|
317
|
+
const interfaceResult = checkInterface(elementName, item);
|
|
318
|
+
if (interfaceResult.refusalCode) {
|
|
319
|
+
refuseHere(interfaceResult.refusalCode, interfaceResult.message);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
describeOutput = interfaceResult.describeOutput;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// With an interface the declaration lives in the describe output; without one, on the item itself.
|
|
326
|
+
const overrideDeclaration = interfaceSpec
|
|
327
|
+
? describeOutput.overridesFramework
|
|
328
|
+
: item !== null && item !== undefined
|
|
329
|
+
? item.overridesFramework
|
|
330
|
+
: undefined;
|
|
331
|
+
if (overrideDeclaration) {
|
|
332
|
+
const declarationProblem = findOverrideDeclarationProblem(overrideDeclaration);
|
|
333
|
+
if (declarationProblem) {
|
|
334
|
+
refuseHere('PLUGIN_OVERRIDE_DECLARATION_INVALID', `'${elementName}': ${declarationProblem}`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const priorRecord = provenanceByName[elementName];
|
|
340
|
+
let overrides;
|
|
341
|
+
if (priorRecord) {
|
|
342
|
+
const isDeclaredOverride = priorRecord.provenance == 'framework' && provenance == 'instance' && overrideDeclaration;
|
|
343
|
+
if (!isDeclaredOverride) {
|
|
344
|
+
refuseHere('PLUGIN_NAME_COLLISION', `'${elementName}' is already loaded from ${priorRecord.filePath}; refused from`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const frameworkVersion = declaredVersionByName[elementName];
|
|
348
|
+
const { replacesVersion, reason } = overrideDeclaration;
|
|
349
|
+
if (frameworkVersion === undefined) {
|
|
350
|
+
logList.push(
|
|
351
|
+
`OVERRIDE_TARGET_UNVERSIONED: instance '${elementName}' (${filePath}) replaces framework ${priorRecord.filePath}, which declares no version; replacesVersion ${JSON.stringify(replacesVersion)} cannot be checked`,
|
|
352
|
+
);
|
|
353
|
+
} else if (frameworkVersion !== replacesVersion) {
|
|
354
|
+
logList.push(
|
|
355
|
+
`OVERRIDE_TARGET_CHANGED: instance '${elementName}' (${filePath}) was written against framework version ${JSON.stringify(replacesVersion)}; framework now declares ${JSON.stringify(frameworkVersion)} (${priorRecord.filePath})`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
overrides = { provenance: priorRecord.provenance, filePath: priorRecord.filePath, version: frameworkVersion, reason };
|
|
359
|
+
} else if (overrideDeclaration) {
|
|
360
|
+
logList.push(`OVERRIDE_TARGET_ABSENT: '${elementName}' (${filePath}) declares overridesFramework but no framework element of that name is loaded`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
provenanceByName[elementName] = overrides ? { provenance, filePath, overrides } : { provenance, filePath };
|
|
364
|
+
declaredVersionByName[elementName] = interfaceSpec ? describeOutput[interfaceSpec.versionFieldName] : undefined;
|
|
365
|
+
libraryObject[elementName] = item; //used only for producing display listings
|
|
366
|
+
externalElements[elementName] = item;
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const formatProvenanceSuffix = (elementName) => {
|
|
370
|
+
const provenanceRecord = provenanceByName[elementName];
|
|
371
|
+
if (libraryPathMode != 'pathList' || !provenanceRecord) {
|
|
372
|
+
return '';
|
|
373
|
+
}
|
|
374
|
+
const { provenance, filePath, overrides } = provenanceRecord;
|
|
375
|
+
const overridesString = overrides
|
|
376
|
+
? `; overrides framework v${overrides.version === undefined ? '(undeclared)' : overrides.version} (${overrides.filePath}) because ${overrides.reason}`
|
|
377
|
+
: '';
|
|
378
|
+
return ` [${provenance}: ${filePath}${overridesString}]`;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const getProvenance = (elementName) => {
|
|
382
|
+
if (libraryPathMode != 'pathList') {
|
|
383
|
+
throw `PROVENANCE_NOT_RECORDED: ${libraryName} records provenance only for libraries loaded with setLibraryPathList`;
|
|
384
|
+
}
|
|
385
|
+
const provenanceRecord = provenanceByName[elementName];
|
|
386
|
+
if (!provenanceRecord) {
|
|
387
|
+
throw `PROVENANCE_UNKNOWN_NAME: ${libraryName} has no element named '${elementName}'`;
|
|
388
|
+
}
|
|
389
|
+
return provenanceRecord.qtClone();
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const getProvenanceList = () =>
|
|
393
|
+
Object.keys(libraryObject).map((elementName) => ({ elementName, ...getProvenance(elementName) }));
|
|
394
|
+
|
|
395
|
+
const getRefusalList = () => refusalList.map((refusalRecord) => ({ ...refusalRecord }));
|
|
396
|
+
|
|
95
397
|
// ======================================================================================
|
|
96
398
|
// LIBRARY OBJECT
|
|
97
399
|
|
|
@@ -113,7 +415,7 @@ const moduleFunction =
|
|
|
113
415
|
(typeof libraryObject[name]).match(/(string|number)/i)
|
|
114
416
|
? libraryObject[name]
|
|
115
417
|
: formatProperty(libraryObject[name])
|
|
116
|
-
}\n\t`,
|
|
418
|
+
}${formatProvenanceSuffix(name)}\n\t`,
|
|
117
419
|
`\n${libraryName} Library Elements\n\t`,
|
|
118
420
|
)
|
|
119
421
|
.replace(/\n\t$/, '');
|
|
@@ -151,6 +453,11 @@ const moduleFunction =
|
|
|
151
453
|
{ excludeMode: true },
|
|
152
454
|
),
|
|
153
455
|
);
|
|
456
|
+
if (libraryPathMode == 'pathList') {
|
|
457
|
+
externalElements.qtSelectProperties(['setLibraryPathList', 'setInterface', 'setForbiddenRequireCheck'], {
|
|
458
|
+
excludeMode: true,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
154
461
|
Object.freeze(externalElements);
|
|
155
462
|
Object.seal(externalElements);
|
|
156
463
|
};
|
|
@@ -158,7 +465,240 @@ const moduleFunction =
|
|
|
158
465
|
// ======================================================================================
|
|
159
466
|
// LOAD MODULES (actual execution)
|
|
160
467
|
|
|
161
|
-
const loadModules = (
|
|
468
|
+
const loadModules = (loadOptions, callback) => {
|
|
469
|
+
if (libraryPathMode == 'pathList') {
|
|
470
|
+
return loadModulesFromPathList(loadOptions, callback);
|
|
471
|
+
}
|
|
472
|
+
if (interfaceSpec || forbiddenRequireSpec) {
|
|
473
|
+
refuse({
|
|
474
|
+
refusalCode: 'REQUIRES_PATH_LIST',
|
|
475
|
+
elementName: '',
|
|
476
|
+
filePath: libraryPath,
|
|
477
|
+
message: `${libraryName} setInterface and setForbiddenRequireCheck apply only to libraries loaded with setLibraryPathList`,
|
|
478
|
+
});
|
|
479
|
+
return deliverResult(refusalList[refusalList.length - 1].message, undefined, callback);
|
|
480
|
+
}
|
|
481
|
+
return loadModulesFromSinglePath(loadOptions, callback);
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
// --------------------------------------------------------------------------------
|
|
485
|
+
// LOAD MODULES FROM A PATH LIST
|
|
486
|
+
|
|
487
|
+
const moduleFilePattern = /\.(c|m)?js$/;
|
|
488
|
+
// Static string-literal requires only. A require built from a variable or a ${} template is not seen.
|
|
489
|
+
const requireCallPattern = /\brequire\s*\(\s*(['"`])([^'"`$]+)\1\s*\)/g;
|
|
490
|
+
|
|
491
|
+
const listModuleSourceFiles = (entryPath, isDirectory) =>
|
|
492
|
+
isDirectory
|
|
493
|
+
? fs
|
|
494
|
+
.readdirSync(entryPath, { recursive: true })
|
|
495
|
+
.filter((relativePath) => moduleFilePattern.test(relativePath))
|
|
496
|
+
.map((relativePath) => path.join(entryPath, relativePath))
|
|
497
|
+
.filter((sourceFilePath) => fs.statSync(sourceFilePath).isFile())
|
|
498
|
+
: [entryPath];
|
|
499
|
+
|
|
500
|
+
const requirableExtensionList = ['.js', '.cjs', '.mjs', '.json'];
|
|
501
|
+
|
|
502
|
+
const isInsidePath = (candidatePath, containmentRootPath) =>
|
|
503
|
+
candidatePath == containmentRootPath || candidatePath.startsWith(containmentRootPath + path.sep);
|
|
504
|
+
|
|
505
|
+
const isExistingFile = (candidatePath) => fs.existsSync(candidatePath) && fs.statSync(candidatePath).isFile();
|
|
506
|
+
|
|
507
|
+
const readPackageMain = (dirPath) => {
|
|
508
|
+
const packageJsonPath = path.join(dirPath, 'package.json');
|
|
509
|
+
if (!isExistingFile(packageJsonPath)) {
|
|
510
|
+
return '';
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
// Plugin-supplied JSON, like a plugin's require(), can only be guarded here.
|
|
514
|
+
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).main || '';
|
|
515
|
+
} catch (err) {
|
|
516
|
+
return '';
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// The file Node would load for a path (exact, with an extension, package.json main, or index), by its realpath.
|
|
521
|
+
// Returns '' when nothing resolves; the caller treats that as forbidden rather than guessing.
|
|
522
|
+
const resolveRequirePath = (requestedPath) => {
|
|
523
|
+
const isDirectory = fs.existsSync(requestedPath) && fs.statSync(requestedPath).isDirectory();
|
|
524
|
+
const packageMain = isDirectory ? readPackageMain(requestedPath) : '';
|
|
525
|
+
const candidatePathList = [
|
|
526
|
+
requestedPath,
|
|
527
|
+
...requirableExtensionList.map((extension) => `${requestedPath}${extension}`),
|
|
528
|
+
...(packageMain ? [path.resolve(requestedPath, packageMain), ...requirableExtensionList.map((extension) => `${path.resolve(requestedPath, packageMain)}${extension}`)] : []),
|
|
529
|
+
...requirableExtensionList.map((extension) => path.join(requestedPath, `index${extension}`)),
|
|
530
|
+
];
|
|
531
|
+
const resolvedPath = candidatePathList.find(isExistingFile);
|
|
532
|
+
return resolvedPath ? fs.realpathSync(resolvedPath) : '';
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// Why a path is not acceptable as plugin code inside containmentRootPath, or '' when it is.
|
|
536
|
+
const findPathProblem = (resolvedPath, containmentRootPath) => {
|
|
537
|
+
if (!resolvedPath) {
|
|
538
|
+
return 'does not resolve to a file';
|
|
539
|
+
}
|
|
540
|
+
if (!isInsidePath(resolvedPath, containmentRootPath)) {
|
|
541
|
+
return `resolves outside ${containmentRootPath} (${resolvedPath})`;
|
|
542
|
+
}
|
|
543
|
+
if (!requirableExtensionList.includes(path.extname(resolvedPath))) {
|
|
544
|
+
return `resolves to a non-module file (${resolvedPath})`;
|
|
545
|
+
}
|
|
546
|
+
return '';
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const findRequireProblem = ({ requiredName, sourceFilePath, containmentRootPath }) => {
|
|
550
|
+
if (requiredName.startsWith('.') || path.isAbsolute(requiredName)) {
|
|
551
|
+
// Node resolves relative to the requiring file's realpath, so a symlinked file is judged where it really is.
|
|
552
|
+
const requestedPath = path.resolve(path.dirname(fs.realpathSync(sourceFilePath)), requiredName);
|
|
553
|
+
return findPathProblem(resolveRequirePath(requestedPath), containmentRootPath);
|
|
554
|
+
}
|
|
555
|
+
const { allowedModuleList } = forbiddenRequireSpec;
|
|
556
|
+
const isAllowed = allowedModuleList.includes(requiredName) || allowedModuleList.includes(requiredName.replace(/^node:/, ''));
|
|
557
|
+
return isAllowed ? '' : 'is not an allowed module';
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const findForbiddenRequireList = ({ entryPath, isDirectory, rootDirPath }) => {
|
|
561
|
+
// A directory plugin may use only its own directory; a single-file plugin may use its root.
|
|
562
|
+
const containmentRootPath = fs.realpathSync(isDirectory ? entryPath : rootDirPath);
|
|
563
|
+
const entryProblem = findPathProblem(resolveRequirePath(entryPath), containmentRootPath);
|
|
564
|
+
if (entryProblem) {
|
|
565
|
+
return [`the module itself ${entryProblem}`];
|
|
566
|
+
}
|
|
567
|
+
return listModuleSourceFiles(entryPath, isDirectory).flatMap((sourceFilePath) => {
|
|
568
|
+
const sourceFileProblem = findPathProblem(fs.realpathSync(sourceFilePath), containmentRootPath);
|
|
569
|
+
if (sourceFileProblem) {
|
|
570
|
+
return [`${sourceFilePath} ${sourceFileProblem}`];
|
|
571
|
+
}
|
|
572
|
+
return [...fs.readFileSync(sourceFilePath, 'utf8').matchAll(requireCallPattern)]
|
|
573
|
+
.map((requireMatch) => ({ requiredName: requireMatch[2], requireProblem: findRequireProblem({ requiredName: requireMatch[2], sourceFilePath, containmentRootPath }) }))
|
|
574
|
+
.filter(({ requireProblem }) => requireProblem)
|
|
575
|
+
.map(({ requiredName, requireProblem }) => `require('${requiredName}') in ${sourceFilePath} ${requireProblem}`);
|
|
576
|
+
});
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const loadOneModule = ({ entryPath, provenance, moduleArgs, tolerateLoadErrors }) => {
|
|
580
|
+
loadContext = { provenance, filePath: entryPath, addedNameList: [] };
|
|
581
|
+
let loadErrorString = '';
|
|
582
|
+
try {
|
|
583
|
+
// The one catch in path-list mode: a third-party module's top-level throw can only be caught here.
|
|
584
|
+
require(entryPath)(moduleArgs);
|
|
585
|
+
} catch (err) {
|
|
586
|
+
loadErrorString = err.toString().split('\n').qtFirst('n/a');
|
|
587
|
+
}
|
|
588
|
+
const { addedNameList } = loadContext;
|
|
589
|
+
loadContext = undefined;
|
|
590
|
+
|
|
591
|
+
if (loadErrorString && tolerateLoadErrors) {
|
|
592
|
+
const listedString = `PLUGIN_LOAD_FAILED: ${path.basename(entryPath)}: ${loadErrorString}`;
|
|
593
|
+
exceptionList.push(listedString);
|
|
594
|
+
logList.push(`${listedString} (${entryPath}; listed because tolerateLoadErrors)`);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (loadErrorString) {
|
|
598
|
+
refuse({ refusalCode: 'PLUGIN_LOAD_FAILED', elementName: '', filePath: entryPath, message: `${entryPath}: ${loadErrorString}` });
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (!addedNameList.length) {
|
|
602
|
+
logList.push(`MODULE_ADDED_NOTHING: ${provenance} ${entryPath} loaded but called library.add() for nothing`);
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const loadOneRoot = ({ pathListEntry, moduleArgs, tolerateLoadErrors }) => {
|
|
607
|
+
const { dirPath: rootDirPath, provenance } = pathListEntry;
|
|
608
|
+
const scanForbiddenRequires = forbiddenRequireSpec && forbiddenRequireSpec.provenanceList.includes(provenance);
|
|
609
|
+
|
|
610
|
+
fs.readdirSync(rootDirPath).forEach((entryName) => {
|
|
611
|
+
const entryPath = path.join(rootDirPath, entryName);
|
|
612
|
+
const isDirectory = fs.statSync(entryPath).isDirectory();
|
|
613
|
+
if (entryName.startsWith('.') || (!isDirectory && !moduleFilePattern.test(entryName))) {
|
|
614
|
+
logList.push(`SKIPPED_NON_MODULE: ${provenance} ${entryPath}`);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (scanForbiddenRequires) {
|
|
618
|
+
const forbiddenRequireList = findForbiddenRequireList({ entryPath, isDirectory, rootDirPath });
|
|
619
|
+
if (forbiddenRequireList.length) {
|
|
620
|
+
refuse({
|
|
621
|
+
refusalCode: 'PLUGIN_FORBIDDEN_REQUIRE',
|
|
622
|
+
elementName: '',
|
|
623
|
+
filePath: entryPath,
|
|
624
|
+
message: `${provenance} ${entryPath} may require only allowed modules or its own files; found ${forbiddenRequireList.join(', ')}`,
|
|
625
|
+
});
|
|
626
|
+
return; // never executed
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
loadOneModule({ entryPath, provenance, moduleArgs, tolerateLoadErrors });
|
|
630
|
+
});
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
let pathListLoadCount = 0;
|
|
634
|
+
|
|
635
|
+
const loadModulesFromPathList = ({ passThroughParameters, tolerateLoadErrors = false } = {}, callback) => {
|
|
636
|
+
const asynchronousPipePlus = new require('qtools-asynchronous-pipe-plus')();
|
|
637
|
+
const pipeRunner = asynchronousPipePlus.pipeRunner;
|
|
638
|
+
const taskListPlus = asynchronousPipePlus.taskListPlus;
|
|
639
|
+
|
|
640
|
+
const taskList = new taskListPlus();
|
|
641
|
+
|
|
642
|
+
// --------------------------------------------------------------------------------
|
|
643
|
+
// REFUSE A SECOND LOAD (every element would collide with itself)
|
|
644
|
+
|
|
645
|
+
taskList.push((args, next) => {
|
|
646
|
+
if (pathListLoadCount > 0) {
|
|
647
|
+
next(`LOAD_MODULES_REPEATED: ${libraryName} path-list libraries load once; loadModules was already called`, args);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
pathListLoadCount++;
|
|
651
|
+
next('', args);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// --------------------------------------------------------------------------------
|
|
655
|
+
// REFUSE A CUSTOM ADD FUNCTION (it cannot report which file failed)
|
|
656
|
+
|
|
657
|
+
taskList.push((args, next) => {
|
|
658
|
+
if (add !== defaultAdd) {
|
|
659
|
+
next(`CUSTOM_ADD_REQUIRES_SINGLE_PATH: ${libraryName} setCustomAddFunction is not supported with setLibraryPathList`, args);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
next('', args);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// --------------------------------------------------------------------------------
|
|
666
|
+
// LOAD ROOTS BY PROVENANCE: FRAMEWORK FIRST, THEN INSTANCE, WHATEVER THE LIST ORDER
|
|
667
|
+
|
|
668
|
+
taskList.push((args, next) => {
|
|
669
|
+
const moduleArgs = { dotD: { logList, library }, passThroughParameters };
|
|
670
|
+
const orderedEntryList = ['framework', 'instance'].flatMap((provenance) =>
|
|
671
|
+
libraryPathList.filter((pathListEntry) => pathListEntry.provenance == provenance && !pathListEntry.rootIsAbsent),
|
|
672
|
+
);
|
|
673
|
+
orderedEntryList.forEach((pathListEntry) => loadOneRoot({ pathListEntry, moduleArgs, tolerateLoadErrors }));
|
|
674
|
+
next('', args);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// --------------------------------------------------------------------------------
|
|
678
|
+
// REPORT EVERY REFUSAL AT ONCE
|
|
679
|
+
|
|
680
|
+
taskList.push((args, next) => {
|
|
681
|
+
if (refusalList.length) {
|
|
682
|
+
next(refusalList.map(({ message }) => message).join('\n'), args);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
next('', args);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// --------------------------------------------------------------------------------
|
|
689
|
+
// INIT AND EXECUTE THE PIPELINE
|
|
690
|
+
|
|
691
|
+
let pipeResult;
|
|
692
|
+
pipeRunner(taskList.getList(), {}, (err) => {
|
|
693
|
+
pipeResult = deliverResult(err, undefined, callback);
|
|
694
|
+
});
|
|
695
|
+
return pipeResult;
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
// --------------------------------------------------------------------------------
|
|
699
|
+
// LOAD MODULES FROM ONE PATH (1.0.13 behaviour, unchanged)
|
|
700
|
+
|
|
701
|
+
const loadModulesFromSinglePath = ({ passThroughParameters }, callback) => {
|
|
162
702
|
const asynchronousPipePlus = new require(
|
|
163
703
|
'qtools-asynchronous-pipe-plus',
|
|
164
704
|
)();
|
|
@@ -215,6 +755,12 @@ const moduleFunction =
|
|
|
215
755
|
|
|
216
756
|
const externalElements = {
|
|
217
757
|
setLibraryPath,
|
|
758
|
+
setLibraryPathList,
|
|
759
|
+
setInterface,
|
|
760
|
+
setForbiddenRequireCheck,
|
|
761
|
+
getProvenance,
|
|
762
|
+
getProvenanceList,
|
|
763
|
+
getRefusalList,
|
|
218
764
|
setCustomAddFunction,
|
|
219
765
|
logList,
|
|
220
766
|
library,
|
|
@@ -223,6 +769,11 @@ const moduleFunction =
|
|
|
223
769
|
qtdProcessLog: logList.formatList,
|
|
224
770
|
};
|
|
225
771
|
externalElements.seal = sealActual(externalElements);
|
|
772
|
+
// Elements become own properties of this object, so an element may not take an API name, nor a name
|
|
773
|
+
// Object.prototype carries: '__proto__' would replace the prototype, and a qtools-functional-library
|
|
774
|
+
// method such as qtSelectProperties (installed on Object.prototype) would be shadowed, breaking seal().
|
|
775
|
+
apiNameList = Object.keys(externalElements);
|
|
776
|
+
reservedNameList = [...apiNameList, ...Object.getOwnPropertyNames(Object.prototype)];
|
|
226
777
|
return externalElements;
|
|
227
778
|
};
|
|
228
779
|
|