hello.mjs
npm publish
:将包上传到 npm 注册表$PATH
在本章中,我们将学习如何通过 Node.js ESM 模块实现 shell 脚本。有两种常用的方法
您应该大致熟悉以下两个主题
Windows 并不真正支持用 JavaScript 编写的独立 shell 脚本。因此,我们将首先研究如何为 Unix 编写*带有*文件名扩展名的独立脚本。这些知识将帮助我们创建包含 shell 脚本的包。稍后,我们将学习
通过包安装 shell 脚本是§13 “安装 npm 包和运行 bin 脚本”的主题。
让我们将一个 ESM 模块转换为一个 Unix shell 脚本,我们可以运行它,而无需将其放在包中。原则上,我们可以为 ESM 模块选择两个文件名扩展名
.mjs
文件始终被解释为 ESM 模块。
如果最近的 package.json
具有以下条目,则 .js
文件才会被解释为 ESM 模块
"type": "module"
但是,由于我们要创建一个独立的脚本,因此我们不能依赖于 package.json
的存在。因此,我们必须使用文件名扩展名 .mjs
(我们将在后面介绍解决方法)。
以下文件名为 hello.mjs
import * as os from 'node:os';
const {username} = os.userInfo();
console.log(`Hello ${username}!`);
我们已经可以运行此文件
node hello.mjs
我们需要做两件事,以便我们可以像这样运行 hello.mjs
./hello.mjs
这些事情是
hello.mjs
的开头添加一个*hashbang*行hello.mjs
可执行在 Unix shell 脚本中,第一行是*hashbang* - 元数据,它告诉 shell 如何执行文件。例如,这是 Node.js 脚本最常见的 hashbang
#!/usr/bin/env node
此行之所以称为“hashbang”,是因为它以井号和感叹号开头。它也经常被称为“shebang”。
如果一行以井号开头,则在大多数 Unix shell(sh、bash、zsh 等)中,它都是注释。因此,这些 shell 会忽略 hashbang。Node.js 也会忽略它,但前提是它是第一行。
为什么我们不使用这个 hashbang?
#!/usr/bin/node
并非所有 Unix 都在该路径下安装 Node.js 二进制文件。那么这条路径呢?
#!node
唉,并非所有 Unix 都允许使用相对路径。这就是为什么我们通过绝对路径引用 env
,并使用它为我们运行 node
。
有关 Unix hashbang 的更多信息,请参阅 Alex Ewerlöf 的“Node.js shebang”。
如果我们想将命令行选项之类的参数传递给 Node.js 二进制文件怎么办?
一种适用于许多 Unix 的解决方案是为 env
使用选项 -S
,这可以防止它将其所有参数解释为二进制文件的单个名称
#!/usr/bin/env -S node --disable-proto=throw
在 macOS 上,即使没有 -S
,前面的命令也可以工作;在 Linux 上通常不行。
如果我们在 Windows 上使用文本编辑器创建一个应该在 Unix 或 Windows 上作为脚本运行的 ESM 模块,则必须添加一个 hashbang。如果我们这样做,第一行将以 Windows 行终止符 \r\n
结尾
#!/usr/bin/env node\r\n
在 Unix 上运行具有此类 hashbang 的文件会产生以下错误
env: node\r: No such file or directory
也就是说,env
认为可执行文件的名称是 node\r
。有两种方法可以解决此问题。
首先,某些编辑器会自动检查文件中已经使用了哪些行终止符,并继续使用它们。例如,Visual Studio Code 在右下角的状态栏中显示当前行终止符(它称之为“行尾序列”)
LF
(换行)表示 Unix 行终止符 \n
CRLF
(回车符、换行符)表示 Windows 行终止符 \r\n
我们可以通过单击该状态信息来选择行终止符。
其次,我们可以创建一个最小的文件 my-script.mjs
,它只包含我们从不在 Windows 上编辑的 Unix 行终止符
#!/usr/bin/env node
import './main.mjs';
为了成为 shell 脚本,除了具有 hashbang 之外,hello.mjs
还必须是可执行的(文件的权限)
chmod u+x hello.mjs
请注意,我们使文件对创建它的用户(u
)可执行(x
),而不是对所有人可执行。
hello.mjs
hello.mjs
现在是可执行的,如下所示
#!/usr/bin/env node
import * as os from 'node:os';
const {username} = os.userInfo();
console.log(`Hello ${username}!`);
因此,我们可以像这样运行它
./hello.mjs
唉,没有办法告诉 node
将具有任意扩展名的文件解释为 ESM 模块。这就是为什么我们必须使用扩展名 .mjs
。解决方法是可能的,但很复杂,我们将在后面看到。
在本节中,我们将创建一个包含 shell 脚本的 npm 包。然后,我们将研究如何安装这样的包,以便它的脚本在系统的命令行(Unix 或 Windows)中可用。
完成的包可在此处获取
rauschma/demo-shell-scripts
@rauschma/demo-shell-scripts
这些命令在 Unix 和 Windows 上都有效
mkdir demo-shell-scripts
cd demo-shell-scripts
npm init --yes
现在有以下文件
demo-shell-scripts/
package.json
package.json
一种选择是创建一个包,但不要将其发布到 npm 注册表。我们仍然可以在我们的系统上安装这样的包(如下所述)。在这种情况下,我们的 package.json
如下所示
{
"private": true,
"license": "UNLICENSED"
}
说明
"UNLICENSED"
拒绝他人根据任何条款使用该包。package.json
如果我们要将包发布到 npm 注册表,则我们的 package.json
如下所示
{
"name": "@rauschma/demo-shell-scripts",
"version": "1.0.0",
"license": "MIT"
}
对于您自己的包,您需要将 "name"
的值替换为适合您的包名称
全局唯一的名称。此类名称只能用于重要的包,因为我们不想阻止其他人使用该名称。
或*作用域名称*:要发布包,您需要一个 npm 帐户(稍后将说明如何获取)。您的帐户名称可以用作包名称的*作用域*。例如,如果您的帐户名称是 jane
,则可以使用以下包名称
"name": "@jane/demo-shell-scripts"
接下来,我们将安装要在我们的一个脚本中使用的依赖项 - 包 lodash-es
(Lodash 的 ESM 版本)
npm install lodash-es
此命令
创建目录 node_modules
。
将包 lodash-es
安装到其中。
将以下属性添加到 package.json
"dependencies": {
"lodash-es": "^4.17.21"
}
创建文件 package-lock.json
。
如果我们仅在开发过程中使用某个包,我们可以将其添加到 "devDependencies"
而不是 "dependencies"
中,并且 npm 只会在我们在包目录中运行 npm install
时安装它,而不会在我们将其作为依赖项安装时安装它。单元测试库是典型的开发依赖项。
我们可以通过以下两种方式安装开发依赖项
npm install some-package
。npm install some-package --save-dev
,然后手动将 some-package
的条目从 "dependencies"
移动到 "devDependencies"
。第二种方式意味着我们可以轻松地推迟决定包是依赖项还是开发依赖项。
让我们添加一个自述文件和两个模块 homedir.mjs
和 versions.mjs
,它们是 shell 脚本
demo-shell-scripts/
package.json
package-lock.json
README.md
src/
homedir.mjs
versions.mjs
我们必须将两个 shell 脚本告知 npm,以便它可以为我们安装它们。这就是 package.json
中属性 "bin"
的用途
"bin": {
"homedir": "./src/homedir.mjs",
"versions": "./src/versions.mjs"
}
如果我们安装此包,则名为 homedir
和 versions
的两个 shell 脚本将可用。
您可能更喜欢 shell 脚本的文件名扩展名 .js
。然后,您必须将以下两个属性添加到 package.json
,而不是之前的属性
"type": "module",
"bin": {
"homedir": "./src/homedir.js",
"versions": "./src/versions.js"
}
第一个属性告诉 Node.js 应该将 .js
文件解释为 ESM 模块(而不是默认的 CommonJS 模块)。
homedir.mjs
看起来像这样
#!/usr/bin/env node
import {homedir} from 'node:os';
console.log('Homedir: ' + homedir());
如果我们想在 Unix 上使用此模块,则必须以前面提到的 hashbang 开头。它从内置模块 node:os
导入函数 homedir()
,调用它并将结果记录到控制台(即标准输出)。
请注意,homedir.mjs
不必是可执行的;npm 在安装 "bin"
脚本时会确保其可执行性(我们很快就会看到)。
versions.mjs
具有以下内容
#!/usr/bin/env node
import {pick} from 'lodash-es';
console.log(
pick(process.versions, ['node', 'v8', 'unicode'])
; )
我们从 Lodash 导入函数 pick()
,并使用它来显示对象 process.versions
的三个属性。
我们可以像这样运行,例如 homedir.mjs
cd demo-shell-scripts/
node src/homedir.mjs
像 homedir.mjs
这样的脚本在 Unix 上不需要是可执行的,因为 npm 通过可执行符号链接安装它
$PATH
中列出的目录中。node_modules/.bin/
要在 Windows 上安装 homedir.mjs
,npm 会创建三个文件
homedir.bat
是一个命令行 shell 脚本,它使用 node
来执行 homedir.mjs
。homedir.ps1
对 PowerShell 执行相同的操作。homedir
对 Cygwin、MinGW 和 MSYS 执行相同的操作。npm 将这些文件添加到目录中
%Path%
中列出的目录中。node_modules/.bin/
让我们将软件包 @rauschma/demo-shell-scripts
(我们之前创建的)发布到 npm。在我们使用 npm publish
上传软件包之前,我们应该检查是否已正确配置所有内容。
发布时使用以下机制来排除和包含文件
顶级文件 .gitignore
中列出的文件将被排除。
.npmignore
覆盖 .gitignore
,该文件具有相同的格式。package.json
属性 "files"
包含一个数组,其中包含要包含的文件的名称。这意味着我们可以选择列出要排除的文件(在 .npmignore
中)或要包含的文件。
默认情况下会排除某些文件和目录,例如
node_modules
.*.swp
._*
.DS_Store
.git
.gitignore
.npmignore
.npmrc
npm-debug.log
除了这些默认值之外,点文件(名称以点开头的文件)将被包含在内。
以下文件永远不会被排除
package.json
README.md
及其变体CHANGELOG
及其变体LICENSE
、LICENCE
npm 文档有更多详细信息,说明发布时包含和排除的内容。
在我们上传软件包之前,我们可以检查几件事。
npm install
的空运行会在不上传任何内容的情况下运行该命令
npm publish --dry-run
这将显示将上传哪些文件以及有关该软件包的一些统计信息。
我们还可以创建一个软件包的存档,因为它将存在于 npm 注册表中
npm pack
此命令在当前目录中创建文件 rauschma-demo-shell-scripts-1.0.0.tgz
。
我们可以使用以下两个命令中的任何一个在不将软件包发布到 npm 注册表的情况下全局安装它
npm link
npm install . -g
要查看是否有效,我们可以打开一个新的 shell 并检查这两个命令是否可用。我们还可以列出所有全局安装的软件包
npm ls -g
要将我们的软件包作为依赖项安装,我们必须执行以下命令(当我们在目录 demo-shell-scripts
中时)
cd ..
mkdir sibling-directory
cd sibling-directory
npm init --yes
npm install ../demo-shell-scripts
我们现在可以使用以下两个命令中的任何一个运行,例如 homedir
npx homedir
./node_modules/.bin/homedir
npm publish
:将软件包上传到 npm 注册表在我们上传软件包之前,我们需要创建一个 npm 用户帐户。npm 文档介绍了如何做到这一点。
然后我们终于可以发布我们的软件包了
npm publish --access public
我们必须指定公共访问权限,因为默认值为
未作用域软件包的 public
作用域软件包的 restricted
。此设置使软件包私有 - 这是一个付费的 npm 功能,主要由公司使用,并且与 package.json
中的 "private":true
不同。引用 npm:“使用 npm 私有软件包,您可以使用 npm 注册表来托管只有您和选定的合作者才能看到的代码,从而允许您在项目中管理和使用私有代码以及公共代码。”
选项 --access
仅在我们第一次发布时有效。之后,我们可以省略它,并需要使用npm access
来更改访问级别。
我们可以通过 package.json
中的publishConfig.access
更改初始 npm publish
的默认值
"publishConfig": {
"access": "public"
}
一旦我们上传了具有特定版本的软件包,我们就不能再次使用该版本,我们必须增加该版本三个组件中的任何一个
major.minor.patch
major
。minor
。patch
。我们可能希望在每次上传软件包之前执行一些步骤,例如
这可以通过 package.json
属性 `“scripts” 自动完成。该属性可能如下所示
"scripts": {
"build": "tsc",
"test": "mocha --ui qunit",
"dry": "npm publish --dry-run",
"prepublishOnly": "npm run test && npm run build"
}
mocha
是一个单元测试库。 tsc
是 TypeScript 编译器。
以下软件包脚本在 npm publish
之前运行
"prepare"
npm pack
之前npm publish
之前npm install
之后"prepublishOnly"
仅在 npm publish
之前运行。有关此主题的更多信息,请参阅§15“通过 npm 软件包脚本运行跨平台任务”。
Node.js 二进制文件 node
使用文件名扩展名来检测文件是哪种模块。目前没有命令行选项可以覆盖它。默认值为 CommonJS,这不是我们想要的。
但是,我们可以创建自己的可执行文件来运行 Node.js,例如将其命名为 node-esm
。然后,如果我们将第一行更改为以下内容,则可以将之前的独立脚本 hello.mjs
重命名为 hello
(没有任何扩展名)
#!/usr/bin/env node-esm
以前,env
的参数是 node
。
这是 Andrea Giammarchi 提出的node-esm
的实现
#!/usr/bin/env sh
input_file=$1
shift
exec node --input-type=module - $@ < $input_file
此可执行文件通过标准输入将脚本的内容发送到 node
。命令行选项 --input-type=module
告诉 Node.js 它接收到的文本是一个 ESM 模块。
我们还使用以下 Unix shell 功能
$1
包含传递给 node-esm
的第一个参数 - 脚本的路径。shift
删除参数 $0
(node-esm
的路径),并通过 $@
将剩余的参数传递给 node
。exec
使用运行 node
的进程替换当前进程。这确保脚本以与 node
相同的代码退出。-
) 将 Node 的参数与脚本的参数分开。在我们使用 node-esm
之前,我们必须确保它是可执行的,并且可以通过 $PATH
找到。稍后将说明如何做到这一点。
我们已经看到,我们不能为文件指定模块类型,只能为标准输入指定模块类型。因此,我们可以编写一个 Unix shell 脚本 hello
,它使用 Node.js 将自身作为 ESM 模块运行(基于sambal.org 的工作)
#!/bin/sh
':' // ; cat "$0" | node --input-type=module - $@ ; exit $?
import * as os from 'node:os';
const {username} = os.userInfo();
console.log(`Hello ${username}!`);
本章开头介绍了我们在这里使用的大多数 shell 功能。 $?
包含最后执行的 shell 命令的退出代码。这使 hello
能够以与 node
相同的代码退出。
此脚本使用的关键技巧是第二行既是 Unix shell 脚本代码又是 JavaScript 代码
作为 shell 脚本代码,它运行带引号的命令 ':'
,该命令除了扩展其参数和执行重定向外什么也不做。它的唯一参数是路径 //
。然后它将当前文件的内容通过管道传输到 node
二进制文件。
作为 JavaScript 代码,它是字符串 ':'
(它被解释为表达式语句并且什么也不做),后跟注释。
对 JavaScript 隐藏 shell 代码的另一个好处是,JavaScript 编辑器在处理和显示语法时不会感到困惑。
.mjs
在 Windows 上创建独立 Node.js shell 脚本的一种选择是使用文件名扩展名 .mjs
并将其配置为通过 node
运行具有该扩展名的文件。唉,这只适用于命令行 shell,不适用于 PowerShell。
另一个缺点是我们不能以这种方式将参数传递给脚本
>more args.mjs
console.log(process.argv);
>.\args.mjs one two
[
'C:\\Program Files\\nodejs\\node.exe',
'C:\\Users\\jane\\args.mjs'
]
>node args.mjs one two
[
'C:\\Program Files\\nodejs\\node.exe',
'C:\\Users\\jane\\args.mjs',
'one',
'two'
]
我们如何配置 Windows 以便命令行 shell 直接运行诸如 args.mjs
之类的文件?
文件关联指定了当我们在 shell 中输入文件名时使用哪个应用程序打开文件。如果我们将文件名扩展名 .mjs
与 Node.js 二进制文件相关联,我们就可以在 shell 中运行 ESM 模块。一种方法是通过“设置”应用程序,如 Tim Fisher 在“如何在 Windows 中更改文件关联” 中所述。
如果我们另外将 .MJS
添加到变量 %PATHEXT%
中,我们甚至可以在引用 ESM 模块时省略文件名扩展名。可以通过“设置”应用程序永久更改此环境变量 - 搜索“变量”。
在 Windows 上,我们面临着没有像 hashbang 这样的机制的挑战。因此,我们必须使用一种类似于我们在 Unix 上用于无扩展名文件的解决方法:我们创建一个脚本,该脚本通过 Node.js 在自身内部运行 JavaScript 代码。
命令行 shell 脚本的文件名扩展名为 .bat
。我们可以通过 script.bat
或 script
运行名为 script.bat
的脚本。
如果我们将 hello.mjs
转换为命令行 shell 脚本 hello.bat
,则它看起来像这样
:: /*
@echo off
more +5 %~f0 | node --input-type=module - %*
exit /b %errorlevel%
*/
import * as os from 'node:os';
const {username} = os.userInfo();
console.log(`Hello ${username}!`);
通过 node
将此代码作为文件运行需要两个不存在的功能
因此,我们别无选择,只能将文件的内容通过管道传输到 node
。我们还使用以下命令行 shell 功能
%~f0
包含当前脚本的完整路径,包括其文件名扩展名。相反,%0
包含用于调用脚本的命令。因此,前一个 shell 变量使我们能够通过 hello
或 hello.bat
调用脚本。%*
包含命令的参数——我们将传递给 node
。%errorlevel%
包含最后执行的命令的退出代码。我们使用该值退出,退出代码与 node
指定的代码相同。我们可以使用与上一节类似的技巧,将 hello.mjs
转换为 PowerShell 脚本 hello.ps1
,如下所示
-Content $PSCommandPath | Select-Object -Skip 3 | node --input-type=module - $args
Get
exit $LastExitCode<#
import * as os from 'node:os';
const {username} = os.userInfo();
console.log(`Hello ${username}!`);
// #>
我们可以通过以下任一方式运行此脚本
.\hello.ps1
.\hello
但是,在我们这样做之前,我们需要设置一个执行策略,允许我们运行 PowerShell 脚本(有关执行策略的更多信息)
Restricted
,它不允许我们运行任何脚本。RemoteSigned
允许我们运行未签名的本地脚本。下载的脚本必须签名。这是 Windows 服务器上的默认设置。以下命令允许我们运行本地脚本
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
npm 包 pkg
将 Node.js 包转换为原生二进制文件,即使在未安装 Node.js 的系统上也可以运行。它支持以下平台:Linux、macOS 和 Windows。
在大多数 shell 中,我们可以输入文件名而不直接引用文件,它们会在多个目录中搜索具有该名称的文件并运行它。这些目录通常列在一个特殊的 shell 变量中
$PATH
访问它。%Path%
访问它。$Env:PATH
访问它。我们需要 PATH 变量用于两个目的
node-esm
。$PATH
大多数 Unix shell 都有变量 $PATH
,它列出了当我们输入命令时 shell 在其中查找可执行文件的所有路径。它的值可能如下所示
$ echo $PATH
/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin
以下命令适用于大多数 shell(来源),并在我们离开当前 shell 之前更改 $PATH
export PATH="$PATH:$HOME/bin"
如果两个 shell 变量之一包含空格,则需要使用引号。
$PATH
在 Unix 上,如何配置 $PATH
取决于 shell。您可以通过以下方式找出您正在运行哪个 shell
echo $0
macOS 使用 Zsh,永久配置 $PATH
的最佳位置是启动脚本 $HOME/.zprofile
——像这样
path+=('/Library/TeX/texbin')
export PATH
在 Windows 上,可以通过“设置”应用配置(永久)命令行 shell 和 PowerShell 的默认环境变量——搜索“变量”。