mirror of https://github.com/sveltejs/svelte
[WIP] Refactor, change where validation occurs (#1721)
Refactor, change where validation occurspull/1744/head
parent
b7e07c5389
commit
9031c16905
@ -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}✂]` : '',
|
||||
''
|
||||
];
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,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,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`
|
||||
});
|
@ -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,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,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;
|
@ -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,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,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...
|
||||
}
|
@ -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…
Reference in new issue