diff --git a/internal.js b/internal.js new file mode 100644 index 0000000000..cdb0307058 --- /dev/null +++ b/internal.js @@ -0,0 +1,615 @@ +function append(target, node) { + target.appendChild(node); +} + +function insert(target, node, anchor) { + target.insertBefore(node, anchor); +} + +function detachNode(node) { + node.parentNode.removeChild(node); +} + +function detachBetween(before, after) { + while (before.nextSibling && before.nextSibling !== after) { + before.parentNode.removeChild(before.nextSibling); + } +} + +function detachBefore(after) { + while (after.previousSibling) { + after.parentNode.removeChild(after.previousSibling); + } +} + +function detachAfter(before) { + while (before.nextSibling) { + before.parentNode.removeChild(before.nextSibling); + } +} + +function reinsertBetween(before, after, target) { + while (before.nextSibling && before.nextSibling !== after) { + target.appendChild(before.parentNode.removeChild(before.nextSibling)); + } +} + +function reinsertChildren(parent, target) { + while (parent.firstChild) target.appendChild(parent.firstChild); +} + +function reinsertAfter(before, target) { + while (before.nextSibling) target.appendChild(before.nextSibling); +} + +function reinsertBefore(after, target) { + var parent = after.parentNode; + while (parent.firstChild !== after) target.appendChild(parent.firstChild); +} + +function destroyEach(iterations, detach) { + for (var i = 0; i < iterations.length; i += 1) { + if (iterations[i]) iterations[i].d(detach); + } +} + +function createFragment() { + return document.createDocumentFragment(); +} + +function createElement(name) { + return document.createElement(name); +} + +function createSvgElement(name) { + return document.createElementNS('http://www.w3.org/2000/svg', name); +} + +function createText(data) { + return document.createTextNode(data); +} + +function createComment() { + return document.createComment(''); +} + +function addListener(node, event, handler, options) { + node.addEventListener(event, handler, options); +} + +function removeListener(node, event, handler, options) { + node.removeEventListener(event, handler, options); +} + +function setAttribute(node, attribute, value) { + if (value == null) node.removeAttribute(attribute); + else node.setAttribute(attribute, value); +} + +function setAttributes(node, attributes) { + for (var key in attributes) { + if (key === 'style') { + node.style.cssText = attributes[key]; + } else if (key in node) { + node[key] = attributes[key]; + } else { + setAttribute(node, key, attributes[key]); + } + } +} + +function setCustomElementData(node, prop, value) { + if (prop in node) { + node[prop] = value; + } else if (value) { + setAttribute(node, prop, value); + } else { + node.removeAttribute(prop); + } +} + +function setXlinkAttribute(node, attribute, value) { + node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value); +} + +function getBindingGroupValue(group) { + var value = []; + for (var i = 0; i < group.length; i += 1) { + if (group[i].checked) value.push(group[i].__value); + } + return value; +} + +function toNumber(value) { + return value === '' ? undefined : +value; +} + +function timeRangesToArray(ranges) { + var array = []; + for (var i = 0; i < ranges.length; i += 1) { + array.push({ start: ranges.start(i), end: ranges.end(i) }); + } + return array; +} + +function children (element) { + return Array.from(element.childNodes); +} + +function claimElement (nodes, name, attributes, svg) { + for (var i = 0; i < nodes.length; i += 1) { + var node = nodes[i]; + if (node.nodeName === name) { + for (var j = 0; j < node.attributes.length; j += 1) { + var attribute = node.attributes[j]; + if (!attributes[attribute.name]) node.removeAttribute(attribute.name); + } + return nodes.splice(i, 1)[0]; // TODO strip unwanted attributes + } + } + + return svg ? createSvgElement(name) : createElement(name); +} + +function claimText (nodes, data) { + for (var i = 0; i < nodes.length; i += 1) { + var node = nodes[i]; + if (node.nodeType === 3) { + node.data = data; + return nodes.splice(i, 1)[0]; + } + } + + return createText(data); +} + +function setData(text, data) { + text.data = '' + data; +} + +function setInputType(input, type) { + try { + input.type = type; + } catch (e) {} +} + +function setStyle(node, key, value) { + node.style.setProperty(key, value); +} + +function selectOption(select, value) { + for (var i = 0; i < select.options.length; i += 1) { + var option = select.options[i]; + + if (option.__value === value) { + option.selected = true; + return; + } + } +} + +function selectOptions(select, value) { + for (var i = 0; i < select.options.length; i += 1) { + var option = select.options[i]; + option.selected = ~value.indexOf(option.__value); + } +} + +function selectValue(select) { + var selectedOption = select.querySelector(':checked') || select.options[0]; + return selectedOption && selectedOption.__value; +} + +function selectMultipleValue(select) { + return [].map.call(select.querySelectorAll(':checked'), function(option) { + return option.__value; + }); +} + +function addResizeListener(element, fn) { + if (getComputedStyle(element).position === 'static') { + element.style.position = 'relative'; + } + + const object = document.createElement('object'); + object.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;'); + object.type = 'text/html'; + + let win; + + object.onload = () => { + win = object.contentDocument.defaultView; + win.addEventListener('resize', fn); + }; + + if (/Trident/.test(navigator.userAgent)) { + element.appendChild(object); + object.data = 'about:blank'; + } else { + object.data = 'about:blank'; + element.appendChild(object); + } + + return { + cancel: () => { + win && win.removeEventListener && win.removeEventListener('resize', fn); + element.removeChild(object); + } + }; +} + +function toggleClass(element, name, toggle) { + element.classList.toggle(name, !!toggle); +} + +let update_scheduled = false; + +const dirty_components = []; + +function schedule_update(component) { + dirty_components.push(component); + if (!update_scheduled) { + update_scheduled = true; + queueMicrotask(flush); + } +} + +function flush() { + while (dirty_components.length) { + dirty_components.pop().__update(); + } + + update_scheduled = false; +} + +function queueMicrotask(callback) { + Promise.resolve().then(callback); +} + +function noop() {} + +function assign(tar, src) { + for (var k in src) tar[k] = src[k]; + return tar; +} + +function assignTrue(tar, src) { + for (var k in src) tar[k] = 1; + return tar; +} + +function isPromise(value) { + return value && typeof value.then === 'function'; +} + +function callAfter(fn, i) { + if (i === 0) fn(); + return () => { + if (!--i) fn(); + }; +} + +function addLoc(element, file, line, column, char) { + element.__svelte_meta = { + loc: { file, line, column, char } + }; +} + +function exclude(src, prop) { + const tar = {}; + for (const k in src) k === prop || (tar[k] = src[k]); + return tar; +} + +function run(fn) { + fn(); +} + +function linear(t) { + return t; +} + +function generateRule({ a, b, delta, duration }, ease, fn) { + const step = 16.666 / duration; + let keyframes = '{\n'; + + for (let p = 0; p <= 1; p += step) { + const t = a + delta * ease(p); + keyframes += p * 100 + `%{${fn(t, 1 - t)}}\n`; + } + + return keyframes + `100% {${fn(b, 1 - b)}}\n}`; +} + +// https://github.com/darkskyapp/string-hash/blob/master/index.js +function hash(str) { + let hash = 5381; + let i = str.length; + + while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i); + return hash >>> 0; +} + +function wrapTransition(component, node, fn, params, intro) { + let obj = fn.call(component, node, params); + let duration; + let ease; + let cssText; + + let initialised = false; + + return { + t: intro ? 0 : 1, + running: false, + program: null, + pending: null, + + run(b, callback) { + if (typeof obj === 'function') { + transitionManager.wait().then(() => { + obj = obj(); + this._run(b, callback); + }); + } else { + this._run(b, callback); + } + }, + + _run(b, callback) { + duration = obj.duration || 300; + ease = obj.easing || linear; + + const program = { + start: window.performance.now() + (obj.delay || 0), + b, + callback: callback || noop + }; + + if (intro && !initialised) { + if (obj.css && obj.delay) { + cssText = node.style.cssText; + node.style.cssText += obj.css(0, 1); + } + + if (obj.tick) obj.tick(0, 1); + initialised = true; + } + + if (!b) { + program.group = outros.current; + outros.current.remaining += 1; + } + + if (obj.delay) { + this.pending = program; + } else { + this.start(program); + } + + if (!this.running) { + this.running = true; + transitionManager.add(this); + } + }, + + start(program) { + component.fire(`${program.b ? 'intro' : 'outro'}.start`, { node }); + + program.a = this.t; + program.delta = program.b - program.a; + program.duration = duration * Math.abs(program.b - program.a); + program.end = program.start + program.duration; + + if (obj.css) { + if (obj.delay) node.style.cssText = cssText; + + const rule = generateRule(program, ease, obj.css); + transitionManager.addRule(rule, program.name = '__svelte_' + hash(rule)); + + node.style.animation = (node.style.animation || '') + .split(', ') + .filter(anim => anim && (program.delta < 0 || !/__svelte/.test(anim))) + .concat(`${program.name} ${program.duration}ms linear 1 forwards`) + .join(', '); + } + + this.program = program; + this.pending = null; + }, + + update(now) { + const program = this.program; + if (!program) return; + + const p = now - program.start; + this.t = program.a + program.delta * ease(p / program.duration); + if (obj.tick) obj.tick(this.t, 1 - this.t); + }, + + done() { + const program = this.program; + this.t = program.b; + + if (obj.tick) obj.tick(this.t, 1 - this.t); + + component.fire(`${program.b ? 'intro' : 'outro'}.end`, { node }); + + if (!program.b && !program.invalidated) { + program.group.callbacks.push(() => { + program.callback(); + if (obj.css) transitionManager.deleteRule(node, program.name); + }); + + if (--program.group.remaining === 0) { + program.group.callbacks.forEach(run); + } + } else { + if (obj.css) transitionManager.deleteRule(node, program.name); + } + + this.running = !!this.pending; + }, + + abort(reset) { + if (this.program) { + if (reset && obj.tick) obj.tick(1, 0); + if (obj.css) transitionManager.deleteRule(node, this.program.name); + this.program = this.pending = null; + this.running = false; + } + }, + + invalidate() { + if (this.program) { + this.program.invalidated = true; + } + } + }; +} + +let outros = {}; + +function groupOutros() { + outros.current = { + remaining: 0, + callbacks: [] + }; +} + +var transitionManager = { + running: false, + transitions: [], + bound: null, + stylesheet: null, + activeRules: {}, + promise: null, + + add(transition) { + this.transitions.push(transition); + + if (!this.running) { + this.running = true; + requestAnimationFrame(this.bound || (this.bound = this.next.bind(this))); + } + }, + + addRule(rule, name) { + if (!this.stylesheet) { + const style = createElement('style'); + document.head.appendChild(style); + transitionManager.stylesheet = style.sheet; + } + + if (!this.activeRules[name]) { + this.activeRules[name] = true; + this.stylesheet.insertRule(`@keyframes ${name} ${rule}`, this.stylesheet.cssRules.length); + } + }, + + next() { + this.running = false; + + const now = window.performance.now(); + let i = this.transitions.length; + + while (i--) { + const transition = this.transitions[i]; + + if (transition.program && now >= transition.program.end) { + transition.done(); + } + + if (transition.pending && now >= transition.pending.start) { + transition.start(transition.pending); + } + + if (transition.running) { + transition.update(now); + this.running = true; + } else if (!transition.pending) { + this.transitions.splice(i, 1); + } + } + + if (this.running) { + requestAnimationFrame(this.bound); + } else if (this.stylesheet) { + let i = this.stylesheet.cssRules.length; + while (i--) this.stylesheet.deleteRule(i); + this.activeRules = {}; + } + }, + + deleteRule(node, name) { + node.style.animation = node.style.animation + .split(', ') + .filter(anim => anim && anim.indexOf(name) === -1) + .join(', '); + }, + + wait() { + if (!transitionManager.promise) { + transitionManager.promise = Promise.resolve(); + transitionManager.promise.then(() => { + transitionManager.promise = null; + }); + } + + return transitionManager.promise; + } +}; + +class SvelteComponent { + constructor(options) { + this.__get_state = this.__init( + fn => this.__inject_props = fn + ); + + this.__dirty = null; + + if (options.props) { + this.__inject_props(options.props); + } + + if (options.target) { + this.__mount(options.target); + } + } + + $on(eventName, callback) { + + } + + $destroy() { + this.__destroy(true); + } + + __make_dirty() { + if (this.__dirty) return; + this.__dirty = {}; + schedule_update(this); + } + + __mount(target, anchor) { + this.__fragment = this.__create_fragment(this.__get_state()); + this.__fragment.c(); + this.__fragment.m(target, anchor); + } + + __set(key, value) { + this.__inject_props({ [key]: value }); + this.__make_dirty(); + this.__dirty[key] = true; + } + + __update() { + this.__fragment.p(this.__dirty, this.__get_state()); + this.__dirty = null; + } + + __destroy(detach) { + this.__fragment.d(detach); + } +} + +export { append, insert, detachNode, detachBetween, detachBefore, detachAfter, reinsertBetween, reinsertChildren, reinsertAfter, reinsertBefore, destroyEach, createFragment, createElement, createSvgElement, createText, createComment, addListener, removeListener, setAttribute, setAttributes, setCustomElementData, setXlinkAttribute, getBindingGroupValue, toNumber, timeRangesToArray, children, claimElement, claimText, setData, setInputType, setStyle, selectOption, selectOptions, selectValue, selectMultipleValue, addResizeListener, toggleClass, schedule_update, flush, linear, generateRule, hash, wrapTransition, outros, groupOutros, transitionManager, noop, assign, assignTrue, isPromise, callAfter, addLoc, exclude, run, SvelteComponent }; diff --git a/mocha.opts b/mocha.opts index 427b029758..af6b17a845 100644 --- a/mocha.opts +++ b/mocha.opts @@ -1 +1,2 @@ +--bail test/test.js \ No newline at end of file diff --git a/rollup.config.js b/rollup.config.js index b83f70ed94..898abd0889 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -20,7 +20,7 @@ export default [ json(), typescript({ include: 'src/**', - exclude: 'src/shared/**', + exclude: 'src/internal/**', typescript: require('typescript') }) ], @@ -40,7 +40,7 @@ export default [ commonjs(), buble({ include: 'src/**', - exclude: 'src/shared/**', + exclude: 'src/internal/**', target: { node: 4 } @@ -79,11 +79,11 @@ export default [ experimentalCodeSplitting: true }, - /* shared.js */ + /* internal.js */ { - input: 'src/shared/index.js', + input: 'src/internal/index.js', output: { - file: 'shared.js', + file: 'internal.js', format: 'es' } } diff --git a/src/compile/Component.ts b/src/compile/Component.ts index cf673bf62f..45db4e0e93 100644 --- a/src/compile/Component.ts +++ b/src/compile/Component.ts @@ -10,7 +10,7 @@ import namespaces from '../utils/namespaces'; import { removeNode } from '../utils/removeNode'; import nodeToString from '../utils/nodeToString'; import wrapModule from './wrapModule'; -import annotateWithScopes from '../utils/annotateWithScopes'; +import { createScopes } from '../utils/annotateWithScopes'; import getName from '../utils/getName'; import Stylesheet from './css/Stylesheet'; import { test } from '../config'; @@ -67,7 +67,7 @@ function getIndentExclusionRanges(node: Node) { return ranges; } -function removeIndentation( +function increaseIndentation( code: MagicString, start: number, end: number, @@ -75,13 +75,16 @@ function removeIndentation( ranges: Node[] ) { const str = code.original.slice(start, end); - const pattern = new RegExp(`^${indentationLevel}`, 'gm'); - let match; + const lines = str.split('\n'); - while (match = pattern.exec(str)) { - // TODO bail if we're inside an exclusion range - code.remove(start + match.index, start + match.index + indentationLevel.length); - } + let c = start; + lines.forEach(line => { + if (line) { + code.prependRight(c, '\t\t\t'); // TODO detect indentation + } + + c += line.length + 1; + }); } // We need to tell estree-walker that it should always @@ -131,7 +134,7 @@ export default class Component { actions: Set; }; - declarations: Declaration[]; + declarations: string[]; refCallees: Node[]; @@ -342,85 +345,12 @@ export default class Component { return sigil.slice(1) + name; }); - let importedHelpers; - - if (options.shared) { - if (format !== 'es' && format !== 'cjs') { - throw new Error(`Components with shared helpers must be compiled with \`format: 'es'\` or \`format: 'cjs'\``); - } - - importedHelpers = Array.from(helpers).sort().map(name => { - const alias = this.alias(name); - return { name, alias }; - }); - } else { - let inlineHelpers = ''; - - const component = this; - - importedHelpers = []; - - helpers.forEach(name => { - const str = shared[name]; - const code = new MagicString(str); - const expression = parseExpressionAt(str, 0); - - let { scope } = annotateWithScopes(expression); - - walk(expression, { - enter(node: Node, parent: Node) { - if (node._scope) scope = node._scope; - - if ( - node.type === 'Identifier' && - isReference(node, parent) && - !scope.has(node.name) - ) { - if (node.name in shared) { - // this helper function depends on another one - const dependency = node.name; - helpers.add(dependency); - - const alias = component.alias(dependency); - if (alias !== node.name) { - code.overwrite(node.start, node.end, alias); - } - } - } - }, - - leave(node: Node) { - if (node._scope) scope = scope.parent; - }, - }); - - if (name === 'transitionManager' || name === 'outros') { - // special case - const global = name === 'outros' - ? `_svelteOutros` - : `_svelteTransitionManager`; - - inlineHelpers += `\n\nvar ${this.alias(name)} = window.${global} || (window.${global} = ${code});\n\n`; - } else if (name === 'escaped' || name === 'missingComponent' || name === 'invalidAttributeNameCharacter') { - // vars are an awkward special case... would be nice to avoid this - const alias = this.alias(name); - inlineHelpers += `\n\nconst ${alias} = ${code};` - } else { - const alias = this.alias(expression.id.name); - if (alias !== expression.id.name) { - code.overwrite(expression.id.start, expression.id.end, alias); - } - - inlineHelpers += `\n\n${code}`; - } - }); - - result += inlineHelpers; - } + const importedHelpers = Array.from(helpers).concat('SvelteComponent').sort().map(name => { + const alias = this.alias(name); + return { name, alias }; + }); - const sharedPath = options.shared === true - ? 'svelte/shared.js' - : options.shared || ''; + const sharedPath = options.shared || 'svelte/internal.js'; const module = wrapModule(result, format, name, options, banner, sharedPath, importedHelpers, this.imports, this.shorthandImports, this.source); @@ -571,306 +501,6 @@ export default class Component { }); } - processDefaultExport(node, indentExclusionRanges) { - const { templateProperties, source, code } = this; - - if (node.declaration.type !== 'ObjectExpression') { - this.error(node.declaration, { - code: `invalid-default-export`, - message: `Default export must be an object literal` - }); - } - - checkForComputedKeys(this, node.declaration.properties); - checkForDupes(this, node.declaration.properties); - - const props = this.properties; - - node.declaration.properties.forEach((prop: Node) => { - props.set(getName(prop.key), prop); - }); - - const validPropList = Object.keys(propValidators); - - // ensure all exported props are valid - node.declaration.properties.forEach((prop: Node) => { - const name = getName(prop.key); - const propValidator = propValidators[name]; - - if (propValidator) { - propValidator(this, prop); - } else { - const match = fuzzymatch(name, validPropList); - if (match) { - this.error(prop, { - code: `unexpected-property`, - message: `Unexpected property '${name}' (did you mean '${match}'?)` - }); - } else if (/FunctionExpression/.test(prop.value.type)) { - this.error(prop, { - code: `unexpected-property`, - message: `Unexpected property '${name}' (did you mean to include it in 'methods'?)` - }); - } else { - this.error(prop, { - code: `unexpected-property`, - message: `Unexpected property '${name}'` - }); - } - } - }); - - if (props.has('namespace')) { - const ns = nodeToString(props.get('namespace').value); - this.namespace = namespaces[ns] || ns; - } - - node.declaration.properties.forEach((prop: Node) => { - templateProperties[getName(prop.key)] = prop; - }); - - ['helpers', 'events', 'components', 'transitions', 'actions', 'animations'].forEach(key => { - if (templateProperties[key]) { - templateProperties[key].value.properties.forEach((prop: Node) => { - this[key].add(getName(prop.key)); - }); - } - }); - - const addArrowFunctionExpression = (type: string, name: string, node: Node) => { - const { body, params, async } = node; - const fnKeyword = async ? 'async function' : 'function'; - - const paramString = params.length ? - `[✂${params[0].start}-${params[params.length - 1].end}✂]` : - ``; - - const block = body.type === 'BlockStatement' - ? deindent` - ${fnKeyword} ${name}(${paramString}) [✂${body.start}-${body.end}✂] - ` - : deindent` - ${fnKeyword} ${name}(${paramString}) { - return [✂${body.start}-${body.end}✂]; - } - `; - - this.declarations.push({ type, name, block, node }); - }; - - const addFunctionExpression = (type: string, name: string, node: Node) => { - const { async } = node; - const fnKeyword = async ? 'async function' : 'function'; - - let c = node.start; - while (this.source[c] !== '(') c += 1; - - const block = deindent` - ${fnKeyword} ${name}[✂${c}-${node.end}✂]; - `; - - this.declarations.push({ type, name, block, node }); - }; - - const addValue = (type: string, name: string, node: Node) => { - const block = deindent` - var ${name} = [✂${node.start}-${node.end}✂]; - `; - - this.declarations.push({ type, name, block, node }); - }; - - const addDeclaration = ( - type: string, - key: string, - node: Node, - allowShorthandImport?: boolean, - disambiguator?: string, - conflicts?: Record - ) => { - const qualified = disambiguator ? `${disambiguator}-${key}` : key; - - if (node.type === 'Identifier' && node.name === key) { - this.templateVars.set(qualified, key); - return; - } - - let deconflicted = key; - if (conflicts) while (deconflicted in conflicts) deconflicted += '_' - - let name = this.getUniqueName(deconflicted); - this.templateVars.set(qualified, name); - - if (allowShorthandImport && node.type === 'Literal' && typeof node.value === 'string') { - this.shorthandImports.push({ name, source: node.value }); - return; - } - - // deindent - const indentationLevel = getIndentationLevel(source, node.start); - if (indentationLevel) { - removeIndentation(code, node.start, node.end, indentationLevel, indentExclusionRanges); - } - - if (node.type === 'ArrowFunctionExpression') { - addArrowFunctionExpression(type, name, node); - } else if (node.type === 'FunctionExpression') { - addFunctionExpression(type, name, node); - } else { - addValue(type, name, node); - } - }; - - if (templateProperties.components) { - templateProperties.components.value.properties.forEach((property: Node) => { - addDeclaration('components', getName(property.key), property.value, true, 'components'); - }); - } - - if (templateProperties.computed) { - const dependencies = new Map(); - - const fullStateComputations = []; - - templateProperties.computed.value.properties.forEach((prop: Node) => { - const key = getName(prop.key); - const value = prop.value; - - addDeclaration('computed', key, value, false, 'computed', { - state: true, - changed: true - }); - - const param = value.params[0]; - - const hasRestParam = ( - param.properties && - param.properties.some(prop => prop.type === 'RestElement') - ); - - if (param.type !== 'ObjectPattern' || hasRestParam) { - fullStateComputations.push({ key, deps: null, hasRestParam }); - } else { - const deps = param.properties.map(prop => prop.key.name); - - deps.forEach(dep => { - this.expectedProperties.add(dep); - }); - dependencies.set(key, deps); - } - }); - - const visited = new Set(); - - const visit = (key: string) => { - if (!dependencies.has(key)) return; // not a computation - - if (visited.has(key)) return; - visited.add(key); - - const deps = dependencies.get(key); - deps.forEach(visit); - - this.computations.push({ key, deps, hasRestParam: false }); - - const prop = templateProperties.computed.value.properties.find((prop: Node) => getName(prop.key) === key); - }; - - templateProperties.computed.value.properties.forEach((prop: Node) => - visit(getName(prop.key)) - ); - - if (fullStateComputations.length > 0) { - this.computations.push(...fullStateComputations); - } - } - - if (templateProperties.data) { - addDeclaration('data', 'data', templateProperties.data.value); - } - - if (templateProperties.events) { - templateProperties.events.value.properties.forEach((property: Node) => { - addDeclaration('events', getName(property.key), property.value, false, 'events'); - }); - } - - if (templateProperties.helpers) { - templateProperties.helpers.value.properties.forEach((property: Node) => { - addDeclaration('helpers', getName(property.key), property.value, false, 'helpers'); - }); - } - - if (templateProperties.methods) { - addDeclaration('methods', 'methods', templateProperties.methods.value); - - templateProperties.methods.value.properties.forEach(property => { - this.methods.add(getName(property.key)); - }); - } - - if (templateProperties.namespace) { - const ns = nodeToString(templateProperties.namespace.value); - this.namespace = namespaces[ns] || ns; - } - - if (templateProperties.oncreate) { - addDeclaration('oncreate', 'oncreate', templateProperties.oncreate.value); - } - - if (templateProperties.ondestroy) { - addDeclaration('ondestroy', 'ondestroy', templateProperties.ondestroy.value); - } - - if (templateProperties.onstate) { - addDeclaration('onstate', 'onstate', templateProperties.onstate.value); - } - - if (templateProperties.onupdate) { - addDeclaration('onupdate', 'onupdate', templateProperties.onupdate.value); - } - - if (templateProperties.preload) { - addDeclaration('preload', 'preload', templateProperties.preload.value); - } - - if (templateProperties.props) { - this.props = templateProperties.props.value.elements.map((element: Node) => nodeToString(element)); - } - - if (templateProperties.setup) { - addDeclaration('setup', 'setup', templateProperties.setup.value); - } - - if (templateProperties.store) { - addDeclaration('store', 'store', templateProperties.store.value); - } - - if (templateProperties.tag) { - this.tag = nodeToString(templateProperties.tag.value); - } - - if (templateProperties.transitions) { - templateProperties.transitions.value.properties.forEach((property: Node) => { - addDeclaration('transitions', getName(property.key), property.value, false, 'transitions'); - }); - } - - if (templateProperties.animations) { - templateProperties.animations.value.properties.forEach((property: Node) => { - addDeclaration('animations', getName(property.key), property.value, false, 'animations'); - }); - } - - if (templateProperties.actions) { - templateProperties.actions.value.properties.forEach((property: Node) => { - addDeclaration('actions', getName(property.key), property.value, false, 'actions'); - }); - } - - this.defaultExport = node; - } - walkJs() { const { js } = this.ast; if (!js) return; @@ -882,10 +512,11 @@ export default class Component { const indentationLevel = getIndentationLevel(source, js.content.body[0].start); const indentExclusionRanges = getIndentExclusionRanges(js.content); - const { scope, globals } = annotateWithScopes(js.content); + const { scope, globals } = createScopes(js.content); scope.declarations.forEach(name => { this.userVars.add(name); + this.declarations.push(name); }); globals.forEach(name => { @@ -895,19 +526,15 @@ export default class Component { const body = js.content.body.slice(); // slice, because we're going to be mutating the original body.forEach(node => { - // check there are no named exports - if (node.type === 'ExportNamedDeclaration') { - this.error(node, { - code: `named-export`, - message: `A component can only have a default export` - }); - } - if (node.type === 'ExportDefaultDeclaration') { - this.processDefaultExport(node, indentExclusionRanges); + this.error(node, { + code: `default-export`, + message: `A component cannot have a default export` + }) } // imports need to be hoisted out of the IIFE + // TODO hoist other stuff where possible else if (node.type === 'ImportDeclaration') { removeNode(code, js.content, node); imports.push(node); @@ -918,15 +545,6 @@ export default class Component { } }); - if (indentationLevel) { - if (this.defaultExport) { - removeIndentation(code, js.content.start, this.defaultExport.start, indentationLevel, indentExclusionRanges); - removeIndentation(code, this.defaultExport.end, js.content.end, indentationLevel, indentExclusionRanges); - } else { - removeIndentation(code, js.content.start, js.content.end, indentationLevel, indentExclusionRanges); - } - } - let a = js.content.start; while (/\s/.test(source[a])) a += 1; diff --git a/src/compile/nodes/Action.ts b/src/compile/nodes/Action.ts index f9460d6c8b..fcf96b46c2 100644 --- a/src/compile/nodes/Action.ts +++ b/src/compile/nodes/Action.ts @@ -13,13 +13,6 @@ export default class Action extends Node { component.used.actions.add(this.name); - if (!component.actions.has(this.name)) { - component.error(this, { - code: `missing-action`, - message: `Missing action '${this.name}'` - }); - } - this.expression = info.expression ? new Expression(component, this, scope, info.expression) : null; diff --git a/src/compile/render-dom/Block.ts b/src/compile/render-dom/Block.ts index c7ca63605c..40c8742abc 100644 --- a/src/compile/render-dom/Block.ts +++ b/src/compile/render-dom/Block.ts @@ -212,7 +212,7 @@ export default class Block { return new Block(Object.assign({}, this, { key: null }, options, { parent: this })); } - toString() { + getContents(localKey?: string) { const { dev } = this.renderer.options; if (this.hasIntroMethod || this.hasOutroMethod) { @@ -233,9 +233,7 @@ export default class Block { const properties = new CodeBuilder(); - let localKey; - if (this.key) { - localKey = this.getUniqueName('key'); + if (localKey) { properties.addBlock(`key: ${localKey},`); } @@ -359,22 +357,32 @@ export default class Block { `); } + return deindent` + ${this.variables.size > 0 && + `var ${Array.from(this.variables.keys()) + .map(key => { + const init = this.variables.get(key); + return init !== undefined ? `${key} = ${init}` : key; + }) + .join(', ')};`} + + ${!this.builders.init.isEmpty() && this.builders.init} + + return { + ${properties} + }; + `.replace(/(#+)(\w*)/g, (match: string, sigil: string, name: string) => { + return sigil === '#' ? this.alias(name) : sigil.slice(1) + name; + }); + } + + toString() { + const localKey = this.key && this.getUniqueName('key'); + return deindent` ${this.comment && `// ${escape(this.comment)}`} function ${this.name}(#component${this.key ? `, ${localKey}` : ''}, ctx) { - ${this.variables.size > 0 && - `var ${Array.from(this.variables.keys()) - .map(key => { - const init = this.variables.get(key); - return init !== undefined ? `${key} = ${init}` : key; - }) - .join(', ')};`} - - ${!this.builders.init.isEmpty() && this.builders.init} - - return { - ${properties} - }; + ${this.getContents(localKey)} } `.replace(/(#+)(\w*)/g, (match: string, sigil: string, name: string) => { return sigil === '#' ? this.alias(name) : sigil.slice(1) + name; diff --git a/src/compile/render-dom/Renderer.ts b/src/compile/render-dom/Renderer.ts index 7e8632f341..0a79549e45 100644 --- a/src/compile/render-dom/Renderer.ts +++ b/src/compile/render-dom/Renderer.ts @@ -43,7 +43,7 @@ export default class Renderer { // main block this.block = new Block({ renderer: this, - name: '@create_main_fragment', + name: null, key: null, bindings: new Map(), @@ -64,8 +64,6 @@ export default class Renderer { null ); - this.blocks.push(this.block); - this.blocks.forEach(block => { if (typeof block !== 'string') { block.assignVariableNames(); diff --git a/src/compile/render-dom/index.ts b/src/compile/render-dom/index.ts index 039f901731..b92f86a12a 100644 --- a/src/compile/render-dom/index.ts +++ b/src/compile/render-dom/index.ts @@ -30,53 +30,6 @@ export default function dom( if (options.customElement) block.builders.create.addLine(`this.c = @noop;`); const builder = new CodeBuilder(); - const computationBuilder = new CodeBuilder(); - const computationDeps = new Set(); - - if (computations.length) { - computations.forEach(({ key, deps, hasRestParam }) => { - if (renderer.readonly.has(key)) { - // bindings - throw new Error( - `Cannot have a computed value '${key}' that clashes with a read-only property` - ); - } - - renderer.readonly.add(key); - - if (deps) { - deps.forEach(dep => { - computationDeps.add(dep); - }); - - const condition = `${deps.map(dep => `changed.${dep}`).join(' || ')}`; - const statement = `if (this._differs(state.${key}, (state.${key} = %computed-${key}(state)))) changed.${key} = true;`; - - computationBuilder.addConditional(condition, statement); - } else { - // computed property depends on entire state object — - // these must go at the end - computationBuilder.addLine( - `if (this._differs(state.${key}, (state.${key} = %computed-${key}(@exclude(state, "${key}"))))) changed.${key} = true;` - ); - } - }); - } - - if (component.javascript) { - const componentDefinition = new CodeBuilder(); - component.declarations.forEach(declaration => { - componentDefinition.addBlock(declaration.block); - }); - - const js = ( - component.javascript[0] + - componentDefinition + - component.javascript[1] - ); - - builder.addBlock(js); - } if (component.options.dev) { builder.addLine(`const ${renderer.fileVar} = ${JSON.stringify(component.file)};`); @@ -106,139 +59,10 @@ export default function dom( builder.addBlock(block.toString()); }); - const sharedPath: string = options.shared === true - ? 'svelte/shared.js' - : options.shared || ''; - - const proto = sharedPath - ? `@proto` - : deindent` - { - ${['destroy', 'get', 'fire', 'on', 'set', '_set', '_stage', '_mount', '_differs'] - .map(n => `${n}: @${n}`) - .join(',\n')} - }`; - const debugName = `<${component.customElement ? component.tag : name}>`; - // generate initial state object const expectedProperties = Array.from(component.expectedProperties); const globals = expectedProperties.filter(prop => globalWhitelist.has(prop)); - const storeProps = expectedProperties.filter(prop => prop[0] === '$'); - const initialState = []; - - if (globals.length > 0) { - initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`); - } - - if (storeProps.length > 0) { - initialState.push(`this.store._init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`); - } - - if (templateProperties.data) { - initialState.push(`%data()`); - } else if (globals.length === 0 && storeProps.length === 0) { - initialState.push('{}'); - } - - initialState.push(`options.data`); - - const hasInitHooks = !!(templateProperties.oncreate || templateProperties.onstate || templateProperties.onupdate); - - const constructorBody = deindent` - ${options.dev && deindent` - this._debugName = '${debugName}'; - ${!component.customElement && deindent` - if (!options || (!options.target && !options.root)) { - throw new Error("'target' is a required option"); - }`} - ${storeProps.length > 0 && !templateProperties.store && deindent` - if (!options.store) { - throw new Error("${debugName} references store properties, but no store was provided"); - }`} - `} - - @init(this, options); - ${templateProperties.store && `this.store = %store();`} - ${component.refs.size > 0 && `this.refs = {};`} - this._state = ${initialState.reduce((state, piece) => `@assign(${state}, ${piece})`)}; - ${storeProps.length > 0 && `this.store._add(this, [${storeProps.map(prop => `"${prop.slice(1)}"`)}]);`} - ${renderer.metaBindings} - ${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`} - ${options.dev && - Array.from(component.expectedProperties).map(prop => { - if (globalWhitelist.has(prop)) return; - if (computations.find(c => c.key === prop)) return; - - const message = component.components.has(prop) ? - `${debugName} expected to find '${prop}' in \`data\`, but found it in \`components\` instead` : - `${debugName} was created without expected data property '${prop}'`; - - const conditions = [`!('${prop}' in this._state)`]; - if (component.customElement) conditions.push(`!('${prop}' in this.attributes)`); - - return `if (${conditions.join(' && ')}) console.warn("${message}");` - })} - ${renderer.bindingGroups.length && - `this._bindingGroups = [${Array(renderer.bindingGroups.length).fill('[]').join(', ')}];`} - this._intro = ${component.options.skipIntroByDefault ? '!!options.intro' : 'true'}; - - ${templateProperties.onstate && `this._handlers.state = [%onstate];`} - ${templateProperties.onupdate && `this._handlers.update = [%onupdate];`} - - ${(templateProperties.ondestroy || storeProps.length) && ( - `this._handlers.destroy = [${ - [templateProperties.ondestroy && `%ondestroy`, storeProps.length && `@removeFromStore`].filter(Boolean).join(', ') - }];` - )} - - ${renderer.slots.size && `this._slotted = options.slots || {};`} - - ${component.customElement ? - deindent` - this.attachShadow({ mode: 'open' }); - ${css.code && `this.shadowRoot.innerHTML = \`\`;`} - ` : - (component.stylesheet.hasStyles && options.css !== false && - `if (!document.getElementById("${component.stylesheet.id}-style")) @add_css();`) - } - - ${templateProperties.onstate && `%onstate.call(this, { changed: @assignTrue({}, this._state), current: this._state });`} - - this._fragment = @create_main_fragment(this, this._state); - - ${hasInitHooks && deindent` - this.root._oncreate.push(() => { - ${templateProperties.oncreate && `%oncreate.call(this);`} - this.fire("update", { changed: @assignTrue({}, this._state), current: this._state }); - }); - `} - - ${component.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) { - ${component.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); - - ${(component.hasComponents || renderer.hasComplexBindings || hasInitHooks || renderer.hasIntroTransitions) && - `@flush(this);`} - } - `} - - ${component.options.skipIntroByDefault && `this._intro = true;`} - `; if (component.customElement) { const props = component.props || Array.from(component.expectedProperties); @@ -247,7 +71,6 @@ export default function dom( class ${name} extends HTMLElement { constructor(options = {}) { super(); - ${constructorBody} } static get observedAttributes() { @@ -282,57 +105,30 @@ export default function dom( `} } - @assign(${name}.prototype, ${proto}); - ${templateProperties.methods && `@assign(${name}.prototype, %methods);`} - @assign(${name}.prototype, { - _mount(target, anchor) { - target.insertBefore(this, anchor); - } - }); - customElements.define("${component.tag}", ${name}); `); } else { builder.addBlock(deindent` - function ${name}(options) { - ${constructorBody} - } + class ${name} extends @SvelteComponent { + __init() { + ${component.javascript} - @assign(${name}.prototype, ${proto}); - ${templateProperties.methods && `@assign(${name}.prototype, %methods);`} + return () => ({ ${(component.declarations).join(', ')} }); + } + + __create_fragment(ctx) { + ${block.getContents()} + } + } `); } const immutable = templateProperties.immutable ? templateProperties.immutable.value.value : options.immutable; - builder.addBlock(deindent` - ${options.dev && deindent` - ${name}.prototype._checkReadOnly = function _checkReadOnly(newState) { - ${Array.from(renderer.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(); return component.generate(result, options, { banner: `/* ${component.file ? `${component.file} ` : ``}generated by Svelte v${"__VERSION__"} */`, - sharedPath, name, format, }); diff --git a/src/compile/render-dom/wrappers/Element/index.ts b/src/compile/render-dom/wrappers/Element/index.ts index 47c1bc57f3..d951fb445c 100644 --- a/src/compile/render-dom/wrappers/Element/index.ts +++ b/src/compile/render-dom/wrappers/Element/index.ts @@ -829,10 +829,10 @@ export default class ElementWrapper extends Wrapper { ); block.addVariable(name); - const fn = `%actions-${action.name}`; + const fn = `ctx.${action.name}`; block.builders.mount.addLine( - `${name} = ${fn}.call(#component, ${this.var}${snippet ? `, ${snippet}` : ''}) || {};` + `${name} = ${fn}.call(null, ${this.var}${snippet ? `, ${snippet}` : ''}) || {};` ); if (dependencies && dependencies.size > 0) { @@ -842,12 +842,12 @@ export default class ElementWrapper extends Wrapper { block.builders.update.addConditional( conditional, - `${name}.update.call(#component, ${snippet});` + `${name}.update.call(null, ${snippet});` ); } block.builders.destroy.addLine( - `if (${name} && typeof ${name}.destroy === 'function') ${name}.destroy.call(#component);` + `if (${name} && typeof ${name}.destroy === 'function') ${name}.destroy();` ); }); } @@ -860,7 +860,7 @@ export default class ElementWrapper extends Wrapper { snippet = expression.snippet; dependencies = expression.dependencies; } else { - snippet = `ctx${quotePropIfNecessary(name)}`; + snippet = `${quotePropIfNecessary(name)}`; dependencies = new Set([name]); } const updater = `@toggleClass(${this.var}, "${name}", ${snippet});`; diff --git a/src/compile/render-ssr/index.ts b/src/compile/render-ssr/index.ts index 8edc101910..bf6ed3b9ba 100644 --- a/src/compile/render-ssr/index.ts +++ b/src/compile/render-ssr/index.ts @@ -50,29 +50,7 @@ export default function ssr( let js = null; if (component.javascript) { - const componentDefinition = new CodeBuilder(); - - // not all properties are relevant to SSR (e.g. lifecycle hooks) - const relevant = new Set([ - 'data', - 'components', - 'computed', - 'helpers', - 'preload', - 'store' - ]); - - component.declarations.forEach(declaration => { - if (relevant.has(declaration.type)) { - componentDefinition.addBlock(declaration.block); - } - }); - - js = ( - component.javascript[0] + - componentDefinition + - component.javascript[1] - ); + // TODO } const debugName = `<${component.customElement ? component.tag : name}>`; diff --git a/src/internal/SvelteComponent.js b/src/internal/SvelteComponent.js new file mode 100644 index 0000000000..bacbee4d8c --- /dev/null +++ b/src/internal/SvelteComponent.js @@ -0,0 +1,54 @@ +import { schedule_update } from './scheduler'; + +export class SvelteComponent { + constructor(options) { + this.__get_state = this.__init( + fn => this.__inject_props = fn + ); + + this.__dirty = null; + + if (options.props) { + this.__inject_props(options.props); + } + + if (options.target) { + this.__mount(options.target); + } + } + + $on(eventName, callback) { + + } + + $destroy() { + this.__destroy(true); + } + + __make_dirty() { + if (this.__dirty) return; + this.__dirty = {}; + schedule_update(this); + } + + __mount(target, anchor) { + this.__fragment = this.__create_fragment(this.__get_state()); + this.__fragment.c(); + this.__fragment.m(target, anchor); + } + + __set(key, value) { + this.__inject_props({ [key]: value }); + this.__make_dirty(); + this.__dirty[key] = true; + } + + __update() { + this.__fragment.p(this.__dirty, this.__get_state()); + this.__dirty = null; + } + + __destroy(detach) { + this.__fragment.d(detach); + } +} \ No newline at end of file diff --git a/src/internal/dom.js b/src/internal/dom.js new file mode 100644 index 0000000000..1411a7693e --- /dev/null +++ b/src/internal/dom.js @@ -0,0 +1,243 @@ +export function append(target, node) { + target.appendChild(node); +} + +export function insert(target, node, anchor) { + target.insertBefore(node, anchor); +} + +export function detachNode(node) { + node.parentNode.removeChild(node); +} + +export function detachBetween(before, after) { + while (before.nextSibling && before.nextSibling !== after) { + before.parentNode.removeChild(before.nextSibling); + } +} + +export function detachBefore(after) { + while (after.previousSibling) { + after.parentNode.removeChild(after.previousSibling); + } +} + +export function detachAfter(before) { + while (before.nextSibling) { + before.parentNode.removeChild(before.nextSibling); + } +} + +export function reinsertBetween(before, after, target) { + while (before.nextSibling && before.nextSibling !== after) { + target.appendChild(before.parentNode.removeChild(before.nextSibling)); + } +} + +export function reinsertChildren(parent, target) { + while (parent.firstChild) target.appendChild(parent.firstChild); +} + +export function reinsertAfter(before, target) { + while (before.nextSibling) target.appendChild(before.nextSibling); +} + +export function reinsertBefore(after, target) { + var parent = after.parentNode; + while (parent.firstChild !== after) target.appendChild(parent.firstChild); +} + +export function destroyEach(iterations, detach) { + for (var i = 0; i < iterations.length; i += 1) { + if (iterations[i]) iterations[i].d(detach); + } +} + +export function createFragment() { + return document.createDocumentFragment(); +} + +export function createElement(name) { + return document.createElement(name); +} + +export function createSvgElement(name) { + return document.createElementNS('http://www.w3.org/2000/svg', name); +} + +export function createText(data) { + return document.createTextNode(data); +} + +export function createComment() { + return document.createComment(''); +} + +export function addListener(node, event, handler, options) { + node.addEventListener(event, handler, options); +} + +export function removeListener(node, event, handler, options) { + node.removeEventListener(event, handler, options); +} + +export function setAttribute(node, attribute, value) { + if (value == null) node.removeAttribute(attribute); + else node.setAttribute(attribute, value); +} + +export function setAttributes(node, attributes) { + for (var key in attributes) { + if (key === 'style') { + node.style.cssText = attributes[key]; + } else if (key in node) { + node[key] = attributes[key]; + } else { + setAttribute(node, key, attributes[key]); + } + } +} + +export function setCustomElementData(node, prop, value) { + if (prop in node) { + node[prop] = value; + } else if (value) { + setAttribute(node, prop, value); + } else { + node.removeAttribute(prop); + } +} + +export function setXlinkAttribute(node, attribute, value) { + node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value); +} + +export function getBindingGroupValue(group) { + var value = []; + for (var i = 0; i < group.length; i += 1) { + if (group[i].checked) value.push(group[i].__value); + } + return value; +} + +export function toNumber(value) { + return value === '' ? undefined : +value; +} + +export function timeRangesToArray(ranges) { + var array = []; + for (var i = 0; i < ranges.length; i += 1) { + array.push({ start: ranges.start(i), end: ranges.end(i) }); + } + return array; +} + +export function children (element) { + return Array.from(element.childNodes); +} + +export function claimElement (nodes, name, attributes, svg) { + for (var i = 0; i < nodes.length; i += 1) { + var node = nodes[i]; + if (node.nodeName === name) { + for (var j = 0; j < node.attributes.length; j += 1) { + var attribute = node.attributes[j]; + if (!attributes[attribute.name]) node.removeAttribute(attribute.name); + } + return nodes.splice(i, 1)[0]; // TODO strip unwanted attributes + } + } + + return svg ? createSvgElement(name) : createElement(name); +} + +export function claimText (nodes, data) { + for (var i = 0; i < nodes.length; i += 1) { + var node = nodes[i]; + if (node.nodeType === 3) { + node.data = data; + return nodes.splice(i, 1)[0]; + } + } + + return createText(data); +} + +export function setData(text, data) { + text.data = '' + data; +} + +export function setInputType(input, type) { + try { + input.type = type; + } catch (e) {} +} + +export function setStyle(node, key, value) { + node.style.setProperty(key, value); +} + +export function selectOption(select, value) { + for (var i = 0; i < select.options.length; i += 1) { + var option = select.options[i]; + + if (option.__value === value) { + option.selected = true; + return; + } + } +} + +export function selectOptions(select, value) { + for (var i = 0; i < select.options.length; i += 1) { + var option = select.options[i]; + option.selected = ~value.indexOf(option.__value); + } +} + +export function selectValue(select) { + var selectedOption = select.querySelector(':checked') || select.options[0]; + return selectedOption && selectedOption.__value; +} + +export function selectMultipleValue(select) { + return [].map.call(select.querySelectorAll(':checked'), function(option) { + return option.__value; + }); +} + +export function addResizeListener(element, fn) { + if (getComputedStyle(element).position === 'static') { + element.style.position = 'relative'; + } + + const object = document.createElement('object'); + object.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;'); + object.type = 'text/html'; + + let win; + + object.onload = () => { + win = object.contentDocument.defaultView; + win.addEventListener('resize', fn); + }; + + if (/Trident/.test(navigator.userAgent)) { + element.appendChild(object); + object.data = 'about:blank'; + } else { + object.data = 'about:blank'; + element.appendChild(object); + } + + return { + cancel: () => { + win && win.removeEventListener && win.removeEventListener('resize', fn); + element.removeChild(object); + } + }; +} + +export function toggleClass(element, name, toggle) { + element.classList.toggle(name, !!toggle); +} diff --git a/src/internal/index.js b/src/internal/index.js new file mode 100644 index 0000000000..e1dc837d56 --- /dev/null +++ b/src/internal/index.js @@ -0,0 +1,5 @@ +export * from './dom.js'; +export * from './scheduler.js'; +export * from './transitions.js'; +export * from './utils.js'; +export * from './SvelteComponent.js'; \ No newline at end of file diff --git a/src/internal/scheduler.js b/src/internal/scheduler.js new file mode 100644 index 0000000000..c9e028c87f --- /dev/null +++ b/src/internal/scheduler.js @@ -0,0 +1,23 @@ +let update_scheduled = false; + +const dirty_components = []; + +export function schedule_update(component) { + dirty_components.push(component); + if (!update_scheduled) { + update_scheduled = true; + queueMicrotask(flush); + } +} + +export function flush() { + while (dirty_components.length) { + dirty_components.pop().__update(); + } + + update_scheduled = false; +} + +function queueMicrotask(callback) { + Promise.resolve().then(callback); +} \ No newline at end of file diff --git a/src/internal/transitions.js b/src/internal/transitions.js new file mode 100644 index 0000000000..8dfa50b8ff --- /dev/null +++ b/src/internal/transitions.js @@ -0,0 +1,256 @@ +import { createElement } from './dom.js'; +import { noop, run } from './utils.js'; + +export function linear(t) { + return t; +} + +export function generateRule({ a, b, delta, duration }, ease, fn) { + const step = 16.666 / duration; + let keyframes = '{\n'; + + for (let p = 0; p <= 1; p += step) { + const t = a + delta * ease(p); + keyframes += p * 100 + `%{${fn(t, 1 - t)}}\n`; + } + + return keyframes + `100% {${fn(b, 1 - b)}}\n}`; +} + +// https://github.com/darkskyapp/string-hash/blob/master/index.js +export function hash(str) { + let hash = 5381; + let i = str.length; + + while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i); + return hash >>> 0; +} + +export function wrapTransition(component, node, fn, params, intro) { + let obj = fn.call(component, node, params); + let duration; + let ease; + let cssText; + + let initialised = false; + + return { + t: intro ? 0 : 1, + running: false, + program: null, + pending: null, + + run(b, callback) { + if (typeof obj === 'function') { + transitionManager.wait().then(() => { + obj = obj(); + this._run(b, callback); + }); + } else { + this._run(b, callback); + } + }, + + _run(b, callback) { + duration = obj.duration || 300; + ease = obj.easing || linear; + + const program = { + start: window.performance.now() + (obj.delay || 0), + b, + callback: callback || noop + }; + + if (intro && !initialised) { + if (obj.css && obj.delay) { + cssText = node.style.cssText; + node.style.cssText += obj.css(0, 1); + } + + if (obj.tick) obj.tick(0, 1); + initialised = true; + } + + if (!b) { + program.group = outros.current; + outros.current.remaining += 1; + } + + if (obj.delay) { + this.pending = program; + } else { + this.start(program); + } + + if (!this.running) { + this.running = true; + transitionManager.add(this); + } + }, + + start(program) { + component.fire(`${program.b ? 'intro' : 'outro'}.start`, { node }); + + program.a = this.t; + program.delta = program.b - program.a; + program.duration = duration * Math.abs(program.b - program.a); + program.end = program.start + program.duration; + + if (obj.css) { + if (obj.delay) node.style.cssText = cssText; + + const rule = generateRule(program, ease, obj.css); + transitionManager.addRule(rule, program.name = '__svelte_' + hash(rule)); + + node.style.animation = (node.style.animation || '') + .split(', ') + .filter(anim => anim && (program.delta < 0 || !/__svelte/.test(anim))) + .concat(`${program.name} ${program.duration}ms linear 1 forwards`) + .join(', '); + } + + this.program = program; + this.pending = null; + }, + + update(now) { + const program = this.program; + if (!program) return; + + const p = now - program.start; + this.t = program.a + program.delta * ease(p / program.duration); + if (obj.tick) obj.tick(this.t, 1 - this.t); + }, + + done() { + const program = this.program; + this.t = program.b; + + if (obj.tick) obj.tick(this.t, 1 - this.t); + + component.fire(`${program.b ? 'intro' : 'outro'}.end`, { node }); + + if (!program.b && !program.invalidated) { + program.group.callbacks.push(() => { + program.callback(); + if (obj.css) transitionManager.deleteRule(node, program.name); + }); + + if (--program.group.remaining === 0) { + program.group.callbacks.forEach(run); + } + } else { + if (obj.css) transitionManager.deleteRule(node, program.name); + } + + this.running = !!this.pending; + }, + + abort(reset) { + if (this.program) { + if (reset && obj.tick) obj.tick(1, 0); + if (obj.css) transitionManager.deleteRule(node, this.program.name); + this.program = this.pending = null; + this.running = false; + } + }, + + invalidate() { + if (this.program) { + this.program.invalidated = true; + } + } + }; +} + +export let outros = {}; + +export function groupOutros() { + outros.current = { + remaining: 0, + callbacks: [] + }; +} + +export var transitionManager = { + running: false, + transitions: [], + bound: null, + stylesheet: null, + activeRules: {}, + promise: null, + + add(transition) { + this.transitions.push(transition); + + if (!this.running) { + this.running = true; + requestAnimationFrame(this.bound || (this.bound = this.next.bind(this))); + } + }, + + addRule(rule, name) { + if (!this.stylesheet) { + const style = createElement('style'); + document.head.appendChild(style); + transitionManager.stylesheet = style.sheet; + } + + if (!this.activeRules[name]) { + this.activeRules[name] = true; + this.stylesheet.insertRule(`@keyframes ${name} ${rule}`, this.stylesheet.cssRules.length); + } + }, + + next() { + this.running = false; + + const now = window.performance.now(); + let i = this.transitions.length; + + while (i--) { + const transition = this.transitions[i]; + + if (transition.program && now >= transition.program.end) { + transition.done(); + } + + if (transition.pending && now >= transition.pending.start) { + transition.start(transition.pending); + } + + if (transition.running) { + transition.update(now); + this.running = true; + } else if (!transition.pending) { + this.transitions.splice(i, 1); + } + } + + if (this.running) { + requestAnimationFrame(this.bound); + } else if (this.stylesheet) { + let i = this.stylesheet.cssRules.length; + while (i--) this.stylesheet.deleteRule(i); + this.activeRules = {}; + } + }, + + deleteRule(node, name) { + node.style.animation = node.style.animation + .split(', ') + .filter(anim => anim && anim.indexOf(name) === -1) + .join(', '); + }, + + wait() { + if (!transitionManager.promise) { + transitionManager.promise = Promise.resolve(); + transitionManager.promise.then(() => { + transitionManager.promise = null; + }); + } + + return transitionManager.promise; + } +}; diff --git a/src/internal/utils.js b/src/internal/utils.js new file mode 100644 index 0000000000..2077c25e01 --- /dev/null +++ b/src/internal/utils.js @@ -0,0 +1,38 @@ +export function noop() {} + +export function assign(tar, src) { + for (var k in src) tar[k] = src[k]; + return tar; +} + +export function assignTrue(tar, src) { + for (var k in src) tar[k] = 1; + return tar; +} + +export function isPromise(value) { + return value && typeof value.then === 'function'; +} + +export function callAfter(fn, i) { + if (i === 0) fn(); + return () => { + if (!--i) fn(); + }; +} + +export function addLoc(element, file, line, column, char) { + element.__svelte_meta = { + loc: { file, line, column, char } + }; +} + +export function exclude(src, prop) { + const tar = {}; + for (const k in src) k === prop || (tar[k] = src[k]); + return tar; +} + +export function run(fn) { + fn(); +} \ No newline at end of file diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts index f35cb204f3..8aba3dcc83 100644 --- a/src/parse/state/tag.ts +++ b/src/parse/state/tag.ts @@ -384,20 +384,38 @@ function readAttribute(parser: Parser, uniqueNames: Set) { parser.allowWhitespace(); - const directive = readDirective(parser, start, name); - if (directive) return directive; - let value = parser.eat('=') ? readAttributeValue(parser) : true; + const end = parser.index; + + const colon_index = name.indexOf(':'); + + if (colon_index !== -1) { + const type = get_directive_type(name.slice(0, colon_index)); + name = name.slice(colon_index + 1); + + return { + start, + end, + type, + name, + expression: value[0].expression + }; + } return { start, - end: parser.index, + end, type: 'Attribute', name, value, }; } +function get_directive_type(name) { + if (name === 'use') return 'Action'; + throw new Error(`TODO directive ${name}`); +} + function readAttributeValue(parser: Parser) { const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null; diff --git a/src/utils/annotateWithScopes.ts b/src/utils/annotateWithScopes.ts index 08ac93387c..4771b4660f 100644 --- a/src/utils/annotateWithScopes.ts +++ b/src/utils/annotateWithScopes.ts @@ -13,6 +13,8 @@ export function createScopes(expression: Node) { if (/Function/.test(node.type)) { if (node.type === 'FunctionDeclaration') { scope.declarations.add(node.id.name); + scope = new Scope(scope, false); + map.set(node, scope); } else { scope = new Scope(scope, false); map.set(node, scope); @@ -30,9 +32,9 @@ export function createScopes(expression: Node) { } else if (node.type === 'BlockStatement') { scope = new Scope(scope, true); map.set(node, scope); - } else if (/(Function|Class|Variable)Declaration/.test(node.type)) { + } else if (/(Class|Variable)Declaration/.test(node.type)) { scope.addDeclaration(node); - } else if (isReference(node, parent)) { + } else if (node.type === 'Identifier' && isReference(node, parent)) { if (!scope.has(node.name)) { globals.add(node.name); } @@ -49,49 +51,6 @@ export function createScopes(expression: Node) { return { map, scope, globals }; } -// TODO remove this in favour of weakmap version -export default function annotateWithScopes(expression: Node) { - const globals = new Set(); - let scope = new Scope(null, false); - - walk(expression, { - enter(node: Node, parent: Node) { - if (/Function/.test(node.type)) { - if (node.type === 'FunctionDeclaration') { - scope.declarations.add(node.id.name); - } else { - node._scope = scope = new Scope(scope, false); - if (node.id) scope.declarations.add(node.id.name); - } - - node.params.forEach((param: Node) => { - extractNames(param).forEach(name => { - scope.declarations.add(name); - }); - }); - } else if (/For(?:In|Of)Statement/.test(node.type)) { - node._scope = scope = new Scope(scope, true); - } else if (node.type === 'BlockStatement') { - node._scope = scope = new Scope(scope, true); - } else if (/(Function|Class|Variable)Declaration/.test(node.type)) { - scope.addDeclaration(node); - } else if (isReference(node, parent)) { - if (!scope.has(node.name)) { - globals.add(node.name); - } - } - }, - - leave(node: Node) { - if (node._scope) { - scope = scope.parent; - } - }, - }); - - return { scope, globals }; -} - export class Scope { parent: Scope; block: boolean; diff --git a/test/cli/samples/sourcemap-inline/src/Main.html b/test/cli/samples/sourcemap-inline/src/Main.html index 86a14718f1..ad51c3d3d0 100644 --- a/test/cli/samples/sourcemap-inline/src/Main.html +++ b/test/cli/samples/sourcemap-inline/src/Main.html @@ -2,7 +2,7 @@ import { onmount } from 'svelte'; onmount(() => { - console.log( 'here' ); + console.log('here'); }); diff --git a/test/cli/samples/sourcemap/src/Main.html b/test/cli/samples/sourcemap/src/Main.html index 86a14718f1..ad51c3d3d0 100644 --- a/test/cli/samples/sourcemap/src/Main.html +++ b/test/cli/samples/sourcemap/src/Main.html @@ -2,7 +2,7 @@ import { onmount } from 'svelte'; onmount(() => { - console.log( 'here' ); + console.log('here'); }); diff --git a/test/runtime/index.js b/test/runtime/index.js index 690bf8c9ab..530f5d85cc 100644 --- a/test/runtime/index.js +++ b/test/runtime/index.js @@ -3,7 +3,7 @@ import chalk from 'chalk'; import * as path from "path"; import * as fs from "fs"; import * as acorn from "acorn"; -import { transitionManager } from "../../shared.js"; +import { transitionManager } from "../../internal.js"; import { showOutput, @@ -25,7 +25,7 @@ function getName(filename) { return base[0].toUpperCase() + base.slice(1); } -describe("runtime", () => { +describe.only("runtime", () => { before(() => { svelte = loadSvelte(false); svelte$ = loadSvelte(true); @@ -161,10 +161,10 @@ describe("runtime", () => { if (config.test) { return Promise.resolve(config.test(assert, component, target, window, raf)).then(() => { - component.destroy(); + component.$destroy(); }); } else { - component.destroy(); + component.$destroy(); assert.htmlEqual(target.innerHTML, ""); } }) @@ -195,11 +195,10 @@ describe("runtime", () => { }); } - const shared = path.resolve("shared.js"); + const internal = path.resolve("internal.js"); fs.readdirSync("test/runtime/samples").forEach(dir => { - runTest(dir, shared, false); - runTest(dir, shared, true); - runTest(dir, null, false); + runTest(dir, internal, false); + runTest(dir, internal, true); }); it("fails if options.target is missing in dev mode", () => { diff --git a/test/server-side-rendering/samples/import-non-component/_actual.html b/test/server-side-rendering/samples/import-non-component/_actual.html index 25e562b3df..9e9b43e236 100644 --- a/test/server-side-rendering/samples/import-non-component/_actual.html +++ b/test/server-side-rendering/samples/import-non-component/_actual.html @@ -1,2 +1,2 @@ -
i got 99 problems
-
the answer is 42
\ No newline at end of file +
i got undefined problems
+
the answer is undefined
\ No newline at end of file