add voerkai18n-loader for webpack

This commit is contained in:
wxzhang 2023-01-27 19:39:40 +08:00
parent af07ef7d93
commit 22123a8e36
15 changed files with 360 additions and 241 deletions

View File

@ -94,6 +94,15 @@ module.exports = function(srcPath,{moduleType='cjs',isTypeScript,debug = true,la
tasks.error(e.message)
}
try{
tasks.add("生成IdMap文件")
const entryContent = isTypeScript ? "export default {}" : (moduleType=='cjs' ? "module.exports={}" :"export default {}")
fs.writeFileSync(path.join(lngPath,`idMap.${isTypeScript ? 'ts' : 'js'}`),entryContent)
tasks.complete()
}catch(e){
tasks.error(e.message)
}
try{
tasks.add(t("安装运行时依赖@voerkai18n/runtime"))
installPackage('@voerkai18n/runtime')

View File

@ -16,11 +16,21 @@ const { translate,i18nScope } = require("@voerkai18n/runtime")
const scope = new VoerkaI18nScope({
id : "{{scopeId}}", // 当前作用域的id自动取当前工程的package.json的name
debug : false, // 是否在控制台输出高度信息
default : {}, // 默认语言包
messages : {}, // 当前语言包
idMap : {}, // 消息id映射列表
formatters, // 扩展自定义格式化器
loaders : {} // 语言包加载器
default : {}, // 默认语言包
messages : {}, // 当前语言包
idMap : {}, // 消息id映射列表
formatters : {}, // 扩展自定义格式化器
loaders : {}, // 语言包加载器
languages: [
{
name: "zh",
title: "中文"
},
{
name: "en",
title: "英文"
}
]
})
// 翻译函数
const scopedTtranslate = translate.bind(scope)

View File

@ -12,11 +12,21 @@ const { translate,VoerkaI18nScope } = runtime
const scope = new VoerkaI18nScope({
id : "{{scopeId}}", // 当前作用域的id自动取当前工程的package.json的name
debug : false, // 是否在控制台输出高度信息
default : {}, // 默认语言包
messages : {}, // 当前语言包
idMap : {}, // 消息id映射列表
formatters : {}, // 扩展自定义格式化器
loaders : {} // 语言包加载器
default : {}, // 默认语言包
messages : {}, // 当前语言包
idMap : {}, // 消息id映射列表
formatters : {}, // 扩展自定义格式化器
loaders : {}, // 语言包加载器
languages: [
{
name: "zh",
title: "中文"
},
{
name: "en",
title: "英文"
}
]
})
// 翻译函数
const scopedTtranslate = translate.bind(scope)

View File

@ -60,6 +60,19 @@ module.exports = class VoerkaI18nScope {
* - 将en配置为默认回退语言
*/
_initiLanguages(){
if(!isPlainObject(this._languages)){
console.warn("[VoerkaI18n] 无效的语言配置")
this._languages = [
{
name: "zh",
title: "中文"
},
{
name: "en",
title: "英文"
}
]
}
Object.entries(this._languages).forEach(([name,language])=>{
if(!language.fallback) language.fallback = "en"
})

View File

@ -476,6 +476,123 @@ function installPackage(packageName){
}
/**
* 读取当前工程下languages/idMap.(js|ts)文件
*
* @param {*} location 项目根文件夹或者当前项目下的任意一个文件夹
* @returns
*/
function readIdMapFile(location="./"){
let searchIdMapFiles = []
if(!path.isAbsolute(location)){
location = path.join(process.cwd(),location)
}
searchIdMapFiles.push(path.join(location,"src","languages/idMap.js"))
searchIdMapFiles.push(path.join(location,"languages/idMap.js"))
searchIdMapFiles.push(path.join(location,"idMap.js"))
searchIdMapFiles.push(path.join(location,"src","languages/idMap.ts"))
searchIdMapFiles.push(path.join(location,"languages/idMap.ts"))
searchIdMapFiles.push(path.join(location,"idMap.ts"))
let projectRoot = getProjectRootFolder(location)
searchIdMapFiles.push(path.join(projectRoot,"src","languages/idMap.js"))
searchIdMapFiles.push(path.join(projectRoot,"languages/idMap.js"))
searchIdMapFiles.push(path.join(projectRoot,"idMap.js"))
searchIdMapFiles.push(path.join(projectRoot,"src","languages/idMap.ts"))
searchIdMapFiles.push(path.join(projectRoot,"languages/idMap.ts"))
searchIdMapFiles.push(path.join(projectRoot,"idMap.ts"))
let idMapFile
for( idMapFile of searchIdMapFiles){
// 如果不存在idMap文件则尝试从location/languages/中导入
if(fs.existsSync(idMapFile)){
try{
// 由于idMap.js可能是esm或cjs并且babel插件不支持异步
// 当require(idMap.js)失败时对esm模块尝试采用直接读取的方式
return require(idMapFile)
}catch(e){
// 出错原因可能是因为无效require esm模块由于idMap.js文件格式相对简单因此尝试直接读取解析
try{
let idMapContent = fs.readFileSync(idMapFile).toString()
idMapContent = idMapContent.trim().replace(/^\s*export\s*default\s/g,"")
return JSON.parse(idMapContent)
}catch{ }
}
}
}
throw new Error(`${idMapFile}文件不存在,无法对翻译文本进行转换。`)
return {}
}
//const TranslateRegex = /\bt\(\s*("|'){1}(?:((?<namespace>\w+)::))?(?<text>[^\1]*?)(?=(\1\s*\))|(\1\s*\,))/gm
// 匹配t('xxxx')的正则表达式
const TranslateRegex =/(?<=\bt\(\s*("|'){1})(?<text>[^\1]*?)(?=(\1\s*\))|(\1\s*\,))/gm
/**
* 将code中的t("xxxx")使用idMap进行映射为t("1"),t("2")的形式
*
* @param {*} code
* @param {*} idmap
*/
function replaceTranslateText(code, idmap) {
return code.replaceAll(TranslateRegex, (message) => {
if(message in idmap) {
return idmap[message]
}else{
let result
// 为什么要decodeURIComponent/unescape? 一些vite插件会将中文编码转义导致无法进行替换,所以要解码一下
try{
result = decodeURIComponent(message.replaceAll("\\u","%u"))
return result in idmap ? idmap[result] : message
}catch{
return message
}
}
})
// decodeURI 或 decodeURIComponent 对特殊字符进行转义序列编码和解码。
}
// 匹配 import {t } from 的正则表达式
const importTRegex = /^[^\w\r\n\s]*import\s*\{(.*)\bt\b(.*)\}\s*from/gm
/**
* 判定代码中是否导入了Translate函数
* @param {*} code
* @returns
*/
function hasImportTranslateFunction(code){
return importTRegex.test(code)
}
function importTranslateFunction(code,sourceFile,langPath){
let importSource = path.relative(path.dirname(sourceFile),langPath)
if(!importSource.startsWith(".")){
importSource = "./" + importSource
}
importSource= importSource.replaceAll("\\","/")
const extName = path.extname(sourceFile)
// Vue文件
if(extName==".vue"){
// 优先在<script setup></script>中导入
const setupScriptRegex = /(^\s*\<script.*\s*setup\s*.*\>)/gmi
if(setupScriptRegex.test(code)){
code = code.replace(setupScriptRegex,`$1\nimport { t } from '${importSource}';`)
}else{// 如果没有<script setup>则在<script></script>中导入
code = code.replace(/(^\s*\<script.*\>)/gmi,`$1\nimport { t } from '${importSource}';`)
}
}else if(['.jsx','.js','.ts','.tsx'].includes(extName)){
// 普通js/ts文件直接添加到最前面
code = code = `import { t } from '${importSource}';\n${code}`
}
return code
}
module.exports = {
fileMatcher, // 文件名称匹配器
getProjectRootFolder, // 查找获取项目根目录
@ -492,8 +609,12 @@ module.exports = {
deepMerge, // 深度合并对象
getDataTypeName, // 获取指定变量类型名称
isGitRepo, // 判断当前工程是否是git工程
fileIsExists,
isTypeScriptProject,
getPackageTool,
installPackage
fileIsExists, // 检查文件是否存在
isTypeScriptProject, // 当前是否是TypeScript工程
getPackageTool, // 获取当前工程使用的包工具如pnpm,yarn,npm
installPackage, // 安装指定的包
readIdMapFile, // 读取当前工程下的idMap文件
replaceTranslateText,
hasImportTranslateFunction,
importTranslateFunction // 在代码中导入t函数
}

View File

@ -1,7 +1,7 @@
import type { PluginOption } from "vite"
export interface Voerkai18nPluginOptions{
location?: string // 指定当前工程目录
autoImport?: boolean // 是否自动导入t函数
autoImport?: boolean | string[] // 是否自动导入t函数,或者[".js"]代表只对js文件进行自动导入允许只对约定的扩展名进行自动导入
debug?:boolean // 是否输出调试信息,当=true时在控制台输出转换匹配的文件清单
patterns?:(string | RegExp)[]
}

View File

@ -1,89 +1,16 @@
const path = require("path")
const fs = require("fs")
const { fileMatcher,getProjectRootFolder,getProjectLanguageFolder } = require("@voerkai18n/utils")
//const TranslateRegex = /\bt\(\s*("|'){1}(?:((?<namespace>\w+)::))?(?<text>[^\1]*?)(?=(\1\s*\))|(\1\s*\,))/gm
const TranslateRegex =/(?<=\bt\(\s*("|'){1})(?<text>[^\1]*?)(?=(\1\s*\))|(\1\s*\,))/gm
// 匹配正则表达式
const importTRegex = /^[^\w\r\n\s]*import\s*\{(.*)\bt\b(.*)\}\s*from/gm
const {
fileMatcher,
getProjectRootFolder,
getProjectLanguageFolder,
readIdMapFile,
hasImportTranslateFunction,
replaceTranslateText,
importTranslateFunction
} = require("@voerkai18n/utils")
/**
* 读取idMap.js文件
*
*
*
* @param {*} options
* @returns
*/
function readIdMapFile(options){
let { location } = options
let searchIdMapFiles = []
if(!path.isAbsolute(location)){
location = path.join(process.cwd(),location)
}
searchIdMapFiles.push(path.join(location,"src","languages/idMap.js"))
searchIdMapFiles.push(path.join(location,"languages/idMap.js"))
searchIdMapFiles.push(path.join(location,"idMap.js"))
searchIdMapFiles.push(path.join(location,"src","languages/idMap.ts"))
searchIdMapFiles.push(path.join(location,"languages/idMap.ts"))
searchIdMapFiles.push(path.join(location,"idMap.ts"))
let projectRoot = getProjectRootFolder(location)
searchIdMapFiles.push(path.join(projectRoot,"src","languages/idMap.js"))
searchIdMapFiles.push(path.join(projectRoot,"languages/idMap.js"))
searchIdMapFiles.push(path.join(projectRoot,"idMap.js"))
searchIdMapFiles.push(path.join(projectRoot,"src","languages/idMap.ts"))
searchIdMapFiles.push(path.join(projectRoot,"languages/idMap.ts"))
searchIdMapFiles.push(path.join(projectRoot,"idMap.ts"))
let idMapFile
for( idMapFile of searchIdMapFiles){
// 如果不存在idMap文件则尝试从location/languages/中导入
if(fs.existsSync(idMapFile)){
try{
// 由于idMap.js可能是esm或cjs并且babel插件不支持异步
// 当require(idMap.js)失败时对esm模块尝试采用直接读取的方式
return require(idMapFile)
}catch(e){
// 出错原因可能是因为无效require esm模块由于idMap.js文件格式相对简单因此尝试直接读取解析
try{
let idMapContent = fs.readFileSync(idMapFile).toString()
idMapContent = idMapContent.trim().replace(/^\s*export\s*default\s/g,"")
return JSON.parse(idMapContent)
}catch{ }
}
}
}
// 所有尝试完成后触发错误
throw new Error(`${idMapFile}文件不存在,无法对翻译文本进行转换。\n原因可能是babel-plugin-voerkai18n插件的location参数未指向有效的语言包所在的目录。`)
}
function replaceCode(code, idmap) {
return code.replaceAll(TranslateRegex, (message) => {
if(message in idmap) {
return idmap[message]
}else{
const msg = unescape(message.replaceAll("\\u","%u"))
return msg in idmap ? idmap[msg] : message
}
})
}
/**
* 判定代码中是否导入了Translate函数
* @param {*} code
* @returns
*/
function hasImportTranslateFunction(code){
return importTRegex.test(code)
}
/**
options = {
@ -95,7 +22,7 @@ function hasImportTranslateFunction(code){
module.exports = function VoerkaI18nPlugin(opts={}) {
let options = Object.assign({
location: "./", // 指定当前工程目录
autoImport: false, // 是否自动导入t函数
autoImport: false, // 是否自动导入t函数
debug:false, // 是否输出调试信息,当=true时在控制台输出转换匹配的文件清单
patterns:[
"!\.(svg|css|json|scss|less|sass)$",
@ -126,7 +53,7 @@ module.exports = function VoerkaI18nPlugin(opts={}) {
})
let idMap
try{
idMap = readIdMapFile(options)
idMap = readIdMapFile(options.location)
}catch(e){
console.warn("读取idMap.js文件失败@voerkai18n/vite未启用")
return
@ -138,40 +65,23 @@ module.exports = function VoerkaI18nPlugin(opts={}) {
let [isMatched,pattern] = debug ? matcher.test(id) : [matcher.test(id),null]
if(isMatched){
if(debug){
console.log(`File=${path.relative(projectRoot,id)}, pattern=[${pattern}], import from "${path.relative(path.dirname(id),languageFolder)}"`)
console.log(`[VoerkaI18n] File=${path.relative(projectRoot,id)}, pattern=[${pattern}], import from "${path.relative(path.dirname(id),languageFolder)}"`)
}
try{
// 判断是否使用了t函数
if(TranslateRegex.test(src)){
let code = replaceCode(src,idMap)
let code = replaceTranslateText(src,idMap)
// 如果没有导入t函数则尝试自动导入
if(autoImport && !hasImportTranslateFunction(code)){
let importSource = path.relative(path.dirname(id),languageFolder)
if(!importSource.startsWith(".")){
importSource = "./" + importSource
}
importSource=importSource.replace("\\","/")
const extName = path.extname(id)
// 转换Vue文件
if(extName==".vue"){
// 优先在<script setup></script>中导入
const setupScriptRegex = /(^\s*\<script.*\s*setup\s*.*\>)/gmi
if(setupScriptRegex.test(code)){
code = code.replace(setupScriptRegex,`$1\nimport { t } from '${importSource}';`)
}else{// 如果没有<script setup>则在<script></script>中导入
code = code.replace(/(^\s*\<script.*\>)/gmi,`$1\nimport { t } from '${importSource}';`)
}
}else if(['.js','.ts'].includes(extName)){// 普通js/ts文件需要添加到最前面
code = code = `import { t } from '${importSource}';\n${code}`
}
}
if(autoImport && !hasImportTranslateFunction(code)){
code = importTranslateFunction(code,id,languageFolder)
}
return {
code,
map: null
}
}
}catch(e){
console.warn(`vite-plugin-voerkai18n转换<${id}>文件出错:${e.message}`)
console.warn(`[voerkai18n]转换<${id}>文件出错:${e.message}`)
}
}
return {

View File

@ -1,117 +0,0 @@
const path = require("path")
const fs = require("fs")
/**
*
* 匹配指定路径或文件名称
*
* const matcher = fileMatcher([
* "<pattern>", // 匹配正则表达式字符串
* "!<pattern>", // 以!开头代表否定匹配
* /正则表达式/
* ],{
* basePath:"<指定一个基准目录,所有不是以此开头的均视为不匹配>",
* defaultPatterns:["<默认排除的模式>","<默认排除的模式>","<默认排除的模式>"],
* debug:<true/>false,是否输出调试信息,=true时.test()方法返回[<true/false>,pattern] *
* })
*
*
*
*
* @param {*} patterns
* @param {*} basePath 如果指定basePath则所有不是以basePath开头的文件都排除
* @param {*} defaultPatterns 默认的匹配模式
* @param {*} debug 是否输出调试信息
*/
function fileMatcher(patterns,{basePath,defaultPatterns=[],debug=true}={}) {
if(basePath) {
basePath = path.normalize(basePath)
}
//[[pattern,exclude],[pattern,false],[pattern,true]]
let finalPatterns = []
let inputPatterns = Array.isArray(patterns) ? patterns : (patterns ? [patterns] : [])
// 默认排除模式
if(defaultPatterns.length===0){
finalPatterns.push([/.*\/node_modules\/.*/,true])
finalPatterns.push([/.*\/languages\/.*/,true]) // 默认排除语言文件
finalPatterns.push([/\.babelrc/,true])
finalPatterns.push([/babel\.config\.js/,true])
finalPatterns.push([/package\.json$/,true])
finalPatterns.push([/vite\.config\.js$/,true])
finalPatterns.push([/^plugin-vue:.*/,true])
}
inputPatterns.forEach(pattern=>{
if(typeof pattern === "string"){
pattern.replaceAll("**",".*")
pattern.replaceAll("?","[^\/]?")
pattern.replaceAll(/(?<!\.)\*/g,"[^\/]*")
// 以!开头的表示排除
if(pattern.startsWith("!")){
finalPatterns.unshift([new RegExp(pattern.substring(1),"g"),true])
}else{
finalPatterns.push([new RegExp(pattern,"g"),false])
}
}else{
finalPatterns.push([pattern,false])
}
})
return {
patterns:finalPatterns,
basePath,
test: (filename) => {
let isMatched = false
let file = filename
// 如果指定basePath则文件名称必须是以basePath开头
if(basePath){
if(path.isAbsolute(file)){
if(!path.normalize(file).startsWith(basePath)){
return debug ? [false,`!^${basePath}`] : false
}else{
isMatched = true
}
}
}
if(finalPatterns.length===0){
return debug ? [true,"*"] : true
}else{
for(const pattern of finalPatterns){
pattern[0].lastIndex = 0
if(pattern[1]===true){
if(pattern[0].test(file)) return debug ? [false,pattern[0].toString()] : false
}else{
if(pattern[0].test(file)) return debug ? [true,pattern[0].toString()] : true
}
}
}
return debug ? [isMatched,"*"] : isMatched
}
}
}
function getProjectRootFolder(folder,exclueCurrent=false){
if(!path.isAbsolute(folder)){
folder = path.join(process.cwd(),folder)
}
try{
const pkgFile =exclueCurrent ?
path.join(folder, "..", "package.json")
: path.join(folder, "package.json")
if(fs.existsSync(pkgFile)){
return path.dirname(pkgFile)
}
const parent = path.dirname(folder)
if(parent===folder) return null
return getProjectRootFolder(parent,false)
}catch(e){
return process.cwd()
}
}
module.exports = {
fileMatcher,
getProjectRootFolder
}

View File

@ -1,4 +1,3 @@
export {}
import type { VoerkaI18nSupportedLanguages, VoerkaI18nTranslate } from "@Voerkai18n/runtime"
import type { InjectionKey,Plugin } from "vue"

View File

@ -0,0 +1,6 @@
t("中国")
t("中华人民共和国")
t('中国')
t('中华人民共和国')
t("中国",1)
t("中华人民共和国","fdf")

View File

@ -0,0 +1,67 @@
const { runLoaders } = require("loader-runner")
const path = require("path")
const fs = require("fs")
runLoaders({
resource: path.join(__dirname, "data","app.js"),
// String: Absolute path to the resource (optionally including query string)
// loaders: [path.join(__dirname, "loader.js?x=1")],
loaders:[
{
loader:path.join(__dirname, "loader.js"),
options:{
a:1
}
}
],
// String[]: Absolute paths to the loaders (optionally including query string)
// {loader, options}[]: Absolute paths to the loaders with options object
context: { minimize: true },
// Additional loader context which is used as base context
// processResource: (loaderContext, resourcePath, callback) => {
// console.log("loaderContext=",resourcePath)
// callback()
// },
// Optional: A function to process the resource
// Must have signature function(context, path, function(err, buffer))
// By default readResource is used and the resource is added a fileDependency
readResource: fs.readFile.bind(fs)
// Optional: A function to read the resource
// Only used when 'processResource' is not provided
// Must have signature function(path, function(err, buffer))
// By default fs.readFile is used
}, function(err, result) {
if(err){
console.error("替换失败!!!")
console.error(err.stack)
}else{
console.log("********** 成功 **********")
console.log(result.result)
}
// err: Error?
// result.result: Buffer | String
// The result
// only available when no error occured
// result.resourceBuffer: Buffer
// The raw resource as Buffer (useful for SourceMaps)
// only available when no error occured
// result.cacheable: Bool
// Is the result cacheable or do it require reexecution?
// result.fileDependencies: String[]
// An array of paths (existing files) on which the result depends on
// result.missingDependencies: String[]
// An array of paths (not existing files) on which the result depends on
// result.contextDependencies: String[]
// An array of paths (directories) on which the result depends on
})

View File

@ -0,0 +1,39 @@
const path = require('path');
const fs = require('fs');
const {
getProjectRootFolder,
getProjectLanguageFolder,
readIdMapFile,
replaceTranslateText,
hasImportTranslateFunction,
importTranslateFunction
} = require('@voerkai18n/utils')
function voerkaI18nLoader(content, map, meta) {
const { autoImport,debug } =Object.assign({
autoImport: false, // 是否自动导入t函数
debug:false // 输出一些调试信息
},this.query || {})
try{
const projectPath = getProjectRootFolder(this.resourcePath)
const lngPath = getProjectLanguageFolder(projectPath)
if(debug){
console.log("[voerkai18n-loader]",`source=${this.resourcePath}`)
}
// 是否自动导入t函数
if(autoImport && !hasImportTranslateFunction(content) ){
content = importTranslateFunction(content, this.resourcePath , lngPath)
}
const idMap = readIdMapFile(projectPath)
return replaceTranslateText(content,idMap)
}catch(e){
if(debug){
console.error("[voerkai18n-loader]",this.resourcePath,e.stack)
}
}
return content
}
module.exports = voerkaI18nLoader;

View File

@ -0,0 +1,19 @@
{
"name": "voerkai18n-loader",
"version": "1.0.0",
"description": "voerkai18n loader for webpack",
"main": "loader.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"loader-runner": "^4.3.0",
"loader-utils": "^3.2.1"
},
"dependencies": {
"@voerkai18n/utils": "workspace:^1.0.21"
}
}

View File

@ -0,0 +1,4 @@
module.exports = {
中国:1,
中华人民共和国:2
}

31
pnpm-lock.yaml generated
View File

@ -247,7 +247,7 @@ importers:
rollup-plugin-terser: ^7.0.2
dependencies:
'@babel/runtime': 7.20.7
'@babel/runtime-corejs3': 7.20.7
'@babel/runtime-corejs3': 7.20.13
core-js: 3.27.1
devDependencies:
'@babel/cli': 7.18.10_@babel+core@7.18.10
@ -288,6 +288,17 @@ importers:
'@voerkai18n/runtime': link:../runtime
vue: 3.2.45
packages/webpack:
specifiers:
'@voerkai18n/utils': workspace:^1.0.21
loader-runner: ^4.3.0
loader-utils: ^3.2.1
dependencies:
'@voerkai18n/utils': link:../utils
devDependencies:
loader-runner: 4.3.0
loader-utils: 3.2.1
packages:
/@ampproject/remapping/2.2.0:
@ -1504,6 +1515,14 @@ packages:
core-js-pure: 3.24.1
regenerator-runtime: 0.13.9
/@babel/runtime-corejs3/7.20.13:
resolution: {integrity: sha512-p39/6rmY9uvlzRiLZBIB3G9/EBr66LBMcYm7fIDeSBNdRjF2AGD3rFZucUyAgGHC2N+7DdLvVi33uTjSE44FIw==}
engines: {node: '>=6.9.0'}
dependencies:
core-js-pure: 3.27.1
regenerator-runtime: 0.13.11
dev: false
/@babel/runtime-corejs3/7.20.7:
resolution: {integrity: sha512-jr9lCZ4RbRQmCR28Q8U8Fu49zvFqLxTY9AMOUz+iyMohMoAgpEcVxY+wJNay99oXOpOcCTODkk70NDN2aaJEeg==}
engines: {node: '>=6.9.0'}
@ -8160,6 +8179,11 @@ packages:
pinkie-promise: 2.0.1
strip-bom: 2.0.0
/loader-runner/4.3.0:
resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
engines: {node: '>=6.11.5'}
dev: true
/loader-utils/1.4.0:
resolution: {integrity: sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==}
engines: {node: '>=4.0.0'}
@ -8169,6 +8193,11 @@ packages:
json5: 1.0.1
dev: true
/loader-utils/3.2.1:
resolution: {integrity: sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==}
engines: {node: '>= 12.13.0'}
dev: true
/locate-path/5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}