Skip to content

Commit b557f8e

Browse files
authored
experimental: component options injection plugin (#159)
* feat: component option injection plugin for intlify tools * add warning
1 parent ec99399 commit b557f8e

11 files changed

+396
-25
lines changed

.eslintrc.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ module.exports = {
1616
'plugin:vue-libs/recommended',
1717
'plugin:@typescript-eslint/recommended',
1818
'plugin:@typescript-eslint/eslint-recommended',
19-
'prettier/@typescript-eslint',
20-
'plugin:prettier/recommended'
19+
'plugin:prettier/recommended',
20+
'prettier'
2121
],
2222
plugins: ['@typescript-eslint'],
2323
parser: 'vue-eslint-parser',
@@ -30,6 +30,7 @@ module.exports = {
3030
'@typescript-eslint/explicit-function-return-type': 'off',
3131
'@typescript-eslint/member-delimiter-style': 'off',
3232
'@typescript-eslint/no-use-before-define': 'off',
33-
'@typescript-eslint/no-non-null-assertion': 'off'
33+
'@typescript-eslint/no-non-null-assertion': 'off',
34+
'@typescript-eslint/ban-ts-comment': 'off'
3435
}
3536
}

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@
3737
"@types/loader-utils": "^2.0.0",
3838
"@types/memory-fs": "^0.3.2",
3939
"@types/node": "^14.14.10",
40-
"@types/webpack": "^4.41.1",
40+
"@types/webpack": "^4.41.26",
4141
"@types/webpack-merge": "^4.1.5",
4242
"@typescript-eslint/eslint-plugin": "^4.15.0",
4343
"@typescript-eslint/parser": "^4.15.0",
4444
"@vue/compiler-sfc": "^3.0.5",
4545
"babel-loader": "^8.1.0",
4646
"eslint": "^7.19.0",
47-
"eslint-config-prettier": "^7.2.0",
47+
"eslint-config-prettier": "^8.1.0",
4848
"eslint-plugin-prettier": "^3.3.1",
4949
"eslint-plugin-vue-libs": "^4.0.0",
5050
"jest": "^26.6.3",
@@ -64,7 +64,7 @@
6464
"vue": "^3.0.4",
6565
"vue-i18n": "^9.0.0-rc.5",
6666
"vue-loader": "^16.1.2",
67-
"webpack": "^4.44.2",
67+
"webpack": "^4.46.0",
6868
"webpack-cli": "^3.3.12",
6969
"webpack-dev-server": "^3.11.0",
7070
"webpack-merge": "^4.2.2"

src/plugin.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import webpack from 'webpack'
2+
3+
declare class IntlifyVuePlugin implements webpack.Plugin {
4+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5+
constructor(optoins: Record<string, any>)
6+
apply(compiler: webpack.Compiler): void
7+
}
8+
9+
let Plugin: typeof IntlifyVuePlugin
10+
11+
console.warn(
12+
`[@intlify/vue-i18n-loader] IntlifyVuePlugin is experimental! This plugin is used for Intlify tools. Don't use this plugin to enhancement Component options.`
13+
)
14+
15+
if (webpack.version && webpack.version[0] > '4') {
16+
// webpack5 and upper
17+
Plugin = require('./pluginWebpack5').default // eslint-disable-line @typescript-eslint/no-var-requires
18+
} else {
19+
// webpack4 and lower
20+
Plugin = require('./pluginWebpack4').default // eslint-disable-line @typescript-eslint/no-var-requires
21+
}
22+
23+
export default Plugin

src/pluginWebpack4.ts

+244
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
3+
import { parse as parseQuery } from 'querystring'
4+
import webpack from 'webpack'
5+
import { ReplaceSource } from 'webpack-sources'
6+
import { isFunction, isObject, isRegExp, isString } from '@intlify/shared'
7+
const Dependency = require('webpack/lib/Dependency') // eslint-disable-line @typescript-eslint/no-var-requires
8+
// const Template = require('webpack/lib/Template') // eslint-disable-line @typescript-eslint/no-var-requires
9+
10+
const PLUGIN_ID = 'IntlifyVuePlugin'
11+
12+
interface NormalModule extends webpack.compilation.Module {
13+
resource?: string
14+
request?: string
15+
userRequest?: string
16+
addDependency(dep: unknown): void
17+
parser?: webpack.compilation.normalModuleFactory.Parser
18+
loaders?: Array<{
19+
loader: string
20+
options: any
21+
indent?: string
22+
type?: string
23+
}>
24+
}
25+
26+
type InjectionValues = Record<string, any>
27+
28+
class VueComponentDependency extends Dependency {
29+
static Template: VueComponentDependencyTemplate
30+
script?: NormalModule
31+
templa?: NormalModule
32+
values: InjectionValues
33+
statement: any
34+
35+
constructor(
36+
script: NormalModule | undefined,
37+
template: NormalModule | undefined,
38+
values: InjectionValues,
39+
statement: any
40+
) {
41+
super()
42+
this.script = script
43+
this.template = template
44+
this.values = values
45+
this.statement = statement
46+
}
47+
48+
updateHash(hash: any) {
49+
super.updateHash(hash)
50+
const scriptModule = this.script
51+
hash.update(
52+
(scriptModule &&
53+
(!scriptModule.buildMeta || scriptModule.buildMeta.exportsType)) + ''
54+
)
55+
hash.update((scriptModule && scriptModule.id) + '')
56+
const templateModule = this.templa
57+
hash.update(
58+
(templateModule &&
59+
(!templateModule.buildMeta || templateModule.buildMeta.exportsType)) +
60+
''
61+
)
62+
hash.update((templateModule && templateModule.id) + '')
63+
}
64+
}
65+
66+
/*
67+
function getRequestAndIndex(
68+
script: NormalModule,
69+
template: NormalModule
70+
): [string, number] {
71+
if (script && template) {
72+
return [script.userRequest!, 1]
73+
} else if (script && !template) {
74+
return [script.userRequest!, 0]
75+
} else if (!script && template) {
76+
return [template.userRequest!, 0]
77+
} else {
78+
return ['', -1]
79+
}
80+
}
81+
*/
82+
83+
function stringifyObj(obj: Record<string, any>): string {
84+
return `Object({${Object.keys(obj)
85+
.map(key => {
86+
const code = obj[key]
87+
return `${JSON.stringify(key)}:${toCode(code)}`
88+
})
89+
.join(',')}})`
90+
}
91+
92+
function toCode(code: any): string {
93+
if (code === null) {
94+
return 'null'
95+
}
96+
97+
if (code === undefined) {
98+
return 'undefined'
99+
}
100+
101+
if (isString(code)) {
102+
return JSON.stringify(code)
103+
}
104+
105+
if (isRegExp(code) && code.toString) {
106+
return code.toString()
107+
}
108+
109+
if (isFunction(code) && code.toString) {
110+
return '(' + code.toString() + ')'
111+
}
112+
113+
if (isObject(code)) {
114+
return stringifyObj(code)
115+
}
116+
117+
return code + ''
118+
}
119+
120+
function generateCode(dep: VueComponentDependency, importVar: string): string {
121+
// const [request, index] = getRequestAndIndex(dep.script!, dep.template!)
122+
// if (!require) {
123+
// return ''
124+
// }
125+
126+
// const importVar = `${Template.toIdentifier(
127+
// `${request}`
128+
// )}__WEBPACK_IMPORTED_MODULE_${index.toString()}__["default"]`
129+
130+
const injectionCodes = ['']
131+
Object.keys(dep.values).forEach(key => {
132+
const code = dep.values[key]
133+
if (isFunction(code)) {
134+
injectionCodes.push(`${importVar}.${key} = ${JSON.stringify(code(dep))}`)
135+
} else {
136+
injectionCodes.push(`${importVar}.${key} = ${toCode(code)}`)
137+
}
138+
})
139+
140+
let ret = injectionCodes.join('\n')
141+
ret = ret.length > 0 ? `\n${ret}\n` : ''
142+
return (ret += `/* harmony default export */ __webpack_exports__["default"] = (${importVar});`)
143+
}
144+
145+
class VueComponentDependencyTemplate {
146+
apply(dep: VueComponentDependency, source: ReplaceSource) {
147+
const repleacements = source.replacements
148+
const orgReplace = repleacements[repleacements.length - 1]
149+
if (dep.statement.declaration.start !== orgReplace.start) {
150+
return
151+
}
152+
153+
const code = generateCode(dep, orgReplace.content)
154+
// console.log('generateCode', code, dep.statement, orgReplace)
155+
// source.insert(dep.statement.declaration.range[0], code)
156+
source.replace(orgReplace.start, orgReplace.end, code)
157+
}
158+
}
159+
160+
VueComponentDependency.Template = VueComponentDependencyTemplate
161+
162+
function getScriptBlockModule(parser: any): NormalModule | undefined {
163+
return parser.state.current.dependencies.find((dep: any) => {
164+
const req = dep.userRequest || dep.request
165+
if (req && dep.originModule) {
166+
const query = parseQuery(req)
167+
return query.type === 'script' && query.lang === 'js'
168+
} else {
169+
return false
170+
}
171+
})
172+
}
173+
174+
function getTemplateBlockModule(parser: any): NormalModule | undefined {
175+
return parser.state.current.dependencies.find((dep: any) => {
176+
const req = dep.userRequest || dep.request
177+
if (req && dep.originModule) {
178+
const query = parseQuery(req)
179+
return query.type === 'template'
180+
} else {
181+
return false
182+
}
183+
})
184+
}
185+
186+
function toVueComponentDependency(parser: any, values: InjectionValues) {
187+
return function vueComponentDependencyw(statement: any) {
188+
// console.log('toVueComponentDependency##statement', statement)
189+
const dep = new VueComponentDependency(
190+
getScriptBlockModule(parser),
191+
getTemplateBlockModule(parser),
192+
values,
193+
statement
194+
)
195+
// dep.loc = expr.loc
196+
parser.state.current.addDependency(dep)
197+
return true
198+
}
199+
}
200+
201+
export default class IntlifyVuePlugin implements webpack.Plugin {
202+
injections: InjectionValues
203+
204+
constructor(injections: InjectionValues = {}) {
205+
this.injections = injections
206+
}
207+
208+
apply(compiler: webpack.Compiler): void {
209+
const injections = this.injections
210+
211+
compiler.hooks.compilation.tap(
212+
PLUGIN_ID,
213+
(compilation, { normalModuleFactory }) => {
214+
// compilation.dependencyFactories.set(
215+
// // @ts-ignore
216+
// VueComponentDependency,
217+
// new NullFactory()
218+
// )
219+
compilation.dependencyTemplates.set(
220+
// @ts-ignore
221+
VueComponentDependency,
222+
// @ts-ignore
223+
new VueComponentDependency.Template()
224+
)
225+
226+
normalModuleFactory.hooks.parser
227+
.for('javascript/auto')
228+
.tap(PLUGIN_ID, parser => {
229+
parser.hooks.exportExpression.tap(
230+
PLUGIN_ID,
231+
(statement, declaration) => {
232+
if (declaration.name === 'script') {
233+
// console.log('exportExpression', statement, declaration)
234+
return toVueComponentDependency(parser, injections)(statement)
235+
}
236+
}
237+
)
238+
})
239+
}
240+
)
241+
}
242+
}
243+
244+
/* eslint-enable @typescript-eslint/no-explicit-any */

src/pluginWebpack5.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import webpack from 'webpack'
2+
3+
export default class IntlifyVuePlugin implements webpack.Plugin {
4+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
5+
apply(compiler: webpack.Compiler): void {
6+
console.error('[@intlify/vue-i18n-loader] Cannot support webpack5 yet')
7+
}
8+
}

test/fixtures/global.vue

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
<template>
2+
<h1>Meta</h1>
3+
</template>
4+
5+
<script>
6+
export default {
7+
name: 'Meta'
8+
}
9+
</script>
10+
111
<i18n global>
212
{
313
"en": {

test/fixtures/plugin/basic.vue

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<template>
2+
<h1>Meta</h1>
3+
</template>
4+
5+
<script>
6+
export default {
7+
name: 'Meta'
8+
}
9+
</script>
10+
11+
<i18n>
12+
{
13+
"ja": "hello"
14+
}
15+
</i18n>

test/fixtures/plugin/script.vue

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
export default {
3+
name: 'Meta'
4+
}
5+
</script>

test/plugin.test.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { bundleAndRun, bundleEx } from './utils'
2+
3+
const options = {
4+
a: 1,
5+
b: 'hello',
6+
c: {
7+
a: 1,
8+
nest: {
9+
foo: 'hello'
10+
}
11+
},
12+
d: () => 'hello'
13+
}
14+
15+
test('basic', async () => {
16+
const { module } = await bundleAndRun('./plugin/basic.vue', bundleEx, {
17+
intlify: options
18+
})
19+
expect(module.a).toEqual(1)
20+
expect(module.b).toEqual('hello')
21+
expect(module.c.a).toEqual(1)
22+
expect(module.c.nest.foo).toEqual('hello')
23+
expect(module.d).toEqual('hello')
24+
})
25+
26+
test('script only', async () => {
27+
const { module } = await bundleAndRun('./plugin/script.vue', bundleEx, {
28+
intlify: options
29+
})
30+
expect(module.a).toEqual(1)
31+
expect(module.b).toEqual('hello')
32+
expect(module.c.a).toEqual(1)
33+
expect(module.c.nest.foo).toEqual('hello')
34+
expect(module.d).toEqual('hello')
35+
})

0 commit comments

Comments
 (0)