@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/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
+ };
@@ -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: 'Uses https protocol, requires certificates',
98
- default: false,
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
 
@@ -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 = serveConfig.mfe || serveConfig.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 && !serveConfig.mfe),
193
- // Disable the pages of nebula serve in MFE mode, it only serves the bundle
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
- // in MFE mode there is nothing to build, the bundle of the visualization is built by nebula build
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
- if (!serveConfig.mfe) {
210
- // The hub is served for all paths that ask for a page: @qlik/api detects the authorization code of the OAuth2
211
- // callback at /auth/login/callback in the URL, to complete the token exchange in the page
212
- app.use((req, res, next) => {
213
- if (req.method === 'GET' && !path.extname(req.path) && req.accepts('html')) {
214
- // the folder is the root, a path with a segment that starts with a dot, such as .pnpm, is not sent otherwise
215
- res.sendFile('index.html', { root: contentBase });
216
- } else {
217
- next();
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(httpsKeyPath), cert: fs.readFileSync(httpsCertPath) }, app)
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
- if (serveConfig.mfe) {
239
- const bundleUrl = `${url}/pkg/${encodeURIComponent(snName)}`;
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
- if (!serveConfig.mfe) {
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
- const initiateWatch = async ({ snPath, snName, host }) => {
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
- app.get('/pkg/:name', async (req, res) => {
59
- const { name } = req.params;
60
- await done;
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
- watcher = await build({
144
- watch: serveConfig.mfe ? 'systemjs' : 'esm',
145
- external: provided,
146
- config: serveConfig.config,
147
- cwd: context,
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
- try {
151
- const externalPkg = require(path.resolve(context, 'package.json'));
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
- } catch (e) {
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 && !serveConfig.mfe,
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.2",
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.2",
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.2",
52
- "@nebula.js/nucleus": "8.0.0-alpha.2",
53
- "@nebula.js/snapshooter": "8.0.0-alpha.2",
54
- "@nebula.js/stardust": "8.0.0-alpha.2",
55
- "@nebula.js/ui": "8.0.0-alpha.2",
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
  }