1 | import { dirname, relative, resolve } from 'path';
|
2 | import { Promise } from 'sander';
|
3 | import { parse } from 'acorn';
|
4 | import MagicString from 'magic-string';
|
5 | import analyse from './ast/analyse';
|
6 | import { has, keys } from './utils/object';
|
7 | import { sequence } from './utils/promise';
|
8 | import getLocation from './utils/getLocation';
|
9 | import makeLegalIdentifier from './utils/makeLegalIdentifier';
|
10 |
|
11 | const emptyArrayPromise = Promise.resolve([]);
|
12 |
|
13 | export default class Module {
|
14 | constructor ({ path, code, bundle }) {
|
15 | this.bundle = bundle;
|
16 | this.path = path;
|
17 | this.relativePath = relative( bundle.base, path ).slice( 0, -3 );
|
18 |
|
19 | this.code = new MagicString( code, {
|
20 | filename: path
|
21 | });
|
22 |
|
23 | this.suggestedNames = {};
|
24 | this.comments = [];
|
25 |
|
26 | try {
|
27 | this.ast = parse( code, {
|
28 | ecmaVersion: 6,
|
29 | sourceType: 'module',
|
30 | onComment: ( block, text, start, end ) => this.comments.push({ block, text, start, end })
|
31 | });
|
32 | } catch ( err ) {
|
33 | err.file = path;
|
34 | throw err;
|
35 | }
|
36 |
|
37 | this.analyse();
|
38 | }
|
39 |
|
40 | analyse () {
|
41 |
|
42 | this.imports = {};
|
43 | this.exports = {};
|
44 |
|
45 | let commentIndex = 0;
|
46 | let previousNode;
|
47 | let previousEnd = 0;
|
48 |
|
49 | this.ast.body.forEach( node => {
|
50 | let source;
|
51 |
|
52 |
|
53 |
|
54 | if ( node.type === 'ImportDeclaration' ) {
|
55 | source = node.source.value;
|
56 |
|
57 | node.specifiers.forEach( specifier => {
|
58 | const isDefault = specifier.type === 'ImportDefaultSpecifier';
|
59 | const isNamespace = specifier.type === 'ImportNamespaceSpecifier';
|
60 |
|
61 | const localName = specifier.local.name;
|
62 | const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name;
|
63 |
|
64 | if ( has( this.imports, localName ) ) {
|
65 | const err = new Error( `Duplicated import '${localName}'` );
|
66 | err.file = this.path;
|
67 | err.loc = getLocation( this.code.original, specifier.start );
|
68 | throw err;
|
69 | }
|
70 |
|
71 | this.imports[ localName ] = {
|
72 | source,
|
73 | name,
|
74 | localName
|
75 | };
|
76 | });
|
77 | }
|
78 |
|
79 | else if ( /^Export/.test( node.type ) ) {
|
80 |
|
81 |
|
82 |
|
83 | if ( node.type === 'ExportDefaultDeclaration' ) {
|
84 | const isDeclaration = /Declaration$/.test( node.declaration.type );
|
85 |
|
86 | this.exports.default = {
|
87 | node,
|
88 | name: 'default',
|
89 | localName: isDeclaration ? node.declaration.id.name : 'default',
|
90 | isDeclaration
|
91 | };
|
92 | }
|
93 |
|
94 |
|
95 |
|
96 |
|
97 | else if ( node.type === 'ExportNamedDeclaration' ) {
|
98 |
|
99 | source = node.source && node.source.value;
|
100 |
|
101 | if ( node.specifiers.length ) {
|
102 |
|
103 | node.specifiers.forEach( specifier => {
|
104 | const localName = specifier.local.name;
|
105 | const exportedName = specifier.exported.name;
|
106 |
|
107 | this.exports[ exportedName ] = {
|
108 | localName,
|
109 | exportedName
|
110 | };
|
111 |
|
112 | if ( source ) {
|
113 | this.imports[ localName ] = {
|
114 | source,
|
115 | localName,
|
116 | name: exportedName
|
117 | };
|
118 | }
|
119 | });
|
120 | }
|
121 |
|
122 | else {
|
123 | let declaration = node.declaration;
|
124 |
|
125 | let name;
|
126 |
|
127 | if ( declaration.type === 'VariableDeclaration' ) {
|
128 |
|
129 | name = declaration.declarations[0].id.name;
|
130 | } else {
|
131 |
|
132 | name = declaration.id.name;
|
133 | }
|
134 |
|
135 | this.exports[ name ] = {
|
136 | node,
|
137 | localName: name,
|
138 | expression: declaration
|
139 | };
|
140 | }
|
141 | }
|
142 | }
|
143 | });
|
144 |
|
145 |
|
146 |
|
147 | analyse( this.ast, this.code, this );
|
148 |
|
149 | this.definedNames = this.ast._scope.names.slice();
|
150 |
|
151 | this.canonicalNames = {};
|
152 |
|
153 | this.definitions = {};
|
154 | this.definitionPromises = {};
|
155 | this.modifications = {};
|
156 |
|
157 | this.ast.body.forEach( statement => {
|
158 | Object.keys( statement._defines ).forEach( name => {
|
159 | this.definitions[ name ] = statement;
|
160 | });
|
161 |
|
162 | Object.keys( statement._modifies ).forEach( name => {
|
163 | if ( !has( this.modifications, name ) ) {
|
164 | this.modifications[ name ] = [];
|
165 | }
|
166 |
|
167 | this.modifications[ name ].push( statement );
|
168 | });
|
169 | });
|
170 | }
|
171 |
|
172 | getCanonicalName ( localName ) {
|
173 | if ( has( this.suggestedNames, localName ) ) {
|
174 | localName = this.suggestedNames[ localName ];
|
175 | }
|
176 |
|
177 | if ( !has( this.canonicalNames, localName ) ) {
|
178 | let canonicalName;
|
179 |
|
180 | if ( has( this.imports, localName ) ) {
|
181 | const importDeclaration = this.imports[ localName ];
|
182 | const module = importDeclaration.module;
|
183 |
|
184 | if ( importDeclaration.name === '*' ) {
|
185 | canonicalName = module.suggestedNames[ '*' ];
|
186 | } else {
|
187 | let exporterLocalName;
|
188 |
|
189 | if ( module.isExternal ) {
|
190 | exporterLocalName = importDeclaration.name;
|
191 | } else {
|
192 | const exportDeclaration = module.exports[ importDeclaration.name ];
|
193 | exporterLocalName = exportDeclaration.localName;
|
194 | }
|
195 |
|
196 | canonicalName = module.getCanonicalName( exporterLocalName );
|
197 | }
|
198 | }
|
199 |
|
200 | else {
|
201 | canonicalName = localName;
|
202 | }
|
203 |
|
204 | this.canonicalNames[ localName ] = canonicalName;
|
205 | }
|
206 |
|
207 | return this.canonicalNames[ localName ];
|
208 | }
|
209 |
|
210 | define ( name ) {
|
211 |
|
212 | if ( has( this.definitionPromises, name ) ) {
|
213 | return emptyArrayPromise;
|
214 | }
|
215 |
|
216 | let promise;
|
217 |
|
218 |
|
219 | if ( has( this.imports, name ) ) {
|
220 | const importDeclaration = this.imports[ name ];
|
221 |
|
222 | promise = this.bundle.fetchModule( importDeclaration.source, this.path )
|
223 | .then( module => {
|
224 | importDeclaration.module = module;
|
225 |
|
226 | if ( importDeclaration.name === 'default' ) {
|
227 |
|
228 | const localName = importDeclaration.localName;
|
229 | const suggestion = has( this.suggestedNames, localName ) ? this.suggestedNames[ localName ] : localName;
|
230 | module.suggestName( 'default', suggestion );
|
231 | }
|
232 |
|
233 | if ( module.isExternal ) {
|
234 | if ( importDeclaration.name === 'default' ) {
|
235 | module.needsDefault = true;
|
236 | } else {
|
237 | module.needsNamed = true;
|
238 | }
|
239 |
|
240 | module.importedByBundle.push( importDeclaration );
|
241 | return emptyArrayPromise;
|
242 | }
|
243 |
|
244 | if ( importDeclaration.name === '*' ) {
|
245 | module.suggestName( '*', importDeclaration.localName );
|
246 | module.suggestName( 'default', `${importDeclaration.localName}__default` );
|
247 |
|
248 |
|
249 | if ( !~this.bundle.internalNamespaceModules.indexOf( module ) ) {
|
250 | this.bundle.internalNamespaceModules.push( module );
|
251 | }
|
252 |
|
253 | return module.expandAllStatements();
|
254 | }
|
255 |
|
256 | const exportDeclaration = module.exports[ importDeclaration.name ];
|
257 |
|
258 | if ( !exportDeclaration ) {
|
259 | throw new Error( `Module ${module.path} does not export ${importDeclaration.name} (imported by ${this.path})` );
|
260 | }
|
261 |
|
262 | return module.define( exportDeclaration.localName );
|
263 | });
|
264 | }
|
265 |
|
266 |
|
267 | else if ( name === 'default' && this.exports.default.isDeclaration ) {
|
268 |
|
269 |
|
270 | promise = this.define( this.exports.default.name );
|
271 | }
|
272 |
|
273 | else {
|
274 | let statement;
|
275 |
|
276 | if ( name === 'default' ) {
|
277 |
|
278 | statement = this.exports.default.node;
|
279 | }
|
280 |
|
281 | else {
|
282 | statement = this.definitions[ name ];
|
283 | }
|
284 |
|
285 | if ( statement && !statement._included ) {
|
286 | promise = this.expandStatement( statement );
|
287 | }
|
288 | }
|
289 |
|
290 | this.definitionPromises[ name ] = promise || emptyArrayPromise;
|
291 | return this.definitionPromises[ name ];
|
292 | }
|
293 |
|
294 | expandStatement ( statement ) {
|
295 | if ( statement._included ) return emptyArrayPromise;
|
296 | statement._included = true;
|
297 |
|
298 | let result = [];
|
299 |
|
300 |
|
301 |
|
302 | const dependencies = Object.keys( statement._dependsOn );
|
303 |
|
304 | return sequence( dependencies, name => {
|
305 | return this.define( name ).then( definition => {
|
306 | result.push.apply( result, definition );
|
307 | });
|
308 | })
|
309 |
|
310 |
|
311 | .then( () => {
|
312 | result.push( statement );
|
313 | })
|
314 |
|
315 |
|
316 |
|
317 | .then( () => {
|
318 | return sequence( keys( statement._defines ), name => {
|
319 | const modifications = has( this.modifications, name ) && this.modifications[ name ];
|
320 |
|
321 | if ( modifications ) {
|
322 | return sequence( modifications, statement => {
|
323 | if ( !statement._included ) {
|
324 | return this.expandStatement( statement )
|
325 | .then( statements => {
|
326 | result.push.apply( result, statements );
|
327 | });
|
328 | }
|
329 | })
|
330 | }
|
331 | });
|
332 | })
|
333 |
|
334 |
|
335 | .then( () => {
|
336 | return result;
|
337 | });
|
338 | }
|
339 |
|
340 | expandAllStatements ( isEntryModule ) {
|
341 | let allStatements = [];
|
342 |
|
343 | return sequence( this.ast.body, statement => {
|
344 |
|
345 | if ( statement._included ) return;
|
346 |
|
347 |
|
348 | if ( statement.type === 'ImportDeclaration' ) {
|
349 |
|
350 |
|
351 | if ( !statement.specifiers.length ) {
|
352 | return this.bundle.fetchModule( statement.source.value, this.path )
|
353 | .then( module => {
|
354 | statement.module = module;
|
355 | return module.expandAllStatements();
|
356 | })
|
357 | .then( statements => {
|
358 | allStatements.push.apply( allStatements, statements );
|
359 | });
|
360 | }
|
361 |
|
362 | return;
|
363 | }
|
364 |
|
365 |
|
366 | if ( statement.type === 'ExportNamedDeclaration' && statement.specifiers.length ) {
|
367 |
|
368 | if ( isEntryModule ) {
|
369 | return this.expandStatement( statement )
|
370 | .then( statements => {
|
371 | allStatements.push.apply( allStatements, statements );
|
372 | });
|
373 | }
|
374 |
|
375 | return;
|
376 | }
|
377 |
|
378 |
|
379 | return this.expandStatement( statement )
|
380 | .then( statements => {
|
381 | allStatements.push.apply( allStatements, statements );
|
382 | });
|
383 | }).then( () => {
|
384 | return allStatements;
|
385 | });
|
386 | }
|
387 |
|
388 | rename ( name, replacement ) {
|
389 | this.canonicalNames[ name ] = replacement;
|
390 | }
|
391 |
|
392 | suggestName ( exportName, suggestion ) {
|
393 | if ( !this.suggestedNames[ exportName ] ) {
|
394 | this.suggestedNames[ exportName ] = makeLegalIdentifier( suggestion );
|
395 | }
|
396 | }
|
397 | }
|