UNPKG

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