UNPKG

7.88 kBJavaScriptView Raw
1'use strict';
2
3var path = require('path');
4var acorn = require('acorn');
5var escodegen = require('escodegen');
6var estraverse = require('estraverse');
7var sourceMap = require('source-map');
8var mergeSourceMap = require('merge-source-map');
9var compile = require('./compile');
10var defaults = require('./defaults');
11var runtimePath = require.resolve('./runtime');
12
13var CONSTS = compile.Compiler.CONSTS;
14var LOCAL_MODULE = /^\.+\//;
15
16// 获取默认设置
17var getDefaults = function getDefaults(options) {
18 // new defaults
19 var setting = {
20 imports: runtimePath,
21 bail: true,
22 cache: false,
23 debug: false,
24
25 sourceMap: false,
26 sourceRoot: options.sourceRoot
27 };
28
29 for (var name in options) {
30 setting[name] = options[name];
31 }
32
33 return defaults.$extend(setting);
34};
35
36// 转换外部模板文件引入语句的 filename 参数节点
37// 所有绝对路径都转换成相对路径
38var convertFilenameNode = function convertFilenameNode(node, options) {
39 if (node.type === 'Literal') {
40 var resolvePath = options.resolveFilename(node.value, options);
41 var dirname = path.dirname(options.filename);
42 var relativePath = path.relative(dirname, resolvePath);
43
44 if (LOCAL_MODULE.test(relativePath)) {
45 node.value = relativePath;
46 } else {
47 node.value = './' + relativePath;
48 }
49
50 delete node.raw;
51 }
52
53 return node;
54};
55
56// 获取原始渲染函数的 sourceMap
57var getOldSourceMap = function getOldSourceMap(mappings, _ref) {
58 var sourceRoot = _ref.sourceRoot,
59 source = _ref.source,
60 file = _ref.file;
61
62 var oldSourceMap = new sourceMap.SourceMapGenerator({
63 file: file,
64 sourceRoot: sourceRoot
65 });
66
67 mappings.forEach(function (mapping) {
68 mapping.source = source;
69 oldSourceMap.addMapping(mapping);
70 });
71
72 return oldSourceMap.toJSON();
73};
74
75/**
76 * 预编译模版,将模板编译成 javascript 代码
77 * 使用静态分析,将模板内部之间依赖转换成 `require()`
78 * @param {Object} options 编译选项
79 * @return {Object}
80 */
81var precompile = function precompile() {
82 var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
83
84 if (typeof options.filename !== 'string') {
85 throw Error('template.precompile(): "options.filename" required');
86 }
87
88 options = getDefaults(options);
89
90 var code = null;
91 var sourceMap = null;
92 var ast = null;
93 var imports = options.imports;
94 var functions = [CONSTS.INCLUDE, CONSTS.EXTEND];
95
96 if (typeof imports !== 'string') {
97 throw Error('template.precompile(): "options.imports" is a file. Example:\n' + 'options: { imports: require.resolve("art-template/lib/runtime") }\n');
98 } else {
99 options.imports = require(imports);
100 }
101
102 var isLocalModule = LOCAL_MODULE.test(imports);
103 var tplImportsPath = isLocalModule ? imports : path.relative(path.dirname(options.filename), imports);
104 var fn = compile(options);
105
106 code = '(' + fn.toString() + ')';
107 ast = acorn.parse(code, {
108 locations: options.sourceMap
109 });
110
111 var extendNode = null;
112 var enter = function enter(node) {
113 if (node.type === 'VariableDeclarator' && functions.indexOf(node.id.name) !== -1) {
114 // TODO 对变量覆盖进行抛错
115 if (node.id.name === CONSTS.INCLUDE) {
116 node['init'] = {
117 type: 'FunctionExpression',
118 params: [{
119 type: 'Identifier',
120 name: 'content'
121 }],
122 body: {
123 type: 'BlockStatement',
124 body: [{
125 type: 'ExpressionStatement',
126 expression: {
127 type: 'AssignmentExpression',
128 operator: '+=',
129 left: {
130 type: 'Identifier',
131 name: CONSTS.OUT
132 },
133 right: {
134 type: 'Identifier',
135 name: 'content'
136 }
137 }
138 }, {
139 type: 'ReturnStatement',
140 argument: {
141 type: 'Identifier',
142 name: CONSTS.OUT
143 }
144 }]
145 }
146 };
147 return node;
148 } else {
149 this.remove();
150 }
151 } else if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && functions.indexOf(node.callee.name) !== -1) {
152 var replaceNode = void 0;
153 switch (node.callee.name) {
154 case CONSTS.EXTEND:
155 extendNode = convertFilenameNode(node.arguments[0], options);
156 replaceNode = {
157 type: 'AssignmentExpression',
158 operator: '=',
159 left: {
160 type: 'Identifier',
161 name: CONSTS.FROM
162 },
163 right: {
164 type: 'Literal',
165 value: true
166 }
167 };
168
169 break;
170
171 case CONSTS.INCLUDE:
172 var filename = node.arguments.shift();
173 var filenameNode = filename.name === CONSTS.FROM ? extendNode : convertFilenameNode(filename, options);
174 var paramNodes = node.arguments.length ? node.arguments : [{
175 type: 'Identifier',
176 name: CONSTS.DATA
177 }];
178
179 replaceNode = node;
180 replaceNode['arguments'] = [{
181 type: 'CallExpression',
182 callee: {
183 type: 'CallExpression',
184 callee: {
185 type: 'Identifier',
186 name: 'require'
187 },
188 arguments: [filenameNode]
189 },
190 arguments: paramNodes
191 }];
192
193 break;
194 }
195
196 return replaceNode;
197 }
198 };
199
200 ast = estraverse.replace(ast, {
201 enter: enter
202 });
203
204 if (options.sourceMap) {
205 var sourceRoot = options.sourceRoot;
206 var source = path.relative(sourceRoot, options.filename);
207 var file = path.basename(source);
208 var gen = escodegen.generate(ast, {
209 sourceMap: source,
210 file: file,
211 sourceMapRoot: sourceRoot,
212 sourceMapWithCode: true
213 });
214 code = gen.code;
215
216 var newSourceMap = gen.map.toJSON();
217 var oldSourceMap = getOldSourceMap(fn.mappings, {
218 sourceRoot: sourceRoot,
219 source: source,
220 file: file
221 });
222 sourceMap = mergeSourceMap(oldSourceMap, newSourceMap);
223 sourceMap.file = file;
224 sourceMap.sourcesContent = fn.sourcesContent;
225 } else {
226 code = escodegen.generate(ast);
227 }
228
229 code = code.replace(/^\(|\)[;\s]*?$/g, '');
230 code = 'var ' + CONSTS.IMPORTS + ' = require(' + JSON.stringify(tplImportsPath) + ');\n' + 'module.exports = ' + code + ';';
231
232 return {
233 code: code,
234 ast: ast,
235 sourceMap: sourceMap,
236 toString: function toString() {
237 return code;
238 }
239 };
240};
241
242module.exports = precompile;
\No newline at end of file