Merge remote-tracking branch 'origin/main' into svelte-custom-renderer

svelte-custom-renderer
paoloricciuti 2 days ago
commit c22f2fa603

@ -28,7 +28,7 @@ jobs:
- name: Get PR ref
if: github.event_name != 'workflow_dispatch'
id: pr
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { data: pull } = await github.rest.pulls.get({
@ -46,12 +46,12 @@ jobs:
core.setFailed('PR is from a fork');
}
core.setOutput('ref', pull.head.ref);
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
if: github.event_name == 'workflow_dispatch' || steps.pr.outcome == 'success'
with:
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || steps.pr.outputs.ref }}
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
- uses: actions/setup-node@v6
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24
cache: pnpm

@ -32,9 +32,9 @@ jobs:
os: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: ${{ matrix.node-version }}
cache: pnpm
@ -48,9 +48,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 22
cache: pnpm
@ -65,9 +65,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24
cache: pnpm
@ -82,9 +82,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24
cache: pnpm
@ -105,9 +105,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24
cache: pnpm

@ -17,7 +17,7 @@ jobs:
contents: read # to clone the repo
steps:
- name: Check User Permissions
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
id: check-permissions
with:
script: |
@ -56,7 +56,7 @@ jobs:
}
- name: Get PR Data
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
id: get-pr-data
with:
script: |
@ -106,7 +106,7 @@ jobs:
- name: Generate Token
id: generate-token
uses: actions/create-github-app-token@v2
uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2
with:
app-id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }}
private-key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }}
@ -115,7 +115,7 @@ jobs:
svelte-ecosystem-ci
- name: Trigger Downstream Workflow
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
id: trigger
env:
COMMENT: ${{ github.event.comment.body }}

@ -1,229 +0,0 @@
name: pkg.pr.new
on:
pull_request_target:
types: [opened, synchronize]
push:
branches: [main]
workflow_dispatch:
inputs:
sha:
description: 'Commit SHA to build'
required: true
type: string
pr:
description: 'PR number to comment on'
required: true
type: number
permissions: {}
jobs:
build:
# Skip pull_request_target events from forks — maintainers can use workflow_dispatch instead
if: >
github.event_name != 'pull_request_target' ||
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
# No permissions — this job runs user-controlled code
permissions: {}
steps:
- uses: actions/checkout@v6
with:
# For pull_request_target, check out the PR head.
# For workflow_dispatch, check out the manually specified SHA.
# For push, fall back to the push SHA.
ref: ${{ github.event.pull_request.head.sha || inputs.sha || github.sha }}
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v6
with:
node-version: 22.x
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- run: pnpx pkg-pr-new publish --comment=off --json output.json --compact --no-template './packages/svelte'
- name: Upload output
uses: actions/upload-artifact@v4
with:
name: output
path: ./output.json
# Sanitizes the untrusted output from the build job before it's consumed by
# jobs with elevated permissions. This ensures that only known package names
# and valid SHA prefixes make it through.
sanitize:
needs: build
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Download artifact
uses: actions/download-artifact@v7
with:
name: output
- name: Sanitize output
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const raw = JSON.parse(fs.readFileSync('output.json', 'utf8'));
const ALLOWED_PACKAGES = new Set(['svelte']);
const SHA_PATTERN = /^[0-9a-f]{7}$/;
const packages = (raw.packages || [])
.filter(p => {
if (!ALLOWED_PACKAGES.has(p.name)) {
console.log(`Skipping unexpected package: ${JSON.stringify(p.name)}`);
return false;
}
const sha = p.url?.replace(/^.+@([^@]+)$/, '$1');
if (!sha || !SHA_PATTERN.test(sha)) {
console.log(`Skipping package with invalid SHA: ${JSON.stringify(p.url)}`);
return false;
}
return true;
})
.map(p => ({
name: p.name,
sha: p.url.replace(/^.+@([^@]+)$/, '$1'),
}));
fs.writeFileSync('sanitized-output.json', JSON.stringify({ packages }), 'utf8');
- name: Upload sanitized output
uses: actions/upload-artifact@v4
with:
name: sanitized-output
path: ./sanitized-output.json
comment:
needs: sanitize
if: github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Download sanitized artifact
uses: actions/download-artifact@v7
with:
name: sanitized-output
- name: Resolve PR number
id: pr
uses: actions/github-script@v8
with:
script: |
if (context.eventName === 'pull_request_target') {
core.setOutput('number', context.issue.number);
return;
}
// For workflow_dispatch, use the explicitly provided PR number.
// We can't use listPullRequestsAssociatedWithCommit because fork
// commits don't exist in the base repo, so the API returns nothing.
const pr = Number('${{ inputs.pr }}');
if (!pr || isNaN(pr)) {
core.setFailed('workflow_dispatch requires a valid pr input');
return;
}
core.setOutput('number', pr);
- name: Post or update comment
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const { packages } = JSON.parse(fs.readFileSync('sanitized-output.json', 'utf8'));
if (packages.length === 0) {
console.log('No valid packages found. Skipping comment.');
return;
}
const issue_number = parseInt('${{ steps.pr.outputs.number }}', 10);
const bot_comment_identifier = `<!-- pkg.pr.new comment -->`;
const body = `${bot_comment_identifier}
[Playground](https://svelte.dev/playground?version=pr-${issue_number})
\`\`\`
${packages.map(p => `pnpm add https://pkg.pr.new/${p.name}@${issue_number}`).join('\n')}
\`\`\`
`;
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
});
const existing = comments.data.find(c => c.body.includes(bot_comment_identifier));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
body,
});
}
log:
needs: sanitize
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Download sanitized artifact
uses: actions/download-artifact@v7
with:
name: sanitized-output
- name: Log publish info
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const { packages } = JSON.parse(fs.readFileSync('sanitized-output.json', 'utf8'));
if (packages.length === 0) {
console.log('No valid packages found.');
return;
}
console.log('\n' + '='.repeat(50));
console.log('Publish Information');
console.log('='.repeat(50));
for (const p of packages) {
console.log(`${p.name} - pnpm add https://pkg.pr.new/${p.name}@${p.sha}`);
}
const svelte = packages.find(p => p.name === 'svelte');
if (svelte) {
console.log(`\nPlayground: https://svelte.dev/playground?version=commit-${svelte.sha}`);
}
console.log('='.repeat(50));

@ -23,13 +23,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.x
cache: pnpm

@ -5,7 +5,7 @@
"private": true,
"type": "module",
"license": "MIT",
"packageManager": "pnpm@10.4.0",
"packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800",
"engines": {
"pnpm": ">=9.0.0"
},

@ -1,5 +1,13 @@
# svelte
## 5.56.8
### Patch Changes
- fix: call `onerror` and provide a working `reset` when hydrating a failed boundary ([#18556](https://github.com/sveltejs/svelte/pull/18556))
- fix: preserve select selection when spread attributes omit value ([#18561](https://github.com/sveltejs/svelte/pull/18561))
## 5.56.7
### Patch Changes

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

@ -211,17 +211,68 @@ export class Boundary {
*/
#hydrate_failed_content(error) {
const failed = this.#props.failed;
const { reset, invoke_onerror } = this.#create_reset(error);
// `onerror` may mutate state, which is disallowed while hydrating
queue_micro_task(invoke_onerror);
if (!failed) return;
this.#failed_effect = branch(() => {
failed(
this.#anchor,
() => error,
() => () => {}
() => reset
);
});
}
/**
* Creates the `reset` function for a failed boundary, along with a function
* that invokes `onerror` with it (if provided)
* @param {unknown} error
* @returns {{ reset: () => void, invoke_onerror: () => void }}
*/
#create_reset(error) {
var did_reset = false;
var calling_on_error = false;
const reset = () => {
if (did_reset) {
w.svelte_boundary_reset_noop();
return;
}
did_reset = true;
if (calling_on_error) {
e.svelte_boundary_reset_onerror();
}
if (this.#failed_effect !== null) {
pause_effect(this.#failed_effect, () => {
this.#failed_effect = null;
});
}
this.#run(() => {
this.#render();
});
};
const invoke_onerror = () => {
try {
calling_on_error = true;
this.#props.onerror?.(error, reset);
calling_on_error = false;
} catch (err) {
invoke_error_boundary(err, this.#effect && this.#effect.parent);
}
};
return { reset, invoke_onerror };
}
#hydrate_pending_content() {
const pending = this.#props.pending;
if (!pending) return;
@ -450,43 +501,13 @@ export class Boundary {
set_hydrate_node(skip_nodes());
}
var onerror = this.#props.onerror;
let failed = this.#props.failed;
var did_reset = false;
var calling_on_error = false;
const reset = () => {
if (did_reset) {
w.svelte_boundary_reset_noop();
return;
}
did_reset = true;
if (calling_on_error) {
e.svelte_boundary_reset_onerror();
}
if (this.#failed_effect !== null) {
pause_effect(this.#failed_effect, () => {
this.#failed_effect = null;
});
}
this.#run(() => {
this.#render();
});
};
/** @param {unknown} transformed_error */
const handle_error_result = (transformed_error) => {
try {
calling_on_error = true;
onerror?.(transformed_error, reset);
calling_on_error = false;
} catch (error) {
invoke_error_boundary(error, this.#effect && this.#effect.parent);
}
const { reset, invoke_onerror } = this.#create_reset(transformed_error);
invoke_onerror();
if (failed) {
this.#failed_effect = this.#run(() => {

@ -56,8 +56,10 @@ export function select_option(select, value, mounting = false) {
*/
export function init_select(select) {
var observer = new MutationObserver(() => {
// @ts-ignore
select_option(select, select.__value);
if ('__value' in select) {
// @ts-ignore
select_option(select, select.__value);
}
// Deliberately don't update the potential binding value,
// the model should be preserved unless explicitly changed
});

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

@ -0,0 +1,25 @@
import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
mode: ['client', 'hydrate'],
async test({ assert, component, target }) {
const select = target.querySelector('select');
ok(select);
assert.equal(select.selectedIndex, 0);
component.toggle();
flushSync();
await Promise.resolve();
assert.equal(select.selectedIndex, 0);
component.toggle();
flushSync();
await Promise.resolve();
assert.equal(select.selectedIndex, 0);
}
});

@ -0,0 +1,16 @@
<script>
let show_extra = false;
const attributes = { 'aria-label': 'choice' };
export function toggle() {
show_extra = !show_extra;
}
</script>
<select {...attributes}>
<option value="">Choose an option</option>
<option value="first">First</option>
{#if show_extra}
<option value="extra">Extra</option>
{/if}
</select>

@ -0,0 +1,19 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['hydrate'],
ssrHtml: '<p>failed: error</p> <button>reset</button>',
transformError: () => 'error',
test({ assert, target, logs }) {
// `onerror` is called upon hydration with the deserialized error
assert.deepEqual(logs, ['onerror: error']);
const btn = target.querySelector('button');
btn?.click();
flushSync();
assert.htmlEqual(target.innerHTML, '<p>recovered</p> <button>reset</button>');
}
});

@ -0,0 +1,29 @@
<script>
import Child from './child.svelte';
let recovered = $state(false);
let reset_fn = $state();
</script>
<svelte:boundary
onerror={(error, reset) => {
console.log(`onerror: ${error}`);
reset_fn = reset;
}}
>
{#if recovered}
<p>recovered</p>
{:else}
<Child />
{/if}
{#snippet failed(error)}
<p>failed: {error}</p>
{/snippet}
</svelte:boundary>
<button
onclick={() => {
recovered = true;
reset_fn();
}}>reset</button>

@ -0,0 +1,16 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['hydrate'],
ssrHtml: '<p>failed: error</p> <button>reset</button>',
transformError: () => 'error',
test({ assert, target }) {
const btn = target.querySelector('button');
btn?.click();
flushSync();
assert.htmlEqual(target.innerHTML, '<p>recovered</p>');
}
});

@ -0,0 +1,22 @@
<script>
import Child from './child.svelte';
let recovered = $state(false);
</script>
<svelte:boundary>
{#if recovered}
<p>recovered</p>
{:else}
<Child />
{/if}
{#snippet failed(error, reset)}
<p>failed: {error}</p>
<button
onclick={() => {
recovered = true;
reset();
}}>reset</button>
{/snippet}
</svelte:boundary>

@ -1,3 +1,15 @@
minimumReleaseAge: 2880
minimumReleaseAgeExclude:
- '@sveltejs/*'
- svelte
- esrap
- devalue
- zimmerframe
- prettier-plugin-svelte
- svelte-check
- esm-env
blockExoticSubdeps: true
packages:
- 'packages/*'
- 'playgrounds/*'

Loading…
Cancel
Save