@_linked/server 2.0.0 → 2.0.2
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/CHANGELOG.md +12 -0
- package/lib/esm/backend.d.ts +6 -0
- package/lib/esm/backend.js +27 -0
- package/lib/esm/backend.js.map +1 -0
- package/lib/esm/index.d.ts +9 -0
- package/lib/esm/index.js +11 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/ontologies/lincd-server.d.ts +31 -0
- package/lib/esm/ontologies/lincd-server.js +42 -0
- package/lib/esm/ontologies/lincd-server.js.map +1 -0
- package/lib/esm/package.d.ts +4 -0
- package/lib/esm/package.js +3 -0
- package/lib/esm/package.js.map +1 -0
- package/lib/esm/shapes/LincdAPI.d.ts +41 -0
- package/lib/esm/shapes/LincdAPI.js +113 -0
- package/lib/esm/shapes/LincdAPI.js.map +1 -0
- package/lib/esm/shapes/LincdServer.d.ts +79 -0
- package/lib/esm/shapes/LincdServer.js +1257 -0
- package/lib/esm/shapes/LincdServer.js.map +1 -0
- package/lib/esm/shapes/LincdWebApp.d.ts +7 -0
- package/lib/esm/shapes/LincdWebApp.js +40 -0
- package/lib/esm/shapes/LincdWebApp.js.map +1 -0
- package/lib/esm/shapes/filestores/LocalFileStore.d.ts +42 -0
- package/lib/esm/shapes/filestores/LocalFileStore.js +93 -0
- package/lib/esm/shapes/filestores/LocalFileStore.js.map +1 -0
- package/lib/esm/shapes/quadstores/BackendAPIStore.d.ts +33 -0
- package/lib/esm/shapes/quadstores/BackendAPIStore.js +65 -0
- package/lib/esm/shapes/quadstores/BackendAPIStore.js.map +1 -0
- package/lib/esm/shapes/quadstores/BackendAPIStoreProvider.d.ts +13 -0
- package/lib/esm/shapes/quadstores/BackendAPIStoreProvider.js +26 -0
- package/lib/esm/shapes/quadstores/BackendAPIStoreProvider.js.map +1 -0
- package/lib/esm/types/RouteConfig.d.ts +56 -0
- package/lib/esm/types/RouteConfig.js +2 -0
- package/lib/esm/types/RouteConfig.js.map +1 -0
- package/lib/esm/types.d.ts +12 -0
- package/lib/esm/types.js +1 -0
- package/lib/esm/types.js.map +1 -0
- package/lib/esm/utils/Shapes.d.ts +7 -0
- package/lib/esm/utils/Shapes.js +59 -0
- package/lib/esm/utils/Shapes.js.map +1 -0
- package/lib/esm/utils/accessUrl.d.ts +6 -0
- package/lib/esm/utils/accessUrl.js +9 -0
- package/lib/esm/utils/accessUrl.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1257 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import events from 'events';
|
|
13
|
+
import express from 'express';
|
|
14
|
+
import fetchCookie from 'fetch-cookie';
|
|
15
|
+
import * as fsNative from 'fs';
|
|
16
|
+
import * as fs from 'fs/promises';
|
|
17
|
+
import { getWebpackAppConfig } from '@_linked/cli/config-webpack-app';
|
|
18
|
+
import { getLincdPackages } from '@_linked/cli/cli-methods';
|
|
19
|
+
import { getPackageJSON } from '@_linked/cli/utils';
|
|
20
|
+
import { AppContextProvider } from '@_linked/server-utils/components/AppContext';
|
|
21
|
+
import { JSONParser } from '@_linked/server-utils/utils/JSONParser';
|
|
22
|
+
import { JSONWriter } from '@_linked/server-utils/utils/JSONWriter';
|
|
23
|
+
import { Server } from '@_linked/server-utils/utils/Server';
|
|
24
|
+
import { ShapeProvider } from '@_linked/server-utils/utils/ShapeProvider';
|
|
25
|
+
import { CoreMap } from '@_linked/core/collections/CoreMap';
|
|
26
|
+
import { Shape } from '@_linked/core/shapes/Shape';
|
|
27
|
+
import { LinkedErrorLogging } from '@_linked/core/utils/LinkedErrorLogging';
|
|
28
|
+
import { LinkedFileStorage } from '@_linked/core/utils/LinkedFileStorage';
|
|
29
|
+
import { LinkedStorage } from '@_linked/core/utils/LinkedStorage';
|
|
30
|
+
import { autoLoadOntologyData } from '@_linked/core/utils/Package';
|
|
31
|
+
import { getShapeClass, getSuperShapesClasses, } from '@_linked/core/utils/ShapeClass';
|
|
32
|
+
import path from 'path';
|
|
33
|
+
import process from 'process';
|
|
34
|
+
import * as React from 'react';
|
|
35
|
+
import { renderToPipeableStream, renderToStaticMarkup } from 'react-dom/server';
|
|
36
|
+
import { StaticRouter } from 'react-router-dom/server.js';
|
|
37
|
+
import { rimraf } from 'rimraf';
|
|
38
|
+
import sharp from 'sharp';
|
|
39
|
+
import { Transform } from 'stream';
|
|
40
|
+
import { CookieJar } from 'tough-cookie';
|
|
41
|
+
import webpack from 'webpack';
|
|
42
|
+
import webpackDevMiddleware from 'webpack-dev-middleware';
|
|
43
|
+
import webpackHotMiddleware from 'webpack-hot-middleware';
|
|
44
|
+
import { lincdServer } from '../ontologies/lincd-server.js';
|
|
45
|
+
import { linkedShape } from '../package.js';
|
|
46
|
+
import { syncShapes } from '../utils/Shapes.js';
|
|
47
|
+
import { LincdAPI } from './LincdAPI.js';
|
|
48
|
+
//prevent errors in node.js when (s)css files are imported in js
|
|
49
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
50
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
51
|
+
// Install a global fetch that persists cookies across redirects using a CookieJar.
|
|
52
|
+
// This ensures server-side requests behave more like browsers when handling Set-Cookie + redirects.
|
|
53
|
+
const __lincdCookieJar = new CookieJar();
|
|
54
|
+
// Wrap native fetch with fetch-cookie so it reads/writes cookies in the jar.
|
|
55
|
+
const __lincdFetchWithCookies = fetchCookie(fetch, __lincdCookieJar);
|
|
56
|
+
// Expose (optionally) for debugging/tests; not required by app logic.
|
|
57
|
+
globalThis.__lincdCookieJar = __lincdCookieJar;
|
|
58
|
+
// Set the global fetch used throughout the server code path
|
|
59
|
+
globalThis.fetch = __lincdFetchWithCookies;
|
|
60
|
+
process.on('uncaughtException', (err) => {
|
|
61
|
+
console.warn(chalk.red('Asynchronous error caught.'));
|
|
62
|
+
console.error(err);
|
|
63
|
+
// error logging
|
|
64
|
+
LinkedErrorLogging.log(err);
|
|
65
|
+
});
|
|
66
|
+
process.on('unhandledRejection', (err) => {
|
|
67
|
+
console.warn(chalk.red('Unhandled rejection caught.'));
|
|
68
|
+
console.error(err);
|
|
69
|
+
// error logging
|
|
70
|
+
LinkedErrorLogging.log(err);
|
|
71
|
+
});
|
|
72
|
+
process.on('warning', (e) => console.warn(e.stack));
|
|
73
|
+
//allow more listeners for when we have many concurrent users
|
|
74
|
+
events.EventEmitter.prototype.setMaxListeners(500);
|
|
75
|
+
// const jsdom = require("jsdom");
|
|
76
|
+
// const { JSDOM } = jsdom;
|
|
77
|
+
// const { document } = (new JSDOM(`...`)).window;
|
|
78
|
+
//
|
|
79
|
+
// global['document'] = document;
|
|
80
|
+
global['reactStaticRenderer'] = renderToStaticMarkup;
|
|
81
|
+
autoLoadOntologyData(true);
|
|
82
|
+
let LincdServer = class LincdServer extends Shape {
|
|
83
|
+
/**
|
|
84
|
+
* yarn linked start sends the contents of linked.config.js as an object to this constructor
|
|
85
|
+
* @param n
|
|
86
|
+
*/
|
|
87
|
+
constructor(config) {
|
|
88
|
+
super(typeof config === 'string' || (config && 'id' in config)
|
|
89
|
+
? config
|
|
90
|
+
: undefined);
|
|
91
|
+
this.cachedPaths = new Map();
|
|
92
|
+
this.latestManifest = null;
|
|
93
|
+
this.cssMode = 'scss-modules';
|
|
94
|
+
this.analyse = false;
|
|
95
|
+
this.shapeProviders = new CoreMap();
|
|
96
|
+
this.genericProviders = new CoreMap();
|
|
97
|
+
//from resizedFileName to full resized path (CDN or similar)
|
|
98
|
+
this.resizePathsMap = new Map();
|
|
99
|
+
if (config && typeof config !== 'string' && !('id' in config)) {
|
|
100
|
+
this.config = config;
|
|
101
|
+
}
|
|
102
|
+
this.api = new LincdAPI({ id: process.env.SITE_ROOT + '/api' });
|
|
103
|
+
//Ensure the Server utility (for Server.call()) directly accesses
|
|
104
|
+
// this server on the backend instead of going through a network call
|
|
105
|
+
Server.setLocalServer(this);
|
|
106
|
+
}
|
|
107
|
+
/*async getLincdDependencies(): Promise<string[]> {
|
|
108
|
+
let lincdDependencies = [];
|
|
109
|
+
let dependencies = this.package.dependencies;
|
|
110
|
+
|
|
111
|
+
await Promise.all(
|
|
112
|
+
Object.keys(dependencies).map((dependencyPkgName) => {
|
|
113
|
+
let modulePackageJson = getModulePackageJSON(dependencyPkgName);
|
|
114
|
+
if (modulePackageJson['lincd']) {
|
|
115
|
+
lincdDependencies.push(modulePackageJson.name);
|
|
116
|
+
}
|
|
117
|
+
// //TODO: also iteratively look into dependencies of this dependency
|
|
118
|
+
|
|
119
|
+
// let packagePath;
|
|
120
|
+
// try {
|
|
121
|
+
// packagePath = require.resolve(`${dependencyPkgName}`);
|
|
122
|
+
// } catch (err) {
|
|
123
|
+
// console.warn('Could not find package ' + dependencyPkgName+'. Error: '+err.toString());
|
|
124
|
+
// return;
|
|
125
|
+
// }
|
|
126
|
+
// packagePath = path.dirname(packagePath) + '/package.json'
|
|
127
|
+
// return fs
|
|
128
|
+
// .readFile(packagePath, 'utf-8')
|
|
129
|
+
// .then((res) => {
|
|
130
|
+
// let pkg = JSON.parse(res);
|
|
131
|
+
// if (pkg['lincd']) {
|
|
132
|
+
// lincdDependencies.push(pkg.name);
|
|
133
|
+
// }
|
|
134
|
+
// //TODO: also iteratively look into dependencies of this dependency
|
|
135
|
+
// })
|
|
136
|
+
// .catch((err) => {
|
|
137
|
+
// console.log('Could not read package.json file: '+err);
|
|
138
|
+
// });
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
return lincdDependencies;
|
|
142
|
+
}*/
|
|
143
|
+
get app() {
|
|
144
|
+
return this.server;
|
|
145
|
+
}
|
|
146
|
+
initPackage() {
|
|
147
|
+
this.package = JSON.parse(fsNative.readFileSync(path.resolve(process.cwd(), 'package.json'), 'utf-8'));
|
|
148
|
+
}
|
|
149
|
+
async initOnly() {
|
|
150
|
+
await this.initOntologies();
|
|
151
|
+
await this.initStores();
|
|
152
|
+
this.initPackage();
|
|
153
|
+
this.server = express();
|
|
154
|
+
await this.initBackendProviders();
|
|
155
|
+
syncShapes();
|
|
156
|
+
return this;
|
|
157
|
+
}
|
|
158
|
+
// async serveData(req,res) {
|
|
159
|
+
// let nodeURI = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
|
|
160
|
+
//
|
|
161
|
+
// let store = LinkedStorage.getDatasets().find(store => {
|
|
162
|
+
// return nodeURI.includes(store.namedNode.uri)
|
|
163
|
+
// })
|
|
164
|
+
// }
|
|
165
|
+
async start() {
|
|
166
|
+
this.initPackage();
|
|
167
|
+
// Static assets should come from the static store URL (versioned path),
|
|
168
|
+
// not the upload store URL. storage-config sets STATIC_ACCESS_URL accordingly.
|
|
169
|
+
// In development we deliberately use a relative path so bundle URLs work
|
|
170
|
+
// regardless of which PORT the dev server bound to (browsers resolve
|
|
171
|
+
// relative URLs against window.location.origin). Hardcoding SITE_ROOT
|
|
172
|
+
// baked :4000 into every SSR'd HTML page.
|
|
173
|
+
const isDevAssets = process.env.NODE_ENV === 'development';
|
|
174
|
+
const staticAccessURL = isDevAssets
|
|
175
|
+
? ''
|
|
176
|
+
: (process.env.STATIC_ACCESS_URL ||
|
|
177
|
+
LinkedFileStorage.accessURL ||
|
|
178
|
+
'').replace(/\/$/, '');
|
|
179
|
+
const staticAsset = (assetPath) => `${staticAccessURL}/public${assetPath}`;
|
|
180
|
+
//for apps with multiple bundles this should be read from the webpack build manifest
|
|
181
|
+
this.assets = {
|
|
182
|
+
'main.js': staticAsset('/bundles/main.bundle.js') + '?v=' + this.package.version, //output js bundle from Webpack
|
|
183
|
+
'main.css': staticAsset('/bundles/main.css'), //output css from Webpack
|
|
184
|
+
};
|
|
185
|
+
try {
|
|
186
|
+
const manifestPath = path.resolve(process.cwd(), 'public/bundles/manifest.json');
|
|
187
|
+
if (fsNative.existsSync(manifestPath)) {
|
|
188
|
+
const manifestRaw = fsNative.readFileSync(manifestPath, 'utf-8');
|
|
189
|
+
this.assets.manifest = JSON.parse(manifestRaw);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
console.warn('Could not load bundle manifest:', err);
|
|
194
|
+
}
|
|
195
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
196
|
+
if (!isProduction && this.config.cssMode === 'tailwind') {
|
|
197
|
+
this.assets['tailwind-cdn'] = 'https://cdn.tailwindcss.com';
|
|
198
|
+
}
|
|
199
|
+
//for multicore we use PM2, and each instance needs to listen to port 0.
|
|
200
|
+
// whilst the main thread will listen to env.PORT automatically
|
|
201
|
+
// const PORT = this.config.multiCore ? 0 : process.env.PORT || 3000;
|
|
202
|
+
// const publicPort = process.env.PORT || 3000;
|
|
203
|
+
//update: back to original setup. multicore handles itself inside @semantu/multicore
|
|
204
|
+
const PORT = parseInt(process.env.PORT) || 4000;
|
|
205
|
+
this.server = express();
|
|
206
|
+
const dirName = path.resolve(process.cwd(), 'frontend');
|
|
207
|
+
await this.initOntologies();
|
|
208
|
+
//TODO: when we do not keep all data in memory (and thus do not need to rely on in memory data for page requests), we can possibly remove await here, since all stores handle their own initialisation before executing commands
|
|
209
|
+
await this.initStores();
|
|
210
|
+
this.initGarbageCollection();
|
|
211
|
+
// this.app.use((req, res, next) => {
|
|
212
|
+
// console.log('start request');
|
|
213
|
+
// next();
|
|
214
|
+
// });
|
|
215
|
+
// before controllers
|
|
216
|
+
await this.initBackendProviders();
|
|
217
|
+
syncShapes();
|
|
218
|
+
//START OF EXPRESS ROUTES AND MIDDLEWARE
|
|
219
|
+
//use cors
|
|
220
|
+
// var corsOptions = {
|
|
221
|
+
// origin: ['http://localhost:4001', 'https://www.mynd.site'],
|
|
222
|
+
// optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
|
|
223
|
+
// };
|
|
224
|
+
// //accept JSON bodies
|
|
225
|
+
// this.server.use(bodyParser.json({limit: '50mb'}));
|
|
226
|
+
this.server.use(express.json({ limit: '50mb' }));
|
|
227
|
+
// this.server.use(cors(corsOptions));
|
|
228
|
+
//
|
|
229
|
+
// //compress server output with gzip,
|
|
230
|
+
//UPDATE ive set level to 2 (low compression fast speed) because responses were taking too long (98% of server time was used by compress)
|
|
231
|
+
// this.server.use(compress({level: 2}));
|
|
232
|
+
await this.callGenericBackendProvidersMethod('setupBeforeControllers');
|
|
233
|
+
// if development, run webpack from the server
|
|
234
|
+
// for production you need to build the bundles before starting the server
|
|
235
|
+
const skipBuild = process.env.NO_WEBPACK === 'true';
|
|
236
|
+
if (isDevelopment && !skipBuild) {
|
|
237
|
+
//three levels up because of lib/esm (2) instead of src (1)
|
|
238
|
+
//@ts-ignore
|
|
239
|
+
// const getWebpackConfig = (await import('../../../site.webpack.config.cjs')).default;
|
|
240
|
+
// let webpackConfig = await getWebpackConfig();
|
|
241
|
+
let webpackConfig = await getWebpackAppConfig();
|
|
242
|
+
// const compare = (c1,c2,path?='') => {
|
|
243
|
+
// for(let key1 of Object.keys(c1)) {
|
|
244
|
+
// if(typeof c1[key1] === 'object') {
|
|
245
|
+
// compare(c1[key1],c2[key1],path+='.'+key1);
|
|
246
|
+
// } else {
|
|
247
|
+
// if(c1[key1] !== c2[key1]) {
|
|
248
|
+
// console.log("Difference "+path+": ");
|
|
249
|
+
// console.log(c1[key1]);
|
|
250
|
+
// console.log(c2[key1]);
|
|
251
|
+
// }
|
|
252
|
+
// }
|
|
253
|
+
// }
|
|
254
|
+
// }
|
|
255
|
+
// compare(webpackConfig,webpackConfig2);
|
|
256
|
+
//default to having webpack cache on and filesystem, unless explicitly set to false
|
|
257
|
+
if (this.cacheWebpack === false) {
|
|
258
|
+
webpackConfig.cache.type = 'memory';
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
webpackConfig.cache.type = 'filesystem';
|
|
262
|
+
}
|
|
263
|
+
//clean dist/build folder
|
|
264
|
+
await rimraf(webpackConfig.output.path).catch((err) => {
|
|
265
|
+
if (err) {
|
|
266
|
+
console.warn(err);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
const compiler = webpack(webpackConfig, (err, stats) => {
|
|
270
|
+
//watch build completed
|
|
271
|
+
if (err) {
|
|
272
|
+
console.error(err);
|
|
273
|
+
}
|
|
274
|
+
// Output stats JSON for analysis
|
|
275
|
+
if (this.analyse) {
|
|
276
|
+
fs.writeFile('./data/webpack-stats.json', JSON.stringify(stats.toJson({ all: true }), null, 2)).then(() => {
|
|
277
|
+
console.log('Webpack stats written to ./data/webpack-stats.json');
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
if (!compiler) {
|
|
282
|
+
//something went wrong with webpack config, error will be logged above
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
compiler.hooks.afterEmit.tap('cleanup-the-require-cache', () => {
|
|
286
|
+
// After webpack rebuild, clear the files from the require cache,
|
|
287
|
+
// so that next server side render wil be in sync
|
|
288
|
+
// console.log(Object.keys(require.cache).filter(k => k.includes(dirName)).join("\n"));
|
|
289
|
+
// if (typeof require !== 'undefined') {
|
|
290
|
+
// Object.keys(require.cache)
|
|
291
|
+
// .filter((key) => key.includes(dirName))
|
|
292
|
+
// .forEach((key) => delete require.cache[key]);
|
|
293
|
+
// }
|
|
294
|
+
});
|
|
295
|
+
//after the first emit (which means bundles are ready and the site is running) also rebuild the index files of the site
|
|
296
|
+
let updatedMetadata = false;
|
|
297
|
+
compiler.hooks.afterEmit.tap('update-metadata', () => {
|
|
298
|
+
if (!updatedMetadata) {
|
|
299
|
+
updatedMetadata = true;
|
|
300
|
+
//log updated paths
|
|
301
|
+
// buildMetadata()
|
|
302
|
+
// .then((updatedPaths) => {
|
|
303
|
+
// // if(updatedPaths && updatedPaths.length)
|
|
304
|
+
// // {
|
|
305
|
+
// // console.log(chalk.blueBright('Updated metadata:\n - '+updatedPaths.join('\n - ')));
|
|
306
|
+
// // }
|
|
307
|
+
// })
|
|
308
|
+
// .catch((err) => {
|
|
309
|
+
// console.warn('Could not update metadata: ' + err);
|
|
310
|
+
// });
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
this.server.use(webpackDevMiddleware(compiler, {
|
|
314
|
+
serverSideRender: true,
|
|
315
|
+
publicPath: webpackConfig.output.publicPath,
|
|
316
|
+
stats: {
|
|
317
|
+
children: true,
|
|
318
|
+
version: false,
|
|
319
|
+
chunks: false,
|
|
320
|
+
assets: false,
|
|
321
|
+
entrypoints: false,
|
|
322
|
+
modules: false,
|
|
323
|
+
},
|
|
324
|
+
writeToDisk: true,
|
|
325
|
+
}));
|
|
326
|
+
compiler.hooks.afterEmit.tap('store-manifest', (compilation) => {
|
|
327
|
+
try {
|
|
328
|
+
// Read manifest from the filesystem after webpack writes it
|
|
329
|
+
const manifestPath = path.resolve(compilation.options.output.path, 'manifest.json');
|
|
330
|
+
if (fsNative.existsSync(manifestPath)) {
|
|
331
|
+
const manifestContent = fsNative.readFileSync(manifestPath, 'utf-8');
|
|
332
|
+
this.latestManifest = JSON.parse(manifestContent);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
console.warn('Failed to parse manifest from webpack emit:', err);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
this.server.use(webpackHotMiddleware(compiler, {
|
|
340
|
+
log: false,
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
// //map URL routes to file paths
|
|
344
|
+
const oneYear = 1000 * 60 * 60 * 24 * 365; // in milliseconds
|
|
345
|
+
const oneMonth = 1000 * 60 * 60 * 24 * 30; // in milliseconds
|
|
346
|
+
this.server.use('/public', express.static('./public', {
|
|
347
|
+
maxAge: oneYear, // Tell browser to cache for 1 year
|
|
348
|
+
immutable: true, // Suggest that the content won't change
|
|
349
|
+
}));
|
|
350
|
+
this.server.use('/uploads', express.static('./data/uploads', {
|
|
351
|
+
maxAge: oneYear, // Tell browser to cache for 1 year
|
|
352
|
+
immutable: true, // Suggest that the content won't change
|
|
353
|
+
}));
|
|
354
|
+
this.server.use('/', express.static('./public/root'));
|
|
355
|
+
this.server.use('/favicon.ico', express.static('./public/favicon.ico', {
|
|
356
|
+
maxAge: oneMonth, // Tell browser to cache for 1 year
|
|
357
|
+
immutable: true, // Suggest that the content won't change
|
|
358
|
+
}));
|
|
359
|
+
this.server.use('/.well-known', express.static('./public/.well-known'));
|
|
360
|
+
// this.server.post('/data',this.handleErrorsJson(async (req,res) => this.serveData(req, res)));
|
|
361
|
+
this.server.get('/resized/*', async (req, res) => {
|
|
362
|
+
this.resizeImage(req, res);
|
|
363
|
+
});
|
|
364
|
+
// Scoped-package variants (@scope/pkg). Register BEFORE the unscoped
|
|
365
|
+
// routes — Express `:pkg` won't consume slashes, so a call to
|
|
366
|
+
// `/call/@_linked/auth/signinDev` would otherwise fall through to the
|
|
367
|
+
// 3-segment `:pkg/:shape/:method` route and be misinterpreted as a
|
|
368
|
+
// shape-method call (pkg=`@_linked`, shape=`auth`). These handlers
|
|
369
|
+
// recognise the `@scope/pkg` prefix and rebuild the full package name
|
|
370
|
+
// before dispatching to the same handler used for unscoped packages.
|
|
371
|
+
this.server.post('/call/@:scope/:pkg/:method', this.handleErrorsJson(async (req, res) => {
|
|
372
|
+
req.params.pkg = `@${req.params.scope}/${req.params.pkg}`;
|
|
373
|
+
return this.processBackendMethodCall(req, res);
|
|
374
|
+
}));
|
|
375
|
+
this.server.post('/call/@:scope/:pkg/:shape/:method', this.handleErrorsJson(async (req, res) => {
|
|
376
|
+
req.params.pkg = `@${req.params.scope}/${req.params.pkg}`;
|
|
377
|
+
return this.processShapeMethodCall(req, res);
|
|
378
|
+
}));
|
|
379
|
+
this.server.post('/call/:pkg/:method', this.handleErrorsJson(async (req, res) => this.processBackendMethodCall(req, res)));
|
|
380
|
+
this.server.post('/call/:pkg/:shape/:method', this.handleErrorsJson(async (req, res) => this.processShapeMethodCall(req, res)));
|
|
381
|
+
this.server.post('/api/:method/:action?', this.handleErrorsJson(async (req, res) => this.processAPICall(req, res, 'post')));
|
|
382
|
+
this.server.get('/api/:method/:action?', this.handleErrorsJson(async (req, res) => this.processAPICall(req, res, 'get')));
|
|
383
|
+
// this.server.post(
|
|
384
|
+
// '/api/query/:method',
|
|
385
|
+
// this.handleErrorsJson(async (req, res) => this.processQuery(req, res)),
|
|
386
|
+
// );
|
|
387
|
+
// now that all the middleware is defined, we initialise the providers, before we define a catch-all route
|
|
388
|
+
// before catch all
|
|
389
|
+
// await this.initBackendProviders();
|
|
390
|
+
await this.callGenericBackendProvidersMethod('setupBeforeCatchAllControllers');
|
|
391
|
+
// HEAD catch-all for maintenance/health check (client fetches SITE_ROOT with method: HEAD)
|
|
392
|
+
this.server.head('__health', (_req, res) => {
|
|
393
|
+
res.sendStatus(200);
|
|
394
|
+
});
|
|
395
|
+
this.server.get('*', this.handleErrors(async (req, res) => {
|
|
396
|
+
//make sure the frontend bundle has finished building
|
|
397
|
+
// await this.waitForWebpack();
|
|
398
|
+
this.render(req, res);
|
|
399
|
+
}));
|
|
400
|
+
// after controller
|
|
401
|
+
await this.callGenericBackendProvidersMethod('setupAfterControllers');
|
|
402
|
+
//remove http(s):// and remove port :[port]
|
|
403
|
+
const HOST = process.env.SITE_ROOT.replace(/https?:\/\//, '').replace(/:\d+$/, '');
|
|
404
|
+
//backlog of 1024 means maximum of 1024 connections in the queue (higher than default)
|
|
405
|
+
this.httpServer = this.server.listen({ port: PORT, backlog: 1024 }, () => {
|
|
406
|
+
console.log(`Up and running at http://localhost:${PORT}`);
|
|
407
|
+
// open(`http://localhost:${PORT}`)
|
|
408
|
+
});
|
|
409
|
+
// Ensure all inactive connections are terminated by the ALB, by setting this a few seconds higher than the ALB idle timeout
|
|
410
|
+
this.httpServer.keepAliveTimeout = 60000;
|
|
411
|
+
// Ensure the headersTimeout is set higher than the keepAliveTimeout due to this nodejs regression bug: https://github.com/nodejs/node/issues/27363
|
|
412
|
+
this.httpServer.headersTimeout = 61000;
|
|
413
|
+
this.httpServer.on('error', function (error) {
|
|
414
|
+
if (error['syscall'] !== 'listen') {
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
const isPipe = (portOrPipe) => Number.isNaN(portOrPipe);
|
|
418
|
+
const bind = isPipe(PORT) ? 'Pipe ' + PORT : 'Port ' + PORT;
|
|
419
|
+
switch (error['code']) {
|
|
420
|
+
case 'EACCES':
|
|
421
|
+
console.error(bind + ' requires elevated privileges');
|
|
422
|
+
process.exit(1);
|
|
423
|
+
case 'EADDRINUSE':
|
|
424
|
+
console.error(bind + ' is already in use');
|
|
425
|
+
process.exit(1);
|
|
426
|
+
default:
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
return this;
|
|
431
|
+
}
|
|
432
|
+
async initOntologies() { }
|
|
433
|
+
/**
|
|
434
|
+
*
|
|
435
|
+
* @returns
|
|
436
|
+
* @todo Check if default file store is set, if not, set it
|
|
437
|
+
*/
|
|
438
|
+
async initStores() {
|
|
439
|
+
return Promise.all(LinkedStorage.getDatasets().map((store) => {
|
|
440
|
+
return store.init ? store.init() : Promise.resolve();
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
// get generic backend providers for handle controller methods
|
|
444
|
+
async callGenericBackendProvidersMethod(method, ...args) {
|
|
445
|
+
for (let genericProvider of this.genericProviders.values()) {
|
|
446
|
+
if (!genericProvider || !genericProvider[method]) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (typeof genericProvider[method] == 'function') {
|
|
450
|
+
await Promise.resolve(genericProvider[method](...args));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Removes temporary nodes from memory if they've been seen twice
|
|
456
|
+
* Current interval is once per hour.
|
|
457
|
+
* So after 2 hours temporary nodes are removed.
|
|
458
|
+
*/
|
|
459
|
+
initGarbageCollection() {
|
|
460
|
+
// let seenLastTime:NodeSet<NamedNode> = null;
|
|
461
|
+
// setInterval(() => {
|
|
462
|
+
//
|
|
463
|
+
// let tempNodes = new NodeSet<NamedNode>();
|
|
464
|
+
// NamedNode.getAllNamedNodes().forEach(n => {
|
|
465
|
+
// if(n.isTemporaryNode) {
|
|
466
|
+
// if(seenLastTime && seenLastTime.has(n))
|
|
467
|
+
// {
|
|
468
|
+
// n.remove();
|
|
469
|
+
// } else {
|
|
470
|
+
// tempNodes.add(n);
|
|
471
|
+
// }
|
|
472
|
+
// }
|
|
473
|
+
// })
|
|
474
|
+
// seenLastTime = tempNodes;
|
|
475
|
+
// }, 1000 * 10); //every 10 sec
|
|
476
|
+
// // }, 1000 * 60 * 60); //every hour
|
|
477
|
+
}
|
|
478
|
+
async initBackendProviders() {
|
|
479
|
+
//get all local workspace lincd packages, then filter to only those
|
|
480
|
+
//in this app's dependency tree. This avoids loading npm-installed
|
|
481
|
+
//legacy packages that may use an old version of the core.
|
|
482
|
+
const allLocalPackages = getLincdPackages();
|
|
483
|
+
const localPackageMap = new Map(allLocalPackages.map((pkg) => [pkg.packageName, pkg]));
|
|
484
|
+
const relevantPackages = this.filterPackagesByDependencyTree(localPackageMap, this.package);
|
|
485
|
+
for (let [pkgName, pkg] of relevantPackages) {
|
|
486
|
+
await this.indexPackageBackendProviders(pkgName);
|
|
487
|
+
await this.indexLincdPackage(pkgName);
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
await fs
|
|
491
|
+
.readFile(path.join(process.cwd(), 'package.json'), 'utf-8')
|
|
492
|
+
.then(async (contents) => {
|
|
493
|
+
let pkg = JSON.parse(contents);
|
|
494
|
+
await this.indexPackageBackendProviders(pkg.name, false, path.join(process.cwd(), 'src', 'backend.ts'));
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
console.warn(err);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Filters local workspace packages to only those reachable from the app's
|
|
503
|
+
* dependency tree. Mirrors the logic used by `buildAll` in lincd-cli.
|
|
504
|
+
*/
|
|
505
|
+
filterPackagesByDependencyTree(allPackages, appPackageJson) {
|
|
506
|
+
const relevantPackages = new Map();
|
|
507
|
+
const packagesToCheck = new Set();
|
|
508
|
+
const processedPackages = new Set();
|
|
509
|
+
// Start with direct dependencies from the app
|
|
510
|
+
if (appPackageJson.dependencies) {
|
|
511
|
+
for (const dep of Object.keys(appPackageJson.dependencies)) {
|
|
512
|
+
if (allPackages.has(dep)) {
|
|
513
|
+
packagesToCheck.add(dep);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// Recursively follow each package's dependencies
|
|
518
|
+
while (packagesToCheck.size > 0) {
|
|
519
|
+
const packageName = packagesToCheck.values().next().value;
|
|
520
|
+
packagesToCheck.delete(packageName);
|
|
521
|
+
if (processedPackages.has(packageName)) {
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
processedPackages.add(packageName);
|
|
525
|
+
const packageDetails = allPackages.get(packageName);
|
|
526
|
+
if (packageDetails) {
|
|
527
|
+
relevantPackages.set(packageName, packageDetails);
|
|
528
|
+
const pkg = getPackageJSON(packageDetails.path);
|
|
529
|
+
if (pkg === null || pkg === void 0 ? void 0 : pkg.dependencies) {
|
|
530
|
+
for (const dep of Object.keys(pkg.dependencies)) {
|
|
531
|
+
if (allPackages.has(dep) && !processedPackages.has(dep)) {
|
|
532
|
+
packagesToCheck.add(dep);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return relevantPackages;
|
|
539
|
+
}
|
|
540
|
+
async resizeImage(req, res) {
|
|
541
|
+
var _a;
|
|
542
|
+
//check if the w or h query parameters are set
|
|
543
|
+
let width = req.query.w;
|
|
544
|
+
let height = req.query.h;
|
|
545
|
+
//if not
|
|
546
|
+
if (!width && !height) {
|
|
547
|
+
//redirect to the original image
|
|
548
|
+
res.redirect(req.originalUrl.replace('/resized', '/uploads'));
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
let imageFileName = req.query.src; // example: https://cdnurl.com/uploads/1710814765388-935b511c9_cropped.jpeg
|
|
552
|
+
const accessURL = LinkedFileStorage.accessURL;
|
|
553
|
+
if (imageFileName) {
|
|
554
|
+
// TODO: restrict resizing to images that are stored by LinkedFileStorage.fileExists()
|
|
555
|
+
// if (imageFileName.startsWith(accessURL)) {
|
|
556
|
+
// const exists = await LinkedFileStorage.fileExists(
|
|
557
|
+
// imageFileName.replace(`${accessURL}/`, ''),
|
|
558
|
+
// );
|
|
559
|
+
// extract the base name and extension from the imageFileName
|
|
560
|
+
// example: /uploads/resized/935b511c9_cropped.jpeg
|
|
561
|
+
const url = new URL(imageFileName);
|
|
562
|
+
const { name, ext } = path.parse(url.pathname);
|
|
563
|
+
// append the width and height parameters to the base name
|
|
564
|
+
// example: 935b511c9_cropped_w190.jpeg or 935b511c9_cropped_w190h190.jpeg
|
|
565
|
+
const newName = `${name}${width ? '_w' + width : ''}${height ? 'h' + height : ''}`;
|
|
566
|
+
// create a new pathname with dimensions
|
|
567
|
+
// example: /uploads/resized/935b511c9_cropped.jpeg -> /uploads/resized/935b511c9_cropped_w190.jpeg
|
|
568
|
+
const newPathname = path.join(path.dirname(url.pathname), 'resized', `${newName}${ext}`);
|
|
569
|
+
// remove the leading slash from the pathname
|
|
570
|
+
// example: /uploads/resized/935b511c9_cropped_w190.jpeg -> uploads/resized/935b511c9_cropped_w190.jpeg
|
|
571
|
+
const resizedImageFileName = newPathname.startsWith('/')
|
|
572
|
+
? newPathname.slice(1)
|
|
573
|
+
: newPathname;
|
|
574
|
+
if (this.resizePathsMap.has(resizedImageFileName)) {
|
|
575
|
+
res.redirect(this.resizePathsMap.get(resizedImageFileName));
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
// check if the resized image already exists in the CDN
|
|
579
|
+
const imageExists = await LinkedFileStorage.fileExists(resizedImageFileName);
|
|
580
|
+
// if exists, redirect to the resized image
|
|
581
|
+
if (imageExists) {
|
|
582
|
+
const resizedPathOnCdn = accessURL + '/' + resizedImageFileName;
|
|
583
|
+
//save to cache
|
|
584
|
+
this.resizePathsMap.set(resizedImageFileName, resizedPathOnCdn);
|
|
585
|
+
//redirect this request to the resized image
|
|
586
|
+
res.redirect(resizedPathOnCdn);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
console.log(`${process.pid} - ${process.env.PORT}: Resizing image: ${imageFileName} to ${width} x ${height || ''}`);
|
|
591
|
+
//in multicore development, we need to access the site from 127.0.0.1, and so all requests need to go there
|
|
592
|
+
// // if (process.env.NODE_ENV === 'development' && process.env.NUM_WORKER_PROCESSES) {
|
|
593
|
+
// // imageFileName = imageFileName.replace('localhost', '127.0.0.1');
|
|
594
|
+
// // }
|
|
595
|
+
// get the image from imageFileName
|
|
596
|
+
const image = await globalThis
|
|
597
|
+
.fetch(imageFileName)
|
|
598
|
+
.then((res) => res.arrayBuffer())
|
|
599
|
+
.then((arrayBuffer) => Buffer.from(arrayBuffer))
|
|
600
|
+
.catch((err) => {
|
|
601
|
+
console.warn('Could not fetch image from URL: ' + err);
|
|
602
|
+
return null;
|
|
603
|
+
});
|
|
604
|
+
// if image is null, return 404
|
|
605
|
+
if (!image) {
|
|
606
|
+
res.status(404).send({ error: 'Could not fetch image from URL' });
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
// get the format of the image
|
|
610
|
+
let format;
|
|
611
|
+
try {
|
|
612
|
+
format = await sharp(image)
|
|
613
|
+
.metadata()
|
|
614
|
+
.then((meta) => meta.format);
|
|
615
|
+
}
|
|
616
|
+
catch (err) {
|
|
617
|
+
console.warn('Unsupported image format: ' + err);
|
|
618
|
+
res.status(400).send({ error: 'Unsupported image format' });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
// set the output options based on the format for quality and compression
|
|
622
|
+
let outputOptions;
|
|
623
|
+
switch (format) {
|
|
624
|
+
case 'jpeg':
|
|
625
|
+
outputOptions = { quality: 90 };
|
|
626
|
+
break;
|
|
627
|
+
case 'png':
|
|
628
|
+
outputOptions = { compressionLevel: 9 };
|
|
629
|
+
break;
|
|
630
|
+
case 'webp':
|
|
631
|
+
outputOptions = { quality: 90 };
|
|
632
|
+
break;
|
|
633
|
+
// add more formats here
|
|
634
|
+
default:
|
|
635
|
+
outputOptions = {};
|
|
636
|
+
}
|
|
637
|
+
// resize the image
|
|
638
|
+
const resizedImage = await sharp(image)
|
|
639
|
+
.resize(width ? parseInt(width) : null, height ? parseInt(height) : null)
|
|
640
|
+
.toFormat(format, outputOptions)
|
|
641
|
+
.toBuffer()
|
|
642
|
+
.catch((err) => {
|
|
643
|
+
console.warn('Could not resize image: ' + err);
|
|
644
|
+
return null;
|
|
645
|
+
});
|
|
646
|
+
// if resizedImage is null, return 500
|
|
647
|
+
if (!resizedImage) {
|
|
648
|
+
res.status(500).send({ error: 'Could not resize image' });
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
// upload the resized image to the CDN
|
|
652
|
+
const resizedPathOnCdn = await LinkedFileStorage.saveFile(newPathname, resizedImage);
|
|
653
|
+
//save to cache
|
|
654
|
+
this.resizePathsMap.set(resizedImageFileName, resizedPathOnCdn);
|
|
655
|
+
//redirect this request to the resized image
|
|
656
|
+
res.redirect(resizedPathOnCdn);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
imageFileName = (_a = req.originalUrl.split('/resized/')[1]) === null || _a === void 0 ? void 0 : _a.split('?')[0];
|
|
662
|
+
}
|
|
663
|
+
//if this request has not been made (and stored on the HD) before
|
|
664
|
+
let [trueFileName, ...extensions] = imageFileName.split('.');
|
|
665
|
+
let extension = extensions.join('.');
|
|
666
|
+
let resizedImageFileName = trueFileName +
|
|
667
|
+
'_' +
|
|
668
|
+
(width ? 'w' + width : '') +
|
|
669
|
+
(height ? 'h' + height : '') +
|
|
670
|
+
'.' +
|
|
671
|
+
extension;
|
|
672
|
+
let resizedFilePath = path.join(process.cwd(), 'data', 'uploads', 'resized', resizedImageFileName);
|
|
673
|
+
if (!fsNative.existsSync(resizedFilePath)) {
|
|
674
|
+
//ensure the resized folder exists
|
|
675
|
+
// if (!fsNative.existsSync(path.join(process.cwd(), 'data', 'uploads', 'resized'))) {
|
|
676
|
+
// fsNative.mkdirSync(path.join(process.cwd(), 'data', 'uploads', 'resized'), {recursive: true});
|
|
677
|
+
// }
|
|
678
|
+
//then lets resize and store the image:
|
|
679
|
+
// resize the image with sharp and return it
|
|
680
|
+
let originalImagePath = path.join(process.cwd(), 'data', 'uploads', imageFileName);
|
|
681
|
+
if (!fsNative.existsSync(originalImagePath)) {
|
|
682
|
+
console.warn('Could not find original image at ' + originalImagePath);
|
|
683
|
+
return res.status(404).send({ error: 'Could not find original image' });
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
let image = sharp(originalImagePath);
|
|
687
|
+
image.resize(width ? parseInt(width) : null, height ? parseInt(height) : null);
|
|
688
|
+
//write image to disk
|
|
689
|
+
await image
|
|
690
|
+
.toFile(resizedFilePath)
|
|
691
|
+
.then(() => {
|
|
692
|
+
// console.log('resized image written to disk: ' + resizedFilePath);
|
|
693
|
+
})
|
|
694
|
+
.catch((err) => {
|
|
695
|
+
console.warn('Could not write resized image to disk at ' +
|
|
696
|
+
resizedFilePath +
|
|
697
|
+
': ' +
|
|
698
|
+
err);
|
|
699
|
+
});
|
|
700
|
+
//
|
|
701
|
+
// //get content type from the file extension
|
|
702
|
+
// let contentType;
|
|
703
|
+
// let extension = imageFileName.split('.').pop();
|
|
704
|
+
// switch (extension) {
|
|
705
|
+
// case 'jpg':
|
|
706
|
+
// case 'jpeg':
|
|
707
|
+
// contentType = 'image/jpeg';
|
|
708
|
+
// break;
|
|
709
|
+
// case 'png':
|
|
710
|
+
// contentType = 'image/png';
|
|
711
|
+
// break;
|
|
712
|
+
// case 'gif':
|
|
713
|
+
// contentType = 'image/gif';
|
|
714
|
+
// break;
|
|
715
|
+
// default:
|
|
716
|
+
// contentType = 'image/jpeg';
|
|
717
|
+
// }
|
|
718
|
+
// res.setHeader('Content-Type', contentType);
|
|
719
|
+
// image.pipe(res);
|
|
720
|
+
}
|
|
721
|
+
catch (err) {
|
|
722
|
+
console.warn(err);
|
|
723
|
+
res.status(500).send({ error: 'Could not resize image' });
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
//send the resized image
|
|
727
|
+
res.sendFile(resizedFilePath);
|
|
728
|
+
}
|
|
729
|
+
async indexLincdPackage(pkg, warnIfNotFound = false) {
|
|
730
|
+
if (pkg === this.package.name) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
// console.log(`🔍 Loading package: ${pkg}`);
|
|
735
|
+
// console.log(
|
|
736
|
+
// `🔍 Module resolution for ${pkg}:`,
|
|
737
|
+
//@ts-ignore
|
|
738
|
+
// await import.meta.resolve(pkg)
|
|
739
|
+
// );
|
|
740
|
+
await import(pkg);
|
|
741
|
+
// console.log(`✅ Successfully loaded: ${pkg}`);
|
|
742
|
+
}
|
|
743
|
+
catch (e) {
|
|
744
|
+
let providerNotFound = e.code === 'MODULE_NOT_FOUND' &&
|
|
745
|
+
e.message.indexOf(`Cannot find package '${pkg}'`) !== -1;
|
|
746
|
+
if (providerNotFound) {
|
|
747
|
+
// console.warn('Error loading ' + providerPath + ': ' + e.stack);
|
|
748
|
+
if (warnIfNotFound) {
|
|
749
|
+
console.warn(chalk.magenta(`Could not load package ${pkg}`, typeof module !== 'undefined' && typeof exports !== 'undefined'
|
|
750
|
+
? //@ts-ignore
|
|
751
|
+
' at ' + (await import.meta.resolve(pkg))
|
|
752
|
+
: ''));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
console.warn(chalk.red(`Error loading '${pkg}' ${typeof module !== 'undefined' && typeof exports !== 'undefined'
|
|
757
|
+
? //@ts-ignore
|
|
758
|
+
' at ' + (await import.meta.resolve(pkg))
|
|
759
|
+
: ''}: ${e.message}\n`), e.stack);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async indexPackageBackendProviders(pkg, warnIfNotFound = false, backendIndexFilePath) {
|
|
764
|
+
if (!backendIndexFilePath) {
|
|
765
|
+
backendIndexFilePath = `${pkg}/backend`;
|
|
766
|
+
}
|
|
767
|
+
let backendProviderExports;
|
|
768
|
+
let genericBackendProvider;
|
|
769
|
+
let shapeProviders = [];
|
|
770
|
+
await import(backendIndexFilePath)
|
|
771
|
+
.then((backendProviderExports) => {
|
|
772
|
+
//instantiate the exported provider classes and add them to the right place
|
|
773
|
+
Object.keys(backendProviderExports).forEach((key) => {
|
|
774
|
+
let providerClass = backendProviderExports[key];
|
|
775
|
+
//always send an instance of the express server
|
|
776
|
+
//TODO: do not create an instance, just save the class and instantiate it when needed
|
|
777
|
+
let provider = new providerClass(this.server, this);
|
|
778
|
+
if (provider instanceof ShapeProvider) {
|
|
779
|
+
shapeProviders.push(provider);
|
|
780
|
+
if (!Object.getOwnPropertyNames(provider).includes('shape')) {
|
|
781
|
+
console.warn(chalk.red(`${Object.getPrototypeOf(provider).constructor.name} in package ${pkg}
|
|
782
|
+
is not properly linked to a shape. Use public shape = SomeShape.`));
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
if (genericBackendProvider) {
|
|
787
|
+
console.warn(`Package ${pkg} exports two generic backend providers. Only one will work`);
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
genericBackendProvider = provider;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
})
|
|
795
|
+
.catch((e) => {
|
|
796
|
+
const match = e.message.match(/module \'([^\']+)'/);
|
|
797
|
+
//check that the imported /backend path is not found (and only that path, not an import IN that file that is not found, that should still throw an error)
|
|
798
|
+
let providerNotFound = e.code === 'ERR_MODULE_NOT_FOUND' &&
|
|
799
|
+
e.message.indexOf(`Cannot find module`) !== -1 &&
|
|
800
|
+
match &&
|
|
801
|
+
match[1] &&
|
|
802
|
+
match[1].includes('/backend');
|
|
803
|
+
if (providerNotFound) {
|
|
804
|
+
// console.warn('Error loading ' + providerPath + ': ' + e.stack);
|
|
805
|
+
if (warnIfNotFound) {
|
|
806
|
+
console.warn(chalk.magenta(`Could not find backend file of package ${pkg}.
|
|
807
|
+
Check:\n
|
|
808
|
+
- Make sure backend.ts exists and is included in tsconfig.json\n
|
|
809
|
+
- Make sure the package name in src/package.ts matches the package name in package.json`));
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
console.warn(chalk.red(`Could not load backend file of module '${pkg}' from ${process.cwd()}:\n`), e.stack);
|
|
814
|
+
}
|
|
815
|
+
genericBackendProvider = null;
|
|
816
|
+
});
|
|
817
|
+
this.genericProviders.set(pkg, genericBackendProvider);
|
|
818
|
+
this.shapeProviders.set(pkg, shapeProviders);
|
|
819
|
+
return { backendProviderExports, shapeProviders };
|
|
820
|
+
}
|
|
821
|
+
async processBackendMethodCall(request, response) {
|
|
822
|
+
this.noCache(response);
|
|
823
|
+
await this.initRequest(request, response);
|
|
824
|
+
let { pkg, method } = request.params;
|
|
825
|
+
let { args } = JSONParser.parseObject(request.body);
|
|
826
|
+
return this.callBackendMethod(pkg, method, args, request, response).then((result) => {
|
|
827
|
+
//some methods of backend providers may choose to work with request/response directly and will not return anything
|
|
828
|
+
//so only if a result is returned
|
|
829
|
+
if (typeof result !== 'undefined') {
|
|
830
|
+
//do we convert it to JSON and send it to the frontend
|
|
831
|
+
this.sendJson(response, result);
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
//in other cases, we still need to close the request and send an empty response
|
|
835
|
+
if (!response.headersSent) {
|
|
836
|
+
this.sendJson(response, null);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
noCache(response) {
|
|
842
|
+
response.setHeader('Surrogate-Control', 'no-store');
|
|
843
|
+
response.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
844
|
+
response.setHeader('Expires', '0');
|
|
845
|
+
}
|
|
846
|
+
async processAPICall(request, response, method) {
|
|
847
|
+
this.noCache(response);
|
|
848
|
+
const result = await this.api.process(request, response, method);
|
|
849
|
+
}
|
|
850
|
+
// async processQuery(request, response) {
|
|
851
|
+
//
|
|
852
|
+
// this.api.process(request, response);
|
|
853
|
+
// }
|
|
854
|
+
async processShapeMethodCall(request, response) {
|
|
855
|
+
var _a, _b;
|
|
856
|
+
this.noCache(response);
|
|
857
|
+
// console.log('process shape call');
|
|
858
|
+
// response.on('finish', function() {
|
|
859
|
+
// console.log('shape method call request finish');
|
|
860
|
+
// });
|
|
861
|
+
//
|
|
862
|
+
// response.on('close', function() {
|
|
863
|
+
// console.log('close');
|
|
864
|
+
// });
|
|
865
|
+
//
|
|
866
|
+
// response.on('end', function() {
|
|
867
|
+
// console.log('end');
|
|
868
|
+
// });
|
|
869
|
+
//
|
|
870
|
+
// response.on('header', function() {
|
|
871
|
+
// console.log('header');
|
|
872
|
+
// console.log(response.statusCode);
|
|
873
|
+
// });
|
|
874
|
+
await this.initRequest(request, response);
|
|
875
|
+
let { pkg, shape, method } = request.params;
|
|
876
|
+
let { shapeURI, instanceNode, args } = JSONParser.parseObject(request.body);
|
|
877
|
+
if (!shapeURI) {
|
|
878
|
+
if ((_a = request.query) === null || _a === void 0 ? void 0 : _a.shapeURI) {
|
|
879
|
+
shapeURI = (_b = request.query) === null || _b === void 0 ? void 0 : _b.shapeURI;
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
response.status(500).send({
|
|
883
|
+
error: 'Invalid server call request: ' + request.originalUrl,
|
|
884
|
+
});
|
|
885
|
+
console.warn(chalk.red('Invalid server call request: ' + request.originalUrl));
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return this.callShapeMethod(pkg, method, shapeURI, instanceNode, args, request, response).then((result) => {
|
|
890
|
+
//we return json if something was returned or, if nothing was returned, we still close the request if the method has not accessed response itself already to send things over
|
|
891
|
+
if (typeof result !== 'undefined' || !response.headersSent) {
|
|
892
|
+
this.sendJson(response, result);
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
async callShapeMethod(pkg, method, shapeURI, instanceNode, args, request, response) {
|
|
897
|
+
var _a;
|
|
898
|
+
//- index module providers if not done yet
|
|
899
|
+
if (!this.shapeProviders.has(pkg)) {
|
|
900
|
+
await this.indexPackageBackendProviders(pkg, true);
|
|
901
|
+
}
|
|
902
|
+
let packageShapeProviders = this.shapeProviders.get(pkg);
|
|
903
|
+
let findProviderForShape = (nodeShapeId) => {
|
|
904
|
+
return packageShapeProviders.find((provider) => {
|
|
905
|
+
var _a, _b;
|
|
906
|
+
//access the static shape (which is a linkedShape() / Shape class)
|
|
907
|
+
//then access the SHACL NodeShape of that shape class, and its id
|
|
908
|
+
return (provider instanceof ShapeProvider &&
|
|
909
|
+
((_b = (_a = provider.shape) === null || _a === void 0 ? void 0 : _a.shape) === null || _b === void 0 ? void 0 : _b.id) === nodeShapeId);
|
|
910
|
+
});
|
|
911
|
+
};
|
|
912
|
+
try {
|
|
913
|
+
//- find matching provider
|
|
914
|
+
let shapeClass = getShapeClass(shapeURI);
|
|
915
|
+
let shapeProvider = findProviderForShape(shapeClass.shape.id);
|
|
916
|
+
if (!shapeProvider) {
|
|
917
|
+
let superShapeClasses = getSuperShapesClasses(shapeClass);
|
|
918
|
+
for (let superShapeClass of superShapeClasses) {
|
|
919
|
+
let superShapeProvider = findProviderForShape((_a = superShapeClass.shape) === null || _a === void 0 ? void 0 : _a.id);
|
|
920
|
+
if (superShapeProvider) {
|
|
921
|
+
shapeProvider = superShapeProvider;
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (shapeProvider) {
|
|
927
|
+
//NOTE: if this is a direct call from backend to backend, we won't know the request & response here because those don't get passed to the Server utility
|
|
928
|
+
//if this is an issue, we need to see how we can get those back
|
|
929
|
+
if (request && response) {
|
|
930
|
+
//give the provider a chance to prepare for this request
|
|
931
|
+
//wrap the call in a promise and wait for it, because providers MAY return a promise
|
|
932
|
+
await Promise.resolve(shapeProvider.initRequest(request, response));
|
|
933
|
+
}
|
|
934
|
+
//see if the shapeProvider implements the called method
|
|
935
|
+
if (shapeProvider[method]) {
|
|
936
|
+
//prepare the first argument
|
|
937
|
+
//we want to convert the instance node into an instance of the shape that this shapeProvider provides for
|
|
938
|
+
//let's find the shape class
|
|
939
|
+
if (shapeProvider.shape) {
|
|
940
|
+
//instance nodes are not always sent. A shape can also call a shape provider from a static method without a shape instance.
|
|
941
|
+
if (instanceNode) {
|
|
942
|
+
let providerShapeClass = shapeProvider.shape;
|
|
943
|
+
let instance = new providerShapeClass({
|
|
944
|
+
id: instanceNode.id,
|
|
945
|
+
});
|
|
946
|
+
//always send instanceNode as first argument to static provider methods
|
|
947
|
+
args.unshift(instance);
|
|
948
|
+
}
|
|
949
|
+
try {
|
|
950
|
+
//call the method with the given arguments
|
|
951
|
+
let result = await Promise.resolve(shapeProvider[method].apply(shapeProvider, args));
|
|
952
|
+
return result;
|
|
953
|
+
}
|
|
954
|
+
catch (e) {
|
|
955
|
+
console.warn(`Error whilst calling ${Object.getPrototypeOf(shapeProvider).constructor.name}.${method}(): `, e);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
console.warn(`${Object.getPrototypeOf(shapeProvider).constructor.name} does not define its own 'static shape' property. Please connect the provider to a shape.`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
return this.sendError(response, 501, `${Object.getPrototypeOf(shapeProvider).constructor.name} does not have a method called ${method}`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
else {
|
|
967
|
+
return this.sendError(response, 501, "Could not find provider for shape '" + shapeURI + "'");
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
catch (err) {
|
|
971
|
+
console.warn(`Error whilst trying to access provider of ${pkg}: `, err);
|
|
972
|
+
}
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
handleErrors(fn) {
|
|
976
|
+
return async function (req, res, next) {
|
|
977
|
+
try {
|
|
978
|
+
return await fn(req, res);
|
|
979
|
+
}
|
|
980
|
+
catch (x) {
|
|
981
|
+
console.log(x);
|
|
982
|
+
next(x);
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
handleErrorsJson(fn) {
|
|
987
|
+
return async (req, res, next) => {
|
|
988
|
+
try {
|
|
989
|
+
return await fn(req, res);
|
|
990
|
+
}
|
|
991
|
+
catch (err) {
|
|
992
|
+
this.sendError(res, 500, 'internal server error' + (isDevelopment ? ': ' + (err === null || err === void 0 ? void 0 : err.stack) : ''), 'internal server error: ' + (err === null || err === void 0 ? void 0 : err.stack));
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
sendError(res, statusCode = 500, message, logMessage) {
|
|
997
|
+
res === null || res === void 0 ? void 0 : res.status(statusCode);
|
|
998
|
+
if (message) {
|
|
999
|
+
res === null || res === void 0 ? void 0 : res.send({ error: message });
|
|
1000
|
+
console.warn(chalk.red(logMessage || message));
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
async render(req, res) {
|
|
1004
|
+
var _a, _b, _c, _d;
|
|
1005
|
+
res.socket.on('error', (error) => {
|
|
1006
|
+
console.error('Fatal socket error', error);
|
|
1007
|
+
});
|
|
1008
|
+
//when render() is called, the request is always an 'initial page request', and the server which will return HTML (so this is a SSR request)
|
|
1009
|
+
//in that case we send data back to the frontend in a <script> tag.
|
|
1010
|
+
//We initiate that object here
|
|
1011
|
+
if (req['frontendData'] == null) {
|
|
1012
|
+
req['frontendData'] = {};
|
|
1013
|
+
}
|
|
1014
|
+
//if we are caching this page
|
|
1015
|
+
if (((_a = this.config.server) === null || _a === void 0 ? void 0 : _a.cachePaths) &&
|
|
1016
|
+
this.config.server.cachePaths.includes(req.url)) {
|
|
1017
|
+
//if there is a cach for this path, send it
|
|
1018
|
+
if (this.cachedPaths.has(req.url)) {
|
|
1019
|
+
// console.log(req.url + ': Returning cached path');
|
|
1020
|
+
const html = this.cachedPaths.get(req.url);
|
|
1021
|
+
// res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1022
|
+
// res.setHeader('Content-Length', Buffer.byteLength(html, 'utf8').toString());
|
|
1023
|
+
// res.status(200).end(html);
|
|
1024
|
+
res.send(html);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
let didError = false;
|
|
1029
|
+
let App = ((_b = this.config.server) === null || _b === void 0 ? void 0 : _b.loadAppComponent)
|
|
1030
|
+
? await this.config.server.loadAppComponent()
|
|
1031
|
+
: null;
|
|
1032
|
+
await this.initRequest(req, res);
|
|
1033
|
+
let { requestLD, requestObject } = await this.getRequestData(req, res);
|
|
1034
|
+
//on the backend we need to inform the hook of the request-data value
|
|
1035
|
+
//on the frontend it will be read from the HTML
|
|
1036
|
+
// setRequestData(requestObject);
|
|
1037
|
+
// Abandon and switch to client rendering if enough time passes.
|
|
1038
|
+
// Try lowering this to see the client recover.
|
|
1039
|
+
// Abandon and switch to client rendering if enough time passes.
|
|
1040
|
+
// Try lowering this to see the client recover.
|
|
1041
|
+
let stream;
|
|
1042
|
+
let timedout = false;
|
|
1043
|
+
// const timeout = setTimeout(() => {
|
|
1044
|
+
// stream.abort(`${req.url}: ⏱ SSR stream took too long — force abort`);
|
|
1045
|
+
// if (!res.headersSent) {
|
|
1046
|
+
// res.statusCode = 500;
|
|
1047
|
+
// res.end('SSR timed out');
|
|
1048
|
+
// }
|
|
1049
|
+
// timedout = true;
|
|
1050
|
+
// }, 8_000);
|
|
1051
|
+
let manifest = this.latestManifest || this.assets.manifest || {};
|
|
1052
|
+
let preloadScripts = [];
|
|
1053
|
+
let preloadStyles = [];
|
|
1054
|
+
let matchedRouteKey = null;
|
|
1055
|
+
// Load routes config if available and extract preload chunks for the current route
|
|
1056
|
+
if ((_c = this.config.server) === null || _c === void 0 ? void 0 : _c.loadRoutes) {
|
|
1057
|
+
try {
|
|
1058
|
+
const routesModule = await this.config.server.loadRoutes();
|
|
1059
|
+
const ROUTES = routesModule.ROUTES ||
|
|
1060
|
+
((_d = routesModule.default) === null || _d === void 0 ? void 0 : _d.ROUTES) ||
|
|
1061
|
+
routesModule;
|
|
1062
|
+
// Match the current request path to a route
|
|
1063
|
+
let matchedRoute = null;
|
|
1064
|
+
for (const [key, route] of Object.entries(ROUTES)) {
|
|
1065
|
+
if (!(route === null || route === void 0 ? void 0 : route.path))
|
|
1066
|
+
continue;
|
|
1067
|
+
const pathPattern = route.path
|
|
1068
|
+
.replace(/:\w+\??/g, '([^/]+)')
|
|
1069
|
+
.replace(/\*/g, '.*');
|
|
1070
|
+
const regex = new RegExp('^' + pathPattern + '$');
|
|
1071
|
+
if (regex.test(req.path)) {
|
|
1072
|
+
matchedRoute = route;
|
|
1073
|
+
matchedRouteKey = key;
|
|
1074
|
+
break;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
// If we found a matching route with preloadChunks, resolve them to URLs (both JS and CSS)
|
|
1078
|
+
if ((matchedRoute === null || matchedRoute === void 0 ? void 0 : matchedRoute.preloadChunks) &&
|
|
1079
|
+
Array.isArray(matchedRoute.preloadChunks)) {
|
|
1080
|
+
preloadScripts = matchedRoute.preloadChunks
|
|
1081
|
+
.map((chunkName) => manifest[`${chunkName}.js`] ||
|
|
1082
|
+
manifest[`${chunkName}.bundle.js`] ||
|
|
1083
|
+
manifest[`${chunkName}.mjs`])
|
|
1084
|
+
.filter(Boolean);
|
|
1085
|
+
// Also collect CSS chunks for the same routes
|
|
1086
|
+
preloadStyles = matchedRoute.preloadChunks
|
|
1087
|
+
.map((chunkName) => manifest[`${chunkName}.css`])
|
|
1088
|
+
.filter(Boolean);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
catch (err) {
|
|
1092
|
+
console.warn('Failed to resolve preload chunks:', err);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
// Add matched route key to request object for Html component
|
|
1096
|
+
req['matchedRouteKey'] = matchedRouteKey;
|
|
1097
|
+
stream = renderToPipeableStream(React.createElement(React.StrictMode, null,
|
|
1098
|
+
React.createElement(StaticRouter, { location: req.url },
|
|
1099
|
+
React.createElement(AppContextProvider, { assets: this.assets, requestLD: requestLD, requestObject: requestObject, preloadScripts: preloadScripts, preloadStyles: preloadStyles, expressRequest: req, expressResponse: res },
|
|
1100
|
+
React.createElement(App, null)))), {
|
|
1101
|
+
bootstrapScripts: [
|
|
1102
|
+
this.assets['main.js'], //generated webpack bundle from frontend/src
|
|
1103
|
+
],
|
|
1104
|
+
onShellReady: function () {
|
|
1105
|
+
var _a;
|
|
1106
|
+
res.statusCode = didError ? 500 : 200;
|
|
1107
|
+
res.setHeader('Content-type', 'text/html');
|
|
1108
|
+
// Create a caching transform stream à la mxstbr.com/thoughts/streaming-ssr
|
|
1109
|
+
if (((_a = this.config.server) === null || _a === void 0 ? void 0 : _a.cachePaths) &&
|
|
1110
|
+
this.config.server.cachePaths.includes(req.url)) {
|
|
1111
|
+
const bufferedChunks = [];
|
|
1112
|
+
const cacheStream = new Transform({
|
|
1113
|
+
transform(chunk, _enc, cb) {
|
|
1114
|
+
bufferedChunks.push(chunk); // keep a copy
|
|
1115
|
+
cb(null, chunk); // forward unchanged
|
|
1116
|
+
},
|
|
1117
|
+
flush: (cb) => {
|
|
1118
|
+
const html = Buffer.concat(bufferedChunks).toString('utf8');
|
|
1119
|
+
this.cachedPaths.set(req.url, html);
|
|
1120
|
+
if (this.config.server.cacheTimeout) {
|
|
1121
|
+
setTimeout(() => this.cachedPaths.delete(req.url), this.config.server.cacheTimeout);
|
|
1122
|
+
}
|
|
1123
|
+
clearTimeout(timeout); // rendering finished → stop timer
|
|
1124
|
+
cb();
|
|
1125
|
+
},
|
|
1126
|
+
});
|
|
1127
|
+
// Pipe the caching stream into the real response
|
|
1128
|
+
cacheStream.pipe(res);
|
|
1129
|
+
// React may **only be piped once**, so pipe it to the cacheStream
|
|
1130
|
+
stream.pipe(cacheStream);
|
|
1131
|
+
}
|
|
1132
|
+
else {
|
|
1133
|
+
stream.pipe(res);
|
|
1134
|
+
}
|
|
1135
|
+
}.bind(this),
|
|
1136
|
+
onShellError(x) {
|
|
1137
|
+
didError = true;
|
|
1138
|
+
console.error(x);
|
|
1139
|
+
},
|
|
1140
|
+
});
|
|
1141
|
+
// Abandon and switch to client rendering if enough time passes.
|
|
1142
|
+
// Try lowering this to see the client recover.
|
|
1143
|
+
const timeout = setTimeout(() => {
|
|
1144
|
+
stream.abort('⏱ SSR stream took too long — force abort');
|
|
1145
|
+
if (!res.headersSent) {
|
|
1146
|
+
res.statusCode = 500;
|
|
1147
|
+
res.end('SSR timed out');
|
|
1148
|
+
}
|
|
1149
|
+
}, 10000);
|
|
1150
|
+
res.on('close', () => {
|
|
1151
|
+
clearTimeout(timeout); // if you're using a safety timeout
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
//If we ever need to use multiple webpack entry-points and different bundle names, then uncomment this
|
|
1155
|
+
/*getWebpackAssets()
|
|
1156
|
+
{
|
|
1157
|
+
// const { devMiddleware } = res.locals.webpack;
|
|
1158
|
+
// const outputFileSystem = devMiddleware.outputFileSystem;
|
|
1159
|
+
// const jsonWebpackStats = devMiddleware.stats.toJson();
|
|
1160
|
+
// const { assetsByChunkName, outputPath } = jsonWebpackStats;
|
|
1161
|
+
//
|
|
1162
|
+
// let normalizedAssets = normalizeAssets(assetsByChunkName.main);
|
|
1163
|
+
// let cssAssets = normalizedAssets
|
|
1164
|
+
// .filter((path) => path.endsWith(".css"))
|
|
1165
|
+
// .map((filePath) => outputFileSystem.readFileSync(path.join(outputPath, filePath)))
|
|
1166
|
+
// .join("\n");
|
|
1167
|
+
//
|
|
1168
|
+
// let jsAssets = normalizeAssets(assetsByChunkName.main)
|
|
1169
|
+
// .filter((path) => path.endsWith(".js"))
|
|
1170
|
+
// .map((path) => `<script src="${path}"></script>`)
|
|
1171
|
+
// .join("\n")}
|
|
1172
|
+
}*/
|
|
1173
|
+
sendJson(res, obj) {
|
|
1174
|
+
let jsonObject = JSONWriter.toJsObject(obj);
|
|
1175
|
+
res.json(jsonObject);
|
|
1176
|
+
}
|
|
1177
|
+
async initRequest(request, response) {
|
|
1178
|
+
// initialise the request for all providers, do it synchroniously, one after the other
|
|
1179
|
+
let p = Promise.resolve();
|
|
1180
|
+
[...this.genericProviders.values()]
|
|
1181
|
+
.filter(Boolean)
|
|
1182
|
+
.forEach((backendProvider) => {
|
|
1183
|
+
p = p
|
|
1184
|
+
.then(() => {
|
|
1185
|
+
return backendProvider.initRequest(request, response);
|
|
1186
|
+
})
|
|
1187
|
+
.catch((err) => {
|
|
1188
|
+
console.warn(`Error during initRequest for provider ${Object.getPrototypeOf(backendProvider).constructor.name}: `, err);
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
1191
|
+
return p;
|
|
1192
|
+
}
|
|
1193
|
+
async getRequestData(request, response) {
|
|
1194
|
+
// Phase 1: providers return plain JSON data via supplyDataForRequest.
|
|
1195
|
+
// requestLD is kept as empty string for now — SSR data seeding will be
|
|
1196
|
+
// reworked in Phase 2/3 to inject query results instead of graph data.
|
|
1197
|
+
let requestData = {};
|
|
1198
|
+
await Promise.all([...this.genericProviders.values()].map((backendProvider) => {
|
|
1199
|
+
if (backendProvider) {
|
|
1200
|
+
return Promise.resolve(backendProvider.supplyDataForRequest(request, response, requestData)).catch((err) => {
|
|
1201
|
+
console.warn(`Error requesting page-request data from ${Object.getPrototypeOf(backendProvider).constructor.name}: `, err);
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
}));
|
|
1205
|
+
let requestLD = '';
|
|
1206
|
+
let requestObject = JSONWriter.stringify(request['frontendData']);
|
|
1207
|
+
return { requestLD, requestObject };
|
|
1208
|
+
}
|
|
1209
|
+
async callBackendMethod(pkg, method, args, request, response) {
|
|
1210
|
+
if (!this.genericProviders.has(pkg)) {
|
|
1211
|
+
await this.indexPackageBackendProviders(pkg, true);
|
|
1212
|
+
}
|
|
1213
|
+
//retrieve the indexed provider class and create a new instance for this request
|
|
1214
|
+
let genericBackendProvider = this.genericProviders.get(pkg);
|
|
1215
|
+
let result;
|
|
1216
|
+
if (!genericBackendProvider) {
|
|
1217
|
+
console.warn(`${chalk.magenta(pkg)} does not have a generic backend provider. If you can edit this package, make sure 'backend.ts' is included in 'tsconfig.json' and that it exports a provider.`);
|
|
1218
|
+
return null;
|
|
1219
|
+
}
|
|
1220
|
+
//test if there is a matching method in the backend provider
|
|
1221
|
+
if (!genericBackendProvider[method]) {
|
|
1222
|
+
console.warn(`Generic provider '${Object.getPrototypeOf(genericBackendProvider).constructor.name}' of ${pkg} does not have a method called ${method}`);
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1225
|
+
try {
|
|
1226
|
+
//TODO: remove init request.
|
|
1227
|
+
//TODO: refactor this.request and this.response to a request parameter
|
|
1228
|
+
//NOTE: if this is a direct call from backend to backend, we won't know the request & response here because those don't get passed to the Server utility
|
|
1229
|
+
//if this is an issue, we need to see how we can get those back
|
|
1230
|
+
if (request && response) {
|
|
1231
|
+
//initialise the request for this specific provider
|
|
1232
|
+
//wrap the call in a promise and wait for it, because providers MAY return a promise
|
|
1233
|
+
await Promise.resolve(genericBackendProvider.initRequest(request, response));
|
|
1234
|
+
}
|
|
1235
|
+
// args.push(request);
|
|
1236
|
+
// args.push(response);
|
|
1237
|
+
//call the method with the given arguments and return the result as json
|
|
1238
|
+
result = await Promise.resolve(genericBackendProvider[method].apply(genericBackendProvider, args));
|
|
1239
|
+
}
|
|
1240
|
+
catch (e) {
|
|
1241
|
+
console.warn(`Error whilst calling ${method}() in provider ${Object.getPrototypeOf(genericBackendProvider).constructor.name} of package ${pkg}:\n`, e);
|
|
1242
|
+
// error logging
|
|
1243
|
+
LinkedErrorLogging.log(e);
|
|
1244
|
+
}
|
|
1245
|
+
return result;
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
/**
|
|
1249
|
+
* indicates that instances of this shape need to have this rdf.type
|
|
1250
|
+
*/
|
|
1251
|
+
LincdServer.targetClass = lincdServer.LincdServer;
|
|
1252
|
+
LincdServer = __decorate([
|
|
1253
|
+
linkedShape,
|
|
1254
|
+
__metadata("design:paramtypes", [Object])
|
|
1255
|
+
], LincdServer);
|
|
1256
|
+
export { LincdServer };
|
|
1257
|
+
//# sourceMappingURL=LincdServer.js.map
|