mirror of
https://github.com/retailcrm/vue-i18n-loader.git
synced 2024-11-25 14:56:04 +03:00
248 lines
8.6 KiB
JavaScript
248 lines
8.6 KiB
JavaScript
|
const qs = require('querystring')
|
||
|
const walk = require('acorn-walk')
|
||
|
|
||
|
const NullFactory = require('webpack/lib/NullFactory')
|
||
|
|
||
|
const VueI18NDependency = require('./dependencies/VueI18NDependency')
|
||
|
const VueI18NMetadata = require('./metadata/VueI18NMetadata')
|
||
|
|
||
|
const utils = require('./utils')
|
||
|
|
||
|
/**
|
||
|
* @typedef {webpack.Compiler} Compiler
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {webpack.compilation.normalModuleFactory.Parser} Parser
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {webpack.compilation.Module} Module
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {Object} Loader
|
||
|
* @property {string} loader
|
||
|
* @property {*} options
|
||
|
* @property {string|undefined} indent
|
||
|
* @property {string|undefined} type
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* @typedef {Object} NormalModuleExtend
|
||
|
* @property {string|undefined} resource
|
||
|
* @property {string|undefined} request
|
||
|
* @property {string|undefined} userRequest
|
||
|
* @property {Parser} parser
|
||
|
* @property {Array<Loader>} loaders
|
||
|
* @method addDependency(dep): void
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {Module & NormalModuleExtend} NormalModule
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @param {CallExpression} e
|
||
|
* @param objName
|
||
|
* @param propName
|
||
|
* @return {boolean}
|
||
|
*/
|
||
|
const matchesVarCall = (e, [objName, propName]) => e.callee.type === 'MemberExpression'
|
||
|
&& e.callee.object.type === 'Identifier'
|
||
|
&& e.callee.object.name === objName
|
||
|
&& e.callee.property.name === propName
|
||
|
|
||
|
/**
|
||
|
* @param {CallExpression} e
|
||
|
* @param objName
|
||
|
* @param propName
|
||
|
* @return {boolean}
|
||
|
*/
|
||
|
const matchesThisCall = (e, [objName, propName]) => e.callee.type === 'MemberExpression'
|
||
|
&& e.callee.object.type === 'MemberExpression'
|
||
|
&& e.callee.object.object.type === 'ThisExpression'
|
||
|
&& e.callee.object.property.name === objName
|
||
|
&& e.callee.property.name === propName
|
||
|
|
||
|
/**
|
||
|
* @param {CallExpression} e
|
||
|
* @param componentName
|
||
|
* @return {boolean}
|
||
|
*/
|
||
|
const matchesComponentCall = (e, componentName) => {
|
||
|
const { callee, arguments: [calledComponentName] } = e
|
||
|
|
||
|
return callee.type === 'Identifier'
|
||
|
&& callee.name === '_c'
|
||
|
&& calledComponentName
|
||
|
&& calledComponentName.type === 'Literal'
|
||
|
&& calledComponentName.value === componentName
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {CallExpression} e
|
||
|
* @returns {null|ObjectExpression}
|
||
|
*/
|
||
|
const getDirectiveObject = (e) => {
|
||
|
/** @type {ObjectExpression} */
|
||
|
const objectExpression = e.arguments[1]
|
||
|
|
||
|
if (e.callee.type === 'Identifier'
|
||
|
&& e.callee.name === '_c'
|
||
|
&& isObjectExpression(objectExpression)
|
||
|
) {
|
||
|
/** @type {Property} */
|
||
|
const directives = objectExpression.properties.find(prop => hasPropertyWithDirectives(prop))
|
||
|
|
||
|
if (isArrayProp(directives)) {
|
||
|
/** @type {ObjectExpression} */
|
||
|
const object = directives.value.elements.find(elem => isObjectExpression(elem))
|
||
|
/** @type {Property} */
|
||
|
const prop = object.properties.find(prop => prop.value.value === 'v-t')
|
||
|
|
||
|
return prop ? object : null
|
||
|
}
|
||
|
|
||
|
return null
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {ObjectExpression} directiveObject
|
||
|
* @return {Property}
|
||
|
*/
|
||
|
const getPathToDirectiveValue = (directiveObject) => {
|
||
|
/** @type {Property} */
|
||
|
let property = directiveObject.properties.find(prop => prop.key.name === 'value')
|
||
|
|
||
|
// если директива используется как: <p v-t="{ path: 'directive_detected' }"/>
|
||
|
if (property.value.type === 'ObjectExpression') {
|
||
|
property = property.value.properties.find(prop => prop.key.name === 'path')
|
||
|
}
|
||
|
|
||
|
return property
|
||
|
}
|
||
|
|
||
|
const isLiteral = node => node && node.type === 'Literal'
|
||
|
|
||
|
const isObjectExpression = node => node && node.type === 'ObjectExpression'
|
||
|
|
||
|
const hasPropertyWithDirectives = property => property && property.key.name === 'directives'
|
||
|
|
||
|
const isArrayProp = property => property && property.value.type === 'ArrayExpression'
|
||
|
|
||
|
module.exports = class VueI18NLoaderPlugin {
|
||
|
constructor () {
|
||
|
this.name = 'VueI18NLoaderPlugin'
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {Compiler} compiler
|
||
|
*/
|
||
|
apply (compiler) {
|
||
|
const metadata = new VueI18NMetadata()
|
||
|
|
||
|
const rootPath = compiler.options.context || process.cwd()
|
||
|
|
||
|
const createId = resourcePath => utils.createId(rootPath, resourcePath)
|
||
|
|
||
|
compiler.hooks.compilation.tap(this.name, (compilation, { normalModuleFactory }) => {
|
||
|
compilation.hooks.normalModuleLoader.tap(this.name, loaderCtx => {
|
||
|
loaderCtx.__vueI18NMetadata = metadata
|
||
|
})
|
||
|
|
||
|
compilation.dependencyFactories.set(VueI18NDependency, new NullFactory())
|
||
|
compilation.dependencyTemplates.set(VueI18NDependency, new VueI18NDependency.Template())
|
||
|
|
||
|
/**
|
||
|
* @param {Parser} parser
|
||
|
*/
|
||
|
const handler = (parser) => {
|
||
|
parser.hooks.program.tap(this.name, ast => {
|
||
|
const [resourcePath, resourceQuery] = parser.state.module.resource.split('?')
|
||
|
|
||
|
const id = createId(resourcePath)
|
||
|
const query = qs.parse(resourceQuery)
|
||
|
|
||
|
if (resourcePath.endsWith('.vue') && ['template', 'script'].includes(query.type)) {
|
||
|
const addDependency = path => {
|
||
|
const dep = new VueI18NDependency(id, path.value, path.range, metadata)
|
||
|
dep.loc = path.loc
|
||
|
|
||
|
parser.state.current.addDependency(dep)
|
||
|
}
|
||
|
|
||
|
walk.simple(ast, {
|
||
|
/**
|
||
|
* @param {CallExpression} expression
|
||
|
*/
|
||
|
CallExpression (expression) {
|
||
|
// @TODO:
|
||
|
// Calculate object name _vm - seek `var _vm = this` in ast
|
||
|
// Maybe another visitor method could help, it will be called earlier,
|
||
|
// cause of AST structure
|
||
|
// Suggestion: seek `$createElement` renaming & extract identifier from it
|
||
|
// (to be sure it is done correctly)
|
||
|
// VariableDeclarator, id / init, init.type = ThisExpression
|
||
|
if ((
|
||
|
matchesVarCall(expression, ['_vm', '$t']) // _vm.$t
|
||
|
|| matchesVarCall(expression, ['_vm', '$tc']) // _vm.$tc
|
||
|
|| matchesThisCall(expression, ['$i18n', 't']) // this.$i18n.t
|
||
|
|| matchesThisCall(expression, ['$i18n', 'tc']) // this.$i18n.tc
|
||
|
)) {
|
||
|
const [path] = expression.arguments
|
||
|
|
||
|
if (isLiteral(path)) {
|
||
|
addDependency(path)
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if (matchesComponentCall(expression, 'i18n')) { // _c('i18n', ...)
|
||
|
/** @type {ObjectExpression} */
|
||
|
const vNodeData = expression.arguments[1]
|
||
|
|
||
|
/** @type {Property} */
|
||
|
const attrs = vNodeData.properties.find(prop => prop.key.name === 'attrs')
|
||
|
|
||
|
if (!attrs) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
/** @type {Property} */
|
||
|
const path = attrs.value.properties.find(prop => prop.key.name === 'path')
|
||
|
|
||
|
if (path && isLiteral(path.value)) {
|
||
|
addDependency(path.value)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** @type {ObjectExpression} */
|
||
|
const directives = getDirectiveObject(expression)
|
||
|
|
||
|
if (directives) {
|
||
|
/** @type {Property} */
|
||
|
const path = getPathToDirectiveValue(directives)
|
||
|
|
||
|
if (isLiteral(path.value)) {
|
||
|
addDependency(path.value)
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
})
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
const parser = normalModuleFactory.hooks.parser
|
||
|
|
||
|
parser.for('javascript/auto').tap(this.name, handler)
|
||
|
parser.for('javascript/dynamic').tap(this.name, handler)
|
||
|
parser.for('javascript/esm').tap(this.name, handler)
|
||
|
})
|
||
|
}
|
||
|
}
|