From fb5dd95bb03f57fa37574899352b22a8c232929b Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Sat, 26 Nov 2016 18:49:26 -0500 Subject: [PATCH] IIFE and UMD builds (#27) --- compiler/generate/index.js | 2 +- compiler/generate/utils/getGlobals.js | 43 ++++++++ compiler/generate/utils/getIntro.js | 63 ++++++++--- compiler/generate/utils/getOutro.js | 13 ++- compiler/index.js | 6 +- test/test.js | 146 +++++++++++++++++++++----- 6 files changed, 224 insertions(+), 49 deletions(-) create mode 100644 compiler/generate/utils/getGlobals.js diff --git a/compiler/generate/index.js b/compiler/generate/index.js index 61a568f5c8..0da39a932f 100644 --- a/compiler/generate/index.js +++ b/compiler/generate/index.js @@ -503,7 +503,7 @@ export default function generate ( parsed, source, options ) { compiled.append( finalChunk ); - addString( '\n\n' + getOutro( format, constructorName, imports ) ); + addString( '\n\n' + getOutro( format, constructorName, options, imports ) ); return { code: compiled.toString(), diff --git a/compiler/generate/utils/getGlobals.js b/compiler/generate/utils/getGlobals.js new file mode 100644 index 0000000000..0d3e7bad14 --- /dev/null +++ b/compiler/generate/utils/getGlobals.js @@ -0,0 +1,43 @@ +export default function getGlobals ( imports, { globals, onerror, onwarn } ) { + const globalFn = getGlobalFn( globals ); + + return imports.map( x => { + let name = globalFn( x.source.value ); + + if ( !name ) { + if ( x.name.startsWith( '__import' ) ) { + const error = new Error( `Could not determine name for imported module '${x.source.value}' – use options.globals` ); + if ( onerror ) { + onerror( error ); + } else { + throw error; + } + } + + else { + const warning = { + message: `No name was supplied for imported module '${x.source.value}'. Guessing '${x.name}', but you should use options.globals` + }; + + if ( onwarn ) { + onwarn( warning ); + } else { + console.warn( warning ); // eslint-disable-line no-console + } + } + + name = x.name; + } + + return name; + }); +} + +function getGlobalFn ( globals ) { + if ( typeof globals === 'function' ) return globals; + if ( typeof globals === 'object' ) { + return id => globals[ id ]; + } + + return () => undefined; +} diff --git a/compiler/generate/utils/getIntro.js b/compiler/generate/utils/getIntro.js index e39d801bb0..6ad8fa9f8b 100644 --- a/compiler/generate/utils/getIntro.js +++ b/compiler/generate/utils/getIntro.js @@ -1,31 +1,29 @@ -export default function getIntro ( format, options, imports ) { - const dependencies = imports.map( declaration => { - return { - source: declaration.source.value, - name: declaration.name - }; - }); +import deindent from './deindent.js'; +import getGlobals from './getGlobals.js'; +export default function getIntro ( format, options, imports ) { if ( format === 'es' ) return ''; - if ( format === 'amd' ) return getAmdIntro( options.amd, dependencies ); - if ( format === 'cjs' ) return getCjsIntro( dependencies ); + if ( format === 'amd' ) return getAmdIntro( options, imports ); + if ( format === 'cjs' ) return getCjsIntro( options, imports ); + if ( format === 'iife' ) return getIifeIntro( options, imports ); + if ( format === 'umd' ) return getUmdIntro( options, imports ); throw new Error( `Not implemented: ${format}` ); } -function getAmdIntro ( options = {}, dependencies ) { - const sourceString = dependencies.length ? - `[ ${dependencies.map( dep => `'${dep.source}'` ).join( ', ' )} ], ` : +function getAmdIntro ( options, imports ) { + const sourceString = imports.length ? + `[ ${imports.map( declaration => `'${declaration.source.value}'` ).join( ', ' )} ], ` : ''; - const paramString = dependencies.length ? ` ${dependencies.map( dep => dep.name ).join( ', ' )} ` : ''; + const id = options.amd && options.amd.id; - return `define(${options.id ? ` '${options.id}', ` : ''}${sourceString}function (${paramString}) { 'use strict';\n\n`; + return `define(${id ? ` '${id}', ` : ''}${sourceString}function (${paramString( imports )}) { 'use strict';\n\n`; } -function getCjsIntro ( dependencies ) { - const requireBlock = dependencies - .map( dep => `var ${dep.name} = require( '${dep.source}' );` ) +function getCjsIntro ( options, imports ) { + const requireBlock = imports + .map( declaration => `var ${declaration.name} = require( '${declaration.source.value}' );` ) .join( '\n\n' ); if ( requireBlock ) { @@ -34,3 +32,34 @@ function getCjsIntro ( dependencies ) { return `'use strict';\n\n`; } + +function getIifeIntro ( options, imports ) { + if ( !options.name ) { + throw new Error( `Missing required 'name' option for IIFE export` ); + } + + return `var ${options.name} = (function (${paramString( imports )}) { 'use strict';\n\n`; +} + +function getUmdIntro ( options, imports ) { + if ( !options.name ) { + throw new Error( `Missing required 'name' option for UMD export` ); + } + + const amdId = options.amd && options.amd.id ? `'${options.amd.id}', ` : ''; + + const amdDeps = imports.length ? `[${imports.map( declaration => `'${declaration.source.value}'` ).join( ', ')}], ` : ''; + const cjsDeps = imports.map( declaration => `require('${declaration.source.value}')` ).join( ', ' ); + const globalDeps = getGlobals( imports, options ); + + return deindent` + (function ( global, factory ) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(${cjsDeps}) : + typeof define === 'function' && define.amd ? define(${amdId}${amdDeps}factory) : + (global.${options.name} = factory(${globalDeps})); + }(this, (function (${paramString( imports )}) { 'use strict';` + '\n\n'; +} + +function paramString ( imports ) { + return imports.length ? ` ${imports.map( dep => dep.name ).join( ', ' )} ` : ''; +} diff --git a/compiler/generate/utils/getOutro.js b/compiler/generate/utils/getOutro.js index c6a7961d75..283151267b 100644 --- a/compiler/generate/utils/getOutro.js +++ b/compiler/generate/utils/getOutro.js @@ -1,4 +1,6 @@ -export default function getOutro ( format, name, imports ) { +import getGlobals from './getGlobals.js'; + +export default function getOutro ( format, name, options, imports ) { if ( format === 'es' ) { return `export default ${name};`; } @@ -11,5 +13,14 @@ export default function getOutro ( format, name, imports ) { return `module.exports = ${name};`; } + if ( format === 'iife' ) { + const globals = getGlobals( imports, options ); + return `return ${name};\n\n}(${globals.join( ', ' )}));`; + } + + if ( format === 'umd' ) { + return `return ${name};\n\n})));`; + } + throw new Error( `Not implemented: ${format}` ); } diff --git a/compiler/index.js b/compiler/index.js index 069417ed96..1ee0dcfa42 100644 --- a/compiler/index.js +++ b/compiler/index.js @@ -7,7 +7,11 @@ export function compile ( source, options = {} ) { if ( !options.onwarn ) { options.onwarn = warning => { - console.warn( `(${warning.loc.line}:${warning.loc.column}) – ${warning.message}` ); // eslint-disable-line no-console + if ( warning.loc ) { + console.warn( `(${warning.loc.line}:${warning.loc.column}) – ${warning.message}` ); // eslint-disable-line no-console + } else { + console.warn( warning.message ); // eslint-disable-line no-console + } }; } diff --git a/test/test.js b/test/test.js index 611781d5e1..a858e3093b 100644 --- a/test/test.js +++ b/test/test.js @@ -280,6 +280,67 @@ describe( 'svelte', () => { }); describe( 'formats', () => { + function testAmd ( code, expectedId, dependencies, html ) { + const fn = new Function( 'define', code ); + + return env().then( window => { + function define ( id, deps, factory ) { + assert.equal( id, expectedId ); + assert.deepEqual( deps, Object.keys( dependencies ) ); + + const SvelteComponent = factory( ...Object.keys( dependencies ).map( key => dependencies[ key ] ) ); + + const main = window.document.body.querySelector( 'main' ); + const component = new SvelteComponent({ target: main }); + + assert.htmlEqual( main.innerHTML, html ); + + component.teardown(); + } + + define.amd = true; + + fn( define ); + }); + } + + function testCjs ( code, dependencyById, html ) { + const fn = new Function( 'module', 'exports', 'require', code ); + + return env().then( window => { + const module = { exports: {} }; + const require = id => { + return dependencyById[ id ]; + }; + + fn( module, module.exports, require ); + + const SvelteComponent = module.exports; + + const main = window.document.body.querySelector( 'main' ); + const component = new SvelteComponent({ target: main }); + + assert.htmlEqual( main.innerHTML, html ); + + component.teardown(); + }); + } + + function testIife ( code, name, globals, html ) { + const fn = new Function( Object.keys( globals ), `${code}\n\nreturn ${name};` ); + + return env().then( window => { + const SvelteComponent = fn( ...Object.keys( globals ).map( key => globals[ key ] ) ); + + const main = window.document.body.querySelector( 'main' ); + const component = new SvelteComponent({ target: main }); + + assert.htmlEqual( main.innerHTML, html ); + + component.teardown(); + }); + } + describe( 'amd', () => { it( 'generates an AMD module', () => { const source = deindent` @@ -301,23 +362,7 @@ describe( 'svelte', () => { amd: { id: 'foo' } }); - const fn = new Function( 'define', code ); - - return env().then( window => { - fn( ( id, dependencies, factory ) => { - assert.equal( id, 'foo' ); - assert.deepEqual( dependencies, [ 'answer' ]); - - const SvelteComponent = factory( 42 ); - - const main = window.document.body.querySelector( 'main' ); - const component = new SvelteComponent({ target: main }); - - assert.htmlEqual( main.innerHTML, `
42
` ); - - component.teardown(); - }); - }); + return testAmd( code, 'foo', { answer: 42 }, `
42
` ); }); }); @@ -341,25 +386,68 @@ describe( 'svelte', () => { format: 'cjs' }); - const fn = new Function( 'module', 'require', code ); + return testCjs( code, { answer: 42 }, `
42
` ); + }); + }); - return env().then( window => { - const module = {}; - const require = id => { - if ( id === 'answer' ) return 42; - }; + describe( 'iife', () => { + it( 'generates a self-executing script', () => { + const source = deindent` +
{{answer}}
- fn( module, require ); + + `; - const main = window.document.body.querySelector( 'main' ); - const component = new SvelteComponent({ target: main }); + const { code } = compile( source, { + format: 'iife', + name: 'Foo', + globals: { + answer: 'answer' + } + }); - assert.htmlEqual( main.innerHTML, `
42
` ); + return testIife( code, 'Foo', { answer: 42 }, `
42
` ); + }); + }); - component.teardown(); + describe( 'umd', () => { + it( 'generates a UMD build', () => { + const source = deindent` +
{{answer}}
+ + + `; + + const { code } = compile( source, { + format: 'umd', + name: 'Foo', + globals: { + answer: 'answer' + }, + amd: { + id: 'foo' + } }); + + return testAmd( code, 'foo', { answer: 42 }, `
42
` ) + .then( () => testCjs( code, { answer: 42 }, `
42
` ) ) + .then( () => testIife( code, 'Foo', { answer: 42 }, `
42
` ) ); }); }); });