UNPKG

14.1 kBJavaScriptView Raw
1/**
2 * Tencent is pleased to support the open source community by making WePY available.
3 * Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved.
4 *
5 * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
6 * http://opensource.org/licenses/MIT
7 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
8 */
9const fs = require('fs-extra');
10const path = require('path');
11const chokidar = require('chokidar');
12const ResolverFactory = require('enhanced-resolve').ResolverFactory;
13const node = require('enhanced-resolve/lib/node');
14const NodeJsInputFileSystem = require('enhanced-resolve/lib/NodeJsInputFileSystem');
15const CachedInputFileSystem = require('enhanced-resolve/lib/CachedInputFileSystem');
16const parseOptions = require('./parseOptions');
17const moduleSet = require('./moduleSet');
18const fileDep = require('./fileDep');
19const loader = require('./loader');
20const logger = require('./util/logger');
21const VENDOR_DIR = require('./util/const').VENDOR_DIR;
22const Hook = require('./hook');
23const tag = require('./tag');
24const walk = require('acorn/dist/walk');
25const { isArr } = require('./util/tools');
26
27const initCompiler = require('./init/compiler');
28const initParser = require('./init/parser');
29const initPlugin = require('./init/plugin');
30
31class Compile extends Hook {
32 constructor (opt) {
33 super();
34 let self = this;
35
36 this.version = require('../package.json').version;
37 this.options = opt;
38
39 if (!path.isAbsolute(opt.entry)) {
40 this.options.entry = path.resolve(path.join(opt.src, opt.entry + opt.wpyExt));
41 }
42
43 this.resolvers = {};
44 this.running = false;
45
46 this.context = process.cwd();
47
48 let appConfig = opt.appConfig || {};
49 let userDefinedTags = appConfig.tags || {};
50
51 this.tags = {
52 htmlTags: tag.combineTag(tag.HTML_TAGS, userDefinedTags.htmlTags),
53 wxmlTags: tag.combineTag(tag.WXML_TAGS, userDefinedTags.wxmlTags),
54 html2wxmlMap: tag.combineTagMap(tag.HTML2WXML_MAP, userDefinedTags.html2wxmlMap)
55 };
56
57 this.logger = logger;
58
59 this.inputFileSystem = new CachedInputFileSystem(new NodeJsInputFileSystem(), 60000);
60
61 this.options.resolve.extensions = ['.js', '.ts', '.json', '.node', '.wxs', this.options.wpyExt];
62
63 this.resolvers.normal = ResolverFactory.createResolver(Object.assign({
64 fileSystem: this.inputFileSystem
65 }, this.options.resolve));
66
67 this.resolvers.context = ResolverFactory.createResolver(Object.assign({
68 fileSystem: this.inputFileSystem,
69 resolveToContext: true
70 }, this.options.resolve));
71
72 this.resolvers.normal.resolveSync = node.create.sync(Object.assign({
73 fileSystem: this.inputFileSystem
74 }, this.options.resolve));
75
76 this.resolvers.context.resolveSync = node.create.sync(Object.assign({
77 fileSystem: this.inputFileSystem,
78 resolveToContext: true
79 }, this.options.resolve));
80
81
82 let fnNormalBak = this.resolvers.normal.resolve;
83 this.resolvers.normal.resolve = function (...args) {
84 return new Promise((resolve, reject) => {
85 args.push(function (err, filepath, meta) {
86 if (err) {
87 reject(err);
88 } else {
89 resolve({path: filepath, meta: meta});
90 }
91 });
92 fnNormalBak.apply(self.resolvers.normal, args);
93 });
94 };
95 let fnContextBak = this.resolvers.context.resolve;
96 this.resolvers.context.resolve = function (...args) {
97 return new Promise((resolve, reject) => {
98 args.push(function (err, filepath, meta) {
99 if (err) {
100 reject(err);
101 } else {
102 resolve({path: filepath, meta: meta});
103 }
104 });
105 fnContextBak.apply(self.resolvers.context, args);
106 });
107 };
108
109
110 }
111
112 clear (type) {
113 this.hook('process-clear', type);
114 return this;
115 }
116
117 init () {
118 this.register('process-clear', type => {
119 this.compiled = {};
120 this.involved = {};
121 this.vendors = new moduleSet();
122 this.assets = new moduleSet();
123 this.fileDep = new fileDep();
124 });
125
126 ['output-app', 'output-pages', 'output-components'].forEach(k => {
127 this.register(k, data => {
128 if (!isArr(data))
129 data = [data];
130
131 data.forEach(v => this.output('wpy', v));
132 });
133 });
134
135 this.register('output-vendor', data => {
136 this.output('vendor', data);
137 });
138
139 this.register('output-assets', list => {
140 list.forEach(file => {
141 this.output('assets', file);
142 });
143 });
144
145 this.register('output-static', () => {
146 let paths = this.options.static;
147 let copy = (p) => {
148 let relative = path.relative(path.join(this.context, this.options.src), path.join(this.context, p));
149 return fs.copy(path.join(this.context, p), path.join(this.context, this.options.target, relative[0] === '.' ? p : relative))
150 };
151 if (typeof paths === 'string')
152 return copy(paths);
153 else if (isArr(paths))
154 return Promise.all(paths.map(p => copy(p)))
155 });
156
157 initPlugin(this);
158 initParser(this);
159
160 this.hook('process-clear', 'init');
161
162 return initCompiler(this, this.options.compilers);
163 }
164
165 start () {
166 if (this.running) {
167 return;
168 }
169
170 this.running = true;
171 this.logger.info('build app', 'start...');
172
173 this.hookUnique('wepy-parser-wpy', { path: this.options.entry, type: 'app' }).then(app => {
174
175 let sfc = app.sfc;
176 let script = sfc.script;
177 let styles = sfc.styles;
178 let config = sfc.config;
179
180 let appConfig = config.parsed.output;
181 if (!appConfig.pages || appConfig.pages.length === 0) {
182 appConfig.pages = [];
183 this.hookUnique('error-handler', {
184 type: 'warn',
185 ctx: app,
186 message: `Missing "pages" in App config`
187 });
188 }
189 let pages = appConfig.pages.map(v => {
190 return path.resolve(app.file, '..', v);
191 });
192
193 if (appConfig.subPackages || appConfig.subpackages) {
194 (appConfig.subpackages || appConfig.subPackages).forEach(sub => {
195 sub.pages.forEach(v => {
196 pages.push(path.resolve(app.file, '../'+sub.root || '', v));
197 });
198
199 });
200 }
201
202 let tasks = pages.map(v => {
203 let file;
204
205 file = v + this.options.wpyExt;
206 if (fs.existsSync(file)) {
207 return this.hookUnique('wepy-parser-wpy', { path: file, type: 'page' });
208 }
209 file = v + '.js';
210 if (fs.existsSync(file)) {
211 return this.hookUnique('wepy-parser-component', { path: file, type: 'page', npm: false });
212 }
213 this.hookUnique('error-handler', {
214 type: 'error',
215 ctx: app,
216 message: `Can not resolve page: ${v}`
217 });
218 });
219
220 this.hookSeq('build-app', app);
221 this.hookUnique('output-app', app);
222 return Promise.all(tasks);
223 }).then(this.buildComps.bind(this));
224 }
225
226 buildComps (comps) {
227 function buildComponents (comps) {
228 if (!comps) {
229 return null;
230 }
231 this.hookSeq('build-components', comps);
232 this.hookUnique('output-components', comps);
233
234 let components = [];
235 let originalComponents = [];
236 let tasks = [];
237
238 comps.forEach(comp => {
239 let config = comp.sfc.config || {};
240 let parsed = config.parsed || {};
241 let parsedComponents = parsed.components || [];
242
243 parsedComponents.forEach(com => {
244 if (com.type === 'wepy') { // wepy 组件
245 tasks.push(this.hookUnique('wepy-parser-wpy', com));
246 } else if (com.type === 'weapp') { // 原生组件
247 tasks.push(this.hookUnique('wepy-parser-component', com));
248 }
249 });
250 });
251
252 if (tasks.length) {
253 return Promise.all(tasks).then(buildComponents.bind(this));
254 } else {
255 return Promise.resolve();
256 }
257 }
258
259 return buildComponents.bind(this)(comps).then(() => {
260 let vendorData = this.hookSeq('build-vendor', {});
261 this.hookUnique('output-vendor', vendorData);
262 }).then(() => {
263 let assetsData = this.hookSeq('build-assets');
264 this.hookUnique('output-assets', assetsData);
265 }).then(() => {
266 return this.hookUnique('output-static');
267 }).then(() => {
268 this.hookSeq('process-done');
269 this.running = false;
270 this.logger.info('build', 'finished');
271 if (this.options.watch) {
272 this.logger.info('watching...');
273 this.watch();
274 }
275 }).catch(e => {
276 this.running = false;
277 if (e.message !== 'EXIT') {
278 this.logger.error(e);
279 }
280 if (this.logger.level() !== 'trace') {
281 this.logger.error('compile', 'Compile failed. Add "--log trace" to see more details');
282 } else {
283 this.logger.error('compile', 'Compile failed.');
284 }
285 if (this.options.watch) {
286 this.logger.info('watching...');
287 this.watch();
288 }
289 });
290 }
291
292 partialBuild (files) {
293 if (this.running) {
294 return;
295 }
296 this.running = true;
297 this.logger.info('build wpy files', 'start...');
298
299 // just compile these files of wpyExt
300 const tasks = files.map(file => {
301 if (fs.existsSync(file)) {
302 return this.hookUnique('wepy-parser-wpy', { path: file, type: 'page' });
303 }
304 this.hookUnique('error-handler', {
305 type: 'error',
306 ctx: {},
307 message: `Can not resolve page: ${file}`
308 });
309 });
310
311 Promise.all(tasks).then(this.buildComps.bind(this));
312 }
313
314 watch () {
315 if (this.watchInitialized) {
316 return;
317 }
318 this.watchInitialized = true;
319 let watchOption = Object.assign({ ignoreInitial: true, depth: 99 }, this.options.watchOption || {});
320 let target = path.resolve(this.context, this.options.target);
321
322 if (watchOption.ignore) {
323 let type = Object.prototype.toString.call(watchOption.ignore);
324 if (type === '[object String]' || type === '[object RegExp]') {
325 watchOption.ignored = [watchOption.ignored];
326 watchOption.ignored.push(this.options.target);
327 } else if (type === '[object Array]') {
328 watchOption.ignored.push(this.options.target);
329 }
330 } else {
331 watchOption.ignored = [this.options.target];
332 }
333
334 chokidar.watch([this.options.src], watchOption).on('all', (evt, filepath) => {
335 if (evt === 'change') {
336 let buildTask = {
337 changed: path.resolve(filepath),
338 partial: true,
339 files: []
340 };
341 this.hookAsyncSeq('before-wepy-watch-file-changed', buildTask).then(task => {
342 if (task.partial) {
343 if (task.files.length) {
344 this.partialBuild(task.files);
345 }
346 } else {
347 this.start();
348 }
349 });
350 }
351 });
352 }
353
354 applyCompiler (node, ctx) {
355 ctx.id = this.assets.add(ctx.file);
356
357 if (node.lang) {
358 let hookKey = 'wepy-compiler-' + node.lang;
359
360 if (!this.hasHook(hookKey)) {
361 throw `Missing plugins ${hookKey}`;
362 }
363
364 // If node has src, then do not change involved
365 if (!node.src) {
366 this.involved[ctx.file] = 1;
367 }
368
369 let task;
370
371 // If file is not changed, and compiled cache exsit.
372 // Style may have dependences, maybe the dependences file changed. so ignore the cache for the style who have deps.
373 if (ctx.useCache && node.compiled && (node.compiled.dep || []).length === 0) {
374 task = Promise.resolve(node);
375 } else {
376 task = this.hookUnique(hookKey, node, ctx);
377 }
378 return task.then(node => {
379 return this.hookAsyncSeq('before-wepy-parser-' + node.type, { node, ctx });
380 })
381 .then(({ node, ctx }) => {
382 return this.hookUnique('wepy-parser-' + node.type, node, ctx);
383 });
384 }
385 }
386
387 getTarget (file, targetDir) {
388 let relative = path.relative(path.join(this.context, this.options.src), file);
389 let targetFile = path.join(this.context, targetDir || this.options.target, relative);
390 return targetFile;
391 }
392
393 getModuleTarget (file, targetDir) {
394 let relative = path.relative(this.context, file);
395 let dirs = relative.split(path.sep);
396 dirs.shift(); // shift node_modules
397 relative = dirs.join(path.sep);
398 let targetFile = path.join(this.context, targetDir || this.options.target, VENDOR_DIR, relative);
399 return targetFile;
400 }
401
402 outputFile (filename, code, encoding) {
403 this.hookAsyncSeq('output-file', { filename, code, encoding })
404 .then(({ filename, code, encoding }) => {
405 if (!code) {
406 logger.silly('output', 'empty content: ' + filename);
407 } else {
408 logger.silly('output', 'write file: ' + filename);
409
410 fs.outputFile(filename, code, encoding || 'utf-8', (err) => {
411 if (err) {
412 console.log(err);
413 }
414 });
415 }
416 }).catch(e => {
417 if (e.handler) {
418 this.hookUnique('error-handler', e.handler, e.error, e.pos);
419 } else {
420 // TODO
421 throw e
422 }
423 });
424 }
425
426 output (type, item) {
427 let filename, code, encoding;
428
429 if (type === 'wpy') {
430 const sfc = item.sfc;
431 const outputMap = {
432 script: 'js',
433 styles: 'wxss',
434 config: 'json',
435 template: 'wxml'
436 };
437
438 Object.keys(outputMap).forEach(k => {
439 if (sfc[k] && sfc[k].outputCode) {
440 filename = item.outputFile + '.' + outputMap[k];
441 code = sfc[k].outputCode;
442
443 this.outputFile(filename, code, encoding);
444 }
445 })
446 } else {
447 filename = item.targetFile;
448 code = item.outputCode;
449 encoding = item.encoding;
450
451 this.outputFile(filename, code, encoding);
452 }
453 }
454}
455
456exports = module.exports = (program) => {
457 let opt = parseOptions.convert(program);
458
459 return new Compile(opt);
460};