noxt-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # noxt-js
2
+
3
+ A zero-config JSX web server powered by [Express](https://expressjs.com/) and [noxt-js-middleware](https://npmjs.com/package/noxt-js-middleware).
4
+ Run it with `npx noxt-js` and drop `.jsx` files into your `views/` folder — routes are created automatically.
5
+
6
+ No React required. No heavy framework. Just JSX + Express.
7
+
8
+ ---
9
+
10
+ ## Installation
11
+
12
+ ```sh
13
+ npm install noxt-js
14
+ ```
15
+
16
+ or run it directly without installing:
17
+
18
+ ```sh
19
+ npx noxt-js
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Quick Start
25
+
26
+ ```sh
27
+ npx noxt-js
28
+ ```
29
+
30
+ By default, this will:
31
+
32
+ - Serve pages from `./views/`
33
+ - Start an HTTP server on port `3000`
34
+ - Look for configuration in `noxt.config.yaml` (optional)
35
+
36
+ ---
37
+
38
+ ## Configuration
39
+
40
+ `noxt-js` reads options from `noxt.config.yaml` in your project root, or from CLI flags.
41
+ CLI flags override config file values.
42
+
43
+ Example `noxt.config.yaml`:
44
+
45
+ ```yaml
46
+ port: 4000
47
+ host: localhost
48
+ views: views
49
+ logLevel: info
50
+ ssl: false
51
+ ```
52
+
53
+ Run with CLI overrides:
54
+
55
+ ```sh
56
+ npx noxt-js --port 8080 --views src/pages
57
+ ```
58
+
59
+ ### Available options
60
+
61
+ - **`port`**: HTTP port number (default: `3000`)
62
+ - **`host`**: Hostname or IP (default: `0.0.0.0`)
63
+ - **`views`**: Directory containing `.jsx` files (default: `views`)
64
+ - **`logLevel`**: One of `error`, `warn`, `info`, `debug`
65
+ - **`ssl`**:
66
+ - `false` (disable SSL)
67
+ - or object with `cert` and `key` paths for HTTPS
68
+
69
+ ---
70
+
71
+ ## Context
72
+
73
+ You can provide shared helpers/utilities to all components via a `context.js` file (or any path you specify in config). For example:
74
+
75
+ ```js
76
+ // context.js
77
+ export async function fetchUser(id) {
78
+ return db.users.findById(id);
79
+ }
80
+ ```
81
+
82
+ Then in a page:
83
+
84
+ ```jsx
85
+ export const route = '/user/:id';
86
+
87
+ export default async function UserPage({ id }, { fetchUser }) {
88
+ const user = await fetchUser(id);
89
+ return <h1>{user.name}</h1>;
90
+ }
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Pages & Routing
96
+
97
+ - Any `.jsx` file in `views/` is loaded as a component.
98
+ - If it exports `route`, it becomes a page at that route.
99
+ - Props come from route params, query string, and optional `params` export.
100
+
101
+ Example:
102
+
103
+ ```jsx
104
+ export const route = '/hello/:name';
105
+
106
+ export default function HelloPage({ name }) {
107
+ return <h1>Hello, {name}!</h1>;
108
+ }
109
+ ```
110
+
111
+ ---
112
+
113
+
114
+ ---
115
+
116
+ ## License
117
+
118
+ LGPL-3.0-or-later
@@ -0,0 +1,65 @@
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
+
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
+ });
package/config.js ADDED
@@ -0,0 +1,83 @@
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
+ console.log(`Importing context module: ${mod}`);
45
+ if (typeof mod !== 'string' || mod.length === 0 || !fs.existsSync(path.resolve(process.cwd(), mod))) {
46
+ return new Error(`Each context module must be a valid module path. Invalid: ${mod}`);
47
+ }
48
+ // import module
49
+ }
50
+ return val;
51
+ },
52
+ }
53
+ /**
54
+ * Register a handler for a config option
55
+ * The handler is called with the config value, and should return the processed value
56
+ * or an Error object if the value is invalid
57
+ *
58
+ * @param {string} key
59
+ * @param {Function} handler
60
+ */
61
+ export function registerConfigHandler(key, handler = val=>val) {
62
+ console.log(`Registering config handler for ${key}`);
63
+ handlers[key] = handler;
64
+ }
65
+
66
+ export async function processConfig(config, warn) {
67
+ const ret = {};
68
+ for (const key in config) {
69
+ if (handlers[key]) {
70
+ const result = await handlers[key](config[key]);
71
+ if (result instanceof Error) {
72
+ console.error(`Invalid config option "${key}": ${result.message}`);
73
+ process.exit(1);
74
+ } else {
75
+ if (result !== undefined) config[key] = result;
76
+ }
77
+ } else if (warn) {
78
+ console.warn(`Unknown config option: ${key}`);
79
+ }
80
+ ret[key] = config[key];
81
+ }
82
+ return config;
83
+ }
@@ -0,0 +1,7 @@
1
+ port: 3000
2
+ ssl: false
3
+ host: localhost
4
+ logLevel: info
5
+ views: []
6
+ static: []
7
+ context: []
package/index.js ADDED
@@ -0,0 +1,88 @@
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
+ Object.assign(context, await import(resolve(mod)));
31
+ }
32
+ config = context.config = await processConfig(config,true);
33
+ console.log('Final config:', config);
34
+ await context.init?.(context);
35
+
36
+ const app = express();
37
+
38
+ // Basic middleware
39
+ app.use(logger('dev'));
40
+ app.use(json());
41
+ app.use(urlencoded({ extended: false }));
42
+ app.use(cookieParser());
43
+
44
+ // Reload server for development
45
+ if (process.env.NODE_ENV !== 'production') {
46
+ app.use(reloadServer);
47
+ }
48
+
49
+ // Serve static files if provided
50
+ for (const dir of [].concat(config.static).flat(Infinity).filter(Boolean)) {
51
+ app.use(serve_static(resolve(dir)));
52
+ }
53
+
54
+
55
+
56
+ // Mount the Noxt router
57
+ const router = await noxt({
58
+ directory: config.views,
59
+ context: context,
60
+ layout: config.layout,
61
+ });
62
+ app.use(router);
63
+
64
+ // Default error handler
65
+ app.use((err, req, res, next) => {
66
+ res.status(err.status || 500);
67
+ res.send(err.message || 'Internal Server Error');
68
+ });
69
+
70
+ let server;
71
+ if (config.ssl && config.ssl !== false) {
72
+ const sslOptions = {
73
+ key: readFileSync(resolve(config.ssl.key)),
74
+ cert: readFileSync(resolve(config.ssl.cert)),
75
+ };
76
+ if (config.ssl.ca) sslOptions.ca = readFileSync(resolve(config.ssl.ca));
77
+ server = createServer(sslOptions, app);
78
+ server.listen(config.port, () => {
79
+ console.log(`Noxt HTTPS server running on https://localhost:${config.port}`);
80
+ });
81
+ } else {
82
+ server = app.listen(config.port, () => {
83
+ console.log(`Noxt HTTP server running on http://localhost:${config.port}`);
84
+ });
85
+ }
86
+
87
+ return { app, server };
88
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "noxt-server",
3
+ "version": "0.1.0",
4
+ "description": "Server for noxt-js-middleware with CLI and config support",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "noxt-server": "./bin/noxt-server"
9
+ },
10
+ "repository": "https://github.com/zocky/noxt-js",
11
+ "scripts": {
12
+ "test": "echo \"No tests yet\" && exit 0"
13
+ },
14
+ "keywords": [
15
+ "express",
16
+ "jsx",
17
+ "noxt",
18
+ "server",
19
+ "ssr",
20
+ "async",
21
+ "cli"
22
+ ],
23
+ "author": "Zoran Obradović [https://github.com/zocky]",
24
+ "license": "GPL-3.0-or-later",
25
+ "peerDependencies": {
26
+ "cookie-parser": "^1.4.6",
27
+ "express": "^5.0.0",
28
+ "js-yaml": "^4.1.0",
29
+ "minimist": "^1.2.8",
30
+ "morgan": "^1.10.0",
31
+ "noxt-js-middleware": "^1.0.0"
32
+ },
33
+ "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": "file:../noxt-js-middleware"
40
+ },
41
+
42
+ "engines": {
43
+ "node": ">=18"
44
+ }
45
+ }
@@ -0,0 +1,15 @@
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
+ });
@@ -0,0 +1,43 @@
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
+ });