@nebula.js/cli-serve 2.0.0-beta.1

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/dist/njs.png ADDED
Binary file
@@ -0,0 +1,13 @@
1
+ version: '3.1'
2
+
3
+ services:
4
+ engine:
5
+ image: qlikcore/engine:${ENGINE_VERSION:-12.972.0}
6
+ # restart: always
7
+ command: |
8
+ -S AcceptEULA=${ACCEPT_EULA:-no}
9
+ -S DocumentDirectory=/apps
10
+ ports:
11
+ - ${ENGINE_PORT:-9076}:9076
12
+ volumes:
13
+ - ${APPS_PATH:-./data/apps}:/apps
package/lib/engine.js ADDED
@@ -0,0 +1,49 @@
1
+ const path = require('path');
2
+ const { spawn } = require('child_process');
3
+
4
+ const execCmd = (cmd, cmdArgs = [], opts) =>
5
+ new Promise((resolve, reject) => {
6
+ const child = spawn(cmd, cmdArgs, opts);
7
+ const res = {
8
+ exitCode: null,
9
+ out: '',
10
+ err: '',
11
+ };
12
+ child.on('error', reject);
13
+ child.stdout.on('data', (chunk) => {
14
+ res.out += chunk.toString();
15
+ });
16
+ child.stderr.on('data', (chunk) => {
17
+ res.err += chunk.toString();
18
+ });
19
+ child.on('exit', (exitCode) => {
20
+ res.exitCode = exitCode;
21
+ if (exitCode === 0) {
22
+ resolve(res);
23
+ } else {
24
+ reject(res);
25
+ }
26
+ });
27
+ });
28
+
29
+ const useEngine = ({ ACCEPT_EULA = false, cwd = path.resolve(__dirname, '../'), files }) => {
30
+ if (ACCEPT_EULA !== true) {
31
+ throw new Error('Need to accept EULA in order to start engine container');
32
+ }
33
+ console.error('Starting engine container...');
34
+ const f = (files || []).reduce((acc, curr) => [...acc, '-f', curr], []);
35
+ process.env.ACCEPT_EULA = 'yes';
36
+ const stopEngine = async () => {
37
+ console.error('Stopping engine container...');
38
+ try {
39
+ await execCmd('docker-compose', [...f, 'down', '--remove-orphans'], {
40
+ cwd,
41
+ });
42
+ } catch (err) {
43
+ console.error(err);
44
+ }
45
+ };
46
+ return execCmd('docker-compose', [...f, 'up', '-d', '--build'], { cwd }).then(() => stopEngine);
47
+ };
48
+
49
+ module.exports = useEngine;
@@ -0,0 +1,80 @@
1
+ /* eslint global-require: 0 */
2
+
3
+ const fs = require('fs');
4
+
5
+ const defaultFilename = 'nebula.config.js';
6
+ const RX = new RegExp(`${defaultFilename.replace(/\./g, '\\.')}$`);
7
+
8
+ const options = {
9
+ config: {
10
+ type: 'string',
11
+ description: 'Path to config file',
12
+ default: defaultFilename,
13
+ alias: 'c',
14
+ },
15
+ entry: {
16
+ type: 'string',
17
+ description: 'File entrypoint',
18
+ },
19
+ type: {
20
+ type: 'string',
21
+ description: 'Generic object type',
22
+ },
23
+ build: {
24
+ type: 'boolean',
25
+ default: true,
26
+ },
27
+ host: {
28
+ type: 'string',
29
+ default: 'localhost',
30
+ },
31
+ port: {
32
+ type: 'number',
33
+ },
34
+ disableHostCheck: {
35
+ type: 'boolean',
36
+ default: false,
37
+ },
38
+ resources: {
39
+ type: 'string',
40
+ description: 'Path to a folder that will be served as static files under /resources',
41
+ },
42
+ scripts: {
43
+ type: 'array',
44
+ description: 'Array of scripts to inject',
45
+ },
46
+ stylesheets: {
47
+ type: 'array',
48
+ description: 'Array of stylesheets to inject',
49
+ },
50
+ 'enigma.host': {
51
+ type: 'string',
52
+ default: 'localhost',
53
+ },
54
+ 'enigma.port': {
55
+ type: 'number',
56
+ default: 9076,
57
+ },
58
+ webIntegrationId: {
59
+ type: 'string',
60
+ },
61
+ ACCEPT_EULA: {
62
+ type: 'boolean',
63
+ default: false,
64
+ },
65
+ };
66
+
67
+ module.exports = (yargs) =>
68
+ yargs.options(options).config('config', (configPath) => {
69
+ if (configPath === null) {
70
+ return {};
71
+ }
72
+ if (!fs.existsSync(configPath)) {
73
+ if (RX.test(configPath)) {
74
+ // do nothing if default filename doesn't exist
75
+ return {};
76
+ }
77
+ throw new Error(`Config ${configPath} not found`);
78
+ }
79
+ return require(configPath).serve || {};
80
+ });
@@ -0,0 +1,7 @@
1
+ export default {
2
+ component: {
3
+ mounted() {
4
+ console.log('mounted');
5
+ },
6
+ },
7
+ };
package/lib/serve.js ADDED
@@ -0,0 +1,191 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const portfinder = require('portfinder');
4
+ const extend = require('extend');
5
+ const yargs = require('yargs');
6
+ const WebSocket = require('ws').Server;
7
+ const chokidar = require('chokidar');
8
+ const build = require('@nebula.js/cli-build');
9
+
10
+ const initConfig = require('./init-config');
11
+
12
+ const webpackServe = require('./webpack.serve');
13
+ const useEngine = require('./engine');
14
+
15
+ const initiateWatch = async ({ snPath, snName, host }) => {
16
+ // TODO - timeout
17
+ let onInitiated;
18
+ const done = new Promise((resolve) => {
19
+ onInitiated = resolve;
20
+ });
21
+
22
+ const wsPort = await portfinder.getPortPromise({ port: 5001, host });
23
+
24
+ const ws = new WebSocket({
25
+ port: wsPort,
26
+ clientTracking: true,
27
+ });
28
+
29
+ const choker = chokidar.watch(snPath, {
30
+ ignoreInitial: false,
31
+ disableGlobbing: true,
32
+ });
33
+
34
+ choker.on('add', () => {
35
+ onInitiated();
36
+ });
37
+
38
+ choker.on('change', () => {
39
+ [...ws.clients].forEach((client) => {
40
+ client.send(
41
+ JSON.stringify({
42
+ changed: [snName],
43
+ })
44
+ );
45
+ });
46
+ });
47
+
48
+ const cache = {};
49
+
50
+ return {
51
+ addRoutes(app) {
52
+ app.get('/pkg/:name', async (req, res) => {
53
+ const { name } = req.params;
54
+ await done;
55
+
56
+ let file;
57
+ if (cache[name]) {
58
+ file = cache[name];
59
+ } else if (name === snName) {
60
+ file = snPath;
61
+ } else if (/\.map$/.test(name)) {
62
+ const sources = Object.keys(cache);
63
+ for (let i = 0; i < sources.length; i++) {
64
+ const p = cache[sources[i]];
65
+ const filename = path.basename(p);
66
+ if (`${filename}.map` === name) {
67
+ file = path.resolve(path.dirname(p), name);
68
+ break;
69
+ }
70
+ }
71
+ } else {
72
+ file = require.resolve(name, {
73
+ paths: [process.cwd()],
74
+ });
75
+ }
76
+
77
+ cache[name] = file;
78
+
79
+ if (fs.existsSync(file)) {
80
+ const f = fs.readFileSync(file);
81
+ res.send(f);
82
+ } else {
83
+ res.sendStatus('404');
84
+ }
85
+ });
86
+ },
87
+ async close() {
88
+ await ws.close();
89
+ await choker.close();
90
+ },
91
+ port: wsPort,
92
+ };
93
+ };
94
+
95
+ module.exports = async (argv) => {
96
+ let context = process.cwd();
97
+ let defaultServeConfig = {};
98
+ let runFromDirectory = false;
99
+
100
+ if (!argv.$0) {
101
+ const yargsArgs = argv.config ? ['--config', argv.config] : [];
102
+ defaultServeConfig = initConfig(yargs(yargsArgs)).argv;
103
+ }
104
+
105
+ const serveConfig = extend(true, {}, defaultServeConfig, argv);
106
+
107
+ let stopEngine = () => {};
108
+ if (serveConfig.ACCEPT_EULA) {
109
+ stopEngine = await useEngine(serveConfig);
110
+ }
111
+ const host = serveConfig.host || 'localhost';
112
+ const port = serveConfig.port || (await portfinder.getPortPromise({ host }));
113
+ const enigmaConfig = serveConfig.enigma;
114
+
115
+ let snPath;
116
+ let snName;
117
+ let watcher;
118
+ let snUrl;
119
+ if (/^https?/.test(serveConfig.entry)) {
120
+ snName = 'remote';
121
+ snUrl = serveConfig.entry;
122
+ } else if (serveConfig.entry) {
123
+ snPath = path.resolve(context, serveConfig.entry);
124
+ const stat = fs.statSync(snPath);
125
+ if (stat.isDirectory()) {
126
+ runFromDirectory = true;
127
+ context = snPath;
128
+ } else {
129
+ const parsed = path.parse(snPath);
130
+ snName = parsed.name;
131
+ }
132
+ } else {
133
+ runFromDirectory = true;
134
+ }
135
+
136
+ if (runFromDirectory) {
137
+ if (serveConfig.build !== false) {
138
+ watcher = await build({
139
+ watch: true,
140
+ config: serveConfig.config,
141
+ cwd: context,
142
+ });
143
+ }
144
+ try {
145
+ const externalPkg = require(path.resolve(context, 'package.json')); // eslint-disable-line global-require
146
+ const externalEntry = externalPkg.main;
147
+ snName = externalPkg.name;
148
+ snPath = path.resolve(context, externalEntry);
149
+ } catch (e) {
150
+ //
151
+ }
152
+ }
153
+
154
+ const ww = snUrl ? null : await initiateWatch({ snPath, snName: serveConfig.type || snName, host });
155
+
156
+ const server = await webpackServe({
157
+ host,
158
+ port,
159
+ disableHostCheck: serveConfig.disableHostCheck,
160
+ enigmaConfig,
161
+ webIntegrationId: serveConfig.webIntegrationId,
162
+ snName: serveConfig.type || snName,
163
+ snUrl,
164
+ dev: process.env.MONO === 'true',
165
+ open: serveConfig.open !== false,
166
+ entryWatcher: ww,
167
+ watcher,
168
+ serveConfig,
169
+ });
170
+
171
+ const close = async () => {
172
+ await stopEngine();
173
+ if (watcher) {
174
+ watcher.close();
175
+ }
176
+ if (ww) {
177
+ await ww.close();
178
+ }
179
+ server.close();
180
+ };
181
+
182
+ ['SIGINT', 'SIGTERM'].forEach((signal) => {
183
+ process.on(signal, close);
184
+ });
185
+
186
+ // eslint-disable-next-line consistent-return
187
+ return {
188
+ url: server.url,
189
+ close,
190
+ };
191
+ };
package/lib/sn.js ADDED
@@ -0,0 +1 @@
1
+ // dummy entrypoint
@@ -0,0 +1,58 @@
1
+ const express = require('express');
2
+ const bodyParser = require('body-parser');
3
+
4
+ module.exports = function router({ base, snapshotUrl, snapshooter }) {
5
+ const r = express.Router();
6
+
7
+ r.use(
8
+ bodyParser.json({
9
+ limit: '10mb',
10
+ })
11
+ );
12
+
13
+ // returns an existing image
14
+ r.get('/image/:id', (req, res) => {
15
+ const p = snapshooter.getStoredImage(req.params.id);
16
+ if (p) {
17
+ res.contentType('image/png');
18
+ res.end(p, 'binary');
19
+ } else {
20
+ res.sendStatus('404');
21
+ }
22
+ });
23
+
24
+ r.post('/capture', async (req, res) => {
25
+ try {
26
+ const key = await snapshooter.captureImageOfSnapshot(req.body);
27
+ res.send({
28
+ url: `${base}/image/${key}`,
29
+ });
30
+ } catch (e) {
31
+ console.error(e);
32
+ res.sendStatus('500');
33
+ }
34
+ });
35
+
36
+ // post snapshot
37
+ r.post('/snapshot', (req, res) => {
38
+ const key = snapshooter.storeSnapshot(req.body);
39
+ res.json({
40
+ url: `${snapshotUrl}?snapshot=${key}`,
41
+ });
42
+ });
43
+
44
+ r.get('/snapshot/:id', (req, res) => {
45
+ const p = snapshooter.getStoredSnapshot(req.params.id);
46
+ if (p) {
47
+ res.json(p);
48
+ } else {
49
+ res.sendStatus('404');
50
+ }
51
+ });
52
+
53
+ r.get('/snapshots', (req, res) => {
54
+ res.json(snapshooter.getStoredSnapshots());
55
+ });
56
+
57
+ return r;
58
+ };
@@ -0,0 +1,83 @@
1
+ /* eslint no-param-reassign: 0 */
2
+
3
+ const puppeteer = require('puppeteer');
4
+
5
+ function snapshooter({ snapshotUrl, chrome = {} } = {}) {
6
+ const snapshots = {};
7
+ const images = {};
8
+ let browser;
9
+ let cachedPage;
10
+ let rendering = false;
11
+
12
+ const hasConnectOptions = chrome.browserWSEndpoint || chrome.browserURL;
13
+
14
+ const createBrowser = () => (hasConnectOptions ? puppeteer.connect(chrome) : puppeteer.launch(chrome));
15
+
16
+ const doIt = async (snapshot, runCached) => {
17
+ browser = browser || (await createBrowser());
18
+ let page;
19
+ if (runCached) {
20
+ cachedPage = cachedPage || (await browser.newPage());
21
+ page = cachedPage;
22
+ } else {
23
+ page = await browser.newPage();
24
+ }
25
+ await page.setViewport({
26
+ width: snapshot.meta.size.width,
27
+ height: snapshot.meta.size.height,
28
+ });
29
+ await page.goto(`${snapshotUrl}?snapshot=${snapshot.key}`);
30
+ try {
31
+ await page.waitFor(
32
+ () =>
33
+ (document.querySelector('.njs-viz') &&
34
+ +document.querySelector('.njs-viz').getAttribute('data-render-count') > 0) ||
35
+ document.querySelector('[data-njs-error]')
36
+ );
37
+ } catch (e) {
38
+ // empty
39
+ }
40
+ const image = await page.screenshot();
41
+ images[snapshot.key] = image;
42
+ rendering = false;
43
+ return snapshot.key;
44
+ };
45
+
46
+ return {
47
+ getStoredImage(id) {
48
+ return images[id];
49
+ },
50
+ getStoredSnapshot(id) {
51
+ return snapshots[id];
52
+ },
53
+ getStoredSnapshots() {
54
+ return Object.keys(snapshots).map((key) => snapshots[key]);
55
+ },
56
+ storeSnapshot(snapshot) {
57
+ if (!snapshot) {
58
+ throw new Error('Empty snapshot');
59
+ }
60
+ if (!snapshot.key) {
61
+ snapshot.key = String(+Date.now());
62
+ }
63
+ snapshots[snapshot.key] = snapshot;
64
+ return snapshot.key;
65
+ },
66
+ async captureImageOfSnapshot(snapshot) {
67
+ this.storeSnapshot(snapshot);
68
+ if (rendering) {
69
+ return doIt(snapshot);
70
+ }
71
+ rendering = true;
72
+ const key = await doIt(snapshot, true);
73
+ rendering = false;
74
+ return key;
75
+ },
76
+ async close() {
77
+ if (browser) {
78
+ await browser.close();
79
+ }
80
+ },
81
+ };
82
+ }
83
+ module.exports = snapshooter;
@@ -0,0 +1,125 @@
1
+ const path = require('path');
2
+ const webpack = require('webpack');
3
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
4
+
5
+ const babelPath = require.resolve('babel-loader');
6
+ const babelPresetEnvPath = require.resolve('@babel/preset-env');
7
+ const babelPresetReactPath = require.resolve('@babel/preset-react');
8
+ const sourceMapLoaderPath = require.resolve('source-map-loader');
9
+ const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
10
+
11
+ const favicon = path.resolve(__dirname, '../../../docs/assets/njs.png');
12
+
13
+ const crypto = require('crypto');
14
+ const { version } = require('../package.json');
15
+
16
+ const versionHash = crypto.createHash('md5').update(version).digest('hex').slice(0, 4);
17
+
18
+ const cfg = ({ srcDir, distDir, dev = false, serveConfig = {} }) => {
19
+ const config = {
20
+ mode: dev ? 'development' : 'production',
21
+ entry: {
22
+ eRender: [path.resolve(srcDir, 'eRender')],
23
+ eDev: [path.resolve(srcDir, 'eDev')],
24
+ eHub: [path.resolve(srcDir, 'eHub')],
25
+ },
26
+ devtool: dev ? 'eval-cheap-module-source-map' : false,
27
+ output: {
28
+ path: distDir,
29
+ filename: '[name].js',
30
+ },
31
+ resolve: {
32
+ alias: {
33
+ '@nebula.js/stardust': path.resolve(__dirname, '../../../apis/stardust/src'),
34
+ '@nebula.js/snapshooter/client': path.resolve(__dirname, '../../../apis/snapshooter/src/renderer'),
35
+ '@nebula.js/theme': path.resolve(__dirname, '../../../apis/theme/src'),
36
+ '@nebula.js/conversion': path.resolve(__dirname, '../../../apis/conversion/src'),
37
+ '@nebula.js/locale/all.json$': path.resolve(__dirname, '../../../apis/locale/all.json'),
38
+ '@nebula.js/locale': path.resolve(__dirname, '../../../apis/locale/src'),
39
+ fixtures: path.resolve(__dirname, '../../../test/component'),
40
+ },
41
+ extensions: ['.dev.js', '.js', '.jsx'],
42
+ },
43
+ module: {
44
+ rules: [
45
+ {
46
+ test: /\.css$/,
47
+ include: [/node_modules[/\\]monaco-editor/],
48
+ use: ['style-loader', 'css-loader'],
49
+ },
50
+ {
51
+ test: /\.ttf$/,
52
+ include: [/node_modules[/\\]monaco-editor/],
53
+ use: ['file-loader'],
54
+ },
55
+ {
56
+ enforce: 'pre',
57
+ test: /\.js?$/,
58
+ loader: sourceMapLoaderPath,
59
+ },
60
+ {
61
+ test: /\.jsx?$/,
62
+ sideEffects: false,
63
+ include: [srcDir, /nucleus/, /ui[/\\]icons/],
64
+ use: {
65
+ loader: babelPath,
66
+ options: {
67
+ presets: [
68
+ [
69
+ babelPresetEnvPath,
70
+ {
71
+ modules: false,
72
+ targets: {
73
+ browsers: ['last 2 chrome versions'],
74
+ },
75
+ },
76
+ ],
77
+ babelPresetReactPath,
78
+ ],
79
+ },
80
+ },
81
+ },
82
+ ],
83
+ },
84
+ plugins: [
85
+ new webpack.DefinePlugin({
86
+ __NEBULA_DEV__: true,
87
+ 'process.env.NEBULA_VERSION': JSON.stringify(version),
88
+ 'process.env.NEBULA_VERSION_HASH': JSON.stringify(versionHash),
89
+ }),
90
+ new MonacoWebpackPlugin({ languages: ['json'] }),
91
+ new HtmlWebpackPlugin({
92
+ template: path.resolve(srcDir, 'eRender.html'),
93
+ filename: 'eRender.html',
94
+ chunks: ['eRender'],
95
+ favicon,
96
+ scripts: serveConfig.scripts,
97
+ stylesheets: serveConfig.stylesheets,
98
+ }),
99
+ new HtmlWebpackPlugin({
100
+ template: path.resolve(srcDir, 'eDev.html'),
101
+ filename: 'eDev.html',
102
+ chunks: ['eDev'],
103
+ favicon,
104
+ }),
105
+ new HtmlWebpackPlugin({
106
+ template: path.resolve(srcDir, 'eHub.html'),
107
+ filename: 'eHub.html',
108
+ chunks: ['eHub'],
109
+ favicon,
110
+ }),
111
+ ],
112
+ };
113
+
114
+ return config;
115
+ };
116
+
117
+ if (!process.env.DEFAULTS) {
118
+ module.exports = cfg;
119
+ } else {
120
+ module.exports = cfg({
121
+ srcDir: path.resolve(__dirname, '../web'),
122
+ distDir: path.resolve(__dirname, '../dist'),
123
+ dev: false,
124
+ });
125
+ }
@@ -0,0 +1,69 @@
1
+ const path = require('path');
2
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
3
+
4
+ const isSrc = /^([.]{2}\/)/;
5
+ const namespace = /^webpack:\/\/([^/]+)\//;
6
+ const NM = /node_modules/;
7
+ const WP = /\/\(?webpack\)?/;
8
+
9
+ const cfg = ({ srcDir = path.resolve(__dirname, '../dist'), serveConfig = {} }) => {
10
+ const folderName = process.cwd().split('/').slice(-1)[0];
11
+
12
+ const config = {
13
+ mode: 'development',
14
+ entry: path.resolve(__dirname, './sn.js'),
15
+ devtool: false,
16
+ infrastructureLogging: {
17
+ level: 'error',
18
+ },
19
+ stats: 'errors-only',
20
+ output: {
21
+ path: path.resolve(srcDir, 'temp'),
22
+ filename: '[name].js',
23
+ devtoolModuleFilenameTemplate: (info) => {
24
+ // attempt to improve the mapping of multiple libraries into
25
+ // a better folder structure for easier debugging
26
+
27
+ // if source files are referenced as "webpack://foo/"
28
+ // then output those references as "foo:///"
29
+ const ns = namespace.exec(info.resourcePath);
30
+ if (ns && !NM.test(info.resourcePath) && !WP.test(info.resourcePath)) {
31
+ return `${ns[1]}:///${info.resourcePath.replace(namespace, '')}`;
32
+ }
33
+
34
+ // if source files are referenced as relative paths: "../bla"
35
+ // then output those references as "folderName:///bla"
36
+ if (!info.moduleId && isSrc.test(info.resourcePath)) {
37
+ const pp = info.resourcePath.replace(isSrc, '');
38
+ return `${folderName || 'sn'}:///${pp}`;
39
+ }
40
+
41
+ // otherwise output using webpack default pattern
42
+ return `webpack://${info.namespace}/${info.resourcePath}`;
43
+ },
44
+ },
45
+ resolve: {
46
+ alias: {
47
+ fixtures: path.resolve(process.cwd(), 'test/component'),
48
+ },
49
+ },
50
+ plugins: [
51
+ new HtmlWebpackPlugin({
52
+ template: path.resolve(srcDir, 'eRender.html'),
53
+ filename: 'eRender.html',
54
+ inject: 'head',
55
+ scripts: serveConfig.scripts,
56
+ stylesheets: serveConfig.stylesheets,
57
+ }),
58
+ new HtmlWebpackPlugin({
59
+ template: path.resolve(srcDir, 'eDev.html'),
60
+ filename: 'eDev.html',
61
+ inject: 'head',
62
+ }),
63
+ ],
64
+ };
65
+
66
+ return config;
67
+ };
68
+
69
+ module.exports = cfg;