|
1 | 1 | import Koa from 'koa'
|
2 | 2 | import cors from '@koa/cors'
|
3 | 3 | import KoaRouter from '@koa/router'
|
| 4 | +import KoaMount from 'koa-mount' |
4 | 5 | import { WebApp } from 'meteor/webapp'
|
5 | 6 | import { Meteor } from 'meteor/meteor'
|
6 | 7 | import { getRandomString } from '@sofie-automation/corelib/dist/lib'
|
7 | 8 | import _ from 'underscore'
|
8 |
| -import { public_dir } from '../../lib' |
| 9 | +import { getRootSubpath, public_dir } from '../../lib' |
9 | 10 | import staticServe from 'koa-static'
|
10 | 11 | import { logger } from '../../logging'
|
11 | 12 | import { PackageInfo } from '../../coreSystem'
|
12 | 13 | import { profiler } from '../profiler'
|
| 14 | +import fs from 'fs/promises' |
13 | 15 |
|
14 | 16 | declare module 'http' {
|
15 | 17 | interface IncomingMessage {
|
@@ -78,48 +80,86 @@ Meteor.startup(() => {
|
78 | 80 |
|
79 | 81 | // serve the webui through koa
|
80 | 82 | // This is to avoid meteor injecting anything into the served html
|
81 |
| - const webuiServer = staticServe(public_dir) |
82 |
| - koaApp.use(webuiServer) |
| 83 | + const webuiServer = staticServe(public_dir, { |
| 84 | + index: false, // Performed manually |
| 85 | + }) |
| 86 | + koaApp.use(KoaMount(getRootSubpath() || '/', webuiServer)) |
83 | 87 | logger.debug(`Serving static files from ${public_dir}`)
|
84 | 88 |
|
85 |
| - // Serve the meteor runtime config |
86 |
| - rootRouter.get('/meteor-runtime-config.js', async (ctx) => { |
87 |
| - const versionExtended: string = PackageInfo.versionExtended || PackageInfo.version // package version |
88 |
| - |
89 |
| - ctx.body = `window.__meteor_runtime_config__ = (${JSON.stringify({ |
90 |
| - // @ts-expect-error missing types for internal meteor detail |
91 |
| - ...__meteor_runtime_config__, |
92 |
| - sofieVersionExtended: versionExtended, |
93 |
| - })})` |
94 |
| - }) |
| 89 | + if (Meteor.isDevelopment) { |
| 90 | + // Serve the meteor runtime config. In production, this gets baked into the html |
| 91 | + rootRouter.get(getRootSubpath() + '/meteor-runtime-config.js', async (ctx) => { |
| 92 | + ctx.body = getExtendedMeteorRuntimeConfig() |
| 93 | + }) |
| 94 | + } |
95 | 95 |
|
96 | 96 | koaApp.use(rootRouter.routes()).use(rootRouter.allowedMethods())
|
97 | 97 |
|
98 | 98 | koaApp.use(async (ctx, next) => {
|
99 | 99 | if (ctx.method !== 'GET') return next()
|
100 | 100 |
|
| 101 | + // Ensure the path is scoped to the root subpath |
| 102 | + const rootSubpath = getRootSubpath() |
| 103 | + if (!ctx.path.startsWith(rootSubpath)) return next() |
| 104 | + |
101 | 105 | // Don't use the fallback for certain paths
|
102 |
| - if (ctx.path.startsWith('/assets/')) return next() |
| 106 | + if (ctx.path.startsWith(rootSubpath + '/assets/')) return next() |
103 | 107 |
|
104 | 108 | // Don't use the fallback for anything handled by another router
|
105 | 109 | // This does not feel efficient, but koa doesn't appear to have any shared state between the router handlers
|
106 | 110 | for (const bindPath of boundRouterPaths) {
|
107 | 111 | if (ctx.path.startsWith(bindPath)) return next()
|
108 | 112 | }
|
109 | 113 |
|
110 |
| - // fallback to the root file |
111 |
| - ctx.path = '/' |
112 |
| - return webuiServer(ctx, next) |
| 114 | + // fallback to serving html |
| 115 | + return serveIndexHtml(ctx, next) |
113 | 116 | })
|
114 | 117 | })
|
115 | 118 |
|
| 119 | +function getExtendedMeteorRuntimeConfig() { |
| 120 | + const versionExtended: string = PackageInfo.versionExtended || PackageInfo.version // package version |
| 121 | + |
| 122 | + return `window.__meteor_runtime_config__ = (${JSON.stringify({ |
| 123 | + // @ts-expect-error missing types for internal meteor detail |
| 124 | + ...__meteor_runtime_config__, |
| 125 | + sofieVersionExtended: versionExtended, |
| 126 | + })})` |
| 127 | +} |
| 128 | + |
| 129 | +async function serveIndexHtml(ctx: Koa.ParameterizedContext, next: Koa.Next) { |
| 130 | + try { |
| 131 | + // Read the file |
| 132 | + const indexFileBuffer = await fs.readFile(public_dir + '/index.html', 'utf8') |
| 133 | + const indexFileStr = indexFileBuffer.toString() |
| 134 | + |
| 135 | + const rootPath = getRootSubpath() |
| 136 | + |
| 137 | + // Perform various runtime modifications, to ensure paths have the correct absolute prefix |
| 138 | + let modifiedFile = indexFileStr |
| 139 | + modifiedFile = modifiedFile.replace( |
| 140 | + // Replace the http load with injected js, to avoid risk of issues where this load fails and the app gets confused |
| 141 | + '<script type="text/javascript" src="/meteor-runtime-config.js"></script>', |
| 142 | + `<script type="text/javascript">${getExtendedMeteorRuntimeConfig()}</script>` |
| 143 | + ) |
| 144 | + modifiedFile = modifiedFile.replaceAll('href="/', `href="${rootPath}/`) |
| 145 | + modifiedFile = modifiedFile.replaceAll('href="./', `href="${rootPath}/`) |
| 146 | + modifiedFile = modifiedFile.replaceAll('src="./', `src="${rootPath}/`) |
| 147 | + |
| 148 | + ctx.body = modifiedFile |
| 149 | + } catch (e) { |
| 150 | + return next() |
| 151 | + } |
| 152 | +} |
| 153 | + |
116 | 154 | export function bindKoaRouter(koaRouter: KoaRouter, bindPath: string): void {
|
| 155 | + const bindPathWithPrefix = getRootSubpath() + bindPath |
| 156 | + |
117 | 157 | // Track this path as having a router
|
118 |
| - let bindPathFull = bindPath |
| 158 | + let bindPathFull = bindPathWithPrefix |
119 | 159 | if (!bindPathFull.endsWith('/')) bindPathFull += '/'
|
120 | 160 | boundRouterPaths.push(bindPathFull)
|
121 | 161 |
|
122 |
| - rootRouter.use(bindPath, koaRouter.routes()).use(bindPath, koaRouter.allowedMethods()) |
| 162 | + rootRouter.use(bindPathWithPrefix, koaRouter.routes()).use(bindPathWithPrefix, koaRouter.allowedMethods()) |
123 | 163 | }
|
124 | 164 |
|
125 | 165 | const REVERSE_PROXY_COUNT = process.env.HTTP_FORWARDED_COUNT ? parseInt(process.env.HTTP_FORWARDED_COUNT) : 0
|
|
0 commit comments