opensource-portal/middleware/react.ts

254 строки
9.2 KiB
TypeScript

//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import { NextFunction, Response } from 'express';
import fs from 'fs';
import path from 'path';
import appPackage from '../package.json';
import { getStaticBlobCacheFallback } from '../lib/staticBlobCacheFallback';
import { getProviders, splitSemiColonCommas } from '../lib/transitional';
import type { ReposAppRequest, SiteConfiguration } from '../interfaces';
import { IndividualContext } from '../business/user';
const staticReactPackageNameKey = 'static-react-package-name';
const staticClientPackageName = appPackage[staticReactPackageNameKey];
const staticReactFlightingPackageNameKey = 'static-react-flight-package-name';
const staticClientFlightingPackageName = appPackage[staticReactFlightingPackageNameKey];
type PackageJsonSubset = {
name: string;
version: string;
continuousDeployment: {
commitId: string;
buildId: string;
branchName: string;
};
flights?: Record<string, string>;
};
type BasicFlightingOptions = {
enabled: boolean;
};
type ContentOptions = {
html: string;
package: PackageJsonSubset;
};
type FlightingOptions = BasicFlightingOptions &
ContentOptions & {
divertEveryone?: boolean;
staticFlightIds?: Set<string>;
flightName?: string;
};
export function injectReactClient() {
const standardContent = getReactScriptsIndex(staticClientPackageName);
let flightingBasics: BasicFlightingOptions = null;
let flightingOptions: FlightingOptions = null;
return function injectedRoute(req: ReposAppRequest, res: Response, next: NextFunction) {
const { config } = getProviders(req);
// special passthrough
if (req.path.includes('/byClient')) {
return next();
}
if (!flightingOptions) {
flightingBasics = evaluateFlightConditions(req);
flightingOptions = flightingBasics as FlightingOptions;
}
const activeContext = (req.individualContext || req.apiContext) as IndividualContext;
const flightEnabled = flightingBasics?.enabled === true;
const flightAvailable = flightEnabled && flightingOptions?.html;
const flightName = flightingOptions?.flightName;
const userFlighted =
flightingOptions?.divertEveryone === true ||
(activeContext?.corporateIdentity?.id &&
flightingOptions?.staticFlightIds?.has(activeContext.corporateIdentity.id));
const userFlightOverride =
req.query.flight === '0' || req.query.flight === '1' ? req.query.flight : undefined;
let inFlight = flightAvailable && (userFlighted || req.query.flight === '1');
if (inFlight && req.query.flight === '0') {
inFlight = false;
}
//
const servePackage = (inFlight ? flightingOptions : standardContent).package;
const meta: Record<string, string> = {
'served-client-package': servePackage.name,
'served-client-version': servePackage.version,
'served-client-flight-default': userFlighted ? '1' : '0',
// Repos app config
'portal-environment': config.debug.environmentName,
};
// Feature flags on the client side from the static list
if (activeContext?.corporateIdentity?.id) {
const userClientFlags = getUserClientFeatureFlags(config, activeContext.corporateIdentity.id);
if (userClientFlags.length > 0) {
meta['server-features'] = userClientFlags.join(',');
}
}
// Override
if (inFlight) {
meta['served-client-flight'] = flightName;
}
if (userFlightOverride !== undefined) {
meta['served-client-flight-override'] = userFlightOverride;
}
// App Service
config?.webServer?.appService?.slot && (meta['app-service-slot'] = config.webServer.appService.slot);
config?.webServer?.appService?.name && (meta['app-service-name'] = config.webServer.appService.name);
config?.webServer?.appService?.region &&
(meta['app-service-region'] = config.webServer.appService.region);
// Repos app framework
config?.web?.app && (meta['app-name'] = config.web.app);
// Source control
let commitId = servePackage.continuousDeployment?.commitId;
if (commitId === '__Build_SourceVersion__') {
commitId = '';
}
let branchName = servePackage.continuousDeployment?.branchName || '';
if (branchName === '__Build_BranchName__') {
branchName = '';
}
commitId && (meta['source-control-client-commit-id'] = commitId);
branchName && (meta['source-control-client-branch-name'] = branchName);
commitId = appPackage.continuousDeployment?.commitId;
if (commitId === '__Build_SourceVersion__') {
commitId = '';
}
branchName = appPackage.continuousDeployment?.branchName;
if (branchName === '__Build_BranchName__') {
branchName = '';
}
commitId && (meta['source-control-server-commit-id'] = commitId);
branchName && (meta['source-control-server-branch-name'] = branchName);
// Debug-time
config?.github?.codespaces?.connected && (meta['github-codespaces-connected'] = '1');
config?.github?.codespaces?.name && (meta['github-codespaces-name'] = config.github.codespaces.name);
const addon =
Object.keys(meta)
.map((key) => {
return ` <meta name="${key}" content="${meta[key]}" />`;
})
.join('\n') + '\n';
res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
res.header('Expires', '-1');
res.header('Pragma', 'no-cache');
res.header('x-ms-repos-site', 'react');
if (inFlight) {
res.header('x-ms-repos-flight', flightName);
}
const clientHtml = inFlight ? flightingOptions.html : standardContent.html;
const html = augmentHtmlHeader(clientHtml, addon);
return res.send(html);
};
}
function evaluateFlightConditions(req: ReposAppRequest): FlightingOptions | BasicFlightingOptions {
const { config } = getProviders(req);
if (config?.client?.flighting?.enabled === true && staticClientFlightingPackageName) {
const options = getReactScriptsIndex(staticClientFlightingPackageName) as FlightingOptions;
const branchName = options.package.continuousDeployment?.branchName;
const flights = options.package.flights;
options.flightName = (flights || {})[branchName] || 'unknown';
options.enabled = true;
options.divertEveryone = config.client.flighting.divertEveryone;
options.staticFlightIds = new Set<string>(
Array.isArray(config.client.flighting.corporateIds)
? config.client.flighting.corporateIds
: splitSemiColonCommas(config.client.flighting.corporateIds)
);
return options;
}
return {
enabled: false,
};
}
function getUserClientFeatureFlags(config: SiteConfiguration, corporateId: string) {
const featureFlagList = config?.client?.flighting?.featureFlagUsers;
if (featureFlagList && typeof featureFlagList === 'object') {
const flights = [];
const flightNames = Object.getOwnPropertyNames(featureFlagList);
for (const flight of flightNames) {
const flightIds = featureFlagList[flight];
if (flightIds && flightIds.includes(corporateId)) {
flights.push(flight);
}
}
return flights;
}
return [];
}
function augmentHtmlHeader(html: string, augmentedHeader: string) {
const headEnd = html.indexOf('</head>');
const head = html.substring(0, headEnd);
const body = html.substring(headEnd);
const newHead = head + augmentedHeader;
const newHtml = newHead + body;
return newHtml;
}
type CacheBuffer = {
buffer: Buffer;
contentType: string;
};
const localFallbackBlobCache = new Map<string, CacheBuffer>();
export async function TryFallbackToBlob(req: ReposAppRequest, res: Response): Promise<boolean> {
if (!req.path) {
return false;
}
const providers = getProviders(req);
const baseUrl = '/react' + req.originalUrl;
if (localFallbackBlobCache.has(baseUrl)) {
providers.insights.trackEvent({ name: 'FallbackToBlob', properties: { baseUrl } });
const entry = localFallbackBlobCache.get(baseUrl);
if (entry.contentType) {
res.contentType(entry.contentType);
}
res.send(entry.buffer);
return true;
}
const fallbackBlob = await getStaticBlobCacheFallback(providers);
const [buffer, contentType] = await fallbackBlob.get(baseUrl);
if (buffer) {
providers.insights.trackEvent({ name: 'FallbackToBlob', properties: { baseUrl } });
localFallbackBlobCache.set(baseUrl, { buffer, contentType });
if (contentType) {
res.contentType(contentType);
}
res.send(buffer);
return true;
}
return false;
}
function getReactScriptsIndex(packageName: string): ContentOptions {
try {
const staticModernReactApp = require(packageName);
const staticPackageFile = require(`${packageName}/package.json`);
const previewClientFolder = staticModernReactApp;
if (typeof previewClientFolder !== 'string') {
throw new Error(`The return value of the preview package ${packageName} must be a string/path`);
}
const indexPageContent = fs.readFileSync(path.join(previewClientFolder, 'client.html'), {
encoding: 'utf8',
});
return {
html: indexPageContent,
package: staticPackageFile,
};
} catch (hostClientError) {
console.error(
`The static client could not be loaded via package ${packageName}. Note that index.html needs to be named client.html in build/.`
);
throw hostClientError;
}
}