UNPKG

10.5 kBJavaScriptView Raw
1import { dirname, relative, resolve } from 'path';
2import { Promise } from 'sander';
3import { parse } from 'acorn';
4import MagicString from 'magic-string';
5import analyse from './ast/analyse';
6import { has, keys } from './utils/object';
7import { sequence } from './utils/promise';
8import getLocation from './utils/getLocation';
9import makeLegalIdentifier from './utils/makeLegalIdentifier';
10
11const emptyArrayPromise = Promise.resolve([]);
12
13export 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 ); // remove .js
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 // imports and exports, indexed by ID
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 // import foo from './foo';
53 // import { bar } from './bar';
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 // export default function foo () {}
81 // export default foo;
82 // export default 42;
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 // export { foo, bar, baz }
95 // export var foo = 42;
96 // export function foo () {}
97 else if ( node.type === 'ExportNamedDeclaration' ) {
98 // export { foo } from './foo';
99 source = node.source && node.source.value;
100
101 if ( node.specifiers.length ) {
102 // export { foo, bar, baz }
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 // export var foo = 42
129 name = declaration.declarations[0].id.name;
130 } else {
131 // export function foo () {}
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 // shortcut cycles. TODO this won't work everywhere...
212 if ( has( this.definitionPromises, name ) ) {
213 return emptyArrayPromise;
214 }
215
216 let promise;
217
218 // The definition for this name is in a different module
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 // TODO this seems ropey
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 // we need to create an internal namespace
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 // The definition is in this module
267 else if ( name === 'default' && this.exports.default.isDeclaration ) {
268 // We have something like `export default foo` - so we just start again,
269 // searching for `foo` instead of default
270 promise = this.define( this.exports.default.name );
271 }
272
273 else {
274 let statement;
275
276 if ( name === 'default' ) {
277 // TODO can we use this.definitions[ name ], as below?
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 // We have a statement, and it hasn't been included yet. First, include
301 // the statements it depends on
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 // then include the statement itself
311 .then( () => {
312 result.push( statement );
313 })
314
315 // then include any statements that could modify the
316 // thing(s) this statement defines
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 // the `result` is an array of statements needed to define `name`
335 .then( () => {
336 return result;
337 });
338 }
339
340 expandAllStatements ( isEntryModule ) {
341 let allStatements = [];
342
343 return sequence( this.ast.body, statement => {
344 // skip already-included statements
345 if ( statement._included ) return;
346
347 // skip import declarations
348 if ( statement.type === 'ImportDeclaration' ) {
349 // unless they're empty, in which case assume we're importing them for the side-effects
350 // THIS IS NOT FOOLPROOF. Probably need /*rollup: include */ or similar
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 // skip `export { foo, bar, baz }`
366 if ( statement.type === 'ExportNamedDeclaration' && statement.specifiers.length ) {
367 // but ensure they are defined, if this is the entry module
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 // include everything else
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}