UNPKG

8.12 kBJavaScriptView Raw
1import { basename, dirname, extname, resolve } from 'path';
2import { readFile, Promise } from 'sander';
3import MagicString from 'magic-string';
4import { keys, has } from './utils/object';
5import { sequence } from './utils/promise';
6import Module from './Module';
7import ExternalModule from './ExternalModule';
8import finalisers from './finalisers/index';
9import replaceIdentifiers from './utils/replaceIdentifiers';
10import makeLegalIdentifier from './utils/makeLegalIdentifier';
11import { defaultResolver } from './utils/resolvePath';
12
13function badExports ( option, keys ) {
14 throw new Error( `'${option}' was specified for options.exports, but entry module has following exports: ${keys.join(', ')}` );
15}
16
17export default class Bundle {
18 constructor ( options ) {
19 this.entryPath = resolve( options.entry ).replace( /\.js$/, '' ) + '.js';
20 this.base = dirname( this.entryPath );
21
22 this.resolvePath = options.resolvePath || defaultResolver;
23
24 this.entryModule = null;
25 this.modulePromises = {};
26 this.statements = [];
27 this.externalModules = [];
28 this.defaultExportName = null;
29 this.internalNamespaceModules = [];
30 }
31
32 fetchModule ( importee, importer ) {
33 return Promise.resolve( importer === null ? importee : this.resolvePath( importee, importer ) )
34 .then( path => {
35 if ( !path ) {
36 // external module
37 if ( !has( this.modulePromises, importee ) ) {
38 const module = new ExternalModule( importee );
39 this.externalModules.push( module );
40 this.modulePromises[ importee ] = Promise.resolve( module );
41 }
42
43 return this.modulePromises[ importee ];
44 }
45
46 if ( !has( this.modulePromises, path ) ) {
47 this.modulePromises[ path ] = readFile( path, { encoding: 'utf-8' })
48 .then( code => {
49 const module = new Module({
50 path,
51 code,
52 bundle: this
53 });
54
55 return module;
56 });
57 }
58
59 return this.modulePromises[ path ];
60 });
61 }
62
63 build () {
64 // bring in top-level AST nodes from the entry module
65 return this.fetchModule( this.entryPath, null )
66 .then( entryModule => {
67 this.entryModule = entryModule;
68
69 if ( entryModule.exports.default ) {
70 let defaultExportName = makeLegalIdentifier( basename( this.entryPath ).slice( 0, -extname( this.entryPath ).length ) );
71 while ( entryModule.ast._scope.contains( defaultExportName ) ) {
72 defaultExportName = `_${defaultExportName}`;
73 }
74
75 entryModule.suggestName( 'default', defaultExportName );
76 }
77
78 return entryModule.expandAllStatements( true );
79 })
80 .then( statements => {
81 this.statements = statements;
82 this.deconflict();
83 });
84
85 }
86
87 deconflict () {
88 let definers = {};
89 let conflicts = {};
90
91 // Discover conflicts (i.e. two statements in separate modules both define `foo`)
92 this.statements.forEach( statement => {
93 keys( statement._defines ).forEach( name => {
94 if ( has( definers, name ) ) {
95 conflicts[ name ] = true;
96 } else {
97 definers[ name ] = [];
98 }
99
100 // TODO in good js, there shouldn't be duplicate definitions
101 // per module... but some people write bad js
102 definers[ name ].push( statement._module );
103 });
104 });
105
106 // Assign names to external modules
107 this.externalModules.forEach( module => {
108 let name = makeLegalIdentifier( module.id );
109
110 while ( has( definers, name ) ) {
111 name = `_${name}`;
112 }
113
114 module.name = name;
115 });
116
117 // Rename conflicting identifiers so they can live in the same scope
118 keys( conflicts ).forEach( name => {
119 const modules = definers[ name ];
120
121 modules.pop(); // the module closest to the entryModule gets away with keeping things as they are
122
123 modules.forEach( module => {
124 module.rename( name, name + '$' + ~~( Math.random() * 100000 ) ); // TODO proper deconfliction mechanism
125 });
126 });
127 }
128
129 generate ( options = {} ) {
130 let magicString = new MagicString.Bundle({ separator: '' });
131
132 // Determine export mode - 'default', 'named', 'none'
133 let exportMode = this.getExportMode( options.exports );
134
135 let previousMargin = 0;
136
137 // Apply new names and add to the output bundle
138 this.statements.forEach( statement => {
139 let replacements = {};
140
141 keys( statement._dependsOn )
142 .concat( keys( statement._defines ) )
143 .forEach( name => {
144 const canonicalName = statement._module.getCanonicalName( name );
145
146 if ( name !== canonicalName ) {
147 replacements[ name ] = canonicalName;
148 }
149 });
150
151 const source = statement._source.clone().trim();
152
153 // modify exports as necessary
154 if ( /^Export/.test( statement.type ) ) {
155 // skip `export { foo, bar, baz }`
156 if ( statement.type === 'ExportNamedDeclaration' && statement.specifiers.length ) {
157 return;
158 }
159
160 // remove `export` from `export var foo = 42`
161 if ( statement.type === 'ExportNamedDeclaration' && statement.declaration.type === 'VariableDeclaration' ) {
162 source.remove( statement.start, statement.declaration.start );
163 }
164
165 // remove `export` from `export class Foo {...}` or `export default Foo`
166 // TODO default exports need different treatment
167 else if ( statement.declaration.id ) {
168 source.remove( statement.start, statement.declaration.start );
169 }
170
171 // declare variables for expressions
172 else {
173 const name = statement.type === 'ExportDefaultDeclaration' ? 'default' : 'TODO';
174 const canonicalName = statement._module.getCanonicalName( name );
175 source.overwrite( statement.start, statement.declaration.start, `var ${canonicalName} = ` );
176 }
177 }
178
179 replaceIdentifiers( statement, source, replacements );
180
181 // add leading comments
182 if ( statement._leadingComments.length ) {
183 const commentBlock = statement._leadingComments.map( comment => {
184 return comment.block ?
185 `/*${comment.text}*/` :
186 `//${comment.text}`;
187 }).join( '\n' );
188
189 magicString.addSource( new MagicString( commentBlock ) );
190 }
191
192 // add margin
193 const margin = Math.max( statement._margin[0], previousMargin );
194 const newLines = new Array( margin ).join( '\n' );
195
196 // add the statement itself
197 magicString.addSource({
198 content: source,
199 separator: newLines
200 });
201
202 // add trailing comments
203 const comment = statement._trailingComment;
204 if ( comment ) {
205 const commentBlock = comment.block ?
206 ` /*${comment.text}*/` :
207 ` //${comment.text}`;
208
209 magicString.append( commentBlock );
210 }
211
212 previousMargin = statement._margin[1];
213 });
214
215 // prepend bundle with internal namespaces
216 const indentString = magicString.getIndentString();
217 const namespaceBlock = this.internalNamespaceModules.map( module => {
218 const exportKeys = keys( module.exports );
219
220 return `var ${module.getCanonicalName('*')} = {\n` +
221 exportKeys.map( key => `${indentString}get ${key} () { return ${module.getCanonicalName(key)}; }` ).join( ',\n' ) +
222 `\n};\n\n`;
223 }).join( '' );
224
225 magicString.prepend( namespaceBlock );
226
227 const finalise = finalisers[ options.format || 'es6' ];
228
229 if ( !finalise ) {
230 throw new Error( `You must specify an output type - valid options are ${keys( finalisers ).join( ', ' )}` );
231 }
232
233 magicString = finalise( this, magicString.trim(), exportMode, options );
234
235 return {
236 code: magicString.toString(),
237 map: magicString.generateMap({
238 includeContent: true,
239 file: options.dest
240 // TODO
241 })
242 };
243 }
244
245 getExportMode ( exportMode ) {
246 const exportKeys = keys( this.entryModule.exports );
247
248 if ( exportMode === 'default' ) {
249 if ( exportKeys.length !== 1 || exportKeys[0] !== 'default' ) {
250 badExports( 'default', exportKeys );
251 }
252 } else if ( exportMode === 'none' && exportKeys.length ) {
253 badExports( 'none', exportKeys );
254 }
255
256 if ( !exportMode || exportMode === 'auto' ) {
257 if ( exportKeys.length === 0 ) {
258 exportMode = 'none';
259 } else if ( exportKeys.length === 1 && exportKeys[0] === 'default' ) {
260 exportMode = 'default';
261 } else {
262 exportMode = 'named';
263 }
264 }
265
266 if ( !/(?:default|named|none)/.test( exportMode ) ) {
267 throw new Error( `options.exports must be 'default', 'named', 'none', 'auto', or left unspecified (defaults to 'auto')` );
268 }
269
270 return exportMode;
271 }
272}