noxt-server 0.1.4 → 0.1.6
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/bin/noxt-server +2 -63
- package/default.config.yaml +1 -0
- package/dev-ui/DevIndex.jsx +17 -0
- package/dev-ui/DevModule.jsx +87 -0
- package/noxt-server.js +32 -0
- package/package.json +9 -12
- package/units/config.js +89 -0
- package/units/env.js +14 -0
- package/units/express.js +65 -0
- package/units/fetch-cache-fs.js +54 -0
- package/units/fetch-cache.js +48 -0
- package/units/fetch-node.js +34 -0
- package/units/fetch.js +27 -0
- package/units/hooks.js +27 -0
- package/units/logger.js +15 -0
- package/units/noxt-dev.js +13 -0
- package/units/noxt-plugin.js +125 -0
- package/units/noxt-router-dev.js +32 -0
- package/units/noxt-router.js +25 -0
- package/units/noxt.js +13 -0
- package/units/plugin.js +6 -0
- package/units/reload.js +76 -0
- package/units/services.js +21 -0
- package/units/static.js +21 -0
- package/units/utils.js +78 -0
- package/config.js +0 -82
- package/index.js +0 -89
- package/reload_client.js +0 -15
- package/reload_server.js +0 -43
package/bin/noxt-server
CHANGED
|
@@ -1,65 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import minimist from 'minimist';
|
|
5
|
-
import yaml from 'js-yaml';
|
|
6
|
-
import { startServer } from '../index.js';
|
|
7
2
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// read deafault config
|
|
11
|
-
let config = yaml.load(fs.readFileSync(path.resolve(__dirname, '../default.config.yaml'), 'utf8'));
|
|
12
|
-
|
|
13
|
-
// read local config if exists
|
|
14
|
-
const configPath = 'noxt.config.yaml';
|
|
15
|
-
const configFullPath = path.resolve(process.cwd(), configPath);
|
|
16
|
-
if (fs.existsSync(configFullPath)) {
|
|
17
|
-
console.log(`Using config file: ${configFullPath}`);
|
|
18
|
-
try {
|
|
19
|
-
const localConfig = yaml.load(fs.readFileSync(configFullPath, 'utf8'));
|
|
20
|
-
Object.assign(config, localConfig);
|
|
21
|
-
} catch (e) {
|
|
22
|
-
console.error(`Error loading config file: ${e.message}`);
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
} else {
|
|
26
|
-
// guess some defaults based on common project structure
|
|
27
|
-
if (fs.existsSync(path.resolve(process.cwd(), 'views'))) {
|
|
28
|
-
config.views.push('views');
|
|
29
|
-
}
|
|
30
|
-
if (fs.existsSync(path.resolve(process.cwd(), 'pages'))) {
|
|
31
|
-
config.views.push('pages');
|
|
32
|
-
}
|
|
33
|
-
if (fs.existsSync(path.resolve(process.cwd(), 'templates'))) {
|
|
34
|
-
config.views.push('templates');
|
|
35
|
-
}
|
|
36
|
-
if (fs.existsSync(path.resolve(process.cwd(), 'public'))) {
|
|
37
|
-
config.static.push('public');
|
|
38
|
-
}
|
|
39
|
-
if (fs.existsSync(path.resolve(process.cwd(), 'static'))) {
|
|
40
|
-
config.static.push('static');
|
|
41
|
-
}
|
|
42
|
-
if (fs.existsSync(path.resolve(process.cwd(), 'app.js'))) {
|
|
43
|
-
config.context = 'app.js';
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
// override config with command line options
|
|
47
|
-
const options = minimist(process.argv.slice(2), {
|
|
48
|
-
string: ['views'], // treat as strings
|
|
49
|
-
unknown: (arg) => true
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
for (const key in options) {
|
|
53
|
-
if (key === '_') continue; // ignore non-keyed args
|
|
54
|
-
if (!(key in config)) {
|
|
55
|
-
console.warn(`Unknown config option: ${key}`);
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
config[key] = options[key] ?? config[key];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
startServer(config).catch(err => {
|
|
62
|
-
console.error(`Error starting server: ${err.message}`);
|
|
63
|
-
console.log(err.stack);
|
|
64
|
-
process.exit(1);
|
|
65
|
-
});
|
|
3
|
+
console.warn('Deprecated. Use noxt-cli instead.');
|
|
4
|
+
process.exit(1);
|
package/default.config.yaml
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const route = '/';
|
|
2
|
+
|
|
3
|
+
export default function DevIndex({ }, { noxt, DevModule }) {
|
|
4
|
+
const { modules, components } = noxt;
|
|
5
|
+
return (
|
|
6
|
+
<div>
|
|
7
|
+
<h1>Dev UI</h1>
|
|
8
|
+
<ul class="noxt list">
|
|
9
|
+
{Object.keys(modules).map((name) => (
|
|
10
|
+
<div class="nost item">
|
|
11
|
+
{name}: <DevModule.Link name={name} text={name} />
|
|
12
|
+
</div>
|
|
13
|
+
))}
|
|
14
|
+
</ul>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import yaml from 'js-yaml';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export const route = '/module/:name';
|
|
5
|
+
export const params = {
|
|
6
|
+
module: ({ name }, { noxt }) => noxt.modules[name],
|
|
7
|
+
Template: ({ name }, { noxt }) => noxt.components[name]
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default async function DevModule({ name, Template }, { query, req, res }) {
|
|
11
|
+
if (!Template) return <p>No component named “{name}”.</p>;
|
|
12
|
+
return (
|
|
13
|
+
<div>
|
|
14
|
+
<h1>Dev UI - {name}</h1>
|
|
15
|
+
{await Template.renderWithRequest(query, req, res, query = {})}
|
|
16
|
+
<Node node={await Template.evaluateWithRequest(query, req, res, query = {})} />
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function Node({ node }) {
|
|
22
|
+
if (!node) return null;
|
|
23
|
+
if (typeof node === 'string' || typeof node === 'number') return node;
|
|
24
|
+
|
|
25
|
+
if (Array.isArray(node)) {
|
|
26
|
+
return (
|
|
27
|
+
<div>
|
|
28
|
+
{node.map((n) => (
|
|
29
|
+
<Node node={n} />
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (typeof node === 'boolean') return node;
|
|
35
|
+
if (typeof node === 'object') {
|
|
36
|
+
if (node.html) return <pre>{node.html.slice(0, 100)}</pre>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { type, props: { children, ...props } = {} } = node;
|
|
40
|
+
let kind = 'tag';
|
|
41
|
+
|
|
42
|
+
if (type?.isNoxtFragment) {
|
|
43
|
+
return (
|
|
44
|
+
<noxt-node type="fragment">
|
|
45
|
+
{children && <noxt-children><Node node={children} /></noxt-children>}
|
|
46
|
+
</noxt-node>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
let name = type;
|
|
50
|
+
|
|
51
|
+
if (type?.isNoxtComponent) {
|
|
52
|
+
kind = 'component';
|
|
53
|
+
name = type.noxtName;
|
|
54
|
+
} else if (typeof type === 'function') {
|
|
55
|
+
kind = 'function';
|
|
56
|
+
name = type.constructor.name + ' ' + (type.name || 'anon');
|
|
57
|
+
} else {
|
|
58
|
+
name = type;
|
|
59
|
+
}
|
|
60
|
+
return (
|
|
61
|
+
<noxt-node type={kind}>
|
|
62
|
+
<noxt-name>{name}</noxt-name>
|
|
63
|
+
<noxt-props>
|
|
64
|
+
{Object.entries(props).map(([k, v]) => {
|
|
65
|
+
const value = safeStringify(v);
|
|
66
|
+
const text = value.length > 30 ? <details ><summary>{value.slice(0, 27) + '...'}</summary><pre>{yaml.dump(v)}</pre></details> : value;
|
|
67
|
+
return <noxt-prop>
|
|
68
|
+
<noxt-key>{k}</noxt-key>
|
|
69
|
+
<noxt-value>{text}</noxt-value>
|
|
70
|
+
</noxt-prop>
|
|
71
|
+
})}
|
|
72
|
+
</noxt-props>
|
|
73
|
+
{children && <noxt-children><Node node={children} /></noxt-children>}
|
|
74
|
+
</noxt-node>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function safeStringify(value, space = 0) {
|
|
79
|
+
return JSON.stringify(value, (k, v) => {
|
|
80
|
+
if (v == null) return v; // use null, drop undefined
|
|
81
|
+
const t = typeof v;
|
|
82
|
+
if (t === 'boolean' || t === 'string' || t === 'number') return v;
|
|
83
|
+
if (Array.isArray(v)) return v;
|
|
84
|
+
if (t === 'object' && (v.constructor === Object || v.constructor === null)) return v;
|
|
85
|
+
return '[' + t + ' ' + v.constructor.name + ']';
|
|
86
|
+
}, space);
|
|
87
|
+
}
|
package/noxt-server.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { fileURLToPath } from 'url';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
import MLM from 'mlm-core';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
|
|
8
|
+
export async function startServer({ config, recipe }) {
|
|
9
|
+
|
|
10
|
+
const mlmInstance = MLM({
|
|
11
|
+
import:
|
|
12
|
+
p => import(p),
|
|
13
|
+
resolveModule:
|
|
14
|
+
name => name.startsWith('app/')
|
|
15
|
+
? `${process.cwd()}/units/${name.slice(4)}.js`
|
|
16
|
+
: `${__dirname}/units/${name}.js`
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
const report = await mlmInstance.analyze(recipe ?? 'noxt-dev');
|
|
20
|
+
if (!report.success) {
|
|
21
|
+
console.log(report.order.join(', '));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
await mlmInstance.install(recipe ?? 'noxt-dev');
|
|
26
|
+
await mlmInstance.start(config);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.log(await mlmInstance.analyze(recipe ?? 'noxt-dev'));
|
|
29
|
+
console.error(e);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "noxt-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Server for noxt-js-middleware with CLI and config support",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "
|
|
6
|
+
"main": "noxt-server.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"noxt-server": "./bin/noxt-server"
|
|
9
9
|
},
|
|
@@ -22,21 +22,18 @@
|
|
|
22
22
|
],
|
|
23
23
|
"author": "Zoran Obradović [https://github.com/zocky]",
|
|
24
24
|
"license": "GPL-3.0-or-later",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"noxt-js-middleware": "^1.0.5",
|
|
27
|
+
"mlm-core": "^1.0.5",
|
|
27
28
|
"express": "^5.0.0",
|
|
29
|
+
"cookie-parser": "^1.4.6",
|
|
28
30
|
"js-yaml": "^4.1.0",
|
|
29
31
|
"minimist": "^1.2.8",
|
|
30
|
-
"
|
|
31
|
-
"noxt-js-middleware": ">=1.0.3"
|
|
32
|
+
"node-fetch-cache": "^5.0.2"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"js-yaml": "^4.1.0",
|
|
37
|
-
"minimist": "^1.2.8",
|
|
38
|
-
"morgan": "^1.10.0",
|
|
39
|
-
"noxt-js-middleware": "^1.0.3"
|
|
35
|
+
"noxt-js-middleware": "../noxt-js-middleware",
|
|
36
|
+
"mlm-core": "../mlm-core"
|
|
40
37
|
},
|
|
41
38
|
"engines": {
|
|
42
39
|
"node": ">=18"
|
package/units/config.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export const info = {
|
|
2
|
+
name: 'config',
|
|
3
|
+
version: '1.0.0',
|
|
4
|
+
description: 'Config ',
|
|
5
|
+
requires: ['utils','services'],
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const config_is = {};
|
|
9
|
+
const config_defaults = {};
|
|
10
|
+
const config_defs = {}
|
|
11
|
+
|
|
12
|
+
function merge_deep(target, ...sources) {
|
|
13
|
+
if (!sources.length) return target;
|
|
14
|
+
|
|
15
|
+
const source = sources.shift();
|
|
16
|
+
|
|
17
|
+
if (is_plain(target) && is_plain(source)) {
|
|
18
|
+
for (const key in source) {
|
|
19
|
+
if (source.hasOwnProperty(key)) {
|
|
20
|
+
// If target doesn't have this key, simply assign
|
|
21
|
+
if (!target.hasOwnProperty(key)) {
|
|
22
|
+
target[key] = source[key];
|
|
23
|
+
}
|
|
24
|
+
// If both target and source have this key and both are plain objects, recurse
|
|
25
|
+
else if (is_plain(target[key]) && is_plain(source[key])) {
|
|
26
|
+
merge_deep(target[key], source[key]);
|
|
27
|
+
}
|
|
28
|
+
// If target has a non-plain object value, ignore (don't overwrite or try to merge into it)
|
|
29
|
+
// else: do nothing (keep target's existing value)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return merge_deep(target, ...sources);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function is_plain(obj) {
|
|
38
|
+
return obj !== null &&
|
|
39
|
+
typeof obj === 'object' &&
|
|
40
|
+
(obj.constructor === Object || obj.constructor === null);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default mlm => ({
|
|
44
|
+
'define.config': () => ({}),
|
|
45
|
+
'register.config': (defs, unit) => {
|
|
46
|
+
for (const key in defs) {
|
|
47
|
+
const def = defs[key];
|
|
48
|
+
mlm.assert.not(key in config_defs, 'Duplicate config key ' + key);
|
|
49
|
+
mlm.assert.is({
|
|
50
|
+
is: 'any|none',
|
|
51
|
+
default: 'any|none'
|
|
52
|
+
}, def, `config.${key}`);
|
|
53
|
+
|
|
54
|
+
if ('default' in def) {
|
|
55
|
+
config_defaults[key] = mlm.utils.eval(def.default);
|
|
56
|
+
}
|
|
57
|
+
config_defs[key] = {
|
|
58
|
+
is: def.is,
|
|
59
|
+
default: def.default,
|
|
60
|
+
unit: unit
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
'utils.merge_deep': merge_deep,
|
|
65
|
+
'services.config': () => new class ConfigService {
|
|
66
|
+
process(config) {
|
|
67
|
+
const merged = merge_deep({}, config_defaults, config);
|
|
68
|
+
const ret = {};
|
|
69
|
+
for (const key in config_defs) {
|
|
70
|
+
const { is: type } = config_defs[key];
|
|
71
|
+
if (key in merged) {
|
|
72
|
+
mlm.assert.is(type, merged[key], `config.${key}`);
|
|
73
|
+
ret[key] = merged[key];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return ret;
|
|
77
|
+
}
|
|
78
|
+
get_defs() {
|
|
79
|
+
return {
|
|
80
|
+
config_defs,
|
|
81
|
+
config_is,
|
|
82
|
+
config_defaults
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
onStart(config) {
|
|
87
|
+
Object.assign(mlm.config, mlm.services.config.process(config));
|
|
88
|
+
}
|
|
89
|
+
})
|
package/units/env.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const info = {
|
|
2
|
+
name: 'env',
|
|
3
|
+
version: '1.0.0',
|
|
4
|
+
description: 'Environment',
|
|
5
|
+
}
|
|
6
|
+
/*
|
|
7
|
+
export default mlm => ({
|
|
8
|
+
'define.DEV': () => process.env.NODE_ENV !== 'production',
|
|
9
|
+
'define.PROD': () => process.env.NODE_ENV === 'production',
|
|
10
|
+
'onBeforeLoad': () => {
|
|
11
|
+
//process.env.NODE_ENV ||= 'development'
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
*/
|
package/units/express.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const info = {
|
|
2
|
+
name: 'express',
|
|
3
|
+
description: 'Sets up an Express server',
|
|
4
|
+
requires: ['plugin'],
|
|
5
|
+
npm: {
|
|
6
|
+
'express': '^5.0.0',
|
|
7
|
+
'cookie-parser': '^1.4.6'
|
|
8
|
+
},
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let app;
|
|
12
|
+
const middlewareNames = new Set();
|
|
13
|
+
const middlewares = [];
|
|
14
|
+
export default mlm => ({
|
|
15
|
+
'define.express': { get: () => app },
|
|
16
|
+
'register.middleware': (conf, unit) => {
|
|
17
|
+
for (const key in conf) {
|
|
18
|
+
mlm.assert.not(key in middlewareNames, 'Duplicate middleware ' + key);
|
|
19
|
+
middlewareNames.add(key);
|
|
20
|
+
let c = conf[key];
|
|
21
|
+
if (mlm.is.function(c)) {
|
|
22
|
+
c = { create: c };
|
|
23
|
+
}
|
|
24
|
+
mlm.assert.is({
|
|
25
|
+
path: 'string|none',
|
|
26
|
+
create: 'function'
|
|
27
|
+
}, c, 'middleware');
|
|
28
|
+
mlm.assert.is.function(c.create, 'middleware');
|
|
29
|
+
middlewares.push({ ...c, unit: unit.name });
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
'config.port': {
|
|
33
|
+
is: v => Number.isInteger(v) && v > 0 && v < 65536,
|
|
34
|
+
default: 3000
|
|
35
|
+
},
|
|
36
|
+
'config.host': {
|
|
37
|
+
is: 'string',
|
|
38
|
+
default: 'localhost'
|
|
39
|
+
},
|
|
40
|
+
'middleware.json': async (app) => {
|
|
41
|
+
const { default: express } = await mlm.import('express');
|
|
42
|
+
return express.json();
|
|
43
|
+
},
|
|
44
|
+
'middleware.urlencoded': async (app) => {
|
|
45
|
+
const { default: express } = await mlm.import('express');
|
|
46
|
+
return express.urlencoded({ extended: true });
|
|
47
|
+
},
|
|
48
|
+
async onStart() {
|
|
49
|
+
const { default: express } = await mlm.import('express');
|
|
50
|
+
app = express();
|
|
51
|
+
app.use((req, res, next) => {
|
|
52
|
+
mlm.log(req.method, req.url);
|
|
53
|
+
next();
|
|
54
|
+
})
|
|
55
|
+
for (const middleware of middlewares) {
|
|
56
|
+
const mw = await middleware.create(app);
|
|
57
|
+
if (middleware.path) {
|
|
58
|
+
app.use(middleware.path, mw);
|
|
59
|
+
} else {
|
|
60
|
+
app.use(mw);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
app.listen(mlm.config.port, mlm.config.host);
|
|
64
|
+
},
|
|
65
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const info = {
|
|
2
|
+
name: 'fetch-cache-fs',
|
|
3
|
+
version: '1.0.0',
|
|
4
|
+
description: 'Cached fetch with filesystem cache',
|
|
5
|
+
requires: ['noxt-plugin'],
|
|
6
|
+
provides: ['#fetch'],
|
|
7
|
+
npm: {
|
|
8
|
+
'node-fetch-cache': '^1.0.0',
|
|
9
|
+
},
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let fetchCache = null;
|
|
13
|
+
|
|
14
|
+
export default mlm => ({
|
|
15
|
+
'config.fetch': {
|
|
16
|
+
is: {
|
|
17
|
+
cacheDir: 'string',
|
|
18
|
+
ttl: 'integer|none',
|
|
19
|
+
enabled: 'boolean',
|
|
20
|
+
retry: 'positiveInteger',
|
|
21
|
+
timeout: 'positiveInteger'
|
|
22
|
+
},
|
|
23
|
+
default: {
|
|
24
|
+
enabled: true,
|
|
25
|
+
cacheDir: '.cache/fetch-cache',
|
|
26
|
+
ttl: 60 * 60 * 1000,
|
|
27
|
+
retry: 2,
|
|
28
|
+
timeout: 5000
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
'pageContext.fetch': ({ ctx }) => globalFetch.bind(null, ctx),
|
|
32
|
+
'serverContext.fetch': () => globalFetch,
|
|
33
|
+
'onStart': async () => {
|
|
34
|
+
const { default: nodeFetchCache, FileSystemCache } = await mlm.import('node-fetch-cache');
|
|
35
|
+
const cfg = mlm.config.fetch;
|
|
36
|
+
fetchCache = nodeFetchCache.create({
|
|
37
|
+
cache: cfg.enabled ? new FileSystemCache({ cacheDirectory: cfg.cacheDir }) : undefined,
|
|
38
|
+
ttl: cfg.ttl,
|
|
39
|
+
retry: cfg.retry,
|
|
40
|
+
timeout: cfg.timeout
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
async function globalFetch(ctx, url, options = {}) {
|
|
46
|
+
let res = await fetchCache(url, options);
|
|
47
|
+
if (ctx.req.headers.pragma === 'no-cache' && !res.isCacheMiss) {
|
|
48
|
+
console.log('reload', url);
|
|
49
|
+
res.ejectFromCache();
|
|
50
|
+
res = await fetch(url, options);
|
|
51
|
+
}
|
|
52
|
+
if (!res.ok) throw new Error(res.statusText);
|
|
53
|
+
return res;
|
|
54
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const info = {
|
|
2
|
+
name: 'fetch-cache',
|
|
3
|
+
version: '1.0.0',
|
|
4
|
+
requires: ['noxt-plugin'],
|
|
5
|
+
provides: ['#fetch'],
|
|
6
|
+
description: 'Cached fetch',
|
|
7
|
+
npm: {
|
|
8
|
+
'node-fetch-cache': '^1.0.0',
|
|
9
|
+
},
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let fetchCache = null;
|
|
13
|
+
|
|
14
|
+
export default mlm => ({
|
|
15
|
+
'config.fetch': {
|
|
16
|
+
is: {
|
|
17
|
+
ttl: 'integer|none',
|
|
18
|
+
retry: 'positiveInteger|none',
|
|
19
|
+
timeout: 'positiveInteger|none'
|
|
20
|
+
},
|
|
21
|
+
default: {
|
|
22
|
+
ttl: 60 * 60 * 1000,
|
|
23
|
+
retry: 2,
|
|
24
|
+
timeout: 5000
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
'pageContext.fetch': (props, ctx) => globalFetch.bind(null, ctx),
|
|
29
|
+
'serverContext.fetch': () => globalFetch,
|
|
30
|
+
|
|
31
|
+
async onStart() {
|
|
32
|
+
const { default: nodeFetchCache } = await mlm.import('node-fetch-cache');
|
|
33
|
+
const cfg = mlm.config.fetch;
|
|
34
|
+
fetchCache = await nodeFetchCache.create({
|
|
35
|
+
cache: undefined,
|
|
36
|
+
ttl: cfg.ttl,
|
|
37
|
+
retry: cfg.retry,
|
|
38
|
+
timeout: cfg.timeout
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
async function globalFetch(ctx, url, options = {}) {
|
|
44
|
+
if (ctx.req.headers.pragma === 'no-cache') options.cache = 'no-cache';
|
|
45
|
+
const res = await fetchCache(url, options);
|
|
46
|
+
if (!res.ok) throw new Error(res.statusText);
|
|
47
|
+
return res;
|
|
48
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const info = {
|
|
2
|
+
name: 'fetch-node',
|
|
3
|
+
version: '1.0.0',
|
|
4
|
+
description: 'Native fetch',
|
|
5
|
+
requires: ['noxt-plugin'],
|
|
6
|
+
provides: ['#fetch'],
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default mlm => ({
|
|
10
|
+
name: 'fetch-node',
|
|
11
|
+
requires: ['noxt-plugin'],
|
|
12
|
+
provides: ['#fetch'],
|
|
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
|
+
'serverContext.fetch': () => fetchOrThrow,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
async function fetchOrThrow(ctx, url, options = {}) {
|
|
31
|
+
const res = await fetch(url, options);
|
|
32
|
+
if (!res.ok) throw new Error(res.statusText);
|
|
33
|
+
return res;
|
|
34
|
+
}
|
package/units/fetch.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const info = {
|
|
2
|
+
version: '1.0.0',
|
|
3
|
+
description: 'Preload props from urls',
|
|
4
|
+
requires: ['noxt-plugin','#fetch'],
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default mlm => ({
|
|
8
|
+
'componentExports.fetch': async ({exported, props, ctx}) => {
|
|
9
|
+
for (const id in exported) {
|
|
10
|
+
let url = await mlm.utils.eval(exported[id], props, ctx);
|
|
11
|
+
mlm.log('fetching', id, url);
|
|
12
|
+
try {
|
|
13
|
+
const now = performance.now();
|
|
14
|
+
const res = await ctx.fetch(url);
|
|
15
|
+
mlm.log('fetched', id, url, (performance.now() - now).toFixed(3) +'ms');
|
|
16
|
+
props[id] = await res.json();
|
|
17
|
+
//mlm.log('fetched', url, JSON.stringify(props[id]).length);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
20
|
+
mlm.throw(new Error('error fetching ' + url + ': ' + e));
|
|
21
|
+
}
|
|
22
|
+
mlm.error('fetch', url, e);
|
|
23
|
+
props[id] = null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
})
|
package/units/hooks.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const info = {
|
|
2
|
+
name: 'hooks',
|
|
3
|
+
version: '1.0.0',
|
|
4
|
+
description: 'Hooks',
|
|
5
|
+
requires: ['utils','services']
|
|
6
|
+
}
|
|
7
|
+
const hooks = {}
|
|
8
|
+
const hook_handlers = {}
|
|
9
|
+
|
|
10
|
+
export default mlm => {
|
|
11
|
+
return ({
|
|
12
|
+
'define.hooks': () => mlm.utils.readOnly(hooks, 'hooks'),
|
|
13
|
+
'register.hooks': mlm.utils.collector(hooks, {
|
|
14
|
+
is: 'function',
|
|
15
|
+
mode: 'object',
|
|
16
|
+
map: (fn, key) => (...args) => {
|
|
17
|
+
for (const handler of hook_handlers[key] ?? []) {
|
|
18
|
+
fn(handler, ...args);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}),
|
|
22
|
+
'register.on': mlm.utils.collector(hook_handlers, {
|
|
23
|
+
is: 'function',
|
|
24
|
+
mode: 'array'
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
}
|
package/units/logger.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const info = {
|
|
2
|
+
name: 'logger',
|
|
3
|
+
version: '1.0.0',
|
|
4
|
+
description: 'Minimal logger',
|
|
5
|
+
requires: ['express'],
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default mlm => ({
|
|
9
|
+
'middleware.logger': async (app) => {
|
|
10
|
+
return (req, res, next) => {
|
|
11
|
+
console.log(req.method, req.url);
|
|
12
|
+
next();
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
})
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export const info = {
|
|
2
|
+
name: 'noxt-plugin',
|
|
3
|
+
description: 'Noxt Plugin',
|
|
4
|
+
requires: ['express','#noxt-router'],
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const pageContextHooks = {};
|
|
8
|
+
const serverContextHooks = {};
|
|
9
|
+
const componentExportsHooks = {};
|
|
10
|
+
const registerPageHooks = {};
|
|
11
|
+
const registerComponentHooks = {};
|
|
12
|
+
|
|
13
|
+
export default mlm => {
|
|
14
|
+
|
|
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
|
+
const globalServerContext = {};
|
|
53
|
+
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
|
|
62
|
+
},
|
|
63
|
+
'serverContext.utils': () => mlm.utils,
|
|
64
|
+
'serverContext.DEV': () => mlm.DEV,
|
|
65
|
+
'serverContext.PROD': () => mlm.PROD,
|
|
66
|
+
onStart: async () => {
|
|
67
|
+
mlm.services.noxt.noxt_context({ ctx: globalServerContext });
|
|
68
|
+
//mlm.log('ctx', mlm.serverContext);
|
|
69
|
+
},
|
|
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);
|
|
79
|
+
},
|
|
80
|
+
arrayRunner(pageContextHooks,
|
|
81
|
+
async (key, fn, args) => {
|
|
82
|
+
args.ctx[key] = await fn(args)
|
|
83
|
+
}
|
|
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
|
+
}
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
report() {
|
|
108
|
+
return {
|
|
109
|
+
pageContext: pageContextHooks,
|
|
110
|
+
serverContext: serverContextHooks,
|
|
111
|
+
componentExports: componentExportsHooks,
|
|
112
|
+
registerPage: registerPageHooks,
|
|
113
|
+
registerComponent: registerComponentHooks
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
'registerPage.Link'({ component, module }) {
|
|
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 } };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const info = {
|
|
2
|
+
name: 'noxt-router-dev',
|
|
3
|
+
description: 'Sets up a Noxt Router',
|
|
4
|
+
requires: ['plugin'],
|
|
5
|
+
provides: ['#noxt-router'],
|
|
6
|
+
npm: {
|
|
7
|
+
'noxt-js-middleware': '^1.0.4'
|
|
8
|
+
},
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default mlm => ({
|
|
12
|
+
'config.views': {
|
|
13
|
+
is: ['string'],
|
|
14
|
+
default: ['views']
|
|
15
|
+
},
|
|
16
|
+
'middleware.noxt': async () => {
|
|
17
|
+
const { default: noxt } = await mlm.import('noxt-js-middleware');
|
|
18
|
+
const noxtRouter = await noxt({
|
|
19
|
+
context: mlm.noxt_context,
|
|
20
|
+
views: mlm.config.views,
|
|
21
|
+
hooks: mlm.services.noxt.noxt_hooks
|
|
22
|
+
})
|
|
23
|
+
const devRouter = await noxt({
|
|
24
|
+
context: mlm.noxt_context,
|
|
25
|
+
views: mlm.config.views,
|
|
26
|
+
hooks: mlm.services.noxt.noxt_hooks,
|
|
27
|
+
noxt: noxtRouter
|
|
28
|
+
})
|
|
29
|
+
noxtRouter.use('/dev', devRouter)
|
|
30
|
+
return noxtRouter
|
|
31
|
+
}
|
|
32
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const info = {
|
|
2
|
+
name: 'noxt-router',
|
|
3
|
+
description: 'Sets up a Noxt Router',
|
|
4
|
+
requires: ['plugin'],
|
|
5
|
+
provides: ['#noxt-router'],
|
|
6
|
+
npm: {
|
|
7
|
+
'noxt-js-middleware': '^1.0.4'
|
|
8
|
+
},
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default mlm => ({
|
|
12
|
+
'config.views': {
|
|
13
|
+
is: ['string'],
|
|
14
|
+
default: ['views']
|
|
15
|
+
},
|
|
16
|
+
'middleware.noxt': async (app) => {
|
|
17
|
+
const { default: noxt } = await mlm.import('noxt-js-middleware');
|
|
18
|
+
const noxtRouter = await noxt({
|
|
19
|
+
context: mlm.noxt_context,
|
|
20
|
+
views: mlm.config.views,
|
|
21
|
+
hooks: mlm.services.noxt.noxt_hooks
|
|
22
|
+
})
|
|
23
|
+
return noxtRouter
|
|
24
|
+
},
|
|
25
|
+
})
|
package/units/noxt.js
ADDED
package/units/plugin.js
ADDED
package/units/reload.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export const info = {
|
|
2
|
+
name: 'reload',
|
|
3
|
+
description: 'Hot reload middleware',
|
|
4
|
+
requires: ['noxt-plugin'],
|
|
5
|
+
npm: {
|
|
6
|
+
'express': '^5.0.0',
|
|
7
|
+
},
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const clients = new Set();
|
|
11
|
+
export default mlm => ({
|
|
12
|
+
'pageContext.reload': ({ ctx }) => ctx.slot('script', 'noxt-reload', '/_reload.js'),
|
|
13
|
+
'middleware.reload': async () => {
|
|
14
|
+
const { default: express } = await mlm.import('express');
|
|
15
|
+
const router = express.Router();
|
|
16
|
+
router.get('/_reload.js', (req, res) => {
|
|
17
|
+
res.set('Content-Type', 'text/javascript');
|
|
18
|
+
res.send(reloadJs);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
router.get('/_events', (req, res) => {
|
|
22
|
+
res.writeHead(200, {
|
|
23
|
+
'Content-Type': 'text/event-stream',
|
|
24
|
+
'Cache-Control': 'no-cache',
|
|
25
|
+
'Connection': 'keep-alive',
|
|
26
|
+
'content-encoding': 'none'
|
|
27
|
+
});
|
|
28
|
+
res.write('\nevent:connected\ndata:\n\n');
|
|
29
|
+
clients.add(res);
|
|
30
|
+
res.on('close', () => {
|
|
31
|
+
clients.delete(res);
|
|
32
|
+
res.end();
|
|
33
|
+
})
|
|
34
|
+
req.on('error', () => clients.delete(res));
|
|
35
|
+
res.on('error', () => clients.delete(res));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return router
|
|
39
|
+
},
|
|
40
|
+
onStart() {
|
|
41
|
+
process.on('SIGUSR2', handleSignal)
|
|
42
|
+
},
|
|
43
|
+
onStop() {
|
|
44
|
+
process.removeListener('SIGUSR2', handleSignal)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
async function handleSignal() {
|
|
49
|
+
const promises = Array.from(clients).map(async client => {
|
|
50
|
+
client.write('event: reload\ndata:\n\n');
|
|
51
|
+
return new Promise(resolve => client.end(resolve));
|
|
52
|
+
});
|
|
53
|
+
await Promise.all(promises);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const reloadJs = `
|
|
58
|
+
const eventSource = new EventSource('/_events');
|
|
59
|
+
let reload = false;
|
|
60
|
+
eventSource.addEventListener('connected', () => {
|
|
61
|
+
if (reload) {
|
|
62
|
+
console.log('%c NOXT ','color: #ffd; background-color: #080', 'Reloading...' );
|
|
63
|
+
window.location.reload();
|
|
64
|
+
} else {
|
|
65
|
+
console.log('%c NOXT ', 'color: #ffd; background-color: #080', 'Connected.' );
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
});
|
|
69
|
+
eventSource.addEventListener('reload', () => {
|
|
70
|
+
console.log('%c NOXT ','color: #ffd; background-color: #080', 'Restarting...' );
|
|
71
|
+
reload = true;
|
|
72
|
+
});
|
|
73
|
+
window.addEventListener('beforeunload', () => {
|
|
74
|
+
eventSource.close();
|
|
75
|
+
});
|
|
76
|
+
`
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const info = {
|
|
2
|
+
name: 'services',
|
|
3
|
+
version: '1.0.0',
|
|
4
|
+
description: 'Services',
|
|
5
|
+
requires: ['utils']
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const services = {}
|
|
9
|
+
|
|
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
|
|
19
|
+
}
|
|
20
|
+
}),
|
|
21
|
+
})
|
package/units/static.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const info = {
|
|
2
|
+
name: 'static',
|
|
3
|
+
description: 'Static middleware',
|
|
4
|
+
requires: ['express'],
|
|
5
|
+
npm: {
|
|
6
|
+
'serve-static': '^1.15.0'
|
|
7
|
+
},
|
|
8
|
+
}
|
|
9
|
+
|
|
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
|
+
})
|
package/units/utils.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export const info = {
|
|
2
|
+
name: 'utils',
|
|
3
|
+
version: '1.0.0',
|
|
4
|
+
description: 'Utils',
|
|
5
|
+
//requires: ['env']
|
|
6
|
+
}
|
|
7
|
+
export default mlm => {
|
|
8
|
+
|
|
9
|
+
const utils = {};
|
|
10
|
+
utils.eval = (fn,...args) => typeof fn === 'function' ? fn(...args) : fn;
|
|
11
|
+
utils.readOnly = (obj, { label = 'object' } = {}) => new Proxy(obj, {
|
|
12
|
+
get: (t, k) => t[k],
|
|
13
|
+
set: (t, k, v) => { mlm.throw(label + ' is read-only'); }
|
|
14
|
+
});
|
|
15
|
+
utils.collector = (target, {
|
|
16
|
+
map, is, filter,
|
|
17
|
+
label = 'object',
|
|
18
|
+
mode = 'object',
|
|
19
|
+
override = false
|
|
20
|
+
} = {}) => {
|
|
21
|
+
const modes = {
|
|
22
|
+
array: () => source => {
|
|
23
|
+
for (let value of source) {
|
|
24
|
+
if (filter && !filter(value)) continue;
|
|
25
|
+
if (is) mlm.assert.is(is, value, label);
|
|
26
|
+
if (map) value = map(value);
|
|
27
|
+
target.push(value);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
object: () => source => {
|
|
31
|
+
for (const key in source) {
|
|
32
|
+
if (!override && key in target) {
|
|
33
|
+
mlm.throw('Duplicate key' + key + ' in ' + label);
|
|
34
|
+
}
|
|
35
|
+
let value = source[key];
|
|
36
|
+
if (filter && !filter(value, key)) continue;
|
|
37
|
+
if (is) mlm.assert.is( is,value, label + '.' + key);
|
|
38
|
+
if (map) value = map(value, key);
|
|
39
|
+
target[key] = value;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
arrays: () => source => {
|
|
43
|
+
for (const key in source) {
|
|
44
|
+
let values = [source[key]].flat(Infinity);
|
|
45
|
+
target[key] ??= [];
|
|
46
|
+
for (let value of values) {
|
|
47
|
+
if (filter && !filter(value, key)) continue;
|
|
48
|
+
if (is) mlm.assert.is(is, value, label + '.' + key);
|
|
49
|
+
if (map) value = map(value, key);
|
|
50
|
+
target[key].push(value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
directory: () => source => {
|
|
55
|
+
for (const key in source) {
|
|
56
|
+
let values = source[key];
|
|
57
|
+
target[key] ??= {};
|
|
58
|
+
for (const id in values) {
|
|
59
|
+
if (!override && id in target[key]) {
|
|
60
|
+
mlm.throw('Duplicate key' + id + ' in ' + label + '.' + key);
|
|
61
|
+
}
|
|
62
|
+
let value = values[id];
|
|
63
|
+
if (filter && !filter(value, key)) continue;
|
|
64
|
+
if (is) mlm.assert.is(is, value, label + '.' + key);
|
|
65
|
+
if (map) value = map(value, key);
|
|
66
|
+
target[key][id] = value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
mlm.assert(mode in modes, 'Unknown mode ' + mode);
|
|
72
|
+
return modes[mode]();
|
|
73
|
+
}
|
|
74
|
+
return ({
|
|
75
|
+
'define.utils': () => utils.readOnly(utils, 'utils'),
|
|
76
|
+
'register.utils': utils.collector(utils, { is: 'function', mode: 'object' }),
|
|
77
|
+
})
|
|
78
|
+
}
|
package/config.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
export const handlers = {
|
|
5
|
-
port: (val) => Number.isInteger(val) && val > 0 && val < 65536 ? + val : new Error('Port must be an integer between 1 and 65535'),
|
|
6
|
-
host: (val) => typeof val === 'string' && val.length > 0 ? val : new Error('Host must be a non-empty string'),
|
|
7
|
-
views: (val) => {
|
|
8
|
-
val = [].concat(val); // ensure array
|
|
9
|
-
for (const dir of val) {
|
|
10
|
-
if (typeof dir !== 'string' || dir.length === 0 || !fs.existsSync(path.resolve(process.cwd(), dir))) {
|
|
11
|
-
return new Error(`Each views directory must be a valid directory path. Invalid: ${dir}`);
|
|
12
|
-
}
|
|
13
|
-
return val;
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
static: (val) => {
|
|
17
|
-
if (!val) return val; // allow empty
|
|
18
|
-
val = [].concat(val); // ensure array
|
|
19
|
-
for (const dir of val) {
|
|
20
|
-
if (typeof dir !== 'string' || dir.length === 0 || !fs.existsSync(path.resolve(process.cwd(), dir))) {
|
|
21
|
-
return new Error(`Each static directory must be a valid directory path. Invalid: ${dir}`);
|
|
22
|
-
}
|
|
23
|
-
return val;
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
logLevel: (val) => ['error', 'warn', 'info', 'debug'].includes(val) ? val : new Error('Log level must be one of: error, warn, info, debug'),
|
|
27
|
-
ssl: (val) => {
|
|
28
|
-
// either boolean false, or object with cert and key and any other options for https.createServer
|
|
29
|
-
if (val === false) return val;
|
|
30
|
-
if (typeof val === 'object' && val !== null && typeof val.cert === 'string' && val.cert.length > 0 && typeof val.key === 'string' && val.key.length > 0) {
|
|
31
|
-
// check if files exist
|
|
32
|
-
if (!fs.existsSync(path.resolve(process.cwd(), val.cert))) return new Error('SSL certificate file does not exist');
|
|
33
|
-
if (!fs.existsSync(path.resolve(process.cwd(), val.key))) return new Error('SSL key file does not exist');
|
|
34
|
-
return val;
|
|
35
|
-
}
|
|
36
|
-
return new Error('SSL must be a boolean false, or an object with cert and key');
|
|
37
|
-
},
|
|
38
|
-
logFile: (val) => (!val || typeof val === 'string' && val.length > 0) ? val : new Error('Log file must be a non-empty string'),
|
|
39
|
-
context: async (val) => {
|
|
40
|
-
// import a list of modules, and return an object with their exports
|
|
41
|
-
if (!val) return []; // allow empty
|
|
42
|
-
val = [].concat(val); // ensure array
|
|
43
|
-
for (const mod of val) {
|
|
44
|
-
if (typeof mod !== 'string' || mod.length === 0 || !fs.existsSync(path.resolve(process.cwd(), mod))) {
|
|
45
|
-
return new Error(`Each context module must be a valid module path. Invalid: ${mod}`);
|
|
46
|
-
}
|
|
47
|
-
// import module
|
|
48
|
-
}
|
|
49
|
-
return val;
|
|
50
|
-
},
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Register a handler for a config option
|
|
54
|
-
* The handler is called with the config value, and should return the processed value
|
|
55
|
-
* or an Error object if the value is invalid
|
|
56
|
-
*
|
|
57
|
-
* @param {string} key
|
|
58
|
-
* @param {Function} handler
|
|
59
|
-
*/
|
|
60
|
-
export function registerConfigHandler(key, handler = val=>val) {
|
|
61
|
-
console.log(`Registering config handler for ${key}`);
|
|
62
|
-
handlers[key] = handler;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function processConfig(config, warn) {
|
|
66
|
-
const ret = {};
|
|
67
|
-
for (const key in config) {
|
|
68
|
-
if (handlers[key]) {
|
|
69
|
-
const result = await handlers[key](config[key]);
|
|
70
|
-
if (result instanceof Error) {
|
|
71
|
-
console.error(`Invalid config option "${key}": ${result.message}`);
|
|
72
|
-
process.exit(1);
|
|
73
|
-
} else {
|
|
74
|
-
if (result !== undefined) config[key] = result;
|
|
75
|
-
}
|
|
76
|
-
} else if (warn) {
|
|
77
|
-
console.warn(`Unknown config option: ${key}`);
|
|
78
|
-
}
|
|
79
|
-
ret[key] = config[key];
|
|
80
|
-
}
|
|
81
|
-
return config;
|
|
82
|
-
}
|
package/index.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import express, { json, urlencoded } from 'express';
|
|
2
|
-
import serve_static from 'serve-static';
|
|
3
|
-
import { resolve } from 'path';
|
|
4
|
-
import cookieParser from 'cookie-parser';
|
|
5
|
-
import logger from 'morgan';
|
|
6
|
-
import noxt from 'noxt-js-middleware';
|
|
7
|
-
import { readFileSync } from 'fs';
|
|
8
|
-
import { createServer } from 'https';
|
|
9
|
-
import reloadServer from './reload_server.js';
|
|
10
|
-
import { processConfig } from './config.js';
|
|
11
|
-
export { registerConfigHandler } from './config.js';
|
|
12
|
-
export { registerPageExportHandler } from 'noxt-js-middleware';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Starts a Noxt server based on a fully resolved config object.
|
|
16
|
-
* @param {Object} config - Configuration object
|
|
17
|
-
* @param {number} config.port - Port to listen on
|
|
18
|
-
* @param {string|string[]} config.views - Path(s) to JSX components
|
|
19
|
-
* @param {Object} config.context - Context object to inject into components
|
|
20
|
-
* @param {string} [config.staticDir] - Optional static files directory
|
|
21
|
-
* @param {Object|boolean} [config.ssl] - false or { cert, key, ca }
|
|
22
|
-
* @param {string} [config.layout] - Default layout component name
|
|
23
|
-
* @returns {Promise<{app: express.Application, server: import('http').Server|import('https').Server}>}
|
|
24
|
-
*/
|
|
25
|
-
export async function startServer (config) {
|
|
26
|
-
// Load context
|
|
27
|
-
const context = {};
|
|
28
|
-
config = await processConfig(config, false);
|
|
29
|
-
for (const mod of [].concat(config.context).flat(Infinity).filter(Boolean)) {
|
|
30
|
-
console.log(`Importing context module: ${mod}`);
|
|
31
|
-
Object.assign(context, await import(resolve(mod)));
|
|
32
|
-
}
|
|
33
|
-
config = context.config = await processConfig(config,true);
|
|
34
|
-
console.log('Final config:', config);
|
|
35
|
-
await context.init?.(context);
|
|
36
|
-
|
|
37
|
-
const app = express();
|
|
38
|
-
|
|
39
|
-
// Basic middleware
|
|
40
|
-
app.use(logger('dev'));
|
|
41
|
-
app.use(json());
|
|
42
|
-
app.use(urlencoded({ extended: false }));
|
|
43
|
-
app.use(cookieParser());
|
|
44
|
-
|
|
45
|
-
// Reload server for development
|
|
46
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
47
|
-
app.use(reloadServer);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Serve static files if provided
|
|
51
|
-
for (const dir of [].concat(config.static).flat(Infinity).filter(Boolean)) {
|
|
52
|
-
app.use(serve_static(resolve(dir)));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// Mount the Noxt router
|
|
58
|
-
const router = await noxt({
|
|
59
|
-
directory: config.views,
|
|
60
|
-
context: context,
|
|
61
|
-
layout: config.layout,
|
|
62
|
-
});
|
|
63
|
-
app.use(router);
|
|
64
|
-
|
|
65
|
-
// Default error handler
|
|
66
|
-
app.use((err, req, res, next) => {
|
|
67
|
-
res.status(err.status || 500);
|
|
68
|
-
res.send(err.message || 'Internal Server Error');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
let server;
|
|
72
|
-
if (config.ssl && config.ssl !== false) {
|
|
73
|
-
const sslOptions = {
|
|
74
|
-
key: readFileSync(resolve(config.ssl.key)),
|
|
75
|
-
cert: readFileSync(resolve(config.ssl.cert)),
|
|
76
|
-
};
|
|
77
|
-
if (config.ssl.ca) sslOptions.ca = readFileSync(resolve(config.ssl.ca));
|
|
78
|
-
server = createServer(sslOptions, app);
|
|
79
|
-
server.listen(config.port, () => {
|
|
80
|
-
console.log(`Noxt HTTPS server running on https://localhost:${config.port}`);
|
|
81
|
-
});
|
|
82
|
-
} else {
|
|
83
|
-
server = app.listen(config.port, () => {
|
|
84
|
-
console.log(`Noxt HTTP server running on http://localhost:${config.port}`);
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return { app, server };
|
|
89
|
-
}
|
package/reload_client.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
const eventSource = new EventSource('/_events');
|
|
2
|
-
let reload = false;
|
|
3
|
-
eventSource.addEventListener('connected', () => {
|
|
4
|
-
if (reload) {
|
|
5
|
-
console.log('%c Reloading... ','color: #ffd; background-color: #080');
|
|
6
|
-
window.location.reload();
|
|
7
|
-
} else {
|
|
8
|
-
console.log('%c Connected ', 'color: #ffd; background-color: #080' );
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
});
|
|
12
|
-
eventSource.addEventListener('reload', () => {
|
|
13
|
-
console.log('%c Restarting... ','color: #ffd; background-color: #080');
|
|
14
|
-
reload = true;
|
|
15
|
-
});
|
package/reload_server.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
5
|
-
|
|
6
|
-
const router = express.Router();
|
|
7
|
-
export default router;
|
|
8
|
-
|
|
9
|
-
const clients = new Set();
|
|
10
|
-
router.get('/_events', async (req, res) => {
|
|
11
|
-
res.writeHead(200, {
|
|
12
|
-
'Content-Type': 'text/event-stream',
|
|
13
|
-
'Cache-Control': 'no-cache',
|
|
14
|
-
'Connection': 'keep-alive',
|
|
15
|
-
'content-encoding': 'none'
|
|
16
|
-
});
|
|
17
|
-
res.write('\nevent:connected\ndata:\n\n');
|
|
18
|
-
clients.add(res);
|
|
19
|
-
console.log('client connected');
|
|
20
|
-
res.on('close', () => {
|
|
21
|
-
console.log('client disconnected');
|
|
22
|
-
clients.delete(res);
|
|
23
|
-
res.end();
|
|
24
|
-
})
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const reloadJs = fs.readFileSync(path.join(__dirname, 'reload_client.js'), 'utf8');
|
|
28
|
-
router.get('/_reload.js', async (req, res) => {
|
|
29
|
-
res.set('Content-Type', 'text/javascript');
|
|
30
|
-
res.send(reloadJs);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
process.on('SIGUSR2', async () => {
|
|
35
|
-
console.log('beforeExit');
|
|
36
|
-
const promises = Array.from(clients).map(async client => {
|
|
37
|
-
console.log('restarting client')
|
|
38
|
-
client.write('event: reload\ndata:\n\n');
|
|
39
|
-
return new Promise(resolve => client.end(resolve));
|
|
40
|
-
});
|
|
41
|
-
await Promise.all(promises);
|
|
42
|
-
process.exit(0);
|
|
43
|
-
});
|