From 19443b81bc4584b582297ea35359d732e5309e55 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Tue, 18 May 2021 21:22:38 +0200 Subject: [PATCH 1/5] Properly escape extra args --- lib/run-script-pkg.js | 8 +- package-lock.json | 175 +++++++++++++++++++++++++++++++++-------- package.json | 1 + test/run-script-pkg.js | 19 +++-- 4 files changed, 163 insertions(+), 40 deletions(-) diff --git a/lib/run-script-pkg.js b/lib/run-script-pkg.js index ccde173..72b242b 100644 --- a/lib/run-script-pkg.js +++ b/lib/run-script-pkg.js @@ -4,6 +4,10 @@ const packageEnvs = require('./package-envs.js') const { isNodeGypPackage, defaultGypInstallScript } = require('@npmcli/node-gyp') const signalManager = require('./signal-manager.js') const isServerPackage = require('./is-server-package.js') +const { ShellString, unquoted } = require('puka'); + +const sh = (...args) => + ShellString.sh(...args).toString(process.env.__FAKE_TESTING_PLATFORM__ || process.platform) // you wouldn't like me when I'm angry... const bruce = (id, event, cmd) => @@ -31,7 +35,7 @@ const runScriptPkg = async options => { if (options.cmd) cmd = options.cmd else if (pkg.scripts && pkg.scripts[event]) - cmd = pkg.scripts[event] + args.map(a => ` ${JSON.stringify(a)}`).join('') + cmd = sh`${unquoted(pkg.scripts[event])} ${args}` else if ( // If there is no preinstall or install script, default to rebuilding node-gyp packages. event === 'install' && !scripts.install && @@ -41,7 +45,7 @@ const runScriptPkg = async options => { ) cmd = defaultGypInstallScript else if (event === 'start' && await isServerPackage(path)) - cmd = 'node server.js' + args.map(a => ` ${JSON.stringify(a)}`).join('') + cmd = sh`node server.js ${args}` if (!cmd) return { code: 0, signal: null } diff --git a/package-lock.json b/package-lock.json index 77c30ca..78e7bde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@npmcli/promise-spawn": "^1.3.2", "infer-owner": "^1.0.4", "node-gyp": "^7.1.0", + "puka": "^1.0.1", "read-package-json-fast": "^2.0.1" }, "devDependencies": { @@ -3591,6 +3592,14 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, + "node_modules/puka": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/puka/-/puka-1.0.1.tgz", + "integrity": "sha512-ssjRZxBd7BT3dte1RR3VoeT2cT/ODH8x+h0rUF1rMqB0srHYf48stSDWfiYakTp5UBZMxroZhB2+ExLDHm7W3g==", + "engines": { + "node": ">=4" + } + }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -4238,7 +4247,136 @@ "bundleDependencies": [ "ink", "treport", - "@types/react" + "@types/react", + "@babel/code-frame", + "@babel/compat-data", + "@babel/core", + "@babel/generator", + "@babel/helper-annotate-as-pure", + "@babel/helper-compilation-targets", + "@babel/helper-function-name", + "@babel/helper-get-function-arity", + "@babel/helper-member-expression-to-functions", + "@babel/helper-module-imports", + "@babel/helper-module-transforms", + "@babel/helper-optimise-call-expression", + "@babel/helper-plugin-utils", + "@babel/helper-replace-supers", + "@babel/helper-simple-access", + "@babel/helper-split-export-declaration", + "@babel/helper-validator-identifier", + "@babel/helper-validator-option", + "@babel/helpers", + "@babel/highlight", + "@babel/parser", + "@babel/plugin-proposal-object-rest-spread", + "@babel/plugin-syntax-jsx", + "@babel/plugin-syntax-object-rest-spread", + "@babel/plugin-transform-destructuring", + "@babel/plugin-transform-parameters", + "@babel/plugin-transform-react-jsx", + "@babel/template", + "@babel/traverse", + "@babel/types", + "@types/prop-types", + "@types/scheduler", + "@types/yoga-layout", + "ansi-escapes", + "ansi-styles", + "ansicolors", + "arrify", + "astral-regex", + "auto-bind", + "balanced-match", + "brace-expansion", + "browserslist", + "caller-callsite", + "caller-path", + "callsites", + "caniuse-lite", + "cardinal", + "chalk", + "ci-info", + "cli-cursor", + "cli-truncate", + "color-convert", + "color-name", + "colorette", + "commondir", + "concat-map", + "convert-source-map", + "csstype", + "debug", + "electron-to-chromium", + "emoji-regex", + "escalade", + "escape-string-regexp", + "esprima", + "events-to-array", + "find-cache-dir", + "find-up", + "fs.realpath", + "gensync", + "glob", + "globals", + "has-flag", + "import-jsx", + "inflight", + "inherits", + "is-ci", + "is-fullwidth-code-point", + "js-tokens", + "jsesc", + "json5", + "locate-path", + "lodash", + "lodash.throttle", + "log-update", + "loose-envify", + "make-dir", + "mimic-fn", + "minimatch", + "minimist", + "minipass", + "ms", + "node-releases", + "object-assign", + "once", + "onetime", + "p-limit", + "p-locate", + "p-try", + "path-exists", + "path-is-absolute", + "pkg-dir", + "prop-types", + "punycode", + "react-is", + "react-reconciler", + "redeyed", + "resolve-from", + "restore-cursor", + "rimraf", + "safe-buffer", + "scheduler", + "semver", + "signal-exit", + "slice-ansi", + "source-map", + "string-length", + "string-width", + "supports-color", + "tap-parser", + "tap-yaml", + "to-fast-properties", + "type-fest", + "unicode-length", + "widest-line", + "wrap-ansi", + "wrappy", + "yallist", + "yaml", + "yoga-layout-prebuilt" ], "dev": true, "dependencies": { @@ -4731,14 +4869,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tap/node_modules/ansi-regex": { - "version": "3.0.0", - "extraneous": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/tap/node_modules/ansi-styles": { "version": "3.2.1", "dev": true, @@ -5910,17 +6040,6 @@ "node": ">=8" } }, - "node_modules/tap/node_modules/strip-ansi": { - "version": "4.0.0", - "extraneous": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/tap/node_modules/supports-color": { "version": "5.5.0", "dev": true, @@ -9541,6 +9660,11 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, + "puka": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/puka/-/puka-1.0.1.tgz", + "integrity": "sha512-ssjRZxBd7BT3dte1RR3VoeT2cT/ODH8x+h0rUF1rMqB0srHYf48stSDWfiYakTp5UBZMxroZhB2+ExLDHm7W3g==" + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -10397,10 +10521,6 @@ "type-fest": "^0.21.3" } }, - "ansi-regex": { - "version": "3.0.0", - "extraneous": true - }, "ansi-styles": { "version": "3.2.1", "bundled": true, @@ -11181,13 +11301,6 @@ } } }, - "strip-ansi": { - "version": "4.0.0", - "extraneous": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, "supports-color": { "version": "5.5.0", "bundled": true, diff --git a/package.json b/package.json index 756f87f..a648f4f 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@npmcli/promise-spawn": "^1.3.2", "infer-owner": "^1.0.4", "node-gyp": "^7.1.0", + "puka": "^1.0.1", "read-package-json-fast": "^2.0.1" }, "files": [ diff --git a/test/run-script-pkg.js b/test/run-script-pkg.js index bbfe4bd..5fb3a3e 100644 --- a/test/run-script-pkg.js +++ b/test/run-script-pkg.js @@ -1,6 +1,7 @@ const { EventEmitter } = require('events') const t = require('tap') const requireInject = require('require-inject') +const isWindows = require('../lib/is-windows.js') let fakeIsNodeGypPackage = false let SIGNAL = null @@ -96,7 +97,7 @@ t.test('pkg has server.js, start not specified, with args', async t => { scripts: {}, }, }) - t.strictSame(res, ['sh', ['-c', 'node server.js "a" "b" "c"'], { + t.strictSame(res, ['sh', ['-c', 'node server.js a b c'], { stdioString: false, event: 'start', path, @@ -105,10 +106,10 @@ t.test('pkg has server.js, start not specified, with args', async t => { environ: 'value', }, stdio: 'pipe', - cmd: 'node server.js "a" "b" "c"', + cmd: 'node server.js a b c', }, { event: 'start', - script: 'node server.js "a" "b" "c"', + script: 'node server.js a b c', pkgid: 'foo@1.2.3', path, }]) @@ -288,6 +289,10 @@ t.test('pkg has foo script', t => runScriptPkg({ path: 'path', }]))) +const expectedCommand = isWindows + ? 'bar a --flag "markdown `code`" ^^^"$X^^^ \\\\\\^^^"blah\\\\\\^^^"^^^" $PWD ^^^%CD^^^% "^" ! \\ ">" "<" "|" "&" \' ^^^"\\^^^"^^^" ` " " ""' + : "bar a --flag 'markdown `code`' '$X \\\"blah\\\"' '$PWD' %CD% ^ ! '\\' '>' '<' '|' '&' \\' '\"' '`' ' ' ''" + t.test('pkg has foo script, with args', t => runScriptPkg({ event: 'foo', path: 'path', @@ -302,8 +307,8 @@ t.test('pkg has foo script, with args', t => runScriptPkg({ foo: 'bar', }, }, - args: ['a', 'b', 'c'], -}).then(res => t.strictSame(res, ['sh', ['-c', 'bar "a" "b" "c"'], { + args: ['a', '--flag', 'markdown `code`', '$X \\"blah\\"', '$PWD', '%CD%', '^', '!', '\\', '>', '<', '|', '&', "'", '"', '`', ' ', ''], +}).then(res => t.strictSame(res, ['sh', ['-c', expectedCommand,], { stdioString: false, event: 'foo', path: 'path', @@ -312,10 +317,10 @@ t.test('pkg has foo script, with args', t => runScriptPkg({ environ: 'value', }, stdio: 'pipe', - cmd: 'bar "a" "b" "c"', + cmd: expectedCommand, }, { event: 'foo', - script: 'bar "a" "b" "c"', + script: expectedCommand, pkgid: 'foo@1.2.3', path: 'path', }]))) From 2004eccc4d25ffed7d16217d7a2aa12779e8d705 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 20 May 2021 20:14:49 +0200 Subject: [PATCH 2/5] Add test case for double escaping on Windows --- test/run-script-pkg.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/test/run-script-pkg.js b/test/run-script-pkg.js index 5fb3a3e..2540aff 100644 --- a/test/run-script-pkg.js +++ b/test/run-script-pkg.js @@ -308,7 +308,7 @@ t.test('pkg has foo script, with args', t => runScriptPkg({ }, }, args: ['a', '--flag', 'markdown `code`', '$X \\"blah\\"', '$PWD', '%CD%', '^', '!', '\\', '>', '<', '|', '&', "'", '"', '`', ' ', ''], -}).then(res => t.strictSame(res, ['sh', ['-c', expectedCommand,], { +}).then(res => t.strictSame(res, ['sh', ['-c', expectedCommand], { stdioString: false, event: 'foo', path: 'path', @@ -325,6 +325,25 @@ t.test('pkg has foo script, with args', t => runScriptPkg({ path: 'path', }]))) +t.test('args are double escaped on Windows after a pipe', t => runScriptPkg({ + event: 'foo', + path: 'path', + scriptShell: 'sh', + pkg: { + scripts: { + foo: 'bar | baz "qux"', + }, + }, + args: ['"'], +}).then(([, [, cmd]]) => + t.strictSame( + cmd, + isWindows + ? 'bar | baz "qux" ^^^^^^^"\\^^^^^^^"^^^^^^^"' + : `bar | baz "qux" '"'` + ) +)) + t.test('pkg has no install or preinstall script, but node-gyp files are present', async t => { fakeIsNodeGypPackage = true From d1c4c26e53f2e1b786cf24a36dd4f71880149c21 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 20 May 2021 21:59:01 +0200 Subject: [PATCH 3/5] Fix `node server.js` args escaping on Windows --- lib/node.cmd | 2 + lib/run-script-pkg.js | 12 +++++- test/run-script-pkg.js | 96 ++++++++++++++++++++++++------------------ 3 files changed, 69 insertions(+), 41 deletions(-) create mode 100644 lib/node.cmd diff --git a/lib/node.cmd b/lib/node.cmd new file mode 100644 index 0000000..8ed7ea7 --- /dev/null +++ b/lib/node.cmd @@ -0,0 +1,2 @@ +@ECHO off +node %* \ No newline at end of file diff --git a/lib/run-script-pkg.js b/lib/run-script-pkg.js index 72b242b..fab8029 100644 --- a/lib/run-script-pkg.js +++ b/lib/run-script-pkg.js @@ -1,9 +1,11 @@ +const pathModule = require('path') const makeSpawnArgs = require('./make-spawn-args.js') const promiseSpawn = require('@npmcli/promise-spawn') const packageEnvs = require('./package-envs.js') const { isNodeGypPackage, defaultGypInstallScript } = require('@npmcli/node-gyp') const signalManager = require('./signal-manager.js') const isServerPackage = require('./is-server-package.js') +const isWindows = require('./is-windows.js') const { ShellString, unquoted } = require('puka'); const sh = (...args) => @@ -45,7 +47,15 @@ const runScriptPkg = async options => { ) cmd = defaultGypInstallScript else if (event === 'start' && await isServerPackage(path)) - cmd = sh`node server.js ${args}` + // `node` probably means `C:\Program Files\nodejs\node.exe`. + // But it can also point to `.\node_modules\.bin\node.cmd` if you install a + // Node.js version using `npm i node`. + // So it might point to an executable, or a batch file. They require + // different escaping. Also, `puka` only supports batch-style escaping. + // The solution is to always use a batch file on Windows. + cmd = isWindows + ? sh`${pathModule.join(__dirname, 'node.cmd')} server.js ${args}` + : sh`node server.js ${args}` if (!cmd) return { code: 0, signal: null } diff --git a/test/run-script-pkg.js b/test/run-script-pkg.js index 2540aff..b8b7edd 100644 --- a/test/run-script-pkg.js +++ b/test/run-script-pkg.js @@ -2,6 +2,15 @@ const { EventEmitter } = require('events') const t = require('tap') const requireInject = require('require-inject') const isWindows = require('../lib/is-windows.js') +const { sh } = require("puka") + +if (!process.env.__FAKE_TESTING_PLATFORM__) { + const fake = isWindows ? 'posix' : 'win32' + t.spawn(process.execPath, [__filename, fake], { env: { + ...process.env, + __FAKE_TESTING_PLATFORM__: fake, + }}) +} let fakeIsNodeGypPackage = false let SIGNAL = null @@ -41,6 +50,9 @@ t.test('pkg has no scripts, no server.js', t => runScriptPkg({ }).then(res => t.strictSame(res, { code: 0, signal: null }))) t.test('pkg has server.js, start not specified', async t => { + const expectedCommand = isWindows + ? sh`${require('path').resolve(__dirname, '../lib/node.cmd')} server.js` + : 'node server.js' const path = t.testdir({ 'server.js': '' }) const res = await runScriptPkg({ event: 'start', @@ -59,7 +71,7 @@ t.test('pkg has server.js, start not specified', async t => { scripts: {}, }, }) - t.strictSame(res, ['sh', ['-c', 'node server.js'], { + t.strictSame(res, ['sh', ['-c', expectedCommand], { stdioString: false, event: 'start', path, @@ -68,16 +80,19 @@ t.test('pkg has server.js, start not specified', async t => { environ: 'value', }, stdio: 'pipe', - cmd: 'node server.js', + cmd: expectedCommand, }, { event: 'start', - script: 'node server.js', + script: expectedCommand, pkgid: 'foo@1.2.3', path, }]) }) t.test('pkg has server.js, start not specified, with args', async t => { + const expectedCommand = isWindows + ? sh`${require('path').resolve(__dirname, '../lib/node.cmd')} server.js a b c` + : 'node server.js a b c' const path = t.testdir({ 'server.js': '' }) const res = await runScriptPkg({ event: 'start', @@ -97,7 +112,7 @@ t.test('pkg has server.js, start not specified, with args', async t => { scripts: {}, }, }) - t.strictSame(res, ['sh', ['-c', 'node server.js a b c'], { + t.strictSame(res, ['sh', ['-c', expectedCommand], { stdioString: false, event: 'start', path, @@ -106,10 +121,10 @@ t.test('pkg has server.js, start not specified, with args', async t => { environ: 'value', }, stdio: 'pipe', - cmd: 'node server.js a b c', + cmd: expectedCommand, }, { event: 'start', - script: 'node server.js a b c', + script: expectedCommand, pkgid: 'foo@1.2.3', path, }]) @@ -289,41 +304,42 @@ t.test('pkg has foo script', t => runScriptPkg({ path: 'path', }]))) -const expectedCommand = isWindows - ? 'bar a --flag "markdown `code`" ^^^"$X^^^ \\\\\\^^^"blah\\\\\\^^^"^^^" $PWD ^^^%CD^^^% "^" ! \\ ">" "<" "|" "&" \' ^^^"\\^^^"^^^" ` " " ""' - : "bar a --flag 'markdown `code`' '$X \\\"blah\\\"' '$PWD' %CD% ^ ! '\\' '>' '<' '|' '&' \\' '\"' '`' ' ' ''" - -t.test('pkg has foo script, with args', t => runScriptPkg({ - event: 'foo', - path: 'path', - scriptShell: 'sh', - env: { - environ: 'value', - }, - stdio: 'pipe', - pkg: { - _id: 'foo@1.2.3', - scripts: { - foo: 'bar', +t.test('pkg has foo script, with args', t => { + const expectedCommand = isWindows + ? 'bar a --flag "markdown `code`" ^^^"$X^^^ \\\\\\^^^"blah\\\\\\^^^"^^^" $PWD ^^^%CD^^^% "^" ! \\ ">" "<" "|" "&" \' ^^^"\\^^^"^^^" ` " " ""' + : "bar a --flag 'markdown `code`' '$X \\\"blah\\\"' '$PWD' %CD% ^ ! '\\' '>' '<' '|' '&' \\' '\"' '`' ' ' ''" + return runScriptPkg({ + event: 'foo', + path: 'path', + scriptShell: 'sh', + env: { + environ: 'value', }, - }, - args: ['a', '--flag', 'markdown `code`', '$X \\"blah\\"', '$PWD', '%CD%', '^', '!', '\\', '>', '<', '|', '&', "'", '"', '`', ' ', ''], -}).then(res => t.strictSame(res, ['sh', ['-c', expectedCommand], { - stdioString: false, - event: 'foo', - path: 'path', - scriptShell: 'sh', - env: { - environ: 'value', - }, - stdio: 'pipe', - cmd: expectedCommand, -}, { - event: 'foo', - script: expectedCommand, - pkgid: 'foo@1.2.3', - path: 'path', -}]))) + stdio: 'pipe', + pkg: { + _id: 'foo@1.2.3', + scripts: { + foo: 'bar', + }, + }, + args: ['a', '--flag', 'markdown `code`', '$X \\"blah\\"', '$PWD', '%CD%', '^', '!', '\\', '>', '<', '|', '&', "'", '"', '`', ' ', ''], + }).then(res => t.strictSame(res, ['sh', ['-c', expectedCommand], { + stdioString: false, + event: 'foo', + path: 'path', + scriptShell: 'sh', + env: { + environ: 'value', + }, + stdio: 'pipe', + cmd: expectedCommand, + }, { + event: 'foo', + script: expectedCommand, + pkgid: 'foo@1.2.3', + path: 'path', + }])) +}) t.test('args are double escaped on Windows after a pipe', t => runScriptPkg({ event: 'foo', From 74df3abe1d476a8837a2cd316e28f6bca6914cf1 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Thu, 20 May 2021 23:02:21 +0200 Subject: [PATCH 4/5] Add node.cmd to "files" in package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index a648f4f..ba397bc 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ }, "files": [ "lib/**/*.js", + "lib/node.cmd", "lib/node-gyp-bin" ], "main": "lib/run-script.js", From 8a5639cca2b4e9b0ca8ddc9b189e0437a372f717 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Fri, 21 May 2021 08:22:56 +0200 Subject: [PATCH 5/5] Rename node.cmd to nodewrapper.cmd to avoid potential infinite loops --- lib/{node.cmd => nodewrapper.cmd} | 0 lib/run-script-pkg.js | 2 +- package.json | 2 +- test/run-script-pkg.js | 4 ++-- 4 files changed, 4 insertions(+), 4 deletions(-) rename lib/{node.cmd => nodewrapper.cmd} (100%) diff --git a/lib/node.cmd b/lib/nodewrapper.cmd similarity index 100% rename from lib/node.cmd rename to lib/nodewrapper.cmd diff --git a/lib/run-script-pkg.js b/lib/run-script-pkg.js index fab8029..057cc2a 100644 --- a/lib/run-script-pkg.js +++ b/lib/run-script-pkg.js @@ -54,7 +54,7 @@ const runScriptPkg = async options => { // different escaping. Also, `puka` only supports batch-style escaping. // The solution is to always use a batch file on Windows. cmd = isWindows - ? sh`${pathModule.join(__dirname, 'node.cmd')} server.js ${args}` + ? sh`${pathModule.join(__dirname, 'nodewrapper.cmd')} server.js ${args}` : sh`node server.js ${args}` if (!cmd) diff --git a/package.json b/package.json index ba397bc..c7d7716 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ }, "files": [ "lib/**/*.js", - "lib/node.cmd", + "lib/nodewrapper.cmd", "lib/node-gyp-bin" ], "main": "lib/run-script.js", diff --git a/test/run-script-pkg.js b/test/run-script-pkg.js index b8b7edd..864e57d 100644 --- a/test/run-script-pkg.js +++ b/test/run-script-pkg.js @@ -51,7 +51,7 @@ t.test('pkg has no scripts, no server.js', t => runScriptPkg({ t.test('pkg has server.js, start not specified', async t => { const expectedCommand = isWindows - ? sh`${require('path').resolve(__dirname, '../lib/node.cmd')} server.js` + ? sh`${require('path').resolve(__dirname, '../lib/nodewrapper.cmd')} server.js` : 'node server.js' const path = t.testdir({ 'server.js': '' }) const res = await runScriptPkg({ @@ -91,7 +91,7 @@ t.test('pkg has server.js, start not specified', async t => { t.test('pkg has server.js, start not specified, with args', async t => { const expectedCommand = isWindows - ? sh`${require('path').resolve(__dirname, '../lib/node.cmd')} server.js a b c` + ? sh`${require('path').resolve(__dirname, '../lib/nodewrapper.cmd')} server.js a b c` : 'node server.js a b c' const path = t.testdir({ 'server.js': '' }) const res = await runScriptPkg({