Skip to content
This repository has been archived by the owner on Jul 4, 2023. It is now read-only.

Commit

Permalink
feat(serve): add a --watch flag for serve mode
Browse files Browse the repository at this point in the history
When watch is enabled, ncdc will watch your config file and fixtures for changes. If changes are
detected, the ncdc server will restart

fix #28
  • Loading branch information
tamj0rd2 committed Apr 25, 2020
1 parent e8994d3 commit aa85eab
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 43 deletions.
13 changes: 6 additions & 7 deletions acceptance-tests/cli-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import { ChildProcess, exec } from 'child_process'
import strip from 'strip-ansi'
import isomorphicUnfetch from 'isomorphic-unfetch'

const waitForX = (condition: () => Promise<boolean> | boolean): Promise<void> =>
const waitForX = (condition: () => Promise<boolean> | boolean, timeout: number): Promise<void> =>
new Promise<void>((resolve, reject) => {
const timeout = 10
const retryPeriod = 0.5
let tries = 0

Expand Down Expand Up @@ -56,9 +55,9 @@ export type CleanupTask = () => void
export const fetch = (endpoint: string, init?: RequestInit): Promise<Response> =>
isomorphicUnfetch(`${SERVE_HOST}${endpoint}`, init)

export const prepareServe = (cleanupTasks: CleanupTask[]) => async (
checkAvailability = true,
export const prepareServe = (cleanupTasks: CleanupTask[], timeout = 5) => async (
args = '',
checkAvailability = true,
): Promise<ServeResult> => {
let hasExited = false
const command = `LOG_LEVEL=debug CHOKIDAR_USEPOLLING=1 ./bin/ncdc serve ${CONFIG_FILE} -c ${FIXTURE_FOLDER}/tsconfig.json ${args}`
Expand Down Expand Up @@ -99,7 +98,7 @@ export const prepareServe = (cleanupTasks: CleanupTask[]) => async (
const foundIndex = searchableOutput.findIndex((s) => {
if (typeof target === 'string') return strip(s).includes(target)
return target.test(s)
})
}, timeout)

if (foundIndex === -1) {
outputPointer = currentOutput.length
Expand All @@ -108,7 +107,7 @@ export const prepareServe = (cleanupTasks: CleanupTask[]) => async (

outputPointer += foundIndex + 1
return true
}).catch(
}, timeout).catch(
failNicely(
`Did not find the string "${target}" in the output${
outputPointer ? ' on or after message ' + outputPointer : ''
Expand All @@ -121,7 +120,7 @@ export const prepareServe = (cleanupTasks: CleanupTask[]) => async (
waitForX(async () => {
const { status } = await fetch('/')
return status === 200
}).catch(failNicely(`The ncdc server was not contactable at ${SERVE_HOST}/`))
}, timeout).catch(failNicely(`The ncdc server was not contactable at ${SERVE_HOST}/`))

if (checkAvailability) await waitUntilAvailable()
return { getAllOutput: () => strip(getRawOutput()), waitForOutput, waitUntilAvailable }
Expand Down
16 changes: 8 additions & 8 deletions acceptance-tests/serve.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe('ncdc serve', () => {
)

// act
const { waitForOutput } = await serve(false)
const { waitForOutput } = await serve('', false)

// assert
await waitForOutput('no such file or directory')
Expand All @@ -70,7 +70,7 @@ describe('ncdc serve', () => {
it('restarts when config.yml is changed', async () => {
// arrange
const configWrapper = new ConfigWrapper().addConfig()
const { waitForOutput, waitUntilAvailable } = await serve()
const { waitForOutput, waitUntilAvailable } = await serve('--watch')
const resInitial = await fetch('/api/books/789')
expect(resInitial.status).toBe(200)

Expand All @@ -87,7 +87,7 @@ describe('ncdc serve', () => {
it('logs a message and kills the server when config.yml has been deleted', async () => {
// arrange
const configWrapper = new ConfigWrapper().addConfig()
const { waitForOutput } = await serve()
const { waitForOutput } = await serve('--watch')
const resInitial = await fetch('/api/books/yay')
expect(resInitial.status).toBe(200)

Expand All @@ -103,7 +103,7 @@ describe('ncdc serve', () => {
it('can recover from config.yml being deleted when file is re-added', async () => {
// arrange
const configWrapper = new ConfigWrapper().addConfig()
const { waitForOutput } = await serve()
const { waitForOutput } = await serve('--watch')
configWrapper.deleteYaml()
await waitForOutput(MESSAGE_RSTARTING_FAILURE)

Expand All @@ -130,7 +130,7 @@ describe('ncdc serve', () => {
})

// act
const { waitForOutput, waitUntilAvailable } = await serve()
const { waitForOutput, waitUntilAvailable } = await serve('--watch')
configWrapper.editFixture(fixtureName, (f) => ({ ...f, title: 'shit meme' }))

await waitForOutput(/change event detected for .*response.json/)
Expand All @@ -156,7 +156,7 @@ describe('ncdc serve', () => {
})

// act
const { waitForOutput } = await serve()
const { waitForOutput } = await serve('--watch')
configWrapper.deleteFixture(fixtureName)

// assert
Expand All @@ -175,7 +175,7 @@ describe('ncdc serve', () => {
author: 'me',
})

const { waitForOutput, waitUntilAvailable } = await serve()
const { waitForOutput, waitUntilAvailable } = await serve('--watch')
configWrapper.deleteFixture(fixtureName)
await waitForOutput(MESSAGE_RSTARTING_FAILURE)

Expand Down Expand Up @@ -211,7 +211,7 @@ describe('ncdc serve', () => {
title: 'string',
})

serve = await prepareServe(typecheckingCleanup)()
serve = await prepareServe(typecheckingCleanup, 10)('--watch')

await expect(fetch('/api/books/hello')).resolves.toMatchObject({ status: 200 })
})
Expand Down
6 changes: 6 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ services:
- TEST_MODE=acceptance
volumes:
- ./:/usr/src/app

acceptance:
build: .
command: bash -c 'yarn compile && yarn test'
environment:
- TEST_MODE=acceptance
11 changes: 9 additions & 2 deletions src/commands/serve/handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,19 @@ describe('handler', () => {
})

it('handles when a config path is not supplied', async () => {
await handler({ force: false, port: 8001, tsconfigPath: randomString() })
await handler({ force: false, port: 8001, tsconfigPath: randomString(), watch: false })

expect(mockHandleError).toBeCalledWith({ message: 'config path must be supplied' })
})

it('handles when port is not a number', async () => {
await handler({ force: false, port: NaN, tsconfigPath: randomString(), configPath: randomString() })
await handler({
force: false,
port: NaN,
tsconfigPath: randomString(),
configPath: randomString(),
watch: false,
})

expect(mockHandleError).toBeCalledWith({ message: 'port must be a number' })
})
Expand All @@ -60,6 +66,7 @@ describe('handler', () => {
port: 4000,
tsconfigPath: randomString(),
configPath: randomString(),
watch: false,
}

describe('runs the server with the correct configs', () => {
Expand Down
55 changes: 29 additions & 26 deletions src/commands/serve/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface ServeArgs {
tsconfigPath: string
schemaPath?: string
force: boolean
watch: boolean
}

export type StartServer = (port: number, routes: Config[], typeValidator?: TypeValidator) => StartServerResult
Expand Down Expand Up @@ -64,7 +65,7 @@ const createHandler = (
createTypeValidator: CreateTypeValidator,
startServer: StartServer,
) => async (args: ServeArgs): Promise<void> => {
const { configPath, port, tsconfigPath, schemaPath, force } = args
const { configPath, port, tsconfigPath, schemaPath, force, watch } = args

if (!configPath) return handleError({ message: 'config path must be supplied' })
if (isNaN(port)) return handleError({ message: 'port must be a number' })
Expand Down Expand Up @@ -114,36 +115,38 @@ const createHandler = (
return handleError(err)
}

const configWatcher = chokidar.watch([absoluteConfigPath, ...result.pathsToWatch], {
ignoreInitial: true,
})
if (watch) {
const configWatcher = chokidar.watch([absoluteConfigPath, ...result.pathsToWatch], {
ignoreInitial: true,
})

configWatcher.on('all', async (e, path) => {
logger.info(`${e} event detected for ${path}`)
logger.info('Attempting to restart ncdc server')
configWatcher.on('all', async (e, path) => {
logger.info(`${e} event detected for ${path}`)
logger.info('Attempting to restart ncdc server')

if (e === 'unlink') {
// makes sure that we can still watch the file for changes after its deletion
configWatcher.add(path)
}
if (e === 'unlink') {
// makes sure that we can still watch the file for changes after its deletion
configWatcher.add(path)
}

try {
await result.startServerResult.close()
} catch (err) {
logger.error(`Could not restart the server: ${err.message}`)
return
}
try {
await result.startServerResult.close()
} catch (err) {
logger.error(`Could not restart the server: ${err.message}`)
return
}

try {
result = await prepAndStartServer()
} catch (err) {
logger.error(`Could not restart ncdc server: ${err.message}`)
return
}
try {
result = await prepAndStartServer()
} catch (err) {
logger.error(`Could not restart ncdc server: ${err.message}`)
return
}

configWatcher.unwatch('**/*')
configWatcher.add([absoluteConfigPath, ...result.pathsToWatch])
})
configWatcher.unwatch('**/*')
configWatcher.add([absoluteConfigPath, ...result.pathsToWatch])
})
}
}

export default createHandler
5 changes: 5 additions & 0 deletions src/commands/serve/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ const builder = (yargs: Argv): Argv<ServeArgs> =>
type: 'number',
default: 4000,
})
.option('watch', {
describe: 'watches the provided config file and fixtures then restarts if there are changes',
type: 'boolean',
default: false,
})
.option(consts.SCHEMA_PATH, {
type: consts.SCHEMA_PATH_TYPE,
description: consts.SCHEMA_PATH_DESCRIPTION,
Expand Down

0 comments on commit aa85eab

Please sign in to comment.