From b0fed256f71aeaed9a0dac3cde81f6d98ee0724b Mon Sep 17 00:00:00 2001 From: Andrew Walker Date: Tue, 21 Feb 2023 10:23:13 +0000 Subject: [PATCH 01/70] feat: Add -replacestate/-keepfocus to SvelteKit anchor tag props (#8281) --- elements/index.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/elements/index.d.ts b/elements/index.d.ts index d57d97ad28..9a4284af22 100644 --- a/elements/index.d.ts +++ b/elements/index.d.ts @@ -540,10 +540,12 @@ export interface HTMLAttributes extends AriaAttributes, D 'bind:textContent'?: string | undefined | null; // SvelteKit + 'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null; 'data-sveltekit-noscroll'?: true | '' | 'off' | undefined | null; 'data-sveltekit-preload-code'?: true | '' | 'eager' | 'viewport' | 'hover' | 'tap' | 'off' | undefined | null; 'data-sveltekit-preload-data'?: true | '' | 'hover' | 'tap' | 'off' | undefined | null; 'data-sveltekit-reload'?: true | '' | 'off' | undefined | null; + 'data-sveltekit-replacestate'?: true | '' | 'off' | undefined | null; } export type HTMLAttributeAnchorTarget = From d5e46d647ee3f16945c3f475c8265f2e3e121875 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 21 Feb 2023 11:24:53 +0100 Subject: [PATCH 02/70] chore: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8b4979fde..610e92f4a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Make `noreferrer` warning less zealous ([#6289](https://github.com/sveltejs/svelte/issues/6289)) * `trusted-types` CSP compatibility for Web Components ([#8134](https://github.com/sveltejs/svelte/issues/8134)) +* Add `data-sveltekit-replacestate` and `data-sveltekit-keepfocus` attribute typings ([#8281](https://github.com/sveltejs/svelte/issues/8281)) ## 3.55.1 From 4dd12c0c61c647a9c5fafc4bde05c1bf7bcb8c1c Mon Sep 17 00:00:00 2001 From: Maxime Dupont Date: Tue, 21 Feb 2023 11:33:01 +0100 Subject: [PATCH 03/70] docs: fix fly animation description (#8236) --- site/content/docs/04-run-time.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/content/docs/04-run-time.md b/site/content/docs/04-run-time.md index bf5574ad44..ef21112034 100644 --- a/site/content/docs/04-run-time.md +++ b/site/content/docs/04-run-time.md @@ -698,7 +698,7 @@ out:fly={params} --- -Animates the x and y positions and the opacity of an element. `in` transitions animate from an element's current (default) values to the provided values, passed as parameters. `out` transitions animate from the provided values to an element's default values. +Animates the x and y positions and the opacity of an element. `in` transitions animate from the provided values, passed as parameters to the element's default values. `out` transitions animate from the element's default values to the provided values. `fly` accepts the following parameters: From 7a6eee51e02c59cb2ae9d6276c19531ccf7fee6c Mon Sep 17 00:00:00 2001 From: Mike Plummer Date: Tue, 21 Feb 2023 04:54:00 -0600 Subject: [PATCH 04/70] docs: Expand testing FAQ section (#8205) --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- .../content/faq/800-how-do-i-test-svelte-apps.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/site/content/faq/800-how-do-i-test-svelte-apps.md b/site/content/faq/800-how-do-i-test-svelte-apps.md index 0e9bc9e146..5ebabdb214 100644 --- a/site/content/faq/800-how-do-i-test-svelte-apps.md +++ b/site/content/faq/800-how-do-i-test-svelte-apps.md @@ -2,12 +2,20 @@ question: How do I test Svelte apps? --- -We recommend trying to separate your view logic from your business logic. Data transformation or cross component state management is best kept outside of Svelte components. You can test those parts like you would test any JavaScript functionality that way. When it comes to testing the components, it is best to test the logic of the component and remember that the Svelte library has its own tests and you do not need to test implementation details provided by Svelte. +How your application is structured and where logic is defined will determine the best way to ensure it is properly tested. It is important to note that not all logic belongs within a component - this includes concerns such as data transformation, cross-component state management, and logging, among others. Remember that the Svelte library has its own test suite, so you do not need to write tests to validate implementation details provided by Svelte. -There are a few approaches that people take when testing, but it generally involves compiling the component and mounting it to something and then performing the tests. You essentially need to create a bundle for each component you're testing (since svelte is a compiler and not a normal library) and then mount them. You can mount to a JSDOM instance. Or you can use a real browser powered by a library like Playwright, Puppeteer, WebdriverIO or Cypress. +A Svelte application will typically have three different types of tests: Unit, Component, and End-to-End (E2E). -Some resources for getting started with unit testing: +*Unit Tests*: Focus on testing business logic in isolation. Often this is validating individual functions and edge cases. By minimizing the surface area of these tests they can be kept lean and fast, and by extracting as much logic as possible from your Svelte components more of your application can be covered using them. When creating a new SvelteKit project, you will be asked whether you would like to setup [Vitest](https://vitest.dev/) for unit testing. There are a number of other test runners that could be used as well. + +*Component Tests*: Validating that a Svelte component mounts and interacts as expected throughout its lifecycle requires a tool that provides a Document Object Model (DOM). Components can be compiled (since Svelte is a compiler and not a normal library) and mounted to allow asserting against element structure, listeners, state, and all the other capabilities provided by a Svelte component. Tools for component testing range from an in-memory implementation like jsdom paired with a test runner like [Vitest](https://vitest.dev/) to solutions that leverage an actual browser to provide a visual testing capability such as [Playwright](https://playwright.dev/docs/test-components) or [Cypress](https://www.cypress.io/). + +*End-to-End Tests*: To ensure your users are able to interact with your application it is necessary to test it as a whole in a manner as close to production as possible. This is done by writing end-to-end (E2E) tests which load and interact with a deployed version of your application in order to simulate how the user will interact with your application. When creating a new SvelteKit project, you will be asked whether you would like to setup [Playwright](https://playwright.dev/) for end-to-end testing. There are many other E2E test libraries available for use as well. + +Some resources for getting started with testing: - [Svelte Testing Library](https://testing-library.com/docs/svelte-testing-library/example/) +- [Svelte Component Testing in Cypress](https://docs.cypress.io/guides/component-testing/svelte/overview) - [Example using vitest](https://github.com/vitest-dev/vitest/tree/main/examples/svelte) - [Example using uvu test runner with JSDOM](https://github.com/lukeed/uvu/tree/master/examples/svelte) -- [Component testing in real browser](https://webdriver.io/docs/component-testing/svelte) +- [Test Svelte components using Vitest & Playwright](https://davipon.hashnode.dev/test-svelte-component-using-vitest-playwright) +- [Component testing with WebdriverIO](https://webdriver.io/docs/component-testing/svelte) From 213049cc2e6dfddb872c2286a2a8e5f00c22d73e Mon Sep 17 00:00:00 2001 From: Hyunbin <47051820+hyunbinseo@users.noreply.github.com> Date: Tue, 21 Feb 2023 20:07:33 +0900 Subject: [PATCH 05/70] docs: update dialog example (#8200) * feat: update dialog example * button always autofocusses, allows us to simplify --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .../15-composition/05-modal/App.svelte | 36 +++--- .../15-composition/05-modal/Modal.svelte | 118 ++++++++---------- 2 files changed, 68 insertions(+), 86 deletions(-) diff --git a/site/content/examples/15-composition/05-modal/App.svelte b/site/content/examples/15-composition/05-modal/App.svelte index cfd3856447..af93ba39af 100644 --- a/site/content/examples/15-composition/05-modal/App.svelte +++ b/site/content/examples/15-composition/05-modal/App.svelte @@ -4,26 +4,26 @@ let showModal = false; - -{#if showModal} - -

- modal - adjective mod·al \ˈmō-dəl\ -

+ +

+ modal + adjective mod·al \ˈmō-dəl\ +

-
    -
  1. of or relating to modality in logic
  2. -
  3. containing provisions as to the mode of procedure or the manner of taking effect —used of a contract or legacy
  4. -
  5. of or relating to a musical mode
  6. -
  7. of or relating to structure as opposed to substance
  8. -
  9. of, relating to, or constituting a grammatical form or category characteristically indicating predication
  10. -
  11. of or relating to a statistical mode
  12. -
+
    +
  1. of or relating to modality in logic
  2. +
  3. + containing provisions as to the mode of procedure or the manner of taking effect —used of a contract or legacy +
  4. +
  5. of or relating to a musical mode
  6. +
  7. of or relating to structure as opposed to substance
  8. +
  9. of, relating to, or constituting a grammatical form or category characteristically indicating predication
  10. +
  11. of or relating to a statistical mode
  12. +
- merriam-webster.com -
-{/if} + merriam-webster.com +
diff --git a/site/content/examples/15-composition/05-modal/Modal.svelte b/site/content/examples/15-composition/05-modal/Modal.svelte index baa57824bc..acdbd50ab1 100644 --- a/site/content/examples/15-composition/05-modal/Modal.svelte +++ b/site/content/examples/15-composition/05-modal/Modal.svelte @@ -1,81 +1,63 @@ - - - - - + + (showModal = false)} + on:click|self={() => dialog.close()} +> +
+ +
+ +
+ + +
+
\ No newline at end of file + From 324b791d81203546d11af2a3d1062353c1b5b8b7 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 21 Feb 2023 13:04:12 +0100 Subject: [PATCH 06/70] docs: note #if to wrap text only taken from #7070 --- site/content/docs/03-template-syntax.md | 1 + 1 file changed, 1 insertion(+) diff --git a/site/content/docs/03-template-syntax.md b/site/content/docs/03-template-syntax.md index 481fbd4405..74a2069c68 100644 --- a/site/content/docs/03-template-syntax.md +++ b/site/content/docs/03-template-syntax.md @@ -206,6 +206,7 @@ Additional conditions can be added with `{:else if expression}`, optionally endi {/if} ``` +(Blocks don't have to wrap elements, they can also wrap text within elements!) ### {#each ...} From 85f882f23dda881c2478f7828a82065aca6bbeb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jhorman=20Ruswel=20=E3=85=A0=E3=85=A0?= Date: Tue, 21 Feb 2023 07:13:50 -0500 Subject: [PATCH 07/70] docs: update Keyed Each Blocks tutorial (#8188) log which element is removed --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .../04-logic/05-keyed-each-blocks/app-a/Thing.svelte | 7 +++++++ .../04-logic/05-keyed-each-blocks/app-b/Thing.svelte | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/site/content/tutorial/04-logic/05-keyed-each-blocks/app-a/Thing.svelte b/site/content/tutorial/04-logic/05-keyed-each-blocks/app-a/Thing.svelte index 02e6fa7640..e91026fb98 100644 --- a/site/content/tutorial/04-logic/05-keyed-each-blocks/app-a/Thing.svelte +++ b/site/content/tutorial/04-logic/05-keyed-each-blocks/app-a/Thing.svelte @@ -1,4 +1,6 @@

diff --git a/site/content/tutorial/04-logic/05-keyed-each-blocks/app-b/Thing.svelte b/site/content/tutorial/04-logic/05-keyed-each-blocks/app-b/Thing.svelte index 02e6fa7640..e91026fb98 100644 --- a/site/content/tutorial/04-logic/05-keyed-each-blocks/app-b/Thing.svelte +++ b/site/content/tutorial/04-logic/05-keyed-each-blocks/app-b/Thing.svelte @@ -1,4 +1,6 @@

From f6ef6a9349a04cc8cab1dbb091c34621a2eace8f Mon Sep 17 00:00:00 2001 From: Ben Bucksch <1907525+benbucksch@users.noreply.github.com> Date: Tue, 21 Feb 2023 13:52:39 +0100 Subject: [PATCH 08/70] fix: don't throw when calling writable() unsubscribe twice (#8186) Fixes one case of #4765 --------- Co-authored-by: Ben Bucksch Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Yuichiro Yamashita --- src/runtime/store/index.ts | 2 +- test/store/index.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/runtime/store/index.ts b/src/runtime/store/index.ts index e947fa0740..16832f10d9 100644 --- a/src/runtime/store/index.ts +++ b/src/runtime/store/index.ts @@ -98,7 +98,7 @@ export function writable(value?: T, start: StartStopNotifier = noop): Writ return () => { subscribers.delete(subscriber); - if (subscribers.size === 0) { + if (subscribers.size === 0 && stop) { stop(); stop = null; } diff --git a/test/store/index.ts b/test/store/index.ts index b6fc5940e1..389f29244b 100644 --- a/test/store/index.ts +++ b/test/store/index.ts @@ -88,6 +88,14 @@ describe('store', () => { unsubscribe(); }); + + it('no error even if unsubscribe calls twice', () => { + let num = 0; + const store = writable(num, set => set(num += 1)); + const unsubscribe = store.subscribe(() => { }); + unsubscribe(); + assert.doesNotThrow(() => unsubscribe()); + }); }); describe('readable', () => { From 2a5b48838648305b933de9d9ab5c9654ca24848a Mon Sep 17 00:00:00 2001 From: Valter Kraemer Date: Tue, 21 Feb 2023 16:21:48 +0200 Subject: [PATCH 09/70] fix: empty value attribute selector doesn't produce "Unused CSS selector" warning (#8122) Fixes: #8042 --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Yuichiro Yamashita --- src/compiler/compile/css/Selector.ts | 2 +- .../_config.js | 25 +++++++++++++++++++ .../expected.css | 1 + .../expected.html | 1 + .../input.svelte | 11 ++++++++ 5 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 test/css/samples/unused-selector-empty-attribute/_config.js create mode 100644 test/css/samples/unused-selector-empty-attribute/expected.css create mode 100644 test/css/samples/unused-selector-empty-attribute/expected.html create mode 100644 test/css/samples/unused-selector-empty-attribute/input.svelte diff --git a/src/compiler/compile/css/Selector.ts b/src/compiler/compile/css/Selector.ts index 07e2dc439e..5feb59ec0c 100644 --- a/src/compiler/compile/css/Selector.ts +++ b/src/compiler/compile/css/Selector.ts @@ -350,7 +350,7 @@ function attribute_matches(node: CssNode, name: string, expected_value: string, const attr = node.attributes.find((attr: CssNode) => attr.name === name); if (!attr) return false; if (attr.is_true) return operator === null; - if (!expected_value) return true; + if (expected_value == null) return true; if (attr.chunks.length === 1) { const value = attr.chunks[0]; diff --git a/test/css/samples/unused-selector-empty-attribute/_config.js b/test/css/samples/unused-selector-empty-attribute/_config.js new file mode 100644 index 0000000000..6f77c6a3e1 --- /dev/null +++ b/test/css/samples/unused-selector-empty-attribute/_config.js @@ -0,0 +1,25 @@ +export default { + warnings: [{ + filename: 'SvelteComponent.svelte', + code: 'css-unused-selector', + message: 'Unused CSS selector "img[alt=""]"', + start: { + character: 87, + column: 1, + line: 8 + }, + end: { + character: 98, + column: 12, + line: 8 + }, + pos: 87, + frame: ` + 6: } + 7: + 8: img[alt=""] { + ^ + 9: border: 1px solid red; + 10: }` + }] +}; diff --git a/test/css/samples/unused-selector-empty-attribute/expected.css b/test/css/samples/unused-selector-empty-attribute/expected.css new file mode 100644 index 0000000000..987fa142db --- /dev/null +++ b/test/css/samples/unused-selector-empty-attribute/expected.css @@ -0,0 +1 @@ +img[alt].svelte-xyz{border:1px solid green} \ No newline at end of file diff --git a/test/css/samples/unused-selector-empty-attribute/expected.html b/test/css/samples/unused-selector-empty-attribute/expected.html new file mode 100644 index 0000000000..889908c217 --- /dev/null +++ b/test/css/samples/unused-selector-empty-attribute/expected.html @@ -0,0 +1 @@ +a foo \ No newline at end of file diff --git a/test/css/samples/unused-selector-empty-attribute/input.svelte b/test/css/samples/unused-selector-empty-attribute/input.svelte new file mode 100644 index 0000000000..0200e537be --- /dev/null +++ b/test/css/samples/unused-selector-empty-attribute/input.svelte @@ -0,0 +1,11 @@ +a foo + + From aa9b2dd5f3c54ae9918ec6659760cfaf1f7d8892 Mon Sep 17 00:00:00 2001 From: Carlos Ivanchuk <105121122+buhodev@users.noreply.github.com> Date: Tue, 21 Feb 2023 12:27:49 -0400 Subject: [PATCH 10/70] fix: omit a11y warning on `

{@html string}

``` -> Svelte doesn't perform any sanitization of the expression inside `{@html ...}` before it gets inserted into the DOM. In other words, if you use this feature it's critical that you manually escape HTML that comes from sources you don't trust, otherwise you risk exposing your users to XSS attacks. +> **Warning!** Svelte doesn't perform any sanitization of the expression inside `{@html ...}` before it gets inserted into the DOM. In other words, if you use this feature it's **critical** that you manually escape HTML that comes from sources you don't trust, otherwise you risk exposing your users to XSS attacks. diff --git a/site/content/tutorial/18-special-tags/meta.json b/site/content/tutorial/18-special-tags/meta.json new file mode 100644 index 0000000000..06e1732030 --- /dev/null +++ b/site/content/tutorial/18-special-tags/meta.json @@ -0,0 +1,3 @@ +{ + "title": "Special tags" +} \ No newline at end of file From a0cedf8d82279113ef86ebc4dc77b6ec6cb609db Mon Sep 17 00:00:00 2001 From: Yuichiro Yamashita Date: Wed, 22 Feb 2023 17:46:49 +0900 Subject: [PATCH 13/70] fix: flush remaining `afterUpdate` before `destroy` (#7516) fixes #7476 --- src/runtime/internal/Component.ts | 4 ++- src/runtime/internal/scheduler.ts | 13 ++++++++- .../Component.svelte | 27 +++++++++++++++++++ .../action-update-before-destroy/_config.js | 16 +++++++++++ .../action-update-before-destroy/main.svelte | 10 +++++++ 5 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 test/runtime/samples/action-update-before-destroy/Component.svelte create mode 100644 test/runtime/samples/action-update-before-destroy/_config.js create mode 100644 test/runtime/samples/action-update-before-destroy/main.svelte diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index 5aec24c651..a8a500b25b 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -1,4 +1,4 @@ -import { add_render_callback, flush, schedule_update, dirty_components } from './scheduler'; +import { add_render_callback, flush, flush_render_callbacks, schedule_update, dirty_components } from './scheduler'; import { current_component, set_current_component } from './lifecycle'; import { blank_object, is_empty, is_function, run, run_all, noop } from './utils'; import { children, detach, start_hydrating, end_hydrating } from './dom'; @@ -51,6 +51,8 @@ export function mount_component(component, target, anchor, customElement) { export function destroy_component(component, detaching) { const $$ = component.$$; if ($$.fragment !== null) { + flush_render_callbacks($$.after_update); + run_all($$.on_destroy); $$.fragment && $$.fragment.d(detaching); diff --git a/src/runtime/internal/scheduler.ts b/src/runtime/internal/scheduler.ts index f95ba446f5..5327e7b5b5 100644 --- a/src/runtime/internal/scheduler.ts +++ b/src/runtime/internal/scheduler.ts @@ -5,7 +5,7 @@ export const dirty_components = []; export const intros = { enabled: false }; export const binding_callbacks = []; -const render_callbacks = []; +let render_callbacks = []; const flush_callbacks = []; const resolved_promise = Promise.resolve(); @@ -122,3 +122,14 @@ function update($$) { $$.after_update.forEach(add_render_callback); } } + +/** + * Useful for example to execute remaining `afterUpdate` callbacks before executing `destroy`. + */ +export function flush_render_callbacks(fns: Function[]): void { + const filtered = []; + const targets = []; + render_callbacks.forEach((c) => fns.indexOf(c) === -1 ? filtered.push(c) : targets.push(c)); + targets.forEach((c) => c()); + render_callbacks = filtered; +} diff --git a/test/runtime/samples/action-update-before-destroy/Component.svelte b/test/runtime/samples/action-update-before-destroy/Component.svelte new file mode 100644 index 0000000000..e38a0fff64 --- /dev/null +++ b/test/runtime/samples/action-update-before-destroy/Component.svelte @@ -0,0 +1,27 @@ + + + +{#if selected} +
{item.id}
+{/if} diff --git a/test/runtime/samples/action-update-before-destroy/_config.js b/test/runtime/samples/action-update-before-destroy/_config.js new file mode 100644 index 0000000000..a822c4d8a5 --- /dev/null +++ b/test/runtime/samples/action-update-before-destroy/_config.js @@ -0,0 +1,16 @@ +export default { + html: ` + +
1
+ `, + async test({ assert, target, window }) { + const button = target.querySelector('button'); + const event = new window.MouseEvent('click'); + const messages = []; + const log = console.log; + console.log = msg => messages.push(msg); + await button.dispatchEvent(event); + console.log = log; + assert.deepEqual(messages, ['afterUpdate', 'onDestroy']); + } +}; diff --git a/test/runtime/samples/action-update-before-destroy/main.svelte b/test/runtime/samples/action-update-before-destroy/main.svelte new file mode 100644 index 0000000000..7f99ce0f38 --- /dev/null +++ b/test/runtime/samples/action-update-before-destroy/main.svelte @@ -0,0 +1,10 @@ + + +{#each Object.values($items) as item (item.id)} + +{/each} From e3e912ab58033c8f4e2b669bc923e2ce30134b8f Mon Sep 17 00:00:00 2001 From: brunnerh Date: Wed, 22 Feb 2023 09:51:26 +0100 Subject: [PATCH 14/70] Fix accessibility of options tutorial. (#8308) Use button instead of non-interactive element (div). (Removes a Svelte a11y warning.) --- .../08-svelte-options/app-a/Todo.svelte | 14 ++++++++------ .../08-svelte-options/app-b/Todo.svelte | 14 ++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/site/content/tutorial/16-special-elements/08-svelte-options/app-a/Todo.svelte b/site/content/tutorial/16-special-elements/08-svelte-options/app-a/Todo.svelte index 57d4dcc79a..cc3856316c 100644 --- a/site/content/tutorial/16-special-elements/08-svelte-options/app-a/Todo.svelte +++ b/site/content/tutorial/16-special-elements/08-svelte-options/app-a/Todo.svelte @@ -4,22 +4,24 @@ export let todo; - let div; + let button; afterUpdate(() => { - flash(div); + flash(button); }); -
+
+ \ No newline at end of file + diff --git a/site/content/tutorial/16-special-elements/08-svelte-options/app-b/Todo.svelte b/site/content/tutorial/16-special-elements/08-svelte-options/app-b/Todo.svelte index fa81d35989..848f167d02 100644 --- a/site/content/tutorial/16-special-elements/08-svelte-options/app-b/Todo.svelte +++ b/site/content/tutorial/16-special-elements/08-svelte-options/app-b/Todo.svelte @@ -6,22 +6,24 @@ export let todo; - let div; + let button; afterUpdate(() => { - flash(div); + flash(button); }); -
+
+ \ No newline at end of file + From a71b8b99585bd636dd0e3ea250c9a092afb0533d Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 22 Feb 2023 16:18:36 +0100 Subject: [PATCH 15/70] chore: improve parser performance (#8303) - fast path for attribute quote marks common case - all regexes exclusively passed into read or match_regex which are only successful if matched at the beginning are altered so that the regex has this condition built in, preventing it from searching past the start index --------- Co-authored-by: Yuichiro Yamashita --- src/compiler/parse/index.ts | 8 ++++++++ src/compiler/parse/read/script.ts | 3 ++- src/compiler/parse/read/style.ts | 5 +++-- src/compiler/parse/state/mustache.ts | 2 +- src/compiler/parse/state/tag.ts | 19 ++++++++++--------- 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/compiler/parse/index.ts b/src/compiler/parse/index.ts index 8de6563c8b..86c27b33cf 100644 --- a/src/compiler/parse/index.ts +++ b/src/compiler/parse/index.ts @@ -132,6 +132,10 @@ export class Parser { return this.template.slice(this.index, this.index + str.length) === str; } + /** + * Match a regex at the current index + * @param pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance + */ match_regex(pattern: RegExp) { const match = pattern.exec(this.template.slice(this.index)); if (!match || match.index !== 0) return null; @@ -148,6 +152,10 @@ export class Parser { } } + /** + * Search for a regex starting at the current index and return the result if it matches + * @param pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance + */ read(pattern: RegExp) { const result = this.match_regex(pattern); if (result) this.index += result.length; diff --git a/src/compiler/parse/read/script.ts b/src/compiler/parse/read/script.ts index 02506ab3d5..43974c26ab 100644 --- a/src/compiler/parse/read/script.ts +++ b/src/compiler/parse/read/script.ts @@ -6,6 +6,7 @@ import parser_errors from '../errors'; import { regex_not_newline_characters } from '../../utils/patterns'; const regex_closing_script_tag = /<\/script\s*>/; +const regex_starts_with_closing_script_tag = /^<\/script\s*>/; function get_context(parser: Parser, attributes: any[], start: number): string { const context = attributes.find(attribute => attribute.name === 'context'); @@ -32,7 +33,7 @@ export default function read_script(parser: Parser, start: number, attributes: N } const source = parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data; - parser.read(regex_closing_script_tag); + parser.read(regex_starts_with_closing_script_tag); let ast: Program; diff --git a/src/compiler/parse/read/style.ts b/src/compiler/parse/read/style.ts index 560d377cec..3ed510c3d0 100644 --- a/src/compiler/parse/read/style.ts +++ b/src/compiler/parse/read/style.ts @@ -7,6 +7,7 @@ import { Style } from '../../interfaces'; import parser_errors from '../errors'; const regex_closing_style_tag = /<\/style\s*>/; +const regex_starts_with_closing_style_tag = /^<\/style\s*>/; export default function read_style(parser: Parser, start: number, attributes: Node[]): Style { const content_start = parser.index; @@ -21,7 +22,7 @@ export default function read_style(parser: Parser, start: number, attributes: No // discard styles when css is disabled if (parser.css_mode === 'none') { - parser.read(regex_closing_style_tag); + parser.read(regex_starts_with_closing_style_tag); return null; } @@ -76,7 +77,7 @@ export default function read_style(parser: Parser, start: number, attributes: No } }); - parser.read(regex_closing_style_tag); + parser.read(regex_starts_with_closing_style_tag); const end = parser.index; diff --git a/src/compiler/parse/state/mustache.ts b/src/compiler/parse/state/mustache.ts index c3b58afe2a..328a043677 100644 --- a/src/compiler/parse/state/mustache.ts +++ b/src/compiler/parse/state/mustache.ts @@ -33,7 +33,7 @@ function trim_whitespace(block: TemplateNode, trim_before: boolean, trim_after: } } -const regex_whitespace_with_closing_curly_brace = /\s*}/; +const regex_whitespace_with_closing_curly_brace = /^\s*}/; export default function mustache(parser: Parser) { const start = parser.index; diff --git a/src/compiler/parse/state/tag.ts b/src/compiler/parse/state/tag.ts index 9d1bf5e5c2..efcff645a7 100644 --- a/src/compiler/parse/state/tag.ts +++ b/src/compiler/parse/state/tag.ts @@ -12,6 +12,9 @@ import { closing_tag_omitted, decode_character_references } from '../utils/html' // eslint-disable-next-line no-useless-escape const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/; +/** Invalid attribute characters if the attribute is not surrounded by quotes */ +const regex_starts_with_invalid_attr_value = /^(\/>|[\s"'=<>`])/; + const meta_tags = new Map([ ['svelte:head', 'Head'], ['svelte:options', 'Options'], @@ -293,7 +296,7 @@ function read_tag_name(parser: Parser) { // eslint-disable-next-line no-useless-escape const regex_token_ending_character = /[\s=\/>"']/; -const regex_quote_characters = /["']/; +const regex_starts_with_quote_characters = /^["']/; function read_attribute(parser: Parser, unique_names: Set) { const start = parser.index; @@ -368,7 +371,7 @@ function read_attribute(parser: Parser, unique_names: Set) { parser.allow_whitespace(); value = read_attribute_value(parser); end = parser.index; - } else if (parser.match_regex(regex_quote_characters)) { + } else if (parser.match_regex(regex_starts_with_quote_characters)) { parser.error(parser_errors.unexpected_token('='), parser.index); } @@ -475,15 +478,13 @@ function read_attribute_value(parser: Parser) { }]; } - const regex = ( - quote_mark === "'" ? /'/ : - quote_mark === '"' ? /"/ : - /(\/>|[\s"'=<>`])/ - ); - let value; try { - value = read_sequence(parser, () => !!parser.match_regex(regex), 'in attribute value'); + value = read_sequence(parser, () => { + // handle common case of quote marks existing outside of regex for performance reasons + if (quote_mark) return parser.match(quote_mark); + return !!parser.match_regex(regex_starts_with_invalid_attr_value); + }, 'in attribute value'); } catch (error) { if (error.code === 'parse-error') { // if the attribute value didn't close + self-closing tag From 35599972233dceee9d2074ad85ebac9f5a43730a Mon Sep 17 00:00:00 2001 From: ngtr6788 <88808276+ngtr6788@users.noreply.github.com> Date: Wed, 22 Feb 2023 11:14:21 -0500 Subject: [PATCH 16/70] feat: implement a11y `aria-activedescendant-has-tabindex` (#8172) #820 --- site/content/docs/06-accessibility-warnings.md | 12 ++++++++++++ src/compiler/compile/compiler_warnings.ts | 4 ++++ src/compiler/compile/nodes/Element.ts | 5 +++++ .../a11y-aria-activedescendant/input.svelte | 17 +++++++++++++++++ .../a11y-aria-activedescendant/warnings.json | 17 +++++++++++++++++ 5 files changed, 55 insertions(+) create mode 100644 test/validator/samples/a11y-aria-activedescendant/input.svelte create mode 100644 test/validator/samples/a11y-aria-activedescendant/warnings.json diff --git a/site/content/docs/06-accessibility-warnings.md b/site/content/docs/06-accessibility-warnings.md index 8d3d44a94e..7aa3873cce 100644 --- a/site/content/docs/06-accessibility-warnings.md +++ b/site/content/docs/06-accessibility-warnings.md @@ -19,6 +19,18 @@ Enforce no `accesskey` on element. Access keys are HTML attributes that allow we --- +### `a11y-aria-activedescendant-has-tabindex` + +An element with `aria-activedescendant` must be tabbable, so it must either have an inherent `tabindex` or declare `tabindex` as an attribute. + +```sv + +
+ +``` + +--- + ### `a11y-aria-attributes` Certain reserved DOM elements do not support ARIA roles, states and properties. This is often because they are not visible, for example `meta`, `html`, `script`, `style`. This rule enforces that these DOM elements do not contain the `aria-*` props. diff --git a/src/compiler/compile/compiler_warnings.ts b/src/compiler/compile/compiler_warnings.ts index 3f552eb8b8..a10fe6155c 100644 --- a/src/compiler/compile/compiler_warnings.ts +++ b/src/compiler/compile/compiler_warnings.ts @@ -187,6 +187,10 @@ export default { code: 'a11y-no-noninteractive-tabindex', message: 'A11y: noninteractive element cannot have nonnegative tabIndex value' }, + a11y_aria_activedescendant_has_tabindex: { + code: 'a11y-aria-activedescendant-has-tabindex', + message: 'A11y: Elements with attribute aria-activedescendant should have tabindex value' + }, redundant_event_modifier_for_touch: { code: 'redundant-event-modifier', message: 'Touch event handlers that don\'t use the \'event\' object are passive by default' diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index 62e0de51bf..8635355a12 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -484,6 +484,11 @@ export default class Element extends Node { component.warn(attribute, compiler_warnings.a11y_incorrect_attribute_type(schema, name)); } } + + // aria-activedescendant-has-tabindex + if (name === 'aria-activedescendant' && !is_interactive_element(this.name, attribute_map) && !attribute_map.has('tabindex')) { + component.warn(attribute, compiler_warnings.a11y_aria_activedescendant_has_tabindex); + } } // aria-role diff --git a/test/validator/samples/a11y-aria-activedescendant/input.svelte b/test/validator/samples/a11y-aria-activedescendant/input.svelte new file mode 100644 index 0000000000..22570e8c50 --- /dev/null +++ b/test/validator/samples/a11y-aria-activedescendant/input.svelte @@ -0,0 +1,17 @@ + + + + + + + + + + +
+
+
+ + +
+ diff --git a/test/validator/samples/a11y-aria-activedescendant/warnings.json b/test/validator/samples/a11y-aria-activedescendant/warnings.json new file mode 100644 index 0000000000..8749c1c125 --- /dev/null +++ b/test/validator/samples/a11y-aria-activedescendant/warnings.json @@ -0,0 +1,17 @@ +[ + { + "code": "a11y-aria-activedescendant-has-tabindex", + "end": { + "character": 568, + "column": 36, + "line": 16 + }, + "message": "A11y: Elements with attribute aria-activedescendant should have tabindex value", + "pos": 537, + "start": { + "character": 537, + "column": 5, + "line": 16 + } + } +] From c9a269c149cf968e5e21946fff8a3d14fa972058 Mon Sep 17 00:00:00 2001 From: Vaibhav Date: Wed, 22 Feb 2023 10:19:33 -0600 Subject: [PATCH 17/70] fix: silence no content a11y warning if the tag has an aria-label (#8299) fixes #8296 --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- src/compiler/compile/nodes/Element.ts | 10 ++++++++++ .../a11y-anchor-aria-label-has-no-content/input.svelte | 1 + .../warnings.json | 1 + 3 files changed, 12 insertions(+) create mode 100644 test/validator/samples/a11y-anchor-aria-label-has-no-content/input.svelte create mode 100644 test/validator/samples/a11y-anchor-aria-label-has-no-content/warnings.json diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index 8635355a12..4d1cb758fe 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -225,6 +225,7 @@ export default class Element extends Node { namespace: string; needs_manual_style_scoping: boolean; tag_expr: Expression; + contains_a11y_label: boolean; get is_dynamic_element() { return this.name === 'svelte:element'; @@ -625,6 +626,7 @@ export default class Element extends Node { const id_attribute = attribute_map.get('id'); const name_attribute = attribute_map.get('name'); const target_attribute = attribute_map.get('target'); + const aria_label_attribute = attribute_map.get('aria-label'); // links with target="_blank" should have noopener or noreferrer: https://developer.chrome.com/docs/lighthouse/best-practices/external-anchors-use-rel-noopener/ // modern browsers add noopener by default, so we only need to check legacy browsers @@ -647,6 +649,13 @@ export default class Element extends Node { } } + if (aria_label_attribute) { + const aria_value = aria_label_attribute.get_static_value(); + if (aria_value != '') { + this.contains_a11y_label = true; + } + } + if (href_attribute) { const href_value = href_attribute.get_static_value(); @@ -930,6 +939,7 @@ export default class Element extends Node { validate_content() { if (!a11y_required_content.has(this.name)) return; + if (this.contains_a11y_label) return; if ( this.bindings .some((binding) => ['textContent', 'innerHTML'].includes(binding.name)) diff --git a/test/validator/samples/a11y-anchor-aria-label-has-no-content/input.svelte b/test/validator/samples/a11y-anchor-aria-label-has-no-content/input.svelte new file mode 100644 index 0000000000..c13df04b22 --- /dev/null +++ b/test/validator/samples/a11y-anchor-aria-label-has-no-content/input.svelte @@ -0,0 +1 @@ + diff --git a/test/validator/samples/a11y-anchor-aria-label-has-no-content/warnings.json b/test/validator/samples/a11y-anchor-aria-label-has-no-content/warnings.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/test/validator/samples/a11y-anchor-aria-label-has-no-content/warnings.json @@ -0,0 +1 @@ +[] From 53de73d08cdf2fb62dca7949767f120f3fac9629 Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Thu, 23 Feb 2023 03:02:42 +0800 Subject: [PATCH 18/70] feat: simpler output for reactive vars if dependencies are all static (#7942) --- src/compiler/compile/Component.ts | 19 +++++- src/compiler/compile/render_dom/index.ts | 17 ++++-- src/compiler/compile/render_dom/invalidate.ts | 1 + src/compiler/interfaces.ts | 1 + .../reactive-class-optimized/expected.js | 11 +--- test/js/samples/reactive-values/expected.js | 60 +++++++++++++++++++ test/js/samples/reactive-values/input.svelte | 7 +++ 7 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 test/js/samples/reactive-values/expected.js create mode 100644 test/js/samples/reactive-values/input.svelte diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index c0d703892b..ffac6e1f07 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -38,6 +38,7 @@ import compiler_warnings from './compiler_warnings'; import compiler_errors from './compiler_errors'; import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore'; import check_enable_sourcemap from './utils/check_enable_sourcemap'; +import is_dynamic from './render_dom/wrappers/shared/is_dynamic'; interface ComponentOptions { namespace?: string; @@ -1380,12 +1381,11 @@ export default class Component { module_dependencies.add(name); } } - const is_writable_or_mutated = - variable && (variable.writable || variable.mutated); + if ( should_add_as_dependency && (!owner || owner === component.instance_scope) && - (name[0] === '$' || is_writable_or_mutated) + (name[0] === '$' || variable) ) { dependencies.add(name); } @@ -1409,6 +1409,19 @@ export default class Component { const { expression } = node.body as ExpressionStatement; const declaration = expression && (expression as AssignmentExpression).left; + const is_dependency_static = Array.from(dependencies).every( + dependency => dependency !== '$$props' && dependency !== '$$restProps' && !is_dynamic(this.var_lookup.get(dependency)) + ); + + if (is_dependency_static) { + assignees.forEach(assignee => { + const variable = component.var_lookup.get(assignee); + if (variable) { + variable.is_reactive_static = true; + } + }); + } + unsorted_reactive_declarations.push({ assignees, dependencies, diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts index 58b7a8317b..bc56f2eb27 100644 --- a/src/compiler/compile/render_dom/index.ts +++ b/src/compiler/compile/render_dom/index.ts @@ -390,13 +390,13 @@ export default function dom( const resubscribable_reactive_store_unsubscribers = reactive_stores .filter(store => { const variable = component.var_lookup.get(store.name.slice(1)); - return variable && (variable.reassigned || variable.export_name); + return variable && (variable.reassigned || variable.export_name) && !variable.is_reactive_static; }) .map(({ name }) => b`$$self.$$.on_destroy.push(() => ${`$$unsubscribe_${name.slice(1)}`}());`); if (has_definition) { - const reactive_declarations: (Node | Node[]) = []; - const fixed_reactive_declarations: Node[] = []; // not really 'reactive' but whatever + const reactive_declarations: Node[] = []; + const fixed_reactive_declarations: Array = []; // not really 'reactive' but whatever component.reactive_declarations.forEach(d => { const dependencies = Array.from(d.dependencies); @@ -417,6 +417,15 @@ export default function dom( reactive_declarations.push(statement); } else { fixed_reactive_declarations.push(statement); + for (const assignee of d.assignees) { + const variable = component.var_lookup.get(assignee); + if (variable && variable.subscribable) { + fixed_reactive_declarations.push(b` + ${component.compile_options.dev && b`@validate_store(${assignee}, '${assignee}');`} + @component_subscribe($$self, ${assignee}, $$value => $$invalidate(${renderer.context_lookup.get('$' + assignee).index}, ${'$' + assignee} = $$value)); + `); + } + } } }); @@ -430,7 +439,7 @@ export default function dom( const name = $name.slice(1); const store = component.var_lookup.get(name); - if (store && (store.reassigned || store.export_name)) { + if (store && (store.reassigned || store.export_name) && !store.is_reactive_static) { const unsubscribe = `$$unsubscribe_${name}`; const subscribe = `$$subscribe_${name}`; const i = renderer.context_lookup.get($name).index; diff --git a/src/compiler/compile/render_dom/invalidate.ts b/src/compiler/compile/render_dom/invalidate.ts index ffc5b6b42e..d9efdb5142 100644 --- a/src/compiler/compile/render_dom/invalidate.ts +++ b/src/compiler/compile/render_dom/invalidate.ts @@ -19,6 +19,7 @@ export function invalidate(renderer: Renderer, scope: Scope, node: Node, names: !variable.hoistable && !variable.global && !variable.module && + !variable.is_reactive_static && ( variable.referenced || variable.subscribable || diff --git a/src/compiler/interfaces.ts b/src/compiler/interfaces.ts index eb2cc336ed..402f9ff5e1 100644 --- a/src/compiler/interfaces.ts +++ b/src/compiler/interfaces.ts @@ -223,6 +223,7 @@ export interface Var { subscribable?: boolean; is_reactive_dependency?: boolean; imported?: boolean; + is_reactive_static?: boolean; } export interface CssResult { diff --git a/test/js/samples/reactive-class-optimized/expected.js b/test/js/samples/reactive-class-optimized/expected.js index 1d0606ad60..f75a4015b0 100644 --- a/test/js/samples/reactive-class-optimized/expected.js +++ b/test/js/samples/reactive-class-optimized/expected.js @@ -9,7 +9,6 @@ import { noop, safe_not_equal, space, - subscribe, toggle_class } from "svelte/internal"; @@ -133,13 +132,8 @@ let reactiveModuleVar = Math.random(); function instance($$self, $$props, $$invalidate) { let reactiveDeclaration; let $reactiveStoreVal; - - let $reactiveDeclaration, - $$unsubscribe_reactiveDeclaration = noop, - $$subscribe_reactiveDeclaration = () => ($$unsubscribe_reactiveDeclaration(), $$unsubscribe_reactiveDeclaration = subscribe(reactiveDeclaration, $$value => $$invalidate(3, $reactiveDeclaration = $$value)), reactiveDeclaration); - + let $reactiveDeclaration; component_subscribe($$self, reactiveStoreVal, $$value => $$invalidate(2, $reactiveStoreVal = $$value)); - $$self.$$.on_destroy.push(() => $$unsubscribe_reactiveDeclaration()); nonReactiveGlobal = Math.random(); const reactiveConst = { x: Math.random() }; reactiveModuleVar += 1; @@ -148,7 +142,8 @@ function instance($$self, $$props, $$invalidate) { reactiveConst.x += 1; } - $: $$subscribe_reactiveDeclaration($$invalidate(1, reactiveDeclaration = reactiveModuleVar * 2)); + $: reactiveDeclaration = reactiveModuleVar * 2; + component_subscribe($$self, reactiveDeclaration, $$value => $$invalidate(3, $reactiveDeclaration = $$value)); return [reactiveConst, reactiveDeclaration, $reactiveStoreVal, $reactiveDeclaration]; } diff --git a/test/js/samples/reactive-values/expected.js b/test/js/samples/reactive-values/expected.js new file mode 100644 index 0000000000..7ed435d6ad --- /dev/null +++ b/test/js/samples/reactive-values/expected.js @@ -0,0 +1,60 @@ +/* generated by Svelte vX.Y.Z */ +import { + SvelteComponent, + detach, + element, + init, + insert, + noop, + safe_not_equal, + set_data, + space, + text +} from "svelte/internal"; + +function create_fragment(ctx) { + let h1; + let t3; + let t4; + + return { + c() { + h1 = element("h1"); + h1.textContent = `Hello ${name}!`; + t3 = space(); + t4 = text(/*foo*/ ctx[0]); + }, + m(target, anchor) { + insert(target, h1, anchor); + insert(target, t3, anchor); + insert(target, t4, anchor); + }, + p(ctx, [dirty]) { + if (dirty & /*foo*/ 1) set_data(t4, /*foo*/ ctx[0]); + }, + i: noop, + o: noop, + d(detaching) { + if (detaching) detach(h1); + if (detaching) detach(t3); + if (detaching) detach(t4); + } + }; +} + +let name = 'world'; + +function instance($$self) { + let foo; + $: foo = name + name; + return [foo]; +} + +class Component extends SvelteComponent { + constructor(options) { + super(); + init(this, options, instance, create_fragment, safe_not_equal, {}); + } +} + +export default Component; \ No newline at end of file diff --git a/test/js/samples/reactive-values/input.svelte b/test/js/samples/reactive-values/input.svelte new file mode 100644 index 0000000000..d713217771 --- /dev/null +++ b/test/js/samples/reactive-values/input.svelte @@ -0,0 +1,7 @@ + + +

Hello {name}!

+{foo} \ No newline at end of file From 709264a94cbd29093349ac86e3edd0acc6af46b0 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 22 Feb 2023 20:08:46 +0100 Subject: [PATCH 19/70] chore: update changelog --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a47296f80..f240db6dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,18 @@ ## Unreleased +* Add a11y warnings: + * `aria-activedescendant-has-tabindex`: elements with `aria-activedescendant` need to have a `tabindex` ([#8172](https://github.com/sveltejs/svelte/pull/8172)) + * +* Omit a11y warning on `