Skip to content

feat: functional template generation #15538

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
de8a38b
feat: templateless template generation
paoloricciuti Mar 18, 2025
575908a
fix: auto import fumble
paoloricciuti Mar 18, 2025
8a737f1
feat: add option `preventTemplateCloning` and functions transformation
paoloricciuti Mar 18, 2025
7c7a857
feat: make all tests pass with functional templates
paoloricciuti Mar 20, 2025
8424395
chore: remove `prevent-template-cloning` test
paoloricciuti Mar 20, 2025
c75c429
chore: run test suite on both `functional` and `string` templating
paoloricciuti Mar 20, 2025
64219ed
chore: change options from `boolean` to `list` (templatingMode: "func…
paoloricciuti Mar 20, 2025
95ce311
chore: revert unneeded change to `package.json`
paoloricciuti Mar 20, 2025
59902cc
chore: simplify `process_children`
paoloricciuti Mar 20, 2025
132ea2b
Merge remote-tracking branch 'origin/main' into templateless-template…
paoloricciuti Mar 20, 2025
403b17e
chore: update snapshots
paoloricciuti Mar 20, 2025
ba237c7
chore: don't write different `rendered`
paoloricciuti Mar 20, 2025
4daa63a
fix: snapshot test runner
paoloricciuti Mar 20, 2025
72f93e3
chore: sprinkle comments here and there
paoloricciuti Mar 21, 2025
ad56847
fix: silly goose am i 🪿
paoloricciuti Mar 21, 2025
d8afd8e
fix (this broke the sandbox)
Rich-Harris Mar 21, 2025
a48df4a
Merge remote-tracking branch 'origin/main' into templateless-template…
paoloricciuti Mar 24, 2025
be39867
chore: alterative functional templating syntax (#15599)
paoloricciuti Apr 1, 2025
eaf2aaf
Merge branch 'main' into templateless-template-generation
Rich-Harris Apr 17, 2025
97817a9
fix snapshot tests
Rich-Harris Apr 17, 2025
15c6306
Merge remote-tracking branch 'origin/main' into templateless-template…
paoloricciuti Apr 22, 2025
084f7f9
chore: update purity snapshot
paoloricciuti Apr 22, 2025
084e313
chore: update purity snapshot, this time in the proper way
paoloricciuti Apr 22, 2025
d294a66
chore: add `each-index-non-null` snapshot
paoloricciuti Apr 22, 2025
2d0db55
Merge remote-tracking branch 'origin/main' into templateless-template…
paoloricciuti Apr 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/smart-boats-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: functional template generation
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export function client_component(analysis, options) {
in_constructor: false,
instance_level_snippets: [],
module_level_snippets: [],
is_functional_template_mode: options.templatingMode === 'functional',

// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* @import { ComponentContext, TemplateOperations, ComponentClientTransformState } from "../types.js"
* @import { Identifier, Expression } from "estree"
* @import { AST, Namespace } from '#compiler'
* @import { SourceLocation } from '#shared'
*/
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 {Namespace} namespace
* @param {ComponentClientTransformState} state
* @returns
*/
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'
).concat(state.is_functional_template_mode ? '_fn' : '');
}

/**
* @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) {
/**
* @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 = [
state.is_functional_template_mode
? template_to_functions(state.template)
: b.template([b.quasi(template_to_string(state.template), true)], [])
];

if (flags) {
args.push(b.literal(flags));
}

add_template(template_name, args);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/**
* @import { TemplateOperations } from "../types.js"
* @import { Namespace } from "#compiler"
* @import { CallExpression, Statement, ObjectExpression, Identifier, ArrayExpression, Property, Expression, Literal } from "estree"
*/
import { NAMESPACE_SVG, NAMESPACE_MATHML } from '../../../../../constants.js';
import * as b from '../../../../utils/builders.js';
import { regex_is_valid_identifier } from '../../../patterns.js';
import fix_attribute_casing from './fix-attribute-casing.js';

/**
* @param {TemplateOperations} items
*/
export function template_to_functions(items) {
let elements = b.array([]);

/**
* @type {Array<Element>}
*/
let elements_stack = [];

/**
* @type {Element | undefined}
*/
let last_current_element;

// if the first item is a comment we need to add another comment for effect.start
if (items[0].kind === 'create_anchor') {
items.unshift({ kind: 'create_anchor' });
}

for (let instruction of items) {
// on push element we add the element to the stack, from this moment on every insert will
// happen on the last element in the stack
if (instruction.kind === 'push_element' && last_current_element) {
elements_stack.push(last_current_element);
continue;
}
// we closed one element, we remove it from the stack and eventually revert back
// the namespace to the previous one
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 === 'create_element'
? []
: [instruction.kind === 'set_prop' ? last_current_element : elements_stack.at(-1)]),
...(instruction.args ?? [])
]
);

// with set_prop we don't need to do anything else, in all other cases we also need to
// append the element/node/anchor to the current active element or push it in the elements array
if (instruction.kind !== 'set_prop') {
if (elements_stack.length >= 1 && value !== undefined) {
map.insert(/** @type {Element} */ (elements_stack.at(-1)), value);
} else if (value !== undefined) {
elements.elements.push(value);
}
// keep track of the last created element (it will be pushed to the stack after the props are set)
if (instruction.kind === 'create_element') {
last_current_element = /** @type {Element} */ (value);
}
}
}

return elements;
}

/**
* @typedef {ObjectExpression} Element
*/

/**
* @typedef {void | null | ArrayExpression} Anchor
*/

/**
* @typedef {void | Literal} Text
*/

/**
* @typedef { Element | Anchor| Text } Node
*/

/**
* @param {string} element
* @returns {Element}
*/
function create_element(element) {
return b.object([b.prop('init', b.id('e'), b.literal(element))]);
}

/**
*
* @param {Element} element
* @param {string} name
* @param {Expression} init
* @returns {Property}
*/
function get_or_create_prop(element, name, init) {
let prop = element.properties.find(
(prop) => prop.type === 'Property' && /** @type {Identifier} */ (prop.key).name === name
);
if (!prop) {
prop = b.prop('init', b.id(name), init);
element.properties.push(prop);
}
return /** @type {Property} */ (prop);
}

/**
* @param {Element} element
* @param {string} data
* @returns {Anchor}
*/
function create_anchor(element, data = '') {
if (!element) return data ? b.array([b.literal(data)]) : null;
const c = get_or_create_prop(element, 'c', b.array([]));
/** @type {ArrayExpression} */ (c.value).elements.push(data ? b.array([b.literal(data)]) : null);
}

/**
* @param {Element} element
* @param {string} value
* @returns {Text}
*/
function create_text(element, value) {
if (!element) return b.literal(value);
const c = get_or_create_prop(element, 'c', b.array([]));
/** @type {ArrayExpression} */ (c.value).elements.push(b.literal(value));
}

/**
*
* @param {Element} element
* @param {string} prop
* @param {string} value
*/
function set_prop(element, prop, value) {
const p = get_or_create_prop(element, 'p', b.object([]));

if (prop === 'is') {
element.properties.push(b.prop('init', b.id(prop), b.literal(value)));
return;
}

const prop_correct_case = fix_attribute_casing(prop);

const is_valid_id = regex_is_valid_identifier.test(prop_correct_case);

/** @type {ObjectExpression} */ (p.value).properties.push(
b.prop(
'init',
(is_valid_id ? b.id : b.literal)(prop_correct_case),
b.literal(value),
!is_valid_id
)
);
}

/**
*
* @param {Element} element
* @param {Element} child
*/
function insert(element, child) {
const c = get_or_create_prop(element, 'c', b.array([]));
/** @type {ArrayExpression} */ (c.value).elements.push(child);
}

let map = {
create_element,
create_text,
create_anchor,
set_prop,
insert
};
Loading