Skip to content

API to create and return a component constructor #354

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Mar 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Svelte changelog

## 1.10.3

* Prevent `'</script>'` string occurence breaking pages ([#349](https://github.com/sveltejs/svelte/pull/349))
* Allow reference to whitelisted globals without properties ([#333](https://github.com/sveltejs/svelte/pull/333))
* Don't remove `&nbsp;` incorrectly ([#348](https://github.com/sveltejs/svelte/issues/348))
* `let` -> `var` in `addCss` block ([#351](https://github.com/sveltejs/svelte/pull/351))

## 1.10.2

* Accept any case for doctype declarations ([#336](https://github.com/sveltejs/svelte/issues/336))
* Allow non-top-level `<script>` and `<style>` tags to pass through without processing ([#335](https://github.com/sveltejs/svelte/issues/335))

## 1.10.1

* typecheck argument in _set when in dev mode ([#342](https://github.com/sveltejs/svelte/issues/342))
* Prevent duplicate helpers in non-shared mode ([#337](https://github.com/sveltejs/svelte/issues/337))

## 1.10.0

* Component self-references with `<:Self/>` ([#51](https://github.com/sveltejs/svelte/issues/51))
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "svelte",
"version": "1.10.0",
"version": "1.10.3",
"description": "The magical disappearing UI framework",
"main": "compiler/svelte.js",
"files": [
Expand Down
3 changes: 1 addition & 2 deletions src/generators/Generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export default class Generator {

if ( globalWhitelist[ name ] ) {
code.prependRight( node.start, `( '${name}' in root ? root.` );
code.appendLeft( node.object.end, ` : ${name} )` );
code.appendLeft( node.object ? node.object.end : node.end, ` : ${name} )` );
} else {
code.prependRight( node.start, `root.` );
}
Expand Down Expand Up @@ -361,4 +361,3 @@ export default class Generator {
if ( visitor.leave ) visitor.leave( this, node );
}
}

18 changes: 14 additions & 4 deletions src/generators/dom/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,14 @@ export default function dom ( parsed, source, options, names ) {
_set: new CodeBuilder()
};

if ( options.dev ) {
builders._set.addBlock ( deindent`
if ( typeof newState !== 'object' ) {
throw new Error( 'Component .set was called without an object of data key-values to update.' );
}
`);
}

builders._set.addLine( 'var oldState = this._state;' );
builders._set.addLine( 'this._state = Object.assign( {}, oldState, newState );' );

Expand Down Expand Up @@ -247,7 +255,7 @@ export default function dom ( parsed, source, options, names ) {

if ( parsed.css && options.css !== false ) {
builders.main.addBlock( deindent`
let addedCss = false;
var addedCss = false;
function addCss () {
var style = ${generator.helper( 'createElement' )}( 'style' );
style.textContent = ${JSON.stringify( processCss( parsed, generator.code ) )};
Expand Down Expand Up @@ -410,11 +418,13 @@ export default function dom ( parsed, source, options, names ) {
`import { ${names.join( ', ' )} } from ${JSON.stringify( sharedPath )}`
);
} else {
builders.main.addBlock( `var ${generator.aliases.dispatchObservers} = ${shared.dispatchObservers.toString()}` );

Object.keys( generator.uses ).forEach( key => {
const fn = shared[ key ]; // eslint-disable-line import/namespace
builders.main.addBlock( fn.toString() );
if ( key !== generator.aliases[ key ] ) {
builders.main.addBlock( `var ${generator.aliases[ key ]} = ${fn.toString()}}` );
} else {
builders.main.addBlock( fn.toString() );
}
});
}

Expand Down
4 changes: 2 additions & 2 deletions src/generators/server-side-rendering/visitors/Element.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Component from './Component.js';
import voidElementNames from '../../../utils/voidElementNames.js';
import isVoidElementName from '../../../utils/isVoidElementName.js';

export default {
enter ( generator, node ) {
Expand Down Expand Up @@ -44,7 +44,7 @@ export default {
return;
}

if ( !voidElementNames.test( node.name ) ) {
if ( !isVoidElementName( node.name ) ) {
generator.append( `</${node.name}>` );
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/generators/shared/utils/getIntro.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default function getIntro ( format, options, imports ) {
if ( format === 'cjs' ) return getCjsIntro( options, imports );
if ( format === 'iife' ) return getIifeIntro( options, imports );
if ( format === 'umd' ) return getUmdIntro( options, imports );
if ( format === 'eval' ) return getEvalIntro( options, imports );

throw new Error( `Not implemented: ${format}` );
}
Expand Down Expand Up @@ -60,6 +61,10 @@ function getUmdIntro ( options, imports ) {
}(this, (function (${paramString( imports )}) { 'use strict';` + '\n\n';
}

function getEvalIntro ( options, imports ) {
return `(function (${paramString( imports )}) { 'use strict';\n\n`;
}

function paramString ( imports ) {
return imports.length ? ` ${imports.map( dep => dep.name ).join( ', ' )} ` : '';
}
Expand Down
5 changes: 5 additions & 0 deletions src/generators/shared/utils/getOutro.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export default function getOutro ( format, name, options, imports ) {
return `return ${name};\n\n}(${globals.join( ', ' )}));`;
}

if ( format === 'eval' ) {
const globals = getGlobals( imports, options );
return `return ${name};\n\n}(${globals.join( ', ' )}));`;
}

if ( format === 'umd' ) {
return `return ${name};\n\n})));`;
}
Expand Down
24 changes: 24 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,28 @@ export function compile ( source, _options ) {
return compiler( parsed, source, options, names );
}

export function create ( source, _options = {} ) {
_options.format = 'eval';

const compiled = compile( source, _options );

if ( !compiled || !compiled.code ) {
return;
}

let result;
try {
result = ( 1, eval )( compiled.code );
} catch ( err ) {
if ( _options.onerror ) {
_options.onerror( err );
} else {
throw err;
}
return;
}

return result;
}

export { parse, validate, version as VERSION };
2 changes: 1 addition & 1 deletion src/parse/patterns.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const whitespace = /\s/;
export const whitespace = /[ \t\r\n]/;
2 changes: 1 addition & 1 deletion src/parse/read/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function readScript ( parser, start, attributes ) {
break;
}

if ( parser.eat( '</script>' ) ) {
if ( parser.eat( '<\/script>' ) ) {
break;
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/parse/state/tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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';
import isVoidElementName from '../../utils/isVoidElementName.js';

const validTagName = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
const invalidUnquotedAttributeCharacters = /[\s"'=<>\/`]/;
Expand Down Expand Up @@ -84,7 +84,7 @@ export default function tag ( parser ) {
parser.allowWhitespace();

if ( isClosingTag ) {
if ( voidElementNames.test( name ) ) {
if ( isVoidElementName( name ) ) {
parser.error( `<${name}> is a void element and cannot have children, or a closing tag`, start );
}

Expand Down Expand Up @@ -130,13 +130,13 @@ export default function tag ( parser ) {

parser.allowWhitespace();

// special cases – <script> and <style>
if ( name in specials ) {
// special cases – top-level <script> and <style>
if ( name in specials && parser.stack.length === 1 ) {
const special = specials[ name ];

if ( parser[ special.property ] ) {
parser.index = start;
parser.error( `You can only have one <${name}> tag per component` );
parser.error( `You can only have one top-level <${name}> tag per component` );
}

parser.eat( '>', true );
Expand All @@ -155,7 +155,7 @@ export default function tag ( parser ) {

parser.current().children.push( element );

const selfClosing = parser.eat( '/' ) || voidElementNames.test( name );
const selfClosing = parser.eat( '/' ) || isVoidElementName( name );

parser.eat( '>', true );

Expand Down
5 changes: 5 additions & 0 deletions src/utils/isVoidElementName.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const voidElementNames = /^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/;

export default function isVoidElementName ( name ) {
return voidElementNames.test( name ) || name.toLowerCase() === '!doctype';
}
1 change: 0 additions & 1 deletion src/utils/voidElementNames.js

This file was deleted.

40 changes: 40 additions & 0 deletions test/create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import deindent from '../src/utils/deindent.js';
import assert from 'assert';
import { svelte } from './helpers.js';

describe( 'create', () => {
it( 'should return a component constructor', () => {
const source = deindent`
<div>{{prop}}</div>
`;

const component = svelte.create( source );
assert( component instanceof Function );
});

it( 'should throw error when source is invalid ', done => {
const source = deindent`
<div>{{prop}</div>
`;

const component = svelte.create( source, {
onerror: () => {
done();
}
});

assert.equal( component, undefined );
});

it( 'should return undefined when source is invalid ', () => {
const source = deindent`
<div>{{prop}</div>
`;

const component = svelte.create( source, {
onerror: () => {}
});

assert.equal( component, undefined );
});
});
42 changes: 42 additions & 0 deletions test/formats.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ function testIife ( code, name, globals, html ) {
});
}

function testEval ( code, name, globals, html ) {
const fn = new Function( Object.keys( globals ), `return ${code};` );

return env().then( window => {
const SvelteComponent = fn( ...Object.keys( globals ).map( key => globals[ key ] ) );

const main = window.document.body.querySelector( 'main' );
const component = new SvelteComponent({ target: main });

assert.htmlEqual( main.innerHTML, html );

component.destroy();
});
}

describe( 'formats', () => {
before( setupHtmlEqual );

Expand Down Expand Up @@ -175,4 +190,31 @@ describe( 'formats', () => {
.then( () => testIife( code, 'Foo', { answer: 42 }, `<div>42</div>` ) );
});
});

describe( 'eval', () => {
it( 'generates a self-executing script that returns the component on eval', () => {
const source = deindent`
<div>{{answer}}</div>

<script>
import answer from 'answer';

export default {
data () {
return { answer };
}
};
</script>
`;

const { code } = svelte.compile( source, {
format: 'eval',
globals: {
answer: 'answer'
}
});

return testEval( code, 'Foo', { answer: 42 }, `<div>42</div>` );
});
});
});
7 changes: 7 additions & 0 deletions test/generator/globals-accessible-directly/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default {
html: '',

test ( assert, component, target ) {
assert.htmlEqual( target.innerHTML, 'NaN' );
}
};
1 change: 1 addition & 0 deletions test/generator/globals-accessible-directly/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{NaN}}
8 changes: 8 additions & 0 deletions test/generator/nbsp/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default {
html: `<span>&nbsp;</span>`,

test ( assert, component, target ) {
const text = target.querySelector( 'span' ).textContent;
assert.equal( text.charCodeAt( 0 ), 160 );
}
};
1 change: 1 addition & 0 deletions test/generator/nbsp/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<span>&nbsp;</span>
2 changes: 1 addition & 1 deletion test/parser/error-multiple-styles/error.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"message": "You can only have one <style> tag per component",
"message": "You can only have one top-level <style> tag per component",
"loc": {
"line": 9,
"column": 0
Expand Down
1 change: 1 addition & 0 deletions test/parser/nbsp/input.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<span>&nbsp;</span>
27 changes: 27 additions & 0 deletions test/parser/nbsp/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"hash": 2678229240,
"html": {
"start": 0,
"end": 19,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 19,
"type": "Element",
"name": "span",
"attributes": [],
"children": [
{
"start": 6,
"end": 12,
"type": "Text",
"data": " "
}
]
}
]
},
"css": null,
"js": null
}