Convert src/compiler/compile/render_ssr/index.ts to JavaScript

pull/8569/head
Simon Holthausen 3 years ago
parent 6d5d01efe3
commit 74469618bd

@ -15,9 +15,10 @@ import Text from './handlers/Text.js';
import Title from './handlers/Title.js'; import Title from './handlers/Title.js';
import { collapse_template_literal } from '../utils/collapse_template_literal.js'; import { collapse_template_literal } from '../utils/collapse_template_literal.js';
import { escape_template } from '../utils/stringify.js'; import { escape_template } from '../utils/stringify.js';
function noop() {} function noop() {}
/** @type {Record<string, Handler>} */ /** @type {Record<string, {(node: any, renderer: Renderer, options: import('../../interfaces.js').CompileOptions): void}>} */
const handlers = { const handlers = {
AwaitBlock, AwaitBlock,
Body: noop, Body: noop,
@ -39,18 +40,14 @@ const handlers = {
Title, Title,
Window: noop Window: noop
}; };
/** */
export default class Renderer { export default class Renderer {
/** @default false */
has_bindings = false; has_bindings = false;
/** @type {import('estree').Identifier} */ /** @type {import('estree').Identifier} */
name = undefined; name = undefined;
/** /** @type {Array<{ current: { value: string }; literal: import('estree').TemplateLiteral }>} */
* @default []
* @type {Array<{ current: { value: string }; literal: TemplateLiteral }>}
*/
stack = []; stack = [];
/** @type {{ value: string }} */ /** @type {{ value: string }} */
@ -59,10 +56,7 @@ export default class Renderer {
/** @type {import('estree').TemplateLiteral} */ /** @type {import('estree').TemplateLiteral} */
literal = undefined; literal = undefined;
/** /** @type {import('../../interfaces.js').AppendTarget[]} */
* @default []
* @type {AppendTarget[]}
*/
targets = []; targets = [];
constructor({ name }) { constructor({ name }) {
this.name = name; this.name = name;
@ -111,8 +105,8 @@ export default class Renderer {
} }
/** /**
* @param {INode[]} nodes * @param {import('../nodes/interfaces.js').INode[]} nodes
* @param {RenderOptions} options * @param {import('./private.js').RenderOptions} options
*/ */
render(nodes, options) { render(nodes, options) {
nodes.forEach((node) => { nodes.forEach((node) => {
@ -124,11 +118,3 @@ export default class Renderer {
}); });
} }
} }
/** @typedef {(node: any, renderer: Renderer, options: CompileOptions) => void} Handler */
/** @typedef {Object} RenderOptions
* @property {(c:number)=>{line:number;column:number}} locate
* @property {string} [head_id]
* @property {boolean} [has_added_svelte_hash]
*/

@ -4,7 +4,7 @@ import { get_const_tags } from './shared/get_const_tags.js';
/** /**
* @param {import('../../nodes/AwaitBlock.js').default} node * @param {import('../../nodes/AwaitBlock.js').default} node
* @param {import('../Renderer.js').default} renderer * @param {import('../Renderer.js').default} renderer
* @param {import('../Renderer.js').RenderOptions} options * @param {import('../private.js').RenderOptions} options
*/ */
export default function (node, renderer, options) { export default function (node, renderer, options) {
renderer.push(); renderer.push();

@ -1,7 +1,7 @@
/** /**
* @param {import('../../nodes/Comment.js').default} node * @param {import('../../nodes/Comment.js').default} node
* @param {import('../Renderer.js').default} renderer * @param {import('../Renderer.js').default} renderer
* @param {import('../Renderer.js').RenderOptions} options * @param {import('../private.js').RenderOptions} options
*/ */
export default function (node, renderer, options) { export default function (node, renderer, options) {
if (options.preserveComments) { if (options.preserveComments) {

@ -3,7 +3,7 @@ import { x, p } from 'code-red';
/** /**
* @param {import('../../nodes/DebugTag.js').default} node * @param {import('../../nodes/DebugTag.js').default} node
* @param {import('../Renderer.js').default} renderer * @param {import('../Renderer.js').default} renderer
* @param {import('../Renderer.js').RenderOptions} options * @param {import('../private.js').RenderOptions} options
*/ */
export default function (node, renderer, options) { export default function (node, renderer, options) {
if (!options.dev) return; if (!options.dev) return;

@ -4,7 +4,7 @@ import { get_const_tags } from './shared/get_const_tags.js';
/** /**
* @param {import('../../nodes/EachBlock.js').default} node * @param {import('../../nodes/EachBlock.js').default} node
* @param {import('../Renderer.js').default} renderer * @param {import('../Renderer.js').default} renderer
* @param {import('../Renderer.js').RenderOptions} options * @param {import('../private.js').RenderOptions} options
*/ */
export default function (node, renderer, options) { export default function (node, renderer, options) {
const args = [node.context_node]; const args = [node.context_node];

@ -15,7 +15,7 @@ import { regex_starts_with_newline } from '../../../utils/patterns.js';
/** /**
* @param {import('../../nodes/Element.js').default} node * @param {import('../../nodes/Element.js').default} node
* @param {import('../Renderer.js').default} renderer * @param {import('../Renderer.js').default} renderer
* @param {import('../Renderer.js').RenderOptions} options * @param {import('../private.js').RenderOptions} options
*/ */
export default function (node, renderer, options) { export default function (node, renderer, options) {
const children = remove_whitespace_children(node.children, node.next); const children = remove_whitespace_children(node.children, node.next);

@ -3,7 +3,7 @@ import { x } from 'code-red';
/** /**
* @param {import('../../nodes/Head.js').default} node * @param {import('../../nodes/Head.js').default} node
* @param {import('../Renderer.js').default} renderer * @param {import('../Renderer.js').default} renderer
* @param {import('../Renderer.js').RenderOptions} options * @param {import('../private.js').RenderOptions} options
*/ */
export default function (node, renderer, options) { export default function (node, renderer, options) {
const head_options = { const head_options = {

@ -1,7 +1,7 @@
/** /**
* @param {import('../../nodes/RawMustacheTag.js').default} node * @param {import('../../nodes/RawMustacheTag.js').default} node
* @param {import('../Renderer.js').default} renderer * @param {import('../Renderer.js').default} renderer
* @param {import('../Renderer.js').RenderOptions} options * @param {import('../private.js').RenderOptions} options
*/ */
export default function (node, renderer, options) { export default function (node, renderer, options) {
if (options.hydratable) renderer.add_string('<!-- HTML_TAG_START -->'); if (options.hydratable) renderer.add_string('<!-- HTML_TAG_START -->');

@ -4,7 +4,7 @@ import { get_const_tags } from './shared/get_const_tags.js';
/** /**
* @param {import('../../nodes/IfBlock.js').default} node * @param {import('../../nodes/IfBlock.js').default} node
* @param {import('../Renderer.js').default} renderer * @param {import('../Renderer.js').default} renderer
* @param {import('../Renderer.js').RenderOptions} options * @param {import('../private.js').RenderOptions} options
*/ */
export default function (node, renderer, options) { export default function (node, renderer, options) {
const condition = node.expression.node; const condition = node.expression.node;

@ -18,7 +18,7 @@ function get_prop_value(attribute) {
/** /**
* @param {import('../../nodes/InlineComponent.js').default} node * @param {import('../../nodes/InlineComponent.js').default} node
* @param {import('../Renderer.js').default} renderer * @param {import('../Renderer.js').default} renderer
* @param {import('../Renderer.js').RenderOptions} options * @param {import('../private.js').RenderOptions} options
*/ */
export default function (node, renderer, options) { export default function (node, renderer, options) {
const binding_props = []; const binding_props = [];

@ -1,7 +1,7 @@
/** /**
* @param {import('../../nodes/KeyBlock.js').default} node * @param {import('../../nodes/KeyBlock.js').default} node
* @param {import('../Renderer.js').default} renderer * @param {import('../Renderer.js').default} renderer
* @param {import('../Renderer.js').RenderOptions} options * @param {import('../private.js').RenderOptions} options
*/ */
export default function (node, renderer, options) { export default function (node, renderer, options) {
renderer.render(node.children, options); renderer.render(node.children, options);

@ -5,7 +5,7 @@ import { get_slot_scope } from './shared/get_slot_scope.js';
/** /**
* @param {import('../../nodes/Slot.js').default} node * @param {import('../../nodes/Slot.js').default} node
* @param {import('../Renderer.js').default} renderer * @param {import('../Renderer.js').default} renderer
* @param {import('../Renderer.js').RenderOptions & { * @param {import('../private.js').RenderOptions & {
* slot_scopes: Map<any, any>; * slot_scopes: Map<any, any>;
* }} options * }} options
*/ */

@ -6,7 +6,7 @@ import { get_const_tags } from './shared/get_const_tags.js';
/** /**
* @param {import('../../nodes/SlotTemplate.js').default} node * @param {import('../../nodes/SlotTemplate.js').default} node
* @param {import('../Renderer.js').default} renderer * @param {import('../Renderer.js').default} renderer
* @param {import('../Renderer.js').RenderOptions & { * @param {import('../private.js').RenderOptions & {
* slot_scopes: Map<any, any>; * slot_scopes: Map<any, any>;
* }} options * }} options
*/ */

@ -3,7 +3,7 @@ import { x } from 'code-red';
/** /**
* @param {any} node * @param {any} node
* @param {import('../Renderer.js').default} renderer * @param {import('../Renderer.js').default} renderer
* @param {import('../Renderer.js').RenderOptions} _options * @param {import('../private.js').RenderOptions} _options
*/ */
export default function (node, renderer, _options) { export default function (node, renderer, _options) {
const snippet = node.expression.node; const snippet = node.expression.node;

@ -3,7 +3,7 @@ import { escape_html } from '../../utils/stringify.js';
/** /**
* @param {import('../../nodes/Text.js').default} node * @param {import('../../nodes/Text.js').default} node
* @param {import('../Renderer.js').default} renderer * @param {import('../Renderer.js').default} renderer
* @param {import('../Renderer.js').RenderOptions} _options * @param {import('../private.js').RenderOptions} _options
*/ */
export default function (node, renderer, _options) { export default function (node, renderer, _options) {
let text = node.data; let text = node.data;
@ -12,7 +12,7 @@ export default function (node, renderer, _options) {
} else if ( } else if (
!node.parent || !node.parent ||
node.parent.type !== 'Element' || node.parent.type !== 'Element' ||
(/** @type {import('../../nodes/Element.js').default} */ (node.parent).name !== 'script' && /** @type {import('../../nodes/Element.js').default} */ ((node.parent).name !== 'script' &&
/** @type {import('../../nodes/Element.js').default} */ (node.parent).name !== 'style') /** @type {import('../../nodes/Element.js').default} */ (node.parent).name !== 'style')
) { ) {
// unless this Text node is inside a <script> or <style> element, escape &,<,> // unless this Text node is inside a <script> or <style> element, escape &,<,>

@ -3,7 +3,7 @@ import { x } from 'code-red';
/** /**
* @param {import('../../nodes/Title.js').default} node * @param {import('../../nodes/Title.js').default} node
* @param {import('../Renderer.js').default} renderer * @param {import('../Renderer.js').default} renderer
* @param {import('../Renderer.js').RenderOptions} options * @param {import('../private.js').RenderOptions} options
*/ */
export default function (node, renderer, options) { export default function (node, renderer, options) {
renderer.push(); renderer.push();

@ -1,176 +1,133 @@
import { b } from 'code-red'; import { b } from 'code-red';
import Component from '../Component'; import { string_literal } from '../utils/stringify.js';
import { CompileOptions, CssResult } from '../../interfaces'; import Renderer from './Renderer.js';
import { string_literal } from '../utils/stringify';
import Renderer from './Renderer';
import { INode as TemplateNode } from '../nodes/interfaces'; // TODO
import Text from '../nodes/Text';
import { LabeledStatement, Statement, Node } from 'estree';
import { extract_names } from 'periscopic'; import { extract_names } from 'periscopic';
import { walk } from 'estree-walker'; import { walk } from 'estree-walker';
import { invalidate } from '../render_dom/invalidate.js';
import { invalidate } from '../render_dom/invalidate'; import check_enable_sourcemap from '../utils/check_enable_sourcemap.js';
import check_enable_sourcemap from '../utils/check_enable_sourcemap';
/**
export default function ssr( * @param {import('../Component.js').default} component
component: Component, * @param {import('../../interfaces.js').CompileOptions} options
options: CompileOptions * @returns {{ js: import("C:/repos/svelte/svelte/node_modules/.pnpm/@types+estree@1.0.0/node_modules/@types/estree/index").Node[]; css: CssResult; }}
): { js: Node[]; css: CssResult } { */
const renderer = new Renderer({ export default function ssr(component, options) {
name: component.name const renderer = new Renderer({
}); name: component.name
});
const { name } = component; const { name } = component;
// create $$render function
// create $$render function renderer.render(trim(component.fragment.children), Object.assign({
renderer.render( locate: component.locate
trim(component.fragment.children), }, options));
Object.assign( // TODO put this inside the Renderer class
{ const literal = renderer.pop();
locate: component.locate // TODO concatenate CSS maps
}, const css = options.customElement
options ? { code: null, map: null }
) : component.stylesheet.render(options.filename);
); const uses_rest = component.var_lookup.has('$$restProps');
const props = component.vars.filter((variable) => !variable.module && variable.export_name);
// TODO put this inside the Renderer class const rest = uses_rest
const literal = renderer.pop(); ? b `let $$restProps = @compute_rest_props($$props, [${props
.map((prop) => `"${prop.export_name}"`)
// TODO concatenate CSS maps .join(',')}]);`
const css = options.customElement : null;
? { code: null, map: null } const uses_slots = component.var_lookup.has('$$slots');
: component.stylesheet.render(options.filename); const slots = uses_slots ? b `let $$slots = @compute_slots(#slots);` : null;
const reactive_stores = component.vars.filter((variable) => variable.name[0] === '$' && variable.name[1] !== '$');
const uses_rest = component.var_lookup.has('$$restProps'); const reactive_store_subscriptions = reactive_stores
const props = component.vars.filter((variable) => !variable.module && variable.export_name); .filter((store) => {
const rest = uses_rest const variable = component.var_lookup.get(store.name.slice(1));
? b`let $$restProps = @compute_rest_props($$props, [${props return !variable || variable.hoistable;
.map((prop) => `"${prop.export_name}"`) })
.join(',')}]);` .map(({ name }) => {
: null; const store_name = name.slice(1);
return b `
const uses_slots = component.var_lookup.has('$$slots'); ${component.compile_options.dev && b `@validate_store(${store_name}, '${store_name}');`}
const slots = uses_slots ? b`let $$slots = @compute_slots(#slots);` : null;
const reactive_stores = component.vars.filter(
(variable) => variable.name[0] === '$' && variable.name[1] !== '$'
);
const reactive_store_subscriptions = reactive_stores
.filter((store) => {
const variable = component.var_lookup.get(store.name.slice(1));
return !variable || variable.hoistable;
})
.map(({ name }) => {
const store_name = name.slice(1);
return b`
${component.compile_options.dev && b`@validate_store(${store_name}, '${store_name}');`}
${`$$unsubscribe_${store_name}`} = @subscribe(${store_name}, #value => ${name} = #value) ${`$$unsubscribe_${store_name}`} = @subscribe(${store_name}, #value => ${name} = #value)
`; `;
}); });
const reactive_store_unsubscriptions = reactive_stores.map( const reactive_store_unsubscriptions = reactive_stores.map(({ name }) => b `${`$$unsubscribe_${name.slice(1)}`}()`);
({ name }) => b`${`$$unsubscribe_${name.slice(1)}`}()` const reactive_store_declarations = reactive_stores.map(({ name }) => {
); const store_name = name.slice(1);
const store = component.var_lookup.get(store_name);
const reactive_store_declarations = reactive_stores.map(({ name }) => { if (store && store.reassigned) {
const store_name = name.slice(1); const unsubscribe = `$$unsubscribe_${store_name}`;
const store = component.var_lookup.get(store_name); const subscribe = `$$subscribe_${store_name}`;
return b `let ${name}, ${unsubscribe} = @noop, ${subscribe} = () => (${unsubscribe}(), ${unsubscribe} = @subscribe(${store_name}, $$value => ${name} = $$value), ${store_name})`;
if (store && store.reassigned) { }
const unsubscribe = `$$unsubscribe_${store_name}`; return b `let ${name}, ${`$$unsubscribe_${store_name}`};`;
const subscribe = `$$subscribe_${store_name}`; });
// instrument get/set store value
return b`let ${name}, ${unsubscribe} = @noop, ${subscribe} = () => (${unsubscribe}(), ${unsubscribe} = @subscribe(${store_name}, $$value => ${name} = $$value), ${store_name})`; if (component.ast.instance) {
} let scope = component.instance_scope;
return b`let ${name}, ${`$$unsubscribe_${store_name}`};`; const map = component.instance_scope_map;
}); walk(component.ast.instance.content, {
enter(node) {
// instrument get/set store value if (map.has(node)) {
if (component.ast.instance) { scope = map.get(node);
let scope = component.instance_scope; }
const map = component.instance_scope_map; },
leave(node) {
walk(component.ast.instance.content, { if (map.has(node)) {
enter(node: Node) { scope = scope.parent;
if (map.has(node)) { }
scope = map.get(node); if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
} const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument;
}, const names = new Set(extract_names(/** @type {import('estree').Node} */ (assignee)));
leave(node: Node) { const to_invalidate = new Set();
if (map.has(node)) { for (const name of names) {
scope = scope.parent; const variable = component.var_lookup.get(name);
} if (variable &&
!variable.hoistable &&
if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') { !variable.global &&
const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument; !variable.module &&
const names = new Set(extract_names(assignee as Node)); (variable.subscribable || variable.name[0] === '$')) {
const to_invalidate = new Set<string>(); to_invalidate.add(variable.name);
}
for (const name of names) { }
const variable = component.var_lookup.get(name); if (to_invalidate.size) {
if ( this.replace(invalidate(/** @type {any} */ ({ component }), scope, node, to_invalidate, true));
variable && }
!variable.hoistable && }
!variable.global && }
!variable.module && });
(variable.subscribable || variable.name[0] === '$') }
) { component.rewrite_props(({ name, reassigned }) => {
to_invalidate.add(variable.name); const value = `$${name}`;
} let insert = reassigned
} ? b `${`$$subscribe_${name}`}()`
: b `${`$$unsubscribe_${name}`} = @subscribe(${name}, #value => $${value} = #value)`;
if (to_invalidate.size) { if (component.compile_options.dev) {
this.replace(invalidate({ component } as any, scope, node, to_invalidate, true)); insert = b `@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 = instance_javascript
component.rewrite_props(({ name, reassigned }) => { ? component.vars
const value = `$${name}`; .filter((variable) => !variable.module && variable.export_name)
.map((prop) => {
let insert = reassigned return b `if ($$props.${prop.export_name} === void 0 && $$bindings.${prop.export_name} && ${prop.name} !== void 0) $$bindings.${prop.export_name}(${prop.name});`;
? b`${`$$subscribe_${name}`}()` })
: b`${`$$unsubscribe_${name}`} = @subscribe(${name}, #value => $${value} = #value)`; : [];
const injected = Array.from(component.injected_reactive_declaration_vars).filter((name) => {
if (component.compile_options.dev) { const variable = component.var_lookup.get(name);
insert = b`@validate_store(${name}, '${name}'); ${insert}`; return variable.injected;
} });
const reactive_declarations = component.reactive_declarations.map((d) => {
return insert; const body = ( /** @type {import('estree').LabeledStatement} */(d.node)).body;
}); let statement = b `${body}`;
if (!d.declaration) {
const instance_javascript = component.extract_javascript(component.ast.instance); // TODO do not add label if it's not referenced
statement = b `$: { ${statement} }`;
// TODO only do this for props with a default value }
const parent_bindings = instance_javascript return statement;
? component.vars });
.filter((variable) => !variable.module && variable.export_name) const main = renderer.has_bindings
.map((prop) => { ? b `
return b`if ($$props.${prop.export_name} === void 0 && $$bindings.${prop.export_name} && ${prop.name} !== void 0) $$bindings.${prop.export_name}(${prop.name});`;
})
: [];
const injected = Array.from(component.injected_reactive_declaration_vars).filter((name) => {
const variable = component.var_lookup.get(name);
return variable.injected;
});
const reactive_declarations = component.reactive_declarations.map((d) => {
const body: Statement = (d.node as LabeledStatement).body;
let statement = b`${body}`;
if (!d.declaration) {
// TODO do not add label if it's not referenced
statement = b`$: { ${statement} }`;
}
return statement;
});
const main = renderer.has_bindings
? b`
let $$settled; let $$settled;
let $$rendered; let $$rendered;
@ -186,37 +143,32 @@ export default function ssr(
return $$rendered; return $$rendered;
` `
: b` : b `
${reactive_declarations} ${reactive_declarations}
${reactive_store_unsubscriptions} ${reactive_store_unsubscriptions}
return ${literal};`; return ${literal};`;
const blocks = [
const blocks = [ ...injected.map((name) => b `let ${name};`),
...injected.map((name) => b`let ${name};`), rest,
rest, slots,
slots, ...reactive_store_declarations,
...reactive_store_declarations, ...reactive_store_subscriptions,
...reactive_store_subscriptions, instance_javascript,
instance_javascript, ...parent_bindings,
...parent_bindings, css.code && b `$$result.css.add(#css);`,
css.code && b`$$result.css.add(#css);`, main
main ].filter(Boolean);
].filter(Boolean); const css_sourcemap_enabled = check_enable_sourcemap(options.enableSourcemap, 'css');
const js = b `
const css_sourcemap_enabled = check_enable_sourcemap(options.enableSourcemap, 'css'); ${css.code
? b `
const js = b`
${
css.code
? b`
const #css = { const #css = {
code: "${css.code}", code: "${css.code}",
map: ${css_sourcemap_enabled && css.map ? string_literal(css.map.toString()) : 'null'} map: ${css_sourcemap_enabled && css.map ? string_literal(css.map.toString()) : 'null'}
};` };`
: null : null}
}
${component.extract_javascript(component.ast.module)} ${component.extract_javascript(component.ast.module)}
@ -226,28 +178,32 @@ export default function ssr(
${blocks} ${blocks}
}); });
`; `;
return { js, css };
return { js, css };
} }
function trim(nodes: TemplateNode[]) { /** @param {TemplateNode[]} nodes */
let start = 0; function trim(nodes) {
for (; start < nodes.length; start += 1) { let start = 0;
const node = nodes[start] as Text; for (; start < nodes.length; start += 1) {
if (node.type !== 'Text') break; const node = /** @type {import('../nodes/Text.js').default} */ (nodes[start]);
if (node.type !== 'Text')
break;
node.data = node.data.replace(/^\s+/, '');
if (node.data)
break;
}
let end = nodes.length;
for (; end > start; end -= 1) {
const node = /** @type {import('../nodes/Text.js').default} */ (nodes[end - 1]);
if (node.type !== 'Text')
break;
node.data = node.data.trimRight();
if (node.data)
break;
}
return nodes.slice(start, end);
}
node.data = node.data.replace(/^\s+/, '');
if (node.data) break;
}
let end = nodes.length;
for (; end > start; end -= 1) {
const node = nodes[end - 1] as Text;
if (node.type !== 'Text') break;
node.data = node.data.trimRight();
if (node.data) break;
}
return nodes.slice(start, end);
}

@ -0,0 +1,7 @@
import { CompileOptions } from '../../interfaces.js';
export interface RenderOptions extends CompileOptions {
locate: (c: number) => { line: number; column: number };
head_id?: string;
has_added_svelte_hash?: boolean;
}
Loading…
Cancel
Save