本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
源码地址
是什么
一个用于解析package.json文件的包
处理什么问题
- 解析package.json文件
- 提供有效的解析错误信息
- 规范化读取到的package.json数据
用法
安装
npm install read-pkg
例子
import { readPackage, readPackageSync } from 'read-pkg';
console.log(readPackageSync()); // 同步用法
console.log(await readPackage()); // 异步用法
原理
在了解了上面用法,我们看看内部是怎么处理下面的问题的:
路径获取
源码中,主要是通过几个api的配合来获取项目路径
- url.fileURLToPath : 完全解析的特定于平台的 Node.js 文件路径
- process.cwd : 返回当前进程的目录
- path.resolved : 将文件路径解析为绝对路径
import {fileURLToPath} from 'node:url';
import path from 'node:path';
const toPath = urlOrPath => urlOrPath instanceof URL ? fileURLToPath(urlOrPath) : urlOrPath;
export function readPackageSync({cwd, normalize = true} = {}) {
// 获取目录
cwd = toPath(cwd) || process.cwd();
// 配合path.resolve获取package.json文件路径
const filePath = path.resolve(cwd, 'package.json');
// ....
}
内容读取
通过fs模块来处理:
- fs.readFileSync:同步读取文件
- fs.promises.readFile:异步读取文件,通过返回promise的方式
import fs, {promises as fsPromises} from 'node:fs';
export async function readPackage({cwd, normalize = true} = {}) {
// ....
// 异步通过调用 fs.promises.readFile 来处理
const json = parseJson(await fsPromises.readFile(filePath, 'utf8'));
// ...
}
export function readPackageSync({cwd, normalize = true} = {}) {
// ....
// 同步通过 fs.readFileSync 来处理
const json = parseJson(fs.readFileSync(filePath, 'utf8'));
// ....
}
解析内容
源码中主要通过第三方包parse-json 来处理,parse-json已经提供了一系列解析处理,如错误处理。
import parseJson from 'parse-json';
export async function readPackage({cwd, normalize = true} = {}) {
// ....
const json = parseJson(await fsPromises.readFile(filePath, 'utf8'));
// ...
}
我们来看看parse-json包里是怎么处理的.
// parse-json/index.js
'use strict';
const errorEx = require('error-ex');
const fallback = require('json-parse-even-better-errors');
const {default: LinesAndColumns} = require('lines-and-columns');
const {codeFrameColumns} = require('@babel/code-frame');
const JSONError = errorEx('JSONError', {
fileName: errorEx.append('in %s'),
codeFrame: errorEx.append('\n\n%s\n')
});
const parseJson = (string, reviver, filename) => {
if (typeof reviver === 'string') {
filename = reviver;
reviver = null;
}
try {
try {
return JSON.parse(string, reviver);
} catch (error) {
// 包装一层,通过 json-parse-even-better-errors 进行处理
fallback(string, reviver);
throw error;
}
} catch (error) {
// 针对错误,进行一系列匹配,来进行精确抛出错误位置
error.message = error.message.replace(/\n/g, '');
const indexMatch = error.message.match(/in JSON at position (\d+) while parsing/);
const jsonError = new JSONError(error);
if (filename) {
jsonError.fileName = filename;
}
if (indexMatch && indexMatch.length > 0) {
const lines = new LinesAndColumns(string);
const index = Number(indexMatch[1]);
const location = lines.locationForIndex(index);
const codeFrame = codeFrameColumns(
string,
{start: {line: location.line + 1, column: location.column + 1}},
{highlightCode: true}
);
jsonError.codeFrame = codeFrame;
}
throw jsonError;
}
};
parseJson.JSONError = JSONError;
module.exports = parseJson;
规范化
这里主要是针对package.json中的元数据进行一些fix,通过第三方包normalize-package-data来进行处理。
// ....
import normalizePackageData from 'normalize-package-data';
export async function readPackage({cwd, normalize = true} = {}) {
// ...
if (normalize) {
// 规范化package.json中的数据
normalizePackageData(json);
}
// ....
}
测试用例
这里我们来了解下read-pkg中的测试用例,看看是怎么测试的
import {fileURLToPath, pathToFileURL} from 'url';
import path from 'path';
// 使用 ava 工具来测试
import test from 'ava';
import {readPackage, readPackageSync} from '../index.js';
// 这里主要是获取项目的根目录
const dirname = path.dirname(fileURLToPath(import.meta.url));
process.chdir(dirname);
const rootCwd = path.join(dirname, '..');
// 异步测试
test('async', async t => {
const package_ = await readPackage();
t.is(package_.name, 'unicorn');
t.truthy(package_._id);
});
test('async - cwd option', async t => {
const package_ = await readPackage({cwd: rootCwd});
t.is(package_.name, 'read-pkg');
t.deepEqual(
await readPackage({cwd: pathToFileURL(rootCwd)}),
package_,
);
});
// 同步测试
test('sync', t => {
const package_ = readPackageSync();
t.is(package_.name, 'unicorn');
t.truthy(package_._id);
});
test('sync - cwd option', t => {
const package_ = readPackageSync({cwd: rootCwd});
t.is(package_.name, 'read-pkg');
t.deepEqual(
readPackageSync({cwd: pathToFileURL(rootCwd)}),
package_,
);
});
从上面测试用例,我们可以了解到一些东西:
- ava 测试工具的一些用法
- import.meta.url:返回当前模块的 URL 路径
- process.chdir:更改 Node.js 进程的当前工作目录,如果失败则抛出异常(例如,如果指定的 directory 不存在)
- url.pathToFileURL:该函数确保 path 被绝对解析,并且在转换为文件网址时正确编码网址控制字符
总结
通过对read-pkg源码的学习,可以学习到一些知识点:
- node基础知识
-
- fs模块
-
-
- fs.readFileSync
- fs.promises.readFile
-
-
- process模块
-
-
- chdir:更改当前进程工作目录
- cwd
-
-
- url模块
-
-
- fileURLToPath
- pathToFileURL
-
-
- import.meta.url
- 文件内容获取过程
-
- 路径获取
- 内容读取
- json文件解析
-
- 通过第三方包parse-json来处理
- 了解到测试工具 ava