mirror of https://github.com/sveltejs/svelte
parent
575908afc1
commit
8a737f1070
@ -1,12 +1,98 @@
|
||||
/**
|
||||
* @import { TemplateOperations } from "../types.js"
|
||||
* @import { ComponentContext, TemplateOperations, ComponentClientTransformState } from "../types.js"
|
||||
* @import { Identifier, Expression } from "estree"
|
||||
* @import { AST, Namespace } from '#compiler'
|
||||
* @import { SourceLocation } from '#shared'
|
||||
*/
|
||||
import { TEMPLATE_FRAGMENT } from '../../../../../constants.js';
|
||||
import { dev } from '../../../../state.js';
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { template_to_functions } from './to-functions.js';
|
||||
import { template_to_string } from './to-string.js';
|
||||
|
||||
/**
|
||||
* @param {TemplateOperations} items
|
||||
*
|
||||
* @param {Namespace} namespace
|
||||
* @param {ComponentClientTransformState} state
|
||||
* @returns
|
||||
*/
|
||||
export function transform_template(items) {
|
||||
// here we will check if we need to use `$.template` or create a series of `document.createElement` calls
|
||||
return template_to_string(items);
|
||||
function get_template_function(namespace, state) {
|
||||
const contains_script_tag = state.metadata.context.template_contains_script_tag;
|
||||
return namespace === 'svg'
|
||||
? contains_script_tag
|
||||
? '$.svg_template_with_script'
|
||||
: '$.ns_template'
|
||||
: namespace === 'mathml'
|
||||
? '$.mathml_template'
|
||||
: contains_script_tag
|
||||
? '$.template_with_script'
|
||||
: '$.template';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceLocation[]} locations
|
||||
*/
|
||||
function build_locations(locations) {
|
||||
return b.array(
|
||||
locations.map((loc) => {
|
||||
const expression = b.array([b.literal(loc[0]), b.literal(loc[1])]);
|
||||
|
||||
if (loc.length === 3) {
|
||||
expression.elements.push(build_locations(loc[2]));
|
||||
}
|
||||
|
||||
return expression;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ComponentClientTransformState} state
|
||||
* @param {ComponentContext} context
|
||||
* @param {Namespace} namespace
|
||||
* @param {Identifier} template_name
|
||||
* @param {number} [flags]
|
||||
*/
|
||||
export function transform_template(state, context, namespace, template_name, flags) {
|
||||
if (context.state.prevent_template_cloning) {
|
||||
context.state.hoisted.push(
|
||||
b.var(
|
||||
template_name,
|
||||
template_to_functions(
|
||||
state.template,
|
||||
namespace,
|
||||
flags != null && (flags & TEMPLATE_FRAGMENT) !== 0
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Identifier} template_name
|
||||
* @param {Expression[]} args
|
||||
*/
|
||||
const add_template = (template_name, args) => {
|
||||
let call = b.call(get_template_function(namespace, state), ...args);
|
||||
if (dev) {
|
||||
call = b.call(
|
||||
'$.add_locations',
|
||||
call,
|
||||
b.member(b.id(context.state.analysis.name), '$.FILENAME', true),
|
||||
build_locations(state.locations)
|
||||
);
|
||||
}
|
||||
|
||||
context.state.hoisted.push(b.var(template_name, call));
|
||||
};
|
||||
|
||||
/** @type {Expression[]} */
|
||||
const args = [b.template([b.quasi(template_to_string(state.template), true)], [])];
|
||||
|
||||
if (flags) {
|
||||
args.push(b.literal(flags));
|
||||
}
|
||||
|
||||
add_template(template_name, args);
|
||||
}
|
||||
|
@ -0,0 +1,188 @@
|
||||
/**
|
||||
* @import { TemplateOperations } from "../types.js"
|
||||
* @import { Namespace } from "#compiler"
|
||||
* @import { Statement } from "estree"
|
||||
*/
|
||||
import { NAMESPACE_SVG } from 'svelte/internal/client';
|
||||
import * as b from '../../../../utils/builders.js';
|
||||
import { NAMESPACE_MATHML } from '../../../../../constants.js';
|
||||
|
||||
class Scope {
|
||||
declared = new Map();
|
||||
|
||||
/**
|
||||
* @param {string} _name
|
||||
*/
|
||||
generate(_name) {
|
||||
let name = _name.replace(/[^a-zA-Z0-9_$]/g, '_').replace(/^[0-9]/, '_');
|
||||
if (!this.declared.has(name)) {
|
||||
this.declared.set(name, 1);
|
||||
return name;
|
||||
}
|
||||
let count = this.declared.get(name);
|
||||
this.declared.set(name, count + 1);
|
||||
return `${name}_${count}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TemplateOperations} items
|
||||
* @param {Namespace} namespace
|
||||
* @param {boolean} use_fragment
|
||||
*/
|
||||
export function template_to_functions(items, namespace, use_fragment = false) {
|
||||
let elements = [];
|
||||
|
||||
let body = [];
|
||||
|
||||
let scope = new Scope();
|
||||
|
||||
/**
|
||||
* @type {Array<Element>}
|
||||
*/
|
||||
let elements_stack = [];
|
||||
|
||||
/**
|
||||
* @type {Element | undefined}
|
||||
*/
|
||||
let last_current_element;
|
||||
|
||||
for (let instruction of items) {
|
||||
if (instruction.kind === 'push_element' && last_current_element) {
|
||||
elements_stack.push(last_current_element);
|
||||
continue;
|
||||
}
|
||||
if (instruction.kind === 'pop_element') {
|
||||
elements_stack.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
// @ts-expect-error we can't be here if `swap_current_element` but TS doesn't know that
|
||||
const value = map[instruction.kind](
|
||||
...[
|
||||
...(instruction.kind === 'set_prop' ? [last_current_element] : [scope]),
|
||||
...(instruction.kind === 'create_element' ? [namespace] : []),
|
||||
...(instruction.args ?? [])
|
||||
]
|
||||
);
|
||||
|
||||
if (value) {
|
||||
body.push(value.call);
|
||||
}
|
||||
|
||||
if (instruction.kind !== 'set_prop') {
|
||||
if (elements_stack.length >= 1 && value) {
|
||||
const { call } = map.insert(/** @type {Element} */ (elements_stack.at(-1)), value);
|
||||
body.push(call);
|
||||
} else if (value) {
|
||||
elements.push(b.id(value.name));
|
||||
}
|
||||
if (instruction.kind === 'create_element') {
|
||||
last_current_element = /** @type {Element} */ (value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (elements.length > 1 || use_fragment) {
|
||||
const fragment = scope.generate('fragment');
|
||||
body.push(b.var(fragment, b.call('document.createDocumentFragment')));
|
||||
body.push(b.call(fragment + '.append', ...elements));
|
||||
body.push(b.return(b.id(fragment)));
|
||||
} else {
|
||||
body.push(b.return(elements[0]));
|
||||
}
|
||||
|
||||
return b.arrow([], b.block(body));
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {{ call: Statement, name: string }} Element
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{ call: Statement, name: string }} Anchor
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{ call: Statement, name: string }} Text
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef { Element | Anchor| Text } Node
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Scope} scope
|
||||
* @param {Namespace} namespace
|
||||
* @param {string} element
|
||||
* @returns {Element}
|
||||
*/
|
||||
function create_element(scope, namespace, element) {
|
||||
const name = scope.generate(element);
|
||||
let fn = namespace !== 'html' ? 'document.createElementNS' : 'document.createElement';
|
||||
let args = [b.literal(element)];
|
||||
if (namespace !== 'html') {
|
||||
args.unshift(namespace === 'svg' ? b.literal(NAMESPACE_SVG) : b.literal(NAMESPACE_MATHML));
|
||||
}
|
||||
return {
|
||||
call: b.var(name, b.call(fn, ...args)),
|
||||
name
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Scope} scope
|
||||
* @param {string} data
|
||||
* @returns {Anchor}
|
||||
*/
|
||||
function create_anchor(scope, data = '') {
|
||||
const name = scope.generate('comment');
|
||||
return {
|
||||
call: b.var(name, b.call('document.createComment', b.literal(data))),
|
||||
name
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Scope} scope
|
||||
* @param {string} value
|
||||
* @returns {Text}
|
||||
*/
|
||||
function create_text(scope, value) {
|
||||
const name = scope.generate('text');
|
||||
return {
|
||||
call: b.var(name, b.call('document.createTextNode', b.literal(value))),
|
||||
name
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Element} el
|
||||
* @param {string} prop
|
||||
* @param {string} value
|
||||
*/
|
||||
function set_prop(el, prop, value) {
|
||||
return {
|
||||
call: b.call(el.name + '.setAttribute', b.literal(prop), b.literal(value))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Element} el
|
||||
* @param {Node} child
|
||||
* @param {Node} [anchor]
|
||||
*/
|
||||
function insert(el, child, anchor) {
|
||||
return {
|
||||
call: b.call(el.name + '.insertBefore', b.id(child.name), b.id(anchor?.name ?? 'undefined'))
|
||||
};
|
||||
}
|
||||
|
||||
let map = {
|
||||
create_element,
|
||||
create_text,
|
||||
create_anchor,
|
||||
set_prop,
|
||||
insert
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
compileOptions: {
|
||||
preventTemplateCloning: true
|
||||
}
|
||||
});
|
@ -0,0 +1,49 @@
|
||||
import 'svelte/internal/disclose-version';
|
||||
import * as $ from 'svelte/internal/client';
|
||||
|
||||
function increment(_, counter) {
|
||||
counter.count += 1;
|
||||
}
|
||||
|
||||
var root = () => {
|
||||
var button = document.createElement('button');
|
||||
var text = document.createTextNode(' ');
|
||||
|
||||
button.insertBefore(text, undefined)
|
||||
|
||||
var text_1 = document.createTextNode(' ');
|
||||
var comment = document.createComment('');
|
||||
var text_2 = document.createTextNode(' ');
|
||||
var fragment = document.createDocumentFragment();
|
||||
|
||||
fragment.append(button, text_1, comment, text_2)
|
||||
return fragment;
|
||||
};
|
||||
|
||||
export default function Prevent_template_cloning($$anchor) {
|
||||
let counter = $.proxy({ count: 0 });
|
||||
const promise = $.derived(() => Promise.resolve(counter));
|
||||
var fragment = root();
|
||||
var button = $.first_child(fragment);
|
||||
|
||||
button.__click = [increment, counter];
|
||||
|
||||
var text = $.child(button);
|
||||
|
||||
$.reset(button);
|
||||
|
||||
var node = $.sibling(button, 2);
|
||||
|
||||
$.await(node, () => $.get(promise), null, ($$anchor, counter) => {});
|
||||
|
||||
var text_1 = $.sibling(node);
|
||||
|
||||
$.template_effect(() => {
|
||||
$.set_text(text, `clicks: ${counter.count ?? ''}`);
|
||||
$.set_text(text_1, ` ${counter.count ?? ''}`);
|
||||
});
|
||||
|
||||
$.append($$anchor, fragment);
|
||||
}
|
||||
|
||||
$.delegate(['click']);
|
@ -0,0 +1,14 @@
|
||||
import * as $ from 'svelte/internal/server';
|
||||
|
||||
export default function Prevent_template_cloning($$payload) {
|
||||
let counter = { count: 0 };
|
||||
const promise = Promise.resolve(counter);
|
||||
|
||||
function increment() {
|
||||
counter.count += 1;
|
||||
}
|
||||
|
||||
$$payload.out += `<button>clicks: ${$.escape(counter.count)}</button> <!---->`;
|
||||
$.await(promise, () => {}, (counter) => {}, () => {});
|
||||
$$payload.out += `<!----> ${$.escape(counter.count)}`;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
<script>
|
||||
let counter = $state({ count: 0 });
|
||||
const promise = $derived(Promise.resolve(counter))
|
||||
|
||||
function increment() {
|
||||
counter.count += 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={increment}>
|
||||
clicks: {counter.count}
|
||||
</button>
|
||||
|
||||
{#await promise then counter}{/await}
|
||||
|
||||
{counter.count}
|
Loading…
Reference in new issue