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 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
- const __dirname = path.dirname(new URL(import.meta.url).pathname);
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);
@@ -2,6 +2,7 @@ port: 3000
2
2
  ssl: false
3
3
  host: localhost
4
4
  logLevel: info
5
+ recipe: app/main
5
6
  views: []
6
7
  static: []
7
8
  context: []
@@ -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.4",
3
+ "version": "0.1.6",
4
4
  "description": "Server for noxt-js-middleware with CLI and config support",
5
5
  "type": "module",
6
- "main": "index.js",
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
- "peerDependencies": {
26
- "cookie-parser": "^1.4.6",
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
- "morgan": "^1.10.0",
31
- "noxt-js-middleware": ">=1.0.3"
32
+ "node-fetch-cache": "^5.0.2"
32
33
  },
33
34
  "devDependencies": {
34
- "cookie-parser": "^1.4.6",
35
- "express": "^5.0.0",
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"
@@ -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
+ */
@@ -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
+ }
@@ -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,13 @@
1
+ export const info = {
2
+ name: 'noxt-dev',
3
+ description: 'Noxt Dev server',
4
+ requires: [
5
+ 'express',
6
+ 'logger',
7
+ 'static',
8
+ 'noxt-router-dev',
9
+ 'reload',
10
+ 'fetch-cache-fs',
11
+ 'fetch',
12
+ ],
13
+ }
@@ -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
@@ -0,0 +1,13 @@
1
+ export const info = {
2
+ name: 'noxt',
3
+ version: '1.0.0',
4
+ description: 'Noxt server',
5
+ requires: [
6
+ 'express',
7
+ 'logger',
8
+ 'static',
9
+ 'noxt-router',
10
+ 'fetch-cache-fs',
11
+ 'fetch',
12
+ ],
13
+ }
@@ -0,0 +1,6 @@
1
+ export const info = {
2
+ name: 'plugin',
3
+ version: '1.0.0',
4
+ description: 'Base',
5
+ requires: ['utils','services','hooks','config']
6
+ }
@@ -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
+ })
@@ -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
- });