diff --git a/.changeset/loose-sloths-guess.md b/.changeset/loose-sloths-guess.md deleted file mode 100644 index 450040349d..0000000000 --- a/.changeset/loose-sloths-guess.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: allow passing `ShadowRootInit` object to custom element `shadow` option diff --git a/.changeset/purple-eagles-enjoy.md b/.changeset/purple-eagles-enjoy.md new file mode 100644 index 0000000000..318f85a946 --- /dev/null +++ b/.changeset/purple-eagles-enjoy.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: use symbols for encapsulated event delegation diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8dcf1e45dc..23d814d527 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,7 +86,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v6 with: - node-version: 18 + node-version: 24 cache: pnpm - name: install run: pnpm install --frozen-lockfile diff --git a/.github/workflows/pkg.pr.new-comment.yml b/.github/workflows/pkg.pr.new-comment.yml deleted file mode 100644 index 64495cc5c8..0000000000 --- a/.github/workflows/pkg.pr.new-comment.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: Update pkg.pr.new comment - -on: - workflow_run: - workflows: ['Publish Any Commit'] - types: - - completed - -permissions: - pull-requests: write - -jobs: - build: - name: 'Update comment' - runs-on: ubuntu-latest - steps: - - name: Download artifact - uses: actions/download-artifact@v7 - with: - name: output - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ github.event.workflow_run.id }} - - - run: ls -R . - - name: 'Post or update comment' - uses: actions/github-script@v8 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs'); - const output = JSON.parse(fs.readFileSync('output.json', 'utf8')); - - const bot_comment_identifier = ``; - - const body = (number) => `${bot_comment_identifier} - - [Playground](https://svelte.dev/playground?version=pr-${number}) - - \`\`\` - ${output.packages.map((p) => `pnpm add https://pkg.pr.new/${p.name}@${number}`).join('\n')} - \`\`\` - `; - - async function find_bot_comment(issue_number) { - if (!issue_number) return null; - const comments = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue_number, - }); - return comments.data.find((comment) => - comment.body.includes(bot_comment_identifier) - ); - } - - async function create_or_update_comment(issue_number) { - if (!issue_number) { - console.log('No issue number provided. Cannot post or update comment.'); - return; - } - - const existing_comment = await find_bot_comment(issue_number); - if (existing_comment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing_comment.id, - body: body(issue_number), - }); - } else { - await github.rest.issues.createComment({ - issue_number: issue_number, - owner: context.repo.owner, - repo: context.repo.repo, - body: body(issue_number), - }); - } - } - - async function log_publish_info() { - const svelte_package = output.packages.find(p => p.name === 'svelte'); - const svelte_sha = svelte_package.url.replace(/^.+@([^@]+)$/, '$1'); - console.log('\n' + '='.repeat(50)); - console.log('Publish Information'); - console.log('='.repeat(50)); - console.log('\nPublished Packages:'); - console.log(output.packages.map((p) => `${p.name} - pnpm add https://pkg.pr.new/${p.name}@${p.url.replace(/^.+@([^@]+)$/, '$1')}`).join('\n')); - if(svelte_sha){ - console.log('\nPlayground URL:'); - console.log(`\nhttps://svelte.dev/playground?version=commit-${svelte_sha}`) - } - console.log('\n' + '='.repeat(50)); - } - - if (output.event_name === 'pull_request') { - if (output.number) { - await create_or_update_comment(output.number); - } - } else if (output.event_name === 'push') { - const pull_requests = await github.rest.pulls.list({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - head: `${context.repo.owner}:${output.ref.replace('refs/heads/', '')}`, - }); - - if (pull_requests.data.length > 0) { - await create_or_update_comment(pull_requests.data[0].number); - } else { - console.log( - 'No open pull request found for this push. Logging publish information to console:' - ); - await log_publish_info(); - } - } diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml index 252cbed769..e7cc1facad 100644 --- a/.github/workflows/pkg.pr.new.yml +++ b/.github/workflows/pkg.pr.new.yml @@ -1,16 +1,36 @@ -name: Publish Any Commit -on: [push, pull_request] +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 permissions: {} jobs: build: - permissions: {} - + # 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@v4 - uses: actions/setup-node@v6 with: @@ -24,21 +44,192 @@ jobs: run: pnpm build - run: pnpx pkg-pr-new publish --comment=off --json output.json --compact --no-template './packages/svelte' - - name: Add metadata to output + + - 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; + } + + const { data: pulls } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: '${{ inputs.sha }}', + }); + + const open = pulls.filter(p => p.state === 'open'); + + if (open.length === 0) { + core.setFailed(`No open PR found for commit ${{ inputs.sha }}`); + return; + } + + if (open.length > 1) { + const nums = open.map(p => `#${p.number}`).join(', '); + core.setFailed(`Multiple open PRs found for commit ${{ inputs.sha }}: ${nums}`); + return; + } + + core.setOutput('number', open[0].number); + + - name: Post or update comment uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); - const output = JSON.parse(fs.readFileSync('output.json', 'utf8')); - output.number = context.issue.number; - output.event_name = context.eventName; - output.ref = context.ref; - fs.writeFileSync('output.json', JSON.stringify(output), 'utf8'); - - name: Upload output - uses: actions/upload-artifact@v6 + 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 = ``; + + 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: output - path: ./output.json + 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; + } - - run: ls -R . + 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)); diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index 6fbf3b8895..d763b6578f 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -1,5 +1,6 @@ --- title: $state +tags: rune-state --- The `$state` rune allows you to create _reactive state_, which means that your UI _reacts_ when it changes. diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index d3e46eb22d..35cb6c1912 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -1,5 +1,6 @@ --- title: $derived +tags: rune-derived --- Derived state is declared with the `$derived` rune: diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index 6c42f55795..d41c5b8e6a 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -1,5 +1,6 @@ --- title: $effect +tags: rune-effect --- Effects are functions that run when state updates, and can be used for things like calling third-party libraries, drawing on `` elements, or making network requests. They only run in the browser, not during server-side rendering. diff --git a/documentation/docs/02-runes/05-$props.md b/documentation/docs/02-runes/05-$props.md index bd5ae16853..737cdf523e 100644 --- a/documentation/docs/02-runes/05-$props.md +++ b/documentation/docs/02-runes/05-$props.md @@ -1,5 +1,6 @@ --- title: $props +tags: rune-props --- The inputs to a component are referred to as _props_, which is short for _properties_. You pass props to components just like you pass attributes to elements: diff --git a/documentation/docs/02-runes/07-$inspect.md b/documentation/docs/02-runes/07-$inspect.md index 6d47e30e27..f67e250b45 100644 --- a/documentation/docs/02-runes/07-$inspect.md +++ b/documentation/docs/02-runes/07-$inspect.md @@ -1,5 +1,6 @@ --- title: $inspect +tags: rune-inspect --- > [!NOTE] `$inspect` only works during development. In a production build it becomes a noop. diff --git a/documentation/docs/03-template-syntax/02-if.md b/documentation/docs/03-template-syntax/02-if.md index 1378733e6f..499f6d219c 100644 --- a/documentation/docs/03-template-syntax/02-if.md +++ b/documentation/docs/03-template-syntax/02-if.md @@ -1,5 +1,6 @@ --- title: {#if ...} +tags: template-if --- ```svelte diff --git a/documentation/docs/03-template-syntax/03-each.md b/documentation/docs/03-template-syntax/03-each.md index 57ed0def71..2fe7f28bf2 100644 --- a/documentation/docs/03-template-syntax/03-each.md +++ b/documentation/docs/03-template-syntax/03-each.md @@ -1,5 +1,6 @@ --- title: {#each ...} +tags: template-each --- ```svelte @@ -12,7 +13,7 @@ title: {#each ...} {#each expression as name, index}...{/each} ``` -Iterating over values can be done with an each block. The values in question can be arrays, array-like objects (i.e. anything with a `length` property), or iterables like `Map` and `Set`— in other words, anything that can be used with `Array.from`. +Iterating over values can be done with an each block. The values in question can be arrays, array-like objects (i.e. anything with a `length` property), or iterables like `Map` and `Set`. (Internally, they are converted to arrays with [`Array.from`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from).) If the value is `null` or `undefined`, it is treated the same as an empty array (which will cause [else blocks](#Else-blocks) to be rendered, where applicable). diff --git a/documentation/docs/03-template-syntax/04-key.md b/documentation/docs/03-template-syntax/04-key.md index 10b6ab4358..6c88e28f86 100644 --- a/documentation/docs/03-template-syntax/04-key.md +++ b/documentation/docs/03-template-syntax/04-key.md @@ -1,5 +1,6 @@ --- title: {#key ...} +tags: template-key --- ```svelte diff --git a/documentation/docs/03-template-syntax/05-await.md b/documentation/docs/03-template-syntax/05-await.md index 842b3c7e32..aebaf8d33a 100644 --- a/documentation/docs/03-template-syntax/05-await.md +++ b/documentation/docs/03-template-syntax/05-await.md @@ -1,5 +1,6 @@ --- title: {#await ...} +tags: template-await --- ```svelte diff --git a/documentation/docs/03-template-syntax/08-@html.md b/documentation/docs/03-template-syntax/08-@html.md index 6d8a8be0c6..36b8ad36b3 100644 --- a/documentation/docs/03-template-syntax/08-@html.md +++ b/documentation/docs/03-template-syntax/08-@html.md @@ -1,5 +1,6 @@ --- title: {@html ...} +tags: template-html --- To inject raw HTML into your component, use the `{@html ...}` tag: diff --git a/documentation/docs/03-template-syntax/09-@attach.md b/documentation/docs/03-template-syntax/09-@attach.md index b25fbb32a6..0087923b15 100644 --- a/documentation/docs/03-template-syntax/09-@attach.md +++ b/documentation/docs/03-template-syntax/09-@attach.md @@ -1,5 +1,6 @@ --- title: {@attach ...} +tags: attachments --- Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates. @@ -82,6 +83,14 @@ Attachments can also be created inline ([demo](/playground/untitled#H4sIAAAAAAAA > [!NOTE] > The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state. +## Conditional attachments + +Falsy values like `false` or `undefined` are treated as no attachment, enabling conditional usage: + +```svelte +
...
+``` + ## Passing attachments to components When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments. diff --git a/documentation/docs/03-template-syntax/14-transition.md b/documentation/docs/03-template-syntax/14-transition.md index c51175c272..9b14b7b25b 100644 --- a/documentation/docs/03-template-syntax/14-transition.md +++ b/documentation/docs/03-template-syntax/14-transition.md @@ -1,5 +1,6 @@ --- title: transition: +tags: transitions --- A _transition_ is triggered by an element entering or leaving the DOM as a result of a state change. diff --git a/documentation/docs/03-template-syntax/15-in-and-out.md b/documentation/docs/03-template-syntax/15-in-and-out.md index f4e37c845e..8adb62bf14 100644 --- a/documentation/docs/03-template-syntax/15-in-and-out.md +++ b/documentation/docs/03-template-syntax/15-in-and-out.md @@ -1,5 +1,6 @@ --- title: in: and out: +tags: transitions --- The `in:` and `out:` directives are identical to [`transition:`](transition), except that the resulting transitions are not bidirectional — an `in` transition will continue to 'play' alongside the `out` transition, rather than reversing, if the block is outroed while the transition is in progress. If an out transition is aborted, transitions will restart from scratch. @@ -7,7 +8,7 @@ The `in:` and `out:` directives are identical to [`transition:`](transition), ex ```svelte diff --git a/documentation/docs/03-template-syntax/17-style.md b/documentation/docs/03-template-syntax/17-style.md index aa61cdcde3..8b25c221d6 100644 --- a/documentation/docs/03-template-syntax/17-style.md +++ b/documentation/docs/03-template-syntax/17-style.md @@ -1,5 +1,6 @@ --- title: style: +tags: template-style --- The `style:` directive provides a shorthand for setting multiple styles on an element. diff --git a/documentation/docs/03-template-syntax/18-class.md b/documentation/docs/03-template-syntax/18-class.md index db85db4b37..5d7cb3100d 100644 --- a/documentation/docs/03-template-syntax/18-class.md +++ b/documentation/docs/03-template-syntax/18-class.md @@ -1,5 +1,6 @@ --- title: class +tags: template-style --- There are two ways to set classes on elements: the `class` attribute, and the `class:` directive. diff --git a/documentation/docs/04-styling/01-scoped-styles.md b/documentation/docs/04-styling/01-scoped-styles.md index eae26d0cb1..cdfbeab19b 100644 --- a/documentation/docs/04-styling/01-scoped-styles.md +++ b/documentation/docs/04-styling/01-scoped-styles.md @@ -1,5 +1,6 @@ --- title: Scoped styles +tags: styles-scoped --- Svelte components can include a `