From 32342c3a4e8e63ac6d230cf51dc32c3150167a63 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 12 May 2023 15:17:03 +0200 Subject: [PATCH] Convert src/compiler/compile/render_dom/Renderer.ts to JavaScript --- src/compiler/compile/render_dom/Block.js | 42 +- src/compiler/compile/render_dom/Renderer.ts | 611 ++++++++++---------- 2 files changed, 330 insertions(+), 323 deletions(-) diff --git a/src/compiler/compile/render_dom/Block.js b/src/compiler/compile/render_dom/Block.js index 803f4d82d0..f325a131a2 100644 --- a/src/compiler/compile/render_dom/Block.js +++ b/src/compiler/compile/render_dom/Block.js @@ -1,7 +1,28 @@ import { b, x } from 'code-red'; import { is_head } from './wrappers/shared/is_head.js'; import { regex_double_quotes } from '../../utils/patterns.js'; + 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} [bindings] + * @property {Set} [dependencies] + */ + /** @type {Block} */ parent; @@ -131,6 +152,7 @@ export default class Block { this.aliases = new Map(); if (this.key) this.aliases.set('key', this.get_unique_name('key')); } + assign_variable_names() { /** @type {Set} */ const seen = new Set(); @@ -206,6 +228,7 @@ export default class Block { this.outros += 1; if (!local && this.parent) this.parent.add_outro(); } + add_animation() { this.has_animation = true; } @@ -431,6 +454,7 @@ export default class Block { this.has_animation ); } + render() { 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} [bindings] - * @property {Set} [dependencies] - */ diff --git a/src/compiler/compile/render_dom/Renderer.ts b/src/compiler/compile/render_dom/Renderer.ts index 973507e2d7..73d1c9878e 100644 --- a/src/compiler/compile/render_dom/Renderer.ts +++ b/src/compiler/compile/render_dom/Renderer.ts @@ -1,310 +1,311 @@ -import Block from './Block'; -import { CompileOptions, Var } from '../../interfaces'; -import Component from '../Component'; -import FragmentWrapper from './wrappers/Fragment'; +import Block from './Block.js'; +import FragmentWrapper from './wrappers/Fragment.js'; import { x } from 'code-red'; -import { - Node, - Identifier, - MemberExpression, - 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 { - name: string; - index: Literal; - is_contextual: boolean; - is_non_contextual: boolean; - variable: Var; - priority: number; -} - -type BitMasks = Array<{ - n: number; - names: string[]; -}>; - -export interface BindingGroup { - binding_group: (to_reference?: boolean) => Node; - contexts: string[]; - list_dependencies: Set; - keypath: string; - add_element: (block: Block, element: Identifier) => void; - render: (block: Block) => void; -} - +import flatten_reference from '../utils/flatten_reference.js'; +import { reserved_keywords } from '../utils/reserved_keywords.js'; +import { renderer_invalidate } from './invalidate.js'; export default class Renderer { - component: Component; // TODO Maybe Renderer shouldn't know about Component? - options: CompileOptions; - - context: ContextMember[] = []; - initial_context: ContextMember[] = []; - context_lookup: Map = new Map(); - context_overflow: boolean; - blocks: Array = []; - readonly: Set = new Set(); - meta_bindings: Array = []; // initial values for e.g. window.innerWidth, if there's a meta tag - binding_groups: Map = new Map(); - - block: Block; - fragment: FragmentWrapper; - - file_var: Identifier; - locate: (c: number) => { line: number; column: number }; - - constructor(component: Component, options: CompileOptions) { - this.component = component; - this.options = options; - this.locate = component.locate; // TODO messy - - this.file_var = options.dev && this.component.get_unique_name('file'); - - component.vars - .filter((v) => !v.hoistable || (v.export_name && !v.module)) - .forEach((v) => this.add_to_context(v.name)); - - // ensure store values are included in context - component.vars.filter((v) => v.subscribable).forEach((v) => this.add_to_context(`$${v.name}`)); - - reserved_keywords.forEach((keyword) => { - if (component.var_lookup.has(keyword)) { - this.add_to_context(keyword); - } - }); - - if (component.slots.size > 0) { - this.add_to_context('$$scope'); - this.add_to_context('#slots'); - } - - // main block - this.block = new Block({ - renderer: this, - name: null, - type: 'component', - key: null, - - bindings: new Map(), - - dependencies: new Set() - }); - - this.block.has_update_method = true; - - this.fragment = new FragmentWrapper( - this, - this.block, - component.fragment.children, - null, - true, - null - ); - - // TODO messy - this.blocks.forEach((block) => { - if (block instanceof Block) { - block.assign_variable_names(); - } - }); - - this.block.assign_variable_names(); - - this.fragment.render(this.block, null, x`#nodes` as Identifier); - - this.context_overflow = this.context.length > 31; - - this.context.forEach((member) => { - const { variable } = member; - if (variable) { - member.priority += 2; - if (variable.mutated || variable.reassigned) member.priority += 4; - - // these determine whether variable is included in initial context - // array, so must have the highest priority - if (variable.is_reactive_dependency && (variable.mutated || variable.reassigned)) - member.priority += 16; - if (variable.export_name) member.priority += 32; - if (variable.referenced) member.priority += 64; - } else if (member.is_non_contextual) { - // determine whether variable is included in initial context - // array, so must have the highest priority - member.priority += 8; - } - - if (!member.is_contextual) { - member.priority += 1; - } - }); - - 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)); - let i = this.context.length; - while (i--) { - const member = this.context[i]; - if (member.variable) { - if ( - member.variable.referenced || - member.variable.export_name || - (member.variable.is_reactive_dependency && - (member.variable.mutated || member.variable.reassigned)) - ) - break; - } else if (member.is_non_contextual) { - break; - } - } - this.initial_context = this.context.slice(0, i + 1); - } - - add_to_context(name: string, contextual = false) { - if (!this.context_lookup.has(name)) { - const member: ContextMember = { - name, - index: { type: 'Literal', value: this.context.length }, // index is updated later, but set here to preserve order within groups - is_contextual: false, - is_non_contextual: false, // shadowed vars could be contextual and non-contextual - variable: null, - priority: 0 - }; - - this.context_lookup.set(name, member); - this.context.push(member); - } - - const member = this.context_lookup.get(name); - - if (contextual) { - member.is_contextual = true; - } else { - member.is_non_contextual = true; - member.variable = this.component.var_lookup.get(name); - } - - return member; - } - - invalidate(name: string, value?: unknown, main_execution_context: boolean = false) { - return renderer_invalidate(this, name, value, main_execution_context); - } - - dirty(names: string[], is_reactive_declaration = false): Expression { - const renderer = this; - - const dirty = (is_reactive_declaration ? x`$$self.$$.dirty` : x`#dirty`) as - | Identifier - | MemberExpression; - - const get_bitmask = () => { - const bitmask: BitMasks = []; - names.forEach((name) => { - const member = renderer.context_lookup.get(name); - - if (!member) return; - - if (member.index.value === -1) { - throw new Error('unset index'); - } - - const value = member.index.value as number; - const i = (value / 31) | 0; - const n = 1 << value % 31; - - if (!bitmask[i]) bitmask[i] = { n: 0, names: [] }; - - bitmask[i].n |= n; - bitmask[i].names.push(name); - }); - return bitmask; - }; - - // TODO: context-overflow make it less gross - return { - // Using a ParenthesizedExpression allows us to create - // the expression lazily. TODO would be better if - // context was determined before rendering, so that - // this indirection was unnecessary - type: 'ParenthesizedExpression', - get expression() { - const bitmask = get_bitmask(); - - if (!bitmask.length) { - return x`${dirty} & /*${names.join(', ')}*/ 0` as BinaryExpression; - } - - if (renderer.context_overflow) { - return bitmask - .map((b, i) => ({ b, i })) - .filter(({ b }) => b) - .map(({ b, i }) => x`${dirty}[${i}] & /*${b.names.join(', ')}*/ ${b.n}`) - .reduce((lhs, rhs) => x`${lhs} | ${rhs}`); - } - - 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 - // therefore, they can only be evaluated later in a getter function - get_initial_dirty(): UnaryExpression | ArrayExpression { - const _this = this; - // TODO: context-overflow make it less gross - const val: UnaryExpression = x`-1` as UnaryExpression; - return { - get type() { - return _this.context_overflow ? 'ArrayExpression' : 'UnaryExpression'; - }, - // as [-1] - get elements() { - const elements = []; - for (let i = 0; i < _this.context.length; i += 31) { - elements.push(val); - } - return elements; - }, - // as -1 - operator: val.operator, - prefix: val.prefix, - argument: val.argument - }; - } - - reference(node: string | Identifier | MemberExpression, ctx: string | void = '#ctx') { - if (typeof node === 'string') { - node = { type: 'Identifier', name: node }; - } - - const { name, nodes } = flatten_reference(node); - const member = this.context_lookup.get(name); - - // TODO is this correct? - if (this.component.var_lookup.get(name)) { - this.component.add_reference(node, name); - } - - if (member !== undefined) { - const replacement = x`/*${member.name}*/ ${ctx}[${member.index}]` as MemberExpression; - - if (nodes[0].loc) replacement.object.loc = nodes[0].loc; - nodes[0] = replacement; - - return nodes.reduce((lhs, rhs) => x`${lhs}.${rhs}`); - } + /** @type {import('../Component.js').default} */ + component; // TODO Maybe Renderer shouldn't know about Component? + + /** @type {import('../../interfaces.js').CompileOptions} */ + options; + + /** @type {ContextMember[]} */ + context = []; + + /** @type {ContextMember[]} */ + initial_context = []; + + /** @type {Map} */ + context_lookup = new Map(); + + /** @type {boolean} */ + context_overflow; + + /** @type {Array} */ + blocks = []; + + /** @type {Set} */ + readonly = new Set(); + + /** @type {Array} */ + meta_bindings = []; // initial values for e.g. window.innerWidth, if there's a meta tag + + /** @type {Map} */ + binding_groups = new Map(); + + /** @type {import('./Block.js').default} */ + block; + + /** @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.options = options; + this.locate = component.locate; // TODO messy + this.file_var = options.dev && this.component.get_unique_name('file'); + component.vars + .filter((v) => !v.hoistable || (v.export_name && !v.module)) + .forEach((v) => this.add_to_context(v.name)); + // ensure store values are included in context + component.vars.filter((v) => v.subscribable).forEach((v) => this.add_to_context(`$${v.name}`)); + reserved_keywords.forEach((keyword) => { + if (component.var_lookup.has(keyword)) { + this.add_to_context(keyword); + } + }); + if (component.slots.size > 0) { + this.add_to_context('$$scope'); + this.add_to_context('#slots'); + } + // main block + this.block = new Block({ + renderer: this, + name: null, + type: 'component', + key: null, + bindings: new Map(), + dependencies: new Set() + }); + this.block.has_update_method = true; + this.fragment = new FragmentWrapper(this, this.block, component.fragment.children, null, true, null); + // TODO messy + this.blocks.forEach((block) => { + if (block instanceof Block) { + block.assign_variable_names(); + } + }); + this.block.assign_variable_names(); + this.fragment.render(this.block, null, /** @type {import('estree').Identifier} */ (x `#nodes`)); + this.context_overflow = this.context.length > 31; + this.context.forEach((member) => { + const { variable } = member; + if (variable) { + member.priority += 2; + if (variable.mutated || variable.reassigned) + member.priority += 4; + // these determine whether variable is included in initial context + // array, so must have the highest priority + if (variable.is_reactive_dependency && (variable.mutated || variable.reassigned)) + member.priority += 16; + if (variable.export_name) + member.priority += 32; + if (variable.referenced) + member.priority += 64; + } + else if (member.is_non_contextual) { + // determine whether variable is included in initial context + // array, so must have the highest priority + member.priority += 8; + } + if (!member.is_contextual) { + member.priority += 1; + } + }); + this.context.sort((a, b) => b.priority - a.priority || ( /** @type {number} */(a.index.value)) - ( /** @type {number} */(b.index.value))); + this.context.forEach((member, i) => (member.index.value = i)); + let i = this.context.length; + while (i--) { + const member = this.context[i]; + if (member.variable) { + if (member.variable.referenced || + member.variable.export_name || + (member.variable.is_reactive_dependency && + (member.variable.mutated || member.variable.reassigned))) + break; + } + else if (member.is_non_contextual) { + break; + } + } + this.initial_context = this.context.slice(0, i + 1); + } + + /** + * @param {string} name + * @param {any} contextual + */ + add_to_context(name, contextual = false) { + if (!this.context_lookup.has(name)) { + + /** @type {ContextMember} */ + const member = { + name, + index: { type: 'Literal', value: this.context.length }, + is_contextual: false, + is_non_contextual: false, + variable: null, + priority: 0 + }; + this.context_lookup.set(name, member); + this.context.push(member); + } + const member = this.context_lookup.get(name); + if (contextual) { + member.is_contextual = true; + } + else { + member.is_non_contextual = true; + member.variable = this.component.var_lookup.get(name); + } + return member; + } + + /** + * @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); + } + + /** + * @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 dirty = /** @type {| import('estree').Identifier + | import('estree').MemberExpression} */ ((is_reactive_declaration ? x `$$self.$$.dirty` : x `#dirty`)); + const get_bitmask = () => { + + /** @type {BitMasks} */ + const bitmask = []; + names.forEach((name) => { + const member = renderer.context_lookup.get(name); + if (!member) + return; + if (member.index.value === -1) { + throw new Error('unset index'); + } + const value = /** @type {number} */ (member.index.value); + const i = (value / 31) | 0; + const n = 1 << value % 31; + if (!bitmask[i]) + bitmask[i] = { n: 0, names: [] }; + bitmask[i].n |= n; + bitmask[i].names.push(name); + }); + return bitmask; + }; + // TODO: context-overflow make it less gross + return /** @type {any} */ ({ + // Using a ParenthesizedExpression allows us to create + // the expression lazily. TODO would be better if + // context was determined before rendering, so that + // this indirection was unnecessary + type: 'ParenthesizedExpression', + get expression() { + const bitmask = get_bitmask(); + if (!bitmask.length) { + return /** @type {import('estree').BinaryExpression} */ (x `${dirty} & /*${names.join(', ')}*/ 0`); + } + if (renderer.context_overflow) { + return bitmask + .map((b, i) => ({ b, i })) + .filter(({ b }) => b) + .map(({ b, i }) => x `${dirty}[${i}] & /*${b.names.join(', ')}*/ ${b.n}`) + .reduce((lhs, rhs) => x `${lhs} | ${rhs}`); + } + return /** @type {import('estree').BinaryExpression} */ (x `${dirty} & /*${names.join(', ')}*/ ${bitmask[0].n}`); + } + }); + } + // 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 + + /** @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; + // TODO: context-overflow make it less gross + + /** @type {import('estree').UnaryExpression} */ + const val = /** @type {import('estree').UnaryExpression} */ (x `-1`); + return { + get type() { + return _this.context_overflow ? 'ArrayExpression' : 'UnaryExpression'; + }, + // as [-1] + get elements() { + const elements = []; + for (let i = 0; i < _this.context.length; i += 31) { + elements.push(val); + } + return elements; + }, + // as -1 + operator: val.operator, + prefix: val.prefix, + argument: val.argument + }; + } + + /** + * @param {string | import('estree').Identifier | import('estree').MemberExpression} node + * @param {string | void} ctx + */ + reference(node, ctx = '#ctx') { + if (typeof node === 'string') { + node = { type: 'Identifier', name: node }; + } + const { name, nodes } = flatten_reference(node); + const member = this.context_lookup.get(name); + // TODO is this correct? + if (this.component.var_lookup.get(name)) { + this.component.add_reference(node, name); + } + if (member !== undefined) { + const replacement = /** @type {import('estree').MemberExpression} */ (x `/*${member.name}*/ ${ctx}[${member.index}]`); + if (nodes[0].loc) + replacement.object.loc = nodes[0].loc; + nodes[0] = replacement; + return nodes.reduce((lhs, rhs) => x `${lhs}.${rhs}`); + } + return node; + } + + /** @param {import('./Block.js').default | import('estree').Node | import('estree').Node[]} block */ + remove_block(block) { + this.blocks.splice(this.blocks.indexOf(block), 1); + } +} - return node; - } - remove_block(block: Block | Node | Node[]) { - 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} list_dependencies + * @property {string} keypath + * @property {(block:Block,element:Identifier)=>void} add_element + * @property {(block:Block)=>void} render + */ \ No newline at end of file