feat(cli): Optimise serving static assets (#4182)

* simplify static server setup

* use consistent ignoredEndpoints across all routes

* remove unnecessary lodash usage

* stop keeping generated assets in memory

* pre-generate all static assets
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2022-09-29 11:27:20 +02:00
committed by GitHub
parent 2714b4ced7
commit 8b0ccc017b
3 changed files with 80 additions and 86 deletions

View File

@@ -30,14 +30,15 @@
/* eslint-disable no-await-in-loop */
import { exec as callbackExec } from 'child_process';
import { promises, readFileSync } from 'fs';
import { existsSync, readFileSync } from 'fs';
import { access as fsAccess, readFile, writeFile, mkdir } from 'fs/promises';
import os from 'os';
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path';
import { createHmac } from 'crypto';
import { promisify } from 'util';
import cookieParser from 'cookie-parser';
import express from 'express';
import _ from 'lodash';
import send from 'send';
import { FindManyOptions, getConnectionManager, In } from 'typeorm';
// eslint-disable-next-line import/no-extraneous-dependencies
import axios, { AxiosRequestConfig } from 'axios';
@@ -397,15 +398,12 @@ class App {
this.endpointWebhook,
this.endpointWebhookTest,
this.endpointPresetCredentials,
];
if (!config.getEnv('publicApi.disabled')) {
ignoredEndpoints.push(this.publicApiEndpoint);
}
// eslint-disable-next-line prefer-spread
ignoredEndpoints.push.apply(ignoredEndpoints, excludeEndpoints.split(':'));
config.getEnv('publicApi.disabled') ? this.publicApiEndpoint : '',
...excludeEndpoints.split(':'),
].filter((u) => !!u);
// eslint-disable-next-line no-useless-escape
const authIgnoreRegex = new RegExp(`^\/(${_(ignoredEndpoints).compact().join('|')})\/?.*$`);
const authIgnoreRegex = new RegExp(`^\/(${ignoredEndpoints.join('|')})\/?.*$`);
// Check for basic auth credentials if activated
const basicAuthActive = config.getEnv('security.basicAuth.active');
@@ -664,18 +662,7 @@ class App {
history({
rewrites: [
{
from: new RegExp(
`^/(${[
'healthz',
'metrics',
'css',
'js',
this.restEndpoint,
this.endpointWebhook,
this.endpointWebhookTest,
...(excludeEndpoints.length ? excludeEndpoints.split(':') : []),
].join('|')})/?.*$`,
),
from: new RegExp(`^/(${[this.restEndpoint, ...ignoredEndpoints].join('|')})/?.*$`),
to: (context) => {
return context.parsedUrl.pathname!.toString();
},
@@ -967,7 +954,7 @@ class App {
const headersPath = pathJoin(packagesPath, 'nodes-base', 'dist', 'nodes', 'headers');
try {
await promises.access(`${headersPath}.js`);
await fsAccess(`${headersPath}.js`);
} catch (_) {
return; // no headers available
}
@@ -1192,12 +1179,12 @@ class App {
timezone,
);
const signatureMethod = _.get(oauthCredentials, 'signatureMethod') as string;
const signatureMethod = oauthCredentials.signatureMethod as string;
const oAuthOptions: clientOAuth1.Options = {
consumer: {
key: _.get(oauthCredentials, 'consumerKey') as string,
secret: _.get(oauthCredentials, 'consumerSecret') as string,
key: oauthCredentials.consumerKey as string,
secret: oauthCredentials.consumerSecret as string,
},
signature_method: signatureMethod,
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -1220,7 +1207,7 @@ class App {
const options: RequestOptions = {
method: 'POST',
url: _.get(oauthCredentials, 'requestTokenUrl') as string,
url: oauthCredentials.requestTokenUrl as string,
data: oauthRequestData,
};
@@ -1237,9 +1224,7 @@ class App {
const responseJson = Object.fromEntries(paramsParser.entries());
const returnUri = `${_.get(oauthCredentials, 'authUrl')}?oauth_token=${
responseJson.oauth_token
}`;
const returnUri = `${oauthCredentials.authUrl}?oauth_token=${responseJson.oauth_token}`;
// Encrypt the data
const credentials = new Credentials(
@@ -1332,7 +1317,7 @@ class App {
const options: AxiosRequestConfig = {
method: 'POST',
url: _.get(oauthCredentials, 'accessTokenUrl') as string,
url: oauthCredentials.accessTokenUrl as string,
params: {
oauth_token,
oauth_verifier,
@@ -1753,35 +1738,11 @@ class App {
if (!config.getEnv('endpoints.disableUi')) {
// Read the index file and replace the path placeholder
const editorUiPath = require.resolve('n8n-editor-ui');
const filePath = pathJoin(pathDirname(editorUiPath), 'dist', 'index.html');
const n8nPath = config.getEnv('path');
const basePathRegEx = /\/{{BASE_PATH}}\//g;
let readIndexFile = readFileSync(filePath, 'utf8');
readIndexFile = readIndexFile.replace(basePathRegEx, n8nPath);
readIndexFile = readIndexFile.replace(/\/favicon.ico/g, `${n8nPath}favicon.ico`);
const cssPath = pathJoin(pathDirname(editorUiPath), 'dist', '**/*.css');
const cssFiles: Record<string, string> = {};
glob.sync(cssPath).forEach((filePath) => {
let readFile = readFileSync(filePath, 'utf8');
readFile = readFile.replace(basePathRegEx, n8nPath);
cssFiles[filePath.replace(pathJoin(pathDirname(editorUiPath), 'dist'), '')] = readFile;
});
const jsPath = pathJoin(pathDirname(editorUiPath), 'dist', '**/*.js');
const jsFiles: Record<string, string> = {};
glob.sync(jsPath).forEach((filePath) => {
let readFile = readFileSync(filePath, 'utf8');
readFile = readFile.replace(basePathRegEx, n8nPath);
jsFiles[filePath.replace(pathJoin(pathDirname(editorUiPath), 'dist'), '')] = readFile;
});
const hooksUrls = config.getEnv('externalFrontendHooksUrls');
let scriptsString = '';
if (hooksUrls) {
scriptsString = hooksUrls.split(';').reduce((acc, curr) => {
return `${acc}<script src="${curr}"></script>`;
@@ -1802,42 +1763,38 @@ class App {
scriptsString += phLoadingScript;
}
const editorUiDistDir = pathJoin(pathDirname(require.resolve('n8n-editor-ui')), 'dist');
const generatedStaticDir = pathJoin(__dirname, '../public');
const firstLinkedScriptSegment = '<link href="/js/';
readIndexFile = readIndexFile.replace(
firstLinkedScriptSegment,
scriptsString + firstLinkedScriptSegment,
);
const compileFile = async (fileName: string) => {
const filePath = pathJoin(editorUiDistDir, fileName);
if (/(index.html)|.*\.(js|css)/.test(filePath) && existsSync(filePath)) {
const srcFile = await readFile(filePath, 'utf8');
let payload = srcFile.replace(basePathRegEx, n8nPath);
if (filePath === 'index.html') {
payload = payload
.replace(/\/favicon.ico/g, `${n8nPath}favicon.ico`)
.replace(firstLinkedScriptSegment, scriptsString + firstLinkedScriptSegment);
}
const destFile = pathJoin(generatedStaticDir, fileName);
await mkdir(pathDirname(destFile), { recursive: true });
await writeFile(destFile, payload, 'utf-8');
}
};
// Serve the altered index.html file separately
this.app.get(`/index.html`, async (req: express.Request, res: express.Response) => {
res.send(readIndexFile);
await compileFile('index.html');
const files = await glob('**/*.{css,js}', { cwd: editorUiDistDir });
await Promise.all(files.map(compileFile));
this.app.use('/', express.static(generatedStaticDir), express.static(editorUiDistDir));
const startTime = new Date().toUTCString();
this.app.use('/index.html', (req, res, next) => {
res.setHeader('Last-Modified', startTime);
next();
});
this.app.get('/assets/*.css', async (req: express.Request, res: express.Response) => {
res.type('text/css').send(cssFiles[req.url]);
});
this.app.get('/assets/*.js', async (req: express.Request, res: express.Response) => {
res.type('text/javascript').send(jsFiles[req.url]);
});
// Serve the website
this.app.use(
'/',
express.static(pathJoin(pathDirname(editorUiPath), 'dist'), {
index: 'index.html',
setHeaders: (res, path) => {
if (res.req && res.req.url === '/index.html') {
// Set last modified date manually to n8n start time so
// that it hopefully refreshes the page when a new version
// got used
res.setHeader('Last-Modified', startTime);
}
},
}),
);
}
const startTime = new Date().toUTCString();
}
}