noxt-server 0.1.13 → 0.1.14

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/noxt-server.js CHANGED
@@ -28,8 +28,9 @@ export async function startServer({ config, recipe }) {
28
28
  const mlm = mlmInstance.context;
29
29
  await mlm.services.config.merge(config);
30
30
  console.log(mlm.config);
31
- await mlmInstance.start();
31
+ mlmInstance.start();
32
32
 
33
+ mlmInstance.repl();
33
34
  } catch (e) {
34
35
  console.log(await mlmInstance.analyze(recipe ?? 'noxt-dev'));
35
36
  console.error(e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noxt-server",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Server for noxt-js-middleware with CLI and config support",
5
5
  "type": "module",
6
6
  "main": "noxt-server.js",
@@ -23,8 +23,8 @@
23
23
  "author": "Zoran Obradović [https://github.com/zocky]",
24
24
  "license": "GPL-3.0-or-later",
25
25
  "dependencies": {
26
- "noxt-js-middleware": "^1.0.5",
27
- "mlm-core": "^1.0.5",
26
+ "noxt-js-middleware": "^1.0.6",
27
+ "mlm-core": "^1.0.6",
28
28
  "express": "^5.0.0",
29
29
  "cookie-parser": "^1.4.6",
30
30
  "js-yaml": "^4.1.0",
package/units/config.js CHANGED
@@ -5,6 +5,7 @@ export const info = {
5
5
  requires: ['utils','services'],
6
6
  }
7
7
 
8
+ const config = {};
8
9
  const config_is = {};
9
10
  const config_defaults = {};
10
11
  const config_defs = {}
@@ -54,7 +55,11 @@ function isPlainObject(value) {
54
55
  }
55
56
 
56
57
  export default mlm => ({
57
- 'define.config': () => ({}),
58
+ 'define.config': {
59
+ get: () => config,
60
+ enumerable: true,
61
+ configurable: false
62
+ },
58
63
  'register.config': (defs, unit) => {
59
64
  for (const key in defs) {
60
65
  const def = defs[key];
@@ -78,8 +83,8 @@ export default mlm => ({
78
83
  },
79
84
  'utils.merge_deep': merge_deep,
80
85
  'services.config': () => new class ConfigService {
81
- process(config) {
82
- const merged = merge_deep({}, config_defaults, config);
86
+ process(userConfig) {
87
+ const merged = merge_deep({}, config_defaults, userConfig);
83
88
  const ret = {};
84
89
  for (const key in config_defs) {
85
90
  const { is: type, normalize } = config_defs[key];
@@ -94,10 +99,10 @@ export default mlm => ({
94
99
  }
95
100
  return ret;
96
101
  }
97
- merge(config) {
98
- mlm.log('config', config,mlm.config);
99
- Object.assign(mlm.config, this.process(config));
100
-
102
+ merge(userConfig) {
103
+ console.log('initial',config_defaults)
104
+ Object.assign(config, this.process(userConfig));
105
+ console.log(userConfig,'final',config)
101
106
  }
102
107
  get_defs() {
103
108
  return {
package/units/env.js CHANGED
@@ -3,12 +3,12 @@ export const info = {
3
3
  version: '1.0.0',
4
4
  description: 'Environment',
5
5
  }
6
- /*
6
+
7
7
  export default mlm => ({
8
8
  'define.DEV': () => process.env.NODE_ENV !== 'production',
9
9
  'define.PROD': () => process.env.NODE_ENV === 'production',
10
10
  'onBeforeLoad': () => {
11
- //process.env.NODE_ENV ||= 'development'
11
+ process.env.NODE_ENV ||= 'development'
12
12
  }
13
13
  })
14
- */
14
+
package/units/express.js CHANGED
@@ -56,6 +56,8 @@ export default mlm => ({
56
56
  app.use(mw);
57
57
  }
58
58
  }
59
- app.listen(mlm.config.port, mlm.config.host);
59
+ app.listen(mlm.config.port, mlm.config.host, () => {
60
+ mlm.log(`Listening on ${mlm.config.host}:${mlm.config.port}`);
61
+ });
60
62
  },
61
63
  })
@@ -11,19 +11,6 @@ export default mlm => ({
11
11
  requires: ['noxt-plugin'],
12
12
  provides: ['#fetch'],
13
13
  description: 'Native fetch',
14
- 'config.fetch': {
15
- is: {
16
- ttl: 'integer|none',
17
- retry: 'positiveInteger|none',
18
- timeout: 'positiveInteger|none'
19
- },
20
- default: {
21
- ttl: 60 * 60 * 1000,
22
- retry: 2,
23
- timeout: 5000
24
- }
25
- },
26
-
27
14
  'serverContext.fetch': () => fetchOrThrow,
28
15
  });
29
16
 
package/units/fetch.js CHANGED
@@ -8,7 +8,7 @@ export default mlm => ({
8
8
  'componentExports.fetch': async ({exported, props, ctx}) => {
9
9
  for (const id in exported) {
10
10
  let url = await mlm.utils.eval(exported[id], props, ctx);
11
- mlm.log('fetching', id, url);
11
+ // mlm.log('fetching', id, url);
12
12
  try {
13
13
  const now = performance.now();
14
14
  const res = await ctx.fetch(url);
package/units/plugin.js CHANGED
@@ -2,5 +2,5 @@ export const info = {
2
2
  name: 'plugin',
3
3
  version: '1.0.0',
4
4
  description: 'Base',
5
- requires: ['utils','services','hooks','config']
5
+ requires: ['env','utils','services','hooks','config']
6
6
  }
package/units/utils.js CHANGED
@@ -7,16 +7,16 @@ export const info = {
7
7
  export default mlm => {
8
8
 
9
9
  const utils = {};
10
- utils.eval = (fn,...args) => typeof fn === 'function' ? fn(...args) : fn;
10
+ utils.eval = (fn, ...args) => typeof fn === 'function' ? fn(...args) : fn;
11
11
  utils.readOnly = (obj, { label = 'object' } = {}) => new Proxy(obj, {
12
12
  get: (t, k) => t[k],
13
13
  set: (t, k, v) => { mlm.throw(label + ' is read-only'); }
14
14
  });
15
- utils.collector = (target, {
16
- map, is, filter,
17
- label = 'object',
18
- mode = 'object',
19
- override = false
15
+ utils.collector = (target, {
16
+ map, is, filter,
17
+ label = 'object',
18
+ mode = 'object',
19
+ override = false
20
20
  } = {}) => {
21
21
  const modes = {
22
22
  array: () => source => {
@@ -34,7 +34,7 @@ export default mlm => {
34
34
  }
35
35
  let value = source[key];
36
36
  if (filter && !filter(value, key)) continue;
37
- if (is) mlm.assert.is( is,value, label + '.' + key);
37
+ if (is) mlm.assert.is(is, value, label + '.' + key);
38
38
  if (map) value = map(value, key);
39
39
  target[key] = value;
40
40
  }
@@ -73,6 +73,6 @@ export default mlm => {
73
73
  }
74
74
  return ({
75
75
  'define.utils': () => utils.readOnly(utils, 'utils'),
76
- 'register.utils': utils.collector(utils, { is: 'function', mode: 'object' }),
76
+ 'register.utils': utils.collector(utils, { is: 'function', mode: 'object', map: (fn, key) => { mlm.log('utils', key); return fn; } }),
77
77
  })
78
78
  }
package/units.txt ADDED
@@ -0,0 +1,848 @@
1
+ ------
2
+ fetch.js
3
+ -----
4
+ export const info = {
5
+ version: '1.0.0',
6
+ description: 'Preload props from urls',
7
+ requires: ['noxt-plugin','#fetch'],
8
+ }
9
+
10
+ export default mlm => ({
11
+ 'componentExports.fetch': async ({exported, props, ctx}) => {
12
+ for (const id in exported) {
13
+ let url = await mlm.utils.eval(exported[id], props, ctx);
14
+ // mlm.log('fetching', id, url);
15
+ try {
16
+ const now = performance.now();
17
+ const res = await ctx.fetch(url);
18
+ mlm.log('fetched', id, url, (performance.now() - now).toFixed(3) +'ms');
19
+ props[id] = await res.json();
20
+ //mlm.log('fetched', url, JSON.stringify(props[id]).length);
21
+ } catch (e) {
22
+ if (process.env.NODE_ENV !== 'production') {
23
+ mlm.throw(new Error('error fetching ' + url + ': ' + e));
24
+ }
25
+ mlm.error('fetch', url, e);
26
+ props[id] = null;
27
+ }
28
+ }
29
+ },
30
+ })
31
+ ------
32
+ config.js
33
+ -----
34
+ export const info = {
35
+ name: 'config',
36
+ version: '1.0.0',
37
+ description: 'Config ',
38
+ requires: ['utils','services'],
39
+ }
40
+
41
+ const config_is = {};
42
+ const config_defaults = {};
43
+ const config_defs = {}
44
+
45
+ function merge_deep(target, ...sources) {
46
+ // If no sources, return target
47
+ if (sources.length === 0) return target;
48
+
49
+ // If target is not a plain object, return the first source (overwrite)
50
+ if (!isPlainObject(target)) {
51
+ return sources[0];
52
+ }
53
+
54
+ const result = Object.assign({}, target);
55
+
56
+ for (const source of sources) {
57
+ // If source is not a plain object, skip (treat as scalar)
58
+ if (!isPlainObject(source)) {
59
+ continue;
60
+ }
61
+
62
+ for (const [key, sourceValue] of Object.entries(source)) {
63
+ const targetValue = result[key];
64
+
65
+ // If both values are plain objects, merge recursively
66
+ if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
67
+ result[key] = merge_deep(targetValue, sourceValue);
68
+ }
69
+ // Otherwise, overwrite target with source value
70
+ else {
71
+ result[key] = sourceValue;
72
+ }
73
+ }
74
+ }
75
+
76
+ return result;
77
+ }
78
+
79
+ // Helper function to check if value is a plain object
80
+ function isPlainObject(value) {
81
+ if (value === null || typeof value !== 'object') {
82
+ return false;
83
+ }
84
+
85
+ const proto = Object.getPrototypeOf(value);
86
+ return proto === null || proto === Object.prototype;
87
+ }
88
+
89
+ export default mlm => ({
90
+ '$global.config': () => ({}),
91
+ '$define.config': (defs, unit) => {
92
+ for (const key in defs) {
93
+ const def = defs[key];
94
+ mlm.assert.not(key in config_defs, 'Duplicate config key ' + key);
95
+ mlm.assert.is({
96
+ is: 'any|none',
97
+ default: 'any|none',
98
+ normalize: 'function|none'
99
+ }, def, `config.${key}`);
100
+
101
+ if ('default' in def) {
102
+ config_defaults[key] = mlm.utils.eval(def.default);
103
+ }
104
+ config_defs[key] = {
105
+ normalize: def.normalize,
106
+ is: def.is,
107
+ default: def.default,
108
+ unit: unit
109
+ };
110
+ }
111
+ },
112
+ 'utils.merge_deep': merge_deep,
113
+ 'services.config': () => new class ConfigService {
114
+ process(config) {
115
+ const merged = merge_deep({}, config_defaults, config);
116
+ const ret = {};
117
+ for (const key in config_defs) {
118
+ const { is: type, normalize } = config_defs[key];
119
+ if (key in merged) {
120
+ if (normalize) {
121
+ merged[key] = normalize(merged[key]);
122
+ }
123
+
124
+ mlm.assert.is(type, merged[key], `config.${key}`);
125
+ ret[key] = merged[key];
126
+ }
127
+ }
128
+ return ret;
129
+ }
130
+ merge(config) {
131
+ mlm.log('config', config,mlm.config);
132
+ Object.assign(mlm.config, this.process(config));
133
+
134
+ }
135
+ get_defs() {
136
+ return {
137
+ config_defs,
138
+ config_is,
139
+ config_defaults
140
+ };
141
+ }
142
+ },
143
+ })
144
+ ------
145
+ fetch-cache.js
146
+ -----
147
+ export const info = {
148
+ name: 'fetch-cache',
149
+ version: '1.0.0',
150
+ requires: ['noxt-plugin'],
151
+ provides: ['#fetch'],
152
+ description: 'Cached fetch',
153
+ npm: {
154
+ 'node-fetch-cache': '^1.0.0',
155
+ },
156
+ }
157
+
158
+ let fetchCache = null;
159
+
160
+ export default mlm => ({
161
+ 'config.fetch': {
162
+ is: {
163
+ ttl: 'integer|none',
164
+ retry: 'positiveInteger|none',
165
+ timeout: 'positiveInteger|none'
166
+ },
167
+ default: {
168
+ ttl: 60 * 60 * 1000,
169
+ retry: 2,
170
+ timeout: 5000
171
+ }
172
+ },
173
+
174
+ 'pageContext.fetch': (props, ctx) => globalFetch.bind(null, ctx),
175
+ 'serverContext.fetch': () => globalFetch,
176
+
177
+ async onStart() {
178
+ const { default: nodeFetchCache } = await mlm.import('node-fetch-cache');
179
+ const cfg = mlm.config.fetch;
180
+ fetchCache = await nodeFetchCache.create({
181
+ cache: undefined,
182
+ ttl: cfg.ttl,
183
+ retry: cfg.retry,
184
+ timeout: cfg.timeout
185
+ });
186
+ }
187
+ });
188
+
189
+ async function globalFetch(ctx, url, options = {}) {
190
+ if (ctx.req.headers.pragma === 'no-cache') options.cache = 'no-cache';
191
+ const res = await fetchCache(url, options);
192
+ if (!res.ok) throw new Error(res.statusText);
193
+ return res;
194
+ }
195
+ ------
196
+ services.js
197
+ -----
198
+ export const info = {
199
+ name: 'services',
200
+ version: '1.0.0',
201
+ description: 'Services',
202
+ requires: ['utils']
203
+ }
204
+
205
+ const services = {}
206
+
207
+ export default mlm => ({
208
+ '$global.services': () => mlm.utils.readOnly(services, 'services'),
209
+ '$define.services': mlm.utils.collector(services, {
210
+ is: 'function',
211
+ mode: 'object',
212
+ map: (fn, key) => {
213
+ const service = fn(mlm)
214
+ mlm.assert.is.object(service, 'service');
215
+ return service
216
+ }
217
+ }),
218
+ })
219
+ ------
220
+ noxt.js
221
+ -----
222
+ export const info = {
223
+ name: 'noxt',
224
+ version: '1.0.0',
225
+ description: 'Noxt server',
226
+ requires: [
227
+ 'express',
228
+ 'logger',
229
+ 'static',
230
+ 'noxt-router',
231
+ 'fetch-cache-fs',
232
+ 'fetch',
233
+ ],
234
+ }
235
+ ------
236
+ utils.js
237
+ -----
238
+ export const info = {
239
+ name: 'utils',
240
+ version: '1.0.0',
241
+ description: 'Utils',
242
+ //requires: ['env']
243
+ }
244
+ export default mlm => {
245
+
246
+ const utils = {};
247
+ utils.eval = (fn,...args) => typeof fn === 'function' ? fn(...args) : fn;
248
+ utils.readOnly = (obj, { label = 'object' } = {}) => new Proxy(obj, {
249
+ get: (t, k) => t[k],
250
+ set: (t, k, v) => { mlm.throw(label + ' is read-only'); }
251
+ });
252
+ utils.collector = (target, {
253
+ map, is, filter,
254
+ label = 'object',
255
+ mode = 'object',
256
+ override = false
257
+ } = {}) => {
258
+ const modes = {
259
+ array: () => source => {
260
+ for (let value of source) {
261
+ if (filter && !filter(value)) continue;
262
+ if (is) mlm.assert.is(is, value, label);
263
+ if (map) value = map(value);
264
+ target.push(value);
265
+ }
266
+ },
267
+ object: () => source => {
268
+ for (const key in source) {
269
+ if (!override && key in target) {
270
+ mlm.throw('Duplicate key' + key + ' in ' + label);
271
+ }
272
+ let value = source[key];
273
+ if (filter && !filter(value, key)) continue;
274
+ if (is) mlm.assert.is( is,value, label + '.' + key);
275
+ if (map) value = map(value, key);
276
+ target[key] = value;
277
+ }
278
+ },
279
+ arrays: () => source => {
280
+ for (const key in source) {
281
+ let values = [source[key]].flat(Infinity);
282
+ target[key] ??= [];
283
+ for (let value of values) {
284
+ if (filter && !filter(value, key)) continue;
285
+ if (is) mlm.assert.is(is, value, label + '.' + key);
286
+ if (map) value = map(value, key);
287
+ target[key].push(value);
288
+ }
289
+ }
290
+ },
291
+ directory: () => source => {
292
+ for (const key in source) {
293
+ let values = source[key];
294
+ target[key] ??= {};
295
+ for (const id in values) {
296
+ if (!override && id in target[key]) {
297
+ mlm.throw('Duplicate key' + id + ' in ' + label + '.' + key);
298
+ }
299
+ let value = values[id];
300
+ if (filter && !filter(value, key)) continue;
301
+ if (is) mlm.assert.is(is, value, label + '.' + key);
302
+ if (map) value = map(value, key);
303
+ target[key][id] = value;
304
+ }
305
+ }
306
+ }
307
+ }
308
+ mlm.assert(mode in modes, 'Unknown mode ' + mode);
309
+ return modes[mode]();
310
+ }
311
+ return ({
312
+ '$global.utils': () => utils.readOnly(utils, 'utils'),
313
+ '$define.utils': utils.collector(utils, { is: 'function', mode: 'object' }),
314
+ })
315
+ }
316
+ ------
317
+ fetch-node.js
318
+ -----
319
+ export const info = {
320
+ name: 'fetch-node',
321
+ version: '1.0.0',
322
+ description: 'Native fetch',
323
+ requires: ['noxt-plugin'],
324
+ provides: ['#fetch'],
325
+ }
326
+
327
+ export default mlm => ({
328
+ name: 'fetch-node',
329
+ requires: ['noxt-plugin'],
330
+ provides: ['#fetch'],
331
+ description: 'Native fetch',
332
+ 'serverContext.fetch': () => fetchOrThrow,
333
+ });
334
+
335
+ async function fetchOrThrow(ctx, url, options = {}) {
336
+ const res = await fetch(url, options);
337
+ if (!res.ok) throw new Error(res.statusText);
338
+ return res;
339
+ }
340
+ ------
341
+ plugin.js
342
+ -----
343
+ export const info = {
344
+ name: 'plugin',
345
+ version: '1.0.0',
346
+ description: 'Base',
347
+ requires: ['utils','services','hooks','config']
348
+ }
349
+ ------
350
+ reload.js
351
+ -----
352
+ export const info = {
353
+ name: 'reload',
354
+ description: 'Hot reload middleware',
355
+ requires: ['noxt-plugin'],
356
+ npm: {
357
+ 'express': '^5.0.0',
358
+ },
359
+ }
360
+
361
+ const clients = new Set();
362
+ export default mlm => ({
363
+ 'pageContext.reload': ({ ctx }) => ctx.slot('script', 'noxt-reload', '/_reload.js'),
364
+ 'middleware.reload': async () => {
365
+ const { default: express } = await mlm.import('express');
366
+ const router = express.Router();
367
+ router.get('/_reload.js', (req, res) => {
368
+ res.set('Content-Type', 'text/javascript');
369
+ res.send(reloadJs);
370
+ });
371
+
372
+ router.get('/_events', (req, res) => {
373
+ res.writeHead(200, {
374
+ 'Content-Type': 'text/event-stream',
375
+ 'Cache-Control': 'no-cache',
376
+ 'Connection': 'keep-alive',
377
+ 'content-encoding': 'none'
378
+ });
379
+ res.write('\nevent:connected\ndata:\n\n');
380
+ clients.add(res);
381
+ res.on('close', () => {
382
+ clients.delete(res);
383
+ res.end();
384
+ })
385
+ req.on('error', () => clients.delete(res));
386
+ res.on('error', () => clients.delete(res));
387
+ });
388
+
389
+ return router
390
+ },
391
+ onStart() {
392
+ process.on('SIGUSR2', handleSignal)
393
+ },
394
+ onStop() {
395
+ process.removeListener('SIGUSR2', handleSignal)
396
+ }
397
+ })
398
+
399
+ async function handleSignal() {
400
+ const promises = Array.from(clients).map(async client => {
401
+ client.write('event: reload\ndata:\n\n');
402
+ return new Promise(resolve => client.end(resolve));
403
+ });
404
+ await Promise.all(promises);
405
+ process.exit(0);
406
+ };
407
+
408
+ const reloadJs = `
409
+ const eventSource = new EventSource('/_events');
410
+ let reload = false;
411
+ eventSource.addEventListener('connected', () => {
412
+ if (reload) {
413
+ console.log('%c NOXT ','color: #ffd; background-color: #080', 'Reloading...' );
414
+ window.location.reload();
415
+ } else {
416
+ console.log('%c NOXT ', 'color: #ffd; background-color: #080', 'Connected.' );
417
+ }
418
+
419
+ });
420
+ eventSource.addEventListener('reload', () => {
421
+ console.log('%c NOXT ','color: #ffd; background-color: #080', 'Restarting...' );
422
+ reload = true;
423
+ });
424
+ window.addEventListener('beforeunload', () => {
425
+ eventSource.close();
426
+ });
427
+ `
428
+ ------
429
+ logger.js
430
+ -----
431
+ export const info = {
432
+ name: 'logger',
433
+ version: '1.0.0',
434
+ description: 'Minimal logger',
435
+ requires: ['express'],
436
+ }
437
+
438
+ export default mlm => ({
439
+ 'middleware.logger': async (app) => {
440
+ return (req, res, next) => {
441
+ mlm.log(req.method, req.url);
442
+ next();
443
+ }
444
+ },
445
+ })
446
+ ------
447
+ express.js
448
+ -----
449
+ export const info = {
450
+ name: 'express',
451
+ description: 'Sets up an Express server',
452
+ requires: ['plugin'],
453
+ npm: {
454
+ 'express': '^5.0.0',
455
+ 'cookie-parser': '^1.4.6'
456
+ },
457
+ }
458
+
459
+ let app;
460
+ const middlewareNames = new Set();
461
+ const middlewares = [];
462
+ export default mlm => ({
463
+ '$global.express': { get: () => app },
464
+ '$define.middleware': (conf, unit) => {
465
+ for (const key in conf) {
466
+ mlm.assert.not(key in middlewareNames, 'Duplicate middleware ' + key);
467
+ middlewareNames.add(key);
468
+ let c = conf[key];
469
+ if (mlm.is.function(c)) {
470
+ c = { create: c };
471
+ }
472
+ mlm.assert.is({
473
+ path: 'string|none',
474
+ create: 'function'
475
+ }, c, 'middleware');
476
+ mlm.assert.is.function(c.create, 'middleware');
477
+ middlewares.push({ ...c, unit: unit.name });
478
+ }
479
+ },
480
+ 'config.port': {
481
+ is: v => Number.isInteger(v) && v > 0 && v < 65536,
482
+ default: 3000
483
+ },
484
+ 'config.host': {
485
+ is: 'string',
486
+ default: 'localhost'
487
+ },
488
+ 'middleware.json': async (app) => {
489
+ const { default: express } = await mlm.import('express');
490
+ return express.json();
491
+ },
492
+ 'middleware.urlencoded': async (app) => {
493
+ const { default: express } = await mlm.import('express');
494
+ return express.urlencoded({ extended: true });
495
+ },
496
+ async onStart() {
497
+ const { default: express } = await mlm.import('express');
498
+ app = express();
499
+ for (const middleware of middlewares) {
500
+ const mw = await middleware.create(app);
501
+ if (middleware.path) {
502
+ app.use(middleware.path, mw);
503
+ } else {
504
+ app.use(mw);
505
+ }
506
+ }
507
+ app.listen(mlm.config.port, mlm.config.host, () => {
508
+ mlm.log(`Listening on ${mlm.config.host}:${mlm.config.port}`);
509
+ });
510
+ },
511
+ })
512
+ ------
513
+ noxt-dev.js
514
+ -----
515
+ export const info = {
516
+ name: 'noxt-dev',
517
+ description: 'Noxt Dev server',
518
+ requires: [
519
+ 'express',
520
+ 'logger',
521
+ 'static',
522
+ 'noxt-router-dev',
523
+ 'reload',
524
+ 'fetch-cache-fs',
525
+ 'fetch',
526
+ ],
527
+ }
528
+ ------
529
+ fetch-cache-fs.js
530
+ -----
531
+ export const info = {
532
+ name: 'fetch-cache-fs',
533
+ version: '1.0.0',
534
+ description: 'Cached fetch with filesystem cache',
535
+ requires: ['noxt-plugin'],
536
+ provides: ['#fetch'],
537
+ npm: {
538
+ 'node-fetch-cache': '^1.0.0',
539
+ },
540
+ }
541
+
542
+ let fetchCache = null;
543
+
544
+ export default mlm => ({
545
+ 'config.fetch': {
546
+ is: {
547
+ cacheDir: 'string',
548
+ ttl: 'integer|none',
549
+ enabled: 'boolean',
550
+ retry: 'positiveInteger',
551
+ timeout: 'positiveInteger'
552
+ },
553
+ default: {
554
+ enabled: true,
555
+ cacheDir: '.cache/fetch-cache',
556
+ ttl: 60 * 60 * 1000,
557
+ retry: 2,
558
+ timeout: 5000
559
+ },
560
+ },
561
+ 'pageContext.fetch': ({ ctx }) => globalFetch.bind(null, ctx),
562
+ 'serverContext.fetch': () => globalFetch,
563
+ 'onStart': async () => {
564
+ const { default: nodeFetchCache, FileSystemCache } = await mlm.import('node-fetch-cache');
565
+ const cfg = mlm.config.fetch;
566
+ fetchCache = nodeFetchCache.create({
567
+ cache: cfg.enabled ? new FileSystemCache({ cacheDirectory: cfg.cacheDir }) : undefined,
568
+ ttl: cfg.ttl,
569
+ retry: cfg.retry,
570
+ timeout: cfg.timeout
571
+ });
572
+ }
573
+ });
574
+
575
+ async function globalFetch(ctx, url, options = {}) {
576
+ let res = await fetchCache(url, options);
577
+ if (ctx.req.headers.pragma === 'no-cache' && !res.isCacheMiss) {
578
+ console.log('reload', url);
579
+ res.ejectFromCache();
580
+ res = await fetch(url, options);
581
+ }
582
+ if (!res.ok) throw new Error(res.statusText);
583
+ return res;
584
+ }
585
+ ------
586
+ noxt-plugin.js
587
+ -----
588
+ export const info = {
589
+ name: 'noxt-plugin',
590
+ description: 'Noxt Plugin',
591
+ requires: ['express','#noxt-router'],
592
+ }
593
+
594
+ const pageContextHooks = {};
595
+ const serverContextHooks = {};
596
+ const componentExportsHooks = {};
597
+ const registerPageHooks = {};
598
+ const registerComponentHooks = {};
599
+
600
+ export default mlm => {
601
+
602
+ const collector = (coll, desc) => (conf, unit) => {
603
+ for (const key in conf) {
604
+ const fn = conf[key];
605
+ mlm.assert.is.function(fn, desc);
606
+ mlm.assert.not(key in coll, 'Duplicate ' + desc + ' ' + key);
607
+ coll[key] = ({ fn, unit: unit.name });
608
+ }
609
+ }
610
+
611
+ const runner = (
612
+ coll, each = (_, fn, ...args) => fn(...args)
613
+ ) => async (...args) => {
614
+ for (const key in coll) {
615
+ const { fn } = coll[key];
616
+ await each(key, fn, ...args);
617
+ }
618
+ }
619
+
620
+ const arrayCollector = (coll, desc) => (conf, unit) => {
621
+ for (const key in conf) {
622
+ const fn = conf[key];
623
+ mlm.assert.is.function(fn, desc);
624
+ coll[key] ??= [];
625
+ coll[key].push({ fn, unit: unit.name });
626
+ }
627
+ }
628
+
629
+ const arrayRunner = (
630
+ coll, each = (_, fn, ...args) => fn(...args)
631
+ ) => async (...args) => {
632
+ for (const key in coll) {
633
+ const handlers = coll[key];
634
+ for (const handler of handlers) {
635
+ await each(key, handler.fn, ...args);
636
+ }
637
+ }
638
+ }
639
+ const globalServerContext = {};
640
+ return ({
641
+ '$define.pageContext': arrayCollector(pageContextHooks, 'page context handler'),
642
+ '$define.serverContext': arrayCollector(serverContextHooks, 'server context handler'),
643
+ '$define.componentExports': arrayCollector(componentExportsHooks, 'page export handler'),
644
+ '$define.registerPage': collector(registerPageHooks, 'register page handler'),
645
+ '$define.registerComponent': collector(registerComponentHooks, 'register component handler'),
646
+ '$global.noxt_context': {
647
+ get: () => globalServerContext,
648
+ enumerable: true
649
+ },
650
+ 'serverContext.utils': () => mlm.utils,
651
+ 'serverContext.DEV': () => mlm.DEV,
652
+ 'serverContext.PROD': () => mlm.PROD,
653
+ onStart: async () => {
654
+ mlm.services.noxt.noxt_context({ ctx: globalServerContext });
655
+ //mlm.log('ctx', mlm.serverContext);
656
+ },
657
+ 'services.noxt': () => new class PluginService {
658
+ noxt_context = arrayRunner(serverContextHooks,
659
+ async (key, fn, { ctx }) => ctx[key] = await fn({ ctx })
660
+ );
661
+
662
+ noxt_hooks = {
663
+ beforeRequest: [
664
+ ({ ctx }) => {
665
+ Object.assign(ctx, mlm.noxt_context);
666
+ },
667
+ arrayRunner(pageContextHooks,
668
+ async (key, fn, args) => {
669
+ args.ctx[key] = await fn(args)
670
+ }
671
+ ),
672
+ ],
673
+ beforeRender: arrayRunner(componentExportsHooks,
674
+ async (key, fn, { module, props, ctx }) => {
675
+ //mlm.log('beforeRender', unit, key, Object.keys(props));
676
+ if (!(key in module)) return;
677
+ const exported = module[key];
678
+ await fn({ exported, props, ctx, module })
679
+ //mlm.log('beforeRender', key, Object.keys(props));
680
+ }
681
+ ),
682
+ registerPage: runner(registerPageHooks,
683
+ async (key, fn, { module, component }) => {
684
+ await fn({ module, component })
685
+ }
686
+ ),
687
+ registerComponent: runner(registerComponentHooks,
688
+ async (key, fn, { module, component }) => {
689
+ await fn({ module, component })
690
+ }
691
+ )
692
+ }
693
+
694
+ report() {
695
+ return {
696
+ pageContext: pageContextHooks,
697
+ serverContext: serverContextHooks,
698
+ componentExports: componentExportsHooks,
699
+ registerPage: registerPageHooks,
700
+ registerComponent: registerComponentHooks
701
+ }
702
+ }
703
+ },
704
+ 'registerPage.Link'({ component, module }) {
705
+ const As = module.Link ?? 'a';
706
+ component.Link = ({ text, children, attrs, ...props }, ctx) => {
707
+ const href = component.getRoutePath(props, ctx);
708
+ return { type: As, props: { ...attrs, href, children: text ?? children } };
709
+ }
710
+ }
711
+ })
712
+ }
713
+ ------
714
+ noxt-router-dev.js
715
+ -----
716
+ export const info = {
717
+ name: 'noxt-router-dev',
718
+ description: 'Sets up a Noxt Router',
719
+ requires: ['plugin'],
720
+ provides: ['#noxt-router'],
721
+ npm: {
722
+ 'noxt-js-middleware': '^1.0.4'
723
+ },
724
+ }
725
+
726
+ export default mlm => ({
727
+ 'config.views': {
728
+ normalize: p => [p].flat(),
729
+ is: ['string'],
730
+ default: ['views']
731
+ },
732
+ 'middleware.noxt': async () => {
733
+ const { default: noxt } = await mlm.import('noxt-js-middleware');
734
+ const noxtRouter = await noxt({
735
+ context: mlm.noxt_context,
736
+ views: mlm.config.views,
737
+ hooks: mlm.services.noxt.noxt_hooks
738
+ })
739
+ const devRouter = await noxt({
740
+ context: mlm.noxt_context,
741
+ views: mlm.config.views,
742
+ hooks: mlm.services.noxt.noxt_hooks,
743
+ noxt: noxtRouter
744
+ })
745
+ noxtRouter.use('/dev', devRouter)
746
+ return noxtRouter
747
+ }
748
+ })
749
+ ------
750
+ hooks.js
751
+ -----
752
+ export const info = {
753
+ name: 'hooks',
754
+ version: '1.0.0',
755
+ description: 'Hooks',
756
+ requires: ['utils','services']
757
+ }
758
+ const hooks = {}
759
+ const hook_handlers = {}
760
+
761
+ export default mlm => {
762
+ return ({
763
+ '$global.hooks': () => mlm.utils.readOnly(hooks, 'hooks'),
764
+ '$define.hooks': mlm.utils.collector(hooks, {
765
+ is: 'function',
766
+ mode: 'object',
767
+ map: (fn, key) => (...args) => {
768
+ for (const handler of hook_handlers[key] ?? []) {
769
+ fn(handler, ...args);
770
+ }
771
+ }
772
+ }),
773
+ '$define.on': mlm.utils.collector(hook_handlers, {
774
+ is: 'function',
775
+ mode: 'array'
776
+ })
777
+ })
778
+ }
779
+ ------
780
+ static.js
781
+ -----
782
+ export const info = {
783
+ name: 'static',
784
+ description: 'Static middleware',
785
+ requires: ['express'],
786
+ npm: {
787
+ 'serve-static': '^1.15.0'
788
+ },
789
+ }
790
+
791
+ export default mlm => ({
792
+ 'config.static': {
793
+ is: 'string',
794
+ default: 'public'
795
+ },
796
+ 'middleware.static': async (app) => {
797
+ const { default: serve_static } = await mlm.import('serve-static');
798
+ const { resolve } = await mlm.import('path');
799
+ const dir = mlm.config.static;
800
+ return serve_static(resolve(dir));
801
+ },
802
+ })
803
+ ------
804
+ env.js
805
+ -----
806
+ export const info = {
807
+ name: 'env',
808
+ version: '1.0.0',
809
+ description: 'Environment',
810
+ }
811
+ /*
812
+ export default mlm => ({
813
+ '$global.DEV': () => process.env.NODE_ENV !== 'production',
814
+ '$global.PROD': () => process.env.NODE_ENV === 'production',
815
+ 'onBeforeLoad': () => {
816
+ //process.env.NODE_ENV ||= 'development'
817
+ }
818
+ })
819
+ */
820
+ ------
821
+ noxt-router.js
822
+ -----
823
+ export const info = {
824
+ name: 'noxt-router',
825
+ description: 'Sets up a Noxt Router',
826
+ requires: ['plugin'],
827
+ provides: ['#noxt-router'],
828
+ npm: {
829
+ 'noxt-js-middleware': '^1.0.4'
830
+ },
831
+ }
832
+
833
+ export default mlm => ({
834
+ 'config.views': {
835
+ normalize: p => [p].flat(),
836
+ is: ['string'],
837
+ default: ['views']
838
+ },
839
+ 'middleware.noxt': async (app) => {
840
+ const { default: noxt } = await mlm.import('noxt-js-middleware');
841
+ const noxtRouter = await noxt({
842
+ context: mlm.noxt_context,
843
+ views: mlm.config.views,
844
+ hooks: mlm.services.noxt.noxt_hooks
845
+ })
846
+ return noxtRouter
847
+ },
848
+ })