|
|
|
|
@ -12,122 +12,136 @@ import check_enable_sourcemap from '../utils/check_enable_sourcemap.js';
|
|
|
|
|
* @returns {{ js: import("C:/repos/svelte/svelte/node_modules/.pnpm/@types+estree@1.0.0/node_modules/@types/estree/index").Node[]; css: CssResult; }}
|
|
|
|
|
*/
|
|
|
|
|
export default function ssr(component, options) {
|
|
|
|
|
const renderer = new Renderer({
|
|
|
|
|
name: component.name
|
|
|
|
|
});
|
|
|
|
|
const { name } = component;
|
|
|
|
|
// create $$render function
|
|
|
|
|
renderer.render(trim(component.fragment.children), Object.assign({
|
|
|
|
|
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 }
|
|
|
|
|
: component.stylesheet.render(options.filename);
|
|
|
|
|
const uses_rest = component.var_lookup.has('$$restProps');
|
|
|
|
|
const props = component.vars.filter((variable) => !variable.module && variable.export_name);
|
|
|
|
|
const rest = uses_rest
|
|
|
|
|
? b `let $$restProps = @compute_rest_props($$props, [${props
|
|
|
|
|
.map((prop) => `"${prop.export_name}"`)
|
|
|
|
|
.join(',')}]);`
|
|
|
|
|
: null;
|
|
|
|
|
const uses_slots = component.var_lookup.has('$$slots');
|
|
|
|
|
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}');`}
|
|
|
|
|
const renderer = new Renderer({
|
|
|
|
|
name: component.name
|
|
|
|
|
});
|
|
|
|
|
const { name } = component;
|
|
|
|
|
// create $$render function
|
|
|
|
|
renderer.render(
|
|
|
|
|
trim(component.fragment.children),
|
|
|
|
|
Object.assign(
|
|
|
|
|
{
|
|
|
|
|
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 }
|
|
|
|
|
: component.stylesheet.render(options.filename);
|
|
|
|
|
const uses_rest = component.var_lookup.has('$$restProps');
|
|
|
|
|
const props = component.vars.filter((variable) => !variable.module && variable.export_name);
|
|
|
|
|
const rest = uses_rest
|
|
|
|
|
? b`let $$restProps = @compute_rest_props($$props, [${props
|
|
|
|
|
.map((prop) => `"${prop.export_name}"`)
|
|
|
|
|
.join(',')}]);`
|
|
|
|
|
: null;
|
|
|
|
|
const uses_slots = component.var_lookup.has('$$slots');
|
|
|
|
|
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)
|
|
|
|
|
`;
|
|
|
|
|
});
|
|
|
|
|
const reactive_store_unsubscriptions = reactive_stores.map(({ 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);
|
|
|
|
|
if (store && store.reassigned) {
|
|
|
|
|
const unsubscribe = `$$unsubscribe_${store_name}`;
|
|
|
|
|
const subscribe = `$$subscribe_${store_name}`;
|
|
|
|
|
return b `let ${name}, ${unsubscribe} = @noop, ${subscribe} = () => (${unsubscribe}(), ${unsubscribe} = @subscribe(${store_name}, $$value => ${name} = $$value), ${store_name})`;
|
|
|
|
|
}
|
|
|
|
|
return b `let ${name}, ${`$$unsubscribe_${store_name}`};`;
|
|
|
|
|
});
|
|
|
|
|
// instrument get/set store value
|
|
|
|
|
if (component.ast.instance) {
|
|
|
|
|
let scope = component.instance_scope;
|
|
|
|
|
const map = component.instance_scope_map;
|
|
|
|
|
walk(component.ast.instance.content, {
|
|
|
|
|
enter(node) {
|
|
|
|
|
if (map.has(node)) {
|
|
|
|
|
scope = map.get(node);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
leave(node) {
|
|
|
|
|
if (map.has(node)) {
|
|
|
|
|
scope = scope.parent;
|
|
|
|
|
}
|
|
|
|
|
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)));
|
|
|
|
|
const to_invalidate = new Set();
|
|
|
|
|
for (const name of names) {
|
|
|
|
|
const variable = component.var_lookup.get(name);
|
|
|
|
|
if (variable &&
|
|
|
|
|
!variable.hoistable &&
|
|
|
|
|
!variable.global &&
|
|
|
|
|
!variable.module &&
|
|
|
|
|
(variable.subscribable || variable.name[0] === '$')) {
|
|
|
|
|
to_invalidate.add(variable.name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (to_invalidate.size) {
|
|
|
|
|
this.replace(invalidate(/** @type {any} */ ({ component }), scope, node, to_invalidate, true));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
component.rewrite_props(({ name, reassigned }) => {
|
|
|
|
|
const value = `$${name}`;
|
|
|
|
|
let insert = reassigned
|
|
|
|
|
? b `${`$$subscribe_${name}`}()`
|
|
|
|
|
: b `${`$$unsubscribe_${name}`} = @subscribe(${name}, #value => $${value} = #value)`;
|
|
|
|
|
if (component.compile_options.dev) {
|
|
|
|
|
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.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 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 = ( /** @type {import('estree').LabeledStatement} */(d.node)).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 `
|
|
|
|
|
});
|
|
|
|
|
const reactive_store_unsubscriptions = reactive_stores.map(
|
|
|
|
|
({ 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);
|
|
|
|
|
if (store && store.reassigned) {
|
|
|
|
|
const unsubscribe = `$$unsubscribe_${store_name}`;
|
|
|
|
|
const subscribe = `$$subscribe_${store_name}`;
|
|
|
|
|
return b`let ${name}, ${unsubscribe} = @noop, ${subscribe} = () => (${unsubscribe}(), ${unsubscribe} = @subscribe(${store_name}, $$value => ${name} = $$value), ${store_name})`;
|
|
|
|
|
}
|
|
|
|
|
return b`let ${name}, ${`$$unsubscribe_${store_name}`};`;
|
|
|
|
|
});
|
|
|
|
|
// instrument get/set store value
|
|
|
|
|
if (component.ast.instance) {
|
|
|
|
|
let scope = component.instance_scope;
|
|
|
|
|
const map = component.instance_scope_map;
|
|
|
|
|
walk(component.ast.instance.content, {
|
|
|
|
|
enter(node) {
|
|
|
|
|
if (map.has(node)) {
|
|
|
|
|
scope = map.get(node);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
leave(node) {
|
|
|
|
|
if (map.has(node)) {
|
|
|
|
|
scope = scope.parent;
|
|
|
|
|
}
|
|
|
|
|
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)));
|
|
|
|
|
const to_invalidate = new Set();
|
|
|
|
|
for (const name of names) {
|
|
|
|
|
const variable = component.var_lookup.get(name);
|
|
|
|
|
if (
|
|
|
|
|
variable &&
|
|
|
|
|
!variable.hoistable &&
|
|
|
|
|
!variable.global &&
|
|
|
|
|
!variable.module &&
|
|
|
|
|
(variable.subscribable || variable.name[0] === '$')
|
|
|
|
|
) {
|
|
|
|
|
to_invalidate.add(variable.name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (to_invalidate.size) {
|
|
|
|
|
this.replace(
|
|
|
|
|
invalidate(/** @type {any} */ ({ component }), scope, node, to_invalidate, true)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
component.rewrite_props(({ name, reassigned }) => {
|
|
|
|
|
const value = `$${name}`;
|
|
|
|
|
let insert = reassigned
|
|
|
|
|
? b`${`$$subscribe_${name}`}()`
|
|
|
|
|
: b`${`$$unsubscribe_${name}`} = @subscribe(${name}, #value => $${value} = #value)`;
|
|
|
|
|
if (component.compile_options.dev) {
|
|
|
|
|
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.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 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 = /** @type {import('estree').LabeledStatement} */ (d.node).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 $$rendered;
|
|
|
|
|
|
|
|
|
|
@ -143,32 +157,34 @@ export default function ssr(component, options) {
|
|
|
|
|
|
|
|
|
|
return $$rendered;
|
|
|
|
|
`
|
|
|
|
|
: b `
|
|
|
|
|
: b`
|
|
|
|
|
${reactive_declarations}
|
|
|
|
|
|
|
|
|
|
${reactive_store_unsubscriptions}
|
|
|
|
|
|
|
|
|
|
return ${literal};`;
|
|
|
|
|
const blocks = [
|
|
|
|
|
...injected.map((name) => b `let ${name};`),
|
|
|
|
|
rest,
|
|
|
|
|
slots,
|
|
|
|
|
...reactive_store_declarations,
|
|
|
|
|
...reactive_store_subscriptions,
|
|
|
|
|
instance_javascript,
|
|
|
|
|
...parent_bindings,
|
|
|
|
|
css.code && b `$$result.css.add(#css);`,
|
|
|
|
|
main
|
|
|
|
|
].filter(Boolean);
|
|
|
|
|
const css_sourcemap_enabled = check_enable_sourcemap(options.enableSourcemap, 'css');
|
|
|
|
|
const js = b `
|
|
|
|
|
${css.code
|
|
|
|
|
? b `
|
|
|
|
|
const blocks = [
|
|
|
|
|
...injected.map((name) => b`let ${name};`),
|
|
|
|
|
rest,
|
|
|
|
|
slots,
|
|
|
|
|
...reactive_store_declarations,
|
|
|
|
|
...reactive_store_subscriptions,
|
|
|
|
|
instance_javascript,
|
|
|
|
|
...parent_bindings,
|
|
|
|
|
css.code && b`$$result.css.add(#css);`,
|
|
|
|
|
main
|
|
|
|
|
].filter(Boolean);
|
|
|
|
|
const css_sourcemap_enabled = check_enable_sourcemap(options.enableSourcemap, 'css');
|
|
|
|
|
const js = b`
|
|
|
|
|
${
|
|
|
|
|
css.code
|
|
|
|
|
? b`
|
|
|
|
|
const #css = {
|
|
|
|
|
code: "${css.code}",
|
|
|
|
|
map: ${css_sourcemap_enabled && css.map ? string_literal(css.map.toString()) : 'null'}
|
|
|
|
|
};`
|
|
|
|
|
: null}
|
|
|
|
|
: null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
${component.extract_javascript(component.ast.module)}
|
|
|
|
|
|
|
|
|
|
@ -178,32 +194,24 @@ export default function ssr(component, options) {
|
|
|
|
|
${blocks}
|
|
|
|
|
});
|
|
|
|
|
`;
|
|
|
|
|
return { js, css };
|
|
|
|
|
return { js, css };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param {TemplateNode[]} nodes */
|
|
|
|
|
function trim(nodes) {
|
|
|
|
|
let start = 0;
|
|
|
|
|
for (; start < nodes.length; start += 1) {
|
|
|
|
|
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);
|
|
|
|
|
let start = 0;
|
|
|
|
|
for (; start < nodes.length; start += 1) {
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|