@journeyapps/reactor-lib-server 2.1.4 → 2.1.6

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.
@@ -0,0 +1,9 @@
1
+ export interface ReactorModuleConfig {
2
+ name: string;
3
+ slug: string;
4
+ env: string[];
5
+ loader?: {
6
+ fragment: string;
7
+ backgroundColor: string;
8
+ };
9
+ }
@@ -0,0 +1,34 @@
1
+ import { ReactorModuleConfig } from './ReactorConfig';
2
+ export interface ReactorModuleOptions {
3
+ directory: string;
4
+ resolveGlobalEnv: (key: string) => string | undefined;
5
+ }
6
+ export declare class ReactorModule {
7
+ options: ReactorModuleOptions;
8
+ protected confFile: string;
9
+ protected conf?: ReactorModuleConfig;
10
+ protected confPackage?: {
11
+ name: string;
12
+ version: string;
13
+ };
14
+ protected fragment: string | null;
15
+ constructor(options: ReactorModuleOptions);
16
+ getEnvs(): Record<string, string>;
17
+ get name(): string;
18
+ get packageJson(): {
19
+ name: string;
20
+ version: string;
21
+ };
22
+ get config(): ReactorModuleConfig;
23
+ get loaderPayload(): {
24
+ fragmentData: string;
25
+ background: string;
26
+ } | null;
27
+ }
28
+ export declare const loadModules: (options: {
29
+ env: {
30
+ MODULES: string[];
31
+ } & {
32
+ [key: string]: any;
33
+ };
34
+ }) => ReactorModule[];
@@ -0,0 +1,21 @@
1
+ import { Request, Response } from 'express';
2
+ import { CheerioAPI } from 'cheerio';
3
+ export interface CreateHtmlGeneratorOptions {
4
+ domTransform?: (data: CheerioAPI) => void;
5
+ indexFile: string;
6
+ templateVars?: {};
7
+ title: string;
8
+ }
9
+ export declare const createHtmlGenerator: (options: CreateHtmlGeneratorOptions) => Promise<(env?: Record<string, string>) => string>;
10
+ export interface CreateBaseIndexMiddlewareOptions extends CreateHtmlGeneratorOptions {
11
+ transform?: (req: Request, res: Response, content: string) => Promise<string>;
12
+ getEnv?: (req: Request) => Record<string, string | undefined>;
13
+ }
14
+ export declare const createBaseIndexMiddleware: (options: CreateBaseIndexMiddlewareOptions) => Promise<(req: Request, res: Response) => Promise<void>>;
15
+ /**
16
+ * Escape environment variables to protect against XSS.
17
+ *
18
+ * The resulting value is both valid JSON and valid JS, and is safe to inject directly in a <script> tag.
19
+ */
20
+ export declare function escapeEnv(values: Record<string, string | undefined>): string;
21
+ export declare function escapeForHtmlScript(value: any): string;
@@ -0,0 +1,5 @@
1
+ export * from './ReactorModule';
2
+ export * from './ReactorConfig';
3
+ export * from './basic-html';
4
+ export * from './reactor-html';
5
+ export * from './logging';
@@ -0,0 +1,2 @@
1
+ import { Logger } from '@journeyapps/common-logger';
2
+ export declare const reactorServerLogger: Logger;
@@ -0,0 +1,8 @@
1
+ import { Application } from 'express';
2
+ import { ReactorModule } from './ReactorModule';
3
+ import { CheerioAPI } from 'cheerio';
4
+ export declare const serveModules: (options: {
5
+ app: Application;
6
+ modules: ReactorModule[];
7
+ }) => void;
8
+ export declare const createModuleLoaderContentTransformer: ($: CheerioAPI, modules: ReactorModule[]) => void;
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ // !----------- MODULE ----------
3
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadModules = exports.ReactorModule = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("fs"));
6
+ const path = tslib_1.__importStar(require("path"));
7
+ const logging_1 = require("./logging");
8
+ const common_logger_1 = require("@journeyapps/common-logger");
9
+ class ReactorModule {
10
+ constructor(options) {
11
+ this.options = options;
12
+ this.confFile = path.join(options.directory, 'reactor.config.json');
13
+ this.confPackage = require(path.join(options.directory, 'package.json'));
14
+ this.fragment = null;
15
+ if (fs.existsSync(this.confFile)) {
16
+ this.conf = require(this.confFile);
17
+ }
18
+ else {
19
+ throw new Error(`No config file for ${options.directory} at ${this.confFile}`);
20
+ }
21
+ this.getEnvs();
22
+ }
23
+ getEnvs() {
24
+ return this.config.env.reduce((env, key) => {
25
+ const value = this.options.resolveGlobalEnv(key);
26
+ if (value == null) {
27
+ throw new Error(`Environment variable '${key}' required by Reactor module '${this.name}' is missing`);
28
+ }
29
+ env[key] = value;
30
+ return env;
31
+ }, {});
32
+ }
33
+ get name() {
34
+ return this.config.name;
35
+ }
36
+ get packageJson() {
37
+ return this.confPackage;
38
+ }
39
+ get config() {
40
+ var _a;
41
+ return Object.assign(Object.assign({}, this.conf), { env: (_a = this.conf.env) !== null && _a !== void 0 ? _a : [] });
42
+ }
43
+ get loaderPayload() {
44
+ if (this.config.loader) {
45
+ if (!this.fragment) {
46
+ this.fragment = fs.readFileSync(path.join(path.dirname(this.confFile), this.config.loader.fragment), {
47
+ encoding: 'utf-8'
48
+ });
49
+ }
50
+ return {
51
+ background: this.config.loader.backgroundColor,
52
+ fragmentData: this.fragment
53
+ };
54
+ }
55
+ return null;
56
+ }
57
+ }
58
+ exports.ReactorModule = ReactorModule;
59
+ const loadModules = (options) => {
60
+ return options.env.MODULES.map((m) => {
61
+ let directory = path.join(process.cwd(), m);
62
+ if (m.startsWith('@')) {
63
+ // this should be the directory that contains the reactor config file
64
+ directory = path.resolve(path.dirname(require.resolve(m, { paths: [process.cwd()] })), '..');
65
+ }
66
+ const module = new ReactorModule({
67
+ resolveGlobalEnv: (key) => {
68
+ return options.env[key];
69
+ },
70
+ directory: directory
71
+ });
72
+ logging_1.reactorServerLogger.info(common_logger_1.Log.green('Loaded module'), common_logger_1.Log.bold(common_logger_1.Log.cyan(module.name)), common_logger_1.Log.purple(`${module.packageJson.name}@${module.packageJson.version}`), common_logger_1.Log.gray(`slug: ${module.config.slug}`), common_logger_1.Log.dim(directory));
73
+ logging_1.reactorServerLogger.debug(common_logger_1.Log.dim('Module environment'), common_logger_1.Log.bold(module.name), module.config.env);
74
+ return module;
75
+ });
76
+ };
77
+ exports.loadModules = loadModules;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createBaseIndexMiddleware = exports.createHtmlGenerator = void 0;
4
+ exports.escapeEnv = escapeEnv;
5
+ exports.escapeForHtmlScript = escapeForHtmlScript;
6
+ const tslib_1 = require("tslib");
7
+ const _ = tslib_1.__importStar(require("lodash"));
8
+ const fs = tslib_1.__importStar(require("fs"));
9
+ const path = tslib_1.__importStar(require("path"));
10
+ const cheerio_1 = require("cheerio");
11
+ _.templateSettings.interpolate = /<%=([\s\S]+?)%>/g;
12
+ const readFileCached = _.memoize((fileName) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
13
+ return yield fs.promises.readFile(fileName, 'utf8');
14
+ }));
15
+ const createHtmlGenerator = (options) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
16
+ var _a;
17
+ let index_data = yield readFileCached(options.indexFile);
18
+ // append the core stuff the to head which deals with ENV variables
19
+ let index_fragment = yield readFileCached(path.join(__dirname, '../media/core-fragment.html'));
20
+ const $ = (0, cheerio_1.load)(index_data);
21
+ $('head').prepend(`<title>${options.title}</title>`);
22
+ $('head').prepend(index_fragment);
23
+ // optionally mutate even more
24
+ (_a = options.domTransform) === null || _a === void 0 ? void 0 : _a.call(options, $);
25
+ // now turn it into a template so we can do variable substitution
26
+ const template = _.template($.html());
27
+ return (env = {}) => {
28
+ return template(Object.assign({ ENV: escapeEnv(Object.assign(Object.assign({}, env), { NODE_ENV: process.env.NODE_ENV || 'development' })) }, (options.templateVars || {})));
29
+ };
30
+ });
31
+ exports.createHtmlGenerator = createHtmlGenerator;
32
+ const createBaseIndexMiddleware = (options) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
33
+ const generator = yield (0, exports.createHtmlGenerator)(options);
34
+ return (req, res) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
35
+ var _a, _b;
36
+ try {
37
+ let env = ((_a = options.getEnv) === null || _a === void 0 ? void 0 : _a.call(options, req)) || {};
38
+ let index_data = generator(env);
39
+ if (options.transform) {
40
+ index_data = yield ((_b = options.transform) === null || _b === void 0 ? void 0 : _b.call(options, req, res, index_data));
41
+ }
42
+ res.send(index_data);
43
+ res.end();
44
+ }
45
+ catch (err) {
46
+ res.status(500).end();
47
+ }
48
+ });
49
+ });
50
+ exports.createBaseIndexMiddleware = createBaseIndexMiddleware;
51
+ /**
52
+ * Escape environment variables to protect against XSS.
53
+ *
54
+ * The resulting value is both valid JSON and valid JS, and is safe to inject directly in a <script> tag.
55
+ */
56
+ function escapeEnv(values) {
57
+ for (let value of Object.values(values)) {
58
+ if (value != null && typeof value != 'string') {
59
+ // This is not expected to happen. We just do this as an extra safety check, to avoid injecting nested values.
60
+ throw new Error(`Invalid environment variable: ${value}`);
61
+ }
62
+ }
63
+ return escapeForHtmlScript(values);
64
+ }
65
+ function escapeForHtmlScript(value) {
66
+ /*
67
+ Some background: https://dzone.com/articles/preventing-xss-vulnerabilities-when-developing-rub
68
+ https://portswigger.net/web-security/cross-site-scripting/preventing
69
+ Values injected in a <script> tag needs to be escaped for:
70
+ 1. JavaScript: JSON.stringify covers this.
71
+ 2. HTML: </script> in the middle of a JSON string still breaks out of the script. Using unicode escape sequences
72
+ covers this. At minimum, '<' and '>' should be escaped. Being extra cautious, we escape most characters except for a small allow-list.
73
+ */
74
+ const jsSafe = JSON.stringify(value);
75
+ // Our allow-list includes valid JSON control characters, allowing this to work on objects in addition to strings.
76
+ return jsSafe.replace(/[^\w. ",:{}\\\[\]]/gi, (c) => '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4));
77
+ }
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./ReactorModule"), exports);
5
+ tslib_1.__exportStar(require("./ReactorConfig"), exports);
6
+ tslib_1.__exportStar(require("./basic-html"), exports);
7
+ tslib_1.__exportStar(require("./reactor-html"), exports);
8
+ tslib_1.__exportStar(require("./logging"), exports);
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.reactorServerLogger = void 0;
4
+ const common_logger_1 = require("@journeyapps/common-logger");
5
+ exports.reactorServerLogger = new common_logger_1.Logger({
6
+ name: 'Reactor server',
7
+ level: common_logger_1.LogLevel.INFO,
8
+ transport: new common_logger_1.NodeConsoleLoggerTransport()
9
+ });
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createModuleLoaderContentTransformer = exports.serveModules = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const express_1 = tslib_1.__importDefault(require("express"));
6
+ const path = tslib_1.__importStar(require("path"));
7
+ const serveModules = (options) => {
8
+ options.modules.forEach((m) => {
9
+ options.app.use(`/module/${m.config.slug}`, express_1.default.static(path.join(m.options.directory, 'dist-module')));
10
+ });
11
+ };
12
+ exports.serveModules = serveModules;
13
+ const createModuleLoaderContentTransformer = ($, modules) => {
14
+ let module_names_window = modules.map((m) => m.packageJson.name);
15
+ modules.forEach((m) => $('head').append(`<script nonce="SCRIPT_NONCE"
16
+ defer
17
+ data-module="${m.packageJson.name}"
18
+ src="/module/${m.config.slug}/bundle.js"
19
+ type="text/javascript" />`));
20
+ $('head').append(`
21
+ <script nonce="SCRIPT_NONCE">
22
+ window.addEventListener('DOMContentLoaded', async (event) => {
23
+ // get all the reactor module classes
24
+ const module_classes = ${JSON.stringify(module_names_window)}.map(m => {
25
+ if(!window[m]){
26
+ throw new Error('Reactor module "' + m + '" was not loaded correctly.');
27
+ }
28
+ return window[m].default;
29
+ });
30
+
31
+ // call preboot on all of them if they have that method (such as the ReactorModule which inits the kernel)
32
+ module_classes.forEach(module => {
33
+ if (module.preboot) {
34
+ module.preboot(module_classes);
35
+ }
36
+ });
37
+ });
38
+ </script>`);
39
+ };
40
+ exports.createModuleLoaderContentTransformer = createModuleLoaderContentTransformer;
@@ -0,0 +1,7 @@
1
+ <script type='application/javascript' nonce='SCRIPT_NONCE'>
2
+ if (!window.process) {
3
+ window.process = {
4
+ env: <%= ENV %>
5
+ };
6
+ }
7
+ </script>
@@ -0,0 +1,97 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
+ <meta name="theme-color" content="<%= LOADER_BACKGROUND_COLOR %>" />
7
+ <meta name="mobile-web-app-capable" content="yes" />
8
+ <meta name="apple-mobile-web-app-capable" content="yes" />
9
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
10
+ <style>
11
+ html,
12
+ body {
13
+ background: <%= LOADER_BACKGROUND_COLOR %>;
14
+ }
15
+
16
+ @keyframes fadein {
17
+ 0%{
18
+ opacity: 0;
19
+ }
20
+ 100%{
21
+ opacity: 1;
22
+ }
23
+ }
24
+
25
+ .loader {
26
+ opacity: 0;
27
+ top: 50%;
28
+ left: 50%;
29
+ transform: translateX(-50%) translateY(-50%);
30
+ position: absolute;
31
+ display: flex;
32
+ flex-direction: column;
33
+ align-items: center;
34
+ animation: fadein;
35
+ animation-duration: 0.3s;
36
+ animation-delay: 0.5s;
37
+ animation-direction: normal;
38
+ animation-fill-mode: forwards;
39
+ }
40
+
41
+ .loading-bar-wrapper {
42
+ width: 200px;
43
+ height: 4px;
44
+ background: rgba(255, 255, 255, 0.1);
45
+ margin-top: 40px;
46
+ }
47
+
48
+ .loading-text {
49
+ margin-top: 6px;
50
+ font-size: 11px;
51
+ color: white;
52
+ opacity: 0.2;
53
+ font-family: sans-serif;
54
+ }
55
+
56
+ .loading-bar {
57
+ width: 0%;
58
+ transition: width 0.3s;
59
+ height: 100%;
60
+ background: rgba(255, 255, 255, 0.3);
61
+ will-change: width;
62
+ }
63
+ </style>
64
+ <script nonce="SCRIPT_NONCE">
65
+ let loaded = new Set();
66
+ window.reactorModuleLoaded = (module) => {
67
+ const scripts = Array.from(document.scripts).filter((s) => !!s.dataset['module']);
68
+ // could be multiple modules with the same name due to workers etc...
69
+ if(loaded.has(module)){
70
+ return;
71
+ }
72
+ loaded.add(module)
73
+ const loaderBar = document.querySelector('.loading-bar');
74
+ const loaderText = document.querySelector('.loading-text');
75
+ if (!loaderBar) {
76
+ return;
77
+ }
78
+ const percent = parseInt((loaded.size / scripts.length) * 100);
79
+ loaderBar.style.width = `${percent}%`;
80
+ if (percent === 100) {
81
+ loaderText.innerHTML = `Booting!`;
82
+ } else {
83
+ loaderText.innerHTML = `Loaded: ${module}`;
84
+ }
85
+ };
86
+ </script>
87
+ </head>
88
+ <body>
89
+ <div class="loader">
90
+ <div class="loading-bar-wrapper">
91
+ <div class="loading-bar"></div>
92
+ </div>
93
+ <div class="loading-text">Loading...</div>
94
+ </div>
95
+ <div id="application"></div>
96
+ </body>
97
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@journeyapps/reactor-lib-server",
3
- "version": "2.1.4",
3
+ "version": "2.1.6",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -15,7 +15,7 @@
15
15
  "typings": "./dist/@types/index",
16
16
  "dependencies": {
17
17
  "@journeyapps/common-logger": "1.1.2",
18
- "@journeyapps/reactor-lib-utils": "2.0.16",
18
+ "@journeyapps/reactor-lib-utils": "2.0.18",
19
19
  "cheerio": "^1.2.0",
20
20
  "express": "^5.2.1",
21
21
  "lodash": "^4.18.1",
@@ -26,6 +26,11 @@
26
26
  "@types/express-session": "^1.19.0",
27
27
  "@types/lodash": "^4.17.25"
28
28
  },
29
+ "files": [
30
+ "dist",
31
+ "!dist/tsconfig.tsbuildinfo",
32
+ "media"
33
+ ],
29
34
  "scripts": {
30
35
  "build": "../node_modules/.bin/tsc --build"
31
36
  }