@lumjs/core 1.5.2 → 1.6.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/lib/index.js CHANGED
@@ -19,6 +19,17 @@
19
19
  */
20
20
  const types = require('./types');
21
21
 
22
+ /**
23
+ * A helper for building modules
24
+ *
25
+ * @alias module:@lumjs/core.ModuleBuilder
26
+ * @see module:@lumjs/core/modulebuilder
27
+ */
28
+ const Builder = require('./modulebuilder');
29
+
30
+ // And we'll use the builder to define the rest of this.
31
+ const {has,can,from} = Builder.build(module);
32
+
22
33
  /**
23
34
  * Define properties on an object or function
24
35
  * @name module:@lumjs/core.def
@@ -33,59 +44,14 @@ const types = require('./types');
33
44
  * @see module:@lumjs/core/types.lazy
34
45
  */
35
46
 
36
- // Get a descriptor for one of our sub-modules.
37
- function lib(name, def={})
38
- {
39
- let value = def.value;
40
-
41
- if (value === undefined)
42
- {
43
- const module = def.module ?? name;
44
- value = require('./'+module);
45
-
46
- if (value && def.prop && value[def.prop] !== undefined)
47
- {
48
- value = value[def.prop];
49
- }
50
- }
51
-
52
- const desc =
53
- {
54
- configurable: false,
55
- enumerable: true,
56
- writable: false,
57
- value,
58
- }
59
-
60
- return desc;
61
- }
62
-
63
- // Export one of our always loaded properties.
64
- function has(name, def)
65
- {
66
- types.def(exports, name, lib(name, def));
67
- }
68
-
69
- // Export one of our lazy loaded properties.
70
- function can(name, def)
71
- {
72
- types.lazy(exports, name, () => lib(name, def), {enumerable: true});
73
- }
74
-
75
- // Export a set of always loaded properties from a sub-module
76
- function from(modname, ...libs)
77
- {
78
- for (const lib of libs)
79
- {
80
- has(lib, {module: modname, prop: lib});
81
- }
82
- }
83
-
84
47
  // Our fundamental bits.
85
48
  has('types', {value: types});
86
49
  has('def', {value: types.def});
87
50
  has('lazy', {value: types.lazy});
88
51
 
52
+ // The module builder itself.
53
+ has('ModuleBuilder', {value: Builder});
54
+
89
55
  /**
90
56
  * Array utility functions «Lazy»
91
57
  * @name module:@lumjs/core.arrays
@@ -119,7 +85,7 @@ can('flags');
119
85
  * @name module:@lumjs/core.obj
120
86
  * @type {module:@lumjs/core/obj}
121
87
  */
122
- has('obj');
88
+ can('obj');
123
89
 
124
90
  /**
125
91
  * Functions for getting values and properties with fallback defaults «Lazy»
@@ -174,7 +140,7 @@ from('meta', 'stacktrace', 'AbstractClass', 'Functions', 'NYI');
174
140
  * @function
175
141
  * @see module:@lumjs/core/enum
176
142
  */
177
- has('Enum', {module: 'enum'});
143
+ can('Enum', {module: 'enum'});
178
144
 
179
145
  /**
180
146
  * Make an object support the *Observable* API «Lazy»
@@ -0,0 +1,336 @@
1
+
2
+ const {S,F,B,isObj,needObj,needType,def,lazy} = require('./types');
3
+
4
+ function clone(obj)
5
+ {
6
+ const copy = {};
7
+ for (const name in obj)
8
+ {
9
+ copy[name] = obj[name];
10
+ }
11
+ return copy;
12
+ }
13
+
14
+ // Methods we want to export in the functional API.
15
+ const BUILD_METHODS = ['has', 'can', 'from'];
16
+
17
+ /**
18
+ * A class to make building modules easier.
19
+ *
20
+ * Basically wraps calls to `require()`, `def()`, `lazy()`,
21
+ * and assignments to `module.exports` into a simple set of methods.
22
+ *
23
+ * @exports module:@lumjs/core/modulebuilder
24
+ */
25
+ class ModuleBuilder
26
+ {
27
+ /**
28
+ * Build a new ModuleBuilder instance.
29
+ *
30
+ * @param {object} targetModule - The `module` context variable.
31
+ * @param {object} [opts] - Options to change some default settings.
32
+ * @param {boolean} [opts.configurable=false] Default `configurable` value.
33
+ * @param {boolean} [opts.enumerable=true] Default `enumerable` value.
34
+ * @param {boolean} [opts.writable=false] Default `writable` value.
35
+ *
36
+ * @param {number} [opts.nested=NESTED_ERROR] How to handle nested names.
37
+ *
38
+ * If the `name` parameter in either the `has()` or `can()` method is passed
39
+ * as the path to a nested module with one or more `/` characters,
40
+ * this mode will determine how we derive the property name.
41
+ *
42
+ * For each mode, we'll use an example `name` of `./some/nested/path`.
43
+ *
44
+ * - `ModuleBuilder.NESTED_ERROR` `[0]` (default mode)
45
+ * Throw an `Error` saying paths are unhandled.
46
+ * - `ModuleBuilder.NESTED_FIRST` `[1]`
47
+ * Use the first (non-dot) path element, e.g. `some`
48
+ * - `ModuleBuilder.NESTED_LAST` `[2]`
49
+ * Use the last path element, e.g. `path`
50
+ * - `ModuleBuilder.NESTED_CAMEL` `[3]`
51
+ * Convert the name to camelCase, e.g. `someNestedPath`
52
+ *
53
+ * It's always possible to set the `conf.module` parameter manually as well,
54
+ * which avoids the need to generate a separate parameter name.
55
+ *
56
+ */
57
+ constructor(targetModule, opts={})
58
+ {
59
+ needObj(opts, 'opts was not an object');
60
+ needObj(targetModule, 'targetModule was not an object');
61
+ needObj(targetModule.exports, 'targetModule.exports was not an object');
62
+ needType(F, targetModule.require, 'targetModule.require was not a function');
63
+
64
+ this.module = targetModule;
65
+
66
+ this.configurable = opts.configurable ?? false;
67
+ this.enumerable = opts.enumerable ?? true;
68
+ this.writable = opts.writable ?? false;
69
+
70
+ this.nested = opts.nested ?? ModuleBuilder.NESTED_ERROR;
71
+
72
+ this.strings = opts.strings;
73
+ this.locale = opts.locale;
74
+ }
75
+
76
+ // Get a descriptor for a module export.
77
+ requireDescriptor(name, conf={})
78
+ {
79
+ let value = conf.value;
80
+
81
+ if (value === undefined)
82
+ {
83
+ if (typeof conf.module === S)
84
+ name = conf.module;
85
+ if (!name.startsWith('./'))
86
+ name = './'+name;
87
+
88
+ value = this.module.require(name);
89
+
90
+ if (value && conf.prop && value[conf.prop] !== undefined)
91
+ {
92
+ value = value[conf.prop];
93
+ }
94
+ }
95
+
96
+ const configurable = conf.configurable ?? this.configurable;
97
+ const enumerable = conf.enumerable ?? this.enumerable;
98
+ const writable = conf.writable ?? this.writable;
99
+
100
+ return {configurable, enumerable, writable, value};
101
+ }
102
+
103
+ // Normalize a property name.
104
+ $normalizeProperty(name)
105
+ {
106
+ if (name.startsWith('./'))
107
+ { // Get rid of the prefix.
108
+ name = name.substring(2);
109
+ }
110
+
111
+ if (name.includes('/'))
112
+ { // Multiple paths found.
113
+ const names = name.split('/');
114
+ if (this.nested === ModuleBuilder.NESTED_FIRST)
115
+ {
116
+ name = names[0];
117
+ }
118
+ else if (this.nested === ModuleBuilder.NESTED_LAST)
119
+ {
120
+ name = names[names.length-1];
121
+ }
122
+ else if (this.nested === ModuleBuilder.NESTED_CAMEL)
123
+ {
124
+ name = this.$normalizeWithCamelCase(names);
125
+ }
126
+ else
127
+ {
128
+ throw new Error("No valid nested path handling method was set");
129
+ }
130
+ }
131
+
132
+ return name;
133
+ }
134
+
135
+ $normalizeWithCamelCase(names)
136
+ {
137
+ if (this.strings === undefined)
138
+ {
139
+ this.strings = require('./strings');
140
+ }
141
+ if (this.locale === undefined)
142
+ {
143
+ this.locale = this.strings.getLocale();
144
+ }
145
+ let name = names.shift().toLocaleLowerCase(this.locale);
146
+ for (const path in names)
147
+ {
148
+ name += this.strings.ucfirst(path, true, this.locale);
149
+ }
150
+ return name;
151
+ }
152
+
153
+ /**
154
+ * Export a module directly.
155
+ *
156
+ * @param {string} name - The property name to export.
157
+ *
158
+ * @param {object} [conf] Additional export configuration options.
159
+ * @param {string} [conf.module=`./${name}`] The module to `require()`.
160
+ * @param {string} [conf.prop] If set, we want an exported property
161
+ * of this name from the loaded module.
162
+ *
163
+ * If not set, the entire loaded module will be our exported value.
164
+ *
165
+ * @param {boolean} [conf.configurable=this.configurable]
166
+ * Descriptor `configurable` value.
167
+ * @param {boolean} [conf.enumerable=this.enumerable]
168
+ * Descriptor `enumerable` value.
169
+ * @param {boolean} [conf.writable=this.writable]
170
+ * Descriptor `writable` value.
171
+ *
172
+ * @param {*} [conf.value] An explicit value to set.
173
+ *
174
+ * This skips the `require()` call entirely.
175
+ *
176
+ * @returns {object} `this`
177
+ */
178
+ has(name, conf={})
179
+ {
180
+ const pname = this.$normalizeProperty(name);
181
+ const desc = this.requireDescriptor(name, conf);
182
+ def(this.module.exports, pname, desc);
183
+ return this;
184
+ }
185
+
186
+ /**
187
+ * Export a lazy-loaded module.
188
+ *
189
+ * @param {string} name - The property name to export.
190
+ *
191
+ * @param {object} [conf] Additional export configuration options.
192
+ *
193
+ * In addition to all the options supported by
194
+ * [has()]{@link module:@lumjs/core/modules.has}
195
+ * this also supports one further option.
196
+ *
197
+ * @param {object} [conf.lazy] Advanced options for the `lazy()` call.
198
+ *
199
+ * If not specified, sane defaults will be used, that ensures the
200
+ * `enumerable` descriptor property for the lazy getter is set the
201
+ * same as the final descriptor property once its loaded.
202
+ *
203
+ * @returns {object} `this`
204
+ */
205
+ can(name, conf={})
206
+ {
207
+ const pname = this.$normalizeProperty(name);
208
+ const enumerable = conf.enumerable ?? this.enumerable;
209
+ const lazyOpts = isObj(conf.lazy) ? conf.lazy : {enumerable};
210
+ const getter = () => this.requireDescriptor(name, conf);
211
+ lazy(this.module.exports, pname, getter, lazyOpts);
212
+ return this;
213
+ }
214
+
215
+ /**
216
+ * Re-export some exported properties from a module.
217
+ *
218
+ * By default this uses `has()` to define the exports, but that
219
+ * can be changed using special parameter values.
220
+ *
221
+ * @param {string} modname - The module to export the properties from.
222
+ * @param {...any} libs - What we're exporting.
223
+ *
224
+ * If this is a `string` it is the name of an exported property we want to
225
+ * re-export directly.
226
+ *
227
+ * If this is `true`, all subsequent values will be exported using `can()`.
228
+ *
229
+ * If this is `false`, all subsequent values will be exported using `has()`.
230
+ *
231
+ * If this is an `object`, then it's considered the `conf` parameter for
232
+ * the `has()` or `can()` methods. If the `conf` does not have a `module`
233
+ * property, the `modname` will be assigned as the `module` property.
234
+ * You cannot set the `prop` property, as it's overwritten for every
235
+ * exported property.
236
+ *
237
+ * @returns {object} `this`
238
+ */
239
+ from(modname, ...libs)
240
+ {
241
+ let func = 'has';
242
+ let curConf = {module: modname};
243
+ for (const lib of libs)
244
+ {
245
+ if (isObj(lib))
246
+ { // Change the current config.
247
+ curConf = clone(lib);
248
+ if (curConf.module === undefined)
249
+ { // Make sure the module name is assigned.
250
+ curConf.module = modname;
251
+ }
252
+ }
253
+ else if (typeof lib === B)
254
+ { // Change the function we're calling.
255
+ func = lib ? 'can' : 'has';
256
+ }
257
+ else if (typeof lib === S)
258
+ {
259
+ const conf = clone(curConf);
260
+ conf.prop = lib;
261
+ this[func](lib, conf);
262
+ }
263
+ else
264
+ {
265
+ throw new TypeError("libs must be strings, booleans, or objects");
266
+ }
267
+ }
268
+ return this;
269
+ }
270
+
271
+ /**
272
+ * Create a functional API for the Builder class.
273
+ *
274
+ * Basically makes a new `Builder` instance, then creates standalone
275
+ * closure functions that wrap the main instance methods.
276
+ *
277
+ * These functions can be imported into a namespace directly.
278
+ * They each have a special `builder` property which is a reference
279
+ * to the underlying `Builder` instance.
280
+ *
281
+ * There's also a `builder` property in the exported function list.
282
+ *
283
+ * Example usage:
284
+ *
285
+ * ```js
286
+ * const {has,can,from} = require('@lumjs/core').modules.build(module);
287
+ *
288
+ *
289
+ * // exports.foo = require('./foo');
290
+ * has('foo');
291
+ *
292
+ * // exports.someNestedPath = require('./some/nested/path');
293
+ * has('./some/nested/path');
294
+ *
295
+ * ```
296
+ *
297
+ * @param {object} targetModule - The `module` for the Builder instance.
298
+ * @param {object} [opts] - Any options for the Builder instance.
299
+ * @returns {object} An object containing the closure functions.
300
+ */
301
+ static build(targetModule, opts)
302
+ {
303
+ const builder = new this(targetModule, opts);
304
+ const funcs = {builder};
305
+ for (const name of BUILD_METHODS)
306
+ {
307
+ const func = function()
308
+ {
309
+ return builder[name](...arguments);
310
+ }
311
+ def(func, 'builder', builder);
312
+ funcs[name] = func;
313
+ }
314
+ return funcs;
315
+ }
316
+
317
+ /**
318
+ * A static alternative to `new ModuleBuilder()`;
319
+ *
320
+ * @param {object} targetModule
321
+ * @param {object} [opts]
322
+ * @returns {object} The new `ModuleBuilder` instance.
323
+ */
324
+ static new(targetModule, opts)
325
+ {
326
+ return new this(targetModule, opts);
327
+ }
328
+
329
+ }
330
+
331
+ def(ModuleBuilder, 'NESTED_ERROR', 0)
332
+ def(ModuleBuilder, 'NESTED_FIRST', 1);
333
+ def(ModuleBuilder, 'NESTED_LAST', 2);
334
+ def(ModuleBuilder, 'NESTED_CAMEL', 3);
335
+
336
+ module.exports = ModuleBuilder;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumjs/core",
3
- "version": "1.5.2",
3
+ "version": "1.6.0",
4
4
  "main": "lib/index.js",
5
5
  "exports":
6
6
  {
@@ -12,6 +12,7 @@
12
12
  "./flags": "./lib/flags.js",
13
13
  "./obj": "./lib/obj/index.js",
14
14
  "./opt": "./lib/opt.js",
15
+ "./modulebuilder": "./lib/modulebuilder.js",
15
16
  "./modules": "./lib/modules.js",
16
17
  "./meta": "./lib/meta.js",
17
18
  "./enum": "./lib/enum.js",