UNPKG

12.9 kBJavaScriptView Raw
1import { Promise } from 'sander';
2import { parse } from 'acorn';
3import MagicString from 'magic-string';
4import Statement from './Statement';
5import walk from './ast/walk';
6import analyse from './ast/analyse';
7import { blank, keys } from './utils/object';
8import { sequence } from './utils/promise';
9import { isImportDeclaration, isExportDeclaration } from './utils/map-helpers';
10import getLocation from './utils/getLocation';
11import makeLegalIdentifier from './utils/makeLegalIdentifier';
12
13const emptyArrayPromise = Promise.resolve([]);
14
15export default class Module {
16 constructor ({ path, source, bundle }) {
17 this.source = source;
18
19 this.bundle = bundle;
20 this.path = path;
21
22 this.magicString = new MagicString( source, {
23 filename: path
24 });
25
26 this.suggestedNames = blank();
27 this.comments = [];
28
29 // Try to extract a list of top-level statements/declarations. If
30 // the parse fails, attach file info and abort
31 try {
32 const ast = parse( source, {
33 ecmaVersion: 6,
34 sourceType: 'module',
35 onComment: ( block, text, start, end ) => this.comments.push({ block, text, start, end })
36 });
37
38 walk( ast, {
39 enter: node => {
40 this.magicString.addSourcemapLocation( node.start );
41 this.magicString.addSourcemapLocation( node.end );
42 }
43 });
44
45 this.statements = ast.body.map( ( node, i ) => {
46 const magicString = this.magicString.snip( node.start, node.end ).trim();
47 return new Statement( node, magicString, this, i );
48 });
49 } catch ( err ) {
50 err.code = 'PARSE_ERROR';
51 err.file = path;
52 throw err;
53 }
54
55 this.importDeclarations = this.statements.filter( isImportDeclaration );
56 this.exportDeclarations = this.statements.filter( isExportDeclaration );
57
58 this.analyse();
59 }
60
61 analyse () {
62 // imports and exports, indexed by ID
63 this.imports = blank();
64 this.exports = blank();
65
66 this.importDeclarations.forEach( statement => {
67 const node = statement.node;
68 const source = node.source.value;
69
70 node.specifiers.forEach( specifier => {
71 const isDefault = specifier.type === 'ImportDefaultSpecifier';
72 const isNamespace = specifier.type === 'ImportNamespaceSpecifier';
73
74 const localName = specifier.local.name;
75 const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name;
76
77 if ( this.imports[ localName ] ) {
78 const err = new Error( `Duplicated import '${localName}'` );
79 err.file = this.path;
80 err.loc = getLocation( this.source, specifier.start );
81 throw err;
82 }
83
84 this.imports[ localName ] = {
85 source,
86 name,
87 localName
88 };
89 });
90 });
91
92 this.exportDeclarations.forEach( statement => {
93 const node = statement.node;
94 const source = node.source && node.source.value;
95
96 // export default function foo () {}
97 // export default foo;
98 // export default 42;
99 if ( node.type === 'ExportDefaultDeclaration' ) {
100 const isDeclaration = /Declaration$/.test( node.declaration.type );
101 const declaredName = isDeclaration && node.declaration.id.name;
102 const identifier = node.declaration.type === 'Identifier' && node.declaration.name;
103
104 this.exports.default = {
105 statement,
106 name: 'default',
107 localName: declaredName || 'default',
108 declaredName,
109 identifier,
110 isDeclaration,
111 isModified: false // in case of `export default foo; foo = somethingElse`
112 };
113 }
114
115 // export { foo, bar, baz }
116 // export var foo = 42;
117 // export function foo () {}
118 else if ( node.type === 'ExportNamedDeclaration' ) {
119 if ( node.specifiers.length ) {
120 // export { foo, bar, baz }
121 node.specifiers.forEach( specifier => {
122 const localName = specifier.local.name;
123 const exportedName = specifier.exported.name;
124
125 this.exports[ exportedName ] = {
126 localName,
127 exportedName
128 };
129
130 // export { foo } from './foo';
131 if ( source ) {
132 this.imports[ localName ] = {
133 source,
134 localName,
135 name: localName
136 };
137 }
138 });
139 }
140
141 else {
142 let declaration = node.declaration;
143
144 let name;
145
146 if ( declaration.type === 'VariableDeclaration' ) {
147 // export var foo = 42
148 name = declaration.declarations[0].id.name;
149 } else {
150 // export function foo () {}
151 name = declaration.id.name;
152 }
153
154 this.exports[ name ] = {
155 statement,
156 localName: name,
157 expression: declaration
158 };
159 }
160 }
161 });
162
163 analyse( this.magicString, this );
164
165 this.canonicalNames = blank();
166
167 this.definitions = blank();
168 this.definitionPromises = blank();
169 this.modifications = blank();
170
171 this.statements.forEach( statement => {
172 keys( statement.defines ).forEach( name => {
173 this.definitions[ name ] = statement;
174 });
175
176 keys( statement.modifies ).forEach( name => {
177 if ( !this.modifications[ name ] ) {
178 this.modifications[ name ] = [];
179 }
180
181 this.modifications[ name ].push( statement );
182 });
183 });
184
185 this.statements.forEach( statement => {
186 keys( statement.dependsOn ).forEach( name => {
187 if ( !this.definitions[ name ] && !this.imports[ name ] ) {
188 this.bundle.assumedGlobals[ name ] = true;
189 }
190 });
191 });
192 }
193
194 findDeclaration ( localName ) {
195 const importDeclaration = this.imports[ localName ];
196
197 // name was defined by another module
198 if ( importDeclaration ) {
199 const module = importDeclaration.module;
200
201 if ( module.isExternal ) return null;
202
203 const exportDeclaration = module.exports[ importDeclaration.name ];
204 return module.findDeclaration( exportDeclaration.localName );
205 }
206
207 // name was defined by this module, if any
208 let i = this.statements.length;
209 while ( i-- ) {
210 const statement = this.statements[i];
211 const declaration = this.statements[i].scope.declarations[ localName ];
212 if ( declaration ) {
213 return declaration;
214 }
215 }
216
217 return null;
218 }
219
220 getCanonicalName ( localName ) {
221 // Special case
222 if ( localName === 'default' && this.exports.default && this.exports.default.isModified ) {
223 let canonicalName = makeLegalIdentifier( this.path.replace( this.bundle.base + '/', '' ).replace( /\.js$/, '' ) );
224 while ( this.definitions[ canonicalName ] ) {
225 canonicalName = `_${canonicalName}`;
226 }
227
228 return canonicalName;
229 }
230
231 if ( this.suggestedNames[ localName ] ) {
232 localName = this.suggestedNames[ localName ];
233 }
234
235 if ( !this.canonicalNames[ localName ] ) {
236 let canonicalName;
237
238 if ( this.imports[ localName ] ) {
239 const importDeclaration = this.imports[ localName ];
240 const module = importDeclaration.module;
241
242 if ( importDeclaration.name === '*' ) {
243 canonicalName = module.suggestedNames[ '*' ];
244 } else {
245 let exporterLocalName;
246
247 if ( module.isExternal ) {
248 exporterLocalName = importDeclaration.name;
249 } else {
250 const exportDeclaration = module.exports[ importDeclaration.name ];
251 exporterLocalName = exportDeclaration.localName;
252 }
253
254 canonicalName = module.getCanonicalName( exporterLocalName );
255 }
256 }
257
258 else {
259 canonicalName = localName;
260 }
261
262 this.canonicalNames[ localName ] = canonicalName;
263 }
264
265 return this.canonicalNames[ localName ];
266 }
267
268 define ( name ) {
269 // shortcut cycles. TODO this won't work everywhere...
270 if ( this.definitionPromises[ name ] ) {
271 return emptyArrayPromise;
272 }
273
274 let promise;
275
276 // The definition for this name is in a different module
277 if ( this.imports[ name ] ) {
278 const importDeclaration = this.imports[ name ];
279
280 promise = this.bundle.fetchModule( importDeclaration.source, this.path )
281 .then( module => {
282 importDeclaration.module = module;
283
284 // suggest names. TODO should this apply to non default/* imports?
285 if ( importDeclaration.name === 'default' ) {
286 // TODO this seems ropey
287 const localName = importDeclaration.localName;
288 let suggestion = this.suggestedNames[ localName ] || localName;
289
290 // special case - the module has its own import by this name
291 while ( !module.isExternal && module.imports[ suggestion ] ) {
292 suggestion = `_${suggestion}`;
293 }
294
295 module.suggestName( 'default', suggestion );
296 } else if ( importDeclaration.name === '*' ) {
297 const localName = importDeclaration.localName;
298 const suggestion = this.suggestedNames[ localName ] || localName;
299 module.suggestName( '*', suggestion );
300 module.suggestName( 'default', `${suggestion}__default` );
301 }
302
303 if ( module.isExternal ) {
304 if ( importDeclaration.name === 'default' ) {
305 module.needsDefault = true;
306 } else {
307 module.needsNamed = true;
308 }
309
310 module.importedByBundle.push( importDeclaration );
311 return emptyArrayPromise;
312 }
313
314 if ( importDeclaration.name === '*' ) {
315 // we need to create an internal namespace
316 if ( !~this.bundle.internalNamespaceModules.indexOf( module ) ) {
317 this.bundle.internalNamespaceModules.push( module );
318 }
319
320 return module.expandAllStatements();
321 }
322
323 const exportDeclaration = module.exports[ importDeclaration.name ];
324
325 if ( !exportDeclaration ) {
326 throw new Error( `Module ${module.path} does not export ${importDeclaration.name} (imported by ${this.path})` );
327 }
328
329 return module.define( exportDeclaration.localName );
330 });
331 }
332
333 // The definition is in this module
334 else if ( name === 'default' && this.exports.default.isDeclaration ) {
335 // We have something like `export default foo` - so we just start again,
336 // searching for `foo` instead of default
337 promise = this.define( this.exports.default.name );
338 }
339
340 else {
341 let statement;
342
343 statement = name === 'default' ? this.exports.default.statement : this.definitions[ name ];
344 promise = statement && !statement.isIncluded ? statement.expand() : emptyArrayPromise;
345
346 // Special case - `export default foo; foo += 1` - need to be
347 // vigilant about maintaining the correct order of the export
348 // declaration. Otherwise, the export declaration will always
349 // go at the end of the expansion, because the expansion of
350 // `foo` will include statements *after* the declaration
351 if ( name === 'default' && this.exports.default.identifier && this.exports.default.isModified ) {
352 const defaultExportStatement = this.exports.default.statement;
353 promise = promise.then( statements => {
354 // remove the default export statement...
355 // TODO could this be statements.pop()?
356 statements.splice( statements.indexOf( defaultExportStatement ), 1 );
357
358 const len = statements.length;
359 let i;
360 let inserted = false;
361
362 for ( i = 0; i < len; i += 1 ) {
363 if ( statements[i].module === this && statements[i].index > defaultExportStatement.index ) {
364 statements.splice( i, 0, defaultExportStatement );
365 inserted = true;
366 break;
367 }
368 }
369
370 if ( !inserted ) statements.push( statement );
371 return statements;
372 });
373 }
374 }
375
376 this.definitionPromises[ name ] = promise || emptyArrayPromise;
377 return this.definitionPromises[ name ];
378 }
379
380 expandAllStatements ( isEntryModule ) {
381 let allStatements = [];
382
383 return sequence( this.statements, statement => {
384 // A statement may have already been included, in which case we need to
385 // curb rollup's enthusiasm and move it down here. It remains to be seen
386 // if this approach is bulletproof
387 if ( statement.isIncluded ) {
388 const index = allStatements.indexOf( statement );
389 if ( ~index ) {
390 allStatements.splice( index, 1 );
391 allStatements.push( statement );
392 }
393
394 return;
395 }
396
397 // skip import declarations...
398 if ( statement.isImportDeclaration ) {
399 // ...unless they're empty, in which case assume we're importing them for the side-effects
400 // THIS IS NOT FOOLPROOF. Probably need /*rollup: include */ or similar
401 if ( !statement.node.specifiers.length ) {
402 return this.bundle.fetchModule( statement.node.source.value, this.path )
403 .then( module => {
404 statement.module = module;
405 return module.expandAllStatements();
406 })
407 .then( statements => {
408 allStatements.push.apply( allStatements, statements );
409 });
410 }
411
412 return;
413 }
414
415 // skip `export { foo, bar, baz }`...
416 if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.specifiers.length ) {
417 // ...but ensure they are defined, if this is the entry module
418 if ( isEntryModule ) {
419 return statement.expand().then( statements => {
420 allStatements.push.apply( allStatements, statements );
421 });
422 }
423
424 return;
425 }
426
427 // include everything else
428 return statement.expand().then( statements => {
429 allStatements.push.apply( allStatements, statements );
430 });
431 }).then( () => {
432 return allStatements;
433 });
434 }
435
436 rename ( name, replacement ) {
437 this.canonicalNames[ name ] = replacement;
438 }
439
440 suggestName ( exportName, suggestion ) {
441 if ( !this.suggestedNames[ exportName ] ) {
442 this.suggestedNames[ exportName ] = makeLegalIdentifier( suggestion );
443 }
444 }
445}