Merge branch 'master' of github.com:sveltejs/svelte

pull/1355/head v2.0.0
Rich Harris 7 years ago
commit d215279ef1

@ -76,14 +76,11 @@ The Svelte compiler optionally takes a second argument, an object of configurati
| | **Values** | **Description** | **Default** |
|---|---|---|---|
| `generate` | `'dom'`, `'ssr'` | Whether to generate JavaScript code intended for use on the client (`'dom'`), or for use in server-side rendering (`'ssr'`). | `'dom'` |
| `generate` | `'dom'`, `'ssr'`, `false` | Whether to generate JavaScript code intended for use on the client (`'dom'`), or for use in server-side rendering (`'ssr'`). If `false`, component will be parsed and validated but no code will be emitted | `'dom'` |
| `dev` | `true`, `false` | Whether to enable run-time checks in the compiled component. These are helpful during development, but slow your component down. | `false` |
| `css` | `true`, `false` | Whether to include code to inject your component's styles into the DOM. | `true` |
| `store` | `true`, `false` | Whether to support store integration on the compiled component. | `false` |
| `hydratable` | `true`, `false` | Whether to support hydration on the compiled component. | `false` |
| `customElement` | `true`, `false`, `{ tag, props }` | Whether to compile this component to a custom element. If `tag`/`props` are passed, compiles to a custom element and overrides the values exported by the component. | `false` |
| `cascade` | `true`, `false` | Whether to cascade all of the component's styles to child components. If `false`, only selectors wrapped in `:global(...)` and keyframe IDs beginning with `-global-` are cascaded. | `true` |
| `parser` | `v2` | Opt in to [v2 syntax](https://github.com/sveltejs/svelte-upgrade#svelte-v2-syntax-changes). | `undefined` |
| `bind` | `boolean` | If `false`, disallows `bind:` directives | `true` |
| | | |
| `shared` | `true`, `false`, `string` | Whether to import various helpers from a shared external library. When you have a project with multiple components, this reduces the overall size of your JavaScript bundle, at the expense of having immediately-usable component. You can pass a string of the module path to use, or `true` will import from `'svelte/shared.js'`. | `false` |
@ -94,7 +91,7 @@ The Svelte compiler optionally takes a second argument, an object of configurati
| `filename` | `string` | The filename to use in sourcemaps and compiler error and warning messages. | `'SvelteComponent.html'` |
| `amd`.`id` | `string` | The AMD module ID to use for the `'amd'` and `'umd'` output formats. | `undefined` |
| `globals` | `object`, `function` | When outputting to the `'umd'`, `'iife'` or `'eval'` formats, an object or function mapping the names of imported dependencies to the names of global variables. | `{}` |
| `preserveComments` | `boolean` | Include comments in rendering. Currently, only applies to SSR rendering | `false` |
| `preserveComments` | `boolean` | Include comments in rendering. Currently, only applies to SSR rendering | `false` |
| | | |
| `onerror` | `function` | Specify a callback for when Svelte encounters an error while compiling the component. Passed two arguments: the error object, and another function that is Svelte's default onerror handling. | (exception is thrown) |
| `onwarn` | `function` | Specify a callback for when Svelte encounters a non-fatal warning while compiling the component. Passed two arguments: the warning object, and another function that is Svelte's default onwarn handling. | (warning is logged to console) |

@ -58,7 +58,7 @@
"glob": "^7.1.1",
"is-reference": "^1.1.0",
"jsdom": "^11.6.1",
"locate-character": "^2.0.0",
"locate-character": "^2.0.5",
"magic-string": "^0.22.3",
"mocha": "^3.2.0",
"nightmare": "^2.10.0",

@ -26,6 +26,8 @@ function collapseTimings(timings) {
}
export default class Stats {
onwarn: (warning: Warning) => void;
startTime: number;
currentTiming: Timing;
currentChildren: Timing[];
@ -33,11 +35,15 @@ export default class Stats {
stack: Timing[];
warnings: Warning[];
constructor() {
constructor({ onwarn }: {
onwarn: (warning: Warning) => void
}) {
this.startTime = now();
this.stack = [];
this.currentChildren = this.timings = [];
this.onwarn = onwarn;
this.warnings = [];
}
@ -99,4 +105,9 @@ export default class Stats {
hooks
};
}
warn(warning) {
this.warnings.push(warning);
this.onwarn(warning);
}
}

@ -31,18 +31,18 @@ class Rule {
return this.selectors.some(s => s.used);
}
minify(code: MagicString, cascade: boolean, dev: boolean) {
minify(code: MagicString, dev: boolean) {
let c = this.node.start;
let started = false;
this.selectors.forEach((selector, i) => {
if (cascade || selector.used) {
if (selector.used) {
const separator = started ? ',' : '';
if ((selector.node.start - c) > separator.length) {
code.overwrite(c, selector.node.start, separator);
}
if (!cascade) selector.minify(code);
selector.minify(code);
c = selector.node.end;
started = true;
@ -66,39 +66,12 @@ class Rule {
code.remove(c, this.node.block.end - 1);
}
transform(code: MagicString, id: string, keyframes: Map<string, string>, cascade: boolean) {
transform(code: MagicString, id: string, keyframes: Map<string, string>) {
if (this.parent && this.parent.node.type === 'Atrule' && this.parent.node.name === 'keyframes') return true;
const attr = `.${id}`;
if (cascade) {
this.selectors.forEach(selector => {
// TODO disable cascading (without :global(...)) in v2
const { start, end, children } = selector.node;
const css = code.original;
const selectorString = css.slice(start, end);
const firstToken = children[0];
let transformed;
if (firstToken.type === 'TypeSelector') {
const insert = firstToken.end;
const head = firstToken.name === '*' ? '' : css.slice(start, insert);
const tail = css.slice(insert, end);
transformed = `${head}${attr}${tail},${attr} ${selectorString}`;
} else {
transformed = `${attr}${selectorString},${attr} ${selectorString}`;
}
code.overwrite(start, end, transformed);
});
} else {
this.selectors.forEach(selector => selector.transform(code, attr));
}
this.selectors.forEach(selector => selector.transform(code, attr));
this.declarations.forEach(declaration => declaration.transform(code, keyframes));
}
@ -182,7 +155,7 @@ class Atrule {
return true; // TODO
}
minify(code: MagicString, cascade: boolean, dev: boolean) {
minify(code: MagicString, dev: boolean) {
if (this.node.name === 'media') {
const expressionChar = code.original[this.node.expression.start];
let c = this.node.start + (expressionChar === '(' ? 6 : 7);
@ -215,9 +188,9 @@ class Atrule {
let c = this.node.block.start + 1;
this.children.forEach(child => {
if (cascade || child.isUsed(dev)) {
if (child.isUsed(dev)) {
code.remove(c, child.node.start);
child.minify(code, cascade, dev);
child.minify(code, dev);
c = child.node.end;
}
});
@ -226,7 +199,7 @@ class Atrule {
}
}
transform(code: MagicString, id: string, keyframes: Map<string, string>, cascade: boolean) {
transform(code: MagicString, id: string, keyframes: Map<string, string>) {
if (this.node.name === 'keyframes') {
this.node.expression.children.forEach(({ type, name, start, end }: Node) => {
if (type === 'Identifier') {
@ -240,7 +213,7 @@ class Atrule {
}
this.children.forEach(child => {
child.transform(code, id, keyframes, cascade);
child.transform(code, id, keyframes);
})
}
@ -264,7 +237,6 @@ const keys = {};
export default class Stylesheet {
source: string;
parsed: Parsed;
cascade: boolean;
filename: string;
dev: boolean;
@ -276,10 +248,9 @@ export default class Stylesheet {
nodesWithCssClass: Set<Node>;
constructor(source: string, parsed: Parsed, filename: string, cascade: boolean, dev: boolean) {
constructor(source: string, parsed: Parsed, filename: string, dev: boolean) {
this.source = source;
this.parsed = parsed;
this.cascade = cascade;
this.filename = filename;
this.dev = dev;
@ -356,11 +327,6 @@ export default class Stylesheet {
if (parent.type === 'Element') stack.unshift(<Element>parent);
}
if (this.cascade) {
if (stack.length === 0) this.nodesWithCssClass.add(node);
return;
}
for (let i = 0; i < this.children.length; i += 1) {
const child = this.children[i];
child.apply(node, stack);
@ -389,15 +355,15 @@ export default class Stylesheet {
if (shouldTransformSelectors) {
this.children.forEach((child: (Atrule|Rule)) => {
child.transform(code, this.id, this.keyframes, this.cascade);
child.transform(code, this.id, this.keyframes);
});
}
let c = 0;
this.children.forEach(child => {
if (this.cascade || child.isUsed(this.dev)) {
if (child.isUsed(this.dev)) {
code.remove(c, child.node.start);
child.minify(code, this.cascade, this.dev);
child.minify(code, this.dev);
c = child.node.end;
}
});
@ -421,27 +387,27 @@ export default class Stylesheet {
}
warnOnUnusedSelectors(onwarn: (warning: Warning) => void) {
if (this.cascade) return;
let locator;
const handler = (selector: Selector) => {
const pos = selector.node.start;
if (!locator) locator = getLocator(this.source);
const { line, column } = locator(pos);
if (!locator) locator = getLocator(this.source, { offsetLine: 1 });
const start = locator(pos);
const end = locator(selector.node.end);
const frame = getCodeFrame(this.source, line, column);
const frame = getCodeFrame(this.source, start.line - 1, start.column);
const message = `Unused CSS selector`;
onwarn({
code: `css-unused-selector`,
message,
frame,
loc: { line: line + 1, column },
start,
end,
pos,
filename: this.filename,
toString: () => `${message} (${line + 1}:${column})\n${frame}`,
toString: () => `${message} (${start.line}:${start.column})\n${frame}`,
});
};

@ -5,7 +5,6 @@ import { getLocator } from 'locate-character';
import Stats from '../Stats';
import deindent from '../utils/deindent';
import CodeBuilder from '../utils/CodeBuilder';
import getCodeFrame from '../utils/getCodeFrame';
import flattenReference from '../utils/flattenReference';
import reservedNames from '../utils/reservedNames';
import namespaces from '../utils/namespaces';
@ -84,7 +83,6 @@ export default class Generator {
source: string;
name: string;
options: CompileOptions;
v2: boolean;
customElement: CustomElementOptions;
tag: string;
@ -95,6 +93,7 @@ export default class Generator {
helpers: Set<string>;
components: Set<string>;
events: Set<string>;
methods: Set<string>;
transitions: Set<string>;
actions: Set<string>;
importedComponents: Map<string, string>;
@ -133,8 +132,6 @@ export default class Generator {
stats.start('compile');
this.stats = stats;
this.v2 = options.parser === 'v2';
this.ast = clone(parsed);
this.parsed = parsed;
@ -145,6 +142,7 @@ export default class Generator {
this.helpers = new Set();
this.components = new Set();
this.events = new Set();
this.methods = new Set();
this.transitions = new Set();
this.actions = new Set();
this.importedComponents = new Map();
@ -369,31 +367,13 @@ export default class Generator {
})
};
Object.getOwnPropertyNames(String.prototype).forEach(name => {
const descriptor = Object.getOwnPropertyDescriptor(String.prototype, name);
if (typeof descriptor.value === 'function') {
Object.defineProperty(css, name, {
value: (...args) => {
return css.code === null
? null
: css.code[name].call(css.code, ...args);
}
});
}
});
this.stats.stop('compile');
return {
ast: this.ast,
js,
css,
stats: this.stats.render(this),
// TODO deprecate
code: js.code,
map: js.map,
cssMap: css.map
stats: this.stats.render(this)
};
}
@ -441,6 +421,7 @@ export default class Generator {
code,
source,
computations,
methods,
templateProperties,
imports
} = this;
@ -578,9 +559,7 @@ export default class Generator {
const key = getName(prop.key);
const value = prop.value;
const deps = this.v2
? value.params[0].properties.map(prop => prop.key.name)
: value.params.map(param => param.type === 'AssignmentPattern' ? param.left.name : param.name);
const deps = value.params[0].properties.map(prop => prop.key.name);
deps.forEach(dep => {
this.expectedProperties.add(dep);
@ -632,6 +611,10 @@ export default class Generator {
if (templateProperties.methods && dom) {
addDeclaration('methods', templateProperties.methods.value);
templateProperties.methods.value.properties.forEach(prop => {
this.methods.add(prop.key.name);
});
}
if (templateProperties.namespace) {
@ -639,12 +622,10 @@ export default class Generator {
this.namespace = namespaces[ns] || ns;
}
if (templateProperties.onrender) templateProperties.oncreate = templateProperties.onrender; // remove after v2
if (templateProperties.oncreate && dom) {
addDeclaration('oncreate', templateProperties.oncreate.value);
}
if (templateProperties.onteardown) templateProperties.ondestroy = templateProperties.onteardown; // remove after v2
if (templateProperties.ondestroy && dom) {
addDeclaration('ondestroy', templateProperties.ondestroy.value);
}
@ -802,7 +783,7 @@ export default class Generator {
node.generator = generator;
if (node.type === 'Element' && (node.name === ':Component' || node.name === ':Self' || node.name === 'svelte:component' || node.name === 'svelte:self' || generator.components.has(node.name))) {
if (node.type === 'Element' && (node.name === 'svelte:component' || node.name === 'svelte:self' || generator.components.has(node.name))) {
node.type = 'Component';
Object.setPrototypeOf(node, nodes.Component.prototype);
} else if (node.type === 'Element' && node.name === 'title' && parentIsHead(parent)) { // TODO do this in parse?
@ -886,7 +867,7 @@ export default class Generator {
this.skip();
}
if (node.type === 'Component' && (node.name === ':Component' || node.name === 'svelte:component')) {
if (node.type === 'Component' && node.name === 'svelte:component') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
}

@ -47,7 +47,7 @@ export class DomGenerator extends Generator {
this.legacy = options.legacy;
this.needsEncapsulateHelper = false;
// initial values for e.g. window.innerWidth, if there's a <:Window> meta tag
// initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
this.metaBindings = [];
}
}
@ -89,7 +89,7 @@ export default function dom(
});
if (generator.readonly.has(key)) {
// <:Window> bindings
// <svelte:window> bindings
throw new Error(
`Cannot have a computed value '${key}' that clashes with a read-only property`
);
@ -99,11 +99,7 @@ export default function dom(
const condition = `${deps.map(dep => `changed.${dep}`).join(' || ')}`;
const call = generator.v2
? `%computed-${key}(state)`
: `%computed-${key}(${deps.map(dep => `state.${dep}`).join(', ')})`;
const statement = `if (this._differs(state.${key}, (state.${key} = ${call}))) changed.${key} = true;`;
const statement = `if (this._differs(state.${key}, (state.${key} = %computed-${key}(state)))) changed.${key} = true;`;
computationBuilder.addConditional(condition, statement);
});
@ -143,8 +139,8 @@ export default function dom(
? `@proto`
: deindent`
{
${['destroy', 'get', 'fire', 'observe', 'on', 'set', 'teardown', '_set', '_mount', '_unmount', '_differs']
.map(n => `${n}: @${n === 'teardown' ? 'destroy' : n}`)
${['destroy', 'get', 'fire', 'on', 'set', '_set', '_mount', '_unmount', '_differs']
.map(n => `${n}: @${n}`)
.join(',\n')}
}`;
@ -153,7 +149,7 @@ export default function dom(
// generate initial state object
const expectedProperties = Array.from(generator.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = options.store || templateProperties.store ? expectedProperties.filter(prop => prop[0] === '$') : [];
const storeProps = expectedProperties.filter(prop => prop[0] === '$');
const initialState = [];
if (globals.length > 0) {
@ -294,7 +290,7 @@ export default function dom(
${props.map(prop => deindent`
get ${prop}() {
return this.get('${prop}');
return this.get().${prop};
}
set ${prop}(value) {

@ -159,12 +159,8 @@ function getEventHandler(
dependencies: string[],
value: string,
) {
let storeDependencies = [];
if (generator.options.store) {
storeDependencies = dependencies.filter(prop => prop[0] === '$').map(prop => prop.slice(1));
dependencies = dependencies.filter(prop => prop[0] !== '$');
}
const storeDependencies = dependencies.filter(prop => prop[0] === '$').map(prop => prop.slice(1));
dependencies = dependencies.filter(prop => prop[0] !== '$');
if (block.contexts.has(name)) {
const tail = attribute.value.type === 'MemberExpression'
@ -207,7 +203,7 @@ function getEventHandler(
let props;
let storeProps;
if (generator.options.store && name[0] === '$') {
if (name[0] === '$') {
props = [];
storeProps = [`${name.slice(1)}: ${value}`];
} else {

@ -45,8 +45,8 @@ export default class Component extends Node {
this.var = block.getUniqueName(
(
(this.name === ':Self' || this.name === 'svelte:self') ? this.generator.name :
(this.name === ':Component' || this.name === 'svelte:component') ? 'switch_instance' :
this.name === 'svelte:self' ? this.generator.name :
this.name === 'svelte:component' ? 'switch_instance' :
this.name
).toLowerCase()
);
@ -73,11 +73,11 @@ export default class Component extends Node {
const componentInitProperties = [`root: #component.root`];
if (this.children.length > 0) {
const slots = Array.from(this._slots).map(name => `${quoteIfNecessary(name, generator.legacy)}: @createFragment()`);
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${generator.legacy ? `["default"]` : `.default`}`, 'nodes');
child.build(block, `${this.var}._slotted.default`, 'nodes');
});
}
@ -139,7 +139,7 @@ export default class Component extends Node {
const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
changes.push(condition ? `${condition} && ${value}` : value);
} else {
const obj = `{ ${quoteIfNecessary(name, this.generator.legacy)}: ${value} }`;
const obj = `{ ${quoteIfNecessary(name)}: ${value} }`;
initialProps.push(obj);
const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
@ -217,7 +217,7 @@ export default class Component extends Node {
${binding.dependencies
.map((name: string) => {
const isStoreProp = generator.options.store && name[0] === '$';
const isStoreProp = name[0] === '$';
const prop = isStoreProp ? name.slice(1) : name;
const newState = isStoreProp ? 'newStoreState' : 'newState';
@ -230,7 +230,7 @@ export default class Component extends Node {
}
else {
const isStoreProp = generator.options.store && key[0] === '$';
const isStoreProp = key[0] === '$';
const prop = isStoreProp ? key.slice(1) : key;
const newState = isStoreProp ? 'newStoreState' : 'newState';
@ -293,7 +293,7 @@ export default class Component extends Node {
`;
}
if (this.name === ':Component' || this.name === 'svelte:component') {
if (this.name === 'svelte:component') {
const switch_value = block.getUniqueName('switch_value');
const switch_props = block.getUniqueName('switch_props');
@ -387,7 +387,7 @@ export default class Component extends Node {
block.builders.destroy.addLine(`if (${name}) ${name}.destroy(false);`);
} else {
const expression = (this.name === ':Self' || this.name === 'svelte:self')
const expression = this.name === 'svelte:self'
? generator.name
: `%components-${this.name}`;
@ -440,7 +440,7 @@ export default class Component extends Node {
}
remount(name: string) {
return `${this.var}._mount(${name}._slotted${this.generator.legacy ? `["default"]` : `.default`}, null);`;
return `${this.var}._mount(${name}._slotted.default, null);`;
}
}

@ -455,6 +455,6 @@ export default class EachBlock extends Node {
remount(name: string) {
// TODO consider keyed blocks
return `for (var #i = 0; #i < ${this.iterations}.length; #i += 1) ${this.iterations}[#i].m(${name}._slotted${this.generator.legacy ? `["default"]` : `.default`}, null);`;
return `for (var #i = 0; #i < ${this.iterations}.length; #i += 1) ${this.iterations}[#i].m(${name}._slotted.default, null);`;
}
}

@ -461,7 +461,7 @@ export default class Element extends Node {
if (attr.type === 'Attribute') {
const { dynamic, value, dependencies } = mungeAttribute(attr, block);
const snippet = `{ ${quoteIfNecessary(attr.name, this.generator.legacy)}: ${value} }`;
const snippet = `{ ${quoteIfNecessary(attr.name)}: ${value} }`;
initialProps.push(snippet);
const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
@ -519,10 +519,19 @@ export default class Element extends Node {
if (!validCalleeObjects.has(flattened.name)) {
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
if (flattened.name[0] === '$' && !generator.methods.has(flattened.name)) {
generator.code.overwrite(
attribute.expression.start,
attribute.expression.start + 1,
`${block.alias('component')}.store.`
);
} else {
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
}
if (shouldHoist) eventHandlerUsesComponent = true; // this feels a bit hacky but it works!
}
@ -580,16 +589,8 @@ export default class Element extends Node {
});
`);
if (generator.options.dev) {
block.builders.hydrate.addBlock(deindent`
if (${handlerName}.teardown) {
console.warn("Return 'destroy()' from custom event handlers. Returning 'teardown()' has been deprecated and will be unsupported in Svelte 2");
}
`);
}
block.builders.destroy.addLine(deindent`
${handlerName}[${handlerName}.destroy ? 'destroy' : 'teardown']();
${handlerName}.destroy();
`);
} else {
const handler = deindent`
@ -789,7 +790,7 @@ export default class Element extends Node {
return `@appendNode(${this.var}, ${name}._slotted.${this.getStaticAttributeValue('slot')});`;
}
return `@appendNode(${this.var}, ${name}._slotted${this.generator.legacy ? `["default"]` : `.default`});`;
return `@appendNode(${this.var}, ${name}._slotted.default);`;
}
addCssClass() {
@ -839,7 +840,7 @@ function getClaimStatement(
) {
const attributes = node.attributes
.filter((attr: Node) => attr.type === 'Attribute')
.map((attr: Node) => `${quoteProp(attr.name, generator.legacy)}: true`)
.map((attr: Node) => `${quoteIfNecessary(attr.name)}: true`)
.join(', ');
const name = namespace ? node.name : node.name.toUpperCase();
@ -849,13 +850,6 @@ function getClaimStatement(
: `{}`}, ${namespace === namespaces.svg ? true : false})`;
}
function quoteProp(name: string, legacy: boolean) {
const isLegacyPropName = legacy && reservedNames.has(name);
if (/[^a-zA-Z_$0-9]/.test(name) || isLegacyPropName) return `"${name}"`;
return name;
}
function stringifyAttributeValue(value: Node | true) {
if (value === true) return '';
if (value.length === 0) return `=""`;

@ -28,6 +28,6 @@ export default class MustacheTag extends Tag {
}
remount(name: string) {
return `@appendNode(${this.var}, ${name}._slotted${this.generator.legacy ? `["default"]` : `.default`});`;
return `@appendNode(${this.var}, ${name}._slotted.default);`;
}
}

@ -91,6 +91,6 @@ export default class RawMustacheTag extends Tag {
}
remount(name: string) {
return `@appendNode(${this.var}, ${name}._slotted${this.generator.legacy ? `["default"]` : `.default`});`;
return `@appendNode(${this.var}, ${name}._slotted.default);`;
}
}

@ -37,7 +37,7 @@ export default class Slot extends Element {
generator.slots.add(slotName);
const content_name = block.getUniqueName(`slot_content_${slotName}`);
const prop = !isValidIdentifier(slotName) || (generator.legacy && reservedNames.has(slotName)) ? `["${slotName}"]` : `.${slotName}`;
const prop = !isValidIdentifier(slotName) ? `["${slotName}"]` : `.${slotName}`;
block.addVariable(content_name, `#component._slotted${prop}`);
const needsAnchorBefore = this.prev ? this.prev.type !== 'Element' : !parentNode;

@ -60,6 +60,6 @@ export default class Text extends Node {
}
remount(name: string) {
return `@appendNode(${this.var}, ${name}._slotted${this.generator.legacy ? `["default"]` : `.default`});`;
return `@appendNode(${this.var}, ${name}._slotted.default);`;
}
}

@ -86,16 +86,8 @@ export default class Window extends Node {
});
`);
if (generator.options.dev) {
block.builders.hydrate.addBlock(deindent`
if (${handlerName}.teardown) {
console.warn("Return 'destroy()' from custom event handlers. Returning 'teardown()' has been deprecated and will be unsupported in Svelte 2");
}
`);
}
block.builders.destroy.addLine(deindent`
${handlerName}[${handlerName}.destroy ? 'destroy' : 'teardown']();
${handlerName}.destroy();
`);
} else {
block.builders.init.addBlock(deindent`
@ -180,41 +172,23 @@ export default class Window extends Node {
});
// special case... might need to abstract this out if we add more special cases
if (bindings.scrollX && bindings.scrollY) {
const observerCallback = block.getUniqueName(`scrollobserver`);
block.builders.init.addBlock(deindent`
function ${observerCallback}() {
${lock} = true;
clearTimeout(${timeout});
var x = ${bindings.scrollX
? `#component.get("${bindings.scrollX}")`
: `window.pageXOffset`};
var y = ${bindings.scrollY
? `#component.get("${bindings.scrollY}")`
: `window.pageYOffset`};
window.scrollTo(x, y);
${timeout} = setTimeout(${clear}, 100);
}
`);
if (bindings.scrollX)
block.builders.init.addLine(
`#component.observe("${bindings.scrollX}", ${observerCallback});`
);
if (bindings.scrollY)
block.builders.init.addLine(
`#component.observe("${bindings.scrollY}", ${observerCallback});`
);
} else if (bindings.scrollX || bindings.scrollY) {
const isX = !!bindings.scrollX;
if (bindings.scrollX || bindings.scrollY) {
block.builders.init.addBlock(deindent`
#component.observe("${bindings.scrollX || bindings.scrollY}", function(${isX ? 'x' : 'y'}) {
${lock} = true;
clearTimeout(${timeout});
window.scrollTo(${isX ? 'x, window.pageYOffset' : 'window.pageXOffset, y'});
${timeout} = setTimeout(${clear}, 100);
#component.on("state", ({ changed, current }) => {
if (${
[bindings.scrollX, bindings.scrollY].map(
binding => binding && `changed["${binding}"]`
).filter(Boolean).join(' || ')
}) {
${lock} = true;
clearTimeout(${timeout});
window.scrollTo(${
bindings.scrollX ? `current["${bindings.scrollX}"]` : `window.pageXOffset`
}, ${
bindings.scrollY ? `current["${bindings.scrollY}"]` : `window.pageYOffset`
});
${timeout} = setTimeout(${clear}, 100);
}
});
`);
}

@ -58,7 +58,7 @@ export default class Node {
if (child.type === 'Comment') return;
// special case — this is an easy way to remove whitespace surrounding
// <:Window/>. lil hacky but it works
// <svelte:window/>. lil hacky but it works
if (child.type === 'Window') {
windowComponent = child;
return;
@ -163,6 +163,6 @@ export default class Node {
}
remount(name: string) {
return `${this.var}.m(${name}._slotted${this.generator.legacy ? `["default"]` : `.default`}, null);`;
return `${this.var}.m(${name}._slotted.default, null);`;
}
}

@ -55,7 +55,7 @@ export default function mungeAttribute(attribute: Node, block: Block): MungedAtt
return {
spread: false,
name: attribute.name,
value: isNaN(value.data) ? stringify(value.data) : value.data,
value: stringify(value.data),
dynamic: false,
dependencies: []
};

@ -75,7 +75,7 @@ export default function ssr(
// generate initial state object
const expectedProperties = Array.from(generator.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = options.store || templateProperties.store ? expectedProperties.filter(prop => prop[0] === '$') : [];
const storeProps = expectedProperties.filter(prop => prop[0] === '$');
const initialState = [];
if (globals.length > 0) {
@ -84,9 +84,7 @@ export default function ssr(
if (storeProps.length > 0) {
const initialize = `_init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`
if (options.store || templateProperties.store) {
initialState.push(`options.store.${initialize}`);
}
initialState.push(`options.store.${initialize}`);
}
if (templateProperties.data) {
@ -139,7 +137,7 @@ export default function ssr(
${computations.map(
({ key, deps }) =>
`state.${key} = %computed-${key}(${generator.v2 ? 'state' : deps.map(dep => `state.${dep}`).join(', ')});`
`state.${key} = %computed-${key}(state);`
)}
${generator.bindings.length &&
@ -163,47 +161,6 @@ export default function ssr(
};
var warned = false;
${name}.renderCss = function() {
if (!warned) {
console.error('Component.renderCss(...) is deprecated and will be removed in v2 — use Component.render(...).css instead');
warned = true;
}
var components = [];
${generator.stylesheet.hasStyles &&
deindent`
components.push({
filename: ${name}.filename,
css: ${name}.css && ${name}.css.code,
map: ${name}.css && ${name}.css.map
});
`}
${templateProperties.components &&
deindent`
var seen = {};
function addComponent(component) {
var result = component.renderCss();
result.components.forEach(x => {
if (seen[x.filename]) return;
seen[x.filename] = true;
components.push(x);
});
}
${templateProperties.components.value.properties.map((prop: Node) => {
return `addComponent(%components-${getName(prop.key)});`;
})}
`}
return {
css: components.map(x => x.css).join('\\n'),
map: null,
components
};
};
${templateProperties.preload && `${name}.preload = %preload;`}

@ -57,7 +57,7 @@ export default function visitComponent(
if (attribute.value.length === 1) {
const chunk = attribute.value[0];
if (chunk.type === 'Text') {
return isNaN(chunk.data) ? stringify(chunk.data) : chunk.data;
return stringify(chunk.data);
}
block.contextualise(chunk.expression);
@ -87,11 +87,11 @@ export default function visitComponent(
.concat(bindingProps)
.join(', ')} }`;
const isDynamicComponent = node.name === ':Component' || node.name === 'svelte:component';
const isDynamicComponent = node.name === 'svelte:component';
if (isDynamicComponent) block.contextualise(node.expression);
const expression = (
(node.name === ':Self' || node.name === 'svelte:self') ? generator.name :
node.name === 'svelte:self' ? generator.name :
isDynamicComponent ? `((${node.metadata.snippet}) || __missingComponent)` :
`%components-${node.name}`
);
@ -103,9 +103,7 @@ export default function visitComponent(
let open = `\${${expression}._render(__result, ${props}`;
const options = [];
if (generator.options.store) {
options.push(`store: options.store`);
}
options.push(`store: options.store`);
if (node.children.length) {
const appendTarget: AppendTarget = {

@ -23,9 +23,9 @@ function normalizeOptions(options: CompileOptions): CompileOptions {
}
function defaultOnwarn(warning: Warning) {
if (warning.loc) {
if (warning.start) {
console.warn(
`(${warning.loc.line}:${warning.loc.column}) ${warning.message}`
`(${warning.start.line}:${warning.start.column}) ${warning.message}`
); // eslint-disable-line no-console
} else {
console.warn(warning.message); // eslint-disable-line no-console
@ -106,11 +106,13 @@ export async function preprocess(source: string, options: PreprocessOptions) {
};
}
export function compile(source: string, _options: CompileOptions) {
function compile(source: string, _options: CompileOptions) {
const options = normalizeOptions(_options);
let parsed: Parsed;
const stats = new Stats();
const stats = new Stats({
onwarn: options.onwarn
});
try {
stats.start('parse');
@ -122,37 +124,33 @@ export function compile(source: string, _options: CompileOptions) {
}
stats.start('stylesheet');
const stylesheet = new Stylesheet(source, parsed, options.filename, options.cascade !== false, options.dev);
const stylesheet = new Stylesheet(source, parsed, options.filename, options.dev);
stats.stop('stylesheet');
stats.start('validate');
// TODO remove this when we remove svelte.validate from public API — we
// can use the stats object instead
const onwarn = options.onwarn;
options.onwarn = warning => {
stats.warnings.push(warning);
onwarn(warning);
};
validate(parsed, source, stylesheet, options);
validate(parsed, source, stylesheet, stats, options);
stats.stop('validate');
if (options.generate === false) {
return { ast: parsed, stats, js: null, css: null };
}
const compiler = options.generate === 'ssr' ? generateSSR : generate;
return compiler(parsed, source, stylesheet, options, stats);
};
export function create(source: string, _options: CompileOptions = {}) {
function create(source: string, _options: CompileOptions = {}) {
_options.format = 'eval';
const compiled = compile(source, _options);
if (!compiled || !compiled.code) {
if (!compiled || !compiled.js.code) {
return;
}
try {
return (0, eval)(compiled.code);
return (new Function(`return ${compiled.js.code}`))();
} catch (err) {
if (_options.onerror) {
_options.onerror(err);
@ -163,4 +161,4 @@ export function create(source: string, _options: CompileOptions = {}) {
}
}
export { parse, validate, Stylesheet, version as VERSION };
export { parse, create, compile, version as VERSION };

@ -21,14 +21,13 @@ export interface Parser {
}
export interface Parsed {
hash: number;
html: Node;
css: Node;
js: Node;
}
export interface Warning {
loc?: { line: number; column: number; pos?: number };
start?: { line: number; column: number; pos?: number };
end?: { line: number; column: number; };
pos?: number;
code: string;
@ -44,7 +43,7 @@ export interface CompileOptions {
format?: ModuleFormat;
name?: string;
filename?: string;
generate?: string;
generate?: string | false;
globals?: ((id: string) => string) | object;
amd?: {
id?: string;
@ -56,19 +55,15 @@ export interface CompileOptions {
dev?: boolean;
immutable?: boolean;
shared?: boolean | string;
cascade?: boolean;
hydratable?: boolean;
legacy?: boolean;
customElement?: CustomElementOptions | true;
css?: boolean;
store?: boolean;
preserveComments?: boolean | false;
onerror?: (error: Error) => void;
onwarn?: (warning: Warning) => void;
parser?: 'v2';
}
export interface GenerateOptions {

@ -3,23 +3,19 @@ import { locate, Location } from 'locate-character';
import fragment from './state/fragment';
import { whitespace } from '../utils/patterns';
import { trimStart, trimEnd } from '../utils/trim';
import getCodeFrame from '../utils/getCodeFrame';
import reservedNames from '../utils/reservedNames';
import fullCharCodeAt from '../utils/fullCharCodeAt';
import hash from '../utils/hash';
import { Node, Parsed } from '../interfaces';
import error from '../utils/error';
interface ParserOptions {
filename?: string;
bind?: boolean;
parser?: 'v2';
}
type ParserState = (parser: Parser) => (ParserState | void);
export class Parser {
readonly v2: boolean;
readonly template: string;
readonly filename?: string;
@ -34,8 +30,6 @@ export class Parser {
allowBindings: boolean;
constructor(template: string, options: ParserOptions) {
this.v2 = options.parser === 'v2';
if (typeof template !== 'string') {
throw new TypeError('Template must be a string');
}
@ -229,7 +223,6 @@ export default function parse(
): Parsed {
const parser = new Parser(template, options);
return {
hash: hash(parser.template),
html: parser.html,
css: parser.css,
js: parser.js,

@ -165,10 +165,10 @@ export function readDirective(
// assume the mistake was wrapping the directive arguments.
// this could yield false positives! but hopefully not too many
let message = 'directive values should not be wrapped';
const expressionEnd = parser.template.indexOf((parser.v2 ? '}' : '}}'), expressionStart);
const expressionEnd = parser.template.indexOf('}', expressionStart);
if (expressionEnd !== -1) {
const value = parser.template.slice(expressionStart + (parser.v2 ? 1 : 2), expressionEnd);
message += ` — use '${value}', not '${parser.v2 ? `{${value}}` : `{{${value}}}`}'`;
const value = parser.template.slice(expressionStart + 1, expressionEnd);
message += ` — use '${value}', not '{${value}}'`;
}
parser.error({
code: `invalid-directive-value`,

@ -6,7 +6,7 @@ const literals = new Map([['true', true], ['false', false], ['null', null]]);
export default function readExpression(parser: Parser) {
const start = parser.index;
const name = parser.readUntil(parser.v2 ? /\s*}/ : /\s*}}/);
const name = parser.readUntil(/\s*}/);
if (name && /^[a-z]+$/.test(name)) {
const end = start + name.length;

@ -8,7 +8,7 @@ export default function fragment(parser: Parser) {
return tag;
}
if (parser.match(parser.v2 ? '{' : '{{')) {
if (parser.match('{')) {
return mustache;
}

@ -32,7 +32,7 @@ function trimWhitespace(block: Node, trimBefore: boolean, trimAfter: boolean) {
export default function mustache(parser: Parser) {
const start = parser.index;
parser.index += parser.v2 ? 1 : 2;
parser.index += 1;
parser.allowWhitespace();
@ -64,7 +64,7 @@ export default function mustache(parser: Parser) {
parser.eat(expected, true);
parser.allowWhitespace();
parser.eat(parser.v2 ? '}' : '}}', true);
parser.eat('}', true);
while (block.elseif) {
block.end = parser.index;
@ -86,7 +86,7 @@ export default function mustache(parser: Parser) {
block.end = parser.index;
parser.stack.pop();
} else if (parser.eat(parser.v2 ? ':elseif' : 'elseif')) {
} else if (parser.eat(':elseif')) {
const block = parser.current();
if (block.type !== 'IfBlock')
parser.error({
@ -99,7 +99,7 @@ export default function mustache(parser: Parser) {
const expression = readExpression(parser);
parser.allowWhitespace();
parser.eat(parser.v2 ? '}' : '}}', true);
parser.eat('}', true);
block.else = {
start: parser.index,
@ -118,7 +118,7 @@ export default function mustache(parser: Parser) {
};
parser.stack.push(block.else.children[0]);
} else if (parser.eat(parser.v2 ? ':else' : 'else')) {
} else if (parser.eat(':else')) {
const block = parser.current();
if (block.type !== 'IfBlock' && block.type !== 'EachBlock') {
parser.error({
@ -128,7 +128,7 @@ export default function mustache(parser: Parser) {
}
parser.allowWhitespace();
parser.eat(parser.v2 ? '}' : '}}', true);
parser.eat('}', true);
block.else = {
start: parser.index,
@ -138,7 +138,7 @@ export default function mustache(parser: Parser) {
};
parser.stack.push(block.else);
} else if (parser.eat(parser.v2 ? ':then' : 'then')) {
} else if (parser.eat(':then')) {
// TODO DRY out this and the next section
const pendingBlock = parser.current();
if (pendingBlock.type === 'PendingBlock') {
@ -150,7 +150,7 @@ export default function mustache(parser: Parser) {
awaitBlock.value = parser.readIdentifier();
parser.allowWhitespace();
parser.eat(parser.v2 ? '}' : '}}', true);
parser.eat('}', true);
const thenBlock: Node = {
start,
@ -162,7 +162,7 @@ export default function mustache(parser: Parser) {
awaitBlock.then = thenBlock;
parser.stack.push(thenBlock);
}
} else if (parser.eat(parser.v2 ? ':catch' : 'catch')) {
} else if (parser.eat(':catch')) {
const thenBlock = parser.current();
if (thenBlock.type === 'ThenBlock') {
thenBlock.end = start;
@ -173,7 +173,7 @@ export default function mustache(parser: Parser) {
awaitBlock.error = parser.readIdentifier();
parser.allowWhitespace();
parser.eat(parser.v2 ? '}' : '}}', true);
parser.eat('}', true);
const catchBlock: Node = {
start,
@ -336,7 +336,7 @@ export default function mustache(parser: Parser) {
parser.allowWhitespace();
}
parser.eat(parser.v2 ? '}' : '}}', true);
parser.eat('}', true);
parser.current().children.push(block);
parser.stack.push(block);
@ -346,44 +346,12 @@ export default function mustache(parser: Parser) {
childBlock.start = parser.index;
parser.stack.push(childBlock);
}
} else if (parser.eat('yield')) {
// {{yield}}
// TODO deprecate
parser.allowWhitespace();
if (parser.v2) {
const expressionEnd = parser.index;
parser.eat('}', true);
parser.current().children.push({
start,
end: parser.index,
type: 'MustacheTag',
expression: {
start: expressionEnd - 5,
end: expressionEnd,
type: 'Identifier',
name: 'yield'
}
});
} else {
parser.eat('}}', true);
parser.current().children.push({
start,
end: parser.index,
type: 'Element',
name: 'slot',
attributes: [],
children: []
});
}
} else if (parser.eat(parser.v2 ? '@html' : '{')) {
} else if (parser.eat('@html')) {
// {{{raw}}} mustache
const expression = readExpression(parser);
parser.allowWhitespace();
parser.eat(parser.v2 ? '}' : '}}}', true);
parser.eat('}', true);
parser.current().children.push({
start,
@ -395,7 +363,7 @@ export default function mustache(parser: Parser) {
const expression = readExpression(parser);
parser.allowWhitespace();
parser.eat(parser.v2 ? '}' : '}}', true);
parser.eat('}', true);
parser.current().children.push({
start,

@ -11,8 +11,6 @@ import { Node } from '../../interfaces';
const validTagName = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
const metaTags = new Map([
[':Window', 'Window'],
[':Head', 'Head'],
['svelte:window', 'Window'],
['svelte:head', 'Head']
]);
@ -34,6 +32,9 @@ const specials = new Map([
],
]);
const SELF = 'svelte:self';
const COMPONENT = 'svelte:component';
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
const disallowedContents = new Map([
['li', new Set(['li'])],
@ -85,7 +86,7 @@ export default function tag(parser: Parser) {
if (metaTags.has(name)) {
const slug = metaTags.get(name).toLowerCase();
if (isClosingTag) {
if ((name === ':Window' || name === 'svelte:window') && parser.current().children.length) {
if (name === 'svelte:window' && parser.current().children.length) {
parser.error({
code: `invalid-window-content`,
message: `<${name}> cannot have children`
@ -175,14 +176,6 @@ export default function tag(parser: Parser) {
}
}
if (name === ':Component') {
parser.eat('{', true);
element.expression = readExpression(parser);
parser.allowWhitespace();
parser.eat('}', true);
parser.allowWhitespace();
}
const uniqueNames = new Set();
let attribute;
@ -198,7 +191,7 @@ export default function tag(parser: Parser) {
parser.allowWhitespace();
}
if (parser.v2 && name === 'svelte:component') {
if (name === 'svelte:component') {
// TODO post v2, treat this just as any other attribute
const index = element.attributes.findIndex(attr => attr.name === 'this');
if (!~index) {
@ -264,21 +257,11 @@ export default function tag(parser: Parser) {
element.end = parser.index;
} else if (name === 'style') {
// special case
if (parser.v2) {
const start = parser.index;
const data = parser.readUntil(/<\/style>/);
const end = parser.index;
element.children.push({ start, end, type: 'Text', data });
parser.eat('</style>', true);
} else {
element.children = readSequence(
parser,
() =>
parser.template.slice(parser.index, parser.index + 8) === '</style>'
);
parser.read(/<\/style>/);
element.end = parser.index;
}
const start = parser.index;
const data = parser.readUntil(/<\/style>/);
const end = parser.index;
element.children.push({ start, end, type: 'Text', data });
parser.eat('</style>', true);
} else {
parser.stack.push(element);
}
@ -287,10 +270,6 @@ export default function tag(parser: Parser) {
function readTagName(parser: Parser) {
const start = parser.index;
// TODO hoist these back to the top, post-v2
const SELF = parser.v2 ? 'svelte:self' : ':Self';
const COMPONENT = parser.v2 ? 'svelte:component' : ':Component';
if (parser.eat(SELF)) {
// check we're inside a block, otherwise this
// will cause infinite recursion
@ -334,14 +313,14 @@ function readTagName(parser: Parser) {
function readAttribute(parser: Parser, uniqueNames: Set<string>) {
const start = parser.index;
if (parser.eat(parser.v2 ? '{' : '{{')) {
if (parser.eat('{')) {
parser.allowWhitespace();
if (parser.eat('...')) {
const expression = readExpression(parser);
parser.allowWhitespace();
parser.eat(parser.v2 ? '}' : '}}', true);
parser.eat('}', true);
return {
start,
@ -350,13 +329,6 @@ function readAttribute(parser: Parser, uniqueNames: Set<string>) {
expression
};
} else {
if (!parser.v2) {
parser.error({
code: `expected-spread`,
message: 'Expected spread operator (...)'
});
}
const valueStart = parser.index;
const name = parser.readIdentifier();
@ -449,7 +421,7 @@ function readSequence(parser: Parser, done: () => boolean) {
});
return chunks;
} else if (parser.eat(parser.v2 ? '{' : '{{')) {
} else if (parser.eat('{')) {
if (currentChunk.data) {
currentChunk.end = index;
chunks.push(currentChunk);
@ -457,7 +429,7 @@ function readSequence(parser: Parser, done: () => boolean) {
const expression = readExpression(parser);
parser.allowWhitespace();
parser.eat(parser.v2 ? '}' : '}}', true);
parser.eat('}', true);
chunks.push({
start: index,

@ -9,7 +9,7 @@ export default function text(parser: Parser) {
while (
parser.index < parser.template.length &&
!parser.match('<') &&
!parser.match(parser.v2 ? '{' : '{{')
!parser.match('{')
) {
data += parser.template[parser.index++];
}

@ -2,29 +2,28 @@ import * as fs from 'fs';
import * as path from 'path';
import { compile } from '../index.ts';
let compileOptions = {};
let compileOptions = {
extensions: ['.html']
};
function capitalise(name) {
return name[0].toUpperCase() + name.slice(1);
}
export default function register(options) {
const { extensions } = options;
if (extensions) {
_deregister('.html');
extensions.forEach(_register);
if (options.extensions) {
compileOptions.extensions.forEach(deregisterExtension);
options.extensions.forEach(registerExtension);
}
// TODO make this the default and remove in v2
if (options) compileOptions = options;
compileOptions = options;
}
function _deregister(extension) {
function deregisterExtension(extension) {
delete require.extensions[extension];
}
function _register(extension) {
function registerExtension(extension) {
require.extensions[extension] = function(module, filename) {
const name = path.basename(filename)
.slice(0, -path.extname(filename).length)
@ -37,10 +36,10 @@ function _register(extension) {
generate: 'ssr'
});
const {code} = compile(fs.readFileSync(filename, 'utf-8'), options);
const { js } = compile(fs.readFileSync(filename, 'utf-8'), options);
return module._compile(code, filename);
return module._compile(js.code, filename);
};
}
_register('.html');
registerExtension('.html');

@ -29,7 +29,7 @@
"plugin:import/warnings"
],
"parserOptions": {
"ecmaVersion": 6,
"ecmaVersion": 9,
"sourceType": "module"
},
"settings": {

@ -9,18 +9,13 @@ fs.readdirSync(__dirname).forEach(file => {
const source = fs.readFileSync(path.join(__dirname, file), 'utf-8');
const ast = acorn.parse(source, {
ecmaVersion: 6,
ecmaVersion: 9,
sourceType: 'module'
});
ast.body.forEach(node => {
if (node.type !== 'ExportNamedDeclaration') return;
// check no ES6+ slipped in
acorn.parse(source.slice(node.declaration.start, node.end), {
ecmaVersion: 5
});
const declaration = node.declaration;
if (!declaration) return;

@ -51,13 +51,8 @@ export function fire(eventName, data) {
}
}
export function getDev(key) {
if (key) console.warn("`let x = component.get('x')` is deprecated. Use `let { x } = component.get()` instead");
return get.call(this, key);
}
export function get(key) {
return key ? this._state[key] : this._state;
export function get() {
return this._state;
}
export function init(component, options) {
@ -69,37 +64,7 @@ export function init(component, options) {
component.store = component.root.store || options.store;
}
export function observe(key, callback, options) {
var fn = callback.bind(this);
if (!options || options.init !== false) {
fn(this.get()[key], undefined);
}
return this.on(options && options.defer ? 'update' : 'state', function(event) {
if (event.changed[key]) fn(event.current[key], event.previous && event.previous[key]);
});
}
export function observeDev(key, callback, options) {
console.warn("this.observe(key, (newValue, oldValue) => {...}) is deprecated. Use\n\n // runs before DOM updates\n this.on('state', ({ changed, current, previous }) => {...});\n\n // runs after DOM updates\n this.on('update', ...);\n\n...or add the observe method from the svelte-extras package");
var c = (key = '' + key).search(/[.[]/);
if (c > -1) {
var message =
'The first argument to component.observe(...) must be the name of a top-level property';
if (c > 0)
message += ", i.e. '" + key.slice(0, c) + "' rather than '" + key + "'";
throw new Error(message);
}
return observe.call(this, key, callback, options);
}
export function on(eventName, handler) {
if (eventName === 'teardown') return this.on('destroy', handler);
var handlers = this._handlers[eventName] || (this._handlers[eventName] = []);
handlers.push(handler);
@ -111,17 +76,6 @@ export function on(eventName, handler) {
};
}
export function onDev(eventName, handler) {
if (eventName === 'teardown') {
console.warn(
"Use component.on('destroy', ...) instead of component.on('teardown', ...) which has been deprecated and will be unsupported in Svelte 2"
);
return this.on('destroy', handler);
}
return on.call(this, eventName, handler);
}
export function run(fn) {
fn();
}
@ -193,31 +147,27 @@ export function removeFromStore() {
}
export var proto = {
destroy: destroy,
get: get,
fire: fire,
observe: observe,
on: on,
set: set,
teardown: destroy,
destroy,
get,
fire,
on,
set,
_recompute: noop,
_set: _set,
_mount: _mount,
_unmount: _unmount,
_differs: _differs
_set,
_mount,
_unmount,
_differs
};
export var protoDev = {
destroy: destroyDev,
get: getDev,
fire: fire,
observe: observeDev,
on: onDev,
get,
fire,
on,
set: setDev,
teardown: destroyDev,
_recompute: noop,
_set: _set,
_mount: _mount,
_unmount: _unmount,
_differs: _differs
_set,
_mount,
_unmount,
_differs
};

@ -3,14 +3,14 @@ import getCodeFrame from '../utils/getCodeFrame';
class CompileError extends Error {
code: string;
loc: { line: number, column: number };
start: { line: number, column: number };
end: { line: number, column: number };
pos: number;
filename: string;
frame: string;
toString() {
return `${this.message} (${this.loc.line}:${this.loc.column})\n${this.frame}`;
return `${this.message} (${this.start.line}:${this.start.column})\n${this.frame}`;
}
}
@ -25,16 +25,16 @@ export default function error(message: string, props: {
const error = new CompileError(message);
error.name = props.name;
const start = locate(props.source, props.start);
const end = locate(props.source, props.end || props.start);
const start = locate(props.source, props.start, { offsetLine: 1 });
const end = locate(props.source, props.end || props.start, { offsetLine: 1 });
error.code = props.code;
error.loc = { line: start.line + 1, column: start.column };
error.end = { line: end.line + 1, column: end.column };
error.start = start;
error.end = end;
error.pos = props.start;
error.filename = props.filename;
error.frame = getCodeFrame(props.source, start.line, start.column);
error.frame = getCodeFrame(props.source, start.line - 1, start.column);
throw error;
}

@ -1,7 +1,7 @@
import isValidIdentifier from './isValidIdentifier';
import reservedNames from './reservedNames';
export default function quoteIfNecessary(name: string, legacy?: boolean) {
if (!isValidIdentifier(name) || (legacy && reservedNames.has(name))) return `"${name}"`;
export default function quoteIfNecessary(name) {
if (!isValidIdentifier(name)) return `"${name}"`;
return name;
}

@ -31,8 +31,6 @@ export default function validateHtml(validator: Validator, html: Node) {
else if (node.type === 'Element') {
const isComponent =
node.name === ':Self' ||
node.name === ':Component' ||
node.name === 'svelte:self' ||
node.name === 'svelte:component' ||
validator.components.has(node.name);

@ -19,8 +19,7 @@ export default function validateElement(
}
if (!isComponent && /^[A-Z]/.test(node.name[0])) {
// TODO upgrade to validator.error in v2
validator.warn(node, {
validator.error(node, {
code: `missing-component`,
message: `${node.name} component is not defined`
});
@ -298,7 +297,7 @@ function checkSlotAttribute(validator: Validator, node: Node, attribute: Node, s
const parent = stack[i];
if (parent.type === 'Element') {
// if we're inside a component or a custom element, gravy
if (parent.name === ':Self' || parent.name === ':Component' || parent.name === 'svelte:self' || parent.name === 'svelte:component' || validator.components.has(parent.name)) return;
if (parent.name === 'svelte:self' || parent.name === 'svelte:component' || validator.components.has(parent.name)) return;
if (/-/.test(parent.name)) return;
}

@ -31,35 +31,33 @@ export default function validateEventHandlerCallee(
return;
}
if (name === 'store' && attribute.expression.callee.type === 'MemberExpression') {
if (!validator.options.store) {
validator.warn(attribute.expression, {
code: `options-missing-store`,
message: 'compile with `store: true` in order to call store methods'
});
}
return;
}
if (
(callee.type === 'Identifier' && validBuiltins.has(callee.name)) ||
validator.methods.has(callee.name)
)
) {
return;
}
if (name[0] === '$') {
// assume it's a store method
return;
}
const validCallees = ['this.*', 'event.*', 'options.*', 'console.*'].concat(
validator.options.store ? 'store.*' : [],
Array.from(validBuiltins),
Array.from(validator.methods.keys())
);
let message = `'${validator.source.slice(
callee.start,
callee.end
)}' is an invalid callee (should be one of ${list(validCallees)})`;
let message = `'${validator.source.slice(callee.start, callee.end)}' is an invalid callee ` ;
if (callee.type === 'Identifier' && validator.helpers.has(callee.name)) {
message += `. '${callee.name}' exists on 'helpers', did you put it in the wrong place?`;
if (name === 'store') {
message += `(did you mean '$${validator.source.slice(callee.start + 6, callee.end)}(...)'?)`;
} else {
message += `(should be one of ${list(validCallees)})`;
if (callee.type === 'Identifier' && validator.helpers.has(callee.name)) {
message += `. '${callee.name}' exists on 'helpers', did you put it in the wrong place?`;
}
}
validator.warn(attribute.expression, {

@ -6,7 +6,7 @@ export default function validateHead(validator: Validator, node: Node, refs: Map
if (node.attributes.length) {
validator.error(node.attributes[0], {
code: `invalid-attribute`,
message: `<:Head> should not have any attributes or directives`
message: `<svelte:head> should not have any attributes or directives`
});
}

@ -23,7 +23,7 @@ export default function validateWindow(validator: Validator, node: Node, refs: M
validator.error(attribute.value, {
code: `invalid-binding`,
message: `Bindings on <:Window/> must be to top-level properties, e.g. '${parts[parts.length - 1]}' rather than '${parts.join('.')}'`
message: `Bindings on <svelte:window> must be to top-level properties, e.g. '${parts[parts.length - 1]}' rather than '${parts.join('.')}'`
});
}
@ -34,7 +34,7 @@ export default function validateWindow(validator: Validator, node: Node, refs: M
? 'innerHeight'
: fuzzymatch(attribute.name, validBindings);
const message = `'${attribute.name}' is not a valid binding on <:Window>`;
const message = `'${attribute.name}' is not a valid binding on <svelte:window>`;
if (match) {
validator.error(attribute, {

@ -5,15 +5,15 @@ import getCodeFrame from '../utils/getCodeFrame';
import Stats from '../Stats';
import error from '../utils/error';
import Stylesheet from '../css/Stylesheet';
import Stats from '../Stats';
import { Node, Parsed, CompileOptions, Warning } from '../interfaces';
export class Validator {
readonly source: string;
readonly filename: string;
readonly v2: boolean;
readonly stats: Stats;
options: CompileOptions;
onwarn: ({}) => void;
locator?: (pos: number) => Location;
namespace: string;
@ -34,12 +34,12 @@ export class Validator {
actions: Set<string>;
};
constructor(parsed: Parsed, source: string, options: CompileOptions) {
constructor(parsed: Parsed, source: string, stats: Stats, options: CompileOptions) {
this.source = source;
this.stats = stats;
this.filename = options.filename;
this.onwarn = options.onwarn;
this.options = options;
this.v2 = options.parser === 'v2';
this.namespace = null;
this.defaultExport = null;
@ -73,18 +73,18 @@ export class Validator {
}
warn(pos: { start: number, end: number }, { code, message }: { code: string, message: string }) {
if (!this.locator) this.locator = getLocator(this.source);
if (!this.locator) this.locator = getLocator(this.source, { offsetLine: 1 });
const start = this.locator(pos.start);
const end = this.locator(pos.end);
const frame = getCodeFrame(this.source, start.line, start.column);
const frame = getCodeFrame(this.source, start.line - 1, start.column);
this.onwarn({
this.stats.warn({
code,
message,
frame,
loc: { line: start.line + 1, column: start.column },
end: { line: end.line + 1, column: end.column },
start,
end,
pos: pos.start,
filename: this.filename,
toString: () => `${message} (${start.line + 1}:${start.column})\n${frame}`,
@ -96,9 +96,10 @@ export default function validate(
parsed: Parsed,
source: string,
stylesheet: Stylesheet,
stats: Stats,
options: CompileOptions
) {
const { onwarn, onerror, name, filename, store, dev, parser } = options;
const { onerror, name, filename, dev, parser } = options;
try {
if (name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(name)) {
@ -108,7 +109,7 @@ export default function validate(
if (name && /^[a-z]/.test(name)) {
const message = `options.name should be capitalised`;
onwarn({
stats.warn({
code: `options-lowercase-name`,
message,
filename,
@ -116,11 +117,9 @@ export default function validate(
});
}
const validator = new Validator(parsed, source, {
onwarn,
const validator = new Validator(parsed, source, stats, {
name,
filename,
store,
dev,
parser
});

@ -27,9 +27,9 @@ export default function components(validator: Validator, prop: Node) {
}
if (!/^[A-Z]/.test(name)) {
validator.warn(component, {
validator.error(component, {
code: `component-lowercase`,
message: `Component names should be capitalised`
message: `Component names must be capitalised`
});
}
});

@ -30,14 +30,14 @@ export default function computed(validator: Validator, prop: Node) {
if (!isValidIdentifier(name)) {
const suggestion = name.replace(/[^_$a-z0-9]/ig, '_').replace(/^\d/, '_$&');
validator.error(computation, {
validator.error(computation.key, {
code: `invalid-computed-name`,
message: `Computed property name '${name}' is invalid — must be a valid identifier such as ${suggestion}`
});
}
if (reservedNames.has(name)) {
validator.error(computation, {
validator.error(computation.key, {
code: `invalid-computed-name`,
message: `Computed property name '${name}' is invalid — cannot be a JavaScript reserved word`
});
@ -75,36 +75,19 @@ export default function computed(validator: Validator, prop: Node) {
});
}
if (validator.v2) {
if (params.length > 1) {
validator.error(computation.value, {
code: `invalid-computed-arguments`,
message: `Computed properties must take a single argument`
});
}
const param = params[0];
if (param.type !== 'ObjectPattern') {
// TODO in v2, allow the entire object to be passed in
validator.error(computation.value, {
code: `invalid-computed-argument`,
message: `Computed property argument must be a destructured object pattern`
});
}
} else {
params.forEach((param: Node) => {
const valid =
param.type === 'Identifier' ||
(param.type === 'AssignmentPattern' &&
param.left.type === 'Identifier');
if (params.length > 1) {
validator.error(computation.value, {
code: `invalid-computed-arguments`,
message: `Computed properties must take a single argument`
});
}
if (!valid) {
// TODO change this for v2
validator.error(param, {
code: `invalid-computed-arguments`,
message: `Computed properties cannot use destructuring in function parameters`
});
}
const param = params[0];
if (param.type !== 'ObjectPattern') {
// TODO post-v2, allow the entire object to be passed in
validator.error(computation.value, {
code: `invalid-computed-argument`,
message: `Computed property argument must be a destructured object pattern`
});
}
});

@ -4,13 +4,11 @@ import {
_differs,
_differsImmutable,
get,
observe,
on,
fire
} from './shared.js';
function Store(state, options) {
this._observers = { pre: blankObject(), post: blankObject() };
this._handlers = {};
this._dependents = [];
@ -110,20 +108,8 @@ assign(Store.prototype, {
get: get,
// TODO remove this method
observe: observe,
on: on,
onchange: function(callback) {
// TODO remove this method
console.warn("store.onchange is deprecated in favour of store.on('state', event => {...})");
return this.on('state', function(event) {
callback(event.current, event.changed);
});
},
set: function(newState) {
var oldState = this._state,
changed = this._changed = {},

@ -4,7 +4,7 @@ import { svelte, deindent } from "../helpers.js";
describe("create", () => {
it("should return a component constructor", () => {
const source = deindent`
<div>{{prop}}</div>
<div>{prop}</div>
`;
const component = svelte.create(source);
@ -13,7 +13,7 @@ describe("create", () => {
it("should throw error when source is invalid ", done => {
const source = deindent`
<div>{{prop}</div>
<div>{prop</div>
`;
const component = svelte.create(source, {
@ -27,7 +27,7 @@ describe("create", () => {
it("should return undefined when source is invalid ", () => {
const source = deindent`
<div>{{prop}</div>
<div>{prop</div>
`;
const component = svelte.create(source, {

@ -78,10 +78,10 @@ describe('css', () => {
);
// check the code is valid
checkCodeIsValid(dom.code);
checkCodeIsValid(ssr.code);
checkCodeIsValid(dom.js.code);
checkCodeIsValid(ssr.js.code);
assert.equal(dom.css.toString(), ssr.css.toString());
assert.equal(dom.css.code, ssr.css.code);
assert.deepEqual(
domWarnings.map(normalizeWarning),
@ -89,13 +89,13 @@ describe('css', () => {
);
assert.deepEqual(domWarnings.map(normalizeWarning), expectedWarnings);
fs.writeFileSync(`test/css/samples/${dir}/_actual.css`, dom.css);
fs.writeFileSync(`test/css/samples/${dir}/_actual.css`, dom.css.code);
const expected = {
html: read(`test/css/samples/${dir}/expected.html`),
css: read(`test/css/samples/${dir}/expected.css`)
};
assert.equal(dom.css.replace(/svelte(-ref)?-[a-z0-9]+/g, (m, $1) => $1 ? m : 'svelte-xyz'), expected.css);
assert.equal(dom.css.code.replace(/svelte(-ref)?-[a-z0-9]+/g, (m, $1) => $1 ? m : 'svelte-xyz'), expected.css);
// verify that the right elements have scoping selectors
if (expected.html !== null) {
@ -104,7 +104,7 @@ describe('css', () => {
// dom
try {
const Component = eval(
`(function () { ${dom.code}; return SvelteComponent; }())`
`(function () { ${dom.js.code}; return SvelteComponent; }())`
);
const target = window.document.querySelector('main');
@ -120,14 +120,14 @@ describe('css', () => {
window.document.head.innerHTML = ''; // remove added styles
} catch (err) {
console.log(dom.code);
console.log(dom.js.code);
throw err;
}
// ssr
try {
const component = eval(
`(function () { ${ssr.code}; return SvelteComponent; }())`
`(function () { ${ssr.js.code}; return SvelteComponent; }())`
);
assert.equal(
@ -138,7 +138,7 @@ describe('css', () => {
normalizeHtml(window, expected.html)
);
} catch (err) {
console.log(ssr.code);
console.log(ssr.js.code);
throw err;
}
}

@ -1,3 +0,0 @@
export default {
cascade: false
};

@ -1,3 +0,0 @@
export default {
cascade: false
};

@ -1 +1 @@
div.svelte-xyz,.svelte-xyz div{color:red}
div.svelte-xyz{color:red}

@ -1,3 +0,0 @@
export default {
cascade: false
};

@ -1,3 +0,0 @@
export default {
cascade: false
};

@ -1,3 +0,0 @@
export default {
cascade: false
};

@ -1,3 +0,0 @@
export default {
cascade: false
};

@ -1,3 +0,0 @@
export default {
cascade: false
};

@ -1 +0,0 @@
@keyframes svelte-xyz-why{0%{color:red}100%{color:blue}}.animated.svelte-xyz{animation:svelte-xyz-why 2s}.also-animated.svelte-xyz{animation:not-defined-here 2s}

@ -1,17 +0,0 @@
<div class='animated'>animated</div>
<div class='also-animated'>also animated</div>
<style>
@keyframes why {
0% { color: red; }
100% { color: blue; }
}
.animated {
animation: why 2s;
}
.also-animated {
animation: not-defined-here 2s;
}
</style>

@ -1,3 +0,0 @@
export default {
cascade: false
};

@ -1,3 +0,0 @@
export default {
cascade: false
};

@ -1,7 +0,0 @@
<div></div>
<style>
* {
color: red;
}
</style>

@ -1,3 +0,0 @@
export default {
cascade: false
};

@ -1 +0,0 @@
div.svelte-xyz{color:red}div.foo.svelte-xyz{color:blue}.foo.svelte-xyz{font-weight:bold}

@ -1,16 +0,0 @@
<div>red</div>
<div class='foo'>bold/blue</div>
<style>
div {
color: red;
}
div.foo {
color: blue;
}
.foo {
font-weight: bold;
}
</style>

@ -1,3 +0,0 @@
export default {
cascade: false
};

@ -1 +1 @@
div.svelte-xyz,.svelte-xyz div{--test:10}
div.svelte-xyz{--test:10}

@ -1,13 +1,17 @@
export default {
cascade: false,
warnings: [{
filename: "SvelteComponent.html",
code: `css-unused-selector`,
message: "Unused CSS selector",
loc: {
start: {
line: 4,
column: 1
column: 1,
character: 31
},
end: {
line: 4,
column: 3,
character: 33
},
pos: 31,
frame: `

@ -1 +1 @@
@keyframes svelte-xyz-why{0%{color:red}100%{color:blue}}.svelte-xyz.animated,.svelte-xyz .animated{animation:svelte-xyz-why 2s}
@keyframes svelte-xyz-why{0%{color:red}100%{color:blue}}.animated.svelte-xyz{animation:svelte-xyz-why 2s}.also-animated.svelte-xyz{animation:not-defined-here 2s}

@ -1,4 +1,5 @@
<div class='animated'>animated</div>
<div class='also-animated'>also animated</div>
<style>
@keyframes why {
@ -9,4 +10,8 @@
.animated {
animation: why 2s;
}
.also-animated {
animation: not-defined-here 2s;
}
</style>

@ -1 +1 @@
@media only screen and (min-width: 400px){div.svelte-xyz,.svelte-xyz div{color:red}}
@media only screen and (min-width: 400px){div.svelte-xyz{color:red}}

@ -1 +1 @@
@media(min-width: 400px){.svelte-xyz.large-screen,.svelte-xyz .large-screen{display:block}}
@media(min-width: 400px){.large-screen.svelte-xyz{display:block}}

@ -1,6 +1,4 @@
export default {
cascade: false,
data: {
dynamic: 'x'
}

@ -3,7 +3,7 @@
</span>
<span class='foo'>
<span class='bar'>{{dynamic}}</span>
<span class='bar'>{dynamic}</span>
</span>
<style>

@ -1,5 +1,4 @@
export default {
cascade: false,
data: {
dynamic: 'whatever'
}

@ -1,5 +1,5 @@
<div>
<p data-foo='{{dynamic}}'>this is styled</p>
<p data-foo='{dynamic}'>this is styled</p>
<p data-foo='baz'>this is unstyled</p>
</div>

@ -1,4 +1,4 @@
<p class='{{unknown}}'>this is styled</p>
<p class='{unknown}'>this is styled</p>
<p class='bar'>this is unstyled</p>
<style>

@ -1,6 +1,4 @@
export default {
cascade: false,
data: {
raw: '<p>raw</p>'
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save