通过npm包脚本运行跨平台任务的详细指南

1,045 阅读6分钟

npm软件包管理器让我们可以为任务定义小的shell脚本,并通过npm run 来执行它们。在这篇博文中,我们将探讨这一点,以及我们如何以跨平台(Unixes和Windows)的方式编写它们。

npm包脚本是通过package.json 的属性"scripts" 来定义的:

{
  ···
  "scripts": {
    "tsc": "tsc",
    "tscwatch": "tsc --watch",
    "tscclean": "shx rm -rf ./dist/*"
  },
  ···
}

"scripts" 的值是一个对象,其中每个属性都定义了一个包脚本:

  • 属性键定义了脚本的名称。
  • 属性值定义了脚本运行时要做的事情。

如果我们输入:

npm run 

则npm会在shell中执行名称为script-name 的脚本。例如,我们可以使用:

npm run tscwatch

来在shell中运行下面的命令。

tsc --watch

在这篇文章中,我们偶尔会使用npm 选项-s ,它是--silent 的缩写,告诉npm run 产生较少的输出。

npm -s run 

这个选项将在关于日志的章节中详细介绍。

运行软件包脚本的较短npm命令

一些软件包脚本可以通过较短的npm命令来运行:

命令相当于
npm test,npm tnpm run test
npm startnpm run start
npm stopnpm run stop
npm restartnpm run restart
  • npm start:如果没有软件包脚本"start" ,npm会运行node server.js
  • npm restart:如果没有软件包脚本"restart", npm运行"prerestart","stop","start","postrestart"

哪个shell是用来运行软件包脚本的?

默认情况下,npm在Windows上通过cmd.exe ,在Unix上通过/bin/sh 来运行软件包脚本。我们可以通过npm的配置设置script-shell 来改变它。

然而,这样做很少是个好主意。许多现有的跨平台脚本是为shcmd.exe 编写的,并且会停止工作。

阻止软件包脚本的自动运行

有些脚本名称是为生命周期脚本保留的,当我们执行某些npm命令时,npm会运行这些脚本。

例如,每当我们执行npm install (不带参数)时,npm就会运行脚本"postinstall"生命周期脚本将在后面详细介绍。

如果配置设置ignore-scriptstrue ,npm 将永远不会自动运行脚本,只有在我们直接调用它们时才会自动运行。

在 Unix 上为软件包脚本获取标签完成功能

在Unix上,npm支持通过Tab完成命令和软件包脚本名称。 npm completion.我们可以通过在我们的.profile /.zprofile /.bash_profile / 等中添加这一行来安装它。

. <(npm completion)

如果你需要在非Unix平台上使用tab完成,可以在网上搜索一下,比如 "npm tab completion PowerShell"。

列出和组织软件包脚本

npm run 没有名字就会列出可用的脚本。如果存在以下脚本。

"scripts": {
  "tsc": "tsc",
  "tscwatch": "tsc --watch",
  "serve": "serve ./site/"
}

那么它们就会像这样列出。

% npm run
Scripts available via `npm run-script`:
  tsc
    tsc
  tscwatch
    tsc --watch
  serve
    serve ./site/

添加分隔符

如果有很多软件包脚本,我们可以滥用脚本名称作为分隔符(脚本"help" ,将在下一小节解释):

  "scripts": {
    "help": "scripts-help -w 40",
    "\n========== Building ==========": "",
    "tsc": "tsc",
    "tscwatch": "tsc --watch",
    "\n========== Serving ==========": "",
    "serve": "serve ./site/"
  },

现在,这些脚本被列出如下:

% npm run
Scripts available via `npm run-script`:
  help
    scripts-help -w 40

========== Building ==========

  tsc
    tsc
  tscwatch
    tsc --watch
  
========== Serving ==========

  serve
    serve ./site/

注意,预留换行的技巧(\n)在Unix和Windows上都适用。

打印帮助信息

包脚本"help" 通过@rauschma/scripts-help 的 bin 脚本scripts-help 打印帮助信息。我们通过package.json 属性"scripts-help" 来提供描述("tscwatch" 的值被缩写了,以便适合于单行)。

"scripts-help": {
  "tsc": "Compile the TypeScript to JavaScript.",
  "tscwatch": "Watch the TypeScript source code [...]",
  "serve": "Serve the generated website via a local server."
}

这就是帮助信息的模样。

% npm -s run help
Package “demo”

╔══════╤══════════════════════════╗
║ help │ scripts-help -w 40       ║
╚══════╧══════════════════════════╝

Building

╔══════════╤══════════════════════════════════════════╗
║ tsc      │ Compile the TypeScript to JavaScript.    ║
╟──────────┼──────────────────────────────────────────╢
║ tscwatch │ Watch the TypeScript source code and     ║
║          │ compile it incrementally when and if     ║
║          │ there are changes.                       ║
╚══════════╧══════════════════════════════════════════╝

Serving

╔═══════╤══════════════════════════════════════════╗
║ serve │ Serve the generated website via a local  ║
║       │ server.                                  ║
╚═══════╧══════════════════════════════════════════╝

软件包脚本的种类

如果脚本使用了某些名称,它们会在某些情况下自动运行:

  • 前期脚本后期脚本是在脚本之前和之后运行的。
  • 生命周期脚本是在用户执行某个动作时运行的,比如npm install

所有其他的脚本都被称为直接运行的脚本

前脚本和后脚本

每当npm运行一个软件包脚本PS ,它就会自动运行以下脚本--如果它们存在的话:

  • prePS 事前(一个前脚本)
  • postPS 事后(一个后脚本)

下面的脚本包含前脚本prehello 和后脚本posthello

"scripts": {
  "hello": "echo hello",
  "prehello": "echo BEFORE",
  "posthello": "echo AFTER"
},

这就是我们运行hello 所发生的情况:

% npm -s run hello
BEFORE
hello
AFTER

生命周期脚本

npm会在npm命令期间运行生命周期脚本,比如:

  • npm publish (将软件包上传到npm注册表)
  • npm pack (为注册表的包、包目录等创建档案)
  • npm install (用于安装从npm注册表以外的来源下载的软件包的依赖关系,而不需要参数)

如果任何一个生命周期脚本失败,整个命令就会立即停止,并出现错误。

生命周期脚本的用例是什么?

  • 编译TypeScript:如果一个软件包包含TypeScript代码,我们通常会在使用它之前将其编译为JavaScript代码。虽然后者的代码通常不会被检查到版本控制中,但它必须被上传到npm注册表,这样软件包就可以从JavaScript中使用。生命周期脚本让我们在npm publish ,上传包之前编译TypeScript代码。这确保了在npm注册表中,JavaScript代码始终与我们的TypeScript代码保持同步。它还可以确保我们的TypeScript代码没有静态类型错误,因为当遇到这些错误时,编译(也就是发布)会停止。

  • 运行测试:我们也可以使用生命周期脚本,在发布包之前运行测试。如果测试失败,该包将不会被发布。

这些是最重要的生命周期脚本(关于所有生命周期脚本的详细信息,见npm文档):

  • "prepare":
    • 在创建包存档(.tgz 文件)之前运行。
      • 期间npm publish
      • 期间npm pack
    • 当一个软件包从git或本地路径安装时运行。
    • 当使用npm install ,没有参数,或者当一个包被全局安装时,运行。
  • "prepack" 在创建软件包档案( 文件)之前运行。.tgz
    • 期间npm publish
    • 期间npm pack
  • "prepublishOnly" 只在 期间运行。npm publish
  • "install" 在没有参数的情况下使用 ,或者在全局安装软件包时运行。npm install
    • 注意,我们还可以创建一个前脚本"preinstall" 和/或后脚本"postinstall" 。它们的名字使npm在运行它们的时候更加清晰。

下表总结了这些生命周期脚本的运行时间:

prepublishOnlyprepackprepareinstall
npm publish
npm pack
npm install
全局安装
通过git安装,路径

**注意事项:**自动做事情总是有点棘手的。我通常遵循这些规则。

  • 我为自己自动化(例如,通过prepublishOnly )。
  • 我不为别人做自动操作(例如通过postinstall )。

运行软件包脚本的shell环境

在本节中,我们偶尔会使用

node -p 

来运行expr 中的JavaScript代码,并将结果打印到终端--比如说。

% node -p "'hello everyone!'.toUpperCase()" 
HELLO EVERYONE!

当前目录

当包脚本运行时,当前目录总是包目录,与我们在其根目录树中的位置无关。我们可以通过在package.json 中添加以下脚本来确认这一点。

"cwd": "node -p \"process.cwd()\""

让我们在Unix上试试cwd

% cd /Users/robin/new-package/src/util 
% npm -s run cwd
/Users/robin/new-package

以这种方式改变当前目录,有助于编写软件包脚本,因为我们可以使用相对于软件包目录的路径。

shell的PATH

当一个模块M 从一个指定符以包的名字开头的模块P ,Node.js会穿过node_modules 目录,直到找到P 的目录:

  • 第一个node_modules ,在M 的父目录下(如果它存在)。
  • 第二个node_modules ,在M 的父目录下(如果它存在)。
  • 以此类推,直到它到达文件系统的根。

也就是说,M 继承了其祖先目录的node_modules 目录。

类似的继承也发生在bin脚本上,当我们安装软件包时,bin脚本被存储在node_modules/.binnpm run 暂时将条目添加到shell的PATH变量中( Unix中为$PATH ,Windows中为%Path% ):

  • node_modules/.bin 在软件包的目录中
  • node_modules/.bin 在软件包目录的父目录中
  • 等等。

为了看到这些添加的内容,我们可以使用下面的软件包脚本:

"bin-dirs": "node -p \"JS\""

JS 代表有这个JavaScript代码的单行:

(process.env.PATH ?? process.env.Path)
.split(path.delimiter)
.filter(p => p.includes('.bin'))

在Unix上,如果我们运行bin-dirs ,我们会得到以下输出:

% npm -s run bin-dirs
[
  '/Users/robin/new-package/node_modules/.bin',
  '/Users/robin/node_modules/.bin',
  '/Users/node_modules/.bin',
  '/node_modules/.bin'
]

在Windows上,我们会得到。

>npm -s run bin-dirs
[
  'C:\\Users\\charlie\\new-package\\node_modules\\.bin',
  'C:\\Users\\charlie\\node_modules\\.bin',
  'C:\\Users\\node_modules\\.bin',
  'C:\\node_modules\\.bin'
]

在包脚本中使用环境变量

在Make、Grunt和Gulp等任务运行器中,变量很重要,因为它们有助于减少冗余。唉,虽然包脚本没有自己的变量,但我们可以通过使用环境变量(也叫shell变量)来解决这个缺陷。

我们可以使用下面的命令来列出特定平台的环境变量。

  • Unix。env
  • Windows 命令外壳。SET
  • 两个平台都是。node -p process.env

在macOS上,结果看起来像这样:

TERM_PROGRAM=Apple_Terminal
SHELL=/bin/zsh
TMPDIR=/var/folders/ph/sz0384m11vxf5byk12fzjms40000gn/T/
USER=robin
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
PWD=/Users/robin/new-package
HOME=/Users/robin
LOGNAME=robin
···

在Windows命令壳中,结果看起来是这样的:

Path=C:\Windows;C:\Users\charlie\AppData\Roaming\npm;···
PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
PROMPT=$P$G
TEMP=C:\Users\charlie\AppData\Local\Temp
TMP=C:\Users\charlie\AppData\Local\Temp
USERNAME=charlie
USERPROFILE=C:\Users\charlie
···

此外,npm在运行软件包脚本之前会临时添加更多环境变量。为了看看最终的结果是什么样子的,我们可以使用以下命令。

npm run env

这个命令调用了一个内置的包脚本。让我们来试试这个package.json

{
  "name": "@my-scope/new-package",
  "version": "1.0.0",
  "bin": {
    "hello": "./hello.mjs"
  },
  "config": {
    "stringProp": "yes",
    "arrayProp": ["a", "b", "c"],
    "objectProp": {
      "one": 1,
      "two": 2
    }
  }
}

npm的所有临时变量的名字都是以npm_ 。让我们只打印这些,按字母顺序排列。

npm run env | grep npm_ | sort

npm_ 变量有一个分层的结构。在npm_lifecycle_ ,我们可以找到当前运行的软件包脚本的名称和定义。

npm_lifecycle_event: 'env',
npm_lifecycle_script: 'env',

在Windows上,npm_lifecycle_script ,在这种情况下就是SET

在前缀npm_config_ ,我们可以看到npm的一些配置设置(这些设置在npm文档中有所描述)。这些是几个例子。

npm_config_cache: '/Users/robin/.npm',
npm_config_global_prefix: '/usr/local',
npm_config_globalconfig: '/usr/local/etc/npmrc',
npm_config_local_prefix: '/Users/robin/new-package',
npm_config_prefix: '/usr/local'
npm_config_user_agent: 'npm/8.15.0 node/v18.7.0 darwin arm64 workspaces/false',
npm_config_userconfig: '/Users/robin/.npmrc',

前缀npm_package_ 让我们可以访问package.json 的内容。它的顶层看起来是这样的。

npm_package_json: '/Users/robin/new-package/package.json',
npm_package_name: '@my-scope/new-package',
npm_package_version: '1.0.0',

npm_package_bin_ 下,我们可以找到package.json 的属性"bin"

npm_package_bin_hello: 'hello.mjs',

npm_package_config_ 条目让我们可以访问"config" 的属性。

npm_package_config_arrayProp: 'a\n\nb\n\nc',
npm_package_config_objectProp_one: '1',
npm_package_config_objectProp_two: '2',
npm_package_config_stringProp: 'yes',

这意味着,"config" 可以让我们设置变量,以便在包脚本中使用。下一小节将进一步探讨这个问题。

注意对象被转换为 "嵌套 "条目(第2行和第3行),而数组(第1行)和数字(第2行和第3行)被转换为字符串。

这些是剩下的npm_ 环境变量。

npm_command: 'run-script',
npm_execpath: '/usr/local/lib/node_modules/npm/bin/npm-cli.js',
npm_node_execpath: '/usr/local/bin/node',

获取和设置环境变量

下面的package.json 演示了我们如何在包脚本中访问通过"config" 定义的变量。

{
  "scripts": {
    "hi:unix": "echo $​npm_package_config_hi",
    "hi:windows": "echo %​npm_package_config_hi%"
  },
  "config": {
    "hi": "HELLO"
  }
}

唉,现在还没有内置的跨平台的方法来从包脚本中访问环境变量。

不过,有一些软件包的bin脚本可以帮助我们。

软件包env-var让我们获得环境变量。

"scripts": {
  "hi": "env-var echo {{npm_package_config_hi}}"
}

软件cross-env可以让我们设置环境变量。

"scripts": {
  "build": "cross-env FIRST=one SECOND=two node ./build.mjs"
}

通过.env 文件设置环境变量

也有一些软件包让我们通过.env 文件来设置环境变量。这些文件有如下格式。

# Comment
SECRET_HOST="https://example.com"
SECRET_KEY="123456789" # another comment

使用一个独立于package.json 的文件,使我们能够将这些数据保持在版本控制之外。

这些是支持.env 文件的软件包。

  • dotenv支持它们的JavaScript模块。我们可以预装它。

    node -r dotenv/config app.mjs
    

    而且我们可以导入它。

    import dotenv from 'dotenv';
    dotenv.config();
    console.log(process.env);
    
  • node-env-run让我们通过shell命令使用.env 文件。

    # Loads `.env` and runs an arbitrary shell script.
    # If there are CLI options, we need to use `--`.
    nodenv --exec node -- -p process.env.SECRET
    
    # Loads `.env` and uses `node` to run `script.mjs`.
    nodenv script.mjs
    
  • env-cmd是前一个包的替代品。

    # Loads `.env` and runs an arbitrary shell script
    env-cmd node -p process.env.SECRET
    

    该包有更多的功能:在变量集之间切换,更多的文件格式,等等。

包脚本的参数

让我们来探讨一下参数是如何传递给我们通过包脚本调用的shell命令的。我们将使用下面的package.json

{
  ···
  "scripts": {
    "args": "log-args"
  },
  "dependencies": {
    "log-args": "^1.0.0"
  }
}

bin脚本log-args ,看起来像这样。

for (const [key,value] of Object.entries(process.env)) {
  if (key.startsWith('npm_config_arg')) {
    console.log(`${key}=${JSON.stringify(value)}`);
  }
}
console.log(process.argv.slice(2));

位置参数按预期工作。

% npm -s run args three positional arguments
[ 'three', 'positional', 'arguments' ]

npm run 消耗选项并为它们创建环境变量。它们不会被添加到 。process.argv

% npm -s run args --arg1='first arg' --arg2='second arg'
npm_config_arg2="second arg"
npm_config_arg1="first arg"
[]

如果我们想让选项显示在process.argv ,我们必须使用选项终止符 -- 。这个终结符通常插在软件包脚本的名称之后。

% npm -s run args -- --arg1='first arg' --arg2='second arg' 
[ '--arg1=first arg', '--arg2=second arg' ]

但我们也可以在该名称之前插入。

% npm -s run -- args --arg1='first arg' --arg2='second arg' 
[ '--arg1=first arg', '--arg2=second arg' ]

npm的日志级别(产生的输出量多少)

npm支持以下的日志级别。

日志级别npm 选项别名
无声--loglevel silent-s --silent
错误--loglevel error
警告--loglevel warn-q --quiet
通知--loglevel notice
http--loglevel http
时间--loglevel timing
信息--loglevel info-d
冗长的--loglevel verbose-dd --verbose
愚蠢的--loglevel silly-ddd

日志是指两种活动:

  • 向终端打印信息
  • 将信息写到npm日志中

下面的小节描述了:

  • 日志级别如何影响这些活动。原则上,silent 的日志最少,而silly 的日志最多。

  • 如何配置日志。前面的表格显示了如何通过命令行选项临时改变日志级别,但还有更多的设置。而且我们可以临时或永久地改变它们。

打印到终端的日志级别和信息

在默认情况下,软件包脚本在终端输出时是比较粗略的。以下面这个package.json 文件为例。

{
  "name": "@my-scope/new-package",
  "version": "1.0.0",
  "scripts": {
    "hello": "echo Hello",
    "err": "more does-not-exist.txt"
  },
  ···
}

这是在日志级别高于silent ,并且软件包脚本没有错误退出的情况下发生的。

% npm run hello

> @my-scope/new-package@1.0.0 hello
> echo Hello

Hello

这是在日志级别高于silent ,并且打包脚本失败时发生的情况。

% npm run err      

> @my-scope/new-package@1.0.0 err
> more does-not-exist.txt

does-not-exist.txt: No such file or directory

随着日志级别silent ,输出变得不那么杂乱。

% npm -s run hello
Hello

% npm -s run err
does-not-exist.txt: No such file or directory

一些错误被-s 吞噬了。

% npm -s run abc
%

我们至少需要日志级别error 才能看到它们。

% npm --loglevel error run abc
npm ERR! Missing script: "abc"
npm ERR! 
npm ERR! To see a list of scripts, run:
npm ERR!   npm run

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/robin/.npm/_logs/2072-08-30T14_59_40_474Z-debug-0.log

不幸的是,日志级别silent 也会抑制npm run 的输出(没有参数):

% npm -s run
%

写入npm日志的日志级别和信息

默认情况下,日志被写入npm cache目录,其路径我们可以通过npm config 得到。

% npm config get cache
/Users/robin/.npm

日志目录的内容看起来像这样。

% ls -1 /Users/robin/.npm/_logs
2072-08-28T11_44_38_499Z-debug-0.log
2072-08-28T11_45_45_703Z-debug-0.log
2072-08-28T11_52_04_345Z-debug-0.log

日志中的每一行都以行索引和日志级别开始。这是一个写有日志级别notice 的日志的例子。有趣的是,即使是比notice "更粗略 "的日志级别(如silly )也会出现在其中。

0 verbose cli /usr/local/bin/node /usr/local/bin/npm
1 info using npm@8.15.0
···
33 silly logfile done cleaning log files
34 timing command:run Completed in 9ms
···

如果npm run 返回时有错误,相应的日志就会像这样结束。

34 timing command:run Completed in 7ms
35 verbose exit 1
36 timing npm Completed in 28ms
37 verbose code 1

如果没有错误,相应的日志就会像这样结束:

34 timing command:run Completed in 7ms
35 verbose exit 0
36 timing npm Completed in 26ms
37 info ok

配置日志

npm config list --long 打印各种设置的默认值。这些是日志相关设置的默认值:

% npm config list --long | grep log
loglevel = "notice"
logs-dir = null
logs-max = 10

如果logs-dir 的值是null ,npm使用npm缓存目录内的目录_logs (如前所述)。

  • logs-dir 让我们覆盖默认值,以便npm将其日志写到我们选择的目录中。
  • logs-max 让我们配置在npm删除旧文件之前有多少文件被写入日志目录。如果我们把 设置为 0,就永远不会写日志。logs-max
  • loglevel 让我们配置npm的日志级别。

要永久地改变这些设置,我们还可以使用npm config - 例如。

  • 获取当前的日志级别:

    npm config get loglevel
    
  • 永久设置当前的日志级别:

    npm config set loglevel silent
    
  • 永久地将日志级别重设为内置默认值。

    npm config delete loglevel
    

我们也可以通过命令行选项临时改变设置--例如:。

npm --loglevel silent run build

其他改变设置的方法(如使用环境变量)由npm文档解释。

npm install 期间运行的生命周期脚本的输出

npm install (没有参数)期间运行的生命周期脚本的输出是隐藏的。我们可以通过(暂时或永久)将foreground-scripts 设置为true 来改变这一点。

对npm日志工作方式的观察

  • 在使用npm run 时,只有日志级别silent 会关闭额外的输出。
  • 日志级别对是否创建日志文件以及向其写入什么内容没有影响。
  • 错误信息不会被写入日志中。

跨平台的shell脚本

最常用于包脚本的两个shell是:

  • sh 在Unix上
  • cmd.exe 在Windows上

在这一节中,我们将研究在这两种shell中都适用的结构。

路径和引号

提示。

  • 使用相对路径,其分段由斜线分隔。Windows接受斜线作为分隔符,尽管你在该平台上通常使用反斜线。

  • 双引号参数。虽然sh 支持单引号,但Windows命令壳并不支持。不幸的是,当我们在包脚本定义中使用双引号时,我们必须转义双引号。

    "dir": "mkdir \"\my dir""
    

串联命令

有两种方法可以使命令链在两个平台上都有效:

  • && 之后的命令只有在前一个命令成功的情况下才会被执行(退出代码为0)。
  • || 之后的命令只有在前一条命令失败(退出代码不是0)时才会被执行。

在忽略退出代码的情况下进行链式操作,在不同的平台上是不同的。

  • Unix:;
  • Windows 命令外壳。&

下面的交互演示了&&|| 在 Unix 上的工作方式(在 Windows 上,我们会使用dir 而不是ls )。

% ls unknown && echo "SUCCESS" || echo "FAILURE"
ls: unknown: No such file or directory
FAILURE

% ls package.json && echo "SUCCESS" || echo "FAILURE"
package.json
SUCCESS

包脚本的退出代码

退出代码可以通过shell变量访问。

  • Unix:$?
  • Windows 命令 shell。%errorlevel%

npm run 返回时的退出代码与上次执行的shell脚本的退出代码相同。

{
  ···
  "scripts": {
    "hello": "echo Hello",
    "err": "more does-not-exist.txt"
  }
}

以下是在Unix上发生的交互。

% npm -s run hello ; echo $?
Hello
0
% npm -s run err ; echo $?
does-not-exist.txt: No such file or directory
1

管道和重定向输入和输出

  • 在命令之间进行管道连接。|
  • 将输出写到文件中。cmd > stdout-saved-to-file.txt
  • 从文件中读取输入。cmd < stdin-from-file.txt

在两种平台上都能使用的命令

以下命令在两个平台上都存在(但在选项方面有所不同)。

  • cd
  • echo.在Windows上的注意事项:双引号会被打印出来,而不是被忽略。
  • exit
  • mkdir
  • more
  • rmdir
  • sort

运行bin脚本和软件包的内部模块

下面的package.json ,演示了在依赖关系中调用bin脚本的三种方式:

{
  "scripts": {
    "hi1": "./node_modules/.bin/cowsay Hello",
    "hi2": "cowsay Hello",
    "hi3": "npx cowsay Hello"
  },
  "dependencies": {
    "cowsay": "^1.5.0"
  }
}

解释一下:

  • hi1:依赖关系中的 bin 脚本被安装在目录node_modules/.bin 中。

  • hi2:正如我们所看到的,npm在执行软件包脚本的同时,会将node_modules/.bin 添加到shell的PATH中。这意味着我们可以使用本地的bin脚本,就像它们是全局安装的一样。

  • hi3:当npx 运行一个脚本时,它也会将node_modules/.bin 添加到shell PATH中。

在Unix上,我们可以直接调用软件包本地脚本--如果它们有hashbangs并且是可执行的。然而,这在Windows上是行不通的,这就是为什么最好通过node 来调用它们。

"build": "node ./build.mjs"

node --evalnode --print

当一个包脚本的功能变得过于复杂时,通过Node.js模块来实现它往往是一个好主意--这使得编写跨平台代码变得很容易。

然而,我们也可以使用node 命令来运行小型的JavaScript片段,这对于以跨平台的方式执行小型任务很有用。相关的选项是。

以下命令在Unix和Windows上都能运行(只有注释是Unix专用的):

# Print a string to the terminal (cross-platform echo)
node -p "'How are you?'"

# Print the value of an environment variable
# (Alas, we can’t change variables via `process.env`)
node -p process.env.USER # only Unix
node -p process.env.USERNAME # only Windows
node -p "process.env.USER ?? process.env.USERNAME"

# Print all environment variables
node -p process.env

# Print the current working directory
node -p "process.cwd()"

# Print the path of the current home directory
node -p "os.homedir()"

# Print the path of the current temporary directory
node -p "os.tmpdir()"

# Print the contents of a text file
node -p "fs.readFileSync('package.json', 'utf-8')"

# Write a string to a file
node -e "fs.writeFileSync('file.txt', 'Text content', 'utf-8')"

如果我们需要特定平台的行结束符,我们可以使用os.EOL --例如,我们可以将前面命令中的'Text content' 替换为:

`line 1${os.EOL}line2${os.EOL}`

观察:

  • 如果JavaScript代码包含圆括号的话,一定要把它放在双引号里--否则Unix会抱怨。
  • 所有内置模块都可以通过变量访问。这就是为什么我们不需要导入osfs
  • fs 支持更多的文件系统操作。这些在博文 "在Node.js上使用文件系统 "中都有记录

常用操作的辅助包

从命令行运行软件包脚本

npm-quick-run提供了一个bin脚本nr ,让我们使用缩写来运行包脚本--例如:

  • nr m -w 执行 (如果 是第一个名字以 "m "开头的包脚本)。"npm run mocha -- -w" "mocha"
  • nr c:o 运行软件包脚本 。"cypress:open"
  • 等等。

同时或按顺序运行多个脚本

同时运行shell脚本:

  • Unix:&
  • Windows 命令外壳。start

以下两个软件包为我们提供了跨平台的选项,以及相关的功能:

  • concurrently同时运行多个shell命令--例如。

    concurrently "npm run clean" "npm run build"
    
  • npm-run-all提供了几种功能--例如。

    • 顺序调用包脚本的一种更方便的方式。以下两个命令是等价的:

      npm-run-all clean lint build
      npm run clean && npm run lint && npm run build
      
    • 同时运行包脚本:

      npm-run-all --parallel lint build
      
    • 使用通配符来运行多个脚本 - 例如,watch:* 代表所有名称以watch: 开始的包脚本(watch:html,watch:js, 等等:

      npm-run-all "watch:*"
      npm-run-all --parallel "watch:*"
      

文件系统操作

shx让我们使用 "Unix语法 "来运行各种文件系统操作。它所做的一切,都可以在Unix和Windows上运行。: 创建一个目录:

"create-asset-dir": "shx mkdir ./assets"

删除一个目录:

"remove-asset-dir": "shx rm -rf ./assets"

清除一个目录(为了安全起见,使用双引号,通配符* ):

"tscclean": "shx rm -rf \"./dist/*\""

复制一个文件:

"copy-index": "shx cp ./html/index.html ./out/index.html"

删除一个文件:

"remove-index": "shx rm ./out/index.html"

shx 是基于JavaScript库ShellJS的,该库列出了所有支持的命令。除了我们已经看到的Unix命令之外,它还模拟了。, , , , , , , , , , , , , , , 及其他。cat chmod echo find grep head ln ls mv pwd sed sort tail touch uniq

将文件或目录放进垃圾桶

trash-cli在macOS(10.12以上)、Linux和Windows(8以上)上工作。它将文件和目录放入垃圾桶,支持路径和glob模式。这些是使用它的例子。

trash tmp-file.txt
trash tmp-dir
trash "*.jpg"

复制文件树

copyfiles让我们可以复制文件树。

下面是copyfiles 的一个使用案例。在TypeScript中,我们可以导入非代码资产,如CSS和图片。TypeScript编译器将代码编译到一个 "dist"(输出)目录,但忽略了非代码资产。这个跨平台的shell命令将它们复制到dist目录中:

copyfiles --up 1 "./ts/**/*.{css,png,svg,gif}" ./dist

TypeScript 编译:

my-pkg/ts/client/picker.ts  -> my-pkg/dist/client/picker.js

copy-assets 副本:

my-pkg/ts/client/picker.css -> my-pkg/dist/client/picker.css
my-pkg/ts/client/icon.svg   -> my-pkg/dist/client/icon.svg

观察文件

onchange,观察文件并在每次文件变化时运行shell命令--例如:

onchange 'app/**/*.js' 'test/**/*.js' -- npm test

一个常见的替代方案(在许多其他方案中)。

HTTP服务器

在开发过程中,拥有一个HTTP服务器往往是很有用的。以下的软件包(包括许多其他的)可以提供帮助。

扩展包脚本的能力

per-env:在脚本之间切换,取决于$NODE_ENV

bin脚本per-env让我们运行一个包脚本SCRIPT ,并根据环境变量NODE_ENV 的值,自动在(例如)SCRIPT:developmentSCRIPT:stagingSCRIPT:production 之间切换。

定义操作系统特定的脚本

bin脚本cross-os,根据当前的操作系统在脚本之间进行切换:

{
  "scripts": {
    "user": "cross-os user"
  },
  "cross-os": {
    "user": {
      "darwin": "echo $USER",
      "win32": "echo %USERNAME%",
      "linux": "echo $USER"
    }
  },
  ···
}

支持的属性值有。darwin,freebsd,linux,sunos,win32