object-blockers
Rich Harris 6 months ago
commit 88a6f47ad7

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: hoist snippets above const in same block

@ -1,5 +0,0 @@
---
'svelte': minor
---
feat: export `parseCss` from `svelte/compiler`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: better code generation for const tags with async dependencies

@ -1,5 +1,17 @@
# svelte
## 5.48.0
### Minor Changes
- feat: export `parseCss` from `svelte/compiler` ([#17496](https://github.com/sveltejs/svelte/pull/17496))
### Patch Changes
- fix: handle non-string values in `svelte:element` `this` attribute ([#17499](https://github.com/sveltejs/svelte/pull/17499))
- fix: faster deduplication of dependencies ([#17503](https://github.com/sveltejs/svelte/pull/17503))
## 5.47.1
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.47.1",
"version": "5.48.0",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -170,6 +170,7 @@ export function client_component(analysis, options) {
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
consts: /** @type {any} */ (null),
snippets: /** @type {any} */ (null),
let_directives: /** @type {any} */ (null),
update: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),

@ -49,6 +49,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly update: Statement[];
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
readonly after_update: Statement[];
/** Transformed `{#snippets }` declarations */
readonly snippets: Statement[];
/** Transformed `{@const }` declarations */
readonly consts: Statement[];
/** Transformed async `{@const }` declarations (if any) and those coming after them */

@ -1,4 +1,4 @@
/** @import { Pattern } from 'estree' */
/** @import { Expression, Identifier, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/** @import { ExpressionMetadata } from '../../../nodes.js' */
@ -88,8 +88,8 @@ export function ConstTag(node, context) {
/**
* @param {ComponentContext['state']} state
* @param {import('estree').Identifier} id
* @param {import('estree').Expression} expression
* @param {Identifier} id
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
* @param {import('#compiler').Binding[]} bindings
*/
@ -99,7 +99,9 @@ function add_const_declaration(state, id, expression, metadata, bindings) {
const after = dev ? [b.stmt(b.call('$.get', id))] : [];
const has_await = metadata.has_await;
const blockers = [...metadata.dependencies].map((dep) => dep.blocker).filter((b) => b !== null);
const blockers = [...metadata.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== state.async_consts?.id);
if (has_await || state.async_consts || blockers.length > 0) {
const run = (state.async_consts ??= {
@ -112,7 +114,9 @@ function add_const_declaration(state, id, expression, metadata, bindings) {
const assignment = b.assignment('=', id, expression);
const body = after.length === 0 ? assignment : b.block([b.stmt(assignment), ...after]);
if (blockers.length > 0) {
if (blockers.length === 1) {
run.thunks.push(b.thunk(/** @type {Expression} */ (blockers[0])));
} else if (blockers.length > 0) {
run.thunks.push(b.thunk(b.call('Promise.all', b.array(blockers))));
}

@ -1,13 +1,13 @@
/** @import { Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../../../constants.js';
import * as b from '#compiler/builders';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../../../constants.js';
import { clean_nodes, infer_namespace } from '../../utils.js';
import { transform_template } from '../transform-template/index.js';
import { Template } from '../transform-template/template.js';
import { process_children } from './shared/fragment.js';
import { build_render_statement, Memoizer } from './shared/utils.js';
import { Template } from '../transform-template/template.js';
/**
* @param {AST.Fragment} node
@ -60,6 +60,7 @@ export function Fragment(node, context) {
const state = {
...context.state,
init: [],
snippets: [],
consts: [],
let_directives: [],
update: [],
@ -150,7 +151,7 @@ export function Fragment(node, context) {
}
}
body.push(...state.let_directives, ...state.consts);
body.push(...state.snippets, ...state.let_directives, ...state.consts);
if (state.async_consts && state.async_consts.thunks.length > 0) {
body.push(b.var(state.async_consts.id, b.call('$.run', b.array(state.async_consts.thunks))));

@ -329,7 +329,7 @@ export function RegularElement(node, context) {
);
/** @type {typeof state} */
const child_state = { ...state, init: [], update: [], after_update: [] };
const child_state = { ...state, init: [], update: [], after_update: [], snippets: [] };
for (const node of hoisted) {
context.visit(node, child_state);
@ -441,6 +441,7 @@ export function RegularElement(node, context) {
// Wrap children in `{...}` to avoid declaration conflicts
context.state.init.push(
b.block([
...child_state.snippets,
...child_state.init,
...element_state.init,
child_state.update.length > 0 ? build_render_statement(child_state) : b.empty,

@ -89,6 +89,6 @@ export function SnippetBlock(node, context) {
context.state.instance_level_snippets.push(declaration);
}
} else {
context.state.init.push(declaration);
context.state.snippets.push(declaration);
}
}

@ -77,7 +77,7 @@ export function SvelteBoundary(node, context) {
/** @type {Statement[]} */
const statements = [];
context.visit(child, { ...context.state, init: statements });
context.visit(child, { ...context.state, snippets: statements });
const snippet = /** @type {VariableDeclaration} */ (statements[0]);

@ -117,10 +117,10 @@ export function SvelteElement(node, context) {
);
if (dev) {
statements.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag)));
if (node.fragment.nodes.length > 0) {
statements.push(b.stmt(b.call('$.validate_void_dynamic_element', get_tag)));
}
statements.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag)));
}
const location = dev && locator(node.start);

@ -333,7 +333,7 @@ export function build_component(node, component_name, loc, context) {
// can be used as props without creating conflicts
context.visit(child, {
...context.state,
init: snippet_declarations
snippets: snippet_declarations
});
push_prop(b.prop('init', child.expression, child.expression));

@ -15,7 +15,7 @@ export function ConstTag(node, context) {
const has_await = node.metadata.expression.has_await;
const blockers = [...node.metadata.expression.dependencies]
.map((dep) => dep.blocker)
.filter((b) => b !== null);
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);
if (has_await || context.state.async_consts || blockers.length > 0) {
const run = (context.state.async_consts ??= {
@ -30,7 +30,9 @@ export function ConstTag(node, context) {
context.state.init.push(b.let(identifier.name));
}
if (blockers.length > 0) {
if (blockers.length === 1) {
run.thunks.push(b.thunk(/** @type {Expression} */ (blockers[0])));
} else if (blockers.length > 0) {
run.thunks.push(b.thunk(b.call('Promise.all', b.array(blockers))));
}

@ -29,10 +29,10 @@ export function SvelteElement(node, context) {
tag = b.id(tag_id);
}
context.state.init.push(b.stmt(b.call('$.validate_dynamic_element_tag', b.thunk(tag))));
if (node.fragment.nodes.length > 0) {
context.state.init.push(b.stmt(b.call('$.validate_void_dynamic_element', b.thunk(tag))));
}
context.state.init.push(b.stmt(b.call('$.validate_dynamic_element_tag', b.thunk(tag))));
}
const state = {

@ -309,6 +309,20 @@ export function update_reaction(reaction) {
if (previous_reaction !== null && previous_reaction !== reaction) {
read_version++;
// update the `rv` of the previous reaction's deps — both existing and new —
// so that they are not added again
if (previous_reaction.deps !== null) {
for (let i = 0; i < previous_skipped_deps; i += 1) {
previous_reaction.deps[i].rv = read_version;
}
}
if (previous_deps !== null) {
for (const dep of previous_deps) {
dep.rv = read_version;
}
}
if (untracked_writes !== null) {
if (previous_untracked_writes === null) {
previous_untracked_writes = untracked_writes;
@ -526,7 +540,7 @@ export function get(signal) {
skipped_deps++;
} else if (new_deps === null) {
new_deps = [signal];
} else if (!new_deps.includes(signal)) {
} else {
new_deps.push(signal);
}
}

@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
export const VERSION = '5.47.1';
export const VERSION = '5.48.0';
export const PUBLIC_VERSION = '5';

@ -1,3 +1,15 @@
import { test } from '../../test';
export default test({});
/** @type {typeof console.log} */
let log;
export default test({
before_test() {
log = console.log;
console.log = () => {};
},
after_test() {
console.log = log;
}
});

@ -53,6 +53,8 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
await compile_directory(cwd, 'server', config.compileOptions);
}
config.before_test?.();
const target = window.document.body;
const head = window.document.head;
@ -72,8 +74,6 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
head.innerHTML = override_head ?? rendered.head;
}
config.before_test?.();
try {
const snapshot = config.snapshot ? config.snapshot(target) : {};

@ -0,0 +1,17 @@
import { test } from '../../test';
export default test({
mode: ['client', 'server'],
compileOptions: {
dev: true
},
get props() {
return { tag: true };
},
error:
'svelte_element_invalid_this_value\n' +
'The `this` prop on `<svelte:element>` must be a string, if defined'
});

@ -0,0 +1,5 @@
<script>
export let tag;
</script>
<svelte:element this={tag}>content</svelte:element>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
test() {}
});

@ -0,0 +1,4 @@
{#if true}
{@const xx = test}
{#snippet test()}{/snippet}
{/if}

@ -1,16 +1,21 @@
<script lang="ts">
import { hydratable } from 'svelte';
hydratable('key', () => Promise.resolve({
const a = await hydratable('key', () => Promise.resolve({
nested: Promise.resolve({
one: Promise.resolve(1),
}),
two: Promise.resolve(2),
}));
hydratable('key', () => Promise.resolve({
const b = await hydratable('key', () => Promise.resolve({
nested: Promise.resolve({
one: Promise.resolve(2),
}),
two: Promise.resolve(2),
}));
</script>
</script>
<p>{await (await (a.nested)).one}</p>
<p>{await a.two}</p>
<p>{await (await (b.nested)).one}</p>
<p>{await b.two}</p>

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({ compileOptions: { experimental: { async: true } } });

@ -0,0 +1,35 @@
import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/client';
var root_1 = $.from_html(`<p> </p>`);
export default function Async_const($$anchor) {
var fragment = $.comment();
var node = $.first_child(fragment);
{
var consequent = ($$anchor) => {
let a;
let b;
var promises = $.run([
async () => a = (await $.save($.async_derived(async () => (await $.save(1))())))(),
() => b = $.derived(() => $.get(a) + 1)
]);
var p = root_1();
var text = $.child(p, true);
$.reset(p);
$.template_effect(() => $.set_text(text, $.get(b)), void 0, void 0, [promises[1]]);
$.append($$anchor, p);
};
$.if(node, ($$render) => {
if (true) $$render(consequent);
});
}
$.append($$anchor, fragment);
}

@ -0,0 +1,33 @@
import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/server';
export default function Async_const($$renderer) {
if (true) {
$$renderer.push('<!--[-->');
let a;
let b;
var promises = $$renderer.run([
async () => {
a = (await $.save(1))();
},
() => {
b = a + 1;
}
]);
$$renderer.push(`<p>`);
$$renderer.async([promises[1]], ($$renderer) => {
$$renderer.push(() => $.escape(b));
});
$$renderer.push(`</p>`);
} else {
$$renderer.push('<!--[!-->');
}
$$renderer.push(`<!--]-->`);
}

@ -0,0 +1,6 @@
{#if true}
{@const a = await 1}
{@const b = a + 1}
<p>{b}</p>
{/if}
Loading…
Cancel
Save