diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf73a1f6cb..046ad335f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,8 @@ jobs: os: ubuntu-latest - node-version: 22 os: ubuntu-latest + - node-version: 24 + os: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -41,6 +43,23 @@ jobs: - run: pnpm test env: CI: true + TestNoAsync: + permissions: {} + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm playwright install chromium + - run: pnpm test runtime-runes + env: + CI: true + SVELTE_NO_ASYNC: true Lint: permissions: {} runs-on: ubuntu-latest diff --git a/.github/workflows/ecosystem-ci-trigger.yml b/.github/workflows/ecosystem-ci-trigger.yml index 71df3242e8..9be1f00104 100644 --- a/.github/workflows/ecosystem-ci-trigger.yml +++ b/.github/workflows/ecosystem-ci-trigger.yml @@ -8,9 +8,17 @@ jobs: trigger: runs-on: ubuntu-latest if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') + permissions: + issues: write # to add / delete reactions + pull-requests: write # to read PR data, and to add labels + actions: read # to check workflow status + contents: read # to clone the repo steps: - - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - - uses: actions/github-script@v6 + - name: monitor action permissions + uses: GitHubSecurityLab/actions-permissions/monitor@v1 + - name: check user authorization # user needs triage permission + uses: actions/github-script@v7 + id: check-permissions with: script: | const user = context.payload.sender.login @@ -29,7 +37,7 @@ jobs: } if (hasTriagePermission) { - console.log('Allowed') + console.log('User is allowed. Adding +1 reaction.') await github.rest.reactions.createForIssueComment({ owner: context.repo.owner, repo: context.repo.repo, @@ -37,16 +45,18 @@ jobs: content: '+1', }) } else { - console.log('Not allowed') + console.log('User is not allowed. Adding -1 reaction.') await github.rest.reactions.createForIssueComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: context.payload.comment.id, content: '-1', }) - throw new Error('not allowed') + throw new Error('User does not have the necessary permissions.') } - - uses: actions/github-script@v6 + + - name: Get PR Data + uses: actions/github-script@v7 id: get-pr-data with: script: | @@ -59,21 +69,27 @@ jobs: return { num: context.issue.number, branchName: pr.head.ref, + commit: pr.head.sha, repo: pr.head.repo.full_name } - - id: generate-token - uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 #keep pinned for security reasons, currently 1.8.0 + + - name: Generate Token + id: generate-token + uses: actions/create-github-app-token@v2 with: - app_id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }} - private_key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }} - repository: '${{ github.repository_owner }}/svelte-ecosystem-ci' - - uses: actions/github-script@v6 + app-id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }} + private-key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }} + repositories: | + svelte + svelte-ecosystem-ci + + - name: Trigger Downstream Workflow + uses: actions/github-script@v7 id: trigger env: COMMENT: ${{ github.event.comment.body }} with: github-token: ${{ steps.generate-token.outputs.token }} - result-encoding: string script: | const comment = process.env.COMMENT.trim() const prData = ${{ steps.get-pr-data.outputs.result }} @@ -89,6 +105,7 @@ jobs: prNumber: '' + prData.num, branchName: prData.branchName, repo: prData.repo, + commit: prData.commit, suite: suite === '' ? '-' : suite } }) diff --git a/.prettierignore b/.prettierignore index d5c124353c..5e1d9b1aa7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,6 +7,7 @@ packages/**/config/*.js # packages/svelte packages/svelte/messages/**/*.md +packages/svelte/scripts/_bundle.js packages/svelte/src/compiler/errors.js packages/svelte/src/compiler/warnings.js packages/svelte/src/internal/client/errors.js @@ -25,17 +26,7 @@ packages/svelte/tests/hydration/samples/*/_expected.html packages/svelte/tests/hydration/samples/*/_override.html packages/svelte/types packages/svelte/compiler/index.js -playgrounds/sandbox/input/**.svelte -playgrounds/sandbox/output - -# sites/svelte.dev -sites/svelte.dev/static/svelte-app.json -sites/svelte.dev/scripts/svelte-app/ -sites/svelte.dev/src/routes/_components/Supporters/contributors.jpg -sites/svelte.dev/src/routes/_components/Supporters/contributors.js -sites/svelte.dev/src/routes/_components/Supporters/donors.jpg -sites/svelte.dev/src/routes/_components/Supporters/donors.js -sites/svelte.dev/src/lib/generated +playgrounds/sandbox/src/* **/node_modules **/.svelte-kit diff --git a/.prettierrc b/.prettierrc index c4fd5d9f2f..c2d09a4289 100644 --- a/.prettierrc +++ b/.prettierrc @@ -17,12 +17,6 @@ "useTabs": false, "tabWidth": 2 } - }, - { - "files": ["sites/svelte-5-preview/src/routes/docs/content/**/*.md"], - "options": { - "printWidth": 60 - } } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 21a2a11c84..4d360cbc8a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,3 @@ { - "search.exclude": { - "sites/svelte-5-preview/static/*": true - }, "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e2628f84f..c2d3e45049 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,10 +105,10 @@ Test samples are kept in `/test/xxx/samples` folder. pnpm test validator ``` -1. To filter tests _within_ a test suite, use `pnpm test -- -t `, for example: +1. To filter tests _within_ a test suite, use `pnpm test -t `, for example: ```bash - pnpm test validator -- -t a11y-alt-text + pnpm test validator -t a11y-alt-text ``` (You can also do `FILTER= pnpm test ` which removes other tests rather than simply skipping them — this will result in faster and more compact test results, but it's non-idiomatic. Choose your fighter.) diff --git a/benchmarking/compare/index.js b/benchmarking/compare/index.js index 9d8d279c35..8f38686a29 100644 --- a/benchmarking/compare/index.js +++ b/benchmarking/compare/index.js @@ -67,19 +67,29 @@ for (let i = 0; i < results[0].length; i += 1) { for (const metric of ['time', 'gc_time']) { const times = results.map((result) => +result[i][metric]); let min = Infinity; + let max = -Infinity; let min_index = -1; for (let b = 0; b < times.length; b += 1) { - if (times[b] < min) { - min = times[b]; + const time = times[b]; + + if (time < min) { + min = time; min_index = b; } + + if (time > max) { + max = time; + } } if (min !== 0) { - console.group(`${metric}: fastest is ${branches[min_index]}`); + console.group(`${metric}: fastest is ${char(min_index)} (${branches[min_index]})`); times.forEach((time, b) => { - console.log(`${branches[b]}: ${time.toFixed(2)}ms (${((time / min) * 100).toFixed(2)}%)`); + const SIZE = 20; + const n = Math.round(SIZE * (time / max)); + + console.log(`${char(b)}: ${'◼'.repeat(n)}${' '.repeat(SIZE - n)} ${time.toFixed(2)}ms`); }); console.groupEnd(); } @@ -87,3 +97,7 @@ for (let i = 0; i < results[0].length; i += 1) { console.groupEnd(); } + +function char(i) { + return String.fromCharCode(97 + i); +} diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index fc96e6517e..3fbcda7243 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -20,9 +20,7 @@ Unlike other frameworks you may have encountered, there is no API for interactin If `$state` is used with an array or a simple object, the result is a deeply reactive _state proxy_. [Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) allow Svelte to run code when you read or write properties, including via methods like `array.push(...)`, triggering granular updates. -> [!NOTE] Classes like `Set` and `Map` will not be proxied, but Svelte provides reactive implementations for various built-ins like these that can be imported from [`svelte/reactivity`](./svelte-reactivity). - -State is proxified recursively until Svelte finds something other than an array or simple object. In a case like this... +State is proxified recursively until Svelte finds something other than an array or simple object (like a class or an object created with `Object.create`). In a case like this... ```js let todos = $state([ @@ -52,7 +50,7 @@ todos.push({ }); ``` -> [!NOTE] When you update properties of proxies, the original object is _not_ mutated. +> [!NOTE] When you update properties of proxies, the original object is _not_ mutated. If you need to use your own proxy handlers in a state proxy, [you should wrap the object _after_ wrapping it in `$state`](https://svelte.dev/playground/hello-world?version=latest#H4sIAAAAAAAACpWR3WoDIRCFX2UqhWyIJL3erAulL9C7XnQLMe5ksbUqOpsfln33YuyGFNJC8UKdc2bOhw7Myk9kJXsJ0nttO9jcR5KEG9AWJDwHdzwxznbaYGTl68Do5JM_FRifuh-9X8Y9Gkq1rYx4q66cJbQUWcmqqIL2VDe2IYMEbvuOikBADi-GJDSkXG-phId0G-frye2DO2psQYDFQ0Ys8gQO350dUkEydEg82T0GOs0nsSG9g2IqgxACZueo2ZUlpdvoDC6N64qsg1QKY8T2bpZp8gpIfbCQ85Zn50Ud82HkeY83uDjspenxv3jXcSDyjPWf9L1vJf0GH666J-jLu1ery4dV257IWXBWGa0-xFDMQdTTn2ScxWKsn86ROsLwQxqrVR5QM84Ij8TKFD2-cUZSm4O2LSt30kQcvwCgCmfZnAIAAA==). Note that if you destructure a reactive value, the references are not reactive — as in normal JavaScript, they are evaluated at the point of destructuring: @@ -67,16 +65,15 @@ todos[0].done = !todos[0].done; ### Classes -You can also use `$state` in class fields (whether public or private): +Class instances are not proxied. Instead, you can use `$state` in class fields (whether public or private), or as the first assignment to a property immediately inside the `constructor`: ```js // @errors: 7006 2554 class Todo { done = $state(false); - text = $state(); constructor(text) { - this.text = text; + this.text = $state(text); } reset() { @@ -110,10 +107,9 @@ You can either use an inline function... // @errors: 7006 2554 class Todo { done = $state(false); - text = $state(); constructor(text) { - this.text = text; + this.text = $state(text); } +++reset = () => {+++ @@ -123,6 +119,10 @@ class Todo { } ``` +### Built-in classes + +Svelte provides reactive implementations of built-in classes like `Set`, `Map`, `Date` and `URL` that can be imported from [`svelte/reactivity`](svelte-reactivity). + ## `$state.raw` In cases where you don't want objects and arrays to be deeply reactive you can use `$state.raw`. @@ -163,6 +163,8 @@ let count = $state(0, { > The `onchange` function is [untracked](svelte#untrack). +As with `$state`, you can declare class fields using `$state.raw`. + ## `$state.snapshot` To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`: diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index 2464aa9295..0123868c4e 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -94,6 +94,27 @@ let selected = $derived(items[index]); ...you can change (or `bind:` to) properties of `selected` and it will affect the underlying `items` array. If `items` was _not_ deeply reactive, mutating `selected` would have no effect. +## Destructuring + +If you use destructuring with a `$derived` declaration, the resulting variables will all be reactive — this... + +```js +function stuff() { return { a: 1, b: 2, c: 3 } } +// ---cut--- +let { a, b, c } = $derived(stuff()); +``` + +...is roughly equivalent to this: + +```js +function stuff() { return { a: 1, b: 2, c: 3 } } +// ---cut--- +let _stuff = $derived(stuff()); +let a = $derived(_stuff.a); +let b = $derived(_stuff.b); +let c = $derived(_stuff.c); +``` + ## Update propagation Svelte uses something called _push-pull reactivity_ — when state is updated, everything that depends on the state (whether directly or indirectly) is immediately notified of the change (the 'push'), but derived values are not re-evaluated until they are actually read (the 'pull'). diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index 46ea9b81e9..5820e178a0 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -221,6 +221,21 @@ The `$effect.tracking` rune is an advanced feature that tells you whether or not It is used to implement abstractions like [`createSubscriber`](/docs/svelte/svelte-reactivity#createSubscriber), which will create listeners to update reactive values but _only_ if those values are being tracked (rather than, for example, read inside an event handler). +## `$effect.pending` + +When using [`await`](await-expressions) in components, the `$effect.pending()` rune tells you how many promises are pending in the current [boundary](svelte-boundary), not including child boundaries ([demo](/playground/untitled#H4sIAAAAAAAAE3WRMU_DMBCF_8rJdHDUqilILGkaiY2RgY0yOPYZWbiOFV8IleX_jpMUEAIWS_7u-d27c2ROnJBV7B6t7WDsequAozKEqmAbpo3FwKqnyOjsJ90EMr-8uvN-G97Q0sRaEfAvLjtH6CjbsDrI3nhqju5IFgkEHGAVSBDy62L_SdtvejPTzEU4Owl6cJJM50AoxcUG2gLiVM31URgChyM89N3JBORcF3BoICA9mhN2A3G9gdvdrij2UJYgejLaSCMsKLTivNj0SEOf7WEN7ZwnHV1dfqd2dTsQ5QCdk9bI10PkcxexXqcmH3W51Jt_le2kbH8os9Y3UaTcNLYpDx-Xab6GTHXpZ128MhpWqDVK2np0yrgXXqQpaLa4APDLBkIF8bd2sYql0Sn_DeE7sYr6AdNzvgljR-MUq7SwAdMHeUtgHR4CAAA=)): + +```svelte + + + +

{a} + {b} = {await add(a, b)}

+ +{#if $effect.pending()} +

pending promises: {$effect.pending()}

+{/if} +``` + ## `$effect.root` The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for nested effects that you want to manually control. This rune also allows for the creation of effects outside of the component initialisation phase. @@ -269,11 +284,11 @@ In general, `$effect` is best considered something of an escape hatch — useful If you're using an effect because you want to be able to reassign the derived value (to build an optimistic UI, for example) note that [deriveds can be directly overridden]($derived#Overriding-derived-values) as of Svelte 5.25. -You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAACpVRy26DMBD8FcvKgUhtoIdeHBwp31F6MGSJkBbHwksEQvx77aWQqooq9bgzOzP7mGTdIHipPiZJowOpGJAv0po2VmfnDv4OSBErjYdneHWzBJaCjcx91TWOToUtCIEE3cig0OIty44r5l1oDtjOkyFIsv3GINQ_CNYyGegd1DVUlCR7oU9iilDUcP8S8roYs9n8p2wdYNVFm4csTx872BxNCcjr5I11fdgonEkXsjP2CoUUZWMv6m6wBz2x7yxaM-iJvWeRsvSbSVeUy5i0uf8vKA78NIeJLSZWv1I8jQjLdyK4XuTSeIdmVKJGGI4LdjVOiezwDu1yG74My8PLCQaSiroe5s_5C2PHrkVGAgAA)): +You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAAE5WRTWrDMBCFryKGLBJoY3fRjWIHeoiu6i6UZBwEY0VE49TB-O6VxrFTSih0qe_Ne_OjHpxpEDS8O7ZMeIAnqC1hAP3RA1990hKI_Fb55v06XJA4sZ0J-IjvT47RcYyBIuzP1vO2chVHHFjxiQ2pUr3k-SZRQlbBx_LIFoEN4zJfzQph_UMQr4hRXmBd456Xy5Uqt6pPKHmkfmzyPAZL2PCnbRpg8qWYu63I7lu4gswOSRYqrPNt3CgeqqzgbNwRK1A76w76YqjFspfcQTWmK3vJHlQm1puSTVSeqdOc_r9GaeCHfUSY26TXry6Br4RSK3C6yMEGT-aqVU3YbUZ2NF6rfP2KzXgbuYzY46czdgyazy0On_FlLH3F-UDXhgIO35UGlA1rAgAA)): ```svelte ``` diff --git a/documentation/docs/02-runes/07-$inspect.md b/documentation/docs/02-runes/07-$inspect.md index ff3d64757b..13ac8b79a3 100644 --- a/documentation/docs/02-runes/07-$inspect.md +++ b/documentation/docs/02-runes/07-$inspect.md @@ -52,6 +52,7 @@ This rune, added in 5.14, causes the surrounding function to be _traced_ in deve import { doSomeWork } from './elsewhere'; $effect(() => { + +++// $inspect.trace must be the first statement of a function body+++ +++$inspect.trace();+++ doSomeWork(); }); diff --git a/documentation/docs/03-template-syntax/01-basic-markup.md b/documentation/docs/03-template-syntax/01-basic-markup.md index fe5f8b02aa..feecfe033e 100644 --- a/documentation/docs/03-template-syntax/01-basic-markup.md +++ b/documentation/docs/03-template-syntax/01-basic-markup.md @@ -82,12 +82,14 @@ As with elements, `name={name}` can be replaced with the `{name}` shorthand. ``` +## Spread attributes + _Spread attributes_ allow many attributes or properties to be passed to an element or component at once. -An element or component can have multiple spread attributes, interspersed with regular ones. +An element or component can have multiple spread attributes, interspersed with regular ones. Order matters — if `things.a` exists it will take precedence over `a="b"`, while `c="d"` would take precedence over `things.c`: ```svelte - + ``` ## Events diff --git a/documentation/docs/03-template-syntax/03-each.md b/documentation/docs/03-template-syntax/03-each.md index 70666f6a57..006cadd152 100644 --- a/documentation/docs/03-template-syntax/03-each.md +++ b/documentation/docs/03-template-syntax/03-each.md @@ -43,7 +43,9 @@ An each block can also specify an _index_, equivalent to the second argument in {#each expression as name, index (key)}...{/each} ``` -If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to diff the list when data changes, rather than adding or removing items at the end. The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change. +If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to intelligently update the list when data changes by inserting, moving and deleting items, rather than adding or removing items at the end and updating the state in the middle. + +The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change. ```svelte {#each items as item (item.id)} diff --git a/documentation/docs/03-template-syntax/09-@attach.md b/documentation/docs/03-template-syntax/09-@attach.md new file mode 100644 index 0000000000..b25fbb32a6 --- /dev/null +++ b/documentation/docs/03-template-syntax/09-@attach.md @@ -0,0 +1,166 @@ +--- +title: {@attach ...} +--- + +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. + +Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM. + +> [!NOTE] +> Attachments are available in Svelte 5.29 and newer. + +```svelte + + + +
...
+``` + +An element can have any number of attachments. + +## Attachment factories + +A useful pattern is for a function, such as `tooltip` in this example, to _return_ an attachment ([demo](/playground/untitled#H4sIAAAAAAAAE3VT0XLaMBD8lavbDiaNCUlbHhTItG_5h5AH2T5ArdBppDOEMv73SkbGJGnH47F9t3un3TsfMyO3mInsh2SW1Sa7zlZKo8_E0zHjg42pGAjxBPxp7cTvUHOMldLjv-IVGUbDoUw295VTlh-WZslqa8kxsLL2ACtHWxh175NffnQfAAGikSGxYQGfPEvGfPSIWtOH0TiBVo2pWJEBJtKhQp4YYzjG9JIdcuMM5IZqHMPioY8vOSA997zQoevf4a7heO7cdp34olRiTGr07OhwH1IdoO2A7dLMbwahZq6MbRhKZWqxk7rBxTGVbuHmhCgb5qDgmIx_J6XtHHukHTrYYqx_YpzYng8aO4RYayql7hU-1ZJl0akqHBE_D9KLolwL-Dibzc7iSln9XjtqTF1UpMkJ2EmXR-BgQErsN4pxIJKr0RVO1qrxAqaTO4fbc9bKulZm3cfDY3aZDgvFGErWjmzhN7KmfX5rXyDeX8Pt1mU-hXjdBOrtuB97vK4GPUtmJ41XcRMEGDLD8do0nJ73zhUhSlyRw0t3vPqD8cjfLs-axiFgNBrkUd9Ulp50c-GLxlXAVlJX-ffpZyiSn7H0eLCUySZQcQdXlxj4El0Yv_FZvIKElqqGTruVLhzu7VRKCh22_5toOyxsWqLwwzK-cCbYNdg-hy-p9D7sbiZWUnts_wLUOF3CJgQAAA==)): + +```svelte + + + + + + +``` + +Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).) + +## Inline attachments + +Attachments can also be created inline ([demo](/playground/untitled#H4sIAAAAAAAAE71Wf3OaWBT9KoyTTnW3MS-I3dYmnWXVtnRAazRJzbozRSQEApiRhwKO333vuY8m225m_9yZGOT9OPfcc84D943UTfxGr_G7K6Xr3TVeNW7D2M8avT_3DVk-YAoDNF4vNB8e2tnWjyXGlm7mPzfurVPpp5JgGmeZtwkf5PtFupCxLzVvHa832rl2lElX-s2Xm2DZFNqp_hs-rZetd4v07ORpT3qmQHu7MF2td0BZp8k6z_xkvfXP902_pZ2_1_aYWEiqm0kN8I4r79qbdZ6umnq3q_2iNf22F4dE6qt2oimwdpim_uY6XMm7Fuo-IQT_iTD_CeGTHwZ38ieIJUFQRxirR1Xf39Dw0X5z0I72Af4tD61vvPNwWKQnqmfPTbduhsEd2J3vO_oBd3dc6fF2X7umNdWGf0vBRhSS6qoV7cCXfTXWfKmvWG61_si_vfU92Wz-E4RhsLhNIYinsox9QKGVd8-tuACCeKXRX12P-T_eKf7fhTq0Hvt-f3ailtSeoxJHRo1-58NoPe1UiBc1hkL8Yeh45y_vQ3mcuNl9T8s3cXPRWLnS7YWJG_gn2Tb4tUjid8jua-PVl08j_ab8I14mH8Llx0s5Tz5Err4ql52r_GYg0mVy1bEGZuD0ze64b5TWYFiM-16wSuJ4JT5vfVpDcztrcG_YkRU4s6HxufzDWF4XuVeJ1P10IbzBemt3Vp1V2e04ZXfrJd7Wicyd039brRIv_RIVu_nXi7X1cfL2sy66ztToUp1TO7qJ7NlwZ0f30pld5qNSVE5o6PbMojFHjgZB7oSicPpGteyLclQap7SvY0dXtM_LR1NT2JFHey3aaxa0VxCeYJ7RMHemoiCcgPZV9pR7o7kgcOjeGliYk9hjDZx8FAq6enwlTPSZj_vYPw9Il64dXdIY8ZmapzwfEd8-1ZyaxWhqkIZOibXUd-6Upqi1pD4uMicCV1GA_7zi73UN8BaF4sC8peJtMjfmjbHZBFwq5ov50qRaE0l96NZggnW4KqypYRAW-uhSz9ADvklwJF2J-5W0Z5fQPBhDX92R6I_0IFxRgDftge4l4dP-gH1hjD7uqU6fsOEZ9UNrCdPB-nys6uXgY6O3ZMd9sy5T9PghqrWHdjo4jB51CgLiKJaDYYA-7WgYONf1FbjkI-mE3EAfUY_rijfuJ_CVPaR50oe9JF7Q0pI8Dw3osxxYHdYPGbp2CnwHF8KvwJv2wEv0Z3ilQI6U9uwbZxbYJXvEmjjQjjCHkvNLvNg3yhzXQd1olamsT4IRrZmX0MUDpwL7R8zzHj7pSh9hPHFSHjLezKqAST51uC5zmtQ87skDUaneLokT5RbXkPWSYz53Abgjc8_o4KFGUZ-Hgv2Z1l5OTYM9D-HfUD0L-EwxH5wRnIG61gS-khfgY1bq7IAP_DA4l5xRuh9xlm8yGjutc8t-wHtkhWv3hc7aqGwiK5KzgvM5xRkZYn193uEln-su55j1GaIv7oM4iPrsVHiG0Dx7TR9-1lBfqFdwfvSd5LNL5xyZVp5NoHFZ57FkfiF6vKs4k5zvIfrX5xX6MXmt0gM5MTu8DjnhukrHHzTRd3jm0dma0_f_x5cxP9f4jBdqHvmbq2fUjzqcKh2Cp-yWj9ntcHanXmBXxhu7Q--eyjhfNFpaV7zgz4nWEUb7zUOhpevjjf_gu_KZ99pxFlZ-T3sttkmYqrco_26q35v0Ewzv5EZPbnL_8BfduWGMnyyN3q0bZ_7hb_7KG_L4CQAA)): + +```svelte + + { + const context = canvas.getContext('2d'); + + $effect(() => { + context.fillStyle = color; + context.fillRect(0, 0, canvas.width, canvas.height); + }); + }} +> +``` + +> [!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. + +## 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. + +This allows you to create _wrapper components_ that augment elements ([demo](/playground/untitled#H4sIAAAAAAAAE3VUS3ObMBD-KxvajnFqsJM2PhA7TXrKob31FjITAbKtRkiMtDhJPfz3LiAMdpxhGJvdb1_fPnaeYjn3Iu-WIbJ04028lZDcetHDzsO3olbVApI74F1RhHbLJdayhFl-Sp5qhVwhufEWNjWiwJtYxSjyQhsEFEXxBiujcxg1_8O_dnQ9APwsEbVyiHDafjrvDZCgkiO4MLCEzxYZcn90z6XUZ6OxA61KlaIgV6i1pFC-sxjDrlbHaDiWRoGvdMbHsLzp5DES0mJnRxGaRBvcBHb7yFUTCQeunEWYcYtGv12TqgFUDbCK1WLaM6IWQhUlQiJUFm2ZLPly51xXMG0Rjoyd69C7UqqG2nu95QZyXvtvLVpri2-SN4hoLXXCZFfhQ8aQBU1VgdEaH_vSgyBZR_BpPp_vi0tY-rw2ulRZkGqpTQRbZvwa2BPgFC8bgbw31CbjJjAsE6WNYBZeGp7vtQXLMqHWnZx-5kM1TR5ycpkZXQR2wzL94l8Ur1C_3-g168SfQf1MyfRi3LW9fs77emJEw5QV9SREoLTq06tcczq7d6xEUcJX2vAhO1b843XK34e5unZEMBr15ekuKEusluWAF8lXhE2ZTP2r2RcIHJ-163FPKerCgYJLOB9i4GvNwviI5-gAQiFFBk3tBTOU3HFXEk0R8o86WvUD64aINhv5K3oRmpJXkw8uxMG6Hh6JY9X7OwGSqfUy9tDG3sHNoEi0d_d_fv9qndxRU0VClFqo3KVo3U655Hnt1PXB3Qra2Y2QGdEwgTAMCxopsoxOe6SD0gD8movDhT0LAnhqlE8gVCpLWnRoV7OJCkFAwEXitrYL1W7p7pbiE_P7XH6E_rihODm5s52XtiH9Ekaw0VgI9exadWL1uoEYjPtg2672k5szsxbKyWB2fdT0w5Y_0hcT8oXOlRetmLS8-g-6TLXXQgYAAA==)): + +```svelte + + + + + +``` + +```svelte + + + + + + +``` + +## Controlling when attachments re-run + +Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`): + +```js +// @errors: 7006 2304 2552 +function foo(bar) { + return (node) => { + veryExpensiveSetupWork(node); + update(node, bar); + }; +} +``` + +In the rare case that this is a problem (for example, if `foo` does expensive and unavoidable setup work) consider passing the data inside a function and reading it in a child effect: + +```js +// @errors: 7006 2304 2552 +function foo(+++getBar+++) { + return (node) => { + veryExpensiveSetupWork(node); + ++++ $effect(() => { + update(node, getBar()); + });+++ + } +} +``` + +## Creating attachments programmatically + +To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey). + +## Converting actions to attachments + +If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components. diff --git a/documentation/docs/03-template-syntax/09-@const.md b/documentation/docs/03-template-syntax/10-@const.md similarity index 100% rename from documentation/docs/03-template-syntax/09-@const.md rename to documentation/docs/03-template-syntax/10-@const.md diff --git a/documentation/docs/03-template-syntax/10-@debug.md b/documentation/docs/03-template-syntax/11-@debug.md similarity index 100% rename from documentation/docs/03-template-syntax/10-@debug.md rename to documentation/docs/03-template-syntax/11-@debug.md diff --git a/documentation/docs/03-template-syntax/11-bind.md b/documentation/docs/03-template-syntax/12-bind.md similarity index 80% rename from documentation/docs/03-template-syntax/11-bind.md rename to documentation/docs/03-template-syntax/12-bind.md index c23f3b5232..de57815687 100644 --- a/documentation/docs/03-template-syntax/11-bind.md +++ b/documentation/docs/03-template-syntax/12-bind.md @@ -4,7 +4,7 @@ title: bind: Data ordinarily flows down, from parent to child. The `bind:` directive allows data to flow the other way, from child to parent. -The general syntax is `bind:property={expression}`, where `expression` is an _lvalue_ (i.e. a variable or an object property). When the expression is an identifier with the same name as the property, we can omit the expression — in other words these are equivalent: +The general syntax is `bind:property={expression}`, where `expression` is an [_lvalue_](https://press.rebus.community/programmingfundamentals/chapter/lvalue-and-rvalue/) (i.e. a variable or an object property). When the expression is an identifier with the same name as the property, we can omit the expression — in other words these are equivalent: ```svelte @@ -117,28 +117,52 @@ Since 5.6.0, if an `` has a `defaultChecked` attribute and is part of a f ``` +## `` + +Checkboxes can be in an [indeterminate](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/indeterminate) state, independently of whether they are checked or unchecked: + +```svelte + + +
+ + + {#if indeterminate} + waiting... + {:else if checked} + checked + {:else} + unchecked + {/if} +
+``` + ## `` -Inputs that work together can use `bind:group`. +Inputs that work together can use `bind:group` ([demo](/playground/untitled#H4sIAAAAAAAAE62T32_TMBDH_5XDQkpbrct7SCMGEvCEECDxsO7BSW6L2c227EvbKOv_jp0f6jYhQKJv5_P3PvdL1wstH1Bk4hMSGdgbRzUssFaM9VJciFtF6EV23QvubNRFR_BPUVfWXvodEkdfKT3-zl8Zzag5YETuK6csF1u9ZUIGNo4VkYQNvPYsGRfJF5JKJ8s3QRJE6WoFb2Nq6K-ck13u2Sl9Vxxhlc6QUBIFnz9Brm9ifJ6esun81XoNd860FmtwslYGlLYte5AO4aHlVhJ1gIeKWq92COt1iMtJlkhFPkgh1rHZiiF6K6BUus4G5KafGznCTlIbVUMfQZUWMJh5OrL-C_qjMYSwb1DyiH7iOEuCb1ZpWTUjfHqcwC_GWDVY3ZfmME_SGttSmD9IHaYatvWHIc6xLyqad3mq6KuqcCwnWn9p8p-p71BqP2IH81zc9w2in-od7XORP7ayCpd5YCeXI_-p59mObPF9WmwGpx3nqS2Gzw8TO3zOaS5_GqUXyQUkS3h8hOSz0ZhMESHGc0c4Hm3MAn00t1wrb0l2GZRkqvt4sXwczm6Qh8vnUJzI2LV4vAkvqWgfehTZrSSPx19WiVfFfAQAAA==)): ```svelte + - - - + + + - - - - + + + + ``` > [!NOTE] `bind:group` only works if the inputs are in the same Svelte component. @@ -227,6 +251,7 @@ You can give the `` a default value by adding a `selected` attribute to ``` +## `window` and `document` + +To bind to properties of `window` and `document`, see [``](svelte-window) and [``](svelte-document). + ## Contenteditable bindings Elements with the `contenteditable` attribute support the following bindings: @@ -278,6 +307,10 @@ All visible elements have the following readonly bindings, measured with a `Resi - [`clientHeight`](https://developer.mozilla.org/en-US/docs/Web/API/Element/clientHeight) - [`offsetWidth`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth) - [`offsetHeight`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight) +- [`contentRect`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/contentRect) +- [`contentBoxSize`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/contentBoxSize) +- [`borderBoxSize`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/borderBoxSize) +- [`devicePixelContentBoxSize`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/devicePixelContentBoxSize) ```svelte
@@ -285,7 +318,7 @@ All visible elements have the following readonly bindings, measured with a `Resi
``` -> [!NOTE] `display: inline` elements do not have a width or height (except for elements with 'intrinsic' dimensions, like `` and ``), and cannot be observed with a `ResizeObserver`. You will need to change the `display` style of these elements to something else, such as `inline-block`. +> [!NOTE] `display: inline` elements do not have a width or height (except for elements with 'intrinsic' dimensions, like `` and ``), and cannot be observed with a `ResizeObserver`. You will need to change the `display` style of these elements to something else, such as `inline-block`. Note that CSS transformations do not trigger `ResizeObserver` callbacks. ## bind:this diff --git a/documentation/docs/03-template-syntax/12-use.md b/documentation/docs/03-template-syntax/13-use.md similarity index 93% rename from documentation/docs/03-template-syntax/12-use.md rename to documentation/docs/03-template-syntax/13-use.md index 45de023578..5f5321a1c0 100644 --- a/documentation/docs/03-template-syntax/12-use.md +++ b/documentation/docs/03-template-syntax/13-use.md @@ -2,6 +2,9 @@ title: use: --- +> [!NOTE] +> In Svelte 5.29 and newer, consider using [attachments](@attach) instead, as they are more flexible and composable. + Actions are functions that are called when an element is mounted. They are added with the `use:` directive, and will typically use an `$effect` so that they can reset any state when the element is unmounted: ```svelte diff --git a/documentation/docs/03-template-syntax/13-transition.md b/documentation/docs/03-template-syntax/14-transition.md similarity index 100% rename from documentation/docs/03-template-syntax/13-transition.md rename to documentation/docs/03-template-syntax/14-transition.md diff --git a/documentation/docs/03-template-syntax/14-in-and-out.md b/documentation/docs/03-template-syntax/15-in-and-out.md similarity index 100% rename from documentation/docs/03-template-syntax/14-in-and-out.md rename to documentation/docs/03-template-syntax/15-in-and-out.md diff --git a/documentation/docs/03-template-syntax/15-animate.md b/documentation/docs/03-template-syntax/16-animate.md similarity index 100% rename from documentation/docs/03-template-syntax/15-animate.md rename to documentation/docs/03-template-syntax/16-animate.md diff --git a/documentation/docs/03-template-syntax/17-style.md b/documentation/docs/03-template-syntax/17-style.md index 749376c6e2..aa61cdcde3 100644 --- a/documentation/docs/03-template-syntax/17-style.md +++ b/documentation/docs/03-template-syntax/17-style.md @@ -34,8 +34,10 @@ To mark a style as important, use the `|important` modifier:
...
``` -When `style:` directives are combined with `style` attributes, the directives will take precedence: +When `style:` directives are combined with `style` attributes, the directives will take precedence, +even over `!important` properties: ```svelte -
This will be red
+
This will be red
+
This will still be red
``` diff --git a/documentation/docs/03-template-syntax/19-await-expressions.md b/documentation/docs/03-template-syntax/19-await-expressions.md new file mode 100644 index 0000000000..4e5ec28b26 --- /dev/null +++ b/documentation/docs/03-template-syntax/19-await-expressions.md @@ -0,0 +1,144 @@ +--- +title: await +--- + +As of Svelte 5.36, you can use the `await` keyword inside your components in three places where it was previously unavailable: + +- at the top level of your component's ` + + + + +

{a} + {b} = {await add(a, b)}

+``` + +...if you increment `a`, the contents of the `

` will _not_ immediately update to read this — + +```html +

2 + 2 = 3

+``` + +— instead, the text will update to `2 + 2 = 4` when `add(a, b)` resolves. + +Updates can overlap — a fast update will be reflected in the UI while an earlier slow update is still ongoing. + +## Concurrency + +Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup... + +```svelte +

{await one()}

+

{await two()}

+``` + +...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential. + +This does not apply to sequential `await` expressions inside your ` + + + + (reset = r)}> + + + {#snippet failed(e)} +

oops! {e.message}

+ {/snippet} +
+``` + ### transition_slide_display ``` diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 6196a85ade..20f57770d1 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -274,6 +274,12 @@ A `:global` selector cannot modify an existing selector A `:global` selector can only be modified if it is a descendant of other selectors ``` +### css_global_block_invalid_placement + +``` +A `:global` selector cannot be inside a pseudoclass +``` + ### css_global_invalid_placement ``` @@ -474,6 +480,12 @@ Expected token %token% Expected whitespace ``` +### experimental_async + +``` +Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true` +``` + ### export_undefined ``` @@ -528,6 +540,12 @@ The arguments keyword cannot be used within the template or at the top level of %message% ``` +### legacy_await_invalid + +``` +Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode +``` + ### legacy_export_invalid ``` @@ -840,6 +858,38 @@ Cannot reassign or bind to snippet parameter This snippet is shadowing the prop `%prop%` with the same name ``` +### state_field_duplicate + +``` +`%name%` has already been declared on this class +``` + +An assignment to a class field that uses a `$state` or `$derived` rune is considered a _state field declaration_. The declaration can happen in the class body... + +```js +class Counter { + count = $state(0); +} +``` + +...or inside the constructor... + +```js +class Counter { + constructor() { + this.count = $state(0); + } +} +``` + +...but it can only happen once. + +### state_field_invalid_assignment + +``` +Cannot assign to a state field before its declaration +``` + ### state_invalid_export ``` @@ -849,7 +899,7 @@ Cannot export state from a module if it is reassigned. Either export a function ### state_invalid_placement ``` -`%rune%(...)` can only be used as a variable declaration initializer or a class field +`%rune%(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor. ``` ### store_invalid_scoped_subscription diff --git a/documentation/docs/98-reference/.generated/compile-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md index 0e94cbadb2..2af9021a6a 100644 --- a/documentation/docs/98-reference/.generated/compile-warnings.md +++ b/documentation/docs/98-reference/.generated/compile-warnings.md @@ -586,6 +586,14 @@ Attributes should not contain ':' characters to prevent ambiguity with Svelte di Quoted attributes on components and custom elements will be stringified in a future version of Svelte. If this isn't what you want, remove the quotes ``` +### bidirectional_control_characters + +``` +A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences +``` + +Bidirectional control characters can alter the direction in which text appears to be in. For example, via control characters, you can make `defabc` look like `abcdef`. As a result, if you were to unknowingly copy and paste some code that has these control characters, they may alter the behavior of your code in ways you did not intend. See [trojansource.codes](https://trojansource.codes/) for more information. + ### bind_invalid_each_rest ``` @@ -624,6 +632,31 @@ In some situations a selector may target an element that is not 'visible' to the ``` +### custom_element_props_identifier + +``` +Using a rest element or a non-destructured declaration with `$props()` means that Svelte can't infer what properties to expose when creating a custom element. Consider destructuring all the props or explicitly specifying the `customElement.props` option. +``` + +### element_implicitly_closed + +``` +This element is implicitly closed by the following `%tag%`, which can cause an unexpected DOM structure. Add an explicit `%closing%` to avoid surprises. +``` + +In HTML, some elements are implicitly closed by another element. For example, you cannot nest a `

` inside another `

`: + +```html + +

hello

+ + +

+

hello

+``` + +Similarly, a parent element's closing tag will implicitly close all child elements, even if the ` +### await_outside_boundary + +``` +Cannot await outside a `` with a `pending` snippet +``` + +The `await` keyword can only appear in a `$derived(...)` or template expression, or at the top level of a component's ` + + + + (reset = r)}> + + + {#snippet failed(e)} +

oops! {e.message}

+ {/snippet} +
+``` + ## transition_slide_display > The `slide` transition does not work correctly for elements with `display: %value%` diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index aabcbeae48..2b0c5eafdf 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -70,6 +70,10 @@ This turned out to be buggy and unpredictable, particularly when working with de > `$effect()` can only be used as an expression statement +## experimental_async + +> Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true` + ## export_undefined > `%name%` is not defined @@ -98,6 +102,10 @@ This turned out to be buggy and unpredictable, particularly when working with de > The arguments keyword cannot be used within the template or at the top level of a component +## legacy_await_invalid + +> Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode + ## legacy_export_invalid > Cannot use `export let` in runes mode — use `$props()` instead @@ -212,13 +220,41 @@ It's possible to export a snippet from a ` * diff --git a/packages/svelte/src/attachments/index.js b/packages/svelte/src/attachments/index.js new file mode 100644 index 0000000000..b9fde9b6d9 --- /dev/null +++ b/packages/svelte/src/attachments/index.js @@ -0,0 +1,113 @@ +/** @import { Action, ActionReturn } from '../action/public' */ +/** @import { Attachment } from './public' */ +import { noop, render_effect } from 'svelte/internal/client'; +import { ATTACHMENT_KEY } from '../constants.js'; +import { untrack } from '../index-client.js'; +import { teardown } from '../internal/client/reactivity/effects.js'; + +/** + * Creates an object key that will be recognised as an attachment when the object is spread onto an element, + * as a programmatic alternative to using `{@attach ...}`. This can be useful for library authors, though + * is generally not needed when building an app. + * + * ```svelte + * + * + * + * ``` + * @since 5.29 + */ +export function createAttachmentKey() { + return Symbol(ATTACHMENT_KEY); +} + +/** + * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior. + * It's useful if you want to start using attachments on components but you have actions provided by a library. + * + * Note that the second argument, if provided, must be a function that _returns_ the argument to the + * action function, not the argument itself. + * + * ```svelte + * + *
...
+ * + * + *
bar)}>...
+ * ``` + * @template {EventTarget} E + * @template {unknown} T + * @overload + * @param {Action | ((element: E, arg: T) => void | ActionReturn)} action The action function + * @param {() => T} fn A function that returns the argument for the action + * @returns {Attachment} + */ +/** + * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior. + * It's useful if you want to start using attachments on components but you have actions provided by a library. + * + * Note that the second argument, if provided, must be a function that _returns_ the argument to the + * action function, not the argument itself. + * + * ```svelte + * + *
...
+ * + * + *
bar)}>...
+ * ``` + * @template {EventTarget} E + * @overload + * @param {Action | ((element: E) => void | ActionReturn)} action The action function + * @returns {Attachment} + */ +/** + * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior. + * It's useful if you want to start using attachments on components but you have actions provided by a library. + * + * Note that the second argument, if provided, must be a function that _returns_ the argument to the + * action function, not the argument itself. + * + * ```svelte + * + *
...
+ * + * + *
bar)}>...
+ * ``` + * + * @template {EventTarget} E + * @template {unknown} T + * @param {Action | ((element: E, arg: T) => void | ActionReturn)} action The action function + * @param {() => T} fn A function that returns the argument for the action + * @returns {Attachment} + * @since 5.32 + */ +export function fromAction(action, fn = /** @type {() => T} */ (noop)) { + return (element) => { + const { update, destroy } = untrack(() => action(element, fn()) ?? {}); + + if (update) { + var ran = false; + render_effect(() => { + const arg = fn(); + if (ran) update(arg); + }); + ran = true; + } + + if (destroy) { + teardown(destroy); + } + }; +} diff --git a/packages/svelte/src/attachments/public.d.ts b/packages/svelte/src/attachments/public.d.ts new file mode 100644 index 0000000000..caf1342d0a --- /dev/null +++ b/packages/svelte/src/attachments/public.d.ts @@ -0,0 +1,12 @@ +/** + * An [attachment](https://svelte.dev/docs/svelte/@attach) is a function that runs when an element is mounted + * to the DOM, and optionally returns a function that is called when the element is later removed. + * + * It can be attached to an element with an `{@attach ...}` tag, or by spreading an object containing + * a property created with [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments#createAttachmentKey). + */ +export interface Attachment { + (element: T): void | (() => void); +} + +export * from './index.js'; diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index aa328764e1..599d3e8248 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -15,10 +15,12 @@ class InternalCompileError extends Error { constructor(code, message, position) { super(message); this.stack = ''; // avoid unnecessary noise; don't set it as a class property or it becomes enumerable + // We want to extend from Error so that various bundler plugins properly handle it. // But we also want to share the same object shape with that of warnings, therefore // we create an instance of the shared class an copy over its properties. this.#diagnostic = new CompileDiagnostic(code, message, position); + Object.assign(this, this.#diagnostic); this.name = 'CompileError'; } @@ -168,6 +170,15 @@ export function effect_invalid_placement(node) { e(node, 'effect_invalid_placement', `\`$effect()\` can only be used as an expression statement\nhttps://svelte.dev/e/effect_invalid_placement`); } +/** + * Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true` + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function experimental_async(node) { + e(node, 'experimental_async', `Cannot use \`await\` in deriveds and template expressions, or at the top level of a component, unless the \`experimental.async\` compiler option is \`true\`\nhttps://svelte.dev/e/experimental_async`); +} + /** * `%name%` is not defined * @param {null | number | NodeLike} node @@ -233,6 +244,15 @@ export function invalid_arguments_usage(node) { e(node, 'invalid_arguments_usage', `The arguments keyword cannot be used within the template or at the top level of a component\nhttps://svelte.dev/e/invalid_arguments_usage`); } +/** + * Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function legacy_await_invalid(node) { + e(node, 'legacy_await_invalid', `Cannot use \`await\` in deriveds and template expressions, or at the top level of a component, unless in runes mode\nhttps://svelte.dev/e/legacy_await_invalid`); +} + /** * Cannot use `export let` in runes mode — use `$props()` instead * @param {null | number | NodeLike} node @@ -461,6 +481,25 @@ export function snippet_parameter_assignment(node) { e(node, 'snippet_parameter_assignment', `Cannot reassign or bind to snippet parameter\nhttps://svelte.dev/e/snippet_parameter_assignment`); } +/** + * `%name%` has already been declared on this class + * @param {null | number | NodeLike} node + * @param {string} name + * @returns {never} + */ +export function state_field_duplicate(node, name) { + e(node, 'state_field_duplicate', `\`${name}\` has already been declared on this class\nhttps://svelte.dev/e/state_field_duplicate`); +} + +/** + * Cannot assign to a state field before its declaration + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function state_field_invalid_assignment(node) { + e(node, 'state_field_invalid_assignment', `Cannot assign to a state field before its declaration\nhttps://svelte.dev/e/state_field_invalid_assignment`); +} + /** * Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties * @param {null | number | NodeLike} node @@ -471,13 +510,13 @@ export function state_invalid_export(node) { } /** - * `%rune%(...)` can only be used as a variable declaration initializer or a class field + * `%rune%(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor. * @param {null | number | NodeLike} node * @param {string} rune * @returns {never} */ export function state_invalid_placement(node, rune) { - e(node, 'state_invalid_placement', `\`${rune}(...)\` can only be used as a variable declaration initializer or a class field\nhttps://svelte.dev/e/state_invalid_placement`); + e(node, 'state_invalid_placement', `\`${rune}(...)\` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.\nhttps://svelte.dev/e/state_invalid_placement`); } /** @@ -581,6 +620,15 @@ export function css_global_block_invalid_modifier_start(node) { e(node, 'css_global_block_invalid_modifier_start', `A \`:global\` selector can only be modified if it is a descendant of other selectors\nhttps://svelte.dev/e/css_global_block_invalid_modifier_start`); } +/** + * A `:global` selector cannot be inside a pseudoclass + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function css_global_block_invalid_placement(node) { + e(node, 'css_global_block_invalid_placement', `A \`:global\` selector cannot be inside a pseudoclass\nhttps://svelte.dev/e/css_global_block_invalid_placement`); +} + /** * `:global(...)` can be at the start or end of a selector sequence, but not in the middle * @param {null | number | NodeLike} node @@ -788,7 +836,9 @@ export function bind_invalid_expression(node) { * @returns {never} */ export function bind_invalid_name(node, name, explanation) { - e(node, 'bind_invalid_name', `${explanation ? `\`bind:${name}\` is not a valid binding. ${explanation}` : `\`bind:${name}\` is not a valid binding`}\nhttps://svelte.dev/e/bind_invalid_name`); + e(node, 'bind_invalid_name', `${explanation + ? `\`bind:${name}\` is not a valid binding. ${explanation}` + : `\`bind:${name}\` is not a valid binding`}\nhttps://svelte.dev/e/bind_invalid_name`); } /** diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js index 42427dd9c4..a378af34ee 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -3,7 +3,6 @@ /** @import { AST } from './public.js' */ import { walk as zimmerframe_walk } from 'zimmerframe'; import { convert } from './legacy.js'; -import { parse as parse_acorn } from './phases/1-parse/acorn.js'; import { parse as _parse } from './phases/1-parse/index.js'; import { remove_typescript_nodes } from './phases/1-parse/remove_typescript_nodes.js'; import { analyze_component, analyze_module } from './phases/2-analyze/index.js'; @@ -21,9 +20,8 @@ export { default as preprocess } from './preprocess/index.js'; */ export function compile(source, options) { source = remove_bom(source); - state.reset_warning_filter(options.warningFilter); + state.reset({ warning: options.warningFilter, filename: options.filename }); const validated = validate_component_options(options, ''); - state.reset(source, validated); let parsed = _parse(source); @@ -43,6 +41,11 @@ export function compile(source, options) { instance: parsed.instance && remove_typescript_nodes(parsed.instance), module: parsed.module && remove_typescript_nodes(parsed.module) }; + if (combined_options.customElementOptions?.extend) { + combined_options.customElementOptions.extend = remove_typescript_nodes( + combined_options.customElementOptions?.extend + ); + } } const analysis = analyze_component(parsed, source, combined_options); @@ -60,11 +63,10 @@ export function compile(source, options) { */ export function compileModule(source, options) { source = remove_bom(source); - state.reset_warning_filter(options.warningFilter); + state.reset({ warning: options.warningFilter, filename: options.filename }); const validated = validate_module_options(options, ''); - state.reset(source, validated); - const analysis = analyze_module(parse_acorn(source, false), validated); + const analysis = analyze_module(source, validated); return transform_module(analysis, source, validated); } @@ -92,6 +94,7 @@ export function compileModule(source, options) { * @returns {Record} */ +// TODO 6.0 remove unused `filename` /** * The parse function parses a component, returning only its abstract syntax tree. * @@ -100,14 +103,15 @@ export function compileModule(source, options) { * * The `loose` option, available since 5.13.0, tries to always return an AST even if the input will not successfully compile. * + * The `filename` option is unused and will be removed in Svelte 6.0. + * * @param {string} source * @param {{ filename?: string; rootDir?: string; modern?: boolean; loose?: boolean }} [options] * @returns {AST.Root | LegacyRoot} */ -export function parse(source, { filename, rootDir, modern, loose } = {}) { +export function parse(source, { modern, loose } = {}) { source = remove_bom(source); - state.reset_warning_filter(() => false); - state.reset(source, { filename: filename ?? '(unknown)', rootDir }); + state.reset({ warning: () => false, filename: undefined }); const ast = _parse(source, loose); return to_public_ast(source, ast, modern); diff --git a/packages/svelte/src/compiler/legacy.js b/packages/svelte/src/compiler/legacy.js index e3f88c8f1d..85345bca4a 100644 --- a/packages/svelte/src/compiler/legacy.js +++ b/packages/svelte/src/compiler/legacy.js @@ -378,7 +378,8 @@ export function convert(source, ast) { end: node.end, expression: node.expression, parameters: node.parameters, - children: node.body.nodes.map((child) => visit(child)) + children: node.body.nodes.map((child) => visit(child)), + typeParams: node.typeParams }; }, // @ts-expect-error @@ -450,6 +451,7 @@ export function convert(source, ast) { SpreadAttribute(node) { return { ...node, type: 'Spread' }; }, + // @ts-ignore StyleSheet(node, context) { return { ...node, diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 75a9a64905..6b2e6cda70 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -9,7 +9,7 @@ import { parse } from '../phases/1-parse/index.js'; import { regex_valid_component_name } from '../phases/1-parse/state/element.js'; import { analyze_component } from '../phases/2-analyze/index.js'; import { get_rune } from '../phases/scope.js'; -import { reset, reset_warning_filter } from '../state.js'; +import { reset, UNKNOWN_FILENAME } from '../state.js'; import { extract_identifiers, extract_all_identifiers_from_expression, @@ -134,8 +134,7 @@ export function migrate(source, { filename, use_ts } = {}) { return start + style_placeholder + end; }); - reset_warning_filter(() => false); - reset(source, { filename: filename ?? '(unknown)' }); + reset({ warning: () => false, filename }); let parsed = parse(source); @@ -146,7 +145,10 @@ export function migrate(source, { filename, use_ts } = {}) { ...validate_component_options({}, ''), ...parsed_options, customElementOptions, - filename: filename ?? '(unknown)' + filename: filename ?? UNKNOWN_FILENAME, + experimental: { + async: true + } }; const str = new MagicString(source); @@ -603,15 +605,15 @@ const instance_script = { ); // Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = .. // means that foo and bar are the props (i.e. the leafs are the prop names), not x and z. - // const tmp = state.scope.generate('tmp'); - // const paths = extract_paths(declarator.id); + // const tmp = b.id(state.scope.generate('tmp')); + // const paths = extract_paths(declarator.id, tmp); // state.props_pre.push( - // b.declaration('const', b.id(tmp), visit(declarator.init!) as Expression) + // b.declaration('const', tmp, visit(declarator.init!) as Expression) // ); // for (const path of paths) { // const name = (path.node as Identifier).name; // const binding = state.scope.get(name)!; - // const value = path.expression!(b.id(tmp)); + // const value = path.expression; // if (binding.kind === 'bindable_prop' || binding.kind === 'rest_prop') { // state.props.push({ // local: name, @@ -1307,7 +1309,7 @@ const template = { name = state.scope.generate(slot_name); if (name !== slot_name) { throw new MigrationError( - 'This migration would change the name of a slot making the component unusable' + `This migration would change the name of a slot (${slot_name} to ${name}) making the component unusable` ); } } @@ -1880,7 +1882,7 @@ function handle_identifier(node, state, path) { let new_name = state.scope.generate(name); if (new_name !== name) { throw new MigrationError( - 'This migration would change the name of a slot making the component unusable' + `This migration would change the name of a slot (${name} to ${new_name}) making the component unusable` ); } } diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index 36f7688c49..77ce4a461c 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -1,18 +1,32 @@ /** @import { Comment, Program } from 'estree' */ +/** @import { AST } from '#compiler' */ import * as acorn from 'acorn'; import { walk } from 'zimmerframe'; import { tsPlugin } from '@sveltejs/acorn-typescript'; const ParserWithTS = acorn.Parser.extend(tsPlugin()); +/** + * @typedef {Comment & { + * start: number; + * end: number; + * }} CommentWithLocation + */ + /** * @param {string} source + * @param {AST.JSComment[]} comments * @param {boolean} typescript * @param {boolean} [is_script] */ -export function parse(source, typescript, is_script) { +export function parse(source, comments, typescript, is_script) { const parser = typescript ? ParserWithTS : acorn.Parser; - const { onComment, add_comments } = get_comment_handlers(source); + + const { onComment, add_comments } = get_comment_handlers( + source, + /** @type {CommentWithLocation[]} */ (comments) + ); + // @ts-ignore const parse_statement = parser.prototype.parseStatement; @@ -36,7 +50,7 @@ export function parse(source, typescript, is_script) { ast = parser.parse(source, { onComment, sourceType: 'module', - ecmaVersion: 13, + ecmaVersion: 16, locations: true }); } finally { @@ -53,18 +67,24 @@ export function parse(source, typescript, is_script) { /** * @param {string} source + * @param {Comment[]} comments * @param {boolean} typescript * @param {number} index * @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} */ -export function parse_expression_at(source, typescript, index) { +export function parse_expression_at(source, comments, typescript, index) { const parser = typescript ? ParserWithTS : acorn.Parser; - const { onComment, add_comments } = get_comment_handlers(source); + + const { onComment, add_comments } = get_comment_handlers( + source, + /** @type {CommentWithLocation[]} */ (comments), + index + ); const ast = parser.parseExpressionAt(source, index, { onComment, sourceType: 'module', - ecmaVersion: 13, + ecmaVersion: 16, locations: true }); @@ -78,26 +98,20 @@ export function parse_expression_at(source, typescript, index) { * to add them after the fact. They are needed in order to support `svelte-ignore` comments * in JS code and so that `prettier-plugin-svelte` doesn't remove all comments when formatting. * @param {string} source + * @param {CommentWithLocation[]} comments + * @param {number} index */ -function get_comment_handlers(source) { - /** - * @typedef {Comment & { - * start: number; - * end: number; - * }} CommentWithLocation - */ - - /** @type {CommentWithLocation[]} */ - const comments = []; - +function get_comment_handlers(source, comments, index = 0) { return { /** * @param {boolean} block * @param {string} value * @param {number} start * @param {number} end + * @param {import('acorn').Position} [start_loc] + * @param {import('acorn').Position} [end_loc] */ - onComment: (block, value, start, end) => { + onComment: (block, value, start, end, start_loc, end_loc) => { if (block && /\n/.test(value)) { let a = start; while (a > 0 && source[a - 1] !== '\n') a -= 1; @@ -109,13 +123,26 @@ function get_comment_handlers(source) { value = value.replace(new RegExp(`^${indentation}`, 'gm'), ''); } - comments.push({ type: block ? 'Block' : 'Line', value, start, end }); + comments.push({ + type: block ? 'Block' : 'Line', + value, + start, + end, + loc: { + start: /** @type {import('acorn').Position} */ (start_loc), + end: /** @type {import('acorn').Position} */ (end_loc) + } + }); }, /** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */ add_comments(ast) { if (comments.length === 0) return; + comments = comments + .filter((comment) => comment.start >= index) + .map(({ type, value, start, end }) => ({ type, value, start, end })); + walk(ast, null, { _(node, { next, path }) { let comment; diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index 6cc5b58aa6..77cc2bf3fa 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -1,4 +1,5 @@ /** @import { AST } from '#compiler' */ +/** @import { Comment } from 'estree' */ // @ts-expect-error acorn type definitions are borked in the release we use import { isIdentifierStart, isIdentifierChar } from 'acorn'; import fragment from './state/fragment.js'; @@ -8,6 +9,7 @@ import { create_fragment } from './utils/create.js'; import read_options from './read/options.js'; import { is_reserved } from '../../../utils.js'; import { disallow_children } from '../2-analyze/visitors/shared/special-element.js'; +import * as state from '../../state.js'; const regex_position_indicator = / \(\d+:\d+\)$/; @@ -87,6 +89,7 @@ export class Parser { type: 'Root', fragment: create_fragment(), options: null, + comments: [], metadata: { ts: this.ts } @@ -299,6 +302,8 @@ export class Parser { * @returns {AST.Root} */ export function parse(template, loose = false) { + state.set_source(template); + const parser = new Parser(template, loose); return parser.root; } diff --git a/packages/svelte/src/compiler/phases/1-parse/read/context.js b/packages/svelte/src/compiler/phases/1-parse/read/context.js index f4c73dcf40..282288e2a2 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/context.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js @@ -1,7 +1,7 @@ /** @import { Location } from 'locate-character' */ /** @import { Pattern } from 'estree' */ /** @import { Parser } from '../index.js' */ -import { is_bracket_open, is_bracket_close, get_bracket_close } from '../utils/bracket.js'; +import { match_bracket } from '../utils/bracket.js'; import { parse_expression_at } from '../acorn.js'; import { regex_not_newline_characters } from '../../patterns.js'; import * as e from '../../../errors.js'; @@ -33,7 +33,9 @@ export default function read_pattern(parser) { }; } - if (!is_bracket_open(parser.template[i])) { + const char = parser.template[i]; + + if (char !== '{' && char !== '[') { e.expected_pattern(i); } @@ -57,7 +59,12 @@ export default function read_pattern(parser) { space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1); const expression = /** @type {any} */ ( - parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, parser.ts, start - 1) + parse_expression_at( + `${space_with_newline}(${pattern_string} = 1)`, + parser.root.comments, + parser.ts, + start - 1 + ) ).left; expression.typeAnnotation = read_type_annotation(parser); @@ -71,75 +78,6 @@ export default function read_pattern(parser) { } } -/** - * @param {Parser} parser - * @param {number} start - */ -function match_bracket(parser, start) { - const bracket_stack = []; - - let i = start; - - while (i < parser.template.length) { - let char = parser.template[i++]; - - if (char === "'" || char === '"' || char === '`') { - i = match_quote(parser, i, char); - continue; - } - - if (is_bracket_open(char)) { - bracket_stack.push(char); - } else if (is_bracket_close(char)) { - const popped = /** @type {string} */ (bracket_stack.pop()); - const expected = /** @type {string} */ (get_bracket_close(popped)); - - if (char !== expected) { - e.expected_token(i - 1, expected); - } - - if (bracket_stack.length === 0) { - return i; - } - } - } - - e.unexpected_eof(parser.template.length); -} - -/** - * @param {Parser} parser - * @param {number} start - * @param {string} quote - */ -function match_quote(parser, start, quote) { - let is_escaped = false; - let i = start; - - while (i < parser.template.length) { - const char = parser.template[i++]; - - if (is_escaped) { - is_escaped = false; - continue; - } - - if (char === quote) { - return i; - } - - if (char === '\\') { - is_escaped = true; - } - - if (quote === '`' && char === '$' && parser.template[i] === '{') { - i = match_bracket(parser, i); - } - } - - e.unterminated_string_constant(start); -} - /** * @param {Parser} parser * @returns {any} @@ -163,13 +101,13 @@ function read_type_annotation(parser) { // parameters as part of a sequence expression instead, and will then error on optional // parameters (`?:`). Therefore replace that sequence with something that will not error. parser.template.slice(parser.index).replace(/\?\s*:/g, ':'); - let expression = parse_expression_at(template, parser.ts, a); + let expression = parse_expression_at(template, parser.root.comments, parser.ts, a); // `foo: bar = baz` gets mangled — fix it if (expression.type === 'AssignmentExpression') { let b = expression.right.start; while (template[b] !== '=') b -= 1; - expression = parse_expression_at(template.slice(0, b), parser.ts, a); + expression = parse_expression_at(template.slice(0, b), parser.root.comments, parser.ts, a); } // `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that diff --git a/packages/svelte/src/compiler/phases/1-parse/read/expression.js b/packages/svelte/src/compiler/phases/1-parse/read/expression.js index a596cdf572..5d21f85792 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/expression.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/expression.js @@ -34,12 +34,24 @@ export function get_loose_identifier(parser, opening_token) { */ export default function read_expression(parser, opening_token, disallow_loose) { try { - const node = parse_expression_at(parser.template, parser.ts, parser.index); + let comment_index = parser.root.comments.length; + + const node = parse_expression_at( + parser.template, + parser.root.comments, + parser.ts, + parser.index + ); let num_parens = 0; - if (node.leadingComments !== undefined && node.leadingComments.length > 0) { - parser.index = node.leadingComments.at(-1).end; + let i = parser.root.comments.length; + while (i-- > comment_index) { + const comment = parser.root.comments[i]; + if (comment.end < node.start) { + parser.index = comment.end; + break; + } } for (let i = parser.index; i < /** @type {number} */ (node.start); i += 1) { @@ -47,9 +59,9 @@ export default function read_expression(parser, opening_token, disallow_loose) { } let index = /** @type {number} */ (node.end); - if (node.trailingComments !== undefined && node.trailingComments.length > 0) { - index = node.trailingComments.at(-1).end; - } + + const last_comment = parser.root.comments.at(-1); + if (last_comment && last_comment.end > index) index = last_comment.end; while (num_parens > 0) { const char = parser.template[index]; diff --git a/packages/svelte/src/compiler/phases/1-parse/read/script.js b/packages/svelte/src/compiler/phases/1-parse/read/script.js index 9d9ed3a1ef..9ce449f200 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/script.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/script.js @@ -16,7 +16,7 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module']; /** * @param {Parser} parser * @param {number} start - * @param {Array} attributes + * @param {Array} attributes * @returns {AST.Script} */ export function read_script(parser, start, attributes) { @@ -34,7 +34,7 @@ export function read_script(parser, start, attributes) { let ast; try { - ast = acorn.parse(source, parser.ts, true); + ast = acorn.parse(source, parser.root.comments, parser.ts, true); } catch (err) { parser.acorn_error(err); } diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 56dbe124b7..80ab234d92 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -12,13 +12,14 @@ const REGEX_NTH_OF = const REGEX_WHITESPACE_OR_COLON = /[\s:]/; const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/; const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/; +const REGEX_UNICODE_SEQUENCE = /^\\[0-9a-fA-F]{1,6}(\r\n|\s)?/; const REGEX_COMMENT_CLOSE = /\*\//; const REGEX_HTML_COMMENT_CLOSE = /-->/; /** * @param {Parser} parser * @param {number} start - * @param {Array} attributes + * @param {Array} attributes * @returns {AST.CSS.StyleSheet} */ export default function read_style(parser, start, attributes) { @@ -580,25 +581,26 @@ function read_identifier(parser) { e.css_expected_identifier(start); } - let escaped = false; - while (parser.index < parser.template.length) { const char = parser.template[parser.index]; - if (escaped) { - identifier += '\\' + char; - escaped = false; - } else if (char === '\\') { - escaped = true; + if (char === '\\') { + const sequence = parser.match_regex(REGEX_UNICODE_SEQUENCE); + if (sequence) { + identifier += String.fromCodePoint(parseInt(sequence.slice(1), 16)); + parser.index += sequence.length; + } else { + identifier += '\\' + parser.template[parser.index + 1]; + parser.index += 2; + } } else if ( /** @type {number} */ (char.codePointAt(0)) >= 160 || REGEX_VALID_IDENTIFIER_CHAR.test(char) ) { identifier += char; + parser.index++; } else { break; } - - parser.index++; } if (identifier === '') { diff --git a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js index aba94ee20d..cb498c3c13 100644 --- a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js +++ b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js @@ -115,6 +115,19 @@ const visitors = { TSDeclareFunction() { return b.empty; }, + ClassBody(node, context) { + const body = []; + for (const _child of node.body) { + const child = context.visit(_child); + if (child.type !== 'PropertyDefinition' || !child.declare) { + body.push(child); + } + } + return { + ...node, + body + }; + }, ClassDeclaration(node, context) { if (node.declare) { return b.empty; diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 66946a8f8d..87332f647d 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -93,7 +93,16 @@ export default function element(parser) { } } - if (parent.type !== 'RegularElement' && !parser.loose) { + if (parent.type === 'RegularElement') { + if (!parser.last_auto_closed_tag || parser.last_auto_closed_tag.tag !== name) { + const end = parent.fragment.nodes[0]?.start ?? start; + w.element_implicitly_closed( + { start: parent.start, end }, + ``, + `` + ); + } + } else if (!parser.loose) { if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) { e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason); } else { @@ -186,6 +195,8 @@ export default function element(parser) { parser.allow_whitespace(); if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) { + const end = parent.fragment.nodes[0]?.start ?? start; + w.element_implicitly_closed({ start: parent.start, end }, `<${name}>`, ``); parent.end = start; parser.pop(); parser.last_auto_closed_tag = { @@ -284,6 +295,8 @@ export default function element(parser) { } else { element.tag = get_attribute_expression(definition); } + + element.metadata.expression = create_expression_metadata(); } if (is_top_level_script_or_style) { @@ -480,7 +493,7 @@ function read_static_attribute(parser) { /** * @param {Parser} parser - * @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | null} + * @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null} */ function read_attribute(parser) { const start = parser.index; @@ -488,6 +501,27 @@ function read_attribute(parser) { if (parser.eat('{')) { parser.allow_whitespace(); + if (parser.eat('@attach')) { + parser.require_whitespace(); + + const expression = read_expression(parser); + parser.allow_whitespace(); + parser.eat('}', true); + + /** @type {AST.AttachTag} */ + const attachment = { + type: 'AttachTag', + start, + end: parser.index, + expression, + metadata: { + expression: create_expression_metadata() + } + }; + + return attachment; + } + if (parser.eat('...')) { const expression = read_expression(parser); diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 0eb98c27e8..ba091ef7ec 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -8,9 +8,12 @@ import { parse_expression_at } from '../acorn.js'; import read_pattern from '../read/context.js'; import read_expression, { get_loose_identifier } from '../read/expression.js'; import { create_fragment } from '../utils/create.js'; +import { match_bracket } from '../utils/bracket.js'; const regex_whitespace_with_closing_curly_brace = /^\s*}/; +const pointy_bois = { '<': '>' }; + /** @param {Parser} parser */ export default function tag(parser) { const start = parser.index; @@ -60,7 +63,10 @@ function open(parser) { end: -1, test: read_expression(parser), consequent: create_fragment(), - alternate: null + alternate: null, + metadata: { + expression: create_expression_metadata() + } }); parser.allow_whitespace(); @@ -241,7 +247,10 @@ function open(parser) { error: null, pending: null, then: null, - catch: null + catch: null, + metadata: { + expression: create_expression_metadata() + } }); if (parser.eat('then')) { @@ -323,7 +332,10 @@ function open(parser) { start, end: -1, expression, - fragment: create_fragment() + fragment: create_fragment(), + metadata: { + expression: create_expression_metadata() + } }); parser.stack.push(block); @@ -351,6 +363,22 @@ function open(parser) { const params_start = parser.index; + // snippets could have a generic signature, e.g. `#snippet foo(...)` + /** @type {string | undefined} */ + let type_params; + + // if we match a generic opening + if (parser.ts && parser.match('<')) { + const start = parser.index; + const end = match_bracket(parser, start, pointy_bois); + + type_params = parser.template.slice(start + 1, end - 1); + + parser.index = end; + } + + parser.allow_whitespace(); + const matched = parser.eat('(', true, false); if (matched) { @@ -370,7 +398,12 @@ function open(parser) { let function_expression = matched ? /** @type {ArrowFunctionExpression} */ ( - parse_expression_at(prelude + `${params} => {}`, parser.ts, params_start) + parse_expression_at( + prelude + `${params} => {}`, + parser.root.comments, + parser.ts, + params_start + ) ) : { params: [] }; @@ -388,6 +421,7 @@ function open(parser) { end: name_end, name }, + typeParams: type_params, parameters: function_expression.params, body: create_fragment(), metadata: { @@ -441,7 +475,10 @@ function next(parser) { elseif: true, test: expression, consequent: create_fragment(), - alternate: null + alternate: null, + metadata: { + expression: create_expression_metadata() + } }); parser.stack.push(child); @@ -604,7 +641,10 @@ function special(parser) { type: 'HtmlTag', start, end: parser.index, - expression + expression, + metadata: { + expression: create_expression_metadata() + } }); return; @@ -679,6 +719,9 @@ function special(parser) { declarations: [{ type: 'VariableDeclarator', id, init, start: id.start, end: init.end }], start: start + 2, // start at const, not at @const end: parser.index - 1 + }, + metadata: { + expression: create_expression_metadata() } }); } @@ -705,6 +748,7 @@ function special(parser) { end: parser.index, expression: /** @type {AST.RenderTag['expression']} */ (expression), metadata: { + expression: create_expression_metadata(), dynamic: false, arguments: [], path: [], diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js index b7c8cb43cd..8c69a58c99 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js @@ -1,34 +1,5 @@ -const SQUARE_BRACKET_OPEN = '['; -const SQUARE_BRACKET_CLOSE = ']'; -const CURLY_BRACKET_OPEN = '{'; -const CURLY_BRACKET_CLOSE = '}'; -const PARENTHESES_OPEN = '('; -const PARENTHESES_CLOSE = ')'; - -/** @param {string} char */ -export function is_bracket_open(char) { - return char === SQUARE_BRACKET_OPEN || char === CURLY_BRACKET_OPEN; -} - -/** @param {string} char */ -export function is_bracket_close(char) { - return char === SQUARE_BRACKET_CLOSE || char === CURLY_BRACKET_CLOSE; -} - -/** @param {string} open */ -export function get_bracket_close(open) { - if (open === SQUARE_BRACKET_OPEN) { - return SQUARE_BRACKET_CLOSE; - } - - if (open === CURLY_BRACKET_OPEN) { - return CURLY_BRACKET_CLOSE; - } - - if (open === PARENTHESES_OPEN) { - return PARENTHESES_CLOSE; - } -} +/** @import { Parser } from '../index.js' */ +import * as e from '../../../errors.js'; /** * @param {number} num @@ -121,7 +92,7 @@ function count_leading_backslashes(string, search_start_index) { * @returns {number | undefined} The index of the closing bracket, or undefined if not found. */ export function find_matching_bracket(template, index, open) { - const close = get_bracket_close(open); + const close = default_brackets[open]; let brackets = 1; let i = index; while (brackets > 0 && i < template.length) { @@ -162,3 +133,81 @@ export function find_matching_bracket(template, index, open) { } return undefined; } + +/** @type {Record} */ +const default_brackets = { + '{': '}', + '(': ')', + '[': ']' +}; + +/** + * @param {Parser} parser + * @param {number} start + * @param {Record} brackets + */ +export function match_bracket(parser, start, brackets = default_brackets) { + const close = Object.values(brackets); + const bracket_stack = []; + + let i = start; + + while (i < parser.template.length) { + let char = parser.template[i++]; + + if (char === "'" || char === '"' || char === '`') { + i = match_quote(parser, i, char); + continue; + } + + if (char in brackets) { + bracket_stack.push(char); + } else if (close.includes(char)) { + const popped = /** @type {string} */ (bracket_stack.pop()); + const expected = /** @type {string} */ (brackets[popped]); + + if (char !== expected) { + e.expected_token(i - 1, expected); + } + + if (bracket_stack.length === 0) { + return i; + } + } + } + + e.unexpected_eof(parser.template.length); +} + +/** + * @param {Parser} parser + * @param {number} start + * @param {string} quote + */ +function match_quote(parser, start, quote) { + let is_escaped = false; + let i = start; + + while (i < parser.template.length) { + const char = parser.template[i++]; + + if (is_escaped) { + is_escaped = false; + continue; + } + + if (char === quote) { + return i; + } + + if (char === '\\') { + is_escaped = true; + } + + if (quote === '`' && char === '$' && parser.template[i] === '{') { + i = match_bracket(parser, i); + } + } + + e.unterminated_string_constant(start); +} diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/html.js b/packages/svelte/src/compiler/phases/1-parse/utils/html.js index a68acb996f..a0c2a5b06f 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/html.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/html.js @@ -72,6 +72,8 @@ const NUL = 0; // to replace them ourselves // // Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters +// Also see: https://en.wikipedia.org/wiki/Plane_(Unicode) +// Also see: https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream /** @param {number} code */ function validate_code(code) { @@ -116,5 +118,10 @@ function validate_code(code) { return code; } + // supplementary special-purpose plane 0xe0000 - 0xe07f and 0xe0100 - 0xe01ef + if ((code >= 917504 && code <= 917631) || (code >= 917760 && code <= 917999)) { + return code; + } + return NUL; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index 2dc3435648..d6052c9c3e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -68,8 +68,12 @@ const css_visitors = { const global = node.children.find(is_global); if (global) { - const idx = node.children.indexOf(global); + const is_nested = context.path.at(-2)?.type === 'PseudoClassSelector'; + if (is_nested && !global.selectors[0].args) { + e.css_global_block_invalid_placement(global.selectors[0]); + } + const idx = node.children.indexOf(global); if (global.selectors[0].args !== null && idx !== 0 && idx !== node.children.length - 1) { // ensure `:global(...)` is not used in the middle of a selector (but multiple `global(...)` in sequence are ok) for (let i = idx + 1; i < node.children.length; i++) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index fbe6ca1cd3..79e8fbb02c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -532,12 +532,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, } case 'ClassSelector': { - if ( - !attribute_matches(element, 'class', name, '~=', false) && - !element.attributes.some( - (attribute) => attribute.type === 'ClassDirective' && attribute.name === name - ) - ) { + if (!attribute_matches(element, 'class', name, '~=', false)) { return false; } @@ -633,14 +628,33 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv if (attribute.type === 'SpreadAttribute') return true; if (attribute.type === 'BindDirective' && attribute.name === name) return true; + const name_lower = name.toLowerCase(); + // match attributes against the corresponding directive but bail out on exact matching + if (attribute.type === 'StyleDirective' && name_lower === 'style') return true; + if (attribute.type === 'ClassDirective' && name_lower === 'class') { + if (operator === '~=') { + if (attribute.name === expected_value) return true; + } else { + return true; + } + } + if (attribute.type !== 'Attribute') continue; - if (attribute.name.toLowerCase() !== name.toLowerCase()) continue; + if (attribute.name.toLowerCase() !== name_lower) continue; if (attribute.value === true) return operator === null; if (expected_value === null) return true; if (is_text_attribute(attribute)) { - return test_attribute(operator, expected_value, case_insensitive, attribute.value[0].data); + const matches = test_attribute( + operator, + expected_value, + case_insensitive, + attribute.value[0].data + ); + // continue if we still may match against a class/style directive + if (!matches && (name_lower === 'class' || name_lower === 'style')) continue; + return matches; } const chunks = get_attribute_chunks(attribute.value); @@ -649,7 +663,7 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv /** @type {string[]} */ let prev_values = []; for (const chunk of chunks) { - const current_possible_values = get_possible_values(chunk, name === 'class'); + const current_possible_values = get_possible_values(chunk, name_lower === 'class'); // impossible to find out all combinations if (!current_possible_values) return true; diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 2e36a89649..d407b44556 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -3,6 +3,7 @@ /** @import { AnalysisState, Visitors } from './types' */ /** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ import { walk } from 'zimmerframe'; +import { parse } from '../1-parse/acorn.js'; import * as e from '../../errors.js'; import * as w from '../../warnings.js'; import { extract_identifiers } from '../../utils/ast.js'; @@ -18,8 +19,10 @@ import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js'; import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js'; import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js'; import { AssignmentExpression } from './visitors/AssignmentExpression.js'; +import { AttachTag } from './visitors/AttachTag.js'; import { Attribute } from './visitors/Attribute.js'; import { AwaitBlock } from './visitors/AwaitBlock.js'; +import { AwaitExpression } from './visitors/AwaitExpression.js'; import { BindDirective } from './visitors/BindDirective.js'; import { CallExpression } from './visitors/CallExpression.js'; import { ClassBody } from './visitors/ClassBody.js'; @@ -43,9 +46,11 @@ import { ImportDeclaration } from './visitors/ImportDeclaration.js'; import { KeyBlock } from './visitors/KeyBlock.js'; import { LabeledStatement } from './visitors/LabeledStatement.js'; import { LetDirective } from './visitors/LetDirective.js'; +import { Literal } from './visitors/Literal.js'; import { MemberExpression } from './visitors/MemberExpression.js'; import { NewExpression } from './visitors/NewExpression.js'; import { OnDirective } from './visitors/OnDirective.js'; +import { PropertyDefinition } from './visitors/PropertyDefinition.js'; import { RegularElement } from './visitors/RegularElement.js'; import { RenderTag } from './visitors/RenderTag.js'; import { SlotElement } from './visitors/SlotElement.js'; @@ -63,6 +68,7 @@ import { SvelteSelf } from './visitors/SvelteSelf.js'; import { SvelteWindow } from './visitors/SvelteWindow.js'; import { SvelteBoundary } from './visitors/SvelteBoundary.js'; import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js'; +import { TemplateElement } from './visitors/TemplateElement.js'; import { Text } from './visitors/Text.js'; import { TitleElement } from './visitors/TitleElement.js'; import { TransitionDirective } from './visitors/TransitionDirective.js'; @@ -71,6 +77,7 @@ import { UseDirective } from './visitors/UseDirective.js'; import { VariableDeclarator } from './visitors/VariableDeclarator.js'; import is_reference from 'is-reference'; import { mark_subtree_dynamic } from './visitors/shared/fragment.js'; +import * as state from '../../state.js'; /** * @type {Visitors} @@ -131,8 +138,10 @@ const visitors = { }, ArrowFunctionExpression, AssignmentExpression, + AttachTag, Attribute, AwaitBlock, + AwaitExpression, BindDirective, CallExpression, ClassBody, @@ -156,9 +165,11 @@ const visitors = { KeyBlock, LabeledStatement, LetDirective, + Literal, MemberExpression, NewExpression, OnDirective, + PropertyDefinition, RegularElement, RenderTag, SlotElement, @@ -176,6 +187,7 @@ const visitors = { SvelteWindow, SvelteBoundary, TaggedTemplateExpression, + TemplateElement, Text, TransitionDirective, TitleElement, @@ -201,9 +213,14 @@ function js(script, root, allow_reactive_declarations, parent) { body: [] }; - const { scope, scopes } = create_scopes(ast, root, allow_reactive_declarations, parent); + const { scope, scopes, has_await } = create_scopes( + ast, + root, + allow_reactive_declarations, + parent + ); - return { ast, scope, scopes }; + return { ast, scope, scopes, has_await }; } /** @@ -223,12 +240,18 @@ function get_component_name(filename) { const RESERVED = ['$$props', '$$restProps', '$$slots']; /** - * @param {Program} ast + * @param {string} source * @param {ValidatedModuleCompileOptions} options * @returns {Analysis} */ -export function analyze_module(ast, options) { - const { scope, scopes } = create_scopes(ast, new ScopeRoot(), false, null); +export function analyze_module(source, options) { + /** @type {AST.JSComment[]} */ + const comments = []; + + state.set_source(source); + const ast = parse(source, comments, false, false); + + const { scope, scopes, has_await } = create_scopes(ast, new ScopeRoot(), false, null); for (const [name, references] of scope.references) { if (name[0] !== '$' || RESERVED.includes(name)) continue; @@ -245,21 +268,30 @@ export function analyze_module(ast, options) { /** @type {Analysis} */ const analysis = { - module: { ast, scope, scopes }, + module: { ast, scope, scopes, has_await }, name: options.filename, accessors: false, runes: true, immutable: true, - tracing: false + tracing: false, + async_deriveds: new Set(), + comments, + classes: new Map() }; + state.adjust({ + dev: options.dev, + rootDir: options.rootDir, + runes: true + }); + walk( /** @type {Node} */ (ast), { scope, scopes, analysis: /** @type {ComponentAnalysis} */ (analysis), - derived_state: [], + state_fields: new Map(), // TODO the following are not needed for modules, but we have to pass them in order to avoid type error, // and reducing the type would result in a lot of tedious type casts elsewhere - find a good solution one day ast_type: /** @type {any} */ (null), @@ -289,7 +321,12 @@ export function analyze_component(root, source, options) { const module = js(root.module, scope_root, false, null); const instance = js(root.instance, scope_root, true, module.scope); - const { scope, scopes } = create_scopes(root.fragment, scope_root, false, instance.scope); + const { scope, scopes, has_await } = create_scopes( + root.fragment, + scope_root, + false, + instance.scope + ); /** @type {Template} */ const template = { ast: root.fragment, scope, scopes }; @@ -397,7 +434,9 @@ export function analyze_component(root, source, options) { const component_name = get_component_name(options.filename); - const runes = options.runes ?? Array.from(module.scope.references.keys()).some(is_rune); + const runes = + options.runes ?? + (has_await || instance.has_await || Array.from(module.scope.references.keys()).some(is_rune)); if (!runes) { for (let check of synthetic_stores_legacy_check) { @@ -420,9 +459,34 @@ export function analyze_component(root, source, options) { module, instance, template, + comments: root.comments, elements: [], runes, + // if we are not in runes mode but we have no reserved references ($$props, $$restProps) + // and no `export let` we might be in a wannabe runes component that is using runes in an external + // module...we need to fallback to the runic behavior + maybe_runes: + !runes && + // if they explicitly disabled runes, use the legacy behavior + options.runes !== false && + ![...module.scope.references.keys()].some((name) => + ['$$props', '$$restProps'].includes(name) + ) && + !instance.ast.body.some( + (node) => + node.type === 'LabeledStatement' || + (node.type === 'ExportNamedDeclaration' && + ((node.declaration && + node.declaration.type === 'VariableDeclaration' && + node.declaration.kind === 'let') || + node.specifiers.some( + (specifier) => + specifier.local.type === 'Identifier' && + instance.scope.get(specifier.local.name)?.declaration_kind === 'let' + ))) + ), tracing: false, + classes: new Map(), immutable: runes || options.immutable, exports: [], uses_props: false, @@ -462,9 +526,17 @@ export function analyze_component(root, source, options) { source, undefined_exports: new Map(), snippet_renderers: new Map(), - snippets: new Set() + snippets: new Set(), + async_deriveds: new Set() }; + state.adjust({ + component_name: analysis.name, + dev: options.dev, + rootDir: options.rootDir, + runes + }); + if (!runes) { // every exported `let` or `var` declaration becomes a prop, everything else becomes an export for (const node of instance.ast.body) { @@ -618,7 +690,7 @@ export function analyze_component(root, source, options) { has_props_rune: false, component_slots: new Set(), expression: null, - derived_state: [], + state_fields: new Map(), function_depth: scope.function_depth, reactive_statement: null }; @@ -685,7 +757,7 @@ export function analyze_component(root, source, options) { reactive_statement: null, component_slots: new Set(), expression: null, - derived_state: [], + state_fields: new Map(), function_depth: scope.function_depth }; diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index 17c8123de1..080239bac0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -1,6 +1,6 @@ import type { Scope } from '../scope.js'; import type { ComponentAnalysis, ReactiveStatement } from '../types.js'; -import type { AST, ExpressionMetadata, ValidatedCompileOptions } from '#compiler'; +import type { AST, ExpressionMetadata, StateField, ValidatedCompileOptions } from '#compiler'; export interface AnalysisState { scope: Scope; @@ -18,7 +18,10 @@ export interface AnalysisState { component_slots: Set; /** Information about the current expression/directive/block value */ expression: ExpressionMetadata | null; - derived_state: { name: string; private: boolean }[]; + + /** Used to analyze class state */ + state_fields: Map; + function_depth: number; // legacy stuff diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js index a64c89cd88..39358f72fc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js @@ -8,7 +8,7 @@ import { validate_assignment } from './shared/utils.js'; * @param {Context} context */ export function AssignmentExpression(node, context) { - validate_assignment(node, node.left, context.state); + validate_assignment(node, node.left, context); if (context.state.reactive_statement) { const id = node.left.type === 'MemberExpression' ? object(node.left) : node.left; @@ -23,5 +23,9 @@ export function AssignmentExpression(node, context) { } } + if (context.state.expression) { + context.state.expression.has_assignment = true; + } + context.next(); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AttachTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AttachTag.js new file mode 100644 index 0000000000..1e318f228d --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AttachTag.js @@ -0,0 +1,13 @@ +/** @import { AST } from '#compiler' */ +/** @import { Context } from '../types' */ + +import { mark_subtree_dynamic } from './shared/fragment.js'; + +/** + * @param {AST.AttachTag} node + * @param {Context} context + */ +export function AttachTag(node, context) { + mark_subtree_dynamic(context.path); + context.next({ ...context.state, expression: node.metadata.expression }); +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 3ba81767cc..b13f3f89b6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -192,8 +192,13 @@ function get_delegated_event(event_name, handler, context) { return unhoisted; } - // If we are referencing a binding that is shadowed in another scope then bail out. - if (local_binding !== null && binding !== null && local_binding.node !== binding.node) { + // If we are referencing a binding that is shadowed in another scope then bail out (unless it's declared within the function). + if ( + local_binding !== null && + binding !== null && + local_binding.node !== binding.node && + scope.declarations.get(reference) !== binding + ) { return unhoisted; } @@ -211,7 +216,7 @@ function get_delegated_event(event_name, handler, context) { if ( binding !== null && - // Bail out if the the binding is a rest param + // Bail out if the binding is a rest param (binding.declaration_kind === 'rest_param' || // Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode, (((!context.state.analysis.runes && binding.kind === 'each') || diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js index a71f325154..5aa04ba3b9 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js @@ -41,5 +41,8 @@ export function AwaitBlock(node, context) { mark_subtree_dynamic(context.path); - context.next(); + context.visit(node.expression, { ...context.state, expression: node.metadata.expression }); + if (node.pending) context.visit(node.pending); + if (node.then) context.visit(node.then); + if (node.catch) context.visit(node.catch); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js new file mode 100644 index 0000000000..af7d0307e9 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -0,0 +1,30 @@ +/** @import { AwaitExpression } from 'estree' */ +/** @import { Context } from '../types' */ +import * as e from '../../../errors.js'; + +/** + * @param {AwaitExpression} node + * @param {Context} context + */ +export function AwaitExpression(node, context) { + let suspend = context.state.ast_type === 'instance' && context.state.function_depth === 1; + + if (context.state.expression) { + context.state.expression.has_await = true; + suspend = true; + } + + // disallow top-level `await` or `await` in template expressions + // unless a) in runes mode and b) opted into `experimental.async` + if (suspend) { + if (!context.state.options.experimental.async) { + e.experimental_async(node); + } + + if (!context.state.analysis.runes) { + e.legacy_await_invalid(node); + } + } + + context.next(); +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js index 18ea79262b..9f02e7fa5a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js @@ -158,7 +158,7 @@ export function BindDirective(node, context) { return; } - validate_assignment(node, node.expression, context.state); + validate_assignment(node, node.expression, context); const assignee = node.expression; const left = object(assignee); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 979fb62316..612e41eeb9 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -7,6 +7,7 @@ import { get_parent } from '../../../utils/ast.js'; import { is_pure, is_safe_identifier } from './shared/utils.js'; import { dev, locate_node, source } from '../../../state.js'; import * as b from '#compiler/builders'; +import { create_expression_metadata } from '../../nodes.js'; /** * @param {CallExpression} node @@ -114,12 +115,13 @@ export function CallExpression(node, context) { case '$state': case '$state.raw': case '$derived': - case '$derived.by': - if ( - (parent.type !== 'VariableDeclarator' || - get_parent(context.path, -3).type === 'ConstTag') && - !(parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) - ) { + case '$derived.by': { + const valid = + is_variable_declaration(parent, context) || + is_class_property_definition(parent) || + is_class_property_assignment_at_constructor_root(parent, context); + + if (!valid) { e.state_invalid_placement(node, rune); } @@ -132,6 +134,7 @@ export function CallExpression(node, context) { } break; + } case '$effect': case '$effect.pre': @@ -163,6 +166,13 @@ export function CallExpression(node, context) { break; + case '$effect.pending': + if (context.state.expression) { + context.state.expression.has_state = true; + } + + break; + case '$inspect': if (node.arguments.length < 1) { e.rune_invalid_arguments_length(node, rune, 'one or more arguments'); @@ -227,7 +237,19 @@ export function CallExpression(node, context) { } // `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning - if (rune === '$inspect' || rune === '$derived') { + if (rune === '$derived') { + const expression = create_expression_metadata(); + + context.next({ + ...context.state, + function_depth: context.state.function_depth + 1, + expression + }); + + if (expression.has_await) { + context.state.analysis.async_deriveds.add(node); + } + } else if (rune === '$inspect') { context.next({ ...context.state, function_depth: context.state.function_depth + 1 }); } else { context.next(); @@ -272,3 +294,40 @@ function get_function_label(nodes) { return parent.id.name; } } + +/** + * @param {AST.SvelteNode} parent + * @param {Context} context + */ +function is_variable_declaration(parent, context) { + return parent.type === 'VariableDeclarator' && get_parent(context.path, -3).type !== 'ConstTag'; +} + +/** + * @param {AST.SvelteNode} parent + */ +function is_class_property_definition(parent) { + return parent.type === 'PropertyDefinition' && !parent.static && !parent.computed; +} + +/** + * @param {AST.SvelteNode} node + * @param {Context} context + */ +function is_class_property_assignment_at_constructor_root(node, context) { + if ( + node.type === 'AssignmentExpression' && + node.operator === '=' && + node.left.type === 'MemberExpression' && + node.left.object.type === 'ThisExpression' && + ((node.left.property.type === 'Identifier' && !node.left.computed) || + node.left.property.type === 'PrivateIdentifier' || + node.left.property.type === 'Literal') + ) { + // MethodDefinition (-5) -> FunctionExpression (-4) -> BlockStatement (-3) -> ExpressionStatement (-2) -> AssignmentExpression (-1) + const parent = get_parent(context.path, -5); + return parent?.type === 'MethodDefinition' && parent.kind === 'constructor'; + } + + return false; +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js index 0463e4da85..ffc39ac00d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js @@ -1,30 +1,107 @@ -/** @import { ClassBody } from 'estree' */ +/** @import { AssignmentExpression, CallExpression, ClassBody, PropertyDefinition, Expression, PrivateIdentifier, MethodDefinition } from 'estree' */ +/** @import { StateField } from '#compiler' */ /** @import { Context } from '../types' */ +import * as b from '#compiler/builders'; import { get_rune } from '../../scope.js'; +import * as e from '../../../errors.js'; +import { is_state_creation_rune } from '../../../../utils.js'; +import { get_name } from '../../nodes.js'; +import { regex_invalid_identifier_chars } from '../../patterns.js'; /** * @param {ClassBody} node * @param {Context} context */ export function ClassBody(node, context) { - /** @type {{name: string, private: boolean}[]} */ - const derived_state = []; + if (!context.state.analysis.runes) { + context.next(); + return; + } + + /** @type {string[]} */ + const private_ids = []; - for (const definition of node.body) { + for (const prop of node.body) { if ( - definition.type === 'PropertyDefinition' && - (definition.key.type === 'PrivateIdentifier' || definition.key.type === 'Identifier') && - definition.value?.type === 'CallExpression' + (prop.type === 'MethodDefinition' || prop.type === 'PropertyDefinition') && + prop.key.type === 'PrivateIdentifier' ) { - const rune = get_rune(definition.value, context.state.scope); - if (rune === '$derived' || rune === '$derived.by') { - derived_state.push({ - name: definition.key.name, - private: definition.key.type === 'PrivateIdentifier' - }); + private_ids.push(prop.key.name); + } + } + + /** @type {Map} */ + const state_fields = new Map(); + + context.state.analysis.classes.set(node, state_fields); + + /** @type {MethodDefinition | null} */ + let constructor = null; + + /** + * @param {PropertyDefinition | AssignmentExpression} node + * @param {Expression | PrivateIdentifier} key + * @param {Expression | null | undefined} value + */ + function handle(node, key, value) { + const name = get_name(key); + if (name === null) return; + + const rune = get_rune(value, context.state.scope); + + if (rune && is_state_creation_rune(rune)) { + if (state_fields.has(name)) { + e.state_field_duplicate(node, name); } + + state_fields.set(name, { + node, + type: rune, + // @ts-expect-error for public state this is filled out in a moment + key: key.type === 'PrivateIdentifier' ? key : null, + value: /** @type {CallExpression} */ (value) + }); + } + } + + for (const child of node.body) { + if (child.type === 'PropertyDefinition' && !child.computed && !child.static) { + handle(child, child.key, child.value); } + + if (child.type === 'MethodDefinition' && child.kind === 'constructor') { + constructor = child; + } + } + + if (constructor) { + for (const statement of constructor.value.body.body) { + if (statement.type !== 'ExpressionStatement') continue; + if (statement.expression.type !== 'AssignmentExpression') continue; + + const { left, right } = statement.expression; + + if (left.type !== 'MemberExpression') continue; + if (left.object.type !== 'ThisExpression') continue; + if (left.computed && left.property.type !== 'Literal') continue; + + handle(statement.expression, left.property, right); + } + } + + for (const [name, field] of state_fields) { + if (name[0] === '#') { + continue; + } + + let deconflicted = name.replace(regex_invalid_identifier_chars, '_'); + while (private_ids.includes(deconflicted)) { + deconflicted = '_' + deconflicted; + } + + private_ids.push(deconflicted); + field.key = b.private_id(deconflicted); } - context.next({ ...context.state, derived_state }); + context.next({ ...context.state, state_fields }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js index f723f8447c..d5f5f7b2e0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js @@ -32,5 +32,8 @@ export function ConstTag(node, context) { e.const_tag_invalid_placement(node); } - context.next(); + const declaration = node.declaration.declarations[0]; + + context.visit(declaration.id); + context.visit(declaration.init, { ...context.state, expression: node.metadata.expression }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js index bd6c936f99..e6a83921b1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js @@ -1,7 +1,8 @@ -/** @import { AST } from '#compiler' */ +/** @import { AST, Binding } from '#compiler' */ /** @import { Context } from '../types' */ /** @import { Scope } from '../../scope' */ import * as e from '../../../errors.js'; +import { extract_identifiers } from '../../../utils/ast.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js'; @@ -38,5 +39,52 @@ export function EachBlock(node, context) { if (node.key) context.visit(node.key); if (node.fallback) context.visit(node.fallback); + if (!context.state.analysis.runes) { + let mutated = + !!node.context && + extract_identifiers(node.context).some((id) => { + const binding = context.state.scope.get(id.name); + return !!binding?.mutated; + }); + + // collect transitive dependencies... + for (const binding of node.metadata.expression.dependencies) { + collect_transitive_dependencies(binding, node.metadata.transitive_deps); + } + + // ...and ensure they are marked as state, so they can be turned + // into mutable sources and invalidated + if (mutated) { + for (const binding of node.metadata.transitive_deps) { + if ( + binding.kind === 'normal' && + (binding.declaration_kind === 'const' || + binding.declaration_kind === 'let' || + binding.declaration_kind === 'var') + ) { + binding.kind = 'state'; + } + } + } + } + mark_subtree_dynamic(context.path); } + +/** + * @param {Binding} binding + * @param {Set} bindings + * @returns {void} + */ +function collect_transitive_dependencies(binding, bindings) { + if (bindings.has(binding)) { + return; + } + bindings.add(binding); + + if (binding.kind === 'legacy_reactive') { + for (const dep of binding.legacy_dependencies) { + collect_transitive_dependencies(dep, bindings); + } + } +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js index c89b11ad36..7b0e501760 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js @@ -15,5 +15,5 @@ export function HtmlTag(node, context) { // unfortunately this is necessary in order to fix invalid HTML mark_subtree_dynamic(context.path); - context.next(); + context.next({ ...context.state, expression: node.metadata.expression }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index efbbe6cfa2..cced326f9b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -39,7 +39,7 @@ export function Identifier(node, context) { if ( is_rune(node.name) && context.state.scope.get(node.name) === null && - context.state.scope.get(node.name.slice(1)) === null + context.state.scope.get(node.name.slice(1))?.kind !== 'store_sub' ) { /** @type {Expression} */ let current = node; @@ -90,7 +90,11 @@ export function Identifier(node, context) { if (binding) { if (context.state.expression) { context.state.expression.dependencies.add(binding); - context.state.expression.has_state ||= binding.kind !== 'normal'; + context.state.expression.references.add(binding); + context.state.expression.has_state ||= + binding.kind !== 'static' && + !binding.is_function() && + !context.state.scope.evaluate(node).is_known; } if ( diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js index a65771bcfc..dcdae3587f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js @@ -17,5 +17,11 @@ export function IfBlock(node, context) { mark_subtree_dynamic(context.path); - context.next(); + context.visit(node.test, { + ...context.state, + expression: node.metadata.expression + }); + + context.visit(node.consequent); + if (node.alternate) context.visit(node.alternate); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js index 88bb6a98e7..09e604ea66 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js @@ -16,5 +16,6 @@ export function KeyBlock(node, context) { mark_subtree_dynamic(context.path); - context.next(); + context.visit(node.expression, { ...context.state, expression: node.metadata.expression }); + context.visit(node.fragment); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Literal.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Literal.js new file mode 100644 index 0000000000..58684ba71c --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Literal.js @@ -0,0 +1,14 @@ +/** @import { Literal } from 'estree' */ +import * as w from '../../../warnings.js'; +import { regex_bidirectional_control_characters } from '../../patterns.js'; + +/** + * @param {Literal} node + */ +export function Literal(node) { + if (typeof node.value === 'string') { + if (regex_bidirectional_control_characters.test(node.value)) { + w.bidirectional_control_characters(node); + } + } +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js index 245a164c71..0a3b386198 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js @@ -15,8 +15,9 @@ export function MemberExpression(node, context) { } } - if (context.state.expression && !is_pure(node, context)) { - context.state.expression.has_state = true; + if (context.state.expression) { + context.state.expression.has_member_expression = true; + context.state.expression.has_state ||= !is_pure(node, context); } if (!is_safe_identifier(node, context.state.scope)) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js new file mode 100644 index 0000000000..99d05cb47c --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js @@ -0,0 +1,21 @@ +/** @import { PropertyDefinition } from 'estree' */ +/** @import { Context } from '../types' */ +import * as e from '../../../errors.js'; +import { get_name } from '../../nodes.js'; + +/** + * @param {PropertyDefinition} node + * @param {Context} context + */ +export function PropertyDefinition(node, context) { + const name = get_name(node.key); + const field = name && context.state.state_fields.get(name); + + if (field && node !== field.node && node.value) { + if (/** @type {number} */ (node.start) < /** @type {number} */ (field.node.start)) { + e.state_field_invalid_assignment(node); + } + } + + context.next(); +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js index d5689e5d55..fab5d46e1b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RegularElement.js @@ -9,7 +9,7 @@ import * as e from '../../../errors.js'; import * as w from '../../../warnings.js'; import { create_attribute, is_custom_element_node } from '../../nodes.js'; import { regex_starts_with_newline } from '../../patterns.js'; -import { check_element } from './shared/a11y.js'; +import { check_element } from './shared/a11y/index.js'; import { validate_element } from './shared/element.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js index a8c9d408bd..1230ef6b04 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js @@ -54,7 +54,7 @@ export function RenderTag(node, context) { mark_subtree_dynamic(context.path); - context.visit(callee); + context.visit(callee, { ...context.state, expression: node.metadata.expression }); for (const arg of expression.arguments) { const metadata = create_expression_metadata(); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/StyleDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/StyleDirective.js index 7d6eb5be99..9699d3c03b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/StyleDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/StyleDirective.js @@ -32,6 +32,7 @@ export function StyleDirective(node, context) { node.metadata.expression.has_state ||= chunk.metadata.expression.has_state; node.metadata.expression.has_call ||= chunk.metadata.expression.has_call; + node.metadata.expression.has_await ||= chunk.metadata.expression.has_await; } } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js index d50cb80cb8..35af96ba12 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js @@ -2,7 +2,7 @@ /** @import { Context } from '../types' */ import * as e from '../../../errors.js'; -const valid = ['onerror', 'failed']; +const valid = ['onerror', 'failed', 'pending']; /** * @param {AST.SvelteBoundary} node diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js index c45859408c..3f7b0ec6b8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js @@ -2,7 +2,7 @@ /** @import { Context } from '../types' */ import { NAMESPACE_MATHML, NAMESPACE_SVG } from '../../../../constants.js'; import { is_text_attribute } from '../../../utils/ast.js'; -import { check_element } from './shared/a11y.js'; +import { check_element } from './shared/a11y/index.js'; import { validate_element } from './shared/element.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; @@ -62,5 +62,17 @@ export function SvelteElement(node, context) { mark_subtree_dynamic(context.path); - context.next({ ...context.state, parent_element: null }); + context.visit(node.tag, { + ...context.state, + expression: node.metadata.expression + }); + + for (const attribute of node.attributes) { + context.visit(attribute); + } + + context.visit(node.fragment, { + ...context.state, + parent_element: null + }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteSelf.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteSelf.js index b87f082de0..652a447165 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteSelf.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteSelf.js @@ -3,7 +3,7 @@ import { visit_component } from './shared/component.js'; import * as e from '../../../errors.js'; import * as w from '../../../warnings.js'; -import { filename } from '../../../state.js'; +import { filename, UNKNOWN_FILENAME } from '../../../state.js'; /** * @param {AST.SvelteSelf} node @@ -23,9 +23,9 @@ export function SvelteSelf(node, context) { } if (context.state.analysis.runes) { - const name = filename === '(unknown)' ? 'Self' : context.state.analysis.name; + const name = filename === UNKNOWN_FILENAME ? 'Self' : context.state.analysis.name; const basename = - filename === '(unknown)' + filename === UNKNOWN_FILENAME ? 'Self.svelte' : /** @type {string} */ (filename.split(/[/\\]/).pop()); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/TemplateElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/TemplateElement.js new file mode 100644 index 0000000000..978042bbc5 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/TemplateElement.js @@ -0,0 +1,12 @@ +/** @import { TemplateElement } from 'estree' */ +import * as w from '../../../warnings.js'; +import { regex_bidirectional_control_characters } from '../../patterns.js'; + +/** + * @param {TemplateElement} node + */ +export function TemplateElement(node) { + if (regex_bidirectional_control_characters.test(node.value.cooked ?? '')) { + w.bidirectional_control_characters(node); + } +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js index 363a111b7d..a03421e8dd 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js @@ -1,20 +1,52 @@ /** @import { AST } from '#compiler' */ /** @import { Context } from '../types' */ import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js'; -import { regex_not_whitespace } from '../../patterns.js'; +import { regex_bidirectional_control_characters, regex_not_whitespace } from '../../patterns.js'; import * as e from '../../../errors.js'; +import * as w from '../../../warnings.js'; +import { extract_svelte_ignore } from '../../../utils/extract_svelte_ignore.js'; /** * @param {AST.Text} node * @param {Context} context */ export function Text(node, context) { - const in_template = context.path.at(-1)?.type === 'Fragment'; + const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1)); - if (in_template && context.state.parent_element && regex_not_whitespace.test(node.data)) { + if ( + parent.type === 'Fragment' && + context.state.parent_element && + regex_not_whitespace.test(node.data) + ) { const message = is_tag_valid_with_parent('#text', context.state.parent_element); if (message) { e.node_invalid_placement(node, message); } } + + regex_bidirectional_control_characters.lastIndex = 0; + for (const match of node.data.matchAll(regex_bidirectional_control_characters)) { + let is_ignored = false; + + // if we have a svelte-ignore comment earlier in the text, bail + // (otherwise we can only use svelte-ignore on parent elements/blocks) + if (parent.type === 'Fragment') { + for (const child of parent.nodes) { + if (child === node) break; + + if (child.type === 'Comment') { + is_ignored ||= extract_svelte_ignore( + child.start + 4, + child.data, + context.state.analysis.runes + ).includes('bidirectional_control_characters'); + } + } + } + + if (!is_ignored) { + let start = match.index + node.start; + w.bidirectional_control_characters({ start, end: start + match[0].length }); + } + } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js index 741effc67a..ed48e026ac 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js @@ -8,7 +8,7 @@ import { validate_assignment } from './shared/utils.js'; * @param {Context} context */ export function UpdateExpression(node, context) { - validate_assignment(node, node.argument, context.state); + validate_assignment(node, node.argument, context); if (context.state.reactive_statement) { const id = node.argument.type === 'MemberExpression' ? object(node.argument) : node.argument; @@ -21,5 +21,9 @@ export function UpdateExpression(node, context) { } } + if (context.state.expression) { + context.state.expression.has_assignment = true; + } + context.next(); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js index a7d08d315d..89320f3962 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js @@ -4,8 +4,10 @@ import { get_rune } from '../../scope.js'; import { ensure_no_module_import_conflict, validate_identifier_name } from './shared/utils.js'; import * as e from '../../../errors.js'; +import * as w from '../../../warnings.js'; import { extract_paths } from '../../../utils/ast.js'; import { equal } from '../../../utils/assert.js'; +import * as b from '#compiler/builders'; /** * @param {VariableDeclarator} node @@ -17,7 +19,7 @@ export function VariableDeclarator(node, context) { if (context.state.analysis.runes) { const init = node.init; const rune = get_rune(init, context.state.scope); - const paths = extract_paths(node.id); + const { paths } = extract_paths(node.id, b.id('dummy')); for (const path of paths) { validate_identifier_name(context.state.scope.get(/** @type {Identifier} */ (path.node).name)); @@ -52,6 +54,19 @@ export function VariableDeclarator(node, context) { e.props_invalid_identifier(node); } + if ( + context.state.analysis.custom_element && + context.state.options.customElementOptions?.props == null + ) { + let warn_on; + if ( + node.id.type === 'Identifier' || + (warn_on = node.id.properties.find((p) => p.type === 'RestElement')) != null + ) { + w.custom_element_props_identifier(warn_on ?? node.id); + } + } + context.state.analysis.needs_props = true; if (node.id.type === 'Identifier') { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js new file mode 100644 index 0000000000..a1b70f2207 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js @@ -0,0 +1,319 @@ +/** @import { ARIARoleRelationConcept } from 'aria-query' */ +import { roles as roles_map, elementRoles } from 'aria-query'; +// @ts-expect-error package doesn't provide typings +import { AXObjects, elementAXObjects } from 'axobject-query'; + +export const aria_attributes = + 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split( + ' ' + ); + +/** @type {Record} */ +export const a11y_required_attributes = { + a: ['href'], + area: ['alt', 'aria-label', 'aria-labelledby'], + // html-has-lang + html: ['lang'], + // iframe-has-title + iframe: ['title'], + img: ['alt'], + object: ['title', 'aria-label', 'aria-labelledby'] +}; + +export const a11y_distracting_elements = ['blink', 'marquee']; + +// this excludes `` and `