UNPKG

14.3 kBJavaScriptView Raw
1import { walk } from 'estree-walker';
2import MagicString from 'magic-string';
3import { attachScopes, makeLegalIdentifier } from 'rollup-pluginutils';
4import { extractNames, flatten, isReference, isTruthy, isFalsy } from './ast-utils.js';
5import { PREFIX, HELPERS_ID } from './helpers.js';
6import { getName } from './utils.js';
7
8const reserved = 'abstract arguments boolean break byte case catch char class const continue debugger default delete do double else enum eval export extends false final finally float for function goto if implements import in instanceof int interface let long native new null package private protected public return short static super switch synchronized this throw throws transient true try typeof var void volatile while with yield'.split( ' ' );
9const blacklist = { __esModule: true };
10reserved.forEach( word => blacklist[ word ] = true );
11
12const exportsPattern = /^(?:module\.)?exports(?:\.([a-zA-Z_$][a-zA-Z_$0-9]*))?$/;
13
14const firstpassGlobal = /\b(?:require|module|exports|global)\b/;
15const firstpassNoGlobal = /\b(?:require|module|exports)\b/;
16const importExportDeclaration = /^(?:Import|Export(?:Named|Default))Declaration/;
17const functionType = /^(?:FunctionDeclaration|FunctionExpression|ArrowFunctionExpression)$/;
18
19function deconflict ( scope, globals, identifier ) {
20 let i = 1;
21 let deconflicted = identifier;
22
23 while ( scope.contains( deconflicted ) || globals.has( deconflicted ) || deconflicted in blacklist ) deconflicted = `${identifier}_${i++}`;
24 scope.declarations[ deconflicted ] = true;
25
26 return deconflicted;
27}
28
29function tryParse ( parse, code, id ) {
30 try {
31 return parse( code, { allowReturnOutsideFunction: true });
32 } catch ( err ) {
33 err.message += ` in ${id}`;
34 throw err;
35 }
36}
37
38export function checkFirstpass (code, ignoreGlobal) {
39 const firstpass = ignoreGlobal ? firstpassNoGlobal : firstpassGlobal;
40 return firstpass.test(code);
41}
42
43export function checkEsModule ( parse, code, id ) {
44 const ast = tryParse( parse, code, id );
45
46 // if there are top-level import/export declarations, this is ES not CommonJS
47 let hasDefaultExport = false;
48 let isEsModule = false;
49 for ( const node of ast.body ) {
50 if ( node.type === 'ExportDefaultDeclaration' )
51 hasDefaultExport = true;
52 if ( importExportDeclaration.test( node.type ) )
53 isEsModule = true;
54 }
55
56 return { isEsModule, hasDefaultExport, ast };
57}
58
59export function transformCommonjs ( parse, code, id, isEntry, ignoreGlobal, ignoreRequire, customNamedExports, sourceMap, allowDynamicRequire, astCache ) {
60 const ast = astCache || tryParse( parse, code, id );
61
62 const magicString = new MagicString( code );
63
64 const required = {};
65 // Because objects have no guaranteed ordering, yet we need it,
66 // we need to keep track of the order in a array
67 const sources = [];
68
69 let uid = 0;
70
71 let scope = attachScopes( ast, 'scope' );
72 const uses = { module: false, exports: false, global: false, require: false };
73
74 let lexicalDepth = 0;
75 let programDepth = 0;
76
77 const globals = new Set();
78
79 const HELPERS_NAME = deconflict( scope, globals, 'commonjsHelpers' ); // TODO technically wrong since globals isn't populated yet, but ¯\_(ツ)_/¯
80
81 const namedExports = {};
82
83 // TODO handle transpiled modules
84 let shouldWrap = /__esModule/.test( code );
85
86 function isRequireStatement ( node ) {
87 if ( !node ) return;
88 if ( node.type !== 'CallExpression' ) return;
89 if ( node.callee.name !== 'require' || scope.contains( 'require' ) ) return;
90 if ( node.arguments.length !== 1 || (node.arguments[0].type !== 'Literal' && (node.arguments[0].type !== 'TemplateLiteral' || node.arguments[0].expressions.length > 0) ) ) return; // TODO handle these weird cases?
91 if ( ignoreRequire( node.arguments[0].value ) ) return;
92
93 return true;
94 }
95
96 function getRequired ( node, name ) {
97 const source = node.arguments[0].type === 'Literal' ? node.arguments[0].value : node.arguments[0].quasis[0].value.cooked;
98
99 const existing = required[ source ];
100 if ( existing === undefined ) {
101 sources.push( source );
102
103 if ( !name ) {
104 do name = `require$$${uid++}`;
105 while ( scope.contains( name ) );
106 }
107
108 required[ source ] = { source, name, importsDefault: false };
109 }
110
111 return required[ source ];
112 }
113
114 // do a first pass, see which names are assigned to. This is necessary to prevent
115 // illegally replacing `var foo = require('foo')` with `import foo from 'foo'`,
116 // where `foo` is later reassigned. (This happens in the wild. CommonJS, sigh)
117 const assignedTo = new Set();
118 walk( ast, {
119 enter ( node ) {
120 if ( node.type !== 'AssignmentExpression' ) return;
121 if ( node.left.type === 'MemberExpression' ) return;
122
123 extractNames( node.left ).forEach( name => {
124 assignedTo.add( name );
125 });
126 }
127 });
128
129 walk( ast, {
130 enter ( node, parent ) {
131 if ( sourceMap ) {
132 magicString.addSourcemapLocation( node.start );
133 magicString.addSourcemapLocation( node.end );
134 }
135
136 // skip dead branches
137 if ( parent && ( parent.type === 'IfStatement' || parent.type === 'ConditionalExpression' ) ) {
138 if ( node === parent.consequent && isFalsy( parent.test ) ) return this.skip();
139 if ( node === parent.alternate && isTruthy( parent.test ) ) return this.skip();
140 }
141
142 if ( node._skip ) return this.skip();
143
144 programDepth += 1;
145
146 if ( node.scope ) scope = node.scope;
147 if ( functionType.test( node.type ) ) lexicalDepth += 1;
148
149 // if toplevel return, we need to wrap it
150 if ( node.type === 'ReturnStatement' && lexicalDepth === 0 ) {
151 shouldWrap = true;
152 }
153
154 // rewrite `this` as `commonjsHelpers.commonjsGlobal`
155 if ( node.type === 'ThisExpression' && lexicalDepth === 0 ) {
156 uses.global = true;
157 if ( !ignoreGlobal ) magicString.overwrite( node.start, node.end, `${HELPERS_NAME}.commonjsGlobal`, { storeName: true } );
158 return;
159 }
160
161 // rewrite `typeof module`, `typeof module.exports` and `typeof exports` (https://github.com/rollup/rollup-plugin-commonjs/issues/151)
162 if ( node.type === 'UnaryExpression' && node.operator === 'typeof' ) {
163 const flattened = flatten( node.argument );
164 if ( !flattened ) return;
165
166 if ( scope.contains( flattened.name ) ) return;
167
168 if ( flattened.keypath === 'module.exports' || flattened.keypath === 'module' || flattened.keypath === 'exports' ) {
169 magicString.overwrite( node.start, node.end, `'object'`, { storeName: false } );
170 }
171 }
172
173 // rewrite `require` (if not already handled) `global` and `define`, and handle free references to
174 // `module` and `exports` as these mean we need to wrap the module in commonjsHelpers.createCommonjsModule
175 if ( node.type === 'Identifier' ) {
176 if ( isReference( node, parent ) && !scope.contains( node.name ) ) {
177 if ( node.name in uses ) {
178 if ( node.name === 'require' ) {
179 if ( allowDynamicRequire ) return;
180 magicString.overwrite( node.start, node.end, `${HELPERS_NAME}.commonjsRequire`, { storeName: true } );
181 }
182
183 uses[ node.name ] = true;
184 if ( node.name === 'global' && !ignoreGlobal ) {
185 magicString.overwrite( node.start, node.end, `${HELPERS_NAME}.commonjsGlobal`, { storeName: true } );
186 }
187
188 // if module or exports are used outside the context of an assignment
189 // expression, we need to wrap the module
190 if ( node.name === 'module' || node.name === 'exports' ) {
191 shouldWrap = true;
192 }
193 }
194
195 if ( node.name === 'define' ) {
196 magicString.overwrite( node.start, node.end, 'undefined', { storeName: true } );
197 }
198
199 globals.add( node.name );
200 }
201
202 return;
203 }
204
205 // Is this an assignment to exports or module.exports?
206 if ( node.type === 'AssignmentExpression' ) {
207 if ( node.left.type !== 'MemberExpression' ) return;
208
209 const flattened = flatten( node.left );
210 if ( !flattened ) return;
211
212 if ( scope.contains( flattened.name ) ) return;
213
214 const match = exportsPattern.exec( flattened.keypath );
215 if ( !match || flattened.keypath === 'exports' ) return;
216
217 uses[ flattened.name ] = true;
218
219 // we're dealing with `module.exports = ...` or `[module.]exports.foo = ...` –
220 // if this isn't top-level, we'll need to wrap the module
221 if ( programDepth > 3 ) shouldWrap = true;
222
223 node.left._skip = true;
224
225 if ( flattened.keypath === 'module.exports' && node.right.type === 'ObjectExpression' ) {
226 return node.right.properties.forEach( prop => {
227 if ( prop.computed || prop.key.type !== 'Identifier' ) return;
228 const name = prop.key.name;
229 if ( name === makeLegalIdentifier( name ) ) namedExports[ name ] = true;
230 });
231 }
232
233 if ( match[1] ) namedExports[ match[1] ] = true;
234 return;
235 }
236
237 // if this is `var x = require('x')`, we can do `import x from 'x'`
238 if ( node.type === 'VariableDeclarator' && node.id.type === 'Identifier' && isRequireStatement( node.init ) ) {
239 // for now, only do this for top-level requires. maybe fix this in future
240 if ( scope.parent ) return;
241
242 // edge case — CJS allows you to assign to imports. ES doesn't
243 if ( assignedTo.has( node.id.name ) ) return;
244
245 const r = getRequired( node.init, node.id.name );
246 r.importsDefault = true;
247
248 if ( r.name === node.id.name ) {
249 node._shouldRemove = true;
250 }
251 }
252
253 if ( !isRequireStatement( node ) ) return;
254
255 const r = getRequired( node );
256
257 if ( parent.type === 'ExpressionStatement' ) {
258 // is a bare import, e.g. `require('foo');`
259 magicString.remove( parent.start, parent.end );
260 } else {
261 r.importsDefault = true;
262 magicString.overwrite( node.start, node.end, r.name );
263 }
264
265 node.callee._skip = true;
266 },
267
268 leave ( node ) {
269 programDepth -= 1;
270 if ( node.scope ) scope = scope.parent;
271 if ( functionType.test( node.type ) ) lexicalDepth -= 1;
272
273 if ( node.type === 'VariableDeclaration' ) {
274 let keepDeclaration = false;
275 let c = node.declarations[0].start;
276
277 for ( let i = 0; i < node.declarations.length; i += 1 ) {
278 const declarator = node.declarations[i];
279
280 if ( declarator._shouldRemove ) {
281 magicString.remove( c, declarator.end );
282 } else {
283 if ( !keepDeclaration ) {
284 magicString.remove( c, declarator.start );
285 keepDeclaration = true;
286 }
287
288 c = declarator.end;
289 }
290 }
291
292 if ( !keepDeclaration ) {
293 magicString.remove( node.start, node.end );
294 }
295 }
296 }
297 });
298
299 if ( !sources.length && !uses.module && !uses.exports && !uses.require && ( ignoreGlobal || !uses.global ) ) {
300 if ( Object.keys( namedExports ).length ) {
301 throw new Error( `Custom named exports were specified for ${id} but it does not appear to be a CommonJS module` );
302 }
303 return null; // not a CommonJS module
304 }
305
306 const includeHelpers = shouldWrap || uses.global || uses.require;
307 const importBlock = ( includeHelpers ? [ `import * as ${HELPERS_NAME} from '${HELPERS_ID}';` ] : [] ).concat(
308 sources.map( source => {
309 // import the actual module before the proxy, so that we know
310 // what kind of proxy to build
311 return `import '${source}';`;
312 }),
313 sources.map( source => {
314 const { name, importsDefault } = required[ source ];
315 return `import ${importsDefault ? `${name} from ` : ``}'${PREFIX}${source}';`;
316 })
317 ).join( '\n' ) + '\n\n';
318
319 const namedExportDeclarations = [];
320 let wrapperStart = '';
321 let wrapperEnd = '';
322
323 const moduleName = deconflict( scope, globals, getName( id ) );
324 if ( !isEntry ) {
325 const exportModuleExports = {
326 str: `export { ${moduleName} as __moduleExports };`,
327 name: '__moduleExports'
328 };
329
330 namedExportDeclarations.push( exportModuleExports );
331 }
332
333 const name = getName( id );
334
335 function addExport ( x ) {
336 const deconflicted = deconflict( scope, globals, name );
337
338 const declaration = deconflicted === name ?
339 `export var ${x} = ${moduleName}.${x};` :
340 `var ${deconflicted} = ${moduleName}.${x};\nexport { ${deconflicted} as ${x} };`;
341
342 namedExportDeclarations.push({
343 str: declaration,
344 name: x
345 });
346 }
347
348 if ( customNamedExports ) customNamedExports.forEach( addExport );
349
350 const defaultExportPropertyAssignments = [];
351 let hasDefaultExport = false;
352
353 if ( shouldWrap ) {
354 const args = `module${uses.exports ? ', exports' : ''}`;
355
356 wrapperStart = `var ${moduleName} = ${HELPERS_NAME}.createCommonjsModule(function (${args}) {\n`;
357 wrapperEnd = `\n});`;
358 } else {
359 const names = [];
360
361 ast.body.forEach( node => {
362 if ( node.type === 'ExpressionStatement' && node.expression.type === 'AssignmentExpression' ) {
363 const left = node.expression.left;
364 const flattened = flatten( left );
365
366 if ( !flattened ) return;
367
368 const match = exportsPattern.exec( flattened.keypath );
369 if ( !match ) return;
370
371 if ( flattened.keypath === 'module.exports' ) {
372 hasDefaultExport = true;
373 magicString.overwrite( left.start, left.end, `var ${moduleName}` );
374 } else {
375 const name = match[1];
376 const deconflicted = deconflict( scope, globals, name );
377
378 names.push({ name, deconflicted });
379
380 magicString.overwrite( node.start, left.end, `var ${deconflicted}` );
381
382 const declaration = name === deconflicted ?
383 `export { ${name} };` :
384 `export { ${deconflicted} as ${name} };`;
385
386 if ( name !== 'default' ) {
387 namedExportDeclarations.push({
388 str: declaration,
389 name
390 });
391 delete namedExports[name];
392 }
393
394 defaultExportPropertyAssignments.push( `${moduleName}.${name} = ${deconflicted};` );
395 }
396 }
397 });
398
399 if ( !hasDefaultExport ) {
400 wrapperEnd = `\n\nvar ${moduleName} = {\n${
401 names.map( ({ name, deconflicted }) => `\t${name}: ${deconflicted}` ).join( ',\n' )
402 }\n};`;
403 }
404 }
405 Object.keys( namedExports )
406 .filter( key => !blacklist[ key ] )
407 .forEach( addExport );
408
409 const defaultExport = /__esModule/.test( code ) ?
410 `export default ${HELPERS_NAME}.unwrapExports(${moduleName});` :
411 `export default ${moduleName};`;
412
413 const named = namedExportDeclarations
414 .filter( x => x.name !== 'default' || !hasDefaultExport )
415 .map( x => x.str );
416
417 const exportBlock = '\n\n' + [ defaultExport ]
418 .concat( named )
419 .concat( hasDefaultExport ? defaultExportPropertyAssignments : [] )
420 .join( '\n' );
421
422 magicString.trim()
423 .prepend( importBlock + wrapperStart )
424 .trim()
425 .append( wrapperEnd + exportBlock );
426
427 code = magicString.toString();
428 const map = sourceMap ? magicString.generateMap() : null;
429
430 return { code, map };
431}