【若川视野 x 源码共读】第27期 | read-pkg

111 阅读3分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

源码地址

github: read-pkg

是什么

一个用于解析package.json文件的包

处理什么问题

  • 解析package.json文件
  • 提供有效的解析错误信息
  • 规范化读取到的package.json数据

用法

安装

npm install read-pkg

例子

import { readPackage, readPackageSync } from 'read-pkg';

console.log(readPackageSync());	// 同步用法
console.log(await readPackage()); // 异步用法

原理

在了解了上面用法,我们看看内部是怎么处理下面的问题的:

路径获取

源码中,主要是通过几个api的配合来获取项目路径

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模块来处理:

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