pull/573/head
Rich-Harris 8 years ago
parent 48384b846c
commit 4d5107113e

@ -4,6 +4,7 @@ import generate from './generators/dom/index';
import generateSSR from './generators/server-side-rendering/index';
import { assign } from './shared/index.js';
import { version } from '../package.json';
import { Parsed } from './interface';
function normalizeOptions ( options ) {
return assign( {
@ -29,7 +30,7 @@ function normalizeOptions ( options ) {
export function compile ( source, _options ) {
const options = normalizeOptions( _options );
let parsed;
let parsed: Parsed;
try {
parsed = parse( source, options );

@ -15,5 +15,12 @@ export interface Parser {
html: Node;
css: Node;
js: Node;
metaTags: {}
metaTags: {};
}
export interface Parsed {
hash: number;
html: Node;
css: Node;
js: Node;
}

@ -4,7 +4,7 @@ import { whitespace } from '../utils/patterns';
import { trimStart, trimEnd } from '../utils/trim';
import getCodeFrame from '../utils/getCodeFrame';
import hash from './utils/hash';
import { Node } from '../interfaces';
import { Node, Parsed } from '../interfaces';
import CompileError from '../utils/CompileError'
class ParseError extends CompileError {
@ -178,7 +178,7 @@ export class Parser {
}
}
export default function parse ( template: string, options: ParserOptions = {} ) {
export default function parse ( template: string, options: ParserOptions = {} ) :Parsed {
const parser = new Parser( template, options );
return {

@ -1,10 +1,11 @@
import { walk } from 'estree-walker';
import { Node } from '../interfaces';
export default function annotateWithScopes ( expression ) {
export default function annotateWithScopes ( expression: Node ) {
let scope = new Scope( null, false );
walk( expression, {
enter ( node ) {
enter ( node: Node ) {
if ( /Function/.test( node.type ) ) {
if ( node.type === 'FunctionDeclaration' ) {
scope.declarations.add( node.id.name );
@ -13,7 +14,7 @@ export default function annotateWithScopes ( expression ) {
if ( node.id ) scope.declarations.add( node.id.name );
}
node.params.forEach( param => {
node.params.forEach( ( param: Node ) => {
extractNames( param ).forEach( name => {
scope.declarations.add( name );
});
@ -33,7 +34,7 @@ export default function annotateWithScopes ( expression ) {
}
},
leave ( node ) {
leave ( node: Node ) {
if ( node._scope ) {
scope = scope.parent;
}
@ -44,17 +45,21 @@ export default function annotateWithScopes ( expression ) {
}
class Scope {
constructor ( parent, block ) {
parent: Scope
block: boolean
declarations: Set<string>
constructor ( parent: Scope, block: boolean ) {
this.parent = parent;
this.block = block;
this.declarations = new Set();
}
addDeclaration ( node ) {
addDeclaration ( node: Node ) {
if ( node.kind === 'var' && !this.block && this.parent ) {
this.parent.addDeclaration( node );
} else if ( node.type === 'VariableDeclaration' ) {
node.declarations.forEach( declarator => {
node.declarations.forEach( ( declarator: Node ) => {
extractNames( declarator.id ).forEach( name => {
this.declarations.add( name );
});
@ -64,39 +69,39 @@ class Scope {
}
}
has ( name ) {
has ( name: string ) :boolean {
return this.declarations.has( name ) || this.parent && this.parent.has( name );
}
}
function extractNames ( param ) {
const names = [];
function extractNames ( param: Node ) {
const names: string[] = [];
extractors[ param.type ]( names, param );
return names;
}
const extractors = {
Identifier ( names, param ) {
Identifier ( names: string[], param: Node ) {
names.push( param.name );
},
ObjectPattern ( names, param ) {
param.properties.forEach( prop => {
ObjectPattern ( names: string[], param: Node ) {
param.properties.forEach( ( prop: Node ) => {
extractors[ prop.value.type ]( names, prop.value );
});
},
ArrayPattern ( names, param ) {
param.elements.forEach( element => {
ArrayPattern ( names: string[], param: Node ) {
param.elements.forEach( ( element: Node ) => {
if ( element ) extractors[ element.type ]( names, element );
});
},
RestElement ( names, param ) {
RestElement ( names: string[], param: Node ) {
extractors[ param.argument.type ]( names, param.argument );
},
AssignmentPattern ( names, param ) {
AssignmentPattern ( names: string[], param: Node ) {
extractors[ param.left.type ]( names, param.left );
}
};

@ -1,4 +1,6 @@
export default function flatten ( node ) {
import { Node } from '../interfaces';
export default function flatten ( node: Node ) {
const parts = [];
const propEnd = node.end;

@ -1,3 +1,5 @@
import { Node } from '../interfaces';
const keys = {
ObjectExpression: 'properties',
Program: 'body'
@ -8,7 +10,7 @@ const offsets = {
Program: [ 0, 0 ]
};
export function removeNode ( code, parent, node ) {
export function removeNode ( code, parent: Node, node: Node ) {
const key = keys[ parent.type ];
const offset = offsets[ parent.type ];
if ( !key || !offset ) throw new Error( `not implemented: ${parent.type}` );

@ -1,44 +0,0 @@
import deindent from './deindent.js';
export default function toSource ( thing: any ) :string {
if ( typeof thing === 'function' ) {
return normaliseIndentation( thing.toString() );
}
if ( Array.isArray( thing ) ) {
if ( thing.length === 0 ) return '[]';
throw new Error( 'TODO' ); // not currently needed
}
if ( thing && typeof thing === 'object' ) {
const keys = Object.keys( thing );
if ( keys.length === 0 ) return '{}';
const props = keys.map( key => `${key}: ${toSource( thing[ key ] )}` ).join( ',\n' );
return deindent`
{
${props}
}
`;
}
return JSON.stringify( thing );
}
function normaliseIndentation ( str: string ) {
const lines = str.split( '\n' ).slice( 1, -1 );
let minIndentation = Infinity;
lines.forEach( line => {
if ( !/\S/.test( line ) ) return;
const indentation = /^\t*/.exec( line )[0].length;
if ( indentation < minIndentation ) minIndentation = indentation;
});
if ( minIndentation !== Infinity && minIndentation !== 1 ) {
const pattern = new RegExp( `^\\t{${minIndentation - 1}}`, 'gm' );
return str.replace( pattern, '' );
}
return str;
}

@ -1,6 +1,8 @@
import * as namespaces from '../../utils/namespaces';
import validateElement from './validateElement';
import validateWindow from './validateWindow';
import { Validator } from '../index';
import { Node } from '../../interfaces';
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|switch|symbol|text|textPath|title|tref|tspan|unknown|use|view|vkern)$/;
@ -8,10 +10,10 @@ const meta = new Map([
[ ':Window', validateWindow ]
]);
export default function validateHtml ( validator, html ) {
export default function validateHtml ( validator: Validator, html: Node ) {
let elementDepth = 0;
function visit ( node ) {
function visit ( node: Node ) {
if ( node.type === 'Element' ) {
if ( elementDepth === 0 && validator.namespace !== namespaces.svg && svg.test( node.name ) ) {
validator.warn( `<${node.name}> is an SVG element did you forget to add { namespace: 'svg' } ?`, node.start );

@ -1,13 +1,15 @@
import validateEventHandler from './validateEventHandler';
import { Validator } from '../index';
import { Node } from '../../interfaces';
export default function validateElement ( validator, node ) {
export default function validateElement ( validator: Validator, node: Node ) {
const isComponent = node.name === ':Self' || validator.components.has( node.name );
let hasIntro;
let hasOutro;
let hasTransition;
let hasIntro: boolean;
let hasOutro: boolean;
let hasTransition: boolean;
node.attributes.forEach( attribute => {
node.attributes.forEach( ( attribute: Node ) => {
if ( !isComponent && attribute.type === 'Binding' ) {
const { name } = attribute;
@ -35,13 +37,13 @@ export default function validateElement ( validator, node ) {
const type = getType( validator, node );
if ( type !== 'checkbox' && type !== 'radio' ) {
validator.error( `'checked' binding can only be used with <input type="checkbox"> or <input type="radio">` );
validator.error( `'checked' binding can only be used with <input type="checkbox"> or <input type="radio">`, attribute.start );
}
}
else if ( name === 'currentTime' || name === 'duration' || name === 'paused' ) {
if ( node.name !== 'audio' && node.name !== 'video' ) {
validator.error( `'${name}' binding can only be used with <audio> or <video>` );
validator.error( `'${name}' binding can only be used with <audio> or <video>`, attribute.start );
}
}
@ -78,8 +80,8 @@ export default function validateElement ( validator, node ) {
});
}
function getType ( validator, node ) {
const attribute = node.attributes.find( attribute => attribute.name === 'type' );
function getType ( validator: Validator, node: Node ) {
const attribute = node.attributes.find( ( attribute: Node ) => attribute.name === 'type' );
if ( !attribute ) return null;
if ( attribute.value === true ) {

@ -1,5 +1,7 @@
import flattenReference from '../../utils/flattenReference';
import list from '../utils/list';
import { Validator } from '../index';
import { Node } from '../../interfaces';
const validBuiltins = new Set([
'set',
@ -7,7 +9,7 @@ const validBuiltins = new Set([
'destroy'
]);
export default function validateEventHandlerCallee ( validator, attribute ) {
export default function validateEventHandlerCallee ( validator: Validator, attribute: Node ) {
const { callee, start, type } = attribute.expression;
if ( type !== 'CallExpression' ) {

@ -2,6 +2,8 @@ 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',
@ -12,8 +14,8 @@ const validBindings = [
'scrollY'
];
export default function validateWindow ( validator, node ) {
node.attributes.forEach( attribute => {
export default function validateWindow ( validator: Validator, node: Node ) {
node.attributes.forEach( ( attribute: Node ) => {
if ( attribute.type === 'Binding' ) {
if ( attribute.value.type !== 'Identifier' ) {
const { parts } = flattenReference( attribute.value );

@ -3,7 +3,7 @@ import validateHtml from './html/index';
import { getLocator, Location } from 'locate-character';
import getCodeFrame from '../utils/getCodeFrame';
import CompileError from '../utils/CompileError'
import { Node } from '../interfaces';
import { Node, Parsed } from '../interfaces';
class ValidationError extends CompileError {
constructor ( message: string, template: string, index: number, filename: string ) {
@ -21,12 +21,13 @@ export class Validator {
namespace: string;
defaultExport: Node;
properties: Map<string, Node>;
components: Map<string, Node>;
methods: Map<string, Node>;
helpers: Map<string, Node>;
transitions: Map<string, Node>;
constructor ( parsed, source: string, options: { onwarn, name?: string, filename?: string } ) {
constructor ( parsed: Parsed, source: string, options: { onwarn, name?: string, filename?: string } ) {
this.source = source;
this.filename = options !== undefined ? options.filename : undefined;
@ -34,7 +35,8 @@ export class Validator {
this.namespace = null;
this.defaultExport = null;
this.properties = {};
this.properties = new Map();
this.components = new Map();
this.methods = new Map();
this.helpers = new Map();
@ -62,7 +64,7 @@ export class Validator {
}
}
export default function validate ( parsed, source, { onerror, onwarn, name, filename } ) {
export default function validate ( parsed: Parsed, source: string, { onerror, onwarn, name, filename } ) {
try {
if ( name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test( name ) ) {
const error = new Error( `options.name must be a valid identifier` );

@ -8,7 +8,7 @@ import { Node } from '../../interfaces';
const validPropList = Object.keys( propValidators );
export default function validateJs ( validator: Validator, js ) {
export default function validateJs ( validator: Validator, js: Node ) {
js.content.body.forEach( ( node: Node ) => {
// check there are no named exports
if ( node.type === 'ExportNamedDeclaration' ) {
@ -26,16 +26,16 @@ export default function validateJs ( validator: Validator, js ) {
const props = validator.properties;
node.declaration.properties.forEach( ( prop: Node ) => {
props[ prop.key.name ] = prop;
props.set( prop.key.name, prop );
});
// Remove these checks in version 2
if ( props.oncreate && props.onrender ) {
validator.error( 'Cannot have both oncreate and onrender', props.onrender.start );
if ( props.has( 'oncreate' ) && props.has( 'onrender' ) ) {
validator.error( 'Cannot have both oncreate and onrender', props.get( 'onrender' ).start );
}
if ( props.ondestroy && props.onteardown ) {
validator.error( 'Cannot have both ondestroy and onteardown', props.onteardown.start );
if ( props.has( 'ondestroy' ) && props.has( 'onteardown' ) ) {
validator.error( 'Cannot have both ondestroy and onteardown', props.get( 'onteardown' ).start );
}
// ensure all exported props are valid
@ -56,8 +56,8 @@ export default function validateJs ( validator: Validator, js ) {
}
});
if ( props.namespace ) {
const ns = props.namespace.value.value;
if ( props.has( 'namespace' ) ) {
const ns = props.get( 'namespace' ).value.value;
validator.namespace = namespaces[ ns ] || ns;
}
@ -66,8 +66,8 @@ export default function validateJs ( validator: Validator, js ) {
});
[ 'components', 'methods', 'helpers', 'transitions' ].forEach( key => {
if ( validator.properties[ key ] ) {
validator.properties[ key ].value.properties.forEach( prop => {
if ( validator.properties.has( key ) ) {
validator.properties.get( key ).value.properties.forEach( ( prop: Node ) => {
validator[ key ].set( prop.key.name, prop.value );
});
}

Loading…
Cancel
Save