diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index 5a8faea178..f8b0446a68 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -1,3 +1,4 @@ +import { parseExpressionAt } from 'acorn'; import MagicString, { Bundle } from 'magic-string'; import isReference from 'is-reference'; import { walk, childKeys } from 'estree-walker'; @@ -16,6 +17,7 @@ import getName from '../utils/getName'; import Stylesheet from '../css/Stylesheet'; import { test } from '../config'; import Fragment from './nodes/Fragment'; +import shared from './dom/shared'; // TODO move this file import { Node, GenerateOptions, ShorthandImport, Ast, CompileOptions, CustomElementOptions } from '../interfaces'; interface Computation { @@ -210,10 +212,84 @@ export default class Generator { return this.aliases.get(name); } - generate(result: string, options: CompileOptions, { banner = '', sharedPath, helpers, name, format }: GenerateOptions ) { + generate(result: string, options: CompileOptions, { banner = '', helpers, name, format }: GenerateOptions ) { const pattern = /\[✂(\d+)-(\d+)$/; - const module = wrapModule(result, format, name, options, banner, sharedPath, helpers, this.imports, this.shorthandImports, this.source); + let importedHelpers; + + if (options.shared) { + if (format !== 'es' && format !== 'cjs') { + throw new Error(`Components with shared helpers must be compiled with \`format: 'es'\` or \`format: 'cjs'\``); + } + + importedHelpers = Array.from(helpers).sort().map(name => { + const alias = this.alias(name); + return { name, alias }; + }); + } else { + let inlineHelpers = ''; + + const compiler = this; + + importedHelpers = []; + + helpers.forEach(key => { + const str = shared[key]; + const code = new MagicString(str); + const expression = parseExpressionAt(str, 0); + + let { scope } = annotateWithScopes(expression); + + walk(expression, { + enter(node: Node, parent: Node) { + if (node._scope) scope = node._scope; + + if ( + node.type === 'Identifier' && + isReference(node, parent) && + !scope.has(node.name) + ) { + if (node.name in shared) { + // this helper function depends on another one + const dependency = node.name; + helpers.add(dependency); + + const alias = compiler.alias(dependency); + if (alias !== node.name) { + code.overwrite(node.start, node.end, alias); + } + } + } + }, + + leave(node: Node) { + if (node._scope) scope = scope.parent; + }, + }); + + if (key === 'transitionManager') { + // special case + const global = `_svelteTransitionManager`; + + inlineHelpers += `\n\nvar ${this.alias('transitionManager')} = window.${global} || (window.${global} = ${code});\n\n`; + } else { + const alias = this.alias(expression.id.name); + if (alias !== expression.id.name) { + code.overwrite(expression.id.start, expression.id.end, alias); + } + + inlineHelpers += `\n\n${code}`; + } + }); + + result += inlineHelpers; + } + + const sharedPath = options.shared === true + ? 'svelte/shared.js' + : options.shared || ''; + + const module = wrapModule(result, format, name, options, banner, sharedPath, importedHelpers, this.imports, this.shorthandImports, this.source); const parts = module.split('✂]'); const finalChunk = parts.pop(); diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts index 56bf7cca8d..b3ae1581d8 100644 --- a/src/generators/dom/index.ts +++ b/src/generators/dom/index.ts @@ -361,7 +361,7 @@ export default function dom( ${immutable && `${name}.prototype._differs = @_differsImmutable;`} `); - const usedHelpers = new Set(); + const helpers = new Set(); let result = builder .toString() @@ -369,7 +369,7 @@ export default function dom( if (sigil === '@') { if (name in shared) { if (options.dev && `${name}Dev` in shared) name = `${name}Dev`; - usedHelpers.add(name); + helpers.add(name); } return generator.alias(name); @@ -382,70 +382,6 @@ export default function dom( return sigil.slice(1) + name; }); - let helpers; - - if (sharedPath) { - if (format !== 'es' && format !== 'cjs') { - throw new Error(`Components with shared helpers must be compiled with \`format: 'es'\` or \`format: 'cjs'\``); - } - const used = Array.from(usedHelpers).sort(); - helpers = used.map(name => { - const alias = generator.alias(name); - return { name, alias }; - }); - } else { - let inlineHelpers = ''; - - usedHelpers.forEach(key => { - const str = shared[key]; - const code = new MagicString(str); - const expression = parseExpressionAt(str, 0); - - let { scope } = annotateWithScopes(expression); - - walk(expression, { - enter(node: Node, parent: Node) { - if (node._scope) scope = node._scope; - - if ( - node.type === 'Identifier' && - isReference(node, parent) && - !scope.has(node.name) - ) { - if (node.name in shared) { - // this helper function depends on another one - const dependency = node.name; - usedHelpers.add(dependency); - - const alias = generator.alias(dependency); - if (alias !== node.name) - code.overwrite(node.start, node.end, alias); - } - } - }, - - leave(node: Node) { - if (node._scope) scope = scope.parent; - }, - }); - - if (key === 'transitionManager') { - // special case - const global = `_svelteTransitionManager`; - - inlineHelpers += `\n\nvar ${generator.alias('transitionManager')} = window.${global} || (window.${global} = ${code});\n\n`; - } else { - const alias = generator.alias(expression.id.name); - if (alias !== expression.id.name) - code.overwrite(expression.id.start, expression.id.end, alias); - - inlineHelpers += `\n\n${code}`; - } - }); - - result += inlineHelpers; - } - const filename = options.filename && ( typeof process !== 'undefined' ? options.filename.replace(process.cwd(), '').replace(/^[\/\\]/, '') : options.filename ); diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts index 5c34939387..9e56deb2bf 100644 --- a/src/generators/nodes/Element.ts +++ b/src/generators/nodes/Element.ts @@ -868,7 +868,7 @@ export default class Element extends Node { } }); - openingTag += "${__spread([" + args.join(', ') + "])}"; + openingTag += "${@spread([" + args.join(', ') + "])}"; } else { this.attributes.forEach((attribute: Node) => { if (attribute.type !== 'Attribute') return; diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts index 02e722b3d0..dfbd68259b 100644 --- a/src/generators/server-side-rendering/index.ts +++ b/src/generators/server-side-rendering/index.ts @@ -93,6 +93,8 @@ export default function ssr( initialState.push('ctx'); + const helpers = new Set(); + // TODO concatenate CSS maps const result = deindent` ${generator.javascript} @@ -206,31 +208,17 @@ export default function ssr( }; ` } - - ${ - /__spread/.test(generator.renderCode) && deindent` - function __spread(args) { - const attributes = Object.assign({}, ...args); - let str = ''; - - Object.keys(attributes).forEach(name => { - const value = attributes[name]; - if (value === undefined) return; - if (value === true) str += " " + name; - str += " " + name + "=" + JSON.stringify(value); - }); - - return str; - } - ` - } `.replace(/(@+|#+|%+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => { - if (sigil === '@') return generator.alias(name); + if (sigil === '@') { + helpers.add(name); + return generator.alias(name); + } + if (sigil === '%') return generator.templateVars.get(name); return sigil.slice(1) + name; }); - return generator.generate(result, options, { name, format }); + return generator.generate(result, options, { name, format, helpers }); } function trim(nodes) { diff --git a/src/generators/wrapModule.ts b/src/generators/wrapModule.ts index d8fee62f0b..65b63c2077 100644 --- a/src/generators/wrapModule.ts +++ b/src/generators/wrapModule.ts @@ -90,7 +90,7 @@ function es( shorthandImports: ShorthandImport[], source: string ) { - const importHelpers = helpers && ( + const importHelpers = helpers.length > 0 && ( `import { ${helpers.map(h => h.name === h.alias ? h.name : `${h.name} as ${h.alias}`).join(', ')} } from ${JSON.stringify(sharedPath)};` ); @@ -145,11 +145,9 @@ function cjs( helpers: { name: string, alias: string }[], dependencies: Dependency[] ) { - const helperDeclarations = helpers && ( - helpers.map(h => `${h.alias === h.name ? h.name : `${h.name}: ${h.alias}`}`).join(', ') - ); + const helperDeclarations = helpers.map(h => `${h.alias === h.name ? h.name : `${h.name}: ${h.alias}`}`).join(', '); - const helperBlock = helpers && ( + const helperBlock = helpers.length > 0 && ( `var { ${helperDeclarations} } = require(${JSON.stringify(sharedPath)});\n` ); diff --git a/src/interfaces.ts b/src/interfaces.ts index 414b1f368a..2405e81758 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -71,7 +71,7 @@ export interface GenerateOptions { format: ModuleFormat; banner?: string; sharedPath?: string; - helpers?: { name: string, alias: string }[]; + helpers: Set; } export interface ShorthandImport { diff --git a/src/shared/ssr.js b/src/shared/ssr.js new file mode 100644 index 0000000000..c2253f6d23 --- /dev/null +++ b/src/shared/ssr.js @@ -0,0 +1,13 @@ +export function spread(args) { + const attributes = Object.assign({}, ...args); + let str = ''; + + Object.keys(attributes).forEach(name => { + const value = attributes[name]; + if (value === undefined) return; + if (value === true) str += " " + name; + str += " " + name + "=" + JSON.stringify(value); + }); + + return str; +} \ No newline at end of file