網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
引言
在學(xué)習(xí) rollup CLI 之前我們需要了解 npm 中的 prefix,symlink,Executables 這三個(gè)概念。
prefix
當(dāng)我們使用 --global/-g 選項(xiàng)的時(shí)候會(huì)將包安裝到 prefix 目錄下,prefix 默認(rèn)為 node 的安裝位置。在大多數(shù)系統(tǒng)上,它是 /usr/local。
在 Windows 上,它是 %AppData%\npm 目錄中。在 Unix 系統(tǒng)上,它向上一級(jí),因?yàn)?node 通常安裝在{prefix}/bin/node 而不是{prefix}/node.exe。
如果未使用 --global/-g 選項(xiàng)的時(shí)候,它將安裝在當(dāng)前包的根目錄,或者當(dāng)前工作目錄。
具體請(qǐng)參閱 folders
symlink
許多包都有一個(gè)或多個(gè)可執(zhí)行文件,并且希望將其安裝到 PATH 中。npm 剛好提供了這個(gè)功能。
如果想要在用戶安裝包的時(shí)候創(chuàng)建可執(zhí)行文件,請(qǐng)?jiān)?package.json 中提供一個(gè) bin 字段,該字段是命令名稱到本地文件名的映射。在安裝時(shí),npm 會(huì)將該文件符號(hào)鏈接到 prefix/bin 以進(jìn)行全局安裝,或 ./node_modules/.bin/ 用于本地安裝。
Executables(可執(zhí)行文件)
舉個(gè)例子:
npm install --global rollup
當(dāng)我們使用上述方式全局安裝 rollup 的時(shí)候,我們可以 cd 到任何文件目錄下直接使用 rollup 命令來(lái)使用它。其中的原理就是我們需要了解的可執(zhí)行文件的概念:
- 在全局模式下,可執(zhí)行文件在 Unix 上鏈接到 {prefix}/bin,或在 Windows 上直接鏈接到 {prefix}。
- 在本地模式下,可執(zhí)行文件鏈接到 ./node_modules/.bin 中,以便它們可以可用于通過(guò) npm 運(yùn)行的腳本。
簡(jiǎn)單來(lái)說(shuō)就是當(dāng)你使用 npm install 的時(shí)候 npm 會(huì)自動(dòng)為你創(chuàng)建對(duì)應(yīng)的可執(zhí)行文件。如果是使用 npm install 的方式則會(huì)將對(duì)應(yīng)的可執(zhí)行文件放在 /node_modules/.bin 目錄下。如果使用 npm install --global 的方式,對(duì)應(yīng)的可執(zhí)行文件在 Unix 上會(huì)放在{prefix}/bin 目錄,在 Windows 上則是 {prefix} 目錄。
當(dāng)你執(zhí)行 npm run 的時(shí)候,npm 會(huì)在 node 環(huán)境變量(Path)中(例如 C:\Users\victorjiang\AppData\Roaming\npm)找到對(duì)應(yīng)的 node 可執(zhí)行文件并且運(yùn)行它。可執(zhí)行文件包括三個(gè):
- rollup:Unix 系統(tǒng)默認(rèn)的可執(zhí)行文件,必須輸入完整文件名
- rollup.cmd:windows cmd 中默認(rèn)的可執(zhí)行文件
- rollup.ps1:Windows PowerShell 中可執(zhí)行文件,可以跨平臺(tái)
在了解了 prefix,symlink,Executables 這三個(gè)概念之后我們就可以開(kāi)始學(xué)習(xí) rollup 的 CLI 的功能了。
rollup 命令行的開(kāi)發(fā)
Rollup 命令行的源碼在項(xiàng)目的根目錄的 cli 下:
cli
├─ run //定義了runRollup函數(shù),以及加載配置文件等業(yè)務(wù)代碼
├─ cli.ts //命令行解析入口
├─ help.md //rollup幫助文檔
├─ logging.ts //handleError方法定義
cli/cli.ts 代碼定義:
import process from 'node:process';
import help from 'help.md';
import { version } from 'package.json';
import argParser from 'yargs-parser';
import { commandAliases } from '../src/utils/options/mergeOptions';
import run from './run/index';
/**
commandAliases: {
c: 'config',
d: 'dir',
e: 'external',
f: 'format',
g: 'globals',
h: 'help',
i: 'input',
m: 'sourcemap',
n: 'name',
o: 'file',
p: 'plugin',
v: 'version',
w: 'watch'
};
*/
// process 是一個(gè)全局變量,即 global 對(duì)象的屬性。
// 它用于描述當(dāng)前Node.js 進(jìn)程狀態(tài)的對(duì)象,提供了一個(gè)與操作系統(tǒng)的簡(jiǎn)單接口。
// process.argv 屬性返回一個(gè)數(shù)組,由命令行執(zhí)行腳本時(shí)的各個(gè)參數(shù)組成。
// 它的第一個(gè)成員總是node,第二個(gè)成員是腳本文件名,其余成員是腳本文件的參數(shù)。
// process.argv: [
// 'C:\\Program Files\\nodejs\\node.exe',
// 'C:\\Program Files\\nodejs\\node_modules\\rollup\\dist\\bin\\rollup'
// ]
/**
* 1. process.argv.slice(2) 則是從 argv數(shù)組下標(biāo)為2的元素開(kāi)始直到末尾提取元素,舉例來(lái)說(shuō)就是提取諸如 rollup -h 中除了 rollup 之外的參數(shù)
* 2. yargs-parser這個(gè)包的作用是把命令行參數(shù)轉(zhuǎn)換為json對(duì)象,方便訪問(wèn)。
* 例如:"rollup -h" 會(huì)被argParser解析成 { _: [], h: true, help: true }
* "rollup --help" 會(huì)被argParser解析成 { _: [], help: true, h: true }
* 'camel-case-expansion' 表示連字符參數(shù)是否應(yīng)該擴(kuò)展為駝峰大小寫(xiě)別名?默認(rèn)是true.
* 例如: node example.js --foo-bar 會(huì)被解析成 { _: [], 'foo-bar': true, fooBar: true }
*
*/
const command = argParser(process.argv.slice(2), {
alias: commandAliases, //alias參數(shù)表示鍵的別名對(duì)象
configuration: { 'camel-case-expansion': false } //為 argParser 解析器提供配置選項(xiàng), 'camel-case-expansion': false 表示連字符參數(shù)不會(huì)被擴(kuò)展為駝峰大小寫(xiě)別名
});
//process.stdin.isTTY 用于檢測(cè)我們的程序是否直接連到終端
if (command.help || (process.argv.length <= 2 && process.stdin.isTTY)) {
console.log(`\n${help.replace('__VERSION__', version)}\n`);
} else if (command.version) {
console.log(`rollup v${version}`);
} else {
try {
// eslint-disable-next-line unicorn/prefer-module
//瀏覽器是支持source maps的,但node環(huán)境原生不支持source maps。所以我們可以通過(guò)'source-map-support'包來(lái)實(shí)現(xiàn)這個(gè)功能。這樣當(dāng)程序執(zhí)行出錯(cuò)的時(shí)候方便通過(guò)控制臺(tái)定位到源碼位置。
require('source-map-support').install();
} catch {
// do nothing
}
run(command);
}
上面代碼中的 run 方法就是 cli/run/index.ts 中定義的 runRollup 方法,它的主要作用就是為了解析用戶輸入的命令行參數(shù)。
cli/run/index.ts 代碼定義:
import { env } from 'node:process';
import type { MergedRollupOptions } from '../../src/rollup/types';
import { errorDuplicateImportOptions, errorFailAfterWarnings } from '../../src/utils/error';
import { isWatchEnabled } from '../../src/utils/options/mergeOptions';
import { getAliasName } from '../../src/utils/relativeId';
import { loadFsEvents } from '../../src/watch/fsevents-importer';
import { handleError } from '../logging';
import type { BatchWarnings } from './batchWarnings';
import build from './build';
import { getConfigPath } from './getConfigPath';
import { loadConfigFile } from './loadConfigFile';
import loadConfigFromCommand from './loadConfigFromCommand';
export default async function runRollup(command: Record<string, any>): Promise<void> {
let inputSource; //獲取input的值
if (command._.length > 0) {
//獲取非選項(xiàng)值
//例如終端輸入"rollup -i input.js f es" => command: { _: [ 'f', 'es' ], i: 'input.js', input: 'input.js' }
if (command.input) {
handleError(errorDuplicateImportOptions());
}
inputSource = command._;
} else if (typeof command.input === 'string') {
inputSource = [command.input];
} else {
inputSource = command.input;
}
if (inputSource && inputSource.length > 0) {
if (inputSource.some((input: string) => input.includes('='))) {
//"rollup -i input.js f=es" => { _: [ 'f=es' ], i: 'input.js', input: 'input.js' }
command.input = {};
//處理多入口文件的情況
for (const input of inputSource) {
const equalsIndex = input.indexOf('=');
const value = input.slice(Math.max(0, equalsIndex + 1)); //獲取等號(hào)右邊的字符=> “es”
const key = input.slice(0, Math.max(0, equalsIndex)) || getAliasName(input); //獲取等號(hào)左邊的字符=> “f”
command.input[key] = value;
}
} else {
//處理單入口文件的情況
command.input = inputSource;
}
}
if (command.environment) {
//獲取environment參數(shù)用于設(shè)置process.env.[XX]
const environment = Array.isArray(command.environment)
? command.environment
: [command.environment];
for (const argument of environment) {
for (const pair of argument.split(',')) {
const [key, ...value] = pair.split(':');
env[key] = value.length === 0 ? String(true) : value.join(':');
}
}
}
if (isWatchEnabled(command.watch)) {
//觀察模式
await loadFsEvents();
const { watch } = await import('./watch-cli');
watch(command);
} else {
//非觀察模式
try {
const { options, warnings } = await getConfigs(command);
try {
//因?yàn)榕渲梦募梢苑祷匾粋€(gè)數(shù)組,所以需要挨個(gè)執(zhí)行
for (const inputOptions of options) {
//內(nèi)部執(zhí)行 rollup(inputOptions) 進(jìn)行打包
await build(inputOptions, warnings, command.silent);
}
if (command.failAfterWarnings && warnings.warningOccurred) {
warnings.flush();
handleError(errorFailAfterWarnings());
}
} catch (error: any) {
warnings.flush();
handleError(error);
}
} catch (error: any) {
handleError(error);
}
}
}
async function getConfigs(
command: any
): Promise<{ options: MergedRollupOptions[]; warnings: BatchWarnings }> {
if (command.config) {
//獲取配置文件
const configFile = await getConfigPath(command.config);
//讀取配置文件獲取配置項(xiàng)
const { options, warnings } = await loadConfigFile(configFile, command);
return { options, warnings };
}
return await loadConfigFromCommand(command);
}
打包生成 rollup 文件
在 rollup.config.ts 文件中有導(dǎo)出一個(gè)方法:
//rollup.config.ts
export default async function (
command: Record<string, unknown>
): Promise<RollupOptions | RollupOptions[]> {
const { collectLicenses, writeLicense } = getLicenseHandler(
fileURLToPath(new URL('.', import.meta.url))
);
const commonJSBuild: RollupOptions = {
// 'fsevents' is a dependency of 'chokidar' that cannot be bundled as it contains binary code
external: ['fsevents'],
input: {
'loadConfigFile.js': 'cli/run/loadConfigFile.ts',
'rollup.js': 'src/node-entry.ts'
},
onwarn,
output: {
banner: getBanner,
chunkFileNames: 'shared/[name].js',
dir: 'dist',
entryFileNames: '[name]',
exports: 'named',
externalLiveBindings: false,
format: 'cjs',
freeze: false,
generatedCode: 'es2015',
interop: 'default',
manualChunks: { rollup: ['src/node-entry.ts'] },
sourcemap: true
},
plugins: [
...nodePlugins,
addCliEntry(), //添加cli入口文件
esmDynamicImport(),
!command.configTest && collectLicenses(),
!command.configTest && copyTypes('rollup.d.ts')
],
strictDeprecations: true,
treeshake
};
/**
*
當(dāng)我們執(zhí)行npm run build 的時(shí)候就相當(dāng)于執(zhí)行了 rollup --config rollup.config.ts --configPlugin typescript
此時(shí) command 就是如下對(duì)象:
{
_: [],
config: 'rollup.config.ts',
c: 'rollup.config.ts',
configPlugin: 'typescript'
}
*/
if (command.configTest) {
return commonJSBuild;
}
const esmBuild: RollupOptions = {
...commonJSBuild,
input: { 'rollup.js': 'src/node-entry.ts' },
output: {
...commonJSBuild.output,
dir: 'dist/es',
format: 'es',
minifyInternalExports: false,
sourcemap: false
},
plugins: [...nodePlugins, emitModulePackageFile(), collectLicenses(), writeLicense()]
};
const { collectLicenses: collectLicensesBrowser, writeLicense: writeLicenseBrowser } =
getLicenseHandler(fileURLToPath(new URL('browser', import.meta.url)));
const browserBuilds: RollupOptions = {
input: 'src/browser-entry.ts',
onwarn,
output: [
{
banner: getBanner,
file: 'browser/dist/rollup.browser.js',
format: 'umd',
name: 'rollup',
plugins: [copyTypes('rollup.browser.d.ts')],
sourcemap: true
},
{
banner: getBanner,
file: 'browser/dist/es/rollup.browser.js',
format: 'es',
plugins: [emitModulePackageFile()]
}
],
plugins: [
replaceBrowserModules(),
alias(moduleAliases),
nodeResolve({ browser: true }),
json(),
commonjs(),
typescript(),
terser({ module: true, output: { comments: 'some' } }),
collectLicensesBrowser(),
writeLicenseBrowser(),
cleanBeforeWrite('browser/dist')
],
strictDeprecations: true,
treeshake
};
return [commonJSBuild, esmBuild, browserBuilds];
}
請(qǐng)注意上面使用了 addCliEntry 插件。它的代碼定義在 build-plugins/add-cli-entry.ts:
import { chmod } from 'node:fs/promises';
import { resolve } from 'node:path';
import MagicString from 'magic-string';
import type { Plugin } from 'rollup';
const CLI_CHUNK = 'bin/rollup';
export default function addCliEntry(): Plugin {
return {
buildStart() {
this.emitFile({
fileName: CLI_CHUNK,
id: 'cli/cli.ts',
preserveSignature: false,
type: 'chunk'
});
},
name: 'add-cli-entry',
renderChunk(code, chunkInfo) {
if (chunkInfo.fileName === CLI_CHUNK) {
const magicString = new MagicString(code);
//聲明在 shell 中使用 node來(lái)運(yùn)行
magicString.prepend('#!/usr/bin/env node\n\n');
return { code: magicString.toString(), map: magicString.generateMap({ hires: true }) };
}
return null;
},
writeBundle({ dir }) {
return chmod(resolve(dir!, CLI_CHUNK), '755'); //修改文件可讀寫(xiě)權(quán)限,保證執(zhí)行的權(quán)限
/*
在Node.js中,可以調(diào)用fs模塊,有一個(gè)方法chmod,可以用來(lái)修改文件或目錄的讀寫(xiě)權(quán)限。方法chmod有三個(gè)參數(shù),文件路徑、讀寫(xiě)權(quán)限和回調(diào)函數(shù),其中讀寫(xiě)權(quán)限是用代號(hào)表示的,
(1)0600:所有者可讀寫(xiě),其他的用戶不行
(2)0644:所有者可讀寫(xiě),其他的用戶只讀
(3)0740:所有者可讀寫(xiě),所有者所在的組只讀
(4)0755:所有者可讀寫(xiě),其他用戶可讀可執(zhí)行
*/
}
};
}
addCliEntry 插件將 /cli/cli.ts 源碼添加到輸出的 chunk 中,并且在文件的頭部增加一行代碼:'#!/usr/bin/env node\n\n'。
首先解釋一下 #!/usr/bin/env node
- # 在 shell 腳本中單獨(dú)使用代表注釋
- #! 組合使用表示要用在 shell 腳本中
- env 是 Mac 或者 Linux 系統(tǒng)的環(huán)境變量,是一個(gè)可執(zhí)行命令
- env node : 指的是使用當(dāng)前 env 環(huán)境內(nèi)的配置的 Path 路徑下的 node 執(zhí)行
- 當(dāng)前腳本在執(zhí)行 shell 時(shí),會(huì)自動(dòng)從 env 內(nèi)調(diào)用合適的解釋器執(zhí)行
這樣做的目的是為了能夠解析當(dāng)前腳本文件,該命令會(huì)自動(dòng)從當(dāng)前 env 環(huán)境中查找配置的 node 版本來(lái)執(zhí)行腳本。
最終我們使用 npm run build 的命令打包 rollup 源碼的時(shí)候就會(huì)生成 dist/bin/rollup 這個(gè)文件了
#!/usr/bin/env node
/*
@license
Rollup.js v3.2.3
Sat, 28 Jan 2023 07:43:49 GMT - commit 5fa73d941c16a6bcbebaa3ae5bb6aaca8b97d0b7
https://github.com/rollup/rollup
Released under the MIT License.
*/
'use strict';
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: 'Module' } });
const process$1 = require('node:process');
const rollup = require('../shared/rollup.js');
const require$$2 = require('util');
const require$$0 = require('path');
const require$$0$1 = require('fs');
const node_fs = require('node:fs');
const node_path = require('node:path');
const loadConfigFile_js = require('../shared/loadConfigFile.js');
require('node:perf_hooks');
require('node:crypto');
require('node:events');
require('tty');
require('node:url');
# ...
const command = argParser(process$1.argv.slice(2), {
alias: rollup.commandAliases,
configuration: { 'camel-case-expansion': false } //為 argParser 解析器提供配置選項(xiàng), 'camel-case-expansion': false 表示連字符參數(shù)不會(huì)被擴(kuò)展為駝峰大小寫(xiě)別名
});
//process.stdin.isTTY 用于檢測(cè)我們的程序是否直接連到終端
if (command.help || (process$1.argv.length <= 2 && process$1.stdin.isTTY)) {
console.log(`\n${help.replace('__VERSION__', rollup.version)}\n`);
}
else if (command.version) {
console.log(`rollup v${rollup.version}`);
}
else {
try {
// eslint-disable-next-line unicorn/prefer-module
//瀏覽器是支持source maps的,但node環(huán)境原生不支持source maps。所以我們可以通過(guò)'source-map-support'包來(lái)實(shí)現(xiàn)這個(gè)功能。這樣當(dāng)程序執(zhí)行出錯(cuò)的時(shí)候方便通過(guò)控制臺(tái)定位到源碼位置。
require('source-map-support').install();
}
catch {
// do nothing
}
runRollup(command);
}
exports.getConfigPath = getConfigPath;
exports.loadConfigFromCommand = loadConfigFromCommand;
exports.prettyMilliseconds = prettyMilliseconds;
exports.printTimings = printTimings;
//# sourceMappingURL=rollup.map
原文鏈接:https://juejin.cn/post/7193610010318864442
相關(guān)推薦
- 2022-05-22 Python學(xué)習(xí)之os包使用教程詳解_python
- 2022-10-14 Ubuntu在GitHub中配置SSH Key
- 2021-12-07 Kotlin基本數(shù)據(jù)類型詳解_Android
- 2023-04-04 numpy中的norm()函數(shù)求范數(shù)實(shí)例_python
- 2022-06-11 C#實(shí)現(xiàn)DataTable轉(zhuǎn)TXT、CSV文件_C#教程
- 2022-09-17 ASP.NET?Core實(shí)現(xiàn)AES-GCM加密算法_實(shí)用技巧
- 2022-07-13 Android Canvas - StaticLayout 繪制多行文字
- 2022-12-03 C#通過(guò)Builder模式造車_C#教程
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門(mén)
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支