UNPKG

7.31 kBJavaScriptView Raw
1const Asset = require('../Asset');
2const localRequire = require('../utils/localRequire');
3const md5 = require('../utils/md5');
4const {minify} = require('terser');
5const t = require('@babel/types');
6
7class VueAsset extends Asset {
8 constructor(name, options) {
9 super(name, options);
10 this.type = 'js';
11 }
12
13 async parse(code) {
14 // Is being used in component-compiler-utils, errors if not installed...
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, // Used for sourcemaps
25 sourceRoot: '', // Used for sourcemaps. Override so it doesn't use cwd
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 // Generate JS output.
71 let js = this.ast.script ? generated[0].value : '';
72 let supplemental = '';
73
74 // TODO: make it possible to process this code with the normal scope hoister
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 // Process scoped styles if needed.
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
283module.exports = VueAsset;