start refactoring, moving validation into nodes

pull/1721/head
Rich Harris 7 years ago
parent d08051b35c
commit 1dd9215714

@ -64,7 +64,7 @@ export default class Stats {
stop(label) {
if (label !== this.currentTiming.label) {
throw new Error(`Mismatched timing labels`);
throw new Error(`Mismatched timing labels (expected ${this.currentTiming.label}, got ${label})`);
}
this.currentTiming.end = now();

@ -20,6 +20,12 @@ import shared from './shared';
import { DomTarget } from './dom';
import { SsrTarget } from './ssr';
import { Node, GenerateOptions, ShorthandImport, Ast, CompileOptions, CustomElementOptions } from '../interfaces';
import error from '../utils/error';
import getCodeFrame from '../utils/getCodeFrame';
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';
interface Computation {
key: string;
@ -92,6 +98,8 @@ export default class Component {
tag: string;
props: string[];
properties: Map<string, Node>;
defaultExport: Node[];
imports: Node[];
shorthandImports: ShorthandImport[];
@ -110,6 +118,17 @@ export default class Component {
slots: Set<string>;
javascript: string;
used: {
components: Set<string>;
helpers: Set<string>;
events: Set<string>;
animations: Set<string>;
transitions: Set<string>;
actions: Set<string>;
};
refCallees: Node[];
code: MagicString;
bindingGroups: string[];
@ -128,16 +147,19 @@ export default class Component {
aliases: Map<string, string>;
usedNames: Set<string>;
locator: (search: number, startIndex?: number) => {
line: number,
column: number
};
constructor(
ast: Ast,
source: string,
name: string,
stylesheet: Stylesheet,
options: CompileOptions,
stats: Stats,
target: DomTarget | SsrTarget
) {
stats.start('compile');
this.stats = stats;
this.ast = ast;
@ -157,6 +179,17 @@ export default class Component {
this.importedComponents = new Map();
this.slots = new Set();
this.used = {
components: new Set(),
helpers: new Set(),
events: new Set(),
animations: new Set(),
transitions: new Set(),
actions: new Set(),
};
this.refCallees = [];
this.bindingGroups = [];
this.indirectDependencies = new Map();
@ -173,7 +206,7 @@ export default class Component {
this.usesRefs = false;
// styles
this.stylesheet = stylesheet;
this.stylesheet = new Stylesheet(source, ast, this.file, options.dev);
// allow compiler to deconflict user's `import { get } from 'whatever'` and
// Svelte's builtin `import { get, ... } from 'svelte/shared.ts'`;
@ -186,6 +219,7 @@ export default class Component {
this.computations = [];
this.templateProperties = {};
this.properties = new Map();
this.walkJs();
this.name = this.alias(name);
@ -207,7 +241,7 @@ export default class Component {
// this.walkTemplate();
if (!this.customElement) this.stylesheet.reify();
stylesheet.warnOnUnusedSelectors(options.onwarn);
this.stylesheet.warnOnUnusedSelectors(options.onwarn);
}
addSourcemapLocations(node: Node) {
@ -382,8 +416,6 @@ export default class Component {
})
};
this.stats.stop('compile');
return {
ast: this.ast,
js,
@ -430,298 +462,425 @@ export default class Component {
};
}
walkJs() {
const {
code,
source,
computations,
templateProperties,
imports
} = this;
validate() {
const { filename } = this.options;
const { js } = this.ast;
try {
if (this.stylesheet) {
this.stylesheet.validate(this);
}
} catch (err) {
if (onerror) {
onerror(err);
} else {
throw err;
}
}
}
const componentDefinition = new CodeBuilder();
error(
pos: {
start: number,
end: number
},
e : {
code: string,
message: string
}
) {
error(e.message, {
name: 'ValidationError',
code: e.code,
source: this.source,
start: pos.start,
end: pos.end,
filename: this.options.filename
});
}
if (js) {
this.addSourcemapLocations(js.content);
warn(
pos: {
start: number,
end: number
},
warning: {
code: string,
message: string
}
) {
if (!this.locator) {
this.locator = getLocator(this.source, { offsetLine: 1 });
}
const indentation = detectIndentation(source.slice(js.start, js.end));
const indentationLevel = getIndentationLevel(source, js.content.body[0].start);
const indentExclusionRanges = getIndentExclusionRanges(js.content);
const start = this.locator(pos.start);
const end = this.locator(pos.end);
const { scope, globals } = annotateWithScopes(js.content);
const frame = getCodeFrame(this.source, start.line - 1, start.column);
scope.declarations.forEach(name => {
this.userVars.add(name);
});
this.stats.warn({
code: warning.code,
message: warning.message,
frame,
start,
end,
pos: pos.start,
filename: this.options.filename,
toString: () => `${warning.message} (${start.line + 1}:${start.column})\n${frame}`,
});
}
globals.forEach(name => {
this.userVars.add(name);
processDefaultExport(node, componentDefinition, 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`
});
}
const body = js.content.body.slice(); // slice, because we're going to be mutating the original
checkForComputedKeys(this, node.declaration.properties);
checkForDupes(this, node.declaration.properties);
// imports need to be hoisted out of the IIFE
for (let i = 0; i < body.length; i += 1) {
const node = body[i];
if (node.type === 'ImportDeclaration') {
removeNode(code, js.content, node);
imports.push(node);
const props = this.properties;
node.specifiers.forEach((specifier: Node) => {
this.userVars.add(specifier.local.name);
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}'`
});
}
}
});
const defaultExport = this.defaultExport = body.find(
(node: Node) => node.type === 'ExportDefaultDeclaration'
);
if (props.has('namespace')) {
const ns = nodeToString(props.get('namespace').value);
this.namespace = namespaces[ns] || ns;
}
if (defaultExport) {
defaultExport.declaration.properties.forEach((prop: Node) => {
templateProperties[getName(prop.key)] = prop;
});
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));
});
}
['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 = (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}✂]` :
``;
if (body.type === 'BlockStatement') {
componentDefinition.addBlock(deindent`
${fnKeyword} ${name}(${paramString}) [${body.start}-${body.end}]
`);
} else {
componentDefinition.addBlock(deindent`
${fnKeyword} ${name}(${paramString}) {
return [${body.start}-${body.end}];
}
`);
}
};
const addFunctionExpression = (name: string, node: Node) => {
const { async } = node;
const fnKeyword = async ? 'async function' : 'function';
let c = node.start;
while (this.source[c] !== '(') c += 1;
componentDefinition.addBlock(deindent`
${fnKeyword} ${name}[${c}-${node.end}];
`);
};
const addValue = (name: string, node: Node) => {
componentDefinition.addBlock(deindent`
var ${name} = [${node.start}-${node.end}];
`);
};
const addDeclaration = (
key: string,
node: Node,
allowShorthandImport?: boolean,
disambiguator?: string,
conflicts?: Record<string, boolean>
) => {
const qualified = disambiguator ? `${disambiguator}-${key}` : key;
if (node.type === 'Identifier' && node.name === key) {
this.templateVars.set(qualified, key);
return;
const addArrowFunctionExpression = (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}✂]` :
``;
if (body.type === 'BlockStatement') {
componentDefinition.addBlock(deindent`
${fnKeyword} ${name}(${paramString}) [${body.start}-${body.end}]
`);
} else {
componentDefinition.addBlock(deindent`
${fnKeyword} ${name}(${paramString}) {
return [${body.start}-${body.end}];
}
`);
}
};
let deconflicted = key;
if (conflicts) while (deconflicted in conflicts) deconflicted += '_'
const addFunctionExpression = (name: string, node: Node) => {
const { async } = node;
const fnKeyword = async ? 'async function' : 'function';
let name = this.getUniqueName(deconflicted);
this.templateVars.set(qualified, name);
let c = node.start;
while (this.source[c] !== '(') c += 1;
componentDefinition.addBlock(deindent`
${fnKeyword} ${name}[${c}-${node.end}];
`);
};
if (allowShorthandImport && node.type === 'Literal' && typeof node.value === 'string') {
this.shorthandImports.push({ name, source: node.value });
return;
}
const addValue = (name: string, node: Node) => {
componentDefinition.addBlock(deindent`
var ${name} = [${node.start}-${node.end}];
`);
};
// deindent
const indentationLevel = getIndentationLevel(source, node.start);
if (indentationLevel) {
removeIndentation(code, node.start, node.end, indentationLevel, indentExclusionRanges);
}
const addDeclaration = (
key: string,
node: Node,
allowShorthandImport?: boolean,
disambiguator?: string,
conflicts?: Record<string, boolean>
) => {
const qualified = disambiguator ? `${disambiguator}-${key}` : key;
if (node.type === 'Identifier' && node.name === key) {
this.templateVars.set(qualified, key);
return;
}
if (node.type === 'ArrowFunctionExpression') {
addArrowFunctionExpression(name, node);
} else if (node.type === 'FunctionExpression') {
addFunctionExpression(name, node);
} else {
addValue(name, node);
}
};
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(name, node);
} else if (node.type === 'FunctionExpression') {
addFunctionExpression(name, node);
} else {
addValue(name, node);
}
};
if (templateProperties.components) {
templateProperties.components.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, true, 'components');
});
}
if (templateProperties.computed) {
const dependencies = new Map();
if (templateProperties.components) {
templateProperties.components.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, true, 'components');
const fullStateComputations = [];
templateProperties.computed.value.properties.forEach((prop: Node) => {
const key = getName(prop.key);
const value = prop.value;
addDeclaration(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);
}
});
if (templateProperties.computed) {
const dependencies = new Map();
const visited = new Set();
const fullStateComputations = [];
const visit = (key: string) => {
if (!dependencies.has(key)) return; // not a computation
templateProperties.computed.value.properties.forEach((prop: Node) => {
const key = getName(prop.key);
const value = prop.value;
if (visited.has(key)) return;
visited.add(key);
addDeclaration(key, value, false, 'computed', {
state: true,
changed: true
});
const deps = dependencies.get(key);
deps.forEach(visit);
const param = value.params[0];
computations.push({ key, deps, hasRestParam: false });
const hasRestParam = (
param.properties &&
param.properties.some(prop => prop.type === 'RestElement')
);
const prop = templateProperties.computed.value.properties.find((prop: Node) => getName(prop.key) === key);
};
if (param.type !== 'ObjectPattern' || hasRestParam) {
fullStateComputations.push({ key, deps: null, hasRestParam });
} else {
const deps = param.properties.map(prop => prop.key.name);
templateProperties.computed.value.properties.forEach((prop: Node) =>
visit(getName(prop.key))
);
deps.forEach(dep => {
this.expectedProperties.add(dep);
});
dependencies.set(key, deps);
}
});
if (fullStateComputations.length > 0) {
computations.push(...fullStateComputations);
}
}
const visited = new Set();
if (templateProperties.data) {
addDeclaration('data', templateProperties.data.value);
}
const visit = (key: string) => {
if (!dependencies.has(key)) return; // not a computation
if (templateProperties.events) {
templateProperties.events.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'events');
});
}
if (visited.has(key)) return;
visited.add(key);
if (templateProperties.helpers) {
templateProperties.helpers.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'helpers');
});
}
const deps = dependencies.get(key);
deps.forEach(visit);
if (templateProperties.methods) {
addDeclaration('methods', templateProperties.methods.value);
computations.push({ key, deps, hasRestParam: false });
templateProperties.methods.value.properties.forEach(prop => {
this.methods.add(prop.key.name);
});
}
const prop = templateProperties.computed.value.properties.find((prop: Node) => getName(prop.key) === key);
};
if (templateProperties.namespace) {
const ns = nodeToString(templateProperties.namespace.value);
this.namespace = namespaces[ns] || ns;
}
templateProperties.computed.value.properties.forEach((prop: Node) =>
visit(getName(prop.key))
);
if (templateProperties.oncreate) {
addDeclaration('oncreate', templateProperties.oncreate.value);
}
if (fullStateComputations.length > 0) {
computations.push(...fullStateComputations);
}
}
if (templateProperties.ondestroy) {
addDeclaration('ondestroy', templateProperties.ondestroy.value);
}
if (templateProperties.data) {
addDeclaration('data', templateProperties.data.value);
}
if (templateProperties.onstate) {
addDeclaration('onstate', templateProperties.onstate.value);
}
if (templateProperties.events) {
templateProperties.events.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'events');
});
}
if (templateProperties.onupdate) {
addDeclaration('onupdate', templateProperties.onupdate.value);
}
if (templateProperties.helpers) {
templateProperties.helpers.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'helpers');
});
}
if (templateProperties.preload) {
addDeclaration('preload', templateProperties.preload.value);
}
if (templateProperties.methods) {
addDeclaration('methods', templateProperties.methods.value);
if (templateProperties.props) {
this.props = templateProperties.props.value.elements.map((element: Node) => nodeToString(element));
}
templateProperties.methods.value.properties.forEach(prop => {
this.methods.add(prop.key.name);
});
}
if (templateProperties.setup) {
addDeclaration('setup', templateProperties.setup.value);
}
if (templateProperties.namespace) {
const ns = nodeToString(templateProperties.namespace.value);
this.namespace = namespaces[ns] || ns;
}
if (templateProperties.store) {
addDeclaration('store', templateProperties.store.value);
}
if (templateProperties.oncreate) {
addDeclaration('oncreate', templateProperties.oncreate.value);
}
if (templateProperties.tag) {
this.tag = nodeToString(templateProperties.tag.value);
}
if (templateProperties.ondestroy) {
addDeclaration('ondestroy', templateProperties.ondestroy.value);
}
if (templateProperties.transitions) {
templateProperties.transitions.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'transitions');
});
}
if (templateProperties.onstate) {
addDeclaration('onstate', templateProperties.onstate.value);
}
if (templateProperties.animations) {
templateProperties.animations.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'animations');
});
}
if (templateProperties.onupdate) {
addDeclaration('onupdate', templateProperties.onupdate.value);
}
if (templateProperties.actions) {
templateProperties.actions.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'actions');
});
}
if (templateProperties.preload) {
addDeclaration('preload', templateProperties.preload.value);
}
this.defaultExport = node;
}
if (templateProperties.props) {
this.props = templateProperties.props.value.elements.map((element: Node) => nodeToString(element));
}
walkJs() {
const {
code,
source,
imports
} = this;
if (templateProperties.setup) {
addDeclaration('setup', templateProperties.setup.value);
}
const { js } = this.ast;
if (templateProperties.store) {
addDeclaration('store', templateProperties.store.value);
}
const componentDefinition = new CodeBuilder();
if (templateProperties.tag) {
this.tag = nodeToString(templateProperties.tag.value);
}
if (js) {
this.addSourcemapLocations(js.content);
if (templateProperties.transitions) {
templateProperties.transitions.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'transitions');
const indentation = detectIndentation(source.slice(js.start, js.end));
const indentationLevel = getIndentationLevel(source, js.content.body[0].start);
const indentExclusionRanges = getIndentExclusionRanges(js.content);
const { scope, globals } = annotateWithScopes(js.content);
scope.declarations.forEach(name => {
this.userVars.add(name);
});
globals.forEach(name => {
this.userVars.add(name);
});
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 (templateProperties.animations) {
templateProperties.animations.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'animations');
});
if (node.type === 'ExportDefaultDeclaration') {
this.processDefaultExport(node, componentDefinition, indentExclusionRanges);
}
if (templateProperties.actions) {
templateProperties.actions.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'actions');
// imports need to be hoisted out of the IIFE
else if (node.type === 'ImportDeclaration') {
removeNode(code, js.content, node);
imports.push(node);
node.specifiers.forEach((specifier: Node) => {
this.userVars.add(specifier.local.name);
});
}
}
});
if (indentationLevel) {
if (defaultExport) {
removeIndentation(code, js.content.start, defaultExport.start, indentationLevel, indentExclusionRanges);
removeIndentation(code, defaultExport.end, js.content.end, indentationLevel, indentExclusionRanges);
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);
}
@ -733,11 +892,11 @@ export default class Component {
let b = js.content.end;
while (/\s/.test(source[b - 1])) b -= 1;
if (defaultExport) {
if (this.defaultExport) {
this.javascript = '';
if (a !== defaultExport.start) this.javascript += `[✂${a}-${defaultExport.start}✂]`;
if (a !== this.defaultExport.start) this.javascript += `[✂${a}-${this.defaultExport.start}✂]`;
if (!componentDefinition.isEmpty()) this.javascript += componentDefinition;
if (defaultExport.end !== b) this.javascript += `[✂${defaultExport.end}-${b}✂]`;
if (this.defaultExport.end !== b) this.javascript += `[✂${this.defaultExport.end}-${b}✂]`;
} else {
this.javascript = a === b ? null : `[✂${a}-${b}✂]`;
}

@ -27,22 +27,15 @@ export class DomTarget {
}
export default function dom(
ast: Ast,
source: string,
stylesheet: Stylesheet,
options: CompileOptions,
stats: Stats
component: Component,
options: CompileOptions
) {
const format = options.format || 'es';
const target = new DomTarget();
const component = new Component(ast, source, options.name || 'SvelteComponent', stylesheet, options, stats, target);
const {
computations,
name,
templateProperties,
namespace,
templateProperties
} = component;
component.fragment.build();
@ -61,14 +54,14 @@ export default function dom(
if (computations.length) {
computations.forEach(({ key, deps, hasRestParam }) => {
if (target.readonly.has(key)) {
if (component.target.readonly.has(key)) {
// <svelte:window> bindings
throw new Error(
`Cannot have a computed value '${key}' that clashes with a read-only property`
);
}
target.readonly.add(key);
component.target.readonly.add(key);
if (deps) {
deps.forEach(dep => {
@ -113,7 +106,7 @@ export default function dom(
`);
}
target.blocks.forEach(block => {
component.target.blocks.forEach(block => {
builder.addBlock(block.toString());
});
@ -165,7 +158,7 @@ export default function dom(
${component.usesRefs && `this.refs = {};`}
this._state = ${initialState.reduce((state, piece) => `@assign(${state}, ${piece})`)};
${storeProps.length > 0 && `this.store._add(this, [${storeProps.map(prop => `"${prop.slice(1)}"`)}]);`}
${target.metaBindings}
${component.target.metaBindings}
${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`}
${options.dev &&
Array.from(component.expectedProperties).map(prop => {
@ -234,7 +227,7 @@ export default function dom(
this._fragment.c();`}
this._mount(options.target, options.anchor);
${(component.hasComponents || target.hasComplexBindings || hasInitHooks || target.hasIntroTransitions) &&
${(component.hasComponents || component.target.hasComplexBindings || hasInitHooks || component.target.hasIntroTransitions) &&
`@flush(this);`}
}
`}
@ -277,7 +270,7 @@ export default function dom(
this.set({ [attr]: newValue });
}
${(component.hasComponents || target.hasComplexBindings || templateProperties.oncreate || target.hasIntroTransitions) && deindent`
${(component.hasComponents || component.target.hasComplexBindings || templateProperties.oncreate || component.target.hasIntroTransitions) && deindent`
connectedCallback() {
@flush(this);
}
@ -310,7 +303,7 @@ export default function dom(
builder.addBlock(deindent`
${options.dev && deindent`
${name}.prototype._checkReadOnly = function _checkReadOnly(newState) {
${Array.from(target.readonly).map(
${Array.from(component.target.readonly).map(
prop =>
`if ('${prop}' in newState && !this._updatingReadonlyProperty) throw new Error("${debugName}: Cannot set read-only property '${prop}'");`
)}

@ -1,11 +1,10 @@
import { assign } from '../shared';
import Stats from '../Stats';
import parse from '../parse/index';
import Stylesheet from '../css/Stylesheet';
import validate from '../validate';
import generate from './dom/index';
import generateSSR from './ssr/index';
import generate, { DomTarget } from './dom/index';
import generateSSR, { SsrTarget } from './ssr/index';
import { CompileOptions, Warning, Ast } from '../interfaces';
import Component from './Component';
function normalize_options(options: CompileOptions): CompileOptions {
let normalized = assign({ generate: 'dom' }, options);
@ -34,15 +33,37 @@ function default_onerror(error: Error) {
throw error;
}
export default function compile(source: string, _options: CompileOptions) {
const options = normalize_options(_options);
let ast: Ast;
function validate_options(options: CompileOptions, stats: Stats) {
const { name, filename } = options;
if (name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(name)) {
const error = new Error(`options.name must be a valid identifier (got '${name}')`);
throw error;
}
if (name && /^[a-z]/.test(name)) {
const message = `options.name should be capitalised`;
stats.warn({
code: `options-lowercase-name`,
message,
filename,
toString: () => message,
});
}
}
export default function compile(source: string, options: CompileOptions) {
options = normalize_options(options);
const stats = new Stats({
onwarn: options.onwarn
});
let ast: Ast;
try {
validate_options(options, stats);
stats.start('parse');
ast = parse(source, options);
stats.stop('parse');
@ -51,13 +72,18 @@ export default function compile(source: string, _options: CompileOptions) {
return;
}
stats.start('stylesheet');
const stylesheet = new Stylesheet(source, ast, options.filename, options.dev);
stats.stop('stylesheet');
stats.start('create component');
const component = new Component(
ast,
source,
options.name || 'SvelteComponent',
options,
stats,
stats.start('validate');
validate(ast, source, stylesheet, stats, options);
stats.stop('validate');
// 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 };
@ -65,5 +91,5 @@ export default function compile(source: string, _options: CompileOptions) {
const compiler = options.generate === 'ssr' ? generateSSR : generate;
return compiler(ast, source, stylesheet, options, stats);
return compiler(component, options);
}

@ -1,14 +1,12 @@
import CodeBuilder from '../../utils/CodeBuilder';
import deindent from '../../utils/deindent';
import { stringify } from '../../utils/stringify';
import flattenReference from '../../utils/flattenReference';
import isVoidElementName from '../../utils/isVoidElementName';
import validCalleeObjects from '../../utils/validCalleeObjects';
import reservedNames from '../../utils/reservedNames';
import Node from './shared/Node';
import Block from '../dom/Block';
import Binding from './Binding';
import EventHandler from './EventHandler';
import flattenReference from '../../utils/flattenReference';
import fuzzymatch from '../../validate/utils/fuzzymatch';
import list from '../../utils/list';
import validateEventHandlerCallee from '../../validate/html/validateEventHandler';
const associatedEvents = {
innerWidth: 'resize',
@ -33,6 +31,16 @@ const readonly = new Set([
'online',
]);
const validBindings = [
'innerWidth',
'innerHeight',
'outerWidth',
'outerHeight',
'scrollX',
'scrollY',
'online'
];
export default class Window extends Node {
type: 'Window';
handlers: EventHandler[];
@ -46,10 +54,50 @@ export default class Window extends Node {
info.attributes.forEach(node => {
if (node.type === 'EventHandler') {
component.used.events.add(node.name);
validateEventHandlerCallee(component, node); // TODO make this a method of component?
this.handlers.push(new EventHandler(component, this, scope, node));
} else if (node.type === 'Binding') {
}
else if (node.type === 'Binding') {
if (node.value.type !== 'Identifier') {
const { parts } = flattenReference(node.value);
component.error(node.value, {
code: `invalid-binding`,
message: `Bindings on <svelte:window> must be to top-level properties, e.g. '${parts[parts.length - 1]}' rather than '${parts.join('.')}'`
});
}
if (!~validBindings.indexOf(node.name)) {
const match = node.name === 'width'
? 'innerWidth'
: node.name === 'height'
? 'innerHeight'
: fuzzymatch(node.name, validBindings);
const message = `'${node.name}' is not a valid binding on <svelte:window>`;
if (match) {
component.error(node, {
code: `invalid-binding`,
message: `${message} (did you mean '${match}'?)`
});
} else {
component.error(node, {
code: `invalid-binding`,
message: `${message} — valid bindings are ${list(validBindings)}`
});
}
}
this.bindings.push(new Binding(component, this, scope, node));
}
else {
// TODO there shouldn't be anything else here...
}
});
}

@ -169,4 +169,8 @@ export default class Node {
remount(name: string) {
return `${this.var}.m(${name}._slotted.default, null);`;
}
validate() {
throw new Error(`${this.type} does not implement validate method`);
}
}

@ -32,17 +32,11 @@ export class SsrTarget {
}
export default function ssr(
ast: Ast,
source: string,
stylesheet: Stylesheet,
options: CompileOptions,
stats: Stats
component: Component,
options: CompileOptions
) {
const format = options.format || 'cjs';
const target = new SsrTarget();
const component = new Component(ast, source, options.name || 'SvelteComponent', stylesheet, options, stats, target);
const { computations, name, templateProperties } = component;
// create main render() function
@ -123,7 +117,7 @@ export default function ssr(
({ key }) => `ctx.${key} = %computed-${key}(ctx);`
)}
${target.bindings.length &&
${component.target.bindings.length &&
deindent`
var settled = false;
var tmp;
@ -131,11 +125,11 @@ export default function ssr(
while (!settled) {
settled = true;
${target.bindings.join('\n\n')}
${component.target.bindings.join('\n\n')}
}
`}
return \`${target.renderCode}\`;
return \`${component.target.renderCode}\`;
};
${name}.css = {

@ -3,20 +3,20 @@ import list from '../../utils/list';
import validate, { Validator } from '../index';
import validCalleeObjects from '../../utils/validCalleeObjects';
import { Node } from '../../interfaces';
import Component from '../../compile/Component';
const validBuiltins = new Set(['set', 'fire', 'destroy']);
export default function validateEventHandlerCallee(
validator: Validator,
attribute: Node,
refCallees: Node[]
component: Component,
attribute: Node
) {
if (!attribute.expression) return;
const { callee, type } = attribute.expression;
if (type !== 'CallExpression') {
validator.error(attribute.expression, {
component.error(attribute.expression, {
code: `invalid-event-handler`,
message: `Expected a call expression`
});
@ -27,13 +27,13 @@ export default function validateEventHandlerCallee(
if (validCalleeObjects.has(name) || name === 'options') return;
if (name === 'refs') {
refCallees.push(callee);
component.refCallees.push(callee);
return;
}
if (
(callee.type === 'Identifier' && validBuiltins.has(callee.name)) ||
validator.methods.has(callee.name)
component.methods.has(callee.name)
) {
return;
}
@ -45,22 +45,22 @@ export default function validateEventHandlerCallee(
const validCallees = ['this.*', 'refs.*', 'event.*', 'options.*', 'console.*'].concat(
Array.from(validBuiltins),
Array.from(validator.methods.keys())
Array.from(component.methods.keys())
);
let message = `'${validator.source.slice(callee.start, callee.end)}' is an invalid callee ` ;
let message = `'${component.source.slice(callee.start, callee.end)}' is an invalid callee ` ;
if (name === 'store') {
message += `(did you mean '$${validator.source.slice(callee.start + 6, callee.end)}(...)'?)`;
message += `(did you mean '$${component.source.slice(callee.start + 6, callee.end)}(...)'?)`;
} else {
message += `(should be one of ${list(validCallees)})`;
if (callee.type === 'Identifier' && validator.helpers.has(callee.name)) {
if (callee.type === 'Identifier' && component.helpers.has(callee.name)) {
message += `. '${callee.name}' exists on 'helpers', did you put it in the wrong place?`;
}
}
validator.warn(attribute.expression, {
component.warn(attribute.expression, {
code: `invalid-callee`,
message
});

@ -15,7 +15,12 @@ const validBindings = [
'online'
];
export default function validateWindow(validator: Validator, node: Node, refs: Map<string, Node[]>, refCallees: Node[]) {
export default function validateWindow(
validator: Validator,
node: Node,
refs: Map<string, Node[]>,
refCallees: Node[]
) {
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Binding') {
if (attribute.value.type !== 'Identifier') {

Loading…
Cancel
Save