diff --git a/.gitignore b/.gitignore index 5d7f11f151..25a75902db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,18 @@ .DS_Store -node_modules -compiler -ssr -shared.js -scratch -!test/compiler -!test/ssr .nyc_output -coverage -coverage.lcov -test/sourcemaps/samples/*/output.js -test/sourcemaps/samples/*/output.js.map -_actual.* -_actual-bundle.* -src/generators/dom/shared.ts -package-lock.json -.idea/ -*.iml -store.umd.js \ No newline at end of file +node_modules +/cli/ +/compiler/ +/ssr/ +/shared.js +/scratch/ +/coverage/ +/coverage.lcov/ +/test/cli/samples/*/actual +/test/sourcemaps/samples/*/output.js +/test/sourcemaps/samples/*/output.js.map +/src/compile/shared.ts +/package-lock.json +/store.umd.js +/yarn-error.log +_actual*.* \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a2216284b..08da7fd524 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,236 @@ # Svelte changelog +## 2.4.4 + +* Declare missing variable in Store ([#1415](https://github.com/sveltejs/svelte/issues/1415)) +* ALways declare spread levels ([#1413](https://github.com/sveltejs/svelte/issues/1413)) + +## 2.4.3 + +* `ref` directives prevent HTMLified content ([#1407](https://github.com/sveltejs/svelte/issues/1407)) +* Store computed properties update components immediately upon declaration ([#1327](https://github.com/sveltejs/svelte/issues/1327)) + +## 2.4.2 + +* Evaluate `each` key in child scope ([#1397](https://github.com/sveltejs/svelte/issues/1397)) +* Prevent false negatives and positives when detecting cyclical computed store properties ([#1399](https://github.com/sveltejs/svelte/issues/1399)) +* Only update dynamic component props ([#1394](https://github.com/sveltejs/svelte/issues/1394)) + +## 2.4.1 + +* Fix DOM event context ([#1390](https://github.com/sveltejs/svelte/issues/1390)) + +## 2.4.0 + +* Integrate CLI ([#1360](https://github.com/sveltejs/svelte/issues/1360)) +* Allow arbitrary destructuring for each block items, with binding ([#1385](https://github.com/sveltejs/svelte/pull/1385)) +* Each block keys can use arbitrary expressions ([#703](https://github.com/sveltejs/svelte/issues/703)) +* `bind:offsetWidth`, `bind:offsetHeight`, `bind:clientWidth` and `bind:clientHeight` ([#984](https://github.com/sveltejs/svelte/issues/984)) +* Leaner generated code for `each` blocks ([#1287](https://github.com/sveltejs/svelte/issues/1287)) + + +## 2.3.0 + +* Allow computed properties to have entire state object as dependency ([#1303](https://github.com/sveltejs/svelte/issues/1303)) +* Fix `stats` when `options.generate` is `false` ([#1368](https://github.com/sveltejs/svelte/issues/1368)) +* Assign custom methods to custom elements ([#1369](https://github.com/sveltejs/svelte/issues/1369)) +* Fix `this` value in custom event handlers ([#1297](https://github.com/sveltejs/svelte/issues/1297)) +* Re-evaluate `each` values lazily ([#1286](https://github.com/sveltejs/svelte/issues/1286)) +* Preserve outer context in `await` blocks ([#1251](https://github.com/sveltejs/svelte/issues/1251)) + +## 2.2.0 + +* Internal refactoring ([#1367](https://github.com/sveltejs/svelte/pull/1367)) + +## 2.1.1 + +* Report initial `changed` based on state, not expected props ([#1356](https://github.com/sveltejs/svelte/issues/1356)) +* Set state to empty object, not null, on destroy ([#1354](https://github.com/sveltejs/svelte/issues/1354)) +* Prevent stale state in component event handlers ([#1353](https://github.com/sveltejs/svelte/issues/1353)) + +## 2.1.0 + +* Allow shorthand imports ([#1038](https://github.com/sveltejs/svelte/issues/1038)) +* Update spread props inside each blocks ([#1337](https://github.com/sveltejs/svelte/issues/1337)) + +## 2.0.0 + +*See [the blog post](https://svelte.technology/blog/version-2) for information on how to upgrade your apps* + +* New template syntax ([#1318](https://github.com/sveltejs/svelte/issues/1318)) +* Emit ES2015 code, not ES5 ([#1348](https://github.com/sveltejs/svelte/pull/1348)) +* Add `onstate` and `onupdate` hooks, remove `component.observe` method ([#1197](https://github.com/sveltejs/svelte/issues/1197)) +* Use destructuring syntax for computed properties ([#1069](https://github.com/sveltejs/svelte/issues/1069) +* Change signature of `svelte.compile` ([#1298](https://github.com/sveltejs/svelte/pull/1298)) +* Remove `validate` and `Stylesheet` from public API ([#1348](https://github.com/sveltejs/svelte/pull/1348)) +* Don't typecast numeric attributes ([#657](https://github.com/sveltejs/svelte/issues/657)) +* Always compile with `Store` support, and cascading disabled ([#1348](https://github.com/sveltejs/svelte/pull/1348)) +* Remove unused `hash` property from AST ([#1348](https://github.com/sveltejs/svelte/pull/1348)) +* Rename `loc` property to `start` in warnings and errors ([#1348](https://github.com/sveltejs/svelte/pull/1348)) + +## 1.64.1 + +* Fix computed properties in SSR renderer ([#1349](https://github.com/sveltejs/svelte/issues/1349)) + +## 1.64.0 + +* Deprecate passing a string argument to `component.get` ([#1347](https://github.com/sveltejs/svelte/pull/1347)) + +## 1.63.1 + +* Allow `observe` method to be overwritten + +## 1.63.0 + +* Add `onstate` and `onupdate` lifecycle hooks and deprecate `component.observe` ([#1197](https://github.com/sveltejs/svelte/issues/1197)) +* Add `on` and `fire` to `Store`, deprecate `onchange` and `observe` ([#1344](https://github.com/sveltejs/svelte/pull/1344)) +* Require computed properties to have destructured argument in v2 mode ([#1069](https://github.com/sveltejs/svelte/issues/1069)) + +## 1.62.0 + +* Add a `code` field to errors and warnings ([#474](https://github.com/sveltejs/svelte/issues/474)) +* When using v2 syntax, do not use interpolation in non-root `\`;`} + ` : + (compiler.stylesheet.hasStyles && options.css !== false && + `if (!document.getElementById("${compiler.stylesheet.id}-style")) @add_css();`) + } + + ${(hasInitHooks || compiler.hasComponents || target.hasComplexBindings || target.hasIntroTransitions) && deindent` + if (!options.root) { + this._oncreate = []; + ${(compiler.hasComponents || target.hasComplexBindings) && `this._beforecreate = [];`} + ${(compiler.hasComponents || target.hasIntroTransitions) && `this._aftercreate = [];`} + } + `} + + ${compiler.slots.size && `this.slots = {};`} + + this._fragment = @create_main_fragment(this, this._state); + + ${hasInitHooks && deindent` + this.root._oncreate.push(() => { + ${templateProperties.onstate && `%onstate.call(this, { changed: @assignTrue({}, this._state), current: this._state });`} + ${templateProperties.oncreate && `%oncreate.call(this);`} + this.fire("update", { changed: @assignTrue({}, this._state), current: this._state }); + }); + `} + + ${compiler.customElement ? deindent` + this._fragment.c(); + this._fragment.${block.hasIntroMethod ? 'i' : 'm'}(this.shadowRoot, null); + + if (options.target) this._mount(options.target, options.anchor); + ` : deindent` + if (options.target) { + ${compiler.options.hydratable + ? deindent` + var nodes = @children(options.target); + options.hydrate ? this._fragment.l(nodes) : this._fragment.c(); + nodes.forEach(@detachNode); + ` : + deindent` + ${options.dev && `if (options.hydrate) throw new Error("options.hydrate only works if the component was compiled with the \`hydratable: true\` option");`} + this._fragment.c(); + `} + this._mount(options.target, options.anchor); + + ${(compiler.hasComponents || target.hasComplexBindings || hasInitHooks || target.hasIntroTransitions) && deindent` + ${compiler.hasComponents && `this._lock = true;`} + ${(compiler.hasComponents || target.hasComplexBindings) && `@callAll(this._beforecreate);`} + ${(compiler.hasComponents || hasInitHooks) && `@callAll(this._oncreate);`} + ${(compiler.hasComponents || target.hasIntroTransitions) && `@callAll(this._aftercreate);`} + ${compiler.hasComponents && `this._lock = false;`} + `} + } + `} + `; + + if (compiler.customElement) { + const props = compiler.props || Array.from(compiler.expectedProperties); + + builder.addBlock(deindent` + class ${name} extends HTMLElement { + constructor(options = {}) { + super(); + ${constructorBody} + } + + static get observedAttributes() { + return ${JSON.stringify(props)}; + } + + ${props.map(prop => deindent` + get ${prop}() { + return this.get().${prop}; + } + + set ${prop}(value) { + this.set({ ${prop}: value }); + } + `).join('\n\n')} + + ${compiler.slots.size && deindent` + connectedCallback() { + Object.keys(this._slotted).forEach(key => { + this.appendChild(this._slotted[key]); + }); + }`} + + attributeChangedCallback(attr, oldValue, newValue) { + this.set({ [attr]: newValue }); + } + + ${(compiler.hasComponents || target.hasComplexBindings || templateProperties.oncreate || target.hasIntroTransitions) && deindent` + connectedCallback() { + ${compiler.hasComponents && `this._lock = true;`} + ${(compiler.hasComponents || target.hasComplexBindings) && `@callAll(this._beforecreate);`} + ${(compiler.hasComponents || templateProperties.oncreate) && `@callAll(this._oncreate);`} + ${(compiler.hasComponents || target.hasIntroTransitions) && `@callAll(this._aftercreate);`} + ${compiler.hasComponents && `this._lock = false;`} + } + `} + } + + @assign(${name}.prototype, ${proto}); + ${templateProperties.methods && `@assign(${name}.prototype, %methods);`} + @assign(${name}.prototype, { + _mount(target, anchor) { + target.insertBefore(this, anchor); + } + }); + + customElements.define("${compiler.tag}", ${name}); + `); + } else { + builder.addBlock(deindent` + function ${name}(options) { + ${constructorBody} + } + + @assign(${name}.prototype, ${proto}); + ${templateProperties.methods && `@assign(${name}.prototype, %methods);`} + `); + } + + const immutable = templateProperties.immutable ? templateProperties.immutable.value.value : options.immutable; + + builder.addBlock(deindent` + ${options.dev && deindent` + ${name}.prototype._checkReadOnly = function _checkReadOnly(newState) { + ${Array.from(target.readonly).map( + prop => + `if ('${prop}' in newState && !this._updatingReadonlyProperty) throw new Error("${debugName}: Cannot set read-only property '${prop}'");` + )} + }; + `} + + ${computations.length ? deindent` + ${name}.prototype._recompute = function _recompute(changed, state) { + ${computationBuilder} + } + ` : (!sharedPath && `${name}.prototype._recompute = @noop;`)} + + ${templateProperties.setup && `%setup(${name});`} + + ${templateProperties.preload && `${name}.preload = %preload;`} + + ${immutable && `${name}.prototype._differs = @_differsImmutable;`} + `); + + let result = builder.toString(); + + const filename = options.filename && ( + typeof process !== 'undefined' ? options.filename.replace(process.cwd(), '').replace(/^[\/\\]/, '') : options.filename + ); + + return compiler.generate(result, options, { + banner: `/* ${filename ? `${filename} ` : ``}generated by Svelte v${"__VERSION__"} */`, + sharedPath, + name, + format, + }); +} diff --git a/src/compile/nodes/Action.ts b/src/compile/nodes/Action.ts new file mode 100644 index 0000000000..1e8f4311e7 --- /dev/null +++ b/src/compile/nodes/Action.ts @@ -0,0 +1,18 @@ +import Node from './shared/Node'; +import Expression from './shared/Expression'; + +export default class Action extends Node { + type: 'Action'; + name: string; + expression: Expression; + + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + + this.name = info.name; + + this.expression = info.expression + ? new Expression(compiler, this, scope, info.expression) + : null; + } +} \ No newline at end of file diff --git a/src/generators/nodes/Attribute.ts b/src/compile/nodes/Attribute.ts similarity index 75% rename from src/generators/nodes/Attribute.ts rename to src/compile/nodes/Attribute.ts index 08075efee2..b933ca2e7c 100644 --- a/src/generators/nodes/Attribute.ts +++ b/src/compile/nodes/Attribute.ts @@ -1,45 +1,101 @@ import deindent from '../../utils/deindent'; -import { stringify } from '../../utils/stringify'; +import { escape, escapeTemplate, stringify } from '../../utils/stringify'; import fixAttributeCasing from '../../utils/fixAttributeCasing'; -import getExpressionPrecedence from '../../utils/getExpressionPrecedence'; -import { DomGenerator } from '../dom/index'; +import addToSet from '../../utils/addToSet'; +import Compiler from '../Compiler'; import Node from './shared/Node'; import Element from './Element'; +import Text from './Text'; import Block from '../dom/Block'; +import Expression from './shared/Expression'; export interface StyleProp { key: string; value: Node[]; } -export default class Attribute { +export default class Attribute extends Node { type: 'Attribute'; start: number; end: number; - generator: DomGenerator; + compiler: Compiler; parent: Element; name: string; - value: true | Node[] - expression: Node; - - constructor({ - generator, - name, - value, - parent - }: { - generator: DomGenerator, - name: string, - value: Node[], - parent: Element - }) { - this.type = 'Attribute'; - this.generator = generator; - this.parent = parent; - - this.name = name; - this.value = value; + isSpread: boolean; + isTrue: boolean; + isDynamic: boolean; + isSynthetic: boolean; + shouldCache: boolean; + expression?: Expression; + chunks: (Text | Expression)[]; + dependencies: Set; + + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + + if (info.type === 'Spread') { + this.name = null; + this.isSpread = true; + this.isTrue = false; + this.isSynthetic = false; + + this.expression = new Expression(compiler, this, scope, info.expression); + this.dependencies = this.expression.dependencies; + this.chunks = null; + + this.isDynamic = true; // TODO not necessarily + this.shouldCache = false; // TODO does this mean anything here? + } + + else { + this.name = info.name; + this.isTrue = info.value === true; + this.isSynthetic = info.synthetic; + + this.dependencies = new Set(); + + this.chunks = this.isTrue + ? [] + : info.value.map(node => { + if (node.type === 'Text') return node; + + const expression = new Expression(compiler, this, scope, node.expression); + + addToSet(this.dependencies, expression.dependencies); + return expression; + }); + + this.isDynamic = this.dependencies.size > 0; + + this.shouldCache = this.isDynamic + ? this.chunks.length === 1 + ? this.chunks[0].node.type !== 'Identifier' || scope.names.has(this.chunks[0].node.name) + : true + : false; + } + } + + getValue() { + if (this.isTrue) return true; + if (this.chunks.length === 0) return `""`; + + if (this.chunks.length === 1) { + return this.chunks[0].type === 'Text' + ? stringify(this.chunks[0].data) + : this.chunks[0].snippet; + } + + return (this.chunks[0].type === 'Text' ? '' : `"" + `) + + this.chunks + .map(chunk => { + if (chunk.type === 'Text') { + return stringify(chunk.data); + } else { + return chunk.getPrecedence() <= 13 ? `(${chunk.snippet})` : chunk.snippet; + } + }) + .join(' + '); } render(block: Block) { @@ -47,7 +103,7 @@ export default class Attribute { const name = fixAttributeCasing(this.name); if (name === 'style') { - const styleProps = optimizeStyle(this.value); + const styleProps = optimizeStyle(this.chunks); if (styleProps) { this.renderStyle(block, styleProps); return; @@ -62,9 +118,9 @@ export default class Attribute { name === 'value' && (node.name === 'option' || // TODO check it's actually bound (node.name === 'input' && - node.attributes.find( - (attribute: Attribute) => - attribute.type === 'Binding' && /checked|group/.test(attribute.name) + node.bindings.find( + (binding: Binding) => + /checked|group/.test(binding.name) ))); const propertyName = isIndirectlyBoundValue @@ -78,77 +134,48 @@ export default class Attribute { ? '@setXlinkAttribute' : '@setAttribute'; - const isDynamic = this.isDynamic(); - const isLegacyInputType = this.generator.legacy && name === 'type' && this.parent.name === 'input'; + const isLegacyInputType = this.compiler.options.legacy && name === 'type' && this.parent.name === 'input'; - const isDataSet = /^data-/.test(name) && !this.generator.legacy && !node.namespace; + const isDataSet = /^data-/.test(name) && !this.compiler.options.legacy && !node.namespace; const camelCaseName = isDataSet ? name.replace('data-', '').replace(/(-\w)/g, function (m) { return m[1].toUpperCase(); }) : name; - if (isDynamic) { + if (this.isDynamic) { let value; - const allDependencies = new Set(); - let shouldCache; - let hasChangeableIndex; - // TODO some of this code is repeated in Tag.ts — would be good to // DRY it out if that's possible without introducing crazy indirection - if (this.value.length === 1) { - // single {{tag}} — may be a non-string - const { expression } = this.value[0]; - const { indexes } = block.contextualise(expression); - const { dependencies, snippet } = this.value[0].metadata; - - value = snippet; - dependencies.forEach(d => { - allDependencies.add(d); - }); - - hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index)); - - shouldCache = ( - expression.type !== 'Identifier' || - block.contexts.has(expression.name) || - hasChangeableIndex - ); + if (this.chunks.length === 1) { + // single {tag} — may be a non-string + value = this.chunks[0].snippet; } else { - // '{{foo}} {{bar}}' — treat as string concatenation + // '{foo} {bar}' — treat as string concatenation value = - (this.value[0].type === 'Text' ? '' : `"" + `) + - this.value + (this.chunks[0].type === 'Text' ? '' : `"" + `) + + this.chunks .map((chunk: Node) => { if (chunk.type === 'Text') { return stringify(chunk.data); } else { - const { indexes } = block.contextualise(chunk.expression); - const { dependencies, snippet } = chunk.metadata; - - if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) { - hasChangeableIndex = true; - } - - dependencies.forEach(d => { - allDependencies.add(d); - }); - - return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet; + return chunk.getPrecedence() <= 13 + ? `(${chunk.snippet})` + : chunk.snippet; } }) .join(' + '); - - shouldCache = true; } const isSelectValueAttribute = name === 'value' && node.name === 'select'; - const last = (shouldCache || isSelectValueAttribute) && block.getUniqueName( + const shouldCache = this.shouldCache || isSelectValueAttribute; + + const last = shouldCache && block.getUniqueName( `${node.var}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value` ); - if (shouldCache || isSelectValueAttribute) block.addVariable(last); + if (shouldCache) block.addVariable(last); let updater; const init = shouldCache ? `${last} = ${value}` : value; @@ -185,27 +212,25 @@ export default class Attribute { ${last} = ${value}; ${updater} `); - - block.builders.update.addLine(`${last} = ${value};`); } else if (propertyName) { block.builders.hydrate.addLine( `${node.var}.${propertyName} = ${init};` ); - updater = `${node.var}.${propertyName} = ${shouldCache || isSelectValueAttribute ? last : value};`; + updater = `${node.var}.${propertyName} = ${shouldCache ? last : value};`; } else if (isDataSet) { block.builders.hydrate.addLine( `${node.var}.dataset.${camelCaseName} = ${init};` ); - updater = `${node.var}.dataset.${camelCaseName} = ${shouldCache || isSelectValueAttribute ? last : value};`; + updater = `${node.var}.dataset.${camelCaseName} = ${shouldCache ? last : value};`; } else { block.builders.hydrate.addLine( `${method}(${node.var}, "${name}", ${init});` ); - updater = `${method}(${node.var}, "${name}", ${shouldCache || isSelectValueAttribute ? last : value});`; + updater = `${method}(${node.var}, "${name}", ${shouldCache ? last : value});`; } - if (allDependencies.size || hasChangeableIndex || isSelectValueAttribute) { - const dependencies = Array.from(allDependencies); + if (this.dependencies.size || isSelectValueAttribute) { + const dependencies = Array.from(this.dependencies); const changedCheck = ( ( block.hasOutroMethod ? `#outroing || ` : '' ) + dependencies.map(dependency => `changed.${dependency}`).join(' || ') @@ -223,23 +248,22 @@ export default class Attribute { ); } } else { - const value = this.value === true - ? 'true' - : this.value.length === 0 - ? `''` - : stringify(this.value[0].data); + const value = this.getValue(); const statement = ( - isLegacyInputType ? `@setInputType(${node.var}, ${value});` : - propertyName ? `${node.var}.${propertyName} = ${value};` : - isDataSet ? `${node.var}.dataset.${camelCaseName} = ${value};` : - `${method}(${node.var}, "${name}", ${value});` + isLegacyInputType + ? `@setInputType(${node.var}, ${value});` + : propertyName + ? `${node.var}.${propertyName} = ${value};` + : isDataSet + ? `${node.var}.dataset.${camelCaseName} = ${value};` + : `${method}(${node.var}, "${name}", ${value});` ); block.builders.hydrate.addLine(statement); // special case – autofocus. has to be handled in a bit of a weird way - if (this.value === true && name === 'autofocus') { + if (this.isTrue && name === 'autofocus') { block.autofocus = node.var; } } @@ -248,7 +272,7 @@ export default class Attribute { const updateValue = `${node.var}.value = ${node.var}.__value;`; block.builders.hydrate.addLine(updateValue); - if (isDynamic) block.builders.update.addLine(updateValue); + if (this.isDynamic) block.builders.update.addLine(updateValue); } } @@ -260,9 +284,8 @@ export default class Attribute { let value; if (isDynamic(prop.value)) { - const allDependencies = new Set(); + const propDependencies = new Set(); let shouldCache; - let hasChangeableIndex; value = ((prop.value.length === 1 || prop.value[0].type === 'Text') ? '' : `"" + `) + @@ -271,26 +294,21 @@ export default class Attribute { if (chunk.type === 'Text') { return stringify(chunk.data); } else { - const { indexes } = block.contextualise(chunk.expression); - const { dependencies, snippet } = chunk.metadata; - - if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) { - hasChangeableIndex = true; - } + const { dependencies, snippet } = chunk; dependencies.forEach(d => { - allDependencies.add(d); + propDependencies.add(d); }); - return getExpressionPrecedence(chunk.expression) <= 13 ? `( ${snippet} )` : snippet; + return chunk.getPrecedence() <= 13 ? `(${snippet})` : snippet; } }) .join(' + '); - if (allDependencies.size || hasChangeableIndex) { - const dependencies = Array.from(allDependencies); + if (propDependencies.size) { + const dependencies = Array.from(propDependencies); const condition = ( - ( block.hasOutroMethod ? `#outroing || ` : '' ) + + (block.hasOutroMethod ? `#outroing || ` : '') + dependencies.map(dependency => `changed.${dependency}`).join(' || ') ); @@ -309,10 +327,16 @@ export default class Attribute { }); } - isDynamic() { - if (this.value === true || this.value.length === 0) return false; - if (this.value.length > 1) return true; - return this.value[0].type !== 'Text'; + stringifyForSsr() { + return this.chunks + .map((chunk: Node) => { + if (chunk.type === 'Text') { + return escapeTemplate(escape(chunk.data).replace(/"/g, '"')); + } + + return '${@escape(' + chunk.snippet + ')}'; + }) + .join(''); } } @@ -516,7 +540,6 @@ const attributeLookup = { type: { appliesTo: [ 'button', - 'input', 'command', 'embed', 'object', diff --git a/src/compile/nodes/AwaitBlock.ts b/src/compile/nodes/AwaitBlock.ts new file mode 100644 index 0000000000..26bad55cc9 --- /dev/null +++ b/src/compile/nodes/AwaitBlock.ts @@ -0,0 +1,200 @@ +import deindent from '../../utils/deindent'; +import Node from './shared/Node'; +import Block from '../dom/Block'; +import PendingBlock from './PendingBlock'; +import ThenBlock from './ThenBlock'; +import CatchBlock from './CatchBlock'; +import createDebuggingComment from '../../utils/createDebuggingComment'; +import Expression from './shared/Expression'; +import { SsrTarget } from '../ssr'; + +export default class AwaitBlock extends Node { + expression: Expression; + value: string; + error: string; + + pending: PendingBlock; + then: ThenBlock; + catch: CatchBlock; + + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + + this.expression = new Expression(compiler, this, scope, info.expression); + const deps = this.expression.dependencies; + + this.value = info.value; + this.error = info.error; + + this.pending = new PendingBlock(compiler, this, scope, info.pending); + this.then = new ThenBlock(compiler, this, scope.add(this.value, deps), info.then); + this.catch = new CatchBlock(compiler, this, scope.add(this.error, deps), info.catch); + } + + init( + block: Block, + stripWhitespace: boolean, + nextSibling: Node + ) { + this.cannotUseInnerHTML(); + + this.var = block.getUniqueName('await_block'); + block.addDependencies(this.expression.dependencies); + + let isDynamic = false; + let hasIntros = false; + let hasOutros = false; + + ['pending', 'then', 'catch'].forEach(status => { + const child = this[status]; + + child.block = block.child({ + comment: createDebuggingComment(child, this.compiler), + name: this.compiler.getUniqueName(`create_${status}_block`) + }); + + child.initChildren(child.block, stripWhitespace, nextSibling); + this.compiler.target.blocks.push(child.block); + + if (child.block.dependencies.size > 0) { + isDynamic = true; + block.addDependencies(child.block.dependencies); + } + + if (child.block.hasIntroMethod) hasIntros = true; + if (child.block.hasOutroMethod) hasOutros = true; + }); + + this.pending.block.hasUpdateMethod = isDynamic; + this.then.block.hasUpdateMethod = isDynamic; + this.catch.block.hasUpdateMethod = isDynamic; + + this.pending.block.hasIntroMethod = hasIntros; + this.then.block.hasIntroMethod = hasIntros; + this.catch.block.hasIntroMethod = hasIntros; + + this.pending.block.hasOutroMethod = hasOutros; + this.then.block.hasOutroMethod = hasOutros; + this.catch.block.hasOutroMethod = hasOutros; + } + + build( + block: Block, + parentNode: string, + parentNodes: string + ) { + const name = this.var; + + const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes); + const updateMountNode = this.getUpdateMountNode(anchor); + + const { snippet } = this.expression; + + const info = block.getUniqueName(`info`); + const promise = block.getUniqueName(`promise`); + + block.addVariable(promise); + + block.maintainContext = true; + + const infoProps = [ + block.alias('component') === 'component' ? 'component' : `component: #component`, + 'ctx', + 'current: null', + this.pending.block.name && `pending: ${this.pending.block.name}`, + this.then.block.name && `then: ${this.then.block.name}`, + this.catch.block.name && `catch: ${this.catch.block.name}`, + this.then.block.name && `value: '${this.value}'`, + this.catch.block.name && `error: '${this.error}'`, + this.pending.block.hasOutroMethod && `blocks: Array(3)` + ].filter(Boolean); + + block.builders.init.addBlock(deindent` + let ${info} = { + ${infoProps.join(',\n')} + }; + `); + + block.builders.init.addBlock(deindent` + @handlePromise(${promise} = ${snippet}, ${info}); + `); + + block.builders.create.addBlock(deindent` + ${info}.block.c(); + `); + + if (parentNodes) { + block.builders.claim.addBlock(deindent` + ${info}.block.l(${parentNodes}); + `); + } + + const initialMountNode = parentNode || '#target'; + const anchorNode = parentNode ? 'null' : 'anchor'; + + block.builders.mount.addBlock(deindent` + ${info}.block.${this.pending.block.hasIntroMethod ? 'i' : 'm'}(${initialMountNode}, ${info}.anchor = ${anchorNode}); + ${info}.mount = () => ${updateMountNode}; + `); + + const conditions = []; + if (this.expression.dependencies.size > 0) { + conditions.push( + `(${[...this.expression.dependencies].map(dep => `'${dep}' in changed`).join(' || ')})` + ); + } + + conditions.push( + `${promise} !== (${promise} = ${snippet})`, + `@handlePromise(${promise}, ${info})` + ); + + block.builders.update.addLine( + `${info}.ctx = ctx;` + ); + + if (this.pending.block.hasUpdateMethod) { + block.builders.update.addBlock(deindent` + if (${conditions.join(' && ')}) { + // nothing + } else { + ${info}.block.p(changed, @assign(@assign({}, ctx), ${info}.resolved)); + } + `); + } else { + block.builders.update.addBlock(deindent` + ${conditions.join(' && ')} + `); + } + + block.builders.destroy.addBlock(deindent` + ${info}.block.d(${parentNode ? '' : 'detach'}); + ${info} = null; + `); + + [this.pending, this.then, this.catch].forEach(status => { + status.children.forEach(child => { + child.build(status.block, null, 'nodes'); + }); + }); + } + + ssr() { + const target: SsrTarget = this.compiler.target; + const { snippet } = this.expression; + + target.append('${(function(__value) { if(@isPromise(__value)) return `'); + + this.pending.children.forEach((child: Node) => { + child.ssr(); + }); + + target.append('`; return function(ctx) { return `'); + + this.then.children.forEach((child: Node) => { + child.ssr(); + }); + + target.append(`\`;}(Object.assign({}, ctx, { ${this.value}: __value }));}(${snippet})) }`); + } +} diff --git a/src/generators/nodes/Binding.ts b/src/compile/nodes/Binding.ts similarity index 63% rename from src/generators/nodes/Binding.ts rename to src/compile/nodes/Binding.ts index f1725a6d73..e0f928ccfa 100644 --- a/src/generators/nodes/Binding.ts +++ b/src/compile/nodes/Binding.ts @@ -3,8 +3,10 @@ import Element from './Element'; import getObject from '../../utils/getObject'; import getTailSnippet from '../../utils/getTailSnippet'; import flattenReference from '../../utils/flattenReference'; -import { DomGenerator } from '../dom/index'; +import Compiler from '../Compiler'; import Block from '../dom/Block'; +import Expression from './shared/Expression'; +import { dimensions } from '../../utils/patterns'; const readOnlyMediaAttributes = new Set([ 'duration', @@ -13,48 +15,80 @@ const readOnlyMediaAttributes = new Set([ 'played' ]); +// TODO a lot of this element-specific stuff should live in Element — +// Binding should ideally be agnostic between Element and Component + export default class Binding extends Node { name: string; - value: Node; - expression: Node; + value: Expression; + isContextual: boolean; + usesContext: boolean; + obj: string; + prop: string; + + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + + this.name = info.name; + this.value = new Expression(compiler, this, scope, info.value); + + let obj; + let prop; + + const { name } = getObject(this.value.node); + this.isContextual = scope.names.has(name); + + if (this.value.node.type === 'MemberExpression') { + prop = `[✂${this.value.node.property.start}-${this.value.node.property.end}✂]`; + if (!this.value.node.computed) prop = `'${prop}'`; + obj = `[✂${this.value.node.object.start}-${this.value.node.object.end}✂]`; + + this.usesContext = true; + } else { + obj = 'ctx'; + prop = `'${name}'`; + + this.usesContext = scope.names.has(name); + } + + this.obj = obj; + this.prop = prop; + } munge( - block: Block, - allUsedContexts: Set + block: Block ) { const node: Element = this.parent; const needsLock = node.name !== 'input' || !/radio|checkbox|range|color/.test(node.getStaticAttributeValue('type')); - const isReadOnly = node.isMediaNode() && readOnlyMediaAttributes.has(this.name); + const isReadOnly = ( + (node.isMediaNode() && readOnlyMediaAttributes.has(this.name)) || + dimensions.test(this.name) + ); let updateCondition: string; - const { name } = getObject(this.value); - const { contexts } = block.contextualise(this.value); - const { snippet } = this.metadata; + const { name } = getObject(this.value.node); + const { snippet } = this.value; // special case: if you have e.g. `` // and `selected` is an object chosen with a if (binding.name === 'group') { - const bindingGroup = getBindingGroup(generator, binding.value); + const bindingGroup = getBindingGroup(compiler, binding.value.node); if (type === 'checkbox') { return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`; } diff --git a/src/compile/nodes/CatchBlock.ts b/src/compile/nodes/CatchBlock.ts new file mode 100644 index 0000000000..db15f7ceb1 --- /dev/null +++ b/src/compile/nodes/CatchBlock.ts @@ -0,0 +1,13 @@ +import Node from './shared/Node'; +import Block from '../dom/Block'; +import mapChildren from './shared/mapChildren'; + +export default class CatchBlock extends Node { + block: Block; + children: Node[]; + + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + this.children = mapChildren(compiler, parent, scope, info.children); + } +} \ No newline at end of file diff --git a/src/compile/nodes/Comment.ts b/src/compile/nodes/Comment.ts new file mode 100644 index 0000000000..ba20f5750a --- /dev/null +++ b/src/compile/nodes/Comment.ts @@ -0,0 +1,18 @@ +import Node from './shared/Node'; + +export default class Comment extends Node { + type: 'Comment'; + data: string; + + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + this.data = info.data; + } + + ssr() { + // Allow option to preserve comments, otherwise ignore + if (this.compiler.options.preserveComments) { + this.compiler.target.append(``); + } + } +} \ No newline at end of file diff --git a/src/compile/nodes/Component.ts b/src/compile/nodes/Component.ts new file mode 100644 index 0000000000..71c5f8082b --- /dev/null +++ b/src/compile/nodes/Component.ts @@ -0,0 +1,619 @@ +import deindent from '../../utils/deindent'; +import flattenReference from '../../utils/flattenReference'; +import validCalleeObjects from '../../utils/validCalleeObjects'; +import stringifyProps from '../../utils/stringifyProps'; +import CodeBuilder from '../../utils/CodeBuilder'; +import getTailSnippet from '../../utils/getTailSnippet'; +import getObject from '../../utils/getObject'; +import quoteIfNecessary from '../../utils/quoteIfNecessary'; +import { escape, escapeTemplate, stringify } from '../../utils/stringify'; +import Node from './shared/Node'; +import Block from '../dom/Block'; +import Attribute from './Attribute'; +import usesThisOrArguments from '../../validate/js/utils/usesThisOrArguments'; +import mapChildren from './shared/mapChildren'; +import Binding from './Binding'; +import EventHandler from './EventHandler'; +import Expression from './shared/Expression'; +import { AppendTarget } from '../../interfaces'; +import addToSet from '../../utils/addToSet'; + +export default class Component extends Node { + type: 'Component'; + name: string; + expression: Expression; + attributes: Attribute[]; + bindings: Binding[]; + handlers: EventHandler[]; + children: Node[]; + ref: string; + + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + + compiler.hasComponents = true; + + this.name = info.name; + + this.expression = this.name === 'svelte:component' + ? new Expression(compiler, this, scope, info.expression) + : null; + + this.attributes = []; + this.bindings = []; + this.handlers = []; + + info.attributes.forEach(node => { + switch (node.type) { + case 'Attribute': + case 'Spread': + this.attributes.push(new Attribute(compiler, this, scope, node)); + break; + + case 'Binding': + this.bindings.push(new Binding(compiler, this, scope, node)); + break; + + case 'EventHandler': + this.handlers.push(new EventHandler(compiler, this, scope, node)); + break; + + case 'Ref': + // TODO catch this in validation + if (this.ref) throw new Error(`Duplicate refs`); + + compiler.usesRefs = true + this.ref = node.name; + break; + + default: + throw new Error(`Not implemented: ${node.type}`); + } + }); + + this.children = mapChildren(compiler, this, scope, info.children); + } + + init( + block: Block, + stripWhitespace: boolean, + nextSibling: Node + ) { + this.cannotUseInnerHTML(); + + this.attributes.forEach(attr => { + block.addDependencies(attr.dependencies); + }); + + this.bindings.forEach(binding => { + block.addDependencies(binding.value.dependencies); + }); + + this.handlers.forEach(handler => { + block.addDependencies(handler.dependencies); + }); + + this.var = block.getUniqueName( + ( + this.name === 'svelte:self' ? this.compiler.name : + this.name === 'svelte:component' ? 'switch_instance' : + this.name + ).toLowerCase() + ); + + if (this.children.length) { + this._slots = new Set(['default']); + + this.children.forEach(child => { + child.init(block, stripWhitespace, nextSibling); + }); + } + } + + build( + block: Block, + parentNode: string, + parentNodes: string + ) { + const { compiler } = this; + + const name = this.var; + + const componentInitProperties = [`root: #component.root`]; + + if (this.children.length > 0) { + const slots = Array.from(this._slots).map(name => `${quoteIfNecessary(name)}: @createFragment()`); + componentInitProperties.push(`slots: { ${slots.join(', ')} }`); + + this.children.forEach((child: Node) => { + child.build(block, `${this.var}._slotted.default`, 'nodes'); + }); + } + + const statements: string[] = []; + + const name_initial_data = block.getUniqueName(`${name}_initial_data`); + const name_changes = block.getUniqueName(`${name}_changes`); + let name_updating: string; + let beforecreate: string = null; + + const updates: string[] = []; + + const usesSpread = !!this.attributes.find(a => a.isSpread); + + const attributeObject = usesSpread + ? '{}' + : stringifyProps( + this.attributes.map(attr => `${attr.name}: ${attr.getValue()}`) + ); + + if (this.attributes.length || this.bindings.length) { + componentInitProperties.push(`data: ${name_initial_data}`); + } + + if ((!usesSpread && this.attributes.filter(a => a.isDynamic).length) || this.bindings.length) { + updates.push(`var ${name_changes} = {};`); + } + + if (this.attributes.length) { + if (usesSpread) { + const levels = block.getUniqueName(`${this.var}_spread_levels`); + + const initialProps = []; + const changes = []; + + const allDependencies = new Set(); + + this.attributes.forEach(attr => { + addToSet(allDependencies, attr.dependencies); + }); + + this.attributes.forEach(attr => { + const { name, dependencies } = attr; + + const condition = dependencies.size > 0 && (dependencies.size !== allDependencies.size) + ? [...dependencies].map(d => `changed.${d}`).join(' || ') + : null; + + if (attr.isSpread) { + const value = attr.expression.snippet; + initialProps.push(value); + + changes.push(condition ? `${condition} && ${value}` : value); + } else { + const obj = `{ ${quoteIfNecessary(name)}: ${attr.getValue()} }`; + initialProps.push(obj); + + changes.push(condition ? `${condition} && ${obj}` : obj); + } + }); + + block.builders.init.addBlock(deindent` + var ${levels} = [ + ${initialProps.join(',\n')} + ]; + `); + + statements.push(deindent` + for (var #i = 0; #i < ${levels}.length; #i += 1) { + ${name_initial_data} = @assign(${name_initial_data}, ${levels}[#i]); + } + `); + + const conditions = [...allDependencies].map(dep => `changed.${dep}`).join(' || '); + + updates.push(deindent` + var ${name_changes} = ${allDependencies.size === 1 ? `${conditions}` : `(${conditions})`} && @getSpreadUpdate(${levels}, [ + ${changes.join(',\n')} + ]); + `); + } else { + this.attributes + .filter((attribute: Attribute) => attribute.isDynamic) + .forEach((attribute: Attribute) => { + if (attribute.dependencies.size > 0) { + updates.push(deindent` + if (${[...attribute.dependencies] + .map(dependency => `changed.${dependency}`) + .join(' || ')}) ${name_changes}.${attribute.name} = ${attribute.getValue()}; + `); + } + }); + } + } + + if (this.bindings.length) { + compiler.target.hasComplexBindings = true; + + name_updating = block.alias(`${name}_updating`); + block.addVariable(name_updating, '{}'); + + let hasLocalBindings = false; + let hasStoreBindings = false; + + const builder = new CodeBuilder(); + + this.bindings.forEach((binding: Binding) => { + let { name: key } = getObject(binding.value.node); + + let setFromChild; + + if (binding.isContextual) { + const computed = isComputed(binding.value.node); + const tail = binding.value.node.type === 'MemberExpression' ? getTailSnippet(binding.value.node) : ''; + + const head = block.bindings.get(key); + + const lhs = binding.value.node.type === 'MemberExpression' + ? binding.value.snippet + : `${head}${tail} = childState.${binding.name}`; + + setFromChild = deindent` + ${lhs} = childState.${binding.name}; + + ${[...binding.value.dependencies] + .map((name: string) => { + const isStoreProp = name[0] === '$'; + const prop = isStoreProp ? name.slice(1) : name; + const newState = isStoreProp ? 'newStoreState' : 'newState'; + + if (isStoreProp) hasStoreBindings = true; + else hasLocalBindings = true; + + return `${newState}.${prop} = ctx.${name};`; + })} + `; + } + + else { + const isStoreProp = key[0] === '$'; + const prop = isStoreProp ? key.slice(1) : key; + const newState = isStoreProp ? 'newStoreState' : 'newState'; + + if (isStoreProp) hasStoreBindings = true; + else hasLocalBindings = true; + + if (binding.value.node.type === 'MemberExpression') { + setFromChild = deindent` + ${binding.value.snippet} = childState.${binding.name}; + ${newState}.${prop} = ctx.${key}; + `; + } + + else { + setFromChild = `${newState}.${prop} = childState.${binding.name};`; + } + } + + statements.push(deindent` + if (${binding.prop} in ${binding.obj}) { + ${name_initial_data}.${binding.name} = ${binding.value.snippet}; + ${name_updating}.${binding.name} = true; + }` + ); + + builder.addConditional( + `!${name_updating}.${binding.name} && changed.${binding.name}`, + setFromChild + ); + + updates.push(deindent` + if (!${name_updating}.${binding.name} && ${[...binding.value.dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')}) { + ${name_changes}.${binding.name} = ${binding.value.snippet}; + ${name_updating}.${binding.name} = true; + } + `); + }); + + block.maintainContext = true; // TODO put this somewhere more logical + + const initialisers = [ + hasLocalBindings && 'newState = {}', + hasStoreBindings && 'newStoreState = {}', + ].filter(Boolean).join(', '); + + // TODO use component.on('state', ...) instead of _bind + componentInitProperties.push(deindent` + _bind: function(changed, childState) { + var ${initialisers}; + ${builder} + ${hasStoreBindings && `#component.store.set(newStoreState);`} + ${hasLocalBindings && `#component._set(newState);`} + ${name_updating} = {}; + } + `); + + beforecreate = deindent` + #component.root._beforecreate.push(function() { + ${name}._bind({ ${this.bindings.map(b => `${b.name}: 1`).join(', ')} }, ${name}.get()); + }); + `; + } + + this.handlers.forEach(handler => { + handler.var = block.getUniqueName(`${this.var}_${handler.name}`); // TODO this is hacky + handler.render(compiler, block, false); // TODO hoist when possible + if (handler.usesContext) block.maintainContext = true; // TODO is there a better place to put this? + }); + + if (this.name === 'svelte:component') { + const switch_value = block.getUniqueName('switch_value'); + const switch_props = block.getUniqueName('switch_props'); + + const { dependencies, snippet } = this.expression; + + const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes); + + block.builders.init.addBlock(deindent` + var ${switch_value} = ${snippet}; + + function ${switch_props}(ctx) { + ${(this.attributes.length || this.bindings.length) && deindent` + var ${name_initial_data} = ${attributeObject};`} + ${statements} + return { + ${componentInitProperties.join(',\n')} + }; + } + + if (${switch_value}) { + var ${name} = new ${switch_value}(${switch_props}(ctx)); + + ${beforecreate} + } + + ${this.handlers.map(handler => deindent` + function ${handler.var}(event) { + ${handler.snippet} + } + + if (${name}) ${name}.on("${handler.name}", ${handler.var}); + `)} + `); + + block.builders.create.addLine( + `if (${name}) ${name}._fragment.c();` + ); + + if (parentNodes) { + block.builders.claim.addLine( + `if (${name}) ${name}._fragment.l(${parentNodes});` + ); + } + + block.builders.mount.addBlock(deindent` + if (${name}) { + ${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'}); + ${this.ref && `#component.refs.${this.ref} = ${name};`} + } + `); + + const updateMountNode = this.getUpdateMountNode(anchor); + + if (updates.length) { + block.builders.update.addBlock(deindent` + ${updates} + `); + } + + block.builders.update.addBlock(deindent` + if (${switch_value} !== (${switch_value} = ${snippet})) { + if (${name}) ${name}.destroy(); + + if (${switch_value}) { + ${name} = new ${switch_value}(${switch_props}(ctx)); + ${name}._fragment.c(); + + ${this.children.map(child => child.remount(name))} + ${name}._mount(${updateMountNode}, ${anchor}); + + ${this.handlers.map(handler => deindent` + ${name}.on("${handler.name}", ${handler.var}); + `)} + + ${this.ref && `#component.refs.${this.ref} = ${name};`} + } + + ${this.ref && deindent` + else if (#component.refs.${this.ref} === ${name}) { + #component.refs.${this.ref} = null; + }`} + } + `); + + if (updates.length) { + block.builders.update.addBlock(deindent` + else if (${switch_value}) { + ${name}._set(${name_changes}); + ${this.bindings.length && `${name_updating} = {};`} + } + `); + } + + block.builders.destroy.addLine(`if (${name}) ${name}.destroy(${parentNode ? '' : 'detach'});`); + } else { + const expression = this.name === 'svelte:self' + ? compiler.name + : `%components-${this.name}`; + + block.builders.init.addBlock(deindent` + ${(this.attributes.length || this.bindings.length) && deindent` + var ${name_initial_data} = ${attributeObject};`} + ${statements} + var ${name} = new ${expression}({ + ${componentInitProperties.join(',\n')} + }); + + ${beforecreate} + + ${this.handlers.map(handler => deindent` + ${name}.on("${handler.name}", function(event) { + ${handler.snippet || `#component.fire("${handler.name}", event);`} + }); + `)} + + ${this.ref && `#component.refs.${this.ref} = ${name};`} + `); + + block.builders.create.addLine(`${name}._fragment.c();`); + + if (parentNodes) { + block.builders.claim.addLine( + `${name}._fragment.l(${parentNodes});` + ); + } + + block.builders.mount.addLine( + `${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});` + ); + + if (updates.length) { + block.builders.update.addBlock(deindent` + ${updates} + ${name}._set(${name_changes}); + ${this.bindings.length && `${name_updating} = {};`} + `); + } + + block.builders.destroy.addLine(deindent` + ${name}.destroy(${parentNode ? '' : 'detach'}); + ${this.ref && `if (#component.refs.${this.ref} === ${name}) #component.refs.${this.ref} = null;`} + `); + } + } + + remount(name: string) { + return `${this.var}._mount(${name}._slotted.default, null);`; + } + + ssr() { + function stringifyAttribute(chunk: Node) { + if (chunk.type === 'Text') { + return escapeTemplate(escape(chunk.data)); + } + + return '${@escape( ' + chunk.snippet + ')}'; + } + + const bindingProps = this.bindings.map(binding => { + const { name } = getObject(binding.value.node); + const tail = binding.value.node.type === 'MemberExpression' + ? getTailSnippet(binding.value.node) + : ''; + + return `${binding.name}: ctx.${name}${tail}`; + }); + + function getAttributeValue(attribute) { + if (attribute.isTrue) return `true`; + if (attribute.chunks.length === 0) return `''`; + + if (attribute.chunks.length === 1) { + const chunk = attribute.chunks[0]; + if (chunk.type === 'Text') { + return stringify(chunk.data); + } + + return chunk.snippet; + } + + return '`' + attribute.chunks.map(stringifyAttribute).join('') + '`'; + } + + const usesSpread = this.attributes.find(attr => attr.isSpread); + + const props = usesSpread + ? `Object.assign(${ + this.attributes + .map(attribute => { + if (attribute.isSpread) { + return attribute.expression.snippet; + } else { + return `{ ${attribute.name}: ${getAttributeValue(attribute)} }`; + } + }) + .concat(bindingProps.map(p => `{ ${p} }`)) + .join(', ') + })` + : `{ ${this.attributes + .map(attribute => `${attribute.name}: ${getAttributeValue(attribute)}`) + .concat(bindingProps) + .join(', ')} }`; + + const isDynamicComponent = this.name === 'svelte:component'; + + const expression = ( + this.name === 'svelte:self' ? this.compiler.name : + isDynamicComponent ? `((${this.expression.snippet}) || @missingComponent)` : + `%components-${this.name}` + ); + + this.bindings.forEach(binding => { + const conditions = []; + + let node = this; + while (node = node.parent) { + if (node.type === 'IfBlock') { + // TODO handle contextual bindings... + conditions.push(`(${node.expression.snippet})`); + } + } + + conditions.push(`!('${binding.name}' in ctx)`); + + const { name } = getObject(binding.value.node); + + this.compiler.target.bindings.push(deindent` + if (${conditions.reverse().join('&&')}) { + tmp = ${expression}.data(); + if ('${name}' in tmp) { + ctx.${binding.name} = tmp.${name}; + settled = false; + } + } + `); + }); + + let open = `\${${expression}._render(__result, ${props}`; + + const options = []; + options.push(`store: options.store`); + + if (this.children.length) { + const appendTarget: AppendTarget = { + slots: { default: '' }, + slotStack: ['default'] + }; + + this.compiler.target.appendTargets.push(appendTarget); + + this.children.forEach((child: Node) => { + child.ssr(); + }); + + const slotted = Object.keys(appendTarget.slots) + .map(name => `${name}: () => \`${appendTarget.slots[name]}\``) + .join(', '); + + options.push(`slotted: { ${slotted} }`); + + this.compiler.target.appendTargets.pop(); + } + + if (options.length) { + open += `, { ${options.join(', ')} }`; + } + + this.compiler.target.append(open); + this.compiler.target.append(')}'); + } +} + +function isComputed(node: Node) { + while (node.type === 'MemberExpression') { + if (node.computed) return true; + node = node.object; + } + + return false; +} diff --git a/src/compile/nodes/EachBlock.ts b/src/compile/nodes/EachBlock.ts new file mode 100644 index 0000000000..e0bfa8fd34 --- /dev/null +++ b/src/compile/nodes/EachBlock.ts @@ -0,0 +1,483 @@ +import deindent from '../../utils/deindent'; +import Node from './shared/Node'; +import ElseBlock from './ElseBlock'; +import Block from '../dom/Block'; +import createDebuggingComment from '../../utils/createDebuggingComment'; +import Expression from './shared/Expression'; +import mapChildren from './shared/mapChildren'; +import TemplateScope from './shared/TemplateScope'; +import unpackDestructuring from '../../utils/unpackDestructuring'; + +export default class EachBlock extends Node { + type: 'EachBlock'; + + block: Block; + expression: Expression; + + iterations: string; + index: string; + context: string; + key: Expression; + scope: TemplateScope; + contexts: Array<{ name: string, tail: string }>; + + children: Node[]; + else?: ElseBlock; + + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + + this.expression = new Expression(compiler, this, scope, info.expression); + this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring + this.index = info.index; + + this.scope = scope.child(); + + this.contexts = []; + unpackDestructuring(this.contexts, info.context, ''); + + this.contexts.forEach(context => { + this.scope.add(context.key.name, this.expression.dependencies); + }); + + this.key = info.key + ? new Expression(compiler, this, this.scope, info.key) + : null; + + if (this.index) { + // index can only change if this is a keyed each block + const dependencies = this.key ? this.expression.dependencies : []; + this.scope.add(this.index, dependencies); + } + + this.children = mapChildren(compiler, this, this.scope, info.children); + + this.else = info.else + ? new ElseBlock(compiler, this, this.scope, info.else) + : null; + } + + init( + block: Block, + stripWhitespace: boolean, + nextSibling: Node + ) { + this.cannotUseInnerHTML(); + + this.var = block.getUniqueName(`each`); + this.iterations = block.getUniqueName(`${this.var}_blocks`); + this.get_each_context = this.compiler.getUniqueName(`get_${this.var}_context`); + + const { dependencies } = this.expression; + block.addDependencies(dependencies); + + this.block = block.child({ + comment: createDebuggingComment(this, this.compiler), + name: this.compiler.getUniqueName('create_each_block'), + key: this.key, + + bindings: new Map(block.bindings) + }); + + this.each_block_value = this.compiler.getUniqueName('each_value'); + + const indexName = this.index || this.compiler.getUniqueName(`${this.context}_index`); + + this.contexts.forEach(prop => { + this.block.bindings.set(prop.key.name, `ctx.${this.each_block_value}[ctx.${indexName}]${prop.tail}`); + }); + + if (this.index) { + this.block.getUniqueName(this.index); // this prevents name collisions (#1254) + } + + this.contextProps = this.contexts.map(prop => `child_ctx.${prop.key.name} = list[i]${prop.tail};`); + + // TODO only add these if necessary + this.contextProps.push( + `child_ctx.${this.each_block_value} = list;`, + `child_ctx.${indexName} = i;` + ); + + this.compiler.target.blocks.push(this.block); + this.initChildren(this.block, stripWhitespace, nextSibling); + block.addDependencies(this.block.dependencies); + this.block.hasUpdateMethod = this.block.dependencies.size > 0; + + if (this.else) { + this.else.block = block.child({ + comment: createDebuggingComment(this.else, this.compiler), + name: this.compiler.getUniqueName(`${this.block.name}_else`), + }); + + this.compiler.target.blocks.push(this.else.block); + this.else.initChildren( + this.else.block, + stripWhitespace, + nextSibling + ); + this.else.block.hasUpdateMethod = this.else.block.dependencies.size > 0; + } + } + + build( + block: Block, + parentNode: string, + parentNodes: string + ) { + if (this.children.length === 0) return; + + const { compiler } = this; + + const each = this.var; + + const create_each_block = this.block.name; + const iterations = this.iterations; + + const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode(); + const anchor = needsAnchor + ? block.getUniqueName(`${each}_anchor`) + : (this.next && this.next.var) || 'null'; + + // hack the sourcemap, so that if data is missing the bug + // is easy to find + let c = this.start + 2; + while (compiler.source[c] !== 'e') c += 1; + compiler.code.overwrite(c, c + 4, 'length'); + const length = `[✂${c}-${c+4}✂]`; + + const mountOrIntro = this.block.hasIntroMethod ? 'i' : 'm'; + const vars = { + each, + create_each_block, + length, + iterations, + anchor, + mountOrIntro, + }; + + const { snippet } = this.expression; + + block.builders.init.addLine(`var ${this.each_block_value} = ${snippet};`); + + this.compiler.target.blocks.push(deindent` + function ${this.get_each_context}(ctx, list, i) { + const child_ctx = Object.create(ctx); + ${this.contextProps} + return child_ctx; + } + `); + + if (this.key) { + this.buildKeyed(block, parentNode, parentNodes, snippet, vars); + } else { + this.buildUnkeyed(block, parentNode, parentNodes, snippet, vars); + } + + if (needsAnchor) { + block.addElement( + anchor, + `@createComment()`, + parentNodes && `@createComment()`, + parentNode + ); + } + + if (this.else) { + const each_block_else = compiler.getUniqueName(`${each}_else`); + + block.builders.init.addLine(`var ${each_block_else} = null;`); + + // TODO neaten this up... will end up with an empty line in the block + block.builders.init.addBlock(deindent` + if (!${this.each_block_value}.${length}) { + ${each_block_else} = ${this.else.block.name}(#component, ctx); + ${each_block_else}.c(); + } + `); + + block.builders.mount.addBlock(deindent` + if (${each_block_else}) { + ${each_block_else}.${mountOrIntro}(${parentNode || '#target'}, null); + } + `); + + const initialMountNode = parentNode || `${anchor}.parentNode`; + + if (this.else.block.hasUpdateMethod) { + block.builders.update.addBlock(deindent` + if (!${this.each_block_value}.${length} && ${each_block_else}) { + ${each_block_else}.p(changed, ctx); + } else if (!${this.each_block_value}.${length}) { + ${each_block_else} = ${this.else.block.name}(#component, ctx); + ${each_block_else}.c(); + ${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor}); + } else if (${each_block_else}) { + ${each_block_else}.d(1); + ${each_block_else} = null; + } + `); + } else { + block.builders.update.addBlock(deindent` + if (${this.each_block_value}.${length}) { + if (${each_block_else}) { + ${each_block_else}.d(1); + ${each_block_else} = null; + } + } else if (!${each_block_else}) { + ${each_block_else} = ${this.else.block.name}(#component, ctx); + ${each_block_else}.c(); + ${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor}); + } + `); + } + + block.builders.destroy.addBlock(deindent` + if (${each_block_else}) ${each_block_else}.d(${parentNode ? '' : 'detach'}); + `); + } + + this.children.forEach((child: Node) => { + child.build(this.block, null, 'nodes'); + }); + + if (this.else) { + this.else.children.forEach((child: Node) => { + child.build(this.else.block, null, 'nodes'); + }); + } + } + + buildKeyed( + block: Block, + parentNode: string, + parentNodes: string, + snippet: string, + { + each, + create_each_block, + length, + anchor, + mountOrIntro, + } + ) { + const get_key = block.getUniqueName('get_key'); + const blocks = block.getUniqueName(`${each}_blocks`); + const lookup = block.getUniqueName(`${each}_lookup`); + + block.addVariable(blocks, '[]'); + block.addVariable(lookup, `@blankObject()`); + + if (this.children[0].isDomNode()) { + this.block.first = this.children[0].var; + } else { + this.block.first = this.block.getUniqueName('first'); + this.block.addElement( + this.block.first, + `@createComment()`, + parentNodes && `@createComment()`, + null + ); + } + + block.builders.init.addBlock(deindent` + const ${get_key} = ctx => ${this.key.snippet}; + + for (var #i = 0; #i < ${this.each_block_value}.${length}; #i += 1) { + let child_ctx = ${this.get_each_context}(ctx, ${this.each_block_value}, #i); + let key = ${get_key}(child_ctx); + ${blocks}[#i] = ${lookup}[key] = ${create_each_block}(#component, key, child_ctx); + } + `); + + const initialMountNode = parentNode || '#target'; + const updateMountNode = this.getUpdateMountNode(anchor); + const anchorNode = parentNode ? 'null' : 'anchor'; + + block.builders.create.addBlock(deindent` + for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].c(); + `); + + if (parentNodes) { + block.builders.claim.addBlock(deindent` + for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].l(${parentNodes}); + `); + } + + block.builders.mount.addBlock(deindent` + for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].${mountOrIntro}(${initialMountNode}, ${anchorNode}); + `); + + const dynamic = this.block.hasUpdateMethod; + + block.builders.update.addBlock(deindent` + var ${this.each_block_value} = ${snippet}; + + ${blocks} = @updateKeyedEach(${blocks}, #component, changed, ${get_key}, ${dynamic ? '1' : '0'}, ctx, ${this.each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, ${this.get_each_context}); + `); + + block.builders.destroy.addBlock(deindent` + for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].d(${parentNode ? '' : 'detach'}); + `); + } + + buildUnkeyed( + block: Block, + parentNode: string, + parentNodes: string, + snippet: string, + { + create_each_block, + length, + iterations, + anchor, + mountOrIntro, + } + ) { + block.builders.init.addBlock(deindent` + var ${iterations} = []; + + for (var #i = 0; #i < ${this.each_block_value}.${length}; #i += 1) { + ${iterations}[#i] = ${create_each_block}(#component, ${this.get_each_context}(ctx, ${this.each_block_value}, #i)); + } + `); + + const initialMountNode = parentNode || '#target'; + const updateMountNode = this.getUpdateMountNode(anchor); + const anchorNode = parentNode ? 'null' : 'anchor'; + + block.builders.create.addBlock(deindent` + for (var #i = 0; #i < ${iterations}.length; #i += 1) { + ${iterations}[#i].c(); + } + `); + + if (parentNodes) { + block.builders.claim.addBlock(deindent` + for (var #i = 0; #i < ${iterations}.length; #i += 1) { + ${iterations}[#i].l(${parentNodes}); + } + `); + } + + block.builders.mount.addBlock(deindent` + for (var #i = 0; #i < ${iterations}.length; #i += 1) { + ${iterations}[#i].${mountOrIntro}(${initialMountNode}, ${anchorNode}); + } + `); + + const allDependencies = new Set(this.block.dependencies); + const { dependencies } = this.expression; + dependencies.forEach((dependency: string) => { + allDependencies.add(dependency); + }); + + // TODO do this for keyed blocks as well + const condition = Array.from(allDependencies) + .map(dependency => `changed.${dependency}`) + .join(' || '); + + if (condition !== '') { + const forLoopBody = this.block.hasUpdateMethod + ? this.block.hasIntroMethod + ? deindent` + if (${iterations}[#i]) { + ${iterations}[#i].p(changed, child_ctx); + } else { + ${iterations}[#i] = ${create_each_block}(#component, child_ctx); + ${iterations}[#i].c(); + } + ${iterations}[#i].i(${updateMountNode}, ${anchor}); + ` + : deindent` + if (${iterations}[#i]) { + ${iterations}[#i].p(changed, child_ctx); + } else { + ${iterations}[#i] = ${create_each_block}(#component, child_ctx); + ${iterations}[#i].c(); + ${iterations}[#i].m(${updateMountNode}, ${anchor}); + } + ` + : deindent` + ${iterations}[#i] = ${create_each_block}(#component, child_ctx); + ${iterations}[#i].c(); + ${iterations}[#i].${mountOrIntro}(${updateMountNode}, ${anchor}); + `; + + const start = this.block.hasUpdateMethod ? '0' : `${iterations}.length`; + + const outro = block.getUniqueName('outro'); + const destroy = this.block.hasOutroMethod + ? deindent` + function ${outro}(i) { + if (${iterations}[i]) { + ${iterations}[i].o(function() { + ${iterations}[i].d(1); + ${iterations}[i] = null; + }); + } + } + + for (; #i < ${iterations}.length; #i += 1) ${outro}(#i); + ` + : deindent` + for (; #i < ${iterations}.length; #i += 1) { + ${iterations}[#i].d(1); + } + ${iterations}.length = ${this.each_block_value}.${length}; + `; + + block.builders.update.addBlock(deindent` + if (${condition}) { + ${this.each_block_value} = ${snippet}; + + for (var #i = ${start}; #i < ${this.each_block_value}.${length}; #i += 1) { + const child_ctx = ${this.get_each_context}(ctx, ${this.each_block_value}, #i); + + ${forLoopBody} + } + + ${destroy} + } + `); + } + + block.builders.destroy.addBlock(`@destroyEach(${iterations}, detach);`); + } + + remount(name: string) { + // TODO consider keyed blocks + return `for (var #i = 0; #i < ${this.iterations}.length; #i += 1) ${this.iterations}[#i].m(${name}._slotted.default, null);`; + } + + ssr() { + const { compiler } = this; + const { snippet } = this.expression; + + const props = this.contexts.map(prop => `${prop.key.name}: item${prop.tail}`); + + const getContext = this.index + ? `(item, i) => Object.assign({}, ctx, { ${props.join(', ')}, ${this.index}: i })` + : `item => Object.assign({}, ctx, { ${props.join(', ')} })`; + + const open = `\${ ${this.else ? `${snippet}.length ? ` : ''}@each(${snippet}, ${getContext}, ctx => \``; + compiler.target.append(open); + + this.children.forEach((child: Node) => { + child.ssr(); + }); + + const close = `\`)`; + compiler.target.append(close); + + if (this.else) { + compiler.target.append(` : \``); + this.else.children.forEach((child: Node) => { + child.ssr(); + }); + compiler.target.append(`\``); + } + + compiler.target.append('}'); + } +} diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts new file mode 100644 index 0000000000..f0c352f02e --- /dev/null +++ b/src/compile/nodes/Element.ts @@ -0,0 +1,1048 @@ +import deindent from '../../utils/deindent'; +import { stringify, escapeHTML } from '../../utils/stringify'; +import flattenReference from '../../utils/flattenReference'; +import isVoidElementName from '../../utils/isVoidElementName'; +import validCalleeObjects from '../../utils/validCalleeObjects'; +import reservedNames from '../../utils/reservedNames'; +import fixAttributeCasing from '../../utils/fixAttributeCasing'; +import quoteIfNecessary from '../../utils/quoteIfNecessary'; +import Compiler from '../Compiler'; +import Node from './shared/Node'; +import Block from '../dom/Block'; +import Attribute from './Attribute'; +import Binding from './Binding'; +import EventHandler from './EventHandler'; +import Transition from './Transition'; +import Action from './Action'; +import Text from './Text'; +import * as namespaces from '../../utils/namespaces'; +import mapChildren from './shared/mapChildren'; +import { dimensions } from '../../utils/patterns'; + +// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7 +const booleanAttributes = new Set('async autocomplete autofocus autoplay border challenge checked compact contenteditable controls default defer disabled formnovalidate frameborder hidden indeterminate ismap loop multiple muted nohref noresize noshade novalidate nowrap open readonly required reversed scoped scrolling seamless selected sortable spellcheck translate'.split(' ')); + +export default class Element extends Node { + type: 'Element'; + name: string; + scope: any; // TODO + attributes: Attribute[]; + actions: Action[]; + bindings: Binding[]; + handlers: EventHandler[]; + intro: Transition; + outro: Transition; + children: Node[]; + + ref: string; + namespace: string; + + constructor(compiler, parent, scope, info: any) { + super(compiler, parent, scope, info); + this.name = info.name; + this.scope = scope; + + const parentElement = parent.findNearest(/^Element/); + this.namespace = this.name === 'svg' ? + namespaces.svg : + parentElement ? parentElement.namespace : this.compiler.namespace; + + this.attributes = []; + this.actions = []; + this.bindings = []; + this.handlers = []; + + this.intro = null; + this.outro = null; + + if (this.name === 'textarea') { + // this is an egregious hack, but it's the easiest way to get + diff --git a/test/parser/samples/attribute-dynamic-boolean/output.json b/test/parser/samples/attribute-dynamic-boolean/output.json index af1635a55d..4d08df34a0 100644 --- a/test/parser/samples/attribute-dynamic-boolean/output.json +++ b/test/parser/samples/attribute-dynamic-boolean/output.json @@ -1,30 +1,30 @@ { - "hash": 3179574701, + "hash": "7xolfv", "html": { "start": 0, - "end": 45, + "end": 41, "type": "Fragment", "children": [ { "start": 0, - "end": 45, + "end": 41, "type": "Element", "name": "textarea", "attributes": [ { "start": 10, - "end": 33, + "end": 29, "type": "Attribute", "name": "readonly", "value": [ { - "start": 20, - "end": 32, + "start": 19, + "end": 29, "type": "MustacheTag", "expression": { "type": "Identifier", - "start": 22, - "end": 30, + "start": 20, + "end": 28, "name": "readonly" } } diff --git a/test/parser/samples/attribute-dynamic-reserved/input.html b/test/parser/samples/attribute-dynamic-reserved/input.html index 6b149d165f..d973a9dea0 100644 --- a/test/parser/samples/attribute-dynamic-reserved/input.html +++ b/test/parser/samples/attribute-dynamic-reserved/input.html @@ -1 +1 @@ -
\ No newline at end of file +
\ No newline at end of file diff --git a/test/parser/samples/attribute-dynamic-reserved/output.json b/test/parser/samples/attribute-dynamic-reserved/output.json index aef0e5cb63..4b6506b0cd 100644 --- a/test/parser/samples/attribute-dynamic-reserved/output.json +++ b/test/parser/samples/attribute-dynamic-reserved/output.json @@ -1,30 +1,30 @@ { - "hash": 2788845841, + "hash": "l0cddf", "html": { "start": 0, - "end": 29, + "end": 25, "type": "Fragment", "children": [ { "start": 0, - "end": 29, + "end": 25, "type": "Element", "name": "div", "attributes": [ { "start": 5, - "end": 22, + "end": 18, "type": "Attribute", "name": "class", "value": [ { - "start": 12, - "end": 21, + "start": 11, + "end": 18, "type": "MustacheTag", "expression": { "type": "Identifier", - "start": 14, - "end": 19, + "start": 12, + "end": 17, "name": "class" } } diff --git a/test/parser/samples/attribute-dynamic/input.html b/test/parser/samples/attribute-dynamic/input.html index 84a34b91ff..9171ae22a7 100644 --- a/test/parser/samples/attribute-dynamic/input.html +++ b/test/parser/samples/attribute-dynamic/input.html @@ -1 +1 @@ -
{{color}}
+
{color}
diff --git a/test/parser/samples/attribute-dynamic/output.json b/test/parser/samples/attribute-dynamic/output.json index 79ef81065f..7ccb40d313 100644 --- a/test/parser/samples/attribute-dynamic/output.json +++ b/test/parser/samples/attribute-dynamic/output.json @@ -1,19 +1,19 @@ { - "hash": 804348386, + "hash": "ehtsx6", "html": { "start": 0, - "end": 46, + "end": 42, "type": "Fragment", "children": [ { "start": 0, - "end": 46, + "end": 42, "type": "Element", "name": "div", "attributes": [ { "start": 5, - "end": 30, + "end": 28, "type": "Attribute", "name": "style", "value": [ @@ -25,18 +25,18 @@ }, { "start": 19, - "end": 28, + "end": 26, "type": "MustacheTag", "expression": { "type": "Identifier", - "start": 21, - "end": 26, + "start": 20, + "end": 25, "name": "color" } }, { - "start": 28, - "end": 29, + "start": 26, + "end": 27, "type": "Text", "data": ";" } @@ -45,13 +45,13 @@ ], "children": [ { - "start": 31, - "end": 40, + "start": 29, + "end": 36, "type": "MustacheTag", "expression": { "type": "Identifier", - "start": 33, - "end": 38, + "start": 30, + "end": 35, "name": "color" } } diff --git a/test/parser/samples/attribute-multiple/input.html b/test/parser/samples/attribute-multiple/input.html index a5adf38f2c..6f61bd6289 100644 --- a/test/parser/samples/attribute-multiple/input.html +++ b/test/parser/samples/attribute-multiple/input.html @@ -1 +1 @@ -
+
\ No newline at end of file diff --git a/test/parser/samples/attribute-shorthand/input.html b/test/parser/samples/attribute-shorthand/input.html index e26deafb3a..35468de006 100644 --- a/test/parser/samples/attribute-shorthand/input.html +++ b/test/parser/samples/attribute-shorthand/input.html @@ -1 +1 @@ -
\ No newline at end of file +
\ No newline at end of file diff --git a/test/parser/samples/attribute-shorthand/output.json b/test/parser/samples/attribute-shorthand/output.json index b6578c13eb..598356350b 100644 --- a/test/parser/samples/attribute-shorthand/output.json +++ b/test/parser/samples/attribute-shorthand/output.json @@ -2,18 +2,18 @@ "hash": 1705925892, "html": { "start": 0, - "end": 10, + "end": 11, "type": "Fragment", "children": [ { "start": 0, - "end": 10, + "end": 11, "type": "Element", "name": "div", "attributes": [ { "start": 5, - "end": 8, + "end": 9, "type": "Attribute", "name": "id", "value": [ diff --git a/test/parser/samples/attribute-static-boolean/input.html b/test/parser/samples/attribute-static-boolean/input.html index 3ca3bfd9a8..1536f3e1e8 100644 --- a/test/parser/samples/attribute-static-boolean/input.html +++ b/test/parser/samples/attribute-static-boolean/input.html @@ -1 +1 @@ - + \ No newline at end of file diff --git a/test/parser/samples/attribute-static/input.html b/test/parser/samples/attribute-static/input.html index 3cb2e4b233..c6a8a8c95d 100644 --- a/test/parser/samples/attribute-static/input.html +++ b/test/parser/samples/attribute-static/input.html @@ -1 +1 @@ -
+
\ No newline at end of file diff --git a/test/parser/samples/attribute-unique-error/error.json b/test/parser/samples/attribute-unique-error/error.json index b4ab7a57b5..dd14572149 100644 --- a/test/parser/samples/attribute-unique-error/error.json +++ b/test/parser/samples/attribute-unique-error/error.json @@ -1,8 +1,10 @@ { + "code": "duplicate-attribute", "message": "Attributes need to be unique", - "loc": { + "start": { "line": 1, - "column": 17 + "column": 17, + "character": 17 }, "pos": 17 } diff --git a/test/parser/samples/attribute-unique-error/input.html b/test/parser/samples/attribute-unique-error/input.html index 4088350ce0..37fec733b3 100644 --- a/test/parser/samples/attribute-unique-error/input.html +++ b/test/parser/samples/attribute-unique-error/input.html @@ -1 +1 @@ -
+
\ No newline at end of file diff --git a/test/parser/samples/attribute-unquoted/input.html b/test/parser/samples/attribute-unquoted/input.html index 2d388456d0..4bab0df72f 100644 --- a/test/parser/samples/attribute-unquoted/input.html +++ b/test/parser/samples/attribute-unquoted/input.html @@ -1 +1 @@ -
+
\ No newline at end of file diff --git a/test/parser/samples/await-then-catch/input.html b/test/parser/samples/await-then-catch/input.html index 36489b9043..79f5162ace 100644 --- a/test/parser/samples/await-then-catch/input.html +++ b/test/parser/samples/await-then-catch/input.html @@ -1,7 +1,7 @@ -{{#await thePromise}} +{#await thePromise}

loading...

-{{then theValue}} -

the value is {{theValue}}

-{{catch theError}} -

oh no! {{theError.message}}

-{{/await}} \ No newline at end of file +{:then theValue} +

the value is {theValue}

+{:catch theError} +

oh no! {theError.message}

+{/await} \ No newline at end of file diff --git a/test/parser/samples/await-then-catch/output.json b/test/parser/samples/await-then-catch/output.json index 4f149e6a0c..22ad4242e3 100644 --- a/test/parser/samples/await-then-catch/output.json +++ b/test/parser/samples/await-then-catch/output.json @@ -1,143 +1,143 @@ { - "hash": 1040536517, + "hash": "1b28gs9", "html": { "start": 0, - "end": 158, + "end": 148, "type": "Fragment", "children": [ { "start": 0, - "end": 158, + "end": 148, "type": "AwaitBlock", "expression": { "type": "Identifier", - "start": 9, - "end": 19, + "start": 8, + "end": 18, "name": "thePromise" }, "value": "theValue", "error": "theError", "pending": { - "start": 21, - "end": 41, + "start": 19, + "end": 39, "type": "PendingBlock", "children": [ { - "start": 21, - "end": 23, + "start": 19, + "end": 21, "type": "Text", "data": "\n\t" }, { - "start": 23, - "end": 40, + "start": 21, + "end": 38, "type": "Element", "name": "p", "attributes": [], "children": [ { - "start": 26, - "end": 36, + "start": 24, + "end": 34, "type": "Text", "data": "loading..." } ] }, { - "start": 40, - "end": 41, + "start": 38, + "end": 39, "type": "Text", "data": "\n" } ] }, "then": { - "start": 41, - "end": 93, + "start": 39, + "end": 88, "type": "ThenBlock", "children": [ { - "start": 58, - "end": 60, + "start": 55, + "end": 57, "type": "Text", "data": "\n\t" }, { - "start": 60, - "end": 92, + "start": 57, + "end": 87, "type": "Element", "name": "p", "attributes": [], "children": [ { - "start": 63, - "end": 76, + "start": 60, + "end": 73, "type": "Text", "data": "the value is " }, { - "start": 76, - "end": 88, + "start": 73, + "end": 83, "type": "MustacheTag", "expression": { "type": "Identifier", - "start": 78, - "end": 86, + "start": 74, + "end": 82, "name": "theValue" } } ] }, { - "start": 92, - "end": 93, + "start": 87, + "end": 88, "type": "Text", "data": "\n" } ] }, "catch": { - "start": 93, - "end": 148, + "start": 88, + "end": 140, "type": "CatchBlock", "children": [ { - "start": 111, - "end": 113, + "start": 105, + "end": 107, "type": "Text", "data": "\n\t" }, { - "start": 113, - "end": 147, + "start": 107, + "end": 139, "type": "Element", "name": "p", "attributes": [], "children": [ { - "start": 116, - "end": 123, + "start": 110, + "end": 117, "type": "Text", "data": "oh no! " }, { - "start": 123, - "end": 143, + "start": 117, + "end": 135, "type": "MustacheTag", "expression": { "type": "MemberExpression", - "start": 125, - "end": 141, + "start": 118, + "end": 134, "object": { "type": "Identifier", - "start": 125, - "end": 133, + "start": 118, + "end": 126, "name": "theError" }, "property": { "type": "Identifier", - "start": 134, - "end": 141, + "start": 127, + "end": 134, "name": "message" }, "computed": false @@ -146,8 +146,8 @@ ] }, { - "start": 147, - "end": 148, + "start": 139, + "end": 140, "type": "Text", "data": "\n" } diff --git a/test/parser/samples/binding-shorthand/input.html b/test/parser/samples/binding-shorthand/input.html index 31f9c872b0..7f8116bdde 100644 --- a/test/parser/samples/binding-shorthand/input.html +++ b/test/parser/samples/binding-shorthand/input.html @@ -1 +1 @@ - + \ No newline at end of file diff --git a/test/parser/samples/binding-shorthand/output.json b/test/parser/samples/binding-shorthand/output.json index 8a7b615cf4..4c92fb9e20 100644 --- a/test/parser/samples/binding-shorthand/output.json +++ b/test/parser/samples/binding-shorthand/output.json @@ -8,7 +8,7 @@ { "start": 0, "end": 18, - "type": "Element", + "type": "Component", "name": "Widget", "attributes": [ { diff --git a/test/parser/samples/binding/input.html b/test/parser/samples/binding/input.html index 6a7bf8566c..3af20a9ced 100644 --- a/test/parser/samples/binding/input.html +++ b/test/parser/samples/binding/input.html @@ -1 +1 @@ - + \ No newline at end of file diff --git a/test/parser/samples/comment/input.html b/test/parser/samples/comment/input.html index b35c49dac8..3b3ffe3222 100644 --- a/test/parser/samples/comment/input.html +++ b/test/parser/samples/comment/input.html @@ -1 +1 @@ - + \ No newline at end of file diff --git a/test/parser/samples/component-dynamic/input.html b/test/parser/samples/component-dynamic/input.html index 0d5be48552..1dad5eb269 100644 --- a/test/parser/samples/component-dynamic/input.html +++ b/test/parser/samples/component-dynamic/input.html @@ -1 +1 @@ -<:Component {foo ? Foo : Bar}> \ No newline at end of file + \ No newline at end of file diff --git a/test/parser/samples/component-dynamic/output.json b/test/parser/samples/component-dynamic/output.json index 614c28c0a9..67e8f1e4ca 100644 --- a/test/parser/samples/component-dynamic/output.json +++ b/test/parser/samples/component-dynamic/output.json @@ -1,37 +1,37 @@ { - "hash": 410218696, + "hash": "2u5ec3", "html": { "start": 0, - "end": 43, + "end": 62, "type": "Fragment", "children": [ { "start": 0, - "end": 43, - "type": "Element", - "name": ":Component", + "end": 62, + "type": "Component", + "name": "svelte:component", "attributes": [], "children": [], "expression": { "type": "ConditionalExpression", - "start": 13, - "end": 28, + "start": 25, + "end": 40, "test": { "type": "Identifier", - "start": 13, - "end": 16, + "start": 25, + "end": 28, "name": "foo" }, "consequent": { "type": "Identifier", - "start": 19, - "end": 22, + "start": 31, + "end": 34, "name": "Foo" }, "alternate": { "type": "Identifier", - "start": 25, - "end": 28, + "start": 37, + "end": 40, "name": "Bar" } } diff --git a/test/parser/samples/convert-entities-in-element/input.html b/test/parser/samples/convert-entities-in-element/input.html index bc5c086536..f05dc391ad 100644 --- a/test/parser/samples/convert-entities-in-element/input.html +++ b/test/parser/samples/convert-entities-in-element/input.html @@ -1 +1 @@ -

Hello & World

+

Hello & World

\ No newline at end of file diff --git a/test/parser/samples/convert-entities/input.html b/test/parser/samples/convert-entities/input.html index e25e3214f2..161463ec58 100644 --- a/test/parser/samples/convert-entities/input.html +++ b/test/parser/samples/convert-entities/input.html @@ -1 +1 @@ -Hello & World +Hello & World \ No newline at end of file diff --git a/test/parser/samples/css-ref-selector/input.html b/test/parser/samples/css-ref-selector/input.html index 07ab3790f7..696075f93f 100644 --- a/test/parser/samples/css-ref-selector/input.html +++ b/test/parser/samples/css-ref-selector/input.html @@ -4,4 +4,4 @@ ref:foo { color: red; } - + \ No newline at end of file diff --git a/test/parser/samples/css/input.html b/test/parser/samples/css/input.html index 659c4343f6..a573bcf778 100644 --- a/test/parser/samples/css/input.html +++ b/test/parser/samples/css/input.html @@ -4,4 +4,4 @@ div { color: red; } - + \ No newline at end of file diff --git a/test/parser/samples/each-block-destructured/input.html b/test/parser/samples/each-block-destructured/input.html index 7209f5503d..4ddf33ef85 100644 --- a/test/parser/samples/each-block-destructured/input.html +++ b/test/parser/samples/each-block-destructured/input.html @@ -1,3 +1,3 @@ -{{#each animals as [key, value]}} -

{{key}}: {{value}}

-{{/each}} +{#each animals as [key, value]} +

{key}: {value}

+{/each} diff --git a/test/parser/samples/each-block-destructured/output.json b/test/parser/samples/each-block-destructured/output.json index 897fec88b8..554b0cdcb0 100644 --- a/test/parser/samples/each-block-destructured/output.json +++ b/test/parser/samples/each-block-destructured/output.json @@ -1,64 +1,77 @@ { - "hash": 2621498076, "html": { "start": 0, - "end": 70, + "end": 62, "type": "Fragment", "children": [ { "start": 0, - "end": 70, + "end": 62, "type": "EachBlock", "expression": { "type": "Identifier", - "start": 8, - "end": 15, + "start": 7, + "end": 14, "name": "animals" }, "children": [ { - "start": 35, - "end": 60, + "start": 33, + "end": 54, "type": "Element", "name": "p", "attributes": [], "children": [ { - "start": 38, - "end": 45, + "start": 36, + "end": 41, "type": "MustacheTag", "expression": { "type": "Identifier", - "start": 40, - "end": 43, + "start": 37, + "end": 40, "name": "key" } }, { - "start": 45, - "end": 47, + "start": 41, + "end": 43, "type": "Text", "data": ": " }, { - "start": 47, - "end": 56, + "start": 43, + "end": 50, "type": "MustacheTag", "expression": { "type": "Identifier", - "start": 49, - "end": 54, + "start": 44, + "end": 49, "name": "value" } } ] } ], - "destructuredContexts": [ - "key", - "value" - ], - "context": "key_value" + "context": { + "start": 18, + "end": null, + "type": "ArrayPattern", + "elements": [ + { + "start": 19, + "end": 22, + "type": "Identifier", + "name": "key" + }, + { + "start": 24, + "end": 29, + "type": "Identifier", + "name": "value" + } + ] + } } ] }, diff --git a/test/parser/samples/each-block-else/input.html b/test/parser/samples/each-block-else/input.html index 59925e8db7..dc96d8b946 100644 --- a/test/parser/samples/each-block-else/input.html +++ b/test/parser/samples/each-block-else/input.html @@ -1,5 +1,5 @@ -{{#each animals as animal}} -

{{animal}}

-{{else}} +{#each animals as animal} +

{animal}

+{:else}

no animals

-{{/each}} +{/each} diff --git a/test/parser/samples/each-block-else/output.json b/test/parser/samples/each-block-else/output.json index 0dadad5f65..283b0f0418 100644 --- a/test/parser/samples/each-block-else/output.json +++ b/test/parser/samples/each-block-else/output.json @@ -1,58 +1,62 @@ { - "hash": 3238289871, "html": { "start": 0, - "end": 84, + "end": 77, "type": "Fragment", "children": [ { "start": 0, - "end": 84, + "end": 77, "type": "EachBlock", "expression": { "type": "Identifier", - "start": 8, - "end": 15, + "start": 7, + "end": 14, "name": "animals" }, "children": [ { - "start": 29, - "end": 46, + "start": 27, + "end": 42, "type": "Element", "name": "p", "attributes": [], "children": [ { - "start": 32, - "end": 42, + "start": 30, + "end": 38, "type": "MustacheTag", "expression": { "type": "Identifier", - "start": 34, - "end": 40, + "start": 31, + "end": 37, "name": "animal" } } ] } ], - "context": "animal", + "context": { + "start": 18, + "end": 24, + "type": "Identifier", + "name": "animal" + }, "else": { - "start": 55, - "end": 75, + "start": 50, + "end": 70, "type": "ElseBlock", "children": [ { - "start": 57, - "end": 74, + "start": 52, + "end": 69, "type": "Element", "name": "p", "attributes": [], "children": [ { - "start": 60, - "end": 70, + "start": 55, + "end": 65, "type": "Text", "data": "no animals" } diff --git a/test/parser/samples/each-block-indexed/input.html b/test/parser/samples/each-block-indexed/input.html index 5c1c74da66..d5602ec82c 100644 --- a/test/parser/samples/each-block-indexed/input.html +++ b/test/parser/samples/each-block-indexed/input.html @@ -1,3 +1,3 @@ -{{#each animals as animal, i}} -

{{i}}: {{animal}}

-{{/each}} +{#each animals as animal, i} +

{i}: {animal}

+{/each} diff --git a/test/parser/samples/each-block-indexed/output.json b/test/parser/samples/each-block-indexed/output.json index fc8954d6ad..1039e67b7c 100644 --- a/test/parser/samples/each-block-indexed/output.json +++ b/test/parser/samples/each-block-indexed/output.json @@ -1,60 +1,64 @@ { - "hash": 2841674990, "html": { "start": 0, - "end": 66, + "end": 58, "type": "Fragment", "children": [ { "start": 0, - "end": 66, + "end": 58, "type": "EachBlock", "expression": { "type": "Identifier", - "start": 8, - "end": 15, + "start": 7, + "end": 14, "name": "animals" }, "children": [ { - "start": 32, - "end": 56, + "start": 30, + "end": 50, "type": "Element", "name": "p", "attributes": [], "children": [ { - "start": 35, - "end": 40, + "start": 33, + "end": 36, "type": "MustacheTag", "expression": { "type": "Identifier", - "start": 37, - "end": 38, + "start": 34, + "end": 35, "name": "i" } }, { - "start": 40, - "end": 42, + "start": 36, + "end": 38, "type": "Text", "data": ": " }, { - "start": 42, - "end": 52, + "start": 38, + "end": 46, "type": "MustacheTag", "expression": { "type": "Identifier", - "start": 44, - "end": 50, + "start": 39, + "end": 45, "name": "animal" } } ] } ], - "context": "animal", + "context": { + "start": 18, + "end": 24, + "type": "Identifier", + "name": "animal" + }, "index": "i" } ] diff --git a/test/parser/samples/each-block-keyed/input.html b/test/parser/samples/each-block-keyed/input.html index 2bc79c9c8b..46348678d4 100644 --- a/test/parser/samples/each-block-keyed/input.html +++ b/test/parser/samples/each-block-keyed/input.html @@ -1,3 +1,3 @@ -{{#each todos as todo @id}} -

{{todo}}

-{{/each}} +{#each todos as todo (todo.id)} +

{todo}

+{/each} diff --git a/test/parser/samples/each-block-keyed/output.json b/test/parser/samples/each-block-keyed/output.json index 1a16afb82b..e627c5c8c9 100644 --- a/test/parser/samples/each-block-keyed/output.json +++ b/test/parser/samples/each-block-keyed/output.json @@ -1,5 +1,4 @@ { - "hash": 2025411181, "html": { "start": 0, "end": 54, @@ -11,34 +10,56 @@ "type": "EachBlock", "expression": { "type": "Identifier", - "start": 8, - "end": 13, + "start": 7, + "end": 12, "name": "todos" }, "children": [ { - "start": 29, - "end": 44, + "start": 33, + "end": 46, "type": "Element", "name": "p", "attributes": [], "children": [ { - "start": 32, - "end": 40, + "start": 36, + "end": 42, "type": "MustacheTag", "expression": { "type": "Identifier", - "start": 34, - "end": 38, + "start": 37, + "end": 41, "name": "todo" } } ] } ], - "context": "todo", - "key": "id" + "context": { + "start": 16, + "end": 20, + "type": "Identifier", + "name": "todo" + }, + "key": { + "type": "MemberExpression", + "start": 22, + "end": 29, + "object": { + "type": "Identifier", + "start": 22, + "end": 26, + "name": "todo" + }, + "property": { + "type": "Identifier", + "start": 27, + "end": 29, + "name": "id" + }, + "computed": false + } } ] }, diff --git a/test/parser/samples/each-block/input.html b/test/parser/samples/each-block/input.html index 23bfc3c465..83a5e88ddd 100644 --- a/test/parser/samples/each-block/input.html +++ b/test/parser/samples/each-block/input.html @@ -1,3 +1,3 @@ -{{#each animals as animal}} -

{{animal}}

-{{/each}} +{#each animals as animal} +

{animal}

+{/each} diff --git a/test/parser/samples/each-block/output.json b/test/parser/samples/each-block/output.json index e549faca39..a92f3410d1 100644 --- a/test/parser/samples/each-block/output.json +++ b/test/parser/samples/each-block/output.json @@ -1,43 +1,47 @@ { - "hash": 220340986, "html": { "start": 0, - "end": 56, + "end": 50, "type": "Fragment", "children": [ { "start": 0, - "end": 56, + "end": 50, "type": "EachBlock", "expression": { "type": "Identifier", - "start": 8, - "end": 15, + "start": 7, + "end": 14, "name": "animals" }, "children": [ { - "start": 29, - "end": 46, + "start": 27, + "end": 42, "type": "Element", "name": "p", "attributes": [], "children": [ { - "start": 32, - "end": 42, + "start": 30, + "end": 38, "type": "MustacheTag", "expression": { "type": "Identifier", - "start": 34, - "end": 40, + "start": 31, + "end": 37, "name": "animal" } } ] } ], - "context": "animal" + "context": { + "start": 18, + "end": 24, + "type": "Identifier", + "name": "animal" + } } ] }, diff --git a/test/parser/samples/element-with-mustache/input.html b/test/parser/samples/element-with-mustache/input.html index 6a2a43bf79..1e9232da02 100644 --- a/test/parser/samples/element-with-mustache/input.html +++ b/test/parser/samples/element-with-mustache/input.html @@ -1 +1 @@ -

hello {{name}}!

+

hello {name}!

diff --git a/test/parser/samples/element-with-mustache/output.json b/test/parser/samples/element-with-mustache/output.json index fd6799e192..76e71dc327 100644 --- a/test/parser/samples/element-with-mustache/output.json +++ b/test/parser/samples/element-with-mustache/output.json @@ -2,12 +2,12 @@ "hash": 1265376132, "html": { "start": 0, - "end": 24, + "end": 22, "type": "Fragment", "children": [ { "start": 0, - "end": 24, + "end": 22, "type": "Element", "name": "h1", "attributes": [], @@ -20,18 +20,18 @@ }, { "start": 10, - "end": 18, + "end": 16, "type": "MustacheTag", "expression": { "type": "Identifier", - "start": 12, - "end": 16, + "start": 11, + "end": 15, "name": "name" } }, { - "start": 18, - "end": 19, + "start": 16, + "end": 17, "type": "Text", "data": "!" } diff --git a/test/parser/samples/element-with-text/input.html b/test/parser/samples/element-with-text/input.html index 61dba8bc46..fcf3199655 100644 --- a/test/parser/samples/element-with-text/input.html +++ b/test/parser/samples/element-with-text/input.html @@ -1 +1 @@ -test +test \ No newline at end of file diff --git a/test/parser/samples/elements/input.html b/test/parser/samples/elements/input.html index c50eddd41f..937d25b42e 100644 --- a/test/parser/samples/elements/input.html +++ b/test/parser/samples/elements/input.html @@ -1 +1 @@ - + \ No newline at end of file diff --git a/test/parser/samples/error-binding-disabled/error.json b/test/parser/samples/error-binding-disabled/error.json index 36ad59d26f..63f01e7056 100644 --- a/test/parser/samples/error-binding-disabled/error.json +++ b/test/parser/samples/error-binding-disabled/error.json @@ -1,8 +1,10 @@ { + "code": "binding-disabled", "message": "Two-way binding is disabled", - "loc": { + "start": { "line": 1, - "column": 7 + "column": 7, + "character": 7 }, "pos": 7 } \ No newline at end of file diff --git a/test/parser/samples/error-binding-disabled/input.html b/test/parser/samples/error-binding-disabled/input.html index 6a7bf8566c..3af20a9ced 100644 --- a/test/parser/samples/error-binding-disabled/input.html +++ b/test/parser/samples/error-binding-disabled/input.html @@ -1 +1 @@ - + \ No newline at end of file diff --git a/test/parser/samples/error-binding-mustaches/error.json b/test/parser/samples/error-binding-mustaches/error.json index 9d67bfba1a..b2a5c1c564 100644 --- a/test/parser/samples/error-binding-mustaches/error.json +++ b/test/parser/samples/error-binding-mustaches/error.json @@ -1,8 +1,10 @@ { - "message": "bound values should not be wrapped — use 'foo', not '{{foo}}'", - "loc": { + "code": "invalid-directive-value", + "message": "directive values should not be wrapped — use 'foo', not '{foo}'", + "start": { "line": 1, - "column": 19 + "column": 19, + "character": 19 }, "pos": 19 } \ No newline at end of file diff --git a/test/parser/samples/error-binding-mustaches/input.html b/test/parser/samples/error-binding-mustaches/input.html index a334ea7055..2629696c9e 100644 --- a/test/parser/samples/error-binding-mustaches/input.html +++ b/test/parser/samples/error-binding-mustaches/input.html @@ -1 +1 @@ - + diff --git a/test/parser/samples/error-binding-rvalue/error.json b/test/parser/samples/error-binding-rvalue/error.json index 85b4a6f9ac..2d081afac5 100644 --- a/test/parser/samples/error-binding-rvalue/error.json +++ b/test/parser/samples/error-binding-rvalue/error.json @@ -1,8 +1,10 @@ { - "message": "Cannot bind to rvalue", + "code": "invalid-directive-value", + "message": "Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)", "pos": 19, - "loc": { + "start": { "line": 1, - "column": 19 + "column": 19, + "character": 19 } } \ No newline at end of file diff --git a/test/parser/samples/error-comment-unclosed/error.json b/test/parser/samples/error-comment-unclosed/error.json new file mode 100644 index 0000000000..8e355fb821 --- /dev/null +++ b/test/parser/samples/error-comment-unclosed/error.json @@ -0,0 +1,10 @@ +{ + "code": "unexpected-eof", + "message": "comment was left open, expected -->", + "start": { + "line": 1, + "column": 24, + "character": 24 + }, + "pos": 24 +} diff --git a/test/parser/samples/error-comment-unclosed/input.html b/test/parser/samples/error-comment-unclosed/input.html new file mode 100644 index 0000000000..fe6d748e1b --- /dev/null +++ b/test/parser/samples/error-comment-unclosed/input.html @@ -0,0 +1 @@ +`, test ( assert, component, target, window ) { const inputs = [ ...target.querySelectorAll( 'input' ) ]; - const items = component.get( 'items' ); + const items = component.get().items; const event = new window.Event( 'input' ); assert.equal( inputs[0].value, 'one' ); diff --git a/test/runtime/samples/binding-input-text-contextual/main.html b/test/runtime/samples/binding-input-text-contextual/main.html index 5f07fadc26..7b3f35244f 100644 --- a/test/runtime/samples/binding-input-text-contextual/main.html +++ b/test/runtime/samples/binding-input-text-contextual/main.html @@ -1,3 +1,3 @@ -{{#each items as item}} -

{{item}}

-{{/each}} +{#each items as item} +

{item}

+{/each} diff --git a/test/runtime/samples/binding-input-text-deconflicted/main.html b/test/runtime/samples/binding-input-text-deconflicted/main.html index 391a47c3f1..9d631bf040 100644 --- a/test/runtime/samples/binding-input-text-deconflicted/main.html +++ b/test/runtime/samples/binding-input-text-deconflicted/main.html @@ -1,2 +1,2 @@ -

Hello {{component.name}}!

+

Hello {component.name}!

\ No newline at end of file diff --git a/test/runtime/samples/binding-input-text-deep-computed-dynamic/main.html b/test/runtime/samples/binding-input-text-deep-computed-dynamic/main.html index 3a95b383e4..d3afde0a7a 100644 --- a/test/runtime/samples/binding-input-text-deep-computed-dynamic/main.html +++ b/test/runtime/samples/binding-input-text-deep-computed-dynamic/main.html @@ -1,2 +1,2 @@ -
{{JSON.stringify(obj)}}
\ No newline at end of file +
{JSON.stringify(obj)}
\ No newline at end of file diff --git a/test/runtime/samples/binding-input-text-deep-computed/_config.js b/test/runtime/samples/binding-input-text-deep-computed/_config.js index 597395ed52..33cc605a61 100644 --- a/test/runtime/samples/binding-input-text-deep-computed/_config.js +++ b/test/runtime/samples/binding-input-text-deep-computed/_config.js @@ -20,7 +20,7 @@ export default { assert.equal( target.innerHTML, `\n

hello bob

` ); - const user = component.get( 'user' ); + const user = component.get().user; user.name = 'carol'; component.set({ user }); diff --git a/test/runtime/samples/binding-input-text-deep-computed/main.html b/test/runtime/samples/binding-input-text-deep-computed/main.html index 281849a4d0..55ad4b53fa 100644 --- a/test/runtime/samples/binding-input-text-deep-computed/main.html +++ b/test/runtime/samples/binding-input-text-deep-computed/main.html @@ -1,2 +1,2 @@ -

hello {{user.name}}

+

hello {user.name}

diff --git a/test/runtime/samples/binding-input-text-deep-contextual-computed-dynamic/main.html b/test/runtime/samples/binding-input-text-deep-contextual-computed-dynamic/main.html index b2f36850f4..1af3bee5b6 100644 --- a/test/runtime/samples/binding-input-text-deep-contextual-computed-dynamic/main.html +++ b/test/runtime/samples/binding-input-text-deep-contextual-computed-dynamic/main.html @@ -1,4 +1,4 @@ -{{#each objects as obj}} +{#each objects as obj} -
{{JSON.stringify(obj)}}
-{{/each}} \ No newline at end of file +
{JSON.stringify(obj)}
+{/each} \ No newline at end of file diff --git a/test/runtime/samples/binding-input-text-deep-contextual/_config.js b/test/runtime/samples/binding-input-text-deep-contextual/_config.js index 587110786c..8cf72df956 100644 --- a/test/runtime/samples/binding-input-text-deep-contextual/_config.js +++ b/test/runtime/samples/binding-input-text-deep-contextual/_config.js @@ -19,7 +19,7 @@ export default { assert.equal( target.innerHTML, `

one

four

three

` ); - const items = component.get( 'items' ); + const items = component.get().items; items[2].description = 'five'; component.set({ items }); diff --git a/test/runtime/samples/binding-input-text-deep-contextual/main.html b/test/runtime/samples/binding-input-text-deep-contextual/main.html index 210e4e4fea..7d21a2ddb2 100644 --- a/test/runtime/samples/binding-input-text-deep-contextual/main.html +++ b/test/runtime/samples/binding-input-text-deep-contextual/main.html @@ -1,3 +1,3 @@ -{{#each items as item}} -

{{item.description}}

-{{/each}} +{#each items as item} +

{item.description}

+{/each} diff --git a/test/runtime/samples/binding-input-text-deep/_config.js b/test/runtime/samples/binding-input-text-deep/_config.js index 9ef090f864..f1abe383a6 100644 --- a/test/runtime/samples/binding-input-text-deep/_config.js +++ b/test/runtime/samples/binding-input-text-deep/_config.js @@ -19,7 +19,7 @@ export default { assert.equal( target.innerHTML, `\n

hello bob

` ); - const user = component.get( 'user' ); + const user = component.get().user; user.name = 'carol'; component.set({ user }); diff --git a/test/runtime/samples/binding-input-text-deep/main.html b/test/runtime/samples/binding-input-text-deep/main.html index 8ed32a7e90..f7f7d4dbc0 100644 --- a/test/runtime/samples/binding-input-text-deep/main.html +++ b/test/runtime/samples/binding-input-text-deep/main.html @@ -1,2 +1,2 @@ -

hello {{user.name}}

+

hello {user.name}

diff --git a/test/runtime/samples/binding-input-text/_config.js b/test/runtime/samples/binding-input-text/_config.js index ccbe234d12..2c54fc8c32 100644 --- a/test/runtime/samples/binding-input-text/_config.js +++ b/test/runtime/samples/binding-input-text/_config.js @@ -1,21 +1,32 @@ export default { data: { - name: 'world' + name: 'world', }, - html: `\n

hello world

`, - test ( assert, component, target, window ) { - const input = target.querySelector( 'input' ); - assert.equal( input.value, 'world' ); - const event = new window.Event( 'input' ); + html: ` + +

hello world

+ `, + + test(assert, component, target, window) { + const input = target.querySelector('input'); + assert.equal(input.value, 'world'); + + const event = new window.Event('input'); input.value = 'everybody'; - input.dispatchEvent( event ); + input.dispatchEvent(event); - assert.equal( target.innerHTML, `\n

hello everybody

` ); + assert.htmlEqual(target.innerHTML, ` + +

hello everybody

+ `); component.set({ name: 'goodbye' }); - assert.equal( input.value, 'goodbye' ); - assert.equal( target.innerHTML, `\n

hello goodbye

` ); - } + assert.equal(input.value, 'goodbye'); + assert.htmlEqual(target.innerHTML, ` + +

hello goodbye

+ `); + }, }; diff --git a/test/runtime/samples/binding-input-text/main.html b/test/runtime/samples/binding-input-text/main.html index 806f31fbdd..143c9054b1 100644 --- a/test/runtime/samples/binding-input-text/main.html +++ b/test/runtime/samples/binding-input-text/main.html @@ -1,2 +1,2 @@ -

hello {{name}}

+

hello {name}

diff --git a/test/runtime/samples/binding-input-with-event/_config.js b/test/runtime/samples/binding-input-with-event/_config.js index c09f09439b..1b4736e0aa 100644 --- a/test/runtime/samples/binding-input-with-event/_config.js +++ b/test/runtime/samples/binding-input-with-event/_config.js @@ -13,7 +13,7 @@ export default { input.dispatchEvent( event ); assert.equal( input.value, '43' ); - assert.equal( component.get( 'a' ), 43 ); + assert.equal( component.get().a, 43 ); component.destroy(); } diff --git a/test/runtime/samples/binding-select-implicit-option-value/_config.js b/test/runtime/samples/binding-select-implicit-option-value/_config.js index 40a7364797..1ab8de20aa 100644 --- a/test/runtime/samples/binding-select-implicit-option-value/_config.js +++ b/test/runtime/samples/binding-select-implicit-option-value/_config.js @@ -19,14 +19,14 @@ export default { const options = [...target.querySelectorAll('option')]; assert.ok(options[1].selected); - assert.equal(component.get('foo'), 2); + assert.equal(component.get().foo, 2); const change = new window.Event('change'); options[2].selected = true; select.dispatchEvent(change); - assert.equal(component.get('foo'), 3); + assert.equal(component.get().foo, 3); assert.htmlEqual( target.innerHTML, ` - {{#each values as v}} - - {{/each}} + {#each values as v} + + {/each} -

foo: {{foo}}

\ No newline at end of file +

foo: {foo}

\ No newline at end of file diff --git a/test/runtime/samples/binding-select-in-each-block/_config.js b/test/runtime/samples/binding-select-in-each-block/_config.js index 5a333501bb..e95662c740 100644 --- a/test/runtime/samples/binding-select-in-each-block/_config.js +++ b/test/runtime/samples/binding-select-in-each-block/_config.js @@ -25,7 +25,7 @@ export default { selects[1].options[0].selected = true; selects[1].dispatchEvent(change); - assert.deepEqual(component.get('items'), [ + assert.deepEqual(component.get().items, [ { value: 'hullo' }, { value: 'hullo' } ]); } diff --git a/test/runtime/samples/binding-select-in-each-block/main.html b/test/runtime/samples/binding-select-in-each-block/main.html index f61ee5b2ca..68281ebe7a 100644 --- a/test/runtime/samples/binding-select-in-each-block/main.html +++ b/test/runtime/samples/binding-select-in-each-block/main.html @@ -1,6 +1,6 @@ -{{#each items as item}} +{#each items as item} -{{/each}} \ No newline at end of file +{/each} \ No newline at end of file diff --git a/test/runtime/samples/binding-select-in-yield/Modal.html b/test/runtime/samples/binding-select-in-yield/Modal.html index 9e580bb57c..ceeece2ddd 100644 --- a/test/runtime/samples/binding-select-in-yield/Modal.html +++ b/test/runtime/samples/binding-select-in-yield/Modal.html @@ -1,6 +1,6 @@ -{{#if !hidden}} +{#if !hidden} -{{/if}} +{/if} \ No newline at end of file diff --git a/test/runtime/samples/bindings-before-oncreate/Two.html b/test/runtime/samples/bindings-before-oncreate/Two.html index f6fc00fcaf..a1143a68f1 100644 --- a/test/runtime/samples/bindings-before-oncreate/Two.html +++ b/test/runtime/samples/bindings-before-oncreate/Two.html @@ -7,7 +7,7 @@ }, computed: { - foo(bar) { + foo({ bar }) { return bar * 2; } } diff --git a/test/runtime/samples/bindings-coalesced/Foo.html b/test/runtime/samples/bindings-coalesced/Foo.html index 5d453fbb73..3b6b796ccb 100644 --- a/test/runtime/samples/bindings-coalesced/Foo.html +++ b/test/runtime/samples/bindings-coalesced/Foo.html @@ -1,5 +1,5 @@ -

bar in Foo: {{bar}}

-

baz in Foo: {{baz}}

+

bar in Foo: {bar}

+

baz in Foo: {baz}

diff --git a/test/runtime/samples/component-binding-blowback-b/main.html b/test/runtime/samples/component-binding-blowback-b/main.html index 4e3ca43813..301f5b7da8 100644 --- a/test/runtime/samples/component-binding-blowback-b/main.html +++ b/test/runtime/samples/component-binding-blowback-b/main.html @@ -1,11 +1,11 @@
    - {{#each ids as id}} - - {{id}}: value is {{idToValue[id]}} + {#each ids as id} + + {id}: value is {idToValue[id]} - {{/each}} + {/each}
diff --git a/test/runtime/samples/component-binding-blowback-c/main.html b/test/runtime/samples/component-binding-blowback-c/main.html index 3cf809552b..21d69d3c40 100644 --- a/test/runtime/samples/component-binding-blowback-c/main.html +++ b/test/runtime/samples/component-binding-blowback-c/main.html @@ -1,11 +1,11 @@
    - {{#each ids as object @id}} - - {{object.id}}: value is {{idToValue[object.id]}} + {#each ids as object (object.id)} + + {object.id}: value is {idToValue[object.id]} - {{/each}} + {/each}
\ No newline at end of file diff --git a/test/runtime/samples/component-events-console/Widget.html b/test/runtime/samples/component-events-console/Widget.html new file mode 100644 index 0000000000..f54897f249 --- /dev/null +++ b/test/runtime/samples/component-events-console/Widget.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/runtime/samples/component-events-console/_config.js b/test/runtime/samples/component-events-console/_config.js new file mode 100644 index 0000000000..407c3e7edf --- /dev/null +++ b/test/runtime/samples/component-events-console/_config.js @@ -0,0 +1,25 @@ +export default { + html: '', + + test(assert, component, target) { + const button = target.querySelector('button'); + const messages = []; + + const log = console.log; + console.log = msg => { + messages.push(msg); + }; + + try { + button.dispatchEvent(new window.MouseEvent('click')); + assert.deepEqual(messages, [ + 'clicked' + ]); + } catch (err) { + console.log = log; + throw err; + } + + console.log = log; + }, +}; diff --git a/test/runtime/samples/component-events-console/main.html b/test/runtime/samples/component-events-console/main.html new file mode 100644 index 0000000000..13238f6c53 --- /dev/null +++ b/test/runtime/samples/component-events-console/main.html @@ -0,0 +1,9 @@ + + + diff --git a/test/runtime/samples/component-events-each/main.html b/test/runtime/samples/component-events-each/main.html index e429f3f303..3d655c5df6 100644 --- a/test/runtime/samples/component-events-each/main.html +++ b/test/runtime/samples/component-events-each/main.html @@ -1,7 +1,7 @@
- {{#each items as item}} + {#each items as item} - {{/each}} + {/each}
\ No newline at end of file diff --git a/test/runtime/samples/component-nested-deep/Level2.html b/test/runtime/samples/component-nested-deep/Level2.html index 3ab519e45c..5b2ece36c5 100644 --- a/test/runtime/samples/component-nested-deep/Level2.html +++ b/test/runtime/samples/component-nested-deep/Level2.html @@ -1,5 +1,5 @@ level 2 -{{yield}} + diff --git a/test/runtime/samples/component-slot-dynamic/Nested.html b/test/runtime/samples/component-slot-dynamic/Nested.html new file mode 100644 index 0000000000..ff66691089 --- /dev/null +++ b/test/runtime/samples/component-slot-dynamic/Nested.html @@ -0,0 +1,13 @@ + + {foo} + + + diff --git a/test/runtime/samples/component-slot-dynamic/_config.js b/test/runtime/samples/component-slot-dynamic/_config.js new file mode 100644 index 0000000000..17f22b2830 --- /dev/null +++ b/test/runtime/samples/component-slot-dynamic/_config.js @@ -0,0 +1,9 @@ +export default { + html: ` +

override default slot

+ `, + + test(assert, component) { + component.refs.nested.set({ foo: 'b' }); + } +}; diff --git a/test/runtime/samples/component-slot-dynamic/main.html b/test/runtime/samples/component-slot-dynamic/main.html new file mode 100644 index 0000000000..348754d88c --- /dev/null +++ b/test/runtime/samples/component-slot-dynamic/main.html @@ -0,0 +1,13 @@ + +

override default slot

+
+ + diff --git a/test/runtime/samples/component-slot-each-block/main.html b/test/runtime/samples/component-slot-each-block/main.html index b503c5af53..05dd7421c0 100644 --- a/test/runtime/samples/component-slot-each-block/main.html +++ b/test/runtime/samples/component-slot-each-block/main.html @@ -1,7 +1,7 @@ - {{#each things as thing}} - {{thing}} - {{/each}} + {#each things as thing} + {thing} + {/each} diff --git a/test/runtime/samples/computed-values-deconflicted/main.html b/test/runtime/samples/computed-values-deconflicted/main.html index d32fde6167..e5a016084c 100644 --- a/test/runtime/samples/computed-values-deconflicted/main.html +++ b/test/runtime/samples/computed-values-deconflicted/main.html @@ -1,4 +1,4 @@ -{{state}} +{state} \ No newline at end of file diff --git a/test/runtime/samples/computed-values-default/main.html b/test/runtime/samples/computed-values-default/main.html index 74292a3a2e..b26762f3d8 100644 --- a/test/runtime/samples/computed-values-default/main.html +++ b/test/runtime/samples/computed-values-default/main.html @@ -1,9 +1,9 @@ -

{{foo}}

+

{foo}

diff --git a/test/runtime/samples/computed-values-function-dependency/_config.js b/test/runtime/samples/computed-values-function-dependency/_config.js index 38b716b4d7..9320a0aad8 100644 --- a/test/runtime/samples/computed-values-function-dependency/_config.js +++ b/test/runtime/samples/computed-values-function-dependency/_config.js @@ -3,7 +3,7 @@ export default { test ( assert, component, target ) { component.set({ y: 2 }); - assert.equal( component.get( 'x' ), 4 ); + assert.equal( component.get().x, 4 ); assert.equal( target.innerHTML, '

4

' ); component.destroy(); } diff --git a/test/runtime/samples/computed-values-function-dependency/main.html b/test/runtime/samples/computed-values-function-dependency/main.html index d385bf6da8..adf66137fa 100644 --- a/test/runtime/samples/computed-values-function-dependency/main.html +++ b/test/runtime/samples/computed-values-function-dependency/main.html @@ -1,4 +1,4 @@ -

{{x}}

+

{x}

diff --git a/test/runtime/samples/custom-method/_config.js b/test/runtime/samples/custom-method/_config.js index 1586df8a79..fb1aa7f46a 100644 --- a/test/runtime/samples/custom-method/_config.js +++ b/test/runtime/samples/custom-method/_config.js @@ -5,11 +5,11 @@ export default { const event = new window.MouseEvent( 'click' ); button.dispatchEvent( event ); - assert.equal( component.get( 'counter' ), 1 ); + assert.equal( component.get().counter, 1 ); assert.equal( target.innerHTML, '\n\n

1

' ); button.dispatchEvent( event ); - assert.equal( component.get( 'counter' ), 2 ); + assert.equal( component.get().counter, 2 ); assert.equal( target.innerHTML, '\n\n

2

' ); assert.equal( component.foo(), 42 ); diff --git a/test/runtime/samples/custom-method/main.html b/test/runtime/samples/custom-method/main.html index 06ad01ca79..0f1ad3387c 100644 --- a/test/runtime/samples/custom-method/main.html +++ b/test/runtime/samples/custom-method/main.html @@ -1,6 +1,6 @@ -

{{counter}}

+

{counter}

\ No newline at end of file diff --git a/test/runtime/samples/deconflict-non-helpers/_config.js b/test/runtime/samples/deconflict-non-helpers/_config.js index 978e4c27e3..51670f56fe 100644 --- a/test/runtime/samples/deconflict-non-helpers/_config.js +++ b/test/runtime/samples/deconflict-non-helpers/_config.js @@ -2,7 +2,7 @@ export default { html: `ABCD`, test ( assert, component ) { - assert.equal( component.get( 'compute' ), 'ABCD' ); + assert.equal( component.get().compute, 'ABCD' ); component.destroy(); } }; diff --git a/test/runtime/samples/deconflict-non-helpers/main.html b/test/runtime/samples/deconflict-non-helpers/main.html index 8541b090f9..d6ee1eaeae 100644 --- a/test/runtime/samples/deconflict-non-helpers/main.html +++ b/test/runtime/samples/deconflict-non-helpers/main.html @@ -1,4 +1,4 @@ -{{compute}} +{compute} diff --git a/test/runtime/samples/deconflict-template-1/main.html b/test/runtime/samples/deconflict-template-1/main.html index f4fe8de423..395d7b22b3 100644 --- a/test/runtime/samples/deconflict-template-1/main.html +++ b/test/runtime/samples/deconflict-template-1/main.html @@ -1,4 +1,4 @@ -{{value}} +{value} \ No newline at end of file diff --git a/test/runtime/samples/dev-warning-destroy-not-teardown/_config.js b/test/runtime/samples/dev-warning-destroy-not-teardown/_config.js deleted file mode 100644 index 7e9b391f4b..0000000000 --- a/test/runtime/samples/dev-warning-destroy-not-teardown/_config.js +++ /dev/null @@ -1,7 +0,0 @@ -export default { - dev: true, - - warnings: [ - `Use component.on('destroy', ...) instead of component.on('teardown', ...) which has been deprecated and will be unsupported in Svelte 2` - ] -}; \ No newline at end of file diff --git a/test/runtime/samples/dev-warning-destroy-not-teardown/main.html b/test/runtime/samples/dev-warning-destroy-not-teardown/main.html deleted file mode 100644 index 22c2e9c287..0000000000 --- a/test/runtime/samples/dev-warning-destroy-not-teardown/main.html +++ /dev/null @@ -1,9 +0,0 @@ - \ No newline at end of file diff --git a/test/runtime/samples/dev-warning-dynamic-components-misplaced/main.html b/test/runtime/samples/dev-warning-dynamic-components-misplaced/main.html index 37392f27e6..eb6e7d8e8e 100644 --- a/test/runtime/samples/dev-warning-dynamic-components-misplaced/main.html +++ b/test/runtime/samples/dev-warning-dynamic-components-misplaced/main.html @@ -1,4 +1,4 @@ -<:Component {x ? Foo : Bar}/> + \ No newline at end of file diff --git a/test/runtime/samples/dev-warning-readonly-window-binding/main.html b/test/runtime/samples/dev-warning-readonly-window-binding/main.html index 635ae75e9c..87b0403ee3 100644 --- a/test/runtime/samples/dev-warning-readonly-window-binding/main.html +++ b/test/runtime/samples/dev-warning-readonly-window-binding/main.html @@ -1 +1 @@ -<:Window bind:innerWidth='width'/> \ No newline at end of file + \ No newline at end of file diff --git a/test/runtime/samples/dynamic-component-bindings-recreated/Green.html b/test/runtime/samples/dynamic-component-bindings-recreated/Green.html index 692847c1c2..b4faa1b662 100644 --- a/test/runtime/samples/dynamic-component-bindings-recreated/Green.html +++ b/test/runtime/samples/dynamic-component-bindings-recreated/Green.html @@ -1 +1 @@ -

green {{foo}}

\ No newline at end of file +

green {foo}

\ No newline at end of file diff --git a/test/runtime/samples/dynamic-component-bindings-recreated/Red.html b/test/runtime/samples/dynamic-component-bindings-recreated/Red.html index 8f10e3294d..a3e3c792a0 100644 --- a/test/runtime/samples/dynamic-component-bindings-recreated/Red.html +++ b/test/runtime/samples/dynamic-component-bindings-recreated/Red.html @@ -1 +1 @@ -

red {{foo}}

\ No newline at end of file +

red {foo}

\ No newline at end of file diff --git a/test/runtime/samples/dynamic-component-bindings-recreated/main.html b/test/runtime/samples/dynamic-component-bindings-recreated/main.html index b9cdf92660..a26c1cd080 100644 --- a/test/runtime/samples/dynamic-component-bindings-recreated/main.html +++ b/test/runtime/samples/dynamic-component-bindings-recreated/main.html @@ -1,4 +1,4 @@ -<:Component {x ? Green : Red} bind:foo /> + diff --git a/test/runtime/samples/dynamic-component-slot/main.html b/test/runtime/samples/dynamic-component-slot/main.html index fd4fad1369..e8b5c487ad 100644 --- a/test/runtime/samples/dynamic-component-slot/main.html +++ b/test/runtime/samples/dynamic-component-slot/main.html @@ -1,26 +1,26 @@ -<:Component { x ? Foo : Bar } x='{{x}}'> +

element

- {{tag}} + {tag} - {{#if foo}} + {#if foo}

foo

- {{elseif bar}} + {:elseif bar}

bar

- {{else}} + {:else}

neither foo nor bar

- {{/if}} + {/if} text - {{#each things as thing}} - {{thing}} - {{/each}} + {#each things as thing} + {thing} + {/each}
what goes up must come down
- +
diff --git a/test/runtime/samples/each-block-containing-component-in-if/Nested.html b/test/runtime/samples/each-block-containing-component-in-if/Nested.html index b9d76a696d..0ceac58d9c 100644 --- a/test/runtime/samples/each-block-containing-component-in-if/Nested.html +++ b/test/runtime/samples/each-block-containing-component-in-if/Nested.html @@ -1,5 +1,5 @@ -{{#if show}} - {{#each fields as field}} - {{field}} - {{/each}} -{{/if}} \ No newline at end of file +{#if show} + {#each fields as field} + {field} + {/each} +{/if} \ No newline at end of file diff --git a/test/runtime/samples/each-block-containing-component-in-if/main.html b/test/runtime/samples/each-block-containing-component-in-if/main.html index 9a6bbfd10f..cf81af277d 100644 --- a/test/runtime/samples/each-block-containing-component-in-if/main.html +++ b/test/runtime/samples/each-block-containing-component-in-if/main.html @@ -1,5 +1,5 @@
- +
diff --git a/test/runtime/samples/event-handler-custom-context/main.html b/test/runtime/samples/event-handler-custom-context/main.html index 806f330d6c..dd16947f29 100644 --- a/test/runtime/samples/event-handler-custom-context/main.html +++ b/test/runtime/samples/event-handler-custom-context/main.html @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/test/runtime/samples/event-handler-custom/main.html b/test/runtime/samples/event-handler-custom/main.html index b56b4f403b..53232e9b74 100644 --- a/test/runtime/samples/event-handler-custom/main.html +++ b/test/runtime/samples/event-handler-custom/main.html @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/test/runtime/samples/function-in-expression/_config.js b/test/runtime/samples/function-in-expression/_config.js index 14cfb8f89f..9b79770eb9 100644 --- a/test/runtime/samples/function-in-expression/_config.js +++ b/test/runtime/samples/function-in-expression/_config.js @@ -1,6 +1,4 @@ export default { - allowES2015: true, - data: { numbers: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] }, diff --git a/test/runtime/samples/function-in-expression/main.html b/test/runtime/samples/function-in-expression/main.html index 2c98a5124d..1f038d771b 100644 --- a/test/runtime/samples/function-in-expression/main.html +++ b/test/runtime/samples/function-in-expression/main.html @@ -1 +1 @@ -{{ numbers.filter( x => x % 2 ).join( ', ' ) }} \ No newline at end of file +{ numbers.filter( x => x % 2 ).join( ', ' ) } \ No newline at end of file diff --git a/test/runtime/samples/get-after-destroy/_config.js b/test/runtime/samples/get-after-destroy/_config.js new file mode 100644 index 0000000000..c3627bc604 --- /dev/null +++ b/test/runtime/samples/get-after-destroy/_config.js @@ -0,0 +1,13 @@ +export default { + data: { + foo: 1 + }, + + html: `
1
`, + + test(assert, component) { + component.destroy(); + const { foo } = component.get(); + assert.equal(foo, undefined); + } +} \ No newline at end of file diff --git a/test/runtime/samples/get-after-destroy/main.html b/test/runtime/samples/get-after-destroy/main.html new file mode 100644 index 0000000000..ea892526af --- /dev/null +++ b/test/runtime/samples/get-after-destroy/main.html @@ -0,0 +1 @@ +
{foo}
\ No newline at end of file diff --git a/test/runtime/samples/get-state/_config.js b/test/runtime/samples/get-state/_config.js index dd0b72fb16..3c2dae5a81 100644 --- a/test/runtime/samples/get-state/_config.js +++ b/test/runtime/samples/get-state/_config.js @@ -1,7 +1,7 @@ export default { test ( assert, component ) { - assert.equal( component.get('a'), 1 ); - assert.equal( component.get('c'), 3 ); + assert.equal( component.get().a, 1 ); + assert.equal( component.get().c, 3 ); assert.deepEqual( component.get(), { a: 1, b: 2, c: 3 }); } }; diff --git a/test/runtime/samples/get-state/main.html b/test/runtime/samples/get-state/main.html index c8492f4db1..e67adaf72e 100644 --- a/test/runtime/samples/get-state/main.html +++ b/test/runtime/samples/get-state/main.html @@ -6,7 +6,7 @@ }), computed: { - c: ( a, b ) => a + b, + c: ({ a, b }) => a + b, } }; diff --git a/test/runtime/samples/globals-accessible-directly/main.html b/test/runtime/samples/globals-accessible-directly/main.html index 349b4b1d32..77225fa117 100644 --- a/test/runtime/samples/globals-accessible-directly/main.html +++ b/test/runtime/samples/globals-accessible-directly/main.html @@ -1 +1 @@ -{{NaN}} +{NaN} diff --git a/test/runtime/samples/globals-not-dereferenced/main.html b/test/runtime/samples/globals-not-dereferenced/main.html index 771042ed07..ec39251992 100644 --- a/test/runtime/samples/globals-not-dereferenced/main.html +++ b/test/runtime/samples/globals-not-dereferenced/main.html @@ -1 +1 @@ -{{Math.min(x, 5)}} +{Math.min(x, 5)} diff --git a/test/runtime/samples/globals-not-overwritten-by-bindings/_config.js b/test/runtime/samples/globals-not-overwritten-by-bindings/_config.js index a10750e1a0..cf15f47d9d 100644 --- a/test/runtime/samples/globals-not-overwritten-by-bindings/_config.js +++ b/test/runtime/samples/globals-not-overwritten-by-bindings/_config.js @@ -40,7 +40,7 @@ export default { input.checked = true; input.dispatchEvent(change); - assert.ok(component.get('todos').third.done); + assert.ok(component.get().todos.third.done); assert.htmlEqual(target.innerHTML, `
diff --git a/test/runtime/samples/globals-not-overwritten-by-bindings/main.html b/test/runtime/samples/globals-not-overwritten-by-bindings/main.html index ca6c6ccb35..a8b873211c 100644 --- a/test/runtime/samples/globals-not-overwritten-by-bindings/main.html +++ b/test/runtime/samples/globals-not-overwritten-by-bindings/main.html @@ -1,6 +1,6 @@ -{{#each Object.keys(todos) as key}} -
+{#each Object.keys(todos) as key} +
-{{/each}} \ No newline at end of file +{/each} \ No newline at end of file diff --git a/test/runtime/samples/globals-shadowed-by-data/main.html b/test/runtime/samples/globals-shadowed-by-data/main.html index 73fd9437af..7e3b408331 100644 --- a/test/runtime/samples/globals-shadowed-by-data/main.html +++ b/test/runtime/samples/globals-shadowed-by-data/main.html @@ -1,4 +1,4 @@ -{{Math.min(x, 5)}} +{Math.min(x, 5)} \ No newline at end of file diff --git a/test/runtime/samples/immutable-mutable/_config.js b/test/runtime/samples/immutable-mutable/_config.js new file mode 100644 index 0000000000..d7381a38a2 --- /dev/null +++ b/test/runtime/samples/immutable-mutable/_config.js @@ -0,0 +1,18 @@ +export default { + immutable: true, + html: `

Called 0 times.

`, + + test(assert, component, target, window) { + var nested = component.refs.nested; + nested.on('state', ({ changed }) => { + if (changed.foo) { + nested.set({ count: nested.get().count + 1 }); + } + }); + + assert.htmlEqual(target.innerHTML, `

Called 0 times.

`); + + nested.set({ foo: nested.get().foo }); + assert.htmlEqual(target.innerHTML, `

Called 1 times.

`); + } +}; diff --git a/test/runtime/samples/immutable-mutable/main.html b/test/runtime/samples/immutable-mutable/main.html new file mode 100644 index 0000000000..4e36143361 --- /dev/null +++ b/test/runtime/samples/immutable-mutable/main.html @@ -0,0 +1,13 @@ +
+ +
+ + diff --git a/test/runtime/samples/immutable-nested/Nested.html b/test/runtime/samples/immutable-nested/Nested.html new file mode 100644 index 0000000000..1f3d808d63 --- /dev/null +++ b/test/runtime/samples/immutable-nested/Nested.html @@ -0,0 +1,12 @@ +

Called {count} times.

+ + \ No newline at end of file diff --git a/test/runtime/samples/immutable-nested/_config.js b/test/runtime/samples/immutable-nested/_config.js new file mode 100644 index 0000000000..1268a4a988 --- /dev/null +++ b/test/runtime/samples/immutable-nested/_config.js @@ -0,0 +1,18 @@ +export default { + immutable: true, + html: `

Called 0 times.

`, + + test(assert, component, target, window) { + var nested = component.refs.nested; + nested.on('state', ({ changed }) => { + if (changed.foo) { + nested.set({ count: nested.get().count + 1 }); + } + }); + + assert.htmlEqual(target.innerHTML, `

Called 0 times.

`); + + nested.set({ foo: nested.get().foo }); + assert.htmlEqual(target.innerHTML, `

Called 0 times.

`); + } +}; diff --git a/test/runtime/samples/immutable-nested/main.html b/test/runtime/samples/immutable-nested/main.html new file mode 100644 index 0000000000..4e36143361 --- /dev/null +++ b/test/runtime/samples/immutable-nested/main.html @@ -0,0 +1,13 @@ +
+ +
+ + diff --git a/test/runtime/samples/immutable-root/_config.js b/test/runtime/samples/immutable-root/_config.js new file mode 100644 index 0000000000..ab0b4287d8 --- /dev/null +++ b/test/runtime/samples/immutable-root/_config.js @@ -0,0 +1,17 @@ +export default { + immutable: true, + html: `

Called 0 times.

`, + + test(assert, component, target, window) { + component.on('state', ({ changed }) => { + if (changed.foo) { + component.set({ count: component.get().count + 1 }); + } + }); + + assert.htmlEqual(target.innerHTML, `

Called 0 times.

`); + + component.set({ foo: component.get().foo }); + assert.htmlEqual(target.innerHTML, `

Called 0 times.

`); + } +}; diff --git a/test/runtime/samples/immutable-root/main.html b/test/runtime/samples/immutable-root/main.html new file mode 100644 index 0000000000..b54f41743c --- /dev/null +++ b/test/runtime/samples/immutable-root/main.html @@ -0,0 +1,14 @@ +
+

Called {count} times.

+
+ + \ No newline at end of file diff --git a/test/runtime/samples/initial-state-assign/_config.js b/test/runtime/samples/initial-state-assign/_config.js new file mode 100644 index 0000000000..961cffae43 --- /dev/null +++ b/test/runtime/samples/initial-state-assign/_config.js @@ -0,0 +1,7 @@ +export default { + data: { bar: 'bar' }, + html: ` + "foo" + "bar" + `, +}; diff --git a/test/runtime/samples/initial-state-assign/main.html b/test/runtime/samples/initial-state-assign/main.html new file mode 100644 index 0000000000..be3a613784 --- /dev/null +++ b/test/runtime/samples/initial-state-assign/main.html @@ -0,0 +1,10 @@ +{JSON.stringify(foo)} +{JSON.stringify(bar)} + + diff --git a/test/runtime/samples/inline-expressions/main.html b/test/runtime/samples/inline-expressions/main.html index 93c55f37e3..e5da1ff551 100644 --- a/test/runtime/samples/inline-expressions/main.html +++ b/test/runtime/samples/inline-expressions/main.html @@ -1 +1 @@ -

{{a}} + {{b}} = {{a + b}}

+

{a} + {b} = {a + b}

diff --git a/test/runtime/samples/names-deconflicted-nested/main.html b/test/runtime/samples/names-deconflicted-nested/main.html index 7de2cf204d..4d0b8ae1b6 100644 --- a/test/runtime/samples/names-deconflicted-nested/main.html +++ b/test/runtime/samples/names-deconflicted-nested/main.html @@ -1,7 +1,7 @@ -{{#each array as row, i}} +{#each array as row, i}
- {{#each row as cell, j}} - [ {{i}}, {{j}} ] - {{/each}} + {#each row as cell, j} + [ {i}, {j} ] + {/each}
-{{/each}} +{/each} diff --git a/test/runtime/samples/names-deconflicted/Widget.html b/test/runtime/samples/names-deconflicted/Widget.html index 9a6074567b..a004758196 100644 --- a/test/runtime/samples/names-deconflicted/Widget.html +++ b/test/runtime/samples/names-deconflicted/Widget.html @@ -1 +1 @@ -

{{index + 1}}: {{widget.name}}

+

{index + 1}: {widget.name}

diff --git a/test/runtime/samples/names-deconflicted/main.html b/test/runtime/samples/names-deconflicted/main.html index ca6eac71db..282c438267 100644 --- a/test/runtime/samples/names-deconflicted/main.html +++ b/test/runtime/samples/names-deconflicted/main.html @@ -1,6 +1,6 @@ -{{#each widgets as widget, i}} - -{{/each}} +{#each widgets as widget, i} + +{/each} \ No newline at end of file diff --git a/test/runtime/samples/ondestroy-before-cleanup/main.html b/test/runtime/samples/ondestroy-before-cleanup/main.html index c4b7ac879f..cce0356f6a 100644 --- a/test/runtime/samples/ondestroy-before-cleanup/main.html +++ b/test/runtime/samples/ondestroy-before-cleanup/main.html @@ -1,6 +1,6 @@ -{{#if visible}} +{#if visible} -{{/if}} +{/if} + \ No newline at end of file diff --git a/test/runtime/samples/onrender-chain/List.html b/test/runtime/samples/onrender-chain/List.html index 5bf8f12cd8..513c62b2e6 100644 --- a/test/runtime/samples/onrender-chain/List.html +++ b/test/runtime/samples/onrender-chain/List.html @@ -1,6 +1,6 @@ -{{#each items as item}} - -{{/each}} +{#each items as item} + +{/each} \ No newline at end of file diff --git a/test/runtime/samples/onstate-event/_config.js b/test/runtime/samples/onstate-event/_config.js new file mode 100644 index 0000000000..d3f24376f9 --- /dev/null +++ b/test/runtime/samples/onstate-event/_config.js @@ -0,0 +1,49 @@ +export default { + 'skip-ssr': true, + + data: { + foo: 'woo!' + }, + + html: ` +

woo!

+

undefined

+ `, + + test(assert, component, target) { + const history = []; + + component.on('state', ({ changed, current, previous }) => { + history.push({ changed, current, previous }); + component.set({ bar: current.foo.toUpperCase() }); + }); + + component.set({ foo: 'yeah!' }); + assert.htmlEqual(target.innerHTML, ` +

yeah!

+

YEAH!

+ `); + + component.set({ unused: 'x' }); + + assert.deepEqual(history, [ + { + changed: { foo: true }, + current: { foo: 'yeah!' }, + previous: { foo: 'woo!' } + }, + // this is NOT received, because Svelte will not allow + // an event handler to trigger itself recursively + // { + // changed: { bar: true }, + // current: { foo: 'yeah!', bar: 'YEAH!' }, + // previous: { foo: 'yeah!' } + // }, + { + changed: { unused: true }, + current: { foo: 'yeah!', bar: 'YEAH!', unused: 'x' }, + previous: { foo: 'yeah!', bar: 'YEAH!' } + } + ]); + } +}; diff --git a/test/runtime/samples/onstate-event/main.html b/test/runtime/samples/onstate-event/main.html new file mode 100644 index 0000000000..fbcc02f2f8 --- /dev/null +++ b/test/runtime/samples/onstate-event/main.html @@ -0,0 +1,2 @@ +

{foo}

+

{bar}

\ No newline at end of file diff --git a/test/runtime/samples/onstate-no-template/_config.js b/test/runtime/samples/onstate-no-template/_config.js new file mode 100644 index 0000000000..87d7c599ce --- /dev/null +++ b/test/runtime/samples/onstate-no-template/_config.js @@ -0,0 +1,11 @@ +export default { + 'skip-ssr': true, + + data: { + foo: 'woo!' + }, + + test(assert, component) { + assert.deepEqual(component.changed, { foo: 1 }); + } +}; diff --git a/test/runtime/samples/onstate-no-template/main.html b/test/runtime/samples/onstate-no-template/main.html new file mode 100644 index 0000000000..a3c59f99be --- /dev/null +++ b/test/runtime/samples/onstate-no-template/main.html @@ -0,0 +1,7 @@ + diff --git a/test/runtime/samples/onstate/_config.js b/test/runtime/samples/onstate/_config.js new file mode 100644 index 0000000000..48712bb41f --- /dev/null +++ b/test/runtime/samples/onstate/_config.js @@ -0,0 +1,20 @@ +export default { + 'skip-ssr': true, + + data: { + foo: 'woo!' + }, + + html: ` +

woo!

+

WOO!

+ `, + + test(assert, component, target) { + component.set({ foo: 'yeah!' }); + assert.htmlEqual(target.innerHTML, ` +

yeah!

+

YEAH!

+ `); + } +}; diff --git a/test/runtime/samples/onstate/main.html b/test/runtime/samples/onstate/main.html new file mode 100644 index 0000000000..f35c64cd74 --- /dev/null +++ b/test/runtime/samples/onstate/main.html @@ -0,0 +1,10 @@ +

{foo}

+

{bar}

+ + \ No newline at end of file diff --git a/test/runtime/samples/onupdate/_config.js b/test/runtime/samples/onupdate/_config.js new file mode 100644 index 0000000000..41e6041937 --- /dev/null +++ b/test/runtime/samples/onupdate/_config.js @@ -0,0 +1,20 @@ +export default { + 'skip-ssr': true, + + data: { + value: 'hello!' + }, + + html: ` +

hello!

+

hello!

+ `, + + test(assert, component, target) { + component.set({ value: 'goodbye!' }); + assert.htmlEqual(target.innerHTML, ` +

goodbye!

+

goodbye!

+ `); + } +}; diff --git a/test/runtime/samples/onupdate/main.html b/test/runtime/samples/onupdate/main.html new file mode 100644 index 0000000000..49afda13e5 --- /dev/null +++ b/test/runtime/samples/onupdate/main.html @@ -0,0 +1,12 @@ +

{value}

+

+ + \ No newline at end of file diff --git a/test/runtime/samples/option-without-select/main.html b/test/runtime/samples/option-without-select/main.html index 36c101d13d..b0156e95c5 100644 --- a/test/runtime/samples/option-without-select/main.html +++ b/test/runtime/samples/option-without-select/main.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/test/runtime/samples/options/main.html b/test/runtime/samples/options/main.html index 2cd6339235..1053bc4c0a 100644 --- a/test/runtime/samples/options/main.html +++ b/test/runtime/samples/options/main.html @@ -1,4 +1,4 @@ -

{{text}}

+

{text}

+
+ ` +}; \ No newline at end of file diff --git a/test/runtime/samples/script-style-non-top-level/main.html b/test/runtime/samples/script-style-non-top-level/main.html new file mode 100644 index 0000000000..94cf72e4bd --- /dev/null +++ b/test/runtime/samples/script-style-non-top-level/main.html @@ -0,0 +1,4 @@ +
+ + +
\ No newline at end of file diff --git a/test/runtime/samples/select-bind-array/main.html b/test/runtime/samples/select-bind-array/main.html index 7971c4fe10..67c1dfdb91 100644 --- a/test/runtime/samples/select-bind-array/main.html +++ b/test/runtime/samples/select-bind-array/main.html @@ -1,5 +1,5 @@ diff --git a/test/runtime/samples/select-bind-in-array/_config.js b/test/runtime/samples/select-bind-in-array/_config.js index 609fc4592a..32b522982b 100644 --- a/test/runtime/samples/select-bind-in-array/_config.js +++ b/test/runtime/samples/select-bind-in-array/_config.js @@ -8,7 +8,7 @@ export default { }, test ( assert, component, target ) { - const items = component.get('items'); + const items = component.get().items; assert.equal( items[0].id, 'a' ); assert.equal( items[1].id, 'b' ); diff --git a/test/runtime/samples/select-bind-in-array/main.html b/test/runtime/samples/select-bind-in-array/main.html index 1a80b1afa0..9a4d1e82cd 100644 --- a/test/runtime/samples/select-bind-in-array/main.html +++ b/test/runtime/samples/select-bind-in-array/main.html @@ -1,6 +1,6 @@ -{{#each items as item}} +{#each items as item} -{{/each}} +{/each} diff --git a/test/runtime/samples/select-change-handler/_config.js b/test/runtime/samples/select-change-handler/_config.js index 015c8182b4..8ea4d66e82 100644 --- a/test/runtime/samples/select-change-handler/_config.js +++ b/test/runtime/samples/select-change-handler/_config.js @@ -14,8 +14,8 @@ export default { select.dispatchEvent( event ); assert.equal( select.value, 'c' ); - assert.equal( component.get( 'lastChangedTo' ), 'c' ); - assert.equal( component.get( 'selected' ), 'c' ); + assert.equal( component.get().lastChangedTo, 'c' ); + assert.equal( component.get().selected, 'c' ); component.destroy(); } diff --git a/test/runtime/samples/select-change-handler/main.html b/test/runtime/samples/select-change-handler/main.html index 48376dfa93..13d9bd75af 100644 --- a/test/runtime/samples/select-change-handler/main.html +++ b/test/runtime/samples/select-change-handler/main.html @@ -1,7 +1,7 @@ \ No newline at end of file diff --git a/test/runtime/samples/set-in-observe-dedupes-renders/Widget.html b/test/runtime/samples/set-in-observe-dedupes-renders/Widget.html deleted file mode 100644 index 3d414dd204..0000000000 --- a/test/runtime/samples/set-in-observe-dedupes-renders/Widget.html +++ /dev/null @@ -1,17 +0,0 @@ -
{{foo.x}}
- - diff --git a/test/runtime/samples/set-in-observe/main.html b/test/runtime/samples/set-in-observe/main.html deleted file mode 100644 index d07607f5fe..0000000000 --- a/test/runtime/samples/set-in-observe/main.html +++ /dev/null @@ -1,16 +0,0 @@ -

{{foo}}

-

{{bar}}

- - diff --git a/test/runtime/samples/set-in-oncreate/main.html b/test/runtime/samples/set-in-oncreate/main.html index 8e71ec5905..77633574f1 100644 --- a/test/runtime/samples/set-in-oncreate/main.html +++ b/test/runtime/samples/set-in-oncreate/main.html @@ -1,4 +1,4 @@ -

{{foo}}

+

{foo}

\ No newline at end of file diff --git a/test/runtime/samples/set-in-observe-dedupes-renders/_config.js b/test/runtime/samples/set-in-onstate-dedupes-renders/_config.js similarity index 100% rename from test/runtime/samples/set-in-observe-dedupes-renders/_config.js rename to test/runtime/samples/set-in-onstate-dedupes-renders/_config.js diff --git a/test/runtime/samples/set-in-observe-dedupes-renders/main.html b/test/runtime/samples/set-in-onstate-dedupes-renders/main.html similarity index 58% rename from test/runtime/samples/set-in-observe-dedupes-renders/main.html rename to test/runtime/samples/set-in-onstate-dedupes-renders/main.html index f62940a473..6813463ec1 100644 --- a/test/runtime/samples/set-in-observe-dedupes-renders/main.html +++ b/test/runtime/samples/set-in-onstate-dedupes-renders/main.html @@ -1,4 +1,4 @@ - + diff --git a/test/runtime/samples/set-mutated-data/main.html b/test/runtime/samples/set-mutated-data/main.html index 24369f73a4..e076ac55f8 100644 --- a/test/runtime/samples/set-mutated-data/main.html +++ b/test/runtime/samples/set-mutated-data/main.html @@ -1 +1 @@ -{{foo}} \ No newline at end of file +{foo} \ No newline at end of file diff --git a/test/runtime/samples/set-prevents-loop/main.html b/test/runtime/samples/set-prevents-loop/main.html index becfe053fe..685bfeaa14 100644 --- a/test/runtime/samples/set-prevents-loop/main.html +++ b/test/runtime/samples/set-prevents-loop/main.html @@ -1,8 +1,8 @@ -{{#if visible}} +{#if visible} -{{else}} +{:else} -{{/if}} +{/if} diff --git a/test/runtime/samples/spread-component-dynamic-undefined/_config.js b/test/runtime/samples/spread-component-dynamic-undefined/_config.js new file mode 100644 index 0000000000..366750870e --- /dev/null +++ b/test/runtime/samples/spread-component-dynamic-undefined/_config.js @@ -0,0 +1,19 @@ +export default { + data: { + props: { + a: 1, + }, + }, + + html: ``, + + test(assert, component, target) { + component.set({ + props: { + a: 2, + }, + }); + + assert.htmlEqual(target.innerHTML, ``); + }, +}; diff --git a/test/runtime/samples/spread-component-dynamic-undefined/main.html b/test/runtime/samples/spread-component-dynamic-undefined/main.html new file mode 100644 index 0000000000..435cacaa53 --- /dev/null +++ b/test/runtime/samples/spread-component-dynamic-undefined/main.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/runtime/samples/spread-component-dynamic/Foo.html b/test/runtime/samples/spread-component-dynamic/Foo.html new file mode 100644 index 0000000000..e7884fdfee --- /dev/null +++ b/test/runtime/samples/spread-component-dynamic/Foo.html @@ -0,0 +1 @@ +

a: {a}

\ No newline at end of file diff --git a/test/runtime/samples/spread-component-dynamic/_config.js b/test/runtime/samples/spread-component-dynamic/_config.js new file mode 100644 index 0000000000..9637d22578 --- /dev/null +++ b/test/runtime/samples/spread-component-dynamic/_config.js @@ -0,0 +1,21 @@ +export default { + data: { + props: { + a: 1, + }, + }, + + html: ` +

a: 1

+ `, + + test(assert, component, target) { + component.set({ + props: { + a: 2, + }, + }); + + assert.htmlEqual(target.innerHTML, `

a: 2

`); + }, +}; diff --git a/test/runtime/samples/spread-component-dynamic/main.html b/test/runtime/samples/spread-component-dynamic/main.html new file mode 100644 index 0000000000..7b89cf5a19 --- /dev/null +++ b/test/runtime/samples/spread-component-dynamic/main.html @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/test/runtime/samples/spread-component/Widget.html b/test/runtime/samples/spread-component/Widget.html new file mode 100644 index 0000000000..a60afb741c --- /dev/null +++ b/test/runtime/samples/spread-component/Widget.html @@ -0,0 +1,4 @@ +

foo: {foo}

+

baz: {baz} ({typeof baz})

+

qux: {qux}

+

quux: {quux}

diff --git a/test/runtime/samples/spread-component/_config.js b/test/runtime/samples/spread-component/_config.js new file mode 100644 index 0000000000..06acc4906d --- /dev/null +++ b/test/runtime/samples/spread-component/_config.js @@ -0,0 +1,25 @@ +export default { + data: { + props: { + foo: 'lol', + baz: 40 + 2, + qux: `this is a ${'piece of'} string`, + quux: 'core' + } + }, + + html: `

foo: lol

\n

baz: 42 (number)

\n

qux: named

\n

quux: core

`, + + test ( assert, component, target ) { + component.set({ + props: { + foo: 'wut', + baz: 40 + 3, + qux: `this is a ${'rather boring'} string`, + quux: 'heart' + } + }); + + assert.equal( target.innerHTML, `

foo: wut

\n

baz: 43 (number)

\n

qux: named

\n

quux: heart

` ); + } +}; diff --git a/test/server-side-rendering/samples/component-data-static/main.html b/test/runtime/samples/spread-component/main.html similarity index 77% rename from test/server-side-rendering/samples/component-data-static/main.html rename to test/runtime/samples/spread-component/main.html index 0df72c1465..ea7e0a812c 100644 --- a/test/server-side-rendering/samples/component-data-static/main.html +++ b/test/runtime/samples/spread-component/main.html @@ -1,5 +1,5 @@
- +
\ No newline at end of file diff --git a/test/runtime/samples/spread-each-element/_config.js b/test/runtime/samples/spread-each-element/_config.js new file mode 100644 index 0000000000..913a7cc865 --- /dev/null +++ b/test/runtime/samples/spread-each-element/_config.js @@ -0,0 +1,26 @@ +export default { + html: ` +
+
+ `, + + data: { + things: [ + { 'data-a': 1, 'data-b': 2 }, + { 'data-c': 3, 'data-d': 4 } + ] + }, + + test(assert, component, target) { + const { things } = component.get(); + + component.set({ + things: things.reverse() + }); + + assert.htmlEqual(target.innerHTML, ` +
+
+ `); + }, +}; diff --git a/test/runtime/samples/spread-each-element/main.html b/test/runtime/samples/spread-each-element/main.html new file mode 100644 index 0000000000..99ae4eeece --- /dev/null +++ b/test/runtime/samples/spread-each-element/main.html @@ -0,0 +1,3 @@ +{#each things as thing} +
+{/each} \ No newline at end of file diff --git a/test/runtime/samples/spread-element-boolean/_config.js b/test/runtime/samples/spread-element-boolean/_config.js new file mode 100644 index 0000000000..83a1590f29 --- /dev/null +++ b/test/runtime/samples/spread-element-boolean/_config.js @@ -0,0 +1,27 @@ +export default { + data: { + props: { + disabled: true + } + }, + + html: ` + + `, + + test(assert, component, target) { + const button = target.querySelector('button'); + + assert.ok(button.disabled); + + component.set({ + props: { disabled: false } + }); + + assert.htmlEqual( + target.innerHTML, + `` + ); + assert.ok(!button.disabled); + }, +}; diff --git a/test/runtime/samples/spread-element-boolean/main.html b/test/runtime/samples/spread-element-boolean/main.html new file mode 100644 index 0000000000..5cd4df8766 --- /dev/null +++ b/test/runtime/samples/spread-element-boolean/main.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/runtime/samples/spread-element-multiple/_config.js b/test/runtime/samples/spread-element-multiple/_config.js new file mode 100644 index 0000000000..4a1aae2d83 --- /dev/null +++ b/test/runtime/samples/spread-element-multiple/_config.js @@ -0,0 +1,33 @@ +export default { + data: { + a: { + 'data-one': 1, + 'data-two': 2, + }, + c: { + 'data-b': 'overridden', + }, + d: 'deeeeee', + }, + + html: ` +
test
+ `, + + test(assert, component, target) { + component.set({ + a: { + 'data-one': 10 + }, + c: { + 'data-c': 'new' + }, + d: 'DEEEEEE' + }); + + assert.htmlEqual( + target.innerHTML, + `
test
` + ); + }, +}; diff --git a/test/runtime/samples/spread-element-multiple/main.html b/test/runtime/samples/spread-element-multiple/main.html new file mode 100644 index 0000000000..97272e6946 --- /dev/null +++ b/test/runtime/samples/spread-element-multiple/main.html @@ -0,0 +1 @@ +
test
\ No newline at end of file diff --git a/test/runtime/samples/spread-element/_config.js b/test/runtime/samples/spread-element/_config.js new file mode 100644 index 0000000000..bd6c0b891e --- /dev/null +++ b/test/runtime/samples/spread-element/_config.js @@ -0,0 +1,19 @@ +export default { + html: `
red
`, + + test ( assert, component, target ) { + const div = target.querySelector( 'div' ); + + assert.equal( div.dataset.foo, 'bar' ); + assert.equal( div.dataset.named, 'value' ); + + component.set({ color: 'blue', props: { 'data-foo': 'baz', 'data-named': 'qux' } }); + assert.htmlEqual( target.innerHTML, `
blue
` ); + assert.equal( div.dataset.foo, 'baz' ); + assert.equal( div.dataset.named, 'value' ); + + component.set({ color: 'blue', props: {} }); + assert.htmlEqual( target.innerHTML, `
blue
` ); + assert.equal( div.dataset.foo, undefined ); + } +}; diff --git a/test/runtime/samples/spread-element/main.html b/test/runtime/samples/spread-element/main.html new file mode 100644 index 0000000000..391566826f --- /dev/null +++ b/test/runtime/samples/spread-element/main.html @@ -0,0 +1,13 @@ +
{color}
+ + diff --git a/test/runtime/samples/state-deconflicted/main.html b/test/runtime/samples/state-deconflicted/main.html index b5ce111cae..12ff80a0ed 100644 --- a/test/runtime/samples/state-deconflicted/main.html +++ b/test/runtime/samples/state-deconflicted/main.html @@ -1,7 +1,7 @@ -

Current state: {{state}}

+

Current state: {state}

    - {{#each states as state}} -
  • {{state}}
  • - {{/each}} + {#each states as state} +
  • {state}
  • + {/each}
\ No newline at end of file diff --git a/test/runtime/samples/store-binding/_config.js b/test/runtime/samples/store-binding/_config.js index aefc4ec652..7839a9e3ea 100644 --- a/test/runtime/samples/store-binding/_config.js +++ b/test/runtime/samples/store-binding/_config.js @@ -19,7 +19,7 @@ export default { input.value = 'everybody'; input.dispatchEvent(event); - assert.equal(store.get('name'), 'everybody'); + assert.equal(store.get().name, 'everybody'); assert.htmlEqual(target.innerHTML, `

Hello everybody!

diff --git a/test/runtime/samples/store-binding/main.html b/test/runtime/samples/store-binding/main.html index 06410ea770..0f9e902bf3 100644 --- a/test/runtime/samples/store-binding/main.html +++ b/test/runtime/samples/store-binding/main.html @@ -1,4 +1,4 @@ -

Hello {{$name}}!

+

Hello {$name}!

\ No newline at end of file diff --git a/test/runtime/samples/store-component-binding-each/Widget.html b/test/runtime/samples/store-component-binding-each/Widget.html new file mode 100644 index 0000000000..f24d608cd5 --- /dev/null +++ b/test/runtime/samples/store-component-binding-each/Widget.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/runtime/samples/store-component-binding-each/_config.js b/test/runtime/samples/store-component-binding-each/_config.js new file mode 100644 index 0000000000..84977b9b3b --- /dev/null +++ b/test/runtime/samples/store-component-binding-each/_config.js @@ -0,0 +1,30 @@ +import { Store } from '../../../../store.js'; + +const store = new Store({ + a: ['foo', 'bar', 'baz'] +}); + +export default { + store, + + html: ` + +

foo, bar, baz

+ `, + + test(assert, component, target, window) { + const event = new window.MouseEvent('input'); + const inputs = target.querySelectorAll('input'); + + inputs[0].value = 'blah'; + inputs[0].dispatchEvent(event); + + assert.deepEqual(store.get().a, ['blah', 'bar', 'baz']); + assert.htmlEqual(target.innerHTML, ` + +

blah, bar, baz

+ `); + + component.destroy(); + }, +}; diff --git a/test/runtime/samples/store-component-binding-each/main.html b/test/runtime/samples/store-component-binding-each/main.html new file mode 100644 index 0000000000..01be9e6b45 --- /dev/null +++ b/test/runtime/samples/store-component-binding-each/main.html @@ -0,0 +1,15 @@ +{#each $a as x} + +{/each} + +

{$a.join(', ')}

+ + diff --git a/test/runtime/samples/store-component-binding/TextInput.html b/test/runtime/samples/store-component-binding/TextInput.html new file mode 100644 index 0000000000..f24d608cd5 --- /dev/null +++ b/test/runtime/samples/store-component-binding/TextInput.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/runtime/samples/store-component-binding/_config.js b/test/runtime/samples/store-component-binding/_config.js new file mode 100644 index 0000000000..12b0fcbfb4 --- /dev/null +++ b/test/runtime/samples/store-component-binding/_config.js @@ -0,0 +1,40 @@ +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'); + + const changeRecord = []; + store.on('state', ({ changed, current }) => { + changeRecord.push({ changed, current }); + }); + + input.value = 'everybody'; + input.dispatchEvent(event); + + assert.equal(store.get().name, 'everybody'); + assert.htmlEqual(target.innerHTML, ` +

Hello everybody!

+ + `); + + assert.deepEqual(changeRecord, [ + { + current: { name: 'everybody' }, + changed: { name: true } + } + ]); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/store-component-binding/main.html b/test/runtime/samples/store-component-binding/main.html new file mode 100644 index 0000000000..8abfd6c239 --- /dev/null +++ b/test/runtime/samples/store-component-binding/main.html @@ -0,0 +1,10 @@ +

Hello {$name}!

+ + + \ No newline at end of file diff --git a/test/runtime/samples/store-computed-oncreate/_config.js b/test/runtime/samples/store-computed-oncreate/_config.js new file mode 100644 index 0000000000..5e58271bad --- /dev/null +++ b/test/runtime/samples/store-computed-oncreate/_config.js @@ -0,0 +1,11 @@ +import { Store } from '../../../../store.js'; + +export default { + 'skip-ssr': true, + + store: new Store(), + + html: ` +

Hello Brian!

+ ` +}; \ No newline at end of file diff --git a/test/runtime/samples/store-computed-oncreate/main.html b/test/runtime/samples/store-computed-oncreate/main.html new file mode 100644 index 0000000000..ce35a047b5 --- /dev/null +++ b/test/runtime/samples/store-computed-oncreate/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 index 2ad67420ca..c665d86d32 100644 --- a/test/runtime/samples/store-computed/Todo.html +++ b/test/runtime/samples/store-computed/Todo.html @@ -1,11 +1,11 @@ -{{#if isVisible}} -
{{todo.description}}
-{{/if}} +{#if isVisible} +
{todo.description}
+{/if} diff --git a/test/runtime/samples/svg-child-component-declared-namespace-backtick-string/_config.js b/test/runtime/samples/svg-child-component-declared-namespace-backtick-string/_config.js new file mode 100644 index 0000000000..b4a0da70db --- /dev/null +++ b/test/runtime/samples/svg-child-component-declared-namespace-backtick-string/_config.js @@ -0,0 +1,21 @@ +export default { + data: { + x: 0, + y: 0, + width: 100, + height: 100 + }, + + html: ``, + + test ( assert, component, target ) { + const svg = target.querySelector( 'svg' ); + const rect = target.querySelector( 'rect' ); + + assert.equal( svg.namespaceURI, 'http://www.w3.org/2000/svg' ); + assert.equal( rect.namespaceURI, 'http://www.w3.org/2000/svg' ); + + component.set({ width: 150, height: 50 }); + assert.equal( target.innerHTML, `` ); + }, +}; diff --git a/test/runtime/samples/svg-child-component-declared-namespace-backtick-string/main.html b/test/runtime/samples/svg-child-component-declared-namespace-backtick-string/main.html new file mode 100644 index 0000000000..6ae6ad8590 --- /dev/null +++ b/test/runtime/samples/svg-child-component-declared-namespace-backtick-string/main.html @@ -0,0 +1,11 @@ + + + + + diff --git a/test/runtime/samples/svg-child-component-declared-namespace-shorthand/Rect.html b/test/runtime/samples/svg-child-component-declared-namespace-shorthand/Rect.html index c158d7fdf5..536f47107e 100644 --- a/test/runtime/samples/svg-child-component-declared-namespace-shorthand/Rect.html +++ b/test/runtime/samples/svg-child-component-declared-namespace-shorthand/Rect.html @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/test/runtime/samples/transition-js-await-block/_config.js b/test/runtime/samples/transition-js-await-block/_config.js new file mode 100644 index 0000000000..ff9888c88b --- /dev/null +++ b/test/runtime/samples/transition-js-await-block/_config.js @@ -0,0 +1,35 @@ +let fulfil; +let reject; + +let promise = new Promise((f, r) => { + fulfil = f; + reject = r; +}); + +export default { + data: { + promise + }, + + test(assert, component, target, window, raf) { + component.set({ visible: true }); + let p = target.querySelector('p'); + + assert.equal(p.className, 'pending'); + assert.equal(p.foo, 0); + + raf.tick(50); + assert.equal(p.foo, 0.5); + + fulfil(42); + + return promise.then(() => { + raf.tick(80); + let ps = document.querySelectorAll('p'); + assert.equal(ps[0].className, 'pending'); + assert.equal(ps[1].className, 'then'); + assert.equal(ps[0].foo, 0.2); + assert.equal(ps[1].foo, 0.3); + }); + } +}; diff --git a/test/runtime/samples/transition-js-await-block/main.html b/test/runtime/samples/transition-js-await-block/main.html new file mode 100644 index 0000000000..9fc038fd66 --- /dev/null +++ b/test/runtime/samples/transition-js-await-block/main.html @@ -0,0 +1,22 @@ +{#await promise} +

loading...

+{:then value} +

{value}

+{:catch error} +

{error.message}

+{/await} + + \ No newline at end of file diff --git a/test/runtime/samples/transition-js-delay-in-out/main.html b/test/runtime/samples/transition-js-delay-in-out/main.html index 6a0a3f88a7..43400fd67e 100644 --- a/test/runtime/samples/transition-js-delay-in-out/main.html +++ b/test/runtime/samples/transition-js-delay-in-out/main.html @@ -1,6 +1,6 @@ -{{#if visible}} +{#if visible}
delayed
-{{/if}} +{/if} \ No newline at end of file diff --git a/test/runtime/samples/transition-js-nested-intro/_config.js b/test/runtime/samples/transition-js-nested-intro/_config.js new file mode 100644 index 0000000000..9906be26d2 --- /dev/null +++ b/test/runtime/samples/transition-js-nested-intro/_config.js @@ -0,0 +1,21 @@ +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, 0 ); + + raf.tick( 100 ); + assert.equal( div.foo, 0.5 ); + + raf.tick( 125 ); + assert.equal( div.foo, 0.75 ); + + raf.tick( 150 ); + assert.equal( div.foo, 1 ); + + component.destroy(); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/transition-js-nested-intro/main.html b/test/runtime/samples/transition-js-nested-intro/main.html new file mode 100644 index 0000000000..c06e004c8c --- /dev/null +++ b/test/runtime/samples/transition-js-nested-intro/main.html @@ -0,0 +1,10 @@ +{#if visible} + delayed +{/if} + + \ No newline at end of file diff --git a/test/runtime/samples/transition-js-parameterised-with-state/main.html b/test/runtime/samples/transition-js-parameterised-with-state/main.html index 45d9d37a1c..68d036e4da 100644 --- a/test/runtime/samples/transition-js-parameterised-with-state/main.html +++ b/test/runtime/samples/transition-js-parameterised-with-state/main.html @@ -1,6 +1,6 @@ -{{#if visible}} +{#if visible}
fades in
-{{/if}} +{/if} \ No newline at end of file diff --git a/test/runtime/samples/window-event/main.html b/test/runtime/samples/window-event/main.html index b6679a0e44..76f5c02084 100644 --- a/test/runtime/samples/window-event/main.html +++ b/test/runtime/samples/window-event/main.html @@ -1,3 +1,3 @@ -<:Window on:resize='set({ width: this.innerWidth, height: this.innerHeight })'/> + -
{{width}}x{{height}}
\ No newline at end of file +
{width}x{height}
\ No newline at end of file diff --git a/test/server-side-rendering/index.js b/test/server-side-rendering/index.js index 019c452c2f..1d3aaa6825 100644 --- a/test/server-side-rendering/index.js +++ b/test/server-side-rendering/index.js @@ -1,7 +1,7 @@ import assert from "assert"; import * as fs from "fs"; import * as path from "path"; -import glob from 'glob'; +import glob from 'tiny-glob/sync.js'; import { showOutput, @@ -105,14 +105,14 @@ describe("ssr", () => { (config.skip ? it.skip : config.solo ? it.only : it)(dir, () => { const cwd = path.resolve("test/runtime/samples", dir); - glob.sync('**/*.html', { cwd: `test/runtime/samples/${dir}` }).forEach(file => { + glob('**/*.html', { cwd: `test/runtime/samples/${dir}` }).forEach(file => { const resolved = require.resolve(`../runtime/samples/${dir}/${file}`); delete require.cache[resolved]; }); - require("../../ssr/register")({ - store: !!config.store - }); + const compileOptions = config.compileOptions || {}; + + require("../../ssr/register")(compileOptions); try { const component = require(`../runtime/samples/${dir}/main.html`); diff --git a/test/server-side-rendering/samples/attribute-dynamic/main.html b/test/server-side-rendering/samples/attribute-dynamic/main.html index 18732ab91a..1528a49478 100644 --- a/test/server-side-rendering/samples/attribute-dynamic/main.html +++ b/test/server-side-rendering/samples/attribute-dynamic/main.html @@ -1 +1 @@ -
{{color}} {{font}}
+
{color} {font}
diff --git a/test/server-side-rendering/samples/component-binding-renamed/main.html b/test/server-side-rendering/samples/component-binding-renamed/main.html index 7445660354..7f5e588534 100644 --- a/test/server-side-rendering/samples/component-binding-renamed/main.html +++ b/test/server-side-rendering/samples/component-binding-renamed/main.html @@ -1,4 +1,4 @@ -{{y}}{{y}} +{y}{y} diff --git a/test/server-side-rendering/samples/default-data-override/main.html b/test/server-side-rendering/samples/default-data-override/main.html index 3605487855..1014c2df07 100644 --- a/test/server-side-rendering/samples/default-data-override/main.html +++ b/test/server-side-rendering/samples/default-data-override/main.html @@ -1,4 +1,4 @@ -

{{foo}}

+

{foo}

\ No newline at end of file diff --git a/test/stats/samples/imports/_config.js b/test/stats/samples/imports/_config.js new file mode 100644 index 0000000000..f7c15183dd --- /dev/null +++ b/test/stats/samples/imports/_config.js @@ -0,0 +1,18 @@ +export default { + test(assert, stats) { + assert.deepEqual(stats.imports, [ + { + source: 'x', + specifiers: [{ name: 'default', as: 'x' }] + }, + { + source: 'y', + specifiers: [{ name: 'y', as: 'y' }] + }, + { + source: 'z', + specifiers: [{ name: '*', as: 'z' }] + } + ]); + } +}; \ No newline at end of file diff --git a/test/stats/samples/imports/input.html b/test/stats/samples/imports/input.html new file mode 100644 index 0000000000..a7b59691ac --- /dev/null +++ b/test/stats/samples/imports/input.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/test/store/index.js b/test/store/index.js index d8053367a9..8d5dd5749c 100644 --- a/test/store/index.js +++ b/test/store/index.js @@ -5,32 +5,7 @@ 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' @@ -48,12 +23,12 @@ describe('store', () => { foo: 'bar' }); - assert.equal(store.get('foo'), 'bar'); + assert.equal(store.get().foo, 'bar'); }); }); - describe('observe', () => { - it('observes state', () => { + describe('on', () => { + it('listens to an event', () => { let newFoo; let oldFoo; @@ -61,56 +36,39 @@ describe('store', () => { foo: 'bar' }); - store.observe('foo', (n, o) => { - newFoo = n; - oldFoo = o; + store.on('state', ({ changed, current, previous }) => { + newFoo = current.foo; + oldFoo = previous.foo; }); - assert.equal(newFoo, 'bar'); - assert.equal(oldFoo, undefined); - - store.set({ - foo: 'baz' - }); + 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(); + describe('fire', () => { + let answer; - let count = 0; - let args; + const store = new Store(); - store.onchange((state, changed) => { - count += 1; - args = { state, changed }; - }); - - store.set({ foo: 'bar' }); + store.on('custom', event => { + answer = event.answer; + }); - assert.equal(count, 1); - assert.deepEqual(args, { - state: { foo: 'bar' }, - changed: { foo: true } - }); + store.fire('custom', { answer: 42 }); - // this should be a noop - store.set({ foo: 'bar' }); - assert.equal(count, 1); + assert.equal(answer, 42); + }); - // this shouldn't - store.set({ foo: 'baz' }); + it('allows user to cancel state change callback', () => { + const store = new Store(); + const handler = store.on('state', () => {}); - assert.equal(count, 2); - assert.deepEqual(args, { - state: { foo: 'baz' }, - changed: { foo: true } - }); - }); + assert.doesNotThrow(() => { + handler.cancel(); + }, TypeError, 'this._handlers is undefined'); }); describe('computed', () => { @@ -120,16 +78,16 @@ describe('store', () => { }); store.compute('bar', ['foo'], foo => foo * 2); - assert.equal(store.get('bar'), 2); + assert.equal(store.get().bar, 2); const values = []; - store.observe('bar', bar => { - values.push(bar); + store.on('state', ({ current }) => { + values.push(current.bar); }); store.set({ foo: 2 }); - assert.deepEqual(values, [2, 4]); + assert.deepEqual(values, [4]); }); it('computes a property based on another computed property', () => { @@ -139,16 +97,16 @@ describe('store', () => { store.compute('bar', ['foo'], foo => foo * 2); store.compute('baz', ['bar'], bar => bar * 2); - assert.equal(store.get('baz'), 4); + assert.equal(store.get().baz, 4); const values = []; - store.observe('baz', baz => { - values.push(baz); + store.on('state', ({ current }) => { + values.push(current.baz); }); store.set({ foo: 2 }); - assert.deepEqual(values, [4, 8]); + assert.deepEqual(values, [8]); }); it('prevents computed properties from being set', () => { @@ -183,8 +141,57 @@ describe('store', () => { assert.throws(() => { store.compute('a', ['b'], b => b + 1); - store.compute('b', ['a'], a => a + 1); - }, /Cyclical dependency detected/); + store.compute('b', ['c'], c => c + 1); + store.compute('c', ['a'], a => a + 1); + }, /Cyclical dependency detected between a <-> c/); + }); + + it('does not falsely report cycles', () => { + const store = new Store(); + + store.compute('dep4', ['dep1', 'dep2', 'dep3'], (...args) => ['dep4'].concat(...args)); + store.compute('dep1', ['source'], (...args) => ['dep1'].concat(...args)); + store.compute('dep2', ['dep1'], (...args) => ['dep2'].concat(...args)); + store.compute('dep3', ['dep1', 'dep2'], (...args) => ['dep3'].concat(...args)); + store.set({source: 'source'}); + + assert.deepEqual(store.get().dep4, [ + 'dep4', + 'dep1', 'source', + 'dep2', 'dep1', 'source', + 'dep3', 'dep1', 'source', + 'dep2', 'dep1', 'source' + ]); + }); + }); + + describe('immutable', () => { + it('observing state only changes on immutable updates', () => { + let newFoo; + let oldFoo; + let callCount = 0; + let value1 = {}; + let value2 = {}; + + const store = new Store({ + foo: value1 + }, { immutable: true }); + + store.on('state', ({ current, previous }) => { + callCount++; + newFoo = current.foo; + oldFoo = previous.foo; + }); + + store.set({ foo: value1 }); + + assert.equal(callCount, 0); + + store.set({ foo: value2 }); + + assert.equal(callCount, 1); + assert.equal(newFoo, value2); + assert.equal(oldFoo, value1); }); }); }); diff --git a/test/test.js b/test/test.js index b501fd81a0..9bf09879b2 100644 --- a/test/test.js +++ b/test/test.js @@ -1,11 +1,11 @@ -const glob = require("glob"); +const glob = require("tiny-glob/sync.js"); require("./setup"); -glob.sync("**/__test__.js", { cwd: "src" }).forEach(function(file) { +glob("**/__test__.js", { cwd: "src" }).forEach(function(file) { require("../src/" + file); }); -glob.sync("*/index.js", { cwd: "test" }).forEach(function(file) { +glob("*/index.js", { cwd: "test" }).forEach(function(file) { require("./" + file); }); diff --git a/test/validator/index.js b/test/validator/index.js index 176d060faf..852ac4191b 100644 --- a/test/validator/index.js +++ b/test/validator/index.js @@ -1,6 +1,6 @@ import * as fs from "fs"; import assert from "assert"; -import { svelte, tryToLoadJson } from "../helpers.js"; +import { svelte, loadConfig, tryToLoadJson } from "../helpers.js"; describe("validate", () => { fs.readdirSync("test/validator/samples").forEach(dir => { @@ -15,24 +15,36 @@ describe("validate", () => { } (solo ? it.only : skip ? it.skip : it)(dir, () => { - const filename = `test/validator/samples/${dir}/input.html`; - const input = fs.readFileSync(filename, "utf-8").replace(/\s+$/, ""); + const config = loadConfig(`./validator/samples/${dir}/_config.js`); + const input = fs.readFileSync(`test/validator/samples/${dir}/input.html`, "utf-8").replace(/\s+$/, ""); const expectedWarnings = tryToLoadJson(`test/validator/samples/${dir}/warnings.json`) || []; const expectedErrors = tryToLoadJson(`test/validator/samples/${dir}/errors.json`); + let error; try { const warnings = []; - svelte.compile(input, { + const { stats } = svelte.compile(input, { onwarn(warning) { - warnings.push({ - message: warning.message, - pos: warning.pos, - loc: warning.loc - }); - } + const { code, message, pos, start, end } = warning; + warnings.push({ code, message, pos, start, end }); + }, + dev: config.dev, + generate: false + }); + + assert.equal(stats.warnings.length, warnings.length); + stats.warnings.forEach((full, i) => { + const lite = warnings[i]; + assert.deepEqual({ + code: full.code, + message: full.message, + pos: full.pos, + start: full.start, + end: full.end + }, lite); }); assert.deepEqual(warnings, expectedWarnings); @@ -51,8 +63,10 @@ describe("validate", () => { throw new Error(`Expected an error: ${expected.message}`); } + assert.equal(error.code, expected.code); assert.equal(error.message, expected.message); - assert.deepEqual(error.loc, expected.loc); + assert.deepEqual(error.start, expected.start); + assert.deepEqual(error.end, expected.end); assert.equal(error.pos, expected.pos); } }); @@ -61,7 +75,8 @@ describe("validate", () => { it("errors if options.name is illegal", () => { assert.throws(() => { svelte.compile("
", { - name: "not.valid" + name: "not.valid", + generate: false }); }, /options\.name must be a valid identifier/); }); @@ -72,18 +87,38 @@ describe("validate", () => { name: "lowercase", onwarn(warning) { warnings.push({ + code: warning.code, message: warning.message, pos: warning.pos, - loc: warning.loc + start: warning.start }); - } + }, + generate: false }); assert.deepEqual(warnings, [ { + code: `options-lowercase-name`, message: "options.name should be capitalised", pos: undefined, - loc: undefined + start: undefined } ]); }); + + it("does not warn if options.name begins with non-alphabetic character", () => { + const warnings = []; + svelte.compile("
", { + name: "_", + onwarn(warning) { + warnings.push({ + code: warning.code, + message: warning.message, + pos: warning.pos, + start: warning.start + }); + }, + generate: false + }); + assert.deepEqual(warnings, []); + }); }); diff --git a/test/validator/samples/a11y-alt-text/warnings.json b/test/validator/samples/a11y-alt-text/warnings.json index 7bddf9730b..7ce5fb2d8f 100644 --- a/test/validator/samples/a11y-alt-text/warnings.json +++ b/test/validator/samples/a11y-alt-text/warnings.json @@ -1,36 +1,64 @@ [ { + "code": "a11y-missing-attribute", "message": "A11y: element should have an alt attribute", - "loc": { + "start": { "line": 1, - "column": 0 + "column": 0, + "character": 0 + }, + "end": { + "line": 1, + "column": 19, + "character": 19 }, "pos": 0 }, { + "code": "a11y-missing-attribute", "message": "A11y: element should have an alt, aria-label or aria-labelledby attribute", - "loc": { + "start": { + "line": 4, + "column": 1, + "character": 28 + }, + "end": { "line": 4, - "column": 1 + "column": 7, + "character": 34 }, "pos": 28 }, { + "code": "a11y-missing-attribute", "message": "A11y: element should have a title, aria-label or aria-labelledby attribute", - "loc": { + "start": { "line": 7, - "column": 0 + "column": 0, + "character": 43 + }, + "end": { + "line": 7, + "column": 17, + "character": 60 }, "pos": 43 }, { + "code": "a11y-missing-attribute", "message": "A11y: element should have an alt, aria-label or aria-labelledby attribute", - "loc": { + "start": { + "line": 9, + "column": 0, + "character": 62 + }, + "end": { "line": 9, - "column": 0 + "column": 20, + "character": 82 }, "pos": 62 } diff --git a/test/validator/samples/a11y-anchor-has-content/warnings.json b/test/validator/samples/a11y-anchor-has-content/warnings.json index 157bec1f9b..8708311f3c 100644 --- a/test/validator/samples/a11y-anchor-has-content/warnings.json +++ b/test/validator/samples/a11y-anchor-has-content/warnings.json @@ -1,8 +1,15 @@ [{ + "code": "a11y-missing-content", "message": "A11y: element should have child content", - "loc": { + "start": { "line": 1, - "column": 0 + "column": 0, + "character": 0 + }, + "end": { + "line": 1, + "column": 19, + "character": 19 }, "pos": 0 }] \ No newline at end of file diff --git a/test/validator/samples/a11y-anchor-in-svg-is-valid/warnings.json b/test/validator/samples/a11y-anchor-in-svg-is-valid/warnings.json index 1e65bc4986..eb62ce8db6 100644 --- a/test/validator/samples/a11y-anchor-in-svg-is-valid/warnings.json +++ b/test/validator/samples/a11y-anchor-in-svg-is-valid/warnings.json @@ -1,26 +1,47 @@ [ - { - "message": "A11y: element should have an href attribute", - "loc": { - "line": 1, - "column": 11 - }, - "pos": 11 - }, - { - "message": "A11y: '' is not a valid xlink:href attribute", - "loc": { - "line": 2, - "column": 14 - }, - "pos": 65 - }, - { - "message": "A11y: '#' is not a valid xlink:href attribute", - "loc": { - "line": 3, - "column": 14 - }, - "pos": 130 - } + { + "code": "a11y-missing-attribute", + "message": "A11y: element should have an href attribute", + "start": { + "line": 1, + "column": 11, + "character": 11 + }, + "end": { + "line": 1, + "column": 37, + "character": 37 + }, + "pos": 11 + }, + { + "code": "a11y-invalid-attribute", + "message": "A11y: '' is not a valid xlink:href attribute", + "start": { + "line": 2, + "column": 14, + "character": 65 + }, + "end": { + "line": 2, + "column": 27, + "character": 78 + }, + "pos": 65 + }, + { + "code": "a11y-invalid-attribute", + "message": "A11y: '#' is not a valid xlink:href attribute", + "start": { + "line": 3, + "column": 14, + "character": 130 + }, + "end": { + "line": 3, + "column": 28, + "character": 144 + }, + "pos": 130 + } ] diff --git a/test/validator/samples/a11y-anchor-is-valid/warnings.json b/test/validator/samples/a11y-anchor-is-valid/warnings.json index c63418f1bf..532c44ad24 100644 --- a/test/validator/samples/a11y-anchor-is-valid/warnings.json +++ b/test/validator/samples/a11y-anchor-is-valid/warnings.json @@ -1,25 +1,46 @@ [ { + "code": "a11y-missing-attribute", "message": "A11y: element should have an href attribute", - "loc": { + "start": { "line": 1, - "column": 0 + "column": 0, + "character": 0 + }, + "end": { + "line": 1, + "column": 26, + "character": 26 }, "pos": 0 }, { + "code": "a11y-invalid-attribute", "message": "A11y: '' is not a valid href attribute", - "loc": { + "start": { + "line": 2, + "column": 3, + "character": 30 + }, + "end": { "line": 2, - "column": 3 + "column": 10, + "character": 37 }, "pos": 30 }, { + "code": "a11y-invalid-attribute", "message": "A11y: '#' is not a valid href attribute", - "loc": { + "start": { + "line": 3, + "column": 3, + "character": 53 + }, + "end": { "line": 3, - "column": 3 + "column": 11, + "character": 61 }, "pos": 53 } diff --git a/test/validator/samples/a11y-aria-props/warnings.json b/test/validator/samples/a11y-aria-props/warnings.json index 5c0ce2c49e..73d34fc589 100644 --- a/test/validator/samples/a11y-aria-props/warnings.json +++ b/test/validator/samples/a11y-aria-props/warnings.json @@ -1,18 +1,32 @@ [ { + "code": "a11y-unknown-aria-attribute", "message": "A11y: Unknown aria attribute 'aria-labeledby' (did you mean 'labelledby'?)", - "loc": { + "start": { "line": 1, - "column": 20 + "column": 20, + "character": 20 + }, + "end": { + "line": 1, + "column": 40, + "character": 40 }, "pos": 20 }, { + "code": "a11y-missing-attribute", "message": "A11y: element should have an alt, aria-label or aria-labelledby attribute", - "loc": { + "start": { "column": 0, - "line": 1 + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "column": 41, + "character": 41 }, "pos": 0 } diff --git a/test/validator/samples/a11y-aria-role/warnings.json b/test/validator/samples/a11y-aria-role/warnings.json index 152a27daa8..22fb2d52b6 100644 --- a/test/validator/samples/a11y-aria-role/warnings.json +++ b/test/validator/samples/a11y-aria-role/warnings.json @@ -1,9 +1,16 @@ [ { + "code": "a11y-unknown-role", "message": "A11y: Unknown role 'toooltip' (did you mean 'tooltip'?)", - "loc": { + "start": { "line": 1, - "column": 5 + "column": 5, + "character": 5 + }, + "end": { + "line": 1, + "column": 20, + "character": 20 }, "pos": 5 } diff --git a/test/validator/samples/a11y-aria-unsupported-element/warnings.json b/test/validator/samples/a11y-aria-unsupported-element/warnings.json index 5e2c358271..4597e3e868 100644 --- a/test/validator/samples/a11y-aria-unsupported-element/warnings.json +++ b/test/validator/samples/a11y-aria-unsupported-element/warnings.json @@ -1,18 +1,32 @@ [ { + "code": "a11y-aria-attributes", "message": "A11y: should not have aria-* attributes", - "loc": { + "start": { "line": 1, - "column": 6 + "column": 6, + "character": 6 + }, + "end": { + "line": 1, + "column": 25, + "character": 25 }, "pos": 6 }, { + "code": "a11y-misplaced-role", "message": "A11y: should not have role attribute", - "loc": { + "start": { + "line": 2, + "column": 6, + "character": 33 + }, + "end": { "line": 2, - "column": 6 + "column": 20, + "character": 47 }, "pos": 33 } diff --git a/test/validator/samples/a11y-figcaption-wrong-place/warnings.json b/test/validator/samples/a11y-figcaption-wrong-place/warnings.json index 0e5e1a1976..ee5a89d3ff 100644 --- a/test/validator/samples/a11y-figcaption-wrong-place/warnings.json +++ b/test/validator/samples/a11y-figcaption-wrong-place/warnings.json @@ -1,17 +1,31 @@ [ { + "code": "a11y-structure", "message": "A11y:
must be first or last child of
", - "loc": { + "start": { "line": 4, - "column": 1 + "column": 1, + "character": 57 + }, + "end": { + "line": 6, + "column": 14, + "character": 115 }, "pos": 57 }, { + "code": "a11y-structure", "message": "A11y:
must be an immediate child of
", - "loc": { + "start": { "line": 15, - "column": 2 + "column": 2, + "character": 252 + }, + "end": { + "line": 17, + "column": 15, + "character": 328 }, "pos": 252 } diff --git a/test/validator/samples/a11y-heading-has-content/warnings.json b/test/validator/samples/a11y-heading-has-content/warnings.json index 15bb3a162a..9cb77adc02 100644 --- a/test/validator/samples/a11y-heading-has-content/warnings.json +++ b/test/validator/samples/a11y-heading-has-content/warnings.json @@ -1,18 +1,32 @@ [ { + "code": "a11y-missing-content", "message": "A11y:

element should have child content", - "loc": { + "start": { "line": 1, - "column": 0 + "column": 0, + "character": 0 + }, + "end": { + "line": 1, + "column": 9, + "character": 9 }, "pos": 0 }, { + "code": "a11y-hidden", "message": "A11y:

element should not be hidden", - "loc": { + "start": { + "line": 2, + "column": 4, + "character": 14 + }, + "end": { "line": 2, - "column": 4 + "column": 15, + "character": 25 }, "pos": 14 } diff --git a/test/validator/samples/a11y-html-has-lang/input.html b/test/validator/samples/a11y-html-has-lang/input.html index 41f2fc6276..0fa84d90a6 100644 --- a/test/validator/samples/a11y-html-has-lang/input.html +++ b/test/validator/samples/a11y-html-has-lang/input.html @@ -1,5 +1,5 @@ - + diff --git a/test/validator/samples/a11y-html-has-lang/warnings.json b/test/validator/samples/a11y-html-has-lang/warnings.json index f64964bf6a..963a2570db 100644 --- a/test/validator/samples/a11y-html-has-lang/warnings.json +++ b/test/validator/samples/a11y-html-has-lang/warnings.json @@ -1,10 +1,17 @@ [ { - "loc": { + "code": "a11y-missing-attribute", + "message": "A11y: element should have a lang attribute", + "start": { "column": 0, - "line": 5 + "line": 5, + "character": 82 }, - "message": "A11y: element should have a lang attribute", - "pos": 84 + "end": { + "line": 5, + "column": 13, + "character": 95 + }, + "pos": 82 } ] diff --git a/test/validator/samples/a11y-iframe-has-title/input.html b/test/validator/samples/a11y-iframe-has-title/input.html index 2b5060b80e..5e07ca5a08 100644 --- a/test/validator/samples/a11y-iframe-has-title/input.html +++ b/test/validator/samples/a11y-iframe-has-title/input.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/test/validator/samples/a11y-iframe-has-title/warnings.json b/test/validator/samples/a11y-iframe-has-title/warnings.json index 8f69f14415..d71bae1dae 100644 --- a/test/validator/samples/a11y-iframe-has-title/warnings.json +++ b/test/validator/samples/a11y-iframe-has-title/warnings.json @@ -1,9 +1,16 @@ [ { + "code": "a11y-missing-attribute", "message": "A11y: