diff --git a/.eslintignore b/.eslintignore index bc31435419..4a113378ce 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ src/shared shared.js +store.js test/test.js test/setup.js **/_actual.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ab7be640a..50b5a7bbe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Svelte changelog +## 1.43.1 + +* Fix parameterised transitions ([#962](https://github.com/sveltejs/svelte/issues/962)) +* Prevent boolean attributes breaking estree-walker expectations ([#961](https://github.com/sveltejs/svelte/issues/961)) +* Throw error on cyclical store computations ([#964](https://github.com/sveltejs/svelte/pull/964)) + +## 1.43.0 + +* Export `Store` class to manage global state ([#930](https://github.com/sveltejs/svelte/issues/930)) +* Recognise `aria-current` ([#953](https://github.com/sveltejs/svelte/pull/953)) +* Support SSR register options including `extensions` ([#939](https://github.com/sveltejs/svelte/issues/939)) +* Friendlier error for illegal contexts ([#934](https://github.com/sveltejs/svelte/issues/934)) +* Remove whitespace around `<:Window>` components ([#943](https://github.com/sveltejs/svelte/issues/943)) + ## 1.42.1 * Correctly append items inside a slotted `each` block ([#932](https://github.com/sveltejs/svelte/pull/932)) diff --git a/package.json b/package.json index 6bf9fae197..6684fdbd4d 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "svelte", - "version": "1.42.1", + "version": "1.43.1", "description": "The magical disappearing UI framework", "main": "compiler/svelte.js", "files": [ "compiler", "ssr", "shared.js", + "store.js", "README.md" ], "scripts": { diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index 5fd82e274b..323af454c7 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -72,15 +72,8 @@ function removeIndentation( // We need to tell estree-walker that it should always // look for an `else` block, otherwise it might get // the wrong idea about the shape of each/if blocks -childKeys.EachBlock = [ - 'children', - 'else' -]; - -childKeys.IfBlock = [ - 'children', - 'else' -]; +childKeys.EachBlock = childKeys.IfBlock = ['children', 'else']; +childKeys.Attribute = ['value']; export default class Generator { ast: Parsed; @@ -536,6 +529,9 @@ export default class Generator { (param: Node) => param.type === 'AssignmentPattern' ? param.left.name : param.name ); + deps.forEach(dep => { + this.expectedProperties.add(dep); + }); dependencies.set(key, deps); }); @@ -762,6 +758,11 @@ export default class Generator { }); this.skip(); } + + if (node.type === 'Transition' && node.expression) { + node.metadata = contextualise(node.expression, contextDependencies, indexes); + this.skip(); + } }, leave(node: Node, parent: Node) { diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts index f6c007c79d..c6d9e94df2 100644 --- a/src/generators/dom/index.ts +++ b/src/generators/dom/index.ts @@ -184,15 +184,23 @@ export default function dom( const debugName = `<${generator.customElement ? generator.tag : name}>`; // generate initial state object - const globals = Array.from(generator.expectedProperties).filter(prop => globalWhitelist.has(prop)); + const expectedProperties = Array.from(generator.expectedProperties); + const globals = expectedProperties.filter(prop => globalWhitelist.has(prop)); + const storeProps = options.store ? expectedProperties.filter(prop => prop[0] === '$') : []; + const initialState = []; + if (globals.length > 0) { initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`); } + if (storeProps.length > 0) { + initialState.push(`this.store._init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`); + } + if (templateProperties.data) { initialState.push(`%data()`); - } else if (globals.length === 0) { + } else if (globals.length === 0 && storeProps.length === 0) { initialState.push('{}'); } @@ -205,6 +213,7 @@ export default function dom( @init(this, options); ${generator.usesRefs && `this.refs = {};`} this._state = @assign(${initialState.join(', ')}); + ${storeProps.length > 0 && `this.store._add(this, [${storeProps.map(prop => `"${prop.slice(1)}"`)}]);`} ${generator.metaBindings} ${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`} ${options.dev && @@ -215,7 +224,11 @@ export default function dom( ${generator.bindingGroups.length && `this._bindingGroups = [${Array(generator.bindingGroups.length).fill('[]').join(', ')}];`} - ${templateProperties.ondestroy && `this._handlers.destroy = [%ondestroy]`} + ${(templateProperties.ondestroy || storeProps.length) && ( + `this._handlers.destroy = [${ + [templateProperties.ondestroy && `%ondestroy`, storeProps.length && `@removeFromStore`].filter(Boolean).join(', ') + }];` + )} ${generator.slots.size && `this._slotted = options.slots || {};`} diff --git a/src/generators/dom/visitors/Element/addBindings.ts b/src/generators/dom/visitors/Element/addBindings.ts index a2433abf93..2e41cd1b19 100644 --- a/src/generators/dom/visitors/Element/addBindings.ts +++ b/src/generators/dom/visitors/Element/addBindings.ts @@ -195,13 +195,19 @@ export default function addBindings( const usesContext = group.bindings.some(binding => binding.handler.usesContext); const usesState = group.bindings.some(binding => binding.handler.usesState); + const usesStore = group.bindings.some(binding => binding.handler.usesStore); const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n'); const props = new Set(); + const storeProps = new Set(); group.bindings.forEach(binding => { binding.handler.props.forEach(prop => { props.add(prop); }); + + binding.handler.storeProps.forEach(prop => { + storeProps.add(prop); + }); }); // TODO use stringifyProps here, once indenting is fixed // media bindings — awkward special case. The native timeupdate events @@ -222,9 +228,11 @@ export default function addBindings( } ${usesContext && `var context = ${node.var}._svelte;`} ${usesState && `var state = #component.get();`} + ${usesStore && `var $ = #component.store.get();`} ${needsLock && `${lock} = true;`} ${mutations.length > 0 && mutations} - #component.set({ ${Array.from(props).join(', ')} }); + ${props.size > 0 && `#component.set({ ${Array.from(props).join(', ')} });`} + ${storeProps.size > 0 && `#component.store.set({ ${Array.from(storeProps).join(', ')} });`} ${needsLock && `${lock} = false;`} } `); @@ -307,6 +315,13 @@ function getEventHandler( dependencies: string[], value: string, ) { + let storeDependencies = []; + + if (generator.options.store) { + storeDependencies = dependencies.filter(prop => prop[0] === '$').map(prop => prop.slice(1)); + dependencies = dependencies.filter(prop => prop[0] !== '$'); + } + if (block.contexts.has(name)) { const tail = attribute.value.type === 'MemberExpression' ? getTailSnippet(attribute.value) @@ -318,8 +333,10 @@ function getEventHandler( return { usesContext: true, usesState: true, + usesStore: storeDependencies.length > 0, mutation: `${list}[${index}]${tail} = ${value};`, - props: dependencies.map(prop => `${prop}: state.${prop}`) + props: dependencies.map(prop => `${prop}: state.${prop}`), + storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`) }; } @@ -336,16 +353,31 @@ function getEventHandler( return { usesContext: false, usesState: true, + usesStore: storeDependencies.length > 0, mutation: `${snippet} = ${value}`, - props: dependencies.map((prop: string) => `${prop}: state.${prop}`) + props: dependencies.map((prop: string) => `${prop}: state.${prop}`), + storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`) }; } + let props; + let storeProps; + + if (generator.options.store && name[0] === '$') { + props = []; + storeProps = [`${name.slice(1)}: ${value}`]; + } else { + props = [`${name}: ${value}`]; + storeProps = []; + } + return { usesContext: false, usesState: false, + usesStore: false, mutation: null, - props: [`${name}: ${value}`] + props, + storeProps }; } @@ -393,4 +425,4 @@ function isComputed(node: Node) { } return false; -} +} \ No newline at end of file diff --git a/src/generators/dom/visitors/Element/addTransitions.ts b/src/generators/dom/visitors/Element/addTransitions.ts index 26ff6f048e..10d0b90f50 100644 --- a/src/generators/dom/visitors/Element/addTransitions.ts +++ b/src/generators/dom/visitors/Element/addTransitions.ts @@ -21,7 +21,7 @@ export default function addTransitions( if (intro === outro) { const name = block.getUniqueName(`${node.var}_transition`); const snippet = intro.expression - ? intro.expression.metadata.snippet + ? intro.metadata.snippet : '{}'; block.addVariable(name); @@ -51,7 +51,7 @@ export default function addTransitions( if (intro) { block.addVariable(introName); const snippet = intro.expression - ? intro.expression.metadata.snippet + ? intro.metadata.snippet : '{}'; const fn = `%transitions-${intro.name}`; // TODO add built-in transitions? @@ -76,7 +76,7 @@ export default function addTransitions( if (outro) { block.addVariable(outroName); const snippet = outro.expression - ? intro.expression.metadata.snippet + ? outro.metadata.snippet : '{}'; const fn = `%transitions-${outro.name}`; diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts index e08c10ff14..f9c40ddb08 100644 --- a/src/generators/server-side-rendering/index.ts +++ b/src/generators/server-side-rendering/index.ts @@ -73,16 +73,22 @@ export default function ssr( generator.stylesheet.render(options.filename, true); // generate initial state object - // TODO this doesn't work, because expectedProperties isn't populated - const globals = Array.from(generator.expectedProperties).filter(prop => globalWhitelist.has(prop)); + const expectedProperties = Array.from(generator.expectedProperties); + const globals = expectedProperties.filter(prop => globalWhitelist.has(prop)); + const storeProps = options.store ? expectedProperties.filter(prop => prop[0] === '$') : []; + const initialState = []; if (globals.length > 0) { initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`); } + if (storeProps.length > 0) { + initialState.push(`options.store._init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`); + } + if (templateProperties.data) { initialState.push(`%data()`); - } else if (globals.length === 0) { + } else if (globals.length === 0 && storeProps.length === 0) { initialState.push('{}'); } @@ -99,7 +105,7 @@ export default function ssr( return ${templateProperties.data ? `%data()` : `{}`}; }; - ${name}.render = function(state, options) { + ${name}.render = function(state, options = {}) { state = Object.assign(${initialState.join(', ')}); ${computations.map( diff --git a/src/generators/server-side-rendering/visitors/Component.ts b/src/generators/server-side-rendering/visitors/Component.ts index b582dc13d9..74c7e239e4 100644 --- a/src/generators/server-side-rendering/visitors/Component.ts +++ b/src/generators/server-side-rendering/visitors/Component.ts @@ -79,6 +79,11 @@ export default function visitComponent( let open = `\${${expression}.render({${props}}`; + const options = []; + if (generator.options.store) { + options.push(`store: options.store`); + } + if (node.children.length) { const appendTarget: AppendTarget = { slots: { default: '' }, @@ -95,11 +100,15 @@ export default function visitComponent( .map(name => `${name}: () => \`${appendTarget.slots[name]}\``) .join(', '); - open += `, { slotted: { ${slotted} } }`; + options.push(`slotted: { ${slotted} }`); generator.appendTargets.pop(); } + if (options.length) { + open += `, { ${options.join(', ')} }`; + } + generator.append(open); generator.append(')}'); } diff --git a/src/index.ts b/src/index.ts index 529be61618..081660d3d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,8 @@ import generate from './generators/dom/index'; import generateSSR from './generators/server-side-rendering/index'; import { assign } from './shared/index.js'; import Stylesheet from './css/Stylesheet'; -import { Parsed, CompileOptions, Warning } from './interfaces'; +import { Parsed, CompileOptions, Warning, PreprocessOptions, Preprocessor } from './interfaces'; +import { SourceMap } from 'magic-string'; const version = '__VERSION__'; @@ -34,9 +35,74 @@ function defaultOnerror(error: Error) { throw error; } +function parseAttributeValue(value: string) { + return /^['"]/.test(value) ? + value.slice(1, -1) : + value; +} + +function parseAttributes(str: string) { + const attrs = {}; + str.split(/\s+/).filter(Boolean).forEach(attr => { + const [name, value] = attr.split('='); + attrs[name] = value ? parseAttributeValue(value) : true; + }); + return attrs; +} + +async function replaceTagContents(source, type: 'script' | 'style', preprocessor: Preprocessor) { + const exp = new RegExp(`<${type}([\\S\\s]*?)>([\\S\\s]*?)<\\/${type}>`, 'ig'); + const match = exp.exec(source); + + if (match) { + const attributes: Record = parseAttributes(match[1]); + const content: string = match[2]; + const processed: { code: string, map?: SourceMap | string } = await preprocessor({ + content, + attributes + }); + + if (processed && processed.code) { + return ( + source.slice(0, match.index) + + `<${type}>${processed.code}` + + source.slice(match.index + match[0].length) + ); + } + } + + return source; +} + +export async function preprocess(source: string, options: PreprocessOptions) { + const { markup, style, script } = options; + if (!!markup) { + const processed: { code: string, map?: SourceMap | string } = await markup({ content: source }); + source = processed.code; + } + + if (!!style) { + source = await replaceTagContents(source, 'style', style); + } + + if (!!script) { + source = await replaceTagContents(source, 'script', script); + } + + return { + // TODO return separated output, in future version where svelte.compile supports it: + // style: { code: styleCode, map: styleMap }, + // script { code: scriptCode, map: scriptMap }, + // markup { code: markupCode, map: markupMap }, + + toString() { + return source; + } + }; +} + export function compile(source: string, _options: CompileOptions) { const options = normalizeOptions(_options); - let parsed: Parsed; try { @@ -53,7 +119,7 @@ export function compile(source: string, _options: CompileOptions) { const compiler = options.generate === 'ssr' ? generateSSR : generate; return compiler(parsed, source, stylesheet, options); -} +}; export function create(source: string, _options: CompileOptions = {}) { _options.format = 'eval'; @@ -65,7 +131,7 @@ export function create(source: string, _options: CompileOptions = {}) { } try { - return (0,eval)(compiled.code); + return (0, eval)(compiled.code); } catch (err) { if (_options.onerror) { _options.onerror(err); diff --git a/src/interfaces.ts b/src/interfaces.ts index fff52e8da4..0d884f8472 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,3 +1,5 @@ +import {SourceMap} from 'magic-string'; + export interface Node { start: number; end: number; @@ -56,6 +58,7 @@ export interface CompileOptions { legacy?: boolean; customElement?: CustomElementOptions | true; css?: boolean; + store?: boolean; onerror?: (error: Error) => void; onwarn?: (warning: Warning) => void; @@ -77,4 +80,12 @@ export interface Visitor { export interface CustomElementOptions { tag?: string; props?: string[]; -} \ No newline at end of file +} + +export interface PreprocessOptions { + markup?: (options: {content: string}) => { code: string, map?: SourceMap | string }; + style?: Preprocessor; + script?: Preprocessor; +} + +export type Preprocessor = (options: {content: string, attributes: Record}) => { code: string, map?: SourceMap | string }; diff --git a/src/server-side-rendering/register.js b/src/server-side-rendering/register.js index 254c1e4419..bb4ea61e7b 100644 --- a/src/server-side-rendering/register.js +++ b/src/server-side-rendering/register.js @@ -2,16 +2,22 @@ import * as fs from 'fs'; import * as path from 'path'; import { compile } from '../index.ts'; +const compileOptions = {}; + function capitalise(name) { return name[0].toUpperCase() + name.slice(1); } export default function register(options) { const { extensions } = options; + if (extensions) { _deregister('.html'); extensions.forEach(_register); } + + // TODO make this the default and remove in v2 + if ('store' in options) compileOptions.store = options.store; } function _deregister(extension) { @@ -20,13 +26,15 @@ function _deregister(extension) { function _register(extension) { require.extensions[extension] = function(module, filename) { - const {code} = compile(fs.readFileSync(filename, 'utf-8'), { + const options = Object.assign({}, compileOptions, { filename, name: capitalise(path.basename(filename) .replace(new RegExp(`${extension.replace('.', '\\.')}$`), '')), - generate: 'ssr', + generate: 'ssr' }); + const {code} = compile(fs.readFileSync(filename, 'utf-8'), options); + return module._compile(code, filename); }; } diff --git a/src/shared/index.js b/src/shared/index.js index 7f01391128..b63a4ab001 100644 --- a/src/shared/index.js +++ b/src/shared/index.js @@ -65,12 +65,13 @@ export function get(key) { } export function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } export function observe(key, callback, options) { @@ -195,6 +196,10 @@ export var PENDING = {}; export var SUCCESS = {}; export var FAILURE = {}; +export function removeFromStore() { + this.store._remove(this); +} + export var proto = { destroy: destroy, get: get, diff --git a/src/validate/html/a11y.ts b/src/validate/html/a11y.ts index 94061e9b19..aec8f50e81 100644 --- a/src/validate/html/a11y.ts +++ b/src/validate/html/a11y.ts @@ -5,7 +5,7 @@ import validateEventHandler from './validateEventHandler'; import { Validator } from '../index'; import { Node } from '../../interfaces'; -const ariaAttributes = 'activedescendant atomic autocomplete busy checked controls describedby disabled dropeffect expanded flowto grabbed haspopup hidden invalid label labelledby level live multiline multiselectable orientation owns posinset pressed readonly relevant required selected setsize sort valuemax valuemin valuenow valuetext'.split(' '); +const ariaAttributes = 'activedescendant atomic autocomplete busy checked controls current describedby disabled dropeffect expanded flowto grabbed haspopup hidden invalid label labelledby level live multiline multiselectable orientation owns posinset pressed readonly relevant required selected setsize sort valuemax valuemin valuenow valuetext'.split(' '); const ariaAttributeSet = new Set(ariaAttributes); const ariaRoles = 'alert alertdialog application article banner button checkbox columnheader combobox command complementary composite contentinfo definition dialog directory document form grid gridcell group heading img input landmark link list listbox listitem log main marquee math menu menubar menuitem menuitemcheckbox menuitemradio navigation note option presentation progressbar radio radiogroup range region roletype row rowgroup rowheader scrollbar search section sectionhead select separator slider spinbutton status structure tab tablist tabpanel textbox timer toolbar tooltip tree treegrid treeitem widget window'.split(' '); diff --git a/src/validate/html/validateEventHandler.ts b/src/validate/html/validateEventHandler.ts index 2339f801d9..8e8a6122b3 100644 --- a/src/validate/html/validateEventHandler.ts +++ b/src/validate/html/validateEventHandler.ts @@ -1,6 +1,6 @@ import flattenReference from '../../utils/flattenReference'; import list from '../../utils/list'; -import { Validator } from '../index'; +import validate, { Validator } from '../index'; import validCalleeObjects from '../../utils/validCalleeObjects'; import { Node } from '../../interfaces'; @@ -28,6 +28,13 @@ export default function validateEventHandlerCallee( return; } + if (name === 'store' && attribute.expression.callee.type === 'MemberExpression') { + if (!validator.options.store) { + validator.warn('compile with `store: true` in order to call store methods', attribute.expression.start); + } + return; + } + if ( (callee.type === 'Identifier' && validBuiltins.has(callee.name)) || validator.methods.has(callee.name) @@ -35,6 +42,7 @@ export default function validateEventHandlerCallee( return; const validCallees = ['this.*', 'event.*', 'options.*', 'console.*'].concat( + validator.options.store ? 'store.*' : [], Array.from(validBuiltins), Array.from(validator.methods.keys()) ); diff --git a/src/validate/index.ts b/src/validate/index.ts index 17c3e83be9..5ed4d58bd9 100644 --- a/src/validate/index.ts +++ b/src/validate/index.ts @@ -22,6 +22,7 @@ export class Validator { readonly source: string; readonly filename: string; + options: CompileOptions; onwarn: ({}) => void; locator?: (pos: number) => Location; @@ -37,8 +38,8 @@ export class Validator { constructor(parsed: Parsed, source: string, options: CompileOptions) { this.source = source; this.filename = options.filename; - this.onwarn = options.onwarn; + this.options = options; this.namespace = null; this.defaultExport = null; @@ -78,7 +79,7 @@ export default function validate( stylesheet: Stylesheet, options: CompileOptions ) { - const { onwarn, onerror, name, filename } = options; + const { onwarn, onerror, name, filename, store } = options; try { if (name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(name)) { @@ -99,6 +100,7 @@ export default function validate( onwarn, name, filename, + store }); if (parsed.js) { diff --git a/store.js b/store.js new file mode 100644 index 0000000000..f31c659080 --- /dev/null +++ b/store.js @@ -0,0 +1,164 @@ +import { + assign, + blankObject, + differs, + dispatchObservers, + get, + observe +} from './shared.js'; + +function Store(state) { + this._observers = { pre: blankObject(), post: blankObject() }; + this._changeHandlers = []; + this._dependents = []; + + this._computed = blankObject(); + this._sortedComputedProperties = []; + + this._state = assign({}, state); +} + +assign(Store.prototype, { + _add: function(component, props) { + this._dependents.push({ + component: component, + props: props + }); + }, + + _init: function(props) { + var state = {}; + for (var i = 0; i < props.length; i += 1) { + var prop = props[i]; + state['$' + prop] = this._state[prop]; + } + return state; + }, + + _remove: function(component) { + var i = this._dependents.length; + while (i--) { + if (this._dependents[i].component === component) { + this._dependents.splice(i, 1); + return; + } + } + }, + + _sortComputedProperties: function() { + var computed = this._computed; + var sorted = this._sortedComputedProperties = []; + var cycles; + var visited = blankObject(); + + function visit(key) { + if (cycles[key]) { + throw new Error('Cyclical dependency detected'); + } + + if (visited[key]) return; + visited[key] = true; + + var c = computed[key]; + + if (c) { + cycles[key] = true; + c.deps.forEach(visit); + sorted.push(c); + } + } + + for (var key in this._computed) { + cycles = blankObject(); + visit(key); + } + }, + + compute: function(key, deps, fn) { + var store = this; + var value; + + var c = { + deps: deps, + update: function(state, changed, dirty) { + var values = deps.map(function(dep) { + if (dep in changed) dirty = true; + return state[dep]; + }); + + if (dirty) { + var newValue = fn.apply(null, values); + if (differs(newValue, value)) { + value = newValue; + changed[key] = true; + state[key] = value; + } + } + } + }; + + c.update(this._state, {}, true); + + this._computed[key] = c; + this._sortComputedProperties(); + }, + + get: get, + + observe: observe, + + onchange: function(callback) { + this._changeHandlers.push(callback); + return { + cancel: function() { + var index = this._changeHandlers.indexOf(callback); + if (~index) this._changeHandlers.splice(index, 1); + } + }; + }, + + set: function(newState) { + var oldState = this._state, + changed = this._changed = {}, + dirty = false; + + for (var key in newState) { + if (this._computed[key]) throw new Error("'" + key + "' is a read-only property"); + if (differs(newState[key], oldState[key])) changed[key] = dirty = true; + } + if (!dirty) return; + + this._state = assign({}, oldState, newState); + + for (var i = 0; i < this._sortedComputedProperties.length; i += 1) { + this._sortedComputedProperties[i].update(this._state, changed); + } + + for (var i = 0; i < this._changeHandlers.length; i += 1) { + this._changeHandlers[i](this._state, changed); + } + + dispatchObservers(this, this._observers.pre, changed, this._state, oldState); + + var dependents = this._dependents.slice(); // guard against mutations + for (var i = 0; i < dependents.length; i += 1) { + var dependent = dependents[i]; + var componentState = {}; + dirty = false; + + for (var j = 0; j < dependent.props.length; j += 1) { + var prop = dependent.props[j]; + if (prop in changed) { + componentState['$' + prop] = this._state[prop]; + dirty = true; + } + } + + if (dirty) dependent.component.set(componentState); + } + + dispatchObservers(this, this._observers.post, changed, this._state, oldState); + } +}); + +export { Store }; \ No newline at end of file diff --git a/test/js/samples/collapses-text-around-comments/expected-bundle.js b/test/js/samples/collapses-text-around-comments/expected-bundle.js index a5776bc195..c54a4508e7 100644 --- a/test/js/samples/collapses-text-around-comments/expected-bundle.js +++ b/test/js/samples/collapses-text-around-comments/expected-bundle.js @@ -91,12 +91,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/component-static/expected-bundle.js b/test/js/samples/component-static/expected-bundle.js index 94f69ca2ca..27d4a5d5df 100644 --- a/test/js/samples/component-static/expected-bundle.js +++ b/test/js/samples/component-static/expected-bundle.js @@ -67,12 +67,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/computed-collapsed-if/expected-bundle.js b/test/js/samples/computed-collapsed-if/expected-bundle.js index c378d78026..9129d64ebf 100644 --- a/test/js/samples/computed-collapsed-if/expected-bundle.js +++ b/test/js/samples/computed-collapsed-if/expected-bundle.js @@ -67,12 +67,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/css-media-query/expected-bundle.js b/test/js/samples/css-media-query/expected-bundle.js index 11ffa87f21..3eb06a7baa 100644 --- a/test/js/samples/css-media-query/expected-bundle.js +++ b/test/js/samples/css-media-query/expected-bundle.js @@ -87,12 +87,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js b/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js index 86edac7465..a8a14c254a 100644 --- a/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js +++ b/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js @@ -79,12 +79,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/do-use-dataset/expected-bundle.js b/test/js/samples/do-use-dataset/expected-bundle.js index 42de89fd93..1baf806a52 100644 --- a/test/js/samples/do-use-dataset/expected-bundle.js +++ b/test/js/samples/do-use-dataset/expected-bundle.js @@ -83,12 +83,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/dont-use-dataset-in-legacy/expected-bundle.js b/test/js/samples/dont-use-dataset-in-legacy/expected-bundle.js index 2d25e23a10..3ebad459d3 100644 --- a/test/js/samples/dont-use-dataset-in-legacy/expected-bundle.js +++ b/test/js/samples/dont-use-dataset-in-legacy/expected-bundle.js @@ -87,12 +87,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/each-block-changed-check/expected-bundle.js b/test/js/samples/each-block-changed-check/expected-bundle.js index c9723db616..dae7830ecb 100644 --- a/test/js/samples/each-block-changed-check/expected-bundle.js +++ b/test/js/samples/each-block-changed-check/expected-bundle.js @@ -99,12 +99,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/event-handlers-custom/expected-bundle.js b/test/js/samples/event-handlers-custom/expected-bundle.js index 46c109100a..b1974ae869 100644 --- a/test/js/samples/event-handlers-custom/expected-bundle.js +++ b/test/js/samples/event-handlers-custom/expected-bundle.js @@ -79,12 +79,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/if-block-no-update/expected-bundle.js b/test/js/samples/if-block-no-update/expected-bundle.js index 0277095a83..12822a4ed3 100644 --- a/test/js/samples/if-block-no-update/expected-bundle.js +++ b/test/js/samples/if-block-no-update/expected-bundle.js @@ -83,12 +83,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/if-block-simple/expected-bundle.js b/test/js/samples/if-block-simple/expected-bundle.js index 40acbe3959..0985260579 100644 --- a/test/js/samples/if-block-simple/expected-bundle.js +++ b/test/js/samples/if-block-simple/expected-bundle.js @@ -83,12 +83,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/inline-style-optimized-multiple/expected-bundle.js b/test/js/samples/inline-style-optimized-multiple/expected-bundle.js index eff6f53cfa..6260246cc0 100644 --- a/test/js/samples/inline-style-optimized-multiple/expected-bundle.js +++ b/test/js/samples/inline-style-optimized-multiple/expected-bundle.js @@ -83,12 +83,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/inline-style-optimized-url/expected-bundle.js b/test/js/samples/inline-style-optimized-url/expected-bundle.js index 3a126946ad..921d377bf9 100644 --- a/test/js/samples/inline-style-optimized-url/expected-bundle.js +++ b/test/js/samples/inline-style-optimized-url/expected-bundle.js @@ -83,12 +83,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/inline-style-optimized/expected-bundle.js b/test/js/samples/inline-style-optimized/expected-bundle.js index 0c127a6391..8ddc55d215 100644 --- a/test/js/samples/inline-style-optimized/expected-bundle.js +++ b/test/js/samples/inline-style-optimized/expected-bundle.js @@ -83,12 +83,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/inline-style-unoptimized/expected-bundle.js b/test/js/samples/inline-style-unoptimized/expected-bundle.js index d6f545111a..c3ca9e6957 100644 --- a/test/js/samples/inline-style-unoptimized/expected-bundle.js +++ b/test/js/samples/inline-style-unoptimized/expected-bundle.js @@ -83,12 +83,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/input-without-blowback-guard/expected-bundle.js b/test/js/samples/input-without-blowback-guard/expected-bundle.js index 1918f9f6d1..a7a7d25e0c 100644 --- a/test/js/samples/input-without-blowback-guard/expected-bundle.js +++ b/test/js/samples/input-without-blowback-guard/expected-bundle.js @@ -87,12 +87,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/legacy-input-type/expected-bundle.js b/test/js/samples/legacy-input-type/expected-bundle.js index 7211738f45..54a4c3c02d 100644 --- a/test/js/samples/legacy-input-type/expected-bundle.js +++ b/test/js/samples/legacy-input-type/expected-bundle.js @@ -85,12 +85,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/legacy-quote-class/expected-bundle.js b/test/js/samples/legacy-quote-class/expected-bundle.js index 1575bf4064..c6f7bb8405 100644 --- a/test/js/samples/legacy-quote-class/expected-bundle.js +++ b/test/js/samples/legacy-quote-class/expected-bundle.js @@ -102,12 +102,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/media-bindings/expected-bundle.js b/test/js/samples/media-bindings/expected-bundle.js index 80545b9402..bdac2a8f2a 100644 --- a/test/js/samples/media-bindings/expected-bundle.js +++ b/test/js/samples/media-bindings/expected-bundle.js @@ -95,12 +95,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/non-imported-component/expected-bundle.js b/test/js/samples/non-imported-component/expected-bundle.js index 575be613fa..fd201d65b3 100644 --- a/test/js/samples/non-imported-component/expected-bundle.js +++ b/test/js/samples/non-imported-component/expected-bundle.js @@ -81,12 +81,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js b/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js index f1e701bd04..364ee2901d 100644 --- a/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js +++ b/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js @@ -67,12 +67,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/onrender-onteardown-rewritten/expected.js b/test/js/samples/onrender-onteardown-rewritten/expected.js index 19b85a62e0..ae6046dddb 100644 --- a/test/js/samples/onrender-onteardown-rewritten/expected.js +++ b/test/js/samples/onrender-onteardown-rewritten/expected.js @@ -24,7 +24,7 @@ function SvelteComponent(options) { init(this, options); this._state = assign({}, options.data); - this._handlers.destroy = [ondestroy] + this._handlers.destroy = [ondestroy]; var _oncreate = oncreate.bind(this); diff --git a/test/js/samples/setup-method/expected-bundle.js b/test/js/samples/setup-method/expected-bundle.js index 0c545ea70c..df267db975 100644 --- a/test/js/samples/setup-method/expected-bundle.js +++ b/test/js/samples/setup-method/expected-bundle.js @@ -67,12 +67,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/ssr-no-oncreate-etc/expected-bundle.js b/test/js/samples/ssr-no-oncreate-etc/expected-bundle.js index 9a466e06ae..1dfa59b423 100644 --- a/test/js/samples/ssr-no-oncreate-etc/expected-bundle.js +++ b/test/js/samples/ssr-no-oncreate-etc/expected-bundle.js @@ -4,7 +4,7 @@ SvelteComponent.data = function() { return {}; }; -SvelteComponent.render = function(state, options) { +SvelteComponent.render = function(state, options = {}) { state = Object.assign({}, state); return ``.trim(); diff --git a/test/js/samples/ssr-no-oncreate-etc/expected.js b/test/js/samples/ssr-no-oncreate-etc/expected.js index e8e5330377..511e5dd74e 100644 --- a/test/js/samples/ssr-no-oncreate-etc/expected.js +++ b/test/js/samples/ssr-no-oncreate-etc/expected.js @@ -6,7 +6,7 @@ SvelteComponent.data = function() { return {}; }; -SvelteComponent.render = function(state, options) { +SvelteComponent.render = function(state, options = {}) { state = Object.assign({}, state); return ``.trim(); diff --git a/test/js/samples/use-elements-as-anchors/expected-bundle.js b/test/js/samples/use-elements-as-anchors/expected-bundle.js index 8805cc3847..272e039772 100644 --- a/test/js/samples/use-elements-as-anchors/expected-bundle.js +++ b/test/js/samples/use-elements-as-anchors/expected-bundle.js @@ -91,12 +91,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/js/samples/window-binding-scroll/expected-bundle.js b/test/js/samples/window-binding-scroll/expected-bundle.js index 6c357ef732..3a11deb1b6 100644 --- a/test/js/samples/window-binding-scroll/expected-bundle.js +++ b/test/js/samples/window-binding-scroll/expected-bundle.js @@ -87,12 +87,13 @@ function get(key) { } function init(component, options) { - component.options = options; - component._observers = { pre: blankObject(), post: blankObject() }; component._handlers = blankObject(); component._root = options._root || component; component._bind = options._bind; + + component.options = options; + component.store = component._root.options.store; } function observe(key, callback, options) { diff --git a/test/preprocess/index.js b/test/preprocess/index.js new file mode 100644 index 0000000000..29eaff67f1 --- /dev/null +++ b/test/preprocess/index.js @@ -0,0 +1,148 @@ +import assert from 'assert'; +import {svelte} from '../helpers.js'; + +describe('preprocess', () => { + it('preprocesses entire component', () => { + const source = ` +

Hello __NAME__!

+ `; + + const expected = ` +

Hello world!

+ `; + + return svelte.preprocess(source, { + markup: ({ content }) => { + return { + code: content.replace('__NAME__', 'world') + }; + } + }).then(processed => { + assert.equal(processed.toString(), expected); + }); + }); + + it('preprocesses style', () => { + const source = ` +
$brand
+ + + `; + + const expected = ` +
$brand
+ + + `; + + return svelte.preprocess(source, { + style: ({ content }) => { + return { + code: content.replace('$brand', 'purple') + }; + } + }).then(processed => { + assert.equal(processed.toString(), expected); + }); + }); + + it('preprocesses style asynchronously', () => { + const source = ` +
$brand
+ + + `; + + const expected = ` +
$brand
+ + + `; + + return svelte.preprocess(source, { + style: ({ content }) => { + return Promise.resolve({ + code: content.replace('$brand', 'purple') + }); + } + }).then(processed => { + assert.equal(processed.toString(), expected); + }); + }); + + it('preprocesses script', () => { + const source = ` + + `; + + const expected = ` + + `; + + return svelte.preprocess(source, { + script: ({ content }) => { + return { + code: content.replace('__THE_ANSWER__', '42') + }; + } + }).then(processed => { + assert.equal(processed.toString(), expected); + }); + }); + + it('parses attributes', () => { + const source = ` + + `; + + return svelte.preprocess(source, { + style: ({ attributes }) => { + assert.deepEqual(attributes, { + type: 'text/scss', + 'data-foo': 'bar', + bool: true + }); + } + }); + }); + + it('ignores null/undefined returned from preprocessor', () => { + const source = ` + + `; + + const expected = ` + + `; + + return svelte.preprocess(source, { + script: () => null + }).then(processed => { + assert.equal(processed.toString(), expected); + }); + }); +}); \ No newline at end of file diff --git a/test/runtime/index.js b/test/runtime/index.js index b572ec81bc..af60caa84c 100644 --- a/test/runtime/index.js +++ b/test/runtime/index.js @@ -63,6 +63,7 @@ describe("runtime", () => { compileOptions.shared = shared; compileOptions.hydratable = hydrate; compileOptions.dev = config.dev; + compileOptions.store = !!config.store; // check that no ES2015+ syntax slipped in if (!config.allowES2015) { @@ -88,7 +89,7 @@ describe("runtime", () => { } } catch (err) { failed.add(dir); - showOutput(cwd, { shared, format: 'cjs' }, svelte); // eslint-disable-line no-console + showOutput(cwd, { shared, format: 'cjs', store: !!compileOptions.store }, svelte); // eslint-disable-line no-console throw err; } } @@ -130,12 +131,10 @@ describe("runtime", () => { }; }; - global.window = window; - try { SvelteComponent = require(`./samples/${dir}/main.html`); } catch (err) { - showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate }, svelte); // eslint-disable-line no-console + showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console throw err; } @@ -155,7 +154,8 @@ describe("runtime", () => { const options = Object.assign({}, { target, hydrate, - data: config.data + data: config.data, + store: config.store }, config.options || {}); const component = new SvelteComponent(options); @@ -190,12 +190,12 @@ describe("runtime", () => { config.error(assert, err); } else { failed.add(dir); - showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate }, svelte); // eslint-disable-line no-console + showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console throw err; } }) .then(() => { - if (config.show) showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate }, svelte); + if (config.show) showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate, store: !!compileOptions.store }, svelte); }); }); } diff --git a/test/runtime/samples/binding-input-checkbox-group-outside-each/_config.js b/test/runtime/samples/binding-input-checkbox-group-outside-each/_config.js index bff66c903b..a0ef755fb6 100644 --- a/test/runtime/samples/binding-input-checkbox-group-outside-each/_config.js +++ b/test/runtime/samples/binding-input-checkbox-group-outside-each/_config.js @@ -10,19 +10,17 @@ export default { selected: [ values[1] ] }, - 'skip-ssr': true, // values are rendered as [object Object] - html: `

Beta

`, @@ -40,15 +38,15 @@ export default { assert.htmlEqual( target.innerHTML, `

Alpha, Beta

@@ -61,15 +59,15 @@ export default { assert.htmlEqual( target.innerHTML, `

Beta, Gamma

diff --git a/test/runtime/samples/binding-input-checkbox-group/_config.js b/test/runtime/samples/binding-input-checkbox-group/_config.js index bff66c903b..a0ef755fb6 100644 --- a/test/runtime/samples/binding-input-checkbox-group/_config.js +++ b/test/runtime/samples/binding-input-checkbox-group/_config.js @@ -10,19 +10,17 @@ export default { selected: [ values[1] ] }, - 'skip-ssr': true, // values are rendered as [object Object] - html: `

Beta

`, @@ -40,15 +38,15 @@ export default { assert.htmlEqual( target.innerHTML, `

Alpha, Beta

@@ -61,15 +59,15 @@ export default { assert.htmlEqual( target.innerHTML, `

Beta, Gamma

diff --git a/test/runtime/samples/binding-input-radio-group/_config.js b/test/runtime/samples/binding-input-radio-group/_config.js index b557496533..b88dd10d4c 100644 --- a/test/runtime/samples/binding-input-radio-group/_config.js +++ b/test/runtime/samples/binding-input-radio-group/_config.js @@ -10,19 +10,17 @@ export default { selected: values[1] }, - 'skip-ssr': true, // values are rendered as [object Object] - html: `

Beta

`, @@ -40,15 +38,15 @@ export default { assert.htmlEqual( target.innerHTML, `

Alpha

@@ -65,15 +63,15 @@ export default { assert.htmlEqual( target.innerHTML, `

Gamma

diff --git a/test/runtime/samples/component-data-static-boolean-regression/Link.html b/test/runtime/samples/component-data-static-boolean-regression/Link.html new file mode 100644 index 0000000000..ecdf709900 --- /dev/null +++ b/test/runtime/samples/component-data-static-boolean-regression/Link.html @@ -0,0 +1 @@ +link \ No newline at end of file diff --git a/test/runtime/samples/component-data-static-boolean-regression/_config.js b/test/runtime/samples/component-data-static-boolean-regression/_config.js new file mode 100644 index 0000000000..61f8ba0a06 --- /dev/null +++ b/test/runtime/samples/component-data-static-boolean-regression/_config.js @@ -0,0 +1,3 @@ +export default { + html: `link` +}; diff --git a/test/runtime/samples/component-data-static-boolean-regression/main.html b/test/runtime/samples/component-data-static-boolean-regression/main.html new file mode 100644 index 0000000000..5f51d903b3 --- /dev/null +++ b/test/runtime/samples/component-data-static-boolean-regression/main.html @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/test/runtime/samples/dev-warning-readonly-computed/_config.js b/test/runtime/samples/dev-warning-readonly-computed/_config.js index 2706b9610a..95d4ea15c4 100644 --- a/test/runtime/samples/dev-warning-readonly-computed/_config.js +++ b/test/runtime/samples/dev-warning-readonly-computed/_config.js @@ -1,6 +1,10 @@ export default { dev: true, + data: { + a: 42 + }, + test ( assert, component ) { try { component.set({ foo: 1 }); diff --git a/test/runtime/samples/set-clones-input/_config.js b/test/runtime/samples/set-clones-input/_config.js index af04f4b73e..8f365f6330 100644 --- a/test/runtime/samples/set-clones-input/_config.js +++ b/test/runtime/samples/set-clones-input/_config.js @@ -1,10 +1,14 @@ export default { dev: true, + data: { + a: 42 + }, + test ( assert, component ) { const obj = { a: 1 }; component.set( obj ); component.set( obj ); // will fail if the object is not cloned component.destroy(); } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/test/runtime/samples/store-binding/NameInput.html b/test/runtime/samples/store-binding/NameInput.html new file mode 100644 index 0000000000..a5e4f5e48c --- /dev/null +++ b/test/runtime/samples/store-binding/NameInput.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/runtime/samples/store-binding/_config.js b/test/runtime/samples/store-binding/_config.js new file mode 100644 index 0000000000..aefc4ec652 --- /dev/null +++ b/test/runtime/samples/store-binding/_config.js @@ -0,0 +1,28 @@ +import { Store } from '../../../../store.js'; + +const store = new Store({ + name: 'world' +}); + +export default { + store, + + html: ` +

Hello world!

+ + `, + + test(assert, component, target, window) { + const input = target.querySelector('input'); + const event = new window.Event('input'); + + input.value = 'everybody'; + input.dispatchEvent(event); + + assert.equal(store.get('name'), 'everybody'); + assert.htmlEqual(target.innerHTML, ` +

Hello everybody!

+ + `); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/store-binding/main.html b/test/runtime/samples/store-binding/main.html new file mode 100644 index 0000000000..06410ea770 --- /dev/null +++ b/test/runtime/samples/store-binding/main.html @@ -0,0 +1,10 @@ +

Hello {{$name}}!

+ + + \ No newline at end of file diff --git a/test/runtime/samples/store-computed/Todo.html b/test/runtime/samples/store-computed/Todo.html new file mode 100644 index 0000000000..2ad67420ca --- /dev/null +++ b/test/runtime/samples/store-computed/Todo.html @@ -0,0 +1,15 @@ +{{#if isVisible}} +
{{todo.description}}
+{{/if}} + + \ No newline at end of file diff --git a/test/runtime/samples/store-computed/_config.js b/test/runtime/samples/store-computed/_config.js new file mode 100644 index 0000000000..f4e6f49e54 --- /dev/null +++ b/test/runtime/samples/store-computed/_config.js @@ -0,0 +1,63 @@ +import { Store } from '../../../../store.js'; + +class MyStore extends Store { + setFilter(filter) { + this.set({ filter }); + } + + toggleTodo(todo) { + todo.done = !todo.done; + this.set({ todos: this.get('todos') }); + } +} + +const todos = [ + { + description: 'Buy some milk', + done: true, + }, + { + description: 'Do the laundry', + done: true, + }, + { + description: "Find life's true purpose", + done: false, + } +]; + +const store = new MyStore({ + filter: 'all', + todos +}); + +export default { + store, + + html: ` +
Buy some milk
+
Do the laundry
+
Find life's true purpose
+ `, + + test(assert, component, target) { + store.setFilter('pending'); + + assert.htmlEqual(target.innerHTML, ` +
Find life's true purpose
+ `); + + store.toggleTodo(todos[1]); + + assert.htmlEqual(target.innerHTML, ` +
Do the laundry
+
Find life's true purpose
+ `); + + store.setFilter('done'); + + assert.htmlEqual(target.innerHTML, ` +
Buy some milk
+ `); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/store-computed/main.html b/test/runtime/samples/store-computed/main.html new file mode 100644 index 0000000000..5c50839ba3 --- /dev/null +++ b/test/runtime/samples/store-computed/main.html @@ -0,0 +1,11 @@ +{{#each $todos as todo}} + +{{/each}} + + \ No newline at end of file diff --git a/test/runtime/samples/store-event/NameInput.html b/test/runtime/samples/store-event/NameInput.html new file mode 100644 index 0000000000..ecd95d0364 --- /dev/null +++ b/test/runtime/samples/store-event/NameInput.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/runtime/samples/store-event/_config.js b/test/runtime/samples/store-event/_config.js new file mode 100644 index 0000000000..2779db5fc2 --- /dev/null +++ b/test/runtime/samples/store-event/_config.js @@ -0,0 +1,34 @@ +import { Store } from '../../../../store.js'; + +class MyStore extends Store { + setName(name) { + this.set({ name }); + } +} + +const store = new MyStore({ + name: 'world' +}); + +export default { + store, + + html: ` +

Hello world!

+ + `, + + test(assert, component, target, window) { + const input = target.querySelector('input'); + const event = new window.Event('input'); + + input.value = 'everybody'; + input.dispatchEvent(event); + + assert.equal(store.get('name'), 'everybody'); + assert.htmlEqual(target.innerHTML, ` +

Hello everybody!

+ + `); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/store-event/main.html b/test/runtime/samples/store-event/main.html new file mode 100644 index 0000000000..06410ea770 --- /dev/null +++ b/test/runtime/samples/store-event/main.html @@ -0,0 +1,10 @@ +

Hello {{$name}}!

+ + + \ No newline at end of file diff --git a/test/runtime/samples/store/_config.js b/test/runtime/samples/store/_config.js new file mode 100644 index 0000000000..4e266ff095 --- /dev/null +++ b/test/runtime/samples/store/_config.js @@ -0,0 +1,17 @@ +import { Store } from '../../../../store.js'; + +const store = new Store({ + name: 'world' +}); + +export default { + store, + + html: `

Hello world!

`, + + test(assert, component, target) { + store.set({ name: 'everybody' }); + + assert.htmlEqual(target.innerHTML, `

Hello everybody!

`); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/store/main.html b/test/runtime/samples/store/main.html new file mode 100644 index 0000000000..28154934b8 --- /dev/null +++ b/test/runtime/samples/store/main.html @@ -0,0 +1 @@ +

Hello {{$name}}!

\ No newline at end of file diff --git a/test/runtime/samples/transition-js-parameterised/_config.js b/test/runtime/samples/transition-js-parameterised/_config.js new file mode 100644 index 0000000000..d12aa06b89 --- /dev/null +++ b/test/runtime/samples/transition-js-parameterised/_config.js @@ -0,0 +1,17 @@ +export default { + test(assert, component, target, window, raf) { + component.set({ visible: true }); + const div = target.querySelector('div'); + assert.equal(div.foo, 0); + + raf.tick(50); + assert.equal(div.foo, 100); + + raf.tick(100); + assert.equal(div.foo, 200); + + raf.tick(101); + + component.destroy(); + }, +}; diff --git a/test/runtime/samples/transition-js-parameterised/main.html b/test/runtime/samples/transition-js-parameterised/main.html new file mode 100644 index 0000000000..de713ae963 --- /dev/null +++ b/test/runtime/samples/transition-js-parameterised/main.html @@ -0,0 +1,18 @@ +{{#if visible}} +
fades in
+{{/if}} + + \ No newline at end of file diff --git a/test/server-side-rendering/index.js b/test/server-side-rendering/index.js index a9bf847b29..b6bd109c0e 100644 --- a/test/server-side-rendering/index.js +++ b/test/server-side-rendering/index.js @@ -22,7 +22,8 @@ function tryToReadFile(file) { describe("ssr", () => { before(() => { require("../../ssr/register")({ - extensions: ['.svelte', '.html'] + extensions: ['.svelte', '.html'], + store: true }); return setupHtmlEqual(); @@ -98,9 +99,15 @@ describe("ssr", () => { delete require.cache[resolved]; }); + require("../../ssr/register")({ + store: !!config.store + }); + try { const component = require(`../runtime/samples/${dir}/main.html`); - const html = component.render(config.data); + const html = component.render(config.data, { + store: config.store + }); if (config.html) { assert.htmlEqual(html, config.html); diff --git a/test/store/index.js b/test/store/index.js new file mode 100644 index 0000000000..d8053367a9 --- /dev/null +++ b/test/store/index.js @@ -0,0 +1,190 @@ +import fs from 'fs'; +import assert from 'assert'; +import MagicString from 'magic-string'; +import { parse } from 'acorn'; +import { Store } from '../../store.js'; + +describe('store', () => { + it('is written in ES5', () => { + const source = fs.readFileSync('store.js', 'utf-8'); + + const ast = parse(source, { + sourceType: 'module' + }); + + const magicString = new MagicString(source); + ast.body.forEach(node => { + if (/^(Im|Ex)port/.test(node.type)) magicString.remove(node.start, node.end); + }); + + parse(magicString.toString(), { + ecmaVersion: 5 + }); + }); + + describe('get', () => { + it('gets a specific key', () => { + const store = new Store({ + foo: 'bar' + }); + + assert.equal(store.get('foo'), 'bar'); + }); + + it('gets the entire state object', () => { + const store = new Store({ + foo: 'bar' + }); + + assert.deepEqual(store.get(), { foo: 'bar' }); + }); + }); + + describe('set', () => { + it('sets state', () => { + const store = new Store(); + + store.set({ + foo: 'bar' + }); + + assert.equal(store.get('foo'), 'bar'); + }); + }); + + describe('observe', () => { + it('observes state', () => { + let newFoo; + let oldFoo; + + const store = new Store({ + foo: 'bar' + }); + + store.observe('foo', (n, o) => { + newFoo = n; + oldFoo = o; + }); + + assert.equal(newFoo, 'bar'); + assert.equal(oldFoo, undefined); + + store.set({ + foo: 'baz' + }); + + assert.equal(newFoo, 'baz'); + assert.equal(oldFoo, 'bar'); + }); + }); + + describe('onchange', () => { + it('fires a callback when state changes', () => { + const store = new Store(); + + let count = 0; + let args; + + store.onchange((state, changed) => { + count += 1; + args = { state, changed }; + }); + + store.set({ foo: 'bar' }); + + assert.equal(count, 1); + assert.deepEqual(args, { + state: { foo: 'bar' }, + changed: { foo: true } + }); + + // this should be a noop + store.set({ foo: 'bar' }); + assert.equal(count, 1); + + // this shouldn't + store.set({ foo: 'baz' }); + + assert.equal(count, 2); + assert.deepEqual(args, { + state: { foo: 'baz' }, + changed: { foo: true } + }); + }); + }); + + describe('computed', () => { + it('computes a property based on data', () => { + const store = new Store({ + foo: 1 + }); + + store.compute('bar', ['foo'], foo => foo * 2); + assert.equal(store.get('bar'), 2); + + const values = []; + + store.observe('bar', bar => { + values.push(bar); + }); + + store.set({ foo: 2 }); + assert.deepEqual(values, [2, 4]); + }); + + it('computes a property based on another computed property', () => { + const store = new Store({ + foo: 1 + }); + + store.compute('bar', ['foo'], foo => foo * 2); + store.compute('baz', ['bar'], bar => bar * 2); + assert.equal(store.get('baz'), 4); + + const values = []; + + store.observe('baz', baz => { + values.push(baz); + }); + + store.set({ foo: 2 }); + assert.deepEqual(values, [4, 8]); + }); + + it('prevents computed properties from being set', () => { + const store = new Store({ + foo: 1 + }); + + store.compute('bar', ['foo'], foo => foo * 2); + + assert.throws(() => { + store.set({ bar: 'whatever' }); + }, /'bar' is a read-only property/); + }); + + it('allows multiple dependents to depend on the same computed property', () => { + const store = new Store({ + a: 1 + }); + + store.compute('b', ['a'], a => a * 2); + store.compute('c', ['b'], b => b * 3); + store.compute('d', ['b'], b => b * 4); + + assert.deepEqual(store.get(), { a: 1, b: 2, c: 6, d: 8 }); + + // bit cheeky, testing a private property, but whatever + assert.equal(store._sortedComputedProperties.length, 3); + }); + + it('prevents cyclical dependencies', () => { + const store = new Store(); + + assert.throws(() => { + store.compute('a', ['b'], b => b + 1); + store.compute('b', ['a'], a => a + 1); + }, /Cyclical dependency detected/); + }); + }); +}); diff --git a/test/validator/samples/store-unexpected/input.html b/test/validator/samples/store-unexpected/input.html new file mode 100644 index 0000000000..589f28e647 --- /dev/null +++ b/test/validator/samples/store-unexpected/input.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/validator/samples/store-unexpected/warnings.json b/test/validator/samples/store-unexpected/warnings.json new file mode 100644 index 0000000000..bac2841dc9 --- /dev/null +++ b/test/validator/samples/store-unexpected/warnings.json @@ -0,0 +1,8 @@ +[{ + "message": "compile with `store: true` in order to call store methods", + "loc": { + "line": 1, + "column": 18 + }, + "pos": 18 +}] \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index ae736a946c..c38c354a78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,7 +10,7 @@ version "8.0.53" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.53.tgz#396b35af826fa66aad472c8cb7b8d5e277f4e6d8" -"@types/node@^7.0.18", "@types/node@^7.0.48": +"@types/node@^7.0.18": version "7.0.48" resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.48.tgz#24bfdc0aa82e8f6dbd017159c58094a2e06d0abb" @@ -64,8 +64,8 @@ ajv@^4.9.1: json-stable-stringify "^1.0.1" ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.4.0.tgz#32d1cf08dbc80c432f426f12e10b2511f6b46474" + version "5.5.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.1.tgz#b38bb8876d9e86bee994956a04e721e88b248eb2" dependencies: co "^4.6.0" fast-deep-equal "^1.0.0" @@ -441,8 +441,8 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0: supports-color "^4.0.0" chardet@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.0.tgz#0bbe1355ac44d7a3ed4a925707c4ef70f8190f6c" + version "0.4.2" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" chokidar@^1.7.0: version "1.7.0" @@ -538,10 +538,8 @@ commander@2.9.0: graceful-readlink ">= 1.0.0" commander@^2.9.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.0.tgz#2f13615c39c687a77926aa68ef25c099db1e72fb" - dependencies: - "@types/node" "^7.0.48" + version "2.12.2" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555" commondir@^1.0.1: version "1.0.1" @@ -580,8 +578,8 @@ content-type-parser@^1.0.1: resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7" convert-source-map@^1.3.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" + version "1.5.1" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" core-js@^2.4.0: version "2.5.1" @@ -744,12 +742,11 @@ doctrine@1.5.0: esutils "^2.0.2" isarray "^1.0.0" -doctrine@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63" +doctrine@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.2.tgz#68f96ce8efc56cc42651f1faadb4f175273b0075" dependencies: esutils "^2.0.2" - isarray "^1.0.0" dom-serializer@0: version "0.1.0" @@ -890,8 +887,8 @@ eslint-scope@^3.7.1: estraverse "^4.1.1" eslint@^4.3.0: - version "4.11.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.11.0.tgz#39a8c82bc0a3783adf5a39fa27fdd9d36fac9a34" + version "4.12.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.12.1.tgz#5ec1973822b4a066b353770c3c6d69a2a188e880" dependencies: ajv "^5.3.0" babel-code-frame "^6.22.0" @@ -899,7 +896,7 @@ eslint@^4.3.0: concat-stream "^1.6.0" cross-spawn "^5.1.0" debug "^3.0.1" - doctrine "^2.0.0" + doctrine "^2.0.2" eslint-scope "^3.7.1" espree "^3.5.2" esquery "^1.0.0" @@ -908,7 +905,7 @@ eslint@^4.3.0: file-entry-cache "^2.0.0" functional-red-black-tree "^1.0.1" glob "^7.1.2" - globals "^9.17.0" + globals "^11.0.1" ignore "^3.3.3" imurmurhash "^0.1.4" inquirer "^3.0.6" @@ -1030,10 +1027,14 @@ extract-zip@^1.0.3: mkdirp "0.5.0" yauzl "2.4.1" -extsprintf@1.3.0, extsprintf@^1.2.0: +extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + fast-deep-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" @@ -1272,7 +1273,11 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" -globals@^9.17.0, globals@^9.18.0: +globals@^11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.0.1.tgz#12a87bb010e5154396acc535e1e43fc753b0e5e8" + +globals@^9.18.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" @@ -1599,8 +1604,8 @@ is-path-in-cwd@^1.0.0: is-path-inside "^1.0.0" is-path-inside@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" dependencies: path-is-inside "^1.0.1" @@ -1723,8 +1728,8 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" jsdom@^11.1.0: - version "11.4.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.4.0.tgz#a3941a9699cbb0d61f8ab86f6f28f4ad5ea60d04" + version "11.5.1" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.5.1.tgz#5df753b8d0bca20142ce21f4f6c039f99a992929" dependencies: abab "^1.0.3" acorn "^5.1.2" @@ -1737,6 +1742,7 @@ jsdom@^11.1.0: domexception "^1.0.0" escodegen "^1.9.0" html-encoding-sniffer "^1.0.1" + left-pad "^1.2.0" nwmatcher "^1.4.3" parse5 "^3.0.2" pn "^1.0.0" @@ -1839,6 +1845,10 @@ lcid@^1.0.0: dependencies: invert-kv "^1.0.0" +left-pad@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.2.0.tgz#d30a73c6b8201d8f7d8e7956ba9616087a68e0ee" + levn@^0.3.0, levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" @@ -1866,8 +1876,8 @@ load-json-file@^2.0.0: strip-bom "^3.0.0" locate-character@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/locate-character/-/locate-character-2.0.1.tgz#48f9599f342daf26f73db32f45941eae37bae391" + version "2.0.3" + resolved "https://registry.yarnpkg.com/locate-character/-/locate-character-2.0.3.tgz#85a5aedae26b3536c3e97016af164cdaa3ae5ae1" locate-path@^2.0.0: version "2.0.0" @@ -3286,8 +3296,8 @@ typescript@^1.8.9: resolved "https://registry.yarnpkg.com/typescript/-/typescript-1.8.10.tgz#b475d6e0dff0bf50f296e5ca6ef9fbb5c7320f1e" typescript@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.1.tgz#ef39cdea27abac0b500242d6726ab90e0c846631" + version "2.6.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" uglify-js@^2.6: version "2.8.29"