Structured code generation ()

pull/3720/head
Rich Harris 6 years ago committed by GitHub
parent 1b0e391fb0
commit b9f14846b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -22,14 +22,7 @@ module.exports = {
'arrow-spacing': 2,
'no-inner-declarations': 0,
'require-atomic-updates': 'off',
'@typescript-eslint/indent': [
'error',
'tab',
{
SwitchCase: 1,
ignoredNodes: ['TemplateLiteral']
}
],
'@typescript-eslint/indent': 'off',
'@typescript-eslint/camelcase': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/array-type': ['error', 'array-simple'],

92
package-lock.json generated

@ -489,6 +489,18 @@
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true
},
"code-red": {
"version": "0.0.17",
"resolved": "https://registry.npmjs.org/code-red/-/code-red-0.0.17.tgz",
"integrity": "sha512-RJJ48sXYOqyd0J4QelF4dRdYb+4DaLV/jHs4mNoxOdLroUGB840cMc9pMtEAbGKjFFzoTKREypFzqphBD8knMg==",
"dev": true,
"requires": {
"acorn": "^7.0.0",
"is-reference": "^1.1.3",
"periscopic": "^1.0.1",
"sourcemap-codec": "^1.4.6"
}
},
"codecov": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/codecov/-/codecov-3.5.0.tgz",
@ -792,6 +804,13 @@
"resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
"integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"optional": true
}
}
},
@ -1169,9 +1188,9 @@
"dev": true
},
"estree-walker": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
"integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.8.1.tgz",
"integrity": "sha512-H6cJORkqvrNziu0KX2hqOMAlA2CiuAxHeGJXSIoKA/KLv229Dw806J3II6mKTm5xiDX1At1EXCfsOQPB+tMB+g==",
"dev": true
},
"esutils": {
@ -1484,6 +1503,14 @@
"optimist": "^0.6.1",
"source-map": "^0.6.1",
"uglify-js": "^3.1.4"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"har-schema": {
@ -2693,6 +2720,26 @@
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
"dev": true
},
"periscopic": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-1.0.2.tgz",
"integrity": "sha512-KpKBKadLf8THXOxswQBhOY8E1lVVhfUidacPtQBrq7KDXaNkQLUPiTmXagzqpJGECP3/0gDXYFO6CZHVbGvOSw==",
"dev": true,
"requires": {
"is-reference": "^1.1.4"
},
"dependencies": {
"is-reference": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.1.4.tgz",
"integrity": "sha512-uJA/CDPO3Tao3GTrxYn6AwkM4nUPJiGGYu5+cB8qbC7WGFlrKZbiRo7SFKxUAEpFUfiHofWCXBUNhvYJMh+6zw==",
"dev": true,
"requires": {
"@types/estree": "0.0.39"
}
}
}
},
"pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
@ -3049,6 +3096,14 @@
"magic-string": "^0.25.2",
"resolve": "^1.11.0",
"rollup-pluginutils": "^2.8.1"
},
"dependencies": {
"estree-walker": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
"integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
"dev": true
}
}
},
"rollup-plugin-json": {
@ -3116,6 +3171,14 @@
"dev": true,
"requires": {
"estree-walker": "^0.6.1"
},
"dependencies": {
"estree-walker": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
"integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
"dev": true
}
}
},
"run-async": {
@ -3202,9 +3265,9 @@
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true
},
"source-map-support": {
@ -3215,6 +3278,14 @@
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"sourcemap-codec": {
@ -3522,6 +3593,15 @@
"requires": {
"commander": "~2.20.0",
"source-map": "~0.6.1"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"optional": true
}
}
},
"uri-js": {

@ -63,12 +63,13 @@
"acorn": "^7.0.0",
"agadoo": "^1.1.0",
"c8": "^5.0.1",
"code-red": "0.0.17",
"codecov": "^3.5.0",
"css-tree": "1.0.0-alpha22",
"eslint": "^6.3.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-svelte3": "^2.7.3",
"estree-walker": "^0.6.1",
"estree-walker": "^0.8.1",
"is-reference": "^1.1.3",
"jsdom": "^15.1.1",
"kleur": "^3.0.3",
@ -84,7 +85,7 @@
"rollup-plugin-sucrase": "^2.1.0",
"rollup-plugin-typescript": "^1.0.1",
"rollup-plugin-virtual": "^1.0.1",
"source-map": "^0.6.1",
"source-map": "^0.7.3",
"source-map-support": "^0.5.13",
"tiny-glob": "^0.2.6",
"tslib": "^1.10.0",

@ -20,8 +20,7 @@ const ts_plugin = is_publish
const external = id => id.startsWith('svelte/');
const inlined_estree = fs.readFileSync('./node_modules/estree-walker/index.d.ts', 'utf-8').replace(/declare.*\{((.|[\n\r])+)\}/m, '$1');
fs.writeFileSync(`./compiler.d.ts`, `export { compile, parse, preprocess, VERSION } from './types/compiler/index';\n${inlined_estree}`);
fs.writeFileSync(`./compiler.d.ts`, `export { compile, parse, preprocess, VERSION } from './types/compiler/index';`);
export default [
/* runtime */

File diff suppressed because it is too large Load Diff

@ -1,7 +1,7 @@
import deindent from './utils/deindent';
import list from '../utils/list';
import { ModuleFormat, Node } from '../interfaces';
import { stringify_props } from './utils/stringify_props';
import { ModuleFormat } from '../interfaces';
import { b, x } from 'code-red';
import { Identifier, ImportDeclaration } from 'estree';
const wrappers = { esm, cjs };
@ -11,24 +11,26 @@ interface Export {
}
export default function create_module(
code: string,
program: any,
format: ModuleFormat,
name: string,
name: Identifier,
banner: string,
sveltePath = 'svelte',
helpers: Array<{ name: string; alias: string }>,
globals: Array<{ name: string; alias: string }>,
imports: Node[],
module_exports: Export[],
source: string
): string {
helpers: Array<{ name: string; alias: Identifier }>,
globals: Array<{ name: string; alias: Identifier }>,
imports: ImportDeclaration[],
module_exports: Export[]
) {
const internal_path = `${sveltePath}/internal`;
helpers.sort((a, b) => (a.name < b.name) ? -1 : 1);
globals.sort((a, b) => (a.name < b.name) ? -1 : 1);
if (format === 'esm') {
return esm(code, name, banner, sveltePath, internal_path, helpers, globals, imports, module_exports, source);
return esm(program, name, banner, sveltePath, internal_path, helpers, globals, imports, module_exports);
}
if (format === 'cjs') return cjs(code, name, banner, sveltePath, internal_path, helpers, globals, imports, module_exports);
if (format === 'cjs') return cjs(program, name, banner, sveltePath, internal_path, helpers, globals, imports, module_exports);
throw new Error(`options.format is invalid (must be ${list(Object.keys(wrappers))})`);
}
@ -40,107 +42,160 @@ function edit_source(source, sveltePath) {
}
function esm(
code: string,
name: string,
banner: string,
program: any,
name: Identifier,
_banner: string,
sveltePath: string,
internal_path: string,
helpers: Array<{ name: string; alias: string }>,
globals: Array<{ name: string; alias: string }>,
imports: Node[],
module_exports: Export[],
source: string
helpers: Array<{ name: string; alias: Identifier }>,
globals: Array<{ name: string; alias: Identifier }>,
imports: ImportDeclaration[],
module_exports: Export[]
) {
const internal_imports = helpers.length > 0 && (
`import ${stringify_props(helpers.map(h => h.name === h.alias ? h.name : `${h.name} as ${h.alias}`).sort())} from ${JSON.stringify(internal_path)};`
);
const internal_globals = globals.length > 0 && (
`const ${stringify_props(globals.map(g => `${g.name}: ${g.alias}`).sort())} = ${helpers.find(({ name }) => name === 'globals').alias};`
);
const user_imports = imports.length > 0 && (
imports
.map((declaration: Node) => {
const import_source = edit_source(declaration.source.value, sveltePath);
return (
source.slice(declaration.start, declaration.source.start) +
JSON.stringify(import_source) +
source.slice(declaration.source.end, declaration.end)
);
})
.join('\n')
);
return deindent`
${banner}
${internal_imports}
const import_declaration = {
type: 'ImportDeclaration',
specifiers: helpers.map(h => ({
type: 'ImportSpecifier',
local: h.alias,
imported: { type: 'Identifier', name: h.name }
})),
source: { type: 'Literal', value: internal_path }
};
const internal_globals = globals.length > 0 && {
type: 'VariableDeclaration',
kind: 'const',
declarations: [{
type: 'VariableDeclarator',
id: {
type: 'ObjectPattern',
properties: globals.map(g => ({
type: 'Property',
method: false,
shorthand: false,
computed: false,
key: { type: 'Identifier', name: g.name },
value: g.alias,
kind: 'init'
}))
},
init: helpers.find(({ name }) => name === 'globals').alias
}]
};
// edit user imports
imports.forEach(node => {
node.source.value = edit_source(node.source.value, sveltePath);
});
const exports = module_exports.length > 0 && {
type: 'ExportNamedDeclaration',
specifiers: module_exports.map(x => ({
type: 'Specifier',
local: { type: 'Identifier', name: x.name },
exported: { type: 'Identifier', name: x.as }
}))
};
program.body = b`
${import_declaration}
${internal_globals}
${user_imports}
${imports}
${code}
${program.body}
export default ${name};
${module_exports.length > 0 && `export { ${module_exports.map(e => e.name === e.as ? e.name : `${e.name} as ${e.as}`).join(', ')} };`}`;
${exports}
`;
}
function cjs(
code: string,
name: string,
banner: string,
program: any,
name: Identifier,
_banner: string,
sveltePath: string,
internal_path: string,
helpers: Array<{ name: string; alias: string }>,
globals: Array<{ name: string; alias: string }>,
imports: Node[],
helpers: Array<{ name: string; alias: Identifier }>,
globals: Array<{ name: string; alias: Identifier }>,
imports: ImportDeclaration[],
module_exports: Export[]
) {
const declarations = helpers.map(h => `${h.alias === h.name ? h.name : `${h.name}: ${h.alias}`}`).sort();
const internal_imports = helpers.length > 0 && (
`const ${stringify_props(declarations)} = require(${JSON.stringify(internal_path)});\n`
);
const internal_globals = globals.length > 0 && (
`const ${stringify_props(globals.map(g => `${g.name}: ${g.alias}`).sort())} = ${helpers.find(({ name }) => name === 'globals').alias};`
);
const requires = imports.map(node => {
let lhs;
if (node.specifiers[0].type === 'ImportNamespaceSpecifier') {
lhs = node.specifiers[0].local.name;
} else {
const properties = node.specifiers.map(s => {
if (s.type === 'ImportDefaultSpecifier') {
return `default: ${s.local.name}`;
}
return s.local.name === s.imported.name
? s.local.name
: `${s.imported.name}: ${s.local.name}`;
});
lhs = `{ ${properties.join(', ')} }`;
}
const source = edit_source(node.source.value, sveltePath);
return `const ${lhs} = require("${source}");`;
});
const exports = [`exports.default = ${name};`].concat(
module_exports.map(x => `exports.${x.as} = ${x.name};`)
);
return deindent`
${banner}
const internal_requires = {
type: 'VariableDeclaration',
kind: 'const',
declarations: [{
type: 'VariableDeclarator',
id: {
type: 'ObjectPattern',
properties: helpers.map(h => ({
type: 'Property',
method: false,
shorthand: false,
computed: false,
key: { type: 'Identifier', name: h.name },
value: h.alias,
kind: 'init'
}))
},
init: x`require("${internal_path}")`
}]
};
const internal_globals = globals.length > 0 && {
type: 'VariableDeclaration',
kind: 'const',
declarations: [{
type: 'VariableDeclarator',
id: {
type: 'ObjectPattern',
properties: globals.map(g => ({
type: 'Property',
method: false,
shorthand: false,
computed: false,
key: { type: 'Identifier', name: g.name },
value: g.alias,
kind: 'init'
}))
},
init: helpers.find(({ name }) => name === 'globals').alias
}]
};
const user_requires = imports.map(node => ({
type: 'VariableDeclaration',
kind: 'const',
declarations: [{
type: 'VariableDeclarator',
id: node.specifiers[0].type === 'ImportNamespaceSpecifier'
? { type: 'Identifier', name: node.specifiers[0].local.name }
: {
type: 'ObjectPattern',
properties: node.specifiers.map(s => ({
type: 'Property',
method: false,
shorthand: false,
computed: false,
key: s.type === 'ImportSpecifier' ? s.imported : { type: 'Identifier', name: 'default' },
value: s.local,
kind: 'init'
}))
},
init: x`require("${edit_source(node.source.value, sveltePath)}")`
}]
}));
const exports = module_exports.map(x => b`exports.${{ type: 'Identifier', name: x.as }} = ${{ type: 'Identifier', name: x.name }};`);
program.body = b`
"use strict";
${internal_imports}
${internal_requires}
${internal_globals}
${requires}
${user_requires}
${code}
${program.body}
${exports}`;
exports.default = ${name};
${exports}
`;
}

@ -1,17 +1,17 @@
import MagicString from 'magic-string';
import Stylesheet from './Stylesheet';
import { gather_possible_values, UNKNOWN } from './gather_possible_values';
import { Node } from '../../interfaces';
import { CssNode } from './interfaces';
import Component from '../Component';
export default class Selector {
node: Node;
node: CssNode;
stylesheet: Stylesheet;
blocks: Block[];
local_blocks: Block[];
used: boolean;
constructor(node: Node, stylesheet: Stylesheet) {
constructor(node: CssNode, stylesheet: Stylesheet) {
this.node = node;
this.stylesheet = stylesheet;
@ -28,8 +28,8 @@ export default class Selector {
this.used = this.blocks[0].global;
}
apply(node: Node, stack: Node[]) {
const to_encapsulate: Node[] = [];
apply(node: CssNode, stack: CssNode[]) {
const to_encapsulate: CssNode[] = [];
apply_selector(this.stylesheet, this.local_blocks.slice(), node, stack.slice(), to_encapsulate);
@ -126,7 +126,7 @@ export default class Selector {
}
}
function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: Node, stack: Node[], to_encapsulate: any[]): boolean {
function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: CssNode, stack: CssNode[], to_encapsulate: any[]): boolean {
const block = blocks.pop();
if (!block) return false;
@ -222,13 +222,13 @@ function test_attribute(operator, expected_value, case_insensitive, value) {
}
}
function attribute_matches(node: Node, name: string, expected_value: string, operator: string, case_insensitive: boolean) {
function attribute_matches(node: CssNode, name: string, expected_value: string, operator: string, case_insensitive: boolean) {
const spread = node.attributes.find(attr => attr.type === 'Spread');
if (spread) return true;
if (node.bindings.some((binding: Node) => binding.name === name)) return true;
if (node.bindings.some((binding: CssNode) => binding.name === name)) return true;
const attr = node.attributes.find((attr: Node) => attr.name === name);
const attr = node.attributes.find((attr: CssNode) => attr.name === name);
if (!attr) return false;
if (attr.is_true) return operator === null;
if (attr.chunks.length > 1) return true;
@ -250,7 +250,7 @@ function attribute_matches(node: Node, name: string, expected_value: string, ope
return false;
}
function unquote(value: Node) {
function unquote(value: CssNode) {
if (value.type === 'Identifier') return value.name;
const str = value.value;
if (str[0] === str[str.length - 1] && str[0] === "'" || str[0] === '"') {
@ -261,13 +261,13 @@ function unquote(value: Node) {
class Block {
global: boolean;
combinator: Node;
selectors: Node[]
combinator: CssNode;
selectors: CssNode[]
start: number;
end: number;
should_encapsulate: boolean;
constructor(combinator: Node) {
constructor(combinator: CssNode) {
this.combinator = combinator;
this.global = false;
this.selectors = [];
@ -278,7 +278,7 @@ class Block {
this.should_encapsulate = false;
}
add(selector: Node) {
add(selector: CssNode) {
if (this.selectors.length === 0) {
this.start = selector.start;
this.global = selector.type === 'PseudoClassSelector' && selector.name === 'global';
@ -289,12 +289,12 @@ class Block {
}
}
function group_selectors(selector: Node) {
function group_selectors(selector: CssNode) {
let block: Block = new Block(null);
const blocks = [block];
selector.children.forEach((child: Node) => {
selector.children.forEach((child: CssNode) => {
if (child.type === 'WhiteSpace' || child.type === 'Combinator') {
block = new Block(child);
blocks.push(block);

@ -2,20 +2,21 @@ import MagicString from 'magic-string';
import { walk } from 'estree-walker';
import Selector from './Selector';
import Element from '../nodes/Element';
import { Node, Ast } from '../../interfaces';
import { Ast, TemplateNode } from '../../interfaces';
import Component from '../Component';
import { CssNode } from './interfaces';
function remove_css_prefix(name: string): string {
return name.replace(/^-((webkit)|(moz)|(o)|(ms))-/, '');
}
const is_keyframes_node = (node: Node) =>
const is_keyframes_node = (node: CssNode) =>
remove_css_prefix(node.name) === 'keyframes';
const at_rule_has_declaration = ({ block }: Node): true =>
const at_rule_has_declaration = ({ block }: CssNode): true =>
block &&
block.children &&
block.children.find((node: Node) => node.type === 'Declaration');
block.children.find((node: CssNode) => node.type === 'Declaration');
function minify_declarations(
code: MagicString,
@ -48,14 +49,14 @@ function hash(str: string): string {
class Rule {
selectors: Selector[];
declarations: Declaration[];
node: Node;
node: CssNode;
parent: Atrule;
constructor(node: Node, stylesheet, parent?: Atrule) {
constructor(node: CssNode, stylesheet, parent?: Atrule) {
this.node = node;
this.parent = parent;
this.selectors = node.selector.children.map((node: Node) => new Selector(node, stylesheet));
this.declarations = node.block.children.map((node: Node) => new Declaration(node));
this.selectors = node.selector.children.map((node: CssNode) => new Selector(node, stylesheet));
this.declarations = node.block.children.map((node: CssNode) => new Declaration(node));
}
apply(node: Element, stack: Element[]) {
@ -117,16 +118,16 @@ class Rule {
}
class Declaration {
node: Node;
node: CssNode;
constructor(node: Node) {
constructor(node: CssNode) {
this.node = node;
}
transform(code: MagicString, keyframes: Map<string, string>) {
const property = this.node.property && remove_css_prefix(this.node.property.toLowerCase());
if (property === 'animation' || property === 'animation-name') {
this.node.value.children.forEach((block: Node) => {
this.node.value.children.forEach((block: CssNode) => {
if (block.type === 'Identifier') {
const name = block.name;
if (keyframes.has(name)) {
@ -155,11 +156,11 @@ class Declaration {
}
class Atrule {
node: Node;
node: CssNode;
children: Array<Atrule|Rule>;
declarations: Declaration[];
constructor(node: Node) {
constructor(node: CssNode) {
this.node = node;
this.children = [];
this.declarations = [];
@ -191,7 +192,7 @@ class Atrule {
let c = this.node.start + (expression_char === '(' ? 6 : 7);
if (this.node.expression.start > c) code.remove(c, this.node.expression.start);
this.node.expression.children.forEach((query: Node) => {
this.node.expression.children.forEach((query: CssNode) => {
// TODO minify queries
c = query.end;
});
@ -200,7 +201,7 @@ class Atrule {
} else if (this.node.name === 'supports') {
let c = this.node.start + 9;
if (this.node.expression.start - c > 1) code.overwrite(c, this.node.expression.start, ' ');
this.node.expression.children.forEach((query: Node) => {
this.node.expression.children.forEach((query: CssNode) => {
// TODO minify queries
c = query.end;
});
@ -240,7 +241,7 @@ class Atrule {
transform(code: MagicString, id: string, keyframes: Map<string, string>) {
if (is_keyframes_node(this.node)) {
this.node.expression.children.forEach(({ type, name, start, end }: Node) => {
this.node.expression.children.forEach(({ type, name, start, end }: CssNode) => {
if (type === 'Identifier') {
if (name.startsWith('-global-')) {
code.remove(start, start + 8);
@ -288,7 +289,7 @@ export default class Stylesheet {
children: Array<Rule|Atrule> = [];
keyframes: Map<string, string> = new Map();
nodes_with_css_class: Set<Node> = new Set();
nodes_with_css_class: Set<CssNode> = new Set();
constructor(source: string, ast: Ast, filename: string, dev: boolean) {
this.source = source;
@ -305,8 +306,8 @@ export default class Stylesheet {
let depth = 0;
let current_atrule: Atrule = null;
walk(ast.css, {
enter: (node: Node) => {
walk(ast.css as any, {
enter: (node: any) => {
if (node.type === 'Atrule') {
const atrule = new Atrule(node);
stack.push(atrule);
@ -318,7 +319,7 @@ export default class Stylesheet {
}
if (is_keyframes_node(node)) {
node.expression.children.forEach((expression: Node) => {
node.expression.children.forEach((expression: CssNode) => {
if (expression.type === 'Identifier' && !expression.name.startsWith('-global-')) {
this.keyframes.set(expression.name, `${this.id}-${expression.name}`);
}
@ -346,7 +347,7 @@ export default class Stylesheet {
depth += 1;
},
leave: (node: Node) => {
leave: (node: any) => {
if (node.type === 'Atrule') {
stack.pop();
current_atrule = stack[stack.length - 1];
@ -364,7 +365,7 @@ export default class Stylesheet {
if (!this.has_styles) return;
const stack: Element[] = [];
let parent: Node = node;
let parent: TemplateNode = node;
while (parent = parent.parent) {
if (parent.type === 'Element') stack.unshift(parent as Element);
}
@ -376,7 +377,7 @@ export default class Stylesheet {
}
reify() {
this.nodes_with_css_class.forEach((node: Node) => {
this.nodes_with_css_class.forEach((node: Element) => {
node.add_css_class();
});
}
@ -388,8 +389,8 @@ export default class Stylesheet {
const code = new MagicString(this.source);
walk(this.ast.css, {
enter: (node: Node) => {
walk(this.ast.css as any, {
enter: (node: any) => {
code.addSourcemapLocation(node.start);
code.addSourcemapLocation(node.end);
}

@ -1,4 +1,4 @@
import { Node } from '../../interfaces';
import { Node } from 'estree';
export const UNKNOWN = {};

@ -0,0 +1,6 @@
export interface CssNode {
type: string;
start: number;
end: number;
[prop_name: string]: any;
}

@ -1,4 +1,4 @@
import { stringify } from '../utils/stringify';
import { string_literal } from '../utils/stringify';
import add_to_set from '../utils/add_to_set';
import Component from '../Component';
import Node from './shared/Node';
@ -6,6 +6,7 @@ import Element from './Element';
import Text from './Text';
import Expression from './shared/Expression';
import TemplateScope from './shared/TemplateScope';
import { x } from 'code-red';
export default class Attribute extends Node {
type: 'Attribute';
@ -19,7 +20,6 @@ export default class Attribute extends Node {
is_spread: boolean;
is_true: boolean;
is_static: boolean;
is_synthetic: boolean;
expression?: Expression;
chunks: Array<Text | Expression>;
dependencies: Set<string>;
@ -32,7 +32,6 @@ export default class Attribute extends Node {
this.name = null;
this.is_spread = true;
this.is_true = false;
this.is_synthetic = false;
this.expression = new Expression(component, this, scope, info.expression);
this.dependencies = this.expression.dependencies;
@ -45,7 +44,6 @@ export default class Attribute extends Node {
this.name = info.name;
this.is_true = info.value === true;
this.is_static = true;
this.is_synthetic = info.synthetic;
this.dependencies = new Set();
@ -78,28 +76,24 @@ export default class Attribute extends Node {
}
get_value(block) {
if (this.is_true) return true;
if (this.chunks.length === 0) return `""`;
if (this.is_true) return x`true`;
if (this.chunks.length === 0) return x`""`;
if (this.chunks.length === 1) {
return this.chunks[0].type === 'Text'
? stringify((this.chunks[0] as Text).data)
// @ts-ignore todo: probably error
: this.chunks[0].render(block);
? string_literal((this.chunks[0] as Text).data)
: (this.chunks[0] as Expression).manipulate(block);
}
let expression = this.chunks
.map(chunk => chunk.type === 'Text' ? string_literal(chunk.data) : chunk.manipulate(block))
.reduce((lhs, rhs) => x`${lhs} + ${rhs}`);
if (this.chunks[0].type !== 'Text') {
expression = x`"" + ${expression}`;
}
return (this.chunks[0].type === 'Text' ? '' : `"" + `) +
this.chunks
.map(chunk => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
// @ts-ignore todo: probably error
return chunk.get_precedence() <= 13 ? `(${chunk.render()})` : chunk.render();
}
})
.join(' + ');
return expression;
}
get_static_value() {

@ -4,6 +4,7 @@ import Expression from './shared/Expression';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import {dimensions} from "../../utils/patterns";
import { Node as ESTreeNode } from 'estree';
// TODO this should live in a specific binding
const read_only_media_attributes = new Set([
@ -17,9 +18,8 @@ export default class Binding extends Node {
type: 'Binding';
name: string;
expression: Expression;
raw_expression: ESTreeNode; // TODO exists only for bind:this — is there a more elegant solution?
is_contextual: boolean;
obj: string;
prop: string;
is_readonly: boolean;
constructor(component: Component, parent, scope: TemplateScope, info) {
@ -34,9 +34,7 @@ export default class Binding extends Node {
this.name = info.name;
this.expression = new Expression(component, this, scope, info.expression);
let obj;
let prop;
this.raw_expression = JSON.parse(JSON.stringify(info.expression));
const { name } = get_object(this.expression.node);
this.is_contextual = scope.names.has(name);
@ -63,18 +61,6 @@ export default class Binding extends Node {
variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true;
}
if (this.expression.node.type === 'MemberExpression') {
prop = `[✂${this.expression.node.property.start}-${this.expression.node.property.end}✂]`;
if (!this.expression.node.computed) prop = `'${prop}'`;
obj = `[✂${this.expression.node.object.start}-${this.expression.node.object.end}✂]`;
} else {
obj = 'ctx';
prop = `'${name}'`;
}
this.obj = obj;
this.prop = prop;
const type = parent.get_static_attribute_value('type');
this.is_readonly = (

@ -1,49 +1,55 @@
import Node from './shared/Node';
import ElseBlock from './ElseBlock';
import Expression from './shared/Expression';
import map_children from './shared/map_children';
import TemplateScope from './shared/TemplateScope';
import AbstractBlock from './shared/AbstractBlock';
import { Node as INode } from '../../interfaces';
import { new_tail } from '../utils/tail';
import Element from './Element';
import { x } from 'code-red';
import { Node, Identifier, RestElement } from 'estree';
interface Context {
key: INode;
key: Identifier;
name?: string;
tail: string;
modifier: (node: Node) => Node;
}
function unpack_destructuring(contexts: Context[], node: INode, tail: string) {
function unpack_destructuring(contexts: Context[], node: Node, modifier: (node: Node) => Node) {
if (!node) return;
if (node.type === 'Identifier' || node.type === 'RestIdentifier') {
if (node.type === 'Identifier' || (node as any).type === 'RestIdentifier') { // TODO is this right? not RestElement?
contexts.push({
key: node,
tail
key: node as Identifier,
modifier
});
} else if (node.type === 'ArrayPattern') {
node.elements.forEach((element, i) => {
if (element && element.type === 'RestIdentifier') {
unpack_destructuring(contexts, element, `${tail}.slice(${i})`);
if (element && (element as any).type === 'RestIdentifier') {
unpack_destructuring(contexts, element, node => x`${modifier(node)}.slice(${i})` as Node);
} else {
unpack_destructuring(contexts, element, `${tail}[${i}]`);
unpack_destructuring(contexts, element, node => x`${modifier(node)}[${i}]` as Node);
}
});
} else if (node.type === 'ObjectPattern') {
const used_properties = [];
node.properties.forEach((property) => {
if (property.kind === 'rest') {
node.properties.forEach((property, i) => {
if ((property as any).kind === 'rest') { // TODO is this right?
const replacement: RestElement = {
type: 'RestElement',
argument: property.key as Identifier
};
node.properties[i] = replacement as any;
unpack_destructuring(
contexts,
property.value,
`@object_without_properties(${tail}, ${JSON.stringify(used_properties)})`
node => x`@object_without_properties(${modifier(node)}, [${used_properties}])` as Node
);
} else {
used_properties.push(property.key.name);
used_properties.push(x`"${(property.key as Identifier).name}"`);
unpack_destructuring(contexts, property.value,`${tail}.${property.key.name}`);
unpack_destructuring(contexts, property.value, node => x`${modifier(node)}.${(property.key as Identifier).name}` as Node);
}
});
}
@ -77,7 +83,7 @@ export default class EachBlock extends AbstractBlock {
this.scope = scope.child();
this.contexts = [];
unpack_destructuring(this.contexts, info.context, new_tail());
unpack_destructuring(this.contexts, info.context, node => node);
this.contexts.forEach(context => {
this.scope.add(context.key.name, this.expression.dependencies, this);

@ -201,7 +201,7 @@ export default class Element extends Node {
this.scope = scope.child();
this.lets.forEach(l => {
const dependencies = new Set([l.name]);
const dependencies = new Set([l.name.name]);
l.names.forEach(name => {
this.scope.add(name, dependencies, this);

@ -1,16 +1,17 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
import deindent from '../utils/deindent';
import { b, x } from 'code-red';
import Block from '../render_dom/Block';
import { sanitize } from '../../utils/names';
import { Identifier } from 'estree';
export default class EventHandler extends Node {
type: 'EventHandler';
name: string;
modifiers: Set<string>;
expression: Expression;
handler_name: string;
handler_name: Identifier;
uses_context = false;
can_make_passive = false;
@ -31,40 +32,44 @@ export default class EventHandler extends Node {
} else if (info.expression.type === 'Identifier') {
let node = component.node_for_declaration.get(info.expression.name);
if (node && node.type === 'VariableDeclaration') {
// for `const handleClick = () => {...}`, we want the [arrow] function expression node
const declarator = node.declarations.find(d => d.id.name === info.expression.name);
node = declarator && declarator.init;
}
if (node) {
if (node.type === 'VariableDeclaration') {
// for `const handleClick = () => {...}`, we want the [arrow] function expression node
const declarator = node.declarations.find(d => (d.id as Identifier).name === info.expression.name);
node = declarator && declarator.init;
}
if (node && /Function/.test(node.type) && node.params.length === 0) {
this.can_make_passive = true;
if (node && (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration' || node.type === 'ArrowFunctionExpression') && node.params.length === 0) {
this.can_make_passive = true;
}
}
}
} else {
const name = component.get_unique_name(`${sanitize(this.name)}_handler`);
const id = component.get_unique_name(`${sanitize(this.name)}_handler`);
component.add_var({
name,
name: id.name,
internal: true,
referenced: true
});
component.partly_hoisted.push(deindent`
function ${name}(event) {
component.partly_hoisted.push(b`
function ${id}(event) {
@bubble($$self, event);
}
`);
this.handler_name = name;
this.handler_name = id;
}
}
// TODO move this? it is specific to render-dom
render(block: Block) {
if (this.expression) return this.expression.render(block);
if (this.expression) {
return this.expression.manipulate(block);
}
// this.component.add_reference(this.handler_name);
return `ctx.${this.handler_name}`;
return x`#ctx.${this.handler_name}`;
}
}

@ -90,7 +90,7 @@ export default class InlineComponent extends Node {
this.scope = scope.child();
this.lets.forEach(l => {
const dependencies = new Set([l.name]);
const dependencies = new Set([l.name.name]);
l.names.forEach(name => {
this.scope.add(name, dependencies, this);

@ -1,38 +1,51 @@
import Node from './shared/Node';
import Component from '../Component';
import { walk } from 'estree-walker';
import { Identifier } from 'estree';
const applicable = new Set(['Identifier', 'ObjectExpression', 'ArrayExpression', 'Property']);
export default class Let extends Node {
type: 'Let';
name: string;
value: string;
name: Identifier;
value: Identifier;
names: string[] = [];
constructor(component: Component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
this.value = info.expression && `[✂${info.expression.start}-${info.expression.end}✂]`;
this.name = { type: 'Identifier', name: info.name };
const { names } = this;
if (info.expression) {
this.value = info.expression;
walk(info.expression, {
enter: node => {
enter(node) {
if (!applicable.has(node.type)) {
component.error(node, {
component.error(node as any, {
code: 'invalid-let',
message: `let directive value must be an identifier or an object/array pattern`
});
}
if (node.type === 'Identifier') {
this.names.push(node.name);
names.push(node.name);
}
// slightly unfortunate hack
if (node.type === 'ArrayExpression') {
(node as any).type = 'ArrayPattern';
}
if (node.type === 'ObjectExpression') {
(node as any).type = 'ObjectPattern';
}
}
});
} else {
this.names.push(this.name);
names.push(this.name.name);
}
}
}

@ -3,73 +3,24 @@ import { walk } from 'estree-walker';
import is_reference from 'is-reference';
import flatten_reference from '../../utils/flatten_reference';
import { create_scopes, Scope, extract_names } from '../../utils/scope';
import { Node } from '../../../interfaces';
import { globals , sanitize } from '../../../utils/names';
import deindent from '../../utils/deindent';
import { globals, sanitize } from '../../../utils/names';
import Wrapper from '../../render_dom/wrappers/shared/Wrapper';
import TemplateScope from './TemplateScope';
import get_object from '../../utils/get_object';
import Block from '../../render_dom/Block';
import { INode } from '../interfaces';
import is_dynamic from '../../render_dom/wrappers/shared/is_dynamic';
import { x, b, p } from 'code-red';
import { invalidate } from '../../utils/invalidate';
import { Node, FunctionExpression } from 'estree';
import { TemplateNode } from '../../../interfaces';
const binary_operators: Record<string, number> = {
'**': 15,
'*': 14,
'/': 14,
'%': 14,
'+': 13,
'-': 13,
'<<': 12,
'>>': 12,
'>>>': 12,
'<': 11,
'<=': 11,
'>': 11,
'>=': 11,
in: 11,
instanceof: 11,
'==': 10,
'!=': 10,
'===': 10,
'!==': 10,
'&': 9,
'^': 8,
'|': 7
};
const logical_operators: Record<string, number> = {
'&&': 6,
'||': 5
};
const precedence: Record<string, (node?: Node) => number> = {
Literal: () => 21,
Identifier: () => 21,
ParenthesizedExpression: () => 20,
MemberExpression: () => 19,
NewExpression: () => 19, // can be 18 (if no args) but makes no practical difference
CallExpression: () => 19,
UpdateExpression: () => 17,
UnaryExpression: () => 16,
BinaryExpression: (node: Node) => binary_operators[node.operator],
LogicalExpression: (node: Node) => logical_operators[node.operator],
ConditionalExpression: () => 4,
AssignmentExpression: () => 3,
YieldExpression: () => 2,
SpreadElement: () => 1,
SequenceExpression: () => 0
};
type Owner = Wrapper | INode;
type Owner = Wrapper | TemplateNode;
export default class Expression {
type: 'Expression' = 'Expression';
component: Component;
owner: Owner;
node: any;
snippet: string;
references: Set<string>;
dependencies: Set<string> = new Set();
contextual_dependencies: Set<string> = new Set();
@ -78,11 +29,10 @@ export default class Expression {
scope: Scope;
scope_map: WeakMap<Node, Scope>;
is_synthetic: boolean;
declarations: string[] = [];
declarations: Array<(Node | Node[])> = [];
uses_context = false;
rendered: string;
manipulated: Node;
// todo: owner type
constructor(component: Component, owner: Owner, template_scope: TemplateScope, info, lazy?: boolean) {
@ -96,8 +46,6 @@ export default class Expression {
this.node = info;
this.template_scope = template_scope;
this.owner = owner;
// @ts-ignore
this.is_synthetic = owner.is_synthetic;
const { dependencies, contextual_dependencies } = this;
@ -219,46 +167,39 @@ export default class Expression {
});
}
get_precedence() {
return this.node.type in precedence ? precedence[this.node.type](this.node) : 0;
}
// TODO move this into a render-dom wrapper?
render(block?: Block) {
if (this.rendered) return this.rendered;
manipulate(block?: Block) {
// TODO ideally we wouldn't end up calling this method
// multiple times
if (this.manipulated) return this.manipulated;
const {
component,
declarations,
scope_map: map,
template_scope,
owner,
is_synthetic
owner
} = this;
let scope = this.scope;
const { code } = component;
let function_expression;
let dependencies: Set<string>;
let contextual_dependencies: Set<string>;
// rewrite code as appropriate
walk(this.node, {
enter(node: any, parent: any, key: string) {
// don't manipulate shorthand props twice
if (key === 'value' && parent.shorthand) return;
code.addSourcemapLocation(node.start);
code.addSourcemapLocation(node.end);
const node = walk(this.node, {
enter(node: any, parent: any) {
if (node.type === 'Property' && node.shorthand) {
node.value = JSON.parse(JSON.stringify(node.value));
node.shorthand = false;
}
if (map.has(node)) {
scope = map.get(node);
}
if (is_reference(node, parent)) {
const { name, nodes } = flatten_reference(node);
const { name } = flatten_reference(node);
if (scope.has(name)) return;
if (globals.has(name) && !(component.var_lookup.has(name) || template_scope.names.has(name))) return;
@ -274,17 +215,8 @@ export default class Expression {
dependencies.add(name);
component.add_reference(name); // TODO is this redundant/misplaced?
}
} else if (!is_synthetic && is_contextual(component, template_scope, name)) {
code.prependRight(node.start, key === 'key' && parent.shorthand
? `${name}: ctx.`
: 'ctx.');
}
if (node.type === 'MemberExpression') {
nodes.forEach(node => {
code.addSourcemapLocation(node.start);
code.addSourcemapLocation(node.end);
});
} else if (is_contextual(component, template_scope, name)) {
this.replace(x`#ctx.${node}`);
}
this.skip();
@ -307,34 +239,20 @@ export default class Expression {
if (map.has(node)) scope = scope.parent;
if (node === function_expression) {
const name = component.get_unique_name(
const id = component.get_unique_name(
sanitize(get_function_name(node, owner))
);
const args = contextual_dependencies.size > 0
? [`{ ${Array.from(contextual_dependencies).join(', ')} }`]
: [];
let original_params;
if (node.params.length > 0) {
original_params = code.slice(node.params[0].start, node.params[node.params.length - 1].end);
args.push(original_params);
}
const body = code.slice(node.body.start, node.body.end).trim();
const fn = node.type === 'FunctionExpression'
? `${node.async ? 'async ' : ''}function${node.generator ? '*' : ''} ${name}(${args.join(', ')}) ${body}`
: `const ${name} = ${node.async ? 'async ' : ''}(${args.join(', ')}) => ${body};`;
const declaration = b`const ${id} = ${node}`;
if (dependencies.size === 0 && contextual_dependencies.size === 0) {
// we can hoist this out of the component completely
component.fully_hoisted.push(fn);
code.overwrite(node.start, node.end, name);
component.fully_hoisted.push(declaration);
this.replace(id as any);
component.add_var({
name,
name: id.name,
internal: true,
hoistable: true,
referenced: true
@ -343,11 +261,12 @@ export default class Expression {
else if (contextual_dependencies.size === 0) {
// function can be hoisted inside the component init
component.partly_hoisted.push(fn);
code.overwrite(node.start, node.end, `ctx.${name}`);
component.partly_hoisted.push(declaration);
this.replace(x`#ctx.${id}` as any);
component.add_var({
name,
name: id.name,
internal: true,
referenced: true
});
@ -355,29 +274,43 @@ export default class Expression {
else {
// we need a combo block/init recipe
component.partly_hoisted.push(fn);
code.overwrite(node.start, node.end, name);
(node as FunctionExpression).params.unshift({
type: 'ObjectPattern',
properties: Array.from(contextual_dependencies).map(name => p`${name}` as any)
});
component.partly_hoisted.push(declaration);
this.replace(id as any);
component.add_var({
name,
name: id.name,
internal: true,
referenced: true
});
declarations.push(deindent`
function ${name}(${original_params ? '...args' : ''}) {
return ctx.${name}(ctx${original_params ? ', ...args' : ''});
}
`);
}
if (parent && parent.method) {
code.prependRight(node.start, ': ');
if ((node as FunctionExpression).params.length > 0) {
declarations.push(b`
function ${id}(...args) {
return #ctx.${id}(#ctx, ...args);
}
`);
} else {
declarations.push(b`
function ${id}() {
return #ctx.${id}(#ctx);
}
`);
}
}
function_expression = null;
dependencies = null;
contextual_dependencies = null;
if (parent && parent.type === 'Property') {
parent.method = false;
}
}
if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
@ -399,7 +332,7 @@ export default class Expression {
}
});
invalidate(component, scope, code, node, traced);
this.replace(invalidate(component, scope, node, traced));
}
}
});
@ -407,11 +340,11 @@ export default class Expression {
if (declarations.length > 0) {
block.maintain_context = true;
declarations.forEach(declaration => {
block.builders.init.add_block(declaration);
block.chunks.init.push(declaration);
});
}
return this.rendered = `[✂${this.node.start}-${this.node.end}✂]`;
return (this.manipulated = node);
}
}

@ -14,7 +14,7 @@ import Slot from '../Slot';
import Text from '../Text';
import Title from '../Title';
import Window from '../Window';
import { Node } from '../../../interfaces';
import { TemplateNode } from '../../../interfaces';
export type Children = ReturnType<typeof map_children>;
@ -40,7 +40,7 @@ function get_constructor(type) {
}
}
export default function map_children(component, parent, scope, children: Node[]) {
export default function map_children(component, parent, scope, children: TemplateNode[]) {
let last = null;
let ignores = [];

@ -1,52 +1,66 @@
import CodeBuilder from '../utils/CodeBuilder';
import deindent from '../utils/deindent';
import Renderer from './Renderer';
import Wrapper from './wrappers/shared/Wrapper';
import { stringify, escape } from '../utils/stringify';
import { b, x } from 'code-red';
import { Node, Identifier } from 'estree';
import { is_head } from './wrappers/shared/is_head';
export interface BlockOptions {
parent?: Block;
name: string;
name: Identifier;
type: string;
renderer?: Renderer;
comment?: string;
key?: string;
bindings?: Map<string, { object: string; property: string; snippet: string; store: string; tail: string }>;
key?: Identifier;
bindings?: Map<string, {
object: Identifier;
property: Identifier;
snippet: Node;
store: string;
tail: Node;
modifier: (node: Node) => Node;
}>;
dependencies?: Set<string>;
}
export default class Block {
parent?: Block;
renderer: Renderer;
name: string;
name: Identifier;
type: string;
comment?: string;
wrappers: Wrapper[];
key: string;
first: string;
key: Identifier;
first: Identifier;
dependencies: Set<string>;
bindings: Map<string, { object: string; property: string; snippet: string; store: string; tail: string }>;
builders: {
init: CodeBuilder;
create: CodeBuilder;
claim: CodeBuilder;
hydrate: CodeBuilder;
mount: CodeBuilder;
measure: CodeBuilder;
fix: CodeBuilder;
animate: CodeBuilder;
intro: CodeBuilder;
update: CodeBuilder;
outro: CodeBuilder;
destroy: CodeBuilder;
bindings: Map<string, {
object: Identifier;
property: Identifier;
snippet: Node;
store: string;
tail: Node;
modifier: (node: Node) => Node;
}>;
chunks: {
init: Array<Node | Node[]>;
create: Array<Node | Node[]>;
claim: Array<Node | Node[]>;
hydrate: Array<Node | Node[]>;
mount: Array<Node | Node[]>;
measure: Array<Node | Node[]>;
fix: Array<Node | Node[]>;
animate: Array<Node | Node[]>;
intro: Array<Node | Node[]>;
update: Array<Node | Node[]>;
outro: Array<Node | Node[]>;
destroy: Array<Node | Node[]>;
};
event_listeners: string[] = [];
event_listeners: Node[] = [];
maintain_context: boolean;
has_animation: boolean;
@ -56,9 +70,9 @@ export default class Block {
has_outro_method: boolean;
outros: number;
aliases: Map<string, string>;
variables: Map<string, string>;
get_unique_name: (name: string) => string;
aliases: Map<string, Identifier>;
variables: Map<string, { id: Identifier; init?: Node }> = new Map();
get_unique_name: (name: string) => Identifier;
has_update_method = false;
autofocus: string;
@ -80,19 +94,19 @@ export default class Block {
this.bindings = options.bindings;
this.builders = {
init: new CodeBuilder(),
create: new CodeBuilder(),
claim: new CodeBuilder(),
hydrate: new CodeBuilder(),
mount: new CodeBuilder(),
measure: new CodeBuilder(),
fix: new CodeBuilder(),
animate: new CodeBuilder(),
intro: new CodeBuilder(),
update: new CodeBuilder(),
outro: new CodeBuilder(),
destroy: new CodeBuilder(),
this.chunks = {
init: [],
create: [],
claim: [],
hydrate: [],
mount: [],
measure: [],
fix: [],
animate: [],
intro: [],
update: [],
outro: [],
destroy: [],
};
this.has_animation = false;
@ -101,15 +115,14 @@ export default class Block {
this.outros = 0;
this.get_unique_name = this.renderer.component.get_unique_name_maker();
this.variables = new Map();
this.aliases = new Map().set('ctx', this.get_unique_name('ctx'));
this.aliases = new Map();
if (this.key) this.aliases.set('key', this.get_unique_name('key'));
}
assign_variable_names() {
const seen = new Set();
const dupes = new Set();
const seen: Set<string> = new Set();
const dupes: Set<string> = new Set();
let i = this.wrappers.length;
@ -117,13 +130,12 @@ export default class Block {
const wrapper = this.wrappers[i];
if (!wrapper.var) continue;
if (wrapper.parent && wrapper.parent.can_use_innerhtml) continue;
if (seen.has(wrapper.var)) {
dupes.add(wrapper.var);
if (seen.has(wrapper.var.name)) {
dupes.add(wrapper.var.name);
}
seen.add(wrapper.var);
seen.add(wrapper.var.name);
}
const counts = new Map();
@ -134,12 +146,10 @@ export default class Block {
if (!wrapper.var) continue;
if (dupes.has(wrapper.var)) {
const i = counts.get(wrapper.var) || 0;
counts.set(wrapper.var, i + 1);
wrapper.var = this.get_unique_name(wrapper.var + i);
} else {
wrapper.var = this.get_unique_name(wrapper.var);
if (dupes.has(wrapper.var.name)) {
const i = counts.get(wrapper.var.name) || 0;
counts.set(wrapper.var.name, i + 1);
wrapper.var.name = this.get_unique_name(wrapper.var.name + i).name;
}
}
}
@ -153,25 +163,25 @@ export default class Block {
}
add_element(
name: string,
render_statement: string,
claim_statement: string,
parent_node: string,
id: Identifier,
render_statement: Node,
claim_statement: Node,
parent_node: Node,
no_detach?: boolean
) {
this.add_variable(name);
this.builders.create.add_line(`${name} = ${render_statement};`);
this.add_variable(id);
this.chunks.create.push(b`${id} = ${render_statement};`);
if (this.renderer.options.hydratable) {
this.builders.claim.add_line(`${name} = ${claim_statement || render_statement};`);
this.chunks.claim.push(b`${id} = ${claim_statement || render_statement};`);
}
if (parent_node) {
this.builders.mount.add_line(`@append(${parent_node}, ${name});`);
if (parent_node === '@_document.head' && !no_detach) this.builders.destroy.add_line(`@detach(${name});`);
this.chunks.mount.push(b`@append(${parent_node}, ${id});`);
if (is_head(parent_node) && !no_detach) this.chunks.destroy.push(b`@detach(${id});`);
} else {
this.builders.mount.add_line(`@insert(#target, ${name}, anchor);`);
if (!no_detach) this.builders.destroy.add_conditional('detaching', `@detach(${name});`);
this.chunks.mount.push(b`@insert(#target, ${id}, anchor);`);
if (!no_detach) this.chunks.destroy.push(b`if (detaching) @detach(${id});`);
}
}
@ -190,18 +200,16 @@ export default class Block {
this.has_animation = true;
}
add_variable(name: string, init?: string) {
if (name[0] === '#') {
name = this.alias(name.slice(1));
}
if (this.variables.has(name) && this.variables.get(name) !== init) {
throw new Error(
`Variable '${name}' already initialised with a different value`
);
}
add_variable(id: Identifier, init?: Node) {
this.variables.forEach(v => {
if (v.id.name === id.name) {
throw new Error(
`Variable '${id.name}' already initialised with a different value`
);
}
});
this.variables.set(name, init);
this.variables.set(id.name, { id, init });
}
alias(name: string) {
@ -216,215 +224,223 @@ export default class Block {
return new Block(Object.assign({}, this, { key: null }, options, { parent: this }));
}
get_contents(local_key?: string) {
get_contents(key?: any) {
const { dev } = this.renderer.options;
if (this.has_outros) {
this.add_variable('#current');
this.add_variable({ type: 'Identifier', name: '#current' });
if (!this.builders.intro.is_empty()) {
this.builders.intro.add_line(`#current = true;`);
this.builders.mount.add_line(`#current = true;`);
if (this.chunks.intro.length > 0) {
this.chunks.intro.push(b`#current = true;`);
this.chunks.mount.push(b`#current = true;`);
}
if (!this.builders.outro.is_empty()) {
this.builders.outro.add_line(`#current = false;`);
if (this.chunks.outro.length > 0) {
this.chunks.outro.push(b`#current = false;`);
}
}
if (this.autofocus) {
this.builders.mount.add_line(`${this.autofocus}.focus();`);
this.chunks.mount.push(b`${this.autofocus}.focus();`);
}
this.render_listeners();
const properties = new CodeBuilder();
const properties: Record<string, any> = {};
const method_name = (short: string, long: string) => dev ? `${short}: function ${this.get_unique_name(long)}` : short;
const noop = x`@noop`;
if (local_key) {
properties.add_block(`key: ${local_key},`);
}
properties.key = key;
if (this.first) {
properties.add_block(`first: null,`);
this.builders.hydrate.add_line(`this.first = ${this.first};`);
properties.first = x`null`;
this.chunks.hydrate.push(b`this.first = ${this.first};`);
}
if (this.builders.create.is_empty() && this.builders.hydrate.is_empty()) {
properties.add_line(`c: @noop,`);
if (this.chunks.create.length === 0 && this.chunks.hydrate.length === 0) {
properties.create = noop;
} else {
const hydrate = !this.builders.hydrate.is_empty() && (
const hydrate = this.chunks.hydrate.length > 0 && (
this.renderer.options.hydratable
? `this.h()`
: this.builders.hydrate
? b`this.h();`
: this.chunks.hydrate
);
properties.add_block(deindent`
${method_name('c', 'create')}() {
${this.builders.create}
${hydrate}
},
`);
properties.create = x`function create() {
${this.chunks.create}
${hydrate}
}`;
}
if (this.renderer.options.hydratable || !this.builders.claim.is_empty()) {
if (this.builders.claim.is_empty() && this.builders.hydrate.is_empty()) {
properties.add_line(`l: @noop,`);
if (this.renderer.options.hydratable || this.chunks.claim.length > 0) {
if (this.chunks.claim.length === 0 && this.chunks.hydrate.length === 0) {
properties.claim = noop;
} else {
properties.add_block(deindent`
${method_name('l', 'claim')}(nodes) {
${this.builders.claim}
${this.renderer.options.hydratable && !this.builders.hydrate.is_empty() && `this.h();`}
},
`);
properties.claim = x`function claim(#nodes) {
${this.chunks.claim}
${this.renderer.options.hydratable && this.chunks.hydrate.length > 0 && b`this.h();`}
}`;
}
}
if (this.renderer.options.hydratable && !this.builders.hydrate.is_empty()) {
properties.add_block(deindent`
${method_name('h', 'hydrate')}() {
${this.builders.hydrate}
},
`);
if (this.renderer.options.hydratable && this.chunks.hydrate.length > 0) {
properties.hydrate = x`function hydrate() {
${this.chunks.hydrate}
}`;
}
if (this.builders.mount.is_empty()) {
properties.add_line(`m: @noop,`);
if (this.chunks.mount.length === 0) {
properties.mount = noop;
} else {
properties.add_block(deindent`
${method_name('m', 'mount')}(#target, anchor) {
${this.builders.mount}
},
`);
properties.mount = x`function mount(#target, anchor) {
${this.chunks.mount}
}`;
}
if (this.has_update_method || this.maintain_context) {
if (this.builders.update.is_empty() && !this.maintain_context) {
properties.add_line(`p: @noop,`);
if (this.chunks.update.length === 0 && !this.maintain_context) {
properties.update = noop;
} else {
properties.add_block(deindent`
${method_name('p', 'update')}(changed, ${this.maintain_context ? 'new_ctx' : 'ctx'}) {
${this.maintain_context && `ctx = new_ctx;`}
${this.builders.update}
},
`);
const ctx = this.maintain_context ? x`#new_ctx` : x`#ctx`;
properties.update = x`function update(#changed, ${ctx}) {
${this.maintain_context && b`#ctx = ${ctx};`}
${this.chunks.update}
}`;
}
}
if (this.has_animation) {
properties.add_block(deindent`
${method_name('r', 'measure')}() {
${this.builders.measure}
},
${method_name('f', 'fix')}() {
${this.builders.fix}
},
${method_name('a', 'animate')}() {
${this.builders.animate}
},
`);
properties.measure = x`function measure() {
${this.chunks.measure}
}`;
properties.fix = x`function fix() {
${this.chunks.fix}
}`;
properties.animate = x`function animate() {
${this.chunks.animate}
}`;
}
if (this.has_intro_method || this.has_outro_method) {
if (this.builders.intro.is_empty()) {
properties.add_line(`i: @noop,`);
if (this.chunks.intro.length === 0) {
properties.intro = noop;
} else {
properties.add_block(deindent`
${method_name('i', 'intro')}(#local) {
${this.has_outros && `if (#current) return;`}
${this.builders.intro}
},
`);
properties.intro = x`function intro(#local) {
${this.has_outros && b`if (#current) return;`}
${this.chunks.intro}
}`;
}
if (this.builders.outro.is_empty()) {
properties.add_line(`o: @noop,`);
if (this.chunks.outro.length === 0) {
properties.outro = noop;
} else {
properties.add_block(deindent`
${method_name('o', 'outro')}(#local) {
${this.builders.outro}
},
`);
properties.outro = x`function outro(#local) {
${this.chunks.outro}
}`;
}
}
if (this.builders.destroy.is_empty()) {
properties.add_line(`d: @noop`);
if (this.chunks.destroy.length === 0) {
properties.destroy = noop;
} else {
properties.add_block(deindent`
${method_name('d', 'destroy')}(detaching) {
${this.builders.destroy}
}
`);
properties.destroy = x`function destroy(detaching) {
${this.chunks.destroy}
}`;
}
/* eslint-disable @typescript-eslint/indent,indent */
return deindent`
${this.variables.size > 0 &&
`var ${Array.from(this.variables.keys())
.map(key => {
const init = this.variables.get(key);
return init !== undefined ? `${key} = ${init}` : key;
})
.join(', ')};`}
if (!this.renderer.component.compile_options.dev) {
// allow shorthand names
for (const name in properties) {
const property = properties[name];
if (property) property.id = null;
}
}
${!this.builders.init.is_empty() && this.builders.init}
const return_value: any = x`{
key: ${properties.key},
first: ${properties.first},
c: ${properties.create},
l: ${properties.claim},
h: ${properties.hydrate},
m: ${properties.mount},
p: ${properties.update},
r: ${properties.measure},
f: ${properties.fix},
a: ${properties.animate},
i: ${properties.intro},
o: ${properties.outro},
d: ${properties.destroy}
}`;
const body = b`
${Array.from(this.variables.values()).map(({ id, init }) => {
return init
? b`let ${id} = ${init}`
: b`let ${id}`;
})}
${this.chunks.init}
${dev
? deindent`
const block = {
${properties}
};
@dispatch_dev("SvelteRegisterBlock", { block, id: ${this.name || 'create_fragment'}.name, type: "${this.type}", source: ${stringify(this.comment || '', { only_escape_at_symbol: true })}, ctx });
? b`
const block = ${return_value};
@dispatch_dev("SvelteRegisterBlock", { block, id: ${this.name || 'create_fragment'}.name, type: "${this.type}", source: "${this.comment ? this.comment.replace(/"/g, '\\"') : ''}", ctx: #ctx });
return block;`
: deindent`
return {
${properties}
};`
: b`
return ${return_value};`
}
`.replace(/([^{])(#+)(\w*)/g, (_match: string, pre: string, sigil: string, name: string) => {
return pre + (sigil === '#' ? this.alias(name) : sigil.slice(1) + name);
});
/* eslint-enable @typescript-eslint/indent,indent */
`;
return body;
}
render() {
const key = this.key && this.get_unique_name('key');
const args: any[] = [x`#ctx`];
if (key) args.unshift(key);
// TODO include this.comment
// ${this.comment && `// ${escape(this.comment, { only_escape_at_symbol: true })}`}
return b`
function ${this.name}(${args}) {
${this.get_contents(key)}
}
`;
}
render_listeners(chunk: string = '') {
if (this.event_listeners.length > 0) {
this.add_variable(`#dispose${chunk}`);
const dispose: Identifier = {
type: 'Identifier',
name: `#dispose${chunk}`
};
this.add_variable(dispose);
if (this.event_listeners.length === 1) {
this.builders.hydrate.add_line(
`#dispose${chunk} = ${this.event_listeners[0]};`
this.chunks.hydrate.push(
b`${dispose} = ${this.event_listeners[0]};`
);
this.builders.destroy.add_line(
`#dispose${chunk}();`
this.chunks.destroy.push(
b`${dispose}();`
);
} else {
this.builders.hydrate.add_block(deindent`
#dispose${chunk} = [
${this.event_listeners.join(',\n')}
this.chunks.hydrate.push(b`
${dispose} = [
${this.event_listeners}
];
`);
this.builders.destroy.add_line(
`@run_all(#dispose${chunk});`
this.chunks.destroy.push(
b`@run_all(${dispose});`
);
}
}
}
toString() {
const local_key = this.key && this.get_unique_name('key');
return deindent`
${this.comment && `// ${escape(this.comment, { only_escape_at_symbol: true })}`}
function ${this.name}(${this.key ? `${local_key}, ` : ''}ctx) {
${this.get_contents(local_key)}
}
`;
}
}
}

@ -2,21 +2,22 @@ import Block from './Block';
import { CompileOptions } from '../../interfaces';
import Component from '../Component';
import FragmentWrapper from './wrappers/Fragment';
import CodeBuilder from '../utils/CodeBuilder';
import { x } from 'code-red';
import { Node, Identifier } from 'estree';
export default class Renderer {
component: Component; // TODO Maybe Renderer shouldn't know about Component?
options: CompileOptions;
blocks: Array<Block | string> = [];
blocks: Array<Block | Node | Node[]> = [];
readonly: Set<string> = new Set();
meta_bindings: CodeBuilder = new CodeBuilder(); // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
meta_bindings: Array<Node | Node[]> = []; // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
binding_groups: string[] = [];
block: Block;
fragment: FragmentWrapper;
file_var: string;
file_var: Identifier;
locate: (c: number) => { line: number; column: number };
constructor(component: Component, options: CompileOptions) {
@ -49,14 +50,15 @@ export default class Renderer {
null
);
// TODO messy
this.blocks.forEach(block => {
if (typeof block !== 'string') {
if (block instanceof Block) {
block.assign_variable_names();
}
});
this.block.assign_variable_names();
this.fragment.render(this.block, null, 'nodes');
this.fragment.render(this.block, null, x`#nodes` as Identifier);
}
}

@ -1,20 +1,19 @@
import deindent from '../utils/deindent';
import { stringify, escape } from '../utils/stringify';
import CodeBuilder from '../utils/CodeBuilder';
import { b, x, p } from 'code-red';
import Component from '../Component';
import Renderer from './Renderer';
import { CompileOptions } from '../../interfaces';
import { walk } from 'estree-walker';
import { stringify_props } from '../utils/stringify_props';
import add_to_set from '../utils/add_to_set';
import { extract_names } from '../utils/scope';
import { invalidate } from '../utils/invalidate';
import Block from './Block';
import { ClassDeclaration, FunctionExpression, Node, Statement } from 'estree';
export default function dom(
component: Component,
options: CompileOptions
) {
const { name, code } = component;
const { name } = component;
const renderer = new Renderer(component, options);
const { block } = renderer;
@ -22,27 +21,34 @@ export default function dom(
block.has_outro_method = true;
// prevent fragment being created twice (#1063)
if (options.customElement) block.builders.create.add_line(`this.c = @noop;`);
if (options.customElement) block.chunks.create.push(b`this.c = @noop;`);
const builder = new CodeBuilder();
const body = [];
if (component.compile_options.dev) {
builder.add_line(`const ${renderer.file_var} = ${component.file && stringify(component.file, { only_escape_at_symbol: true })};`);
if (renderer.file_var) {
const file = component.file ? x`"${component.file}"` : x`undefined`;
body.push(b`const ${renderer.file_var} = ${file};`);
}
const css = component.stylesheet.render(options.filename, !options.customElement);
const styles = component.stylesheet.has_styles && stringify(options.dev ?
`${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */` :
css.code, { only_escape_at_symbol: true });
const styles = component.stylesheet.has_styles && options.dev
? `${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */`
: css.code;
const add_css = component.get_unique_name('add_css');
if (styles && component.compile_options.css !== false && !options.customElement) {
builder.add_block(deindent`
const should_add_css = (
!options.customElement &&
!!styles &&
options.css !== false
);
if (should_add_css) {
body.push(b`
function ${add_css}() {
var style = @element("style");
style.id = '${component.stylesheet.id}-style';
style.textContent = ${styles};
style.id = "${component.stylesheet.id}-style";
style.textContent = "${styles}";
@append(@_document.head, style);
}
`);
@ -52,24 +58,19 @@ export default function dom(
// TODO the deconflicted names of blocks are reversed... should set them here
const blocks = renderer.blocks.slice().reverse();
blocks.forEach(block => {
builder.add_block(block.toString());
});
body.push(...blocks.map(block => {
// TODO this is a horrible mess — renderer.blocks
// contains a mixture of Blocks and Nodes
if ((block as Block).render) return (block as Block).render();
return block;
}));
if (options.dev && !options.hydratable) {
block.builders.claim.add_line(
'throw new @_Error("options.hydrate only works if the component was compiled with the `hydratable: true` option");'
block.chunks.claim.push(
b`throw new @_Error("options.hydrate only works if the component was compiled with the \`hydratable: true\` option");`
);
}
// TODO injecting CSS this way is kinda dirty. Maybe it should be an
// explicit opt-in, or something?
const should_add_css = (
!options.customElement &&
component.stylesheet.has_styles &&
options.css !== false
);
const uses_props = component.var_lookup.has('$$props');
const $$props = uses_props ? `$$new_props` : `$$props`;
const props = component.vars.filter(variable => !variable.module && variable.export_name);
@ -77,62 +78,77 @@ export default function dom(
/* eslint-disable @typescript-eslint/indent,indent */
const set = (uses_props || writable_props.length > 0 || component.slots.size > 0)
? deindent`
? x`
${$$props} => {
${uses_props && component.invalidate('$$props', `$$props = @assign(@assign({}, $$props), $$new_props)`)}
${uses_props && component.invalidate('$$props', x`$$props = @assign(@assign({}, $$props), $$new_props)`)}
${writable_props.map(prop =>
`if ('${prop.export_name}' in ${$$props}) ${component.invalidate(prop.name, `${prop.name} = ${$$props}.${prop.export_name}`)};`
b`if ('${prop.export_name}' in ${$$props}) ${component.invalidate(prop.name, x`${prop.name} = ${$$props}.${prop.export_name}`)};`
)}
${component.slots.size > 0 &&
`if ('$$scope' in ${$$props}) ${component.invalidate('$$scope', `$$scope = ${$$props}.$$scope`)};`}
b`if ('$$scope' in ${$$props}) ${component.invalidate('$$scope', x`$$scope = ${$$props}.$$scope`)};`}
}
`
: null;
/* eslint-enable @typescript-eslint/indent,indent */
const body = [];
const accessors = [];
const not_equal = component.component_options.immutable ? `@not_equal` : `@safe_not_equal`;
const not_equal = component.component_options.immutable ? x`@not_equal` : x`@safe_not_equal`;
let dev_props_check; let inject_state; let capture_state;
props.forEach(x => {
const variable = component.var_lookup.get(x.name);
props.forEach(prop => {
const variable = component.var_lookup.get(prop.name);
if (!variable.writable || component.component_options.accessors) {
body.push(deindent`
get ${x.export_name}() {
return ${x.hoistable ? x.name : 'this.$$.ctx.' + x.name};
}
`);
accessors.push({
type: 'MethodDefinition',
kind: 'get',
key: { type: 'Identifier', name: prop.export_name },
value: x`function() {
return ${prop.hoistable ? prop.name : x`this.$$.ctx.${prop.name}`}
}`
});
} else if (component.compile_options.dev) {
body.push(deindent`
get ${x.export_name}() {
accessors.push({
type: 'MethodDefinition',
kind: 'get',
key: { type: 'Identifier', name: prop.export_name },
value: x`function() {
throw new @_Error("<${component.tag}>: Props cannot be read directly from the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");
}
`);
}`
});
}
if (component.component_options.accessors) {
if (variable.writable && !renderer.readonly.has(x.name)) {
body.push(deindent`
set ${x.export_name}(${x.name}) {
this.$set({ ${x.name === x.export_name ? x.name : `${x.export_name}: ${x.name}`} });
if (variable.writable && !renderer.readonly.has(prop.name)) {
accessors.push({
type: 'MethodDefinition',
kind: 'set',
key: { type: 'Identifier', name: prop.export_name },
value: x`function(${prop.name}) {
this.$set({ ${prop.export_name}: ${prop.name} });
@flush();
}
`);
}`
});
} else if (component.compile_options.dev) {
body.push(deindent`
set ${x.export_name}(value) {
throw new @_Error("<${component.tag}>: Cannot set read-only property '${x.export_name}'");
}
`);
accessors.push({
type: 'MethodDefinition',
kind: 'set',
key: { type: 'Identifier', name: prop.export_name },
value: x`function(value) {
throw new @_Error("<${component.tag}>: Cannot set read-only property '${prop.export_name}'");
}`
});
}
} else if (component.compile_options.dev) {
body.push(deindent`
set ${x.export_name}(value) {
accessors.push({
type: 'MethodDefinition',
kind: 'set',
key: { type: 'Identifier', name: prop.export_name },
value: x`function(value) {
throw new @_Error("<${component.tag}>: Props cannot be set directly on the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");
}
`);
}`
});
}
});
@ -141,35 +157,35 @@ export default function dom(
const expected = props.filter(prop => !prop.initialised);
if (expected.length) {
dev_props_check = deindent`
const { ctx } = this.$$;
const props = ${options.customElement ? `this.attributes` : `options.props || {}`};
${expected.map(prop => deindent`
if (ctx.${prop.name} === undefined && !('${prop.export_name}' in props)) {
dev_props_check = b`
const { ctx: #ctx } = this.$$;
const props = ${options.customElement ? x`this.attributes` : x`options.props || {}`};
${expected.map(prop => b`
if (#ctx.${prop.name} === undefined && !('${prop.export_name}' in props)) {
@_console.warn("<${component.tag}> was created without expected prop '${prop.export_name}'");
}`)}
`;
}
capture_state = (uses_props || writable_props.length > 0) ? deindent`
capture_state = (uses_props || writable_props.length > 0) ? x`
() => {
return { ${component.vars.filter(prop => prop.writable).map(prop => prop.name).join(", ")} };
return { ${component.vars.filter(prop => prop.writable).map(prop => p`${prop.name}`)} };
}
` : deindent`
` : x`
() => {
return {};
}
`;
const writable_vars = component.vars.filter(variable => !variable.module && variable.writable);
inject_state = (uses_props || writable_vars.length > 0) ? deindent`
inject_state = (uses_props || writable_vars.length > 0) ? x`
${$$props} => {
${uses_props && component.invalidate('$$props', `$$props = @assign(@assign({}, $$props), $$new_props)`)}
${writable_vars.map(prop => deindent`
if ('${prop.name}' in $$props) ${component.invalidate(prop.name, `${prop.name} = ${$$props}.${prop.name}`)};
${uses_props && component.invalidate('$$props', x`$$props = @assign(@assign({}, $$props), $$new_props)`)}
${writable_vars.map(prop => b`
if ('${prop.name}' in $$props) ${component.invalidate(prop.name, x`${prop.name} = ${$$props}.${prop.name}`)};
`)}
}
` : deindent`
` : x`
${$$props} => {}
`;
}
@ -200,7 +216,7 @@ export default function dom(
// onto the initial function call
const names = new Set(extract_names(assignee));
invalidate(component, scope, code, node, names);
this.replace(invalidate(component, scope, node, names));
}
}
});
@ -208,44 +224,41 @@ export default function dom(
component.rewrite_props(({ name, reassigned }) => {
const value = `$${name}`;
const callback = `$value => { ${value} = $$value; $$invalidate('${value}', ${value}) }`;
if (reassigned) {
return `$$subscribe_${name}()`;
return b`${`$$subscribe_${name}`}()`;
}
const component_subscribe = component.helper('component_subscribe');
const callback = x`$$value => { $$invalidate('${value}', ${value} = $$value) }`;
let insert = `${component_subscribe}($$self, ${name}, $${callback})`;
let insert = b`@component_subscribe($$self, ${name}, $${callback})`;
if (component.compile_options.dev) {
const validate_store = component.helper('validate_store');
insert = `${validate_store}(${name}, '${name}'); ${insert}`;
insert = b`@validate_store(${name}, '${name}'); ${insert}`;
}
return insert;
});
}
const args = ['$$self'];
const args = [x`$$self`];
if (props.length > 0 || component.has_reactive_assignments || component.slots.size > 0) {
args.push('$$props', '$$invalidate');
args.push(x`$$props`, x`$$invalidate`);
}
builder.add_block(deindent`
function create_fragment(ctx) {
body.push(b`
function create_fragment(#ctx) {
${block.get_contents()}
}
${component.module_javascript}
${component.extract_javascript(component.ast.module)}
${component.fully_hoisted.length > 0 && component.fully_hoisted.join('\n\n')}
${component.fully_hoisted}
`);
const filtered_declarations = component.vars
.filter(v => ((v.referenced || v.export_name) && !v.hoistable))
.map(v => v.name);
.map(v => p`${v.name}`);
if (uses_props) filtered_declarations.push(`$$props: $$props = ${component.helper('exclude_internal_props')}($$props)`);
if (uses_props) filtered_declarations.push(p`$$props: $$props = @exclude_internal_props($$props)`);
const filtered_props = props.filter(prop => {
const variable = component.var_lookup.get(prop.name);
@ -258,15 +271,17 @@ export default function dom(
const reactive_stores = component.vars.filter(variable => variable.name[0] === '$' && variable.name[1] !== '$');
if (component.slots.size > 0) {
filtered_declarations.push('$$slots', '$$scope');
filtered_declarations.push(p`$$slots`, p`$$scope`);
}
if (renderer.binding_groups.length > 0) {
filtered_declarations.push(`$$binding_groups`);
filtered_declarations.push(p`$$binding_groups`);
}
const instance_javascript = component.extract_javascript(component.ast.instance);
const has_definition = (
component.javascript ||
(instance_javascript && instance_javascript.length > 0) ||
filtered_props.length > 0 ||
uses_props ||
component.partly_hoisted.length > 0 ||
@ -276,7 +291,7 @@ export default function dom(
const definition = has_definition
? component.alias('instance')
: 'null';
: { type: 'Literal', value: null };
const all_reactive_dependencies = new Set();
component.reactive_declarations.forEach(d => {
@ -288,7 +303,7 @@ export default function dom(
const variable = component.var_lookup.get(store.name.slice(1));
return !variable || variable.hoistable;
})
.map(({ name }) => deindent`
.map(({ name }) => b`
${component.compile_options.dev && `@validate_store(${name.slice(1)}, '${name.slice(1)}');`}
@component_subscribe($$self, ${name.slice(1)}, $$value => { ${name} = $$value; $$invalidate('${name}', ${name}); });
`);
@ -298,34 +313,36 @@ export default function dom(
const variable = component.var_lookup.get(store.name.slice(1));
return variable && variable.reassigned;
})
.map(({ name }) => `$$self.$$.on_destroy.push(() => $$unsubscribe_${name.slice(1)}());`);
.map(({ name }) => b`$$self.$$.on_destroy.push(() => ${`$$unsubscribe_${name.slice(1)}`}());`);
if (has_definition) {
const reactive_declarations = [];
const reactive_declarations: (Node | Node[]) = [];
const fixed_reactive_declarations = []; // not really 'reactive' but whatever
component.reactive_declarations
.forEach(d => {
const dependencies = Array.from(d.dependencies);
const uses_props = !!dependencies.find(n => n === '$$props');
const condition = !uses_props && dependencies
.filter(n => {
const variable = component.var_lookup.get(n);
return variable && (variable.writable || variable.mutated);
})
.map(n => `$$dirty.${n}`).join(' || ');
let snippet = `[✂${d.node.body.start}-${d.node.end}✂]`;
if (condition) snippet = `if (${condition}) { ${snippet} }`;
if (condition || uses_props) {
reactive_declarations.push(snippet);
} else {
fixed_reactive_declarations.push(snippet);
}
component.reactive_declarations.forEach(d => {
const dependencies = Array.from(d.dependencies);
const uses_props = !!dependencies.find(n => n === '$$props');
const writable = dependencies.filter(n => {
const variable = component.var_lookup.get(n);
return variable && (variable.writable || variable.mutated);
});
const condition = !uses_props && writable.length > 0 && (writable
.map(n => x`#changed.${n}`)
.reduce((lhs, rhs) => x`${lhs} || ${rhs}`));
let statement = d.node; // TODO remove label (use d.node.body) if it's not referenced
if (condition) statement = b`if (${condition}) { ${statement} }`[0] as Statement;
if (condition || uses_props) {
reactive_declarations.push(statement);
} else {
fixed_reactive_declarations.push(statement);
}
});
const injected = Array.from(component.injected_reactive_declaration_vars).filter(name => {
const variable = component.var_lookup.get(name);
return variable.injected && variable.name[0] !== '$';
@ -337,70 +354,94 @@ export default function dom(
const store = component.var_lookup.get(name);
if (store && store.reassigned) {
return `${$name}, $$unsubscribe_${name} = @noop, $$subscribe_${name} = () => ($$unsubscribe_${name}(), $$unsubscribe_${name} = @subscribe(${name}, $$value => { ${$name} = $$value; $$invalidate('${$name}', ${$name}); }), ${name})`;
const unsubscribe = `$$unsubscribe_${name}`;
const subscribe = `$$subscribe_${name}`;
return b`let ${$name}, ${unsubscribe} = @noop, ${subscribe} = () => (${unsubscribe}(), ${unsubscribe} = @subscribe(${name}, $$value => { ${$name} = $$value; $$invalidate('${$name}', ${$name}); }), ${name})`;
}
return $name;
return b`let ${$name};`;
});
let unknown_props_check;
if (component.compile_options.dev && !component.var_lookup.has('$$props') && writable_props.length) {
unknown_props_check = deindent`
const writable_props = [${writable_props.map(prop => `'${prop.export_name}'`).join(', ')}];
unknown_props_check = b`
const writable_props = [${writable_props.map(prop => x`'${prop.export_name}'`)}];
@_Object.keys($$props).forEach(key => {
if (!writable_props.includes(key) && !key.startsWith('$$')) @_console.warn(\`<${component.tag}> was created with unknown prop '\${key}'\`);
});
`;
}
builder.add_block(deindent`
function ${definition}(${args.join(', ')}) {
${reactive_store_declarations.length > 0 && `let ${reactive_store_declarations.join(', ')};`}
const return_value = {
type: 'ObjectExpression',
properties: filtered_declarations
};
const reactive_dependencies = {
type: 'ObjectPattern',
properties: Array.from(all_reactive_dependencies).map(name => {
return {
type: 'Property',
kind: 'init',
key: { type: 'Identifier', name },
value: { type: 'Literal', value: 1 }
};
})
};
body.push(b`
function ${definition}(${args}) {
${reactive_store_declarations}
${reactive_store_subscriptions}
${resubscribable_reactive_store_unsubscribers}
${component.javascript}
${instance_javascript}
${unknown_props_check}
${component.slots.size && `let { $$slots = {}, $$scope } = $$props;`}
${component.slots.size ? b`let { $$slots = {}, $$scope } = $$props;` : null}
${renderer.binding_groups.length > 0 && `const $$binding_groups = [${renderer.binding_groups.map(_ => `[]`).join(', ')}];`}
${renderer.binding_groups.length > 0 && b`const $$binding_groups = [${renderer.binding_groups.map(_ => x`[]`)}];`}
${component.partly_hoisted.length > 0 && component.partly_hoisted.join('\n\n')}
${component.partly_hoisted}
${set && `$$self.$set = ${set};`}
${set && b`$$self.$set = ${set};`}
${capture_state && `$$self.$capture_state = ${capture_state};`}
${capture_state && x`$$self.$capture_state = ${capture_state};`}
${inject_state && `$$self.$inject_state = ${inject_state};`}
${inject_state && x`$$self.$inject_state = ${inject_state};`}
${injected.length && `let ${injected.join(', ')};`}
${injected.map(name => b`let ${name};`)}
${reactive_declarations.length > 0 && deindent`
$$self.$$.update = ($$dirty = { ${Array.from(all_reactive_dependencies).map(n => `${n}: 1`).join(', ')} }) => {
${reactive_declarations.length > 0 && b`
$$self.$$.update = (#changed = ${reactive_dependencies}) => {
${reactive_declarations}
};
`}
${fixed_reactive_declarations}
return ${stringify_props(filtered_declarations)};
return ${return_value};
}
`);
}
const prop_names = `[${props.map(v => JSON.stringify(v.export_name)).join(', ')}]`;
const prop_names = x`[]`;
// TODO find a more idiomatic way of doing this
props.forEach(v => {
(prop_names as any).elements.push({ type: 'Literal', value: v.export_name });
});
if (options.customElement) {
builder.add_block(deindent`
const declaration = b`
class ${name} extends @SvelteElement {
constructor(options) {
super();
${css.code && `this.shadowRoot.innerHTML = \`<style>${escape(css.code, { only_escape_at_symbol: true }).replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`}
${css.code && b`this.shadowRoot.innerHTML = \`<style>${css.code.replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`}
@init(this, { target: this.shadowRoot }, ${definition}, create_fragment, ${not_equal}, ${prop_names});
@ -411,46 +452,74 @@ export default function dom(
@insert(options.target, this, options.anchor);
}
${(props.length > 0 || uses_props) && deindent`
${(props.length > 0 || uses_props) && b`
if (options.props) {
this.$set(options.props);
@flush();
}`}
}
}
}
`[0] as ClassDeclaration;
if (props.length > 0) {
declaration.body.body.push({
type: 'MethodDefinition',
kind: 'get',
static: true,
computed: false,
key: { type: 'Identifier', name: 'observedAttributes' },
value: x`function() {
return [${props.map(prop => x`"${prop.export_name}"`)}];
}` as FunctionExpression
});
}
${props.length > 0 && deindent`
static get observedAttributes() {
return ${JSON.stringify(props.map(x => x.export_name))};
}`}
declaration.body.body.push(...accessors);
${body.length > 0 && body.join('\n\n')}
}
`);
body.push(declaration);
if (component.tag != null) {
builder.add_block(deindent`
body.push(b`
@_customElements.define("${component.tag}", ${name});
`);
}
} else {
const superclass = options.dev ? 'SvelteComponentDev' : 'SvelteComponent';
const superclass = {
type: 'Identifier',
name: options.dev ? '@SvelteComponentDev' : '@SvelteComponent'
};
builder.add_block(deindent`
class ${name} extends @${superclass} {
const declaration = b`
class ${name} extends ${superclass} {
constructor(options) {
super(${options.dev && `options`});
${should_add_css && `if (!@_document.getElementById("${component.stylesheet.id}-style")) ${add_css}();`}
${should_add_css && b`if (!@_document.getElementById("${component.stylesheet.id}-style")) ${add_css}();`}
@init(this, options, ${definition}, create_fragment, ${not_equal}, ${prop_names});
${options.dev && `@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name}", options, id: create_fragment.name });`}
${options.dev && b`@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name.name}", options, id: create_fragment.name });`}
${dev_props_check}
}
${body.length > 0 && body.join('\n\n')}
}
`);
`[0] as ClassDeclaration;
declaration.body.body.push(...accessors);
body.push(declaration);
}
return flatten(body, []);
}
function flatten(nodes: any[], target: any[]) {
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i];
if (Array.isArray(node)) {
flatten(node, target);
} else {
target.push(node);
}
}
return builder.toString();
return target;
}

@ -3,11 +3,13 @@ import Renderer from '../Renderer';
import Block from '../Block';
import AwaitBlock from '../../nodes/AwaitBlock';
import create_debugging_comment from './shared/create_debugging_comment';
import deindent from '../../utils/deindent';
import { b, x } from 'code-red';
import FragmentWrapper from './Fragment';
import PendingBlock from '../../nodes/PendingBlock';
import ThenBlock from '../../nodes/ThenBlock';
import CatchBlock from '../../nodes/CatchBlock';
import { changed } from './shared/changed';
import { Identifier } from 'estree';
class AwaitBlockBranch extends Wrapper {
node: PendingBlock | ThenBlock | CatchBlock;
@ -54,7 +56,7 @@ export default class AwaitBlockWrapper extends Wrapper {
then: AwaitBlockBranch;
catch: AwaitBlockBranch;
var = 'await_block';
var: Identifier = { type: 'Identifier', name: 'await_block' };
constructor(
renderer: Renderer,
@ -120,13 +122,13 @@ export default class AwaitBlockWrapper extends Wrapper {
render(
block: Block,
parent_node: string,
parent_nodes: string
parent_node: Identifier,
parent_nodes: Identifier
) {
const anchor = this.get_or_create_anchor(block, parent_node, parent_nodes);
const update_mount_node = this.get_update_mount_node(anchor);
const snippet = this.node.expression.render(block);
const snippet = this.node.expression.manipulate(block);
const info = block.get_unique_name(`info`);
const promise = block.get_unique_name(`promise`);
@ -135,34 +137,32 @@ export default class AwaitBlockWrapper extends Wrapper {
block.maintain_context = true;
const info_props = [
'ctx',
'current: null',
'token: null',
this.pending.block.name && `pending: ${this.pending.block.name}`,
this.then.block.name && `then: ${this.then.block.name}`,
this.catch.block.name && `catch: ${this.catch.block.name}`,
this.then.block.name && `value: '${this.node.value}'`,
this.catch.block.name && `error: '${this.node.error}'`,
this.pending.block.has_outro_method && `blocks: [,,,]`
].filter(Boolean);
block.builders.init.add_block(deindent`
let ${info} = {
${info_props.join(',\n')}
};
const info_props: any = x`{
ctx: #ctx,
current: null,
token: null,
pending: ${this.pending.block.name},
then: ${this.then.block.name},
catch: ${this.catch.block.name},
value: ${this.then.block.name && x`"${this.node.value}"`},
error: ${this.catch.block.name && x`"${this.node.error}"`},
blocks: ${this.pending.block.has_outro_method && x`[,,,]`}
}`;
block.chunks.init.push(b`
let ${info} = ${info_props};
`);
block.builders.init.add_block(deindent`
block.chunks.init.push(b`
@handle_promise(${promise} = ${snippet}, ${info});
`);
block.builders.create.add_block(deindent`
block.chunks.create.push(b`
${info}.block.c();
`);
if (parent_nodes && this.renderer.options.hydratable) {
block.builders.claim.add_block(deindent`
block.chunks.claim.push(b`
${info}.block.l(${parent_nodes});
`);
}
@ -172,56 +172,51 @@ export default class AwaitBlockWrapper extends Wrapper {
const has_transitions = this.pending.block.has_intro_method || this.pending.block.has_outro_method;
block.builders.mount.add_block(deindent`
block.chunks.mount.push(b`
${info}.block.m(${initial_mount_node}, ${info}.anchor = ${anchor_node});
${info}.mount = () => ${update_mount_node};
${info}.anchor = ${anchor};
`);
if (has_transitions) {
block.builders.intro.add_line(`@transition_in(${info}.block);`);
block.chunks.intro.push(b`@transition_in(${info}.block);`);
}
const conditions = [];
const dependencies = this.node.expression.dynamic_dependencies();
if (dependencies.length > 0) {
conditions.push(
`(${dependencies.map(dep => `'${dep}' in changed`).join(' || ')})`
);
conditions.push(
`${promise} !== (${promise} = ${snippet})`,
`@handle_promise(${promise}, ${info})`
);
const condition = x`
${changed(dependencies)} &&
${promise} !== (${promise} = ${snippet}) &&
@handle_promise(${promise}, ${info})`;
block.builders.update.add_line(
`${info}.ctx = ctx;`
block.chunks.update.push(
b`${info}.ctx = #ctx;`
);
if (this.pending.block.has_update_method) {
block.builders.update.add_block(deindent`
if (${conditions.join(' && ')}) {
block.chunks.update.push(b`
if (${condition}) {
// nothing
} else {
${info}.block.p(changed, @assign(@assign({}, ctx), ${info}.resolved));
${info}.block.p(#changed, @assign(@assign({}, #ctx), ${info}.resolved));
}
`);
} else {
block.builders.update.add_block(deindent`
${conditions.join(' && ')}
block.chunks.update.push(b`
${condition}
`);
}
} else {
if (this.pending.block.has_update_method) {
block.builders.update.add_block(deindent`
${info}.block.p(changed, @assign(@assign({}, ctx), ${info}.resolved));
block.chunks.update.push(b`
${info}.block.p(#changed, @assign(@assign({}, #ctx), ${info}.resolved));
`);
}
}
if (this.pending.block.has_outro_method) {
block.builders.outro.add_block(deindent`
block.chunks.outro.push(b`
for (let #i = 0; #i < 3; #i += 1) {
const block = ${info}.blocks[#i];
@transition_out(block);
@ -229,14 +224,14 @@ export default class AwaitBlockWrapper extends Wrapper {
`);
}
block.builders.destroy.add_block(deindent`
${info}.block.d(${parent_node ? '' : 'detaching'});
block.chunks.destroy.push(b`
${info}.block.d(${parent_node ? null : 'detaching'});
${info}.token = null;
${info} = null;
`);
[this.pending, this.then, this.catch].forEach(branch => {
branch.fragment.render(branch.block, null, 'nodes');
branch.fragment.render(branch.block, null, x`#nodes` as Identifier);
});
}
}

@ -1,20 +1,21 @@
import Block from '../Block';
import Wrapper from './shared/Wrapper';
import deindent from '../../utils/deindent';
import { b } from 'code-red';
import Body from '../../nodes/Body';
import { Identifier } from 'estree';
export default class BodyWrapper extends Wrapper {
node: Body;
render(block: Block, _parent_node: string, _parent_nodes: string) {
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
this.node.handlers.forEach(handler => {
const snippet = handler.render(block);
block.builders.init.add_block(deindent`
block.chunks.init.push(b`
@_document.body.addEventListener("${handler.name}", ${snippet});
`);
block.builders.destroy.add_block(deindent`
block.chunks.destroy.push(b`
@_document.body.removeEventListener("${handler.name}", ${snippet});
`);
});

@ -3,7 +3,9 @@ import Wrapper from './shared/Wrapper';
import Block from '../Block';
import DebugTag from '../../nodes/DebugTag';
import add_to_set from '../../utils/add_to_set';
import deindent from '../../utils/deindent';
import { b, p } from 'code-red';
import { Identifier, DebuggerStatement } from 'estree';
import { changed } from './shared/changed';
export default class DebugTagWrapper extends Wrapper {
node: DebugTag;
@ -19,59 +21,65 @@ export default class DebugTagWrapper extends Wrapper {
super(renderer, block, parent, node);
}
render(block: Block, _parent_node: string, _parent_nodes: string) {
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
const { renderer } = this;
const { component } = renderer;
if (!renderer.options.dev) return;
const { code, var_lookup } = component;
const { var_lookup } = component;
const start = component.locate(this.node.start + 1);
start.line += 1;
const end = { line: start.line, column: start.column + 6 };
const loc = { start, end };
const debug: DebuggerStatement = {
type: 'DebuggerStatement',
loc
};
if (this.node.expressions.length === 0) {
// Debug all
code.overwrite(this.node.start + 1, this.node.start + 7, 'debugger', {
storeName: true
});
const statement = `[✂${this.node.start + 1}-${this.node.start + 7}✂];`;
block.builders.create.add_line(statement);
block.builders.update.add_line(statement);
block.chunks.create.push(debug);
block.chunks.update.push(debug);
} else {
const { code } = component;
code.overwrite(this.node.start + 1, this.node.start + 7, 'log', {
storeName: true
});
const log = `[✂${this.node.start + 1}-${this.node.start + 7}✂]`;
const log: Identifier = {
type: 'Identifier',
name: 'log',
loc
};
const dependencies = new Set();
const dependencies: Set<string> = new Set();
this.node.expressions.forEach(expression => {
add_to_set(dependencies, expression.dependencies);
});
const condition = Array.from(dependencies).map(d => `changed.${d}`).join(' || ');
const condition = changed(Array.from(dependencies));
const ctx_identifiers = this.node.expressions
const contextual_identifiers = this.node.expressions
.filter(e => {
const looked_up_var = var_lookup.get(e.node.name);
return !(looked_up_var && looked_up_var.hoistable);
const variable = var_lookup.get(e.node.name);
return !(variable && variable.hoistable);
})
.map(e => e.node.name)
.join(', ');
const logged_identifiers = this.node.expressions.map(e => e.node.name).join(', ');
.map(e => p`${e.node.name}`);
const logged_identifiers = this.node.expressions.map(e => p`${e.node.name}`);
block.builders.update.add_block(deindent`
block.chunks.update.push(b`
if (${condition}) {
const { ${ctx_identifiers} } = ctx;
const { ${contextual_identifiers} } = #ctx;
@_console.${log}({ ${logged_identifiers} });
debugger;
${debug};
}
`);
block.builders.create.add_block(deindent`
block.chunks.create.push(b`
{
const { ${ctx_identifiers} } = ctx;
const { ${contextual_identifiers} } = #ctx;
@_console.${log}({ ${logged_identifiers} });
debugger;
${debug};
}
`);
}

@ -4,9 +4,10 @@ import Wrapper from './shared/Wrapper';
import create_debugging_comment from './shared/create_debugging_comment';
import EachBlock from '../../nodes/EachBlock';
import FragmentWrapper from './Fragment';
import deindent from '../../utils/deindent';
import { b, x } from 'code-red';
import ElseBlock from '../../nodes/ElseBlock';
import { attach_head } from '../../utils/tail';
import { Identifier, Node } from 'estree';
import { changed } from './shared/changed';
export class ElseBlockWrapper extends Wrapper {
node: ElseBlock;
@ -51,20 +52,19 @@ export default class EachBlockWrapper extends Wrapper {
fragment: FragmentWrapper;
else?: ElseBlockWrapper;
vars: {
create_each_block: string;
each_block_value: string;
get_each_context: string;
iterations: string;
create_each_block: Identifier;
each_block_value: Identifier;
get_each_context: Identifier;
iterations: Identifier;
fixed_length: number;
data_length: string;
view_length: string;
length: string;
}
context_props: string[];
index_name: string;
context_props: Array<Node | Node[]>;
index_name: Identifier;
var = 'each';
var: Identifier = { type: 'Identifier', name: 'each' };
constructor(
renderer: Renderer,
@ -93,7 +93,9 @@ export default class EachBlockWrapper extends Wrapper {
// TODO this seems messy
this.block.has_animation = this.node.has_animation;
this.index_name = this.node.index || renderer.component.get_unique_name(`${this.node.context}_index`);
this.index_name = this.node.index
? { type: 'Identifier', name: this.node.index }
: renderer.component.get_unique_name(`${this.node.context}_index`);
const fixed_length =
node.expression.node.type === 'ArrayExpression' &&
@ -105,26 +107,32 @@ export default class EachBlockWrapper extends Wrapper {
// is easy to find
let c = this.node.start + 2;
while (renderer.component.source[c] !== 'e') c += 1;
renderer.component.code.overwrite(c, c + 4, 'length');
const start = renderer.component.locate(c);
start.line += 1;
const end = { line: start.line, column: start.column + 4 };
const length = {
type: 'Identifier',
name: 'length',
loc: { start, end }
};
const each_block_value = renderer.component.get_unique_name(`${this.var}_value`);
const iterations = block.get_unique_name(`${this.var}_blocks`);
const each_block_value = renderer.component.get_unique_name(`${this.var.name}_value`);
const iterations = block.get_unique_name(`${this.var.name}_blocks`);
this.vars = {
create_each_block: this.block.name,
each_block_value,
get_each_context: renderer.component.get_unique_name(`get_${this.var}_context`),
get_each_context: renderer.component.get_unique_name(`get_${this.var.name}_context`),
iterations,
length: `[✂${c}-${c+4}✂]`,
// optimisation for array literal
fixed_length,
data_length: fixed_length === null ? `${each_block_value}.[✂${c}-${c+4}✂]` : fixed_length,
view_length: fixed_length === null ? `${iterations}.[✂${c}-${c+4}✂]` : fixed_length
data_length: fixed_length === null ? x`${each_block_value}.${length}` : fixed_length,
view_length: fixed_length === null ? x`${iterations}.length` : fixed_length
};
const store =
node.expression.node.type === 'Identifier' &&
const store =
node.expression.node.type === 'Identifier' &&
node.expression.node.name[0] === '$'
? node.expression.node.name.slice(1)
: null;
@ -133,9 +141,10 @@ export default class EachBlockWrapper extends Wrapper {
this.block.bindings.set(prop.key.name, {
object: this.vars.each_block_value,
property: this.index_name,
snippet: attach_head(`${this.vars.each_block_value}[${this.index_name}]`, prop.tail),
modifier: prop.modifier,
snippet: prop.modifier(x`${this.vars.each_block_value}[${this.index_name}]` as Node),
store,
tail: attach_head(`[${this.index_name}]`, prop.tail)
tail: prop.modifier(x`[${this.index_name}]` as Node)
});
});
@ -171,7 +180,7 @@ export default class EachBlockWrapper extends Wrapper {
}
}
render(block: Block, parent_node: string, parent_nodes: string) {
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
if (this.fragment.nodes.length === 0) return;
const { renderer } = this;
@ -181,29 +190,29 @@ export default class EachBlockWrapper extends Wrapper {
? !this.next.is_dom_node() :
!parent_node || !this.parent.is_dom_node();
this.context_props = this.node.contexts.map(prop => `child_ctx.${prop.key.name} = ${attach_head('list[i]', prop.tail)};`);
this.context_props = this.node.contexts.map(prop => b`child_ctx.${prop.key.name} = ${prop.modifier(x`list[i]`)};`);
if (this.node.has_binding) this.context_props.push(`child_ctx.${this.vars.each_block_value} = list;`);
if (this.node.has_binding || this.node.index) this.context_props.push(`child_ctx.${this.index_name} = i;`);
if (this.node.has_binding) this.context_props.push(b`child_ctx.${this.vars.each_block_value} = list;`);
if (this.node.has_binding || this.node.index) this.context_props.push(b`child_ctx.${this.index_name} = i;`);
const snippet = this.node.expression.render(block);
const snippet = this.node.expression.manipulate(block);
block.builders.init.add_line(`let ${this.vars.each_block_value} = ${snippet};`);
block.chunks.init.push(b`let ${this.vars.each_block_value} = ${snippet};`);
renderer.blocks.push(deindent`
function ${this.vars.get_each_context}(ctx, list, i) {
const child_ctx = @_Object.create(ctx);
renderer.blocks.push(b`
function ${this.vars.get_each_context}(#ctx, list, i) {
const child_ctx = @_Object.create(#ctx);
${this.context_props}
return child_ctx;
}
`);
const initial_anchor_node = parent_node ? 'null' : 'anchor';
const initial_mount_node = parent_node || '#target';
const initial_anchor_node: Identifier = { type: 'Identifier', name: parent_node ? 'null' : 'anchor' };
const initial_mount_node: Identifier = parent_node || { type: 'Identifier', name: '#target' };
const update_anchor_node = needs_anchor
? block.get_unique_name(`${this.var}_anchor`)
: (this.next && this.next.var) || 'null';
const update_mount_node = this.get_update_mount_node(update_anchor_node);
? block.get_unique_name(`${this.var.name}_anchor`)
: (this.next && this.next.var) || { type: 'Identifier', name: 'null' };
const update_mount_node: Identifier = this.get_update_mount_node((update_anchor_node as Identifier));
const args = {
block,
@ -223,7 +232,7 @@ export default class EachBlockWrapper extends Wrapper {
}
if (this.block.has_intro_method || this.block.has_outro_method) {
block.builders.intro.add_block(deindent`
block.chunks.intro.push(b`
for (let #i = 0; #i < ${this.vars.data_length}; #i += 1) {
@transition_in(${this.vars.iterations}[#i]);
}
@ -232,38 +241,38 @@ export default class EachBlockWrapper extends Wrapper {
if (needs_anchor) {
block.add_element(
update_anchor_node,
`@empty()`,
parent_nodes && `@empty()`,
update_anchor_node as Identifier,
x`@empty()`,
parent_nodes && x`@empty()`,
parent_node
);
}
if (this.else) {
const each_block_else = component.get_unique_name(`${this.var}_else`);
const each_block_else = component.get_unique_name(`${this.var.name}_else`);
block.builders.init.add_line(`let ${each_block_else} = null;`);
block.chunks.init.push(b`let ${each_block_else} = null;`);
// TODO neaten this up... will end up with an empty line in the block
block.builders.init.add_block(deindent`
block.chunks.init.push(b`
if (!${this.vars.data_length}) {
${each_block_else} = ${this.else.block.name}(ctx);
${each_block_else} = ${this.else.block.name}(#ctx);
${each_block_else}.c();
}
`);
block.builders.mount.add_block(deindent`
block.chunks.mount.push(b`
if (${each_block_else}) {
${each_block_else}.m(${initial_mount_node}, ${initial_anchor_node});
}
`);
if (this.else.block.has_update_method) {
block.builders.update.add_block(deindent`
block.chunks.update.push(b`
if (!${this.vars.data_length} && ${each_block_else}) {
${each_block_else}.p(changed, ctx);
${each_block_else}.p(#changed, #ctx);
} else if (!${this.vars.data_length}) {
${each_block_else} = ${this.else.block.name}(ctx);
${each_block_else} = ${this.else.block.name}(#ctx);
${each_block_else}.c();
${each_block_else}.m(${update_mount_node}, ${update_anchor_node});
} else if (${each_block_else}) {
@ -272,29 +281,29 @@ export default class EachBlockWrapper extends Wrapper {
}
`);
} else {
block.builders.update.add_block(deindent`
block.chunks.update.push(b`
if (${this.vars.data_length}) {
if (${each_block_else}) {
${each_block_else}.d(1);
${each_block_else} = null;
}
} else if (!${each_block_else}) {
${each_block_else} = ${this.else.block.name}(ctx);
${each_block_else} = ${this.else.block.name}(#ctx);
${each_block_else}.c();
${each_block_else}.m(${update_mount_node}, ${update_anchor_node});
}
`);
}
block.builders.destroy.add_block(deindent`
block.chunks.destroy.push(b`
if (${each_block_else}) ${each_block_else}.d(${parent_node ? '' : 'detaching'});
`);
}
this.fragment.render(this.block, null, 'nodes');
this.fragment.render(this.block, null, x`#nodes` as Identifier);
if (this.else) {
this.else.fragment.render(this.else.block, null, 'nodes');
this.else.fragment.render(this.else.block, null, x`#nodes` as Identifier);
}
}
@ -309,26 +318,26 @@ export default class EachBlockWrapper extends Wrapper {
update_mount_node
}: {
block: Block;
parent_node: string;
parent_nodes: string;
snippet: string;
initial_anchor_node: string;
initial_mount_node: string;
update_anchor_node: string;
update_mount_node: string;
parent_node: Identifier;
parent_nodes: Identifier;
snippet: Node;
initial_anchor_node: Identifier;
initial_mount_node: Identifier;
update_anchor_node: Identifier;
update_mount_node: Identifier;
}) {
const {
create_each_block,
length,
iterations,
data_length,
view_length
} = this.vars;
const get_key = block.get_unique_name('get_key');
const lookup = block.get_unique_name(`${this.var}_lookup`);
const lookup = block.get_unique_name(`${this.var.name}_lookup`);
block.add_variable(iterations, '[]');
block.add_variable(lookup, `new @_Map()`);
block.add_variable(iterations, x`[]`);
block.add_variable(lookup, x`new @_Map()`);
if (this.fragment.nodes[0].is_dom_node()) {
this.block.first = this.fragment.nodes[0].var;
@ -336,39 +345,37 @@ export default class EachBlockWrapper extends Wrapper {
this.block.first = this.block.get_unique_name('first');
this.block.add_element(
this.block.first,
`@empty()`,
parent_nodes && `@empty()`,
x`@empty()`,
parent_nodes && x`@empty()`,
null
);
}
block.builders.init.add_block(deindent`
const ${get_key} = ctx => ${
// @ts-ignore todo: probably error
this.node.key.render()};
block.chunks.init.push(b`
const ${get_key} = #ctx => ${this.node.key.manipulate(block)};
for (let #i = 0; #i < ${this.vars.each_block_value}.${length}; #i += 1) {
let child_ctx = ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i);
for (let #i = 0; #i < ${data_length}; #i += 1) {
let child_ctx = ${this.vars.get_each_context}(#ctx, ${this.vars.each_block_value}, #i);
let key = ${get_key}(child_ctx);
${lookup}.set(key, ${iterations}[#i] = ${create_each_block}(key, child_ctx));
}
`);
block.builders.create.add_block(deindent`
block.chunks.create.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) {
${iterations}[#i].c();
}
`);
if (parent_nodes && this.renderer.options.hydratable) {
block.builders.claim.add_block(deindent`
block.chunks.claim.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) {
${iterations}[#i].l(${parent_nodes});
}
`);
}
block.builders.mount.add_block(deindent`
block.chunks.mount.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) {
${iterations}[#i].m(${initial_mount_node}, ${initial_anchor_node});
}
@ -384,27 +391,27 @@ export default class EachBlockWrapper extends Wrapper {
? `@outro_and_destroy_block`
: `@destroy_block`;
block.builders.update.add_block(deindent`
block.chunks.update.push(b`
const ${this.vars.each_block_value} = ${snippet};
${this.block.has_outros && `@group_outros();`}
${this.node.has_animation && `for (let #i = 0; #i < ${view_length}; #i += 1) ${iterations}[#i].r();`}
${iterations} = @update_keyed_each(${iterations}, changed, ${get_key}, ${dynamic ? '1' : '0'}, ctx, ${this.vars.each_block_value}, ${lookup}, ${update_mount_node}, ${destroy}, ${create_each_block}, ${update_anchor_node}, ${this.vars.get_each_context});
${this.node.has_animation && `for (let #i = 0; #i < ${view_length}; #i += 1) ${iterations}[#i].a();`}
${this.block.has_outros && `@check_outros();`}
${this.block.has_outros && b`@group_outros();`}
${this.node.has_animation && b`for (let #i = 0; #i < ${view_length}; #i += 1) ${iterations}[#i].r();`}
${iterations} = @update_keyed_each(${iterations}, #changed, ${get_key}, ${dynamic ? 1 : 0}, #ctx, ${this.vars.each_block_value}, ${lookup}, ${update_mount_node}, ${destroy}, ${create_each_block}, ${update_anchor_node}, ${this.vars.get_each_context});
${this.node.has_animation && b`for (let #i = 0; #i < ${view_length}; #i += 1) ${iterations}[#i].a();`}
${this.block.has_outros && b`@check_outros();`}
`);
if (this.block.has_outros) {
block.builders.outro.add_block(deindent`
block.chunks.outro.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) {
@transition_out(${iterations}[#i]);
}
`);
}
block.builders.destroy.add_block(deindent`
block.chunks.destroy.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) {
${iterations}[#i].d(${parent_node ? '' : 'detaching'});
${iterations}[#i].d(${parent_node ? null : 'detaching'});
}
`);
}
@ -419,45 +426,44 @@ export default class EachBlockWrapper extends Wrapper {
update_mount_node
}: {
block: Block;
parent_nodes: string;
snippet: string;
initial_anchor_node: string;
initial_mount_node: string;
update_anchor_node: string;
update_mount_node: string;
parent_nodes: Identifier;
snippet: Node;
initial_anchor_node: Identifier;
initial_mount_node: Identifier;
update_anchor_node: Identifier;
update_mount_node: Identifier;
}) {
const {
create_each_block,
length,
iterations,
fixed_length,
data_length,
view_length
} = this.vars;
block.builders.init.add_block(deindent`
block.chunks.init.push(b`
let ${iterations} = [];
for (let #i = 0; #i < ${data_length}; #i += 1) {
${iterations}[#i] = ${create_each_block}(${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i));
${iterations}[#i] = ${create_each_block}(${this.vars.get_each_context}(#ctx, ${this.vars.each_block_value}, #i));
}
`);
block.builders.create.add_block(deindent`
block.chunks.create.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) {
${iterations}[#i].c();
}
`);
if (parent_nodes && this.renderer.options.hydratable) {
block.builders.claim.add_block(deindent`
block.chunks.claim.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) {
${iterations}[#i].l(${parent_nodes});
}
`);
}
block.builders.mount.add_block(deindent`
block.chunks.mount.push(b`
for (let #i = 0; #i < ${view_length}; #i += 1) {
${iterations}[#i].m(${initial_mount_node}, ${initial_anchor_node});
}
@ -469,27 +475,23 @@ export default class EachBlockWrapper extends Wrapper {
all_dependencies.add(dependency);
});
const condition = Array.from(all_dependencies)
.map(dependency => `changed.${dependency}`)
.join(' || ');
const has_transitions = !!(this.block.has_intro_method || this.block.has_outro_method);
if (all_dependencies.size) {
const has_transitions = !!(this.block.has_intro_method || this.block.has_outro_method);
if (condition !== '') {
const for_loop_body = this.block.has_update_method
? deindent`
? b`
if (${iterations}[#i]) {
${iterations}[#i].p(changed, child_ctx);
${has_transitions && `@transition_in(${this.vars.iterations}[#i], 1);`}
${iterations}[#i].p(#changed, child_ctx);
${has_transitions && b`@transition_in(${this.vars.iterations}[#i], 1);`}
} else {
${iterations}[#i] = ${create_each_block}(child_ctx);
${iterations}[#i].c();
${has_transitions && `@transition_in(${this.vars.iterations}[#i], 1);`}
${has_transitions && b`@transition_in(${this.vars.iterations}[#i], 1);`}
${iterations}[#i].m(${update_mount_node}, ${update_anchor_node});
}
`
: has_transitions
? deindent`
? b`
if (${iterations}[#i]) {
@transition_in(${this.vars.iterations}[#i], 1);
} else {
@ -499,7 +501,7 @@ export default class EachBlockWrapper extends Wrapper {
${iterations}[#i].m(${update_mount_node}, ${update_anchor_node});
}
`
: deindent`
: b`
if (!${iterations}[#i]) {
${iterations}[#i] = ${create_each_block}(child_ctx);
${iterations}[#i].c();
@ -507,43 +509,43 @@ export default class EachBlockWrapper extends Wrapper {
}
`;
const start = this.block.has_update_method ? '0' : `#old_length`;
const start = this.block.has_update_method ? 0 : `#old_length`;
let remove_old_blocks;
if (this.block.has_outros) {
const out = block.get_unique_name('out');
block.builders.init.add_block(deindent`
block.chunks.init.push(b`
const ${out} = i => @transition_out(${iterations}[i], 1, 1, () => {
${iterations}[i] = null;
});
`);
remove_old_blocks = deindent`
remove_old_blocks = b`
@group_outros();
for (#i = ${this.vars.each_block_value}.${length}; #i < ${view_length}; #i += 1) {
for (#i = ${data_length}; #i < ${view_length}; #i += 1) {
${out}(#i);
}
@check_outros();
`;
} else {
remove_old_blocks = deindent`
for (${this.block.has_update_method ? `` : `#i = ${this.vars.each_block_value}.${length}`}; #i < ${this.block.has_update_method ? view_length : '#old_length'}; #i += 1) {
remove_old_blocks = b`
for (${this.block.has_update_method ? null : x`#i = ${data_length}`}; #i < ${this.block.has_update_method ? view_length : '#old_length'}; #i += 1) {
${iterations}[#i].d(1);
}
${!fixed_length && `${view_length} = ${this.vars.each_block_value}.${length};`}
${!fixed_length && b`${view_length} = ${data_length};`}
`;
}
// We declare `i` as block scoped here, as the `remove_old_blocks` code
// may rely on continuing where this iteration stopped.
const update = deindent`
${!this.block.has_update_method && `const #old_length = ${this.vars.each_block_value}.length;`}
const update = b`
${!this.block.has_update_method && b`const #old_length = ${this.vars.each_block_value}.length;`}
${this.vars.each_block_value} = ${snippet};
let #i;
for (#i = ${start}; #i < ${this.vars.each_block_value}.${length}; #i += 1) {
const child_ctx = ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i);
for (#i = ${start}; #i < ${data_length}; #i += 1) {
const child_ctx = ${this.vars.get_each_context}(#ctx, ${this.vars.each_block_value}, #i);
${for_loop_body}
}
@ -551,15 +553,15 @@ export default class EachBlockWrapper extends Wrapper {
${remove_old_blocks}
`;
block.builders.update.add_block(deindent`
if (${condition}) {
block.chunks.update.push(b`
if (${changed(Array.from(all_dependencies))}) {
${update}
}
`);
}
if (this.block.has_outros) {
block.builders.outro.add_block(deindent`
block.chunks.outro.push(b`
${iterations} = ${iterations}.filter(@_Boolean);
for (let #i = 0; #i < ${view_length}; #i += 1) {
@transition_out(${iterations}[#i]);
@ -567,6 +569,6 @@ export default class EachBlockWrapper extends Wrapper {
`);
}
block.builders.destroy.add_block(`@destroy_each(${iterations}, detaching);`);
block.chunks.destroy.push(b`@destroy_each(${iterations}, detaching);`);
}
}

@ -2,10 +2,12 @@ import Attribute from '../../../nodes/Attribute';
import Block from '../../Block';
import fix_attribute_casing from './fix_attribute_casing';
import ElementWrapper from './index';
import { stringify } from '../../../utils/stringify';
import deindent from '../../../utils/deindent';
import { string_literal } from '../../../utils/stringify';
import { b, x } from 'code-red';
import Expression from '../../../nodes/shared/Expression';
import Text from '../../../nodes/Text';
import { changed } from '../shared/changed';
import { Literal } from 'estree';
export default class AttributeWrapper {
node: Attribute;
@ -78,16 +80,16 @@ export default class AttributeWrapper {
// DRY it out if that's possible without introducing crazy indirection
if (this.node.chunks.length === 1) {
// single {tag} — may be a non-string
value = (this.node.chunks[0] as Expression).render(block);
value = (this.node.chunks[0] as Expression).manipulate(block);
} else {
// '{foo} {bar}' — treat as string concatenation
const prefix = this.node.chunks[0].type === 'Text' ? '' : `"" + `;
const text = this.node.name === 'class'
value = this.node.name === 'class'
? this.get_class_name_text()
: this.render_chunks().join(' + ');
: this.render_chunks().reduce((lhs, rhs) => x`${lhs} + ${rhs}`);
value = `${prefix}${text}`;
// '{foo} {bar}' — treat as string concatenation
if (this.node.chunks[0].type !== 'Text') {
value = x`"" + ${value}`;
}
}
const is_select_value_attribute =
@ -96,19 +98,19 @@ export default class AttributeWrapper {
const should_cache = (this.node.should_cache() || is_select_value_attribute);
const last = should_cache && block.get_unique_name(
`${element.var}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value`
`${element.var.name}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value`
);
if (should_cache) block.add_variable(last);
let updater;
const init = should_cache ? `${last} = ${value}` : value;
const init = should_cache ? x`${last} = ${value}` : value;
if (is_legacy_input_type) {
block.builders.hydrate.add_line(
`@set_input_type(${element.var}, ${init});`
block.chunks.hydrate.push(
b`@set_input_type(${element.var}, ${init});`
);
updater = `@set_input_type(${element.var}, ${should_cache ? last : value});`;
updater = b`@set_input_type(${element.var}, ${should_cache ? last : value});`;
} else if (is_select_value_attribute) {
// annoying special case
const is_multiple_select = element.node.get_static_attribute_value('multiple');
@ -116,15 +118,15 @@ export default class AttributeWrapper {
const option = block.get_unique_name('option');
const if_statement = is_multiple_select
? deindent`
? b`
${option}.selected = ~${last}.indexOf(${option}.__value);`
: deindent`
: b`
if (${option}.__value === ${last}) {
${option}.selected = true;
break;
}`;
${{ type: 'BreakStatement' }};
}`; // TODO the BreakStatement is gross, but it's unsyntactic otherwise...
updater = deindent`
updater = b`
for (var ${i} = 0; ${i} < ${element.var}.options.length; ${i} += 1) {
var ${option} = ${element.var}.options[${i}];
@ -132,51 +134,50 @@ export default class AttributeWrapper {
}
`;
block.builders.mount.add_block(deindent`
block.chunks.mount.push(b`
${last} = ${value};
${updater}
`);
} else if (property_name) {
block.builders.hydrate.add_line(
`${element.var}.${property_name} = ${init};`
block.chunks.hydrate.push(
b`${element.var}.${property_name} = ${init};`
);
updater = block.renderer.options.dev
? `@prop_dev(${element.var}, "${property_name}", ${should_cache ? last : value});`
: `${element.var}.${property_name} = ${should_cache ? last : value};`;
? b`@prop_dev(${element.var}, "${property_name}", ${should_cache ? last : value});`
: b`${element.var}.${property_name} = ${should_cache ? last : value};`;
} else {
block.builders.hydrate.add_line(
`${method}(${element.var}, "${name}", ${init});`
block.chunks.hydrate.push(
b`${method}(${element.var}, "${name}", ${init});`
);
updater = `${method}(${element.var}, "${name}", ${should_cache ? last : value});`;
updater = b`${method}(${element.var}, "${name}", ${should_cache ? last : value});`;
}
const changed_check = (
(block.has_outros ? `!#current || ` : '') +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
let condition = changed(dependencies);
const update_cached_value = `${last} !== (${last} = ${value})`;
if (should_cache) {
condition = x`${condition} && (${last} !== (${last} = ${value}))`;
}
const condition = should_cache
? (dependencies.length ? `(${changed_check}) && ${update_cached_value}` : update_cached_value)
: changed_check;
if (block.has_outros) {
condition = x`!#current || ${condition}`;
}
block.builders.update.add_conditional(
condition,
updater
);
block.chunks.update.push(b`
if (${condition}) {
${updater}
}`);
} else {
const value = this.node.get_value(block);
const statement = (
is_legacy_input_type
? `@set_input_type(${element.var}, ${value});`
? b`@set_input_type(${element.var}, ${value});`
: property_name
? `${element.var}.${property_name} = ${value};`
: `${method}(${element.var}, "${name}", ${value === true ? '""' : value});`
? b`${element.var}.${property_name} = ${value};`
: b`${method}(${element.var}, "${name}", ${value.type === 'Literal' && (value as Literal).value === true ? x`""` : value});`
);
block.builders.hydrate.add_line(statement);
block.chunks.hydrate.push(statement);
// special case autofocus. has to be handled in a bit of a weird way
if (this.node.is_true && name === 'autofocus') {
@ -185,10 +186,10 @@ export default class AttributeWrapper {
}
if (is_indirectly_bound_value) {
const update_value = `${element.var}.value = ${element.var}.__value;`;
const update_value = b`${element.var}.value = ${element.var}.__value;`;
block.builders.hydrate.add_line(update_value);
if (this.node.get_dependencies().length > 0) block.builders.update.add_line(update_value);
block.chunks.hydrate.push(update_value);
if (this.node.get_dependencies().length > 0) block.chunks.update.push(update_value);
}
}
@ -198,22 +199,19 @@ export default class AttributeWrapper {
if (scoped_css && rendered.length === 2) {
// we have a situation like class={possiblyUndefined}
rendered[0] = `@null_to_empty(${rendered[0]})`;
rendered[0] = x`@null_to_empty(${rendered[0]})`;
}
return rendered.join(' + ');
return rendered.reduce((lhs, rhs) => x`${lhs} + ${rhs}`);
}
render_chunks() {
return this.node.chunks.map((chunk) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
return string_literal(chunk.data);
}
const rendered = chunk.render();
return chunk.get_precedence() <= 13
? `(${rendered})`
: rendered;
return chunk.manipulate();
});
}
@ -226,7 +224,7 @@ export default class AttributeWrapper {
return `="${value.map(chunk => {
return chunk.type === 'Text'
? chunk.data.replace(/"/g, '\\"')
: `\${${chunk.render()}}`;
: `\${${chunk.manipulate()}}`;
}).join('')}"`;
}
}

@ -1,18 +1,13 @@
import { b, x } from 'code-red';
import Binding from '../../../nodes/Binding';
import ElementWrapper from '../Element';
import get_object from '../../../utils/get_object';
import Block from '../../Block';
import Node from '../../../nodes/shared/Node';
import Renderer from '../../Renderer';
import flatten_reference from '../../../utils/flatten_reference';
import EachBlock from '../../../nodes/EachBlock';
import { Node as INode } from '../../../../interfaces';
function get_tail(node: INode) {
const end = node.end;
while (node.type === 'MemberExpression') node = node.object;
return { start: node.end, end };
}
import { changed } from '../shared/changed';
import { Node, Identifier } from 'estree';
export default class BindingWrapper {
node: Binding;
@ -21,11 +16,11 @@ export default class BindingWrapper {
object: string;
handler: {
uses_context: boolean;
mutation: string;
mutation: (Node | Node[]);
contextual_dependencies: Set<string>;
snippet?: string;
snippet?: Node;
};
snippet: string;
snippet: Node;
is_readonly: boolean;
needs_lock: boolean;
@ -56,14 +51,10 @@ export default class BindingWrapper {
this.object = get_object(this.node.expression.node).name;
// TODO unfortunate code is necessary because we need to use `ctx`
// inside the fragment, but not inside the <script>
const contextless_snippet = this.parent.renderer.component.source.slice(this.node.expression.node.start, this.node.expression.node.end);
// view to model
this.handler = get_event_handler(this, parent.renderer, block, this.object, contextless_snippet);
this.handler = get_event_handler(this, parent.renderer, block, this.object, this.node.raw_expression);
this.snippet = this.node.expression.render(block);
this.snippet = this.node.expression.manipulate(block);
this.is_readonly = this.node.is_readonly;
@ -89,28 +80,24 @@ export default class BindingWrapper {
return this.node.is_readonly_media_attribute();
}
render(block: Block, lock: string) {
render(block: Block, lock: Identifier) {
if (this.is_readonly) return;
const { parent } = this;
const update_conditions: string[] = this.needs_lock ? [`!${lock}`] : [];
const update_conditions: any[] = this.needs_lock ? [x`!${lock}`] : [];
const dependency_array = [...this.node.expression.dependencies];
if (dependency_array.length === 1) {
update_conditions.push(`changed.${dependency_array[0]}`);
} else if (dependency_array.length > 1) {
update_conditions.push(
`(${dependency_array.map(prop => `changed.${prop}`).join(' || ')})`
);
if (dependency_array.length > 0) {
update_conditions.push(changed(dependency_array));
}
if (parent.node.name === 'input') {
const type = parent.node.get_static_attribute_value('type');
if (type === null || type === "" || type === "text" || type === "email" || type === "password") {
update_conditions.push(`(${parent.var}.${this.node.name} !== ${this.snippet})`);
update_conditions.push(x`(${parent.var}.${this.node.name} !== ${this.snippet})`);
}
}
@ -123,38 +110,38 @@ export default class BindingWrapper {
{
const binding_group = get_binding_group(parent.renderer, this.node.expression.node);
block.builders.hydrate.add_line(
`ctx.$$binding_groups[${binding_group}].push(${parent.var});`
block.chunks.hydrate.push(
b`#ctx.$$binding_groups[${binding_group}].push(${parent.var});`
);
block.builders.destroy.add_line(
`ctx.$$binding_groups[${binding_group}].splice(ctx.$$binding_groups[${binding_group}].indexOf(${parent.var}), 1);`
block.chunks.destroy.push(
b`#ctx.$$binding_groups[${binding_group}].splice(#ctx.$$binding_groups[${binding_group}].indexOf(${parent.var}), 1);`
);
break;
}
case 'textContent':
update_conditions.push(`${this.snippet} !== ${parent.var}.textContent`);
update_conditions.push(x`${this.snippet} !== ${parent.var}.textContent`);
break;
case 'innerHTML':
update_conditions.push(`${this.snippet} !== ${parent.var}.innerHTML`);
update_conditions.push(x`${this.snippet} !== ${parent.var}.innerHTML`);
break;
case 'currentTime':
case 'playbackRate':
case 'volume':
update_conditions.push(`!@_isNaN(${this.snippet})`);
update_conditions.push(x`!@_isNaN(${this.snippet})`);
break;
case 'paused':
{
// this is necessary to prevent audio restarting by itself
const last = block.get_unique_name(`${parent.var}_is_paused`);
block.add_variable(last, 'true');
const last = block.get_unique_name(`${parent.var.name}_is_paused`);
block.add_variable(last, x`true`);
update_conditions.push(`${last} !== (${last} = ${this.snippet})`);
update_dom = `${parent.var}[${last} ? "pause" : "play"]();`;
update_conditions.push(x`${last} !== (${last} = ${this.snippet})`);
update_dom = b`${parent.var}[${last} ? "pause" : "play"]();`;
break;
}
@ -165,15 +152,26 @@ export default class BindingWrapper {
}
if (update_dom) {
block.builders.update.add_line(
update_conditions.length ? `if (${update_conditions.join(' && ')}) ${update_dom}` : update_dom
);
if (update_conditions.length > 0) {
const condition = update_conditions.reduce((lhs, rhs) => x`${lhs} && ${rhs}`);
block.chunks.update.push(b`
if (${condition}) {
${update_dom}
}
`);
} else {
block.chunks.update.push(update_dom);
}
}
if (this.node.name === 'innerHTML' || this.node.name === 'textContent') {
block.builders.mount.add_block(`if (${this.snippet} !== void 0) ${update_dom}`);
block.chunks.mount.push(b`
if (${this.snippet} !== void 0) {
${update_dom}
}`);
} else if (!/(currentTime|paused)/.test(this.node.name)) {
block.builders.mount.add_block(update_dom);
block.chunks.mount.push(update_dom);
}
}
}
@ -194,25 +192,25 @@ function get_dom_updater(
if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true ?
`@select_options(${element.var}, ${binding.snippet})` :
`@select_option(${element.var}, ${binding.snippet})`;
b`@select_options(${element.var}, ${binding.snippet})` :
b`@select_option(${element.var}, ${binding.snippet})`;
}
if (binding.node.name === 'group') {
const type = node.get_static_attribute_value('type');
const condition = type === 'checkbox'
? `~${binding.snippet}.indexOf(${element.var}.__value)`
: `${element.var}.__value === ${binding.snippet}`;
? x`~${binding.snippet}.indexOf(${element.var}.__value)`
: x`${element.var}.__value === ${binding.snippet}`;
return `${element.var}.checked = ${condition};`;
return b`${element.var}.checked = ${condition};`;
}
if (binding.node.name === 'value') {
return `@set_input_value(${element.var}, ${binding.snippet});`;
return b`@set_input_value(${element.var}, ${binding.snippet});`;
}
return `${element.var}.${binding.node.name} = ${binding.snippet};`;
return b`${element.var}.${binding.node.name} = ${binding.snippet};`;
}
function get_binding_group(renderer: Renderer, value: Node) {
@ -230,68 +228,54 @@ function get_binding_group(renderer: Renderer, value: Node) {
return index;
}
function mutate_store(store, value, tail) {
return tail
? `${store}.update($$value => ($$value${tail} = ${value}, $$value));`
: `${store}.set(${value});`;
}
function get_event_handler(
binding: BindingWrapper,
renderer: Renderer,
block: Block,
name: string,
snippet: string
lhs: Node
): {
uses_context: boolean;
mutation: string;
contextual_dependencies: Set<string>;
snippet?: string;
} {
uses_context: boolean;
mutation: (Node | Node[]);
contextual_dependencies: Set<string>;
lhs?: Node;
} {
const value = get_value_from_dom(renderer, binding.parent, binding);
let store = binding.object[0] === '$' ? binding.object.slice(1) : null;
const contextual_dependencies = new Set(binding.node.expression.contextual_dependencies);
let tail = '';
if (binding.node.expression.node.type === 'MemberExpression') {
const { start, end } = get_tail(binding.node.expression.node);
tail = renderer.component.source.slice(start, end);
}
const context = block.bindings.get(name);
let set_store;
if (context) {
const { object, property, modifier, store } = context;
if (binding.node.is_contextual) {
const binding = block.bindings.get(name);
const { object, property, snippet } = binding;
if (lhs.type === 'Identifier') {
lhs = modifier(x`${object}[${property}]`);
if (binding.store) {
store = binding.store;
tail = `${binding.tail}${tail}`;
contextual_dependencies.add(object.name);
contextual_dependencies.add(property.name);
}
return {
uses_context: true,
mutation: store
? mutate_store(store, value, tail)
: `${snippet}${tail} = ${value};`,
contextual_dependencies: new Set([object, property])
};
if (store) {
set_store = b`${store}.set(${`$${store}`});`;
}
} else {
const object = get_object(lhs);
if (object.name[0] === '$') {
const store = object.name.slice(1);
set_store = b`${store}.set(${object.name});`;
}
}
const mutation = store
? mutate_store(store, value, tail)
: `${snippet} = ${value};`;
if (binding.node.expression.node.type === 'MemberExpression') {
return {
uses_context: binding.node.expression.uses_context,
mutation,
contextual_dependencies: binding.node.expression.contextual_dependencies,
snippet
};
}
const mutation = b`
${lhs} = ${value};
${set_store}
`;
return {
uses_context: false,
uses_context: binding.node.is_contextual || binding.node.expression.uses_context, // TODO this is messy
mutation,
contextual_dependencies: new Set()
contextual_dependencies
};
}
@ -304,14 +288,14 @@ function get_value_from_dom(
const { name } = binding.node;
if (name === 'this') {
return `$$node`;
return x`$$node`;
}
// <select bind:value='selected>
if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true ?
`@select_multiple_value(this)` :
`@select_value(this)`;
x`@select_multiple_value(this)` :
x`@select_value(this)`;
}
const type = node.get_static_attribute_value('type');
@ -320,21 +304,21 @@ function get_value_from_dom(
if (name === 'group') {
const binding_group = get_binding_group(renderer, binding.node.expression.node);
if (type === 'checkbox') {
return `@get_binding_group_value($$binding_groups[${binding_group}])`;
return x`@get_binding_group_value($$binding_groups[${binding_group}])`;
}
return `this.__value`;
return x`this.__value`;
}
// <input type='range|number' bind:value>
if (type === 'range' || type === 'number') {
return `@to_number(this.${name})`;
return x`@to_number(this.${name})`;
}
if ((name === 'buffered' || name === 'seekable' || name === 'played')) {
return `@time_ranges_to_array(this.${name})`;
return x`@time_ranges_to_array(this.${name})`;
}
// everything else
return `this.${name}`;
return x`this.${name}`;
}

@ -1,11 +1,13 @@
import { b, x } from 'code-red';
import Attribute from '../../../nodes/Attribute';
import Block from '../../Block';
import AttributeWrapper from './Attribute';
import ElementWrapper from '../Element';
import { stringify } from '../../../utils/stringify';
import { string_literal } from '../../../utils/stringify';
import add_to_set from '../../../utils/add_to_set';
import Expression from '../../../nodes/shared/Expression';
import Text from '../../../nodes/Text';
import { changed } from '../shared/changed';
export interface StyleProp {
key: string;
@ -25,41 +27,44 @@ export default class StyleAttributeWrapper extends AttributeWrapper {
let value;
if (is_dynamic(prop.value)) {
const prop_dependencies = new Set();
value =
((prop.value.length === 1 || prop.value[0].type === 'Text') ? '' : `"" + `) +
prop.value
.map((chunk) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const snippet = chunk.render();
add_to_set(prop_dependencies, chunk.dynamic_dependencies());
return chunk.get_precedence() <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
const prop_dependencies: Set<string> = new Set();
value = prop.value
.map(chunk => {
if (chunk.type === 'Text') {
return string_literal(chunk.data);
} else {
add_to_set(prop_dependencies, chunk.dynamic_dependencies());
return chunk.manipulate(block);
}
})
.reduce((lhs, rhs) => x`${lhs} + ${rhs}`);
// TODO is this necessary? style.setProperty always treats value as string, no?
// if (prop.value.length === 1 || prop.value[0].type !== 'Text') {
// value = x`"" + ${value}`;
// }
if (prop_dependencies.size) {
const dependencies = Array.from(prop_dependencies);
const condition = (
(block.has_outros ? `!#current || ` : '') +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
block.builders.update.add_conditional(
condition,
`@set_style(${this.parent.var}, "${prop.key}", ${value}${prop.important ? ', 1' : ''});`
);
let condition = changed(Array.from(prop_dependencies));
if (block.has_outros) {
condition = x`!#current || ${condition}`;
}
const update = b`
if (${condition}) {
@set_style(${this.parent.var}, "${prop.key}", ${value}, ${prop.important ? 1 : null});
}`;
block.chunks.update.push(update);
}
} else {
value = stringify((prop.value[0] as Text).data);
value = string_literal((prop.value[0] as Text).data);
}
block.builders.hydrate.add_line(
`@set_style(${this.parent.var}, "${prop.key}", ${value}${prop.important ? ', 1' : ''});`
block.chunks.hydrate.push(
b`@set_style(${this.parent.var}, "${prop.key}", ${value}, ${prop.important ? 1 : null});`
);
});
}

@ -2,12 +2,12 @@ import Renderer from '../../Renderer';
import Element from '../../../nodes/Element';
import Wrapper from '../shared/Wrapper';
import Block from '../../Block';
import { is_void, quote_prop_if_necessary, quote_name_if_necessary, sanitize } from '../../../../utils/names';
import { is_void, sanitize } from '../../../../utils/names';
import FragmentWrapper from '../Fragment';
import { stringify, escape_html, escape } from '../../../utils/stringify';
import { escape_html, string_literal } from '../../../utils/stringify';
import TextWrapper from '../Text';
import fix_attribute_casing from './fix_attribute_casing';
import deindent from '../../../utils/deindent';
import { b, x, p } from 'code-red';
import { namespaces } from '../../../../utils/namespaces';
import AttributeWrapper from './Attribute';
import StyleAttributeWrapper from './StyleAttribute';
@ -20,6 +20,9 @@ import add_actions from '../shared/add_actions';
import create_debugging_comment from '../shared/create_debugging_comment';
import { get_context_merger } from '../shared/get_context_merger';
import bind_this from '../shared/bind_this';
import { changed } from '../shared/changed';
import { is_head } from '../shared/is_head';
import { Identifier } from 'estree';
const events = [
{
@ -114,7 +117,7 @@ export default class ElementWrapper extends Wrapper {
slot_block: Block;
select_binding_dependencies?: Set<string>;
var: string;
var: any;
constructor(
renderer: Renderer,
@ -125,7 +128,10 @@ export default class ElementWrapper extends Wrapper {
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
this.var = node.name.replace(/[^a-zA-Z0-9_$]/g, '_');
this.var = {
type: 'Identifier',
name: node.name.replace(/[^a-zA-Z0-9_$]/g, '_')
};
this.class_dependencies = [];
@ -148,7 +154,7 @@ export default class ElementWrapper extends Wrapper {
if (owner && owner.node.type === 'InlineComponent') {
const name = attribute.get_static_value() as string;
if (!(owner as InlineComponentWrapper).slots.has(name)) {
if (!(owner as unknown as InlineComponentWrapper).slots.has(name)) {
const child_block = block.child({
comment: create_debugging_comment(node, this.renderer.component),
name: this.renderer.component.get_unique_name(`create_${sanitize(name)}_slot`),
@ -156,15 +162,15 @@ export default class ElementWrapper extends Wrapper {
});
const lets = this.node.lets;
const seen = new Set(lets.map(l => l.name));
const seen = new Set(lets.map(l => l.name.name));
(owner as InlineComponentWrapper).node.lets.forEach(l => {
if (!seen.has(l.name)) lets.push(l);
(owner as unknown as InlineComponentWrapper).node.lets.forEach(l => {
if (!seen.has(l.name.name)) lets.push(l);
});
const fn = get_context_merger(lets);
(owner as InlineComponentWrapper).slots.set(name, {
(owner as unknown as InlineComponentWrapper).slots.set(name, {
block: child_block,
scope: this.node.scope,
fn
@ -172,7 +178,7 @@ export default class ElementWrapper extends Wrapper {
this.renderer.blocks.push(child_block);
}
this.slot_block = (owner as InlineComponentWrapper).slots.get(name).block;
this.slot_block = (owner as unknown as InlineComponentWrapper).slots.get(name).block;
block = this.slot_block;
}
}
@ -236,7 +242,7 @@ export default class ElementWrapper extends Wrapper {
}
}
render(block: Block, parent_node: string, parent_nodes: string) {
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
const { renderer } = this;
if (this.node.name === 'noscript') return;
@ -246,66 +252,76 @@ export default class ElementWrapper extends Wrapper {
}
const node = this.var;
const nodes = parent_nodes && block.get_unique_name(`${this.var}_nodes`); // if we're in unclaimable territory, i.e. <head>, parent_nodes is null
const nodes = parent_nodes && block.get_unique_name(`${this.var.name}_nodes`); // if we're in unclaimable territory, i.e. <head>, parent_nodes is null
block.add_variable(node);
const render_statement = this.get_render_statement();
block.builders.create.add_line(
`${node} = ${render_statement};`
block.chunks.create.push(
b`${node} = ${render_statement};`
);
if (renderer.options.hydratable) {
if (parent_nodes) {
block.builders.claim.add_block(deindent`
block.chunks.claim.push(b`
${node} = ${this.get_claim_statement(parent_nodes)};
var ${nodes} = @children(${this.node.name === 'template' ? `${node}.content` : node});
var ${nodes} = @children(${this.node.name === 'template' ? x`${node}.content` : node});
`);
} else {
block.builders.claim.add_line(
`${node} = ${render_statement};`
block.chunks.claim.push(
b`${node} = ${render_statement};`
);
}
}
if (parent_node) {
block.builders.mount.add_line(
`@append(${parent_node}, ${node});`
block.chunks.mount.push(
b`@append(${parent_node}, ${node});`
);
if (parent_node === '@_document.head') {
block.builders.destroy.add_line(`@detach(${node});`);
if (is_head(parent_node)) {
block.chunks.destroy.push(b`@detach(${node});`);
}
} else {
block.builders.mount.add_line(`@insert(#target, ${node}, anchor);`);
block.chunks.mount.push(b`@insert(#target, ${node}, anchor);`);
// TODO we eventually need to consider what happens to elements
// that belong to the same outgroup as an outroing element...
block.builders.destroy.add_conditional('detaching', `@detach(${node});`);
block.chunks.destroy.push(b`if (detaching) @detach(${node});`);
}
// insert static children with textContent or innerHTML
if (!this.node.namespace && this.can_use_innerhtml && this.fragment.nodes.length > 0) {
if (this.fragment.nodes.length === 1 && this.fragment.nodes[0].node.type === 'Text') {
block.builders.create.add_line(
block.chunks.create.push(
// @ts-ignore todo: should it be this.fragment.nodes[0].node.data instead?
`${node}.textContent = ${stringify(this.fragment.nodes[0].data)};`
b`${node}.textContent = ${string_literal(this.fragment.nodes[0].data)};`
);
} else {
const inner_html = escape(
this.fragment.nodes
.map(to_html)
.join('')
);
const state = {
quasi: {
type: 'TemplateElement',
value: { raw: '' }
}
};
const literal = {
type: 'TemplateLiteral',
expressions: [],
quasis: []
};
block.builders.create.add_line(
`${node}.innerHTML = \`${inner_html}\`;`
to_html((this.fragment.nodes as unknown as Array<ElementWrapper | TextWrapper>), block, literal, state);
literal.quasis.push(state.quasi);
block.chunks.create.push(
b`${node}.innerHTML = ${literal};`
);
}
} else {
this.fragment.nodes.forEach((child: Wrapper) => {
child.render(
block,
this.node.name === 'template' ? `${node}.content` : node,
this.node.name === 'template' ? x`${node}.content` : node,
nodes
);
});
@ -330,45 +346,15 @@ export default class ElementWrapper extends Wrapper {
this.add_classes(block);
if (nodes && this.renderer.options.hydratable) {
block.builders.claim.add_line(
`${nodes}.forEach(@detach);`
block.chunks.claim.push(
b`${nodes}.forEach(@detach);`
);
}
function to_html(wrapper: ElementWrapper | TextWrapper) {
if (wrapper.node.type === 'Text') {
if ((wrapper as TextWrapper).use_space()) return ' ';
const parent = wrapper.node.parent as Element;
const raw = parent && (
parent.name === 'script' ||
parent.name === 'style'
);
return (raw ? wrapper.node.data : escape_html(wrapper.node.data))
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/\$/g, '\\$');
}
if (wrapper.node.name === 'noscript') return '';
let open = `<${wrapper.node.name}`;
(wrapper as ElementWrapper).attributes.forEach((attr: AttributeWrapper) => {
open += ` ${fix_attribute_casing(attr.node.name)}${attr.stringify()}`;
});
if (is_void(wrapper.node.name)) return open + '>';
return `${open}>${(wrapper as ElementWrapper).fragment.nodes.map(to_html).join('')}</${wrapper.node.name}>`;
}
if (renderer.options.dev) {
const loc = renderer.locate(this.node.start);
block.builders.hydrate.add_line(
`@add_location(${this.var}, ${renderer.file_var}, ${loc.line}, ${loc.column}, ${this.node.start});`
block.chunks.hydrate.push(
b`@add_location(${this.var}, ${renderer.file_var}, ${loc.line}, ${loc.column}, ${this.node.start});`
);
}
}
@ -377,34 +363,33 @@ export default class ElementWrapper extends Wrapper {
const { name, namespace } = this.node;
if (namespace === 'http://www.w3.org/2000/svg') {
return `@svg_element("${name}")`;
return x`@svg_element("${name}")`;
}
if (namespace) {
return `@_document.createElementNS("${namespace}", "${name}")`;
return x`@_document.createElementNS("${namespace}", "${name}")`;
}
const is = this.attributes.find(attr => attr.node.name === 'is');
if (is) {
return `@element_is("${name}", ${is.render_chunks().join(' + ')});`;
return x`@element_is("${name}", ${is.render_chunks().reduce((lhs, rhs) => x`${lhs} + ${rhs}`)});`;
}
return `@element("${name}")`;
return x`@element("${name}")`;
}
get_claim_statement(nodes: string) {
get_claim_statement(nodes: Identifier) {
const attributes = this.node.attributes
.filter((attr) => attr.type === 'Attribute')
.map((attr) => `${quote_name_if_necessary(attr.name)}: true`)
.join(', ');
.map((attr) => p`${attr.name}: true`);
const name = this.node.namespace
? this.node.name
: this.node.name.toUpperCase();
return `@claim_element(${nodes}, "${name}", ${attributes
? `{ ${attributes} }`
: `{}`}, ${this.node.namespace === namespaces.svg ? true : false})`;
const svg = this.node.namespace === namespaces.svg ? 1 : null;
return x`@claim_element(${nodes}, "${name}", { ${attributes} }, ${svg})`;
}
add_bindings(block: Block) {
@ -415,10 +400,10 @@ export default class ElementWrapper extends Wrapper {
renderer.component.has_reactive_assignments = true;
const lock = this.bindings.some(binding => binding.needs_lock) ?
block.get_unique_name(`${this.var}_updating`) :
block.get_unique_name(`${this.var.name}_updating`) :
null;
if (lock) block.add_variable(lock, 'false');
if (lock) block.add_variable(lock, x`false`);
const groups = events
.map(event => ({
@ -430,10 +415,10 @@ export default class ElementWrapper extends Wrapper {
.filter(group => group.bindings.length);
groups.forEach(group => {
const handler = renderer.component.get_unique_name(`${this.var}_${group.events.join('_')}_handler`);
const handler = renderer.component.get_unique_name(`${this.var.name}_${group.events.join('_')}_handler`);
renderer.component.add_var({
name: handler,
name: handler.name,
internal: true,
referenced: true
});
@ -458,7 +443,7 @@ export default class ElementWrapper extends Wrapper {
// own hands
let animation_frame;
if (group.events[0] === 'timeupdate') {
animation_frame = block.get_unique_name(`${this.var}_animationframe`);
animation_frame = block.get_unique_name(`${this.var.name}_animationframe`);
block.add_variable(animation_frame);
}
@ -470,60 +455,73 @@ export default class ElementWrapper extends Wrapper {
if (has_local_function) {
// need to create a block-local function that calls an instance-level function
if (animation_frame) {
block.builders.init.add_block(deindent`
block.chunks.init.push(b`
function ${handler}() {
@_cancelAnimationFrame(${animation_frame});
if (!${this.var}.paused) {
${animation_frame} = @raf(${handler});
${needs_lock && `${lock} = true;`}
${needs_lock && b`${lock} = true;`}
}
ctx.${handler}.call(${this.var}${contextual_dependencies.size > 0 ? ', ctx' : ''});
#ctx.${handler}.call(${this.var}, ${contextual_dependencies.size > 0 ? '#ctx' : null});
}
`);
} else {
block.builders.init.add_block(deindent`
block.chunks.init.push(b`
function ${handler}() {
${needs_lock && `${lock} = true;`}
ctx.${handler}.call(${this.var}${contextual_dependencies.size > 0 ? ', ctx' : ''});
${needs_lock && b`${lock} = true;`}
#ctx.${handler}.call(${this.var}, ${contextual_dependencies.size > 0 ? '#ctx' : null});
}
`);
}
callee = handler;
} else {
callee = `ctx.${handler}`;
callee = x`#ctx.${handler}`;
}
this.renderer.component.partly_hoisted.push(deindent`
function ${handler}(${contextual_dependencies.size > 0 ? `{ ${Array.from(contextual_dependencies).join(', ')} }` : ``}) {
const arg = contextual_dependencies.size > 0 && {
type: 'ObjectPattern',
properties: Array.from(contextual_dependencies).map(name => {
const id = { type: 'Identifier', name };
return {
type: 'Property',
kind: 'init',
key: id,
value: id
};
})
};
this.renderer.component.partly_hoisted.push(b`
function ${handler}(${arg}) {
${group.bindings.map(b => b.handler.mutation)}
${Array.from(dependencies).filter(dep => dep[0] !== '$').map(dep => `${this.renderer.component.invalidate(dep)};`)}
${Array.from(dependencies).filter(dep => dep[0] !== '$').map(dep => b`${this.renderer.component.invalidate(dep)};`)}
}
`);
group.events.forEach(name => {
if (name === 'resize') {
// special case
const resize_listener = block.get_unique_name(`${this.var}_resize_listener`);
const resize_listener = block.get_unique_name(`${this.var.name}_resize_listener`);
block.add_variable(resize_listener);
block.builders.mount.add_line(
`${resize_listener} = @add_resize_listener(${this.var}, ${callee}.bind(${this.var}));`
block.chunks.mount.push(
b`${resize_listener} = @add_resize_listener(${this.var}, ${callee}.bind(${this.var}));`
);
block.builders.destroy.add_line(
`${resize_listener}.cancel();`
block.chunks.destroy.push(
b`${resize_listener}.cancel();`
);
} else {
block.event_listeners.push(
`@listen(${this.var}, "${name}", ${callee})`
x`@listen(${this.var}, "${name}", ${callee})`
);
}
});
const some_initial_state_is_undefined = group.bindings
.map(binding => `${binding.snippet} === void 0`)
.join(' || ');
.map(binding => x`${binding.snippet} === void 0`)
.reduce((lhs, rhs) => x`${lhs} || ${rhs}`);
const should_initialise = (
this.node.name === 'select' ||
@ -538,28 +536,28 @@ export default class ElementWrapper extends Wrapper {
);
if (should_initialise) {
const callback = has_local_function ? handler : `() => ${callee}.call(${this.var})`;
block.builders.hydrate.add_line(
`if (${some_initial_state_is_undefined}) @add_render_callback(${callback});`
const callback = has_local_function ? handler : x`() => ${callee}.call(${this.var})`;
block.chunks.hydrate.push(
b`if (${some_initial_state_is_undefined}) @add_render_callback(${callback});`
);
}
if (group.events[0] === 'resize') {
block.builders.hydrate.add_line(
`@add_render_callback(() => ${callee}.call(${this.var}));`
block.chunks.hydrate.push(
b`@add_render_callback(() => ${callee}.call(${this.var}));`
);
}
});
if (lock) {
block.builders.update.add_line(`${lock} = false;`);
block.chunks.update.push(b`${lock} = false;`);
}
const this_binding = this.bindings.find(b => b.node.name === 'this');
if (this_binding) {
const binding_callback = bind_this(renderer.component, block, this_binding.node, this.var);
block.builders.mount.add_line(binding_callback);
block.chunks.mount.push(binding_callback);
}
}
@ -584,8 +582,8 @@ export default class ElementWrapper extends Wrapper {
}
add_spread_attributes(block: Block) {
const levels = block.get_unique_name(`${this.var}_levels`);
const data = block.get_unique_name(`${this.var}_data`);
const levels = block.get_unique_name(`${this.var.name}_levels`);
const data = block.get_unique_name(`${this.var.name}_data`);
const initial_props = [];
const updates = [];
@ -594,27 +592,25 @@ export default class ElementWrapper extends Wrapper {
.filter(attr => attr.type === 'Attribute' || attr.type === 'Spread')
.forEach(attr => {
const condition = attr.dependencies.size > 0
? `(${[...attr.dependencies].map(d => `changed.${d}`).join(' || ')})`
? changed(Array.from(attr.dependencies))
: null;
if (attr.is_spread) {
const snippet = attr.expression.render(block);
const snippet = attr.expression.manipulate(block);
initial_props.push(snippet);
updates.push(condition ? `${condition} && ${snippet}` : snippet);
updates.push(condition ? x`${condition} && ${snippet}` : snippet);
} else {
const snippet = `{ ${quote_name_if_necessary(attr.name)}: ${attr.get_value(block)} }`;
const snippet = x`{ ${attr.name}: ${attr.get_value(block)} }`;
initial_props.push(snippet);
updates.push(condition ? `${condition} && ${snippet}` : snippet);
updates.push(condition ? x`${condition} && ${snippet}` : snippet);
}
});
block.builders.init.add_block(deindent`
var ${levels} = [
${initial_props.join(',\n')}
];
block.chunks.init.push(b`
var ${levels} = [${initial_props}];
var ${data} = {};
for (var #i = 0; #i < ${levels}.length; #i += 1) {
@ -622,15 +618,15 @@ export default class ElementWrapper extends Wrapper {
}
`);
const fn = this.node.namespace === namespaces.svg ? `set_svg_attributes` : `set_attributes`;
const fn = this.node.namespace === namespaces.svg ? x`@set_svg_attributes` : x`@set_attributes`;
block.builders.hydrate.add_line(
`@${fn}(${this.var}, ${data});`
block.chunks.hydrate.push(
b`${fn}(${this.var}, ${data});`
);
block.builders.update.add_block(deindent`
@${fn}(${this.var}, @get_spread_update(${levels}, [
${updates.join(',\n')}
block.chunks.update.push(b`
${fn}(${this.var}, @get_spread_update(${levels}, [
${updates}
]));
`);
}
@ -649,63 +645,63 @@ export default class ElementWrapper extends Wrapper {
if (intro === outro) {
// bidirectional transition
const name = block.get_unique_name(`${this.var}_transition`);
const name = block.get_unique_name(`${this.var.name}_transition`);
const snippet = intro.expression
? intro.expression.render(block)
: '{}';
? intro.expression.manipulate(block)
: x`{}`;
block.add_variable(name);
const fn = component.qualify(intro.name);
const intro_block = deindent`
const intro_block = b`
@add_render_callback(() => {
if (!${name}) ${name} = @create_bidirectional_transition(${this.var}, ${fn}, ${snippet}, true);
${name}.run(1);
});
`;
const outro_block = deindent`
const outro_block = b`
if (!${name}) ${name} = @create_bidirectional_transition(${this.var}, ${fn}, ${snippet}, false);
${name}.run(0);
`;
if (intro.is_local) {
block.builders.intro.add_block(deindent`
block.chunks.intro.push(b`
if (#local) {
${intro_block}
}
`);
block.builders.outro.add_block(deindent`
block.chunks.outro.push(b`
if (#local) {
${outro_block}
}
`);
} else {
block.builders.intro.add_block(intro_block);
block.builders.outro.add_block(outro_block);
block.chunks.intro.push(intro_block);
block.chunks.outro.push(outro_block);
}
block.builders.destroy.add_conditional('detaching', `if (${name}) ${name}.end();`);
block.chunks.destroy.push(b`if (detaching && ${name}) ${name}.end();`);
}
else {
const intro_name = intro && block.get_unique_name(`${this.var}_intro`);
const outro_name = outro && block.get_unique_name(`${this.var}_outro`);
const intro_name = intro && block.get_unique_name(`${this.var.name}_intro`);
const outro_name = outro && block.get_unique_name(`${this.var.name}_outro`);
if (intro) {
block.add_variable(intro_name);
const snippet = intro.expression
? intro.expression.render(block)
: '{}';
? intro.expression.manipulate(block)
: x`{}`;
const fn = component.qualify(intro.name);
let intro_block;
if (outro) {
intro_block = deindent`
intro_block = b`
@add_render_callback(() => {
if (${outro_name}) ${outro_name}.end(1);
if (!${intro_name}) ${intro_name} = @create_in_transition(${this.var}, ${fn}, ${snippet});
@ -713,9 +709,9 @@ export default class ElementWrapper extends Wrapper {
});
`;
block.builders.outro.add_line(`if (${intro_name}) ${intro_name}.invalidate();`);
block.chunks.outro.push(b`if (${intro_name}) ${intro_name}.invalidate();`);
} else {
intro_block = deindent`
intro_block = b`
if (!${intro_name}) {
@add_render_callback(() => {
${intro_name} = @create_in_transition(${this.var}, ${fn}, ${snippet});
@ -726,47 +722,47 @@ export default class ElementWrapper extends Wrapper {
}
if (intro.is_local) {
intro_block = deindent`
intro_block = b`
if (#local) {
${intro_block}
}
`;
}
block.builders.intro.add_block(intro_block);
block.chunks.intro.push(intro_block);
}
if (outro) {
block.add_variable(outro_name);
const snippet = outro.expression
? outro.expression.render(block)
: '{}';
? outro.expression.manipulate(block)
: x`{}`;
const fn = component.qualify(outro.name);
if (!intro) {
block.builders.intro.add_block(deindent`
block.chunks.intro.push(b`
if (${outro_name}) ${outro_name}.end(1);
`);
}
// TODO hide elements that have outro'd (unless they belong to a still-outroing
// group) prior to their removal from the DOM
let outro_block = deindent`
let outro_block = b`
${outro_name} = @create_out_transition(${this.var}, ${fn}, ${snippet});
`;
if (outro.is_local) {
outro_block = deindent`
outro_block = b`
if (#local) {
${outro_block}
}
`;
}
block.builders.outro.add_block(outro_block);
block.chunks.outro.push(outro_block);
block.builders.destroy.add_conditional('detaching', `if (${outro_name}) ${outro_name}.end();`);
block.chunks.destroy.push(b`if (detaching && ${outro_name}) ${outro_name}.end();`);
}
}
}
@ -781,23 +777,23 @@ export default class ElementWrapper extends Wrapper {
const stop_animation = block.get_unique_name('stop_animation');
block.add_variable(rect);
block.add_variable(stop_animation, '@noop');
block.add_variable(stop_animation, x`@noop`);
block.builders.measure.add_block(deindent`
block.chunks.measure.push(b`
${rect} = ${this.var}.getBoundingClientRect();
`);
block.builders.fix.add_block(deindent`
block.chunks.fix.push(b`
@fix_position(${this.var});
${stop_animation}();
${outro && `@add_transform(${this.var}, ${rect});`}
${outro && b`@add_transform(${this.var}, ${rect});`}
`);
const params = this.node.animation.expression ? this.node.animation.expression.render(block) : '{}';
const params = this.node.animation.expression ? this.node.animation.expression.manipulate(block) : x`{}`;
const name = component.qualify(this.node.animation.name);
block.builders.animate.add_block(deindent`
block.chunks.animate.push(b`
${stop_animation}();
${stop_animation} = @create_animation(${this.var}, ${rect}, ${name}, ${params});
`);
@ -813,26 +809,82 @@ export default class ElementWrapper extends Wrapper {
let snippet;
let dependencies;
if (expression) {
snippet = expression.render(block);
snippet = expression.manipulate(block);
dependencies = expression.dependencies;
} else {
snippet = `${quote_prop_if_necessary(name)}`;
snippet = name;
dependencies = new Set([name]);
}
const updater = `@toggle_class(${this.var}, "${name}", ${snippet});`;
const updater = b`@toggle_class(${this.var}, "${name}", ${snippet});`;
block.builders.hydrate.add_line(updater);
block.chunks.hydrate.push(updater);
if ((dependencies && dependencies.size > 0) || this.class_dependencies.length) {
const all_dependencies = this.class_dependencies.concat(...dependencies);
const deps = all_dependencies.map(dependency => `changed${quote_prop_if_necessary(dependency)}`).join(' || ');
const condition = all_dependencies.length > 1 ? `(${deps})` : deps;
const condition = changed(all_dependencies);
block.builders.update.add_conditional(
condition,
updater
);
block.chunks.update.push(b`
if (${condition}) {
${updater}
}`);
}
});
}
}
function to_html(wrappers: Array<ElementWrapper | TextWrapper>, block: Block, literal: any, state: any) {
wrappers.forEach(wrapper => {
if (wrapper.node.type === 'Text') {
if ((wrapper as TextWrapper).use_space()) state.quasi.value.raw += ' ';
const parent = wrapper.node.parent as Element;
const raw = parent && (
parent.name === 'script' ||
parent.name === 'style'
);
state.quasi.value.raw += (raw ? wrapper.node.data : escape_html(wrapper.node.data))
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/\$/g, '\\$');
}
else if (wrapper.node.name === 'noscript') {
// do nothing
}
else {
// element
state.quasi.value.raw += `<${wrapper.node.name}`;
(wrapper as ElementWrapper).attributes.forEach((attr: AttributeWrapper) => {
state.quasi.value.raw += ` ${fix_attribute_casing(attr.node.name)}="`;
attr.node.chunks.forEach(chunk => {
if (chunk.type === 'Text') {
state.quasi.value.raw += chunk.data;
} else {
literal.quasis.push(state.quasi);
literal.expressions.push(chunk.manipulate(block));
state.quasi = {
type: 'TemplateElement',
value: { raw: '' }
};
}
});
state.quasi.value.raw += `"`;
});
state.quasi.value.raw += '>';
if (!is_void(wrapper.node.name)) {
to_html((wrapper as ElementWrapper).fragment.nodes as Array<ElementWrapper | TextWrapper>, block, literal, state);
state.quasi.value.raw += `</${wrapper.node.name}>`;
}
}
});
}

@ -17,6 +17,7 @@ import { INode } from '../../nodes/interfaces';
import Renderer from '../Renderer';
import Block from '../Block';
import { trim_start, trim_end } from '../../../utils/trim';
import { Identifier } from 'estree';
const wrappers = {
AwaitBlock,
@ -145,7 +146,7 @@ export default class FragmentWrapper {
}
}
render(block: Block, parent_node: string, parent_nodes: string) {
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
for (let i = 0; i < this.nodes.length; i += 1) {
this.nodes[i].render(block, parent_node, parent_nodes);
}

@ -3,6 +3,8 @@ import Renderer from '../Renderer';
import Block from '../Block';
import Head from '../../nodes/Head';
import FragmentWrapper from './Fragment';
import { x } from 'code-red';
import { Identifier } from 'estree';
export default class HeadWrapper extends Wrapper {
fragment: FragmentWrapper;
@ -29,7 +31,7 @@ export default class HeadWrapper extends Wrapper {
);
}
render(block: Block, _parent_node: string, _parent_nodes: string) {
this.fragment.render(block, '@_document.head', 'nodes');
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
this.fragment.render(block, x`@_document.head` as unknown as Identifier, x`#nodes` as unknown as Identifier);
}
}

@ -6,8 +6,11 @@ import IfBlock from '../../nodes/IfBlock';
import create_debugging_comment from './shared/create_debugging_comment';
import ElseBlock from '../../nodes/ElseBlock';
import FragmentWrapper from './Fragment';
import deindent from '../../utils/deindent';
import { b, x } from 'code-red';
import { walk } from 'estree-walker';
import { is_head } from './shared/is_head';
import { Identifier, Node } from 'estree';
import { changed } from './shared/changed';
function is_else_if(node: ElseBlock) {
return (
@ -19,8 +22,8 @@ class IfBlockBranch extends Wrapper {
block: Block;
fragment: FragmentWrapper;
dependencies?: string[];
condition?: string;
snippet?: string;
condition?: any;
snippet?: Node;
is_dynamic: boolean;
var = null;
@ -54,9 +57,9 @@ class IfBlockBranch extends Wrapper {
if (should_cache) {
this.condition = block.get_unique_name(`show_if`);
this.snippet = expression.render(block);
this.snippet = (expression.manipulate(block) as Node);
} else {
this.condition = expression.render(block);
this.condition = expression.manipulate(block);
}
}
@ -79,7 +82,7 @@ export default class IfBlockWrapper extends Wrapper {
branches: IfBlockBranch[];
needs_update = false;
var = 'if_block';
var: Identifier = { type: 'Identifier', name: 'if_block' };
constructor(
renderer: Renderer,
@ -168,27 +171,27 @@ export default class IfBlockWrapper extends Wrapper {
render(
block: Block,
parent_node: string,
parent_nodes: string
parent_node: Identifier,
parent_nodes: Identifier
) {
const name = this.var;
const needs_anchor = this.next ? !this.next.is_dom_node() : !parent_node || !this.parent.is_dom_node();
const anchor = needs_anchor
? block.get_unique_name(`${name}_anchor`)
? block.get_unique_name(`${this.var.name}_anchor`)
: (this.next && this.next.var) || 'null';
const has_else = !(this.branches[this.branches.length - 1].condition);
const if_name = has_else ? '' : `if (${name}) `;
const if_exists_condition = has_else ? null : name;
const dynamic = this.branches[0].block.has_update_method; // can use [0] as proxy for all, since they necessarily have the same value
const has_intros = this.branches[0].block.has_intro_method;
const has_outros = this.branches[0].block.has_outro_method;
const has_transitions = has_intros || has_outros;
const vars = { name, anchor, if_name, has_else, has_transitions };
const vars = { name, anchor, if_exists_condition, has_else, has_transitions };
const detaching = (parent_node && parent_node !== '@_document.head') ? '' : 'detaching';
const detaching = parent_node && !is_head(parent_node) ? null : 'detaching';
if (this.node.else) {
this.branches.forEach(branch => {
@ -198,7 +201,7 @@ export default class IfBlockWrapper extends Wrapper {
if (has_outros) {
this.render_compound_with_outros(block, parent_node, parent_nodes, dynamic, vars, detaching);
block.builders.outro.add_line(`@transition_out(${name});`);
block.chunks.outro.push(b`@transition_out(${name});`);
} else {
this.render_compound(block, parent_node, parent_nodes, dynamic, vars, detaching);
}
@ -206,126 +209,155 @@ export default class IfBlockWrapper extends Wrapper {
this.render_simple(block, parent_node, parent_nodes, dynamic, vars, detaching);
if (has_outros) {
block.builders.outro.add_line(`@transition_out(${name});`);
block.chunks.outro.push(b`@transition_out(${name});`);
}
}
block.builders.create.add_line(`${if_name}${name}.c();`);
if (if_exists_condition) {
block.chunks.create.push(b`if (${if_exists_condition}) ${name}.c();`);
} else {
block.chunks.create.push(b`${name}.c();`);
}
if (parent_nodes && this.renderer.options.hydratable) {
block.builders.claim.add_line(
`${if_name}${name}.l(${parent_nodes});`
);
if (if_exists_condition) {
block.chunks.claim.push(
b`if (${if_exists_condition}) ${name}.l(${parent_nodes});`
);
} else {
block.chunks.claim.push(
b`${name}.l(${parent_nodes});`
);
}
}
if (has_intros || has_outros) {
block.builders.intro.add_line(`@transition_in(${name});`);
block.chunks.intro.push(b`@transition_in(${name});`);
}
if (needs_anchor) {
block.add_element(
anchor,
`@empty()`,
parent_nodes && `@empty()`,
anchor as Identifier,
x`@empty()`,
parent_nodes && x`@empty()`,
parent_node
);
}
this.branches.forEach(branch => {
branch.fragment.render(branch.block, null, 'nodes');
branch.fragment.render(branch.block, null, x`#nodes` as unknown as Identifier);
});
}
render_compound(
block: Block,
parent_node: string,
_parent_nodes: string,
parent_node: Identifier,
_parent_nodes: Identifier,
dynamic,
{ name, anchor, has_else, if_name, has_transitions },
{ name, anchor, has_else, if_exists_condition, has_transitions },
detaching
) {
const select_block_type = this.renderer.component.get_unique_name(`select_block_type`);
const current_block_type = block.get_unique_name(`current_block_type`);
const current_block_type_and = has_else ? '' : `${current_block_type} && `;
const get_block = has_else
? x`${current_block_type}(#ctx)`
: x`${current_block_type} && ${current_block_type}(#ctx)`;
/* eslint-disable @typescript-eslint/indent,indent */
if (this.needs_update) {
block.builders.init.add_block(deindent`
function ${select_block_type}(changed, ctx) {
block.chunks.init.push(b`
function ${select_block_type}(#changed, #ctx) {
${this.branches.map(({ dependencies, condition, snippet, block }) => condition
? deindent`
? b`
${snippet && (
dependencies.length > 0
? `if ((${condition} == null) || ${dependencies.map(n => `changed.${n}`).join(' || ')}) ${condition} = !!(${snippet})`
: `if (${condition} == null) ${condition} = !!(${snippet})`
? b`if ((${condition} == null) || ${changed(dependencies)}) ${condition} = !!(${snippet})`
: b`if (${condition} == null) ${condition} = !!(${snippet})`
)}
if (${condition}) return ${block.name};`
: `return ${block.name};`)}
: b`return ${block.name};`)}
}
`);
} else {
block.builders.init.add_block(deindent`
function ${select_block_type}(changed, ctx) {
block.chunks.init.push(b`
function ${select_block_type}(#changed, #ctx) {
${this.branches.map(({ condition, snippet, block }) => condition
? `if (${snippet || condition}) return ${block.name};`
: `return ${block.name};`)}
? b`if (${snippet || condition}) return ${block.name};`
: b`return ${block.name};`)}
}
`);
}
/* eslint-enable @typescript-eslint/indent,indent */
block.builders.init.add_block(deindent`
var ${current_block_type} = ${select_block_type}(null, ctx);
var ${name} = ${current_block_type_and}${current_block_type}(ctx);
block.chunks.init.push(b`
let ${current_block_type} = ${select_block_type}(null, #ctx);
let ${name} = ${get_block};
`);
const initial_mount_node = parent_node || '#target';
const anchor_node = parent_node ? 'null' : 'anchor';
block.builders.mount.add_line(
`${if_name}${name}.m(${initial_mount_node}, ${anchor_node});`
);
if (if_exists_condition) {
block.chunks.mount.push(
b`if (${if_exists_condition}) ${name}.m(${initial_mount_node}, ${anchor_node});`
);
} else {
block.chunks.mount.push(
b`${name}.m(${initial_mount_node}, ${anchor_node});`
);
}
if (this.needs_update) {
const update_mount_node = this.get_update_mount_node(anchor);
const change_block = deindent`
${if_name}${name}.d(1);
${name} = ${current_block_type_and}${current_block_type}(ctx);
const change_block = b`
${if_exists_condition ? b`if (${if_exists_condition}) ${name}.d(1)` : b`${name}.d(1)`};
${name} = ${get_block};
if (${name}) {
${name}.c();
${has_transitions && `@transition_in(${name}, 1);`}
${has_transitions && b`@transition_in(${name}, 1);`}
${name}.m(${update_mount_node}, ${anchor});
}
`;
if (dynamic) {
block.builders.update.add_block(deindent`
if (${current_block_type} === (${current_block_type} = ${select_block_type}(changed, ctx)) && ${name}) {
${name}.p(changed, ctx);
block.chunks.update.push(b`
if (${current_block_type} === (${current_block_type} = ${select_block_type}(#changed, #ctx)) && ${name}) {
${name}.p(#changed, #ctx);
} else {
${change_block}
}
`);
} else {
block.builders.update.add_block(deindent`
if (${current_block_type} !== (${current_block_type} = ${select_block_type}(changed, ctx))) {
block.chunks.update.push(b`
if (${current_block_type} !== (${current_block_type} = ${select_block_type}(#changed, #ctx))) {
${change_block}
}
`);
}
} else if (dynamic) {
block.builders.update.add_line(`${name}.p(changed, ctx);`);
block.chunks.update.push(b`${name}.p(#changed, #ctx);`);
}
block.builders.destroy.add_line(`${if_name}${name}.d(${detaching});`);
if (if_exists_condition) {
block.chunks.destroy.push(b`
if (${if_exists_condition}) {
${name}.d(${detaching});
}
`);
} else {
block.chunks.destroy.push(b`
${name}.d(${detaching});
`);
}
}
// if any of the siblings have outros, we need to keep references to the blocks
// (TODO does this only apply to bidi transitions?)
render_compound_with_outros(
block: Block,
parent_node: string,
_parent_nodes: string,
parent_node: Identifier,
_parent_nodes: Identifier,
dynamic,
{ name, anchor, has_else, has_transitions },
detaching
@ -337,51 +369,51 @@ export default class IfBlockWrapper extends Wrapper {
const if_blocks = block.get_unique_name(`if_blocks`);
const if_current_block_type_index = has_else
? ''
: `if (~${current_block_type_index}) `;
? nodes => nodes
: nodes => b`if (~${current_block_type_index}) { ${nodes} }`;
block.add_variable(current_block_type_index);
block.add_variable(name);
/* eslint-disable @typescript-eslint/indent,indent */
block.builders.init.add_block(deindent`
var ${if_block_creators} = [
${this.branches.map(branch => branch.block.name).join(',\n')}
block.chunks.init.push(b`
const ${if_block_creators} = [
${this.branches.map(branch => branch.block.name)}
];
var ${if_blocks} = [];
const ${if_blocks} = [];
${this.needs_update
? deindent`
function ${select_block_type}(changed, ctx) {
? b`
function ${select_block_type}(#changed, #ctx) {
${this.branches.map(({ dependencies, condition, snippet }, i) => condition
? deindent`
${snippet && `if ((${condition} == null) || ${dependencies.map(n => `changed.${n}`).join(' || ')}) ${condition} = !!(${snippet})`}
if (${condition}) return ${String(i)};`
: `return ${i};`)}
${!has_else && `return -1;`}
? b`
${snippet && b`if ((${condition} == null) || ${changed(dependencies)}) ${condition} = !!(${snippet})`}
if (${condition}) return ${i};`
: b`return ${i};`)}
${!has_else && b`return -1;`}
}
`
: deindent`
function ${select_block_type}(changed, ctx) {
: b`
function ${select_block_type}(#changed, #ctx) {
${this.branches.map(({ condition, snippet }, i) => condition
? `if (${snippet || condition}) return ${String(i)};`
: `return ${i};`)}
${!has_else && `return -1;`}
? b`if (${snippet || condition}) return ${i};`
: b`return ${i};`)}
${!has_else && b`return -1;`}
}
`}
`);
/* eslint-enable @typescript-eslint/indent,indent */
if (has_else) {
block.builders.init.add_block(deindent`
${current_block_type_index} = ${select_block_type}(null, ctx);
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](ctx);
block.chunks.init.push(b`
${current_block_type_index} = ${select_block_type}(null, #ctx);
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#ctx);
`);
} else {
block.builders.init.add_block(deindent`
if (~(${current_block_type_index} = ${select_block_type}(null, ctx))) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](ctx);
block.chunks.init.push(b`
if (~(${current_block_type_index} = ${select_block_type}(null, #ctx))) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#ctx);
}
`);
}
@ -389,14 +421,16 @@ export default class IfBlockWrapper extends Wrapper {
const initial_mount_node = parent_node || '#target';
const anchor_node = parent_node ? 'null' : 'anchor';
block.builders.mount.add_line(
`${if_current_block_type_index}${if_blocks}[${current_block_type_index}].m(${initial_mount_node}, ${anchor_node});`
block.chunks.mount.push(
if_current_block_type_index(
b`${if_blocks}[${current_block_type_index}].m(${initial_mount_node}, ${anchor_node});`
)
);
if (this.needs_update) {
const update_mount_node = this.get_update_mount_node(anchor);
const destroy_old_block = deindent`
const destroy_old_block = b`
@group_outros();
@transition_out(${if_blocks}[${previous_block_index}], 1, 1, () => {
${if_blocks}[${previous_block_index}] = null;
@ -404,23 +438,23 @@ export default class IfBlockWrapper extends Wrapper {
@check_outros();
`;
const create_new_block = deindent`
const create_new_block = b`
${name} = ${if_blocks}[${current_block_type_index}];
if (!${name}) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](ctx);
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#ctx);
${name}.c();
}
${has_transitions && `@transition_in(${name}, 1);`}
${has_transitions && b`@transition_in(${name}, 1);`}
${name}.m(${update_mount_node}, ${anchor});
`;
const change_block = has_else
? deindent`
? b`
${destroy_old_block}
${create_new_block}
`
: deindent`
: b`
if (${name}) {
${destroy_old_block}
}
@ -433,88 +467,90 @@ export default class IfBlockWrapper extends Wrapper {
`;
if (dynamic) {
block.builders.update.add_block(deindent`
var ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(changed, ctx);
block.chunks.update.push(b`
let ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(#changed, #ctx);
if (${current_block_type_index} === ${previous_block_index}) {
${if_current_block_type_index}${if_blocks}[${current_block_type_index}].p(changed, ctx);
${if_current_block_type_index(b`${if_blocks}[${current_block_type_index}].p(#changed, #ctx);`)}
} else {
${change_block}
}
`);
} else {
block.builders.update.add_block(deindent`
var ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(changed, ctx);
block.chunks.update.push(b`
let ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(#changed, #ctx);
if (${current_block_type_index} !== ${previous_block_index}) {
${change_block}
}
`);
}
} else if (dynamic) {
block.builders.update.add_line(`${name}.p(changed, ctx);`);
block.chunks.update.push(b`${name}.p(#changed, #ctx);`);
}
block.builders.destroy.add_line(deindent`
${if_current_block_type_index}${if_blocks}[${current_block_type_index}].d(${detaching});
`);
block.chunks.destroy.push(
if_current_block_type_index(b`${if_blocks}[${current_block_type_index}].d(${detaching});`)
);
}
render_simple(
block: Block,
parent_node: string,
_parent_nodes: string,
parent_node: Identifier,
_parent_nodes: Identifier,
dynamic,
{ name, anchor, if_name, has_transitions },
{ name, anchor, if_exists_condition, has_transitions },
detaching
) {
const branch = this.branches[0];
if (branch.snippet) block.add_variable(branch.condition, branch.snippet);
block.builders.init.add_block(deindent`
var ${name} = (${branch.condition}) && ${branch.block.name}(ctx);
block.chunks.init.push(b`
let ${name} = ${branch.condition} && ${branch.block.name}(#ctx);
`);
const initial_mount_node = parent_node || '#target';
const anchor_node = parent_node ? 'null' : 'anchor';
block.builders.mount.add_line(
`if (${name}) ${name}.m(${initial_mount_node}, ${anchor_node});`
block.chunks.mount.push(
b`if (${name}) ${name}.m(${initial_mount_node}, ${anchor_node});`
);
if (branch.dependencies.length > 0) {
const update_mount_node = this.get_update_mount_node(anchor);
const enter = dynamic
? deindent`
? b`
if (${name}) {
${name}.p(changed, ctx);
${has_transitions && `@transition_in(${name}, 1);`}
${name}.p(#changed, #ctx);
${has_transitions && b`@transition_in(${name}, 1);`}
} else {
${name} = ${branch.block.name}(ctx);
${name} = ${branch.block.name}(#ctx);
${name}.c();
${has_transitions && `@transition_in(${name}, 1);`}
${has_transitions && b`@transition_in(${name}, 1);`}
${name}.m(${update_mount_node}, ${anchor});
}
`
: deindent`
: b`
if (!${name}) {
${name} = ${branch.block.name}(ctx);
${name} = ${branch.block.name}(#ctx);
${name}.c();
${has_transitions && `@transition_in(${name}, 1);`}
${has_transitions && b`@transition_in(${name}, 1);`}
${name}.m(${update_mount_node}, ${anchor});
} ${has_transitions && `else @transition_in(${name}, 1);`}
} else {
${has_transitions && b`@transition_in(${name}, 1);`}
}
`;
if (branch.snippet) {
block.builders.update.add_block(`if (${branch.dependencies.map(n => `changed.${n}`).join(' || ')}) ${branch.condition} = ${branch.snippet}`);
block.chunks.update.push(b`if (${changed(branch.dependencies)}) ${branch.condition} = ${branch.snippet}`);
}
// no `p()` here — we don't want to update outroing nodes,
// as that will typically result in glitching
if (branch.block.has_outro_method) {
block.builders.update.add_block(deindent`
block.chunks.update.push(b`
if (${branch.condition}) {
${enter}
} else if (${name}) {
@ -526,7 +562,7 @@ export default class IfBlockWrapper extends Wrapper {
}
`);
} else {
block.builders.update.add_block(deindent`
block.chunks.update.push(b`
if (${branch.condition}) {
${enter}
} else if (${name}) {
@ -536,11 +572,19 @@ export default class IfBlockWrapper extends Wrapper {
`);
}
} else if (dynamic) {
block.builders.update.add_block(
`if (${branch.condition}) ${name}.p(changed, ctx);`
);
block.chunks.update.push(b`
if (${branch.condition}) ${name}.p(#changed, #ctx);
`);
}
block.builders.destroy.add_line(`${if_name}${name}.d(${detaching});`);
if (if_exists_condition) {
block.chunks.destroy.push(b`
if (${if_exists_condition}) ${name}.d(${detaching});
`);
} else {
block.chunks.destroy.push(b`
${name}.d(${detaching});
`);
}
}
}

@ -3,10 +3,9 @@ import Renderer from '../../Renderer';
import Block from '../../Block';
import InlineComponent from '../../../nodes/InlineComponent';
import FragmentWrapper from '../Fragment';
import { quote_name_if_necessary, quote_prop_if_necessary, sanitize } from '../../../../utils/names';
import { stringify_props } from '../../../utils/stringify_props';
import { sanitize } from '../../../../utils/names';
import add_to_set from '../../../utils/add_to_set';
import deindent from '../../../utils/deindent';
import { b, x, p } from 'code-red';
import Attribute from '../../../nodes/Attribute';
import get_object from '../../../utils/get_object';
import create_debugging_comment from '../shared/create_debugging_comment';
@ -15,10 +14,12 @@ import EachBlock from '../../../nodes/EachBlock';
import TemplateScope from '../../../nodes/shared/TemplateScope';
import is_dynamic from '../shared/is_dynamic';
import bind_this from '../shared/bind_this';
import { changed } from '../shared/changed';
import { Node, Identifier, ObjectExpression } from 'estree';
export default class InlineComponentWrapper extends Wrapper {
var: string;
slots: Map<string, { block: Block; scope: TemplateScope; fn?: string }> = new Map();
var: Identifier;
slots: Map<string, { block: Block; scope: TemplateScope; fn?: Node }> = new Map();
node: InlineComponent;
fragment: FragmentWrapper;
@ -61,11 +62,14 @@ export default class InlineComponentWrapper extends Wrapper {
}
});
this.var = (
this.node.name === 'svelte:self' ? renderer.component.name :
this.node.name === 'svelte:component' ? 'switch_instance' :
sanitize(this.node.name)
).toLowerCase();
this.var = {
type: 'Identifier',
name: (
this.node.name === 'svelte:self' ? renderer.component.name.name :
this.node.name === 'svelte:component' ? 'switch_instance' :
sanitize(this.node.name)
).toLowerCase()
};
if (this.node.children.length) {
const default_slot = block.child({
@ -102,42 +106,50 @@ export default class InlineComponentWrapper extends Wrapper {
render(
block: Block,
parent_node: string,
parent_nodes: string
parent_node: Identifier,
parent_nodes: Identifier
) {
const { renderer } = this;
const { component } = renderer;
const name = this.var;
const component_opts = [];
const component_opts = x`{}` as ObjectExpression;
const statements: string[] = [];
const updates: string[] = [];
const statements: Array<Node | Node[]> = [];
const updates: Array<Node | Node[]> = [];
let props;
const name_changes = block.get_unique_name(`${name}_changes`);
const name_changes = block.get_unique_name(`${name.name}_changes`);
const uses_spread = !!this.node.attributes.find(a => a.is_spread);
const slot_props = Array.from(this.slots).map(([name, slot]) => `${quote_name_if_necessary(name)}: [${slot.block.name}${slot.fn ? `, ${slot.fn}` : ''}]`);
const initial_props = slot_props.length > 0
? [`$$slots: ${stringify_props(slot_props)}`, `$$scope: { ctx }`]
const initial_props = this.slots.size > 0
? [
p`$$slots: {
${Array.from(this.slots).map(([name, slot]) => {
return p`${name}: [${slot.block.name}, ${slot.fn || null}]`;
})}
}`,
p`$$scope: {
ctx: #ctx
}`
]
: [];
const attribute_object = uses_spread
? stringify_props(initial_props)
: stringify_props(
this.node.attributes.map(attr => `${quote_name_if_necessary(attr.name)}: ${attr.get_value(block)}`).concat(initial_props)
);
? x`{ ${initial_props} }`
: x`{
${this.node.attributes.map(attr => p`${attr.name}: ${attr.get_value(block)}`)},
${initial_props}
}`;
if (this.node.attributes.length || this.node.bindings.length || initial_props.length) {
if (!uses_spread && this.node.bindings.length === 0) {
component_opts.push(`props: ${attribute_object}`);
component_opts.properties.push(p`props: ${attribute_object}`);
} else {
props = block.get_unique_name(`${name}_props`);
component_opts.push(`props: ${props}`);
props = block.get_unique_name(`${name.name}_props`);
component_opts.properties.push(p`props: ${props}`);
}
}
@ -145,7 +157,7 @@ export default class InlineComponentWrapper extends Wrapper {
const default_slot = this.slots.get('default');
this.fragment.nodes.forEach((child) => {
child.render(default_slot.block, null, 'nodes');
child.render(default_slot.block, null, x`#nodes` as unknown as Identifier);
});
}
@ -154,7 +166,7 @@ export default class InlineComponentWrapper extends Wrapper {
// will complain that options.target is missing. This would
// work better if components had separate public and private
// APIs
component_opts.push(`$$inline: true`);
component_opts.properties.push(p`$$inline: true`);
}
const fragment_dependencies = new Set(this.fragment ? ['$$scope'] : []);
@ -172,17 +184,17 @@ export default class InlineComponentWrapper extends Wrapper {
const dynamic_attributes = this.node.attributes.filter(a => a.get_dependencies().length > 0);
if (!uses_spread && (dynamic_attributes.length > 0 || this.node.bindings.length > 0 || non_let_dependencies.length > 0)) {
updates.push(`var ${name_changes} = {};`);
updates.push(b`const ${name_changes} = {};`);
}
if (this.node.attributes.length) {
if (uses_spread) {
const levels = block.get_unique_name(`${this.var}_spread_levels`);
const levels = block.get_unique_name(`${this.var.name}_spread_levels`);
const initial_props = [];
const changes = [];
const all_dependencies = new Set();
const all_dependencies: Set<string> = new Set();
this.node.attributes.forEach(attr => {
add_to_set(all_dependencies, attr.dependencies);
@ -192,53 +204,59 @@ export default class InlineComponentWrapper extends Wrapper {
const { name, dependencies } = attr;
const condition = dependencies.size > 0 && (dependencies.size !== all_dependencies.size)
? `(${Array.from(dependencies).map(d => `changed.${d}`).join(' || ')})`
? changed(Array.from(dependencies))
: null;
if (attr.is_spread) {
const value = attr.expression.render(block);
const value = attr.expression.manipulate(block);
initial_props.push(value);
let value_object = value;
if (attr.expression.node.type !== 'ObjectExpression') {
value_object = `@get_spread_object(${value})`;
value_object = x`@get_spread_object(${value})`;
}
changes.push(condition ? `${condition} && ${value_object}` : value_object);
changes.push(condition ? x`${condition} && ${value_object}` : value_object);
} else {
const obj = `{ ${quote_name_if_necessary(name)}: ${attr.get_value(block)} }`;
const obj = x`{ ${name}: ${attr.get_value(block)} }`;
initial_props.push(obj);
changes.push(condition ? `${condition} && ${obj}` : `${levels}[${i}]`);
changes.push(condition ? x`${condition} && ${obj}` : x`${levels}[${i}]`);
}
});
block.builders.init.add_block(deindent`
var ${levels} = [
${initial_props.join(',\n')}
block.chunks.init.push(b`
const ${levels} = [
${initial_props}
];
`);
statements.push(deindent`
for (var #i = 0; #i < ${levels}.length; #i += 1) {
statements.push(b`
for (let #i = 0; #i < ${levels}.length; #i += 1) {
${props} = @assign(${props}, ${levels}[#i]);
}
`);
const conditions = Array.from(all_dependencies).map(dep => `changed.${dep}`).join(' || ');
updates.push(deindent`
var ${name_changes} = ${conditions ? `(${conditions}) ? @get_spread_update(${levels}, [
${changes.join(',\n')}
]) : {}` : '{}'};
`);
if (all_dependencies.size) {
const condition = changed(Array.from(all_dependencies));
updates.push(b`
const ${name_changes} = ${condition} ? @get_spread_update(${levels}, [
${changes}
]) : {}
`);
} else {
updates.push(b`
const ${name_changes} = {};
`);
}
} else {
dynamic_attributes.forEach((attribute: Attribute) => {
const dependencies = attribute.get_dependencies();
if (dependencies.length > 0) {
const condition = dependencies.map(dependency => `changed.${dependency}`).join(' || ');
const condition = changed(dependencies);
updates.push(deindent`
if (${condition}) ${name_changes}${quote_prop_if_necessary(attribute.name)} = ${attribute.get_value(block)};
updates.push(b`
if (${condition}) ${name_changes}.${attribute.name} = ${attribute.get_value(block)};
`);
}
});
@ -246,7 +264,10 @@ export default class InlineComponentWrapper extends Wrapper {
}
if (non_let_dependencies.length > 0) {
updates.push(`if (${non_let_dependencies.map(n => `changed.${n}`).join(' || ')}) ${name_changes}.$$scope = { changed, ctx };`);
updates.push(b`
if (${changed(non_let_dependencies)}) {
${name_changes}.$$scope = { changed: #changed, ctx: #ctx };
}`);
}
const munged_bindings = this.node.bindings.map(binding => {
@ -256,10 +277,10 @@ export default class InlineComponentWrapper extends Wrapper {
return bind_this(component, block, binding, this.var);
}
const name = component.get_unique_name(`${this.var}_${binding.name}_binding`);
const id = component.get_unique_name(`${this.var.name}_${binding.name}_binding`);
component.add_var({
name,
name: id.name,
internal: true,
referenced: true
});
@ -267,24 +288,24 @@ export default class InlineComponentWrapper extends Wrapper {
const updating = block.get_unique_name(`updating_${binding.name}`);
block.add_variable(updating);
const snippet = binding.expression.render(block);
const snippet = binding.expression.manipulate(block);
statements.push(deindent`
statements.push(b`
if (${snippet} !== void 0) {
${props}${quote_prop_if_necessary(binding.name)} = ${snippet};
${props}.${binding.name} = ${snippet};
}`
);
updates.push(deindent`
if (!${updating} && ${[...binding.expression.dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')}) {
${name_changes}${quote_prop_if_necessary(binding.name)} = ${snippet};
updates.push(b`
if (!${updating} && ${changed(Array.from(binding.expression.dependencies))}) {
${name_changes}.${binding.name} = ${snippet};
}
`);
const contextual_dependencies = Array.from(binding.expression.contextual_dependencies);
const dependencies = Array.from(binding.expression.dependencies);
let lhs = component.source.slice(binding.expression.node.start, binding.expression.node.end).trim();
let lhs = binding.raw_expression;
if (binding.is_contextual && binding.expression.node.type === 'Identifier') {
// bind:x={y} — we can't just do `y = x`, we need to
@ -292,17 +313,28 @@ export default class InlineComponentWrapper extends Wrapper {
const { name } = binding.expression.node;
const { object, property, snippet } = block.bindings.get(name);
lhs = snippet;
contextual_dependencies.push(object, property);
contextual_dependencies.push(object.name, property.name);
}
const value = block.get_unique_name('value');
const args = [value];
const args: any[] = [value];
if (contextual_dependencies.length > 0) {
args.push(`{ ${contextual_dependencies.join(', ')} }`);
args.push({
type: 'ObjectPattern',
properties: contextual_dependencies.map(name => {
const id = { type: 'Identifier', name };
return {
type: 'Property',
kind: 'init',
key: id,
value: id
};
})
});
block.builders.init.add_block(deindent`
function ${name}(${value}) {
ctx.${name}.call(null, ${value}, ctx);
block.chunks.init.push(b`
function ${id}(${value}) {
#ctx.${id}.call(null, ${value}, #ctx);
${updating} = true;
@add_flush_callback(() => ${updating} = false);
}
@ -310,17 +342,17 @@ export default class InlineComponentWrapper extends Wrapper {
block.maintain_context = true; // TODO put this somewhere more logical
} else {
block.builders.init.add_block(deindent`
function ${name}(${value}) {
ctx.${name}.call(null, ${value});
block.chunks.init.push(b`
function ${id}(${value}) {
#ctx.${id}.call(null, ${value});
${updating} = true;
@add_flush_callback(() => ${updating} = false);
}
`);
}
const body = deindent`
function ${name}(${args.join(', ')}) {
const body = b`
function ${id}(${args}) {
${lhs} = ${value};
${component.invalidate(dependencies[0])};
}
@ -328,51 +360,51 @@ export default class InlineComponentWrapper extends Wrapper {
component.partly_hoisted.push(body);
return `@binding_callbacks.push(() => @bind(${this.var}, '${binding.name}', ${name}));`;
return b`@binding_callbacks.push(() => @bind(${this.var}, '${binding.name}', ${id}));`;
});
const munged_handlers = this.node.handlers.map(handler => {
let snippet = handler.render(block);
if (handler.modifiers.has('once')) snippet = `@once(${snippet})`;
if (handler.modifiers.has('once')) snippet = x`@once(${snippet})`;
return `${name}.$on("${handler.name}", ${snippet});`;
return b`${name}.$on("${handler.name}", ${snippet});`;
});
if (this.node.name === 'svelte:component') {
const switch_value = block.get_unique_name('switch_value');
const switch_props = block.get_unique_name('switch_props');
const snippet = this.node.expression.render(block);
const snippet = this.node.expression.manipulate(block);
block.builders.init.add_block(deindent`
block.chunks.init.push(b`
var ${switch_value} = ${snippet};
function ${switch_props}(ctx) {
${(this.node.attributes.length || this.node.bindings.length) && deindent`
${props && `let ${props} = ${attribute_object};`}`}
function ${switch_props}(#ctx) {
${(this.node.attributes.length > 0 || this.node.bindings.length > 0) && b`
${props && b`let ${props} = ${attribute_object};`}`}
${statements}
return ${stringify_props(component_opts)};
return ${component_opts};
}
if (${switch_value}) {
var ${name} = new ${switch_value}(${switch_props}(ctx));
var ${name} = new ${switch_value}(${switch_props}(#ctx));
${munged_bindings}
${munged_handlers}
}
`);
block.builders.create.add_line(
`if (${name}) ${name}.$$.fragment.c();`
block.chunks.create.push(
b`if (${name}) ${name}.$$.fragment.c();`
);
if (parent_nodes && this.renderer.options.hydratable) {
block.builders.claim.add_line(
`if (${name}) ${name}.$$.fragment.l(${parent_nodes});`
block.chunks.claim.push(
b`if (${name}) ${name}.$$.fragment.l(${parent_nodes});`
);
}
block.builders.mount.add_block(deindent`
block.chunks.mount.push(b`
if (${name}) {
@mount_component(${name}, ${parent_node || '#target'}, ${parent_node ? 'null' : 'anchor'});
}
@ -382,12 +414,12 @@ export default class InlineComponentWrapper extends Wrapper {
const update_mount_node = this.get_update_mount_node(anchor);
if (updates.length) {
block.builders.update.add_block(deindent`
block.chunks.update.push(b`
${updates}
`);
}
block.builders.update.add_block(deindent`
block.chunks.update.push(b`
if (${switch_value} !== (${switch_value} = ${snippet})) {
if (${name}) {
@group_outros();
@ -399,7 +431,7 @@ export default class InlineComponentWrapper extends Wrapper {
}
if (${switch_value}) {
${name} = new ${switch_value}(${switch_props}(ctx));
${name} = new ${switch_value}(${switch_props}(#ctx));
${munged_bindings}
${munged_handlers}
@ -410,70 +442,64 @@ export default class InlineComponentWrapper extends Wrapper {
} else {
${name} = null;
}
} else if (${switch_value}) {
${updates.length && b`${name}.$set(${name_changes});`}
}
`);
block.builders.intro.add_block(deindent`
block.chunks.intro.push(b`
if (${name}) @transition_in(${name}.$$.fragment, #local);
`);
if (updates.length) {
block.builders.update.add_block(deindent`
else if (${switch_value}) {
${name}.$set(${name_changes});
}
`);
}
block.builders.outro.add_line(
`if (${name}) @transition_out(${name}.$$.fragment, #local);`
block.chunks.outro.push(
b`if (${name}) @transition_out(${name}.$$.fragment, #local);`
);
block.builders.destroy.add_line(`if (${name}) @destroy_component(${name}${parent_node ? '' : ', detaching'});`);
block.chunks.destroy.push(b`if (${name}) @destroy_component(${name}, ${parent_node ? null : 'detaching'});`);
} else {
const expression = this.node.name === 'svelte:self'
? '__svelte:self__' // TODO conflict-proof this
? component.name
: component.qualify(this.node.name);
block.builders.init.add_block(deindent`
${(this.node.attributes.length || this.node.bindings.length) && deindent`
${props && `let ${props} = ${attribute_object};`}`}
block.chunks.init.push(b`
${(this.node.attributes.length > 0 || this.node.bindings.length > 0) && b`
${props && b`let ${props} = ${attribute_object};`}`}
${statements}
var ${name} = new ${expression}(${stringify_props(component_opts)});
const ${name} = new ${expression}(${component_opts});
${munged_bindings}
${munged_handlers}
`);
block.builders.create.add_line(`${name}.$$.fragment.c();`);
block.chunks.create.push(b`${name}.$$.fragment.c();`);
if (parent_nodes && this.renderer.options.hydratable) {
block.builders.claim.add_line(
`${name}.$$.fragment.l(${parent_nodes});`
block.chunks.claim.push(
b`${name}.$$.fragment.l(${parent_nodes});`
);
}
block.builders.mount.add_line(
`@mount_component(${name}, ${parent_node || '#target'}, ${parent_node ? 'null' : 'anchor'});`
block.chunks.mount.push(
b`@mount_component(${name}, ${parent_node || '#target'}, ${parent_node ? 'null' : 'anchor'});`
);
block.builders.intro.add_block(deindent`
block.chunks.intro.push(b`
@transition_in(${name}.$$.fragment, #local);
`);
if (updates.length) {
block.builders.update.add_block(deindent`
block.chunks.update.push(b`
${updates}
${name}.$set(${name_changes});
`);
}
block.builders.destroy.add_block(deindent`
@destroy_component(${name}${parent_node ? '' : ', detaching'});
block.chunks.destroy.push(b`
@destroy_component(${name}, ${parent_node ? null : 'detaching'});
`);
block.builders.outro.add_line(
`@transition_out(${name}.$$.fragment, #local);`
block.chunks.outro.push(
b`@transition_out(${name}.$$.fragment, #local);`
);
}
}

@ -4,25 +4,27 @@ import Tag from './shared/Tag';
import Wrapper from './shared/Wrapper';
import MustacheTag from '../../nodes/MustacheTag';
import RawMustacheTag from '../../nodes/RawMustacheTag';
import { x } from 'code-red';
import { Identifier } from 'estree';
export default class MustacheTagWrapper extends Tag {
var = 't';
var: Identifier = { type: 'Identifier', name: 't' };
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: MustacheTag | RawMustacheTag) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
}
render(block: Block, parent_node: string, parent_nodes: string) {
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
const { init } = this.rename_this_method(
block,
value => `@set_data(${this.var}, ${value});`
value => x`@set_data(${this.var}, ${value});`
);
block.add_element(
this.var,
`@text(${init})`,
parent_nodes && `@claim_text(${parent_nodes}, ${init})`,
x`@text(${init})`,
parent_nodes && x`@claim_text(${parent_nodes}, ${init})`,
parent_node
);
}

@ -1,12 +1,15 @@
import { b, x } from 'code-red';
import Renderer from '../Renderer';
import Block from '../Block';
import Tag from './shared/Tag';
import Wrapper from './shared/Wrapper';
import MustacheTag from '../../nodes/MustacheTag';
import RawMustacheTag from '../../nodes/RawMustacheTag';
import { is_head } from './shared/is_head';
import { Identifier } from 'estree';
export default class RawMustacheTagWrapper extends Tag {
var = 'raw';
var: Identifier = { type: 'Identifier', name: 'raw' };
constructor(
renderer: Renderer,
@ -18,20 +21,20 @@ export default class RawMustacheTagWrapper extends Tag {
this.cannot_use_innerhtml();
}
render(block: Block, parent_node: string, _parent_nodes: string) {
const in_head = parent_node === '@_document.head';
render(block: Block, parent_node: Identifier, _parent_nodes: Identifier) {
const in_head = is_head(parent_node);
const can_use_innerhtml = !in_head && parent_node && !this.prev && !this.next;
if (can_use_innerhtml) {
const insert = content => `${parent_node}.innerHTML = ${content};`;
const insert = content => b`${parent_node}.innerHTML = ${content};`[0];
const { init } = this.rename_this_method(
block,
content => insert(content)
);
block.builders.mount.add_line(insert(init));
block.chunks.mount.push(insert(init));
}
else {
@ -44,20 +47,20 @@ export default class RawMustacheTagWrapper extends Tag {
const { init } = this.rename_this_method(
block,
content => `${html_tag}.p(${content});`
content => x`${html_tag}.p(${content});`
);
const update_anchor = in_head ? 'null' : needs_anchor ? html_anchor : this.next ? this.next.var : 'null';
block.builders.hydrate.add_line(`${html_tag} = new @HtmlTag(${init}, ${update_anchor});`);
block.builders.mount.add_line(`${html_tag}.m(${parent_node || '#target'}${parent_node ? '' : ', anchor'});`);
block.chunks.hydrate.push(b`${html_tag} = new @HtmlTag(${init}, ${update_anchor});`);
block.chunks.mount.push(b`${html_tag}.m(${parent_node || '#target'}, ${parent_node ? null : 'anchor'});`);
if (needs_anchor) {
block.add_element(html_anchor, '@empty()', '@empty()', parent_node);
block.add_element(html_anchor, x`@empty()`, x`@empty()`, parent_node);
}
if (!parent_node || in_head) {
block.builders.destroy.add_conditional('detaching', `${html_tag}.d();`);
block.chunks.destroy.push(b`if (detaching) ${html_tag}.d();`);
}
}
}

@ -3,19 +3,20 @@ import Renderer from '../Renderer';
import Block from '../Block';
import Slot from '../../nodes/Slot';
import FragmentWrapper from './Fragment';
import deindent from '../../utils/deindent';
import { sanitize, quote_prop_if_necessary } from '../../../utils/names';
import { b, p, x } from 'code-red';
import { sanitize } from '../../../utils/names';
import add_to_set from '../../utils/add_to_set';
import get_slot_data from '../../utils/get_slot_data';
import { stringify_props } from '../../utils/stringify_props';
import Expression from '../../nodes/shared/Expression';
import is_dynamic from './shared/is_dynamic';
import { Identifier, ObjectExpression } from 'estree';
import { changed } from './shared/changed';
export default class SlotWrapper extends Wrapper {
node: Slot;
fragment: FragmentWrapper;
var = 'slot';
var: Identifier = { type: 'Identifier', name: 'slot' };
dependencies: Set<string> = new Set(['$$scope']);
constructor(
@ -51,8 +52,8 @@ export default class SlotWrapper extends Wrapper {
render(
block: Block,
parent_node: string,
parent_nodes: string
parent_node: Identifier,
parent_nodes: Identifier
) {
const { renderer } = this;
@ -65,8 +66,8 @@ export default class SlotWrapper extends Wrapper {
get_slot_changes = renderer.component.get_unique_name(`get_${sanitize(slot_name)}_slot_changes`);
get_slot_context = renderer.component.get_unique_name(`get_${sanitize(slot_name)}_slot_context`);
const context_props = get_slot_data(this.node.values, false);
const changes_props = [];
const context = get_slot_data(this.node.values);
const changes = x`{}` as ObjectExpression;
const dependencies = new Set();
@ -90,15 +91,22 @@ export default class SlotWrapper extends Wrapper {
});
if (dynamic_dependencies.length > 0) {
changes_props.push(`${attribute.name}: ${dynamic_dependencies.join(' || ')}`);
const expression = dynamic_dependencies
.map(name => ({ type: 'Identifier', name } as any))
.reduce((lhs, rhs) => x`${lhs} || ${rhs}`);
changes.properties.push(p`${attribute.name}: ${expression}`);
}
});
const arg = dependencies.size > 0 ? `{ ${Array.from(dependencies).join(', ')} }` : '';
const arg = dependencies.size > 0 && {
type: 'ObjectPattern',
properties: Array.from(dependencies).map(name => p`${name}`)
};
renderer.blocks.push(deindent`
const ${get_slot_changes} = (${arg}) => (${stringify_props(changes_props)});
const ${get_slot_context} = (${arg}) => (${stringify_props(context_props)});
renderer.blocks.push(b`
const ${get_slot_changes} = (${arg}) => (${changes});
const ${get_slot_context} = (${arg}) => (${context});
`);
} else {
get_slot_changes = 'null';
@ -108,57 +116,61 @@ export default class SlotWrapper extends Wrapper {
const slot = block.get_unique_name(`${sanitize(slot_name)}_slot`);
const slot_definition = block.get_unique_name(`${sanitize(slot_name)}_slot_template`);
block.builders.init.add_block(deindent`
const ${slot_definition} = ctx.$$slots${quote_prop_if_necessary(slot_name)};
const ${slot} = @create_slot(${slot_definition}, ctx, ${get_slot_context});
block.chunks.init.push(b`
const ${slot_definition} = #ctx.$$slots.${slot_name};
const ${slot} = @create_slot(${slot_definition}, #ctx, ${get_slot_context});
`);
const mount_before = block.builders.mount.toString();
// TODO this is a dreadful hack! Should probably make this nicer
const { create, claim, hydrate, mount, update, destroy } = block.chunks;
block.builders.create.push_condition(`!${slot}`);
block.builders.claim.push_condition(`!${slot}`);
block.builders.hydrate.push_condition(`!${slot}`);
block.builders.mount.push_condition(`!${slot}`);
block.builders.update.push_condition(`!${slot}`);
block.builders.destroy.push_condition(`!${slot}`);
block.chunks.create = [];
block.chunks.claim = [];
block.chunks.hydrate = [];
block.chunks.mount = [];
block.chunks.update = [];
block.chunks.destroy = [];
const listeners = block.event_listeners;
block.event_listeners = [];
this.fragment.render(block, parent_node, parent_nodes);
block.render_listeners(`_${slot}`);
block.render_listeners(`_${slot.name}`);
block.event_listeners = listeners;
block.builders.create.pop_condition();
block.builders.claim.pop_condition();
block.builders.hydrate.pop_condition();
block.builders.mount.pop_condition();
block.builders.update.pop_condition();
block.builders.destroy.pop_condition();
block.builders.create.add_line(
`if (${slot}) ${slot}.c();`
if (block.chunks.create) create.push(b`if (!${slot}) { ${block.chunks.create} }`);
if (block.chunks.claim) claim.push(b`if (!${slot}) { ${block.chunks.claim} }`);
if (block.chunks.hydrate) hydrate.push(b`if (!${slot}) { ${block.chunks.hydrate} }`);
if (block.chunks.mount) mount.push(b`if (!${slot}) { ${block.chunks.mount} }`);
if (block.chunks.update) update.push(b`if (!${slot}) { ${block.chunks.update} }`);
if (block.chunks.destroy) destroy.push(b`if (!${slot}) { ${block.chunks.destroy} }`);
block.chunks.create = create;
block.chunks.claim = claim;
block.chunks.hydrate = hydrate;
block.chunks.mount = mount;
block.chunks.update = update;
block.chunks.destroy = destroy;
block.chunks.create.push(
b`if (${slot}) ${slot}.c();`
);
block.builders.claim.add_line(
`if (${slot}) ${slot}.l(${parent_nodes});`
block.chunks.claim.push(
b`if (${slot}) ${slot}.l(${parent_nodes});`
);
const mount_leadin = block.builders.mount.toString() !== mount_before
? `else`
: `if (${slot})`;
block.builders.mount.add_block(deindent`
${mount_leadin} {
block.chunks.mount.push(b`
if (${slot}) {
${slot}.m(${parent_node || '#target'}, ${parent_node ? 'null' : 'anchor'});
}
`);
block.builders.intro.add_line(
`@transition_in(${slot}, #local);`
block.chunks.intro.push(
b`@transition_in(${slot}, #local);`
);
block.builders.outro.add_line(
`@transition_out(${slot}, #local);`
block.chunks.outro.push(
b`@transition_out(${slot}, #local);`
);
const dynamic_dependencies = Array.from(this.dependencies).filter(name => {
@ -168,20 +180,17 @@ export default class SlotWrapper extends Wrapper {
return is_dynamic(variable);
});
let update_conditions = dynamic_dependencies.map(name => `changed.${name}`).join(' || ');
if (dynamic_dependencies.length > 1) update_conditions = `(${update_conditions})`;
block.builders.update.add_block(deindent`
if (${slot} && ${slot}.p && ${update_conditions}) {
block.chunks.update.push(b`
if (${slot} && ${slot}.p && ${changed(dynamic_dependencies)}) {
${slot}.p(
@get_slot_changes(${slot_definition}, ctx, changed, ${get_slot_changes}),
@get_slot_context(${slot_definition}, ctx, ${get_slot_context})
@get_slot_changes(${slot_definition}, #ctx, #changed, ${get_slot_changes}),
@get_slot_context(${slot_definition}, #ctx, ${get_slot_context})
);
}
`);
block.builders.destroy.add_line(
`if (${slot}) ${slot}.d(detaching);`
block.chunks.destroy.push(
b`if (${slot}) ${slot}.d(detaching);`
);
}
}

@ -2,7 +2,8 @@ import Renderer from '../Renderer';
import Block from '../Block';
import Text from '../../nodes/Text';
import Wrapper from './shared/Wrapper';
import { stringify } from '../../utils/stringify';
import { x } from 'code-red';
import { Identifier } from 'estree';
// Whitespace inside one of these elements will not result in
// a whitespace node being created in any circumstances. (This
@ -33,7 +34,7 @@ export default class TextWrapper extends Wrapper {
node: Text;
data: string;
skip: boolean;
var: string;
var: Identifier;
constructor(
renderer: Renderer,
@ -46,7 +47,7 @@ export default class TextWrapper extends Wrapper {
this.skip = should_skip(this.node);
this.data = data;
this.var = this.skip ? null : 't';
this.var = (this.skip ? null : x`t`) as unknown as Identifier;
}
use_space() {
@ -64,15 +65,15 @@ export default class TextWrapper extends Wrapper {
return true;
}
render(block: Block, parent_node: string, parent_nodes: string) {
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
if (this.skip) return;
const use_space = this.use_space();
block.add_element(
this.var,
use_space ? `@space()` : `@text(${stringify(this.data)})`,
parent_nodes && (use_space ? `@claim_space(${parent_nodes})` : `@claim_text(${parent_nodes}, ${stringify(this.data)})`),
parent_node
use_space ? x`@space()` : x`@text("${this.data}")`,
parent_nodes && (use_space ? x`@claim_space(${parent_nodes})` : x`@claim_text(${parent_nodes}, "${this.data}")`),
parent_node as Identifier
);
}
}

@ -1,10 +1,14 @@
import { b, x } from 'code-red';
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import Title from '../../nodes/Title';
import { stringify } from '../../utils/stringify';
import { string_literal } from '../../utils/stringify';
import add_to_set from '../../utils/add_to_set';
import Text from '../../nodes/Text';
import { Identifier } from 'estree';
import { changed } from './shared/changed';
import MustacheTag from '../../nodes/MustacheTag';
export default class TitleWrapper extends Wrapper {
node: Title;
@ -20,13 +24,13 @@ export default class TitleWrapper extends Wrapper {
super(renderer, block, parent, node);
}
render(block: Block, _parent_node: string, _parent_nodes: string) {
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
const is_dynamic = !!this.node.children.find(node => node.type !== 'Text');
if (is_dynamic) {
let value;
const all_dependencies = new Set();
const all_dependencies: Set<string> = new Set();
// TODO some of this code is repeated in Tag.ts — would be good to
// DRY it out if that's possible without introducing crazy indirection
@ -34,29 +38,25 @@ export default class TitleWrapper extends Wrapper {
// single {tag} — may be a non-string
// @ts-ignore todo: check this
const { expression } = this.node.children[0];
value = expression.render(block);
value = expression.manipulate(block);
add_to_set(all_dependencies, expression.dependencies);
} else {
// '{foo} {bar}' — treat as string concatenation
value =
(this.node.children[0].type === 'Text' ? '' : `"" + `) +
this.node.children
.map((chunk) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
// @ts-ignore todo: check this
const snippet = chunk.expression.render(block);
// @ts-ignore todo: check this
chunk.expression.dependencies.forEach(d => {
all_dependencies.add(d);
});
// @ts-ignore todo: check this
return chunk.expression.get_precedence() <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
value = this.node.children
.map(chunk => {
if (chunk.type === 'Text') return string_literal(chunk.data);
(chunk as MustacheTag).expression.dependencies.forEach(d => {
all_dependencies.add(d);
});
return (chunk as MustacheTag).expression.manipulate(block);
})
.reduce((lhs, rhs) => x`${lhs} + ${rhs}`);
if (this.node.children[0].type !== 'Text') {
value = x`"" + ${value}`;
}
}
const last = this.node.should_cache && block.get_unique_name(
@ -65,37 +65,38 @@ export default class TitleWrapper extends Wrapper {
if (this.node.should_cache) block.add_variable(last);
const init = this.node.should_cache ? `${last} = ${value}` : value;
const init = this.node.should_cache ? x`${last} = ${value}` : value;
block.builders.init.add_line(
`@_document.title = ${init};`
block.chunks.init.push(
b`@_document.title = ${init};`
);
const updater = `@_document.title = ${this.node.should_cache ? last : value};`;
const updater = b`@_document.title = ${this.node.should_cache ? last : value};`;
if (all_dependencies.size) {
const dependencies = Array.from(all_dependencies);
const changed_check = (
(block.has_outros ? `!#current || ` : '') +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
const update_cached_value = `${last} !== (${last} = ${value})`;
let condition = changed(dependencies);
if (block.has_outros) {
condition = x`!#current || ${condition}`;
}
const condition = this.node.should_cache ?
(dependencies.length ? `(${changed_check}) && ${update_cached_value}` : update_cached_value) :
changed_check;
if (this.node.should_cache) {
condition = x`${condition} && (${last} !== (${last} = ${value}))`;
}
block.builders.update.add_conditional(
condition,
updater
);
block.chunks.update.push(b`
if (${condition}) {
${updater}
}`);
}
} else {
const value = this.node.children.length > 0
? stringify((this.node.children[0] as Text).data)
: '""';
? string_literal((this.node.children[0] as Text).data)
: x`""`;
block.builders.hydrate.add_line(`@_document.title = ${value};`);
block.chunks.hydrate.push(b`@_document.title = ${value};`);
}
}
}

@ -1,11 +1,13 @@
import Renderer from '../Renderer';
import Block from '../Block';
import Wrapper from './shared/Wrapper';
import deindent from '../../utils/deindent';
import { b, x } from 'code-red';
import add_event_handlers from './shared/add_event_handlers';
import Window from '../../nodes/Window';
import add_actions from './shared/add_actions';
import { INode } from '../../nodes/interfaces';
import { changed } from './shared/changed';
import { Identifier } from 'estree';
import { TemplateNode } from '../../../interfaces';
const associated_events = {
innerWidth: 'resize',
@ -33,11 +35,11 @@ const readonly = new Set([
export default class WindowWrapper extends Wrapper {
node: Window;
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: INode) {
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: TemplateNode) {
super(renderer, block, parent, node);
}
render(block: Block, _parent_node: string, _parent_nodes: string) {
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
const { renderer } = this;
const { component } = renderer;
@ -73,65 +75,64 @@ export default class WindowWrapper extends Wrapper {
const scrolling_timeout = block.get_unique_name(`scrolling_timeout`);
Object.keys(events).forEach(event => {
const handler_name = block.get_unique_name(`onwindow${event}`);
const id = block.get_unique_name(`onwindow${event}`);
const props = events[event];
if (event === 'scroll') {
// TODO other bidirectional bindings...
block.add_variable(scrolling, 'false');
block.add_variable(clear_scrolling, `() => { ${scrolling} = false }`);
block.add_variable(scrolling, x`false`);
block.add_variable(clear_scrolling, x`() => { ${scrolling} = false }`);
block.add_variable(scrolling_timeout);
const condition = [
bindings.scrollX && `"${bindings.scrollX}" in this._state`,
bindings.scrollY && `"${bindings.scrollY}" in this._state`
].filter(Boolean).join(' || ');
const condition = bindings.scrollX && bindings.scrollY
? x`"${bindings.scrollX}" in this._state || "${bindings.scrollY}" in this._state`
: x`"${bindings.scrollX || bindings.scrollY}" in this._state`;
const x = bindings.scrollX && `this._state.${bindings.scrollX}`;
const y = bindings.scrollY && `this._state.${bindings.scrollY}`;
const scrollX = bindings.scrollX && x`this._state.${bindings.scrollX}`;
const scrollY = bindings.scrollY && x`this._state.${bindings.scrollY}`;
renderer.meta_bindings.add_block(deindent`
renderer.meta_bindings.push(b`
if (${condition}) {
@_scrollTo(${x || '@_window.pageXOffset'}, ${y || '@_window.pageYOffset'});
@_scrollTo(${scrollX || '@_window.pageXOffset'}, ${scrollY || '@_window.pageYOffset'});
}
${x && `${x} = @_window.pageXOffset;`}
${y && `${y} = @_window.pageYOffset;`}
${scrollX && `${scrollX} = @_window.pageXOffset;`}
${scrollY && `${scrollY} = @_window.pageYOffset;`}
`);
block.event_listeners.push(deindent`
block.event_listeners.push(x`
@listen(@_window, "${event}", () => {
${scrolling} = true;
@_clearTimeout(${scrolling_timeout});
${scrolling_timeout} = @_setTimeout(${clear_scrolling}, 100);
ctx.${handler_name}();
#ctx.${id}();
})
`);
} else {
props.forEach(prop => {
renderer.meta_bindings.add_line(
`this._state.${prop.name} = @_window.${prop.value};`
renderer.meta_bindings.push(
b`this._state.${prop.name} = @_window.${prop.value};`
);
});
block.event_listeners.push(deindent`
@listen(@_window, "${event}", ctx.${handler_name})
block.event_listeners.push(x`
@listen(@_window, "${event}", #ctx.${id})
`);
}
component.add_var({
name: handler_name,
name: id.name,
internal: true,
referenced: true
});
component.partly_hoisted.push(deindent`
function ${handler_name}() {
${props.map(prop => `${prop.name} = @_window.${prop.value}; $$invalidate('${prop.name}', ${prop.name});`)}
component.partly_hoisted.push(b`
function ${id}() {
${props.map(prop => b`$$invalidate('${prop.name}', ${prop.name} = @_window.${prop.value});`)}
}
`);
block.builders.init.add_block(deindent`
@add_render_callback(ctx.${handler_name});
block.chunks.init.push(b`
@add_render_callback(#ctx.${id});
`);
component.has_reactive_assignments = true;
@ -139,19 +140,15 @@ export default class WindowWrapper extends Wrapper {
// special case... might need to abstract this out if we add more special cases
if (bindings.scrollX || bindings.scrollY) {
block.builders.update.add_block(deindent`
if (${
[bindings.scrollX, bindings.scrollY].filter(Boolean).map(
b => `changed.${b}`
).join(' || ')
} && !${scrolling}) {
const condition = changed([bindings.scrollX, bindings.scrollY].filter(Boolean));
const scrollX = bindings.scrollX ? x`#ctx.${bindings.scrollX}` : x`@_window.pageXOffset`;
const scrollY = bindings.scrollY ? x`#ctx.${bindings.scrollY}` : x`@_window.pageYOffset`;
block.chunks.update.push(b`
if (${condition} && !${scrolling}) {
${scrolling} = true;
@_clearTimeout(${scrolling_timeout});
@_scrollTo(${
bindings.scrollX ? `ctx.${bindings.scrollX}` : `@_window.pageXOffset`
}, ${
bindings.scrollY ? `ctx.${bindings.scrollY}` : `@_window.pageYOffset`
});
@_scrollTo(${scrollX}, ${scrollY});
${scrolling_timeout} = @_setTimeout(${clear_scrolling}, 100);
}
`);
@ -159,28 +156,28 @@ export default class WindowWrapper extends Wrapper {
// another special case. (I'm starting to think these are all special cases.)
if (bindings.online) {
const handler_name = block.get_unique_name(`onlinestatuschanged`);
const id = block.get_unique_name(`onlinestatuschanged`);
const name = bindings.online;
component.add_var({
name: handler_name,
name: id.name,
internal: true,
referenced: true
});
component.partly_hoisted.push(deindent`
function ${handler_name}() {
${name} = @_navigator.onLine; $$invalidate('${name}', ${name});
component.partly_hoisted.push(b`
function ${id}() {
$$invalidate('${name}', ${name} = @_navigator.onLine);
}
`);
block.builders.init.add_block(deindent`
@add_render_callback(ctx.${handler_name});
block.chunks.init.push(b`
@add_render_callback(#ctx.${id});
`);
block.event_listeners.push(
`@listen(@_window, "online", ctx.${handler_name})`,
`@listen(@_window, "offline", ctx.${handler_name})`
x`@listen(@_window, "online", #ctx.${id})`,
x`@listen(@_window, "offline", #ctx.${id})`
);
component.has_reactive_assignments = true;

@ -1,8 +1,10 @@
import { b, x } from 'code-red';
import Wrapper from './Wrapper';
import Renderer from '../../Renderer';
import Block from '../../Block';
import MustacheTag from '../../../nodes/MustacheTag';
import RawMustacheTag from '../../../nodes/RawMustacheTag';
import { Node } from 'estree';
export default class Tag extends Wrapper {
node: MustacheTag | RawMustacheTag;
@ -16,32 +18,35 @@ export default class Tag extends Wrapper {
rename_this_method(
block: Block,
update: ((value: string) => string)
update: ((value: Node) => (Node | Node[]))
) {
const dependencies = this.node.expression.dynamic_dependencies();
const snippet = this.node.expression.render(block);
let snippet = this.node.expression.manipulate(block);
const value = this.node.should_cache && block.get_unique_name(`${this.var}_value`);
const value = this.node.should_cache && block.get_unique_name(`${this.var.name}_value`);
const content = this.node.should_cache ? value : snippet;
if (this.node.should_cache) block.add_variable(value, `${snippet} + ""`);
snippet = x`${snippet} + ""`;
if (this.node.should_cache) block.add_variable(value, snippet); // TODO may need to coerce snippet to string
if (dependencies.length > 0) {
const changed_check = (
(block.has_outros ? `!#current || ` : '') +
dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')
);
let condition = x`#changed.${dependencies[0]}`;
for (let i = 1; i < dependencies.length; i += 1) {
condition = x`${condition} || #changed.${dependencies[i]}`;
}
if (block.has_outros) {
condition = x`!#current || ${condition}`;
}
const update_cached_value = `${value} !== (${value} = ${snippet} + "")`;
const update_cached_value = x`${value} !== (${value} = ${snippet})`;
const condition = this.node.should_cache
? `(${changed_check}) && ${update_cached_value}`
: changed_check;
if (this.node.should_cache) {
condition = x`${condition} && ${update_cached_value}`;
}
block.builders.update.add_conditional(
condition,
update(content)
);
block.chunks.update.push(b`if (${condition}) ${update(content as Node)}`);
}
return { init: content };

@ -1,23 +1,25 @@
import Renderer from '../../Renderer';
import Block from '../../Block';
import { INode } from '../../../nodes/interfaces';
import { x } from 'code-red';
import { TemplateNode } from '../../../../interfaces';
import { Identifier } from 'estree';
export default class Wrapper {
renderer: Renderer;
parent: Wrapper;
node: INode;
node: TemplateNode;
prev: Wrapper | null;
next: Wrapper | null;
var: string;
var: Identifier;
can_use_innerhtml: boolean;
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: INode
node: TemplateNode
) {
this.node = node;
@ -42,30 +44,30 @@ export default class Wrapper {
if (this.parent) this.parent.cannot_use_innerhtml();
}
get_or_create_anchor(block: Block, parent_node: string, parent_nodes: string) {
get_or_create_anchor(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
// TODO use this in EachBlock and IfBlock — tricky because
// children need to be created first
const needs_anchor = this.next ? !this.next.is_dom_node() : !parent_node || !this.parent.is_dom_node();
const anchor = needs_anchor
? block.get_unique_name(`${this.var}_anchor`)
: (this.next && this.next.var) || 'null';
? block.get_unique_name(`${this.var.name}_anchor`)
: (this.next && this.next.var) || { type: 'Identifier', name: 'null' };
if (needs_anchor) {
block.add_element(
anchor,
`@empty()`,
parent_nodes && `@empty()`,
parent_node
x`@empty()`,
parent_nodes && x`@empty()`,
parent_node as Identifier
);
}
return anchor;
}
get_update_mount_node(anchor: string) {
return (this.parent && this.parent.is_dom_node())
get_update_mount_node(anchor: Identifier): Identifier {
return ((this.parent && this.parent.is_dom_node())
? this.parent.var
: `${anchor}.parentNode`;
: x`${anchor}.parentNode`) as Identifier;
}
is_dom_node() {
@ -76,7 +78,7 @@ export default class Wrapper {
);
}
render(_block: Block, _parent_node: string, _parent_nodes: string) {
render(_block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
throw Error('Wrapper class is not renderable');
}
}

@ -1,3 +1,4 @@
import { b, x } from 'code-red';
import Block from '../../Block';
import Action from '../../../nodes/Action';
import Component from '../../../Component';
@ -14,35 +15,42 @@ export default function add_actions(
let dependencies;
if (expression) {
snippet = expression.render(block);
snippet = expression.manipulate(block);
dependencies = expression.dynamic_dependencies();
}
const name = block.get_unique_name(
const id = block.get_unique_name(
`${action.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_action`
);
block.add_variable(name);
block.add_variable(id);
const fn = component.qualify(action.name);
block.builders.mount.add_line(
`${name} = ${fn}.call(null, ${target}${snippet ? `, ${snippet}` : ''}) || {};`
block.chunks.mount.push(
b`${id} = ${fn}.call(null, ${target}, ${snippet}) || {};`
);
if (dependencies && dependencies.length > 0) {
let conditional = `typeof ${name}.update === 'function' && `;
const deps = dependencies.map(dependency => `changed.${dependency}`).join(' || ');
conditional += dependencies.length > 1 ? `(${deps})` : deps;
let condition = x`@is_function(${id}.update)`;
block.builders.update.add_conditional(
conditional,
`${name}.update.call(null, ${snippet});`
// TODO can this case be handled more elegantly?
if (dependencies.length > 0) {
let changed = x`#changed.${dependencies[0]}`;
for (let i = 1; i < dependencies.length; i += 1) {
changed = x`${changed} || #changed.${dependencies[i]}`;
}
condition = x`${condition} && ${changed}`;
}
block.chunks.update.push(
b`if (${condition}) ${id}.update.call(null, ${snippet});`
);
}
block.builders.destroy.add_line(
`if (${name} && typeof ${name}.destroy === 'function') ${name}.destroy();`
block.chunks.destroy.push(
b`if (${id} && @is_function(${id}.destroy)) ${id}.destroy();`
);
});
}

@ -1,5 +1,9 @@
import Block from '../../Block';
import EventHandler from '../../../nodes/EventHandler';
import { x, p } from 'code-red';
const TRUE = x`true`;
const FALSE = x`false`;
export default function add_event_handlers(
block: Block,
@ -8,35 +12,28 @@ export default function add_event_handlers(
) {
handlers.forEach(handler => {
let snippet = handler.render(block);
if (handler.modifiers.has('preventDefault')) snippet = `@prevent_default(${snippet})`;
if (handler.modifiers.has('stopPropagation')) snippet = `@stop_propagation(${snippet})`;
if (handler.modifiers.has('self')) snippet = `@self(${snippet})`;
let opts_string = '';
if (block.renderer.options.dev) {
if (handler.modifiers.has('stopPropagation')) {
opts_string = ', true';
}
if (handler.modifiers.has('preventDefault')) snippet = x`@prevent_default(${snippet})`;
if (handler.modifiers.has('stopPropagation')) snippet = x`@stop_propagation(${snippet})`;
if (handler.modifiers.has('self')) snippet = x`@self(${snippet})`;
if (handler.modifiers.has('preventDefault')) {
opts_string = ', true' + opts_string;
} else if (opts_string) {
opts_string = ', false' + opts_string;
}
}
const args = [];
const opts = ['passive', 'once', 'capture'].filter(mod => handler.modifiers.has(mod));
if (opts.length) {
opts_string = (opts.length === 1 && opts[0] === 'capture')
? ', true'
: `, { ${opts.map(opt => `${opt}: true`).join(', ')} }`;
} else if (opts_string) {
opts_string = ', false' + opts_string;
args.push((opts.length === 1 && opts[0] === 'capture')
? TRUE
: x`{ ${opts.map(opt => p`${opt}: true`)} }`);
} else if (block.renderer.options.dev) {
args.push(FALSE);
}
if (block.renderer.options.dev) {
args.push(handler.modifiers.has('stopPropagation') ? TRUE : FALSE);
args.push(handler.modifiers.has('preventDefault') ? TRUE : FALSE);
}
block.event_listeners.push(
`@listen(${target}, "${handler.name}", ${snippet}${opts_string})`
x`@listen(${target}, "${handler.name}", ${snippet}, ${args})`
);
});
}

@ -1,14 +1,15 @@
import flatten_reference from '../../../utils/flatten_reference';
import deindent from '../../../utils/deindent';
import { b, x } from 'code-red';
import Component from '../../../Component';
import Block from '../../Block';
import Binding from '../../../nodes/Binding';
import { Identifier } from 'estree';
export default function bind_this(component: Component, block: Block, binding: Binding, variable: string) {
const fn = component.get_unique_name(`${variable}_binding`);
export default function bind_this(component: Component, block: Block, binding: Binding, variable: Identifier) {
const fn = component.get_unique_name(`${variable.name}_binding`);
component.add_var({
name: fn,
name: fn.name,
internal: true,
referenced: true
});
@ -17,33 +18,36 @@ export default function bind_this(component: Component, block: Block, binding: B
let object;
let body;
if (binding.is_contextual && binding.expression.node.type === 'Identifier') {
if (binding.is_contextual && binding.raw_expression.type === 'Identifier') {
// bind:x={y} — we can't just do `y = x`, we need to
// to `array[index] = x;
const { name } = binding.expression.node;
const { name } = binding.raw_expression;
const { snippet } = block.bindings.get(name);
lhs = snippet;
body = `${lhs} = $$value`; // TODO we need to invalidate... something
body = b`${lhs} = $$value`; // TODO we need to invalidate... something
} else {
object = flatten_reference(binding.expression.node).name;
lhs = component.source.slice(binding.expression.node.start, binding.expression.node.end).trim();
object = flatten_reference(binding.raw_expression).name;
lhs = binding.raw_expression;
body = binding.expression.node.type === 'Identifier'
? deindent`
${component.invalidate(object, `${lhs} = $$value`)};
body = binding.raw_expression.type === 'Identifier'
? b`
${component.invalidate(object, x`${lhs} = $$value`)};
`
: deindent`
: b`
${lhs} = $$value;
${component.invalidate(object)};
`;
}
const contextual_dependencies = Array.from(binding.expression.contextual_dependencies);
const contextual_dependencies: Identifier[] = Array.from(binding.expression.contextual_dependencies).map(name => ({
type: 'Identifier',
name
}));
if (contextual_dependencies.length) {
component.partly_hoisted.push(deindent`
function ${fn}(${['$$value', ...contextual_dependencies].join(', ')}) {
component.partly_hoisted.push(b`
function ${fn}($$value, ${contextual_dependencies}) {
if (${lhs} === $$value) return;
@binding_callbacks[$$value ? 'unshift' : 'push'](() => {
${body}
@ -52,37 +56,39 @@ export default function bind_this(component: Component, block: Block, binding: B
`);
const args = [];
for (const arg of contextual_dependencies) {
args.push(arg);
block.add_variable(arg, `ctx.${arg}`);
for (const id of contextual_dependencies) {
args.push(id);
block.add_variable(id, x`#ctx.${id}`);
}
const assign = block.get_unique_name(`assign_${variable}`);
const unassign = block.get_unique_name(`unassign_${variable}`);
const assign = block.get_unique_name(`assign_${variable.name}`);
const unassign = block.get_unique_name(`unassign_${variable.name}`);
block.builders.init.add_block(deindent`
const ${assign} = () => ctx.${fn}(${[variable].concat(args).join(', ')});
const ${unassign} = () => ctx.${fn}(${['null'].concat(args).join(', ')});
block.chunks.init.push(b`
const ${assign} = () => #ctx.${fn}(${variable}, ${args});
const ${unassign} = () => #ctx.${fn}(null, ${args});
`);
const condition = Array.from(contextual_dependencies).map(name => `${name} !== ctx.${name}`).join(' || ');
const condition = Array.from(contextual_dependencies)
.map(name => x`${name} !== #ctx.${name}`)
.reduce((lhs, rhs) => x`${lhs} || ${rhs}`);
// we push unassign and unshift assign so that references are
// nulled out before they're created, to avoid glitches
// with shifting indices
block.builders.update.add_line(deindent`
block.chunks.update.push(b`
if (${condition}) {
${unassign}();
${args.map(a => `${a} = ctx.${a}`).join(', ')};
${args.map(a => b`${a} = #ctx.${a}`)};
${assign}();
}`
);
block.builders.destroy.add_line(`${unassign}();`);
return `${assign}();`;
block.chunks.destroy.push(b`${unassign}();`);
return b`${assign}();`;
}
component.partly_hoisted.push(deindent`
component.partly_hoisted.push(b`
function ${fn}($$value) {
@binding_callbacks[$$value ? 'unshift' : 'push'](() => {
${body}
@ -90,6 +96,6 @@ export default function bind_this(component: Component, block: Block, binding: B
}
`);
block.builders.destroy.add_line(`ctx.${fn}(null);`);
return `ctx.${fn}(${variable});`;
block.chunks.destroy.push(b`#ctx.${fn}(null);`);
return b`#ctx.${fn}(${variable});`;
}

@ -0,0 +1,7 @@
import { x } from 'code-red';
export function changed(dependencies: string[]) {
return dependencies
.map(d => x`#changed.${d}`)
.reduce((lhs, rhs) => x`${lhs} || ${rhs}`);
}

@ -1,9 +1,18 @@
import Let from '../../../nodes/Let';
import { x } from 'code-red';
export function get_context_merger(lets: Let[]) {
if (lets.length === 0) return null;
const input = lets.map(l => l.value ? `${l.name}: ${l.value}` : l.name).join(', ');
const input = {
type: 'ObjectPattern',
properties: lets.map(l => ({
type: 'Property',
kind: 'init',
key: l.name,
value: l.value || l.name
}))
};
const names = new Set();
lets.forEach(l => {
@ -12,7 +21,19 @@ export function get_context_merger(lets: Let[]) {
});
});
const output = Array.from(names).join(', ');
const output = {
type: 'ObjectExpression',
properties: Array.from(names).map(name => {
const id = { type: 'Identifier', name };
return `({ ${input} }) => ({ ${output} })`;
return {
type: 'Property',
kind: 'init',
key: id,
value: id
};
})
};
return x`(${input}) => (${output})`;
}

@ -0,0 +1,3 @@
export function is_head(node) {
return node && node.type === 'MemberExpression' && node.object.name === '@_document' && node.property.name === 'head';
}

@ -13,6 +13,8 @@ import Text from './handlers/Text';
import Title from './handlers/Title';
import { AppendTarget, CompileOptions } from '../../interfaces';
import { INode } from '../nodes/interfaces';
import { Expression, TemplateLiteral, Identifier } from 'estree';
import { escape_template } from '../utils/stringify';
type Handler = (node: any, renderer: Renderer, options: CompileOptions) => void;
@ -43,17 +45,63 @@ export interface RenderOptions extends CompileOptions{
export default class Renderer {
has_bindings = false;
code = '';
name: Identifier;
stack: Array<{ current: { value: string }; literal: TemplateLiteral }> = [];
current: { value: string }; // TODO can it just be `current: string`?
literal: TemplateLiteral;
targets: AppendTarget[] = [];
append(code: string) {
if (this.targets.length) {
const target = this.targets[this.targets.length - 1];
const slot_name = target.slot_stack[target.slot_stack.length - 1];
target.slots[slot_name] += code;
} else {
this.code += code;
constructor({ name }) {
this.name = name;
this.push();
}
add_string(str: string) {
this.current.value += escape_template(str);
}
add_expression(node: Expression) {
this.literal.quasis.push({
type: 'TemplateElement',
value: { raw: this.current.value, cooked: null },
tail: false
});
this.literal.expressions.push(node);
this.current.value = '';
}
push() {
const current = this.current = { value: '' };
const literal = this.literal = {
type: 'TemplateLiteral',
expressions: [],
quasis: []
};
this.stack.push({ current, literal });
}
pop() {
this.literal.quasis.push({
type: 'TemplateElement',
value: { raw: this.current.value, cooked: null },
tail: true
});
const popped = this.stack.pop();
const last = this.stack[this.stack.length - 1];
if (last) {
this.literal = last.literal;
this.current = last.current;
}
return popped.literal;
}
render(nodes: INode[], options: RenderOptions) {

@ -1,16 +1,20 @@
import Renderer, { RenderOptions } from '../Renderer';
import { snip } from '../../utils/snip';
import AwaitBlock from '../../nodes/AwaitBlock';
import { x } from 'code-red';
export default function(node: AwaitBlock, renderer: Renderer, options: RenderOptions) {
renderer.append('${(function(__value) { if(@is_promise(__value)) return `');
renderer.push();
renderer.render(node.pending.children, options);
const pending = renderer.pop();
renderer.append('`; return function(' + (node.value || '') + ') { return `');
renderer.push();
renderer.render(node.then.children, options);
const then = renderer.pop();
const snippet = snip(node.expression);
renderer.append(`\`;}(__value);}(${snippet})) }`);
renderer.add_expression(x`
(function(__value) {
if (@is_promise(__value)) return ${pending};
return (function(${node.value}) { return ${then}; }(__value));
}(${node.expression.node}))
`);
}

@ -1,8 +1,10 @@
import Renderer, { RenderOptions } from '../Renderer';
import Comment from '../../nodes/Comment';
export default function(node: Comment, renderer: Renderer, options: RenderOptions) {
if (options.preserveComments) {
renderer.append(`<!--${node.data}-->`);
}
export default function(_node: Comment, _renderer: Renderer, _options: RenderOptions) {
// TODO preserve comments
// if (options.preserveComments) {
// renderer.append(`<!--${node.data}-->`);
// }
}

@ -1,19 +1,16 @@
import { stringify } from '../../utils/stringify';
import DebugTag from '../../nodes/DebugTag';
import Renderer, { RenderOptions } from '../Renderer';
import { x, p } from 'code-red';
export default function(node: DebugTag, renderer: Renderer, options: RenderOptions) {
if (!options.dev) return;
const filename = options.filename || null;
const { line, column } = options.locate(node.start + 1);
const obj = node.expressions.length === 0
? `{}`
: `{ ${node.expressions
.map(e => e.node.name)
.join(', ')} }`;
const str = '${@debug(' + `${filename && stringify(filename)}, ${line}, ${column}, ${obj})}`;
const obj = x`{
${node.expressions.map(e => p`${e.node.name}`)}
}`;
renderer.append(str);
renderer.add_expression(x`@debug(${filename ? x`"${filename}"` : x`null`}, ${line}, ${column}, ${obj})`);
}

@ -1,29 +1,24 @@
import { snip } from '../../utils/snip';
import Renderer, { RenderOptions } from '../Renderer';
import EachBlock from '../../nodes/EachBlock';
import { x } from 'code-red';
export default function(node: EachBlock, renderer: Renderer, options: RenderOptions) {
const snippet = snip(node.expression);
const { start, end } = node.context_node;
const ctx = node.index
? `([✂${start}-${end}✂], ${node.index})`
: `([✂${start}-${end}✂])`;
const open = `\${${node.else ? `${snippet}.length ? ` : ''}@each(${snippet}, ${ctx} => \``;
renderer.append(open);
const args = [node.context_node];
if (node.index) args.push({ type: 'Identifier', name: node.index });
renderer.push();
renderer.render(node.children, options);
const result = renderer.pop();
const close = `\`)`;
renderer.append(close);
const consequent = x`@each(${node.expression.node}, (${args}) => ${result})`;
if (node.else) {
renderer.append(` : \``);
renderer.push();
renderer.render(node.else.children, options);
renderer.append(`\``);
}
const alternate = renderer.pop();
renderer.append('}');
renderer.add_expression(x`${node.expression.node}.length ? ${consequent} : ${alternate}`);
} else {
renderer.add_expression(consequent);
}
}

@ -1,12 +1,12 @@
import { is_void, quote_prop_if_necessary, quote_name_if_necessary } from '../../../utils/names';
import { is_void } from '../../../utils/names';
import Attribute from '../../nodes/Attribute';
import Class from '../../nodes/Class';
import { snip } from '../../utils/snip';
import { stringify_attribute, stringify_class_attribute } from '../../utils/stringify_attribute';
import { get_attribute_value, get_class_attribute_value } from './shared/get_attribute_value';
import { get_slot_scope } from './shared/get_slot_scope';
import Renderer, { RenderOptions } from '../Renderer';
import Element from '../../nodes/Element';
import Text from '../../nodes/Text';
import { x } from 'code-red';
import Expression from '../../nodes/shared/Expression';
// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7
const boolean_attributes = new Set([
@ -52,11 +52,8 @@ const boolean_attributes = new Set([
export default function(node: Element, renderer: Renderer, options: RenderOptions & {
slot_scopes: Map<any, any>;
}) {
let opening_tag = `<${node.name}`;
// awkward special case
let node_contents;
let value;
const contenteditable = (
node.name !== 'textarea' &&
@ -65,29 +62,21 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
);
const slot = node.get_static_attribute_value('slot');
const component = node.find_nearest(/InlineComponent/);
if (slot && component) {
const slot = node.attributes.find((attribute) => attribute.name === 'slot');
const slot_name = (slot.chunks[0] as Text).data;
const target = renderer.targets[renderer.targets.length - 1];
target.slot_stack.push(slot_name);
target.slots[slot_name] = '';
const lets = node.lets;
const seen = new Set(lets.map(l => l.name));
const nearest_inline_component = node.find_nearest(/InlineComponent/);
component.lets.forEach(l => {
if (!seen.has(l.name)) lets.push(l);
});
options.slot_scopes.set(slot_name, get_slot_scope(node.lets));
if (slot && nearest_inline_component) {
renderer.push();
}
const class_expression = node.classes.map((class_directive: Class) => {
const { expression, name } = class_directive;
const snippet = expression ? snip(expression) : `ctx${quote_prop_if_necessary(name)}`;
return `${snippet} ? "${name}" : ""`;
}).join(', ');
renderer.add_string(`<${node.name}`);
const class_expression = node.classes.length > 0 && node.classes
.map((class_directive: Class) => {
const { expression, name } = class_directive;
const snippet = expression ? expression.node : x`#ctx.${name}`;
return x`${snippet} ? "${name}" : ""`;
})
.reduce((lhs, rhs) => x`${lhs} + ' ' + ${rhs}`);
let add_class_attribute = class_expression ? true : false;
@ -96,53 +85,58 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
const args = [];
node.attributes.forEach(attribute => {
if (attribute.is_spread) {
args.push(snip(attribute.expression));
args.push(attribute.expression.node);
} else {
if (attribute.name === 'value' && node.name === 'textarea') {
node_contents = stringify_attribute(attribute, true);
node_contents = get_attribute_value(attribute);
} else if (attribute.is_true) {
args.push(`{ ${quote_name_if_necessary(attribute.name)}: true }`);
args.push(x`{ ${attribute.name}: true }`);
} else if (
boolean_attributes.has(attribute.name) &&
attribute.chunks.length === 1 &&
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
args.push(`{ ${quote_name_if_necessary(attribute.name)}: ${snip(attribute.chunks[0])} }`);
args.push(x`{ ${attribute.name}: ${(attribute.chunks[0] as Expression).node} }`);
} else if (attribute.name === 'class' && class_expression) {
// Add class expression
args.push(`{ ${quote_name_if_necessary(attribute.name)}: [\`${stringify_class_attribute(attribute)}\`, \`\${${class_expression}}\`].join(' ').trim() }`);
args.push(x`{ ${attribute.name}: [${get_class_attribute_value(attribute)}, ${class_expression}].join(' ').trim() }`);
} else {
args.push(`{ ${quote_name_if_necessary(attribute.name)}: \`${attribute.name === 'class' ? stringify_class_attribute(attribute) : stringify_attribute(attribute, true)}\` }`);
args.push(x`{ ${attribute.name}: ${attribute.name === 'class' ? get_class_attribute_value(attribute) : get_attribute_value(attribute)} }`);
}
}
});
opening_tag += "${@spread([" + args.join(', ') + "])}";
renderer.add_expression(x`@spread([${args}])`);
} else {
node.attributes.forEach((attribute: Attribute) => {
if (attribute.type !== 'Attribute') return;
if (attribute.name === 'value' && node.name === 'textarea') {
node_contents = stringify_attribute(attribute, true);
node_contents = get_attribute_value(attribute);
} else if (attribute.is_true) {
opening_tag += ` ${attribute.name}`;
renderer.add_string(` ${attribute.name}`);
} else if (
boolean_attributes.has(attribute.name) &&
attribute.chunks.length === 1 &&
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
opening_tag += '${' + snip(attribute.chunks[0]) + ' ? " ' + attribute.name + '" : "" }';
renderer.add_string(` `);
renderer.add_expression(x`${(attribute.chunks[0] as Expression).node} ? "${attribute.name}" : ""`);
} else if (attribute.name === 'class' && class_expression) {
add_class_attribute = false;
opening_tag += ` class="\${[\`${stringify_class_attribute(attribute)}\`, ${class_expression}].join(' ').trim() }"`;
renderer.add_string(` class="`);
renderer.add_expression(x`[${get_class_attribute_value(attribute)}, ${class_expression}].join(' ').trim()`);
renderer.add_string(`"`);
} else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') {
const { name } = attribute;
const snippet = snip(attribute.chunks[0]);
opening_tag += '${@add_attribute("' + name + '", ' + snippet + ', ' + (boolean_attributes.has(name) ? 1 : 0) + ')}';
const snippet = (attribute.chunks[0] as Expression).node;
renderer.add_expression(x`@add_attribute("${name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`);
} else {
opening_tag += ` ${attribute.name}="${attribute.name === 'class' ? stringify_class_attribute(attribute) : stringify_attribute(attribute, true)}"`;
renderer.add_string(` ${attribute.name}="`);
renderer.add_expression(attribute.name === 'class' ? get_class_attribute_value(attribute) : get_attribute_value(attribute));
renderer.add_string(`"`);
}
});
}
@ -157,38 +151,62 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
if (name === 'group') {
// TODO server-render group bindings
} else if (contenteditable && (name === 'textContent' || name === 'innerHTML')) {
node_contents = snip(expression);
value = name === 'textContent' ? '@escape($$value)' : '$$value';
node_contents = expression.node;
// TODO where was this used?
// value = name === 'textContent' ? x`@escape($$value)` : x`$$value`;
} else if (binding.name === 'value' && node.name === 'textarea') {
const snippet = snip(expression);
node_contents = '${(' + snippet + ') || ""}';
const snippet = expression.node;
node_contents = x`${snippet} || ""`;
} else {
const snippet = snip(expression);
opening_tag += '${@add_attribute("' + name + '", ' + snippet + ', 1)}';
const snippet = expression.node;
renderer.add_expression(x`@add_attribute("${name}", ${snippet}, 1)`);
}
});
if (add_class_attribute) {
opening_tag += `\${@add_classes([${class_expression}].join(' ').trim())}`;
renderer.add_expression(x`@add_classes([${class_expression}].join(' ').trim())`);
}
opening_tag += '>';
renderer.append(opening_tag);
renderer.add_string('>');
if (node_contents !== undefined) {
if (contenteditable) {
renderer.append('${($$value => $$value === void 0 ? `');
renderer.push();
renderer.render(node.children, options);
renderer.append('` : ' + value + ')(' + node_contents + ')}');
const result = renderer.pop();
renderer.add_expression(x`($$value => $$value === void 0 ? ${result} : $$value)(${node_contents})`);
} else {
renderer.append(node_contents);
renderer.add_expression(node_contents);
}
if (!is_void(node.name)) {
renderer.add_string(`</${node.name}>`);
}
} else if (slot && nearest_inline_component) {
renderer.render(node.children, options);
if (!is_void(node.name)) {
renderer.add_string(`</${node.name}>`);
}
const lets = node.lets;
const seen = new Set(lets.map(l => l.name.name));
nearest_inline_component.lets.forEach(l => {
if (!seen.has(l.name.name)) lets.push(l);
});
options.slot_scopes.set(slot, {
input: get_slot_scope(node.lets),
output: renderer.pop()
});
} else {
renderer.render(node.children, options);
}
if (!is_void(node.name)) {
renderer.append(`</${node.name}>`);
if (!is_void(node.name)) {
renderer.add_string(`</${node.name}>`);
}
}
}

@ -1,10 +1,11 @@
import Renderer, { RenderOptions } from '../Renderer';
import Head from '../../nodes/Head';
import { x } from 'code-red';
export default function(node: Head, renderer: Renderer, options: RenderOptions) {
renderer.append('${($$result.head += `');
renderer.push();
renderer.render(node.children, options);
const result = renderer.pop();
renderer.append('`, "")}');
renderer.add_expression(x`($$result.head += ${result}, "")`);
}

@ -1,7 +1,6 @@
import { snip } from '../../utils/snip';
import Renderer, { RenderOptions } from '../Renderer';
import RawMustacheTag from '../../nodes/RawMustacheTag';
export default function(node: RawMustacheTag, renderer: Renderer, _options: RenderOptions) {
renderer.append('${' + snip(node.expression) + '}');
renderer.add_expression(node.expression.node);
}

@ -1,18 +1,17 @@
import { snip } from '../../utils/snip';
import IfBlock from '../../nodes/IfBlock';
import Renderer, { RenderOptions } from '../Renderer';
export default function(node: IfBlock, renderer: Renderer, options: RenderOptions) {
const snippet = snip(node.expression);
import { x } from 'code-red';
renderer.append('${ ' + snippet + ' ? `');
export default function(node: IfBlock, renderer: Renderer, options: RenderOptions) {
const condition = node.expression.node;
renderer.push();
renderer.render(node.children, options);
const consequent = renderer.pop();
renderer.append('` : `');
if (node.else) {
renderer.render(node.else.children, options);
}
renderer.push();
if (node.else) renderer.render(node.else.children, options);
const alternate = renderer.pop();
renderer.append('` }');
renderer.add_expression(x`${condition} ? ${consequent} : ${alternate}`);
}

@ -1,36 +1,19 @@
import { escape, escape_template, stringify } from '../../utils/stringify';
import { quote_name_if_necessary } from '../../../utils/names';
import { snip } from '../../utils/snip';
import { string_literal } from '../../utils/stringify';
import Renderer, { RenderOptions } from '../Renderer';
import { stringify_props } from '../../utils/stringify_props';
import { get_slot_scope } from './shared/get_slot_scope';
import { AppendTarget } from '../../../interfaces';
import InlineComponent from '../../nodes/InlineComponent';
import { INode } from '../../nodes/interfaces';
import Text from '../../nodes/Text';
function stringify_attribute(chunk: INode) {
if (chunk.type === 'Text') {
return escape_template(escape((chunk as Text).data));
}
return '${@escape(' + snip(chunk) + ')}';
}
function get_attribute_value(attribute) {
if (attribute.is_true) return `true`;
if (attribute.chunks.length === 0) return `''`;
if (attribute.chunks.length === 1) {
const chunk = attribute.chunks[0];
if (chunk.type === 'Text') {
return stringify(chunk.data);
}
return snip(chunk);
}
return '`' + attribute.chunks.map(stringify_attribute).join('') + '`';
import { p, x } from 'code-red';
function get_prop_value(attribute) {
if (attribute.is_true) return x`true`;
if (attribute.chunks.length === 0) return x`''`;
return attribute.chunks
.map(chunk => {
if (chunk.type === 'Text') return string_literal(chunk.data);
return chunk.node;
})
.reduce((lhs, rhs) => x`${lhs} + ${rhs}`);
}
export default function(node: InlineComponent, renderer: Renderer, options: RenderOptions) {
@ -41,10 +24,10 @@ export default function(node: InlineComponent, renderer: Renderer, options: Rend
renderer.has_bindings = true;
// TODO this probably won't work for contextual bindings
const snippet = snip(binding.expression);
const snippet = binding.expression.node;
binding_props.push(`${binding.name}: ${snippet}`);
binding_fns.push(`${binding.name}: $$value => { ${snippet} = $$value; $$settled = false }`);
binding_props.push(p`${binding.name}: ${snippet}`);
binding_fns.push(p`${binding.name}: $$value => { ${snippet} = $$value; $$settled = false }`);
});
const uses_spread = node.attributes.find(attr => attr.is_spread);
@ -52,65 +35,62 @@ export default function(node: InlineComponent, renderer: Renderer, options: Rend
let props;
if (uses_spread) {
props = `@_Object.assign(${
props = x`@_Object.assign(${
node.attributes
.map(attribute => {
if (attribute.is_spread) {
return snip(attribute.expression);
return attribute.expression.node;
} else {
return `{ ${quote_name_if_necessary(attribute.name)}: ${get_attribute_value(attribute)} }`;
return x`{ ${attribute.name}: ${get_prop_value(attribute)} }`;
}
})
.concat(binding_props.map(p => `{ ${p} }`))
.join(', ')
.concat(binding_props.map(p => x`{ ${p} }`))
})`;
} else {
props = stringify_props(
node.attributes
.map(attribute => `${quote_name_if_necessary(attribute.name)}: ${get_attribute_value(attribute)}`)
.concat(binding_props)
);
props = x`{
${node.attributes.map(attribute => p`${attribute.name}: ${get_prop_value(attribute)}`)},
${binding_props}
}`;
}
const bindings = stringify_props(binding_fns);
const bindings = x`{
${binding_fns}
}`;
const expression = (
node.name === 'svelte:self'
? '__svelte:self__' // TODO conflict-proof this
? renderer.name
: node.name === 'svelte:component'
? `((${snip(node.expression)}) || @missing_component)`
: node.name
? x`(${node.expression.node}) || @missing_component`
: node.name.split('.').reduce(((lhs, rhs) => x`${lhs}.${rhs}`) as any)
);
const slot_fns = [];
if (node.children.length) {
const target: AppendTarget = {
slots: { default: '' },
slot_stack: ['default']
};
renderer.targets.push(target);
const slot_scopes = new Map();
slot_scopes.set('default', get_slot_scope(node.lets));
renderer.push();
renderer.render(node.children, Object.assign({}, options, {
slot_scopes
}));
Object.keys(target.slots).forEach(name => {
const slot_scope = slot_scopes.get(name);
slot_scopes.set('default', {
input: get_slot_scope(node.lets),
output: renderer.pop()
});
slot_scopes.forEach(({ input, output }, name) => {
slot_fns.push(
`${quote_name_if_necessary(name)}: (${slot_scope}) => \`${target.slots[name]}\``
p`${name}: (${input}) => ${output}`
);
});
renderer.targets.pop();
}
const slots = stringify_props(slot_fns);
const slots = x`{
${slot_fns}
}`;
renderer.append(`\${@validate_component(${expression}, '${node.name}').$$render($$result, ${props}, ${bindings}, ${slots})}`);
renderer.add_expression(x`@validate_component(${expression}, "${node.name}").$$render($$result, ${props}, ${bindings}, ${slots})`);
}

@ -1,18 +1,18 @@
import { quote_prop_if_necessary } from '../../../utils/names';
import get_slot_data from '../../utils/get_slot_data';
import Renderer, { RenderOptions } from '../Renderer';
import Slot from '../../nodes/Slot';
import { x } from 'code-red';
export default function(node: Slot, renderer: Renderer, options: RenderOptions) {
const prop = quote_prop_if_necessary(node.slot_name);
const slot_data = get_slot_data(node.values, true);
const arg = slot_data.length > 0 ? `{ ${slot_data.join(', ')} }` : '{}';
renderer.append(`\${$$slots${prop} ? $$slots${prop}(${arg}) : \``);
const slot_data = get_slot_data(node.values);
renderer.push();
renderer.render(node.children, options);
const result = renderer.pop();
renderer.append(`\`}`);
renderer.add_expression(x`
$$slots.${node.slot_name}
? $$slots.${node.slot_name}(${slot_data})
: ${result}
`);
}

@ -1,13 +1,15 @@
import { snip } from '../../utils/snip';
import Renderer, { RenderOptions } from '../Renderer';
import { x } from 'code-red';
export default function(node, renderer: Renderer, _options: RenderOptions) {
const snippet = snip(node.expression);
const snippet = node.expression.node;
renderer.append(
renderer.add_expression(
node.parent &&
node.parent.type === 'Element' &&
node.parent.name === 'style'
? '${' + snippet + '}'
: '${@escape(' + snippet + ')}'
? snippet
: x`@escape(${snippet})`
);
}

@ -1,4 +1,4 @@
import { escape_html, escape_template, escape } from '../../utils/stringify';
import { escape_html } from '../../utils/stringify';
import Renderer, { RenderOptions } from '../Renderer';
import Text from '../../nodes/Text';
import Element from '../../nodes/Element';
@ -13,5 +13,6 @@ export default function(node: Text, renderer: Renderer, _options: RenderOptions)
// unless this Text node is inside a <script> or <style> element, escape &,<,>
text = escape_html(text);
}
renderer.append(escape(escape_template(text)));
renderer.add_string(text);
}

@ -2,9 +2,9 @@ import Renderer, { RenderOptions } from '../Renderer';
import Title from '../../nodes/Title';
export default function(node: Title, renderer: Renderer, options: RenderOptions) {
renderer.append(`<title>`);
renderer.add_string(`<title>`);
renderer.render(node.children, options);
renderer.append(`</title>`);
renderer.add_string(`</title>`);
}

@ -0,0 +1,28 @@
import Attribute from '../../../nodes/Attribute';
import { string_literal } from '../../../utils/stringify';
import Text from '../../../nodes/Text';
import { x } from 'code-red';
import Expression from '../../../nodes/shared/Expression';
import { Expression as ESTreeExpression } from 'estree';
export function get_class_attribute_value(attribute: Attribute): ESTreeExpression {
// handle special case — `class={possiblyUndefined}` with scoped CSS
if (attribute.chunks.length === 2 && (attribute.chunks[1] as Text).synthetic) {
const value = (attribute.chunks[0] as Expression).node;
return x`@escape(@null_to_empty(${value})) + "${(attribute.chunks[1] as Text).data}"`;
}
return get_attribute_value(attribute);
}
export function get_attribute_value(attribute: Attribute): ESTreeExpression {
if (attribute.chunks.length === 0) return x`""`;
return attribute.chunks
.map((chunk) => {
return chunk.type === 'Text'
? string_literal(chunk.data.replace(/"/g, '&quot;')) as ESTreeExpression
: x`@escape(${chunk.node})`;
})
.reduce((lhs, rhs) => x`${lhs} + ${rhs}`);
}

@ -1,6 +1,21 @@
import Let from '../../../nodes/Let';
import { ObjectPattern } from 'estree';
export function get_slot_scope(lets: Let[]) {
if (lets.length === 0) return '';
return `{ ${lets.map(l => l.value ? `${l.name}: ${l.value}` : l.name).join(', ')} }`;
export function get_slot_scope(lets: Let[]): ObjectPattern {
if (lets.length === 0) return null;
return {
type: 'ObjectPattern',
properties: lets.map(l => {
return {
type: 'Property',
kind: 'init',
method: false,
shorthand: false,
computed: false,
key: l.name,
value: l.value || l.name
};
})
};
}

@ -1,17 +1,20 @@
import deindent from '../utils/deindent';
import { b } from 'code-red';
import Component from '../Component';
import { CompileOptions } from '../../interfaces';
import { stringify } from '../utils/stringify';
import { string_literal } from '../utils/stringify';
import Renderer from './Renderer';
import { extract_names } from '../utils/scope';
import { INode } from '../nodes/interfaces';
import { INode as TemplateNode } from '../nodes/interfaces'; // TODO
import Text from '../nodes/Text';
import { extract_names } from '../utils/scope';
import { LabeledStatement, Statement, ExpressionStatement, AssignmentExpression } from 'estree';
export default function ssr(
component: Component,
options: CompileOptions
) {
const renderer = new Renderer();
const renderer = new Renderer({
name: component.name
});
const { name } = component;
@ -20,6 +23,9 @@ export default function ssr(
locate: component.locate
}, options));
// TODO put this inside the Renderer class
const literal = renderer.pop();
// TODO concatenate CSS maps
const css = options.customElement ?
{ code: null, map: null } :
@ -30,43 +36,42 @@ export default function ssr(
.map(({ name }) => {
const store_name = name.slice(1);
const store = component.var_lookup.get(store_name);
if (store && store.hoistable) return;
if (store && store.hoistable) return null;
const assignment = `${name} = @get_store_value(${store_name});`;
const assignment = b`${name} = @get_store_value(${store_name});`;
return component.compile_options.dev
? `@validate_store(${store_name}, '${store_name}'); ${assignment}`
? b`@validate_store(${store_name}, '${store_name}'); ${assignment}`
: assignment;
});
})
.filter(Boolean);
// TODO remove this, just use component.vars everywhere
const props = component.vars.filter(variable => !variable.module && variable.export_name);
component.rewrite_props(({ name }) => {
const value = `$${name}`;
if (component.javascript) {
component.rewrite_props(({ name }) => {
const value = `$${name}`;
let insert = b`${value} = @get_store_value(${name})`;
if (component.compile_options.dev) {
insert = b`@validate_store(${name}, '${name}'); ${insert}`;
}
const get_store_value = component.helper('get_store_value');
return insert;
});
let insert = `${value} = ${get_store_value}(${name})`;
if (component.compile_options.dev) {
const validate_store = component.helper('validate_store');
insert = `${validate_store}(${name}, '${name}'); ${insert}`;
}
return insert;
});
}
const instance_javascript = component.extract_javascript(component.ast.instance);
// TODO only do this for props with a default value
const parent_bindings = component.javascript
? props.map(prop => {
return `if ($$props.${prop.export_name} === void 0 && $$bindings.${prop.export_name} && ${prop.name} !== void 0) $$bindings.${prop.export_name}(${prop.name});`;
})
const parent_bindings = instance_javascript
? component.vars
.filter(variable => !variable.module && variable.export_name)
.map(prop => {
return b`if ($$props.${prop.export_name} === void 0 && $$bindings.${prop.export_name} && ${prop.name} !== void 0) $$bindings.${prop.export_name}(${prop.name});`;
})
: [];
const reactive_declarations = component.reactive_declarations.map(d => {
let snippet = `[✂${d.node.body.start}-${d.node.end}✂]`;
const body: Statement = (d.node as LabeledStatement).body;
let statement = b`${body}`;
if (d.declaration) {
const declared = extract_names(d.declaration);
@ -81,21 +86,25 @@ export default function ssr(
// others we can do `let [expression]`
const separate = (
self_dependencies.length > 0 ||
declared.length > injected.length ||
d.node.body.expression.type === 'ParenthesizedExpression'
declared.length > injected.length
);
snippet = separate
? `let ${injected.join(', ')}; ${snippet}`
: `let ${snippet}`;
const { left, right } = (body as ExpressionStatement).expression as AssignmentExpression;
statement = separate
? b`
${injected.map(name => b`let ${name};`)}
${statement}`
: b`
let ${left} = ${right}`;
}
}
return snippet;
return statement;
});
const main = renderer.has_bindings
? deindent`
? b`
let $$settled;
let $$rendered;
@ -106,54 +115,52 @@ export default function ssr(
${reactive_declarations}
$$rendered = \`${renderer.code}\`;
$$rendered = ${literal};
} while (!$$settled);
return $$rendered;
`
: deindent`
: b`
${reactive_store_values}
${reactive_declarations}
return \`${renderer.code}\`;`;
return ${literal};`;
const blocks = [
reactive_stores.length > 0 && `let ${reactive_stores
.map(({ name }) => {
const store_name = name.slice(1);
const store = component.var_lookup.get(store_name);
if (store && store.hoistable) {
const get_store_value = component.helper('get_store_value');
return `${name} = ${get_store_value}(${store_name})`;
}
return name;
})
.join(', ')};`,
component.javascript,
parent_bindings.join('\n'),
css.code && `$$result.css.add(#css);`,
...reactive_stores.map(({ name }) => {
const store_name = name.slice(1);
const store = component.var_lookup.get(store_name);
if (store && store.hoistable) {
return b`let ${name} = @get_store_value(${store_name});`;
}
return b`let ${name};`;
}),
instance_javascript,
...parent_bindings,
css.code && b`$$result.css.add(#css);`,
main
].filter(Boolean);
return (deindent`
${css.code && deindent`
return b`
${css.code ? b`
const #css = {
code: ${css.code ? stringify(css.code) : `''`},
map: ${css.map ? stringify(css.map.toString()) : 'null'}
};`}
code: "${css.code}",
map: ${css.map ? string_literal(css.map.toString()) : 'null'}
};` : null}
${component.module_javascript}
${component.extract_javascript(component.ast.module)}
${component.fully_hoisted.length > 0 && component.fully_hoisted.join('\n\n')}
${component.fully_hoisted}
const ${name} = @create_ssr_component(($$result, $$props, $$bindings, $$slots) => {
${blocks.join('\n\n')}
${blocks}
});
`).trim();
`;
}
function trim(nodes: INode[]) {
function trim(nodes: TemplateNode[]) {
let start = 0;
for (; start < nodes.length; start += 1) {
const node = nodes[start] as Text;

@ -1,103 +0,0 @@
import repeat from '../../utils/repeat';
const whitespace = /^\s+$/;
interface Chunk {
parent?: BlockChunk;
type: 'root'|'line'|'condition';
children?: Chunk[];
line?: string;
block?: boolean;
condition?: string;
}
interface BlockChunk extends Chunk {
type: 'root'|'condition';
children: Chunk[];
parent: BlockChunk;
}
export default class CodeBuilder {
root: BlockChunk = { type: 'root', children: [], parent: null };
last: Chunk;
current: BlockChunk;
constructor(str = '') {
this.current = this.last = this.root;
this.add_line(str);
}
add_conditional(condition: string, body: string) {
if (this.last.type === 'condition' && this.last.condition === condition) {
if (body && !whitespace.test(body)) this.last.children.push({ type: 'line', line: body });
} else {
const next = this.last = { type: 'condition', condition, parent: this.current, children: [] };
this.current.children.push(next);
if (body && !whitespace.test(body)) next.children.push({ type: 'line', line: body });
}
}
add_line(line: string) {
if (line && !whitespace.test(line)) this.current.children.push(this.last = { type: 'line', line });
}
add_block(block: string) {
if (block && !whitespace.test(block)) this.current.children.push(this.last = { type: 'line', line: block, block: true });
}
is_empty() { return !find_line(this.root); }
push_condition(condition: string) {
if (this.last.type === 'condition' && this.last.condition === condition) {
this.current = this.last as BlockChunk;
} else {
const next = this.last = { type: 'condition', condition, parent: this.current, children: [] };
this.current.children.push(next);
this.current = next;
}
}
pop_condition() {
if (!this.current.parent) throw new Error(`Popping a condition that maybe wasn't pushed.`);
this.current = this.current.parent;
}
toString() {
return chunk_to_string(this.root);
}
}
function find_line(chunk: BlockChunk) {
for (const c of chunk.children) {
if (c.type === 'line' || find_line(c as BlockChunk)) return true;
}
return false;
}
function chunk_to_string(chunk: Chunk, level: number = 0, last_block?: boolean, first?: boolean): string {
if (chunk.type === 'line') {
return `${last_block || (!first && chunk.block) ? '\n' : ''}${chunk.line.replace(/^/gm, repeat('\t', level))}`;
} else if (chunk.type === 'condition') {
let t = false;
const lines = chunk.children.map((c, i) => {
const str = chunk_to_string(c, level + 1, t, i === 0);
t = c.type !== 'line' || c.block;
return str;
}).filter(l => !!l);
if (!lines.length) return '';
return `${last_block || (!first) ? '\n' : ''}${repeat('\t', level)}if (${chunk.condition}) {\n${lines.join('\n')}\n${repeat('\t', level)}}`;
} else if (chunk.type === 'root') {
let t = false;
const lines = chunk.children.map((c, i) => {
const str = chunk_to_string(c, 0, t, i === 0);
t = c.type !== 'line' || c.block;
return str;
}).filter(l => !!l);
if (!lines.length) return '';
return lines.join('\n');
}
}

@ -1,171 +1,6 @@
import * as assert from 'assert';
import deindent from './deindent';
import CodeBuilder from './CodeBuilder';
import get_name_from_filename from './get_name_from_filename';
describe('deindent', () => {
it('deindents a simple string', () => {
const deindented = deindent`
deindent me please
`;
assert.equal(deindented, `deindent me please`);
});
it('deindents a multiline string', () => {
const deindented = deindent`
deindent me please
and me as well
`;
assert.equal(deindented, `deindent me please\nand me as well`);
});
it('preserves indentation of inserted values', () => {
const insert = deindent`
line one
line two
`;
const deindented = deindent`
before
${insert}
after
`;
assert.equal(deindented, `before\n\tline one\n\tline two\nafter`);
});
it('removes newlines before an empty expression', () => {
const deindented = deindent`
{
some text
${null}
}`;
assert.equal(deindented, `{\n\tsome text\n}`);
});
it('removes newlines after an empty expression', () => {
const deindented = deindent`
{
${null}
some text
}`;
assert.equal(deindented, `{\n\tsome text\n}`);
});
it('removes newlines around empty expressions', () => {
const deindented = deindent`
{
${null}
some text
${null}
some text
${null}
}`;
assert.equal(deindented, `{\n\tsome text\n\n\tsome text\n}`);
});
});
describe('CodeBuilder', () => {
it('creates an empty block', () => {
const builder = new CodeBuilder();
assert.equal(builder.toString(), '');
});
it('creates a block with a line', () => {
const builder = new CodeBuilder();
builder.add_line('var answer = 42;');
assert.equal(builder.toString(), 'var answer = 42;');
});
it('creates a block with two lines', () => {
const builder = new CodeBuilder();
builder.add_line('var problems = 99;');
builder.add_line('var answer = 42;');
assert.equal(builder.toString(), 'var problems = 99;\nvar answer = 42;');
});
it('adds newlines around blocks', () => {
const builder = new CodeBuilder();
builder.add_line('// line 1');
builder.add_line('// line 2');
builder.add_block(deindent`
if (foo) {
bar();
}
`);
builder.add_line('// line 3');
builder.add_line('// line 4');
assert.equal(
builder.toString(),
deindent`
// line 1
// line 2
if (foo) {
bar();
}
// line 3
// line 4
`
);
});
it('nests codebuilders with correct indentation', () => {
const child = new CodeBuilder();
child.add_block(deindent`
var obj = {
answer: 42
};
`);
const builder = new CodeBuilder();
builder.add_line('// line 1');
builder.add_line('// line 2');
builder.add_block(deindent`
if (foo) {
${child}
}
`);
builder.add_line('// line 3');
builder.add_line('// line 4');
assert.equal(
builder.toString(),
deindent`
// line 1
// line 2
if (foo) {
var obj = {
answer: 42
};
}
// line 3
// line 4
`
);
});
});
describe('get_name_from_filename', () => {
it('uses the basename', () => {
assert.equal(get_name_from_filename('path/to/Widget.svelte'), 'Widget');

@ -1,53 +0,0 @@
const start = /\n(\t+)/;
export default function deindent(
strings: TemplateStringsArray,
...values: any[]
) {
const indentation = start.exec(strings[0])[1];
const pattern = new RegExp(`^${indentation}`, 'gm');
let result = strings[0].replace(start, '').replace(pattern, '');
let current_indentation = get_current_indentation(result);
for (let i = 1; i < strings.length; i += 1) {
let expression = values[i - 1];
const string = strings[i].replace(pattern, '');
if (Array.isArray(expression)) {
expression = expression.length ? expression.join('\n') : null;
}
// discard empty codebuilders
if (expression && expression.is_empty && expression.is_empty()) {
expression = null;
}
if (expression || expression === '') {
const value = String(expression).replace(
/\n/g,
`\n${current_indentation}`
);
result += value + string;
} else {
let c = result.length;
while (/\s/.test(result[c - 1])) c -= 1;
result = result.slice(0, c) + string;
}
current_indentation = get_current_indentation(result);
}
return result.trim().replace(/\t+$/gm, '').replace(/{\n\n/gm, '{\n');
}
function get_current_indentation(str: string) {
let a = str.length;
while (a > 0 && str[a - 1] !== '\n') a -= 1;
let b = a;
while (b < str.length && /\s/.test(str[b])) b += 1;
return str.slice(a, b);
}

@ -1,31 +1,28 @@
import { Node } from '../../interfaces';
import { Node, Identifier } from 'estree';
export default function flatten_reference(node: Node) {
if (node.type === 'Expression') throw new Error('bad');
const nodes = [];
const parts = [];
const prop_end = node.end;
while (node.type === 'MemberExpression') {
nodes.unshift(node.property);
if (!node.computed) {
parts.unshift(node.property.name);
parts.unshift((node.property as Identifier).name);
}
node = node.object;
}
const prop_start = node.end;
const name = node.type === 'Identifier'
? node.name
: node.type === 'ThisExpression' ? 'this' : null;
nodes.unshift(node);
if (!node.computed) {
if (!(node as any).computed) {
parts.unshift(name);
}
return { name, nodes, parts, keypath: `${name}[✂${prop_start}-${prop_end}✂]` };
return { name, nodes, parts };
}

@ -1,8 +1,6 @@
import { Node } from '../../interfaces';
import unwrap_parens from './unwrap_parens';
import { Node, Identifier } from 'estree';
export default function get_object(node: Node) {
node = unwrap_parens(node);
export default function get_object(node: Node): Identifier {
while (node.type === 'MemberExpression') node = node.object;
return node;
return node as Identifier;
}

@ -1,19 +1,31 @@
import { snip } from './snip';
import { stringify_attribute } from './stringify_attribute';
import Attribute from '../nodes/Attribute';
import { p, x } from 'code-red';
import { string_literal } from './stringify';
export default function get_slot_data(values: Map<string, Attribute>, is_ssr: boolean) {
return Array.from(values.values())
.filter(attribute => attribute.name !== 'name')
.map(attribute => {
const value = attribute.is_true
? 'true'
: attribute.chunks.length === 0
? '""'
: attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text'
? snip(attribute.chunks[0])
: '`' + stringify_attribute(attribute, is_ssr) + '`';
export default function get_slot_data(values: Map<string, Attribute>) {
return {
type: 'ObjectExpression',
properties: Array.from(values.values())
.filter(attribute => attribute.name !== 'name')
.map(attribute => {
const value = get_value(attribute);
return p`${attribute.name}: ${value}`;
})
};
}
return `${attribute.name}: ${value}`;
});
// TODO fairly sure this is duplicated at least once
function get_value(attribute: Attribute) {
if (attribute.is_true) return x`true`;
if (attribute.chunks.length === 0) return x`""`;
let value = attribute.chunks
.map(chunk => chunk.type === 'Text' ? string_literal(chunk.data) : chunk.node)
.reduce((lhs, rhs) => x`${lhs} + ${rhs}`);
if (attribute.chunks.length > 1 && attribute.chunks[0].type !== 'Text') {
value = x`"" + ${value}`;
}
return value;
}

@ -1,10 +1,10 @@
import Component from '../Component';
import MagicString from 'magic-string';
import { Node } from '../../interfaces';
import { nodes_match } from '../../utils/nodes_match';
import { Scope } from './scope';
import { x } from 'code-red';
import { Node } from 'estree';
export function invalidate(component: Component, scope: Scope, code: MagicString, node: Node, names: Set<string>) {
export function invalidate(component: Component, scope: Scope, node: Node, names: Set<string>) {
const [head, ...tail] = Array.from(names).filter(name => {
const owner = scope.find_owner(name);
if (owner && owner !== component.instance_scope) return false;
@ -27,24 +27,11 @@ export function invalidate(component: Component, scope: Scope, code: MagicString
if (head) {
component.has_reactive_assignments = true;
if (node.operator === '=' && nodes_match(node.left, node.right) && tail.length === 0) {
code.overwrite(node.start, node.end, component.invalidate(head));
if (node.type === 'AssignmentExpression' && node.operator === '=' && nodes_match(node.left, node.right) && tail.length === 0) {
return component.invalidate(head);
} else {
let suffix = ')';
if (head[0] === '$') {
code.prependRight(node.start, `${component.helper('set_store_value')}(${head.slice(1)}, `);
} else {
let prefix = `$$invalidate`;
const variable = component.var_lookup.get(head);
if (variable.subscribable && variable.reassigned) {
prefix = `$$subscribe_${head}($$invalidate`;
suffix += `)`;
}
code.prependRight(node.start, `${prefix}('${head}', `);
}
const is_store_value = head[0] === '$';
const variable = component.var_lookup.get(head);
const extra_args = tail.map(name => component.invalidate(name));
@ -55,12 +42,23 @@ export function invalidate(component: Component, scope: Scope, code: MagicString
);
if (pass_value) {
extra_args.unshift(head);
extra_args.unshift({
type: 'Identifier',
name: head
});
}
suffix = `${extra_args.map(arg => `, ${arg}`).join('')}${suffix}`;
const callee = is_store_value ? `@set_store_value` : `$$invalidate`;
let invalidate = x`${callee}(${is_store_value ? head.slice(1) : x`"${head}"`}, ${node}, ${extra_args})`;
code.appendLeft(node.end, suffix);
if (variable.subscribable && variable.reassigned) {
const subscribe = `$$subscribe_${head}`;
invalidate = x`${subscribe}(${invalidate})}`;
}
return invalidate;
}
}
return node;
}

@ -1,9 +1,9 @@
import { walk } from 'estree-walker';
import is_reference from 'is-reference';
import { Node } from '../../interfaces';
import { Node as ESTreeNode } from 'estree';
import { Node, VariableDeclaration, ClassDeclaration, VariableDeclarator, ObjectPattern, Property, RestElement, ArrayPattern, Identifier } from 'estree';
import get_object from './get_object';
// TODO replace this with periscopic?
export function create_scopes(expression: Node) {
const map = new WeakMap();
@ -16,7 +16,7 @@ export function create_scopes(expression: Node) {
node.specifiers.forEach(specifier => {
scope.declarations.set(specifier.local.name, specifier);
});
} else if (/Function/.test(node.type)) {
} else if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
if (node.type === 'FunctionDeclaration') {
scope.declarations.set(node.id.name, node);
scope = new Scope(scope, false);
@ -24,7 +24,9 @@ export function create_scopes(expression: Node) {
} else {
scope = new Scope(scope, false);
map.set(node, scope);
if (node.id) scope.declarations.set(node.id.name, node);
if (node.type === 'FunctionExpression' && node.id) {
scope.declarations.set(node.id.name, node);
}
}
node.params.forEach((param) => {
@ -38,7 +40,7 @@ export function create_scopes(expression: Node) {
} else if (node.type === 'BlockStatement') {
scope = new Scope(scope, true);
map.set(node, scope);
} else if (/(Class|Variable)Declaration/.test(node.type)) {
} else if (node.type === 'ClassDeclaration' || node.type === 'VariableDeclaration') {
scope.add_declaration(node);
} else if (node.type === 'CatchClause') {
scope = new Scope(scope, true);
@ -47,7 +49,7 @@ export function create_scopes(expression: Node) {
extract_names(node.param).forEach(name => {
scope.declarations.set(name, node.param);
});
} else if (node.type === 'Identifier' && is_reference(node as ESTreeNode, parent as ESTreeNode)) {
} else if (node.type === 'Identifier' && is_reference(node as Node, parent as Node)) {
if (!scope.has(node.name) && !globals.has(node.name)) {
globals.set(node.name, node);
}
@ -80,16 +82,18 @@ export class Scope {
this.block = block;
}
add_declaration(node: Node) {
if (node.kind === 'var' && this.block && this.parent) {
this.parent.add_declaration(node);
} else if (node.type === 'VariableDeclaration') {
node.declarations.forEach((declarator: Node) => {
extract_names(declarator.id).forEach(name => {
this.declarations.set(name, node);
if (declarator.init) this.initialised_declarations.add(name);
add_declaration(node: VariableDeclaration | ClassDeclaration) {
if (node.type === 'VariableDeclaration') {
if (node.kind === 'var' && this.block && this.parent) {
this.parent.add_declaration(node);
} else {
node.declarations.forEach((declarator: VariableDeclarator) => {
extract_names(declarator.id).forEach(name => {
this.declarations.set(name, node);
if (declarator.init) this.initialised_declarations.add(name);
});
});
});
}
} else {
this.declarations.set(node.id.name, node);
}
@ -107,12 +111,12 @@ export class Scope {
}
}
export function extract_names(param: Node) {
return extract_identifiers(param).map(node => node.name);
export function extract_names(param: Node): string[] {
return extract_identifiers(param).map((node: any) => node.name);
}
export function extract_identifiers(param: Node) {
const nodes: Node[] = [];
export function extract_identifiers(param: Node): Identifier[] {
const nodes: Identifier[] = [];
extractors[param.type] && extractors[param.type](nodes, param);
return nodes;
}
@ -126,8 +130,8 @@ const extractors = {
nodes.push(get_object(param));
},
ObjectPattern(nodes: Node[], param: Node) {
param.properties.forEach((prop: Node) => {
ObjectPattern(nodes: Node[], param: ObjectPattern) {
param.properties.forEach((prop: Property | RestElement) => {
if (prop.type === 'RestElement') {
nodes.push(prop.argument);
} else {
@ -136,17 +140,17 @@ const extractors = {
});
},
ArrayPattern(nodes: Node[], param: Node) {
ArrayPattern(nodes: Node[], param: ArrayPattern) {
param.elements.forEach((element: Node) => {
if (element) extractors[element.type](nodes, element);
});
},
RestElement(nodes: Node[], param: Node) {
RestElement(nodes: Node[], param: any) {
extractors[param.argument.type](nodes, param.argument);
},
AssignmentPattern(nodes: Node[], param: Node) {
AssignmentPattern(nodes: Node[], param: any) {
extractors[param.left.type](nodes, param.left);
}
};

@ -1,3 +0,0 @@
export function snip(expression) {
return `[✂${expression.node.start}-${expression.node.end}✂]`;
}

@ -1,5 +1,8 @@
export function stringify(data: string, options = {}) {
return JSON.stringify(escape(data, options));
export function string_literal(data: string) {
return {
type: 'Literal',
value: data
};
}
export function escape(data: string, { only_escape_at_symbol = false } = {}) {

@ -1,27 +0,0 @@
import Attribute from '../nodes/Attribute';
import { escape_template, escape } from './stringify';
import { snip } from './snip';
import Text from '../nodes/Text';
export function stringify_class_attribute(attribute: Attribute) {
// handle special case — `class={possiblyUndefined}` with scoped CSS
if (attribute.chunks.length === 2 && (attribute.chunks[1] as Text).synthetic) {
return '${@escape(@null_to_empty(' + snip(attribute.chunks[0]) + '))}' + (attribute.chunks[1] as Text).data;
}
return stringify_attribute(attribute, true);
}
export function stringify_attribute(attribute: Attribute, is_ssr: boolean) {
return attribute.chunks
.map((chunk) => {
if (chunk.type === 'Text') {
return escape_template(escape(chunk.data).replace(/"/g, '&quot;'));
}
return is_ssr
? '${@escape(' + snip(chunk) + ')}'
: '${' + snip(chunk) + '}';
})
.join('');
}

@ -1,11 +0,0 @@
export function stringify_props(props: string[]) {
if (!props.length) return '{}';
const joined = props.join(', ');
if (joined.length > 40) {
// make larger data objects readable
return `{\n\t${props.join(',\n\t')}\n}`;
}
return `{ ${joined} }`;
}

@ -1,7 +0,0 @@
export function new_tail(): string {
return '%%tail_head%%';
}
export function attach_head(head: string, tail: string): string {
return tail.replace('%%tail_head%%', head);
}

@ -1,6 +0,0 @@
import { Node } from '../../interfaces';
export default function unwrap_parens(node: Node) {
while (node.type === 'ParenthesizedExpression') node = node.expression;
return node;
}

@ -1,11 +1,18 @@
import { Node, Program } from "estree";
interface BaseNode {
start: number;
end: number;
type: string;
children?: Node[];
children?: TemplateNode[];
[prop_name: string]: any;
}
export interface Fragment extends BaseNode {
type: 'Fragment';
children: TemplateNode[];
}
export interface Text extends BaseNode {
type: 'Text';
data: string;
@ -27,7 +34,7 @@ export type DirectiveType = 'Action'
interface BaseDirective extends BaseNode {
type: DirectiveType;
expression: null|Node;
expression: null | Node;
name: string;
modifiers: string[];
}
@ -40,7 +47,7 @@ export interface Transition extends BaseDirective{
export type Directive = BaseDirective | Transition;
export type Node = Text
export type TemplateNode = Text
| MustacheTag
| BaseNode
| Directive
@ -59,11 +66,28 @@ export interface Parser {
meta_tags: {};
}
export interface Script extends BaseNode {
type: 'Script';
context: string;
content: Program;
}
export interface Style extends BaseNode {
type: 'Style';
attributes: any[]; // TODO
children: any[]; // TODO add CSS node types
content: {
start: number;
end: number;
styles: string;
};
}
export interface Ast {
html: Node;
css: Node;
instance: Node;
module: Node;
html: TemplateNode;
css: Style;
instance: Script;
module: Script;
}
export interface Warning {

@ -6,11 +6,11 @@ export const parse = (source: string) => Parser.parse(source, {
sourceType: 'module',
// @ts-ignore TODO pending release of fixed types
ecmaVersion: 11,
preserveParens: true
locations: true
});
export const parse_expression_at = (source: string, index: number) => Parser.parseExpressionAt(source, index, {
// @ts-ignore TODO pending release of fixed types
ecmaVersion: 11,
preserveParens: true
locations: true
});

@ -3,7 +3,7 @@ import fragment from './state/fragment';
import { whitespace } from '../utils/patterns';
import { reserved } from '../utils/names';
import full_char_code_at from '../utils/full_char_code_at';
import { Node, Ast, ParserOptions } from '../interfaces';
import { TemplateNode, Ast, ParserOptions, Fragment, Style, Script } from '../interfaces';
import error from '../utils/error';
type ParserState = (parser: Parser) => (ParserState | void);
@ -14,11 +14,11 @@ export class Parser {
readonly customElement: boolean;
index = 0;
stack: Node[] = [];
stack: TemplateNode[] = [];
html: Node;
css: Node[] = [];
js: Node[] = [];
html: Fragment;
css: Style[] = [];
js: Script[] = [];
meta_tags = {};
constructor(template: string, options: ParserOptions) {
@ -207,7 +207,7 @@ export default function parse(
): Ast {
const parser = new Parser(template, options);
// TODO we way want to allow multiple <style> tags —
// TODO we may want to allow multiple <style> tags —
// one scoped, one global. for now, only allow one
if (parser.css.length > 1) {
parser.error({

@ -1,6 +1,7 @@
import { parse_expression_at } from '../acorn';
import { Parser } from '../index';
import { Identifier, Node, SimpleLiteral } from 'estree';
import { whitespace } from '../../utils/patterns';
const literals = new Map([['true', true], ['false', false], ['null', null]]);
@ -35,7 +36,30 @@ export default function read_expression(parser: Parser): Node {
try {
const node = parse_expression_at(parser.template, parser.index);
parser.index = node.end;
let num_parens = 0;
for (let i = parser.index; i < node.start; i += 1) {
if (parser.template[i] === '(') num_parens += 1;
}
let index = node.end;
while (num_parens > 0) {
const char = parser.template[index];
if (char === ')') {
num_parens -= 1;
} else if (!whitespace.test(char)) {
parser.error({
code: 'unexpected-token',
message: 'Expected )'
}, index);
}
index += 1;
}
parser.index = index;
return node as Node;
} catch (err) {

@ -1,11 +1,12 @@
import * as acorn from '../acorn';
import repeat from '../../utils/repeat';
import { Parser } from '../index';
import { Node } from '../../interfaces';
import { Script } from '../../interfaces';
import { Node, Program } from 'estree';
const script_closing_tag = '</script>';
function get_context(parser: Parser, attributes: Node[], start: number) {
function get_context(parser: Parser, attributes: any[], start: number): string {
const context = attributes.find(attribute => attribute.name === 'context');
if (!context) return 'default';
@ -28,7 +29,7 @@ function get_context(parser: Parser, attributes: Node[], start: number) {
return value;
}
export default function read_script(parser: Parser, start: number, attributes: Node[]) {
export default function read_script(parser: Parser, start: number, attributes: Node[]): Script {
const script_start = parser.index;
const script_end = parser.template.indexOf(script_closing_tag, script_start);
@ -41,7 +42,7 @@ export default function read_script(parser: Parser, start: number, attributes: N
repeat(' ', script_start) + parser.template.slice(script_start, script_end);
parser.index = script_end + script_closing_tag.length;
let ast;
let ast: Program;
try {
ast = acorn.parse(source);
@ -49,8 +50,11 @@ export default function read_script(parser: Parser, start: number, attributes: N
parser.acorn_error(err);
}
ast.start = script_start;
// TODO is this necessary?
(ast as any).start = script_start;
return {
type: 'Script',
start,
end: parser.index,
context: get_context(parser, attributes, start),

@ -1,9 +1,10 @@
import parse from 'css-tree/lib/parser/index.js';
import { walk } from 'estree-walker';
import { Parser } from '../index';
import { Node } from '../../interfaces';
import { Node } from 'estree';
import { Style } from '../../interfaces';
export default function read_style(parser: Parser, start: number, attributes: Node[]) {
export default function read_style(parser: Parser, start: number, attributes: Node[]): Style {
const content_start = parser.index;
const styles = parser.read_until(/<\/style>/);
const content_end = parser.index;
@ -30,7 +31,7 @@ export default function read_style(parser: Parser, start: number, attributes: No
// tidy up AST
walk(ast, {
enter: (node: Node) => {
enter: (node: any) => { // `any` because this isn't an ESTree node
// replace `ref:a` nodes
if (node.type === 'Selector') {
for (let i = 0; i < node.children.length; i += 1) {
@ -58,6 +59,7 @@ export default function read_style(parser: Parser, start: number, attributes: No
const end = parser.index;
return {
type: 'Style',
start,
end,
attributes,
@ -65,12 +67,12 @@ export default function read_style(parser: Parser, start: number, attributes: No
content: {
start: content_start,
end: content_end,
styles,
},
styles
}
};
}
function is_ref_selector(a: Node, b: Node) {
function is_ref_selector(a: any, b: any) { // TODO add CSS node types
if (!b) return false;
return (

@ -4,9 +4,9 @@ import { closing_tag_omitted } from '../utils/html';
import { whitespace } from '../../utils/patterns';
import { trim_start, trim_end } from '../../utils/trim';
import { Parser } from '../index';
import { Node } from '../../interfaces';
import { TemplateNode } from '../../interfaces';
function trim_whitespace(block: Node, trim_before: boolean, trim_after: boolean) {
function trim_whitespace(block: TemplateNode, trim_before: boolean, trim_after: boolean) {
if (!block.children || block.children.length === 0) return; // AwaitBlock
const first_child = block.children[0];
@ -175,7 +175,7 @@ export default function mustache(parser: Parser) {
parser.eat('}', true);
}
const then_block: Node = {
const then_block: TemplateNode = {
start,
end: null,
type: 'ThenBlock',
@ -200,7 +200,7 @@ export default function mustache(parser: Parser) {
parser.eat('}', true);
}
const catch_block: Node = {
const catch_block: TemplateNode = {
start,
end: null,
type: 'CatchBlock',
@ -232,7 +232,7 @@ export default function mustache(parser: Parser) {
const expression = read_expression(parser);
const block: Node = type === 'AwaitBlock' ?
const block: TemplateNode = type === 'AwaitBlock' ?
{
start,
end: null,

@ -4,7 +4,7 @@ import read_style from '../read/style';
import { decode_character_references, closing_tag_omitted } from '../utils/html';
import { is_void } from '../../utils/names';
import { Parser } from '../index';
import { Directive, DirectiveType, Node, Text } from '../../interfaces';
import { Directive, DirectiveType, TemplateNode, Text } from '../../interfaces';
import fuzzymatch from '../../utils/fuzzymatch';
import list from '../../utils/list';
@ -37,10 +37,8 @@ const specials = new Map([
],
]);
// eslint-disable-next-line no-useless-escape
const SELF = /^svelte:self(?=[\s\/>])/;
// eslint-disable-next-line no-useless-escape
const COMPONENT = /^svelte:component(?=[\s\/>])/;
const SELF = /^svelte:self(?=[\s/>])/;
const COMPONENT = /^svelte:component(?=[\s/>])/;
function parent_is_head(stack) {
let i = stack.length;
@ -112,7 +110,7 @@ export default function tag(parser: Parser) {
: name === 'title' && parent_is_head(parser.stack) ? 'Title'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
const element: Node = {
const element: TemplateNode = {
start,
end: null, // filled in later
type,
@ -406,7 +404,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
end: directive.end,
type: 'Identifier',
name: directive.name
};
} as any;
}
return directive;
@ -447,7 +445,7 @@ function read_attribute_value(parser: Parser) {
return value;
}
function read_sequence(parser: Parser, done: () => boolean): Node[] {
function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
let current_chunk: Text = {
start: parser.index,
end: null,
@ -464,7 +462,7 @@ function read_sequence(parser: Parser, done: () => boolean): Node[] {
}
}
const chunks: Node[] = [];
const chunks: TemplateNode[] = [];
while (parser.index < parser.template.length) {
const index = parser.index;

@ -1,57 +0,0 @@
import MagicString from 'magic-string';
import { Node } from '../interfaces';
import { walk } from 'estree-walker';
import repeat from './repeat';
export function remove_indentation(code: MagicString, node: Node) {
const indent = code.getIndentString();
const pattern = new RegExp(`^${indent}`, 'gm');
const excluded = [];
walk(node, {
enter(node) {
if (node.type === 'TemplateElement') {
excluded.push(node);
}
}
});
const str = code.original.slice(node.start, node.end);
let match;
while (match = pattern.exec(str)) {
const index = node.start + match.index;
while (excluded[0] && excluded[0].end < index) excluded.shift();
if (excluded[0] && excluded[0].start < index) continue;
code.remove(index, index + indent.length);
}
}
export function add_indentation(code: MagicString, node: Node, levels = 1) {
const base_indent = code.getIndentString();
const indent = repeat(base_indent, levels);
const pattern = /\n/gm;
const excluded = [];
walk(node, {
enter(node) {
if (node.type === 'TemplateElement') {
excluded.push(node);
}
}
});
const str = code.original.slice(node.start, node.end);
let match;
while (match = pattern.exec(str)) {
const index = node.start + match.index;
while (excluded[0] && excluded[0].end < index) excluded.shift();
if (excluded[0] && excluded[0].start < index) continue;
code.appendLeft(index + 1, indent);
}
}

@ -105,7 +105,7 @@ export function is_void(name: string) {
return void_element_names.test(name) || name.toLowerCase() === '!doctype';
}
function is_valid(str: string): boolean {
export function is_valid(str: string): boolean {
let i = 0;
while (i < str.length) {
@ -118,16 +118,6 @@ function is_valid(str: string): boolean {
return true;
}
export function quote_name_if_necessary(name: string) {
if (!is_valid(name)) return `"${name}"`;
return name;
}
export function quote_prop_if_necessary(name: string) {
if (!is_valid(name)) return `["${name}"]`;
return `.${name}`;
}
export function sanitize(name: string) {
return name
.replace(/[^a-zA-Z0-9_]+/g, '_')

@ -82,10 +82,24 @@ describe('css', () => {
assert.equal(dom.css.code.replace(/svelte(-ref)?-[a-z0-9]+/g, (m, $1) => $1 ? m : 'svelte-xyz'), expected.css);
let ClientComponent;
let ServerComponent;
// we do this here, rather than in the expected.html !== null
// block, to verify that valid code was generated
const ClientComponent = create(dom.js.code);
const ServerComponent = create(ssr.js.code);
try {
ClientComponent = create(dom.js.code);
} catch (err) {
console.log(dom.js.code);
throw err;
}
try {
ServerComponent = create(ssr.js.code);
} catch (err) {
console.log(dom.js.code);
throw err;
}
// verify that the right elements have scoping selectors
if (expected.html !== null) {

@ -1,3 +1,29 @@
export function deepEqual(a, b, message) {
if (!is_equal(a, b)) {
throw new Error(message || `Expected ${JSON.stringify(a)} to equal ${JSON.stringify(b)}`);
}
}
function is_equal(a, b) {
if (a && typeof a === 'object') {
const is_array = Array.isArray(a);
if (Array.isArray(b) !== is_array) return false;
if (is_array) {
if (a.length !== b.length) return false;
return a.every((value, i) => is_equal(value, b[i]));
}
const a_keys = Object.keys(a).sort();
const b_keys = Object.keys(b).sort();
if (a_keys.join(',') !== b_keys.join(',')) return false;
return a_keys.every(key => is_equal(a[key], b[key]));
}
return a === b;
}
export function equal(a, b, message) {
if (a != b) throw new Error(message || `Expected ${a} to equal ${b}`);
}

@ -109,6 +109,11 @@ describe('custom-elements', function() {
console[type](...args);
});
page.on('error', error => {
console.log('>>> an error happened');
console.error(error);
});
try {
await page.goto('http://localhost:6789');

@ -11,8 +11,9 @@ export default function (target) {
target.innerHTML = '<my-app foo=yes />';
assert.equal(warnings.length, 1);
assert.equal(warnings[0], `<my-app> was created without expected prop 'bar'`);
assert.deepEqual(warnings, [
`<my-app> was created without expected prop 'bar'`
]);
console.warn = warn;
}

@ -47,7 +47,7 @@ export function tryToReadFile(file) {
const virtualConsole = new jsdom.VirtualConsole();
virtualConsole.sendTo(console);
global.window = new jsdom.JSDOM('<main></main>', {virtualConsole}).window;
const window = new jsdom.JSDOM('<main></main>', {virtualConsole}).window;
global.document = window.document;
global.navigator = window.navigator;
global.getComputedStyle = window.getComputedStyle;
@ -182,58 +182,21 @@ export function showOutput(cwd, options = {}, compile = svelte.compile) {
glob('**/*.svelte', { cwd }).forEach(file => {
if (file[0] === '_') return;
const { js } = compile(
fs.readFileSync(`${cwd}/${file}`, 'utf-8'),
Object.assign(options, {
filename: file
})
);
console.log( // eslint-disable-line no-console
`\n>> ${colors.cyan().bold(file)}\n${addLineNumbers(js.code)}\n<< ${colors.cyan().bold(file)}`
);
});
}
const start = /\n(\t+)/;
export function deindent(strings, ...values) {
const indentation = start.exec(strings[0])[1];
const pattern = new RegExp(`^${indentation}`, 'gm');
let result = strings[0].replace(start, '').replace(pattern, '');
let trailingIndentation = getTrailingIndentation(result);
for (let i = 1; i < strings.length; i += 1) {
let expression = values[i - 1];
const string = strings[i].replace(pattern, '');
if (Array.isArray(expression)) {
expression = expression.length ? expression.join('\n') : null;
}
try {
const { js } = compile(
fs.readFileSync(`${cwd}/${file}`, 'utf-8'),
Object.assign(options, {
filename: file
})
);
if (expression || expression === '') {
const value = String(expression).replace(
/\n/g,
`\n${trailingIndentation}`
console.log( // eslint-disable-line no-console
`\n>> ${colors.cyan().bold(file)}\n${addLineNumbers(js.code)}\n<< ${colors.cyan().bold(file)}`
);
result += value + string;
} else {
let c = result.length;
while (/\s/.test(result[c - 1])) c -= 1;
result = result.slice(0, c) + string;
} catch (err) {
console.log(`failed to generate output: ${err.message}`);
}
trailingIndentation = getTrailingIndentation(result);
}
return result.trim().replace(/\t+$/gm, '');
}
function getTrailingIndentation(str) {
let i = str.length;
while (str[i - 1] === ' ' || str[i - 1] === '\t') i -= 1;
return str.slice(i, str.length);
});
}
export function spaces(i) {

@ -1,43 +1,35 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteComponent,
detach,
element,
init,
insert,
is_function,
noop,
safe_not_equal
} from "svelte/internal";
function create_fragment(ctx) {
var button, foo_action;
let button;
let foo_action;
return {
c() {
button = element("button");
button.textContent = "foo";
},
m(target, anchor) {
insert(target, button, anchor);
foo_action = foo.call(null, button, ctx.foo_function) || {};
foo_action = foo.call(null, button, ctx.foo_function) || ({});
},
p(changed, ctx) {
if (typeof foo_action.update === 'function' && changed.bar) {
foo_action.update.call(null, ctx.foo_function);
}
if (is_function(foo_action.update) && changed.bar) foo_action.update.call(null, ctx.foo_function);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) {
detach(button);
}
if (foo_action && typeof foo_action.destroy === 'function') foo_action.destroy();
if (detaching) detach(button);
if (foo_action && is_function(foo_action.destroy)) foo_action.destroy();
}
};
}
@ -47,16 +39,15 @@ function handleFoo(bar) {
}
function foo(node, callback) {
// code goes here
}
function instance($$self, $$props, $$invalidate) {
let { bar } = $$props;
const foo_function = () => handleFoo(bar);
$$self.$set = $$props => {
if ('bar' in $$props) $$invalidate('bar', bar = $$props.bar);
if ("bar" in $$props) $$invalidate("bar", bar = $$props.bar);
};
return { bar, foo_function };

@ -1,4 +1,3 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteComponent,
attr,
@ -6,12 +5,14 @@ import {
element,
init,
insert,
is_function,
noop,
safe_not_equal
} from "svelte/internal";
function create_fragment(ctx) {
var a, link_action;
let a;
let link_action;
return {
c() {
@ -19,39 +20,33 @@ function create_fragment(ctx) {
a.textContent = "Test";
attr(a, "href", "#");
},
m(target, anchor) {
insert(target, a, anchor);
link_action = link.call(null, a) || {};
link_action = link.call(null, a) || ({});
},
p: noop,
i: noop,
o: noop,
d(detaching) {
if (detaching) {
detach(a);
}
if (link_action && typeof link_action.destroy === 'function') link_action.destroy();
if (detaching) detach(a);
if (link_action && is_function(link_action.destroy)) link_action.destroy();
}
};
}
function link(node) {
function onClick(event) {
event.preventDefault();
history.pushState(null, null, event.target.href);
}
function onClick(event) {
event.preventDefault();
history.pushState(null, null, event.target.href);
}
node.addEventListener('click', onClick);
node.addEventListener("click", onClick);
return {
destroy() {
node.removeEventListener('click', onClick);
}
}
return {
destroy() {
node.removeEventListener("click", onClick);
}
};
}
class Component extends SvelteComponent {

@ -1,4 +1,3 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteComponent,
add_render_callback,
@ -10,8 +9,7 @@ import {
} from "svelte/internal";
function create_fragment(ctx) {
var dispose;
let dispose;
add_render_callback(ctx.onlinestatuschanged);
return {
@ -21,12 +19,10 @@ function create_fragment(ctx) {
listen(window, "offline", ctx.onlinestatuschanged)
];
},
m: noop,
p: noop,
i: noop,
o: noop,
d(detaching) {
run_all(dispose);
}
@ -37,7 +33,7 @@ function instance($$self, $$props, $$invalidate) {
let online;
function onlinestatuschanged() {
online = navigator.onLine; $$invalidate('online', online);
$$invalidate("online", online = navigator.onLine);
}
return { online, onlinestatuschanged };

@ -1,4 +1,3 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteComponent,
detach,
@ -11,34 +10,31 @@ import {
} from "svelte/internal";
function create_fragment(ctx) {
var details, dispose;
let details;
let dispose;
return {
c() {
details = element("details");
details.innerHTML = `<summary>summary</summary>content
`;
`;
dispose = listen(details, "toggle", ctx.details_toggle_handler);
},
m(target, anchor) {
insert(target, details, anchor);
details.open = ctx.open;
},
p(changed, ctx) {
if (changed.open) details.open = ctx.open;
if (changed.open) {
details.open = ctx.open;
}
},
i: noop,
o: noop,
d(detaching) {
if (detaching) {
detach(details);
}
if (detaching) detach(details);
dispose();
}
};
@ -49,11 +45,11 @@ function instance($$self, $$props, $$invalidate) {
function details_toggle_handler() {
open = this.open;
$$invalidate('open', open);
$$invalidate("open", open);
}
$$self.$set = $$props => {
if ('open' in $$props) $$invalidate('open', open = $$props.open);
if ("open" in $$props) $$invalidate("open", open = $$props.open);
};
return { open, details_toggle_handler };

@ -1,4 +1,3 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteComponent,
add_render_callback,
@ -12,7 +11,8 @@ import {
} from "svelte/internal";
function create_fragment(ctx) {
var div, div_resize_listener;
let div;
let div_resize_listener;
return {
c() {
@ -20,39 +20,34 @@ function create_fragment(ctx) {
div.textContent = "some content";
add_render_callback(() => ctx.div_resize_handler.call(div));
},
m(target, anchor) {
insert(target, div, anchor);
div_resize_listener = add_resize_listener(div, ctx.div_resize_handler.bind(div));
},
p: noop,
i: noop,
o: noop,
d(detaching) {
if (detaching) {
detach(div);
}
if (detaching) detach(div);
div_resize_listener.cancel();
}
};
}
function instance($$self, $$props, $$invalidate) {
let { w, h } = $$props;
let { w } = $$props;
let { h } = $$props;
function div_resize_handler() {
w = this.offsetWidth;
h = this.offsetHeight;
$$invalidate('w', w);
$$invalidate('h', h);
$$invalidate("w", w);
$$invalidate("h", h);
}
$$self.$set = $$props => {
if ('w' in $$props) $$invalidate('w', w = $$props.w);
if ('h' in $$props) $$invalidate('h', h = $$props.h);
if ("w" in $$props) $$invalidate("w", w = $$props.w);
if ("h" in $$props) $$invalidate("h", h = $$props.h);
};
return { w, h, div_resize_handler };

@ -1,4 +1,3 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteComponent,
append,
@ -16,7 +15,11 @@ import {
} from "svelte/internal";
function create_fragment(ctx) {
var p, t0, t1, input, dispose;
let p;
let t0;
let t1;
let input;
let dispose;
return {
c() {
@ -26,34 +29,26 @@ function create_fragment(ctx) {
input = element("input");
dispose = listen(input, "input", ctx.input_input_handler);
},
m(target, anchor) {
insert(target, p, anchor);
append(p, t0);
insert(target, t1, anchor);
insert(target, input, anchor);
set_input_value(input, ctx.foo);
},
p(changed, ctx) {
if (changed.foo) {
set_data(t0, ctx.foo);
}
if (changed.foo) set_data(t0, ctx.foo);
if (changed.foo && (input.value !== ctx.foo)) set_input_value(input, ctx.foo);
if (changed.foo && input.value !== ctx.foo) {
set_input_value(input, ctx.foo);
}
},
i: noop,
o: noop,
d(detaching) {
if (detaching) {
detach(p);
detach(t1);
detach(input);
}
if (detaching) detach(p);
if (detaching) detach(t1);
if (detaching) detach(input);
dispose();
}
};
@ -64,7 +59,7 @@ function instance($$self, $$props, $$invalidate) {
function input_input_handler() {
foo = this.value;
$$invalidate('foo', foo);
$$invalidate("foo", foo);
}
return { foo, input_input_handler };

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save