diff --git a/.gitignore b/.gitignore index 7f38aa0..5000af5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store node_modules .gobble* -dist \ No newline at end of file +dist +_actual diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b24f5a..2f694ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # rollup changelog +## 0.6.4 + +* Fix CJS bundling with default export + +## 0.6.3 + +* Fix exports and external module imports with some output formats +* Fix endless cycle bug on Windows ([#3](https://github.com/rollup/rollup/pull/3)) - thanks @Bobris + +## 0.6.2 + +* Permit assignments to properties of imported bindings + +## 0.6.1 + +* Support for basic transformers + +## 0.6.0 + +* BREAKING - `rollup.rollup` and `bundle.write` both take a single options argument +* BREAKING - external modules must be declared upfront with `options.external: [...]` +* Non-relative module paths will be resolved by looking for `jsnext:main` fields in the appropriate `package.json` files. This behaviour can be overridden by passing an alternative `resolveExternal` function +* Fix sourcemap options +* Include CLI files in npm package (duh) + ## 0.5.0 * Command line interface diff --git a/README.md b/README.md index 5b4237c..6d9fefe 100644 --- a/README.md +++ b/README.md @@ -91,14 +91,13 @@ This is not a trivial task. There are almost certainly a great many complex edge The example below is aspirational. It isn't yet implemented - it exists in the name of [README driven development](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html). ```js -rollup.rollup( 'app.js', { - // Override the default path resolution - resolvePath: function ( importee, importer ) { - // return a string or a falsy value - if falsy, - // import is kept external to the bundle. - // Alternative, return a Promise that fulfils - // with a string or falsy value - } +rollup.rollup({ + // The bundle's starting point + entry: 'app.js', + + // Any external modules you don't want to include + // in the bundle (includes node built-ins) + external: [ 'path', 'fs', 'some-other-lib' ] }).then( function ( bundle ) { // generate code and a sourcemap const { code, map } = bundle.generate({ diff --git a/bin/runRollup.js b/bin/runRollup.js index e33ef4c..585c1fc 100644 --- a/bin/runRollup.js +++ b/bin/runRollup.js @@ -33,13 +33,16 @@ function bundle ( options, method ) { handleError({ code: 'MISSING_INPUT_OPTION' }); } - return rollup.rollup( options.input ).then( function ( bundle ) { + return rollup.rollup({ + entry: options.input + }).then( function ( bundle ) { var generateOptions = { + dest: options.output, format: options.format }; if ( options.output ) { - return bundle.write( options.output, generateOptions ); + return bundle.write( generateOptions ); } if ( options.sourcemap && options.sourcemap !== 'inline' ) { diff --git a/package.json b/package.json index d95d776..a29e1e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "0.5.0", + "version": "0.6.4", "description": "Next-generation ES6 module bundler", "main": "dist/rollup.js", "jsnext:main": "src/rollup.js", @@ -37,7 +37,7 @@ "gobble-babel": "^5.1.0", "gobble-cli": "^0.4.2", "gobble-esperanto-bundle": "^0.2.0", - "gobble-rollup": "^0.1.1", + "gobble-rollup": "^0.2.0", "mocha": "^2.2.4", "source-map-support": "^0.2.10", "source-map": "^0.1.40" @@ -52,6 +52,7 @@ "files": [ "src", "dist", + "bin", "README.md" ] } diff --git a/src/Bundle.js b/src/Bundle.js index 151f2a4..2b62032 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -6,7 +6,9 @@ import Module from './Module'; import ExternalModule from './ExternalModule'; import finalisers from './finalisers/index'; import makeLegalIdentifier from './utils/makeLegalIdentifier'; -import { defaultResolver } from './utils/resolvePath'; +import ensureArray from './utils/ensureArray'; +import { defaultResolver, defaultExternalResolver } from './utils/resolvePath'; +import { defaultLoader } from './utils/load'; function badExports ( option, keys ) { throw new Error( `'${option}' was specified for options.exports, but entry module has following exports: ${keys.join(', ')}` ); @@ -18,17 +20,26 @@ export default class Bundle { this.base = dirname( this.entryPath ); this.resolvePath = options.resolvePath || defaultResolver; + this.load = options.load || defaultLoader; + + this.resolvePathOptions = { + external: ensureArray( options.external ), + resolveExternal: options.resolveExternal || defaultExternalResolver + }; + + this.loadOptions = { + transform: ensureArray( options.transform ) + }; this.entryModule = null; this.modulePromises = {}; this.statements = []; this.externalModules = []; - this.defaultExportName = null; this.internalNamespaceModules = []; } fetchModule ( importee, importer ) { - return Promise.resolve( importer === null ? importee : this.resolvePath( importee, importer ) ) + return Promise.resolve( importer === null ? importee : this.resolvePath( importee, importer, this.resolvePathOptions ) ) .then( path => { if ( !path ) { // external module @@ -42,7 +53,7 @@ export default class Bundle { } if ( !has( this.modulePromises, path ) ) { - this.modulePromises[ path ] = readFile( path, { encoding: 'utf-8' }) + this.modulePromises[ path ] = Promise.resolve( this.load( path, this.loadOptions ) ) .then( source => { const module = new Module({ path, @@ -85,7 +96,6 @@ export default class Bundle { this.statements = statements; this.deconflict(); }); - } deconflict () { diff --git a/src/Module.js b/src/Module.js index aa3eb3a..b27830d 100644 --- a/src/Module.js +++ b/src/Module.js @@ -19,7 +19,6 @@ export default class Module { this.bundle = bundle; this.path = path; - this.relativePath = relative( bundle.base, path ).slice( 0, -3 ); // remove .js this.magicString = new MagicString( source, { filename: path @@ -40,6 +39,7 @@ export default class Module { walk( ast, { enter: node => { this.magicString.addSourcemapLocation( node.start ); + this.magicString.addSourcemapLocation( node.end ); } }); diff --git a/src/Statement.js b/src/Statement.js index 94d43a2..042d939 100644 --- a/src/Statement.js +++ b/src/Statement.js @@ -156,16 +156,29 @@ export default class Statement { checkForWrites ( scope, node ) { const addNode = ( node, disallowImportReassignments ) => { + let depth = 0; // determine whether we're illegally modifying a binding or namespace + while ( node.type === 'MemberExpression' ) { node = node.object; + depth += 1; } // disallow assignments/updates to imported bindings and namespaces - if ( disallowImportReassignments && has( this.module.imports, node.name ) && !scope.contains( node.name ) ) { - const err = new Error( `Illegal reassignment to import '${node.name}'` ); - err.file = this.module.path; - err.loc = getLocation( this.module.magicString.toString(), node.start ); - throw err; + if ( disallowImportReassignments ) { + const importSpecifier = this.module.imports[ node.name ]; + + if ( importSpecifier && !scope.contains( node.name ) ) { + const minDepth = importSpecifier.name === '*' ? + 2 : // cannot do e.g. `namespace.foo = bar` + 1; // cannot do e.g. `foo = bar`, but `foo.bar = bar` is fine + + if ( depth < minDepth ) { + const err = new Error( `Illegal reassignment to import '${node.name}'` ); + err.file = this.module.path; + err.loc = getLocation( this.module.magicString.toString(), node.start ); + throw err; + } + } } if ( node.type !== 'Identifier' ) { diff --git a/src/finalisers/amd.js b/src/finalisers/amd.js index a81221e..0fae690 100644 --- a/src/finalisers/amd.js +++ b/src/finalisers/amd.js @@ -23,13 +23,16 @@ export default function amd ( bundle, magicString, exportMode, options ) { if ( exportMode === 'default' ) { exportBlock = `return ${bundle.entryModule.getCanonicalName('default')};`; } else { - exportBlock = '\n\n' + Object.keys( exports ).map( name => { + exportBlock = Object.keys( exports ).map( name => { return `exports.${name} = ${exports[name].localName};`; }).join( '\n' ); } + if ( exportBlock ) { + magicString.append( '\n\n' + exportBlock ); + } + return magicString - .append( exportBlock ) .trim() .indent() .append( '\n\n});' ) diff --git a/src/finalisers/es6.js b/src/finalisers/es6.js index f88ea40..b7a2620 100644 --- a/src/finalisers/es6.js +++ b/src/finalisers/es6.js @@ -1,7 +1,26 @@ +import { keys } from '../utils/object'; + export default function es6 ( bundle, magicString, exportMode, options ) { - // TODO - const introBlock = ''; - const exportBlock = ''; + const introBlock = ''; // TODO... + + const exports = bundle.entryModule.exports; + const exportBlock = keys( exports ).map( exportedName => { + const specifier = exports[ exportedName ]; + + const canonicalName = bundle.entryModule.getCanonicalName( specifier.localName ); + + if ( exportedName === 'default' ) { + return `export default ${canonicalName};`; + } + + return exportedName === canonicalName ? + `export { ${exportedName} };` : + `export { ${canonicalName} as ${exportedName} };`; + }).join( '\n' ); + + if ( exportBlock ) { + magicString.append( '\n\n' + exportBlock ); + } return magicString.trim(); } diff --git a/src/finalisers/iife.js b/src/finalisers/iife.js index 39aadc2..da006ae 100644 --- a/src/finalisers/iife.js +++ b/src/finalisers/iife.js @@ -1,10 +1,33 @@ +import { has } from '../utils/object'; +import { getName } from '../utils/map-helpers'; + export default function iife ( bundle, magicString, exportMode, options ) { + const globalNames = options.globals || {}; + + let dependencies = bundle.externalModules.map( module => { + return has( globalNames, module.id ) ? globalNames[ module.id ] : module.name; + }); + + let args = bundle.externalModules.map( getName ); + + if ( exportMode !== 'none' && !options.moduleName ) { + throw new Error( 'You must supply options.moduleName for IIFE bundles' ); + } + + if ( exportMode === 'named' ) { + dependencies.unshift( `(window.${options.moduleName} = {})` ); + args.unshift( 'exports' ); + } + + let intro = `(function (${args}) { 'use strict';\n\n`; + let outro = `\n\n})(${dependencies});`; - const intro = `(function () { 'use strict';\n\n`; - const outro = `\n\n})();`; + if ( exportMode === 'default' ) { + intro = `var ${options.moduleName} = ${intro}`; + magicString.append( `\n\nreturn ${bundle.entryModule.getCanonicalName('default')};` ); + } return magicString - .trim() .indent() .prepend( intro ) .append( outro ); diff --git a/src/finalisers/umd.js b/src/finalisers/umd.js index 53eba7e..09164f7 100644 --- a/src/finalisers/umd.js +++ b/src/finalisers/umd.js @@ -26,23 +26,37 @@ export default function umd ( bundle, magicString, exportMode, options ) { ( has( options, 'moduleId' ) ? `['${options.moduleId}'], ` : `` ) + ( amdDeps.length ? `[${amdDeps.join( ', ' )}], ` : `` ); + const cjsExport = exportMode === 'default' ? `module.exports = ` : ``; + const defaultExport = exportMode === 'default' ? `global.${options.moduleName} = ` : ''; + const intro = `(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(${cjsDeps.join( ', ' )}) : + typeof exports === 'object' && typeof module !== 'undefined' ? ${cjsExport}factory(${cjsDeps.join( ', ' )}) : typeof define === 'function' && define.amd ? define(${amdParams}factory) : - factory(${globalDeps}); + ${defaultExport}factory(${globalDeps}); }(this, function (${args}) { 'use strict'; `.replace( /^\t\t/gm, '' ).replace( /^\t/gm, indentStr ); const exports = bundle.entryModule.exports; - const exportBlock = '\n\n' + Object.keys( exports ).map( name => { - return `exports.${name} = ${exports[name].localName};`; - }).join( '\n' ); + let exportBlock; + + if ( exportMode === 'default' ) { + const canonicalName = bundle.entryModule.getCanonicalName( 'default' ); + exportBlock = `return ${canonicalName};`; + } else { + exportBlock = Object.keys( exports ).map( name => { + const canonicalName = bundle.entryModule.getCanonicalName( exports[ name ].localName ); + return `exports.${name} = ${canonicalName};`; + }).join( '\n' ); + } + + if ( exportBlock ) { + magicString.append( '\n\n' + exportBlock ); + } return magicString - .append( exportBlock ) .trim() .indent() .append( '\n\n}));' ) diff --git a/src/rollup.js b/src/rollup.js index d182df3..3f15fdc 100644 --- a/src/rollup.js +++ b/src/rollup.js @@ -5,28 +5,25 @@ import Bundle from './Bundle'; let SOURCEMAPPING_URL = 'sourceMa'; SOURCEMAPPING_URL += 'ppingURL'; -export function rollup ( entry, options = {} ) { - const bundle = new Bundle({ - entry, - resolvePath: options.resolvePath - }); +export function rollup ( options ) { + if ( !options || !options.entry ) { + throw new Error( 'You must supply options.entry to rollup' ); + } + + const bundle = new Bundle( options ); return bundle.build().then( () => { return { generate: options => bundle.generate( options ), - write: ( dest, options = {} ) => { - let { code, map } = bundle.generate({ - dest, - format: options.format, - globalName: options.globalName, + write: options => { + if ( !options || !options.dest ) { + throw new Error( 'You must supply options.dest to bundle.write' ); + } - // sourcemap options - sourceMap: !!options.sourceMap, - sourceMapFile: options.sourceMapFile, - // sourceMapRoot: options.sourceMapRoot - }); + const dest = options.dest; + let { code, map } = bundle.generate( options ); - let promises = [ writeFile( dest, code ) ]; + let promises = []; if ( options.sourceMap ) { let url; @@ -41,6 +38,7 @@ export function rollup ( entry, options = {} ) { code += `\n//# ${SOURCEMAPPING_URL}=${url}`; } + promises.push( writeFile( dest, code ) ); return Promise.all( promises ); } }; diff --git a/src/utils/ensureArray.js b/src/utils/ensureArray.js new file mode 100644 index 0000000..2669345 --- /dev/null +++ b/src/utils/ensureArray.js @@ -0,0 +1,5 @@ +export default function ensureArray ( thing ) { + if ( Array.isArray( thing ) ) return thing; + if ( thing == undefined ) return []; + return [ thing ]; +} diff --git a/src/utils/load.js b/src/utils/load.js new file mode 100644 index 0000000..9b79d9a --- /dev/null +++ b/src/utils/load.js @@ -0,0 +1,10 @@ +import { readFileSync } from 'sander'; + +export function defaultLoader ( path, options ) { + // TODO support plugins e.g. !css and !json? + const source = readFileSync( path, { encoding: 'utf-8' }); + + return options.transform.reduce( ( source, transformer ) => { + return transformer( source, path ); + }, source ); +} diff --git a/src/utils/resolvePath.js b/src/utils/resolvePath.js index 4b07512..12fcca8 100644 --- a/src/utils/resolvePath.js +++ b/src/utils/resolvePath.js @@ -1,11 +1,56 @@ -import { dirname, isAbsolute, resolve } from 'path'; +import { dirname, isAbsolute, resolve, parse } from 'path'; +import { readFileSync } from 'sander'; -export function defaultResolver ( importee, importer ) { +export function defaultResolver ( importee, importer, options ) { // absolute paths are left untouched if ( isAbsolute( importee ) ) return importee; - // external modules stay external - if ( importee[0] !== '.' ) return false; + // we try to resolve external modules + if ( importee[0] !== '.' ) { + // unless we want to keep it external, that is + if ( ~options.external.indexOf( importee ) ) return null; + + return options.resolveExternal( importee, importer, options ); + } return resolve( dirname( importer ), importee ).replace( /\.js$/, '' ) + '.js'; -} \ No newline at end of file +} + +export function defaultExternalResolver ( id, importer, options ) { + // for now, only node_modules is supported, and only jsnext:main + let parsed = parse( importer ); + let dir = parsed.dir; + + while ( dir !== parsed.root ) { + const pkgPath = resolve( dir, 'node_modules', id, 'package.json' ); + let pkgJson; + + try { + pkgJson = readFileSync( pkgPath ).toString(); + } catch ( err ) { + // noop + } + + if ( pkgJson ) { + let pkg; + + try { + pkg = JSON.parse( pkgJson ); + } catch ( err ) { + throw new Error( `Malformed JSON: ${pkgPath}` ); + } + + const main = pkg[ 'jsnext:main' ]; + + if ( !main ) { + throw new Error( `Package ${id} does not have a jsnext:main field, and so cannot be included in your rollup. Try adding it as an external module instead (e.g. options.external = ['${id}']). See https://github.com/rollup/rollup/wiki/jsnext:main for more info` ); + } + + return resolve( dirname( pkgPath ), main ).replace( /\.js$/, '' ) + '.js'; + } + + dir = dirname( dir ); + } + + throw new Error( `Could not find package ${id} (required by ${importer})` ); +}