diff --git a/.appveyor.yml b/.appveyor.yml index bc20403..22d9ff5 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -2,9 +2,8 @@ build: off environment: matrix: + - nodejs_version: '20' - nodejs_version: '18' - - nodejs_version: '16' - - nodejs_version: '14' install: - ps: Install-Product node $env:nodejs_version diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 8073209..54690b4 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -8,17 +8,16 @@ on: jobs: build_ubuntu: - runs-on: ubuntu-latest strategy: matrix: - node-version: ["14.x", "16.x", "18.x"] + node-version: ["18.x", "20.x"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm i @@ -29,13 +28,33 @@ jobs: strategy: matrix: - node-version: ["14.x", "16.x", "18.x"] + node-version: ["18.x", "20.x"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm i - run: npm test + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: lts + - run: npm i + - run: npm run lint + + prettier: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: lts + - run: npm i + - run: npm run prettier diff --git a/.travis.yml b/.travis.yml index 25d33de..c259ed5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ jobs: script: npm run lint language: node_js node_js: + - 20 - 18 - - 16 - - 14 sudo: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c6c43a..de96b81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # node-dev +## v9.0.0 / 2023-09-16 + +- Drop support for node v14 and v16, the new minimum version of node is v18 (@bjornstar) +- Add support for node v20 (@bjornstar) +- Removed tests for `experimental-specifier-resolution` as it's no longer supported (@bjornstar) +- [CI] Test v18 & v20 (@bjornstar) +- [CI] Perform linting and prettier in github actions (@bjornstar) +- [`devDependencies`] Update most devDependencies to their latest version (@bjornstar) + ## v8.0.0 / 2022-12-30 - Suppress experimental warnings in node v18 (@tmont) diff --git a/lib/index.js b/lib/index.js index b4ecea6..231ae90 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,8 +1,7 @@ const { fork } = require('child_process'); const filewatcher = require('filewatcher'); -const { join } = require('path'); -const semver = require('semver'); -const { pathToFileURL } = require('url'); +const { join } = require('node:path'); +const { pathToFileURL } = require('node:url'); const { clearFactory } = require('./clear'); const { configureDeps, configureIgnore } = require('./ignore'); @@ -92,20 +91,16 @@ module.exports = function ( function start() { isPaused = false; - const args = nodeArgs.slice(); - - args.push(`--require=${resolveMain(localPath('wrap'))}`); - - const loaderName = semver.satisfies(process.version, '>=16.12.0') ? 'load' : 'get-format'; - - const loaderURL = pathToFileURL(resolveMain(localPath(join('loaders', `${loaderName}.mjs`)))); - - args.push(`--experimental-loader=${loaderURL.href}`); + const loaderURL = pathToFileURL(resolveMain(localPath(join('loaders', 'load.mjs')))); child = fork(script, scriptArgs, { cwd: process.cwd(), env: process.env, - execArgv: args + execArgv: [ + ...nodeArgs.slice(), + `--experimental-loader=${loaderURL.href}`, + `--require=${resolveMain(localPath('wrap'))}` + ] }); if (respawn) { diff --git a/lib/loaders/load.mjs b/lib/loaders/load.mjs index 2772f60..bbda231 100644 --- a/lib/loaders/load.mjs +++ b/lib/loaders/load.mjs @@ -1,16 +1,23 @@ -import { createRequire } from 'module'; -import { fileURLToPath } from 'url'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; import { send } from './ipc.mjs'; const require = createRequire(import.meta.url); -export async function load(url, context, defaultLoad) { +let connectedPort; + +export async function initialize({ port } = {}) { + connectedPort = port; +} + +export async function load(url, context, nextLoad) { const required = url.startsWith('file://') ? fileURLToPath(url) : url; send({ required }); + if (connectedPort) connectedPort.postMessage({ required }); try { - return await defaultLoad(url, context, defaultLoad); + return await nextLoad(url, context); } catch (error) { if (error.code !== 'ERR_UNKNOWN_FILE_EXTENSION') throw error; return require('get-package-type')(required).then(format => { diff --git a/lib/register-loader.js b/lib/register-loader.js new file mode 100644 index 0000000..4176fc3 --- /dev/null +++ b/lib/register-loader.js @@ -0,0 +1,27 @@ +const { register } = require('node:module'); +const { join } = require('node:path'); +const { pathToFileURL } = require('node:url'); +const semver = require('semver'); + +const { send } = require('./ipc'); +const localPath = require('./local-path'); +const resolveMain = require('./resolve-main'); + +exports.registerLoader = () => { + if (!semver.satisfies(process.version, '>=20.6.0')) return; + + const loaderURL = pathToFileURL(resolveMain(localPath(join('loaders', 'load.mjs')))); + + const { port1, port2 } = new MessageChannel(); + port1.on('message', ({ required } = {}) => { + send({ required }); + }); + + register(loaderURL.href, { + parentURL: loaderURL.href, + data: { port: port2 }, + transferList: [port2] + }); + + return port1; +}; diff --git a/lib/wrap.js b/lib/wrap.js index 7889ea5..b18d0e3 100755 --- a/lib/wrap.js +++ b/lib/wrap.js @@ -1,11 +1,12 @@ -const { dirname, extname } = require('path'); -const childProcess = require('child_process'); +const childProcess = require('node:child_process'); +const { dirname, extname } = require('node:path'); +const { isMainThread } = require('node:worker_threads'); const { sync: resolve } = require('resolve'); -const { isMainThread } = require('worker_threads'); const { getConfig } = require('./cfg'); const hook = require('./hook'); const { relay, send } = require('./ipc'); +const { registerLoader } = require('./register-loader'); const resolveMain = require('./resolve-main'); const suppressExperimentalWarnings = require('./suppress-experimental-warnings'); @@ -25,8 +26,14 @@ if (process.env.NODE_DEV_PRELOAD) { require(process.env.NODE_DEV_PRELOAD); } -// We want to exit on SIGTERM, but defer to existing SIGTERM handlers. -process.once('SIGTERM', () => process.listenerCount('SIGTERM') || process.exit(0)); +const port = registerLoader(); + +process.once('SIGTERM', () => { + port?.close(); + // We want to exit on SIGTERM, but defer to existing SIGTERM handlers. + if (process.listenerCount('SIGTERM')) return; + process.exit(0); +}); if (fork) { // Overwrite child_process.fork() so that we can hook into forked processes diff --git a/package.json b/package.json index 96afe80..4c7edc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-dev", - "version": "8.0.0", + "version": "9.0.0", "description": "Restarts your app when files are modified", "keywords": [ "restart", @@ -24,35 +24,36 @@ }, "main": "./lib", "engines": { - "node": ">=14" + "node": ">=18" }, "scripts": { "lint": "eslint lib test bin/node-dev", - "test": "node test", - "prepare": "husky install" + "prepare": "husky install", + "prettier": "prettier lib test bin/node-dev", + "test": "node test" }, "dependencies": { "dateformat": "^3.0.3", "dynamic-dedupe": "^0.3.0", - "filewatcher": "~3.0.0", + "filewatcher": "^3.0.1", "get-package-type": "^0.1.0", "minimist": "^1.2.6", "node-notifier": "^8.0.1", - "resolve": "^1.22.0", - "semver": "^7.3.7" + "resolve": "^1.22.6", + "semver": "^7.5.4" }, "devDependencies": { - "@types/node": "^18.11.18", - "eslint": "^8.30.0", - "eslint-plugin-import": "^2.26.0", - "husky": "^8.0.2", - "lint-staged": "^13.1.0", - "prettier": "^2.6.2", - "tap": "^16.3.2", + "@types/node": "^20.6.2", + "eslint": "^8.49.0", + "eslint-plugin-import": "^2.28.1", + "husky": "^8.0.3", + "lint-staged": "^14.0.1", + "prettier": "^3.0.3", + "tap": "^18.0.4", "tap-xunit": "^2.4.1", "touch": "^3.1.0", - "ts-node": "^10.7.0", - "typescript": "^4.6.3" + "ts-node": "^10.9.1", + "typescript": "^5.2.2" }, "lint-staged": { "*.{js,mjs}": "eslint --cache --fix", diff --git a/test/fixture/cluster.js b/test/fixture/cluster.js index cf2ac3b..63f8315 100644 --- a/test/fixture/cluster.js +++ b/test/fixture/cluster.js @@ -1,4 +1,4 @@ -const { disconnect, fork, isMaster, isWorker } = require('cluster'); +const { fork, isMaster, isWorker } = require('node:cluster'); const createWorker = i => { const worker = fork(); @@ -26,15 +26,16 @@ if (isWorker) { } if (isMaster) { + const workers = []; for (let i = 0; i < 2; i += 1) { console.log('Forking worker', i); - createWorker(i); + workers.push(createWorker(i)); } process.once('SIGTERM', () => { console.log('Master received SIGTERM'); - disconnect(() => { - console.log('All workers disconnected.'); - }); + workers.forEach(worker => worker.kill()); + console.log('All workers disconnected.'); + process.exit(0); }); } diff --git a/test/fixture/experimental-specifier-resolution/index.mjs b/test/fixture/experimental-specifier-resolution/index.mjs deleted file mode 100644 index 0ac70ad..0000000 --- a/test/fixture/experimental-specifier-resolution/index.mjs +++ /dev/null @@ -1 +0,0 @@ -export default 'experimental-specifier-resolution'; diff --git a/test/fixture/ipc-server.js b/test/fixture/ipc-server.js index 91fc2fc..afd41bf 100644 --- a/test/fixture/ipc-server.js +++ b/test/fixture/ipc-server.js @@ -19,12 +19,12 @@ server process.on('message', data => { if (data === 'node-dev:restart') { console.log('ipc-server.js - IPC received'); - server.close(); + server.close(() => process.exit(0)); } }); process.once('beforeExit', () => console.log('exit')); process.once('SIGTERM', () => { - if (server.listening) server.close(); + if (server.listening) server.close(() => process.exit(0)); }); diff --git a/test/fixture/resolution.mjs b/test/fixture/resolution.mjs deleted file mode 100644 index c4d373c..0000000 --- a/test/fixture/resolution.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import resolution from './experimental-specifier-resolution'; -import message from './message'; - -setTimeout(() => {}, 10000); - -console.log(resolution); -console.log(message); diff --git a/test/spawn/esmodule.js b/test/spawn/esmodule.js index 1b6b816..1fa2c5f 100644 --- a/test/spawn/esmodule.js +++ b/test/spawn/esmodule.js @@ -2,20 +2,6 @@ const tap = require('tap'); const { spawn, touchFile } = require('../utils'); -tap.test('Supports ECMAScript modules with experimental-specifier-resolution', t => { - spawn('--experimental-specifier-resolution=node resolution.mjs', out => { - if (out.match(/touch message.js/)) { - touchFile('message.js'); - return out2 => { - if (out2.match(/Restarting/)) { - t.match(out2, /\[INFO\] \d{2}:\d{2}:\d{2} Restarting/); - return { exit: t.end.bind(t) }; - } - }; - } - }); -}); - tap.test('Supports ECMAScript modules', t => { spawn('ecma-script-modules.mjs', out => { if (out.match(/touch message.mjs/)) {