Skip to content

Commit

Permalink
Add skuba start fastify support (#1101)
Browse files Browse the repository at this point in the history
* add fastify support

* restore type

* add better types

* add integration tests

* ignore unused

* Add doco and changeset

* nitpick

* fix example

* fix example again
  • Loading branch information
samchungy committed Feb 6, 2023
1 parent ace8bb7 commit b62e585
Show file tree
Hide file tree
Showing 8 changed files with 405 additions and 30 deletions.
7 changes: 7 additions & 0 deletions .changeset/khaki-drinks-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'skuba': minor
---

run: Add Fastify support

`skuba start` can now be used to create a live-reloading server for Fastify based projects. See https://seek-oss.github.io/skuba/docs/cli/run.html#skuba-start for more information.
15 changes: 15 additions & 0 deletions docs/cli/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ interface Export {
// One of these is required.
callback?: () => http.RequestListener;
requestListener?: http.RequestListener;
server?: http.Server;

// Optional; falls back to an available port.
port?: number;
Expand All @@ -130,6 +131,19 @@ const app = new Koa();
export default Object.assign(app, { port });
```
As should [Fastify]:
```typescript
const createApp = async () => {
const app = fastify();
await app.ready();
return app;
};
const app = createApp();

export default app;
```
As should [Express]:
```typescript
Expand Down Expand Up @@ -189,5 +203,6 @@ Execution should pause on the breakpoint until we hit `F5` or the `▶️` butto
[`ts-node`]: https://github.com/typestrong/ts-node
[`tsconfig-paths`]: https://github.com/dividab/tsconfig-paths
[express]: https://expressjs.com/
[fastify]: https://www.fastify.io/
[koa]: https://koajs.com/
[node.js options]: https://nodejs.org/en/docs/guides/debugging-getting-started/#command-line-options
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
"@types/validate-npm-package-name": "4.0.0",
"enhanced-resolve": "5.12.0",
"express": "4.18.2",
"fastify": "4.12.0",
"jsonfile": "6.1.0",
"koa": "2.14.1",
"memfs": "3.4.13",
Expand Down
11 changes: 9 additions & 2 deletions src/wrapper/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,16 @@ export const serveRequestListener = (
port?: number,
) => {
const server = http.createServer(requestListener);
return startServer(server, port);
};

return new Promise<void>((resolve, reject) =>
/**
* Returns a HTTP server wrapped in a promise
*
* This function resolves when the server is closed.
*/
export const startServer = (server: http.Server, port?: number) =>
new Promise<void>((resolve, reject) =>
server
.listen(port)
.on('close', resolve)
Expand All @@ -76,4 +84,3 @@ export const serveRequestListener = (
log.ok('listening on port', log.bold(address.port));
}),
);
};
45 changes: 29 additions & 16 deletions src/wrapper/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import nodeHttp from 'http';
import path from 'path';

import request from 'supertest';
Expand All @@ -12,20 +13,20 @@ const initWrapper = (entryPoint: string) =>

let agent: request.SuperAgentTest;

const serveRequestListener = jest
.spyOn(http, 'serveRequestListener')
.mockImplementation((requestListener) => {
agent = request.agent(requestListener);
const startServer = jest
.spyOn(http, 'startServer')
.mockImplementation((server) => {
agent = request.agent(server);
return Promise.resolve();
});

afterEach(serveRequestListener.mockClear);
afterEach(startServer.mockClear);

test('asyncFunctionHandler', async () => {
// Without `.ts`
await initWrapper('asyncFunctionHandler#handler');

expect(serveRequestListener).toHaveBeenCalledTimes(1);
expect(startServer).toHaveBeenCalledTimes(1);

return Promise.all([
agent
Expand Down Expand Up @@ -67,8 +68,8 @@ test('expressRequestListener', async () => {
// With `.ts`
await initWrapper('expressRequestListener.ts');

expect(serveRequestListener.mock.calls).toEqual([
[expect.any(Function), 12345],
expect(startServer.mock.calls).toEqual([
[expect.any(nodeHttp.Server), 12345],
]);

return Promise.all([
Expand All @@ -84,16 +85,14 @@ test('expressRequestListener', async () => {
test('invalidRequestListener', async () => {
await expect(initWrapper('invalidRequestListener')).resolves.toBeUndefined();

expect(serveRequestListener).not.toHaveBeenCalled();
expect(startServer).not.toHaveBeenCalled();
});

test('koaRequestListener', async () => {
// Without `.ts`
await initWrapper('koaRequestListener');

expect(serveRequestListener.mock.calls).toEqual([
[expect.any(Function), 8080],
]);
expect(startServer.mock.calls).toEqual([[expect.any(nodeHttp.Server), 8080]]);

return Promise.all([
agent
Expand All @@ -105,25 +104,39 @@ test('koaRequestListener', async () => {
]);
});

test('fastifyRequestListener', async () => {
// Without `.ts`
await initWrapper('fastifyRequestListener');

return Promise.all([
agent
.get('/fastify')
.expect(200)
.expect(({ text }) => expect(text).toMatchInlineSnapshot(`"Fastify!"`)),

agent.get('/express').expect(404),
]);
});

test('miscellaneousExportModule', async () => {
await expect(
initWrapper('miscellaneousExportModule'),
).resolves.toBeUndefined();

expect(serveRequestListener).not.toHaveBeenCalled();
expect(startServer).not.toHaveBeenCalled();
});

test('noExportModule', async () => {
await expect(initWrapper('noExportModule')).resolves.toBeUndefined();

expect(serveRequestListener).not.toHaveBeenCalled();
expect(startServer).not.toHaveBeenCalled();
});

test('syncFunctionHandler', async () => {
// With `.ts`
await initWrapper('syncFunctionHandler.ts#handler');

expect(serveRequestListener).toHaveBeenCalledTimes(1);
expect(startServer).toHaveBeenCalledTimes(1);

return Promise.all([
agent
Expand Down Expand Up @@ -155,7 +168,7 @@ test('voidFunctionHandler', async () => {
// With `.ts`
await initWrapper('voidFunctionHandler.ts#handler');

expect(serveRequestListener).toHaveBeenCalledTimes(1);
expect(startServer).toHaveBeenCalledTimes(1);

return (
agent
Expand Down
30 changes: 22 additions & 8 deletions src/wrapper/requestListener.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type http from 'http';
import http from 'http';

import { isFunction, isIpPort, isObject } from '../utils/validation';

import { serveRequestListener } from './http';
import { serveRequestListener, startServer } from './http';

// Express compatibility
interface FunctionConfig extends http.RequestListener {
Expand All @@ -15,11 +15,16 @@ interface ObjectConfig {

requestListener?: http.RequestListener;

default?: unknown;
// Fastify compatibility
server?: http.Server;

default?: Promise<unknown>;
port?: unknown;
}

const isConfig = (data: unknown): data is FunctionConfig | ObjectConfig =>
const isConfig = (
data: unknown,
): data is Promise<FunctionConfig> | Promise<ObjectConfig> =>
isFunction(data) || isObject(data);

interface Args {
Expand All @@ -41,18 +46,29 @@ export const runRequestListener = async ({
return;
}

let config = entryPoint;
let config: FunctionConfig | ObjectConfig = await entryPoint;

if (typeof config === 'object' && isConfig(config.default)) {
// Prefer `export default` over `export =`
config = config.default;
config = await config.default;
}

if (Object.keys(config).length === 0) {
// Assume an executable script with no exports
return;
}

const port = isIpPort(config.port) ? config.port : availablePort;

// Fastify workaround
if (
typeof config !== 'function' &&
config.server &&
config.server instanceof http.Server
) {
return startServer(config.server, port);
}

const requestListener =
typeof config === 'function'
? config
Expand All @@ -63,7 +79,5 @@ export const runRequestListener = async ({
return;
}

const port = isIpPort(config.port) ? config.port : availablePort;

return serveRequestListener(requestListener, port);
};
12 changes: 12 additions & 0 deletions src/wrapper/testing/fastifyRequestListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { fastify } from 'fastify';

const createApp = async () => {
const app = fastify();
app.get('/fastify', (_req, reply) => reply.code(200).send('Fastify!'));
await app.ready();
return app;
};

const app = createApp();

export default app;
Loading

0 comments on commit b62e585

Please sign in to comment.