@ices/locale-webpack-plugin
Version:
webpack plugin for parsing locale files
365 lines • 14.3 kB
JavaScript
;
/**
* 指令解析规则:
* 类似于 c 语言的 #include 指令,选择 # 开头是因为,# 在 yml 里表示注释,不会破坏原有语言规范,文件也能由其他解析器正常解析
* - 每一条 include 指令,都应该以单独的一行声明,且有效内容以 #include 开头
* - \#include "xxx"、#include 'xxx'、#include xxx 以当前上下文目录为根目录匹配相对路径
* - \#include <xxx> 以 node_modules 目录为根目录匹配"绝对"路径
* - \#include "./xxx"、#include "../xxx"、#include "xxx" 为有效指令,即都是从当前上下文目录起计算相对路径匹配
* - \#include <./xxx> 、#include <../xxx>、#include </xxx> 为无效指令,即从 node_modules 目录匹配时,不能使用相对路径或斜杠开头路径
* - \#include <.xxx> 为有效路径,表示 node_modules 目录下面的 .xxx 文件
* - ••#include••"••xxx••"••、••#include••<••xxx••>•• 为有效指令,其中••为空格字符
*
* 新增 <<: 规则支持。
* <<: *xx 为标准中的引用锚点。
* 扩展规则为 <<: "xxx", <<: <xxx>, <<: xxx 为从文件xxx中导入到当前文件。
*
* 文件名解析规则:
* 不带后缀时,以 .yml 和 .yaml 为规则进行解析
* 文件为目录时,以目录下面的 index.yml 和 index.yaml 进行解析
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.removeDirectives = void 0;
const tslib_1 = require("tslib");
const fs_1 = tslib_1.__importDefault(require("fs"));
const path_1 = tslib_1.__importDefault(require("path"));
const utils_1 = require("./utils");
// 匹配指令声明的正则表达式
// 0号元素为指令 分组1为引号、分组2为contextPath、分组3为modulePath
const directiveRegx = /^\s*(?:#include|<<:)(?=[<'"\s](?!['"<>.\\/\s*;]*$))\s*(?:(['"]?)\s*([^'"<>:*?|]+?)\s*\1|<(?!\s*(?:\.*[/\\]|\.{2,}))\s*([^'"<>:*?|]+?)\s*>)[\s;]*$/gm;
// 检查指令是否正确的正则表达式
const checkDirectiveRegx = /^\s*(?:#\s*include|<<:(?!\s+\*))(?:[<'"]|\s(?!\s*$)).*$/gm;
// 解析文件名称的后缀
const resolveExtensions = ['.yml', '.yaml', '.json'];
// 当前工作目录
const cwd = fs_1.default.realpathSync(process.cwd());
/**
* 用于去除警告信息的堆栈内容。
*/
class Warning extends Error {
constructor(message) {
super(message);
this.name = 'Warning';
this.message = `Warning: (include resource) ${message}`;
this.stack = '';
}
}
// 获取匹配正则
function getDirectiveRegx() {
// 因为正则是循环匹配,文件是递归遍历,所以这里每一个节点用的匹配正则都重新实例化一个
return new RegExp(directiveRegx.source, directiveRegx.flags);
}
/**
* 读取文件内容。
* @param file 文件路径
* @param fileSystem 文件系统
*/
async function readFileAsync(file, fileSystem = fs_1.default) {
const fileNode = { file, context: path_1.default.dirname(file), source: '' };
try {
// 这里的文件系统可能由调用方传参,比如使用webpack的内存缓存文件系统
const stats = await new Promise((resolve, reject) => {
fileSystem.stat(file, (err, stats) => (err ? reject(err) : resolve(stats)));
});
if (stats.isSymbolicLink()) {
const realPath = fs_1.default.realpathSync(file);
if (realPath !== file) {
return await readFileAsync(realPath, fileSystem);
}
}
fileNode.isDir = stats.isDirectory();
if (stats.isFile()) {
const source = await new Promise((resolve, reject) => {
fileSystem.readFile(file, (err, content) => (err ? reject(err) : resolve(content)));
});
fileNode.source = source.toString('utf8');
}
fileNode.exists = true;
}
catch (e) {
fileNode.exists = false;
}
return fileNode;
}
/**
* 解析并读取文件内容。
* 如果文件是目录,会从该目录下读取 index[.ext]
* @param filePath 文件路径
* @param fileSystem 文件系统
*/
async function resolveFile(filePath, fileSystem) {
const fileNode = await readFileAsync(filePath, fileSystem);
const { file, isDir, exists } = fileNode;
if (isDir) {
for (const ext of resolveExtensions) {
// 解析目录,以index[.ext]解析文件
const indexFileNode = await readFileAsync(path_1.default.join(file, `index${ext}`), fileSystem);
const { exists, isDir } = indexFileNode;
if (exists && !isDir) {
return indexFileNode;
}
}
}
else if (!exists) {
const extName = path_1.default.extname(file);
for (const ext of resolveExtensions) {
if (ext === extName) {
continue;
}
// 以附加后缀名解析
const extFileNode = await readFileAsync(file + ext, fileSystem);
const { exists, isDir } = extFileNode;
if (exists && !isDir) {
return extFileNode;
}
}
}
return fileNode;
}
/**
* 解析文件路径。
* @param file 待解析的文件路径
* @param resolveAlias 解析别名配置
* @param context 解析上下文根路径
*/
function resolvePath(file, resolveAlias, context) {
let aliasFile = '';
for (let [alias, val] of (0, utils_1.getEntries)(resolveAlias)) {
if (typeof val !== 'string' || /^\s*$/.test(val)) {
continue;
}
const to = val.trim();
if (alias.endsWith('$')) {
if (file === alias.substr(0, alias.length - 1)) {
// 精准匹配
aliasFile = to;
}
}
else if (file.startsWith(alias + '/')) {
aliasFile = path_1.default.join(to, file.substr(alias.length + 1));
}
if (aliasFile) {
break;
}
}
if (aliasFile) {
if (path_1.default.isAbsolute(aliasFile)) {
file = aliasFile;
}
else {
file = path_1.default.join(context, aliasFile);
}
}
else {
file = path_1.default.join(context, file);
}
return file;
}
/**
* 从文件内容中解析include指令。
* 返回所有按导入顺序依赖的文件列表。
* @param fileNode
* @param parentFileNode
* @param resolvedFileMap
* @param fileSystem
* @param resolveAlias
*/
async function parseDirective(fileNode, parentFileNode, resolvedFileMap, fileSystem, resolveAlias = null) {
if (!fileNode.children) {
fileNode.children = [];
}
if (/\.ya?ml$/i.test(fileNode.file)) {
const { context, children, source } = fileNode;
const regx = getDirectiveRegx();
const contents = [];
let contentsLastIndex = 0;
let matched;
// 这里的正则是多行匹配模式
while ((matched = regx.exec(source))) {
// 0号位为指令、1号位为引号、2号位为相对路径、3号位为node_modules路径
let [directive, , contextPath, modulePath] = matched;
contents.push(source.substring(contentsLastIndex, matched.index));
contentsLastIndex = regx.lastIndex;
if (contextPath === null || contextPath === void 0 ? void 0 : contextPath.startsWith('~')) {
modulePath = contextPath;
contextPath = '';
}
const includePath = contextPath
? // 从当前目录的相对路径导入
resolvePath(contextPath, resolveAlias, context)
: // 从 node_modules 导入
resolvePath(modulePath.replace(/^~/, ''), resolveAlias, path_1.default.join(cwd, 'node_modules'));
// 解析文件
const resolvedFile = resolvedFileMap[includePath] || (await resolveFile(includePath, fileSystem));
const { file } = resolvedFile;
const prevIncluded = resolvedFileMap[file];
resolvedFileMap[includePath] = prevIncluded || resolvedFile;
resolvedFileMap[file] = prevIncluded || resolvedFile;
if (file === fileNode.file) {
// 自己导入自己
continue;
}
// 检查文件信息,如果不存在则抛异常退出解析
checkExists(resolvedFile, fileNode, directive);
if (prevIncluded) {
// 已经发起过解析的文件,可能还未处理完成解析,这里标记下,然后在整体解析完成后,展开其子节点列表到对应位置
children.push({ ...resolvedFile, cycleIncluded: true });
// 这里也为排除循环解析的情况
continue;
}
// 解析导入文件里的其他导入
children.push(...(await parseDirective(resolvedFile, fileNode, resolvedFileMap, fileSystem, resolveAlias)));
}
contents.push(source.substring(contentsLastIndex));
// 检查文件是否使用了不符合语法的#include指令,并给出提示
checkContents(contents.map((str) => str.replace(/^\r?\n/, '')).join(''), fileNode, parentFileNode);
}
// 合并引入,并返回包含导入文件和自身的文件列表
return (fileNode.children
.reduce((list, included) => {
// 如果是重复的导入,则不添加进导入文件列表里
// 循环导入的,在整体处理完成后再展开时,排除重复
if (included.cycleIncluded || !list.some((item) => item.file === included.file)) {
list.push(included);
}
return list;
}, [])
// 包含自身文件
.concat(fileNode));
}
/**
* 格式化文件打印路径。
* @param fileNode
* @param parentFileNode
*/
function printFilePath(fileNode, parentFileNode) {
const { file } = fileNode;
const filePath = (0, utils_1.normalizePath)(file, cwd);
let includedBy;
if (parentFileNode) {
const parentPath = (0, utils_1.normalizePath)(parentFileNode.file, cwd);
includedBy = ` (included by: ${parentPath})`;
}
else {
includedBy = '';
}
return filePath + includedBy;
}
/**
* 检查文件是否存在。如果不存在抛出异常,并结束解析。
* @param fileNode
* @param parentFileNode
* @param directive
*/
function checkExists(fileNode, parentFileNode, directive) {
const { exists, isDir } = fileNode;
if (!exists || isDir) {
throw new Error(`[${directive.trim()}] Can not resolve the file: ${printFilePath(fileNode, parentFileNode)}`);
}
return true;
}
/**
* 检查不符合语法的导入指令,并给出提示
* @param content
* @param fileNode
* @param parentFileNode
*/
function checkContents(content, fileNode, parentFileNode) {
const matched = content.match(checkDirectiveRegx);
if (matched) {
if (!fileNode.warnings) {
fileNode.warnings = [];
}
fileNode.warnings.push(new Warning(`Directive syntax error: ${matched
.map((str) => `[${str.trim()}]`)
.join(' ')}\n${printFilePath(fileNode, parentFileNode)}`));
}
}
/**
* 获取警告信息
* @param fileNodeMaps 已解析的文件集map
*/
function serializeWarnings(fileNodeMaps) {
const warningsMap = {};
for (const { file, warnings } of Object.values(fileNodeMaps)) {
if (warningsMap[file] || !warnings || !warnings.length) {
continue;
}
warningsMap[file] = warnings;
}
return Object.values(warningsMap).reduce((array, item) => {
array.push(...item);
return array;
}, []);
}
/**
* 输出合并导入后的文件列表。
* 按数组正序解析,后面的元素覆盖前面元素的内容即可。
* @param fileNodes 待合并的导入文件列表。
*/
function serializeFiles(fileNodes) {
// 展开循环导入的文件
const fileList = [];
for (const node of fileNodes) {
if (node.cycleIncluded && node.children) {
for (const child of node.children) {
if (!fileList.some((item) => item.file === child.file)) {
fileList.push(child);
}
}
}
if (!fileList.some((item) => item.file === node.file)) {
fileList.push(node);
}
}
return fileList.map(({ file, source, context }) => ({ file, source, context }));
}
/**
* 清除文件中的指令声明行。
* @param source 文件内容。
*/
function removeDirectives(source) {
const regx = getDirectiveRegx();
const contents = [];
let contentsLastIndex = 0;
let matched;
// 这里的正则是多行匹配模式
while ((matched = regx.exec(source))) {
contents.push(source.substring(contentsLastIndex, matched.index));
contentsLastIndex = regx.lastIndex;
}
contents.push(source.substring(contentsLastIndex));
return contents.join('');
}
exports.removeDirectives = removeDirectives;
/**
* 解析yml文件导入指令。
* @param file 待解析文件的路径。
* @param fileSystem webpack可缓存的文件系统。
* @param resolveAlias 解析别名配置。
*/
async function parseIncludeAsync(file, fileSystem, resolveAlias) {
const fileNode = await readFileAsync(file, fileSystem);
const resolvedFileMap = {
[fileNode.file]: fileNode,
};
try {
const fileNodes = await parseDirective(fileNode, null, resolvedFileMap, fileSystem, resolveAlias);
// 将结果按导入的顺序整理后返回
return {
files: serializeFiles(fileNodes.concat(fileNode)),
warnings: serializeWarnings(resolvedFileMap),
error: null,
};
}
catch (error) {
// 将已解析的文件列表返回,因为需要添加依赖信息,好在文件更新时,触发重新构建
return {
files: serializeFiles(Object.values(resolvedFileMap).concat(fileNode)),
warnings: serializeWarnings(resolvedFileMap),
error: error,
};
}
}
exports.default = parseIncludeAsync;
//# sourceMappingURL=include.js.map