1 | import { basename, dirname, extname, resolve } from 'path';
|
2 | import { readFile, Promise } from 'sander';
|
3 | import MagicString from 'magic-string';
|
4 | import { keys, has } from './utils/object';
|
5 | import { sequence } from './utils/promise';
|
6 | import Module from './Module';
|
7 | import ExternalModule from './ExternalModule';
|
8 | import finalisers from './finalisers/index';
|
9 | import replaceIdentifiers from './utils/replaceIdentifiers';
|
10 | import makeLegalIdentifier from './utils/makeLegalIdentifier';
|
11 | import { defaultResolver } from './utils/resolvePath';
|
12 |
|
13 | function badExports ( option, keys ) {
|
14 | throw new Error( `'${option}' was specified for options.exports, but entry module has following exports: ${keys.join(', ')}` );
|
15 | }
|
16 |
|
17 | export 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 |
|
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 |
|
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 |
|
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 |
|
101 |
|
102 | definers[ name ].push( statement._module );
|
103 | });
|
104 | });
|
105 |
|
106 |
|
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 |
|
118 | keys( conflicts ).forEach( name => {
|
119 | const modules = definers[ name ];
|
120 |
|
121 | modules.pop();
|
122 |
|
123 | modules.forEach( module => {
|
124 | module.rename( name, name + '$' + ~~( Math.random() * 100000 ) );
|
125 | });
|
126 | });
|
127 | }
|
128 |
|
129 | generate ( options = {} ) {
|
130 | let magicString = new MagicString.Bundle({ separator: '' });
|
131 |
|
132 |
|
133 | let exportMode = this.getExportMode( options.exports );
|
134 |
|
135 | let previousMargin = 0;
|
136 |
|
137 |
|
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 |
|
154 | if ( /^Export/.test( statement.type ) ) {
|
155 |
|
156 | if ( statement.type === 'ExportNamedDeclaration' && statement.specifiers.length ) {
|
157 | return;
|
158 | }
|
159 |
|
160 |
|
161 | if ( statement.type === 'ExportNamedDeclaration' && statement.declaration.type === 'VariableDeclaration' ) {
|
162 | source.remove( statement.start, statement.declaration.start );
|
163 | }
|
164 |
|
165 |
|
166 |
|
167 | else if ( statement.declaration.id ) {
|
168 | source.remove( statement.start, statement.declaration.start );
|
169 | }
|
170 |
|
171 |
|
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 |
|
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 |
|
193 | const margin = Math.max( statement._margin[0], previousMargin );
|
194 | const newLines = new Array( margin ).join( '\n' );
|
195 |
|
196 |
|
197 | magicString.addSource({
|
198 | content: source,
|
199 | separator: newLines
|
200 | });
|
201 |
|
202 |
|
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 |
|
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 |
|
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 | }
|