@nodegx/preview 0.2.4 → 0.2.5
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 +5 -1
- package/bin/nodegx-deploy.js +3 -1
- package/bin/noodl-preview.js +3 -2
- package/dist/backend/LICENSE +21 -0
- package/dist/backend/cli.js +1448 -803
- package/dist/backend/folder/start.cmd +4 -0
- package/dist/backend/folder/start.js +378 -0
- package/dist/backend/folder/start.sh +4 -0
- package/dist/backend/vm/provision-app.sh +194 -0
- package/dist/browser/noodl-graph-target.js +95 -95
- package/dist/browser/noodl-preview-in-page.js +15 -15
- package/dist/external/deploy/index.html +13 -1
- package/dist/external/deploy/index.json +2 -1
- package/dist/external/deploy/noodl.deploy.js +1 -1
- package/dist/external/deploy/serve.js +339 -0
- package/dist/nodegx-deploy.cjs +46140 -5955
- package/dist/nodegx-deploy.inputs.json +590 -401
- package/dist/noodl-preview.cjs +2008 -1590
- package/dist/noodl-preview.inputs.json +405 -401
- package/dist/package.json +1 -1
- package/package.json +2 -3
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Serves a NodeGX deploy folder on this computer, on the first free port.
|
|
4
|
+
*
|
|
5
|
+
* node serve.js first free port from 3000 up
|
|
6
|
+
* node serve.js --port 8080 exactly 8080, or stop and say it is taken
|
|
7
|
+
* PORT=8080 node serve.js the same, for hosts that set PORT
|
|
8
|
+
*
|
|
9
|
+
* Copied into every client-side deploy beside index.html (see DEPLOY.md there). No dependencies:
|
|
10
|
+
* a person with Node and nothing else can open their app.
|
|
11
|
+
*
|
|
12
|
+
* 🔴 **Two consumers.** This file also supplies `listenOnFreePort` to the SSR server, which
|
|
13
|
+
* requires it as `./public/serve.js` — the SSR layout always holds a full client-side deploy in
|
|
14
|
+
* `public/`. One implementation, so the CSR folder and the SSR server cannot disagree about what
|
|
15
|
+
* "free" means. `require.main === module` keeps the static server from starting inside the SSR
|
|
16
|
+
* bundle.
|
|
17
|
+
*
|
|
18
|
+
* 🔴 **"Free" is checked on loopback first, not only by listening.** On macOS a server listening on
|
|
19
|
+
* every interface (`*:3000`) starts happily beside another program's `127.0.0.1:3000` — and the
|
|
20
|
+
* browser, opening `localhost:3000`, reaches the OTHER program. A port counts as free only when
|
|
21
|
+
* 127.0.0.1 and ::1 can both be bound AND the real listen succeeds.
|
|
22
|
+
*/
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const http = require('http');
|
|
27
|
+
const net = require('net');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
const DEFAULT_PORT = 3000;
|
|
31
|
+
const PORTS_TO_TRY = 50;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The port a person asked for, and where they asked: `--port N`, `--port=N`, `-p N`, or `PORT`.
|
|
35
|
+
* `null` when they asked for nothing. A flag wins over the environment.
|
|
36
|
+
*/
|
|
37
|
+
function readPortArg(argv, env) {
|
|
38
|
+
for (let i = 0; i < argv.length; i++) {
|
|
39
|
+
const arg = argv[i];
|
|
40
|
+
if (arg === '--port' || arg === '-p') return parsePort(argv[i + 1], arg);
|
|
41
|
+
if (arg.startsWith('--port=')) return parsePort(arg.slice('--port='.length), '--port');
|
|
42
|
+
}
|
|
43
|
+
if (env.PORT !== undefined && env.PORT !== '') return parsePort(env.PORT, 'PORT');
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parsePort(value, from) {
|
|
48
|
+
const port = Number(value);
|
|
49
|
+
if (value === undefined || !/^\d+$/.test(String(value)) || port > 65535) {
|
|
50
|
+
throw new Error(`${from} wants a port number between 1 and 65535, and got "${value ?? ''}".`);
|
|
51
|
+
}
|
|
52
|
+
return { port, from };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whether `host:port` can be bound right now. A host this machine does not have counts as free. */
|
|
56
|
+
function canBind(port, host) {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
const probe = net.createServer();
|
|
59
|
+
probe.once('error', (error) => {
|
|
60
|
+
resolve(error.code === 'EADDRNOTAVAIL' || error.code === 'EAFNOSUPPORT');
|
|
61
|
+
});
|
|
62
|
+
probe.once('listening', () => probe.close(() => resolve(true)));
|
|
63
|
+
probe.listen({ port, host, exclusive: true });
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function loopbackFree(port) {
|
|
68
|
+
return (await canBind(port, '127.0.0.1')) && (await canBind(port, '::1'));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function listenOn(server, port) {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const onError = (error) => {
|
|
74
|
+
server.off('listening', onListening);
|
|
75
|
+
reject(error);
|
|
76
|
+
};
|
|
77
|
+
const onListening = () => {
|
|
78
|
+
server.off('error', onError);
|
|
79
|
+
resolve(server.address().port);
|
|
80
|
+
};
|
|
81
|
+
server.once('error', onError);
|
|
82
|
+
server.once('listening', onListening);
|
|
83
|
+
server.listen(port);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function takenMessage(asked) {
|
|
88
|
+
const how = asked.from === 'PORT' ? `PORT=${asked.port}` : `${asked.from} ${asked.port}`;
|
|
89
|
+
return (
|
|
90
|
+
`Port ${asked.port} is already in use by another program (you asked for it with ${how}).\n` +
|
|
91
|
+
`Leave the port out to use the first free one from ${DEFAULT_PORT}, or choose another, e.g. --port ${asked.port + 1}.`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Starts `server` listening and resolves with the port it got.
|
|
97
|
+
*
|
|
98
|
+
* - Asked for a port: that port or an error naming it. Never a different one — a host that set
|
|
99
|
+
* `PORT` routes traffic to exactly that port, and a person who typed `--port 8080` will open 8080.
|
|
100
|
+
* - Asked for nothing: the first free port from 3000, then whatever the OS hands out.
|
|
101
|
+
*/
|
|
102
|
+
async function listenOnFreePort(
|
|
103
|
+
server,
|
|
104
|
+
{ argv = process.argv.slice(2), env = process.env, start = DEFAULT_PORT, attempts = PORTS_TO_TRY } = {}
|
|
105
|
+
) {
|
|
106
|
+
const asked = readPortArg(argv, env);
|
|
107
|
+
|
|
108
|
+
if (asked) {
|
|
109
|
+
if (asked.port !== 0 && !(await loopbackFree(asked.port))) throw new Error(takenMessage(asked));
|
|
110
|
+
try {
|
|
111
|
+
return await listenOn(server, asked.port);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error.code === 'EADDRINUSE') throw new Error(takenMessage(asked));
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (let port = start; port < start + attempts && port <= 65535; port++) {
|
|
119
|
+
if (!(await loopbackFree(port))) continue;
|
|
120
|
+
try {
|
|
121
|
+
return await listenOn(server, port);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error.code !== 'EADDRINUSE') throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return listenOn(server, 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const DEPLOY_CONFIG_FILE = 'nodegx-config.json';
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* DEP-001 — the backend a person asked for when starting the app, or `null`:
|
|
133
|
+
*
|
|
134
|
+
* --backend <url> the backend's address
|
|
135
|
+
* --backend-port <n> a backend on this computer: http://localhost:<n>
|
|
136
|
+
* NODEGX_CLOUDSERVICES_ENDPOINT / NODEGX_CLOUDSERVICES_APP_ID the same, for hosts
|
|
137
|
+
*
|
|
138
|
+
* A flag wins over the environment. The page reads the result from `nodegx-config.json`, and the
|
|
139
|
+
* runtime's allow-list (viewer-react `src/deploy-config.ts`) decides what it may change.
|
|
140
|
+
*/
|
|
141
|
+
function readBackendArg(argv, env) {
|
|
142
|
+
let endpoint;
|
|
143
|
+
let from;
|
|
144
|
+
for (let i = 0; i < argv.length; i++) {
|
|
145
|
+
const arg = argv[i];
|
|
146
|
+
const [flag, inline] = arg.startsWith('--') && arg.includes('=') ? [arg.slice(0, arg.indexOf('=')), arg.slice(arg.indexOf('=') + 1)] : [arg, undefined];
|
|
147
|
+
const value = inline !== undefined ? inline : argv[i + 1];
|
|
148
|
+
if (flag === '--backend') {
|
|
149
|
+
endpoint = parseBackendUrl(value, '--backend');
|
|
150
|
+
from = '--backend';
|
|
151
|
+
} else if (flag === '--backend-port') {
|
|
152
|
+
endpoint = `http://localhost:${parsePort(value, '--backend-port').port}`;
|
|
153
|
+
from = '--backend-port';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!endpoint && env.NODEGX_CLOUDSERVICES_ENDPOINT) {
|
|
157
|
+
endpoint = parseBackendUrl(env.NODEGX_CLOUDSERVICES_ENDPOINT, 'NODEGX_CLOUDSERVICES_ENDPOINT');
|
|
158
|
+
from = 'NODEGX_CLOUDSERVICES_ENDPOINT';
|
|
159
|
+
}
|
|
160
|
+
const appId = env.NODEGX_CLOUDSERVICES_APP_ID || undefined;
|
|
161
|
+
if (!endpoint && !appId) return null;
|
|
162
|
+
return { endpoint, appId, from: from || 'NODEGX_CLOUDSERVICES_APP_ID' };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseBackendUrl(value, from) {
|
|
166
|
+
let url;
|
|
167
|
+
try {
|
|
168
|
+
url = new URL(String(value));
|
|
169
|
+
} catch {
|
|
170
|
+
url = null;
|
|
171
|
+
}
|
|
172
|
+
if (!url || (url.protocol !== 'http:' && url.protocol !== 'https:')) {
|
|
173
|
+
throw new Error(`${from} wants the backend's address, like http://localhost:8577, and got "${value ?? ''}".`);
|
|
174
|
+
}
|
|
175
|
+
return String(value).replace(/\/+$/, '');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The config this folder's app should run with: `nodegx-config.json` in `dir` (what the deploy
|
|
180
|
+
* baked), with `override` written over its `cloudservices`. `changed` says whether the override
|
|
181
|
+
* did anything — a server only needs to answer the file itself when it did.
|
|
182
|
+
*/
|
|
183
|
+
function resolveDeployConfig(dir, override) {
|
|
184
|
+
let config = {};
|
|
185
|
+
try {
|
|
186
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(dir, DEPLOY_CONFIG_FILE), 'utf8'));
|
|
187
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) config = parsed;
|
|
188
|
+
} catch {
|
|
189
|
+
// No file, or not JSON: the deploy's baked address stands, and the runtime says so if it matters.
|
|
190
|
+
}
|
|
191
|
+
if (!override) return { config, changed: false };
|
|
192
|
+
|
|
193
|
+
const cloudservices = Object.assign({}, config.cloudservices);
|
|
194
|
+
if (override.endpoint) cloudservices.endpoint = override.endpoint;
|
|
195
|
+
if (override.appId) cloudservices.appId = override.appId;
|
|
196
|
+
return { config: Object.assign({}, config, { cloudservices }), changed: true };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const TYPES = {
|
|
200
|
+
'.html': 'text/html; charset=utf-8',
|
|
201
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
202
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
203
|
+
'.css': 'text/css; charset=utf-8',
|
|
204
|
+
'.json': 'application/json; charset=utf-8',
|
|
205
|
+
'.md': 'text/markdown; charset=utf-8',
|
|
206
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
207
|
+
'.xml': 'application/xml; charset=utf-8',
|
|
208
|
+
'.svg': 'image/svg+xml',
|
|
209
|
+
'.png': 'image/png',
|
|
210
|
+
'.jpg': 'image/jpeg',
|
|
211
|
+
'.jpeg': 'image/jpeg',
|
|
212
|
+
'.gif': 'image/gif',
|
|
213
|
+
'.webp': 'image/webp',
|
|
214
|
+
'.avif': 'image/avif',
|
|
215
|
+
'.ico': 'image/x-icon',
|
|
216
|
+
'.woff': 'font/woff',
|
|
217
|
+
'.woff2': 'font/woff2',
|
|
218
|
+
'.ttf': 'font/ttf',
|
|
219
|
+
'.otf': 'font/otf',
|
|
220
|
+
'.mp3': 'audio/mpeg',
|
|
221
|
+
'.wav': 'audio/wav',
|
|
222
|
+
'.ogg': 'audio/ogg',
|
|
223
|
+
'.mp4': 'video/mp4',
|
|
224
|
+
'.webm': 'video/webm',
|
|
225
|
+
'.pdf': 'application/pdf',
|
|
226
|
+
'.wasm': 'application/wasm'
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A file under `root`, else `index.html` for an app route (a path with no extension — the router
|
|
231
|
+
* uses real paths, so reloading `/about` must still load the app), else 404.
|
|
232
|
+
*/
|
|
233
|
+
function createStaticHandler(root, { deployConfig } = {}) {
|
|
234
|
+
const indexHtml = path.join(root, 'index.html');
|
|
235
|
+
const configBody = deployConfig ? JSON.stringify(deployConfig, null, 2) + '\n' : null;
|
|
236
|
+
|
|
237
|
+
function send(res, file, method) {
|
|
238
|
+
fs.stat(file, (error, stat) => {
|
|
239
|
+
if (error || !stat.isFile()) {
|
|
240
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
241
|
+
res.end('Not found');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
res.writeHead(200, {
|
|
245
|
+
'Content-Type': TYPES[path.extname(file).toLowerCase()] || 'application/octet-stream',
|
|
246
|
+
'Content-Length': stat.size,
|
|
247
|
+
'Cache-Control': 'no-cache'
|
|
248
|
+
});
|
|
249
|
+
if (method === 'HEAD') res.end();
|
|
250
|
+
else fs.createReadStream(file).pipe(res);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return function handle(req, res) {
|
|
255
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
256
|
+
res.writeHead(405, { Allow: 'GET, HEAD' });
|
|
257
|
+
res.end();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let pathname;
|
|
262
|
+
try {
|
|
263
|
+
pathname = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
|
|
264
|
+
} catch {
|
|
265
|
+
res.writeHead(400);
|
|
266
|
+
res.end('Bad request');
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// P26-SERVENUL: `fs.stat` THROWS on a NUL byte rather than calling back, and the throw stopped the process.
|
|
270
|
+
if (pathname.indexOf('\0') !== -1) {
|
|
271
|
+
res.writeHead(400);
|
|
272
|
+
res.end('Bad request');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// DEP-001: started with --backend, the address the page reads is the one asked for, not the file's.
|
|
277
|
+
if (configBody !== null && pathname === '/' + DEPLOY_CONFIG_FILE) {
|
|
278
|
+
res.writeHead(200, { 'Content-Type': TYPES['.json'], 'Cache-Control': 'no-store' });
|
|
279
|
+
res.end(req.method === 'HEAD' ? undefined : configBody);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const file = path.join(root, pathname);
|
|
284
|
+
const outside = file !== root && !file.startsWith(root + path.sep);
|
|
285
|
+
const hidden = pathname.split('/').some((part) => part.startsWith('.'));
|
|
286
|
+
if (outside || hidden) {
|
|
287
|
+
res.writeHead(404);
|
|
288
|
+
res.end('Not found');
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
fs.stat(file, (error, stat) => {
|
|
293
|
+
if (!error && stat.isFile()) return send(res, file, req.method);
|
|
294
|
+
if (!error && stat.isDirectory()) return send(res, path.join(file, 'index.html'), req.method);
|
|
295
|
+
if (!path.extname(pathname)) return send(res, indexHtml, req.method);
|
|
296
|
+
send(res, file, req.method);
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function main() {
|
|
302
|
+
const argv = process.argv.slice(2);
|
|
303
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
304
|
+
console.log(
|
|
305
|
+
'Serves this NodeGX deploy on this computer.\n\n' +
|
|
306
|
+
' node serve.js first free port from 3000 up\n' +
|
|
307
|
+
' node serve.js --port 8080 exactly 8080, or stop if it is taken\n' +
|
|
308
|
+
' node serve.js --backend-port 8577 use the backend on this computer at port 8577\n' +
|
|
309
|
+
' node serve.js --backend <url> use the backend at that address\n'
|
|
310
|
+
);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const root = __dirname;
|
|
315
|
+
let backend;
|
|
316
|
+
try {
|
|
317
|
+
backend = readBackendArg(argv, process.env);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.error(error.message);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
const resolved = resolveDeployConfig(root, backend);
|
|
323
|
+
const server = http.createServer(createStaticHandler(root, { deployConfig: resolved.changed ? resolved.config : null }));
|
|
324
|
+
listenOnFreePort(server)
|
|
325
|
+
.then((port) => {
|
|
326
|
+
const endpoint = resolved.config.cloudservices && resolved.config.cloudservices.endpoint;
|
|
327
|
+
console.log(`Serving ${root}`);
|
|
328
|
+
if (endpoint) console.log(`Backend: ${endpoint} (from ${backend ? backend.from : DEPLOY_CONFIG_FILE})`);
|
|
329
|
+
console.log(`Open http://localhost:${port}/ (Ctrl+C to stop)`);
|
|
330
|
+
})
|
|
331
|
+
.catch((error) => {
|
|
332
|
+
console.error(error.message);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
module.exports = { listenOnFreePort, readPortArg, readBackendArg, resolveDeployConfig, createStaticHandler, DEPLOY_CONFIG_FILE };
|
|
338
|
+
|
|
339
|
+
if (require.main === module) main();
|