update
This commit is contained in:
52
ts/embedded-ui.generated.ts
Normal file
52
ts/embedded-ui.generated.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -6,10 +6,9 @@ process.env.UIP_ENV = process.env.BACKEND_URL?.includes('develop-backend') ? 'de
|
||||
|
||||
export const defaultPublicServer = new UiPublicServer({
|
||||
port: 3000,
|
||||
packageBaseDirectory: './public/',
|
||||
npmRegistryUrl: 'https://registry.npmjs.org/',
|
||||
allowedPackages: [
|
||||
'@pushrocks/smartfile'
|
||||
'@push.rocks/smartfile'
|
||||
],
|
||||
mode: process.env.UIP_ENV === 'dev' ? 'dev' : 'prod',
|
||||
log: false,
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import * as interfaces from './interfaces.js';
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
import * as ntml from './ntml/index.js';
|
||||
import { getEmbeddedFile, hasEmbeddedFile, listEmbeddedFiles } from './embedded-ui.generated.js';
|
||||
|
||||
export interface IPackageConfig {
|
||||
baseDirectory?: string; // Override base directory for this package
|
||||
registry?: string; // Override registry URL for this package
|
||||
}
|
||||
|
||||
export interface IPublicServerOptions {
|
||||
packageBaseDirectory?: string;
|
||||
npmRegistryUrl?: string;
|
||||
allowedPackages?: string[];
|
||||
packageBaseDirectory?: string; // Default base directory (default: './')
|
||||
npmRegistryUrl?: string; // Default registry URL
|
||||
allowedPackages?: string[]; // List of allowed package names
|
||||
packageConfigs?: Record<string, IPackageConfig>; // Per-package configuration
|
||||
port?: number;
|
||||
mode: 'dev' | 'prod';
|
||||
log?: boolean;
|
||||
@@ -17,7 +23,6 @@ export interface IPublicServerOptions {
|
||||
*/
|
||||
export class UiPublicServer {
|
||||
public projectinfo = new plugins.projectinfo.ProjectinfoNpm(paths.packageDir);
|
||||
public readme: string;
|
||||
public startedAt: string;
|
||||
private server: plugins.http.Server;
|
||||
private npmRegistry: plugins.smartnpm.NpmRegistry;
|
||||
@@ -29,12 +34,38 @@ export class UiPublicServer {
|
||||
packageBaseDirectory: './',
|
||||
npmRegistryUrl: 'https://registry.npmjs.org',
|
||||
allowedPackages: [],
|
||||
packageConfigs: {},
|
||||
port: 8080,
|
||||
mode: 'dev',
|
||||
log: true,
|
||||
};
|
||||
public options: IPublicServerOptions;
|
||||
|
||||
/**
|
||||
* Get the base directory for a specific package
|
||||
*/
|
||||
private getPackageBaseDirectory(packageName: string): string {
|
||||
const packageConfig = this.options.packageConfigs?.[packageName];
|
||||
let baseDir = packageConfig?.baseDirectory || this.options.packageBaseDirectory;
|
||||
if (!baseDir.endsWith('/')) {
|
||||
baseDir += '/';
|
||||
}
|
||||
return baseDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the registry for a specific package
|
||||
*/
|
||||
private getPackageRegistry(packageName: string): plugins.smartnpm.NpmRegistry {
|
||||
const packageConfig = this.options.packageConfigs?.[packageName];
|
||||
if (packageConfig?.registry && packageConfig.registry !== this.options.npmRegistryUrl) {
|
||||
return new plugins.smartnpm.NpmRegistry({
|
||||
npmRegistryUrl: packageConfig.registry,
|
||||
});
|
||||
}
|
||||
return this.npmRegistry;
|
||||
}
|
||||
|
||||
constructor(optionsArg?: IPublicServerOptions) {
|
||||
// lets create an npm instance for the registry that we are talking to
|
||||
this.options = {
|
||||
@@ -51,24 +82,12 @@ export class UiPublicServer {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* initializes the readme content
|
||||
*/
|
||||
private async initReadme(): Promise<void> {
|
||||
const readmePath = plugins.path.join(paths.packageDir, 'readme.md');
|
||||
const readmeContent = await plugins.smartfile.fs.toStringSync(readmePath);
|
||||
this.readme = await plugins.smartmarkdown.SmartMarkdown.easyMarkdownToHtml(readmeContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* starts the server
|
||||
*/
|
||||
public async startServer() {
|
||||
console.log('starting the uipublicserver');
|
||||
|
||||
// Initialize readme
|
||||
await this.initReadme();
|
||||
|
||||
const done = plugins.smartpromise.defer();
|
||||
const expressApplication = plugins.express();
|
||||
|
||||
@@ -84,38 +103,83 @@ export class UiPublicServer {
|
||||
|
||||
expressApplication.use(plugins.compression({ filter: shouldCompress }));
|
||||
|
||||
// Serve embedded UI files (index.html, bundle.js, etc.)
|
||||
expressApplication.get('/', async (req, res) => {
|
||||
await this.serveIndexHtml(req, res);
|
||||
});
|
||||
|
||||
expressApplication.get('/index.html', async (req, res) => {
|
||||
await this.serveIndexHtml(req, res);
|
||||
});
|
||||
|
||||
// Serve other embedded static files (bundle.js, css, etc.)
|
||||
expressApplication.get('/bundle.js', async (req, res) => {
|
||||
res.setHeader('cache-control', 'no-cache, no-store, must-revalidate');
|
||||
await this.serveEmbeddedFile('/bundle.js', res);
|
||||
});
|
||||
|
||||
expressApplication.get('/bundle.js.map', async (req, res) => {
|
||||
res.setHeader('cache-control', 'no-cache, no-store, must-revalidate');
|
||||
await this.serveEmbeddedFile('/bundle.js.map', res);
|
||||
});
|
||||
|
||||
// Serve any other embedded files
|
||||
expressApplication.use('/assets', async (req, res, next) => {
|
||||
const filePath = '/assets' + req.path;
|
||||
if (hasEmbeddedFile(filePath)) {
|
||||
await this.serveEmbeddedFile(filePath, res);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
// API endpoint for file listing (dev mode)
|
||||
if (this.options.mode === 'dev') {
|
||||
expressApplication.use('/peek/', async (req, res) => {
|
||||
const host = req.headers.host || 'localhost';
|
||||
const parsedUrl = new plugins.url.URL(`http://${host}${req.url}`);
|
||||
const simpleResponse: interfaces.ISimpleResponse = await this.renderPeek({
|
||||
headers: req.headers,
|
||||
parsedUrl,
|
||||
});
|
||||
for (const header of Object.keys(simpleResponse.headers)) {
|
||||
res.setHeader(header, simpleResponse.headers[header]);
|
||||
expressApplication.get('/api/files/:org/:package', async (req, res) => {
|
||||
const packageName = `${req.params.org}/${req.params.package}`;
|
||||
const version = (req.query.version as string) || '';
|
||||
const distTag = (req.query.disttag as string) || '';
|
||||
|
||||
if (!this.options.allowedPackages.includes(packageName)) {
|
||||
res.status(403).json({ error: 'Package not allowed' });
|
||||
return;
|
||||
}
|
||||
|
||||
const baseDirectory = this.getPackageBaseDirectory(packageName);
|
||||
const registry = this.getPackageRegistry(packageName);
|
||||
|
||||
try {
|
||||
const result = await registry.getFilesFromPackage(
|
||||
packageName,
|
||||
plugins.path.join(baseDirectory),
|
||||
{ version, distTag }
|
||||
);
|
||||
|
||||
const files = result.map(smartfile => {
|
||||
return smartfile.path.replace(plugins.path.join('package', baseDirectory), '');
|
||||
});
|
||||
|
||||
// Get package info for versions
|
||||
const packageInfo = await registry.getPackageInfo(packageName).catch(() => null);
|
||||
const versions = packageInfo?.allVersions?.slice(-20).reverse().map(v => v.version) || [];
|
||||
|
||||
res.json({ files, versions });
|
||||
} catch (err) {
|
||||
res.status(404).json({ error: 'Package not found' });
|
||||
}
|
||||
res.status(simpleResponse.status);
|
||||
res.write(simpleResponse.body);
|
||||
res.end();
|
||||
});
|
||||
|
||||
expressApplication.use('/readme/', async (req, res) => {
|
||||
const host = req.headers.host || 'localhost';
|
||||
const parsedUrl = new plugins.url.URL(`http://${host}${req.url}`);
|
||||
const simpleResponse: interfaces.ISimpleResponse = await this.renderReadme({
|
||||
headers: req.headers,
|
||||
parsedUrl,
|
||||
});
|
||||
for (const header of Object.keys(simpleResponse.headers)) {
|
||||
res.setHeader(header, simpleResponse.headers[header]);
|
||||
}
|
||||
res.status(simpleResponse.status);
|
||||
res.write(simpleResponse.body);
|
||||
res.end();
|
||||
// Peek route serves the SPA
|
||||
expressApplication.get('/peek/*', async (req, res) => {
|
||||
await this.serveIndexHtml(req, res);
|
||||
});
|
||||
|
||||
expressApplication.get('/peek', async (req, res) => {
|
||||
await this.serveIndexHtml(req, res);
|
||||
});
|
||||
}
|
||||
|
||||
// Main package serving route
|
||||
expressApplication.use('/', async (req, res) => {
|
||||
const host = req.headers.host || 'localhost';
|
||||
const parsedUrl = new plugins.url.URL(`http://${host}${req.url}`);
|
||||
@@ -143,6 +207,52 @@ export class UiPublicServer {
|
||||
await done.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve the index.html with configuration injected
|
||||
*/
|
||||
private async serveIndexHtml(req: plugins.express.Request, res: plugins.express.Response) {
|
||||
const embeddedFile = getEmbeddedFile('/index.html');
|
||||
if (!embeddedFile) {
|
||||
res.status(500).send('UI not bundled. Run pnpm bundleUI first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Inject configuration into the HTML
|
||||
let html = embeddedFile.data.toString('utf-8');
|
||||
const config = {
|
||||
version: this.projectinfo.version,
|
||||
mode: this.options.mode,
|
||||
allowedPackages: this.options.allowedPackages,
|
||||
};
|
||||
|
||||
// Inject config script before the closing </head> tag
|
||||
const configScript = `<script>window.__OPENCDN_CONFIG__ = ${JSON.stringify(config)};</script>`;
|
||||
html = html.replace('</head>', `${configScript}\n</head>`);
|
||||
|
||||
res.setHeader('content-type', embeddedFile.contentType);
|
||||
res.setHeader('cache-control', 'no-cache');
|
||||
res.status(200).send(html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve an embedded file
|
||||
*/
|
||||
private async serveEmbeddedFile(filePath: string, res: plugins.express.Response) {
|
||||
const embeddedFile = getEmbeddedFile(filePath);
|
||||
if (!embeddedFile) {
|
||||
res.status(404).send('File not found');
|
||||
return;
|
||||
}
|
||||
|
||||
res.setHeader('content-type', embeddedFile.contentType);
|
||||
res.setHeader('content-length', embeddedFile.data.length.toString());
|
||||
// Only set cache-control if not already set
|
||||
if (!res.getHeader('cache-control')) {
|
||||
res.setHeader('cache-control', 'public, max-age=31536000, immutable');
|
||||
}
|
||||
res.status(200).send(embeddedFile.data);
|
||||
}
|
||||
|
||||
public disableLogging() {
|
||||
this.options.log = false;
|
||||
}
|
||||
@@ -171,25 +281,14 @@ export class UiPublicServer {
|
||||
private render: interfaces.IRenderFunction = async (req) => {
|
||||
const pathArray = req.parsedUrl.pathname.split('/');
|
||||
|
||||
if (pathArray.length < 3) {
|
||||
const textArray = [
|
||||
`serving javascript to the web for Lossless GmbH.<br/>`,
|
||||
`Running in mode: <b>${this.options.mode}</b><br/><br/>`,
|
||||
];
|
||||
if (this.options.mode === 'dev') {
|
||||
textArray.push(
|
||||
`<b>Wondering what we serve?</b> <a href="/peek/">Lets take a peek!</a><br/>`
|
||||
);
|
||||
textArray.push(`<b>Documentation:</b> <a href="/readme/">Read the docs</a>`);
|
||||
}
|
||||
// Handle root path - already served by express routes above
|
||||
if (pathArray.length < 3 || !pathArray[1].startsWith('@')) {
|
||||
return {
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
status: 201,
|
||||
body: await plugins.litNtml.html`
|
||||
${ntml.getBody(this, textArray)}
|
||||
`,
|
||||
status: 404,
|
||||
body: this.getErrorPage('Not Found', 'The requested path was not found.'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -198,11 +297,11 @@ export class UiPublicServer {
|
||||
if (!npmOrg.startsWith('@')) {
|
||||
console.log('malformed npmorg');
|
||||
return {
|
||||
status: 500,
|
||||
status: 400,
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
body: `npmorg "${npmOrg}" must start with @`,
|
||||
body: this.getErrorPage('Bad Request', `npm org "${npmOrg}" must start with @`),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -219,15 +318,17 @@ export class UiPublicServer {
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
status: 503,
|
||||
body: await plugins.litNtml.html`
|
||||
the requested package is not allowlisted for public access
|
||||
`,
|
||||
status: 403,
|
||||
body: this.getErrorPage('Forbidden', 'The requested package is not allowlisted for public access.'),
|
||||
};
|
||||
}
|
||||
|
||||
// Get per-package configuration
|
||||
const baseDirectory = this.getPackageBaseDirectory(packageName);
|
||||
const registry = this.getPackageRegistry(packageName);
|
||||
|
||||
// lets care about the inner package path
|
||||
let filePath = this.options.packageBaseDirectory;
|
||||
let filePath = baseDirectory;
|
||||
let first = true;
|
||||
for (let i = 3; i < pathArray.length; i++) {
|
||||
if (first) {
|
||||
@@ -253,7 +354,7 @@ export class UiPublicServer {
|
||||
smartfile = await this.requestMap[requestDescriptor].promise;
|
||||
} else {
|
||||
this.requestMap[requestDescriptor] = plugins.smartpromise.defer();
|
||||
smartfile = await this.npmRegistry
|
||||
smartfile = await registry
|
||||
.getFileFromPackage(packageName, filePath, {
|
||||
version,
|
||||
distTag,
|
||||
@@ -272,9 +373,9 @@ export class UiPublicServer {
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
body: await ntml.getBody(
|
||||
this,
|
||||
`${packageName}@${version} does not have a file at "${filePath}"`
|
||||
body: this.getErrorPage(
|
||||
'Not Found',
|
||||
`${packageName}@${version || 'latest'} does not have a file at "${filePath}"`
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -298,121 +399,78 @@ export class UiPublicServer {
|
||||
};
|
||||
};
|
||||
|
||||
private renderPeek: interfaces.IRenderFunction = async (req) => {
|
||||
const pathArray = req.parsedUrl.pathname.split('/');
|
||||
const npmOrg = pathArray[1];
|
||||
const npmPackage = pathArray[2];
|
||||
const npmPackageName = `${npmOrg}/${npmPackage}`;
|
||||
const distTag = req.parsedUrl.searchParams.get('disttag');
|
||||
const version = req.parsedUrl.searchParams.get('version');
|
||||
|
||||
// lets care about cors
|
||||
|
||||
if (!npmOrg || !npmPackage || !this.options.allowedPackages.includes(npmPackageName)) {
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
body: `
|
||||
${await ntml.getBody(this, [
|
||||
`<a href="../">../ -> Go back</a><br/>`,
|
||||
`<b>allowlisted packages:</b><br/>`,
|
||||
...this.options.allowedPackages.map((packageArg) => {
|
||||
return `
|
||||
<div>"${packageArg}" -> mapping to "${this.options.packageBaseDirectory}" inside the package <a href="/peek/${packageArg}">Peek...</a></div>
|
||||
`;
|
||||
}),
|
||||
])}
|
||||
`,
|
||||
};
|
||||
/**
|
||||
* Generate a simple error page
|
||||
*/
|
||||
private getErrorPage(title: string, message: string): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${title} - opencdn</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
min-height: 100vh;
|
||||
background: #09090b;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
|
||||
color: #fafafa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
const result = await this.npmRegistry
|
||||
.getFilesFromPackage(npmPackageName, plugins.path.join(this.options.packageBaseDirectory), {
|
||||
version: version,
|
||||
distTag: distTag,
|
||||
})
|
||||
.catch((err) => {
|
||||
return;
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return {
|
||||
status: 404,
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
body: `
|
||||
${await ntml.getBody(this, [
|
||||
`<a href="/peek/">/peek/ -> Go to peek overview</a><br/>`,
|
||||
`<b>Not found: package ${npmPackageName}@${
|
||||
version || 'latest'
|
||||
} is not available on the supplied registry</b><br/>`,
|
||||
])}
|
||||
`,
|
||||
};
|
||||
.card {
|
||||
background: #09090b;
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 8px;
|
||||
padding: 48px;
|
||||
max-width: 500px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
const packageInfo = await this.npmRegistry.getPackageInfo(npmPackageName);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
body: `
|
||||
${await ntml.getBody(this, [
|
||||
`<a href="/peek/">/peek/ -> Go to peek overview</a><br/><br/>`,
|
||||
`<h1>${npmPackageName}@${version || distTag || '<i>inferred</i> latest'}</h1>`,
|
||||
`<b>Versions:</b> ${packageInfo.allVersions
|
||||
.map(
|
||||
(versionArg) =>
|
||||
`<a href="./${npmPackage}?version=${versionArg.version}">${versionArg.version}</a> | `
|
||||
)
|
||||
.join('')}<br/><br/>`,
|
||||
`<b>DistTags:</b> ${
|
||||
packageInfo.allDistTags
|
||||
.map(
|
||||
(distTagArg) =>
|
||||
`<a href="./${npmPackage}?disttag=${distTagArg.name}">${distTagArg.name} (${distTagArg.targetVersion})</a> | `
|
||||
)
|
||||
.join('') || 'no dist tags found!'
|
||||
}<br/><br/>`,
|
||||
`<b>File Overview at version and path:</b><br/>`,
|
||||
`"<b>${npmPackageName}@${
|
||||
version || distTag || '<i>inferred</i> latest'
|
||||
}</b>" under internal path "<b>${this.options.packageBaseDirectory}</b>"<br/></br>`,
|
||||
...result.map((smartfile) => {
|
||||
const displayPath = smartfile.path.replace(
|
||||
plugins.path.join('package', this.options.packageBaseDirectory),
|
||||
''
|
||||
);
|
||||
return `<a href="/${npmPackageName}/${displayPath}?version=${version || ''}&disttag=${
|
||||
distTag || ''
|
||||
}">${displayPath}</a><br/>`;
|
||||
}),
|
||||
])}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
private renderReadme: interfaces.IRenderFunction = async (req) => {
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
body: await ntml.getBody(this, [
|
||||
`<style>
|
||||
pre {
|
||||
display: block;
|
||||
background: #fafafa;
|
||||
border: 1px dotted #CCC;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>`,
|
||||
this.readme,
|
||||
]),
|
||||
};
|
||||
};
|
||||
.icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 24px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #fafafa;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
p {
|
||||
color: #a1a1aa;
|
||||
margin-bottom: 32px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
a {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background: #27272a;
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 6px;
|
||||
color: #fafafa;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
a:hover {
|
||||
border-color: #d4d4d8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="icon">📦</div>
|
||||
<h2>${title}</h2>
|
||||
<p>${message}</p>
|
||||
<a href="/">← Back to Home</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import type { UiPublicServer } from '../npm-publicserver.classes.uipublicserver.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
export const getBody = async (uipublicServerArg: UiPublicServer, contentArg: string | string[]) => {
|
||||
return await plugins.litNtml.html`
|
||||
<head></head>
|
||||
<body>
|
||||
<style>
|
||||
body {
|
||||
margin: 0px;
|
||||
padding: 20px;
|
||||
background: #eeeeeb;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
.main {
|
||||
background: #ffffff;
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
border: 1px dashed #CCCCCC;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
padding-bottom: 60px;
|
||||
position:relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.logo {
|
||||
text-align: center;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px dotted #CCCCCC;
|
||||
margin-left: -20px;
|
||||
margin-top: -20px;
|
||||
margin-right: -20px;
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
<div class="main">
|
||||
<div class="logo">
|
||||
<img src="https://assetbroker.lossless.one/brandfiles/lossless/svg-minimal-bright.svg" onclick="window.location.href = 'https://lossless.com'" />
|
||||
</div>
|
||||
${contentArg}
|
||||
<div class="footer">
|
||||
UiPublicServer v${uipublicServerArg.projectinfo.version} |
|
||||
running since ${uipublicServerArg.startedAt} |
|
||||
<a href="https://lossless.gmbh" target="_blank">Legal Info</a></div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './body.js';
|
||||
@@ -29,7 +29,6 @@ export {
|
||||
// unscoped packages
|
||||
import compression from 'compression';
|
||||
import express from 'express';
|
||||
import * as litNtml from 'lit-ntml';
|
||||
import * as promClient from 'prom-client';
|
||||
|
||||
export { compression, express, litNtml, promClient };
|
||||
export { compression, express, promClient };
|
||||
|
||||
Reference in New Issue
Block a user