-
Notifications
You must be signed in to change notification settings - Fork 30.1k
Commit
node --run <script-in-package-json>
Co-authored-by: Daniel Lemire <[email protected]> PR-URL: #52190 Reviewed-By: Daniel Lemire <[email protected]> Reviewed-By: Vinícius Lourenço Claro Cardoso <[email protected]> Reviewed-By: Geoffrey Booth <[email protected]> Reviewed-By: Moshe Atlow <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Robert Nagy <[email protected]> Reviewed-By: Paolo Insogna <[email protected]> Reviewed-By: Tierney Cyren <[email protected]> Reviewed-By: Chemi Atlow <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Ruy Adorno <[email protected]>
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
'use strict'; | ||
/* eslint-disable node-core/prefer-primordials */ | ||
|
||
// There is no need to add primordials to this file. | ||
// `run.js` is a script only executed when `node --run <script>` is called. | ||
const { | ||
prepareMainThreadExecution, | ||
markBootstrapComplete, | ||
} = require('internal/process/pre_execution'); | ||
const { getPackageJSONScripts } = internalBinding('modules'); | ||
const { execSync } = require('child_process'); | ||
const { resolve, delimiter } = require('path'); | ||
const { escapeShell } = require('internal/shell'); | ||
const { getOptionValue } = require('internal/options'); | ||
const { emitExperimentalWarning } = require('internal/util'); | ||
|
||
prepareMainThreadExecution(false, false); | ||
markBootstrapComplete(); | ||
emitExperimentalWarning('Task runner'); | ||
|
||
// TODO(@anonrig): Search for all package.json's until root folder. | ||
const json_string = getPackageJSONScripts(); | ||
|
||
// Check if package.json exists and is parseable | ||
if (json_string === undefined) { | ||
process.exitCode = 1; | ||
return; | ||
} | ||
const scripts = JSON.parse(json_string); | ||
// Remove the first argument, which are the node binary. | ||
const args = process.argv.slice(1); | ||
const id = getOptionValue('--run'); | ||
let command = scripts[id]; | ||
|
||
if (!command) { | ||
const { error } = require('internal/console/global'); | ||
|
||
error(`Missing script: "${id}"\n`); | ||
|
||
const keys = Object.keys(scripts); | ||
if (keys.length === 0) { | ||
error('There are no scripts available in package.json'); | ||
} else { | ||
error('Available scripts are:'); | ||
for (const script of keys) { | ||
error(` ${script}: ${scripts[script]}`); | ||
} | ||
} | ||
process.exit(1); | ||
return; | ||
} | ||
|
||
const env = process.env; | ||
const cwd = process.cwd(); | ||
const binPath = resolve(cwd, 'node_modules/.bin'); | ||
|
||
// Filter all environment variables that contain the word "path" | ||
const keys = Object.keys(env).filter((key) => /^path$/i.test(key)); | ||
const PATH = keys.map((key) => env[key]); | ||
|
||
// Append only the current folder bin path to the PATH variable. | ||
// TODO(@anonrig): Prepend the bin path of all parent folders. | ||
const paths = [binPath, PATH].join(delimiter); | ||
for (const key of keys) { | ||
env[key] = paths; | ||
} | ||
|
||
// If there are any remaining arguments left, append them to the command. | ||
// This is useful if you want to pass arguments to the script, such as | ||
// `node --run linter -- --help` which runs `biome --check . --help` | ||
if (args.length > 0) { | ||
command += ' ' + escapeShell(args.map((arg) => arg.trim()).join(' ')); | ||
} | ||
execSync(command, { stdio: 'inherit', env, shell: true }); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
'use strict'; | ||
|
||
|
||
// There is no need to add primordials to this file. | ||
// `shell.js` is a script only executed when `node run <script>` is called. | ||
|
||
const forbiddenCharacters = /[\t\n\r "#$&'()*;<>?\\`|~]/; | ||
|
||
/** | ||
* Escapes a string to be used as a shell argument. | ||
* | ||
* Adapted from `promise-spawn` module available under ISC license. | ||
* Ref: https://github.com/npm/promise-spawn/blob/16b36410f9b721dbe190141136432a418869734f/lib/escape.js | ||
* @param {string} input | ||
*/ | ||
function escapeShell(input) { | ||
// If the input is an empty string, return a pair of quotes | ||
if (!input.length) { | ||
return '\'\''; | ||
} | ||
|
||
// Check if input contains any forbidden characters | ||
// If it doesn't, return the input as is. | ||
if (!forbiddenCharacters.test(input)) { | ||
return input; | ||
} | ||
|
||
// Replace single quotes with '\'' and wrap the whole result in a fresh set of quotes | ||
return `'${input.replace(/'/g, '\'\\\'\'')}'` | ||
// If the input string already had single quotes around it, clean those up | ||
.replace(/^(?:'')+(?!$)/, '') | ||
.replace(/\\'''/g, '\\\''); | ||
} | ||
|
||
module.exports = { | ||
escapeShell, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
CUSTOM_ENV="hello world" |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1", | ||
"ada": "ada", | ||
"ada-windows": "ada.bat", | ||
"positional-args": "positional-args", | ||
"positional-args-windows": "positional-args.bat", | ||
"custom-env": "custom-env", | ||
"custom-env-windows": "custom-env.bat" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
'use strict'; | ||
|
||
require('../common'); | ||
const assert = require('node:assert').strict; | ||
const childProcess = require('node:child_process'); | ||
const fixtures = require('../common/fixtures'); | ||
|
||
const child = childProcess.spawnSync( | ||
process.execPath, | ||
[ '--run', 'non-existent-command'], | ||
{ cwd: fixtures.path('run-script'), encoding: 'utf8' }, | ||
); | ||
assert.strictEqual(child.status, 1); | ||
console.log(child.stderr); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
Missing script: "non-existent-command" | ||
|
||
Available scripts are: | ||
test: echo "Error: no test specified" && exit 1 | ||
ada: ada | ||
ada-windows: ada.bat | ||
positional-args: positional-args | ||
positional-args-windows: positional-args.bat | ||
custom-env: custom-env | ||
custom-env-windows: custom-env.bat |