diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..6df74b83 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,142 @@ +name: ci + +on: +- pull_request +- push + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + name: + - Node.js 0.10 + - Node.js 0.12 + - io.js 1.x + - io.js 2.x + - io.js 3.x + - Node.js 4.x + - Node.js 5.x + - Node.js 6.x + - Node.js 7.x + - Node.js 8.x + - Node.js 9.x + - Node.js 10.x + - Node.js 11.x + - Node.js 12.x + - Node.js 13.x + - Node.js 14.x + - Node.js 15.x + + include: + - name: Node.js 0.10 + node-version: "0.10" + npm-i: mocha@3.5.3 rimraf@2.7.1 supertest@2.0.0 + + - name: Node.js 0.12 + node-version: "0.12" + npm-i: mocha@3.5.3 rimraf@2.7.1 supertest@2.0.0 + + - name: io.js 1.x + node-version: "1.8" + npm-i: mocha@3.5.3 rimraf@2.7.1 supertest@2.0.0 + + - name: io.js 2.x + node-version: "2.5" + npm-i: mocha@3.5.3 rimraf@2.7.1 supertest@2.0.0 + + - name: io.js 3.x + node-version: "3.3" + npm-i: mocha@3.5.3 rimraf@2.7.1 supertest@2.0.0 + + - name: Node.js 4.x + node-version: "4.9" + npm-i: mocha@5.2.0 rimraf@2.7.1 supertest@3.4.2 + + - name: Node.js 5.x + node-version: "5.12" + npm-i: mocha@5.2.0 rimraf@2.7.1 supertest@3.4.2 + + - name: Node.js 6.x + node-version: "6.17" + npm-i: mocha@6.2.3 + + - name: Node.js 7.x + node-version: "7.10" + npm-i: mocha@6.2.3 + + - name: Node.js 8.x + node-version: "8.17" + npm-i: mocha@7.2.0 + + - name: Node.js 9.x + node-version: "9.11" + npm-i: mocha@7.2.0 + + - name: Node.js 10.x + node-version: "10.24" + npm-i: mocha@8.4.0 + + - name: Node.js 11.x + node-version: "11.15" + npm-i: mocha@8.4.0 + + - name: Node.js 12.x + node-version: "12.22" + + - name: Node.js 13.x + node-version: "13.14" + + - name: Node.js 14.x + node-version: "14.18" + + - name: Node.js 15.x + node-version: "15.14" + + steps: + - uses: actions/checkout@v2 + + - name: Install Node.js ${{ matrix.node-version }} + shell: bash -eo pipefail -l {0} + run: | + nvm install --default ${{ matrix.node-version }} + dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" + + - name: Configure npm + run: npm config set shrinkwrap false + + - name: Install npm module(s) ${{ matrix.npm-i }} + run: npm install --save-dev ${{ matrix.npm-i }} + if: matrix.npm-i != '' + + - name: Setup Node.js version-specific dependencies + shell: bash + run: | + # eslint for linting + # - remove on Node.js < 10 + if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then + node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ + grep -E '^eslint(-|$)' | \ + sort -r | \ + xargs -n1 npm rm --silent --save-dev + fi + + - name: Install Node.js dependencies + run: npm install + + - name: List environment + id: list_env + shell: bash + run: | + echo "node@$(node -v)" + echo "npm@$(npm -v)" + npm -s ls ||: + (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print "::set-output name=" $2 "::" $3 }' + + - name: Run tests + run: npm run test-ci + + - name: Lint code + if: steps.list_env.outputs.eslint != '' + run: npm run lint diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 13edaf41..00000000 --- a/.travis.yml +++ /dev/null @@ -1,67 +0,0 @@ -language: node_js -node_js: - - "0.10" - - "0.12" - - "1.8" - - "2.5" - - "3.3" - - "4.9" - - "5.12" - - "6.16" - - "7.10" - - "8.15" - - "9.11" -matrix: - fast_finish: true -sudo: false -cache: - directories: - - node_modules -before_install: - # Configure npm - - | - # Skip updating shrinkwrap / lock - npm config set shrinkwrap false - # Setup Node.js version-specific dependencies - - | - # eslint for linting - # - remove on Node.js < 6 - if [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -lt 6 ]]; then - node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ - grep -E '^eslint(-|$)' | \ - xargs npm rm --save-dev - fi - - | - # mocha for testing - # - use 3.x for Node.js < 4 - # - use 5.x for Node.js < 6 - if [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -lt 4 ]]; then - npm install --save-dev mocha@3.5.3 - elif [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -lt 6 ]]; then - npm install --save-dev mocha@5.2.0 - fi - - | - # supertest for http calls - # - use 2.0.0 for Node.js < 4 - # - use 3.4.2 for Node.js < 6 - if [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -lt 4 ]]; then - npm install --save-dev supertest@2.0.0 - elif [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -lt 6 ]]; then - npm install --save-dev supertest@3.4.2 - fi - # Update Node.js modules - - | - # Prune and rebuild node_modules - if [[ -d node_modules ]]; then - npm prune - npm rebuild - fi -script: - # Run test script - - npm run test-ci - # Run linting - - | - # Run linting, depending on eslint install - if [[ -n "$(npm -ps ls eslint)" ]]; then - npm run-script lint - fi diff --git a/README.md b/README.md index ca18976e..fe8b13cf 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![NPM Version][npm-image]][npm-url] [![NPM Downloads][downloads-image]][downloads-url] -[![Linux Build][travis-image]][travis-url] +[![Linux Build][github-actions-ci-image]][github-actions-ci-url] [![Windows Build][appveyor-image]][appveyor-url] ## Installation @@ -44,7 +44,7 @@ This generator can also be further configured with the following command line fl --pug add pug engine support --hbs add handlebars engine support -H, --hogan add hogan.js engine support - -v, --view add view support (dust|ejs|hbs|hjs|jade|pug|twig|vash) (defaults to jade) + -v, --view add view support (dust|ejs|hbs|hjs|jade|pug|twig|vash) (defaults to pug) --no-view use static html instead of view engine -c, --css add stylesheet support (less|stylus|compass|sass) (defaults to plain css) --git add .gitignore @@ -57,9 +57,9 @@ This generator can also be further configured with the following command line fl [npm-image]: https://img.shields.io/npm/v/express-generator.svg [npm-url]: https://npmjs.org/package/express-generator -[travis-image]: https://img.shields.io/travis/expressjs/generator/master.svg?label=linux -[travis-url]: https://travis-ci.org/expressjs/generator [appveyor-image]: https://img.shields.io/appveyor/ci/dougwilson/generator/master.svg?label=windows [appveyor-url]: https://ci.appveyor.com/project/dougwilson/generator [downloads-image]: https://img.shields.io/npm/dm/express-generator.svg [downloads-url]: https://npmjs.org/package/express-generator +[github-actions-ci-image]: https://img.shields.io/github/workflow/status/expressjs/generator/ci/master?label=linux +[github-actions-ci-url]: https://github.com/expressjs/generator/actions/workflows/ci.yml diff --git a/appveyor.yml b/appveyor.yml index 6e5603a1..45a5f29c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,8 +1,5 @@ environment: matrix: - - nodejs_version: "0.10" - - nodejs_version: "0.12" - - nodejs_version: "1.8" - nodejs_version: "2.5" - nodejs_version: "3.3" - nodejs_version: "4.9" @@ -11,6 +8,12 @@ environment: - nodejs_version: "7.10" - nodejs_version: "8.15" - nodejs_version: "9.11" + - nodejs_version: "10.24" + - nodejs_version: "11.15" + - nodejs_version: "12.22" + - nodejs_version: "13.14" + - nodejs_version: "14.18" + - nodejs_version: "15.14" cache: - node_modules install: @@ -33,10 +36,25 @@ install: # mocha for testing # - use 3.x for Node.js < 4 # - use 5.x for Node.js < 6 + # - use 6.x for Node.js < 8 + # - use 7.x for Node.js < 10 + # - use 8.x for Node.js < 12 if ([int]$env:nodejs_version.split(".")[0] -lt 4) { npm install --silent --save-dev mocha@3.5.3 } elseif ([int]$env:nodejs_version.split(".")[0] -lt 6) { npm install --silent --save-dev mocha@5.2.0 + } elseif ([int]$env:nodejs_version.split(".")[0] -lt 8) { + npm install --silent --save-dev mocha@6.2.3 + } elseif ([int]$env:nodejs_version.split(".")[0] -lt 10) { + npm install --silent --save-dev mocha@7.2.0 + } elseif ([int]$env:nodejs_version.split(".")[0] -lt 12) { + npm install --silent --save-dev mocha@8.4.0 + } + - ps: | + # rimraf for testing cleanup + # - use 2.7.1 for Node.js < 6 + if ([int]$env:nodejs_version.split(".")[0] -lt 6) { + npm install --silent --save-dev rimraf@2.7.1 } - ps: | # supertest for http calls diff --git a/bin/express-cli.js b/bin/express-cli.js index d0c80b7f..55a08beb 100755 --- a/bin/express-cli.js +++ b/bin/express-cli.js @@ -52,7 +52,7 @@ program .option(' --pug', 'add pug engine support', renamedOption('--pug', '--view=pug')) .option(' --hbs', 'add handlebars engine support', renamedOption('--hbs', '--view=hbs')) .option('-H, --hogan', 'add hogan.js engine support', renamedOption('--hogan', '--view=hogan')) - .option('-v, --view ', 'add view support (dust|ejs|hbs|hjs|jade|pug|twig|vash) (defaults to jade)') + .option('-v, --view ', 'add view support (dust|ejs|hbs|hjs|jade|pug|twig|vash) (defaults to pug)') .option(' --no-view', 'use static html instead of view engine') .option('-c, --css ', 'add stylesheet support (less|stylus|compass|sass) (defaults to plain css)') .option(' --git', 'add .gitignore') @@ -145,8 +145,8 @@ function createApplication (name, dir) { start: 'node ./bin/www' }, dependencies: { - 'debug': '~2.6.9', - 'express': '~4.16.1' + debug: '~2.6.9', + express: '~4.17.1' } } @@ -166,7 +166,7 @@ function createApplication (name, dir) { // Request logger app.locals.modules.logger = 'morgan' app.locals.uses.push("logger('dev')") - pkg.dependencies.morgan = '~1.9.1' + pkg.dependencies.morgan = '~1.10.0' // Body parsers app.locals.uses.push('express.json()') @@ -175,7 +175,7 @@ function createApplication (name, dir) { // Cookie parser app.locals.modules.cookieParser = 'cookie-parser' app.locals.uses.push('cookieParser()') - pkg.dependencies['cookie-parser'] = '~1.4.4' + pkg.dependencies['cookie-parser'] = '~1.4.5' if (dir !== '.') { mkdir(dir, '.') @@ -212,7 +212,7 @@ function createApplication (name, dir) { if (program.view) { // Copy view templates mkdir(dir, 'views') - pkg.dependencies['http-errors'] = '~1.6.3' + pkg.dependencies['http-errors'] = '~1.7.2' switch (program.view) { case 'dust': copyTemplateMulti('views', dir + '/views', '*.dust') @@ -264,7 +264,7 @@ function createApplication (name, dir) { case 'stylus': app.locals.modules.stylus = 'stylus' app.locals.uses.push("stylus.middleware(path.join(__dirname, 'public'))") - pkg.dependencies['stylus'] = '0.54.5' + pkg.dependencies.stylus = '0.54.5' break } @@ -304,7 +304,7 @@ function createApplication (name, dir) { break case 'pug': app.locals.view = { engine: 'pug' } - pkg.dependencies.pug = '2.0.0-beta11' + pkg.dependencies.pug = '~3.0.2' break case 'twig': app.locals.view = { engine: 'twig' } @@ -456,14 +456,14 @@ function main () { if (program.ejs) program.view = 'ejs' if (program.hbs) program.view = 'hbs' if (program.hogan) program.view = 'hjs' - if (program.pug) program.view = 'pug' + if (program.jade) program.view = 'jade' } // Default view engine if (program.view === true) { - warning('the default view engine will not be jade in future releases\n' + - "use `--view=jade' or `--help' for additional options") - program.view = 'jade' + warning('the default view engine is pug now\n' + + "use `--view=jade' if you want to use jade or `--help' for additional options") + program.view = 'pug' } // Generate application diff --git a/package.json b/package.json index f9bfe584..f0078f63 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "express-generator", "description": "Express' application generator", - "version": "4.16.1", + "version": "5.0.0", "author": "TJ Holowaychuk ", "contributors": [ "Aaron Heckmann ", @@ -25,8 +25,8 @@ "repository": "expressjs/generator", "license": "MIT", "dependencies": { - "commander": "2.15.1", - "ejs": "2.6.1", + "commander": "2.16.0", + "ejs": "2.6.2", "minimatch": "3.0.4", "mkdirp": "0.5.1", "sorted-object": "2.0.1" @@ -37,21 +37,21 @@ "express": "./bin/express-cli.js" }, "devDependencies": { - "eslint": "5.16.0", - "eslint-config-standard": "12.0.0", - "eslint-plugin-import": "2.17.2", - "eslint-plugin-node": "8.0.1", - "eslint-plugin-promise": "4.1.1", - "eslint-plugin-standard": "4.0.0", - "mocha": "6.1.4", - "rimraf": "2.6.3", - "supertest": "4.0.2", - "tree-kill": "1.2.1", + "eslint": "7.32.0", + "eslint-config-standard": "14.1.1", + "eslint-plugin-import": "2.24.2", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-promise": "5.1.0", + "eslint-plugin-standard": "4.1.0", + "mocha": "9.1.0", + "rimraf": "3.0.2", + "supertest": "6.1.4", + "tree-kill": "1.2.2", "uid-safe": "2.1.5", "validate-npm-package-name": "3.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 2.5" }, "files": [ "LICENSE", diff --git a/test/cmd.js b/test/cmd.js index c0711a73..d054ff8e 100644 --- a/test/cmd.js +++ b/test/cmd.js @@ -15,6 +15,7 @@ var APP_START_STOP_TIMEOUT = 10000 var PKG_PATH = path.resolve(__dirname, '..', 'package.json') var BIN_PATH = path.resolve(path.dirname(PKG_PATH), require(PKG_PATH).bin.express) var NPM_INSTALL_TIMEOUT = 300000 // 5 minutes +var STDERR_MAX_BUFFER = 5 * 1024 * 1024 // 5mb var TEMP_DIR = utils.tmpDir() describe('express(1)', function () { @@ -37,8 +38,8 @@ describe('express(1)', function () { }) }) - it('should print jade view warning', function () { - assert.strictEqual(ctx.stderr, "\n warning: the default view engine will not be jade in future releases\n warning: use `--view=jade' or `--help' for additional options\n\n") + it('should print pug view warning', function () { + assert.strictEqual(ctx.stderr, "\n warning: the default view engine is pug now\n warning: use `--view=jade' if you want to use jade or `--help' for additional options\n\n") }) it('should provide debug instructions', function () { @@ -51,10 +52,10 @@ describe('express(1)', function () { assert.notStrictEqual(ctx.files.indexOf('package.json'), -1) }) - it('should have jade templates', function () { - assert.notStrictEqual(ctx.files.indexOf('views/error.jade'), -1) - assert.notStrictEqual(ctx.files.indexOf('views/index.jade'), -1) - assert.notStrictEqual(ctx.files.indexOf('views/layout.jade'), -1) + it('should have pug templates', function () { + assert.notStrictEqual(ctx.files.indexOf('views/error.pug'), -1) + assert.notStrictEqual(ctx.files.indexOf('views/index.pug'), -1) + assert.notStrictEqual(ctx.files.indexOf('views/layout.pug'), -1) }) it('should have a package.json file', function () { @@ -68,12 +69,12 @@ describe('express(1)', function () { ' "start": "node ./bin/www"\n' + ' },\n' + ' "dependencies": {\n' + - ' "cookie-parser": "~1.4.4",\n' + + ' "cookie-parser": "~1.4.5",\n' + ' "debug": "~2.6.9",\n' + - ' "express": "~4.16.1",\n' + - ' "http-errors": "~1.6.3",\n' + - ' "jade": "~1.11.0",\n' + - ' "morgan": "~1.9.1"\n' + + ' "express": "~4.17.1",\n' + + ' "http-errors": "~1.7.2",\n' + + ' "pug": "~3.0.2",\n' + + ' "morgan": "~1.10.0"\n' + ' }\n' + '}\n') }) @@ -222,10 +223,10 @@ describe('express(1)', function () { assert.notStrictEqual(ctx.files.indexOf('foo/package.json'), -1) }) - it('should have jade templates', function () { - assert.notStrictEqual(ctx.files.indexOf('foo/views/error.jade'), -1) - assert.notStrictEqual(ctx.files.indexOf('foo/views/index.jade'), -1) - assert.notStrictEqual(ctx.files.indexOf('foo/views/layout.jade'), -1) + it('should have pug templates', function () { + assert.notStrictEqual(ctx.files.indexOf('foo/views/error.pug'), -1) + assert.notStrictEqual(ctx.files.indexOf('foo/views/index.pug'), -1) + assert.notStrictEqual(ctx.files.indexOf('foo/views/layout.pug'), -1) }) }) @@ -282,6 +283,13 @@ describe('express(1)', function () { assert.notStrictEqual(ctx.files.indexOf('public/stylesheets/style.less'), -1, 'should have style.less file') }) + it('should have less-middleware in package dependencies', function () { + var file = path.resolve(ctx.dir, 'package.json') + var contents = fs.readFileSync(file, 'utf8') + var pkg = JSON.parse(contents) + assert.strictEqual(typeof pkg.dependencies['less-middleware'], 'string') + }) + it('should have installable dependencies', function (done) { this.timeout(NPM_INSTALL_TIMEOUT) npmInstall(ctx.dir, done) @@ -338,6 +346,13 @@ describe('express(1)', function () { assert.notStrictEqual(ctx.files.indexOf('public/stylesheets/style.sass'), -1, 'should have style.sass file') }) + it('should have node-sass-middleware in package dependencies', function () { + var file = path.resolve(ctx.dir, 'package.json') + var contents = fs.readFileSync(file, 'utf8') + var pkg = JSON.parse(contents) + assert.strictEqual(typeof pkg.dependencies['node-sass-middleware'], 'string') + }) + it('should have installable dependencies', function (done) { this.timeout(NPM_INSTALL_TIMEOUT) npmInstall(ctx.dir, done) @@ -394,6 +409,13 @@ describe('express(1)', function () { assert.notStrictEqual(ctx.files.indexOf('public/stylesheets/style.styl'), -1, 'should have style.styl file') }) + it('should have stylus in package dependencies', function () { + var file = path.resolve(ctx.dir, 'package.json') + var contents = fs.readFileSync(file, 'utf8') + var pkg = JSON.parse(contents) + assert.strictEqual(typeof pkg.dependencies.stylus, 'string') + }) + it('should have installable dependencies', function (done) { this.timeout(NPM_INSTALL_TIMEOUT) npmInstall(ctx.dir, done) @@ -475,10 +497,10 @@ describe('express(1)', function () { assert.notStrictEqual(ctx.files.indexOf('.gitignore'), -1, 'should have .gitignore file') }) - it('should have jade templates', function () { - assert.notStrictEqual(ctx.files.indexOf('views/error.jade'), -1) - assert.notStrictEqual(ctx.files.indexOf('views/index.jade'), -1) - assert.notStrictEqual(ctx.files.indexOf('views/layout.jade'), -1) + it('should have pug templates', function () { + assert.notStrictEqual(ctx.files.indexOf('views/error.pug'), -1) + assert.notStrictEqual(ctx.files.indexOf('views/index.pug'), -1) + assert.notStrictEqual(ctx.files.indexOf('views/layout.pug'), -1) }) }) @@ -719,6 +741,13 @@ describe('express(1)', function () { assert.notStrictEqual(ctx.files.indexOf('views/index.dust'), -1, 'should have views/index.dust file') }) + it('should have adaro in package dependencies', function () { + var file = path.resolve(ctx.dir, 'package.json') + var contents = fs.readFileSync(file, 'utf8') + var pkg = JSON.parse(contents) + assert.strictEqual(typeof pkg.dependencies.adaro, 'string') + }) + it('should have installable dependencies', function (done) { this.timeout(NPM_INSTALL_TIMEOUT) npmInstall(ctx.dir, done) @@ -776,6 +805,13 @@ describe('express(1)', function () { assert.notStrictEqual(ctx.files.indexOf('views/index.ejs'), -1, 'should have views/index.ejs file') }) + it('should have ejs in package dependencies', function () { + var file = path.resolve(ctx.dir, 'package.json') + var contents = fs.readFileSync(file, 'utf8') + var pkg = JSON.parse(contents) + assert.strictEqual(typeof pkg.dependencies.ejs, 'string') + }) + it('should have installable dependencies', function (done) { this.timeout(NPM_INSTALL_TIMEOUT) npmInstall(ctx.dir, done) @@ -1004,6 +1040,71 @@ describe('express(1)', function () { }) }) + describe('jade', function () { + var ctx = setupTestEnvironment(this.fullTitle()) + + it('should create basic app with jade templates', function (done) { + run(ctx.dir, ['--view', 'jade'], function (err, stdout) { + if (err) return done(err) + ctx.files = utils.parseCreatedFiles(stdout, ctx.dir) + assert.strictEqual(ctx.files.length, 16) + done() + }) + }) + + it('should have basic files', function () { + assert.notStrictEqual(ctx.files.indexOf('bin/www'), -1) + assert.notStrictEqual(ctx.files.indexOf('app.js'), -1) + assert.notStrictEqual(ctx.files.indexOf('package.json'), -1) + }) + + it('should have jade in package dependencies', function () { + var file = path.resolve(ctx.dir, 'package.json') + var contents = fs.readFileSync(file, 'utf8') + var dependencies = JSON.parse(contents).dependencies + assert.ok(typeof dependencies.jade === 'string') + }) + + it('should have jade templates', function () { + assert.notStrictEqual(ctx.files.indexOf('views/error.jade'), -1) + assert.notStrictEqual(ctx.files.indexOf('views/index.jade'), -1) + assert.notStrictEqual(ctx.files.indexOf('views/layout.jade'), -1) + }) + + it('should have installable dependencies', function (done) { + this.timeout(NPM_INSTALL_TIMEOUT) + npmInstall(ctx.dir, done) + }) + + describe('npm start', function () { + before('start app', function () { + this.app = new AppRunner(ctx.dir) + }) + + after('stop app', function (done) { + this.timeout(APP_START_STOP_TIMEOUT) + this.app.stop(done) + }) + + it('should start app', function (done) { + this.timeout(APP_START_STOP_TIMEOUT) + this.app.start(done) + }) + + it('should respond to HTTP request', function (done) { + request(this.app) + .get('/') + .expect(200, /Express<\/title>/, done) + }) + + it('should generate a 404', function (done) { + request(this.app) + .get('/does_not_exist') + .expect(404, /<h1>Not Found<\/h1>/, done) + }) + }) + }) + describe('twig', function () { var ctx = setupTestEnvironment(this.fullTitle()) @@ -1139,7 +1240,7 @@ describe('express(1)', function () { function npmInstall (dir, callback) { var env = utils.childEnvironment() - exec('npm install', { cwd: dir, env: env }, function (err, stderr) { + exec('npm install', { cwd: dir, env: env, maxBuffer: STDERR_MAX_BUFFER }, function (err, stderr) { if (err) { err.message += stderr callback(err)