From fa5bbbee9f5b8321bff9b3e067fd075a01b783f6 Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Sat, 26 Nov 2016 15:13:03 -0500 Subject: [PATCH] implement AMD and CommonJS outout formats (#27) --- compiler/generate/index.js | 52 ++++++- compiler/generate/utils/getIntro.js | 36 +++++ compiler/generate/utils/getOutro.js | 15 ++ test/test.js | 226 +++++++++++++++++++--------- 4 files changed, 252 insertions(+), 77 deletions(-) create mode 100644 compiler/generate/utils/getIntro.js create mode 100644 compiler/generate/utils/getOutro.js diff --git a/compiler/generate/index.js b/compiler/generate/index.js index 461fb2af4d..61a568f5c8 100644 --- a/compiler/generate/index.js +++ b/compiler/generate/index.js @@ -4,10 +4,13 @@ import deindent from './utils/deindent.js'; import isReference from './utils/isReference.js'; import counter from './utils/counter.js'; import flattenReference from './utils/flattenReference.js'; +import getIntro from './utils/getIntro.js'; +import getOutro from './utils/getOutro.js'; import visitors from './visitors/index.js'; import processCss from './css/process.js'; export default function generate ( parsed, source, options ) { + const format = options.format || 'es'; const renderers = []; const generator = { @@ -155,7 +158,8 @@ export default function generate ( parsed, source, options ) { while ( /[ \t]/.test( source[ a - 1 ] ) ) a -= 1; while ( source[b] === '\n' ) b += 1; - imports.push( source.slice( a, b ).replace( /^\s/, '' ) ); + //imports.push( source.slice( a, b ).replace( /^\s/, '' ) ); + imports.push( node ); generator.code.remove( a, b ); } } @@ -278,9 +282,35 @@ export default function generate ( parsed, source, options ) { dispatchObservers( observers.deferred, newState, oldState ); ` ); + const importBlock = imports + .map( ( declaration, i ) => { + if ( format === 'es' ) { + return source.slice( declaration.start, declaration.end ); + } + + const defaultImport = declaration.specifiers.find( x => x.type === 'ImportDefaultSpecifier' || x.type === 'ImportSpecifier' && x.imported.name === 'default' ); + const namespaceImport = declaration.specifiers.find( x => x.type === 'ImportNamespaceSpecifier' ); + const namedImports = declaration.specifiers.filter( x => x.type === 'ImportSpecifier' && x.imported.name !== 'default' ); + + const name = ( defaultImport || namespaceImport ) ? ( defaultImport || namespaceImport ).local.name : `__import${i}`; + declaration.name = name; // hacky but makes life a bit easier later + + const statements = namedImports.map( specifier => { + return `var ${specifier.local.name} = ${name}.${specifier.imported.name}`; + }); + + if ( defaultImport ) { + statements.push( `${name} = ( ${name} && ${name}.__esModule ) ? ${name}['default'] : ${name};` ); + } + + return statements.join( '\n' ); + }) + .filter( Boolean ) + .join( '\n' ); + if ( parsed.js ) { if ( imports.length ) { - topLevelStatements.push( imports.join( '' ).trim() ); + topLevelStatements.push( importBlock ); } topLevelStatements.push( `[✂${parsed.js.content.start}-${parsed.js.content.end}✂]` ); @@ -343,7 +373,7 @@ export default function generate ( parsed, source, options ) { const initialState = templateProperties.data ? `Object.assign( template.data(), options.data )` : `options.data || {}`; topLevelStatements.push( deindent` - export default function ${constructorName} ( options ) { + function ${constructorName} ( options ) { var component = this;${generator.usesRefs ? `\nthis.refs = {}` : ``} var state = ${initialState};${templateProperties.computed ? `\napplyComputations( state, state, {} );` : ``} @@ -451,13 +481,19 @@ export default function generate ( parsed, source, options ) { const compiled = new Bundle({ separator: '' }); - parts.forEach( str => { - const match = pattern.exec( str ); - + function addString ( str ) { compiled.addSource({ filename: options.filename, - content: new MagicString( str.replace( pattern, '' ) ) + content: new MagicString( str ) }); + } + + addString( getIntro( format, options, imports ) ); + + parts.forEach( str => { + const match = pattern.exec( str ); + + addString( str.replace( pattern, '' ) ); compiled.addSource({ filename: options.filename, @@ -467,6 +503,8 @@ export default function generate ( parsed, source, options ) { compiled.append( finalChunk ); + addString( '\n\n' + getOutro( format, constructorName, imports ) ); + return { code: compiled.toString(), map: compiled.generateMap() diff --git a/compiler/generate/utils/getIntro.js b/compiler/generate/utils/getIntro.js new file mode 100644 index 0000000000..e39d801bb0 --- /dev/null +++ b/compiler/generate/utils/getIntro.js @@ -0,0 +1,36 @@ +export default function getIntro ( format, options, imports ) { + const dependencies = imports.map( declaration => { + return { + source: declaration.source.value, + name: declaration.name + }; + }); + + if ( format === 'es' ) return ''; + if ( format === 'amd' ) return getAmdIntro( options.amd, dependencies ); + if ( format === 'cjs' ) return getCjsIntro( dependencies ); + + throw new Error( `Not implemented: ${format}` ); +} + +function getAmdIntro ( options = {}, dependencies ) { + const sourceString = dependencies.length ? + `[ ${dependencies.map( dep => `'${dep.source}'` ).join( ', ' )} ], ` : + ''; + + const paramString = dependencies.length ? ` ${dependencies.map( dep => dep.name ).join( ', ' )} ` : ''; + + return `define(${options.id ? ` '${options.id}', ` : ''}${sourceString}function (${paramString}) { 'use strict';\n\n`; +} + +function getCjsIntro ( dependencies ) { + const requireBlock = dependencies + .map( dep => `var ${dep.name} = require( '${dep.source}' );` ) + .join( '\n\n' ); + + if ( requireBlock ) { + return `'use strict';\n\n${requireBlock}\n\n`; + } + + return `'use strict';\n\n`; +} diff --git a/compiler/generate/utils/getOutro.js b/compiler/generate/utils/getOutro.js new file mode 100644 index 0000000000..c6a7961d75 --- /dev/null +++ b/compiler/generate/utils/getOutro.js @@ -0,0 +1,15 @@ +export default function getOutro ( format, name, imports ) { + if ( format === 'es' ) { + return `export default ${name};`; + } + + if ( format === 'amd' ) { + return `return ${name};\n\n});`; + } + + if ( format === 'cjs' ) { + return `module.exports = ${name};`; + } + + throw new Error( `Not implemented: ${format}` ); +} diff --git a/test/test.js b/test/test.js index 423dce5613..611781d5e1 100644 --- a/test/test.js +++ b/test/test.js @@ -1,4 +1,5 @@ import { compile, parse, validate } from '../dist/svelte.js'; +import deindent from '../compiler/generate/utils/deindent.js'; import assert from 'assert'; import * as path from 'path'; import * as fs from 'fs'; @@ -26,7 +27,77 @@ function exists ( path ) { } } +function env () { + return new Promise( ( fulfil, reject ) => { + jsdom.env( '
', ( err, window ) => { + if ( err ) { + reject( err ); + } else { + global.document = window.document; + fulfil( window ); + } + }); + }); +} + describe( 'svelte', () => { + before( () => { + function cleanChildren ( node ) { + let previous = null; + + [ ...node.childNodes ].forEach( child => { + if ( child.nodeType === 8 ) { + // comment + node.removeChild( child ); + return; + } + + if ( child.nodeType === 3 ) { + child.data = child.data.replace( /\s{2,}/, '\n' ); + + // text + if ( previous && previous.nodeType === 3 ) { + previous.data += child.data; + previous.data = previous.data.replace( /\s{2,}/, '\n' ); + + node.removeChild( child ); + } + } + + else { + cleanChildren( child ); + } + + previous = child; + }); + + // collapse whitespace + if ( node.firstChild && node.firstChild.nodeType === 3 ) { + node.firstChild.data = node.firstChild.data.replace( /^\s+/, '' ); + if ( !node.firstChild.data ) node.removeChild( node.firstChild ); + } + + if ( node.lastChild && node.lastChild.nodeType === 3 ) { + node.lastChild.data = node.lastChild.data.replace( /\s+$/, '' ); + if ( !node.lastChild.data ) node.removeChild( node.lastChild ); + } + } + + return env().then( window => { + assert.htmlEqual = ( actual, expected, message ) => { + window.document.body.innerHTML = actual.trim(); + cleanChildren( window.document.body, '' ); + actual = window.document.body.innerHTML; + + window.document.body.innerHTML = expected.trim(); + cleanChildren( window.document.body, '' ); + expected = window.document.body.innerHTML; + + assert.deepEqual( actual, expected, message ); + }; + }); + }); + describe( 'parse', () => { fs.readdirSync( 'test/parser' ).forEach( dir => { if ( dir[0] === '.' ) return; @@ -125,63 +196,6 @@ describe( 'svelte', () => { }); describe( 'generate', () => { - before( () => { - function cleanChildren ( node ) { - let previous = null; - - [ ...node.childNodes ].forEach( child => { - if ( child.nodeType === 8 ) { - // comment - node.removeChild( child ); - return; - } - - if ( child.nodeType === 3 ) { - child.data = child.data.replace( /\s{2,}/, '\n' ); - - // text - if ( previous && previous.nodeType === 3 ) { - previous.data += child.data; - previous.data = previous.data.replace( /\s{2,}/, '\n' ); - - node.removeChild( child ); - } - } - - else { - cleanChildren( child ); - } - - previous = child; - }); - - // collapse whitespace - if ( node.firstChild && node.firstChild.nodeType === 3 ) { - node.firstChild.data = node.firstChild.data.replace( /^\s+/, '' ); - if ( !node.firstChild.data ) node.removeChild( node.firstChild ); - } - - if ( node.lastChild && node.lastChild.nodeType === 3 ) { - node.lastChild.data = node.lastChild.data.replace( /\s+$/, '' ); - if ( !node.lastChild.data ) node.removeChild( node.lastChild ); - } - } - - return env().then( window => { - assert.htmlEqual = ( actual, expected, message ) => { - window.document.body.innerHTML = actual.trim(); - cleanChildren( window.document.body, '' ); - actual = window.document.body.innerHTML; - - window.document.body.innerHTML = expected.trim(); - cleanChildren( window.document.body, '' ); - expected = window.document.body.innerHTML; - - assert.deepEqual( actual, expected, message ); - }; - }); - }); - function loadConfig ( dir ) { try { return require( `./compiler/${dir}/_config.js` ).default; @@ -194,19 +208,6 @@ describe( 'svelte', () => { } } - function env () { - return new Promise( ( fulfil, reject ) => { - jsdom.env( '
', ( err, window ) => { - if ( err ) { - reject( err ); - } else { - global.document = window.document; - fulfil( window ); - } - }); - }); - } - fs.readdirSync( 'test/compiler' ).forEach( dir => { if ( dir[0] === '.' ) return; @@ -277,4 +278,89 @@ describe( 'svelte', () => { }); }); }); + + describe( 'formats', () => { + describe( 'amd', () => { + it( 'generates an AMD module', () => { + const source = deindent` +
{{answer}}
+ + + `; + + const { code } = compile( source, { + format: 'amd', + 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(); + }); + }); + }); + }); + + describe( 'cjs', () => { + it( 'generates a CommonJS module', () => { + const source = deindent` +
{{answer}}
+ + + `; + + const { code } = compile( source, { + format: 'cjs' + }); + + const fn = new Function( 'module', 'require', code ); + + return env().then( window => { + const module = {}; + const require = id => { + if ( id === 'answer' ) return 42; + }; + + fn( module, require ); + + const SvelteComponent = module.exports; + + const main = window.document.body.querySelector( 'main' ); + const component = new SvelteComponent({ target: main }); + + assert.htmlEqual( main.innerHTML, `
42
` ); + + component.teardown(); + }); + }); + }); + }); });