diff --git a/src/generate/createGenerator.js b/src/generate/createGenerator.js deleted file mode 100644 index 4a6608d967b9..000000000000 --- a/src/generate/createGenerator.js +++ /dev/null @@ -1,234 +0,0 @@ -import MagicString from 'magic-string'; -import CodeBuilder from '../utils/CodeBuilder.js'; -import { walk } from 'estree-walker'; -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 visitors from './visitors/index.js'; -import globalWhitelist from '../utils/globalWhitelist.js'; - -export default function createGenerator ( parsed, source, names ) { - const generator = { - addElement ( name, renderStatement, needsIdentifier = false ) { - const isToplevel = generator.current.localElementDepth === 0; - if ( needsIdentifier || isToplevel ) { - generator.current.builders.init.addLine( - `var ${name} = ${renderStatement};` - ); - - generator.createMountStatement( name ); - } else { - generator.current.builders.init.addLine( - `${generator.current.target}.appendChild( ${renderStatement} );` - ); - } - - if ( isToplevel ) { - generator.current.builders.detach.addLine( - `${name}.parentNode.removeChild( ${name} );` - ); - } - }, - - createMountStatement ( name ) { - if ( generator.current.target === 'target' ) { - generator.current.builders.mount.addLine( - `target.insertBefore( ${name}, anchor );` - ); - } else { - generator.current.builders.init.addLine( - `${generator.current.target}.appendChild( ${name} );` ); - } - }, - - createAnchor ( _name, description = '' ) { - const name = `${_name}_anchor`; - const statement = `document.createComment( ${JSON.stringify( description )} )`; - generator.addElement( name, statement, true ); - return name; - }, - - generateBlock ( node, name ) { - generator.push({ - name, - target: 'target', - localElementDepth: 0, - builders: generator.getBuilders(), - getUniqueName: generator.getUniqueNameMaker() - }); - // walk the children here - node.children.forEach( generator.visit ); - generator.addRenderer( generator.current ); - generator.pop(); - // unset the children, to avoid them being visited again - node.children = []; - }, - - renderers: [], - - addRenderer ( fragment ) { - if ( fragment.autofocus ) { - fragment.builders.init.addLine( `${fragment.autofocus}.focus();` ); - } - - // minor hack – we need to ensure that any {{{triples}}} are detached - // first, so we append normal detach statements to detachRaw - fragment.builders.detachRaw.addBlock( fragment.builders.detach ); - - if ( !fragment.builders.detachRaw.isEmpty() ) { - fragment.builders.teardown.addBlock( deindent` - if ( detach ) { - ${fragment.builders.detachRaw} - } - ` ); - } - - generator.renderers.push( deindent` - function ${fragment.name} ( ${fragment.params}, component ) { - ${fragment.builders.init} - - return { - mount: function ( target, anchor ) { - ${fragment.builders.mount} - }, - - update: function ( changed, ${fragment.params} ) { - ${fragment.builders.update} - }, - - teardown: function ( detach ) { - ${fragment.builders.teardown} - } - }; - } - ` ); - }, - - addSourcemapLocations ( node ) { - walk( node, { - enter ( node ) { - generator.code.addSourcemapLocation( node.start ); - generator.code.addSourcemapLocation( node.end ); - } - }); - }, - - code: new MagicString( source ), - - components: {}, - - contextualise ( expression, isEventHandler ) { - const usedContexts = []; - const dependencies = []; - - const { contextDependencies, contexts, indexes } = generator.current; - - walk( expression, { - enter ( node, parent ) { - if ( isReference( node, parent ) ) { - const { name } = flattenReference( node ); - - if ( parent && parent.type === 'CallExpression' && node === parent.callee && generator.helpers[ name ] ) { - generator.code.prependRight( node.start, `template.helpers.` ); - } - - else if ( name === 'event' && isEventHandler ) { - // noop - } - - else if ( contexts[ name ] ) { - dependencies.push( ...contextDependencies[ name ] ); - if ( !~usedContexts.indexOf( name ) ) usedContexts.push( name ); - } - - else if ( indexes[ name ] ) { - const context = indexes[ name ]; - if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context ); - } - - else { - if ( globalWhitelist[ name ] ) { - generator.code.prependRight( node.start, `( '${name}' in root ? root.` ); - generator.code.appendLeft( node.object.end, ` : ${name} )` ); - } else { - generator.code.prependRight( node.start, `root.` ); - } - - dependencies.push( name ); - if ( !~usedContexts.indexOf( 'root' ) ) usedContexts.push( 'root' ); - } - - this.skip(); - } - } - }); - - return { - dependencies, - contexts: usedContexts, - snippet: `[✂${expression.start}-${expression.end}✂]`, - string: generator.code.slice( expression.start, expression.end ) - }; - }, - - events: {}, - - getBuilders () { - return { - init: new CodeBuilder(), - mount: new CodeBuilder(), - update: new CodeBuilder(), - detach: new CodeBuilder(), - detachRaw: new CodeBuilder(), - teardown: new CodeBuilder() - }; - }, - - getUniqueName: counter( names ), - - getUniqueNameMaker () { - return counter( names ); - }, - - cssId: parsed.css ? `svelte-${parsed.hash}` : '', - - helpers: {}, - - pop () { - const tail = generator.current; - generator.current = tail.parent; - - return tail; - }, - - push ( fragment ) { - const newFragment = Object.assign( {}, generator.current, fragment, { - parent: generator.current - }); - - generator.current = newFragment; - }, - - usesRefs: false, - - source, - - visit ( node ) { - const visitor = visitors[ node.type ]; - if ( !visitor ) throw new Error( `Not implemented: ${node.type}` ); - - if ( visitor.enter ) visitor.enter( generator, node ); - - if ( node.children ) { - node.children.forEach( child => { - generator.visit( child ); - }); - } - - if ( visitor.leave ) visitor.leave( generator, node ); - } - }; - - return generator; -} diff --git a/src/generators/Generator.js b/src/generators/Generator.js new file mode 100644 index 000000000000..84bd2be36758 --- /dev/null +++ b/src/generators/Generator.js @@ -0,0 +1,319 @@ +import MagicString, { Bundle } from 'magic-string'; +import { walk } from 'estree-walker'; +import isReference from '../utils/isReference.js'; +import counter from './shared/utils/counter.js'; +import flattenReference from '../utils/flattenReference.js'; +import globalWhitelist from '../utils/globalWhitelist.js'; +import getIntro from './shared/utils/getIntro.js'; +import getOutro from './shared/utils/getOutro.js'; + +export default class Generator { + constructor ( parsed, source, names, visitors ) { + this.parsed = parsed; + this.source = source; + this.names = names; + this.visitors = visitors; + + this.imports = []; + this.helpers = {}; + this.components = {}; + this.events = {}; + + this.elementDepth = 0; + + this.code = new MagicString( source ); + this.getUniqueName = counter( names ); + this.cssId = parsed.css ? `svelte-${parsed.hash}` : ''; + this.usesRefs = false; + + this._callbacks = {}; + } + + addSourcemapLocations ( node ) { + walk( node, { + enter: node => { + this.code.addSourcemapLocation( node.start ); + this.code.addSourcemapLocation( node.end ); + } + }); + } + + contextualise ( expression, isEventHandler ) { + const usedContexts = []; + const dependencies = []; + + const { code, helpers } = this; + const { contextDependencies, contexts, indexes } = this.current; + + walk( expression, { + enter ( node, parent ) { + if ( isReference( node, parent ) ) { + const { name } = flattenReference( node ); + + if ( parent && parent.type === 'CallExpression' && node === parent.callee && helpers[ name ] ) { + code.prependRight( node.start, `template.helpers.` ); + } + + else if ( name === 'event' && isEventHandler ) { + // noop + } + + else if ( contexts[ name ] ) { + dependencies.push( ...contextDependencies[ name ] ); + if ( !~usedContexts.indexOf( name ) ) usedContexts.push( name ); + } + + else if ( indexes[ name ] ) { + const context = indexes[ name ]; + if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context ); + } + + else { + if ( globalWhitelist[ name ] ) { + code.prependRight( node.start, `( '${name}' in root ? root.` ); + code.appendLeft( node.object.end, ` : ${name} )` ); + } else { + code.prependRight( node.start, `root.` ); + } + + dependencies.push( name ); + if ( !~usedContexts.indexOf( 'root' ) ) usedContexts.push( 'root' ); + } + + this.skip(); + } + } + }); + + return { + dependencies, + contexts: usedContexts, + snippet: `[✂${expression.start}-${expression.end}✂]`, + string: this.code.slice( expression.start, expression.end ) + }; + } + + fire ( eventName, data ) { + const handlers = eventName in this._callbacks && this._callbacks[ eventName ].slice(); + if ( !handlers ) return; + + for ( let i = 0; i < handlers.length; i += 1 ) { + handlers[i].call( this, data ); + } + } + + generate ( result, options, { name, format } ) { + if ( this.imports.length ) { + const statements = []; + + this.imports.forEach( ( declaration, i ) => { + if ( format === 'es' ) { + statements.push( this.source.slice( declaration.start, declaration.end ) ); + return; + } + + 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 + + namedImports.forEach( specifier => { + statements.push( `var ${specifier.local.name} = ${name}.${specifier.imported.name}` ); + }); + + if ( defaultImport ) { + statements.push( `${name} = ( ${name} && ${name}.__esModule ) ? ${name}['default'] : ${name};` ); + } + }); + + result = `${statements.join( '\n' )}\n\n${result}`; + } + + const pattern = /\[✂(\d+)-(\d+)$/; + + const parts = result.split( '✂]' ); + const finalChunk = parts.pop(); + + const compiled = new Bundle({ separator: '' }); + + function addString ( str ) { + compiled.addSource({ + content: new MagicString( str ) + }); + } + + const intro = getIntro( format, options, this.imports ); + if ( intro ) addString( intro ); + + const { filename } = options; + + parts.forEach( str => { + const chunk = str.replace( pattern, '' ); + if ( chunk ) addString( chunk ); + + const match = pattern.exec( str ); + + const snippet = this.code.snip( +match[1], +match[2] ); + + compiled.addSource({ + filename, + content: snippet + }); + }); + + addString( finalChunk ); + addString( '\n\n' + getOutro( format, name, options, this.imports ) ); + + return { + code: compiled.toString(), + map: compiled.generateMap({ includeContent: true }) + }; + } + + getUniqueNameMaker () { + return counter( this.names ); + } + + parseJs () { + const { source } = this; + const { js } = this.parsed; + + const imports = this.imports; + const computations = []; + const templateProperties = {}; + + if ( js ) { + this.addSourcemapLocations( js.content ); + + // imports need to be hoisted out of the IIFE + for ( let i = 0; i < js.content.body.length; i += 1 ) { + const node = js.content.body[i]; + if ( node.type === 'ImportDeclaration' ) { + let a = node.start; + let b = node.end; + while ( /[ \t]/.test( source[ a - 1 ] ) ) a -= 1; + while ( source[b] === '\n' ) b += 1; + + //imports.push( source.slice( a, b ).replace( /^\s/, '' ) ); + imports.push( node ); + this.code.remove( a, b ); + } + } + + const defaultExport = js.content.body.find( node => node.type === 'ExportDefaultDeclaration' ); + + if ( defaultExport ) { + const finalNode = js.content.body[ js.content.body.length - 1 ]; + if ( defaultExport === finalNode ) { + // export is last property, we can just return it + this.code.overwrite( defaultExport.start, defaultExport.declaration.start, `return ` ); + } else { + // TODO ensure `template` isn't already declared + this.code.overwrite( defaultExport.start, defaultExport.declaration.start, `var template = ` ); + + let i = defaultExport.start; + while ( /\s/.test( source[ i - 1 ] ) ) i--; + + const indentation = source.slice( i, defaultExport.start ); + this.code.appendLeft( finalNode.end, `\n\n${indentation}return template;` ); + } + + defaultExport.declaration.properties.forEach( prop => { + templateProperties[ prop.key.name ] = prop.value; + }); + + this.code.prependRight( js.content.start, 'var template = (function () {' ); + } else { + this.code.prependRight( js.content.start, '(function () {' ); + } + + this.code.appendLeft( js.content.end, '}());' ); + + [ 'helpers', 'events', 'components' ].forEach( key => { + if ( templateProperties[ key ] ) { + templateProperties[ key ].properties.forEach( prop => { + this[ key ][ prop.key.name ] = prop.value; + }); + } + }); + + if ( templateProperties.computed ) { + const dependencies = new Map(); + + templateProperties.computed.properties.forEach( prop => { + const key = prop.key.name; + const value = prop.value; + + const deps = value.params.map( param => param.name ); + dependencies.set( key, deps ); + }); + + const visited = new Set(); + + function visit ( key ) { + if ( !dependencies.has( key ) ) return; // not a computation + + if ( visited.has( key ) ) return; + visited.add( key ); + + const deps = dependencies.get( key ); + deps.forEach( visit ); + + computations.push({ key, deps }); + } + + templateProperties.computed.properties.forEach( prop => visit( prop.key.name ) ); + } + } + + return { + computations, + templateProperties + }; + } + + on ( eventName, handler ) { + const handlers = this._callbacks[ eventName ] || ( this._callbacks[ eventName ] = [] ); + handlers.push( handler ); + } + + pop () { + const tail = this.current; + this.current = tail.parent; + + return tail; + } + + push ( fragment ) { + const newFragment = Object.assign( {}, this.current, fragment, { + parent: this.current + }); + + this.current = newFragment; + } + + visit ( node ) { + const visitor = this.visitors[ node.type ]; + if ( !visitor ) throw new Error( `Not implemented: ${node.type}` ); + + if ( visitor.enter ) visitor.enter( this, node ); + + if ( visitor.type === 'Element' ) { + this.elementDepth += 1; + } + + if ( node.children ) { + node.children.forEach( child => { + this.visit( child ); + }); + } + + if ( visitor.type === 'Element' ) { + this.elementDepth -= 1; + } + + if ( visitor.leave ) visitor.leave( this, node ); + } +} diff --git a/src/generate/index.js b/src/generators/dom/index.js similarity index 54% rename from src/generate/index.js rename to src/generators/dom/index.js index 7a7226be29f3..6c4e40dbd92f 100644 --- a/src/generate/index.js +++ b/src/generators/dom/index.js @@ -1,75 +1,118 @@ -import MagicString, { Bundle } from 'magic-string'; -import CodeBuilder from '../utils/CodeBuilder.js'; -import deindent from '../utils/deindent.js'; -import namespaces from '../utils/namespaces.js'; -import getIntro from './utils/getIntro.js'; -import getOutro from './utils/getOutro.js'; -import processCss from './css/process.js'; -import createGenerator from './createGenerator.js'; - -export default function generate ( parsed, source, options, names ) { - const format = options.format || 'es'; +import deindent from '../../utils/deindent.js'; +import getBuilders from './utils/getBuilders.js'; +import CodeBuilder from '../../utils/CodeBuilder.js'; +import namespaces from '../../utils/namespaces.js'; +import processCss from '../shared/css/process.js'; +import visitors from './visitors/index.js'; +import Generator from '../Generator.js'; + +class DomGenerator extends Generator { + constructor ( parsed, source, names, visitors ) { + super( parsed, source, names, visitors ); + this.renderers = []; + } - const generator = createGenerator( parsed, source, names ); + addElement ( name, renderStatement, needsIdentifier = false ) { + const isToplevel = this.current.localElementDepth === 0; + if ( needsIdentifier || isToplevel ) { + this.current.builders.init.addLine( + `var ${name} = ${renderStatement};` + ); - const templateProperties = {}; - const imports = []; + this.createMountStatement( name ); + } else { + this.current.builders.init.addLine( + `${this.current.target}.appendChild( ${renderStatement} );` + ); + } - if ( parsed.js ) { - generator.addSourcemapLocations( parsed.js.content ); - - // imports need to be hoisted out of the IIFE - for ( let i = 0; i < parsed.js.content.body.length; i += 1 ) { - const node = parsed.js.content.body[i]; - if ( node.type === 'ImportDeclaration' ) { - let a = node.start; - let b = node.end; - while ( /[ \t]/.test( source[ a - 1 ] ) ) a -= 1; - while ( source[b] === '\n' ) b += 1; - - //imports.push( source.slice( a, b ).replace( /^\s/, '' ) ); - imports.push( node ); - generator.code.remove( a, b ); - } + if ( isToplevel ) { + this.current.builders.detach.addLine( + `${name}.parentNode.removeChild( ${name} );` + ); } + } - const defaultExport = parsed.js.content.body.find( node => node.type === 'ExportDefaultDeclaration' ); + addRenderer ( fragment ) { + if ( fragment.autofocus ) { + fragment.builders.init.addLine( `${fragment.autofocus}.focus();` ); + } - if ( defaultExport ) { - const finalNode = parsed.js.content.body[ parsed.js.content.body.length - 1 ]; - if ( defaultExport === finalNode ) { - // export is last property, we can just return it - generator.code.overwrite( defaultExport.start, defaultExport.declaration.start, `return ` ); - } else { - // TODO ensure `template` isn't already declared - generator.code.overwrite( defaultExport.start, defaultExport.declaration.start, `var template = ` ); + // minor hack – we need to ensure that any {{{triples}}} are detached + // first, so we append normal detach statements to detachRaw + fragment.builders.detachRaw.addBlock( fragment.builders.detach ); - let i = defaultExport.start; - while ( /\s/.test( source[ i - 1 ] ) ) i--; + if ( !fragment.builders.detachRaw.isEmpty() ) { + fragment.builders.teardown.addBlock( deindent` + if ( detach ) { + ${fragment.builders.detachRaw} + } + ` ); + } + + this.renderers.push( deindent` + function ${fragment.name} ( ${fragment.params}, component ) { + ${fragment.builders.init} - const indentation = source.slice( i, defaultExport.start ); - generator.code.appendLeft( finalNode.end, `\n\n${indentation}return template;` ); + return { + mount: function ( target, anchor ) { + ${fragment.builders.mount} + }, + + update: function ( changed, ${fragment.params} ) { + ${fragment.builders.update} + }, + + teardown: function ( detach ) { + ${fragment.builders.teardown} + } + }; } + ` ); + } - defaultExport.declaration.properties.forEach( prop => { - templateProperties[ prop.key.name ] = prop.value; - }); + createAnchor ( name, description = '' ) { + const renderStatement = `document.createComment( ${JSON.stringify( description )} )`; + this.addElement( name, renderStatement, true ); + } - generator.code.prependRight( parsed.js.content.start, 'var template = (function () {' ); + createMountStatement ( name ) { + if ( this.current.target === 'target' ) { + this.current.builders.mount.addLine( + `target.insertBefore( ${name}, anchor );` + ); } else { - generator.code.prependRight( parsed.js.content.start, '(function () {' ); + this.current.builders.init.addLine( + `${this.current.target}.appendChild( ${name} );` ); } + } - generator.code.appendLeft( parsed.js.content.end, '}());' ); - - [ 'helpers', 'events', 'components' ].forEach( key => { - if ( templateProperties[ key ] ) { - templateProperties[ key ].properties.forEach( prop => { - generator[ key ][ prop.key.name ] = prop.value; - }); - } + generateBlock ( node, name ) { + this.push({ + name, + target: 'target', + localElementDepth: 0, + builders: getBuilders(), + getUniqueName: this.getUniqueNameMaker() }); + + // walk the children here + node.children.forEach( node => this.visit( node ) ); + this.addRenderer( this.current ); + this.pop(); + + // unset the children, to avoid them being visited again + node.children = []; } +} + +export default function dom ( parsed, source, options, names ) { + const format = options.format || 'es'; + const name = options.name || 'SvelteComponent'; + + const generator = new DomGenerator( parsed, source, names, visitors ); + + const { computations, templateProperties } = generator.parseJs(); let namespace = null; if ( templateProperties.namespace ) { @@ -83,7 +126,6 @@ export default function generate ( parsed, source, options, names ) { name: 'renderMainFragment', namespace, target: 'target', - elementDepth: 0, localElementDepth: 0, contexts: {}, @@ -93,11 +135,11 @@ export default function generate ( parsed, source, options, names ) { indexNames: {}, listNames: {}, - builders: generator.getBuilders(), + builders: getBuilders(), getUniqueName: generator.getUniqueNameMaker() }); - parsed.html.children.forEach( generator.visit ); + parsed.html.children.forEach( node => generator.visit( node ) ); generator.addRenderer( generator.pop() ); @@ -110,37 +152,16 @@ export default function generate ( parsed, source, options, names ) { builders.set.addLine( 'var oldState = state;' ); builders.set.addLine( 'state = Object.assign( {}, oldState, newState );' ); - if ( templateProperties.computed ) { + if ( computations.length ) { const builder = new CodeBuilder(); - const dependencies = new Map(); - - templateProperties.computed.properties.forEach( prop => { - const key = prop.key.name; - const value = prop.value; - - const deps = value.params.map( param => param.name ); - dependencies.set( key, deps ); - }); - - const visited = new Set(); - - function visit ( key ) { - if ( !dependencies.has( key ) ) return; // not a computation - - if ( visited.has( key ) ) return; - visited.add( key ); - - const deps = dependencies.get( key ); - deps.forEach( visit ); + computations.forEach( ({ key, deps }) => { builder.addBlock( deindent` if ( ${deps.map( dep => `( '${dep}' in newState && typeof state.${dep} === 'object' || state.${dep} !== oldState.${dep} )` ).join( ' || ' )} ) { state.${key} = newState.${key} = template.computed.${key}( ${deps.map( dep => `state.${dep}` ).join( ', ' )} ); } ` ); - } - - templateProperties.computed.properties.forEach( prop => visit( prop.key.name ) ); + }); builders.main.addBlock( deindent` function applyComputations ( state, newState, oldState ) { @@ -157,28 +178,6 @@ export default function generate ( parsed, source, options, names ) { dispatchObservers( observers.deferred, newState, oldState ); ` ); - imports.forEach( ( declaration, i ) => { - if ( format === 'es' ) { - builders.main.addLine( source.slice( declaration.start, declaration.end ) ); - return; - } - - 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 - - namedImports.forEach( specifier => { - builders.main.addLine( `var ${specifier.local.name} = ${name}.${specifier.imported.name}` ); - }); - - if ( defaultImport ) { - builders.main.addLine( `${name} = ( ${name} && ${name}.__esModule ) ? ${name}['default'] : ${name};` ); - } - }); - if ( parsed.js ) { builders.main.addBlock( `[✂${parsed.js.content.start}-${parsed.js.content.end}✂]` ); } @@ -199,8 +198,6 @@ export default function generate ( parsed, source, options, names ) { let i = generator.renderers.length; while ( i-- ) builders.main.addBlock( generator.renderers[i] ); - const constructorName = options.name || 'SvelteComponent'; - if ( parsed.css && options.css !== false ) { builders.init.addLine( `if ( !addedCss ) addCss();` ); } @@ -250,7 +247,7 @@ export default function generate ( parsed, source, options, names ) { const initialState = templateProperties.data ? `Object.assign( template.data(), options.data )` : `options.data || {}`; builders.main.addBlock( deindent` - function ${constructorName} ( options ) { + function ${name} ( options ) { options = options || {}; var component = this;${generator.usesRefs ? `\nthis.refs = {}` : ``} @@ -355,48 +352,8 @@ export default function generate ( parsed, source, options, names ) { ` ); if ( templateProperties.methods ) { - builders.main.addBlock( `${constructorName}.prototype = template.methods;` ); + builders.main.addBlock( `${name}.prototype = template.methods;` ); } - const result = builders.main.toString(); - - const pattern = /\[✂(\d+)-(\d+)$/; - - const parts = result.split( '✂]' ); - const finalChunk = parts.pop(); - - const compiled = new Bundle({ separator: '' }); - - function addString ( str ) { - compiled.addSource({ - content: new MagicString( str ) - }); - } - - const intro = getIntro( format, options, imports ); - if ( intro ) addString( intro ); - - const { filename } = options; - - parts.forEach( str => { - const chunk = str.replace( pattern, '' ); - if ( chunk ) addString( chunk ); - - const match = pattern.exec( str ); - - const snippet = generator.code.snip( +match[1], +match[2] ); - - compiled.addSource({ - filename, - content: snippet - }); - }); - - addString( finalChunk ); - addString( '\n\n' + getOutro( format, constructorName, options, imports ) ); - - return { - code: compiled.toString(), - map: compiled.generateMap({ includeContent: true }) - }; + return generator.generate( builders.main.toString(), options, { name, format } ); } diff --git a/src/generators/dom/utils/getBuilders.js b/src/generators/dom/utils/getBuilders.js new file mode 100644 index 000000000000..ca66c1bec5ea --- /dev/null +++ b/src/generators/dom/utils/getBuilders.js @@ -0,0 +1,12 @@ +import CodeBuilder from '../../../utils/CodeBuilder.js'; + +export default function getBuilders () { + return { + init: new CodeBuilder(), + mount: new CodeBuilder(), + update: new CodeBuilder(), + detach: new CodeBuilder(), + detachRaw: new CodeBuilder(), + teardown: new CodeBuilder() + }; +} diff --git a/src/generate/visitors/Comment.js b/src/generators/dom/visitors/Comment.js similarity index 100% rename from src/generate/visitors/Comment.js rename to src/generators/dom/visitors/Comment.js diff --git a/src/generate/visitors/Component.js b/src/generators/dom/visitors/Component.js similarity index 96% rename from src/generate/visitors/Component.js rename to src/generators/dom/visitors/Component.js index c48bf29a420c..113417c2feb4 100644 --- a/src/generate/visitors/Component.js +++ b/src/generators/dom/visitors/Component.js @@ -1,5 +1,5 @@ -import deindent from '../../utils/deindent.js'; -import CodeBuilder from '../../utils/CodeBuilder.js'; +import deindent from '../../../utils/deindent.js'; +import CodeBuilder from '../../../utils/CodeBuilder.js'; import addComponentAttributes from './attributes/addComponentAttributes.js'; export default { @@ -117,7 +117,6 @@ export default { namespace: local.namespace, target: name, parent: generator.current, - elementDepth: generator.current.elementDepth + 1, localElementDepth: generator.current.localElementDepth + 1 }); }, diff --git a/src/generate/visitors/EachBlock.js b/src/generators/dom/visitors/EachBlock.js similarity index 94% rename from src/generate/visitors/EachBlock.js rename to src/generators/dom/visitors/EachBlock.js index 0c6a97672827..b0ec2113e026 100644 --- a/src/generate/visitors/EachBlock.js +++ b/src/generators/dom/visitors/EachBlock.js @@ -1,4 +1,5 @@ -import deindent from '../../utils/deindent.js'; +import deindent from '../../../utils/deindent.js'; +import getBuilders from '../utils/getBuilders.js'; export default { enter ( generator, node ) { @@ -18,7 +19,8 @@ export default { const { dependencies, snippet } = generator.contextualise( node.expression ); - const anchor = generator.createAnchor( name, `#each ${generator.source.slice( node.expression.start, node.expression.end )}` ); + const anchor = `${name}_anchor`; + generator.createAnchor( anchor, `#each ${generator.source.slice( node.expression.start, node.expression.end )}` ); generator.current.builders.init.addBlock( deindent` var ${name}_value = ${snippet}; @@ -136,7 +138,7 @@ export default { listNames, params: blockParams, - builders: generator.getBuilders(), + builders: getBuilders(), getUniqueName: generator.getUniqueNameMaker() }); diff --git a/src/generate/visitors/Element.js b/src/generators/dom/visitors/Element.js similarity index 93% rename from src/generate/visitors/Element.js rename to src/generators/dom/visitors/Element.js index 163378640e11..67759ad60a1f 100644 --- a/src/generate/visitors/Element.js +++ b/src/generators/dom/visitors/Element.js @@ -1,5 +1,5 @@ -import CodeBuilder from '../../utils/CodeBuilder.js'; -import deindent from '../../utils/deindent.js'; +import CodeBuilder from '../../../utils/CodeBuilder.js'; +import deindent from '../../../utils/deindent.js'; import addElementAttributes from './attributes/addElementAttributes.js'; import Component from './Component.js'; @@ -61,7 +61,7 @@ export default { `var ${name} = document.createElementNS( '${local.namespace}', '${node.name}' );` : `var ${name} = document.createElement( '${node.name}' );`; - if ( generator.cssId && !generator.current.elementDepth ) { + if ( generator.cssId && !generator.elementDepth ) { render += `\n${name}.setAttribute( '${generator.cssId}', '' );`; } @@ -88,7 +88,6 @@ export default { namespace: local.namespace, target: name, parent: generator.current, - elementDepth: generator.current.elementDepth + 1, localElementDepth: generator.current.localElementDepth + 1 }); }, diff --git a/src/generate/visitors/IfBlock.js b/src/generators/dom/visitors/IfBlock.js similarity index 90% rename from src/generate/visitors/IfBlock.js rename to src/generators/dom/visitors/IfBlock.js index bf4cd9695368..02133e539d60 100644 --- a/src/generate/visitors/IfBlock.js +++ b/src/generators/dom/visitors/IfBlock.js @@ -1,4 +1,4 @@ -import deindent from '../../utils/deindent.js'; +import deindent from '../../../utils/deindent.js'; function getConditionsAndBlocks ( generator, node, _name, i = 0 ) { generator.addSourcemapLocations( node.expression ); @@ -8,19 +8,22 @@ function getConditionsAndBlocks ( generator, node, _name, i = 0 ) { condition: generator.contextualise( node.expression ).snippet, block: name }]; + generator.generateBlock( node, name ); if ( node.else && node.else.children.length === 1 && node.else.children[0].type === 'IfBlock' ) { conditionsAndBlocks.push( - ...getConditionsAndBlocks( generator, node.else.children[0], _name, i + 1 ) ); + ...getConditionsAndBlocks( generator, node.else.children[0], _name, i + 1 ) + ); } else { const name = `${_name}_${i + 1}`; conditionsAndBlocks.push({ condition: null, block: node.else ? name : null, }); - if (node.else) { + + if ( node.else ) { generator.generateBlock( node.else, name ); } } @@ -37,7 +40,8 @@ export default { const isToplevel = generator.current.localElementDepth === 0; const conditionsAndBlocks = getConditionsAndBlocks( generator, node, generator.getUniqueName( `renderIfBlock` ) ); - const anchor = generator.createAnchor( name, `#if ${generator.source.slice( node.expression.start, node.expression.end )}` ); + const anchor = `${name}_anchor`; + generator.createAnchor( anchor, `#if ${generator.source.slice( node.expression.start, node.expression.end )}` ); generator.current.builders.init.addBlock( deindent` function ${getBlock} ( ${params} ) { diff --git a/src/generate/visitors/MustacheTag.js b/src/generators/dom/visitors/MustacheTag.js similarity index 88% rename from src/generate/visitors/MustacheTag.js rename to src/generators/dom/visitors/MustacheTag.js index 0222d714b691..f956a7be0839 100644 --- a/src/generate/visitors/MustacheTag.js +++ b/src/generators/dom/visitors/MustacheTag.js @@ -1,4 +1,4 @@ -import deindent from '../../utils/deindent.js'; +import deindent from '../../../utils/deindent.js'; export default { enter ( generator, node ) { diff --git a/src/generate/visitors/RawMustacheTag.js b/src/generators/dom/visitors/RawMustacheTag.js similarity index 96% rename from src/generate/visitors/RawMustacheTag.js rename to src/generators/dom/visitors/RawMustacheTag.js index d90e1d7d01e4..191b3c8a9d99 100644 --- a/src/generate/visitors/RawMustacheTag.js +++ b/src/generators/dom/visitors/RawMustacheTag.js @@ -1,4 +1,4 @@ -import deindent from '../../utils/deindent.js'; +import deindent from '../../../utils/deindent.js'; export default { enter ( generator, node ) { @@ -11,6 +11,7 @@ export default { // exists for `Element`s. const before = `${name}_before`; generator.addElement( before, `document.createElement( 'noscript' )`, true ); + const after = `${name}_after`; generator.addElement( after, `document.createElement( 'noscript' )`, true ); diff --git a/src/generate/visitors/Text.js b/src/generators/dom/visitors/Text.js similarity index 87% rename from src/generate/visitors/Text.js rename to src/generators/dom/visitors/Text.js index 758c9c464800..fcc9f9b447ff 100644 --- a/src/generate/visitors/Text.js +++ b/src/generators/dom/visitors/Text.js @@ -5,6 +5,6 @@ export default { } const name = generator.current.getUniqueName( `text` ); - generator.addElement( name, `document.createTextNode( ${JSON.stringify( node.data )} )` ); + generator.addElement( name, `document.createTextNode( ${JSON.stringify( node.data )} )`, false ); } }; diff --git a/src/generate/visitors/YieldTag.js b/src/generators/dom/visitors/YieldTag.js similarity index 79% rename from src/generate/visitors/YieldTag.js rename to src/generators/dom/visitors/YieldTag.js index 68c850b66bbb..a2ac7f57d7b4 100644 --- a/src/generate/visitors/YieldTag.js +++ b/src/generators/dom/visitors/YieldTag.js @@ -1,6 +1,7 @@ export default { enter ( generator ) { - const anchor = generator.createAnchor( 'yield', 'yield' ); + const anchor = `yield_anchor`; + generator.createAnchor( anchor, 'yield' ); generator.current.builders.mount.addLine( `component.yield && component.yield.mount( ${generator.current.target}, ${anchor} );` diff --git a/src/generate/visitors/attributes/addComponentAttributes.js b/src/generators/dom/visitors/attributes/addComponentAttributes.js similarity index 98% rename from src/generate/visitors/attributes/addComponentAttributes.js rename to src/generators/dom/visitors/attributes/addComponentAttributes.js index e5c9fec98420..d1c70a574a26 100644 --- a/src/generate/visitors/attributes/addComponentAttributes.js +++ b/src/generators/dom/visitors/attributes/addComponentAttributes.js @@ -1,5 +1,5 @@ import createBinding from './binding/index.js'; -import deindent from '../../../utils/deindent.js'; +import deindent from '../../../../utils/deindent.js'; export default function addComponentAttributes ( generator, node, local ) { local.staticAttributes = []; diff --git a/src/generate/visitors/attributes/addElementAttributes.js b/src/generators/dom/visitors/attributes/addElementAttributes.js similarity index 98% rename from src/generate/visitors/attributes/addElementAttributes.js rename to src/generators/dom/visitors/attributes/addElementAttributes.js index ec5a67eb7b5f..71adfa3c08c7 100644 --- a/src/generate/visitors/attributes/addElementAttributes.js +++ b/src/generators/dom/visitors/attributes/addElementAttributes.js @@ -1,7 +1,7 @@ import attributeLookup from './lookup.js'; import createBinding from './binding/index.js'; -import deindent from '../../../utils/deindent.js'; -import flattenReference from '../../../utils/flattenReference.js'; +import deindent from '../../../../utils/deindent.js'; +import flattenReference from '../../../../utils/flattenReference.js'; export default function addElementAttributes ( generator, node, local ) { node.attributes.forEach( attribute => { diff --git a/src/generate/visitors/attributes/binding/index.js b/src/generators/dom/visitors/attributes/binding/index.js similarity index 95% rename from src/generate/visitors/attributes/binding/index.js rename to src/generators/dom/visitors/attributes/binding/index.js index 585beda5e480..04113576646e 100644 --- a/src/generate/visitors/attributes/binding/index.js +++ b/src/generators/dom/visitors/attributes/binding/index.js @@ -1,6 +1,6 @@ -import deindent from '../../../../utils/deindent.js'; -import isReference from '../../../../utils/isReference.js'; -import flattenReference from '../../../../utils/flattenReference.js'; +import deindent from '../../../../../utils/deindent.js'; +import isReference from '../../../../../utils/isReference.js'; +import flattenReference from '../../../../../utils/flattenReference.js'; export default function createBinding ( generator, node, attribute, current, local ) { const parts = attribute.value.split( '.' ); diff --git a/src/generate/visitors/attributes/lookup.js b/src/generators/dom/visitors/attributes/lookup.js similarity index 100% rename from src/generate/visitors/attributes/lookup.js rename to src/generators/dom/visitors/attributes/lookup.js diff --git a/src/generate/visitors/index.js b/src/generators/dom/visitors/index.js similarity index 100% rename from src/generate/visitors/index.js rename to src/generators/dom/visitors/index.js diff --git a/src/generators/server-side-rendering/index.js b/src/generators/server-side-rendering/index.js new file mode 100644 index 000000000000..d74c720036bb --- /dev/null +++ b/src/generators/server-side-rendering/index.js @@ -0,0 +1,129 @@ +import deindent from '../../utils/deindent.js'; +import CodeBuilder from '../../utils/CodeBuilder.js'; +import processCss from '../shared/css/process.js'; +import visitors from './visitors/index.js'; +import Generator from '../Generator.js'; + +class SsrGenerator extends Generator { + constructor ( parsed, source, names, visitors ) { + super( parsed, source, names, visitors ); + this.renderCode = ''; + } + + append ( code ) { + this.renderCode += code; + } +} + +export default function ssr ( parsed, source, options, names ) { + const format = options.format || 'cjs'; + const name = options.name || 'SvelteComponent'; + + const generator = new SsrGenerator( parsed, source, names, visitors ); + + const { computations, templateProperties } = generator.parseJs(); + + const builders = { + main: new CodeBuilder(), + render: new CodeBuilder(), + renderCss: new CodeBuilder() + }; + + // create main render() function + generator.push({ + contexts: {}, + indexes: {} + }); + + parsed.html.children.forEach( node => generator.visit( node ) ); + + builders.render.addLine( + templateProperties.data ? `root = Object.assign( template.data(), root || {} );` : `root = root || {};` + ); + + computations.forEach( ({ key, deps }) => { + builders.render.addLine( + `root.${key} = template.computed.${key}( ${deps.map( dep => `root.${dep}` ).join( ', ' )} );` + ); + }); + + builders.render.addBlock( + `return \`${generator.renderCode}\`;` + ); + + // create renderCss() function + builders.renderCss.addBlock( + `var components = [];` + ); + + if ( parsed.css ) { + builders.renderCss.addBlock( deindent` + components.push({ + filename: ${name}.filename, + css: ${JSON.stringify( processCss( parsed ) )}, + map: null // TODO + }); + ` ); + } + + if ( templateProperties.components ) { + builders.renderCss.addBlock( deindent` + var seen = {}; + + function addComponent ( component ) { + var result = component.renderCss(); + result.components.forEach( x => { + if ( seen[ x.filename ] ) return; + seen[ x.filename ] = true; + components.push( x ); + }); + } + ` ); + + templateProperties.components.properties.forEach( prop => { + builders.renderCss.addLine( `addComponent( template.components.${prop.key.name} );` ); + }); + } + + builders.renderCss.addBlock( deindent` + return { + css: components.map( x => x.css ).join( '\\n' ), + map: null, + components + }; + ` ); + + if ( parsed.js ) { + builders.main.addBlock( `[✂${parsed.js.content.start}-${parsed.js.content.end}✂]` ); + } + + builders.main.addBlock( deindent` + var ${name} = {}; + + ${name}.filename = ${JSON.stringify( options.filename )}; + + ${name}.render = function ( root, options ) { + ${builders.render} + }; + + ${name}.renderCss = function () { + ${builders.renderCss} + }; + + var escaped = { + '"': '"', + "'": '&39;', + '&': '&', + '<': '<', + '>': '>' + }; + + function __escape ( html ) { + return String( html ).replace( /["'&<>]/g, match => escaped[ match ] ); + } + ` ); + + const result = builders.main.toString(); + + return generator.generate( result, options, { name, format } ); +} diff --git a/src/generators/server-side-rendering/visitors/Comment.js b/src/generators/server-side-rendering/visitors/Comment.js new file mode 100644 index 000000000000..f32a1e64a8bd --- /dev/null +++ b/src/generators/server-side-rendering/visitors/Comment.js @@ -0,0 +1,3 @@ +export default { + // do nothing +}; diff --git a/src/generators/server-side-rendering/visitors/Component.js b/src/generators/server-side-rendering/visitors/Component.js new file mode 100644 index 000000000000..e97ecaa5dd05 --- /dev/null +++ b/src/generators/server-side-rendering/visitors/Component.js @@ -0,0 +1,46 @@ +export default { + enter ( generator, node ) { + function stringify ( chunk ) { + if ( chunk.type === 'Text' ) return chunk.data; + if ( chunk.type === 'MustacheTag' ) { + const { snippet } = generator.contextualise( chunk.expression ); + return '${__escape( ' + snippet + ')}'; + } + } + + const props = node.attributes.map( attribute => { + let value; + + if ( attribute.value === true ) { + value = `true`; + } else if ( attribute.value.length === 0 ) { + value = `''`; + } else if ( attribute.value.length === 1 ) { + const chunk = attribute.value[0]; + if ( chunk.type === 'Text' ) { + value = isNaN( parseFloat( chunk.data ) ) ? JSON.stringify( chunk.data ) : chunk.data; + } else { + const { snippet } = generator.contextualise( chunk.expression ); + value = snippet; + } + } else { + value = '`' + attribute.value.map( stringify ).join( '' ) + '`'; + } + + return `${attribute.name}: ${value}`; + }).join( ', ' ); + + let open = `\${template.components.${node.name}.render({${props}}`; + + if ( node.children.length ) { + open += `, { yield: () => \``; + } + + generator.append( open ); + }, + + leave ( generator, node ) { + const close = node.children.length ? `\` })}` : ')}'; + generator.append( close ); + } +}; diff --git a/src/generators/server-side-rendering/visitors/EachBlock.js b/src/generators/server-side-rendering/visitors/EachBlock.js new file mode 100644 index 000000000000..de4bfffe6de0 --- /dev/null +++ b/src/generators/server-side-rendering/visitors/EachBlock.js @@ -0,0 +1,32 @@ +export default { + enter ( generator, node ) { + const { dependencies, snippet } = generator.contextualise( node.expression ); + + const open = `\${ ${snippet}.map( ${ node.index ? `( ${node.context}, ${node.index} )` : node.context} => \``; + generator.append( open ); + + // TODO should this be the generator's job? It's duplicated between + // here and the equivalent DOM compiler visitor + const contexts = Object.assign( {}, generator.current.contexts ); + contexts[ node.context ] = true; + + const indexes = Object.assign( {}, generator.current.indexes ); + if ( node.index ) indexes[ node.index ] = node.context; + + const contextDependencies = Object.assign( {}, generator.current.contextDependencies ); + contextDependencies[ node.context ] = dependencies; + + generator.push({ + contexts, + indexes, + contextDependencies + }); + }, + + leave ( generator ) { + const close = `\` ).join( '' )}`; + generator.append( close ); + + generator.pop(); + } +}; diff --git a/src/generators/server-side-rendering/visitors/Element.js b/src/generators/server-side-rendering/visitors/Element.js new file mode 100644 index 000000000000..863abf27db38 --- /dev/null +++ b/src/generators/server-side-rendering/visitors/Element.js @@ -0,0 +1,51 @@ +import Component from './Component.js'; +import voidElementNames from '../../../utils/voidElementNames.js'; + +export default { + enter ( generator, node ) { + if ( node.name in generator.components ) { + Component.enter( generator, node ); + return; + } + + let openingTag = `<${node.name}`; + + node.attributes.forEach( attribute => { + if ( attribute.type !== 'Attribute' ) return; + + let str = ` ${attribute.name}`; + + if ( attribute.value !== true ) { + str += `="` + attribute.value.map( chunk => { + if ( chunk.type === 'Text' ) { + return chunk.data; + } + + const { snippet } = generator.contextualise( chunk.expression ); + return '${' + snippet + '}'; + }).join( '' ) + `"`; + } + + openingTag += str; + }); + + if ( generator.cssId && !generator.elementDepth ) { + openingTag += ` ${generator.cssId}`; + } + + openingTag += '>'; + + generator.append( openingTag ); + }, + + leave ( generator, node ) { + if ( node.name in generator.components ) { + Component.leave( generator, node ); + return; + } + + if ( !voidElementNames.test( node.name ) ) { + generator.append( `` ); + } + } +}; diff --git a/src/generators/server-side-rendering/visitors/IfBlock.js b/src/generators/server-side-rendering/visitors/IfBlock.js new file mode 100644 index 000000000000..a473965afdc5 --- /dev/null +++ b/src/generators/server-side-rendering/visitors/IfBlock.js @@ -0,0 +1,12 @@ +export default { + enter ( generator, node ) { + const { snippet } = generator.contextualise( node.expression ); + generator.append( '${ ' + snippet + ' ? `' ); + }, + + leave ( generator, node ) { + generator.append( '` : `' ); + if ( node.else ) node.else.children.forEach( child => generator.visit( child ) ); + generator.append( '` }' ); + } +}; diff --git a/src/generators/server-side-rendering/visitors/MustacheTag.js b/src/generators/server-side-rendering/visitors/MustacheTag.js new file mode 100644 index 000000000000..9052e2627fc3 --- /dev/null +++ b/src/generators/server-side-rendering/visitors/MustacheTag.js @@ -0,0 +1,6 @@ +export default { + enter ( generator, node ) { + const { snippet } = generator.contextualise( node.expression ); + generator.append( '${__escape( ' + snippet + ' )}' ); + } +}; diff --git a/src/generators/server-side-rendering/visitors/RawMustacheTag.js b/src/generators/server-side-rendering/visitors/RawMustacheTag.js new file mode 100644 index 000000000000..c84de16e2acb --- /dev/null +++ b/src/generators/server-side-rendering/visitors/RawMustacheTag.js @@ -0,0 +1,6 @@ +export default { + enter ( generator, node ) { + const { snippet } = generator.contextualise( node.expression ); + generator.append( '${' + snippet + '}' ); + } +}; diff --git a/src/generators/server-side-rendering/visitors/Text.js b/src/generators/server-side-rendering/visitors/Text.js new file mode 100644 index 000000000000..16630a709d3d --- /dev/null +++ b/src/generators/server-side-rendering/visitors/Text.js @@ -0,0 +1,5 @@ +export default { + enter ( generator, node ) { + generator.append( node.data.replace( /\${/g, '\\${' ) ); + } +}; diff --git a/src/generators/server-side-rendering/visitors/YieldTag.js b/src/generators/server-side-rendering/visitors/YieldTag.js new file mode 100644 index 000000000000..732f96301b06 --- /dev/null +++ b/src/generators/server-side-rendering/visitors/YieldTag.js @@ -0,0 +1,5 @@ +export default { + enter ( generator ) { + generator.append( `\${options.yield()}` ); + } +}; diff --git a/src/generators/server-side-rendering/visitors/index.js b/src/generators/server-side-rendering/visitors/index.js new file mode 100644 index 000000000000..8e125e5d8b29 --- /dev/null +++ b/src/generators/server-side-rendering/visitors/index.js @@ -0,0 +1,19 @@ +import Comment from './Comment.js'; +import EachBlock from './EachBlock.js'; +import Element from './Element.js'; +import IfBlock from './IfBlock.js'; +import MustacheTag from './MustacheTag.js'; +import RawMustacheTag from './RawMustacheTag.js'; +import Text from './Text.js'; +import YieldTag from './YieldTag.js'; + +export default { + Comment, + EachBlock, + Element, + IfBlock, + MustacheTag, + RawMustacheTag, + Text, + YieldTag +}; diff --git a/src/generate/css/process.js b/src/generators/shared/css/process.js similarity index 100% rename from src/generate/css/process.js rename to src/generators/shared/css/process.js diff --git a/src/generate/css/transform.js b/src/generators/shared/css/transform.js similarity index 100% rename from src/generate/css/transform.js rename to src/generators/shared/css/transform.js diff --git a/src/generate/utils/counter.js b/src/generators/shared/utils/counter.js similarity index 100% rename from src/generate/utils/counter.js rename to src/generators/shared/utils/counter.js diff --git a/src/generate/utils/getGlobals.js b/src/generators/shared/utils/getGlobals.js similarity index 100% rename from src/generate/utils/getGlobals.js rename to src/generators/shared/utils/getGlobals.js diff --git a/src/generate/utils/getIntro.js b/src/generators/shared/utils/getIntro.js similarity index 97% rename from src/generate/utils/getIntro.js rename to src/generators/shared/utils/getIntro.js index 3d1e3b9e5d2c..ce0f5525b8a4 100644 --- a/src/generate/utils/getIntro.js +++ b/src/generators/shared/utils/getIntro.js @@ -1,4 +1,4 @@ -import deindent from '../../utils/deindent.js'; +import deindent from '../../../utils/deindent.js'; import getGlobals from './getGlobals.js'; export default function getIntro ( format, options, imports ) { diff --git a/src/generate/utils/getOutro.js b/src/generators/shared/utils/getOutro.js similarity index 100% rename from src/generate/utils/getOutro.js rename to src/generators/shared/utils/getOutro.js diff --git a/src/generate/utils/walkHtml.js b/src/generators/shared/utils/walkHtml.js similarity index 100% rename from src/generate/utils/walkHtml.js rename to src/generators/shared/utils/walkHtml.js diff --git a/src/index.js b/src/index.js index 79a5665dce84..dc2bcefe6a5f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ import parse from './parse/index.js'; import validate from './validate/index.js'; -import generate from './generate/index.js'; -import generateSSR from './server-side-rendering/compile.js'; +import generate from './generators/dom/index.js'; +import generateSSR from './generators/server-side-rendering/index.js'; function normalizeOptions ( options ) { return Object.assign( { diff --git a/src/parse/state/tag.js b/src/parse/state/tag.js index c7902535a007..c3fa1a122258 100644 --- a/src/parse/state/tag.js +++ b/src/parse/state/tag.js @@ -4,9 +4,9 @@ import readStyle from '../read/style.js'; import { readEventHandlerDirective, readBindingDirective } from '../read/directives.js'; import { trimStart, trimEnd } from '../utils/trim.js'; import { decodeCharacterReferences } from '../utils/html.js'; +import voidElementNames from '../../utils/voidElementNames.js'; const validTagName = /^[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/; -const voidElementNames = /^(?:area|base|br|col|command|doctype|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/; const invalidUnquotedAttributeCharacters = /[\s"'=<>\/`]/; const specials = { diff --git a/src/server-side-rendering/compile.js b/src/server-side-rendering/compile.js deleted file mode 100644 index 6bfcfd58e72e..000000000000 --- a/src/server-side-rendering/compile.js +++ /dev/null @@ -1,435 +0,0 @@ -import { walk } from 'estree-walker'; -import deindent from '../utils/deindent.js'; -import isReference from '../utils/isReference.js'; -import flattenReference from '../utils/flattenReference.js'; -import MagicString, { Bundle } from 'magic-string'; -import processCss from '../generate/css/process.js'; - -const voidElementNames = /^(?:area|base|br|col|command|doctype|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i; - -export default function compile ( parsed, source, { filename }) { - const code = new MagicString( source ); - - const templateProperties = {}; - const components = {}; - const helpers = {}; - - const imports = []; - - if ( parsed.js ) { - walk( parsed.js.content, { - enter ( node ) { - code.addSourcemapLocation( node.start ); - code.addSourcemapLocation( node.end ); - } - }); - - // imports need to be hoisted out of the IIFE - for ( let i = 0; i < parsed.js.content.body.length; i += 1 ) { - const node = parsed.js.content.body[i]; - if ( node.type === 'ImportDeclaration' ) { - let a = node.start; - let b = node.end; - while ( /[ \t]/.test( source[ a - 1 ] ) ) a -= 1; - while ( source[b] === '\n' ) b += 1; - - //imports.push( source.slice( a, b ).replace( /^\s/, '' ) ); - imports.push( node ); - code.remove( a, b ); - } - } - - const defaultExport = parsed.js.content.body.find( node => node.type === 'ExportDefaultDeclaration' ); - - if ( defaultExport ) { - const finalNode = parsed.js.content.body[ parsed.js.content.body.length - 1 ]; - if ( defaultExport === finalNode ) { - // export is last property, we can just return it - code.overwrite( defaultExport.start, defaultExport.declaration.start, `return ` ); - } else { - // TODO ensure `template` isn't already declared - code.overwrite( defaultExport.start, defaultExport.declaration.start, `var template = ` ); - - let i = defaultExport.start; - while ( /\s/.test( source[ i - 1 ] ) ) i--; - - const indentation = source.slice( i, defaultExport.start ); - code.appendLeft( finalNode.end, `\n\n${indentation}return template;` ); - } - - defaultExport.declaration.properties.forEach( prop => { - templateProperties[ prop.key.name ] = prop.value; - }); - - code.prependRight( parsed.js.content.start, 'var template = (function () {' ); - } else { - code.prependRight( parsed.js.content.start, '(function () {' ); - } - - code.appendLeft( parsed.js.content.end, '}());' ); - - if ( templateProperties.helpers ) { - templateProperties.helpers.properties.forEach( prop => { - helpers[ prop.key.name ] = prop.value; - }); - } - - if ( templateProperties.components ) { - templateProperties.components.properties.forEach( prop => { - components[ prop.key.name ] = prop.value; - }); - } - } - - let scope = new Set(); - const scopes = [ scope ]; - - function contextualise ( expression ) { - walk( expression, { - enter ( node, parent ) { - if ( isReference( node, parent ) ) { - const { name } = flattenReference( node ); - - if ( parent && parent.type === 'CallExpression' && node === parent.callee && helpers[ name ] ) { - code.prependRight( node.start, `template.helpers.` ); - return; - } - - if ( !scope.has( name ) ) { - code.prependRight( node.start, `data.` ); - } - - this.skip(); - } - } - }); - - return { - snippet: `[✂${expression.start}-${expression.end}✂]`, - string: code.slice( expression.start, expression.end ) - }; - } - - let elementDepth = 0; - - const stringifiers = { - Comment () { - return ''; - }, - - Component ( node ) { - const props = node.attributes.map( attribute => { - let value; - - if ( attribute.value === true ) { - value = `true`; - } else if ( attribute.value.length === 0 ) { - value = `''`; - } else if ( attribute.value.length === 1 ) { - const chunk = attribute.value[0]; - if ( chunk.type === 'Text' ) { - value = isNaN( parseFloat( chunk.data ) ) ? JSON.stringify( chunk.data ) : chunk.data; - } else { - const { snippet } = contextualise( chunk.expression ); - value = snippet; - } - } else { - value = '`' + attribute.value.map( stringify ).join( '' ) + '`'; - } - - return `${attribute.name}: ${value}`; - }).join( ', ' ); - - let params = `{${props}}`; - - if ( node.children.length ) { - params += `, { yield: () => \`${node.children.map( stringify ).join( '' )}\` }`; - } - - return `\${template.components.${node.name}.render(${params})}`; - }, - - EachBlock ( node ) { - const { snippet } = contextualise( node.expression ); - - scope = new Set(); - scope.add( node.context ); - if ( node.index ) scope.add( node.index ); - - scopes.push( scope ); - - const block = `\${ ${snippet}.map( ${ node.index ? `( ${node.context}, ${node.index} )` : node.context} => \`${ node.children.map( stringify ).join( '' )}\` ).join( '' )}`; - - scopes.pop(); - scope = scopes[ scopes.length - 1 ]; - - return block; - }, - - Element ( node ) { - if ( node.name in components ) { - return stringifiers.Component( node ); - } - - let element = `<${node.name}`; - - node.attributes.forEach( attribute => { - if ( attribute.type !== 'Attribute' ) return; - - let str = ` ${attribute.name}`; - - if ( attribute.value !== true ) { - str += `="` + attribute.value.map( chunk => { - if ( chunk.type === 'Text' ) { - return chunk.data; - } - - const { snippet } = contextualise( chunk.expression ); - return '${' + snippet + '}'; - }).join( '' ) + `"`; - } - - element += str; - }); - - if ( parsed.css && elementDepth === 0 ) { - element += ` svelte-${parsed.hash}`; - } - - if ( voidElementNames.test( node.name ) ) { - element += '>'; - } else { - elementDepth += 1; - element += '>' + node.children.map( stringify ).join( '' ) + ``; - elementDepth -= 1; - } - - return element; - }, - - IfBlock ( node ) { - const { snippet } = contextualise( node.expression ); // TODO use snippet, for sourcemap support - - const consequent = node.children.map( stringify ).join( '' ); - const alternate = node.else ? node.else.children.map( stringify ).join( '' ) : ''; - - return '${ ' + snippet + ' ? `' + consequent + '` : `' + alternate + '` }'; - }, - - MustacheTag ( node ) { - const { snippet } = contextualise( node.expression ); // TODO use snippet, for sourcemap support - return '${__escape( String( ' + snippet + ') )}'; - }, - - RawMustacheTag ( node ) { - const { snippet } = contextualise( node.expression ); // TODO use snippet, for sourcemap support - return '${' + snippet + '}'; - }, - - Text ( node ) { - return node.data.replace( /\${/g, '\\${' ); - }, - - YieldTag () { - return `\${options.yield()}`; - } - }; - - function stringify ( node ) { - const stringifier = stringifiers[ node.type ]; - - if ( !stringifier ) { - throw new Error( `Not implemented: ${node.type}` ); - } - - return stringifier( node ); - } - - function createBlock ( node ) { - const str = stringify( node ); - if ( str.slice( 0, 2 ) === '${' ) return str.slice( 2, -1 ); - return '`' + str + '`'; - } - - const blocks = parsed.html.children.map( node => { - return deindent` - rendered += ${createBlock( node )}; - `; - }); - - const topLevelStatements = []; - - const importBlock = imports - .map( ( declaration, i ) => { - 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}`; - - const statements = [ - `var ${name} = require( '${declaration.source.value}' );` - ]; - - namedImports.forEach( specifier => { - statements.push( `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( importBlock ); - } - - topLevelStatements.push( `[✂${parsed.js.content.start}-${parsed.js.content.end}✂]` ); - } - - const renderStatements = [ - templateProperties.data ? `data = Object.assign( template.data(), data || {} );` : `data = data || {};` - ]; - - if ( templateProperties.computed ) { - const statements = []; - const dependencies = new Map(); - - templateProperties.computed.properties.forEach( prop => { - const key = prop.key.name; - const value = prop.value; - - const deps = value.params.map( param => param.name ); - dependencies.set( key, deps ); - }); - - const visited = new Set(); - - function visit ( key ) { - if ( !dependencies.has( key ) ) return; // not a computation - - if ( visited.has( key ) ) return; - visited.add( key ); - - const deps = dependencies.get( key ); - deps.forEach( visit ); - - statements.push( deindent` - data.${key} = template.computed.${key}( ${deps.map( dep => `data.${dep}` ).join( ', ' )} ); - ` ); - } - - templateProperties.computed.properties.forEach( prop => visit( prop.key.name ) ); - - renderStatements.push( statements.join( '\n' ) ); - } - - renderStatements.push( - `var rendered = '';`, - blocks.join( '\n\n' ), - `return rendered;` - ); - - const renderCssStatements = [ - `var components = [];` - ]; - - if ( parsed.css ) { - renderCssStatements.push( deindent` - components.push({ - filename: exports.filename, - css: ${JSON.stringify( processCss( parsed ) )}, - map: null // TODO - }); - ` ); - } - - if ( templateProperties.components ) { - renderCssStatements.push( deindent` - var seen = {}; - - function addComponent ( component ) { - var result = component.renderCss(); - result.components.forEach( x => { - if ( seen[ x.filename ] ) return; - seen[ x.filename ] = true; - components.push( x ); - }); - } - ` ); - - renderCssStatements.push( templateProperties.components.properties.map( prop => `addComponent( template.components.${prop.key.name} );` ).join( '\n' ) ); - } - - renderCssStatements.push( deindent` - return { - css: components.map( x => x.css ).join( '\\n' ), - map: null, - components - }; - ` ); - - topLevelStatements.push( deindent` - exports.filename = ${JSON.stringify( filename )}; - - exports.render = function ( data, options ) { - ${renderStatements.join( '\n\n' )} - }; - - exports.renderCss = function () { - ${renderCssStatements.join( '\n\n' )} - }; - - var escaped = { - '"': '"', - "'": '&39;', - '&': '&', - '<': '<', - '>': '>' - }; - - function __escape ( html ) { - return html.replace( /["'&<>]/g, match => escaped[ match ] ); - } - ` ); - - const rendered = topLevelStatements.join( '\n\n' ); - - const pattern = /\[✂(\d+)-(\d+)$/; - - const parts = rendered.split( '✂]' ); - const finalChunk = parts.pop(); - - const compiled = new Bundle({ separator: '' }); - - function addString ( str ) { - compiled.addSource({ - content: new MagicString( str ) - }); - } - - parts.forEach( str => { - const chunk = str.replace( pattern, '' ); - if ( chunk ) addString( chunk ); - - const match = pattern.exec( str ); - - const snippet = code.snip( +match[1], +match[2] ); - - compiled.addSource({ - filename, - content: snippet - }); - }); - - addString( finalChunk ); - - return { - code: compiled.toString() - }; -} diff --git a/src/utils/voidElementNames.js b/src/utils/voidElementNames.js new file mode 100644 index 000000000000..7df1d452b6a0 --- /dev/null +++ b/src/utils/voidElementNames.js @@ -0,0 +1 @@ +export default /^(?:area|base|br|col|command|doctype|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/; diff --git a/test/server-side-rendering/comment/_actual.html b/test/server-side-rendering/comment/_actual.html index bc1da9408455..e2adc073dcbc 100644 --- a/test/server-side-rendering/comment/_actual.html +++ b/test/server-side-rendering/comment/_actual.html @@ -1,3 +1,3 @@

before

- -

after

\ No newline at end of file + +

after

\ No newline at end of file diff --git a/test/server-side-rendering/component-data-dynamic/_actual.html b/test/server-side-rendering/component-data-dynamic/_actual.html index 97800c17aaff..6aedef46ee3c 100644 --- a/test/server-side-rendering/component-data-dynamic/_actual.html +++ b/test/server-side-rendering/component-data-dynamic/_actual.html @@ -1,4 +1,4 @@

foo: lol

-

baz: 42 (number)

-

qux: this is a piece of string

-

quux: core

\ No newline at end of file +

baz: 42 (number)

+

qux: this is a piece of string

+

quux: core

\ No newline at end of file diff --git a/test/server-side-rendering/component-data-static/_actual.html b/test/server-side-rendering/component-data-static/_actual.html index 9502567ec585..15a2100d4ad9 100644 --- a/test/server-side-rendering/component-data-static/_actual.html +++ b/test/server-side-rendering/component-data-static/_actual.html @@ -1,2 +1,2 @@

foo: bar

-

baz: 42 (number)

\ No newline at end of file +

baz: 42 (number)

\ No newline at end of file diff --git a/test/server-side-rendering/computed/_actual.html b/test/server-side-rendering/computed/_actual.html index f5a05c288231..099eebe604f8 100644 --- a/test/server-side-rendering/computed/_actual.html +++ b/test/server-side-rendering/computed/_actual.html @@ -1,2 +1,2 @@

1 + 2 = 3

-

3 * 3 = 9

\ No newline at end of file +

3 * 3 = 9

\ No newline at end of file diff --git a/test/server-side-rendering/empty-elements-closed/_actual.html b/test/server-side-rendering/empty-elements-closed/_actual.html index 76a9da4e6ea4..643f5b6b4945 100644 --- a/test/server-side-rendering/empty-elements-closed/_actual.html +++ b/test/server-side-rendering/empty-elements-closed/_actual.html @@ -1,2 +1,2 @@ -

\ No newline at end of file +

\ No newline at end of file diff --git a/test/server-side-rendering/import-non-component/_actual.html b/test/server-side-rendering/import-non-component/_actual.html index e3d27c8e3133..893a7b890bbe 100644 --- a/test/server-side-rendering/import-non-component/_actual.html +++ b/test/server-side-rendering/import-non-component/_actual.html @@ -1,2 +1,2 @@
i got 99 problems
-
the answer is 42
\ No newline at end of file +
the answer is 42
\ No newline at end of file diff --git a/test/server-side-rendering/styles-nested/_actual.html b/test/server-side-rendering/styles-nested/_actual.html index 91c42ef47f76..1962e544544a 100644 --- a/test/server-side-rendering/styles-nested/_actual.html +++ b/test/server-side-rendering/styles-nested/_actual.html @@ -1,5 +1,5 @@
red
-
green: foo
-
blue: foo
-
green: bar
-
blue: bar
\ No newline at end of file +
green: foo
+
blue: foo
+
green: bar
+
blue: bar
\ No newline at end of file diff --git a/test/ssr.js b/test/ssr.js index b9856259227d..828b10c797c2 100644 --- a/test/ssr.js +++ b/test/ssr.js @@ -1,7 +1,7 @@ import assert from 'assert'; import * as fs from 'fs'; -import { exists, tryToLoadJson } from './helpers.js'; +import { exists, setupHtmlEqual, tryToLoadJson } from './helpers.js'; function tryToReadFile ( file ) { try { @@ -17,6 +17,8 @@ describe( 'ssr', () => { require( process.env.COVERAGE ? '../src/server-side-rendering/register.js' : '../ssr/register' ); + + return setupHtmlEqual(); }); fs.readdirSync( 'test/server-side-rendering' ).forEach( dir => {