pull/1721/head
Rich Harris 7 years ago
parent 49ed03c361
commit 8b803b7438

@ -26,6 +26,7 @@ import checkForComputedKeys from '../validate/js/utils/checkForComputedKeys';
import checkForDupes from '../validate/js/utils/checkForDupes';
import propValidators from '../validate/js/propValidators';
import fuzzymatch from '../validate/utils/fuzzymatch';
import flattenReference from '../utils/flattenReference';
interface Computation {
key: string;
@ -207,6 +208,7 @@ export default class Component {
// styles
this.stylesheet = new Stylesheet(source, ast, this.file, options.dev);
this.stylesheet.validate(this);
// allow compiler to deconflict user's `import { get } from 'whatever'` and
// Svelte's builtin `import { get, ... } from 'svelte/shared.ts'`;
@ -268,6 +270,25 @@ export default class Component {
}
});
}
this.refCallees.forEach(callee => {
const { parts } = flattenReference(callee);
const ref = parts[1];
if (this.refs.has(ref)) {
// TODO check method is valid, e.g. `audio.stop()` should be `audio.pause()`
} else {
const match = fuzzymatch(ref, Array.from(this.refs.keys()));
let message = `'refs.${ref}' does not exist`;
if (match) message += ` (did you mean 'refs.${match}'?)`;
this.error(callee, {
code: `missing-ref`,
message
});
}
});
}
addSourcemapLocations(node: Node) {
@ -488,22 +509,6 @@ export default class Component {
};
}
validate() {
const { filename } = this.options;
try {
if (this.stylesheet) {
this.stylesheet.validate(this);
}
} catch (err) {
if (onerror) {
onerror(err);
} else {
throw err;
}
}
}
error(
pos: {
start: number,

@ -67,29 +67,29 @@ export default function compile(source: string, options: CompileOptions) {
stats.start('parse');
ast = parse(source, options);
stats.stop('parse');
stats.start('create component');
const component = new Component(
ast,
source,
options.name || 'SvelteComponent',
options,
stats,
// TODO make component generator-agnostic, to allow e.g. WebGL generator
options.generate === 'ssr' ? new SsrTarget() : new DomTarget()
);
stats.stop('create component');
if (options.generate === false) {
return { ast, stats: stats.render(null), js: null, css: null };
}
const compiler = options.generate === 'ssr' ? generateSSR : generate;
return compiler(component, options);
} catch (err) {
options.onerror(err);
return;
}
stats.start('create component');
const component = new Component(
ast,
source,
options.name || 'SvelteComponent',
options,
stats,
// TODO make component generator-agnostic, to allow e.g. WebGL generator
options.generate === 'ssr' ? new SsrTarget() : new DomTarget()
);
stats.stop('create component');
if (options.generate === false) {
return { ast, stats: stats.render(null), js: null, css: null };
}
const compiler = options.generate === 'ssr' ? generateSSR : generate;
return compiler(component, options);
}

@ -27,8 +27,8 @@ export default class Animation extends Node {
});
}
const block = parent.block;
if (!block || block.type !== 'EachBlock' || !bloc.key) {
const block = parent.parent;
if (!block || block.type !== 'EachBlock' || !block.key) {
// TODO can we relax the 'immediate child' rule?
component.error(this, {
code: `invalid-animation`,
@ -36,12 +36,14 @@ export default class Animation extends Node {
});
}
if (block.children.length > 1) {
component.error(this, {
code: `invalid-animation`,
message: `An element that use the animate directive must be the sole child of a keyed each block`
});
}
// TODO reinstate this... it's tricky because we're
// in the process of *creating* block.children
// if (block.children.length > 1) {
// component.error(this, {
// code: `invalid-animation`,
// message: `An element that use the animate directive must be the sole child of a keyed each block`
// });
// }
this.expression = info.expression
? new Expression(component, this, scope, info.expression)

@ -9,5 +9,7 @@ export default class CatchBlock extends Node {
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = mapChildren(component, parent, scope, info.children);
this.warnIfEmptyBlock();
}
}

@ -52,6 +52,8 @@ export default class EachBlock extends Node {
this.children = mapChildren(component, this, this.scope, info.children);
this.warnIfEmptyBlock(); // TODO would be better if EachBlock, IfBlock etc extended an abstract Block class
this.else = info.else
? new ElseBlock(component, this, this.scope, info.else)
: null;

@ -152,9 +152,17 @@ export default class Element extends Node {
this.animation = null;
if (this.name === 'textarea') {
// this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute
if (info.children.length > 0) {
const valueAttribute = info.attributes.find(node => node.name === 'value');
if (valueAttribute) {
component.error(valueAttribute, {
code: `textarea-duplicate-value`,
message: `A <textarea> can have either a value attribute or (equivalently) child content, but not both`
});
}
// this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute
info.attributes.push({
type: 'Attribute',
name: 'value',

@ -10,5 +10,7 @@ export default class ElseBlock extends Node {
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = mapChildren(component, this, scope, info.children);
this.warnIfEmptyBlock();
}
}

@ -34,6 +34,8 @@ export default class IfBlock extends Node {
this.else = info.else
? new ElseBlock(component, this, scope, info.else)
: null;
this.warnIfEmptyBlock();
}
init(

@ -9,5 +9,7 @@ export default class PendingBlock extends Node {
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = mapChildren(component, parent, scope, info.children);
this.warnIfEmptyBlock();
}
}

@ -9,5 +9,7 @@ export default class ThenBlock extends Node {
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = mapChildren(component, parent, scope, info.children);
this.warnIfEmptyBlock();
}
}

@ -169,4 +169,17 @@ export default class Node {
remount(name: string) {
return `${this.var}.m(${name}._slotted.default, null);`;
}
warnIfEmptyBlock() {
if (!/Block$/.test(this.type) || !this.children) return;
if (this.children.length > 1) return;
const child = this.children[0];
if (!child || (child.type === 'Text' && !/[^ \r\n\f\v\t]/.test(child.data))) {
this.component.warn(this, {
code: 'empty-block',
message: 'Empty block'
});
}
}
}

@ -1,4 +1,3 @@
import validateElement from './validateElement';
import a11y from './a11y';
import fuzzymatch from '../utils/fuzzymatch'
import flattenReference from '../../utils/flattenReference';
@ -21,15 +20,6 @@ export default function validateHtml(validator: Validator, html: Node) {
function visit(node: Node) {
if (node.type === 'Element') {
validateElement(
validator,
node,
refs,
refCallees,
stack,
elementStack
);
a11y(validator, node, elementStack);
}
@ -74,23 +64,4 @@ export default function validateHtml(validator: Validator, html: Node) {
}
html.children.forEach(visit);
refCallees.forEach(callee => {
const { parts } = flattenReference(callee);
const ref = parts[1];
if (refs.has(ref)) {
// TODO check method is valid, e.g. `audio.stop()` should be `audio.pause()`
} else {
const match = fuzzymatch(ref, Array.from(refs.keys()));
let message = `'refs.${ref}' does not exist`;
if (match) message += ` (did you mean 'refs.${match}'?)`;
validator.error(callee, {
code: `missing-ref`,
message
});
}
});
}

@ -17,120 +17,7 @@ export default function validateElement(
elementStack: Node[]
) {
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Binding') {
const { name } = attribute;
if (name === 'value') {
if (
node.name !== 'input' &&
node.name !== 'textarea' &&
node.name !== 'select'
) {
component.error(attribute, {
code: `invalid-binding`,
message: `'value' is not a valid binding on <${node.name}> elements`
});
}
if (node.name === 'select') {
const attribute = node.attributes.find(
(attribute: Node) => attribute.name === 'multiple'
);
if (attribute && isDynamic(attribute)) {
component.error(attribute, {
code: `dynamic-multiple-attribute`,
message: `'multiple' attribute cannot be dynamic if select uses two-way binding`
});
}
} else {
checkTypeAttribute(component, node);
}
} else if (name === 'checked' || name === 'indeterminate') {
if (node.name !== 'input') {
component.error(attribute, {
code: `invalid-binding`,
message: `'${name}' is not a valid binding on <${node.name}> elements`
});
}
if (checkTypeAttribute(component, node) !== 'checkbox') {
component.error(attribute, {
code: `invalid-binding`,
message: `'${name}' binding can only be used with <input type="checkbox">`
});
}
} else if (name === 'group') {
if (node.name !== 'input') {
component.error(attribute, {
code: `invalid-binding`,
message: `'group' is not a valid binding on <${node.name}> elements`
});
}
const type = checkTypeAttribute(component, node);
if (type !== 'checkbox' && type !== 'radio') {
component.error(attribute, {
code: `invalid-binding`,
message: `'checked' binding can only be used with <input type="checkbox"> or <input type="radio">`
});
}
} else if (name == 'files') {
if (node.name !== 'input') {
component.error(attribute, {
code: `invalid-binding`,
message: `'files' binding acn only be used with <input type="file">`
});
}
const type = checkTypeAttribute(component, node);
if (type !== 'file') {
component.error(attribute, {
code: `invalid-binding`,
message: `'files' binding can only be used with <input type="file">`
});
}
} else if (
name === 'currentTime' ||
name === 'duration' ||
name === 'paused' ||
name === 'buffered' ||
name === 'seekable' ||
name === 'played' ||
name === 'volume'
) {
if (node.name !== 'audio' && node.name !== 'video') {
component.error(attribute, {
code: `invalid-binding`,
message: `'${name}' binding can only be used with <audio> or <video>`
});
}
} else if (dimensions.test(name)) {
if (node.name === 'svg' && (name === 'offsetWidth' || name === 'offsetHeight')) {
component.error(attribute, {
code: 'invalid-binding',
message: `'${attribute.name}' is not a valid binding on <svg>. Use '${name.replace('offset', 'client')}' instead`
});
} else if (svg.test(node.name)) {
component.error(attribute, {
code: 'invalid-binding',
message: `'${attribute.name}' is not a valid binding on SVG elements`
});
} else if (isVoidElementName(node.name)) {
component.error(attribute, {
code: 'invalid-binding',
message: `'${attribute.name}' is not a valid binding on void elements like <${node.name}>. Use a wrapper element instead`
});
}
} else {
component.error(attribute, {
code: `invalid-binding`,
message: `'${attribute.name}' is not a valid binding`
});
}
} else if (attribute.type === 'Attribute') {
if (attribute.type === 'Attribute') {
if (attribute.name === 'value' && node.name === 'textarea') {
if (node.children.length) {
component.error(attribute, {

Loading…
Cancel
Save