[WIP] Refactor, change where validation occurs (#1721)

Refactor, change where validation occurs
pull/1744/head
Rich Harris 6 years ago committed by GitHub
parent b7e07c5389
commit 9031c16905
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

2
.gitignore vendored

@ -11,6 +11,8 @@ node_modules
/test/cli/samples/*/actual
/test/sourcemaps/samples/*/output.js
/test/sourcemaps/samples/*/output.js.map
/test/sourcemaps/samples/*/output.css
/test/sourcemaps/samples/*/output.css.map
/src/compile/shared.ts
/store.umd.js
/yarn-error.log

@ -1,5 +1,5 @@
import { Node, Warning } from './interfaces';
import Compiler from './compile/Compiler';
import Component from './compile/Component';
const now = (typeof process !== 'undefined' && process.hrtime)
? () => {
@ -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();
@ -73,14 +73,14 @@ export default class Stats {
this.currentChildren = this.currentTiming ? this.currentTiming.children : this.timings;
}
render(compiler: Compiler) {
render(component: Component) {
const timings = Object.assign({
total: now() - this.startTime
}, collapseTimings(this.timings));
// TODO would be good to have this info even
// if options.generate is false
const imports = compiler && compiler.imports.map(node => {
const imports = component && component.imports.map(node => {
return {
source: node.source.value,
specifiers: node.specifiers.map(specifier => {
@ -96,11 +96,11 @@ export default class Stats {
}
});
const hooks: Record<string, boolean> = compiler && {
oncreate: !!compiler.templateProperties.oncreate,
ondestroy: !!compiler.templateProperties.ondestroy,
onstate: !!compiler.templateProperties.onstate,
onupdate: !!compiler.templateProperties.onupdate
const hooks: Record<string, boolean> = component && {
oncreate: !!component.templateProperties.oncreate,
ondestroy: !!component.templateProperties.ondestroy,
onstate: !!component.templateProperties.onstate,
onupdate: !!component.templateProperties.onupdate
};
return {

@ -1,742 +0,0 @@
import { parseExpressionAt } from 'acorn';
import MagicString, { Bundle } from 'magic-string';
import isReference from 'is-reference';
import { walk, childKeys } from 'estree-walker';
import { getLocator } from 'locate-character';
import Stats from '../Stats';
import deindent from '../utils/deindent';
import CodeBuilder from '../utils/CodeBuilder';
import flattenReference from '../utils/flattenReference';
import reservedNames from '../utils/reservedNames';
import namespaces from '../utils/namespaces';
import { removeNode, removeObjectKey } from '../utils/removeNode';
import nodeToString from '../utils/nodeToString';
import wrapModule from './wrapModule';
import annotateWithScopes, { Scope } from '../utils/annotateWithScopes';
import getName from '../utils/getName';
import Stylesheet from '../css/Stylesheet';
import { test } from '../config';
import Fragment from './nodes/Fragment';
import shared from './shared';
import { DomTarget } from './dom/index';
import { SsrTarget } from './ssr/index';
import { Node, GenerateOptions, ShorthandImport, Ast, CompileOptions, CustomElementOptions } from '../interfaces';
interface Computation {
key: string;
deps: string[];
hasRestParam: boolean;
}
function detectIndentation(str: string) {
const pattern = /^[\t\s]{1,4}/gm;
let match;
while (match = pattern.exec(str)) {
if (match[0][0] === '\t') return '\t';
if (match[0].length === 2) return ' ';
}
return ' ';
}
function getIndentationLevel(str: string, b: number) {
let a = b;
while (a > 0 && str[a - 1] !== '\n') a -= 1;
return /^\s*/.exec(str.slice(a, b))[0];
}
function getIndentExclusionRanges(node: Node) {
const ranges: Node[] = [];
walk(node, {
enter(node: Node) {
if (node.type === 'TemplateElement') ranges.push(node);
}
});
return ranges;
}
function removeIndentation(
code: MagicString,
start: number,
end: number,
indentationLevel: string,
ranges: Node[]
) {
const str = code.original.slice(start, end);
const pattern = new RegExp(`^${indentationLevel}`, 'gm');
let match;
while (match = pattern.exec(str)) {
// TODO bail if we're inside an exclusion range
code.remove(start + match.index, start + match.index + indentationLevel.length);
}
}
// We need to tell estree-walker that it should always
// look for an `else` block, otherwise it might get
// the wrong idea about the shape of each/if blocks
childKeys.EachBlock = childKeys.IfBlock = ['children', 'else'];
childKeys.Attribute = ['value'];
export default class Compiler {
stats: Stats;
ast: Ast;
source: string;
name: string;
options: CompileOptions;
fragment: Fragment;
target: DomTarget | SsrTarget;
customElement: CustomElementOptions;
tag: string;
props: string[];
defaultExport: Node[];
imports: Node[];
shorthandImports: ShorthandImport[];
helpers: Set<string>;
components: Set<string>;
events: Set<string>;
methods: Set<string>;
animations: Set<string>;
transitions: Set<string>;
actions: Set<string>;
importedComponents: Map<string, string>;
namespace: string;
hasComponents: boolean;
computations: Computation[];
templateProperties: Record<string, Node>;
slots: Set<string>;
javascript: string;
code: MagicString;
bindingGroups: string[];
indirectDependencies: Map<string, Set<string>>;
expectedProperties: Set<string>;
usesRefs: boolean;
file: string;
fileVar: string;
locate: (c: number) => { line: number, column: number };
stylesheet: Stylesheet;
userVars: Set<string>;
templateVars: Map<string, string>;
aliases: Map<string, string>;
usedNames: Set<string>;
constructor(
ast: Ast,
source: string,
name: string,
stylesheet: Stylesheet,
options: CompileOptions,
stats: Stats,
dom: boolean,
target: DomTarget | SsrTarget
) {
stats.start('compile');
this.stats = stats;
this.ast = ast;
this.source = source;
this.options = options;
this.target = target;
this.imports = [];
this.shorthandImports = [];
this.helpers = new Set();
this.components = new Set();
this.events = new Set();
this.methods = new Set();
this.animations = new Set();
this.transitions = new Set();
this.actions = new Set();
this.importedComponents = new Map();
this.slots = new Set();
this.bindingGroups = [];
this.indirectDependencies = new Map();
this.file = options.filename && (
typeof process !== 'undefined' ? options.filename.replace(process.cwd(), '').replace(/^[\/\\]/, '') : options.filename
);
this.locate = getLocator(this.source);
// track which properties are needed, so we can provide useful info
// in dev mode
this.expectedProperties = new Set();
this.code = new MagicString(source);
this.usesRefs = false;
// styles
this.stylesheet = stylesheet;
// allow compiler to deconflict user's `import { get } from 'whatever'` and
// Svelte's builtin `import { get, ... } from 'svelte/shared.ts'`;
this.userVars = new Set();
this.templateVars = new Map();
this.aliases = new Map();
this.usedNames = new Set();
this.fileVar = options.dev && this.getUniqueName('file');
this.computations = [];
this.templateProperties = {};
this.walkJs(dom);
this.name = this.alias(name);
if (options.customElement === true) {
this.customElement = {
tag: this.tag,
props: this.props
}
} else {
this.customElement = options.customElement;
}
if (this.customElement && !this.customElement.tag) {
throw new Error(`No tag name specified`); // TODO better error
}
this.fragment = new Fragment(this, ast.html);
// this.walkTemplate();
if (!this.customElement) this.stylesheet.reify();
stylesheet.warnOnUnusedSelectors(options.onwarn);
}
addSourcemapLocations(node: Node) {
walk(node, {
enter: (node: Node) => {
this.code.addSourcemapLocation(node.start);
this.code.addSourcemapLocation(node.end);
},
});
}
alias(name: string) {
if (!this.aliases.has(name)) {
this.aliases.set(name, this.getUniqueName(name));
}
return this.aliases.get(name);
}
generate(result: string, options: CompileOptions, { banner = '', name, format }: GenerateOptions ) {
const pattern = /\[✂(\d+)-(\d+)$/;
const helpers = new Set();
// TODO use same regex for both
result = result.replace(options.generate === 'ssr' ? /(@+|#+|%+)(\w*(?:-\w*)?)/g : /(%+|@+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => {
if (sigil === '@') {
if (name in shared) {
if (options.dev && `${name}Dev` in shared) name = `${name}Dev`;
helpers.add(name);
}
return this.alias(name);
}
if (sigil === '%') {
return this.templateVars.get(name);
}
return sigil.slice(1) + name;
});
let importedHelpers;
if (options.shared) {
if (format !== 'es' && format !== 'cjs') {
throw new Error(`Components with shared helpers must be compiled with \`format: 'es'\` or \`format: 'cjs'\``);
}
importedHelpers = Array.from(helpers).sort().map(name => {
const alias = this.alias(name);
return { name, alias };
});
} else {
let inlineHelpers = '';
const compiler = this;
importedHelpers = [];
helpers.forEach(name => {
const str = shared[name];
const code = new MagicString(str);
const expression = parseExpressionAt(str, 0);
let { scope } = annotateWithScopes(expression);
walk(expression, {
enter(node: Node, parent: Node) {
if (node._scope) scope = node._scope;
if (
node.type === 'Identifier' &&
isReference(node, parent) &&
!scope.has(node.name)
) {
if (node.name in shared) {
// this helper function depends on another one
const dependency = node.name;
helpers.add(dependency);
const alias = compiler.alias(dependency);
if (alias !== node.name) {
code.overwrite(node.start, node.end, alias);
}
}
}
},
leave(node: Node) {
if (node._scope) scope = scope.parent;
},
});
if (name === 'transitionManager' || name === 'outros') {
// special case
const global = name === 'outros'
? `_svelteOutros`
: `_svelteTransitionManager`;
inlineHelpers += `\n\nvar ${this.alias(name)} = window.${global} || (window.${global} = ${code});\n\n`;
} else if (name === 'escaped' || name === 'missingComponent' || name === 'invalidAttributeNameCharacter') {
// vars are an awkward special case... would be nice to avoid this
const alias = this.alias(name);
inlineHelpers += `\n\nconst ${alias} = ${code};`
} else {
const alias = this.alias(expression.id.name);
if (alias !== expression.id.name) {
code.overwrite(expression.id.start, expression.id.end, alias);
}
inlineHelpers += `\n\n${code}`;
}
});
result += inlineHelpers;
}
const sharedPath = options.shared === true
? 'svelte/shared.js'
: options.shared || '';
const module = wrapModule(result, format, name, options, banner, sharedPath, importedHelpers, this.imports, this.shorthandImports, this.source);
const parts = module.split('✂]');
const finalChunk = parts.pop();
const compiled = new Bundle({ separator: '' });
function addString(str: string) {
compiled.addSource({
content: new MagicString(str),
});
}
const { filename } = options;
// special case — the source file doesn't actually get used anywhere. we need
// to add an empty file to populate map.sources and map.sourcesContent
if (!parts.length) {
compiled.addSource({
filename,
content: new MagicString(this.source).remove(0, this.source.length),
});
}
parts.forEach((str: string) => {
const chunk = str.replace(pattern, '');
if (chunk) addString(chunk);
const match = pattern.exec(str);
const snippet = this.code.snip(+match[1], +match[2]);
compiled.addSource({
filename,
content: snippet,
});
});
addString(finalChunk);
const css = this.customElement ?
{ code: null, map: null } :
this.stylesheet.render(options.cssOutputFilename, true);
const js = {
code: compiled.toString(),
map: compiled.generateMap({
includeContent: true,
file: options.outputFilename,
})
};
this.stats.stop('compile');
return {
ast: this.ast,
js,
css,
stats: this.stats.render(this)
};
}
getUniqueName(name: string) {
if (test) name = `${name}$`;
let alias = name;
for (
let i = 1;
reservedNames.has(alias) ||
this.userVars.has(alias) ||
this.usedNames.has(alias);
alias = `${name}_${i++}`
);
this.usedNames.add(alias);
return alias;
}
getUniqueNameMaker() {
const localUsedNames = new Set();
function add(name: string) {
localUsedNames.add(name);
}
reservedNames.forEach(add);
this.userVars.forEach(add);
return (name: string) => {
if (test) name = `${name}$`;
let alias = name;
for (
let i = 1;
this.usedNames.has(alias) ||
localUsedNames.has(alias);
alias = `${name}_${i++}`
);
localUsedNames.add(alias);
return alias;
};
}
walkJs(dom: boolean) {
const {
code,
source,
computations,
templateProperties,
imports
} = this;
const { js } = this.ast;
const componentDefinition = new CodeBuilder();
if (js) {
this.addSourcemapLocations(js.content);
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
// 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);
node.specifiers.forEach((specifier: Node) => {
this.userVars.add(specifier.local.name);
});
}
}
const defaultExport = this.defaultExport = body.find(
(node: Node) => node.type === 'ExportDefaultDeclaration'
);
if (defaultExport) {
defaultExport.declaration.properties.forEach((prop: Node) => {
templateProperties[getName(prop.key)] = prop;
});
['helpers', 'events', 'components', 'transitions', 'actions', 'animations'].forEach(key => {
if (templateProperties[key]) {
templateProperties[key].value.properties.forEach((prop: Node) => {
this[key].add(getName(prop.key));
});
}
});
const addArrowFunctionExpression = (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;
}
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();
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);
}
});
const visited = new Set();
const visit = (key: string) => {
if (!dependencies.has(key)) return; // not a computation
if (visited.has(key)) return;
visited.add(key);
const deps = dependencies.get(key);
deps.forEach(visit);
computations.push({ key, deps, hasRestParam: false });
const prop = templateProperties.computed.value.properties.find((prop: Node) => getName(prop.key) === key);
};
templateProperties.computed.value.properties.forEach((prop: Node) =>
visit(getName(prop.key))
);
if (fullStateComputations.length > 0) {
computations.push(...fullStateComputations);
}
}
if (templateProperties.data) {
addDeclaration('data', templateProperties.data.value);
}
if (templateProperties.events && dom) {
templateProperties.events.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'events');
});
}
if (templateProperties.helpers) {
templateProperties.helpers.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'helpers');
});
}
if (templateProperties.methods && dom) {
addDeclaration('methods', templateProperties.methods.value);
templateProperties.methods.value.properties.forEach(prop => {
this.methods.add(prop.key.name);
});
}
if (templateProperties.namespace) {
const ns = nodeToString(templateProperties.namespace.value);
this.namespace = namespaces[ns] || ns;
}
if (templateProperties.oncreate && dom) {
addDeclaration('oncreate', templateProperties.oncreate.value);
}
if (templateProperties.ondestroy && dom) {
addDeclaration('ondestroy', templateProperties.ondestroy.value);
}
if (templateProperties.onstate && dom) {
addDeclaration('onstate', templateProperties.onstate.value);
}
if (templateProperties.onupdate && dom) {
addDeclaration('onupdate', templateProperties.onupdate.value);
}
if (templateProperties.preload) {
addDeclaration('preload', templateProperties.preload.value);
}
if (templateProperties.props) {
this.props = templateProperties.props.value.elements.map((element: Node) => nodeToString(element));
}
if (templateProperties.setup) {
addDeclaration('setup', templateProperties.setup.value);
}
if (templateProperties.store) {
addDeclaration('store', templateProperties.store.value);
}
if (templateProperties.tag) {
this.tag = nodeToString(templateProperties.tag.value);
}
if (templateProperties.transitions) {
templateProperties.transitions.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'transitions');
});
}
if (templateProperties.animations) {
templateProperties.animations.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'animations');
});
}
if (templateProperties.actions) {
templateProperties.actions.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, false, 'actions');
});
}
}
if (indentationLevel) {
if (defaultExport) {
removeIndentation(code, js.content.start, defaultExport.start, indentationLevel, indentExclusionRanges);
removeIndentation(code, defaultExport.end, js.content.end, indentationLevel, indentExclusionRanges);
} else {
removeIndentation(code, js.content.start, js.content.end, indentationLevel, indentExclusionRanges);
}
}
let a = js.content.start;
while (/\s/.test(source[a])) a += 1;
let b = js.content.end;
while (/\s/.test(source[b - 1])) b -= 1;
if (defaultExport) {
this.javascript = '';
if (a !== defaultExport.start) this.javascript += `[✂${a}-${defaultExport.start}✂]`;
if (!componentDefinition.isEmpty()) this.javascript += componentDefinition;
if (defaultExport.end !== b) this.javascript += `[✂${defaultExport.end}-${b}✂]`;
} else {
this.javascript = a === b ? null : `[✂${a}-${b}✂]`;
}
}
}
}

@ -0,0 +1,947 @@
import { parseExpressionAt } from 'acorn';
import MagicString, { Bundle } from 'magic-string';
import isReference from 'is-reference';
import { walk, childKeys } from 'estree-walker';
import { getLocator } from 'locate-character';
import Stats from '../Stats';
import deindent from '../utils/deindent';
import CodeBuilder from '../utils/CodeBuilder';
import reservedNames from '../utils/reservedNames';
import namespaces from '../utils/namespaces';
import { removeNode } from '../utils/removeNode';
import nodeToString from '../utils/nodeToString';
import wrapModule from './wrapModule';
import annotateWithScopes from '../utils/annotateWithScopes';
import getName from '../utils/getName';
import Stylesheet from '../css/Stylesheet';
import { test } from '../config';
import Fragment from './nodes/Fragment';
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';
import flattenReference from '../utils/flattenReference';
interface Computation {
key: string;
deps: string[];
hasRestParam: boolean;
}
interface Declaration {
type: string;
name: string;
node: Node;
block: string;
}
function detectIndentation(str: string) {
const pattern = /^[\t\s]{1,4}/gm;
let match;
while (match = pattern.exec(str)) {
if (match[0][0] === '\t') return '\t';
if (match[0].length === 2) return ' ';
}
return ' ';
}
function getIndentationLevel(str: string, b: number) {
let a = b;
while (a > 0 && str[a - 1] !== '\n') a -= 1;
return /^\s*/.exec(str.slice(a, b))[0];
}
function getIndentExclusionRanges(node: Node) {
// TODO can we fold this into a different pass?
const ranges: Node[] = [];
walk(node, {
enter(node: Node) {
if (node.type === 'TemplateElement') ranges.push(node);
}
});
return ranges;
}
function removeIndentation(
code: MagicString,
start: number,
end: number,
indentationLevel: string,
ranges: Node[]
) {
const str = code.original.slice(start, end);
const pattern = new RegExp(`^${indentationLevel}`, 'gm');
let match;
while (match = pattern.exec(str)) {
// TODO bail if we're inside an exclusion range
code.remove(start + match.index, start + match.index + indentationLevel.length);
}
}
// We need to tell estree-walker that it should always
// look for an `else` block, otherwise it might get
// the wrong idea about the shape of each/if blocks
childKeys.EachBlock = childKeys.IfBlock = ['children', 'else'];
childKeys.Attribute = ['value'];
export default class Component {
stats: Stats;
ast: Ast;
source: string;
name: string;
options: CompileOptions;
fragment: Fragment;
target: DomTarget | SsrTarget;
customElement: CustomElementOptions;
tag: string;
props: string[];
properties: Map<string, Node>;
defaultExport: Node;
imports: Node[];
shorthandImports: ShorthandImport[];
helpers: Set<string>;
components: Set<string>;
events: Set<string>;
methods: Set<string>;
animations: Set<string>;
transitions: Set<string>;
actions: Set<string>;
importedComponents: Map<string, string>;
namespace: string;
hasComponents: boolean;
computations: Computation[];
templateProperties: Record<string, Node>;
slots: Set<string>;
javascript: [string, string];
used: {
components: Set<string>;
helpers: Set<string>;
events: Set<string>;
animations: Set<string>;
transitions: Set<string>;
actions: Set<string>;
};
declarations: Declaration[];
refCallees: Node[];
code: MagicString;
bindingGroups: string[];
indirectDependencies: Map<string, Set<string>>;
expectedProperties: Set<string>;
refs: Set<string>;
file: string;
fileVar: string;
locate: (c: number) => { line: number, column: number };
stylesheet: Stylesheet;
userVars: Set<string>;
templateVars: Map<string, string>;
aliases: Map<string, string>;
usedNames: Set<string>;
locator: (search: number, startIndex?: number) => {
line: number,
column: number
};
constructor(
ast: Ast,
source: string,
name: string,
options: CompileOptions,
stats: Stats,
target: DomTarget | SsrTarget
) {
this.stats = stats;
this.ast = ast;
this.source = source;
this.options = options;
this.target = target;
this.imports = [];
this.shorthandImports = [];
this.helpers = new Set();
this.components = new Set();
this.events = new Set();
this.methods = new Set();
this.animations = new Set();
this.transitions = new Set();
this.actions = new Set();
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.declarations = [];
this.refs = new Set();
this.refCallees = [];
this.bindingGroups = [];
this.indirectDependencies = new Map();
this.file = options.filename && (
typeof process !== 'undefined' ? options.filename.replace(process.cwd(), '').replace(/^[\/\\]/, '') : options.filename
);
this.locate = getLocator(this.source);
// track which properties are needed, so we can provide useful info
// in dev mode
this.expectedProperties = new Set();
this.code = new MagicString(source);
// styles
this.stylesheet = new Stylesheet(source, ast, options.filename, 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'`;
this.userVars = new Set();
this.templateVars = new Map();
this.aliases = new Map();
this.usedNames = new Set();
this.fileVar = options.dev && this.getUniqueName('file');
this.computations = [];
this.templateProperties = {};
this.properties = new Map();
this.walkJs();
this.name = this.alias(name);
if (options.customElement === true) {
this.customElement = {
tag: this.tag,
props: this.props
}
} else {
this.customElement = options.customElement;
}
if (this.customElement && !this.customElement.tag) {
throw new Error(`No tag name specified`); // TODO better error
}
this.fragment = new Fragment(this, ast.html);
// this.walkTemplate();
if (!this.customElement) this.stylesheet.reify();
this.stylesheet.warnOnUnusedSelectors(options.onwarn);
if (this.defaultExport) {
const categories = {
components: 'component',
// TODO helpers require a bit more work — need to analyse all expressions
// helpers: 'helper',
events: 'event definition',
transitions: 'transition',
actions: 'actions',
};
Object.keys(categories).forEach(category => {
const definitions = this.defaultExport.declaration.properties.find(prop => prop.key.name === category);
if (definitions) {
definitions.value.properties.forEach(prop => {
const { name } = prop.key;
if (!this.used[category].has(name)) {
this.warn(prop, {
code: `unused-${category.slice(0, -1)}`,
message: `The '${name}' ${categories[category]} is unused`
});
}
});
}
});
}
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) {
walk(node, {
enter: (node: Node) => {
this.code.addSourcemapLocation(node.start);
this.code.addSourcemapLocation(node.end);
},
});
}
alias(name: string) {
if (!this.aliases.has(name)) {
this.aliases.set(name, this.getUniqueName(name));
}
return this.aliases.get(name);
}
generate(result: string, options: CompileOptions, { banner = '', name, format }: GenerateOptions ) {
const pattern = /\[✂(\d+)-(\d+)$/;
const helpers = new Set();
// TODO use same regex for both
result = result.replace(options.generate === 'ssr' ? /(@+|#+|%+)(\w*(?:-\w*)?)/g : /(%+|@+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => {
if (sigil === '@') {
if (name in shared) {
if (options.dev && `${name}Dev` in shared) name = `${name}Dev`;
helpers.add(name);
}
return this.alias(name);
}
if (sigil === '%') {
return this.templateVars.get(name);
}
return sigil.slice(1) + name;
});
let importedHelpers;
if (options.shared) {
if (format !== 'es' && format !== 'cjs') {
throw new Error(`Components with shared helpers must be compiled with \`format: 'es'\` or \`format: 'cjs'\``);
}
importedHelpers = Array.from(helpers).sort().map(name => {
const alias = this.alias(name);
return { name, alias };
});
} else {
let inlineHelpers = '';
const component = this;
importedHelpers = [];
helpers.forEach(name => {
const str = shared[name];
const code = new MagicString(str);
const expression = parseExpressionAt(str, 0);
let { scope } = annotateWithScopes(expression);
walk(expression, {
enter(node: Node, parent: Node) {
if (node._scope) scope = node._scope;
if (
node.type === 'Identifier' &&
isReference(node, parent) &&
!scope.has(node.name)
) {
if (node.name in shared) {
// this helper function depends on another one
const dependency = node.name;
helpers.add(dependency);
const alias = component.alias(dependency);
if (alias !== node.name) {
code.overwrite(node.start, node.end, alias);
}
}
}
},
leave(node: Node) {
if (node._scope) scope = scope.parent;
},
});
if (name === 'transitionManager' || name === 'outros') {
// special case
const global = name === 'outros'
? `_svelteOutros`
: `_svelteTransitionManager`;
inlineHelpers += `\n\nvar ${this.alias(name)} = window.${global} || (window.${global} = ${code});\n\n`;
} else if (name === 'escaped' || name === 'missingComponent' || name === 'invalidAttributeNameCharacter') {
// vars are an awkward special case... would be nice to avoid this
const alias = this.alias(name);
inlineHelpers += `\n\nconst ${alias} = ${code};`
} else {
const alias = this.alias(expression.id.name);
if (alias !== expression.id.name) {
code.overwrite(expression.id.start, expression.id.end, alias);
}
inlineHelpers += `\n\n${code}`;
}
});
result += inlineHelpers;
}
const sharedPath = options.shared === true
? 'svelte/shared.js'
: options.shared || '';
const module = wrapModule(result, format, name, options, banner, sharedPath, importedHelpers, this.imports, this.shorthandImports, this.source);
const parts = module.split('✂]');
const finalChunk = parts.pop();
const compiled = new Bundle({ separator: '' });
function addString(str: string) {
compiled.addSource({
content: new MagicString(str),
});
}
const { filename } = options;
// special case — the source file doesn't actually get used anywhere. we need
// to add an empty file to populate map.sources and map.sourcesContent
if (!parts.length) {
compiled.addSource({
filename,
content: new MagicString(this.source).remove(0, this.source.length),
});
}
parts.forEach((str: string) => {
const chunk = str.replace(pattern, '');
if (chunk) addString(chunk);
const match = pattern.exec(str);
const snippet = this.code.snip(+match[1], +match[2]);
compiled.addSource({
filename,
content: snippet,
});
});
addString(finalChunk);
const css = this.customElement ?
{ code: null, map: null } :
this.stylesheet.render(options.cssOutputFilename, true);
const js = {
code: compiled.toString(),
map: compiled.generateMap({
includeContent: true,
file: options.outputFilename,
})
};
return {
ast: this.ast,
js,
css,
stats: this.stats.render(this)
};
}
getUniqueName(name: string) {
if (test) name = `${name}$`;
let alias = name;
for (
let i = 1;
reservedNames.has(alias) ||
this.userVars.has(alias) ||
this.usedNames.has(alias);
alias = `${name}_${i++}`
);
this.usedNames.add(alias);
return alias;
}
getUniqueNameMaker() {
const localUsedNames = new Set();
function add(name: string) {
localUsedNames.add(name);
}
reservedNames.forEach(add);
this.userVars.forEach(add);
return (name: string) => {
if (test) name = `${name}$`;
let alias = name;
for (
let i = 1;
this.usedNames.has(alias) ||
localUsedNames.has(alias);
alias = `${name}_${i++}`
);
localUsedNames.add(alias);
return alias;
};
}
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
});
}
warn(
pos: {
start: number,
end: number
},
warning: {
code: string,
message: string
}
) {
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 - 1, start.column);
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}`,
});
}
processDefaultExport(node, indentExclusionRanges) {
const { templateProperties, source, code } = this;
if (node.declaration.type !== 'ObjectExpression') {
this.error(node.declaration, {
code: `invalid-default-export`,
message: `Default export must be an object literal`
});
}
checkForComputedKeys(this, node.declaration.properties);
checkForDupes(this, node.declaration.properties);
const props = this.properties;
node.declaration.properties.forEach((prop: Node) => {
props.set(getName(prop.key), prop);
});
const validPropList = Object.keys(propValidators);
// ensure all exported props are valid
node.declaration.properties.forEach((prop: Node) => {
const name = getName(prop.key);
const propValidator = propValidators[name];
if (propValidator) {
propValidator(this, prop);
} else {
const match = fuzzymatch(name, validPropList);
if (match) {
this.error(prop, {
code: `unexpected-property`,
message: `Unexpected property '${name}' (did you mean '${match}'?)`
});
} else if (/FunctionExpression/.test(prop.value.type)) {
this.error(prop, {
code: `unexpected-property`,
message: `Unexpected property '${name}' (did you mean to include it in 'methods'?)`
});
} else {
this.error(prop, {
code: `unexpected-property`,
message: `Unexpected property '${name}'`
});
}
}
});
if (props.has('namespace')) {
const ns = nodeToString(props.get('namespace').value);
this.namespace = namespaces[ns] || ns;
}
node.declaration.properties.forEach((prop: Node) => {
templateProperties[getName(prop.key)] = prop;
});
['helpers', 'events', 'components', 'transitions', 'actions', 'animations'].forEach(key => {
if (templateProperties[key]) {
templateProperties[key].value.properties.forEach((prop: Node) => {
this[key].add(getName(prop.key));
});
}
});
const addArrowFunctionExpression = (type: string, name: string, node: Node) => {
const { body, params, async } = node;
const fnKeyword = async ? 'async function' : 'function';
const paramString = params.length ?
`[✂${params[0].start}-${params[params.length - 1].end}✂]` :
``;
const block = body.type === 'BlockStatement'
? deindent`
${fnKeyword} ${name}(${paramString}) [${body.start}-${body.end}]
`
: deindent`
${fnKeyword} ${name}(${paramString}) {
return [${body.start}-${body.end}];
}
`;
this.declarations.push({ type, name, block, node });
};
const addFunctionExpression = (type: string, name: string, node: Node) => {
const { async } = node;
const fnKeyword = async ? 'async function' : 'function';
let c = node.start;
while (this.source[c] !== '(') c += 1;
const block = deindent`
${fnKeyword} ${name}[${c}-${node.end}];
`;
this.declarations.push({ type, name, block, node });
};
const addValue = (type: string, name: string, node: Node) => {
const block = deindent`
var ${name} = [${node.start}-${node.end}];
`;
this.declarations.push({ type, name, block, node });
};
const addDeclaration = (
type: string,
key: string,
node: Node,
allowShorthandImport?: boolean,
disambiguator?: string,
conflicts?: Record<string, boolean>
) => {
const qualified = disambiguator ? `${disambiguator}-${key}` : key;
if (node.type === 'Identifier' && node.name === key) {
this.templateVars.set(qualified, key);
return;
}
let deconflicted = key;
if (conflicts) while (deconflicted in conflicts) deconflicted += '_'
let name = this.getUniqueName(deconflicted);
this.templateVars.set(qualified, name);
if (allowShorthandImport && node.type === 'Literal' && typeof node.value === 'string') {
this.shorthandImports.push({ name, source: node.value });
return;
}
// deindent
const indentationLevel = getIndentationLevel(source, node.start);
if (indentationLevel) {
removeIndentation(code, node.start, node.end, indentationLevel, indentExclusionRanges);
}
if (node.type === 'ArrowFunctionExpression') {
addArrowFunctionExpression(type, name, node);
} else if (node.type === 'FunctionExpression') {
addFunctionExpression(type, name, node);
} else {
addValue(type, name, node);
}
};
if (templateProperties.components) {
templateProperties.components.value.properties.forEach((property: Node) => {
addDeclaration('components', getName(property.key), property.value, true, 'components');
});
}
if (templateProperties.computed) {
const dependencies = new Map();
const fullStateComputations = [];
templateProperties.computed.value.properties.forEach((prop: Node) => {
const key = getName(prop.key);
const value = prop.value;
addDeclaration('computed', key, value, false, 'computed', {
state: true,
changed: true
});
const param = value.params[0];
const hasRestParam = (
param.properties &&
param.properties.some(prop => prop.type === 'RestElement')
);
if (param.type !== 'ObjectPattern' || hasRestParam) {
fullStateComputations.push({ key, deps: null, hasRestParam });
} else {
const deps = param.properties.map(prop => prop.key.name);
deps.forEach(dep => {
this.expectedProperties.add(dep);
});
dependencies.set(key, deps);
}
});
const visited = new Set();
const visit = (key: string) => {
if (!dependencies.has(key)) return; // not a computation
if (visited.has(key)) return;
visited.add(key);
const deps = dependencies.get(key);
deps.forEach(visit);
this.computations.push({ key, deps, hasRestParam: false });
const prop = templateProperties.computed.value.properties.find((prop: Node) => getName(prop.key) === key);
};
templateProperties.computed.value.properties.forEach((prop: Node) =>
visit(getName(prop.key))
);
if (fullStateComputations.length > 0) {
this.computations.push(...fullStateComputations);
}
}
if (templateProperties.data) {
addDeclaration('data', 'data', templateProperties.data.value);
}
if (templateProperties.events) {
templateProperties.events.value.properties.forEach((property: Node) => {
addDeclaration('events', getName(property.key), property.value, false, 'events');
});
}
if (templateProperties.helpers) {
templateProperties.helpers.value.properties.forEach((property: Node) => {
addDeclaration('helpers', getName(property.key), property.value, false, 'helpers');
});
}
if (templateProperties.methods) {
addDeclaration('methods', 'methods', templateProperties.methods.value);
templateProperties.methods.value.properties.forEach(property => {
this.methods.add(getName(property.key));
});
}
if (templateProperties.namespace) {
const ns = nodeToString(templateProperties.namespace.value);
this.namespace = namespaces[ns] || ns;
}
if (templateProperties.oncreate) {
addDeclaration('oncreate', 'oncreate', templateProperties.oncreate.value);
}
if (templateProperties.ondestroy) {
addDeclaration('ondestroy', 'ondestroy', templateProperties.ondestroy.value);
}
if (templateProperties.onstate) {
addDeclaration('onstate', 'onstate', templateProperties.onstate.value);
}
if (templateProperties.onupdate) {
addDeclaration('onupdate', 'onupdate', templateProperties.onupdate.value);
}
if (templateProperties.preload) {
addDeclaration('preload', 'preload', templateProperties.preload.value);
}
if (templateProperties.props) {
this.props = templateProperties.props.value.elements.map((element: Node) => nodeToString(element));
}
if (templateProperties.setup) {
addDeclaration('setup', 'setup', templateProperties.setup.value);
}
if (templateProperties.store) {
addDeclaration('store', 'store', templateProperties.store.value);
}
if (templateProperties.tag) {
this.tag = nodeToString(templateProperties.tag.value);
}
if (templateProperties.transitions) {
templateProperties.transitions.value.properties.forEach((property: Node) => {
addDeclaration('transitions', getName(property.key), property.value, false, 'transitions');
});
}
if (templateProperties.animations) {
templateProperties.animations.value.properties.forEach((property: Node) => {
addDeclaration('animations', getName(property.key), property.value, false, 'animations');
});
}
if (templateProperties.actions) {
templateProperties.actions.value.properties.forEach((property: Node) => {
addDeclaration('actions', getName(property.key), property.value, false, 'actions');
});
}
this.defaultExport = node;
}
walkJs() {
const { js } = this.ast;
if (!js) return;
this.addSourcemapLocations(js.content);
const { code, source, imports } = this;
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 (node.type === 'ExportDefaultDeclaration') {
this.processDefaultExport(node, indentExclusionRanges);
}
// 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 (this.defaultExport) {
removeIndentation(code, js.content.start, this.defaultExport.start, indentationLevel, indentExclusionRanges);
removeIndentation(code, this.defaultExport.end, js.content.end, indentationLevel, indentExclusionRanges);
} else {
removeIndentation(code, js.content.start, js.content.end, indentationLevel, indentExclusionRanges);
}
}
let a = js.content.start;
while (/\s/.test(source[a])) a += 1;
let b = js.content.end;
while (/\s/.test(source[b - 1])) b -= 1;
this.javascript = this.defaultExport
? [
a !== this.defaultExport.start ? `[✂${a}-${this.defaultExport.start}✂]` : '',
b !== this.defaultExport.end ?`[✂${this.defaultExport.end}-${b}✂]` : ''
]
: [
a !== b ? `[✂${a}-${b}✂]` : '',
''
];
}
}

@ -1,13 +1,12 @@
import CodeBuilder from '../../utils/CodeBuilder';
import deindent from '../../utils/deindent';
import { escape } from '../../utils/stringify';
import Compiler from '../Compiler';
import { Node } from '../../interfaces';
import Component from '../Component';
export interface BlockOptions {
parent?: Block;
name: string;
compiler?: Compiler;
component?: Component;
comment?: string;
key?: string;
bindings?: Map<string, string>;
@ -16,7 +15,7 @@ export interface BlockOptions {
export default class Block {
parent?: Block;
compiler: Compiler;
component: Component;
name: string;
comment?: string;
@ -59,7 +58,7 @@ export default class Block {
constructor(options: BlockOptions) {
this.parent = options.parent;
this.compiler = options.compiler;
this.component = options.component;
this.name = options.name;
this.comment = options.comment;
@ -91,7 +90,7 @@ export default class Block {
this.hasOutroMethod = false;
this.outros = 0;
this.getUniqueName = this.compiler.getUniqueNameMaker();
this.getUniqueName = this.component.getUniqueNameMaker();
this.variables = new Map();
this.aliases = new Map()
@ -129,11 +128,11 @@ export default class Block {
}
addIntro() {
this.hasIntros = this.hasIntroMethod = this.compiler.target.hasIntroTransitions = true;
this.hasIntros = this.hasIntroMethod = this.component.target.hasIntroTransitions = true;
}
addOutro() {
this.hasOutros = this.hasOutroMethod = this.compiler.target.hasOutroTransitions = true;
this.hasOutros = this.hasOutroMethod = this.component.target.hasOutroTransitions = true;
this.outros += 1;
}
@ -168,7 +167,7 @@ export default class Block {
}
toString() {
const { dev } = this.compiler.options;
const { dev } = this.component.options;
if (this.hasIntroMethod || this.hasOutroMethod) {
this.addVariable('#current');
@ -203,7 +202,7 @@ export default class Block {
properties.addBlock(`c: @noop,`);
} else {
const hydrate = !this.builders.hydrate.isEmpty() && (
this.compiler.options.hydratable
this.component.options.hydratable
? `this.h()`
: this.builders.hydrate
);
@ -216,7 +215,7 @@ export default class Block {
`);
}
if (this.compiler.options.hydratable) {
if (this.component.options.hydratable) {
if (this.builders.claim.isEmpty() && this.builders.hydrate.isEmpty()) {
properties.addBlock(`l: @noop,`);
} else {
@ -229,7 +228,7 @@ export default class Block {
}
}
if (this.compiler.options.hydratable && !this.builders.hydrate.isEmpty()) {
if (this.component.options.hydratable && !this.builders.hydrate.isEmpty()) {
properties.addBlock(deindent`
${dev ? 'h: function hydrate' : 'h'}() {
${this.builders.hydrate}

@ -1,19 +1,12 @@
import MagicString from 'magic-string';
import isReference from 'is-reference';
import { parseExpressionAt } from 'acorn';
import annotateWithScopes from '../../utils/annotateWithScopes';
import { walk } from 'estree-walker';
import deindent from '../../utils/deindent';
import { stringify, escape } from '../../utils/stringify';
import CodeBuilder from '../../utils/CodeBuilder';
import globalWhitelist from '../../utils/globalWhitelist';
import reservedNames from '../../utils/reservedNames';
import Compiler from '../Compiler';
import Component from '../Component';
import Stylesheet from '../../css/Stylesheet';
import Stats from '../../Stats';
import Block from './Block';
import { test } from '../../config';
import { Ast, CompileOptions, Node } from '../../interfaces';
import { Ast, CompileOptions } from '../../interfaces';
export class DomTarget {
blocks: (Block|string)[];
@ -34,28 +27,21 @@ 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 compiler = new Compiler(ast, source, options.name || 'SvelteComponent', stylesheet, options, stats, true, target);
const {
computations,
name,
templateProperties,
namespace,
} = compiler;
templateProperties
} = component;
compiler.fragment.build();
const { block } = compiler.fragment;
component.fragment.build();
const { block } = component.fragment;
if (compiler.options.nestedTransitions) {
if (component.options.nestedTransitions) {
block.hasOutroMethod = true;
}
@ -68,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 => {
@ -96,31 +82,42 @@ export default function dom(
});
}
if (compiler.javascript) {
builder.addBlock(compiler.javascript);
if (component.javascript) {
const componentDefinition = new CodeBuilder();
component.declarations.forEach(declaration => {
componentDefinition.addBlock(declaration.block);
});
const js = (
component.javascript[0] +
componentDefinition +
component.javascript[1]
);
builder.addBlock(js);
}
if (compiler.options.dev) {
builder.addLine(`const ${compiler.fileVar} = ${JSON.stringify(compiler.file)};`);
if (component.options.dev) {
builder.addLine(`const ${component.fileVar} = ${JSON.stringify(component.file)};`);
}
const css = compiler.stylesheet.render(options.filename, !compiler.customElement);
const styles = compiler.stylesheet.hasStyles && stringify(options.dev ?
const css = component.stylesheet.render(options.filename, !component.customElement);
const styles = component.stylesheet.hasStyles && stringify(options.dev ?
`${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */` :
css.code, { onlyEscapeAtSymbol: true });
if (styles && compiler.options.css !== false && !compiler.customElement) {
if (styles && component.options.css !== false && !component.customElement) {
builder.addBlock(deindent`
function @add_css() {
var style = @createElement("style");
style.id = '${compiler.stylesheet.id}-style';
style.id = '${component.stylesheet.id}-style';
style.textContent = ${styles};
@append(document.head, style);
}
`);
}
target.blocks.forEach(block => {
component.target.blocks.forEach(block => {
builder.addBlock(block.toString());
});
@ -137,10 +134,10 @@ export default function dom(
.join(',\n')}
}`;
const debugName = `<${compiler.customElement ? compiler.tag : name}>`;
const debugName = `<${component.customElement ? component.tag : name}>`;
// generate initial state object
const expectedProperties = Array.from(compiler.expectedProperties);
const expectedProperties = Array.from(component.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = expectedProperties.filter(prop => prop[0] === '$');
const initialState = [];
@ -165,32 +162,32 @@ export default function dom(
const constructorBody = deindent`
${options.dev && `this._debugName = '${debugName}';`}
${options.dev && !compiler.customElement &&
${options.dev && !component.customElement &&
`if (!options || (!options.target && !options.root)) throw new Error("'target' is a required option");`}
@init(this, options);
${templateProperties.store && `this.store = %store();`}
${compiler.usesRefs && `this.refs = {};`}
${component.refs.size > 0 && `this.refs = {};`}
this._state = ${initialState.reduce((state, piece) => `@assign(${state}, ${piece})`)};
${storeProps.length > 0 && `this.store._add(this, [${storeProps.map(prop => `"${prop.slice(1)}"`)}]);`}
${target.metaBindings}
${component.target.metaBindings}
${computations.length && `this._recompute({ ${Array.from(computationDeps).map(dep => `${dep}: 1`).join(', ')} }, this._state);`}
${options.dev &&
Array.from(compiler.expectedProperties).map(prop => {
Array.from(component.expectedProperties).map(prop => {
if (globalWhitelist.has(prop)) return;
if (computations.find(c => c.key === prop)) return;
const message = compiler.components.has(prop) ?
const message = component.components.has(prop) ?
`${debugName} expected to find '${prop}' in \`data\`, but found it in \`components\` instead` :
`${debugName} was created without expected data property '${prop}'`;
const conditions = [`!('${prop}' in this._state)`];
if (compiler.customElement) conditions.push(`!('${prop}' in this.attributes)`);
if (component.customElement) conditions.push(`!('${prop}' in this.attributes)`);
return `if (${conditions.join(' && ')}) console.warn("${message}");`
})}
${compiler.bindingGroups.length &&
`this._bindingGroups = [${Array(compiler.bindingGroups.length).fill('[]').join(', ')}];`}
this._intro = ${compiler.options.skipIntroByDefault ? '!!options.intro' : 'true'};
${component.bindingGroups.length &&
`this._bindingGroups = [${Array(component.bindingGroups.length).fill('[]').join(', ')}];`}
this._intro = ${component.options.skipIntroByDefault ? '!!options.intro' : 'true'};
${templateProperties.onstate && `this._handlers.state = [%onstate];`}
${templateProperties.onupdate && `this._handlers.update = [%onupdate];`}
@ -201,15 +198,15 @@ export default function dom(
}];`
)}
${compiler.slots.size && `this._slotted = options.slots || {};`}
${component.slots.size && `this._slotted = options.slots || {};`}
${compiler.customElement ?
${component.customElement ?
deindent`
this.attachShadow({ mode: 'open' });
${css.code && `this.shadowRoot.innerHTML = \`<style>${escape(css.code, { onlyEscapeAtSymbol: true }).replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`}
` :
(compiler.stylesheet.hasStyles && options.css !== false &&
`if (!document.getElementById("${compiler.stylesheet.id}-style")) @add_css();`)
(component.stylesheet.hasStyles && options.css !== false &&
`if (!document.getElementById("${component.stylesheet.id}-style")) @add_css();`)
}
${templateProperties.onstate && `%onstate.call(this, { changed: @assignTrue({}, this._state), current: this._state });`}
@ -223,14 +220,14 @@ export default function dom(
});
`}
${compiler.customElement ? deindent`
${component.customElement ? deindent`
this._fragment.c();
this._fragment.${block.hasIntroMethod ? 'i' : 'm'}(this.shadowRoot, null);
if (options.target) this._mount(options.target, options.anchor);
` : deindent`
if (options.target) {
${compiler.options.hydratable
${component.options.hydratable
? deindent`
var nodes = @children(options.target);
options.hydrate ? this._fragment.l(nodes) : this._fragment.c();
@ -241,16 +238,16 @@ export default function dom(
this._fragment.c();`}
this._mount(options.target, options.anchor);
${(compiler.hasComponents || target.hasComplexBindings || hasInitHooks || target.hasIntroTransitions) &&
${(component.hasComponents || component.target.hasComplexBindings || hasInitHooks || component.target.hasIntroTransitions) &&
`@flush(this);`}
}
`}
${compiler.options.skipIntroByDefault && `this._intro = true;`}
${component.options.skipIntroByDefault && `this._intro = true;`}
`;
if (compiler.customElement) {
const props = compiler.props || Array.from(compiler.expectedProperties);
if (component.customElement) {
const props = component.props || Array.from(component.expectedProperties);
builder.addBlock(deindent`
class ${name} extends HTMLElement {
@ -273,7 +270,7 @@ export default function dom(
}
`).join('\n\n')}
${compiler.slots.size && deindent`
${component.slots.size && deindent`
connectedCallback() {
Object.keys(this._slotted).forEach(key => {
this.appendChild(this._slotted[key]);
@ -284,7 +281,7 @@ export default function dom(
this.set({ [attr]: newValue });
}
${(compiler.hasComponents || target.hasComplexBindings || templateProperties.oncreate || target.hasIntroTransitions) && deindent`
${(component.hasComponents || component.target.hasComplexBindings || templateProperties.oncreate || component.target.hasIntroTransitions) && deindent`
connectedCallback() {
@flush(this);
}
@ -299,7 +296,7 @@ export default function dom(
}
});
customElements.define("${compiler.tag}", ${name});
customElements.define("${component.tag}", ${name});
`);
} else {
builder.addBlock(deindent`
@ -317,7 +314,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}'");`
)}
@ -339,8 +336,8 @@ export default function dom(
let result = builder.toString();
return compiler.generate(result, options, {
banner: `/* ${compiler.file ? `${compiler.file} ` : ``}generated by Svelte v${"__VERSION__"} */`,
return component.generate(result, options, {
banner: `/* ${component.file ? `${component.file} ` : ``}generated by Svelte v${"__VERSION__"} */`,
sharedPath,
name,
format,

@ -0,0 +1,95 @@
import { assign } from '../shared';
import Stats from '../Stats';
import parse from '../parse/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);
const { onwarn, onerror } = normalized;
normalized.onwarn = onwarn
? (warning: Warning) => onwarn(warning, default_onwarn)
: default_onwarn;
normalized.onerror = onerror
? (error: Error) => onerror(error, default_onerror)
: default_onerror;
return normalized;
}
function default_onwarn({ start, message }: Warning) {
if (start) {
console.warn(`(${start.line}:${start.column}) ${message}`);
} else {
console.warn(message);
}
}
function default_onerror(error: Error) {
throw error;
}
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');
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;
}
}

@ -6,13 +6,22 @@ export default class Action extends Node {
name: string;
expression: Expression;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
component.used.actions.add(this.name);
if (!component.actions.has(this.name)) {
component.error(this, {
code: `missing-action`,
message: `Missing action '${this.name}'`
});
}
this.expression = info.expression
? new Expression(compiler, this, scope, info.expression)
? new Expression(component, this, scope, info.expression)
: null;
}
}

@ -1,4 +1,3 @@
import Block from '../dom/Block';
import Node from './shared/Node';
import Expression from './shared/Expression';
@ -7,21 +6,40 @@ export default class Animation extends Node {
name: string;
expression: Expression;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
this.expression = info.expression
? new Expression(compiler, this, scope, info.expression)
: null;
}
component.used.animations.add(this.name);
if (parent.animation) {
component.error(this, {
code: `duplicate-animation`,
message: `An element can only have one 'animate' directive`
});
}
build(
block: Block,
parentNode: string,
parentNodes: string
) {
if (!component.animations.has(this.name)) {
component.error(this, {
code: `missing-animation`,
message: `Missing animation '${this.name}'`
});
}
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`,
message: `An element that use the animate directive must be the immediate child of a keyed each block`
});
}
block.hasAnimation = true;
this.expression = info.expression
? new Expression(component, this, scope, info.expression)
: null;
}
}

@ -2,7 +2,7 @@ import deindent from '../../utils/deindent';
import { escape, escapeTemplate, stringify } from '../../utils/stringify';
import fixAttributeCasing from '../../utils/fixAttributeCasing';
import addToSet from '../../utils/addToSet';
import Compiler from '../Compiler';
import Component from '../Component';
import Node from './shared/Node';
import Element from './Element';
import Text from './Text';
@ -19,7 +19,7 @@ export default class Attribute extends Node {
start: number;
end: number;
compiler: Compiler;
component: Component;
parent: Element;
name: string;
isSpread: boolean;
@ -31,8 +31,8 @@ export default class Attribute extends Node {
chunks: (Text | Expression)[];
dependencies: Set<string>;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
if (info.type === 'Spread') {
this.name = null;
@ -40,7 +40,7 @@ export default class Attribute extends Node {
this.isTrue = false;
this.isSynthetic = false;
this.expression = new Expression(compiler, this, scope, info.expression);
this.expression = new Expression(component, this, scope, info.expression);
this.dependencies = this.expression.dependencies;
this.chunks = null;
@ -60,7 +60,7 @@ export default class Attribute extends Node {
: info.value.map(node => {
if (node.type === 'Text') return node;
const expression = new Expression(compiler, this, scope, node.expression);
const expression = new Expression(component, this, scope, node.expression);
addToSet(this.dependencies, expression.dependencies);
return expression;
@ -98,6 +98,16 @@ export default class Attribute extends Node {
.join(' + ');
}
getStaticValue() {
if (this.isSpread || this.isDynamic) return null;
return this.isTrue
? true
: this.chunks[0]
? this.chunks[0].data
: '';
}
render(block: Block) {
const node = this.parent;
const name = fixAttributeCasing(this.name);
@ -136,9 +146,9 @@ export default class Attribute extends Node {
? '@setXlinkAttribute'
: '@setAttribute';
const isLegacyInputType = this.compiler.options.legacy && name === 'type' && this.parent.name === 'input';
const isLegacyInputType = this.component.options.legacy && name === 'type' && this.parent.name === 'input';
const isDataSet = /^data-/.test(name) && !this.compiler.options.legacy && !node.namespace;
const isDataSet = /^data-/.test(name) && !this.component.options.legacy && !node.namespace;
const camelCaseName = isDataSet ? name.replace('data-', '').replace(/(-\w)/g, function (m) {
return m[1].toUpperCase();
}) : name;

@ -17,18 +17,18 @@ export default class AwaitBlock extends Node {
then: ThenBlock;
catch: CatchBlock;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.expression = new Expression(compiler, this, scope, info.expression);
this.expression = new Expression(component, this, scope, info.expression);
const deps = this.expression.dependencies;
this.value = info.value;
this.error = info.error;
this.pending = new PendingBlock(compiler, this, scope, info.pending);
this.then = new ThenBlock(compiler, this, scope.add(this.value, deps), info.then);
this.catch = new CatchBlock(compiler, this, scope.add(this.error, deps), info.catch);
this.pending = new PendingBlock(component, this, scope, info.pending);
this.then = new ThenBlock(component, this, scope.add(this.value, deps), info.then);
this.catch = new CatchBlock(component, this, scope.add(this.error, deps), info.catch);
}
init(
@ -49,12 +49,12 @@ export default class AwaitBlock extends Node {
const child = this[status];
child.block = block.child({
comment: createDebuggingComment(child, this.compiler),
name: this.compiler.getUniqueName(`create_${status}_block`)
comment: createDebuggingComment(child, this.component),
name: this.component.getUniqueName(`create_${status}_block`)
});
child.initChildren(child.block, stripWhitespace, nextSibling);
this.compiler.target.blocks.push(child.block);
this.component.target.blocks.push(child.block);
if (child.block.dependencies.size > 0) {
isDynamic = true;
@ -77,7 +77,7 @@ export default class AwaitBlock extends Node {
this.then.block.hasOutroMethod = hasOutros;
this.catch.block.hasOutroMethod = hasOutros;
if (hasOutros && this.compiler.options.nestedTransitions) block.addOutro();
if (hasOutros && this.component.options.nestedTransitions) block.addOutro();
}
build(
@ -171,7 +171,7 @@ export default class AwaitBlock extends Node {
`);
}
if (this.pending.block.hasOutroMethod && this.compiler.options.nestedTransitions) {
if (this.pending.block.hasOutroMethod && this.component.options.nestedTransitions) {
const countdown = block.getUniqueName('countdown');
block.builders.outro.addBlock(deindent`
const ${countdown} = @callAfter(#outrocallback, 3);
@ -196,7 +196,7 @@ export default class AwaitBlock extends Node {
}
ssr() {
const target: SsrTarget = <SsrTarget>this.compiler.target;
const target: SsrTarget = <SsrTarget>this.component.target;
const { snippet } = this.expression;
target.append('${(function(__value) { if(@isPromise(__value)) return `');

@ -3,7 +3,7 @@ import Element from './Element';
import getObject from '../../utils/getObject';
import getTailSnippet from '../../utils/getTailSnippet';
import flattenReference from '../../utils/flattenReference';
import Compiler from '../Compiler';
import Component from '../Component';
import Block from '../dom/Block';
import Expression from './shared/Expression';
import { dimensions } from '../../utils/patterns';
@ -16,7 +16,7 @@ const readOnlyMediaAttributes = new Set([
]);
// TODO a lot of this element-specific stuff should live in Element —
// Binding should ideally be agnostic between Element and Component
// Binding should ideally be agnostic between Element and InlineComponent
export default class Binding extends Node {
name: string;
@ -26,11 +26,11 @@ export default class Binding extends Node {
obj: string;
prop: string;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
this.value = new Expression(compiler, this, scope, info.value);
this.value = new Expression(component, this, scope, info.value);
let obj;
let prop;
@ -78,7 +78,7 @@ export default class Binding extends Node {
// TODO should this happen in preprocess?
const dependencies = new Set(this.value.dependencies);
this.value.dependencies.forEach((prop: string) => {
const indirectDependencies = this.compiler.indirectDependencies.get(prop);
const indirectDependencies = this.component.indirectDependencies.get(prop);
if (indirectDependencies) {
indirectDependencies.forEach(indirectDependency => {
dependencies.add(indirectDependency);
@ -87,8 +87,8 @@ export default class Binding extends Node {
});
// view to model
const valueFromDom = getValueFromDom(this.compiler, node, this);
const handler = getEventHandler(this, this.compiler, block, name, snippet, dependencies, valueFromDom);
const valueFromDom = getValueFromDom(this.component, node, this);
const handler = getEventHandler(this, this.component, block, name, snippet, dependencies, valueFromDom);
// model to view
let updateDom = getDomUpdater(node, this, snippet);
@ -96,7 +96,7 @@ export default class Binding extends Node {
// special cases
if (this.name === 'group') {
const bindingGroup = getBindingGroup(this.compiler, this.value.node);
const bindingGroup = getBindingGroup(this.component, this.value.node);
block.builders.hydrate.addLine(
`#component._bindingGroups[${bindingGroup}].push(${node.var});`
@ -184,16 +184,16 @@ function getDomUpdater(
return `${node.var}.${binding.name} = ${snippet};`;
}
function getBindingGroup(compiler: Compiler, value: Node) {
function getBindingGroup(component: Component, value: Node) {
const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
const keypath = parts.join('.');
// TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context
let index = compiler.bindingGroups.indexOf(keypath);
let index = component.bindingGroups.indexOf(keypath);
if (index === -1) {
index = compiler.bindingGroups.length;
compiler.bindingGroups.push(keypath);
index = component.bindingGroups.length;
component.bindingGroups.push(keypath);
}
return index;
@ -201,7 +201,7 @@ function getBindingGroup(compiler: Compiler, value: Node) {
function getEventHandler(
binding: Binding,
compiler: Compiler,
component: Component,
block: Block,
name: string,
snippet: string,
@ -234,9 +234,9 @@ function getEventHandler(
// Svelte tries to `set()` a computed property, which throws an
// error in dev mode. a) it's possible that we should be
// replacing computations with *their* dependencies, and b)
// we should probably populate `compiler.target.readonly` sooner so
// we should probably populate `component.target.readonly` sooner so
// that we don't have to do the `.some()` here
dependenciesArray = dependenciesArray.filter(prop => !compiler.computations.some(computation => computation.key === prop));
dependenciesArray = dependenciesArray.filter(prop => !component.computations.some(computation => computation.key === prop));
return {
usesContext: false,
@ -270,7 +270,7 @@ function getEventHandler(
}
function getValueFromDom(
compiler: Compiler,
component: Component,
node: Element,
binding: Node
) {
@ -285,7 +285,7 @@ function getValueFromDom(
// <input type='checkbox' bind:group='foo'>
if (binding.name === 'group') {
const bindingGroup = getBindingGroup(compiler, binding.value.node);
const bindingGroup = getBindingGroup(component, binding.value.node);
if (type === 'checkbox') {
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
}

@ -6,8 +6,10 @@ export default class CatchBlock extends Node {
block: Block;
children: Node[];
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, parent, scope, info.children);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = mapChildren(component, parent, scope, info.children);
this.warnIfEmptyBlock();
}
}

@ -6,13 +6,13 @@ export default class Class extends Node {
name: string;
expression: Expression;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
this.expression = info.expression
? new Expression(compiler, this, scope, info.expression)
? new Expression(component, this, scope, info.expression)
: null;
}
}

@ -4,15 +4,15 @@ export default class Comment extends Node {
type: 'Comment';
data: string;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.data = info.data;
}
ssr() {
// Allow option to preserve comments, otherwise ignore
if (this.compiler.options.preserveComments) {
this.compiler.target.append(`<!--${this.data}-->`);
if (this.component.options.preserveComments) {
this.component.target.append(`<!--${this.data}-->`);
}
}
}

@ -9,11 +9,11 @@ import { stringify } from '../../utils/stringify';
export default class DebugTag extends Node {
expressions: Expression[];
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.expressions = info.identifiers.map(node => {
return new Expression(compiler, parent, scope, node);
return new Expression(component, parent, scope, node);
});
}
@ -22,9 +22,9 @@ export default class DebugTag extends Node {
parentNode: string,
parentNodes: string,
) {
if (!this.compiler.options.dev) return;
if (!this.component.options.dev) return;
const { code } = this.compiler;
const { code } = this.component;
if (this.expressions.length === 0) {
// Debug all
@ -36,7 +36,7 @@ export default class DebugTag extends Node {
block.builders.create.addLine(statement);
block.builders.update.addLine(statement);
} else {
const { code } = this.compiler;
const { code } = this.component;
code.overwrite(this.start + 1, this.start + 7, 'log', {
storeName: true
});
@ -70,10 +70,10 @@ export default class DebugTag extends Node {
}
ssr() {
if (!this.compiler.options.dev) return;
if (!this.component.options.dev) return;
const filename = this.compiler.file || null;
const { line, column } = this.compiler.locate(this.start + 1);
const filename = this.component.file || null;
const { line, column } = this.component.locate(this.start + 1);
const obj = this.expressions.length === 0
? `ctx`
@ -84,6 +84,6 @@ export default class DebugTag extends Node {
const str = '${@debug(' + `${filename && stringify(filename)}, ${line}, ${column}, ${obj})}`;
this.compiler.target.append(str);
this.component.target.append(str);
}
}

@ -20,14 +20,15 @@ export default class EachBlock extends Node {
key: Expression;
scope: TemplateScope;
contexts: Array<{ name: string, tail: string }>;
hasAnimation: boolean;
children: Node[];
else?: ElseBlock;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.expression = new Expression(compiler, this, scope, info.expression);
this.expression = new Expression(component, this, scope, info.expression);
this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring
this.index = info.index;
@ -37,11 +38,18 @@ export default class EachBlock extends Node {
unpackDestructuring(this.contexts, info.context, '');
this.contexts.forEach(context => {
if (component.helpers.has(context.key.name)) {
component.warn(context.key, {
code: `each-context-clash`,
message: `Context clashes with a helper. Rename one or the other to eliminate any ambiguity`
});
}
this.scope.add(context.key.name, this.expression.dependencies);
});
this.key = info.key
? new Expression(compiler, this, this.scope, info.key)
? new Expression(component, this, this.scope, info.key)
: null;
if (this.index) {
@ -50,10 +58,24 @@ export default class EachBlock extends Node {
this.scope.add(this.index, dependencies);
}
this.children = mapChildren(compiler, this, this.scope, info.children);
this.hasAnimation = false;
this.children = mapChildren(component, this, this.scope, info.children);
if (this.hasAnimation) {
if (this.children.length !== 1) {
const child = this.children.find(child => !!child.animation);
component.error(child.animation, {
code: `invalid-animation`,
message: `An element that use the animate directive must be the sole child of a keyed each block`
});
}
}
this.warnIfEmptyBlock(); // TODO would be better if EachBlock, IfBlock etc extended an abstract Block class
this.else = info.else
? new ElseBlock(compiler, this, this.scope, info.else)
? new ElseBlock(component, this, this.scope, info.else)
: null;
}
@ -66,22 +88,22 @@ export default class EachBlock extends Node {
this.var = block.getUniqueName(`each`);
this.iterations = block.getUniqueName(`${this.var}_blocks`);
this.get_each_context = this.compiler.getUniqueName(`get_${this.var}_context`);
this.get_each_context = this.component.getUniqueName(`get_${this.var}_context`);
const { dependencies } = this.expression;
block.addDependencies(dependencies);
this.block = block.child({
comment: createDebuggingComment(this, this.compiler),
name: this.compiler.getUniqueName('create_each_block'),
comment: createDebuggingComment(this, this.component),
name: this.component.getUniqueName('create_each_block'),
key: this.key,
bindings: new Map(block.bindings)
});
this.each_block_value = this.compiler.getUniqueName('each_value');
this.each_block_value = this.component.getUniqueName('each_value');
const indexName = this.index || this.compiler.getUniqueName(`${this.context}_index`);
const indexName = this.index || this.component.getUniqueName(`${this.context}_index`);
this.contexts.forEach(prop => {
this.block.bindings.set(prop.key.name, `ctx.${this.each_block_value}[ctx.${indexName}]${prop.tail}`);
@ -99,18 +121,18 @@ export default class EachBlock extends Node {
`child_ctx.${indexName} = i;`
);
this.compiler.target.blocks.push(this.block);
this.component.target.blocks.push(this.block);
this.initChildren(this.block, stripWhitespace, nextSibling);
block.addDependencies(this.block.dependencies);
this.block.hasUpdateMethod = this.block.dependencies.size > 0;
if (this.else) {
this.else.block = block.child({
comment: createDebuggingComment(this.else, this.compiler),
name: this.compiler.getUniqueName(`${this.block.name}_else`),
comment: createDebuggingComment(this.else, this.component),
name: this.component.getUniqueName(`${this.block.name}_else`),
});
this.compiler.target.blocks.push(this.else.block);
this.component.target.blocks.push(this.else.block);
this.else.initChildren(
this.else.block,
stripWhitespace,
@ -131,7 +153,7 @@ export default class EachBlock extends Node {
) {
if (this.children.length === 0) return;
const { compiler } = this;
const { component } = this;
const each = this.var;
@ -146,8 +168,8 @@ export default class EachBlock extends Node {
// hack the sourcemap, so that if data is missing the bug
// is easy to find
let c = this.start + 2;
while (compiler.source[c] !== 'e') c += 1;
compiler.code.overwrite(c, c + 4, 'length');
while (component.source[c] !== 'e') c += 1;
component.code.overwrite(c, c + 4, 'length');
const length = `[✂${c}-${c+4}✂]`;
const mountOrIntro = (this.block.hasIntroMethod || this.block.hasOutroMethod) ? 'i' : 'm';
@ -164,7 +186,7 @@ export default class EachBlock extends Node {
block.builders.init.addLine(`var ${this.each_block_value} = ${snippet};`);
this.compiler.target.blocks.push(deindent`
this.component.target.blocks.push(deindent`
function ${this.get_each_context}(ctx, list, i) {
const child_ctx = Object.create(ctx);
${this.contextProps}
@ -188,7 +210,7 @@ export default class EachBlock extends Node {
}
if (this.else) {
const each_block_else = compiler.getUniqueName(`${each}_else`);
const each_block_else = component.getUniqueName(`${each}_else`);
const mountOrIntro = (this.else.block.hasIntroMethod || this.else.block.hasOutroMethod) ? 'i' : 'm';
block.builders.init.addLine(`var ${each_block_else} = null;`);
@ -331,7 +353,7 @@ export default class EachBlock extends Node {
${this.block.hasAnimation && `for (let #i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].a();`}
`);
if (this.block.hasOutros && this.compiler.options.nestedTransitions) {
if (this.block.hasOutros && this.component.options.nestedTransitions) {
const countdown = block.getUniqueName('countdown');
block.builders.outro.addBlock(deindent`
const ${countdown} = @callAfter(#outrocallback, ${blocks}.length);
@ -477,7 +499,7 @@ export default class EachBlock extends Node {
`);
}
if (outroBlock && this.compiler.options.nestedTransitions) {
if (outroBlock && this.component.options.nestedTransitions) {
const countdown = block.getUniqueName('countdown');
block.builders.outro.addBlock(deindent`
${iterations} = ${iterations}.filter(Boolean);
@ -495,7 +517,7 @@ export default class EachBlock extends Node {
}
ssr() {
const { compiler } = this;
const { component } = this;
const { snippet } = this.expression;
const props = this.contexts.map(prop => `${prop.key.name}: item${prop.tail}`);
@ -505,23 +527,23 @@ export default class EachBlock extends Node {
: `item => Object.assign({}, ctx, { ${props.join(', ')} })`;
const open = `\${ ${this.else ? `${snippet}.length ? ` : ''}@each(${snippet}, ${getContext}, ctx => \``;
compiler.target.append(open);
component.target.append(open);
this.children.forEach((child: Node) => {
child.ssr();
});
const close = `\`)`;
compiler.target.append(close);
component.target.append(close);
if (this.else) {
compiler.target.append(` : \``);
component.target.append(` : \``);
this.else.children.forEach((child: Node) => {
child.ssr();
});
compiler.target.append(`\``);
component.target.append(`\``);
}
compiler.target.append('}');
component.target.append('}');
}
}

@ -6,7 +6,7 @@ import validCalleeObjects from '../../utils/validCalleeObjects';
import reservedNames from '../../utils/reservedNames';
import fixAttributeCasing from '../../utils/fixAttributeCasing';
import { quoteNameIfNecessary, quotePropIfNecessary } from '../../utils/quoteIfNecessary';
import Compiler from '../Compiler';
import Component from '../Component';
import Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';
@ -20,6 +20,8 @@ import Text from './Text';
import * as namespaces from '../../utils/namespaces';
import mapChildren from './shared/mapChildren';
import { dimensions } from '../../utils/patterns';
import fuzzymatch from '../validate/utils/fuzzymatch';
import Ref from './Ref';
// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7
const booleanAttributes = new Set([
@ -62,6 +64,47 @@ const booleanAttributes = new Set([
'translate'
]);
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/;
const ariaAttributes = 'activedescendant atomic autocomplete busy checked controls current describedby details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription selected setsize sort valuemax valuemin valuenow valuetext'.split(' ');
const ariaAttributeSet = new Set(ariaAttributes);
const ariaRoles = 'alert alertdialog application article banner button cell checkbox columnheader combobox command complementary composite contentinfo definition dialog directory document feed figure form grid gridcell group heading img input landmark link list listbox listitem log main marquee math menu menubar menuitem menuitemcheckbox menuitemradio navigation none note option presentation progressbar radio radiogroup range region roletype row rowgroup rowheader scrollbar search searchbox section sectionhead select separator slider spinbutton status structure switch tab table tablist tabpanel term textbox timer toolbar tooltip tree treegrid treeitem widget window'.split(' ');
const ariaRoleSet = new Set(ariaRoles);
const a11yRequiredAttributes = {
a: ['href'],
area: ['alt', 'aria-label', 'aria-labelledby'],
// html-has-lang
html: ['lang'],
// iframe-has-title
iframe: ['title'],
img: ['alt'],
object: ['title', 'aria-label', 'aria-labelledby']
};
const a11yDistractingElements = new Set([
'blink',
'marquee'
]);
const a11yRequiredContent = new Set([
// anchor-has-content
'a',
// heading-has-content
'h1',
'h2',
'h3',
'h4',
'h5',
'h6'
])
const invisibleElements = new Set(['meta', 'html', 'script', 'style']);
export default class Element extends Node {
type: 'Element';
name: string;
@ -77,18 +120,25 @@ export default class Element extends Node {
animation?: Animation;
children: Node[];
ref: string;
ref: Ref;
namespace: string;
constructor(compiler, parent, scope, info: any) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info: any) {
super(component, parent, scope, info);
this.name = info.name;
this.scope = scope;
const parentElement = parent.findNearest(/^Element/);
this.namespace = this.name === 'svg' ?
namespaces.svg :
parentElement ? parentElement.namespace : this.compiler.namespace;
parentElement ? parentElement.namespace : this.component.namespace;
if (!this.namespace && svg.test(this.name)) {
this.component.warn(this, {
code: `missing-namespace`,
message: `<${this.name}> is an SVG element did you forget to add { namespace: 'svg' } ?`
});
}
this.attributes = [];
this.actions = [];
@ -102,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',
@ -134,7 +192,7 @@ export default class Element extends Node {
info.attributes.forEach(node => {
switch (node.type) {
case 'Action':
this.actions.push(new Action(compiler, this, scope, node));
this.actions.push(new Action(component, this, scope, node));
break;
case 'Attribute':
@ -142,37 +200,33 @@ export default class Element extends Node {
// special case
if (node.name === 'xmlns') this.namespace = node.value[0].data;
this.attributes.push(new Attribute(compiler, this, scope, node));
this.attributes.push(new Attribute(component, this, scope, node));
break;
case 'Binding':
this.bindings.push(new Binding(compiler, this, scope, node));
this.bindings.push(new Binding(component, this, scope, node));
break;
case 'Class':
this.classes.push(new Class(compiler, this, scope, node));
this.classes.push(new Class(component, this, scope, node));
break;
case 'EventHandler':
this.handlers.push(new EventHandler(compiler, this, scope, node));
this.handlers.push(new EventHandler(component, this, scope, node));
break;
case 'Transition':
const transition = new Transition(compiler, this, scope, node);
const transition = new Transition(component, this, scope, node);
if (node.intro) this.intro = transition;
if (node.outro) this.outro = transition;
break;
case 'Animation':
this.animation = new Animation(compiler, this, scope, node);
this.animation = new Animation(component, this, scope, node);
break;
case 'Ref':
// TODO catch this in validation
if (this.ref) throw new Error(`Duplicate refs`);
compiler.usesRefs = true
this.ref = node.name;
this.ref = new Ref(component, this, scope, node);
break;
default:
@ -180,9 +234,384 @@ export default class Element extends Node {
}
});
this.children = mapChildren(compiler, this, scope, info.children);
this.children = mapChildren(component, this, scope, info.children);
this.validate();
component.stylesheet.apply(this);
}
validate() {
if (a11yDistractingElements.has(this.name)) {
// no-distracting-elements
this.component.warn(this, {
code: `a11y-distracting-elements`,
message: `A11y: Avoid <${this.name}> elements`
});
}
compiler.stylesheet.apply(this);
if (this.name === 'figcaption') {
if (this.parent.name !== 'figure') {
this.component.warn(this, {
code: `a11y-structure`,
message: `A11y: <figcaption> must be an immediate child of <figure>`
});
}
}
if (this.name === 'figure') {
const children = this.children.filter(node => {
if (node.type === 'Comment') return false;
if (node.type === 'Text') return /\S/.test(node.data);
return true;
});
const index = children.findIndex(child => child.name === 'figcaption');
if (index !== -1 && (index !== 0 && index !== children.length - 1)) {
this.component.warn(children[index], {
code: `a11y-structure`,
message: `A11y: <figcaption> must be first or last child of <figure>`
});
}
}
this.validateAttributes();
this.validateBindings();
this.validateContent();
}
validateAttributes() {
const { component } = this;
const attributeMap = new Map();
this.attributes.forEach(attribute => {
if (attribute.isSpread) return;
const name = attribute.name.toLowerCase();
// aria-props
if (name.startsWith('aria-')) {
if (invisibleElements.has(this.name)) {
// aria-unsupported-elements
component.warn(attribute, {
code: `a11y-aria-attributes`,
message: `A11y: <${this.name}> should not have aria-* attributes`
});
}
const type = name.slice(5);
if (!ariaAttributeSet.has(type)) {
const match = fuzzymatch(type, ariaAttributes);
let message = `A11y: Unknown aria attribute 'aria-${type}'`;
if (match) message += ` (did you mean '${match}'?)`;
component.warn(attribute, {
code: `a11y-unknown-aria-attribute`,
message
});
}
if (name === 'aria-hidden' && /^h[1-6]$/.test(this.name)) {
component.warn(attribute, {
code: `a11y-hidden`,
message: `A11y: <${this.name}> element should not be hidden`
});
}
}
// aria-role
if (name === 'role') {
if (invisibleElements.has(this.name)) {
// aria-unsupported-elements
component.warn(attribute, {
code: `a11y-misplaced-role`,
message: `A11y: <${this.name}> should not have role attribute`
});
}
const value = attribute.getStaticValue();
if (value && !ariaRoleSet.has(value)) {
const match = fuzzymatch(value, ariaRoles);
let message = `A11y: Unknown role '${value}'`;
if (match) message += ` (did you mean '${match}'?)`;
component.warn(attribute, {
code: `a11y-unknown-role`,
message
});
}
}
// no-access-key
if (name === 'accesskey') {
component.warn(attribute, {
code: `a11y-accesskey`,
message: `A11y: Avoid using accesskey`
});
}
// no-autofocus
if (name === 'autofocus') {
component.warn(attribute, {
code: `a11y-autofocus`,
message: `A11y: Avoid using autofocus`
});
}
// scope
if (name === 'scope' && this.name !== 'th') {
component.warn(attribute, {
code: `a11y-misplaced-scope`,
message: `A11y: The scope attribute should only be used with <th> elements`
});
}
// tabindex-no-positive
if (name === 'tabindex') {
const value = attribute.getStaticValue();
if (!isNaN(value) && +value > 0) {
component.warn(attribute, {
code: `a11y-positive-tabindex`,
message: `A11y: avoid tabindex values above zero`
});
}
}
if (name === 'slot') {
if (attribute.isDynamic) {
component.error(attribute, {
code: `invalid-slot-attribute`,
message: `slot attribute cannot have a dynamic value`
});
}
let ancestor = this.parent;
do {
if (ancestor.type === 'InlineComponent') break;
if (ancestor.type === 'Element' && /-/.test(ancestor.name)) break;
if (ancestor.type === 'IfBlock' || ancestor.type === 'EachBlock') {
const type = ancestor.type === 'IfBlock' ? 'if' : 'each';
const message = `Cannot place slotted elements inside an ${type}-block`;
component.error(attribute, {
code: `invalid-slotted-content`,
message
});
}
} while (ancestor = ancestor.parent);
if (!ancestor) {
component.error(attribute, {
code: `invalid-slotted-content`,
message: `Element with a slot='...' attribute must be a descendant of a component or custom element`
});
}
}
attributeMap.set(attribute.name, attribute);
});
// handle special cases
if (this.name === 'a') {
const attribute = attributeMap.get('href') || attributeMap.get('xlink:href');
if (attribute) {
const value = attribute.getStaticValue();
if (value === '' || value === '#') {
component.warn(attribute, {
code: `a11y-invalid-attribute`,
message: `A11y: '${value}' is not a valid ${attribute.name} attribute`
});
}
} else {
component.warn(this, {
code: `a11y-missing-attribute`,
message: `A11y: <a> element should have an href attribute`
});
}
}
else {
const requiredAttributes = a11yRequiredAttributes[this.name];
if (requiredAttributes) {
const hasAttribute = requiredAttributes.some(name => attributeMap.has(name));
if (!hasAttribute) {
shouldHaveAttribute(this, requiredAttributes);
}
}
if (this.name === 'input') {
const type = attributeMap.get('type');
if (type && type.getStaticValue() === 'image') {
shouldHaveAttribute(
this,
['alt', 'aria-label', 'aria-labelledby'],
'input type="image"'
);
}
}
}
}
validateBindings() {
const { component } = this;
const checkTypeAttribute = () => {
const attribute = this.attributes.find(
(attribute: Attribute) => attribute.name === 'type'
);
if (!attribute) return null;
if (attribute.isDynamic) {
component.error(attribute, {
code: `invalid-type`,
message: `'type' attribute cannot be dynamic if input uses two-way binding`
});
}
const value = attribute.getStaticValue();
if (value === true) {
component.error(attribute, {
code: `missing-type`,
message: `'type' attribute must be specified`
});
}
return value;
};
this.bindings.forEach(binding => {
const { name } = binding;
if (name === 'value') {
if (
this.name !== 'input' &&
this.name !== 'textarea' &&
this.name !== 'select'
) {
component.error(binding, {
code: `invalid-binding`,
message: `'value' is not a valid binding on <${this.name}> elements`
});
}
if (this.name === 'select') {
const attribute = this.attributes.find(
(attribute: Attribute) => attribute.name === 'multiple'
);
if (attribute && attribute.isDynamic) {
component.error(attribute, {
code: `dynamic-multiple-attribute`,
message: `'multiple' attribute cannot be dynamic if select uses two-way binding`
});
}
} else {
checkTypeAttribute();
}
} else if (name === 'checked' || name === 'indeterminate') {
if (this.name !== 'input') {
component.error(binding, {
code: `invalid-binding`,
message: `'${name}' is not a valid binding on <${this.name}> elements`
});
}
if (checkTypeAttribute() !== 'checkbox') {
component.error(binding, {
code: `invalid-binding`,
message: `'${name}' binding can only be used with <input type="checkbox">`
});
}
} else if (name === 'group') {
if (this.name !== 'input') {
component.error(binding, {
code: `invalid-binding`,
message: `'group' is not a valid binding on <${this.name}> elements`
});
}
const type = checkTypeAttribute();
if (type !== 'checkbox' && type !== 'radio') {
component.error(binding, {
code: `invalid-binding`,
message: `'checked' binding can only be used with <input type="checkbox"> or <input type="radio">`
});
}
} else if (name == 'files') {
if (this.name !== 'input') {
component.error(binding, {
code: `invalid-binding`,
message: `'files' binding acn only be used with <input type="file">`
});
}
const type = checkTypeAttribute();
if (type !== 'file') {
component.error(binding, {
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 (this.name !== 'audio' && this.name !== 'video') {
component.error(binding, {
code: `invalid-binding`,
message: `'${name}' binding can only be used with <audio> or <video>`
});
}
} else if (dimensions.test(name)) {
if (this.name === 'svg' && (name === 'offsetWidth' || name === 'offsetHeight')) {
component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding on <svg>. Use '${name.replace('offset', 'client')}' instead`
});
} else if (svg.test(this.name)) {
component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding on SVG elements`
});
} else if (isVoidElementName(this.name)) {
component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding on void elements like <${this.name}>. Use a wrapper element instead`
});
}
} else {
component.error(binding, {
code: `invalid-binding`,
message: `'${binding.name}' is not a valid binding`
});
}
});
}
validateContent() {
if (!a11yRequiredContent.has(this.name)) return;
if (this.children.length === 0) {
this.component.warn(this, {
code: `a11y-missing-content`,
message: `A11y: <${this.name}> element should have child content`
});
}
}
init(
@ -190,7 +619,7 @@ export default class Element extends Node {
stripWhitespace: boolean,
nextSibling: Node
) {
if (this.name === 'slot' || this.name === 'option' || this.compiler.options.dev) {
if (this.name === 'slot' || this.name === 'option' || this.component.options.dev) {
this.cannotUseInnerHTML();
}
@ -217,7 +646,7 @@ export default class Element extends Node {
if (select && select.selectBindingDependencies) {
select.selectBindingDependencies.forEach(prop => {
attr.dependencies.forEach((dependency: string) => {
this.compiler.indirectDependencies.get(prop).add(dependency);
this.component.indirectDependencies.get(prop).add(dependency);
});
});
}
@ -276,7 +705,7 @@ export default class Element extends Node {
const dependencies = binding.value.dependencies;
this.selectBindingDependencies = dependencies;
dependencies.forEach((prop: string) => {
this.compiler.indirectDependencies.set(prop, new Set());
this.component.indirectDependencies.set(prop, new Set());
});
} else {
this.selectBindingDependencies = null;
@ -284,11 +713,11 @@ export default class Element extends Node {
}
const slot = this.getStaticAttributeValue('slot');
if (slot && this.hasAncestor('Component')) {
if (slot && this.hasAncestor('InlineComponent')) {
this.cannotUseInnerHTML();
this.slotted = true;
// TODO validate slots — no nesting, no dynamic names...
const component = this.findNearest(/^Component/);
const component = this.findNearest(/^InlineComponent/);
component._slots.add(slot);
}
@ -303,11 +732,11 @@ export default class Element extends Node {
parentNode: string,
parentNodes: string
) {
const { compiler } = this;
const { component } = this;
if (this.name === 'slot') {
const slotName = this.getStaticAttributeValue('name') || 'default';
this.compiler.slots.add(slotName);
this.component.slots.add(slotName);
}
if (this.name === 'noscript') return;
@ -318,7 +747,7 @@ export default class Element extends Node {
const slot = this.attributes.find((attribute: Node) => attribute.name === 'slot');
const prop = slot && quotePropIfNecessary(slot.chunks[0].data);
const initialMountNode = this.slotted ?
`${this.findNearest(/^Component/).var}._slotted${prop}` : // TODO this looks bonkers
`${this.findNearest(/^InlineComponent/).var}._slotted${prop}` : // TODO this looks bonkers
parentNode;
block.addVariable(node);
@ -327,10 +756,10 @@ export default class Element extends Node {
`${node} = ${renderStatement};`
);
if (this.compiler.options.hydratable) {
if (this.component.options.hydratable) {
if (parentNodes) {
block.builders.claim.addBlock(deindent`
${node} = ${getClaimStatement(compiler, this.namespace, parentNodes, this)};
${node} = ${getClaimStatement(component, this.namespace, parentNodes, this)};
var ${nodes} = @children(${this.name === 'template' ? `${node}.content` : node});
`);
} else {
@ -454,10 +883,10 @@ export default class Element extends Node {
return `${open}>${node.children.map(toHTML).join('')}</${node.name}>`;
}
if (this.compiler.options.dev) {
const loc = this.compiler.locate(this.start);
if (this.component.options.dev) {
const loc = this.component.locate(this.start);
block.builders.hydrate.addLine(
`@addLoc(${this.var}, ${this.compiler.fileVar}, ${loc.line}, ${loc.column}, ${this.start});`
`@addLoc(${this.var}, ${this.component.fileVar}, ${loc.line}, ${loc.column}, ${this.start});`
);
}
}
@ -467,7 +896,7 @@ export default class Element extends Node {
) {
if (this.bindings.length === 0) return;
if (this.name === 'select' || this.isMediaNode()) this.compiler.target.hasComplexBindings = true;
if (this.name === 'select' || this.isMediaNode()) this.component.target.hasComplexBindings = true;
const needsLock = this.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type'));
@ -576,7 +1005,7 @@ export default class Element extends Node {
.join(' && ');
if (this.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || binding.isReadOnlyMediaAttribute)) {
this.compiler.target.hasComplexBindings = true;
this.component.target.hasComplexBindings = true;
block.builders.hydrate.addLine(
`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
@ -584,7 +1013,7 @@ export default class Element extends Node {
}
if (group.events[0] === 'resize') {
this.compiler.target.hasComplexBindings = true;
this.component.target.hasComplexBindings = true;
block.builders.hydrate.addLine(
`#component.root._beforecreate.push(${handler});`
@ -660,24 +1089,24 @@ export default class Element extends Node {
}
addEventHandlers(block: Block) {
const { compiler } = this;
const { component } = this;
this.handlers.forEach(handler => {
const isCustomEvent = compiler.events.has(handler.name);
const isCustomEvent = component.events.has(handler.name);
if (handler.callee) {
handler.render(this.compiler, block, handler.shouldHoist);
handler.render(this.component, block, handler.shouldHoist);
}
const target = handler.shouldHoist ? 'this' : this.var;
// get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise
const handlerName = (handler.shouldHoist ? compiler : block).getUniqueName(
const handlerName = (handler.shouldHoist ? component : block).getUniqueName(
`${handler.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
);
const component = block.alias('component'); // can't use #component, might be hoisted
const component_name = block.alias('component'); // can't use #component, might be hoisted
// create the handler body
const handlerBody = deindent`
@ -689,14 +1118,14 @@ export default class Element extends Node {
${handler.snippet ?
handler.snippet :
`${component}.fire("${handler.name}", event);`}
`${component_name}.fire("${handler.name}", event);`}
`;
if (isCustomEvent) {
block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${handler.name}.call(${component}, ${this.var}, function(event) {
${handlerName} = %events-${handler.name}.call(${component_name}, ${this.var}, function(event) {
${handlerBody}
});
`);
@ -712,7 +1141,7 @@ export default class Element extends Node {
`;
if (handler.shouldHoist) {
compiler.target.blocks.push(handlerFunction);
component.target.blocks.push(handlerFunction);
} else {
block.builders.init.addBlock(handlerFunction);
}
@ -729,7 +1158,7 @@ export default class Element extends Node {
}
addRef(block: Block) {
const ref = `#component.refs.${this.ref}`;
const ref = `#component.refs.${this.ref.name}`;
block.builders.mount.addLine(
`${ref} = ${this.var};`
@ -947,14 +1376,14 @@ export default class Element extends Node {
return `@append(${name}._slotted.default, ${this.var});`;
}
addCssClass(className = this.compiler.stylesheet.id) {
addCssClass(className = this.component.stylesheet.id) {
const classAttribute = this.attributes.find(a => a.name === 'class');
if (classAttribute && !classAttribute.isTrue) {
if (classAttribute.chunks.length === 1 && classAttribute.chunks[0].type === 'Text') {
(<Text>classAttribute.chunks[0]).data += ` ${className}`;
} else {
(<Node[]>classAttribute.chunks).push(
new Text(this.compiler, this, this.scope, {
new Text(this.component, this, this.scope, {
type: 'Text',
data: ` ${className}`
})
@ -962,7 +1391,7 @@ export default class Element extends Node {
}
} else {
this.attributes.push(
new Attribute(this.compiler, this, this.scope, {
new Attribute(this.component, this, this.scope, {
type: 'Attribute',
name: 'class',
value: [{ type: 'Text', data: className }]
@ -972,16 +1401,16 @@ export default class Element extends Node {
}
ssr() {
const { compiler } = this;
const { component } = this;
let openingTag = `<${this.name}`;
let textareaContents; // awkward special case
const slot = this.getStaticAttributeValue('slot');
if (slot && this.hasAncestor('Component')) {
if (slot && this.hasAncestor('InlineComponent')) {
const slot = this.attributes.find((attribute: Node) => attribute.name === 'slot');
const slotName = slot.chunks[0].data;
const appendTarget = compiler.target.appendTargets[compiler.target.appendTargets.length - 1];
const appendTarget = component.target.appendTargets[component.target.appendTargets.length - 1];
appendTarget.slotStack.push(slotName);
appendTarget.slots[slotName] = '';
}
@ -1049,10 +1478,10 @@ export default class Element extends Node {
openingTag += '>';
compiler.target.append(openingTag);
component.target.append(openingTag);
if (this.name === 'textarea' && textareaContents !== undefined) {
compiler.target.append(textareaContents);
component.target.append(textareaContents);
} else {
this.children.forEach((child: Node) => {
child.ssr();
@ -1060,7 +1489,7 @@ export default class Element extends Node {
}
if (!isVoidElementName(this.name)) {
compiler.target.append(`</${this.name}>`);
component.target.append(`</${this.name}>`);
}
}
}
@ -1081,7 +1510,7 @@ function getRenderStatement(
}
function getClaimStatement(
compiler: Compiler,
component: Component,
namespace: string,
nodes: string,
node: Node
@ -1169,3 +1598,19 @@ const events = [
name === 'volume'
}
];
function shouldHaveAttribute(
node,
attributes: string[],
name = node.name
) {
const article = /^[aeiou]/.test(attributes[0]) ? 'an' : 'a';
const sequence = attributes.length > 1 ?
attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` :
attributes[0];
node.component.warn(node, {
code: `a11y-missing-attribute`,
message: `A11y: <${name}> element should have ${article} ${sequence} attribute`
});
}

@ -7,8 +7,10 @@ export default class ElseBlock extends Node {
children: Node[];
block: Block;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, this, scope, info.children);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = mapChildren(component, this, scope, info.children);
this.warnIfEmptyBlock();
}
}

@ -3,6 +3,9 @@ import Expression from './shared/Expression';
import addToSet from '../../utils/addToSet';
import flattenReference from '../../utils/flattenReference';
import validCalleeObjects from '../../utils/validCalleeObjects';
import list from '../../utils/list';
const validBuiltins = new Set(['set', 'fire', 'destroy']);
export default class EventHandler extends Node {
name: string;
@ -19,21 +22,26 @@ export default class EventHandler extends Node {
args: Expression[];
snippet: string;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
component.used.events.add(this.name);
this.dependencies = new Set();
if (info.expression) {
this.validateExpression(info.expression);
this.callee = flattenReference(info.expression.callee);
this.insertionPoint = info.expression.start;
this.usesComponent = !validCalleeObjects.has(this.callee.name);
this.usesContext = false;
this.args = info.expression.arguments.map(param => {
const expression = new Expression(compiler, this, scope, param);
const expression = new Expression(component, this, scope, param);
addToSet(this.dependencies, expression.dependencies);
if (expression.usesContext) this.usesContext = true;
return expression;
@ -51,28 +59,28 @@ export default class EventHandler extends Node {
this.snippet = null; // TODO handle shorthand events here?
}
this.isCustomEvent = compiler.events.has(this.name);
this.isCustomEvent = component.events.has(this.name);
this.shouldHoist = !this.isCustomEvent && parent.hasAncestor('EachBlock');
}
render(compiler, block, hoisted) { // TODO hoist more event handlers
render(component, block, hoisted) { // TODO hoist more event handlers
if (this.insertionPoint === null) return; // TODO handle shorthand events here?
if (!validCalleeObjects.has(this.callee.name)) {
const component = hoisted ? `component` : block.alias(`component`);
const component_name = hoisted ? `component` : block.alias(`component`);
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
if (this.callee.name[0] === '$' && !compiler.methods.has(this.callee.name)) {
compiler.code.overwrite(
if (this.callee.name[0] === '$' && !component.methods.has(this.callee.name)) {
component.code.overwrite(
this.insertionPoint,
this.insertionPoint + 1,
`${component}.store.`
`${component_name}.store.`
);
} else {
compiler.code.prependRight(
component.code.prependRight(
this.insertionPoint,
`${component}.`
`${component_name}.`
);
}
}
@ -84,11 +92,66 @@ export default class EventHandler extends Node {
if (this.callee && this.callee.name === 'this') {
const node = this.callee.nodes[0];
compiler.code.overwrite(node.start, node.end, this.parent.var, {
component.code.overwrite(node.start, node.end, this.parent.var, {
storeName: true,
contentOnly: true
});
}
}
}
validateExpression(expression) {
const { callee, type } = expression;
if (type !== 'CallExpression') {
this.component.error(expression, {
code: `invalid-event-handler`,
message: `Expected a call expression`
});
}
const { component } = this;
const { name } = flattenReference(callee);
if (validCalleeObjects.has(name) || name === 'options') return;
if (name === 'refs') {
this.component.refCallees.push(callee);
return;
}
if (
(callee.type === 'Identifier' && validBuiltins.has(name)) ||
this.component.methods.has(name)
) {
return;
}
if (name[0] === '$') {
// assume it's a store method
return;
}
const validCallees = ['this.*', 'refs.*', 'event.*', 'options.*', 'console.*'].concat(
Array.from(validBuiltins),
Array.from(this.component.methods.keys())
);
let message = `'${component.source.slice(callee.start, callee.end)}' is an invalid callee ` ;
if (name === 'store') {
message += `(did you mean '$${component.source.slice(callee.start + 6, callee.end)}(...)'?)`;
} else {
message += `(should be one of ${list(validCallees)})`;
if (callee.type === 'Identifier' && component.helpers.has(callee.name)) {
message += `. '${callee.name}' exists on 'helpers', did you put it in the wrong place?`;
}
}
component.warn(expression, {
code: `invalid-callee`,
message
});
}
}

@ -1,5 +1,5 @@
import Node from './shared/Node';
import Compiler from '../Compiler';
import Component from '../Component';
import mapChildren from './shared/mapChildren';
import Block from '../dom/Block';
import TemplateScope from './shared/TemplateScope';
@ -9,17 +9,17 @@ export default class Fragment extends Node {
children: Node[];
scope: TemplateScope;
constructor(compiler: Compiler, info: any) {
constructor(component: Component, info: any) {
const scope = new TemplateScope();
super(compiler, null, scope, info);
super(component, null, scope, info);
this.scope = scope;
this.children = mapChildren(compiler, this, scope, info.children);
this.children = mapChildren(component, this, scope, info.children);
}
init() {
this.block = new Block({
compiler: this.compiler,
component: this.component,
name: '@create_main_fragment',
key: null,
@ -28,7 +28,7 @@ export default class Fragment extends Node {
dependencies: new Set(),
});
this.compiler.target.blocks.push(this.block);
this.component.target.blocks.push(this.block);
this.initChildren(this.block, true, null);
this.block.hasUpdateMethod = true;

@ -1,17 +1,22 @@
import deindent from '../../utils/deindent';
import { stringify } from '../../utils/stringify';
import Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';
import mapChildren from './shared/mapChildren';
export default class Head extends Node {
type: 'Head';
children: any[]; // TODO
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, parent, scope, info.children.filter(child => {
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
if (info.attributes.length) {
component.error(info.attributes[0], {
code: `invalid-attribute`,
message: `<svelte:head> should not have any attributes or directives`
});
}
this.children = mapChildren(component, parent, scope, info.children.filter(child => {
return (child.type !== 'Text' || /\S/.test(child.data));
}));
}
@ -37,12 +42,12 @@ export default class Head extends Node {
}
ssr() {
this.compiler.target.append('${(__result.head += `');
this.component.target.append('${(__result.head += `');
this.children.forEach((child: Node) => {
child.ssr();
});
this.compiler.target.append('`, "")}');
this.component.target.append('`, "")}');
}
}

@ -1,7 +1,7 @@
import deindent from '../../utils/deindent';
import Node from './shared/Node';
import ElseBlock from './ElseBlock';
import Compiler from '../Compiler';
import Component from '../Component';
import Block from '../dom/Block';
import createDebuggingComment from '../../utils/createDebuggingComment';
import Expression from './shared/Expression';
@ -25,15 +25,17 @@ export default class IfBlock extends Node {
block: Block;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.expression = new Expression(compiler, this, scope, info.expression);
this.children = mapChildren(compiler, this, scope, info.children);
this.expression = new Expression(component, this, scope, info.expression);
this.children = mapChildren(component, this, scope, info.children);
this.else = info.else
? new ElseBlock(compiler, this, scope, info.else)
? new ElseBlock(component, this, scope, info.else)
: null;
this.warnIfEmptyBlock();
}
init(
@ -41,7 +43,7 @@ export default class IfBlock extends Node {
stripWhitespace: boolean,
nextSibling: Node
) {
const { compiler } = this;
const { component } = this;
this.cannotUseInnerHTML();
@ -56,8 +58,8 @@ export default class IfBlock extends Node {
block.addDependencies(node.expression.dependencies);
node.block = block.child({
comment: createDebuggingComment(node, compiler),
name: compiler.getUniqueName(`create_if_block`),
comment: createDebuggingComment(node, component),
name: component.getUniqueName(`create_if_block`),
});
blocks.push(node.block);
@ -75,8 +77,8 @@ export default class IfBlock extends Node {
attachBlocks(node.else.children[0]);
} else if (node.else) {
node.else.block = block.child({
comment: createDebuggingComment(node.else, compiler),
name: compiler.getUniqueName(`create_if_block`),
comment: createDebuggingComment(node.else, component),
name: component.getUniqueName(`create_if_block`),
});
blocks.push(node.else.block);
@ -98,7 +100,7 @@ export default class IfBlock extends Node {
attachBlocks(this);
if (compiler.options.nestedTransitions) {
if (component.options.nestedTransitions) {
if (hasIntros) block.addIntro();
if (hasOutros) block.addOutro();
}
@ -109,7 +111,7 @@ export default class IfBlock extends Node {
block.hasOutroMethod = hasOutros;
});
compiler.target.blocks.push(...blocks);
component.target.blocks.push(...blocks);
}
build(
@ -138,7 +140,7 @@ export default class IfBlock extends Node {
if (hasOutros) {
this.buildCompoundWithOutros(block, parentNode, parentNodes, branches, dynamic, vars);
if (this.compiler.options.nestedTransitions) {
if (this.component.options.nestedTransitions) {
block.builders.outro.addBlock(deindent`
if (${name}) ${name}.o(#outrocallback);
else #outrocallback();
@ -150,7 +152,7 @@ export default class IfBlock extends Node {
} else {
this.buildSimple(block, parentNode, parentNodes, branches[0], dynamic, vars);
if (hasOutros && this.compiler.options.nestedTransitions) {
if (hasOutros && this.component.options.nestedTransitions) {
block.builders.outro.addBlock(deindent`
if (${name}) ${name}.o(#outrocallback);
else #outrocallback();
@ -184,7 +186,7 @@ export default class IfBlock extends Node {
dynamic,
{ name, anchor, hasElse, if_name }
) {
const select_block_type = this.compiler.getUniqueName(`select_block_type`);
const select_block_type = this.component.getUniqueName(`select_block_type`);
const current_block_type = block.getUniqueName(`current_block_type`);
const current_block_type_and = hasElse ? '' : `${current_block_type} && `;
@ -247,7 +249,7 @@ export default class IfBlock extends Node {
dynamic,
{ name, anchor, hasElse }
) {
const select_block_type = this.compiler.getUniqueName(`select_block_type`);
const select_block_type = this.component.getUniqueName(`select_block_type`);
const current_block_type_index = block.getUniqueName(`current_block_type_index`);
const previous_block_index = block.getUniqueName(`previous_block_index`);
const if_block_creators = block.getUniqueName(`if_block_creators`);
@ -482,16 +484,16 @@ export default class IfBlock extends Node {
}
ssr() {
const { compiler } = this;
const { component } = this;
const { snippet } = this.expression;
compiler.target.append('${ ' + snippet + ' ? `');
component.target.append('${ ' + snippet + ' ? `');
this.children.forEach((child: Node) => {
child.ssr();
});
compiler.target.append('` : `');
component.target.append('` : `');
if (this.else) {
this.else.children.forEach((child: Node) => {
@ -499,7 +501,7 @@ export default class IfBlock extends Node {
});
}
compiler.target.append('` }');
component.target.append('` }');
}
visitChildren(block: Block, node: Node) {

@ -14,26 +14,40 @@ import EventHandler from './EventHandler';
import Expression from './shared/Expression';
import { AppendTarget } from '../../interfaces';
import addToSet from '../../utils/addToSet';
import Component from '../Component';
import isValidIdentifier from '../../utils/isValidIdentifier';
import Ref from './Ref';
export default class Component extends Node {
type: 'Component';
export default class InlineComponent extends Node {
type: 'InlineComponent';
name: string;
expression: Expression;
attributes: Attribute[];
bindings: Binding[];
handlers: EventHandler[];
children: Node[];
ref: string;
ref: Ref;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component: Component, parent, scope, info) {
super(component, parent, scope, info);
compiler.hasComponents = true;
component.hasComponents = true;
this.name = info.name;
if (this.name !== 'svelte:self' && this.name !== 'svelte:component') {
if (!component.components.has(this.name)) {
component.error(this, {
code: `missing-component`,
message: `${this.name} component is not defined`
});
}
component.used.components.add(this.name);
}
this.expression = this.name === 'svelte:component'
? new Expression(compiler, this, scope, info.expression)
? new Expression(component, this, scope, info.expression)
: null;
this.attributes = [];
@ -42,33 +56,47 @@ export default class Component extends Node {
info.attributes.forEach(node => {
switch (node.type) {
case 'Action':
component.error(node, {
code: `invalid-action`,
message: `Actions can only be applied to DOM elements, not components`
});
case 'Attribute':
case 'Spread':
this.attributes.push(new Attribute(compiler, this, scope, node));
this.attributes.push(new Attribute(component, this, scope, node));
break;
case 'Binding':
this.bindings.push(new Binding(compiler, this, scope, node));
this.bindings.push(new Binding(component, this, scope, node));
break;
case 'Class':
component.error(node, {
code: `invalid-class`,
message: `Classes can only be applied to DOM elements, not components`
});
case 'EventHandler':
this.handlers.push(new EventHandler(compiler, this, scope, node));
this.handlers.push(new EventHandler(component, this, scope, node));
break;
case 'Ref':
// TODO catch this in validation
if (this.ref) throw new Error(`Duplicate refs`);
compiler.usesRefs = true
this.ref = node.name;
this.ref = new Ref(component, this, scope, node);
break;
case 'Transition':
component.error(node, {
code: `invalid-transition`,
message: `Transitions can only be applied to DOM elements, not components`
});
default:
throw new Error(`Not implemented: ${node.type}`);
}
});
this.children = mapChildren(compiler, this, scope, info.children);
this.children = mapChildren(component, this, scope, info.children);
}
init(
@ -96,7 +124,7 @@ export default class Component extends Node {
this.var = block.getUniqueName(
(
this.name === 'svelte:self' ? this.compiler.name :
this.name === 'svelte:self' ? this.component.name :
this.name === 'svelte:component' ? 'switch_instance' :
this.name
).toLowerCase()
@ -110,7 +138,7 @@ export default class Component extends Node {
});
}
if (this.compiler.options.nestedTransitions) {
if (this.component.options.nestedTransitions) {
block.addOutro();
}
}
@ -120,7 +148,7 @@ export default class Component extends Node {
parentNode: string,
parentNodes: string
) {
const { compiler } = this;
const { component } = this;
const name = this.var;
@ -228,7 +256,7 @@ export default class Component extends Node {
}
if (this.bindings.length) {
compiler.target.hasComplexBindings = true;
component.target.hasComplexBindings = true;
name_updating = block.alias(`${name}_updating`);
block.addVariable(name_updating, '{}');
@ -337,7 +365,7 @@ export default class Component extends Node {
this.handlers.forEach(handler => {
handler.var = block.getUniqueName(`${this.var}_${handler.name}`); // TODO this is hacky
handler.render(compiler, block, false); // TODO hoist when possible
handler.render(component, block, false); // TODO hoist when possible
if (handler.usesContext) block.maintainContext = true; // TODO is there a better place to put this?
});
@ -387,7 +415,7 @@ export default class Component extends Node {
block.builders.mount.addBlock(deindent`
if (${name}) {
${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});
${this.ref && `#component.refs.${this.ref} = ${name};`}
${this.ref && `#component.refs.${this.ref.name} = ${name};`}
}
`);
@ -403,7 +431,7 @@ export default class Component extends Node {
block.builders.update.addBlock(deindent`
if (${switch_value} !== (${switch_value} = ${snippet})) {
if (${name}) {
${this.compiler.options.nestedTransitions
${this.component.options.nestedTransitions
? deindent`
@groupOutros();
const old_component = ${name};
@ -432,12 +460,12 @@ export default class Component extends Node {
${name}.on("${handler.name}", ${handler.var});
`)}
${this.ref && `#component.refs.${this.ref} = ${name};`}
${this.ref && `#component.refs.${this.ref.name} = ${name};`}
} else {
${name} = null;
${this.ref && deindent`
if (#component.refs.${this.ref} === ${name}) {
#component.refs.${this.ref} = null;
if (#component.refs.${this.ref.name} === ${name}) {
#component.refs.${this.ref.name} = null;
}`}
}
}
@ -455,7 +483,7 @@ export default class Component extends Node {
block.builders.destroy.addLine(`if (${name}) ${name}.destroy(${parentNode ? '' : 'detach'});`);
} else {
const expression = this.name === 'svelte:self'
? compiler.name
? component.name
: `%components-${this.name}`;
block.builders.init.addBlock(deindent`
@ -474,7 +502,7 @@ export default class Component extends Node {
});
`)}
${this.ref && `#component.refs.${this.ref} = ${name};`}
${this.ref && `#component.refs.${this.ref.name} = ${name};`}
`);
block.builders.create.addLine(`${name}._fragment.c();`);
@ -499,11 +527,11 @@ export default class Component extends Node {
block.builders.destroy.addLine(deindent`
${name}.destroy(${parentNode ? '' : 'detach'});
${this.ref && `if (#component.refs.${this.ref} === ${name}) #component.refs.${this.ref} = null;`}
${this.ref && `if (#component.refs.${this.ref.name} === ${name}) #component.refs.${this.ref.name} = null;`}
`);
}
if (this.compiler.options.nestedTransitions) {
if (this.component.options.nestedTransitions) {
block.builders.outro.addLine(
`if (${name}) ${name}._fragment.o(#outrocallback);`
);
@ -570,7 +598,7 @@ export default class Component extends Node {
const expression = (
this.name === 'svelte:self'
? this.compiler.name
? this.component.name
: this.name === 'svelte:component'
? `((${this.expression.snippet}) || @missingComponent)`
: `%components-${this.name}`
@ -594,7 +622,7 @@ export default class Component extends Node {
const { name } = getObject(binding.value.node);
this.compiler.target.bindings.push(deindent`
this.component.target.bindings.push(deindent`
if (${conditions.reverse().join('&&')}) {
tmp = ${expression}.data();
if ('${name}' in tmp) {
@ -616,7 +644,7 @@ export default class Component extends Node {
slotStack: ['default']
};
this.compiler.target.appendTargets.push(appendTarget);
this.component.target.appendTargets.push(appendTarget);
this.children.forEach((child: Node) => {
child.ssr();
@ -628,15 +656,15 @@ export default class Component extends Node {
options.push(`slotted: { ${slotted} }`);
this.compiler.target.appendTargets.pop();
this.component.target.appendTargets.pop();
}
if (options.length) {
open += `, { ${options.join(', ')} }`;
}
this.compiler.target.append(open);
this.compiler.target.append(')}');
this.component.target.append(open);
this.component.target.append(')}');
}
}

@ -26,7 +26,7 @@ export default class MustacheTag extends Tag {
}
ssr() {
this.compiler.target.append(
this.component.target.append(
this.parent &&
this.parent.type === 'Element' &&
this.parent.name === 'style'

@ -6,8 +6,10 @@ export default class PendingBlock extends Node {
block: Block;
children: Node[];
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, parent, scope, info.children);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = mapChildren(component, parent, scope, info.children);
this.warnIfEmptyBlock();
}
}

@ -95,6 +95,6 @@ export default class RawMustacheTag extends Tag {
}
ssr() {
this.compiler.target.append('${' + this.expression.snippet + '}');
this.component.target.append('${' + this.expression.snippet + '}');
}
}

@ -0,0 +1,31 @@
import Node from './shared/Node';
import isValidIdentifier from '../../utils/isValidIdentifier';
export default class Ref extends Node {
type: 'Ref';
name: string;
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
if (parent.ref) {
component.error({
code: 'duplicate-refs',
message: `Duplicate refs`
});
}
if (!isValidIdentifier(info.name)) {
const suggestion = info.name.replace(/[^_$a-z0-9]/ig, '_').replace(/^\d/, '_$&');
component.error(info, {
code: `invalid-reference-name`,
message: `Reference name '${info.name}' is invalid — must be a valid identifier such as ${suggestion}`
});
} else {
component.refs.add(info.name);
}
this.name = info.name;
}
}

@ -17,6 +17,57 @@ export default class Slot extends Element {
attributes: Attribute[];
children: Node[];
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
info.attributes.forEach(attr => {
if (attr.type !== 'Attribute') {
component.error(attr, {
code: `invalid-slot-directive`,
message: `<slot> cannot have directives`
});
}
if (attr.name !== 'name') {
component.error(attr, {
code: `invalid-slot-attribute`,
message: `"name" is the only attribute permitted on <slot> elements`
});
}
if (attr.value.length !== 1 || attr.value[0].type !== 'Text') {
component.error(attr, {
code: `dynamic-slot-name`,
message: `<slot> name cannot be dynamic`
});
}
const slotName = attr.value[0].data;
if (slotName === 'default') {
component.error(attr, {
code: `invalid-slot-name`,
message: `default is a reserved word — it cannot be used as a slot name`
});
}
// TODO should duplicate slots be disallowed? Feels like it's more likely to be a
// bug than anything. Perhaps it should be a warning
// if (validator.slots.has(slotName)) {
// validator.error(`duplicate '${slotName}' <slot> element`, nameAttribute.start);
// }
// validator.slots.add(slotName);
});
// if (node.attributes.length === 0) && validator.slots.has('default')) {
// validator.error(node, {
// code: `duplicate-slot`,
// message: `duplicate default <slot> element`
// });
// }
}
init(
block: Block,
stripWhitespace: boolean,
@ -36,10 +87,10 @@ export default class Slot extends Element {
parentNode: string,
parentNodes: string
) {
const { compiler } = this;
const { component } = this;
const slotName = this.getStaticAttributeValue('name') || 'default';
compiler.slots.add(slotName);
component.slots.add(slotName);
const content_name = block.getUniqueName(`slot_content_${sanitize(slotName)}`);
const prop = quotePropIfNecessary(slotName);
@ -160,12 +211,12 @@ export default class Slot extends Element {
const slotName = name && name.chunks[0].data || 'default';
const prop = quotePropIfNecessary(slotName);
this.compiler.target.append(`\${options && options.slotted && options.slotted${prop} ? options.slotted${prop}() : \``);
this.component.target.append(`\${options && options.slotted && options.slotted${prop} ? options.slotted${prop}() : \``);
this.children.forEach((child: Node) => {
child.ssr();
});
this.compiler.target.append(`\`}`);
this.component.target.append(`\`}`);
}
}

@ -17,11 +17,11 @@ const elementsWithoutText = new Set([
function shouldSkip(node: Text) {
if (/\S/.test(node.data)) return false;
const parentElement = node.findNearest(/(?:Element|Component|Head)/);
const parentElement = node.findNearest(/(?:Element|InlineComponent|Head)/);
if (!parentElement) return false;
if (parentElement.type === 'Head') return true;
if (parentElement.type === 'Component') return parentElement.children.length === 1 && node === parentElement.children[0];
if (parentElement.type === 'InlineComponent') return parentElement.children.length === 1 && node === parentElement.children[0];
return parentElement.namespace || elementsWithoutText.has(parentElement.name);
}
@ -31,8 +31,8 @@ export default class Text extends Node {
data: string;
shouldSkip: boolean;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.data = info.data;
}
@ -74,6 +74,6 @@ export default class Text extends Node {
// unless this Text node is inside a <script> or <style> element, escape &,<,>
text = escapeHTML(text);
}
this.compiler.target.append(escape(escapeTemplate(text)));
this.component.target.append(escape(escapeTemplate(text)));
}
}

@ -6,8 +6,10 @@ export default class ThenBlock extends Node {
block: Block;
children: Node[];
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, parent, scope, info.children);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = mapChildren(component, parent, scope, info.children);
this.warnIfEmptyBlock();
}
}

@ -8,9 +8,25 @@ export default class Title extends Node {
children: any[]; // TODO
shouldCache: boolean;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.children = mapChildren(compiler, parent, scope, info.children);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.children = mapChildren(component, parent, scope, info.children);
if (info.attributes.length > 0) {
component.error(info.attributes[0], {
code: `illegal-attribute`,
message: `<title> cannot have attributes`
});
}
info.children.forEach(child => {
if (child.type !== 'Text' && child.type !== 'MustacheTag') {
component.error(child, {
code: 'illegal-structure',
message: `<title> can only contain text and {tags}`
});
}
});
this.shouldCache = info.children.length === 1
? (
@ -103,12 +119,12 @@ export default class Title extends Node {
}
ssr() {
this.compiler.target.append(`<title>`);
this.component.target.append(`<title>`);
this.children.forEach((child: Node) => {
child.ssr();
});
this.compiler.target.append(`</title>`);
this.component.target.append(`</title>`);
}
}

@ -4,15 +4,45 @@ import Expression from './shared/Expression';
export default class Transition extends Node {
type: 'Transition';
name: string;
directive: string;
expression: Expression;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
if (!component.transitions.has(info.name)) {
component.error(info, {
code: `missing-transition`,
message: `Missing transition '${info.name}'`
});
}
this.name = info.name;
this.directive = info.intro && info.outro ? 'transition' : info.intro ? 'in' : 'out';
if ((info.intro && parent.intro) || (info.outro && parent.outro)) {
const parentTransition = (parent.intro || parent.outro);
const message = this.directive === parentTransition.directive
? `An element can only have one '${this.directive}' directive`
: `An element cannot have both ${describe(parentTransition)} directive and ${describe(this)} directive`;
component.error(info, {
code: `duplicate-transition`,
message
});
}
this.component.used.transitions.add(this.name);
this.expression = info.expression
? new Expression(compiler, this, scope, info.expression)
? new Expression(component, this, scope, info.expression)
: null;
}
}
function describe(transition: Transition) {
return transition.directive === 'transition'
? `a 'transition'`
: `an '${transition.directive}'`;
}

@ -1,14 +1,11 @@
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';
const associatedEvents = {
innerWidth: 'resize',
@ -33,22 +30,69 @@ const readonly = new Set([
'online',
]);
const validBindings = [
'innerWidth',
'innerHeight',
'outerWidth',
'outerHeight',
'scrollX',
'scrollY',
'online'
];
export default class Window extends Node {
type: 'Window';
handlers: EventHandler[];
bindings: Binding[];
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.handlers = [];
this.bindings = [];
info.attributes.forEach(node => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(compiler, this, scope, node));
} else if (node.type === 'Binding') {
this.bindings.push(new Binding(compiler, this, scope, node));
this.handlers.push(new EventHandler(component, this, scope, node));
}
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...
}
});
}
@ -58,20 +102,20 @@ export default class Window extends Node {
parentNode: string,
parentNodes: string
) {
const { compiler } = this;
const { component } = this;
const events = {};
const bindings: Record<string, string> = {};
this.handlers.forEach(handler => {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
compiler.addSourcemapLocations(handler.expression);
component.addSourcemapLocations(handler.expression);
const isCustomEvent = compiler.events.has(handler.name);
const isCustomEvent = component.events.has(handler.name);
let usesState = handler.dependencies.size > 0;
handler.render(compiler, block, false); // TODO hoist?
handler.render(component, block, false); // TODO hoist?
const handlerName = block.getUniqueName(`onwindow${handler.name}`);
const handlerBody = deindent`
@ -109,7 +153,7 @@ export default class Window extends Node {
this.bindings.forEach(binding => {
// in dev mode, throw if read-only values are written to
if (readonly.has(binding.name)) {
compiler.target.readonly.add(binding.value.node.name);
component.target.readonly.add(binding.value.node.name);
}
bindings[binding.name] = binding.value.node.name;
@ -126,7 +170,7 @@ export default class Window extends Node {
);
// add initial value
compiler.target.metaBindings.push(
component.target.metaBindings.push(
`this._state.${binding.value.node.name} = window.${property};`
);
});
@ -151,13 +195,13 @@ export default class Window extends Node {
if (${lock}) return;
${lock} = true;
`}
${compiler.options.dev && `component._updatingReadonlyProperty = true;`}
${component.options.dev && `component._updatingReadonlyProperty = true;`}
#component.set({
${props}
});
${compiler.options.dev && `component._updatingReadonlyProperty = false;`}
${component.options.dev && `component._updatingReadonlyProperty = false;`}
${event === 'scroll' && `${lock} = false;`}
`;
@ -200,16 +244,16 @@ export default class Window extends Node {
const handlerName = block.getUniqueName(`onlinestatuschanged`);
block.builders.init.addBlock(deindent`
function ${handlerName}(event) {
${compiler.options.dev && `component._updatingReadonlyProperty = true;`}
${component.options.dev && `component._updatingReadonlyProperty = true;`}
#component.set({ ${bindings.online}: navigator.onLine });
${compiler.options.dev && `component._updatingReadonlyProperty = false;`}
${component.options.dev && `component._updatingReadonlyProperty = false;`}
}
window.addEventListener("online", ${handlerName});
window.addEventListener("offline", ${handlerName});
`);
// add initial value
compiler.target.metaBindings.push(
component.target.metaBindings.push(
`this._state.${bindings.online} = navigator.onLine;`
);

@ -1,4 +1,4 @@
import Compiler from '../../Compiler';
import Component from '../../Component';
import { walk } from 'estree-walker';
import isReference from 'is-reference';
import flattenReference from '../../../utils/flattenReference';
@ -54,7 +54,7 @@ const precedence: Record<string, (node?: Node) => number> = {
};
export default class Expression {
compiler: Compiler;
component: Component;
node: any;
snippet: string;
@ -64,11 +64,11 @@ export default class Expression {
thisReferences: Array<{ start: number, end: number }>;
constructor(compiler, parent, scope, info) {
constructor(component, parent, scope, info) {
// TODO revert to direct property access in prod?
Object.defineProperties(this, {
compiler: {
value: compiler
component: {
value: component
}
});
@ -81,7 +81,7 @@ export default class Expression {
const dependencies = new Set();
const { code, helpers } = compiler;
const { code, helpers } = component;
let { map, scope: currentScope } = createScopes(info);
@ -111,11 +111,11 @@ export default class Expression {
if (currentScope.has(name) || (name === 'event' && isEventHandler)) return;
if (compiler.helpers.has(name)) {
if (component.helpers.has(name)) {
let object = node;
while (object.type === 'MemberExpression') object = object.object;
const alias = compiler.templateVars.get(`helpers-${name}`);
const alias = component.templateVars.get(`helpers-${name}`);
if (alias !== name) code.overwrite(object.start, object.end, alias);
return;
}
@ -135,7 +135,7 @@ export default class Expression {
});
} else {
dependencies.add(name);
compiler.expectedProperties.add(name);
component.expectedProperties.add(name);
}
if (node.type === 'MemberExpression') {
@ -163,7 +163,7 @@ export default class Expression {
overwriteThis(name) {
this.thisReferences.forEach(ref => {
this.compiler.code.overwrite(ref.start, ref.end, name, {
this.component.code.overwrite(ref.start, ref.end, name, {
storeName: true
});
});

@ -1,11 +1,11 @@
import Compiler from './../../Compiler';
import Component from './../../Component';
import Block from '../../dom/Block';
import { trimStart, trimEnd } from '../../../utils/trim';
export default class Node {
readonly start: number;
readonly end: number;
readonly compiler: Compiler;
readonly component: Component;
readonly parent: Node;
readonly type: string;
@ -15,7 +15,7 @@ export default class Node {
canUseInnerHTML: boolean;
var: string;
constructor(compiler: Compiler, parent, scope, info: any) {
constructor(component: Component, parent, scope, info: any) {
this.start = info.start;
this.end = info.end;
this.type = info.type;
@ -23,8 +23,8 @@ export default class Node {
// this makes properties non-enumerable, which makes logging
// bearable. might have a performance cost. TODO remove in prod?
Object.defineProperties(this, {
compiler: {
value: compiler
component: {
value: component
},
parent: {
value: parent
@ -86,7 +86,7 @@ export default class Node {
lastChild = null;
cleaned.forEach((child: Node, i: number) => {
child.canUseInnerHTML = !this.compiler.options.hydratable;
child.canUseInnerHTML = !this.component.options.hydratable;
child.init(block, stripWhitespace, cleaned[i + 1] || nextSibling);
@ -169,4 +169,19 @@ export default class Node {
remount(name: string) {
return `${this.var}.m(${name}._slotted.default, null);`;
}
warnIfEmptyBlock() {
if (!this.component.options.dev) return;
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'
});
}
}
}

@ -6,9 +6,9 @@ export default class Tag extends Node {
expression: Expression;
shouldCache: boolean;
constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);
this.expression = new Expression(compiler, this, scope, info.expression);
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.expression = new Expression(component, this, scope, info.expression);
this.shouldCache = (
info.expression.type !== 'Identifier' ||

@ -1,10 +1,10 @@
import AwaitBlock from '../AwaitBlock';
import Comment from '../Comment';
import Component from '../Component';
import EachBlock from '../EachBlock';
import Element from '../Element';
import Head from '../Head';
import IfBlock from '../IfBlock';
import InlineComponent from '../InlineComponent';
import MustacheTag from '../MustacheTag';
import RawMustacheTag from '../RawMustacheTag';
import DebugTag from '../DebugTag';
@ -18,11 +18,11 @@ function getConstructor(type): typeof Node {
switch (type) {
case 'AwaitBlock': return AwaitBlock;
case 'Comment': return Comment;
case 'Component': return Component;
case 'EachBlock': return EachBlock;
case 'Element': return Element;
case 'Head': return Head;
case 'IfBlock': return IfBlock;
case 'InlineComponent': return InlineComponent;
case 'MustacheTag': return MustacheTag;
case 'RawMustacheTag': return RawMustacheTag;
case 'DebugTag': return DebugTag;
@ -34,11 +34,11 @@ function getConstructor(type): typeof Node {
}
}
export default function mapChildren(compiler, parent, scope, children: any[]) {
export default function mapChildren(component, parent, scope, children: any[]) {
let last = null;
return children.map(child => {
const constructor = getConstructor(child.type);
const node = new constructor(compiler, parent, scope, child);
const node = new constructor(component, parent, scope, child);
if (last) last.next = node;
node.prev = last;

@ -1,13 +1,10 @@
import deindent from '../../utils/deindent';
import Compiler from '../Compiler';
import Stats from '../../Stats';
import Stylesheet from '../../css/Stylesheet';
import { removeNode, removeObjectKey } from '../../utils/removeNode';
import getName from '../../utils/getName';
import Component from '../Component';
import globalWhitelist from '../../utils/globalWhitelist';
import { Ast, Node, CompileOptions } from '../../interfaces';
import { Node, CompileOptions } from '../../interfaces';
import { AppendTarget } from '../../interfaces';
import { stringify } from '../../utils/stringify';
import CodeBuilder from '../../utils/CodeBuilder';
export class SsrTarget {
bindings: string[];
@ -32,30 +29,24 @@ 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 compiler = new Compiler(ast, source, options.name || 'SvelteComponent', stylesheet, options, stats, false, target);
const { computations, name, templateProperties } = compiler;
const { computations, name, templateProperties } = component;
// create main render() function
trim(compiler.fragment.children).forEach((node: Node) => {
trim(component.fragment.children).forEach((node: Node) => {
node.ssr();
});
const css = compiler.customElement ?
const css = component.customElement ?
{ code: null, map: null } :
compiler.stylesheet.render(options.filename, true);
component.stylesheet.render(options.filename, true);
// generate initial state object
const expectedProperties = Array.from(compiler.expectedProperties);
const expectedProperties = Array.from(component.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = expectedProperties.filter(prop => prop[0] === '$');
@ -77,11 +68,36 @@ export default function ssr(
initialState.push('ctx');
const helpers = new Set();
let js = null;
if (component.javascript) {
const componentDefinition = new CodeBuilder();
// not all properties are relevant to SSR (e.g. lifecycle hooks)
const relevant = new Set([
'data',
'components',
'computed',
'helpers',
'preload',
'store'
]);
component.declarations.forEach(declaration => {
if (relevant.has(declaration.type)) {
componentDefinition.addBlock(declaration.block);
}
});
js = (
component.javascript[0] +
componentDefinition +
component.javascript[1]
);
}
// TODO concatenate CSS maps
const result = deindent`
${compiler.javascript}
const result = (deindent`
${js}
var ${name} = {};
@ -123,7 +139,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 +147,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 = {
@ -146,9 +162,9 @@ export default function ssr(
var warned = false;
${templateProperties.preload && `${name}.preload = %preload;`}
`;
`).trim();
return compiler.generate(result, options, { name, format });
return component.generate(result, options, { name, format });
}
function trim(nodes) {

@ -0,0 +1,16 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function actions(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
component.error(prop, {
code: `invalid-actions`,
message: `The 'actions' property must be an object literal`
});
}
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
}

@ -1,18 +1,18 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function transitions(validator: Validator, prop: Node) {
export default function transitions(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
validator.error(prop, {
component.error(prop, {
code: `invalid-transitions-property`,
message: `The 'transitions' property must be an object literal`
});
}
checkForDupes(validator, prop.value.properties);
checkForComputedKeys(validator, prop.value.properties);
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach(() => {
// TODO probably some validation that can happen here...

@ -1,33 +1,33 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import getName from '../../../utils/getName';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import getName from '../../../../utils/getName';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function components(validator: Validator, prop: Node) {
export default function components(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
validator.error(prop, {
component.error(prop, {
code: `invalid-components-property`,
message: `The 'components' property must be an object literal`
});
}
checkForDupes(validator, prop.value.properties);
checkForComputedKeys(validator, prop.value.properties);
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach((component: Node) => {
const name = getName(component.key);
prop.value.properties.forEach((node: Node) => {
const name = getName(node.key);
if (name === 'state') {
// TODO is this still true?
validator.error(component, {
component.error(node, {
code: `invalid-name`,
message: `Component constructors cannot be called 'state' due to technical limitations`
});
}
if (!/^[A-Z]/.test(name)) {
validator.error(component, {
component.error(node, {
code: `component-lowercase`,
message: `Component names must be capitalised`
});

@ -1,50 +1,49 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import getName from '../../../utils/getName';
import isValidIdentifier from '../../../utils/isValidIdentifier';
import reservedNames from '../../../utils/reservedNames';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import walkThroughTopFunctionScope from '../../../utils/walkThroughTopFunctionScope';
import isThisGetCallExpression from '../../../utils/isThisGetCallExpression';
import validCalleeObjects from '../../../utils/validCalleeObjects';
import getName from '../../../../utils/getName';
import isValidIdentifier from '../../../../utils/isValidIdentifier';
import reservedNames from '../../../../utils/reservedNames';
import { Node } from '../../../../interfaces';
import walkThroughTopFunctionScope from '../../../../utils/walkThroughTopFunctionScope';
import isThisGetCallExpression from '../../../../utils/isThisGetCallExpression';
import Component from '../../../Component';
const isFunctionExpression = new Set([
'FunctionExpression',
'ArrowFunctionExpression',
]);
export default function computed(validator: Validator, prop: Node) {
export default function computed(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
validator.error(prop, {
component.error(prop, {
code: `invalid-computed-property`,
message: `The 'computed' property must be an object literal`
});
}
checkForDupes(validator, prop.value.properties);
checkForComputedKeys(validator, prop.value.properties);
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach((computation: Node) => {
const name = getName(computation.key);
if (!isValidIdentifier(name)) {
const suggestion = name.replace(/[^_$a-z0-9]/ig, '_').replace(/^\d/, '_$&');
validator.error(computation.key, {
component.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.key, {
component.error(computation.key, {
code: `invalid-computed-name`,
message: `Computed property name '${name}' is invalid — cannot be a JavaScript reserved word`
});
}
if (!isFunctionExpression.has(computation.value.type)) {
validator.error(computation.value, {
component.error(computation.value, {
code: `invalid-computed-value`,
message: `Computed properties can be function expressions or arrow function expressions`
});
@ -54,14 +53,14 @@ export default function computed(validator: Validator, prop: Node) {
walkThroughTopFunctionScope(body, (node: Node) => {
if (isThisGetCallExpression(node) && !node.callee.property.computed) {
validator.error(node, {
component.error(node, {
code: `impure-computed`,
message: `Cannot use this.get(...) — values must be passed into the function as arguments`
});
}
if (node.type === 'ThisExpression') {
validator.error(node, {
component.error(node, {
code: `impure-computed`,
message: `Computed properties should be pure functions — they do not have access to the component instance and cannot use 'this'. Did you mean to put this in 'methods'?`
});
@ -69,14 +68,14 @@ export default function computed(validator: Validator, prop: Node) {
});
if (params.length === 0) {
validator.error(computation.value, {
component.error(computation.value, {
code: `impure-computed`,
message: `A computed value must depend on at least one property`
});
}
if (params.length > 1) {
validator.error(computation.value, {
component.error(computation.value, {
code: `invalid-computed-arguments`,
message: `Computed properties must take a single argument`
});

@ -1,13 +1,13 @@
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
const disallowed = new Set(['Literal', 'ObjectExpression', 'ArrayExpression']);
export default function data(validator: Validator, prop: Node) {
export default function data(component: Component, prop: Node) {
while (prop.type === 'ParenthesizedExpression') prop = prop.expression;
if (disallowed.has(prop.value.type)) {
validator.error(prop.value, {
component.error(prop.value, {
code: `invalid-data-property`,
message: `'data' must be a function`
});

@ -0,0 +1,16 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function events(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
component.error(prop, {
code: `invalid-events-property`,
message: `The 'events' property must be an object literal`
});
}
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
}

@ -1,21 +1,20 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { walk } from 'estree-walker';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import walkThroughTopFunctionScope from '../../../utils/walkThroughTopFunctionScope';
import isThisGetCallExpression from '../../../utils/isThisGetCallExpression';
import { Node } from '../../../../interfaces';
import walkThroughTopFunctionScope from '../../../../utils/walkThroughTopFunctionScope';
import isThisGetCallExpression from '../../../../utils/isThisGetCallExpression';
import Component from '../../../Component';
export default function helpers(validator: Validator, prop: Node) {
export default function helpers(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
validator.error(prop, {
component.error(prop, {
code: `invalid-helpers-property`,
message: `The 'helpers' property must be an object literal`
});
}
checkForDupes(validator, prop.value.properties);
checkForComputedKeys(validator, prop.value.properties);
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach((prop: Node) => {
if (!/FunctionExpression/.test(prop.value.type)) return;
@ -24,14 +23,14 @@ export default function helpers(validator: Validator, prop: Node) {
walkThroughTopFunctionScope(prop.value.body, (node: Node) => {
if (isThisGetCallExpression(node) && !node.callee.property.computed) {
validator.error(node, {
component.error(node, {
code: `impure-helper`,
message: `Cannot use this.get(...) — values must be passed into the helper function as arguments`
});
}
if (node.type === 'ThisExpression') {
validator.error(node, {
component.error(node, {
code: `impure-helper`,
message: `Helpers should be pure functions — they do not have access to the component instance and cannot use 'this'. Did you mean to put this in 'methods'?`
});
@ -41,7 +40,7 @@ export default function helpers(validator: Validator, prop: Node) {
});
if (prop.value.params.length === 0 && !usesArguments) {
validator.warn(prop, {
component.warn(prop, {
code: `impure-helper`,
message: `Helpers should be pure functions, with at least one argument`
});

@ -1,9 +1,9 @@
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function immutable(validator: Validator, prop: Node) {
export default function immutable(component: Component, prop: Node) {
if (prop.value.type !== 'Literal' || typeof prop.value.value !== 'boolean') {
validator.error(prop.value, {
component.error(prop.value, {
code: `invalid-immutable-property`,
message: `'immutable' must be a boolean literal`
});

@ -2,29 +2,29 @@ import checkForAccessors from '../utils/checkForAccessors';
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import usesThisOrArguments from '../utils/usesThisOrArguments';
import getName from '../../../utils/getName';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import getName from '../../../../utils/getName';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
const builtin = new Set(['set', 'get', 'on', 'fire', 'destroy']);
export default function methods(validator: Validator, prop: Node) {
export default function methods(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
validator.error(prop, {
component.error(prop, {
code: `invalid-methods-property`,
message: `The 'methods' property must be an object literal`
});
}
checkForAccessors(validator, prop.value.properties, 'Methods');
checkForDupes(validator, prop.value.properties);
checkForComputedKeys(validator, prop.value.properties);
checkForAccessors(component, prop.value.properties, 'Methods');
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach((prop: Node) => {
const name = getName(prop.key);
if (builtin.has(name)) {
validator.error(prop, {
component.error(prop, {
code: `invalid-method-name`,
message: `Cannot overwrite built-in method '${name}'`
});
@ -32,7 +32,7 @@ export default function methods(validator: Validator, prop: Node) {
if (prop.value.type === 'ArrowFunctionExpression') {
if (usesThisOrArguments(prop.value.body)) {
validator.error(prop, {
component.error(prop, {
code: `invalid-method-value`,
message: `Method '${prop.key.name}' should be a function expression, not an arrow function expression`
});

@ -1,16 +1,16 @@
import * as namespaces from '../../../utils/namespaces';
import nodeToString from '../../../utils/nodeToString'
import * as namespaces from '../../../../utils/namespaces';
import nodeToString from '../../../../utils/nodeToString'
import fuzzymatch from '../../utils/fuzzymatch';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
const valid = new Set(namespaces.validNamespaces);
export default function namespace(validator: Validator, prop: Node) {
export default function namespace(component: Component, prop: Node) {
const ns = nodeToString(prop.value);
if (typeof ns !== 'string') {
validator.error(prop, {
component.error(prop, {
code: `invalid-namespace-property`,
message: `The 'namespace' property must be a string literal representing a valid namespace`
});
@ -19,12 +19,12 @@ export default function namespace(validator: Validator, prop: Node) {
if (!valid.has(ns)) {
const match = fuzzymatch(ns, namespaces.validNamespaces);
if (match) {
validator.error(prop, {
component.error(prop, {
code: `invalid-namespace-property`,
message: `Invalid namespace '${ns}' (did you mean '${match}'?)`
});
} else {
validator.error(prop, {
component.error(prop, {
code: `invalid-namespace-property`,
message: `Invalid namespace '${ns}'`
});

@ -1,11 +1,11 @@
import usesThisOrArguments from '../utils/usesThisOrArguments';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function oncreate(validator: Validator, prop: Node) {
export default function oncreate(component: Component, prop: Node) {
if (prop.value.type === 'ArrowFunctionExpression') {
if (usesThisOrArguments(prop.value.body)) {
validator.error(prop, {
component.error(prop, {
code: `invalid-oncreate-property`,
message: `'oncreate' should be a function expression, not an arrow function expression`
});

@ -1,11 +1,11 @@
import usesThisOrArguments from '../utils/usesThisOrArguments';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function ondestroy(validator: Validator, prop: Node) {
export default function ondestroy(component: Component, prop: Node) {
if (prop.value.type === 'ArrowFunctionExpression') {
if (usesThisOrArguments(prop.value.body)) {
validator.error(prop, {
component.error(prop, {
code: `invalid-ondestroy-property`,
message: `'ondestroy' should be a function expression, not an arrow function expression`
});

@ -0,0 +1,12 @@
import oncreate from './oncreate';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function onrender(component: Component, prop: Node) {
component.warn(prop, {
code: `deprecated-onrender`,
message: `'onrender' has been deprecated in favour of 'oncreate', and will cause an error in Svelte 2.x`
});
oncreate(component, prop);
}

@ -1,11 +1,11 @@
import usesThisOrArguments from '../utils/usesThisOrArguments';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function onstate(validator: Validator, prop: Node) {
export default function onstate(component: Component, prop: Node) {
if (prop.value.type === 'ArrowFunctionExpression') {
if (usesThisOrArguments(prop.value.body)) {
validator.error(prop, {
component.error(prop, {
code: `invalid-onstate-property`,
message: `'onstate' should be a function expression, not an arrow function expression`
});

@ -0,0 +1,12 @@
import ondestroy from './ondestroy';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function onteardown(component: Component, prop: Node) {
component.warn(prop, {
code: `deprecated-onteardown`,
message: `'onteardown' has been deprecated in favour of 'ondestroy', and will cause an error in Svelte 2.x`
});
ondestroy(component, prop);
}

@ -1,11 +1,11 @@
import usesThisOrArguments from '../utils/usesThisOrArguments';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function onupdate(validator: Validator, prop: Node) {
export default function onupdate(component: Component, prop: Node) {
if (prop.value.type === 'ArrowFunctionExpression') {
if (usesThisOrArguments(prop.value.body)) {
validator.error(prop, {
component.error(prop, {
code: `invalid-onupdate-property`,
message: `'onupdate' should be a function expression, not an arrow function expression`
});

@ -0,0 +1,6 @@
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function preload(component: Component, prop: Node) {
// not sure there's anything we need to check here...
}

@ -1,10 +1,10 @@
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import nodeToString from '../../../utils/nodeToString';
import { Node } from '../../../../interfaces';
import nodeToString from '../../../../utils/nodeToString';
import Component from '../../../Component';
export default function props(validator: Validator, prop: Node) {
export default function props(component: Component, prop: Node) {
if (prop.value.type !== 'ArrayExpression') {
validator.error(prop.value, {
component.error(prop.value, {
code: `invalid-props-property`,
message: `'props' must be an array expression, if specified`
});
@ -12,7 +12,7 @@ export default function props(validator: Validator, prop: Node) {
prop.value.elements.forEach((element: Node) => {
if (typeof nodeToString(element) !== 'string') {
validator.error(element, {
component.error(element, {
code: `invalid-props-property`,
message: `'props' must be an array of string literals`
});

@ -1,13 +1,13 @@
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
const disallowed = new Set(['Literal', 'ObjectExpression', 'ArrayExpression']);
export default function setup(validator: Validator, prop: Node) {
export default function setup(component: Component, prop: Node) {
while (prop.type === 'ParenthesizedExpression') prop = prop.expression;
if (disallowed.has(prop.value.type)) {
validator.error(prop.value, {
component.error(prop.value, {
code: `invalid-setup-property`,
message: `'setup' must be a function`
});

@ -0,0 +1,6 @@
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function store(component: Component, prop: Node) {
// not sure there's anything we need to check here...
}

@ -1,18 +1,18 @@
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import nodeToString from '../../../utils/nodeToString';
import { Node } from '../../../../interfaces';
import nodeToString from '../../../../utils/nodeToString';
import Component from '../../../Component';
export default function tag(validator: Validator, prop: Node) {
export default function tag(component: Component, prop: Node) {
const tag = nodeToString(prop.value);
if (typeof tag !== 'string') {
validator.error(prop.value, {
component.error(prop.value, {
code: `invalid-tag-property`,
message: `'tag' must be a string literal`
});
}
if (!/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) {
validator.error(prop.value, {
component.error(prop.value, {
code: `invalid-tag-property`,
message: `tag name must be two or more words joined by the '-' character`
});

@ -1,18 +1,18 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function transitions(validator: Validator, prop: Node) {
export default function transitions(component: Component, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
validator.error(prop, {
component.error(prop, {
code: `invalid-transitions-property`,
message: `The 'transitions' property must be an object literal`
});
}
checkForDupes(validator, prop.value.properties);
checkForComputedKeys(validator, prop.value.properties);
checkForDupes(component, prop.value.properties);
checkForComputedKeys(component, prop.value.properties);
prop.value.properties.forEach(() => {
// TODO probably some validation that can happen here...

@ -1,14 +1,14 @@
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function checkForAccessors(
validator: Validator,
component: Component,
properties: Node[],
label: string
) {
properties.forEach(prop => {
if (prop.kind !== 'init') {
validator.error(prop, {
component.error(prop, {
code: `illegal-accessor`,
message: `${label} cannot use getters and setters`
});

@ -1,13 +1,13 @@
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces';
import Component from '../../../Component';
export default function checkForComputedKeys(
validator: Validator,
component: Component,
properties: Node[]
) {
properties.forEach(prop => {
if (prop.key.computed) {
validator.error(prop, {
component.error(prop, {
code: `computed-key`,
message: `Cannot use computed keys`
});

@ -1,9 +1,9 @@
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
import getName from '../../../utils/getName';
import { Node } from '../../../../interfaces';
import getName from '../../../../utils/getName';
import Component from '../../../Component';
export default function checkForDupes(
validator: Validator,
component: Component,
properties: Node[]
) {
const seen = new Set();
@ -12,7 +12,7 @@ export default function checkForDupes(
const name = getName(prop.key);
if (seen.has(name)) {
validator.error(prop, {
component.error(prop, {
code: `duplicate-property`,
message: `Duplicate property '${name}'`
});

@ -1,6 +1,6 @@
import { walk } from 'estree-walker';
import isReference from 'is-reference';
import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces';
export default function usesThisOrArguments(node: Node) {
let result = false;

@ -1,8 +1,8 @@
import MagicString from 'magic-string';
import Stylesheet from './Stylesheet';
import { gatherPossibleValues, UNKNOWN } from './gatherPossibleValues';
import { Validator } from '../validate/index';
import { Node } from '../interfaces';
import Component from '../compile/Component';
export default class Selector {
node: Node;
@ -97,13 +97,13 @@ export default class Selector {
});
}
validate(validator: Validator) {
validate(component: Component) {
this.blocks.forEach((block) => {
let i = block.selectors.length;
while (i-- > 1) {
const selector = block.selectors[i];
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
validator.error(selector, {
component.error(selector, {
code: `css-invalid-global`,
message: `:global(...) must be the first element in a compound selector`
});
@ -124,7 +124,7 @@ export default class Selector {
for (let i = start; i < end; i += 1) {
if (this.blocks[i].global) {
validator.error(this.blocks[i].selectors[0], {
component.error(this.blocks[i].selectors[0], {
code: `css-invalid-global`,
message: `:global(...) can be at the start or end of a selector sequence, but not in the middle`
});
@ -174,7 +174,7 @@ function applySelector(stylesheet: Stylesheet, blocks: Block[], node: Node, stac
}
else if (selector.type === 'RefSelector') {
if (node.ref === selector.name) {
if (node.ref && node.ref.name === selector.name) {
stylesheet.nodesWithRefCssClass.set(selector.name, node);
toEncapsulate.push({ node, block });
return true;

@ -6,8 +6,8 @@ import getCodeFrame from '../utils/getCodeFrame';
import hash from '../utils/hash';
import removeCSSPrefix from '../utils/removeCSSPrefix';
import Element from '../compile/nodes/Element';
import { Validator } from '../validate/index';
import { Node, Ast, Warning } from '../interfaces';
import Component from '../compile/Component';
const isKeyframesNode = (node: Node) => removeCSSPrefix(node.name) === 'keyframes'
@ -78,9 +78,9 @@ class Rule {
this.declarations.forEach(declaration => declaration.transform(code, keyframes));
}
validate(validator: Validator) {
validate(component: Component) {
this.selectors.forEach(selector => {
selector.validate(validator);
selector.validate(component);
});
}
@ -220,9 +220,9 @@ class Atrule {
})
}
validate(validator: Validator) {
validate(component: Component) {
this.children.forEach(child => {
child.validate(validator);
child.validate(component);
});
}
@ -388,9 +388,9 @@ export default class Stylesheet {
};
}
validate(validator: Validator) {
validate(component: Component) {
this.children.forEach(child => {
child.validate(validator);
child.validate(component);
});
}

@ -1,149 +1,10 @@
import parse from './parse/index';
import validate from './validate/index';
import generate from './compile/dom/index';
import generateSSR from './compile/ssr/index';
import Stats from './Stats';
import { assign } from './shared/index.js';
import Stylesheet from './css/Stylesheet';
import { Ast, CompileOptions, Warning, PreprocessOptions, Preprocessor } from './interfaces';
import { SourceMap } from 'magic-string';
import compile from './compile/index';
import { CompileOptions } from './interfaces';
const version = '__VERSION__';
export function create(source: string, options: CompileOptions = {}) {
options.format = 'eval';
function normalizeOptions(options: CompileOptions): CompileOptions {
let normalizedOptions = assign({ generate: 'dom' }, options);
const { onwarn, onerror } = normalizedOptions;
normalizedOptions.onwarn = onwarn
? (warning: Warning) => onwarn(warning, defaultOnwarn)
: defaultOnwarn;
normalizedOptions.onerror = onerror
? (error: Error) => onerror(error, defaultOnerror)
: defaultOnerror;
return normalizedOptions;
}
function defaultOnwarn(warning: Warning) {
if (warning.start) {
console.warn(
`(${warning.start.line}:${warning.start.column}) ${warning.message}`
); // eslint-disable-line no-console
} else {
console.warn(warning.message); // eslint-disable-line no-console
}
}
function defaultOnerror(error: Error) {
throw error;
}
function parseAttributeValue(value: string) {
return /^['"]/.test(value) ?
value.slice(1, -1) :
value;
}
function parseAttributes(str: string) {
const attrs = {};
str.split(/\s+/).filter(Boolean).forEach(attr => {
const [name, value] = attr.split('=');
attrs[name] = value ? parseAttributeValue(value) : true;
});
return attrs;
}
async function replaceTagContents(source, type: 'script' | 'style', preprocessor: Preprocessor, options: PreprocessOptions) {
const exp = new RegExp(`<${type}([\\S\\s]*?)>([\\S\\s]*?)<\\/${type}>`, 'ig');
const match = exp.exec(source);
if (match) {
const attributes: Record<string, string | boolean> = parseAttributes(match[1]);
const content: string = match[2];
const processed: { code: string, map?: SourceMap | string } = await preprocessor({
content,
attributes,
filename : options.filename
});
if (processed && processed.code) {
return (
source.slice(0, match.index) +
`<${type}>${processed.code}</${type}>` +
source.slice(match.index + match[0].length)
);
}
}
return source;
}
export async function preprocess(source: string, options: PreprocessOptions) {
const { markup, style, script } = options;
if (!!markup) {
const processed: { code: string, map?: SourceMap | string } = await markup({
content: source,
filename: options.filename
});
source = processed.code;
}
if (!!style) {
source = await replaceTagContents(source, 'style', style, options);
}
if (!!script) {
source = await replaceTagContents(source, 'script', script, options);
}
return {
// TODO return separated output, in future version where svelte.compile supports it:
// style: { code: styleCode, map: styleMap },
// script { code: scriptCode, map: scriptMap },
// markup { code: markupCode, map: markupMap },
toString() {
return source;
}
};
}
function compile(source: string, _options: CompileOptions) {
const options = normalizeOptions(_options);
let ast: Ast;
const stats = new Stats({
onwarn: options.onwarn
});
try {
stats.start('parse');
ast = parse(source, options);
stats.stop('parse');
} catch (err) {
options.onerror(err);
return;
}
stats.start('stylesheet');
const stylesheet = new Stylesheet(source, ast, options.filename, options.dev);
stats.stop('stylesheet');
stats.start('validate');
validate(ast, source, stylesheet, stats, options);
stats.stop('validate');
if (options.generate === false) {
return { ast, stats: stats.render(null), js: null, css: null };
}
const compiler = options.generate === 'ssr' ? generateSSR : generate;
return compiler(ast, source, stylesheet, options, stats);
};
function create(source: string, _options: CompileOptions = {}) {
_options.format = 'eval';
const compiled = compile(source, _options);
const compiled = compile(source, options);
if (!compiled || !compiled.js.code) {
return;
@ -152,8 +13,8 @@ function create(source: string, _options: CompileOptions = {}) {
try {
return (new Function(`return ${compiled.js.code}`))();
} catch (err) {
if (_options.onerror) {
_options.onerror(err);
if (options.onerror) {
options.onerror(err);
return;
} else {
throw err;
@ -161,4 +22,7 @@ function create(source: string, _options: CompileOptions = {}) {
}
}
export { parse, create, compile, version as VERSION };
export { default as compile } from './compile/index';
export { default as parse } from './parse/index';
export { default as preprocess } from './preprocess/index';
export const VERSION = '__VERSION__';

@ -1,5 +1,3 @@
import {SourceMap} from 'magic-string';
export interface Node {
start: number;
end: number;
@ -67,7 +65,7 @@ export interface CompileOptions {
// to remove in v3
skipIntroByDefault?: boolean;
nestedTransitions: boolean;
nestedTransitions?: boolean;
}
export interface GenerateOptions {
@ -92,15 +90,6 @@ export interface CustomElementOptions {
props?: string[];
}
export interface PreprocessOptions {
markup?: (options: {content: string, filename: string}) => { code: string, map?: SourceMap | string };
style?: Preprocessor;
script?: Preprocessor;
filename?: string
}
export type Preprocessor = (options: {content: string, attributes: Record<string, string | boolean>, filename?: string}) => { code: string, map?: SourceMap | string };
export interface AppendTarget {
slots: Record<string, string>;
slotStack: string[]

@ -1,17 +1,15 @@
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import { locate, Location } from 'locate-character';
import fragment from './state/fragment';
import { whitespace } from '../utils/patterns';
import { trimStart, trimEnd } from '../utils/trim';
import reservedNames from '../utils/reservedNames';
import fullCharCodeAt from '../utils/fullCharCodeAt';
import { Node, Ast } from '../interfaces';
import { Node, Ast, CustomElementOptions } from '../interfaces';
import error from '../utils/error';
interface ParserOptions {
filename?: string;
bind?: boolean;
customElement?: boolean;
customElement?: CustomElementOptions | true;
}
type ParserState = (parser: Parser) => (ParserState | void);
@ -19,7 +17,7 @@ type ParserState = (parser: Parser) => (ParserState | void);
export class Parser {
readonly template: string;
readonly filename?: string;
readonly customElement: boolean;
readonly customElement: CustomElementOptions | true;
index: number;
stack: Array<Node>;

@ -65,7 +65,7 @@ function parentIsHead(stack) {
while (i--) {
const { type } = stack[i];
if (type === 'Head') return true;
if (type === 'Element' || type === 'Component') return false;
if (type === 'Element' || type === 'InlineComponent') return false;
}
return false;
}
@ -123,7 +123,7 @@ export default function tag(parser: Parser) {
const type = metaTags.has(name)
? metaTags.get(name)
: (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'Component'
: (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent'
: name === 'title' && parentIsHead(parser.stack) ? 'Title'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';

@ -0,0 +1,100 @@
import { SourceMap } from 'magic-string';
export interface PreprocessOptions {
markup?: (options: {
content: string,
filename: string
}) => { code: string, map?: SourceMap | string };
style?: Preprocessor;
script?: Preprocessor;
filename?: string
}
export type Preprocessor = (options: {
content: string,
attributes: Record<string, string | boolean>,
filename?: string
}) => { code: string, map?: SourceMap | string };
function parseAttributeValue(value: string) {
return /^['"]/.test(value) ?
value.slice(1, -1) :
value;
}
function parseAttributes(str: string) {
const attrs = {};
str.split(/\s+/).filter(Boolean).forEach(attr => {
const [name, value] = attr.split('=');
attrs[name] = value ? parseAttributeValue(value) : true;
});
return attrs;
}
async function replaceTagContents(
source,
type: 'script' | 'style',
preprocessor: Preprocessor,
options: PreprocessOptions
) {
const exp = new RegExp(`<${type}([\\S\\s]*?)>([\\S\\s]*?)<\\/${type}>`, 'ig');
const match = exp.exec(source);
if (match) {
const attributes: Record<string, string | boolean> = parseAttributes(match[1]);
const content: string = match[2];
const processed: { code: string, map?: SourceMap | string } = await preprocessor({
content,
attributes,
filename : options.filename
});
if (processed && processed.code) {
return (
source.slice(0, match.index) +
`<${type}>${processed.code}</${type}>` +
source.slice(match.index + match[0].length)
);
}
}
return source;
}
export default async function preprocess(
source: string,
options: PreprocessOptions
) {
const { markup, style, script } = options;
if (!!markup) {
const processed: {
code: string,
map?: SourceMap | string
} = await markup({
content: source,
filename: options.filename
});
source = processed.code;
}
if (!!style) {
source = await replaceTagContents(source, 'style', style, options);
}
if (!!script) {
source = await replaceTagContents(source, 'script', script, options);
}
return {
// TODO return separated output, in future version where svelte.compile supports it:
// style: { code: styleCode, map: styleMap },
// script { code: scriptCode, map: scriptMap },
// markup { code: markupCode, map: markupMap },
toString() {
return source;
}
};
}

@ -1,8 +1,11 @@
import Compiler from '../compile/Compiler';
import Component from '../compile/Component';
import { Node } from '../interfaces';
export default function createDebuggingComment(node: Node, compiler: Compiler) {
const { locate, source } = compiler;
export default function createDebuggingComment(
node: Node,
component: Component
) {
const { locate, source } = component;
let c = node.start;
if (node.type === 'ElseBlock') {

@ -1,240 +0,0 @@
import * as namespaces from '../../utils/namespaces';
import getStaticAttributeValue from '../../utils/getStaticAttributeValue';
import fuzzymatch from '../utils/fuzzymatch';
import validateEventHandler from './validateEventHandler';
import { Validator } from '../index';
import { Node } from '../../interfaces';
const ariaAttributes = 'activedescendant atomic autocomplete busy checked controls current describedby details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription selected setsize sort valuemax valuemin valuenow valuetext'.split(' ');
const ariaAttributeSet = new Set(ariaAttributes);
const ariaRoles = 'alert alertdialog application article banner button cell checkbox columnheader combobox command complementary composite contentinfo definition dialog directory document feed figure form grid gridcell group heading img input landmark link list listbox listitem log main marquee math menu menubar menuitem menuitemcheckbox menuitemradio navigation none note option presentation progressbar radio radiogroup range region roletype row rowgroup rowheader scrollbar search searchbox section sectionhead select separator slider spinbutton status structure switch tab table tablist tabpanel term textbox timer toolbar tooltip tree treegrid treeitem widget window'.split(' ');
const ariaRoleSet = new Set(ariaRoles);
const invisibleElements = new Set(['meta', 'html', 'script', 'style']);
export default function a11y(
validator: Validator,
node: Node,
elementStack: Node[]
) {
if (node.type === 'Text') {
// accessible-emoji
return;
}
if (node.type !== 'Element') return;
const attributeMap = new Map();
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Spread') return;
const name = attribute.name.toLowerCase();
// aria-props
if (name.startsWith('aria-')) {
if (invisibleElements.has(node.name)) {
// aria-unsupported-elements
validator.warn(attribute, {
code: `a11y-aria-attributes`,
message: `A11y: <${node.name}> should not have aria-* attributes`
});
}
const type = name.slice(5);
if (!ariaAttributeSet.has(type)) {
const match = fuzzymatch(type, ariaAttributes);
let message = `A11y: Unknown aria attribute 'aria-${type}'`;
if (match) message += ` (did you mean '${match}'?)`;
validator.warn(attribute, {
code: `a11y-unknown-aria-attribute`,
message
});
}
}
// aria-role
if (name === 'role') {
if (invisibleElements.has(node.name)) {
// aria-unsupported-elements
validator.warn(attribute, {
code: `a11y-misplaced-role`,
message: `A11y: <${node.name}> should not have role attribute`
});
}
const value = getStaticAttributeValue(node, 'role');
if (value && !ariaRoleSet.has(value)) {
const match = fuzzymatch(value, ariaRoles);
let message = `A11y: Unknown role '${value}'`;
if (match) message += ` (did you mean '${match}'?)`;
validator.warn(attribute, {
code: `a11y-unknown-role`,
message
});
}
}
// no-access-key
if (name === 'accesskey') {
validator.warn(attribute, {
code: `a11y-accesskey`,
message: `A11y: Avoid using accesskey`
});
}
// no-autofocus
if (name === 'autofocus') {
validator.warn(attribute, {
code: `a11y-autofocus`,
message: `A11y: Avoid using autofocus`
});
}
// scope
if (name === 'scope' && node.name !== 'th') {
validator.warn(attribute, {
code: `a11y-misplaced-scope`,
message: `A11y: The scope attribute should only be used with <th> elements`
});
}
// tabindex-no-positive
if (name === 'tabindex') {
const value = getStaticAttributeValue(node, 'tabindex');
if (!isNaN(value) && +value > 0) {
validator.warn(attribute, {
code: `a11y-positive-tabindex`,
message: `A11y: avoid tabindex values above zero`
});
}
}
attributeMap.set(attribute.name, attribute);
});
function shouldHaveAttribute(attributes: string[], name = node.name) {
if (attributes.length === 0 || !attributes.some((name: string) => attributeMap.has(name))) {
const article = /^[aeiou]/.test(attributes[0]) ? 'an' : 'a';
const sequence = attributes.length > 1 ?
attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` :
attributes[0];
validator.warn(node, {
code: `a11y-missing-attribute`,
message: `A11y: <${name}> element should have ${article} ${sequence} attribute`
});
}
}
function shouldHaveContent() {
if (node.children.length === 0) {
validator.warn(node, {
code: `a11y-missing-content`,
message: `A11y: <${node.name}> element should have child content`
});
}
}
function shouldHaveValidHref (attribute) {
const href = attributeMap.get(attribute);
const value = getStaticAttributeValue(node, attribute);
if (value === '' || value === '#') {
validator.warn(href, {
code: `a11y-invalid-attribute`,
message: `A11y: '${value}' is not a valid ${attribute} attribute`
});
}
}
if (node.name === 'a') {
if (attributeMap.has('href')) {
// anchor-is-valid
shouldHaveValidHref('href')
} else if (attributeMap.has('xlink:href')) {
// anchor-in-svg-is-valid
shouldHaveValidHref('xlink:href')
} else {
validator.warn(node, {
code: `a11y-missing-attribute`,
message: `A11y: <a> element should have an href attribute`
});
}
// anchor-has-content
shouldHaveContent();
}
if (node.name === 'img') shouldHaveAttribute(['alt']);
if (node.name === 'area') shouldHaveAttribute(['alt', 'aria-label', 'aria-labelledby']);
if (node.name === 'object') shouldHaveAttribute(['title', 'aria-label', 'aria-labelledby']);
if (node.name === 'input' && getStaticAttributeValue(node, 'type') === 'image') {
shouldHaveAttribute(['alt', 'aria-label', 'aria-labelledby'], 'input type="image"');
}
// heading-has-content
if (/^h[1-6]$/.test(node.name)) {
shouldHaveContent();
if (attributeMap.has('aria-hidden')) {
validator.warn(attributeMap.get('aria-hidden'), {
code: `a11y-hidden`,
message: `A11y: <${node.name}> element should not be hidden`
});
}
}
// iframe-has-title
if (node.name === 'iframe') {
shouldHaveAttribute(['title']);
}
// html-has-lang
if (node.name === 'html') {
shouldHaveAttribute(['lang']);
}
// no-distracting-elements
if (node.name === 'marquee' || node.name === 'blink') {
validator.warn(node, {
code: `a11y-distracting-elements`,
message: `A11y: Avoid <${node.name}> elements`
});
}
if (node.name === 'figcaption') {
const parent = elementStack[elementStack.length - 1];
if (parent) {
if (parent.name !== 'figure') {
validator.warn(node, {
code: `a11y-structure`,
message: `A11y: <figcaption> must be an immediate child of <figure>`
});
} else {
const children = parent.children.filter(node => {
if (node.type === 'Comment') return false;
if (node.type === 'Text') return /\S/.test(node.data);
return true;
});
const index = children.indexOf(node);
if (index !== 0 && index !== children.length - 1) {
validator.warn(node, {
code: `a11y-structure`,
message: `A11y: <figcaption> must be first or last child of <figure>`
});
}
}
}
}
}
function getValue(attribute: Node) {
if (attribute.value.length === 0) return '';
if (attribute.value.length === 1 && attribute.value[0].type === 'Text') return attribute.value[0].data;
return null;
}

@ -1,123 +0,0 @@
import validateComponent from './validateComponent';
import validateElement from './validateElement';
import validateWindow from './validateWindow';
import validateHead from './validateHead';
import validateSlot from './validateSlot';
import a11y from './a11y';
import fuzzymatch from '../utils/fuzzymatch'
import flattenReference from '../../utils/flattenReference';
import { Validator } from '../index';
import { Node } from '../../interfaces';
import unpackDestructuring from '../../utils/unpackDestructuring';
function isEmptyBlock(node: Node) {
if (!/Block$/.test(node.type) || !node.children) return false;
if (node.children.length > 1) return false;
const child = node.children[0];
return !child || (child.type === 'Text' && !/[^ \r\n\f\v\t]/.test(child.data));
}
export default function validateHtml(validator: Validator, html: Node) {
const refs = new Map();
const refCallees: Node[] = [];
const stack: Node[] = [];
const elementStack: Node[] = [];
function visit(node: Node) {
if (node.type === 'Window') {
validateWindow(validator, node, refs, refCallees);
}
else if (node.type === 'Head') {
validateHead(validator, node, refs, refCallees);
}
else if (node.type === 'Slot') {
validateSlot(validator, node);
}
else if (node.type === 'Component' || node.name === 'svelte:self' || node.name === 'svelte:component') {
validateComponent(
validator,
node,
refs,
refCallees,
stack,
elementStack
);
}
else if (node.type === 'Element') {
validateElement(
validator,
node,
refs,
refCallees,
stack,
elementStack
);
a11y(validator, node, elementStack);
}
else if (node.type === 'EachBlock') {
const contexts = [];
unpackDestructuring(contexts, node.context, '');
contexts.forEach(prop => {
if (validator.helpers.has(prop.key.name)) {
validator.warn(prop.key, {
code: `each-context-clash`,
message: `Context clashes with a helper. Rename one or the other to eliminate any ambiguity`
});
}
});
}
if (validator.options.dev && isEmptyBlock(node)) {
validator.warn(node, {
code: `empty-block`,
message: 'Empty block'
});
}
if (node.children) {
if (node.type === 'Element') elementStack.push(node);
stack.push(node);
node.children.forEach(visit);
stack.pop();
if (node.type === 'Element') elementStack.pop();
}
if (node.else) {
visit(node.else);
}
if (node.type === 'AwaitBlock') {
visit(node.pending);
visit(node.then);
visit(node.catch);
}
}
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
});
}
});
}

@ -1,59 +0,0 @@
import * as namespaces from '../../utils/namespaces';
import validateEventHandler from './validateEventHandler';
import validate, { Validator } from '../index';
import { Node } from '../../interfaces';
import isValidIdentifier from '../../utils/isValidIdentifier';
export default function validateComponent(
validator: Validator,
node: Node,
refs: Map<string, Node[]>,
refCallees: Node[],
stack: Node[],
elementStack: Node[]
) {
if (node.name !== 'svelte:self' && node.name !== 'svelte:component' && !validator.components.has(node.name)) {
validator.error(node, {
code: `missing-component`,
message: `${node.name} component is not defined`
});
}
validator.used.components.add(node.name);
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Ref') {
if (!isValidIdentifier(attribute.name)) {
const suggestion = attribute.name.replace(/[^_$a-z0-9]/ig, '_').replace(/^\d/, '_$&');
validator.error(attribute, {
code: `invalid-reference-name`,
message: `Reference name '${attribute.name}' is invalid — must be a valid identifier such as ${suggestion}`
});
} else {
if (!refs.has(attribute.name)) refs.set(attribute.name, []);
refs.get(attribute.name).push(node);
}
}
if (attribute.type === 'EventHandler') {
validator.used.events.add(attribute.name);
validateEventHandler(validator, attribute, refCallees);
} else if (attribute.type === 'Transition') {
validator.error(attribute, {
code: `invalid-transition`,
message: `Transitions can only be applied to DOM elements, not components`
});
} else if (attribute.type === 'Action') {
validator.error(attribute, {
code: `invalid-action`,
message: `Actions can only be applied to DOM elements, not components`
});
} else if (attribute.type === 'Class') {
validator.error(attribute, {
code: `invalid-class`,
message: `Classes can only be applied to DOM elements, not components`
});
}
});
}

@ -1,378 +0,0 @@
import * as namespaces from '../../utils/namespaces';
import validateEventHandler from './validateEventHandler';
import validate, { Validator } from '../index';
import { Node } from '../../interfaces';
import { dimensions } from '../../utils/patterns';
import isVoidElementName from '../../utils/isVoidElementName';
import isValidIdentifier from '../../utils/isValidIdentifier';
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/;
export default function validateElement(
validator: Validator,
node: Node,
refs: Map<string, Node[]>,
refCallees: Node[],
stack: Node[],
elementStack: Node[]
) {
if (elementStack.length === 0 && validator.namespace !== namespaces.svg && svg.test(node.name)) {
validator.warn(node, {
code: `missing-namespace`,
message: `<${node.name}> is an SVG element did you forget to add { namespace: 'svg' } ?`
});
}
if (node.name === 'slot') {
const nameAttribute = node.attributes.find((attribute: Node) => attribute.name === 'name');
if (nameAttribute) {
if (nameAttribute.value.length !== 1 || nameAttribute.value[0].type !== 'Text') {
validator.error(nameAttribute, {
code: `dynamic-slot-name`,
message: `<slot> name cannot be dynamic`
});
}
const slotName = nameAttribute.value[0].data;
if (slotName === 'default') {
validator.error(nameAttribute, {
code: `invalid-slot-name`,
message: `default is a reserved word — it cannot be used as a slot name`
});
}
// TODO should duplicate slots be disallowed? Feels like it's more likely to be a
// bug than anything. Perhaps it should be a warning
// if (validator.slots.has(slotName)) {
// validator.error(`duplicate '${slotName}' <slot> element`, nameAttribute.start);
// }
// validator.slots.add(slotName);
} else {
// if (validator.slots.has('default')) {
// validator.error(`duplicate default <slot> element`, node.start);
// }
// validator.slots.add('default');
}
}
if (node.name === 'title') {
if (node.attributes.length > 0) {
validator.error(node.attributes[0], {
code: `illegal-attribute`,
message: `<title> cannot have attributes`
});
}
node.children.forEach(child => {
if (child.type !== 'Text' && child.type !== 'MustacheTag') {
validator.error(child, {
code: 'illegal-structure',
message: `<title> can only contain text and {tags}`
});
}
});
}
let hasIntro: boolean;
let hasOutro: boolean;
let hasTransition: boolean;
let hasAnimation: boolean;
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Ref') {
if (!isValidIdentifier(attribute.name)) {
const suggestion = attribute.name.replace(/[^_$a-z0-9]/ig, '_').replace(/^\d/, '_$&');
validator.error(attribute, {
code: `invalid-reference-name`,
message: `Reference name '${attribute.name}' is invalid — must be a valid identifier such as ${suggestion}`
});
} else {
if (!refs.has(attribute.name)) refs.set(attribute.name, []);
refs.get(attribute.name).push(node);
}
}
if (attribute.type === 'Binding') {
const { name } = attribute;
if (name === 'value') {
if (
node.name !== 'input' &&
node.name !== 'textarea' &&
node.name !== 'select'
) {
validator.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)) {
validator.error(attribute, {
code: `dynamic-multiple-attribute`,
message: `'multiple' attribute cannot be dynamic if select uses two-way binding`
});
}
} else {
checkTypeAttribute(validator, node);
}
} else if (name === 'checked' || name === 'indeterminate') {
if (node.name !== 'input') {
validator.error(attribute, {
code: `invalid-binding`,
message: `'${name}' is not a valid binding on <${node.name}> elements`
});
}
if (checkTypeAttribute(validator, node) !== 'checkbox') {
validator.error(attribute, {
code: `invalid-binding`,
message: `'${name}' binding can only be used with <input type="checkbox">`
});
}
} else if (name === 'group') {
if (node.name !== 'input') {
validator.error(attribute, {
code: `invalid-binding`,
message: `'group' is not a valid binding on <${node.name}> elements`
});
}
const type = checkTypeAttribute(validator, node);
if (type !== 'checkbox' && type !== 'radio') {
validator.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') {
validator.error(attribute, {
code: `invalid-binding`,
message: `'files' binding acn only be used with <input type="file">`
});
}
const type = checkTypeAttribute(validator, node);
if (type !== 'file') {
validator.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') {
validator.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')) {
validator.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)) {
validator.error(attribute, {
code: 'invalid-binding',
message: `'${attribute.name}' is not a valid binding on SVG elements`
});
} else if (isVoidElementName(node.name)) {
validator.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 {
validator.error(attribute, {
code: `invalid-binding`,
message: `'${attribute.name}' is not a valid binding`
});
}
} else if (attribute.type === 'EventHandler') {
validator.used.events.add(attribute.name);
validateEventHandler(validator, attribute, refCallees);
} else if (attribute.type === 'Transition') {
validator.used.transitions.add(attribute.name);
const bidi = attribute.intro && attribute.outro;
if (hasTransition) {
if (bidi) {
validator.error(attribute, {
code: `duplicate-transition`,
message: `An element can only have one 'transition' directive`
});
}
validator.error(attribute, {
code: `duplicate-transition`,
message: `An element cannot have both a 'transition' directive and an '${attribute.intro ? 'in' : 'out'}' directive`
});
}
if ((hasIntro && attribute.intro) || (hasOutro && attribute.outro)) {
if (bidi) {
validator.error(attribute, {
code: `duplicate-transition`,
message: `An element cannot have both an '${hasIntro ? 'in' : 'out'}' directive and a 'transition' directive`
});
}
validator.error(attribute, {
code: `duplicate-transition`,
message: `An element can only have one '${hasIntro ? 'in' : 'out'}' directive`
});
}
if (attribute.intro) hasIntro = true;
if (attribute.outro) hasOutro = true;
if (bidi) hasTransition = true;
if (!validator.transitions.has(attribute.name)) {
validator.error(attribute, {
code: `missing-transition`,
message: `Missing transition '${attribute.name}'`
});
}
} else if (attribute.type === 'Animation') {
validator.used.animations.add(attribute.name);
if (hasAnimation) {
validator.error(attribute, {
code: `duplicate-animation`,
message: `An element can only have one 'animate' directive`
});
}
if (!validator.animations.has(attribute.name)) {
validator.error(attribute, {
code: `missing-animation`,
message: `Missing animation '${attribute.name}'`
});
}
const parent = stack[stack.length - 1];
if (!parent || parent.type !== 'EachBlock' || !parent.key) {
// TODO can we relax the 'immediate child' rule?
validator.error(attribute, {
code: `invalid-animation`,
message: `An element that use the animate directive must be the immediate child of a keyed each block`
});
}
if (parent.children.length > 1) {
validator.error(attribute, {
code: `invalid-animation`,
message: `An element that use the animate directive must be the sole child of a keyed each block`
});
}
hasAnimation = true;
} else if (attribute.type === 'Attribute') {
if (attribute.name === 'value' && node.name === 'textarea') {
if (node.children.length) {
validator.error(attribute, {
code: `textarea-duplicate-value`,
message: `A <textarea> can have either a value attribute or (equivalently) child content, but not both`
});
}
}
if (attribute.name === 'slot') {
checkSlotAttribute(validator, node, attribute, stack);
}
} else if (attribute.type === 'Action') {
validator.used.actions.add(attribute.name);
if (!validator.actions.has(attribute.name)) {
validator.error(attribute, {
code: `missing-action`,
message: `Missing action '${attribute.name}'`
});
}
}
});
}
function checkTypeAttribute(validator: Validator, node: Node) {
const attribute = node.attributes.find(
(attribute: Node) => attribute.name === 'type'
);
if (!attribute) return null;
if (attribute.value === true) {
validator.error(attribute, {
code: `missing-type`,
message: `'type' attribute must be specified`
});
}
if (isDynamic(attribute)) {
validator.error(attribute, {
code: `invalid-type`,
message: `'type' attribute cannot be dynamic if input uses two-way binding`
});
}
return attribute.value[0].data;
}
function checkSlotAttribute(validator: Validator, node: Node, attribute: Node, stack: Node[]) {
if (isDynamic(attribute)) {
validator.error(attribute, {
code: `invalid-slot-attribute`,
message: `slot attribute cannot have a dynamic value`
});
}
let i = stack.length;
while (i--) {
const parent = stack[i];
if (parent.type === 'Component') {
// if we're inside a component or a custom element, gravy
if (parent.name === 'svelte:self' || parent.name === 'svelte:component' || validator.components.has(parent.name)) return;
} else if (parent.type === 'Element') {
if (/-/.test(parent.name)) return;
}
if (parent.type === 'IfBlock' || parent.type === 'EachBlock') {
const message = `Cannot place slotted elements inside an ${parent.type === 'IfBlock' ? 'if' : 'each'}-block`;
validator.error(attribute, {
code: `invalid-slotted-content`,
message
});
}
}
validator.error(attribute, {
code: `invalid-slotted-content`,
message: `Element with a slot='...' attribute must be a descendant of a component or custom element`
});
}
function isDynamic(attribute: Node) {
if (attribute.value === true) return false;
return attribute.value.length > 1 || attribute.value[0].type !== 'Text';
}

@ -1,67 +0,0 @@
import flattenReference from '../../utils/flattenReference';
import list from '../../utils/list';
import validate, { Validator } from '../index';
import validCalleeObjects from '../../utils/validCalleeObjects';
import { Node } from '../../interfaces';
const validBuiltins = new Set(['set', 'fire', 'destroy']);
export default function validateEventHandlerCallee(
validator: Validator,
attribute: Node,
refCallees: Node[]
) {
if (!attribute.expression) return;
const { callee, type } = attribute.expression;
if (type !== 'CallExpression') {
validator.error(attribute.expression, {
code: `invalid-event-handler`,
message: `Expected a call expression`
});
}
const { name } = flattenReference(callee);
if (validCalleeObjects.has(name) || name === 'options') return;
if (name === 'refs') {
refCallees.push(callee);
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.*', 'refs.*', 'event.*', 'options.*', 'console.*'].concat(
Array.from(validBuiltins),
Array.from(validator.methods.keys())
);
let message = `'${validator.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)}(...)'?)`;
} 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, {
code: `invalid-callee`,
message
});
}

@ -1,19 +0,0 @@
import validateElement from './validateElement';
import { Validator } from '../index';
import { Node } from '../../interfaces';
export default function validateHead(validator: Validator, node: Node, refs: Map<string, Node[]>, refCallees: Node[]) {
if (node.attributes.length) {
validator.error(node.attributes[0], {
code: `invalid-attribute`,
message: `<svelte:head> should not have any attributes or directives`
});
}
// TODO ensure only valid elements are included here
node.children.forEach(node => {
if (node.type !== 'Element' && node.type !== 'Title') return; // TODO handle {#if} and friends?
validateElement(validator, node, refs, refCallees, [], []);
});
}

@ -1,56 +0,0 @@
import * as namespaces from '../../utils/namespaces';
import validateEventHandler from './validateEventHandler';
import validate, { Validator } from '../index';
import { Node } from '../../interfaces';
export default function validateSlot(
validator: Validator,
node: Node
) {
node.attributes.forEach(attr => {
if (attr.type !== 'Attribute') {
validator.error(attr, {
code: `invalid-slot-directive`,
message: `<slot> cannot have directives`
});
}
if (attr.name !== 'name') {
validator.error(attr, {
code: `invalid-slot-attribute`,
message: `"name" is the only attribute permitted on <slot> elements`
});
}
if (attr.value.length !== 1 || attr.value[0].type !== 'Text') {
validator.error(attr, {
code: `dynamic-slot-name`,
message: `<slot> name cannot be dynamic`
});
}
const slotName = attr.value[0].data;
if (slotName === 'default') {
validator.error(attr, {
code: `invalid-slot-name`,
message: `default is a reserved word — it cannot be used as a slot name`
});
}
// TODO should duplicate slots be disallowed? Feels like it's more likely to be a
// bug than anything. Perhaps it should be a warning
// if (validator.slots.has(slotName)) {
// validator.error(`duplicate '${slotName}' <slot> element`, nameAttribute.start);
// }
// validator.slots.add(slotName);
});
// if (node.attributes.length === 0) && validator.slots.has('default')) {
// validator.error(node, {
// code: `duplicate-slot`,
// message: `duplicate default <slot> element`
// });
// }
}

@ -1,56 +0,0 @@
import flattenReference from '../../utils/flattenReference';
import fuzzymatch from '../utils/fuzzymatch';
import list from '../../utils/list';
import validateEventHandler from './validateEventHandler';
import { Validator } from '../index';
import { Node } from '../../interfaces';
const validBindings = [
'innerWidth',
'innerHeight',
'outerWidth',
'outerHeight',
'scrollX',
'scrollY',
'online'
];
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') {
const { parts } = flattenReference(attribute.value);
validator.error(attribute.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(attribute.name)) {
const match = attribute.name === 'width'
? 'innerWidth'
: attribute.name === 'height'
? 'innerHeight'
: fuzzymatch(attribute.name, validBindings);
const message = `'${attribute.name}' is not a valid binding on <svelte:window>`;
if (match) {
validator.error(attribute, {
code: `invalid-binding`,
message: `${message} (did you mean '${match}'?)`
});
} else {
validator.error(attribute, {
code: `invalid-binding`,
message: `${message} — valid bindings are ${list(validBindings)}`
});
}
}
} else if (attribute.type === 'EventHandler') {
validator.used.events.add(attribute.name);
validateEventHandler(validator, attribute, refCallees);
}
});
}

@ -1,175 +0,0 @@
import validateJs from './js/index';
import validateHtml from './html/index';
import { getLocator, Location } from 'locate-character';
import getCodeFrame from '../utils/getCodeFrame';
import Stats from '../Stats';
import error from '../utils/error';
import Stylesheet from '../css/Stylesheet';
import { Node, Ast, CompileOptions, Warning } from '../interfaces';
export class Validator {
readonly source: string;
readonly filename: string;
readonly stats: Stats;
options: CompileOptions;
locator?: (pos: number) => Location;
namespace: string;
defaultExport: Node;
properties: Map<string, Node>;
components: Map<string, Node>;
methods: Map<string, Node>;
helpers: Map<string, Node>;
animations: Map<string, Node>;
transitions: Map<string, Node>;
actions: Map<string, Node>;
slots: Set<string>;
used: {
components: Set<string>;
helpers: Set<string>;
events: Set<string>;
animations: Set<string>;
transitions: Set<string>;
actions: Set<string>;
};
constructor(ast: Ast, source: string, stats: Stats, options: CompileOptions) {
this.source = source;
this.stats = stats;
this.filename = options.filename;
this.options = options;
this.namespace = null;
this.defaultExport = null;
this.properties = new Map();
this.components = new Map();
this.methods = new Map();
this.helpers = new Map();
this.animations = new Map();
this.transitions = new Map();
this.actions = 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(),
};
}
error(pos: { start: number, end: number }, { code, message } : { code: string, message: string }) {
error(message, {
name: 'ValidationError',
code,
source: this.source,
start: pos.start,
end: pos.end,
filename: this.filename
});
}
warn(pos: { start: number, end: number }, { code, message }: { code: string, message: string }) {
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 - 1, start.column);
this.stats.warn({
code,
message,
frame,
start,
end,
pos: pos.start,
filename: this.filename,
toString: () => `${message} (${start.line + 1}:${start.column})\n${frame}`,
});
}
}
export default function validate(
ast: Ast,
source: string,
stylesheet: Stylesheet,
stats: Stats,
options: CompileOptions
) {
const { onerror, name, filename, dev, parser } = options;
try {
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,
});
}
const validator = new Validator(ast, source, stats, {
name,
filename,
dev,
parser
});
if (ast.js) {
validateJs(validator, ast.js);
}
if (ast.css) {
stylesheet.validate(validator);
}
if (ast.html) {
validateHtml(validator, ast.html);
}
// need to do a second pass of the JS, now that we've analysed the markup
if (ast.js && validator.defaultExport) {
const categories = {
components: 'component',
// TODO helpers require a bit more work — need to analyse all expressions
// helpers: 'helper',
events: 'event definition',
transitions: 'transition',
actions: 'actions',
};
Object.keys(categories).forEach(category => {
const definitions = validator.defaultExport.declaration.properties.find(prop => prop.key.name === category);
if (definitions) {
definitions.value.properties.forEach(prop => {
const { name } = prop.key;
if (!validator.used[category].has(name)) {
validator.warn(prop, {
code: `unused-${category.slice(0, -1)}`,
message: `The '${name}' ${categories[category]} is unused`
});
}
});
}
});
}
} catch (err) {
if (onerror) {
onerror(err);
} else {
throw err;
}
}
}

@ -1,99 +0,0 @@
import propValidators from './propValidators/index';
import fuzzymatch from '../utils/fuzzymatch';
import checkForDupes from './utils/checkForDupes';
import checkForComputedKeys from './utils/checkForComputedKeys';
import namespaces from '../../utils/namespaces';
import nodeToString from '../../utils/nodeToString';
import getName from '../../utils/getName';
import { Validator } from '../';
import { Node } from '../../interfaces';
const validPropList = Object.keys(propValidators);
export default function validateJs(validator: Validator, js: Node) {
js.content.body.forEach((node: Node) => {
// check there are no named exports
if (node.type === 'ExportNamedDeclaration') {
validator.error(node, {
code: `named-export`,
message: `A component can only have a default export`
});
}
if (node.type === 'ExportDefaultDeclaration') {
if (node.declaration.type !== 'ObjectExpression') {
validator.error(node.declaration, {
code: `invalid-default-export`,
message: `Default export must be an object literal`
});
}
checkForComputedKeys(validator, node.declaration.properties);
checkForDupes(validator, node.declaration.properties);
const props = validator.properties;
node.declaration.properties.forEach((prop: Node) => {
props.set(getName(prop.key), prop);
});
// Remove these checks in version 2
if (props.has('oncreate') && props.has('onrender')) {
validator.error(props.get('onrender'), {
code: `duplicate-oncreate`,
message: 'Cannot have both oncreate and onrender'
});
}
if (props.has('ondestroy') && props.has('onteardown')) {
validator.error(props.get('onteardown'), {
code: `duplicate-ondestroy`,
message: 'Cannot have both ondestroy and onteardown'
});
}
// ensure all exported props are valid
node.declaration.properties.forEach((prop: Node) => {
const name = getName(prop.key);
const propValidator = propValidators[name];
if (propValidator) {
propValidator(validator, prop);
} else {
const match = fuzzymatch(name, validPropList);
if (match) {
validator.error(prop, {
code: `unexpected-property`,
message: `Unexpected property '${name}' (did you mean '${match}'?)`
});
} else if (/FunctionExpression/.test(prop.value.type)) {
validator.error(prop, {
code: `unexpected-property`,
message: `Unexpected property '${name}' (did you mean to include it in 'methods'?)`
});
} else {
validator.error(prop, {
code: `unexpected-property`,
message: `Unexpected property '${name}'`
});
}
}
});
if (props.has('namespace')) {
const ns = nodeToString(props.get('namespace').value);
validator.namespace = namespaces[ns] || ns;
}
validator.defaultExport = node;
}
});
['components', 'methods', 'helpers', 'transitions', 'animations', 'actions'].forEach(key => {
if (validator.properties.has(key)) {
validator.properties.get(key).value.properties.forEach((prop: Node) => {
validator[key].set(getName(prop.key), prop.value);
});
}
});
}

@ -1,16 +0,0 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
export default function actions(validator: Validator, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
validator.error(prop, {
code: `invalid-actions`,
message: `The 'actions' property must be an object literal`
});
}
checkForDupes(validator, prop.value.properties);
checkForComputedKeys(validator, prop.value.properties);
}

@ -1,16 +0,0 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
export default function events(validator: Validator, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
validator.error(prop, {
code: `invalid-events-property`,
message: `The 'events' property must be an object literal`
});
}
checkForDupes(validator, prop.value.properties);
checkForComputedKeys(validator, prop.value.properties);
}

@ -1,12 +0,0 @@
import oncreate from './oncreate';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
export default function onrender(validator: Validator, prop: Node) {
validator.warn(prop, {
code: `deprecated-onrender`,
message: `'onrender' has been deprecated in favour of 'oncreate', and will cause an error in Svelte 2.x`
});
oncreate(validator, prop);
}

@ -1,12 +0,0 @@
import ondestroy from './ondestroy';
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
export default function onteardown(validator: Validator, prop: Node) {
validator.warn(prop, {
code: `deprecated-onteardown`,
message: `'onteardown' has been deprecated in favour of 'ondestroy', and will cause an error in Svelte 2.x`
});
ondestroy(validator, prop);
}

@ -1,6 +0,0 @@
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
export default function preload(validator: Validator, prop: Node) {
// not sure there's anything we need to check here...
}

@ -1,6 +0,0 @@
import { Validator } from '../../index';
import { Node } from '../../../interfaces';
export default function store(validator: Validator, prop: Node) {
// not sure there's anything we need to check here...
}

@ -8,7 +8,7 @@
{
"start": 0,
"end": 18,
"type": "Component",
"type": "InlineComponent",
"name": "Widget",
"attributes": [
{

@ -8,7 +8,7 @@
{
"start": 0,
"end": 62,
"type": "Component",
"type": "InlineComponent",
"name": "svelte:component",
"attributes": [],
"children": [],

@ -32,7 +32,7 @@
{
"start": 17,
"end": 51,
"type": "Component",
"type": "InlineComponent",
"name": "svelte:self",
"attributes": [
{

@ -1,5 +1,5 @@
import assert from 'assert';
import {svelte} from '../helpers.js';
import { svelte } from '../helpers.js';
describe('preprocess', () => {
it('preprocesses entire component', () => {

@ -1,2 +0,0 @@
.foo.svelte-sg04hs{color:red}
/*# sourceMappingURL=output.css.map */

@ -1,12 +0,0 @@
{
"version": 3,
"file": "output.css",
"sources": [
"input.html"
],
"sourcesContent": [
"<p class='foo'>red</p>\n\n<style>\n\t.foo {\n\t\tcolor: red;\n\t}\n</style>"
],
"names": [],
"mappings": "AAGC,IAAI,cAAC,CAAC,AACL,KAAK,CAAE,GAAG,AACX,CAAC"
}
Loading…
Cancel
Save