qtools-library-dot-d 1.0.12 → 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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +89 -0
  3. package/package.json +9 -3
  4. package/qtools-library-dot-d.js +553 -2
  5. package/libDotD_Working_Example/README.md +0 -234
  6. package/libDotD_Working_Example/demoModules_DotD/demo1.js +0 -46
  7. package/libDotD_Working_Example/demoModules_DotD/demo2.js +0 -48
  8. package/libDotD_Working_Example/index.js +0 -133
  9. package/libDotD_Working_Example/node_modules/.package-lock.json +0 -29
  10. package/libDotD_Working_Example/node_modules/qtools-asynchronous-pipe-plus/README.md +0 -213
  11. package/libDotD_Working_Example/node_modules/qtools-asynchronous-pipe-plus/package.json +0 -20
  12. package/libDotD_Working_Example/node_modules/qtools-asynchronous-pipe-plus/qtools-asynchronous-pipe-plus.js +0 -278
  13. package/libDotD_Working_Example/node_modules/qtools-asynchronous-pipe-plus/test.js +0 -79
  14. package/libDotD_Working_Example/node_modules/qtools-functional-library/README.md +0 -110
  15. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-clone/package.json +0 -11
  16. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-clone/qtools-clone.js +0 -103
  17. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-clone/test.js +0 -78
  18. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-get-by-property/package.json +0 -11
  19. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-get-by-property/qtools-get-by-property.js +0 -73
  20. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-get-by-property/test.js +0 -129
  21. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-includes-regex/package.json +0 -11
  22. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-includes-regex/qtools-includes-regex.js +0 -54
  23. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-includes-regex/test.js +0 -76
  24. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-log-output/package.json +0 -11
  25. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-log-output/qtools-log-output.js +0 -114
  26. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-log-output/test.js +0 -130
  27. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-map-properties/package.json +0 -11
  28. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-map-properties/qtools-map-properties.js +0 -99
  29. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-map-properties/test.js +0 -118
  30. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-merge/package.json +0 -11
  31. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-merge/qtools-merge.js +0 -147
  32. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-merge/test.js +0 -214
  33. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-new-method-template/package.json +0 -11
  34. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-new-method-template/qtools-new-method-template.js +0 -70
  35. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-new-method-template/test.js +0 -66
  36. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-number-iterator/package.json +0 -11
  37. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-number-iterator/qtools-number-iterator.js +0 -137
  38. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-number-iterator/test.js +0 -68
  39. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-number-keys-to-array/package.json +0 -11
  40. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-number-keys-to-array/qtools-number-keys-to-array.js +0 -131
  41. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-number-keys-to-array/test.js +0 -181
  42. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-object-sure-path/package.json +0 -11
  43. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-object-sure-path/qtools-object-sure-path.js +0 -204
  44. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-object-sure-path/test.js +0 -345
  45. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-pass-through/package.json +0 -11
  46. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-pass-through/qtools-pass-through.js +0 -58
  47. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-pass-through/test.js +0 -66
  48. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-pop/package.json +0 -11
  49. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-pop/qtools-pop.js +0 -175
  50. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-pop/test.js +0 -118
  51. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-print-debug/package.json +0 -11
  52. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-print-debug/qtools-print-debug.js +0 -352
  53. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-print-debug/test.js +0 -77
  54. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-select-properties/package.json +0 -11
  55. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-select-properties/qtools-select-properties.js +0 -132
  56. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-select-properties/test.js +0 -156
  57. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-template-replace/README.md +0 -3
  58. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-template-replace/package.json +0 -11
  59. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-template-replace/qtools-template-replace.js +0 -106
  60. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-template-replace/test.js +0 -168
  61. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-to-key-value-array/package.json +0 -11
  62. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-to-key-value-array/qtools-to-key-value-array.js +0 -83
  63. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-to-key-value-array/test.js +0 -68
  64. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-to-string/package.json +0 -11
  65. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-to-string/qtools-to-string.js +0 -84
  66. package/libDotD_Working_Example/node_modules/qtools-functional-library/lib/qtools-to-string/test.js +0 -63
  67. package/libDotD_Working_Example/node_modules/qtools-functional-library/package.json +0 -37
  68. package/libDotD_Working_Example/node_modules/qtools-functional-library/qtFunctionalDemo.js +0 -297
  69. package/libDotD_Working_Example/node_modules/qtools-functional-library/qtools-functional-library.js +0 -271
  70. package/libDotD_Working_Example/node_modules/qtools-functional-library/test.js +0 -47
  71. package/libDotD_Working_Example/node_modules/qtools-library-dot-d/README.md +0 -208
  72. package/libDotD_Working_Example/node_modules/qtools-library-dot-d/package.json +0 -15
  73. package/libDotD_Working_Example/node_modules/qtools-library-dot-d/qtools-library-dot-d.js +0 -239
  74. package/libDotD_Working_Example/package-lock.json +0 -37
  75. package/libDotD_Working_Example/package.json +0 -23
@@ -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 = ({ passThroughParameters }, callback) => {
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