1 | const Asset = require('../Asset');
|
2 | const localRequire = require('../utils/localRequire');
|
3 | const md5 = require('../utils/md5');
|
4 | const {minify} = require('terser');
|
5 | const t = require('@babel/types');
|
6 |
|
7 | class VueAsset extends Asset {
|
8 | constructor(name, options) {
|
9 | super(name, options);
|
10 | this.type = 'js';
|
11 | }
|
12 |
|
13 | async parse(code) {
|
14 |
|
15 | this.vueTemplateCompiler = await localRequire(
|
16 | 'vue-template-compiler',
|
17 | this.name
|
18 | );
|
19 | this.vue = await localRequire('@vue/component-compiler-utils', this.name);
|
20 |
|
21 | return this.vue.parse({
|
22 | source: code,
|
23 | needMap: this.options.sourceMaps,
|
24 | filename: this.relativeName,
|
25 | sourceRoot: '',
|
26 | compiler: this.vueTemplateCompiler
|
27 | });
|
28 | }
|
29 |
|
30 | async generate() {
|
31 | let descriptor = this.ast;
|
32 | let parts = [];
|
33 |
|
34 | if (descriptor.script) {
|
35 | parts.push({
|
36 | type: descriptor.script.lang || 'js',
|
37 | value: descriptor.script.content,
|
38 | map: descriptor.script.map
|
39 | });
|
40 | }
|
41 |
|
42 | if (descriptor.template) {
|
43 | parts.push({
|
44 | type: descriptor.template.lang || 'html',
|
45 | value: descriptor.template.content.trim()
|
46 | });
|
47 | }
|
48 |
|
49 | if (descriptor.styles) {
|
50 | for (let style of descriptor.styles) {
|
51 | parts.push({
|
52 | type: style.lang || 'css',
|
53 | value: style.content.trim(),
|
54 | modules: !!style.module
|
55 | });
|
56 | }
|
57 | }
|
58 |
|
59 | return parts;
|
60 | }
|
61 |
|
62 | async postProcess(generated) {
|
63 | let result = [];
|
64 |
|
65 | let hasScoped = this.ast.styles.some(s => s.scoped);
|
66 | let id = md5(this.name).slice(-6);
|
67 | let scopeId = hasScoped ? `data-v-${id}` : null;
|
68 | let optsVar = '$' + id;
|
69 |
|
70 |
|
71 | let js = this.ast.script ? generated[0].value : '';
|
72 | let supplemental = '';
|
73 |
|
74 |
|
75 | if (this.options.scopeHoist) {
|
76 | optsVar = `$${t.toIdentifier(this.id)}$export$default`;
|
77 |
|
78 | if (!js.includes(optsVar)) {
|
79 | optsVar = `$${t.toIdentifier(this.id)}$exports`;
|
80 | if (!js.includes(optsVar)) {
|
81 | supplemental += `
|
82 | var ${optsVar} = {};
|
83 | `;
|
84 |
|
85 | this.cacheData.isCommonJS = true;
|
86 | }
|
87 | }
|
88 | } else {
|
89 | supplemental += `
|
90 | var ${optsVar} = exports.default || module.exports;
|
91 | `;
|
92 | }
|
93 |
|
94 | supplemental += `
|
95 | if (typeof ${optsVar} === 'function') {
|
96 | ${optsVar} = ${optsVar}.options;
|
97 | }
|
98 | `;
|
99 |
|
100 | supplemental += this.compileTemplate(generated, scopeId, optsVar);
|
101 | supplemental += this.compileCSSModules(generated, optsVar);
|
102 | supplemental += this.compileHMR(generated, optsVar);
|
103 |
|
104 | if (this.options.minify && !this.options.scopeHoist) {
|
105 | let {code, error} = minify(supplemental, {toplevel: true});
|
106 | if (error) {
|
107 | throw error;
|
108 | }
|
109 |
|
110 | supplemental = code;
|
111 | if (supplemental) {
|
112 | supplemental = `\n(function(){${supplemental}})();`;
|
113 | }
|
114 | }
|
115 | js += supplemental;
|
116 |
|
117 | if (js) {
|
118 | result.push({
|
119 | type: 'js',
|
120 | value: js,
|
121 | map: this.options.sourceMaps && this.ast.script && generated[0].map
|
122 | });
|
123 | }
|
124 |
|
125 | let css = this.compileStyle(generated, scopeId);
|
126 | if (css) {
|
127 | result.push({
|
128 | type: 'css',
|
129 | value: css
|
130 | });
|
131 | }
|
132 |
|
133 | return result;
|
134 | }
|
135 |
|
136 | compileTemplate(generated, scopeId, optsVar) {
|
137 | let html = generated.find(r => r.type === 'html');
|
138 | if (html) {
|
139 | let isFunctional = this.ast.template.attrs.functional;
|
140 | let template = this.vue.compileTemplate({
|
141 | source: html.value,
|
142 | filename: this.relativeName,
|
143 | compiler: this.vueTemplateCompiler,
|
144 | isProduction: this.options.production,
|
145 | isFunctional,
|
146 | compilerOptions: {
|
147 | scopeId
|
148 | }
|
149 | });
|
150 |
|
151 | if (Array.isArray(template.errors) && template.errors.length >= 1) {
|
152 | throw new Error(template.errors[0]);
|
153 | }
|
154 |
|
155 | return `
|
156 | /* template */
|
157 | Object.assign(${optsVar}, (function () {
|
158 | ${template.code}
|
159 | return {
|
160 | render: render,
|
161 | staticRenderFns: staticRenderFns,
|
162 | _compiled: true,
|
163 | _scopeId: ${JSON.stringify(scopeId)},
|
164 | functional: ${JSON.stringify(isFunctional)}
|
165 | };
|
166 | })());
|
167 | `;
|
168 | }
|
169 |
|
170 | return '';
|
171 | }
|
172 |
|
173 | compileCSSModules(generated, optsVar) {
|
174 | let cssRenditions = generated.filter(r => r.type === 'css');
|
175 | let cssModulesCode = '';
|
176 | this.ast.styles.forEach((style, index) => {
|
177 | if (style.module) {
|
178 | let cssModules = JSON.stringify(cssRenditions[index].cssModules);
|
179 | let name = style.module === true ? '$style' : style.module;
|
180 | cssModulesCode += `\nthis[${JSON.stringify(name)}] = ${cssModules};`;
|
181 | }
|
182 | });
|
183 |
|
184 | if (cssModulesCode) {
|
185 | cssModulesCode = `function hook(){${cssModulesCode}\n}`;
|
186 |
|
187 | let isFunctional =
|
188 | this.ast.template && this.ast.template.attrs.functional;
|
189 | if (isFunctional) {
|
190 | return `
|
191 | /* css modules */
|
192 | (function () {
|
193 | ${cssModulesCode}
|
194 | ${optsVar}._injectStyles = hook;
|
195 | var originalRender = ${optsVar}.render;
|
196 | ${optsVar}.render = function (h, context) {
|
197 | hook.call(context);
|
198 | return originalRender(h, context);
|
199 | };
|
200 | })();
|
201 | `;
|
202 | } else {
|
203 | return `
|
204 | /* css modules */
|
205 | (function () {
|
206 | ${cssModulesCode}
|
207 | ${optsVar}.beforeCreate = ${optsVar}.beforeCreate ? ${optsVar}.beforeCreate.concat(hook) : [hook];
|
208 | })();
|
209 | `;
|
210 | }
|
211 | }
|
212 |
|
213 | return '';
|
214 | }
|
215 |
|
216 | compileStyle(generated, scopeId) {
|
217 | return generated.filter(r => r.type === 'css').reduce((p, r, i) => {
|
218 | let css = r.value;
|
219 | let scoped = this.ast.styles[i].scoped;
|
220 |
|
221 |
|
222 | if (scoped) {
|
223 | let {code, errors} = this.vue.compileStyle({
|
224 | source: css,
|
225 | filename: this.relativeName,
|
226 | id: scopeId,
|
227 | scoped
|
228 | });
|
229 |
|
230 | if (errors.length) {
|
231 | throw errors[0];
|
232 | }
|
233 |
|
234 | css = code;
|
235 | }
|
236 |
|
237 | return p + css;
|
238 | }, '');
|
239 | }
|
240 |
|
241 | compileHMR(generated, optsVar) {
|
242 | if (!this.options.hmr) {
|
243 | return '';
|
244 | }
|
245 |
|
246 | this.addDependency('vue-hot-reload-api');
|
247 | this.addDependency('vue');
|
248 |
|
249 | let cssHMR = '';
|
250 | if (this.ast.styles.length) {
|
251 | cssHMR = `
|
252 | var reloadCSS = require('_css_loader');
|
253 | module.hot.dispose(reloadCSS);
|
254 | module.hot.accept(reloadCSS);
|
255 | `;
|
256 | }
|
257 |
|
258 | let isFunctional = this.ast.template && this.ast.template.attrs.functional;
|
259 |
|
260 | return `
|
261 | /* hot reload */
|
262 | (function () {
|
263 | if (module.hot) {
|
264 | var api = require('vue-hot-reload-api');
|
265 | api.install(require('vue'));
|
266 | if (api.compatible) {
|
267 | module.hot.accept();
|
268 | if (!module.hot.data) {
|
269 | api.createRecord('${optsVar}', ${optsVar});
|
270 | } else {
|
271 | api.${
|
272 | isFunctional ? 'rerender' : 'reload'
|
273 | }('${optsVar}', ${optsVar});
|
274 | }
|
275 | }
|
276 |
|
277 | ${cssHMR}
|
278 | }
|
279 | })();`;
|
280 | }
|
281 | }
|
282 |
|
283 | module.exports = VueAsset;
|