diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..229c704b9b --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,19 @@ + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..d2882acf4a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ + diff --git a/CHANGELOG.md b/CHANGELOG.md index c9f6d9f1ad..d0d494f90e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Svelte changelog +## 1.2.2 + +* Omit directives in server-side rendering ([#163](https://github.com/sveltejs/svelte/issues/167)) +* Handle comments in SSR ([#165](https://github.com/sveltejs/svelte/issues/165)) +* Support calling methods of `event`/`this` in event handlers ([#162](https://github.com/sveltejs/svelte/issues/162)) +* Remove `mount` from public API ([#150](https://github.com/sveltejs/svelte/issues/150)) + +## 1.2.1 + +* Server-side rendering is available as a compiler option (`generate: 'ssr'`) ([#159](https://github.com/sveltejs/svelte/pull/159)) +* Allow call expressions where function is not in `helpers` ([#163](https://github.com/sveltejs/svelte/issues/163)) + ## 1.2.0 * Server-side rendering of HTML ([#148](https://github.com/sveltejs/svelte/pull/148)) and CSS ([#154](https://github.com/sveltejs/svelte/pull/154)) diff --git a/README.md b/README.md index 2e6a2bcff1..669bc0b3b5 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ const { code, map } = svelte.compile( source, { * [charpeni/svelte-example](https://github.com/charpeni/svelte-example) - Some Svelte examples with configured Rollup, Babel, ESLint, directives, Two-Way binding, and nested components * [EmilTholin/svelte-test](https://github.com/EmilTholin/svelte-test) +* [lukechinworth/codenames](https://github.com/lukechinworth/codenames/tree/svelte) – example integration with Redux ## License diff --git a/package.json b/package.json index 41de5e2370..8494bbb5ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "1.2.0", + "version": "1.2.2", "description": "The magical disappearing UI framework", "main": "compiler/svelte.js", "files": [ diff --git a/rollup.config.main.js b/rollup.config.main.js index 8ae1ff8baa..122843f984 100644 --- a/rollup.config.main.js +++ b/rollup.config.main.js @@ -12,5 +12,8 @@ export default { commonjs() ], external: [ 'magic-string' ], + globals: { + 'magic-string': 'MagicString' + }, sourceMap: true }; diff --git a/rollup.config.ssr.js b/rollup.config.ssr.js index 912aaecc3f..988554341d 100644 --- a/rollup.config.ssr.js +++ b/rollup.config.ssr.js @@ -12,7 +12,7 @@ export default { nodeResolve({ jsnext: true, module: true }), commonjs() ], - external: [ path.resolve( 'src/index.js' ), 'magic-string' ], + external: [ path.resolve( 'src/index.js' ), 'fs', 'magic-string' ], paths: { [ path.resolve( 'src/index.js' ) ]: '../compiler/svelte.js' }, diff --git a/src/generate/index.js b/src/generate/index.js index 93dcf79bd6..28d893387d 100644 --- a/src/generate/index.js +++ b/src/generate/index.js @@ -5,6 +5,7 @@ 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 namespaces from '../utils/namespaces.js'; import getIntro from './utils/getIntro.js'; import getOutro from './utils/getOutro.js'; import visitors from './visitors/index.js'; @@ -138,8 +139,8 @@ export default function generate ( parsed, source, options, names ) { if ( isReference( node, parent ) ) { const { name } = flattenReference( node ); - if ( parent && parent.type === 'CallExpression' && node === parent.callee ) { - if ( generator.helpers[ name ] ) generator.code.prependRight( node.start, `template.helpers.` ); + if ( parent && parent.type === 'CallExpression' && node === parent.callee && generator.helpers[ name ] ) { + generator.code.prependRight( node.start, `template.helpers.` ); return; } @@ -278,9 +279,17 @@ export default function generate ( parsed, source, options, names ) { }); } + let namespace = null; + if ( templateProperties.namespace ) { + const ns = templateProperties.namespace.value; + namespace = namespaces[ ns ] || ns; + + // TODO remove the namespace property from the generated code, it's unused past this point + } + generator.push({ name: 'renderMainFragment', - namespace: null, + namespace, target: 'target', elementDepth: 0, localElementDepth: 0, @@ -417,7 +426,7 @@ export default function generate ( parsed, source, options, names ) { builders.init.addBlock( deindent` this.__bindings = []; var mainFragment = renderMainFragment( state, this ); - if ( options.target ) this.mount( options.target ); + if ( options.target ) this._mount( options.target ); while ( this.__bindings.length ) this.__bindings.pop()(); ` ); @@ -425,7 +434,7 @@ export default function generate ( parsed, source, options, names ) { } else { builders.init.addBlock( deindent` var mainFragment = renderMainFragment( state, this ); - if ( options.target ) this.mount( options.target ); + if ( options.target ) this._mount( options.target ); ` ); } @@ -507,7 +516,7 @@ export default function generate ( parsed, source, options, names ) { ${builders.set} }; - this.mount = function mount ( target, anchor ) { + this._mount = function mount ( target, anchor ) { mainFragment.mount( target, anchor ); } @@ -580,8 +589,7 @@ export default function generate ( parsed, source, options, names ) { const intro = getIntro( format, options, imports ); if ( intro ) addString( intro ); - // a filename is necessary for sourcemap generation - const filename = options.filename || 'SvelteComponent.html'; + const { filename } = options; parts.forEach( str => { const chunk = str.replace( pattern, '' ); diff --git a/src/generate/visitors/Component.js b/src/generate/visitors/Component.js index 348caed3a8..168b4a1e21 100644 --- a/src/generate/visitors/Component.js +++ b/src/generate/visitors/Component.js @@ -80,7 +80,7 @@ export default { ` ); if ( isToplevel ) { - local.mount.unshift( `${name}.mount( target, anchor );` ); + local.mount.unshift( `${name}._mount( target, anchor );` ); } if ( local.dynamicAttributes.length ) { diff --git a/src/generate/visitors/attributes/addElementAttributes.js b/src/generate/visitors/attributes/addElementAttributes.js index 532835732c..4873d2d637 100644 --- a/src/generate/visitors/attributes/addElementAttributes.js +++ b/src/generate/visitors/attributes/addElementAttributes.js @@ -1,6 +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'; export default function addElementAttributes ( generator, node, local ) { node.attributes.forEach( attribute => { @@ -114,7 +115,12 @@ export default function addElementAttributes ( generator, node, local ) { else if ( attribute.type === 'EventHandler' ) { // TODO verify that it's a valid callee (i.e. built-in or declared method) generator.addSourcemapLocations( attribute.expression ); - generator.code.prependRight( attribute.expression.start, 'component.' ); + + const flattened = flattenReference( attribute.expression.callee ); + if ( flattened.name !== 'event' && flattened.name !== 'this' ) { + // allow event.stopPropagation(), this.select() etc + generator.code.prependRight( attribute.expression.start, 'component.' ); + } const usedContexts = new Set(); attribute.expression.arguments.forEach( arg => { diff --git a/src/index.js b/src/index.js index 01d0685b9c..173f532037 100644 --- a/src/index.js +++ b/src/index.js @@ -1,23 +1,36 @@ 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'; -export function compile ( source, options = {} ) { - const parsed = parse( source, options ); +function normalizeOptions ( options ) { + return Object.assign( { + generate: 'dom', + + // a filename is necessary for sourcemap generation + filename: 'SvelteComponent.html', - if ( !options.onwarn ) { - options.onwarn = warning => { + onwarn: warning => { 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 } - }; - } + } + }, options ); +} + +export function compile ( source, _options ) { + const options = normalizeOptions( _options ); + const parsed = parse( source, options ); const { names } = validate( parsed, source, options ); - return generate( parsed, source, options, names ); + const compiler = options.generate === 'ssr' + ? generateSSR + : generate; + + return compiler( parsed, source, options, names ); } export { parse, validate }; diff --git a/src/server-side-rendering/compile.js b/src/server-side-rendering/compile.js index 38fdab5502..47ccae587a 100644 --- a/src/server-side-rendering/compile.js +++ b/src/server-side-rendering/compile.js @@ -1,4 +1,3 @@ -import { parse, validate } from '../index.js'; import { walk } from 'estree-walker'; import deindent from '../utils/deindent.js'; import isReference from '../utils/isReference.js'; @@ -8,10 +7,7 @@ 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 ( source, { filename }) { - const parsed = parse( source, {} ); - validate( parsed, source, {} ); - +export default function compile ( parsed, source, { filename }) { const code = new MagicString( source ); const templateProperties = {}; @@ -117,6 +113,10 @@ export default function compile ( source, { filename }) { let elementDepth = 0; const stringifiers = { + Comment () { + return ''; + }, + Component ( node ) { const props = node.attributes.map( attribute => { let value; @@ -174,6 +174,8 @@ export default function compile ( source, { filename }) { let element = `<${node.name}`; node.attributes.forEach( attribute => { + if ( attribute.type !== 'Attribute' ) return; + let str = ` ${attribute.name}`; if ( attribute.value !== true ) { diff --git a/src/server-side-rendering/register.js b/src/server-side-rendering/register.js index 3b147c8b88..cb17b9a710 100644 --- a/src/server-side-rendering/register.js +++ b/src/server-side-rendering/register.js @@ -1,7 +1,10 @@ import * as fs from 'fs'; -import compile from './compile.js'; +import { compile } from '../index.js'; require.extensions[ '.html' ] = function ( module, filename ) { - const { code } = compile( fs.readFileSync( filename, 'utf-8' ), { filename }); + const { code } = compile( fs.readFileSync( filename, 'utf-8' ), { + filename, + generate: 'ssr' + }); return module._compile( code, filename ); }; diff --git a/src/utils/flattenReference.js b/src/utils/flattenReference.js index 2000c8941a..c975c65095 100644 --- a/src/utils/flattenReference.js +++ b/src/utils/flattenReference.js @@ -7,10 +7,10 @@ export default function flatten ( node ) { node = node.object; } - if ( node.type !== 'Identifier' ) return null; + const name = node.type === 'Identifier' ? node.name : node.type === 'ThisExpression' ? 'this' : null; - const name = node.name; - parts.unshift( name ); + if ( !name ) return null; + parts.unshift( name ); return { name, keypath: parts.join( '.' ) }; } diff --git a/src/utils/namespaces.js b/src/utils/namespaces.js new file mode 100644 index 0000000000..2ab0b116f0 --- /dev/null +++ b/src/utils/namespaces.js @@ -0,0 +1,8 @@ +export const html = 'http://www.w3.org/1999/xhtml'; +export const mathml = 'http://www.w3.org/1998/Math/MathML'; +export const svg = 'http://www.w3.org/2000/svg'; +export const xlink = 'http://www.w3.org/1999/xlink'; +export const xml = 'http://www.w3.org/XML/1998/namespace'; +export const xmlns = 'http://www.w3.org/2000/xmlns'; + +export default { html, mathml, svg, xlink, xml, xmlns }; diff --git a/src/validate/html/index.js b/src/validate/html/index.js index d7d7378fbd..a7feafc42b 100644 --- a/src/validate/html/index.js +++ b/src/validate/html/index.js @@ -1,13 +1,31 @@ +import * as namespaces from '../../utils/namespaces.js'; + +const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|switch|symbol|text|textPath|title|tref|tspan|unknown|use|view|vkern)$/; + export default function validateHtml ( validator, html ) { + let elementDepth = 0; + function visit ( node ) { if ( node.type === 'EachBlock' ) { if ( !~validator.names.indexOf( node.context ) ) validator.names.push( node.context ); if ( node.index && !~validator.names.indexOf( node.index ) ) validator.names.push( node.index ); } + if ( node.type === 'Element' ) { + if ( elementDepth === 0 && validator.namespace !== namespaces.svg && svg.test( node.name ) ) { + validator.warn( `<${node.name}> is an SVG element – did you forget to add { namespace: 'svg' } ?`, node.start ); + } + + elementDepth += 1; + } + if ( node.children ) { node.children.forEach( visit ); } + + if ( node.type === 'Element' ) { + elementDepth -= 1; + } } html.children.forEach( visit ); diff --git a/src/validate/index.js b/src/validate/index.js index 89811421e5..47f440cd4a 100644 --- a/src/validate/index.js +++ b/src/validate/index.js @@ -40,17 +40,19 @@ export default function validate ( parsed, source, options ) { templateProperties: {}, - names: [] - }; + names: [], - if ( parsed.html ) { - validateHtml( validator, parsed.html ); - } + namespace: null + }; if ( parsed.js ) { validateJs( validator, parsed.js ); } + if ( parsed.html ) { + validateHtml( validator, parsed.html ); + } + return { names: validator.names }; diff --git a/src/validate/js/index.js b/src/validate/js/index.js index 026a304309..60e34e111b 100644 --- a/src/validate/js/index.js +++ b/src/validate/js/index.js @@ -2,6 +2,7 @@ import propValidators from './propValidators/index.js'; import FuzzySet from './utils/FuzzySet.js'; import checkForDupes from './utils/checkForDupes.js'; import checkForComputedKeys from './utils/checkForComputedKeys.js'; +import namespaces from '../../utils/namespaces.js'; const validPropList = Object.keys( propValidators ); @@ -29,7 +30,7 @@ export default function validateJs ( validator, js ) { checkForDupes( validator, validator.defaultExport.declaration.properties ); validator.defaultExport.declaration.properties.forEach( prop => { - validator.templateProperties[ prop.key.value ] = prop; + validator.templateProperties[ prop.key.name ] = prop; }); validator.defaultExport.declaration.properties.forEach( prop => { @@ -48,5 +49,10 @@ export default function validateJs ( validator, js ) { } } }); + + if ( validator.templateProperties.namespace ) { + const ns = validator.templateProperties.namespace.value.value; + validator.namespace = namespaces[ ns ] || ns; + } } } diff --git a/src/validate/js/propValidators/index.js b/src/validate/js/propValidators/index.js index dcbf004fff..655df8d3cc 100644 --- a/src/validate/js/propValidators/index.js +++ b/src/validate/js/propValidators/index.js @@ -6,6 +6,7 @@ import helpers from './helpers.js'; import methods from './methods.js'; import components from './components.js'; import events from './events.js'; +import namespace from './namespace.js'; export default { data, @@ -15,5 +16,6 @@ export default { helpers, methods, components, - events + events, + namespace }; diff --git a/src/validate/js/propValidators/namespace.js b/src/validate/js/propValidators/namespace.js new file mode 100644 index 0000000000..ca664261ef --- /dev/null +++ b/src/validate/js/propValidators/namespace.js @@ -0,0 +1,5 @@ +export default function namespace ( validator, prop ) { + if ( prop.value.type !== 'Literal' || typeof prop.value.value !== 'string' ) { + validator.error( `The 'namespace' property must be a string literal representing a valid namespace`, prop.start ); + } +} diff --git a/test/generator/computed-function/_config.js b/test/generator/computed-function/_config.js new file mode 100644 index 0000000000..018eb9f3f0 --- /dev/null +++ b/test/generator/computed-function/_config.js @@ -0,0 +1,14 @@ +export default { + html: '
50
', + + test ( assert, component, target ) { + component.set({ range: [ 50, 100 ] }); + assert.htmlEqual( target.innerHTML, '75
' ); + + component.set({ range: [ 50, 60 ] }); + assert.htmlEqual( target.innerHTML, '55
' ); + + component.set({ x: 8 }); + assert.htmlEqual( target.innerHTML, '58
' ); + } +}; diff --git a/test/generator/computed-function/main.html b/test/generator/computed-function/main.html new file mode 100644 index 0000000000..f3fc14727d --- /dev/null +++ b/test/generator/computed-function/main.html @@ -0,0 +1,20 @@ +{{scale(x)}}
+ + diff --git a/test/generator/default-data-function/_config.js b/test/generator/default-data-function/_config.js new file mode 100644 index 0000000000..94658e5450 --- /dev/null +++ b/test/generator/default-data-function/_config.js @@ -0,0 +1,8 @@ +export default { + html: 'before
+ +after
\ No newline at end of file diff --git a/test/server-side-rendering/comment/_expected.html b/test/server-side-rendering/comment/_expected.html new file mode 100644 index 0000000000..ba0d202ece --- /dev/null +++ b/test/server-side-rendering/comment/_expected.html @@ -0,0 +1,2 @@ +before
+after
diff --git a/test/server-side-rendering/comment/main.html b/test/server-side-rendering/comment/main.html new file mode 100644 index 0000000000..8ecd344816 --- /dev/null +++ b/test/server-side-rendering/comment/main.html @@ -0,0 +1,3 @@ +before
+ +after
diff --git a/test/server-side-rendering/directives/_actual.html b/test/server-side-rendering/directives/_actual.html new file mode 100644 index 0000000000..13cd34a990 --- /dev/null +++ b/test/server-side-rendering/directives/_actual.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/server-side-rendering/directives/_expected.html b/test/server-side-rendering/directives/_expected.html new file mode 100644 index 0000000000..f80f9a3d92 --- /dev/null +++ b/test/server-side-rendering/directives/_expected.html @@ -0,0 +1 @@ + diff --git a/test/server-side-rendering/directives/main.html b/test/server-side-rendering/directives/main.html new file mode 100644 index 0000000000..a960ff707e --- /dev/null +++ b/test/server-side-rendering/directives/main.html @@ -0,0 +1 @@ + diff --git a/test/validator/svg-child-component-declared-namespace/input.html b/test/validator/svg-child-component-declared-namespace/input.html new file mode 100644 index 0000000000..c158d7fdf5 --- /dev/null +++ b/test/validator/svg-child-component-declared-namespace/input.html @@ -0,0 +1,7 @@ +