Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added file watcher to now dev #2153

Merged
merged 22 commits into from Apr 16, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 50 additions & 0 deletions @types/nsfw/index.d.ts
@@ -0,0 +1,50 @@
declare function nsfw(
dir: string,
callback: nsfw.EventsCallback,
options?: nsfw.WatcherOptions
): Promise<nsfw.Watcher>;

declare namespace nsfw {
export class Watcher {
start(): Promise<void>;
stop(): Promise<void>;
}
export interface BaseEvent {
action: number;
TooTallNate marked this conversation as resolved.
Show resolved Hide resolved
directory: string;
};
export interface CreatedEvent extends BaseEvent {
action: nsfw.actions.CREATED;
file: string;
}
export interface DeletedEvent extends BaseEvent {
action: nsfw.actions.DELETED;
file: string;
}
export interface ModifiedEvent extends BaseEvent {
action: nsfw.actions.MODIFIED;
file: string;
}
export interface RenamedEvent extends BaseEvent {
action: nsfw.actions.RENAMED;
oldFile: string;
newDirectory: string;
newFile: string;
}
export type Event = CreatedEvent | DeletedEvent | ModifiedEvent | RenamedEvent;
export type EventsCallback = (events: Event[]) => void;
export interface WatcherOptions {
debouceMS?: number;
errorCallback?(errors: Error[]);
};
export enum actions {
TooTallNate marked this conversation as resolved.
Show resolved Hide resolved
CREATED,
DELETED,
MODIFIED,
RENAMED
};
}

declare module 'nsfw' {
export = nsfw;
}
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -169,6 +169,7 @@
"ms": "2.1.1",
"node-fetch": "1.7.3",
"npm-package-arg": "6.1.0",
"nsfw": "1.2.2",
"nyc": "13.2.0",
"ora": "1.3.0",
"pcre-to-regexp": "0.0.5",
Expand Down
2 changes: 2 additions & 0 deletions src/commands/dev/lib/dev-builder.ts
Expand Up @@ -202,6 +202,8 @@ export async function getBuildMatches(
// of Now deployments.
src = src.substring(1);
}

// TODO: use the `files` map from DevServer instead of hitting the filesystem
const entries = Object.values(await collectProjectFiles(src, cwd));

for (const fileRef of entries) {
Expand Down
101 changes: 90 additions & 11 deletions src/commands/dev/lib/dev-server.ts
@@ -1,6 +1,7 @@
import ms from 'ms';
import url from 'url';
import http from 'http';
import nsfw from 'nsfw';
import fs from 'fs-extra';
import chalk from 'chalk';
import rawBody from 'raw-body';
Expand All @@ -10,6 +11,7 @@ import minimatch from 'minimatch';
import httpProxy from 'http-proxy';
import { randomBytes } from 'crypto';
import serveHandler from 'serve-handler';
import { FileFsRef } from '@now/build-utils';
import { parse as parseDotenv } from 'dotenv';
import { lookup as lookupMimeType } from 'mime-types';
import { basename, dirname, extname, join, relative } from 'path';
Expand Down Expand Up @@ -47,31 +49,99 @@ export default class DevServer {
public output: Output;
public env: EnvConfig;
public buildEnv: EnvConfig;
public files: BuilderInputs;

private cachedNowJson: NowConfig | null;
private server: http.Server;
private stopping: boolean;
private buildMatches: Map<string, BuildMatch>;
private inProgressBuilds: Map<string, Promise<void>>;
private originalEnv: EnvConfig;
private nsfw?: nsfw.Watcher;

constructor(cwd: string, options: DevServerOptions) {
this.cwd = cwd;
this.output = options.output;
this.env = {};
this.buildEnv = {};
this.files = {};

this.cachedNowJson = null;
this.server = http.createServer(this.devServerHandler);
this.stopping = false;
this.buildMatches = new Map();
this.inProgressBuilds = new Map();
this.originalEnv = { ...process.env };
}

async getProjectFiles(): Promise<BuilderInputs> {
// TODO: use the file watcher to keep the files list up-to-date
// incrementally, instead of re-globbing the filesystem every time
const files = await collectProjectFiles('**', this.cwd);
return files;
async handleFilesystemEvents(events: nsfw.Event[]): Promise<void> {
const filesChanged: Set<string> = new Set();
leo marked this conversation as resolved.
Show resolved Hide resolved

// First, update the `files` mapping of source files
for (const event of events) {
// TODO: for some reason the type inference isn't working, hence the casting
if (event.action === nsfw.actions.CREATED) {
leo marked this conversation as resolved.
Show resolved Hide resolved
await this.handleFileCreated(event as nsfw.CreatedEvent, filesChanged);
} else if (event.action === nsfw.actions.DELETED) {
this.handleFileDeleted(event as nsfw.DeletedEvent, filesChanged);
} else if (event.action === nsfw.actions.MODIFIED) {
await this.handleFileModified(event as nsfw.ModifiedEvent, filesChanged);
} else if (event.action === nsfw.actions.RENAMED) {
await this.handleFileRenamed(event as nsfw.RenamedEvent, filesChanged);
}
}
console.log('changed', filesChanged);

if (filesChanged.has('now.json')) {
// The `now.json` file was changed, so invalidate the in-memory copy
this.output.debug('Invalidating cached `now.json`');
this.cachedNowJson = null;
}

// Update the build matches in case an entrypoint was created or deleted
const nowJson = await this.getNowJson();
if (nowJson) {
await this.updateBuildMatches(nowJson);
}

// TODO: trigger rebuilds of any existing builds that are dependant
// on one of the files that has changed.
}

async handleFileCreated(event: nsfw.CreatedEvent, changed: Set<string>): Promise<void> {
const fsPath = join(event.directory, event.file);
const name = relative(this.cwd, fsPath);
this.output.debug(`File created: ${name}`);
this.files[name] = await FileFsRef.fromFsPath({ fsPath });
changed.add(name);
}

handleFileDeleted(event: nsfw.DeletedEvent, changed: Set<string>): void {
const name = relative(this.cwd, join(event.directory, event.file));
this.output.debug(`File deleted: ${name}`);
delete this.files[name];
changed.add(name);
}

async handleFileModified(event: nsfw.ModifiedEvent, changed: Set<string>): Promise<void> {
const fsPath = join(event.directory, event.file);
const name = relative(this.cwd, fsPath);
this.output.debug(`File modified: ${name}`);
this.files[name] = await FileFsRef.fromFsPath({ fsPath });
changed.add(name);
}

async handleFileRenamed(event: nsfw.RenamedEvent, changed: Set<string>): Promise<void> {
const oldName = relative(this.cwd, join(event.directory, event.oldFile));
changed.add(oldName);

const fsPath = join(event.newDirectory, event.newFile);
const name = relative(this.cwd, fsPath);
changed.add(name);

this.output.debug(`File renamed: ${oldName} -> ${name}`);
delete this.files[oldName];
this.files[name] = await FileFsRef.fromFsPath({ fsPath });
}

async updateBuildMatches(nowJson: NowConfig): Promise<void> {
Expand Down Expand Up @@ -115,15 +185,19 @@ export default class DevServer {
}

async getNowJson(): Promise<NowConfig | null> {
// TODO: use the file watcher to only invalidate this `nowJson`
// config once a change to the `now.json` file occurs
if (this.cachedNowJson) {
return this.cachedNowJson;
}

this.output.debug('Reading "now.json" file');
const nowJsonPath = getNowJsonPath(this.cwd);

try {
const config: NowConfig = JSON.parse(
await fs.readFile(nowJsonPath, 'utf8')
);
this.validateNowConfig(config);
this.cachedNowJson = config;
return config;
} catch (err) {
if (err.code !== 'ENOENT') {
Expand Down Expand Up @@ -190,7 +264,12 @@ export default class DevServer {
* Launches the `now dev` server.
*/
async start(port: number = 3000): Promise<void> {
let address: string | null = null;
this.files = await collectProjectFiles('**', this.cwd);

// Start the filesystem watcher
this.nsfw = await nsfw(this.cwd, this.handleFilesystemEvents.bind(this));
await this.nsfw.start();

const [env, buildEnv] = await Promise.all([
this.getLocalEnv('.env'),
this.getLocalEnv('.env.build')
Expand All @@ -215,14 +294,14 @@ export default class DevServer {
);
if (needsInitialBuild.length > 0) {
this.output.log('Running initial builds');
const files = await this.getProjectFiles();
for (const match of needsInitialBuild) {
await executeBuild(nowJson, this, files, match);
await executeBuild(nowJson, this, this.files, match);
}
this.output.success('Initial builds complete');
}
}

let address: string | null = null;
while (typeof address !== 'string') {
try {
address = await listen(this.server, port);
Expand Down Expand Up @@ -420,7 +499,7 @@ export default class DevServer {
nowRequestId: string,
nowJson: NowConfig
) => {
const files = await this.getProjectFiles();
const files = this.files;
TooTallNate marked this conversation as resolved.
Show resolved Hide resolved
await this.updateBuildMatches(nowJson);

const {
Expand Down
24 changes: 22 additions & 2 deletions yarn.lock
Expand Up @@ -2946,7 +2946,7 @@ fs-extra@7.0.0:
jsonfile "^4.0.0"
universalify "^0.1.0"

fs-extra@7.0.1:
fs-extra@7.0.1, fs-extra@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
Expand Down Expand Up @@ -4173,6 +4173,11 @@ lodash.get@^4.4.2:
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=

lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=

lodash.islength@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.islength/-/lodash.islength-4.0.1.tgz#4e9868d452575d750affd358c979543dc20ed577"
Expand All @@ -4183,6 +4188,11 @@ lodash.isplainobject@^4.0.6:
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=

lodash.isundefined@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz#23ef3d9535565203a66cefd5b830f848911afb48"
integrity sha1-I+89lTVWUgOmbO/VuDD4SJEa+0g=

lodash.merge@4.6.x, lodash.merge@^4.6.1:
version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54"
Expand Down Expand Up @@ -4556,7 +4566,7 @@ mute-stream@0.0.7:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=

nan@^2.9.2:
nan@^2.0.0, nan@^2.9.2:
version "2.13.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
Expand Down Expand Up @@ -4725,6 +4735,16 @@ npmlog@^4.0.2:
gauge "~2.7.3"
set-blocking "~2.0.0"

nsfw@1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/nsfw/-/nsfw-1.2.2.tgz#95b79b6b0e311268aaa20c5c085b9f3b341b0769"
integrity sha512-YwoS39dkrp6loO0gvh61UbQPiOYwmbAiKqWSYuMeoSkpxxy8rbe/RVgxIJ1L+ua5usLGr0FPSo7NEQnDQOGyIw==
dependencies:
fs-extra "^7.0.0"
lodash.isinteger "^4.0.4"
lodash.isundefined "^3.0.1"
nan "^2.0.0"

number-is-nan@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
Expand Down