Convert src/compiler/compile/render_dom/Renderer.ts to JavaScript

pull/8569/head
Simon Holthausen 3 years ago
parent df3ac56cf1
commit 32342c3a4e

@ -1,7 +1,28 @@
import { b, x } from 'code-red'; import { b, x } from 'code-red';
import { is_head } from './wrappers/shared/is_head.js'; import { is_head } from './wrappers/shared/is_head.js';
import { regex_double_quotes } from '../../utils/patterns.js'; import { regex_double_quotes } from '../../utils/patterns.js';
export default class Block { export default class Block {
/**
* @typedef {Object} Bindings
* @property {import('estree').Identifier} object
* @property {import('estree').Identifier} property
* @property {import('estree').Node} snippet
* @property {string} store
* @property {(node:import('estree').Node) => import('estree').Node} modifier
*/
/**
* @typedef {Object} BlockOptions
* @property {Block} [parent]
* @property {import('estree').Identifier} name
* @property {string} type
* @property {import('./Renderer.js').default} renderer
* @property {string} [comment]
* @property {import('estree').Identifier} [key]
* @property {Map<string,Bindings>} [bindings]
* @property {Set<string>} [dependencies]
*/
/** @type {Block} */ /** @type {Block} */
parent; parent;
@ -131,6 +152,7 @@ export default class Block {
this.aliases = new Map(); this.aliases = new Map();
if (this.key) this.aliases.set('key', this.get_unique_name('key')); if (this.key) this.aliases.set('key', this.get_unique_name('key'));
} }
assign_variable_names() { assign_variable_names() {
/** @type {Set<string>} */ /** @type {Set<string>} */
const seen = new Set(); const seen = new Set();
@ -206,6 +228,7 @@ export default class Block {
this.outros += 1; this.outros += 1;
if (!local && this.parent) this.parent.add_outro(); if (!local && this.parent) this.parent.add_outro();
} }
add_animation() { add_animation() {
this.has_animation = true; this.has_animation = true;
} }
@ -431,6 +454,7 @@ export default class Block {
this.has_animation this.has_animation
); );
} }
render() { render() {
const key = this.key && this.get_unique_name('key'); const key = this.key && this.get_unique_name('key');
@ -486,21 +510,3 @@ export default class Block {
} }
} }
} }
/** @typedef {Object} Bindings
* @property {Identifier} object
* @property {Identifier} property
* @property {Node} snippet
* @property {string} store
* @property {(node:Node)=>Node} modifier
*/
/** @typedef {Object} BlockOptions
* @property {Block} [parent]
* @property {Identifier} name
* @property {string} type
* @property {Renderer} [renderer]
* @property {string} [comment]
* @property {Identifier} [key]
* @property {Map<string,Bindings>} [bindings]
* @property {Set<string>} [dependencies]
*/

@ -1,234 +1,210 @@
import Block from './Block'; import Block from './Block.js';
import { CompileOptions, Var } from '../../interfaces'; import FragmentWrapper from './wrappers/Fragment.js';
import Component from '../Component';
import FragmentWrapper from './wrappers/Fragment';
import { x } from 'code-red'; import { x } from 'code-red';
import { import flatten_reference from '../utils/flatten_reference.js';
Node, import { reserved_keywords } from '../utils/reserved_keywords.js';
Identifier, import { renderer_invalidate } from './invalidate.js';
MemberExpression, export default class Renderer {
Literal,
Expression,
BinaryExpression,
UnaryExpression,
ArrayExpression
} from 'estree';
import flatten_reference from '../utils/flatten_reference';
import { reserved_keywords } from '../utils/reserved_keywords';
import { renderer_invalidate } from './invalidate';
interface ContextMember { /** @type {import('../Component.js').default} */
name: string; component; // TODO Maybe Renderer shouldn't know about Component?
index: Literal;
is_contextual: boolean;
is_non_contextual: boolean;
variable: Var;
priority: number;
}
type BitMasks = Array<{ /** @type {import('../../interfaces.js').CompileOptions} */
n: number; options;
names: string[];
}>;
export interface BindingGroup { /** @type {ContextMember[]} */
binding_group: (to_reference?: boolean) => Node; context = [];
contexts: string[];
list_dependencies: Set<string>;
keypath: string;
add_element: (block: Block, element: Identifier) => void;
render: (block: Block) => void;
}
export default class Renderer { /** @type {ContextMember[]} */
component: Component; // TODO Maybe Renderer shouldn't know about Component? initial_context = [];
options: CompileOptions;
/** @type {Map<string, ContextMember>} */
context_lookup = new Map();
/** @type {boolean} */
context_overflow;
/** @type {Array<import('./Block.js').default | import('estree').Node | import('estree').Node[]>} */
blocks = [];
/** @type {Set<string>} */
readonly = new Set();
context: ContextMember[] = []; /** @type {Array<import('estree').Node | import('estree').Node[]>} */
initial_context: ContextMember[] = []; meta_bindings = []; // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
context_lookup: Map<string, ContextMember> = new Map();
context_overflow: boolean;
blocks: Array<Block | Node | Node[]> = [];
readonly: Set<string> = new Set();
meta_bindings: Array<Node | Node[]> = []; // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
binding_groups: Map<string, BindingGroup> = new Map();
block: Block; /** @type {Map<string, BindingGroup>} */
fragment: FragmentWrapper; binding_groups = new Map();
file_var: Identifier; /** @type {import('./Block.js').default} */
locate: (c: number) => { line: number; column: number }; block;
constructor(component: Component, options: CompileOptions) { /** @type {import('./wrappers/Fragment.js').default} */
fragment;
/** @type {import('estree').Identifier} */
file_var;
/** @type {(c: number) => { line: number; column: number }} */
locate;
/**
* @param {import('../Component.js').default} component
* @param {import('../../interfaces.js').CompileOptions} options
*/
constructor(component, options) {
this.component = component; this.component = component;
this.options = options; this.options = options;
this.locate = component.locate; // TODO messy this.locate = component.locate; // TODO messy
this.file_var = options.dev && this.component.get_unique_name('file'); this.file_var = options.dev && this.component.get_unique_name('file');
component.vars component.vars
.filter((v) => !v.hoistable || (v.export_name && !v.module)) .filter((v) => !v.hoistable || (v.export_name && !v.module))
.forEach((v) => this.add_to_context(v.name)); .forEach((v) => this.add_to_context(v.name));
// ensure store values are included in context // ensure store values are included in context
component.vars.filter((v) => v.subscribable).forEach((v) => this.add_to_context(`$${v.name}`)); component.vars.filter((v) => v.subscribable).forEach((v) => this.add_to_context(`$${v.name}`));
reserved_keywords.forEach((keyword) => { reserved_keywords.forEach((keyword) => {
if (component.var_lookup.has(keyword)) { if (component.var_lookup.has(keyword)) {
this.add_to_context(keyword); this.add_to_context(keyword);
} }
}); });
if (component.slots.size > 0) { if (component.slots.size > 0) {
this.add_to_context('$$scope'); this.add_to_context('$$scope');
this.add_to_context('#slots'); this.add_to_context('#slots');
} }
// main block // main block
this.block = new Block({ this.block = new Block({
renderer: this, renderer: this,
name: null, name: null,
type: 'component', type: 'component',
key: null, key: null,
bindings: new Map(), bindings: new Map(),
dependencies: new Set() dependencies: new Set()
}); });
this.block.has_update_method = true; this.block.has_update_method = true;
this.fragment = new FragmentWrapper(this, this.block, component.fragment.children, null, true, null);
this.fragment = new FragmentWrapper(
this,
this.block,
component.fragment.children,
null,
true,
null
);
// TODO messy // TODO messy
this.blocks.forEach((block) => { this.blocks.forEach((block) => {
if (block instanceof Block) { if (block instanceof Block) {
block.assign_variable_names(); block.assign_variable_names();
} }
}); });
this.block.assign_variable_names(); this.block.assign_variable_names();
this.fragment.render(this.block, null, /** @type {import('estree').Identifier} */ (x `#nodes`));
this.fragment.render(this.block, null, x`#nodes` as Identifier);
this.context_overflow = this.context.length > 31; this.context_overflow = this.context.length > 31;
this.context.forEach((member) => { this.context.forEach((member) => {
const { variable } = member; const { variable } = member;
if (variable) { if (variable) {
member.priority += 2; member.priority += 2;
if (variable.mutated || variable.reassigned) member.priority += 4; if (variable.mutated || variable.reassigned)
member.priority += 4;
// these determine whether variable is included in initial context // these determine whether variable is included in initial context
// array, so must have the highest priority // array, so must have the highest priority
if (variable.is_reactive_dependency && (variable.mutated || variable.reassigned)) if (variable.is_reactive_dependency && (variable.mutated || variable.reassigned))
member.priority += 16; member.priority += 16;
if (variable.export_name) member.priority += 32; if (variable.export_name)
if (variable.referenced) member.priority += 64; member.priority += 32;
} else if (member.is_non_contextual) { if (variable.referenced)
member.priority += 64;
}
else if (member.is_non_contextual) {
// determine whether variable is included in initial context // determine whether variable is included in initial context
// array, so must have the highest priority // array, so must have the highest priority
member.priority += 8; member.priority += 8;
} }
if (!member.is_contextual) { if (!member.is_contextual) {
member.priority += 1; member.priority += 1;
} }
}); });
this.context.sort((a, b) => b.priority - a.priority || ( /** @type {number} */(a.index.value)) - ( /** @type {number} */(b.index.value)));
this.context.sort(
(a, b) => b.priority - a.priority || (a.index.value as number) - (b.index.value as number)
);
this.context.forEach((member, i) => (member.index.value = i)); this.context.forEach((member, i) => (member.index.value = i));
let i = this.context.length; let i = this.context.length;
while (i--) { while (i--) {
const member = this.context[i]; const member = this.context[i];
if (member.variable) { if (member.variable) {
if ( if (member.variable.referenced ||
member.variable.referenced ||
member.variable.export_name || member.variable.export_name ||
(member.variable.is_reactive_dependency && (member.variable.is_reactive_dependency &&
(member.variable.mutated || member.variable.reassigned)) (member.variable.mutated || member.variable.reassigned)))
)
break; break;
} else if (member.is_non_contextual) { }
else if (member.is_non_contextual) {
break; break;
} }
} }
this.initial_context = this.context.slice(0, i + 1); this.initial_context = this.context.slice(0, i + 1);
} }
add_to_context(name: string, contextual = false) { /**
* @param {string} name
* @param {any} contextual
*/
add_to_context(name, contextual = false) {
if (!this.context_lookup.has(name)) { if (!this.context_lookup.has(name)) {
const member: ContextMember = {
/** @type {ContextMember} */
const member = {
name, name,
index: { type: 'Literal', value: this.context.length }, // index is updated later, but set here to preserve order within groups index: { type: 'Literal', value: this.context.length },
is_contextual: false, is_contextual: false,
is_non_contextual: false, // shadowed vars could be contextual and non-contextual is_non_contextual: false,
variable: null, variable: null,
priority: 0 priority: 0
}; };
this.context_lookup.set(name, member); this.context_lookup.set(name, member);
this.context.push(member); this.context.push(member);
} }
const member = this.context_lookup.get(name); const member = this.context_lookup.get(name);
if (contextual) { if (contextual) {
member.is_contextual = true; member.is_contextual = true;
} else { }
else {
member.is_non_contextual = true; member.is_non_contextual = true;
member.variable = this.component.var_lookup.get(name); member.variable = this.component.var_lookup.get(name);
} }
return member; return member;
} }
invalidate(name: string, value?: unknown, main_execution_context: boolean = false) { /**
* @param {string} name
* @param {unknown} [value]
* @param {boolean} main_execution_context
*/
invalidate(name, value, main_execution_context = false) {
return renderer_invalidate(this, name, value, main_execution_context); return renderer_invalidate(this, name, value, main_execution_context);
} }
dirty(names: string[], is_reactive_declaration = false): Expression { /**
* @param {string[]} names
* @param {any} is_reactive_declaration
* @returns {import("C:/repos/svelte/svelte/node_modules/.pnpm/@types+estree@1.0.0/node_modules/@types/estree/index").import('estree').Expression}
*/
dirty(names, is_reactive_declaration = false) {
const renderer = this; const renderer = this;
const dirty = /** @type {| import('estree').Identifier
const dirty = (is_reactive_declaration ? x`$$self.$$.dirty` : x`#dirty`) as | import('estree').MemberExpression} */ ((is_reactive_declaration ? x `$$self.$$.dirty` : x `#dirty`));
| Identifier
| MemberExpression;
const get_bitmask = () => { const get_bitmask = () => {
const bitmask: BitMasks = [];
/** @type {BitMasks} */
const bitmask = [];
names.forEach((name) => { names.forEach((name) => {
const member = renderer.context_lookup.get(name); const member = renderer.context_lookup.get(name);
if (!member)
if (!member) return; return;
if (member.index.value === -1) { if (member.index.value === -1) {
throw new Error('unset index'); throw new Error('unset index');
} }
const value = /** @type {number} */ (member.index.value);
const value = member.index.value as number;
const i = (value / 31) | 0; const i = (value / 31) | 0;
const n = 1 << value % 31; const n = 1 << value % 31;
if (!bitmask[i])
if (!bitmask[i]) bitmask[i] = { n: 0, names: [] }; bitmask[i] = { n: 0, names: [] };
bitmask[i].n |= n; bitmask[i].n |= n;
bitmask[i].names.push(name); bitmask[i].names.push(name);
}); });
return bitmask; return bitmask;
}; };
// TODO: context-overflow make it less gross // TODO: context-overflow make it less gross
return { return /** @type {any} */ ({
// Using a ParenthesizedExpression allows us to create // Using a ParenthesizedExpression allows us to create
// the expression lazily. TODO would be better if // the expression lazily. TODO would be better if
// context was determined before rendering, so that // context was determined before rendering, so that
@ -236,11 +212,9 @@ export default class Renderer {
type: 'ParenthesizedExpression', type: 'ParenthesizedExpression',
get expression() { get expression() {
const bitmask = get_bitmask(); const bitmask = get_bitmask();
if (!bitmask.length) { if (!bitmask.length) {
return x`${dirty} & /*${names.join(', ')}*/ 0` as BinaryExpression; return /** @type {import('estree').BinaryExpression} */ (x `${dirty} & /*${names.join(', ')}*/ 0`);
} }
if (renderer.context_overflow) { if (renderer.context_overflow) {
return bitmask return bitmask
.map((b, i) => ({ b, i })) .map((b, i) => ({ b, i }))
@ -248,18 +222,20 @@ export default class Renderer {
.map(({ b, i }) => x `${dirty}[${i}] & /*${b.names.join(', ')}*/ ${b.n}`) .map(({ b, i }) => x `${dirty}[${i}] & /*${b.names.join(', ')}*/ ${b.n}`)
.reduce((lhs, rhs) => x `${lhs} | ${rhs}`); .reduce((lhs, rhs) => x `${lhs} | ${rhs}`);
} }
return /** @type {import('estree').BinaryExpression} */ (x `${dirty} & /*${names.join(', ')}*/ ${bitmask[0].n}`);
return x`${dirty} & /*${names.join(', ')}*/ ${bitmask[0].n}` as BinaryExpression;
} }
} as any; });
} }
// NOTE: this method may be called before this.context_overflow / this.context is fully defined // NOTE: this method may be called before this.context_overflow / this.context is fully defined
// therefore, they can only be evaluated later in a getter function // therefore, they can only be evaluated later in a getter function
get_initial_dirty(): UnaryExpression | ArrayExpression {
/** @returns {import("C:/repos/svelte/svelte/node_modules/.pnpm/@types+estree@1.0.0/node_modules/@types/estree/index").import('estree').UnaryExpression | import("C:/repos/svelte/svelte/node_modules/.pnpm/@types+estree@1.0.0/node_modules/@types/estree/index").import('estree').ArrayExpression} */
get_initial_dirty() {
const _this = this; const _this = this;
// TODO: context-overflow make it less gross // TODO: context-overflow make it less gross
const val: UnaryExpression = x`-1` as UnaryExpression;
/** @type {import('estree').UnaryExpression} */
const val = /** @type {import('estree').UnaryExpression} */ (x `-1`);
return { return {
get type() { get type() {
return _this.context_overflow ? 'ArrayExpression' : 'UnaryExpression'; return _this.context_overflow ? 'ArrayExpression' : 'UnaryExpression';
@ -279,32 +255,57 @@ export default class Renderer {
}; };
} }
reference(node: string | Identifier | MemberExpression, ctx: string | void = '#ctx') { /**
* @param {string | import('estree').Identifier | import('estree').MemberExpression} node
* @param {string | void} ctx
*/
reference(node, ctx = '#ctx') {
if (typeof node === 'string') { if (typeof node === 'string') {
node = { type: 'Identifier', name: node }; node = { type: 'Identifier', name: node };
} }
const { name, nodes } = flatten_reference(node); const { name, nodes } = flatten_reference(node);
const member = this.context_lookup.get(name); const member = this.context_lookup.get(name);
// TODO is this correct? // TODO is this correct?
if (this.component.var_lookup.get(name)) { if (this.component.var_lookup.get(name)) {
this.component.add_reference(node, name); this.component.add_reference(node, name);
} }
if (member !== undefined) { if (member !== undefined) {
const replacement = x`/*${member.name}*/ ${ctx}[${member.index}]` as MemberExpression; const replacement = /** @type {import('estree').MemberExpression} */ (x `/*${member.name}*/ ${ctx}[${member.index}]`);
if (nodes[0].loc)
if (nodes[0].loc) replacement.object.loc = nodes[0].loc; replacement.object.loc = nodes[0].loc;
nodes[0] = replacement; nodes[0] = replacement;
return nodes.reduce((lhs, rhs) => x `${lhs}.${rhs}`); return nodes.reduce((lhs, rhs) => x `${lhs}.${rhs}`);
} }
return node; return node;
} }
remove_block(block: Block | Node | Node[]) { /** @param {import('./Block.js').default | import('estree').Node | import('estree').Node[]} block */
remove_block(block) {
this.blocks.splice(this.blocks.indexOf(block), 1); this.blocks.splice(this.blocks.indexOf(block), 1);
} }
} }
/**
* @typedef {Array<{
* n: number;
* names: string[];
* }>} BitMasks
*/
/** @typedef {Object} ContextMember
* @property {string} name
* @property {Literal} index
* @property {boolean} is_contextual
* @property {boolean} is_non_contextual
* @property {Var} variable
* @property {number} priority
*/
/** @typedef {Object} BindingGroup
* @property {(to_reference?:boolean)=>Node} binding_group
* @property {string[]} contexts
* @property {Set<string>} list_dependencies
* @property {string} keypath
* @property {(block:Block,element:Identifier)=>void} add_element
* @property {(block:Block)=>void} render
*/
Loading…
Cancel
Save