hello.mjs
npm publish
:將套件上傳到 npm 註冊中心.mjs
$PATH
在本章中,我們將學習如何透過 Node.js ESM 模組實作殼層腳本。有兩種常見的方法可以執行此操作
您應該大致熟悉以下兩個主題
Windows 並不真正支援以 JavaScript 撰寫的獨立殼層腳本。因此,我們將先探討如何為 Unix 撰寫具有檔名副檔名的獨立腳本。這項知識將有助於我們建立包含殼層腳本的套件。稍後,我們將學習
透過套件安裝殼層腳本的主題,請參閱§13「安裝 npm 套件和執行 bin 腳本」。
讓我們將 ESM 模組轉換為 Unix 殼層腳本,我們可以在套件內部執行它。原則上,我們可以在 ESM 模組中選擇兩個檔名副檔名
.mjs
檔案永遠會被解釋為 ESM 模組。
.js
檔案只有在最接近的 package.json
具有以下項目時才會被解釋為 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 script 中,第一行是一個 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
,它可以防止 env
將其所有引數都解釋為二進位檔的單一名稱
#!/usr/bin/env -S node --disable-proto=throw
在 macOS 上,即使沒有 -S
,前一個指令也行得通;在 Linux 上通常不行。
如果我們在 Windows 上使用文字編輯器建立一個 ESM 模組,這個模組應該可以在 Unix 或 Windows 上以腳本執行,我們必須新增一個 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
我們可以按一下該狀態資訊來選擇行終結符。
其次,我們可以在 Windows 上建立一個只有 Unix 行終結符的最小檔案 my-script.mjs
,我們永遠不會在 Windows 上編輯它
#!/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"
。第二種方式表示我們可以輕鬆地延後決定套件是依賴項還是開發依賴項。
讓我們新增自述檔案和兩個 shell 腳本模組 homedir.mjs
和 versions.mjs
demo-shell-scripts/
package.json
package-lock.json
README.md
src/
homedir.mjs
versions.mjs
我們必須告知 npm 這兩個 shell 腳本,以便它可以為我們安裝它們。這就是 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
是命令殼層腳本,它使用 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
。然後,如果我們將第一行變更為
#!/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 程式碼,它是字串 ':'
(它被解釋為表達式陳述式且不會執行任何操作),後面接著註解。
將 shell 程式碼隱藏起來以避免 JavaScript 看到的另一個好處是,在處理和顯示語法時,JavaScript 編輯器不會感到困惑。
.mjs
在 Windows 上建立獨立 Node.js shell 腳本的一個選項是使用檔案副檔名 .mjs
,並設定它讓具有此副檔名的檔案透過 node
執行。唉,這僅適用於命令提示字元,不適用於 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,讓命令提示字元直接執行像是 args.mjs
的檔案?
檔案關聯性 會指定當我們在 shell 中輸入檔案名稱時,要使用哪個應用程式開啟該檔案。如果我們將檔案副檔名 .mjs
與 Node.js 二進位檔關聯起來,我們就可以在 shell 中執行 ESM 模組。執行此操作的方法之一是透過設定應用程式,如 Tim Fisher 在 “如何在 Windows 中變更檔案關聯性” 中所說明的。
如果我們另外將 .MJS
加入變數 %PATHEXT%
,我們甚至可以在參照 ESM 模組時省略檔案副檔名。此環境變數可以透過設定應用程式永久變更,請搜尋「變數」。
在 Windows 上,我們面臨的挑戰是沒有像 hashbangs 那樣的機制。因此,我們必須使用一個解決方法,類似於我們在 Unix 上對沒有副檔名的檔案所使用的解決方法:我們建立一個腳本,透過 Node.js 在其自身內部執行 JavaScript 程式碼。
命令殼層腳本的檔案副檔名為 .bat
。我們可以透過 script.bat
或 script
來執行名為 script.bat
的腳本。
如果我們將 hello.mjs
轉換成命令殼層腳本 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
。我們也使用以下命令殼層功能
%~f0
包含目前腳本的完整路徑,包括其檔案副檔名。相反地,%0
包含用來呼叫腳本的命令。因此,前者殼層變數讓我們可以透過 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 (來源),並會變更 $PATH
,直到我們離開目前的 shell
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 上,命令提示字元和 PowerShell 的預設環境變數可以透過設定應用程式(搜尋「變數」)進行組態(永久)。