@nebula.js/cli-serve 8.0.0-alpha.2 → 8.0.0-alpha.3
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 +23 -12
- package/dist/eRender.js +1 -1
- package/dist/eRender.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/q.js +2 -2
- package/lib/https.js +40 -0
- package/lib/init-config.js +3 -7
- package/lib/rsbuild.serve.js +23 -48
- package/lib/serve.js +40 -61
- package/package.json +9 -8
package/lib/https.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
|
|
5
|
+
export const getCertificatePaths = (home = homedir()) => ({
|
|
6
|
+
keyPath: path.join(home, '.certs/key.pem'),
|
|
7
|
+
certPath: path.join(home, '.certs/cert.pem'),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Serve over https when asked for and over http when asked not to. When it is not asked for either way, https is used
|
|
12
|
+
* if the certificates are installed, as they are on a development machine, and http is used if they are not, as on a CI
|
|
13
|
+
* runner.
|
|
14
|
+
* @param {object} options
|
|
15
|
+
* @param {boolean} [options.https] `--https` or `--no-https`
|
|
16
|
+
* @returns {{ https: boolean, reason: string, keyPath: string, certPath: string }} `reason` tells why it is served as it is
|
|
17
|
+
*/
|
|
18
|
+
export const resolveHttps = ({ https, paths = getCertificatePaths(), exists = fs.existsSync } = {}) => {
|
|
19
|
+
const { keyPath, certPath } = paths;
|
|
20
|
+
const installed = exists(keyPath) && exists(certPath);
|
|
21
|
+
|
|
22
|
+
if (https === true && !installed) {
|
|
23
|
+
const missing = !exists(keyPath) ? `key cert at path ${keyPath}` : `cert at path ${certPath}`;
|
|
24
|
+
throw new Error(`Failed to start using HTTPS. Missing ${missing}, install certificates or use --no-https`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const dir = path.dirname(keyPath);
|
|
28
|
+
let reason;
|
|
29
|
+
if (https === true) {
|
|
30
|
+
reason = 'asked for with the https option';
|
|
31
|
+
} else if (https === false) {
|
|
32
|
+
reason = 'turned off with the https option, as with --no-https';
|
|
33
|
+
} else if (installed) {
|
|
34
|
+
reason = `certificates found in ${dir}`;
|
|
35
|
+
} else {
|
|
36
|
+
reason = `no certificates in ${dir}, install them to serve over https`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { https: https ?? installed, reason, keyPath, certPath };
|
|
40
|
+
};
|
package/lib/init-config.js
CHANGED
|
@@ -34,6 +34,7 @@ const options = {
|
|
|
34
34
|
default: 'localhost',
|
|
35
35
|
},
|
|
36
36
|
port: {
|
|
37
|
+
alias: 'p',
|
|
37
38
|
type: 'number',
|
|
38
39
|
description: 'Specify a port number to listen for requests on',
|
|
39
40
|
},
|
|
@@ -82,11 +83,6 @@ const options = {
|
|
|
82
83
|
description: 'Path to a folder that will be used as basis when locating fixtures',
|
|
83
84
|
default: 'test/component',
|
|
84
85
|
},
|
|
85
|
-
mfe: {
|
|
86
|
-
type: 'boolean',
|
|
87
|
-
description: 'Serves bundle to use in micro frontend',
|
|
88
|
-
default: false,
|
|
89
|
-
},
|
|
90
86
|
open: {
|
|
91
87
|
type: 'boolean',
|
|
92
88
|
description: 'Opens browser on serving nebula',
|
|
@@ -94,8 +90,8 @@ const options = {
|
|
|
94
90
|
},
|
|
95
91
|
https: {
|
|
96
92
|
type: 'boolean',
|
|
97
|
-
description:
|
|
98
|
-
|
|
93
|
+
description:
|
|
94
|
+
'Uses https protocol, requires certificates. Without --https or --no-https, https is used when the certificates are installed',
|
|
99
95
|
},
|
|
100
96
|
};
|
|
101
97
|
|
package/lib/rsbuild.serve.js
CHANGED
|
@@ -3,7 +3,6 @@ import path from 'path';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import http from 'http';
|
|
5
5
|
import https from 'https';
|
|
6
|
-
import { homedir } from 'os';
|
|
7
6
|
import { fileURLToPath } from 'url';
|
|
8
7
|
import chalk from 'chalk';
|
|
9
8
|
import express from 'express';
|
|
@@ -11,12 +10,9 @@ import { createRsbuild } from '@rsbuild/core';
|
|
|
11
10
|
|
|
12
11
|
import snapshooterFn from './snapshot-server.js';
|
|
13
12
|
import snapshotRouter from './snapshot-router.js';
|
|
13
|
+
import { resolveHttps } from './https.js';
|
|
14
14
|
|
|
15
15
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
const homeDir = homedir();
|
|
17
|
-
|
|
18
|
-
const httpsKeyPath = path.join(homeDir, '.certs/key.pem');
|
|
19
|
-
const httpsCertPath = path.join(homeDir, '.certs/cert.pem');
|
|
20
16
|
|
|
21
17
|
const HEADERS = {
|
|
22
18
|
'Access-Control-Allow-Credentials': 'true',
|
|
@@ -71,23 +67,15 @@ export default async ({
|
|
|
71
67
|
webIntegrationId,
|
|
72
68
|
snName,
|
|
73
69
|
snUrl,
|
|
70
|
+
snModule,
|
|
74
71
|
pages = publishedPages,
|
|
75
72
|
open = true,
|
|
76
73
|
entryWatcher,
|
|
77
74
|
serveConfig,
|
|
78
75
|
}) => {
|
|
79
|
-
const HTTPS =
|
|
76
|
+
const { https: HTTPS, reason: httpsReason, keyPath, certPath } = resolveHttps({ https: serveConfig.https });
|
|
80
77
|
const url = `${HTTPS ? 'https' : 'http'}://${host}:${port}`;
|
|
81
78
|
|
|
82
|
-
if (HTTPS) {
|
|
83
|
-
if (!fs.existsSync(httpsKeyPath)) {
|
|
84
|
-
throw new Error(`Failed to start using HTTPS. Missing key cert at path ${httpsKeyPath}`);
|
|
85
|
-
}
|
|
86
|
-
if (!fs.existsSync(httpsCertPath)) {
|
|
87
|
-
throw new Error(`Failed to start using HTTPS. Missing cert at path ${httpsCertPath}`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
79
|
const snapshotRoute = '/njs';
|
|
92
80
|
|
|
93
81
|
const snapshooter = snapshooterFn({ snapshotUrl: `${url}/eRender.html` });
|
|
@@ -156,6 +144,7 @@ export default async ({
|
|
|
156
144
|
supernova: {
|
|
157
145
|
name: snName,
|
|
158
146
|
url: snUrl,
|
|
147
|
+
module: snModule,
|
|
159
148
|
},
|
|
160
149
|
sock: {
|
|
161
150
|
port: entryWatcher && entryWatcher.port,
|
|
@@ -189,38 +178,36 @@ export default async ({
|
|
|
189
178
|
port,
|
|
190
179
|
allowedHosts: disableHostCheck ? true : undefined,
|
|
191
180
|
headers: HEADERS,
|
|
192
|
-
open: Boolean(open
|
|
193
|
-
|
|
194
|
-
publicDir: serveConfig.mfe ? false : { name: contentBase, copyOnBuild: false },
|
|
181
|
+
open: Boolean(open),
|
|
182
|
+
publicDir: { name: contentBase, copyOnBuild: false },
|
|
195
183
|
proxy: {
|
|
196
184
|
'/render': {
|
|
197
185
|
target: `${url}/eRender.html`,
|
|
198
186
|
ignorePath: true,
|
|
187
|
+
// the server calls itself, and the local certificate is not one that node trusts
|
|
188
|
+
secure: false,
|
|
199
189
|
},
|
|
200
190
|
},
|
|
201
191
|
},
|
|
202
192
|
},
|
|
203
193
|
});
|
|
204
194
|
|
|
205
|
-
|
|
206
|
-
const devServer = await rsbuild.createDevServer({ runCompile: !serveConfig.mfe, getPortSilently: true });
|
|
195
|
+
const devServer = await rsbuild.createDevServer({ getPortSilently: true });
|
|
207
196
|
app.use(devServer.middlewares);
|
|
208
197
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
});
|
|
220
|
-
}
|
|
198
|
+
// The hub is served for all paths that ask for a page: @qlik/api detects the authorization code of the OAuth2
|
|
199
|
+
// callback at /auth/login/callback in the URL, to complete the token exchange in the page
|
|
200
|
+
app.use((req, res, next) => {
|
|
201
|
+
if (req.method === 'GET' && !path.extname(req.path) && req.accepts('html')) {
|
|
202
|
+
// the folder is the root, a path with a segment that starts with a dot, such as .pnpm, is not sent otherwise
|
|
203
|
+
res.sendFile('index.html', { root: contentBase });
|
|
204
|
+
} else {
|
|
205
|
+
next();
|
|
206
|
+
}
|
|
207
|
+
});
|
|
221
208
|
|
|
222
209
|
const httpServer = HTTPS
|
|
223
|
-
? https.createServer({ key: fs.readFileSync(
|
|
210
|
+
? https.createServer({ key: fs.readFileSync(keyPath), cert: fs.readFileSync(certPath) }, app)
|
|
224
211
|
: http.createServer(app);
|
|
225
212
|
|
|
226
213
|
const close = async () => {
|
|
@@ -235,15 +222,8 @@ export default async ({
|
|
|
235
222
|
const started = () => {
|
|
236
223
|
console.log(styledBanner());
|
|
237
224
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
console.log('Development server running in MFE mode');
|
|
241
|
-
console.log(`Bundle served at ${chalk.green(bundleUrl)}`);
|
|
242
|
-
console.log('');
|
|
243
|
-
console.log('Use the bundle when overriding the import map');
|
|
244
|
-
} else {
|
|
245
|
-
console.log(`Development server running at ${chalk.green(url)}`);
|
|
246
|
-
}
|
|
225
|
+
console.log(`Development server running at ${chalk.green(url)}`);
|
|
226
|
+
console.log(`Protocol: ${chalk.green(HTTPS ? 'https' : 'http')}, ${httpsReason}`);
|
|
247
227
|
|
|
248
228
|
return { context: '', url, close };
|
|
249
229
|
};
|
|
@@ -265,17 +245,12 @@ export default async ({
|
|
|
265
245
|
}
|
|
266
246
|
};
|
|
267
247
|
|
|
268
|
-
|
|
269
|
-
rsbuild.onDevCompileDone(({ stats }) => start(stats));
|
|
270
|
-
}
|
|
248
|
+
rsbuild.onDevCompileDone(({ stats }) => start(stats));
|
|
271
249
|
|
|
272
250
|
httpServer.on('error', reject);
|
|
273
251
|
httpServer.listen(port, host, async () => {
|
|
274
252
|
devServer.connectWebSocket({ server: httpServer });
|
|
275
253
|
await devServer.afterListen();
|
|
276
|
-
if (serveConfig.mfe) {
|
|
277
|
-
start();
|
|
278
|
-
}
|
|
279
254
|
});
|
|
280
255
|
});
|
|
281
256
|
};
|
package/lib/serve.js
CHANGED
|
@@ -7,6 +7,7 @@ import extend from 'extend';
|
|
|
7
7
|
import yargs from 'yargs';
|
|
8
8
|
import { WebSocketServer } from 'ws';
|
|
9
9
|
import chokidar from 'chokidar';
|
|
10
|
+
import express from 'express';
|
|
10
11
|
import build from '@nebula.js/cli-build';
|
|
11
12
|
|
|
12
13
|
import initConfig from './init-config.js';
|
|
@@ -15,15 +16,15 @@ import rsbuildServe from './rsbuild.serve.js';
|
|
|
15
16
|
|
|
16
17
|
const require = createRequire(import.meta.url);
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
// The files of nebula build are served by their file names, from the root, as the modules import their chunks and are
|
|
20
|
+
// imported by their source maps relative to their own urls.
|
|
21
|
+
export const getModuleUrl = (file) => `/${path.basename(file)}`;
|
|
22
|
+
|
|
23
|
+
// `snPath` is the es module of the visualization and `systemPath` the systemjs module of it
|
|
24
|
+
export const initiateWatch = async ({ snPath, snName, host, systemPath }) => {
|
|
19
25
|
if (!snPath) {
|
|
20
26
|
return null;
|
|
21
27
|
}
|
|
22
|
-
// TODO - timeout
|
|
23
|
-
let onInitiated;
|
|
24
|
-
const done = new Promise((resolve) => {
|
|
25
|
-
onInitiated = resolve;
|
|
26
|
-
});
|
|
27
28
|
|
|
28
29
|
const wsPort = await portfinder.getPortPromise({ port: 5001, host });
|
|
29
30
|
|
|
@@ -37,10 +38,6 @@ const initiateWatch = async ({ snPath, snName, host }) => {
|
|
|
37
38
|
disableGlobbing: true,
|
|
38
39
|
});
|
|
39
40
|
|
|
40
|
-
choker.on('add', () => {
|
|
41
|
-
onInitiated();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
41
|
choker.on('change', () => {
|
|
45
42
|
[...ws.clients].forEach((client) => {
|
|
46
43
|
client.send(
|
|
@@ -51,46 +48,11 @@ const initiateWatch = async ({ snPath, snName, host }) => {
|
|
|
51
48
|
});
|
|
52
49
|
});
|
|
53
50
|
|
|
54
|
-
const cache = {};
|
|
55
|
-
|
|
56
51
|
return {
|
|
57
52
|
addRoutes(app, options) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
let file;
|
|
63
|
-
if (cache[name]) {
|
|
64
|
-
file = cache[name];
|
|
65
|
-
} else if (name === snName) {
|
|
66
|
-
file = snPath;
|
|
67
|
-
} else if (/\.map$/.test(name)) {
|
|
68
|
-
const sources = Object.keys(cache);
|
|
69
|
-
for (let i = 0; i < sources.length; i++) {
|
|
70
|
-
const p = cache[sources[i]];
|
|
71
|
-
const filename = path.basename(p);
|
|
72
|
-
if (`${filename}.map` === name) {
|
|
73
|
-
file = path.resolve(path.dirname(p), name);
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
} else {
|
|
78
|
-
file = require.resolve(name, {
|
|
79
|
-
paths: [process.cwd()],
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
cache[name] = file;
|
|
84
|
-
|
|
85
|
-
if (fs.existsSync(file)) {
|
|
86
|
-
const f = fs.readFileSync(file);
|
|
87
|
-
res.set(options.headers);
|
|
88
|
-
// a module script is only run when the type of it is javascript
|
|
89
|
-
res.type(/\.map$/.test(file) ? 'application/json' : 'text/javascript');
|
|
90
|
-
res.send(f);
|
|
91
|
-
} else {
|
|
92
|
-
res.sendStatus('404');
|
|
93
|
-
}
|
|
53
|
+
// the directories that nebula build writes to, which are not served by their names
|
|
54
|
+
[...new Set([snPath, systemPath].filter(Boolean).map((file) => path.dirname(file)))].forEach((dir) => {
|
|
55
|
+
app.use(express.static(dir, { index: false, setHeaders: (res) => res.set(options.headers) }));
|
|
94
56
|
});
|
|
95
57
|
},
|
|
96
58
|
async close() {
|
|
@@ -121,6 +83,8 @@ export default async (argv, { pages } = {}) => {
|
|
|
121
83
|
let snName;
|
|
122
84
|
let watcher;
|
|
123
85
|
let snUrl;
|
|
86
|
+
let systemPath;
|
|
87
|
+
const watchers = [];
|
|
124
88
|
if (/^https?/.test(serveConfig.entry)) {
|
|
125
89
|
snName = 'remote';
|
|
126
90
|
snUrl = serveConfig.entry;
|
|
@@ -139,25 +103,34 @@ export default async (argv, { pages } = {}) => {
|
|
|
139
103
|
}
|
|
140
104
|
|
|
141
105
|
if (runFromDirectory) {
|
|
106
|
+
let externalPkg = {};
|
|
107
|
+
try {
|
|
108
|
+
externalPkg = require(path.resolve(context, 'package.json'));
|
|
109
|
+
} catch (e) {
|
|
110
|
+
//
|
|
111
|
+
}
|
|
112
|
+
// the web app loads the es module, the micro frontend tooling loads the systemjs module, so both are served
|
|
113
|
+
const formats = ['esm', ...(externalPkg.systemjs ? ['systemjs'] : [])];
|
|
142
114
|
if (serveConfig.build !== false) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
config: serveConfig.config,
|
|
147
|
-
|
|
148
|
-
})
|
|
115
|
+
// the formats of one package are built one after the other, to keep the output of the builds apart
|
|
116
|
+
await formats.reduce(async (previous, format) => {
|
|
117
|
+
await previous;
|
|
118
|
+
watchers.push(await build({ watch: format, external: provided, config: serveConfig.config, cwd: context }));
|
|
119
|
+
}, Promise.resolve());
|
|
120
|
+
// build({ watch }) has no watcher when the build is not possible
|
|
121
|
+
watcher = { close: () => Promise.all(watchers.map((w) => w && w.close())) };
|
|
149
122
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const externalEntry = serveConfig.mfe ? externalPkg.systemjs : externalPkg.module;
|
|
123
|
+
const externalEntry = externalPkg.module;
|
|
124
|
+
if (externalEntry) {
|
|
153
125
|
snName = externalPkg.name;
|
|
154
126
|
snPath = path.resolve(context, externalEntry);
|
|
155
|
-
}
|
|
156
|
-
|
|
127
|
+
}
|
|
128
|
+
if (externalPkg.systemjs) {
|
|
129
|
+
systemPath = path.resolve(context, externalPkg.systemjs);
|
|
157
130
|
}
|
|
158
131
|
}
|
|
159
132
|
|
|
160
|
-
const ww = snUrl ? null : await initiateWatch({ snPath, snName: serveConfig.type || snName, host });
|
|
133
|
+
const ww = snUrl ? null : await initiateWatch({ snPath, snName: serveConfig.type || snName, host, systemPath });
|
|
161
134
|
|
|
162
135
|
const server = await rsbuildServe({
|
|
163
136
|
host,
|
|
@@ -168,12 +141,18 @@ export default async (argv, { pages } = {}) => {
|
|
|
168
141
|
webIntegrationId: serveConfig.webIntegrationId,
|
|
169
142
|
snName: serveConfig.type || snName,
|
|
170
143
|
snUrl,
|
|
144
|
+
snModule: ww ? getModuleUrl(snPath) : undefined,
|
|
171
145
|
pages,
|
|
172
|
-
open: serveConfig.open !== false
|
|
146
|
+
open: serveConfig.open !== false,
|
|
173
147
|
entryWatcher: ww,
|
|
174
148
|
serveConfig,
|
|
175
149
|
});
|
|
176
150
|
|
|
151
|
+
if (systemPath) {
|
|
152
|
+
console.log(`SystemJS module served at ${server.url}${getModuleUrl(systemPath)}`);
|
|
153
|
+
console.log(`ES module served at ${server.url}${getModuleUrl(snPath)}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
177
156
|
const close = async () => {
|
|
178
157
|
if (watcher) {
|
|
179
158
|
watcher.close();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebula.js/cli-serve",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"node": ">=24.0.0"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@nebula.js/cli-build": "8.0.0-alpha.
|
|
34
|
+
"@nebula.js/cli-build": "8.0.0-alpha.3",
|
|
35
35
|
"@qlik/api": "2.21.0",
|
|
36
36
|
"@rsbuild/core": "^2.3.1",
|
|
37
37
|
"body-parser": "2.4.0",
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@mui/material": "^7.3.11",
|
|
50
50
|
"@mui/system": "^7.3.11",
|
|
51
|
-
"@nebula.js/enigma-mocker": "8.0.0-alpha.
|
|
52
|
-
"@nebula.js/nucleus": "8.0.0-alpha.
|
|
53
|
-
"@nebula.js/snapshooter": "8.0.0-alpha.
|
|
54
|
-
"@nebula.js/stardust": "8.0.0-alpha.
|
|
55
|
-
"@nebula.js/ui": "8.0.0-alpha.
|
|
51
|
+
"@nebula.js/enigma-mocker": "8.0.0-alpha.3",
|
|
52
|
+
"@nebula.js/nucleus": "8.0.0-alpha.3",
|
|
53
|
+
"@nebula.js/snapshooter": "8.0.0-alpha.3",
|
|
54
|
+
"@nebula.js/stardust": "8.0.0-alpha.3",
|
|
55
|
+
"@nebula.js/ui": "8.0.0-alpha.3",
|
|
56
56
|
"@playwright/test": "^1.64.1",
|
|
57
57
|
"@qlik/browserslist-config": "3.1.0",
|
|
58
58
|
"@testing-library/dom": "^10.4.2",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"build": "cross-env NODE_ENV=production rsbuild build --config ./rsbuild.config.mjs",
|
|
74
74
|
"build:dev": "cross-env NODE_ENV=development rsbuild build --config ./rsbuild.config.mjs",
|
|
75
75
|
"lint": "eslint web",
|
|
76
|
-
"start": "node dev.js"
|
|
76
|
+
"start": "node dev.js",
|
|
77
|
+
"test": "node --test test/*.test.js"
|
|
77
78
|
}
|
|
78
79
|
}
|