diff --git a/common/changes/@rushstack/rush-serve-plugin/serve-file_2022-10-03-22-06.json b/common/changes/@rushstack/rush-serve-plugin/serve-file_2022-10-03-22-06.json new file mode 100644 index 00000000000..624e7652975 --- /dev/null +++ b/common/changes/@rushstack/rush-serve-plugin/serve-file_2022-10-03-22-06.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/rush-serve-plugin", + "comment": "Allow serving of single files, e.g. to serve a specific file at \"/\" (the root).", + "type": "minor" + } + ], + "packageName": "@rushstack/rush-serve-plugin" +} \ No newline at end of file diff --git a/rush-plugins/rush-serve-plugin/src/RushProjectServeConfigFile.ts b/rush-plugins/rush-serve-plugin/src/RushProjectServeConfigFile.ts index 913a6a013c3..cd7995bc52d 100644 --- a/rush-plugins/rush-serve-plugin/src/RushProjectServeConfigFile.ts +++ b/rush-plugins/rush-serve-plugin/src/RushProjectServeConfigFile.ts @@ -12,13 +12,25 @@ export interface IRushProjectServeJson { routing: IRoutingRuleJson[]; } -export interface IRoutingRuleJson { - projectRelativeFolder: string; +export interface IBaseRoutingRuleJson { servePath: string; immutable?: boolean; } +export interface IRoutingFolderRuleJson extends IBaseRoutingRuleJson { + projectRelativeFile: undefined; + projectRelativeFolder: string; +} + +export interface IRoutingFileRuleJson extends IBaseRoutingRuleJson { + projectRelativeFile: string; + projectRelativeFolder: undefined; +} + +export type IRoutingRuleJson = IRoutingFileRuleJson | IRoutingFolderRuleJson; + export interface IRoutingRule { + type: 'file' | 'folder'; diskPath: string; servePath: string; immutable: boolean; @@ -61,8 +73,11 @@ export class RushServeConfiguration { ); if (serveJson) { for (const rule of serveJson.routing) { + const { projectRelativeFile, projectRelativeFolder } = rule; + const diskPath: string = projectRelativeFolder ?? projectRelativeFile; rules.push({ - diskPath: path.resolve(project.projectFolder, rule.projectRelativeFolder), + type: projectRelativeFile ? 'file' : 'folder', + diskPath: path.resolve(project.projectFolder, diskPath), servePath: rule.servePath, immutable: !!rule.immutable }); diff --git a/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts b/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts index 73a5123a31b..0bd721ee504 100644 --- a/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts +++ b/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts @@ -89,22 +89,44 @@ export async function phasedCommandHandler(options: IPhasedCommandHandlerOptions logger.terminal ); - function setHeaders(response: express.Response, path: string, stat: unknown): void { + const fileRoutingRules: Map = new Map(); + + function setHeaders(response: express.Response, path?: string, stat?: unknown): void { response.set('Access-Control-Allow-Origin', '*'); - response.set('Access-Control-Allow-Methods', 'GET'); + response.set('Access-Control-Allow-Methods', 'GET, OPTIONS'); } for (const rule of routingRules) { - app.use( - rule.servePath, - express.static(rule.diskPath, { - dotfiles: 'ignore', - immutable: rule.immutable, - index: false, - redirect: false, - setHeaders - }) - ); + const { diskPath, servePath } = rule; + if (rule.type === 'file') { + const existingRule: IRoutingRule | undefined = fileRoutingRules.get(servePath); + if (existingRule) { + throw new Error( + `Request to serve "${diskPath}" at "${servePath}" conflicts with existing rule to serve "${existingRule.diskPath}" from this location.` + ); + } else { + fileRoutingRules.set(diskPath, rule); + app.get(servePath, (request: express.Request, response: express.Response) => { + response.sendFile(diskPath, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS' + } + }); + }); + } + } else { + app.use( + servePath, + express.static(diskPath, { + dotfiles: 'ignore', + immutable: rule.immutable, + index: false, + redirect: false, + setHeaders + }) + ); + } } const server: https.Server = https.createServer( diff --git a/rush-plugins/rush-serve-plugin/src/schemas/rush-project-serve.schema.json b/rush-plugins/rush-serve-plugin/src/schemas/rush-project-serve.schema.json index 46b9bfd4952..af1dad75e00 100644 --- a/rush-plugins/rush-serve-plugin/src/schemas/rush-project-serve.schema.json +++ b/rush-plugins/rush-serve-plugin/src/schemas/rush-project-serve.schema.json @@ -20,25 +20,50 @@ "type": "array", "description": "Routing rules", "items": { - "type": "object", - "additionalProperties": false, - "required": ["projectRelativeFolder", "servePath"], - "properties": { - "projectRelativeFolder": { - "type": "string", - "description": "The folder from which to read assets, relative to the root of the current project." - }, + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["projectRelativeFolder", "servePath"], + "properties": { + "projectRelativeFolder": { + "type": "string", + "description": "The folder from which to read assets, relative to the root of the current project." + }, + + "servePath": { + "type": "string", + "description": "The server path at which to serve the assets in \"projectRelativeFolder\"." + }, - "servePath": { - "type": "string", - "description": "The server path at which to serve the assets in \"projectRelativeFolder\"." + "immutable": { + "type": "boolean", + "description": "Enables or disables the `immutable` directive in the `Cache-Control` resoponse header. See (https://expressjs.com/en/4x/api.html#express.static)." + } + } }, + { + "type": "object", + "additionalProperties": false, + "required": ["projectRelativeFile", "servePath"], + "properties": { + "projectRelativeFile": { + "type": "string", + "description": "The file to serve, relative to the root of the current project." + }, + + "servePath": { + "type": "string", + "description": "The server path at which to serve \"projectRelativeFile\"." + }, - "immutable": { - "type": "boolean", - "description": "Enables or disables the `immutable` directive in the `Cache-Control` resoponse header. See (https://expressjs.com/en/4x/api.html#express.static)." + "immutable": { + "type": "boolean", + "description": "Enables or disables the `immutable` directive in the `Cache-Control` resoponse header. See (https://expressjs.com/en/4x/api.html#express.static)." + } + } } - } + ] } } }