Skip to content

Commit

Permalink
feat(rest): allow static assets to be served by a rest server
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Aug 27, 2018
1 parent 3dbd8f8 commit ae72ebc
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 0 deletions.
36 changes: 36 additions & 0 deletions docs/site/Application.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,42 @@ This means you can call these `RestServer` functions to do all of your
server-level setups in the app constructor without having to explicitly retrieve
an instance of your server.

### Serve static files

The `RestServer` allows static files to be served. It can be done by configuring
the RestApplication with `assets`:

```ts
export class MyApplication extends RestApplication {
constructor() {
super({
rest: {
port: 4000,
host: 'my-host',
assets: {
'/html': {
root: rootDirForHtml, // Root directory for static files
options: {}, // Options for serve-static
},
},
},
});
}
}
```

You can also use `static()` to mount static files by code.

```ts
app.static('/html', rootDirForHtml);
```

Please note
[serve-static](https://expressjs.com/en/resources/middleware/serve-static.html)
is used behind the scene to serve static files. Please see
https://expressjs.com/en/starter/static-files.html and
https://expressjs.com/en/4x/api.html#express.static for details.

### Use unique bindings

Use binding names that are prefixed with a unique string that does not overlap
Expand Down
11 changes: 11 additions & 0 deletions packages/rest/src/rest.application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ControllerFactory,
} from './router/routing-table';
import {OperationObject, OpenApiSpec} from '@loopback/openapi-v3-types';
import {ServeStaticOptions} from 'serve-static';

export const ERR_NO_MULTI_SERVER = format(
'RestApplication does not support multiple servers!',
Expand Down Expand Up @@ -83,6 +84,16 @@ export class RestApplication extends Application implements HttpServerLike {
this.restServer.handler(handlerFn);
}

/**
* Mount static assets to the REST server
* @param path The path to serve the static asset
* @param root The root directory
* @param options Options for serve-static
*/
static(path: string, root: string, options?: ServeStaticOptions) {
this.restServer.static(path, root, options);
}

/**
* Register a new Controller-based route.
*
Expand Down
47 changes: 47 additions & 0 deletions packages/rest/src/rest.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
import {RestBindings} from './keys';
import {RequestContext} from './request-context';
import * as express from 'express';
import {ServeStaticOptions} from 'serve-static';

const debug = require('debug')('loopback:rest:server');

Expand Down Expand Up @@ -131,6 +132,7 @@ export class RestServer extends Context implements Server, HttpServerLike {
protected _httpServer: HttpServer | undefined;

protected _expressApp: express.Application;
protected _routerForStaticAssets: express.Router;

get listening(): boolean {
return this._httpServer ? this._httpServer.listening : false;
Expand Down Expand Up @@ -196,6 +198,21 @@ export class RestServer extends Context implements Server, HttpServerLike {
};
this._expressApp.use(cors(corsOptions));

// Place the assets router here before controllers
this._setupRouterForStaticAssets();

// Mount static assets
if (options.assets) {
for (const p in options.assets) {
const asset = options.assets[p];
debug('Mounting static assets to %s: %j', p, asset);
this._routerForStaticAssets.use(
p,
express.static(asset.root, asset.options),
);
}
}

// Mount our router & request handler
this._expressApp.use((req, res, next) => {
this._handleHttpRequest(req, res, options!).catch(next);
Expand All @@ -209,6 +226,17 @@ export class RestServer extends Context implements Server, HttpServerLike {
);
}

/**
* Set up an express router for all static assets so that middleware for
* all directories are invoked at the same phase
*/
protected _setupRouterForStaticAssets() {
if (!this._routerForStaticAssets) {
this._routerForStaticAssets = express.Router();
this._expressApp.use(this._routerForStaticAssets);
}
}

protected _handleHttpRequest(
request: Request,
response: Response,
Expand Down Expand Up @@ -513,6 +541,16 @@ export class RestServer extends Context implements Server, HttpServerLike {
);
}

/**
* Mount static assets to the REST server
* @param path The path to serve the asset
* @param root The root directory
* @param options Options for serve-static
*/
static(path: string, root: string, options?: ServeStaticOptions) {
this._routerForStaticAssets.use(path, express.static(root, options));
}

/**
* Set the OpenAPI specification that defines the REST API schema for this
* server. All routes, parameter definitions and return types will be defined
Expand Down Expand Up @@ -670,6 +708,15 @@ export interface RestServerOptions {
cors?: cors.CorsOptions;
apiExplorerUrl?: string;
sequence?: Constructor<SequenceHandler>;
/**
* Static assets to be served using `express.static()`
*/
assets?: {
[path: string]: {
root: string;
options?: ServeStaticOptions;
};
};
}

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/rest/test/integration/fixtures/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<header>
<title>Test Page</title>
</header>
<body>
<h1>Hello, World!</h1>
</body>
</html>
81 changes: 81 additions & 0 deletions packages/rest/test/integration/rest.server.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,87 @@ describe('RestServer (integration)', () => {
.expect(500);
});

it('allows static assets via config', async () => {
const root = path.join(__dirname, 'fixtures');
const server = await givenAServer({
rest: {
port: 0,
assets: {
'/html': {
root: root,
},
},
},
});

const content = fs
.readFileSync(path.join(root, 'index.html'))
.toString('utf-8');
await createClientForHandler(server.requestHandler)
.get('/html/index.html')
.expect('Content-Type', /text\/html/)
.expect(200, content);

await createClientForHandler(server.requestHandler)
.get('/html/does-not-exist.html')
.expect(404);
});

it('allows static assets via api', async () => {
const root = path.join(__dirname, 'fixtures');
const server = await givenAServer({
rest: {
port: 0,
},
});

server.static('/html', root);
const content = fs
.readFileSync(path.join(root, 'index.html'))
.toString('utf-8');
await createClientForHandler(server.requestHandler)
.get('/html/index.html')
.expect('Content-Type', /text\/html/)
.expect(200, content);
});

it('allows static assets via api after start', async () => {
const root = path.join(__dirname, 'fixtures');
const server = await givenAServer({
rest: {
port: 0,
},
});
await createClientForHandler(server.requestHandler)
.get('/html/index.html')
.expect(404);

server.static('/html', root);

await createClientForHandler(server.requestHandler)
.get('/html/index.html')
.expect(200);
});

it('allows non-static routes after assets', async () => {
const root = path.join(__dirname, 'fixtures');
const server = await givenAServer({
rest: {
port: 0,
assets: {
'/html': {
root: root,
},
},
},
});
server.handler(dummyRequestHandler);

await createClientForHandler(server.requestHandler)
.get('/html/does-not-exist.html')
.expect(200, 'Hello');
});

it('allows cors', async () => {
const server = await givenAServer({rest: {port: 0}});
server.handler(dummyRequestHandler);
Expand Down

0 comments on commit ae72ebc

Please sign in to comment.