Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions meteor/__mocks__/_setupMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ afterEach(() => {
// Expect all log messages that have been explicitly supressed, to have been handled:
SupressLogMessages.expectAllMessagesToHaveBeenHandled()
})

// @ts-expect-error mock meteor runtime config
global.__meteor_runtime_config__ = {}
2 changes: 2 additions & 0 deletions meteor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"indexof": "0.0.1",
"koa": "^2.15.3",
"koa-bodyparser": "^4.4.1",
"koa-mount": "^4.0.0",
"koa-static": "^5.0.0",
"meteor-node-stubs": "^1.2.12",
"moment": "^2.30.1",
Expand Down Expand Up @@ -85,6 +86,7 @@
"@types/jest": "^29.5.14",
"@types/koa": "^2.15.0",
"@types/koa-bodyparser": "^4.3.12",
"@types/koa-mount": "^4",
"@types/koa-static": "^4.0.4",
"@types/koa__cors": "^5.0.0",
"@types/koa__router": "^12.0.4",
Expand Down
3 changes: 2 additions & 1 deletion meteor/server/api/peripheralDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import KoaRouter from '@koa/router'
import bodyParser from 'koa-bodyparser'
import { assertConnectionHasOneOfPermissions } from '../security/auth'
import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio'
import { getRootSubpath } from '../lib'

const apmNamespace = 'peripheralDevice'
export namespace ServerPeripheralDeviceAPI {
Expand Down Expand Up @@ -680,7 +681,7 @@ peripheralDeviceRouter.get('/:deviceId/oauthResponse', async (ctx) => {
.catch(logger.error)
}

ctx.redirect(`/settings/peripheralDevice/${deviceId}`)
ctx.redirect(`${getRootSubpath()}/settings/peripheralDevice/${deviceId}`)
} catch (e) {
ctx.response.type = 'text/plain'
ctx.response.status = 500
Expand Down
3 changes: 2 additions & 1 deletion meteor/server/api/rest/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { peripheralDeviceRouter } from '../peripheralDevice'
import { blueprintsRouter } from '../blueprints/http'
import { createLegacyApiRouter } from './v0/index'
import { heapSnapshotPrivateApiRouter } from '../heapSnapshot'
import { getRootSubpath } from '../../lib'

const LATEST_REST_API = 'v1.0'

Expand All @@ -34,7 +35,7 @@ apiRouter.use(
)

async function redirectToLatest(ctx: koa.ParameterizedContext, _next: koa.Next): Promise<void> {
ctx.redirect(`/api/${LATEST_REST_API}`)
ctx.redirect(`${getRootSubpath()}/api/${LATEST_REST_API}`)
ctx.status = 307
}

Expand Down
78 changes: 59 additions & 19 deletions meteor/server/api/rest/koa.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import Koa from 'koa'
import cors from '@koa/cors'
import KoaRouter from '@koa/router'
import KoaMount from 'koa-mount'
import { WebApp } from 'meteor/webapp'
import { Meteor } from 'meteor/meteor'
import { getRandomString } from '@sofie-automation/corelib/dist/lib'
import _ from 'underscore'
import { public_dir } from '../../lib'
import { getRootSubpath, public_dir } from '../../lib'
import staticServe from 'koa-static'
import { logger } from '../../logging'
import { PackageInfo } from '../../coreSystem'
import { profiler } from '../profiler'
import fs from 'fs/promises'

declare module 'http' {
interface IncomingMessage {
Expand Down Expand Up @@ -78,48 +80,86 @@ Meteor.startup(() => {

// serve the webui through koa
// This is to avoid meteor injecting anything into the served html
const webuiServer = staticServe(public_dir)
koaApp.use(webuiServer)
const webuiServer = staticServe(public_dir, {
index: false, // Performed manually
})
koaApp.use(KoaMount(getRootSubpath() || '/', webuiServer))
logger.debug(`Serving static files from ${public_dir}`)

// Serve the meteor runtime config
rootRouter.get('/meteor-runtime-config.js', async (ctx) => {
const versionExtended: string = PackageInfo.versionExtended || PackageInfo.version // package version

ctx.body = `window.__meteor_runtime_config__ = (${JSON.stringify({
// @ts-expect-error missing types for internal meteor detail
...__meteor_runtime_config__,
sofieVersionExtended: versionExtended,
})})`
})
if (Meteor.isDevelopment) {
// Serve the meteor runtime config. In production, this gets baked into the html
rootRouter.get(getRootSubpath() + '/meteor-runtime-config.js', async (ctx) => {
ctx.body = getExtendedMeteorRuntimeConfig()
})
}

koaApp.use(rootRouter.routes()).use(rootRouter.allowedMethods())

koaApp.use(async (ctx, next) => {
if (ctx.method !== 'GET') return next()

// Ensure the path is scoped to the root subpath
const rootSubpath = getRootSubpath()
if (!ctx.path.startsWith(rootSubpath)) return next()

// Don't use the fallback for certain paths
if (ctx.path.startsWith('/assets/')) return next()
if (ctx.path.startsWith(rootSubpath + '/assets/')) return next()

// Don't use the fallback for anything handled by another router
// This does not feel efficient, but koa doesn't appear to have any shared state between the router handlers
for (const bindPath of boundRouterPaths) {
if (ctx.path.startsWith(bindPath)) return next()
}

// fallback to the root file
ctx.path = '/'
return webuiServer(ctx, next)
// fallback to serving html
return serveIndexHtml(ctx, next)
})
})

function getExtendedMeteorRuntimeConfig() {
const versionExtended: string = PackageInfo.versionExtended || PackageInfo.version // package version

return `window.__meteor_runtime_config__ = (${JSON.stringify({
// @ts-expect-error missing types for internal meteor detail
...__meteor_runtime_config__,
sofieVersionExtended: versionExtended,
})})`
}

async function serveIndexHtml(ctx: Koa.ParameterizedContext, next: Koa.Next) {
try {
// Read the file
const indexFileBuffer = await fs.readFile(public_dir + '/index.html', 'utf8')
const indexFileStr = indexFileBuffer.toString()

const rootPath = getRootSubpath()

// Perform various runtime modifications, to ensure paths have the correct absolute prefix
let modifiedFile = indexFileStr
modifiedFile = modifiedFile.replace(
// Replace the http load with injected js, to avoid risk of issues where this load fails and the app gets confused
'<script type="text/javascript" src="/meteor-runtime-config.js"></script>',
`<script type="text/javascript">${getExtendedMeteorRuntimeConfig()}</script>`
)
modifiedFile = modifiedFile.replaceAll('href="/', `href="${rootPath}/`)
modifiedFile = modifiedFile.replaceAll('href="./', `href="${rootPath}/`)
modifiedFile = modifiedFile.replaceAll('src="./', `src="${rootPath}/`)

ctx.body = modifiedFile
} catch (e) {
return next()
}
}

export function bindKoaRouter(koaRouter: KoaRouter, bindPath: string): void {
const bindPathWithPrefix = getRootSubpath() + bindPath

// Track this path as having a router
let bindPathFull = bindPath
let bindPathFull = bindPathWithPrefix
if (!bindPathFull.endsWith('/')) bindPathFull += '/'
boundRouterPaths.push(bindPathFull)

rootRouter.use(bindPath, koaRouter.routes()).use(bindPath, koaRouter.allowedMethods())
rootRouter.use(bindPathWithPrefix, koaRouter.routes()).use(bindPathWithPrefix, koaRouter.allowedMethods())
}

const REVERSE_PROXY_COUNT = process.env.HTTP_FORWARDED_COUNT ? parseInt(process.env.HTTP_FORWARDED_COUNT) : 0
Expand Down
7 changes: 7 additions & 0 deletions meteor/server/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ export const public_dir = Meteor.isProduction
: // In development, find the webui package and use its public directory
path.join(process.cwd(), '../../../../../../packages/webui/public')

export function getRootSubpath(): string {
// @ts-expect-error Untyped meteor export
const settings: any = __meteor_runtime_config__

return settings.ROOT_URL_PATH_PREFIX || ''
}

/**
* Get the i18next locale object for a given `languageCode`. If the translations file can not be found or it can't be
* parsed, it will return an empty object.
Expand Down
57 changes: 29 additions & 28 deletions meteor/server/webmanifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { logger } from './logging'
import { MongoQuery } from '@sofie-automation/corelib/dist/mongo'
import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio'
import { RundownPlaylists, Rundowns, Studios } from './collections'
import { getLocale, Translations } from './lib'
import { getLocale, getRootSubpath, Translations } from './lib'
import { generateTranslation } from './lib/tempLib'
import { ITranslatableMessage } from '@sofie-automation/blueprints-integration'
import { interpollateTranslation } from '@sofie-automation/corelib/dist/TranslatableMessage'
Expand All @@ -21,20 +21,6 @@ import { bindKoaRouter } from './api/rest/koa'
import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError'

const appShortName = 'Sofie'
const SOFIE_DEFAULT_ICONS: ManifestImageResource[] = [
{
src: '/icons/mstile-144x144.png',
sizes: '144x144',
purpose: 'monochrome',
type: 'image/png',
},
{
src: '/icons/maskable-96x96.png',
sizes: '96x96',
purpose: 'maskable',
type: 'image/png',
},
]

const t = generateTranslation

Expand All @@ -48,6 +34,21 @@ function getShortcutsForStudio(
studio: Pick<DBStudio, '_id' | 'name'>,
studioCount: number
): ShortcutItem[] {
const SOFIE_DEFAULT_ICONS: ManifestImageResource[] = [
{
src: getRootSubpath() + '/icons/mstile-144x144.png',
sizes: '144x144',
purpose: 'monochrome',
type: 'image/png',
},
{
src: getRootSubpath() + '/icons/maskable-96x96.png',
sizes: '96x96',
purpose: 'maskable',
type: 'image/png',
},
]

const multiStudio = studioCount > 1
return [
{
Expand All @@ -61,7 +62,7 @@ function getShortcutsForStudio(
: t('Active Rundown')
),
icons: SOFIE_DEFAULT_ICONS,
url: `/activeRundown/${studio._id}`,
url: getRootSubpath() + `/activeRundown/${studio._id}`,
},
{
id: `${studio._id}_prompter`,
Expand All @@ -74,7 +75,7 @@ function getShortcutsForStudio(
: t('Prompter')
),
icons: SOFIE_DEFAULT_ICONS,
url: `/prompter/${studio._id}`,
url: getRootSubpath() + `/prompter/${studio._id}`,
},
{
id: `${studio._id}_countdowns`,
Expand All @@ -83,7 +84,7 @@ function getShortcutsForStudio(
multiStudio ? t('{{studioName}}: Presenter screen', { studioName: studio.name }) : t('Presenter screen')
),
icons: SOFIE_DEFAULT_ICONS,
url: `/countdowns/${studio._id}/presenter`,
url: getRootSubpath() + `/countdowns/${studio._id}/presenter`,
},
]
}
Expand Down Expand Up @@ -111,31 +112,31 @@ async function getWebManifest(languageCode: string): Promise<JSONSchemaForWebApp
short_name: appShortName,
icons: [
{
src: '/icons/android-chrome-192x192.png',
src: getRootSubpath() + '/icons/android-chrome-192x192.png',
sizes: '192x192',
purpose: 'any',
type: 'image/png',
},
{
src: '/icons/android-chrome-512x512.png',
src: getRootSubpath() + '/icons/android-chrome-512x512.png',
sizes: '512x512',
purpose: 'any',
type: 'image/png',
},
{
src: '/icons/mstile-144x144.png',
src: getRootSubpath() + '/icons/mstile-144x144.png',
sizes: '144x144',
purpose: 'monochrome',
type: 'image/png',
},
{
src: '/icons/maskable-96x96.png',
src: getRootSubpath() + '/icons/maskable-96x96.png',
sizes: '96x96',
purpose: 'maskable',
type: 'image/png',
},
{
src: '/icons/maskable-512x512.png',
src: getRootSubpath() + '/icons/maskable-512x512.png',
sizes: '512x512',
purpose: 'maskable',
type: 'image/png',
Expand All @@ -144,14 +145,14 @@ async function getWebManifest(languageCode: string): Promise<JSONSchemaForWebApp
theme_color: '#2d89ef',
background_color: '#252627',
display: 'fullscreen',
start_url: '/',
scope: '/',
start_url: getRootSubpath() + '/',
scope: getRootSubpath() + '/',
orientation: 'landscape',
shortcuts: shortcuts.length > 0 ? shortcuts : undefined,
protocol_handlers: [
{
protocol: 'web+nrcs',
url: '/url/nrcs?q=%s',
url: getRootSubpath() + '/url/nrcs?q=%s',
},
],
}
Expand Down Expand Up @@ -261,14 +262,14 @@ async function webNrcsRundownRoute(ctx: Koa.ParameterizedContext, parsedUrl: URL
// we couldn't find the External ID for Rundown/Rundown Playlist
logger.debug(`NRCS URL: External ID not found "${externalId}"`)
ctx.body = `Could not find requested object: "${externalId}", see the full list`
ctx.redirect('/')
ctx.redirect(`${getRootSubpath()}/`)
ctx.response.status = 303
return
}

logger.debug(`NRCS URL: External ID found "${externalId}" in "${rundownPlaylist._id}"`)
ctx.body = `Requested object found in Rundown Playlist "${rundownPlaylist._id}"`
ctx.redirect(`/rundown/${rundownPlaylist._id}`)
ctx.redirect(`${getRootSubpath()}/rundown/${rundownPlaylist._id}`)
}

Meteor.startup(() => {
Expand Down
4 changes: 3 additions & 1 deletion meteor/tsconfig-base.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
/* At the time of writing we are not ready for stricter rules */
"strict": true,

"target": "es2022",

"skipLibCheck": true,
"sourceMap": true,
"allowJs": false,
"lib": ["dom", "es6", "dom.iterable", "scripthost", "es2017", "es2018", "es2019", "ES2020.Promise"],
"lib": ["dom", "es2022", "dom.iterable", "scripthost"],

"paths": {
"meteor/*": [
Expand Down
Loading
Loading