noxt-server 0.1.14 → 0.1.16

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.
@@ -1,125 +1,125 @@
1
1
  export const info = {
2
2
  name: 'noxt-plugin',
3
3
  description: 'Noxt Plugin',
4
- requires: ['express','#noxt-router'],
4
+ requires: ['express'],
5
5
  }
6
6
 
7
- const pageContextHooks = {};
8
- const serverContextHooks = {};
9
- const componentExportsHooks = {};
10
- const registerPageHooks = {};
11
- const registerComponentHooks = {};
12
7
 
13
8
  export default mlm => {
9
+ const requestApi = {};
10
+ const componentErrorHooks = {};
11
+ const requestContextHooks = {};
12
+ const serverContextHooks = {};
13
+ const componentExportsHooks = [];
14
+ const registerPageHooks = {};
15
+ const registerComponentHooks = {};
16
+ const componentChangedHooks = {}
14
17
 
15
- const collector = (coll, desc) => (conf, unit) => {
16
- for (const key in conf) {
17
- const fn = conf[key];
18
- mlm.assert.is.function(fn, desc);
19
- mlm.assert.not(key in coll, 'Duplicate ' + desc + ' ' + key);
20
- coll[key] = ({ fn, unit: unit.name });
21
- }
22
- }
23
-
24
- const runner = (
25
- coll, each = (_, fn, ...args) => fn(...args)
26
- ) => async (...args) => {
27
- for (const key in coll) {
28
- const { fn } = coll[key];
29
- await each(key, fn, ...args);
30
- }
31
- }
32
-
33
- const arrayCollector = (coll, desc) => (conf, unit) => {
34
- for (const key in conf) {
35
- const fn = conf[key];
36
- mlm.assert.is.function(fn, desc);
37
- coll[key] ??= [];
38
- coll[key].push({ fn, unit: unit.name });
39
- }
40
- }
41
-
42
- const arrayRunner = (
43
- coll, each = (_, fn, ...args) => fn(...args)
44
- ) => async (...args) => {
45
- for (const key in coll) {
46
- const handlers = coll[key];
47
- for (const handler of handlers) {
48
- await each(key, handler.fn, ...args);
49
- }
50
- }
51
- }
52
18
  const globalServerContext = {};
53
19
  return ({
54
- 'register.pageContext': arrayCollector(pageContextHooks, 'page context handler'),
55
- 'register.serverContext': arrayCollector(serverContextHooks, 'server context handler'),
56
- 'register.componentExports': arrayCollector(componentExportsHooks, 'page export handler'),
57
- 'register.registerPage': collector(registerPageHooks, 'register page handler'),
58
- 'register.registerComponent': collector(registerComponentHooks, 'register component handler'),
59
- 'define.noxt_context': {
60
- get: () => globalServerContext,
61
- enumerable: true
20
+ 'collect.requestContext': {
21
+ target: requestContextHooks,
22
+ is: 'function',
23
+ mode: 'object'
24
+ },
25
+ 'collect.api': {
26
+ target: requestApi,
27
+ is: 'function',
28
+ mode: 'object',
29
+ },
30
+ 'requestContext.api': ({ ctx }) => new Proxy({}, {
31
+ ownKeys: () => Object.keys(requestApi),
32
+ get: (target, key) => (target[key] ??= requestApi[key]?.({ctx}))
33
+ }),
34
+ 'collect.componentExports': {
35
+ target: componentExportsHooks,
36
+ is: 'object',
37
+ mode: 'array'
38
+ },
39
+ 'collect.serverContext': {
40
+ target: serverContextHooks,
41
+ is: 'function',
42
+ mode: 'object'
43
+ },
44
+ 'collect.registerPage': {
45
+ target: registerPageHooks,
46
+ is: 'function',
47
+ mode: 'object',
62
48
  },
49
+ 'collect.registerComponent': {
50
+ target: registerComponentHooks,
51
+ is: 'function',
52
+ mode: 'object',
53
+ },
54
+ 'collect.componentChanged': {
55
+ target: componentChangedHooks,
56
+ is: 'function',
57
+ mode: 'object',
58
+ },
59
+ 'collect.componentError': {
60
+ target: componentErrorHooks,
61
+ is: 'function',
62
+ mode: 'object',
63
+ },
64
+ 'serverContext.services': () => mlm.services,
63
65
  'serverContext.utils': () => mlm.utils,
64
66
  'serverContext.DEV': () => mlm.DEV,
65
67
  'serverContext.PROD': () => mlm.PROD,
66
68
  onStart: async () => {
67
- mlm.services.noxt.noxt_context({ ctx: globalServerContext });
68
- //mlm.log('ctx', mlm.serverContext);
69
+ const ctx = globalServerContext;
70
+ for (const key in serverContextHooks) {
71
+ ctx[key] = await serverContextHooks[key]({ ctx });
72
+ }
69
73
  },
70
- 'services.noxt': () => new class PluginService {
71
- noxt_context = arrayRunner(serverContextHooks,
72
- async (key, fn, { ctx }) => ctx[key] = await fn({ ctx })
73
- );
74
-
75
- noxt_hooks = {
76
- beforeRequest: [
77
- ({ ctx }) => {
78
- Object.assign(ctx, mlm.noxt_context);
74
+ 'services.noxt': () => new class NoxtPluginService {
75
+ get context() {
76
+ return globalServerContext;
77
+ }
78
+ get hooks() {
79
+ return {
80
+ beforeRequest: async ({ ctx }) => {
81
+ Object.assign(ctx, this.context);
82
+ for (const key in requestContextHooks) {
83
+ ctx[key] = await requestContextHooks[key]({ ctx });
84
+ }
79
85
  },
80
- arrayRunner(pageContextHooks,
81
- async (key, fn, args) => {
82
- args.ctx[key] = await fn(args)
86
+ beforeRender: async ({ ...args }) => {
87
+ for (const item of componentExportsHooks) {
88
+ for (const key in item) {
89
+ if (!(key in args.module)) continue;
90
+ await item[key]({ exported: args.module[key], ...args });
91
+ }
92
+ }
93
+ },
94
+ registerPage: async ({ module, component }) => {
95
+ for (const fn of Object.values(registerPageHooks)) {
96
+ await fn({ module, component });
97
+ }
98
+ },
99
+ registerComponent: async (...args) => {
100
+ for (const fn of Object.values(registerComponentHooks)) {
101
+ await fn(...args);
102
+ }
103
+ },
104
+ componentChanged: async ({ module, component }) => {
105
+ for (const fn of Object.values(componentChangedHooks)) {
106
+ await fn({ module, component });
107
+ }
108
+ },
109
+ componentError: async ({ name, error }) => {
110
+ for (const fn of Object.values(componentErrorHooks)) {
111
+ await fn({ name, error });
83
112
  }
84
- ),
85
- ],
86
- beforeRender: arrayRunner(componentExportsHooks,
87
- async (key, fn, { module, props, ctx }) => {
88
- //mlm.log('beforeRender', unit, key, Object.keys(props));
89
- if (!(key in module)) return;
90
- const exported = module[key];
91
- await fn({ exported, props, ctx, module })
92
- //mlm.log('beforeRender', key, Object.keys(props));
93
- }
94
- ),
95
- registerPage: runner(registerPageHooks,
96
- async (key, fn, { module, component }) => {
97
- await fn({ module, component })
98
- }
99
- ),
100
- registerComponent: runner(registerComponentHooks,
101
- async (key, fn, { module, component }) => {
102
- await fn({ module, component })
103
113
  }
104
- )
105
- }
106
-
107
- report() {
108
- return {
109
- pageContext: pageContextHooks,
110
- serverContext: serverContextHooks,
111
- componentExports: componentExportsHooks,
112
- registerPage: registerPageHooks,
113
- registerComponent: registerComponentHooks
114
114
  }
115
115
  }
116
116
  },
117
117
  'registerPage.Link'({ component, module }) {
118
118
  const As = module.Link ?? 'a';
119
- component.Link = ({ text, children, attrs, ...props }, ctx) => {
120
- const href = component.getRoutePath(props, ctx);
121
- return { type: As, props: { ...attrs, href, children: text ?? children } };
119
+ component.Link = ({ text, children, params={}, ...attrs }, ctx) => {
120
+ const href = component.getRoutePath(params, ctx);
121
+ return { type: As, props: { ...attrs, href, children: text ?? children } };
122
122
  }
123
- }
123
+ },
124
124
  })
125
125
  }
@@ -1,11 +1,11 @@
1
1
  export const info = {
2
2
  name: 'noxt-router',
3
3
  description: 'Sets up a Noxt Router',
4
- requires: ['plugin'],
4
+ requires: ['noxt-plugin'],
5
5
  provides: ['#noxt-router'],
6
- npm: {
7
- 'noxt-js-middleware': '^1.0.4'
8
- },
6
+ packages: {
7
+ noxt: 'noxt-js-middleware'
8
+ }
9
9
  }
10
10
 
11
11
  export default mlm => ({
@@ -14,13 +14,30 @@ export default mlm => ({
14
14
  is: ['string'],
15
15
  default: ['views']
16
16
  },
17
- 'middleware.noxt': async (app) => {
18
- const { default: noxt } = await mlm.import('noxt-js-middleware');
17
+ 'prod.middleware.noxt': async (app) => {
18
+ const { default: noxt } = mlm.packages.noxt;
19
+ const noxtRouter = await noxt({
20
+ context: mlm.services.noxt.context,
21
+ views: mlm.config.views,
22
+ hooks: mlm.services.noxt.hooks,
23
+ })
24
+ return noxtRouter
25
+ },
26
+ 'dev.middleware.noxt': async (app) => {
27
+ const { default: noxt } = mlm.packages.noxt;
19
28
  const noxtRouter = await noxt({
20
- context: mlm.noxt_context,
29
+ context: mlm.services.noxt.context,
21
30
  views: mlm.config.views,
22
- hooks: mlm.services.noxt.noxt_hooks
31
+ hooks: mlm.services.noxt.hooks,
32
+ watch: true
23
33
  })
24
34
  return noxtRouter
25
35
  },
36
+ 'dev.componentChanged.reload': ({ name, component }) => {
37
+ mlm.services.reload.refresh();
38
+ },
39
+ 'dev.componentError.reload': async ({ name, error }) => {
40
+ mlm.services.reload.error(error);
41
+ },
42
+ 'dev.requestContext.reload': ({ ctx }) => ctx.slot('script', 'noxt-reload', '/_reload.js'),
26
43
  })
package/units/noxt.js CHANGED
@@ -5,9 +5,11 @@ export const info = {
5
5
  requires: [
6
6
  'express',
7
7
  'logger',
8
+ 'compression',
8
9
  'static',
10
+ 'bundler',
9
11
  'noxt-router',
10
12
  'fetch-cache-fs',
11
13
  'fetch',
12
14
  ],
13
- }
15
+ }
package/units/plugin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export const info = {
2
2
  name: 'plugin',
3
3
  version: '1.0.0',
4
- description: 'Base',
4
+ description: 'Plugin base',
5
5
  requires: ['env','utils','services','hooks','config']
6
6
  }
package/units/reload.js CHANGED
@@ -1,24 +1,28 @@
1
1
  export const info = {
2
2
  name: 'reload',
3
3
  description: 'Hot reload middleware',
4
- requires: ['noxt-plugin'],
5
- npm: {
6
- 'express': '^5.0.0',
7
- },
4
+ requires: ['express'],
5
+ provides: ['#reload'],
8
6
  }
9
7
 
10
8
  const clients = new Set();
11
9
  export default mlm => ({
12
- 'pageContext.reload': ({ ctx }) => ctx.slot('script', 'noxt-reload', '/_reload.js'),
10
+ 'services.reload': () => ({
11
+ refresh: forceReload,
12
+ error: sendError
13
+ }),
14
+ 'define.refresh': () => forceReload,
15
+ 'define.error': () => sendError,
13
16
  'middleware.reload': async () => {
14
- const { default: express } = await mlm.import('express');
15
- const router = express.Router();
17
+ const router = mlm.services.express.Router();
16
18
  router.get('/_reload.js', (req, res) => {
19
+ res.logGroup = 'reload';
17
20
  res.set('Content-Type', 'text/javascript');
18
21
  res.send(reloadJs);
19
22
  });
20
23
 
21
24
  router.get('/_events', (req, res) => {
25
+ res.logGroup = 'reload';
22
26
  res.writeHead(200, {
23
27
  'Content-Type': 'text/event-stream',
24
28
  'Cache-Control': 'no-cache',
@@ -34,10 +38,9 @@ export default mlm => ({
34
38
  req.on('error', () => clients.delete(res));
35
39
  res.on('error', () => clients.delete(res));
36
40
  });
37
-
38
41
  return router
39
42
  },
40
- onStart() {
43
+ onStart() {
41
44
  process.on('SIGUSR2', handleSignal)
42
45
  },
43
46
  onStop() {
@@ -45,6 +48,27 @@ export default mlm => ({
45
48
  }
46
49
  })
47
50
 
51
+ async function forceReload() {
52
+ const promises = Array.from(clients).map(async client => {
53
+ client.write('event: refresh\ndata:\n\n');
54
+ return new Promise(resolve => client.end(resolve));
55
+ });
56
+ await Promise.all(promises);
57
+ }
58
+
59
+ async function sendError(error) {
60
+ const errorData = {
61
+ message: error.message,
62
+ stack: error.stack,
63
+ name: error.name
64
+ };
65
+ const data = JSON.stringify(errorData).replace(/\n/g, '\\n');
66
+
67
+ clients.forEach(client => {
68
+ client.write(`event: error\ndata: ${data}\n\n`);
69
+ });
70
+ }
71
+
48
72
  async function handleSignal() {
49
73
  const promises = Array.from(clients).map(async client => {
50
74
  client.write('event: reload\ndata:\n\n');
@@ -52,11 +76,13 @@ async function handleSignal() {
52
76
  });
53
77
  await Promise.all(promises);
54
78
  process.exit(0);
55
- };
79
+ }
56
80
 
57
81
  const reloadJs = `
58
- const eventSource = new EventSource('/_events');
82
+ const eventSource = new EventSource('/_events');
59
83
  let reload = false;
84
+ let errorOverlay = null;
85
+
60
86
  eventSource.addEventListener('connected', () => {
61
87
  if (reload) {
62
88
  console.log('%c NOXT ','color: #ffd; background-color: #080', 'Reloading...' );
@@ -64,13 +90,76 @@ eventSource.addEventListener('connected', () => {
64
90
  } else {
65
91
  console.log('%c NOXT ', 'color: #ffd; background-color: #080', 'Connected.' );
66
92
  }
67
-
93
+ hideError();
68
94
  });
95
+
69
96
  eventSource.addEventListener('reload', () => {
70
97
  console.log('%c NOXT ','color: #ffd; background-color: #080', 'Restarting...' );
71
98
  reload = true;
72
99
  });
100
+
101
+ eventSource.addEventListener('refresh', () => {
102
+ console.log('%c NOXT ','color: #ffd; background-color: #080', 'Refresh...' );
103
+ window.location.reload();
104
+ });
105
+
106
+ eventSource.addEventListener('error', (e) => {
107
+ const error = JSON.parse(e.data);
108
+ console.error('%c NOXT ','color: #ffd; background-color: #f00', 'Error:', error.message);
109
+ showError(error);
110
+ });
111
+
73
112
  window.addEventListener('beforeunload', () => {
74
113
  eventSource.close();
75
114
  });
115
+
116
+ function showError(error) {
117
+ hideError();
118
+
119
+ errorOverlay = document.createElement('dialog');
120
+ errorOverlay.style.cssText = \`
121
+ position: fixed;
122
+ top: 0;
123
+ max-width: 100vw;
124
+ max-height: 100vh;
125
+ width: 100%;
126
+ height: 100%;
127
+ margin: 0;
128
+ padding: 20px;
129
+ background: rgba(0, 0, 0, 0.95);
130
+ color: #fff;
131
+ border: none;
132
+ font-family: monospace;
133
+ overflow: auto;
134
+ \`;
135
+
136
+ errorOverlay.innerHTML = \`
137
+ <h1 style="color: #f00; margin: 0 0 1em 0;">\${error.name || 'Error'}</h1>
138
+ <p style="font-size: 16px; margin-bottom: 1em;">\${error.message}</p>
139
+ <pre style="font-size: 14px; white-space: pre-wrap; overflow-x: auto;">\${error.stack || ''}</pre>
140
+ \`;
141
+
142
+ document.body.appendChild(errorOverlay);
143
+ errorOverlay.showModal();
144
+
145
+ errorOverlay.addEventListener('click', (e) => {
146
+ if (e.target === errorOverlay) hideError();
147
+ });
148
+
149
+ const closeHandler = (e) => {
150
+ if (e.key === 'Escape') {
151
+ hideError();
152
+ document.removeEventListener('keydown', closeHandler);
153
+ }
154
+ };
155
+ document.addEventListener('keydown', closeHandler);
156
+ }
157
+
158
+ function hideError() {
159
+ if (errorOverlay) {
160
+ errorOverlay.close();
161
+ errorOverlay.remove();
162
+ errorOverlay = null;
163
+ }
164
+ }
76
165
  `
package/units/services.js CHANGED
@@ -5,17 +5,21 @@ export const info = {
5
5
  requires: ['utils']
6
6
  }
7
7
 
8
- const services = {}
9
8
 
10
- export default mlm => ({
11
- 'define.services': () => mlm.utils.readOnly(services, 'services'),
12
- 'register.services': mlm.utils.collector(services, {
13
- is: 'function',
14
- mode: 'object',
15
- map: (fn, key) => {
16
- const service = fn(mlm)
17
- mlm.assert.is.object(service, 'service');
18
- return service
9
+ export default mlm => {
10
+ const services = {}
11
+
12
+ return {
13
+ 'define.services': () => mlm.utils.readOnly(services, 'services'),
14
+ 'collect.services': {
15
+ target: services,
16
+ is: 'function',
17
+ mode: 'object',
18
+ map: (fn, key) => {
19
+ const service = fn(mlm)
20
+ mlm.assert.is.object(service, 'service');
21
+ return service
22
+ }
19
23
  }
20
- }),
21
- })
24
+ }
25
+ }
package/units/static.js CHANGED
@@ -2,20 +2,48 @@ export const info = {
2
2
  name: 'static',
3
3
  description: 'Static middleware',
4
4
  requires: ['express'],
5
- npm: {
6
- 'serve-static': '^1.15.0'
7
- },
5
+ packages: ['express']
8
6
  }
9
7
 
10
- export default mlm => ({
11
- 'config.static': {
12
- is: 'string',
13
- default: 'public'
14
- },
15
- 'middleware.static': async (app) => {
16
- const { default: serve_static } = await mlm.import('serve-static');
17
- const { resolve } = await mlm.import('path');
18
- const dir = mlm.config.static;
19
- return serve_static(resolve(dir));
20
- },
21
- })
8
+ import { resolve } from 'path';
9
+ import fs from 'fs';
10
+
11
+ export default mlm => {
12
+ let watcher;
13
+ return {
14
+ 'config.static': {
15
+ is: 'string',
16
+ default: 'public',
17
+ },
18
+
19
+ 'prod.middleware.static': async () => {
20
+ const dir = mlm.config.static;
21
+ if (!dir) return null;
22
+ return mlm.packages.express.static(resolve(dir), {
23
+ setHeaders: res => res.logGroup = 'static',
24
+ });
25
+ },
26
+ 'dev.middleware.static': async () => {
27
+ const dir = mlm.config.static;
28
+ if (!dir) return null;
29
+ return mlm.packages.express.static(resolve(dir), {
30
+ etag: false,
31
+ cacheControl: false,
32
+ setHeaders: res => res.logGroup = 'static',
33
+ });
34
+ },
35
+ 'dev.onStart'() {
36
+ const dir = mlm.config.static;
37
+ if (!dir) return;
38
+ watcher = fs.watch(dir, { recursive: true }, mlm.utils.debounce(() => {
39
+ mlm.services.reload.refresh()
40
+ }, 50))
41
+ },
42
+ 'dev.onStop'() {
43
+ if (watcher) {
44
+ watcher.close();
45
+ watcher = null;
46
+ }
47
+ }
48
+ }
49
+ }
package/units/utils.js CHANGED
@@ -7,6 +7,13 @@ export const info = {
7
7
  export default mlm => {
8
8
 
9
9
  const utils = {};
10
+ utils.debounce = (fn, delay) => {
11
+ let timeout;
12
+ return (...args) => {
13
+ clearTimeout(timeout);
14
+ timeout = setTimeout(() => fn(...args), delay);
15
+ }
16
+ }
10
17
  utils.eval = (fn, ...args) => typeof fn === 'function' ? fn(...args) : fn;
11
18
  utils.readOnly = (obj, { label = 'object' } = {}) => new Proxy(obj, {
12
19
  get: (t, k) => t[k],
@@ -20,7 +27,7 @@ export default mlm => {
20
27
  } = {}) => {
21
28
  const modes = {
22
29
  array: () => source => {
23
- for (let value of source) {
30
+ for (let value of [].concat(source)) {
24
31
  if (filter && !filter(value)) continue;
25
32
  if (is) mlm.assert.is(is, value, label);
26
33
  if (map) value = map(value);
@@ -71,8 +78,18 @@ export default mlm => {
71
78
  mlm.assert(mode in modes, 'Unknown mode ' + mode);
72
79
  return modes[mode]();
73
80
  }
81
+
74
82
  return ({
75
83
  'define.utils': () => utils.readOnly(utils, 'utils'),
76
84
  'register.utils': utils.collector(utils, { is: 'function', mode: 'object', map: (fn, key) => { mlm.log('utils', key); return fn; } }),
85
+ 'inject.collect': async (confs,unit) => {
86
+ const ret = {}
87
+ for (const key in confs) {
88
+ const { target, ...conf } = confs[key];
89
+ conf.label = key;
90
+ ret[key] = utils.collector(target, conf);
91
+ }
92
+ return {register:ret}
93
+ },
77
94
  })
78
95
  }