rollup?cli開發(fā)全面系統(tǒng)性rollup源碼分析
引言
在學(xué)習(xí) rollup CLI 之前我們需要了解 npm 中的 prefix,symlink,Executables 這三個概念。
prefix
當(dāng)我們使用 --global/-g 選項的時候會將包安裝到 prefix 目錄下,prefix 默認為 node 的安裝位置。在大多數(shù)系統(tǒng)上,它是 /usr/local。
在 Windows 上,它是 %AppData%\npm 目錄中。在 Unix 系統(tǒng)上,它向上一級,因為 node 通常安裝在{prefix}/bin/node 而不是{prefix}/node.exe。
如果未使用 --global/-g 選項的時候,它將安裝在當(dāng)前包的根目錄,或者當(dāng)前工作目錄。
具體請參閱 folders
symlink
許多包都有一個或多個可執(zhí)行文件,并且希望將其安裝到 PATH 中。npm 剛好提供了這個功能。
如果想要在用戶安裝包的時候創(chuàng)建可執(zhí)行文件,請在 package.json 中提供一個 bin 字段,該字段是命令名稱到本地文件名的映射。在安裝時,npm 會將該文件符號鏈接到 prefix/bin 以進行全局安裝,或 ./node_modules/.bin/ 用于本地安裝。
Executables(可執(zhí)行文件)
舉個例子:
npm install --global rollup
當(dāng)我們使用上述方式全局安裝 rollup 的時候,我們可以 cd 到任何文件目錄下直接使用 rollup 命令來使用它。其中的原理就是我們需要了解的可執(zhí)行文件的概念:
- 在全局模式下,可執(zhí)行文件在 Unix 上鏈接到 {prefix}/bin,或在 Windows 上直接鏈接到 {prefix}。
- 在本地模式下,可執(zhí)行文件鏈接到 ./node_modules/.bin 中,以便它們可以可用于通過 npm 運行的腳本。
簡單來說就是當(dāng)你使用 npm install 的時候 npm 會自動為你創(chuàng)建對應(yīng)的可執(zhí)行文件。如果是使用 npm install 的方式則會將對應(yīng)的可執(zhí)行文件放在 /node_modules/.bin 目錄下。如果使用 npm install --global 的方式,對應(yīng)的可執(zhí)行文件在 Unix 上會放在{prefix}/bin 目錄,在 Windows 上則是 {prefix} 目錄。
當(dāng)你執(zhí)行 npm run 的時候,npm 會在 node 環(huán)境變量(Path)中(例如 C:\Users\victorjiang\AppData\Roaming\npm)找到對應(yīng)的 node 可執(zhí)行文件并且運行它。可執(zhí)行文件包括三個:
- rollup:Unix 系統(tǒng)默認的可執(zhí)行文件,必須輸入完整文件名
- rollup.cmd:windows cmd 中默認的可執(zhí)行文件
- rollup.ps1:Windows PowerShell 中可執(zhí)行文件,可以跨平臺
在了解了 prefix,symlink,Executables 這三個概念之后我們就可以開始學(xué)習(xí) rollup 的 CLI 的功能了。
rollup 命令行的開發(fā)
Rollup 命令行的源碼在項目的根目錄的 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 是一個全局變量,即 global 對象的屬性。 // 它用于描述當(dāng)前Node.js 進程狀態(tài)的對象,提供了一個與操作系統(tǒng)的簡單接口。 // process.argv 屬性返回一個數(shù)組,由命令行執(zhí)行腳本時的各個參數(shù)組成。 // 它的第一個成員總是node,第二個成員是腳本文件名,其余成員是腳本文件的參數(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ù)組下標為2的元素開始直到末尾提取元素,舉例來說就是提取諸如 rollup -h 中除了 rollup 之外的參數(shù) * 2. yargs-parser這個包的作用是把命令行參數(shù)轉(zhuǎn)換為json對象,方便訪問。 * 例如:"rollup -h" 會被argParser解析成 { _: [], h: true, help: true } * "rollup --help" 會被argParser解析成 { _: [], help: true, h: true } * 'camel-case-expansion' 表示連字符參數(shù)是否應(yīng)該擴展為駝峰大小寫別名?默認是true. * 例如: node example.js --foo-bar 會被解析成 { _: [], 'foo-bar': true, fooBar: true } * */ const command = argParser(process.argv.slice(2), { alias: commandAliases, //alias參數(shù)表示鍵的別名對象 configuration: { 'camel-case-expansion': false } //為 argParser 解析器提供配置選項, 'camel-case-expansion': false 表示連字符參數(shù)不會被擴展為駝峰大小寫別名 }); //process.stdin.isTTY 用于檢測我們的程序是否直接連到終端 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。所以我們可以通過'source-map-support'包來實現(xiàn)這個功能。這樣當(dāng)程序執(zhí)行出錯的時候方便通過控制臺定位到源碼位置。 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) { //獲取非選項值 //例如終端輸入"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)); //獲取等號右邊的字符=> “es” const key = input.slice(0, Math.max(0, equalsIndex)) || getAliasName(input); //獲取等號左邊的字符=> “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 { //因為配置文件可以返回一個數(shù)組,所以需要挨個執(zhí)行 for (const inputOptions of options) { //內(nèi)部執(zhí)行 rollup(inputOptions) 進行打包 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); //讀取配置文件獲取配置項 const { options, warnings } = await loadConfigFile(configFile, command); return { options, warnings }; } return await loadConfigFromCommand(command); }
打包生成 rollup 文件
在 rollup.config.ts 文件中有導(dǎo)出一個方法:
//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 的時候就相當(dāng)于執(zhí)行了 rollup --config rollup.config.ts --configPlugin typescript 此時 command 就是如下對象: { _: [], 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]; }
請注意上面使用了 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來運行 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'); //修改文件可讀寫權(quán)限,保證執(zhí)行的權(quán)限 /* 在Node.js中,可以調(diào)用fs模塊,有一個方法chmod,可以用來修改文件或目錄的讀寫權(quán)限。方法chmod有三個參數(shù),文件路徑、讀寫權(quán)限和回調(diào)函數(shù),其中讀寫權(quán)限是用代號表示的, (1)0600:所有者可讀寫,其他的用戶不行 (2)0644:所有者可讀寫,其他的用戶只讀 (3)0740:所有者可讀寫,所有者所在的組只讀 (4)0755:所有者可讀寫,其他用戶可讀可執(zhí)行 */ } }; }
addCliEntry 插件將 /cli/cli.ts 源碼添加到輸出的 chunk 中,并且在文件的頭部增加一行代碼:'#!/usr/bin/env node\n\n'。
首先解釋一下 #!/usr/bin/env node
- # 在 shell 腳本中單獨使用代表注釋
- #! 組合使用表示要用在 shell 腳本中
- env 是 Mac 或者 Linux 系統(tǒng)的環(huán)境變量,是一個可執(zhí)行命令
- env node : 指的是使用當(dāng)前 env 環(huán)境內(nèi)的配置的 Path 路徑下的 node 執(zhí)行
- 當(dāng)前腳本在執(zhí)行 shell 時,會自動從 env 內(nèi)調(diào)用合適的解釋器執(zhí)行
這樣做的目的是為了能夠解析當(dāng)前腳本文件,該命令會自動從當(dāng)前 env 環(huán)境中查找配置的 node 版本來執(zhí)行腳本。
最終我們使用 npm run build 的命令打包 rollup 源碼的時候就會生成 dist/bin/rollup 這個文件了
#!/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 解析器提供配置選項, 'camel-case-expansion': false 表示連字符參數(shù)不會被擴展為駝峰大小寫別名 }); //process.stdin.isTTY 用于檢測我們的程序是否直接連到終端 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。所以我們可以通過'source-map-support'包來實現(xiàn)這個功能。這樣當(dāng)程序執(zhí)行出錯的時候方便通過控制臺定位到源碼位置。 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
以上就是rollup cli開發(fā)全面系統(tǒng)性rollup源碼分析的詳細內(nèi)容,更多關(guān)于rollup cli開發(fā)源碼分析的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
TypeScript?5.0?正式發(fā)布及使用指南詳解
這篇文章主要為大家介紹了TypeScript?5.0?正式發(fā)布及使用指南,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03TypeScript學(xué)習(xí)輕松玩轉(zhuǎn)類型操作
這篇文章主要為大家介紹了TypeScript學(xué)習(xí)輕松玩轉(zhuǎn)類型操作,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07TypeScript 基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)哈希表 HashTable教程
這篇文章主要為大家介紹了TypeScript 基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)哈希表 HashTable教程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02TS中Array.reduce提示沒有與此調(diào)用匹配的重載解析
這篇文章主要為大家介紹了TS中Array.reduce提示沒有與此調(diào)用匹配的重載解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-06-06使用typeScript 進行扁平化數(shù)據(jù)轉(zhuǎn)樹實現(xiàn)demo
這篇文章主要介紹了使用typeScript 進行扁平化數(shù)據(jù)轉(zhuǎn)樹實現(xiàn)demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-06-06