UNPKG

@ices/locale-webpack-plugin

Version:
365 lines 14.3 kB
"use strict"; /** * 指令解析规则: * 类似于 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