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

Improve --content #3

Open
wants to merge 8 commits into
base: content
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ environment:
- nodejs_version: '16'
- nodejs_version: '14'
- nodejs_version: '12'
- nodejs_version: '10'

install:
- ps: Install-Product node $env:nodejs_version
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:

strategy:
matrix:
node-version: [10.x, 12.x, 14.x, 16.x]
node-version: [12.x, 14.x, 16.x]

steps:
- uses: actions/checkout@v2
Expand Down
4 changes: 4 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
.appveyor.yml
.eslintrc
.eslintcache
.github
.husky
.nyc_output
.prettierrc
.travis.yml
images
package-lock.json
test
test-results.xml
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@ node_js:
- 16
- 14
- 12
- 10
sudo: false
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# node-dev

## v7.0.0 / 2021-05-04

- [CLI] Improve command-line parsing, restore support for --require with a space
- [README] Move images into repo and fix URLs
- [dependencies] Update `minimist` from `v1.1.3` to `v1.2.5`
- [.npmignore] Add more config files

### Developer Updates

- [CI] Add github workflows
- [CI] Add appveyor
- [CI] Start testing against node v16
- [CI] Stop testing against node v10
- [`test/spawn`] Split `index` into multiple files
- [`test/utils`] Replaced directory of files with a single module that contains two methods: `spawn` and `touchFile`
- [`test/utils/run`] Moved `run` function directly into the `run` file
- [devDependenies] Update `eslint` from `v7.23.0` to `v7.25.0`

## v6.7.0 / 2021-04-07

- [New Option] `--debounce` to control how long to wait before restarting
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ npm install -g node-dev
Status and error messages can be displayed as desktop notification using
[node-notifier](https://www.npmjs.org/package/node-notifier):

![Screenshot](http://fgnass.github.com/images/node-dev.png)
![Screenshot](./images/node-dev.png)

![Screenshot](http://fgnass.github.com/images/node-dev-linux.png)
![Screenshot](./images/node-dev-linux.png)

**Requirements:**

Expand Down
Binary file added images/node-dev-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/node-dev.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
123 changes: 57 additions & 66 deletions lib/cli.js
Original file line number Diff line number Diff line change
@@ -1,88 +1,79 @@
const assert = require('assert');
const minimist = require('minimist');
const { resolve } = require('path');

const { defaultConfig, getConfig } = require('./cfg');
const { getConfig } = require('./cfg');

const configKeys = Object.keys(defaultConfig);
const arrayify = v => (Array.isArray(v) ? [...v] : [v]);
const argify = key => ({ arg: `--${key}`, key });

function resolvePath(unresolvedPath) {
return resolve(process.cwd(), unresolvedPath);
}
const resolvePath = p => resolve(process.cwd(), p);

const doubleDash = s => /^--/.test(s);
const dash = s => /^-[^-]*$/.test(s);
const nodeAlias = { require: 'r' };
const nodeBoolean = ['expose_gc'];
const nodeOptional = ['inspect', 'inspect-brk'];
const nodeString = ['require'];

function getFirstNonOptionArgIndex(args) {
for (let i = 2; i < args.length; i += 1) {
if (!doubleDash(args[i]) && !dash(args[i]) && !dash(args[i - 1] || '')) return i;
}
const nodeDevBoolean = ['clear', 'dedupe', 'fork', 'notify', 'poll', 'respawn', 'vm'];
const nodeDevNumber = ['debounce', 'deps', 'interval'];
const nodeDevString = ['content', 'graceful_ipc', 'ignore', 'timestamp'];

return args.length - 1;
}
const alias = Object.assign({}, nodeAlias);
const boolean = [...nodeBoolean, ...nodeDevBoolean];
const string = [...nodeString, ...nodeDevString];

function unique(k) {
const seen = [];
return o => {
if (!seen.includes(o[k])) {
seen.push(o[k]);
return true;
}
return false;
};
}
const nodeArgsReducer = opts => (out, { arg, key }) => {
const value = opts[key];

module.exports = argv => {
const unknownArgs = [];
if (typeof value === 'boolean') {
value && out.push(arg);
} else if (typeof value !== 'undefined') {
arrayify(value).forEach(v => {
if (arg.includes('=')) {
out.push(`${arg.split('=')[0]}=${v}`);
} else {
out.push(`${arg}=${v}`);
}
});
}

const scriptIndex = getFirstNonOptionArgIndex(argv);
delete opts[key];

const script = argv[scriptIndex];
const scriptArgs = argv.slice(scriptIndex + 1);
const devArgs = argv.slice(2, scriptIndex);
return out;
};

const opts = minimist(devArgs, {
boolean: ['clear', 'dedupe', 'fork', 'notify', 'poll', 'respawn', 'vm'],
string: ['content', 'graceful_ipc', 'ignore', 'timestamp'],
default: getConfig(script),
unknown: arg => {
const key = Object.keys(minimist([arg]))[1];
const nodeOptionalFactory = args => arg => {
const isNodeOptional = nodeOptional.includes(arg.substring(2));
if (isNodeOptional) args.push(arg);
return !isNodeOptional;
};

if (!configKeys.includes(key)) {
unknownArgs.push({ arg, key });
}
}
});
const unknownFactory = args => arg => {
const [, key] = Object.keys(minimist([arg]));
key && !nodeDevNumber.includes(key) && args.push({ arg, key });
};

const nodeArgs = unknownArgs.filter(unique('key')).reduce((out, { arg, key }) => {
const value = opts[key];
module.exports = argv => {
const nodeOptionalArgs = [];
const args = argv.slice(2).filter(nodeOptionalFactory(nodeOptionalArgs));

if (typeof value !== 'boolean' && !arg.includes('=')) {
if (Array.isArray(value)) {
value.forEach(v => out.push(arg, v));
} else {
out.push(arg, value);
}
} else {
out.push(arg);
}
const unknownArgs = [];
const unknown = unknownFactory(unknownArgs);

const {
_: [script, ...scriptArgs]
} = minimist(args, { alias, boolean, string, unknown });

return out;
}, []);
assert(script, 'Could not parse command line arguments');

unknownArgs.forEach(({ key }) => {
delete opts[key];
});
const opts = minimist(args, { alias, boolean, default: getConfig(script) });

const resolveOption = name =>
[...(Array.isArray(opts[name]) ? opts[name] : [opts[name]])].map(resolvePath);
const nodeArgs = [...nodeBoolean.map(argify), ...nodeString.map(argify), ...unknownArgs]
.sort((a, b) => a.key - b.key)
.reduce(nodeArgsReducer(opts), [...nodeOptionalArgs]);

opts.ignore = resolveOption('ignore');
opts.content = resolveOption('content');
const content = arrayify(opts.content === true ? '.' : opts.content).map(resolvePath);
const ignore = arrayify(opts.ignore).map(resolvePath);

return {
script,
scriptArgs,
nodeArgs,
opts
};
return { nodeArgs, opts: { ...opts, content, ignore }, script, scriptArgs };
};
61 changes: 22 additions & 39 deletions lib/content.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,31 @@
const fs = require('fs');
const crypto = require('crypto');
const { createHash } = require('crypto');
const { createReadStream } = require('fs');

const contentMap = {};
const getMD5 = file =>
new Promise((resolve, reject) => {
const hash = createHash('md5');
const stream = createReadStream(file);

const getFileHash = (srcPath, cb) => {
const stream = fs.createReadStream(srcPath);
const md5sum = crypto.createHash('md5');
stream.on('data', data => md5sum.update(data));
stream.on('error', cb).on('close', () => {
cb(null, md5sum.digest('hex'));
stream.on('close', () => resolve(hash.digest('hex')));
stream.on('data', hash.update.bind(hash));
stream.on('error', reject);
});
};

const getFileHashCaught = (file, hashCb) => {
getFileHash(file, (err, hash) => {
if (!err) {
hashCb(hash);
} else {
console.error('Error getting file hash:', err);
hashCb(null);
}
});
};
const contentFactory = prefixes => {
const pathMatch = file => prefixes.some(prefix => file.startsWith(prefix));

const clearContentMap = () => {
Object.keys(contentMap).forEach(key => delete contentMap[key]);
};
const contentMap = {};

const checkContentChanged = (file, changedCb) => {
getFileHashCaught(file, hash => {
const currentHash = contentMap[file];
if (hash) {
contentMap[file] = hash;
changedCb(currentHash !== hash);
} else {
changedCb(true);
}
});
};
const contentChange = file =>
getMD5(file)
.catch(() => null)
.then(hash => {
const prevHash = contentMap[file];
contentMap[file] = hash;
return hash !== prevHash;
});

const addToContentMap = file => {
getFileHashCaught(file, hash => {
if (hash) contentMap[file] = hash;
});
return file => new Promise(resolve => resolve(!pathMatch(file) || contentChange(file)));
};

module.exports = { addToContentMap, checkContentChanged, clearContentMap };
module.exports = { contentFactory };
37 changes: 12 additions & 25 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const { extname } = require('path');
const semver = require('semver');

const { clearFactory } = require('./clear');
const { addToContentMap, checkContentChanged, clearContentMap } = require('./content');
const { contentFactory } = require('./content');
const { configureDeps, configureIgnore } = require('./ignore');
const ipc = require('./ipc');
const localPath = require('./local-path');
Expand Down Expand Up @@ -53,7 +53,6 @@ module.exports = function (
const log = logFactory({ timestamp });
const notify = notifyFactory(notifyEnabled, log);

const isContent = configureIgnore(content);
const isIgnored = configureIgnore(ignore);
const isTooDeep = configureDeps(deps);

Expand All @@ -64,20 +63,18 @@ module.exports = function (

const watcher = filewatcher({ debounce, forcePolling, interval });
let isPaused = false;
let contentChanged = null;

// The child_process
let child;

watcher.on('change', file => {
const onChange = () => {
if (isPaused) return;
isPaused = true;

clearContentMap();
watcher.on('change', file =>
contentChanged(file).then(changed => {
if (!changed) return;
clearOutput();
notify('Restarting', `${file} has been modified`);

watcher.removeAll();
isPaused = true;
if (child) {
// Child is still running, restart upon exit
child.on('exit', start);
Expand All @@ -86,18 +83,8 @@ module.exports = function (
// Child is already stopped, probably due to a previous error
start();
}
};

if (isPaused) return;

if (isContent(file)) {
checkContentChanged(file, changed => {
if (changed) onChange();
});
return;
}
onChange();
});
})
);

watcher.on('fallback', limit => {
log.warn('node-dev ran out of file handles after watching %s files.', limit);
Expand All @@ -111,6 +98,7 @@ module.exports = function (
*/
function start() {
isPaused = false;
contentChanged = contentFactory(content);
const cmd = nodeArgs.concat(wrapper, script, scriptArgs);

if (extname(script) === '.mjs') {
Expand Down Expand Up @@ -140,10 +128,9 @@ module.exports = function (

// Listen for `required` messages and watch the required file.
ipc.on(child, 'required', ({ required }) => {
if (isPaused || isIgnored(required) || isTooDeep(required)) return;
watcher.add(required);
if (isContent(required)) {
addToContentMap(required);
if (!isPaused && !isIgnored(required) && !isTooDeep(required)) {
contentChanged(required);
watcher.add(required);
}
});

Expand Down
Loading