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

@ -15,5 +15,12 @@ export interface Parser {
html: Node; html: Node;
css: Node; css: Node;
js: 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 { trimStart, trimEnd } from '../utils/trim';
import getCodeFrame from '../utils/getCodeFrame'; import getCodeFrame from '../utils/getCodeFrame';
import hash from './utils/hash'; import hash from './utils/hash';
import { Node } from '../interfaces'; import { Node, Parsed } from '../interfaces';
import CompileError from '../utils/CompileError' import CompileError from '../utils/CompileError'
class ParseError extends 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 ); const parser = new Parser( template, options );
return { return {

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

@ -1,3 +1,5 @@
import { Node } from '../interfaces';
const keys = { const keys = {
ObjectExpression: 'properties', ObjectExpression: 'properties',
Program: 'body' Program: 'body'
@ -8,7 +10,7 @@ const offsets = {
Program: [ 0, 0 ] Program: [ 0, 0 ]
}; };
export function removeNode ( code, parent, node ) { export function removeNode ( code, parent: Node, node: Node ) {
const key = keys[ parent.type ]; const key = keys[ parent.type ];
const offset = offsets[ parent.type ]; const offset = offsets[ parent.type ];
if ( !key || !offset ) throw new Error( `not implemented: ${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 * as namespaces from '../../utils/namespaces';
import validateElement from './validateElement'; import validateElement from './validateElement';
import validateWindow from './validateWindow'; 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)$/; 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 ] [ ':Window', validateWindow ]
]); ]);
export default function validateHtml ( validator, html ) { export default function validateHtml ( validator: Validator, html: Node ) {
let elementDepth = 0; let elementDepth = 0;
function visit ( node ) { function visit ( node: Node ) {
if ( node.type === 'Element' ) { if ( node.type === 'Element' ) {
if ( elementDepth === 0 && validator.namespace !== namespaces.svg && svg.test( node.name ) ) { 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 ); 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 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 ); const isComponent = node.name === ':Self' || validator.components.has( node.name );
let hasIntro; let hasIntro: boolean;
let hasOutro; let hasOutro: boolean;
let hasTransition; let hasTransition: boolean;
node.attributes.forEach( attribute => { node.attributes.forEach( ( attribute: Node ) => {
if ( !isComponent && attribute.type === 'Binding' ) { if ( !isComponent && attribute.type === 'Binding' ) {
const { name } = attribute; const { name } = attribute;
@ -35,13 +37,13 @@ export default function validateElement ( validator, node ) {
const type = getType( validator, node ); const type = getType( validator, node );
if ( type !== 'checkbox' && type !== 'radio' ) { 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' ) { else if ( name === 'currentTime' || name === 'duration' || name === 'paused' ) {
if ( node.name !== 'audio' && node.name !== 'video' ) { 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 ) { function getType ( validator: Validator, node: Node ) {
const attribute = node.attributes.find( attribute => attribute.name === 'type' ); const attribute = node.attributes.find( ( attribute: Node ) => attribute.name === 'type' );
if ( !attribute ) return null; if ( !attribute ) return null;
if ( attribute.value === true ) { if ( attribute.value === true ) {

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

@ -2,6 +2,8 @@ import flattenReference from '../../utils/flattenReference';
import fuzzymatch from '../utils/fuzzymatch'; import fuzzymatch from '../utils/fuzzymatch';
import list from '../utils/list'; import list from '../utils/list';
import validateEventHandler from './validateEventHandler'; import validateEventHandler from './validateEventHandler';
import { Validator } from '../index';
import { Node } from '../../interfaces';
const validBindings = [ const validBindings = [
'innerWidth', 'innerWidth',
@ -12,8 +14,8 @@ const validBindings = [
'scrollY' 'scrollY'
]; ];
export default function validateWindow ( validator, node ) { export default function validateWindow ( validator: Validator, node: Node ) {
node.attributes.forEach( attribute => { node.attributes.forEach( ( attribute: Node ) => {
if ( attribute.type === 'Binding' ) { if ( attribute.type === 'Binding' ) {
if ( attribute.value.type !== 'Identifier' ) { if ( attribute.value.type !== 'Identifier' ) {
const { parts } = flattenReference( attribute.value ); const { parts } = flattenReference( attribute.value );

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

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

Loading…
Cancel
Save