diff --git a/.changeset/brave-walls-destroy.md b/.changeset/brave-walls-destroy.md deleted file mode 100644 index 86737de23f1b..000000000000 --- a/.changeset/brave-walls-destroy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: use internal `get_descriptors` helper diff --git a/.changeset/chilly-dolphins-lick.md b/.changeset/chilly-dolphins-lick.md deleted file mode 100644 index 1941f0a6a0e6..000000000000 --- a/.changeset/chilly-dolphins-lick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: remove selector api diff --git a/.changeset/cuddly-pianos-drop.md b/.changeset/cuddly-pianos-drop.md deleted file mode 100644 index 7993ba6289b9..000000000000 --- a/.changeset/cuddly-pianos-drop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: improve bundle code size diff --git a/.changeset/curly-lizards-dream.md b/.changeset/curly-lizards-dream.md deleted file mode 100644 index 1f7d32f4846c..000000000000 --- a/.changeset/curly-lizards-dream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: coerce attribute value to string before comparison diff --git a/.changeset/eight-steaks-shout.md b/.changeset/eight-steaks-shout.md deleted file mode 100644 index 62dbebafafe4..000000000000 --- a/.changeset/eight-steaks-shout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correct update_block index type diff --git a/.changeset/eighty-bikes-camp.md b/.changeset/eighty-bikes-camp.md deleted file mode 100644 index 273e6ad9968e..000000000000 --- a/.changeset/eighty-bikes-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: handle private fields in `class` in `.svelte.js` files diff --git a/.changeset/fresh-weeks-trade.md b/.changeset/fresh-weeks-trade.md deleted file mode 100644 index 224db4c8960e..000000000000 --- a/.changeset/fresh-weeks-trade.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: make operations lazy diff --git a/.changeset/funny-wombats-argue.md b/.changeset/funny-wombats-argue.md deleted file mode 100644 index 2d5a707a170a..000000000000 --- a/.changeset/funny-wombats-argue.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -fix: allow svelte:self in snippets diff --git a/.changeset/good-pianos-jump.md b/.changeset/good-pianos-jump.md deleted file mode 100644 index 0ae35e5937da..000000000000 --- a/.changeset/good-pianos-jump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: check that snippet is not rendered as a component diff --git a/.changeset/honest-icons-change.md b/.changeset/honest-icons-change.md deleted file mode 100644 index 6791a930d5a6..000000000000 --- a/.changeset/honest-icons-change.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -patch: ensure keyed each block fallback to indexed each block diff --git a/.changeset/itchy-lions-wash.md b/.changeset/itchy-lions-wash.md deleted file mode 100644 index fdd9085d5ec4..000000000000 --- a/.changeset/itchy-lions-wash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -fix: allow member access on directives diff --git a/.changeset/khaki-mails-draw.md b/.changeset/khaki-mails-draw.md deleted file mode 100644 index cebc5770f4e4..000000000000 --- a/.changeset/khaki-mails-draw.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: tighten up signals implementation diff --git a/.changeset/lazy-spiders-think.md b/.changeset/lazy-spiders-think.md deleted file mode 100644 index a0e130fc9c7c..000000000000 --- a/.changeset/lazy-spiders-think.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: handle dynamic selects with falsy select values diff --git a/.changeset/lucky-schools-hang.md b/.changeset/lucky-schools-hang.md deleted file mode 100644 index e83a19f3a06e..000000000000 --- a/.changeset/lucky-schools-hang.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ensure dynamic attributes containing call expressions update diff --git a/.changeset/moody-owls-cry.md b/.changeset/moody-owls-cry.md deleted file mode 100644 index 0d94131ee977..000000000000 --- a/.changeset/moody-owls-cry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: exclude internal props from spread attributes diff --git a/.changeset/nasty-clocks-exercise.md b/.changeset/nasty-clocks-exercise.md deleted file mode 100644 index 7362f8c0b25d..000000000000 --- a/.changeset/nasty-clocks-exercise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: corrects a beforeUpdate/afterUpdate bug diff --git a/.changeset/poor-eggs-enjoy.md b/.changeset/poor-eggs-enjoy.md deleted file mode 100644 index 2ca830a1de40..000000000000 --- a/.changeset/poor-eggs-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add missing files binding diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index d691ff97d88a..000000000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "mode": "pre", - "tag": "next", - "initialVersions": { - "svelte": "5.0.0-next.0", - "svelte-playgrounds-demo": "0.0.1", - "svelte-playgrounds-sandbox": "0.0.1", - "svelte-5-preview": "0.5.0", - "svelte.dev": "1.0.0" - }, - "changesets": [ - "brave-walls-destroy", - "chilly-dolphins-lick", - "cuddly-pianos-drop", - "curly-lizards-dream", - "eight-steaks-shout", - "eighty-bikes-camp", - "fresh-weeks-trade", - "funny-wombats-argue", - "good-pianos-jump", - "honest-icons-change", - "itchy-lions-wash", - "khaki-mails-draw", - "lazy-spiders-think", - "lucky-schools-hang", - "moody-owls-cry", - "nasty-clocks-exercise", - "poor-eggs-enjoy", - "quiet-camels-mate", - "rotten-buckets-develop", - "small-papayas-laugh", - "sour-rules-march", - "strong-lemons-provide", - "tall-shrimps-worry", - "thirty-flowers-sit", - "tiny-kings-whisper" - ] -} diff --git a/.changeset/quiet-camels-mate.md b/.changeset/quiet-camels-mate.md deleted file mode 100644 index 6e723bd4ab16..000000000000 --- a/.changeset/quiet-camels-mate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: improve keyblock treeshaking diff --git a/.changeset/rotten-buckets-develop.md b/.changeset/rotten-buckets-develop.md deleted file mode 100644 index 4664f34eaf3d..000000000000 --- a/.changeset/rotten-buckets-develop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -breaking: svelte 5 alpha diff --git a/.changeset/small-papayas-laugh.md b/.changeset/small-papayas-laugh.md deleted file mode 100644 index 43475946cb58..000000000000 --- a/.changeset/small-papayas-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -breaking: remove Component type, keep using SvelteComponent instead diff --git a/.changeset/sour-rules-march.md b/.changeset/sour-rules-march.md deleted file mode 100644 index f948540af742..000000000000 --- a/.changeset/sour-rules-march.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: only escape attribute values for elements, not components diff --git a/.changeset/strong-lemons-provide.md b/.changeset/strong-lemons-provide.md deleted file mode 100644 index 1c8dc17a24be..000000000000 --- a/.changeset/strong-lemons-provide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: handle event attribute spreading with event delegation diff --git a/.changeset/tall-shrimps-worry.md b/.changeset/tall-shrimps-worry.md deleted file mode 100644 index 7f0931130156..000000000000 --- a/.changeset/tall-shrimps-worry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add snippet marker symbol to children prop diff --git a/.changeset/thirty-flowers-sit.md b/.changeset/thirty-flowers-sit.md deleted file mode 100644 index a3d0927788f5..000000000000 --- a/.changeset/thirty-flowers-sit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: support class exports diff --git a/.changeset/tiny-kings-whisper.md b/.changeset/tiny-kings-whisper.md deleted file mode 100644 index e1e5c3a01dba..000000000000 --- a/.changeset/tiny-kings-whisper.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: treat `slot` the same as other props diff --git a/.editorconfig b/.editorconfig index 90846de5ee64..2f52d9993f71 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,5 +11,5 @@ trim_trailing_whitespace = true [test/**/expected.css] insert_final_newline = false -[{package.json,.travis.yml,.eslintrc.json}] +[package.json] indent_style = space diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index fbab8fbc0d02..000000000000 --- a/.eslintignore +++ /dev/null @@ -1,23 +0,0 @@ -# NOTE: In general this should be kept in sync with .eslintignore - -**/dist/** -**/config/** -**/build/** -**/playgrounds/sandbox/** -**/npm/** -**/*.js.flow -**/*.d.ts -**/playwright*/** -**/vite.config.js -**/vite.prod.config.js -**/node_modules - -**/tests/** - -# documentation can contain invalid examples -documentation/** - -# contains a fork of the REPL which doesn't adhere to eslint rules -sites/svelte-5-preview/** -# Wasn't checked previously, reenable at some point -sites/svelte.dev/** diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index e161e50ef18d..000000000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,54 +0,0 @@ -module.exports = { - extends: ['@sveltejs'], - - // TODO: add runes to eslint-plugin-svelte - globals: { - $state: true, - $derived: true, - $effect: true, - $props: true - }, - - overrides: [ - { - // scripts and playground should be console logging so don't lint against them - files: ['playgrounds/**/*', 'scripts/**/*'], - rules: { - 'no-console': 'off' - } - }, - { - // the playgrounds can use public naming conventions since they're examples - files: ['playgrounds/**/*'], - rules: { - 'lube/svelte-naming-convention': 'off' - } - }, - { - files: ['packages/svelte/src/compiler/**/*'], - rules: { - 'no-var': 'error' - } - } - ], - - plugins: ['lube'], - - rules: { - 'no-console': 'error', - 'lube/svelte-naming-convention': ['error', { fixSameNames: true }], - // eslint isn't that well-versed with JSDoc to know that `foo: /** @type{..} */ (foo)` isn't a violation of this rule, so turn it off - 'object-shorthand': 'off', - 'no-var': 'off', - - // TODO: enable these rules and run `pnpm lint:fix` - // skipping that for now so as to avoid impacting real work - '@typescript-eslint/array-type': 'off', - '@typescript-eslint/no-namespace': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-unused-vars': 'off', - 'prefer-const': 'off', - 'svelte/valid-compile': 'off', - quotes: 'off' - } -}; diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 08aa9adcb95b..d79e8b2e21f3 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -22,13 +22,6 @@ body: placeholder: I would like to see... validations: required: true - - type: textarea - id: alternatives - attributes: - label: Alternatives considered - description: "Please provide a clear and concise description of any alternative solutions or features you've considered." - validations: - required: true - type: dropdown id: importance attributes: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a4819f87baa5..aa5f9732b6d3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,15 +1,10 @@ -## Svelte 5 rewrite - -Please note that [the Svelte codebase is currently being rewritten for Svelte 5](https://svelte.dev/blog/runes). Changes should target Svelte 5, which lives on the default branch (`main`). - -If your PR concerns Svelte 4 (including updates to [svelte.dev.docs](https://svelte.dev/docs)), please ensure the base branch is `svelte-4` and not `main`. - ### Before submitting the PR, please make sure you do the following - [ ] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [ ] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [ ] This message body should clearly illustrate what problems it solves. - [ ] Ideally, include a test that fails without this PR but passes with it. +- [ ] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf8e96b0ac79..cf73a1f6cb02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ env: jobs: Tests: + permissions: {} runs-on: ${{ matrix.os }} timeout-minutes: 15 strategy: @@ -25,10 +26,13 @@ jobs: os: ubuntu-latest - node-version: 20 os: ubuntu-latest + - node-version: 22 + os: ubuntu-latest + steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2.2.4 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: pnpm @@ -38,13 +42,38 @@ jobs: env: CI: true Lint: + permissions: {} runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2.2.4 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 with: node-version: 18 cache: pnpm - - run: 'pnpm i && pnpm check && pnpm lint' + - name: install + run: pnpm install --frozen-lockfile + - name: type check + run: pnpm check + - name: lint + if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail (avoids multiple runs uncovering different issues at different steps) + run: pnpm lint + - name: build and check generated types + if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail + run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally with `cd packages/svelte && pnpm generate:types` and commit the changes after you have reviewed them"; git diff; exit 1); } + Benchmarks: + permissions: {} + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm bench + env: + CI: true diff --git a/.github/workflows/ecosystem-ci-trigger.yml b/.github/workflows/ecosystem-ci-trigger.yml index ce7bf04136ac..71df3242e8f1 100644 --- a/.github/workflows/ecosystem-ci-trigger.yml +++ b/.github/workflows/ecosystem-ci-trigger.yml @@ -9,6 +9,7 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') steps: + - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - uses: actions/github-script@v6 with: script: | diff --git a/.github/workflows/pkg.pr.new-comment.yml b/.github/workflows/pkg.pr.new-comment.yml new file mode 100644 index 000000000000..3f1fca5a0bea --- /dev/null +++ b/.github/workflows/pkg.pr.new-comment.yml @@ -0,0 +1,116 @@ +name: Update pkg.pr.new comment + +on: + workflow_run: + workflows: ['Publish Any Commit'] + types: + - completed + +permissions: + pull-requests: write + +jobs: + build: + name: 'Update comment' + runs-on: ubuntu-latest + steps: + - uses: GitHubSecurityLab/actions-permissions/monitor@v1 + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: output + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + + - run: ls -R . + - name: 'Post or update comment' + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const output = JSON.parse(fs.readFileSync('output.json', 'utf8')); + + const bot_comment_identifier = ``; + + const body = (number) => `${bot_comment_identifier} + + [Playground](https://svelte.dev/playground?version=pr-${number}) + + \`\`\` + ${output.packages.map((p) => `pnpm add https://pkg.pr.new/${p.name}@${number}`).join('\n')} + \`\`\` + `; + + async function find_bot_comment(issue_number) { + if (!issue_number) return null; + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue_number, + }); + return comments.data.find((comment) => + comment.body.includes(bot_comment_identifier) + ); + } + + async function create_or_update_comment(issue_number) { + if (!issue_number) { + console.log('No issue number provided. Cannot post or update comment.'); + return; + } + + const existing_comment = await find_bot_comment(issue_number); + if (existing_comment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing_comment.id, + body: body(issue_number), + }); + } else { + await github.rest.issues.createComment({ + issue_number: issue_number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body(issue_number), + }); + } + } + + async function log_publish_info() { + const svelte_package = output.packages.find(p => p.name === 'svelte'); + const svelte_sha = svelte_package.url.replace(/^.+@([^@]+)$/, '$1'); + console.log('\n' + '='.repeat(50)); + console.log('Publish Information'); + console.log('='.repeat(50)); + console.log('\nPublished Packages:'); + console.log(output.packages.map((p) => `${p.name} - pnpm add https://pkg.pr.new/${p.name}@${p.url.replace(/^.+@([^@]+)$/, '$1')}`).join('\n')); + if(svelte_sha){ + console.log('\nPlayground URL:'); + console.log(`\nhttps://svelte.dev/playground?version=commit-${svelte_sha}`) + } + console.log('\n' + '='.repeat(50)); + } + + if (output.event_name === 'pull_request') { + if (output.number) { + await create_or_update_comment(output.number); + } + } else if (output.event_name === 'push') { + const pull_requests = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + head: `${context.repo.owner}:${output.ref.replace('refs/heads/', '')}`, + }); + + if (pull_requests.data.length > 0) { + await create_or_update_comment(pull_requests.data[0].number); + } else { + console.log( + 'No open pull request found for this push. Logging publish information to console:' + ); + await log_publish_info(); + } + } diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml new file mode 100644 index 000000000000..b2b521dc6fdd --- /dev/null +++ b/.github/workflows/pkg.pr.new.yml @@ -0,0 +1,44 @@ +name: Publish Any Commit +on: [push, pull_request] + +jobs: + build: + permissions: {} + + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - run: corepack enable + - uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm build + + - run: pnpx pkg-pr-new publish --comment=off --json output.json --compact --no-template './packages/svelte' + - name: Add metadata to output + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const output = JSON.parse(fs.readFileSync('output.json', 'utf8')); + output.number = context.issue.number; + output.event_name = context.eventName; + output.ref = context.ref; + fs.writeFileSync('output.json', JSON.stringify(output), 'utf8'); + - name: Upload output + uses: actions/upload-artifact@v4 + with: + name: output + path: ./output.json + + - run: ls -R . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24b1229b4296..6debe5662a88 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,23 +12,29 @@ jobs: if: github.repository == 'sveltejs/svelte' permissions: contents: write # to create release (changesets/action) + id-token: write # OpenID Connect token needed for provenance pull-requests: write # to create pull request (changesets/action) name: Release runs-on: ubuntu-latest steps: + - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits fetch-depth: 0 - - uses: pnpm/action-setup@v2.2.4 + - uses: pnpm/action-setup@v4 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18.x cache: pnpm - - run: pnpm install --frozen-lockfile + - name: Install + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally with `cd packages/svelte && pnpm generate:types` and commit the changes after you have reviewed them"; git diff; exit 1); } - name: Create Release Pull Request or Publish to npm id: changesets @@ -38,4 +44,5 @@ jobs: publish: pnpm changeset:publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_CONFIG_PROVENANCE: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 16cac3b27f51..d50343766485 100644 --- a/.gitignore +++ b/.gitignore @@ -16,10 +16,11 @@ coverage .env.test # build output -dist .vercel # OS-specific .DS_Store tmp + +benchmarking/compare/.results diff --git a/.prettierignore b/.prettierignore index 36dcaeb08f04..d5c124353c37 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,25 +1,34 @@ -# NOTE: In general this should be kept in sync with .eslintignore +documentation/docs/ packages/**/dist/*.js packages/**/build/*.js packages/**/npm/**/* packages/**/config/*.js + +# packages/svelte +packages/svelte/messages/**/*.md +packages/svelte/src/compiler/errors.js +packages/svelte/src/compiler/warnings.js +packages/svelte/src/internal/client/errors.js +packages/svelte/src/internal/client/warnings.js +packages/svelte/src/internal/shared/errors.js +packages/svelte/src/internal/shared/warnings.js +packages/svelte/src/internal/server/errors.js +packages/svelte/tests/migrate/samples/*/output.svelte packages/svelte/tests/**/*.svelte packages/svelte/tests/**/_expected* packages/svelte/tests/**/_actual* packages/svelte/tests/**/expected* packages/svelte/tests/**/_output packages/svelte/tests/**/shards/*.test.js -packages/svelte/tests/hydration/samples/*/_before.html -packages/svelte/tests/hydration/samples/*/_before_head.html -packages/svelte/tests/hydration/samples/*/_after.html -packages/svelte/tests/hydration/samples/*/_after_head.html +packages/svelte/tests/hydration/samples/*/_expected.html +packages/svelte/tests/hydration/samples/*/_override.html packages/svelte/types -packages/svelte/compiler.cjs -playgrounds/demo/src +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 @@ -33,7 +42,6 @@ sites/svelte.dev/src/lib/generated **/.vercel .github/CODEOWNERS .prettierignore -.eslintignore .changeset pnpm-lock.yaml pnpm-workspace.yaml diff --git a/.vscode/launch.json b/.vscode/launch.json index 41d8017ce29f..142965ada292 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,33 +1,14 @@ { "version": "0.2.0", "configurations": [ - { - "type": "chrome", - "request": "launch", - "name": "Playground: Browser", - "url": "http://localhost:10001" - }, - { - "type": "node", - "request": "launch", - "runtimeArgs": ["--watch"], - "name": "Playground: Server", - "outputCapture": "std", - "program": "start.js", - "cwd": "${workspaceFolder}/playgrounds/demo", - "cascadeTerminateToConfigurations": ["Playground: Browser"] - }, { "type": "node", "request": "launch", "name": "Run sandbox", - "program": "${workspaceFolder}/playgrounds/sandbox/run.js" - } - ], - "compounds": [ - { - "name": "Playground: Full", - "configurations": ["Playground: Server", "Playground: Browser"] + "program": "${workspaceFolder}/playgrounds/sandbox/run.js", + "env": { + "NODE_OPTIONS": "--stack-trace-limit=10000" + } } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 3446d68abecb..21a2a11c84e3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "search.exclude": { "sites/svelte-5-preview/static/*": true - } + }, + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 924a0a752dcd..0e2628f84f77 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ There are many ways to contribute to Svelte, and many of them do not involve wri - Simply start using Svelte. Go through the [Getting Started](https://svelte.dev/docs#getting-started) guide. Does everything work as expected? If not, we're always looking for improvements. Let us know by [opening an issue](#reporting-new-issues). - Look through the [open issues](https://github.com/sveltejs/svelte/issues). A good starting point would be issues tagged [good first issue](https://github.com/sveltejs/svelte/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). Provide workarounds, ask for clarification, or suggest labels. Help [triage issues](#triaging-issues-and-pull-requests). - If you find an issue you would like to fix, [open a pull request](#pull-requests). -- Read through our [tutorials](https://learn.svelte.dev/). If you find anything that is confusing or can be improved, you can make edits by clicking "Edit this page" at the bottom left of the tutorial page. +- Read through our [tutorials](https://svelte.dev/tutorial). If you find anything that is confusing or can be improved, you can make edits by clicking "Edit this page" at the bottom left of the tutorial page. - Take a look at the [features requested](https://github.com/sveltejs/svelte/labels/feature%20request) by others in the community and consider opening a pull request if you see something you want to work on. Contributions are very welcome. If you think you need help planning your contribution, please ping us on Discord at [svelte.dev/chat](https://svelte.dev/chat) and let us know you are looking for a bit of help. @@ -43,7 +43,7 @@ The maintainers meet on the final Saturday of each month. While these meetings a ### Prioritization -We do our best to review PRs and RFCs as they are sent, but it is difficult to keep up. We welcome help in reviewing PRs, RFC, and issues. If an item aligns with the current priority on our [roadmap](https://svelte.dev/roadmap), it is more likely to be reviewed quickly. PRs to the most important and active ones repositories get reviewed more quickly while PRs to smaller inactive repos may sit for a bit before we periodically come by and review the pending PRs in a batch. +We do our best to review PRs and RFCs as they are sent, but it is difficult to keep up. We welcome help in reviewing PRs, RFCs, and issues. If an item aligns with the current priority on our [roadmap](https://svelte.dev/roadmap), it is more likely to be reviewed quickly. PRs to the most important and active ones repositories get reviewed more quickly while PRs to smaller inactive repos may sit for a bit before we periodically come by and review the pending PRs in a batch. ## Bugs @@ -51,7 +51,7 @@ We use [GitHub issues](https://github.com/sveltejs/svelte/issues) for our public If you have questions about using Svelte, contact us on Discord at [svelte.dev/chat](https://svelte.dev/chat), and we will do our best to answer your questions. -If you see anything you'd like to be implemented, create a [feature request issue](https://github.com/sveltejs/svelte/issues/new?template=feature_request.md) +If you see anything you'd like to be implemented, create a [feature request issue](https://github.com/sveltejs/svelte/issues/new?template=feature_request.yml). ### Reporting new issues @@ -62,8 +62,6 @@ When [opening a new issue](https://github.com/sveltejs/svelte/issues/new/choose) ## Pull requests -> HEADS UP: Svelte 5 will likely change a lot on the compiler. For that reason, please don't open PRs that are large in scope, touch more than a couple of files etc. In other words, bug fixes are fine, but big feature PRs will likely not be merged. - ### Proposing a change If you would like to request a new feature or enhancement but are not yet thinking about opening a pull request, you can also file an issue with [feature template](https://github.com/sveltejs/svelte/issues/new?template=feature_request.yml). @@ -74,10 +72,11 @@ Small pull requests are much easier to review and more likely to get merged. ### Installation -1. Ensure you have [pnpm](https://pnpm.io/installation) installed -1. After cloning the repository, run `pnpm install`. You can do this in the root directory or in the `svelte` project -1. Move into the `svelte` directory with `cd packages/svelte` -1. To compile in watch mode, run `pnpm dev` +Ensure you have [pnpm](https://pnpm.io/installation) installed. After cloning the repository, run `pnpm install`. + +### Developing + +To build the UMD version of `svelte/compiler` (this is only necessary for CommonJS consumers, or in-browser use), run `pnpm build` inside `packages/svelte`. To rebuild whenever source files change, run `pnpm dev`. ### Creating a branch @@ -100,18 +99,28 @@ Test samples are kept in `/test/xxx/samples` folder. > PREREQUISITE: Install chromium via playwright by running `pnpm playwright install chromium` 1. To run test, run `pnpm test`. -1. To run test for a specific feature, you can use the `-g` (aka `--grep`) option. For example, to only run test involving transitions, run `pnpm test -- -g transition`. +1. To run a particular test suite, use `pnpm test `, for example: -##### Running solo test + ```bash + pnpm test validator + ``` -1. To run only one test, rename the test sample folder to end with `.solo`. For example, to run the `test/js/samples/action` only, rename it to `test/js/samples/action.solo`. -1. To run only one test suite, rename the test suite folder to end with `.solo`. For example, to run the `test/js` test suite only, rename it to `test/js.solo`. -1. Remember to rename the test folder back. The CI will fail if there's a solo test. +1. To filter tests _within_ a test suite, use `pnpm test -- -t `, for example: + + ```bash + 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.) ##### Updating `.expected` files -1. Tests suites like `css`, `js`, `server-side-rendering` asserts that the generated output has to match the content in the `.expected` file. For example, in the `js` test suites, the generated js code is compared against the content in `expected.js`. -1. To update the content of the `.expected` file, run the test with `--update` flag. (`pnpm test --update`) +1. Tests suites like `snapshot` and `parser` assert that the generated output matches the existing snapshot. +1. To update these snapshots, run `UPDATE_SNAPSHOTS=true pnpm test`. + +### Typechecking + +To typecheck the codebase, run `pnpm check` inside `packages/svelte`. To typecheck in watch mode, run `pnpm check:watch`. ### Style guide @@ -122,6 +131,10 @@ Test samples are kept in `/test/xxx/samples` folder. - `snake_case` for internal variable names and methods. - `camelCase` for public variable names and methods. +### Generating types + +Types are auto-generated from the source, but the result is checked in to ensure no accidental changes slip through. Run `pnpm generate:types` to regenerate the types. + ### Sending your pull request Please make sure the following is done when submitting a pull request: @@ -130,7 +143,7 @@ Please make sure the following is done when submitting a pull request: 1. Make sure your code lints (`pnpm lint`). 1. Make sure your tests pass (`pnpm test`). -All pull requests should be opened against the `main` branch. Make sure the PR does only one thing, otherwise please split it. +All pull requests should be opened against the `main` branch. Make sure the PR does only one thing, otherwise please split it. If this change should contribute to a version bump, run `npx changeset` at the root of the repository after a code change and select the appropriate packages. #### Breaking changes @@ -145,6 +158,10 @@ When adding a new breaking change, follow this template in your pull request: - **Severity (number of people affected x effort)**: ``` +### Reviewing pull requests + +If you'd like to manually test a pull request in another pnpm project, you can do so by running `pnpm add -D "github:sveltejs/svelte#path:packages/svelte&branch-name"` in that project. + ## License By contributing to Svelte, you agree that your contributions will be licensed under its [MIT license](https://github.com/sveltejs/svelte/blob/master/LICENSE.md). diff --git a/FUNDING.json b/FUNDING.json new file mode 100644 index 000000000000..7ed51db691e6 --- /dev/null +++ b/FUNDING.json @@ -0,0 +1,7 @@ +{ + "drips": { + "ethereum": { + "ownedBy": "0xCE08E02c37d90d75C2bf7D9e55f7606C8DB80E70" + } + } +} diff --git a/LICENSE.md b/LICENSE.md index aa744067687d..f872adf738de 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2016-23 [these people](https://github.com/sveltejs/svelte/graphs/contributors) +Copyright (c) 2016-2025 [Svelte Contributors](https://github.com/sveltejs/svelte/graphs/contributors) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 4dbfee3823f8..7ea71647521a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ -[![Cybernetically enhanced web apps: Svelte](https://sveltejs.github.io/assets/banner.png)](https://svelte.dev) + + + + Svelte - web development for the rest of us + + -[![license](https://img.shields.io/npm/l/svelte.svg)](LICENSE.md) [![Chat](https://img.shields.io/discord/457912077277855764?label=chat&logo=discord)](https://svelte.dev/chat) +[![License](https://img.shields.io/npm/l/svelte.svg)](LICENSE.md) [![Chat](https://img.shields.io/discord/457912077277855764?label=chat&logo=discord)](https://svelte.dev/chat) ## What is Svelte? @@ -22,51 +27,7 @@ You may view [our roadmap](https://svelte.dev/roadmap) if you'd like to see what ## Contributing -Please see the [Contributing Guide](CONTRIBUTING.md) and [svelte package](packages/svelte) for contributing to Svelte. - -### Development - -Pull requests are encouraged and always welcome. [Pick an issue](https://github.com/sveltejs/svelte/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) and help us out! - -To install and work on Svelte locally: - -```bash -git clone https://github.com/sveltejs/svelte.git -cd svelte -pnpm install -``` - -> Do not use Yarn to install the dependencies, as the specific package versions in `pnpm-lock.json` are used to build and test Svelte. - -To build the compiler and all the other modules included in the package: - -```bash -pnpm build -``` - -To watch for changes and continually rebuild the package (this is useful if you're using [`pnpm link`](https://pnpm.io/cli/link) to test out changes in a project locally): - -```bash -pnpm dev -``` - -The compiler is written in JavaScript and uses [JSDoc](https://jsdoc.app/index.html) comments for type-checking. - -### Running Tests - -```bash -pnpm test -``` - -To filter tests, use `-g` (aka `--grep`). For example, to only run tests involving transitions: - -```bash -pnpm test -- -g transition -``` - -### svelte.dev - -The source code for https://svelte.dev lives in the [sites](https://github.com/sveltejs/svelte/tree/master/sites/svelte.dev) folder, with all the documentation right [here](https://github.com/sveltejs/svelte/tree/master/documentation). The site is built with [SvelteKit](https://kit.svelte.dev). +Please see the [Contributing Guide](CONTRIBUTING.md) and the [`svelte`](packages/svelte) package for information on contributing to Svelte. ## Is svelte.dev down? diff --git a/assets/banner.png b/assets/banner.png new file mode 100644 index 000000000000..3428b278bfa3 Binary files /dev/null and b/assets/banner.png differ diff --git a/assets/banner_dark.png b/assets/banner_dark.png new file mode 100644 index 000000000000..1adba40d8e2f Binary files /dev/null and b/assets/banner_dark.png differ diff --git a/benchmarking/.gitignore b/benchmarking/.gitignore new file mode 100644 index 000000000000..53752db253e3 --- /dev/null +++ b/benchmarking/.gitignore @@ -0,0 +1 @@ +output diff --git a/benchmarking/benchmarks/reactivity/index.js b/benchmarking/benchmarks/reactivity/index.js new file mode 100644 index 000000000000..58b3f5cb295b --- /dev/null +++ b/benchmarking/benchmarks/reactivity/index.js @@ -0,0 +1,55 @@ +import { kairo_avoidable_owned, kairo_avoidable_unowned } from './kairo/kairo_avoidable.js'; +import { kairo_broad_owned, kairo_broad_unowned } from './kairo/kairo_broad.js'; +import { kairo_deep_owned, kairo_deep_unowned } from './kairo/kairo_deep.js'; +import { kairo_diamond_owned, kairo_diamond_unowned } from './kairo/kairo_diamond.js'; +import { kairo_mux_unowned, kairo_mux_owned } from './kairo/kairo_mux.js'; +import { kairo_repeated_unowned, kairo_repeated_owned } from './kairo/kairo_repeated.js'; +import { kairo_triangle_owned, kairo_triangle_unowned } from './kairo/kairo_triangle.js'; +import { kairo_unstable_owned, kairo_unstable_unowned } from './kairo/kairo_unstable.js'; +import { mol_bench_owned, mol_bench_unowned } from './mol_bench.js'; +import { + sbench_create_0to1, + sbench_create_1000to1, + sbench_create_1to1, + sbench_create_1to1000, + sbench_create_1to2, + sbench_create_1to4, + sbench_create_1to8, + sbench_create_2to1, + sbench_create_4to1, + sbench_create_signals +} from './sbench.js'; + +// This benchmark has been adapted from the js-reactivity-benchmark (https://github.com/milomg/js-reactivity-benchmark) +// Not all tests are the same, and many parts have been tweaked to capture different data. + +export const reactivity_benchmarks = [ + sbench_create_signals, + sbench_create_0to1, + sbench_create_1to1, + sbench_create_2to1, + sbench_create_4to1, + sbench_create_1000to1, + sbench_create_1to2, + sbench_create_1to4, + sbench_create_1to8, + sbench_create_1to1000, + kairo_avoidable_owned, + kairo_avoidable_unowned, + kairo_broad_owned, + kairo_broad_unowned, + kairo_deep_owned, + kairo_deep_unowned, + kairo_diamond_owned, + kairo_diamond_unowned, + kairo_triangle_owned, + kairo_triangle_unowned, + kairo_mux_owned, + kairo_mux_unowned, + kairo_repeated_owned, + kairo_repeated_unowned, + kairo_unstable_owned, + kairo_unstable_unowned, + mol_bench_owned, + mol_bench_unowned +]; diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js b/benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js new file mode 100644 index 000000000000..9daea6de99cb --- /dev/null +++ b/benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js @@ -0,0 +1,91 @@ +import { assert, fastest_test } from '../../../utils.js'; +import * as $ from 'svelte/internal/client'; +import { busy } from './util.js'; + +function setup() { + let head = $.state(0); + let computed1 = $.derived(() => $.get(head)); + let computed2 = $.derived(() => ($.get(computed1), 0)); + let computed3 = $.derived(() => (busy(), $.get(computed2) + 1)); // heavy computation + let computed4 = $.derived(() => $.get(computed3) + 2); + let computed5 = $.derived(() => $.get(computed4) + 3); + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(computed5); + busy(); // heavy side effect + }); + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + assert($.get(computed5) === 6); + for (let i = 0; i < 1000; i++) { + $.flush(() => { + $.set(head, i); + }); + assert($.get(computed5) === 6); + } + } + }; +} + +export async function kairo_avoidable_unowned() { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(); + destroy(); + } + + const { run, destroy } = setup(); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(); + } + }); + + destroy(); + + return { + benchmark: 'kairo_avoidable_unowned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function kairo_avoidable_owned() { + let run, destroy; + + const destroy_owned = $.effect_root(() => { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(); + destroy(); + } + + ({ run, destroy } = setup()); + }); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(); + } + }); + + // @ts-ignore + destroy(); + destroy_owned(); + + return { + benchmark: 'kairo_avoidable_owned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_broad.js b/benchmarking/benchmarks/reactivity/kairo/kairo_broad.js new file mode 100644 index 000000000000..8dc5710c87db --- /dev/null +++ b/benchmarking/benchmarks/reactivity/kairo/kairo_broad.js @@ -0,0 +1,97 @@ +import { assert, fastest_test } from '../../../utils.js'; +import * as $ from 'svelte/internal/client'; + +function setup() { + let head = $.state(0); + let last = head; + let counter = 0; + + const destroy = $.effect_root(() => { + for (let i = 0; i < 50; i++) { + let current = $.derived(() => { + return $.get(head) + i; + }); + let current2 = $.derived(() => { + return $.get(current) + 1; + }); + $.render_effect(() => { + $.get(current2); + counter++; + }); + last = current2; + } + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + counter = 0; + for (let i = 0; i < 50; i++) { + $.flush(() => { + $.set(head, i); + }); + assert($.get(last) === i + 50); + } + assert(counter === 50 * 50); + } + }; +} + +export async function kairo_broad_unowned() { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(); + destroy(); + } + + const { run, destroy } = setup(); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(); + } + }); + + destroy(); + + return { + benchmark: 'kairo_broad_unowned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function kairo_broad_owned() { + let run, destroy; + + const destroy_owned = $.effect_root(() => { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(); + destroy(); + } + + ({ run, destroy } = setup()); + }); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(); + } + }); + + // @ts-ignore + destroy(); + destroy_owned(); + + return { + benchmark: 'kairo_broad_owned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_deep.js b/benchmarking/benchmarks/reactivity/kairo/kairo_deep.js new file mode 100644 index 000000000000..8690c85f864a --- /dev/null +++ b/benchmarking/benchmarks/reactivity/kairo/kairo_deep.js @@ -0,0 +1,97 @@ +import { assert, fastest_test } from '../../../utils.js'; +import * as $ from 'svelte/internal/client'; + +let len = 50; +const iter = 50; + +function setup() { + let head = $.state(0); + let current = head; + for (let i = 0; i < len; i++) { + let c = current; + current = $.derived(() => { + return $.get(c) + 1; + }); + } + let counter = 0; + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(current); + counter++; + }); + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + counter = 0; + for (let i = 0; i < iter; i++) { + $.flush(() => { + $.set(head, i); + }); + assert($.get(current) === len + i); + } + assert(counter === iter); + } + }; +} + +export async function kairo_deep_unowned() { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(); + destroy(); + } + + const { run, destroy } = setup(); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(); + } + }); + + destroy(); + + return { + benchmark: 'kairo_deep_unowned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function kairo_deep_owned() { + let run, destroy; + + const destroy_owned = $.effect_root(() => { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(); + destroy(); + } + + ({ run, destroy } = setup()); + }); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(); + } + }); + + // @ts-ignore + destroy(); + destroy_owned(); + + return { + benchmark: 'kairo_deep_owned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js b/benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js new file mode 100644 index 000000000000..bf4e07ee8962 --- /dev/null +++ b/benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js @@ -0,0 +1,101 @@ +import { assert, fastest_test } from '../../../utils.js'; +import * as $ from 'svelte/internal/client'; + +let width = 5; + +function setup() { + let head = $.state(0); + let current = []; + for (let i = 0; i < width; i++) { + current.push( + $.derived(() => { + return $.get(head) + 1; + }) + ); + } + let sum = $.derived(() => { + return current.map((x) => $.get(x)).reduce((a, b) => a + b, 0); + }); + let counter = 0; + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(sum); + counter++; + }); + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + assert($.get(sum) === 2 * width); + counter = 0; + for (let i = 0; i < 500; i++) { + $.flush(() => { + $.set(head, i); + }); + assert($.get(sum) === (i + 1) * width); + } + assert(counter === 500); + } + }; +} + +export async function kairo_diamond_unowned() { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(); + destroy(); + } + + const { run, destroy } = setup(); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(); + } + }); + + destroy(); + + return { + benchmark: 'kairo_diamond_unowned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function kairo_diamond_owned() { + let run, destroy; + + const destroy_owned = $.effect_root(() => { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(); + destroy(); + } + + ({ run, destroy } = setup()); + }); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(); + } + }); + + // @ts-ignore + destroy(); + destroy_owned(); + + return { + benchmark: 'kairo_diamond_owned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_mux.js b/benchmarking/benchmarks/reactivity/kairo/kairo_mux.js new file mode 100644 index 000000000000..fc252a27b5f8 --- /dev/null +++ b/benchmarking/benchmarks/reactivity/kairo/kairo_mux.js @@ -0,0 +1,94 @@ +import { assert, fastest_test } from '../../../utils.js'; +import * as $ from 'svelte/internal/client'; + +function setup() { + let heads = new Array(100).fill(null).map((_) => $.state(0)); + const mux = $.derived(() => { + return Object.fromEntries(heads.map((h) => $.get(h)).entries()); + }); + const splited = heads + .map((_, index) => $.derived(() => $.get(mux)[index])) + .map((x) => $.derived(() => $.get(x) + 1)); + + const destroy = $.effect_root(() => { + splited.forEach((x) => { + $.render_effect(() => { + $.get(x); + }); + }); + }); + + return { + destroy, + run() { + for (let i = 0; i < 10; i++) { + $.flush(() => { + $.set(heads[i], i); + }); + assert($.get(splited[i]) === i + 1); + } + for (let i = 0; i < 10; i++) { + $.flush(() => { + $.set(heads[i], i * 2); + }); + assert($.get(splited[i]) === i * 2 + 1); + } + } + }; +} + +export async function kairo_mux_unowned() { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(); + destroy(); + } + + const { run, destroy } = setup(); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(); + } + }); + + destroy(); + + return { + benchmark: 'kairo_mux_unowned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function kairo_mux_owned() { + let run, destroy; + + const destroy_owned = $.effect_root(() => { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(); + destroy(); + } + + ({ run, destroy } = setup()); + }); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(); + } + }); + + // @ts-ignore + destroy(); + destroy_owned(); + + return { + benchmark: 'kairo_mux_owned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js b/benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js new file mode 100644 index 000000000000..3bee06ca0e8f --- /dev/null +++ b/benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js @@ -0,0 +1,98 @@ +import { assert, fastest_test } from '../../../utils.js'; +import * as $ from 'svelte/internal/client'; + +let size = 30; + +function setup() { + let head = $.state(0); + let current = $.derived(() => { + let result = 0; + for (let i = 0; i < size; i++) { + result += $.get(head); + } + return result; + }); + + let counter = 0; + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(current); + counter++; + }); + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + assert($.get(current) === size); + counter = 0; + for (let i = 0; i < 100; i++) { + $.flush(() => { + $.set(head, i); + }); + assert($.get(current) === i * size); + } + assert(counter === 100); + } + }; +} + +export async function kairo_repeated_unowned() { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(); + destroy(); + } + + const { run, destroy } = setup(); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(); + } + }); + + destroy(); + + return { + benchmark: 'kairo_repeated_unowned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function kairo_repeated_owned() { + let run, destroy; + + const destroy_owned = $.effect_root(() => { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(); + destroy(); + } + + ({ run, destroy } = setup()); + }); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(); + } + }); + + // @ts-ignore + destroy(); + destroy_owned(); + + return { + benchmark: 'kairo_repeated_owned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js b/benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js new file mode 100644 index 000000000000..11a419a52e7b --- /dev/null +++ b/benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js @@ -0,0 +1,111 @@ +import { assert, fastest_test } from '../../../utils.js'; +import * as $ from 'svelte/internal/client'; + +let width = 10; + +function count(number) { + return new Array(number) + .fill(0) + .map((_, i) => i + 1) + .reduce((x, y) => x + y, 0); +} + +function setup() { + let head = $.state(0); + let current = head; + let list = []; + for (let i = 0; i < width; i++) { + let c = current; + list.push(current); + current = $.derived(() => { + return $.get(c) + 1; + }); + } + let sum = $.derived(() => { + return list.map((x) => $.get(x)).reduce((a, b) => a + b, 0); + }); + + let counter = 0; + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(sum); + counter++; + }); + }); + + return { + destroy, + run() { + const constant = count(width); + $.flush(() => { + $.set(head, 1); + }); + assert($.get(sum) === constant); + counter = 0; + for (let i = 0; i < 100; i++) { + $.flush(() => { + $.set(head, i); + }); + assert($.get(sum) === constant - width + i * width); + } + assert(counter === 100); + } + }; +} + +export async function kairo_triangle_unowned() { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(); + destroy(); + } + + const { run, destroy } = setup(); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(); + } + }); + + destroy(); + + return { + benchmark: 'kairo_triangle_unowned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function kairo_triangle_owned() { + let run, destroy; + + const destroy_owned = $.effect_root(() => { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(); + destroy(); + } + + ({ run, destroy } = setup()); + }); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(); + } + }); + + // @ts-ignore + destroy(); + destroy_owned(); + + return { + benchmark: 'kairo_triangle_owned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js b/benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js new file mode 100644 index 000000000000..54eb732cb29d --- /dev/null +++ b/benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js @@ -0,0 +1,97 @@ +import { assert, fastest_test } from '../../../utils.js'; +import * as $ from 'svelte/internal/client'; + +function setup() { + let head = $.state(0); + const double = $.derived(() => $.get(head) * 2); + const inverse = $.derived(() => -$.get(head)); + let current = $.derived(() => { + let result = 0; + for (let i = 0; i < 20; i++) { + result += $.get(head) % 2 ? $.get(double) : $.get(inverse); + } + return result; + }); + + let counter = 0; + + const destroy = $.effect_root(() => { + $.render_effect(() => { + $.get(current); + counter++; + }); + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + assert($.get(current) === 40); + counter = 0; + for (let i = 0; i < 100; i++) { + $.flush(() => { + $.set(head, i); + }); + } + assert(counter === 100); + } + }; +} + +export async function kairo_unstable_unowned() { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(); + destroy(); + } + + const { run, destroy } = setup(); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(); + } + }); + + destroy(); + + return { + benchmark: 'kairo_unstable_unowned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function kairo_unstable_owned() { + let run, destroy; + + const destroy_owned = $.effect_root(() => { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(); + destroy(); + } + + ({ run, destroy } = setup()); + }); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1000; i++) { + run(); + } + }); + + // @ts-ignore + destroy(); + destroy_owned(); + + return { + benchmark: 'kairo_unstable_owned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} diff --git a/benchmarking/benchmarks/reactivity/kairo/util.js b/benchmarking/benchmarks/reactivity/kairo/util.js new file mode 100644 index 000000000000..75e3641ab964 --- /dev/null +++ b/benchmarking/benchmarks/reactivity/kairo/util.js @@ -0,0 +1,6 @@ +export function busy() { + let a = 0; + for (let i = 0; i < 1_00; i++) { + a++; + } +} diff --git a/benchmarking/benchmarks/reactivity/mol_bench.js b/benchmarking/benchmarks/reactivity/mol_bench.js new file mode 100644 index 000000000000..536b078d74a4 --- /dev/null +++ b/benchmarking/benchmarks/reactivity/mol_bench.js @@ -0,0 +1,121 @@ +import { assert, fastest_test } from '../../utils.js'; +import * as $ from 'svelte/internal/client'; + +/** + * @param {number} n + */ +function fib(n) { + if (n < 2) return 1; + return fib(n - 1) + fib(n - 2); +} + +/** + * @param {number} n + */ +function hard(n) { + return n + fib(16); +} + +const numbers = Array.from({ length: 5 }, (_, i) => i); + +function setup() { + let res = []; + const A = $.state(0); + const B = $.state(0); + const C = $.derived(() => ($.get(A) % 2) + ($.get(B) % 2)); + const D = $.derived(() => numbers.map((i) => i + ($.get(A) % 2) - ($.get(B) % 2))); + D.equals = function (/** @type {number[]} */ l) { + var r = this.v; + return r !== null && l.length === r.length && l.every((v, i) => v === r[i]); + }; + const E = $.derived(() => hard($.get(C) + $.get(A) + $.get(D)[0])); + const F = $.derived(() => hard($.get(D)[0] && $.get(B))); + const G = $.derived(() => $.get(C) + ($.get(C) || $.get(E) % 2) + $.get(D)[0] + $.get(F)); + + const destroy = $.effect_root(() => { + $.render_effect(() => { + res.push(hard($.get(G))); + }); + $.render_effect(() => { + res.push($.get(G)); + }); + $.render_effect(() => { + res.push(hard($.get(F))); + }); + }); + + return { + destroy, + /** + * @param {number} i + */ + run(i) { + res.length = 0; + $.flush(() => { + $.set(B, 1); + $.set(A, 1 + i * 2); + }); + $.flush(() => { + $.set(A, 2 + i * 2); + $.set(B, 2); + }); + assert(res[0] === 3198 && res[1] === 1601 && res[2] === 3195 && res[3] === 1598); + } + }; +} + +export async function mol_bench_owned() { + let run, destroy; + + const destroy_owned = $.effect_root(() => { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(0); + destroy(); + } + + ({ run, destroy } = setup()); + }); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1e4; i++) { + run(i); + } + }); + + // @ts-ignore + destroy(); + destroy_owned(); + + return { + benchmark: 'mol_bench_owned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function mol_bench_unowned() { + // Do 10 loops to warm up JIT + for (let i = 0; i < 10; i++) { + const { run, destroy } = setup(); + run(0); + destroy(); + } + + const { run, destroy } = setup(); + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 1e4; i++) { + run(i); + } + }); + + destroy(); + + return { + benchmark: 'mol_bench_unowned', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} diff --git a/benchmarking/benchmarks/reactivity/sbench.js b/benchmarking/benchmarks/reactivity/sbench.js new file mode 100644 index 000000000000..ddeaef251485 --- /dev/null +++ b/benchmarking/benchmarks/reactivity/sbench.js @@ -0,0 +1,366 @@ +import { fastest_test } from '../../utils.js'; +import * as $ from '../../../packages/svelte/src/internal/client/index.js'; + +const COUNT = 1e5; + +/** + * @param {number} n + * @param {any[]} sources + */ +function create_data_signals(n, sources) { + for (let i = 0; i < n; i++) { + sources[i] = $.state(i); + } + return sources; +} + +/** + * @param {number} i + */ +function create_computation_0(i) { + $.derived(() => i); +} + +/** + * @param {any} s1 + */ +function create_computation_1(s1) { + $.derived(() => $.get(s1)); +} +/** + * @param {any} s1 + * @param {any} s2 + */ +function create_computation_2(s1, s2) { + $.derived(() => $.get(s1) + $.get(s2)); +} + +function create_computation_1000(ss, offset) { + $.derived(() => { + let sum = 0; + for (let i = 0; i < 1000; i++) { + sum += $.get(ss[offset + i]); + } + return sum; + }); +} + +/** + * @param {number} n + */ +function create_computations_0to1(n) { + for (let i = 0; i < n; i++) { + create_computation_0(i); + } +} + +/** + * @param {number} n + * @param {any[]} sources + */ +function create_computations_1to1(n, sources) { + for (let i = 0; i < n; i++) { + const source = sources[i]; + create_computation_1(source); + } +} + +/** + * @param {number} n + * @param {any[]} sources + */ +function create_computations_2to1(n, sources) { + for (let i = 0; i < n; i++) { + create_computation_2(sources[i * 2], sources[i * 2 + 1]); + } +} + +function create_computation_4(s1, s2, s3, s4) { + $.derived(() => $.get(s1) + $.get(s2) + $.get(s3) + $.get(s4)); +} + +function create_computations_1000to1(n, sources) { + for (let i = 0; i < n; i++) { + create_computation_1000(sources, i * 1000); + } +} + +function create_computations_1to2(n, sources) { + for (let i = 0; i < n / 2; i++) { + const source = sources[i]; + create_computation_1(source); + create_computation_1(source); + } +} + +function create_computations_1to4(n, sources) { + for (let i = 0; i < n / 4; i++) { + const source = sources[i]; + create_computation_1(source); + create_computation_1(source); + create_computation_1(source); + create_computation_1(source); + } +} + +function create_computations_1to8(n, sources) { + for (let i = 0; i < n / 8; i++) { + const source = sources[i]; + create_computation_1(source); + create_computation_1(source); + create_computation_1(source); + create_computation_1(source); + create_computation_1(source); + create_computation_1(source); + create_computation_1(source); + create_computation_1(source); + } +} + +function create_computations_1to1000(n, sources) { + for (let i = 0; i < n / 1000; i++) { + const source = sources[i]; + for (let j = 0; j < 1000; j++) { + create_computation_1(source); + } + } +} + +function create_computations_4to1(n, sources) { + for (let i = 0; i < n; i++) { + create_computation_4( + sources[i * 4], + sources[i * 4 + 1], + sources[i * 4 + 2], + sources[i * 4 + 3] + ); + } +} + +/** + * @param {any} fn + * @param {number} count + * @param {number} scount + */ +function bench(fn, count, scount) { + let sources = create_data_signals(scount, []); + + fn(count, sources); +} + +export async function sbench_create_signals() { + // Do 3 loops to warm up JIT + for (let i = 0; i < 3; i++) { + bench(create_data_signals, COUNT, COUNT); + } + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 100; i++) { + bench(create_data_signals, COUNT, COUNT); + } + }); + + return { + benchmark: 'sbench_create_signals', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function sbench_create_0to1() { + // Do 3 loops to warm up JIT + for (let i = 0; i < 3; i++) { + bench(create_computations_0to1, COUNT, 0); + } + + const { timing } = await fastest_test(10, () => { + const destroy = $.effect_root(() => { + for (let i = 0; i < 10; i++) { + bench(create_computations_0to1, COUNT, 0); + } + }); + destroy(); + }); + + return { + benchmark: 'sbench_create_0to1', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function sbench_create_1to1() { + // Do 3 loops to warm up JIT + for (let i = 0; i < 3; i++) { + bench(create_computations_1to1, COUNT, COUNT); + } + + const { timing } = await fastest_test(10, () => { + const destroy = $.effect_root(() => { + for (let i = 0; i < 10; i++) { + bench(create_computations_1to1, COUNT, COUNT); + } + }); + destroy(); + }); + + return { + benchmark: 'sbench_create_1to1', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function sbench_create_2to1() { + // Do 3 loops to warm up JIT + for (let i = 0; i < 3; i++) { + bench(create_computations_2to1, COUNT / 2, COUNT); + } + + const { timing } = await fastest_test(10, () => { + const destroy = $.effect_root(() => { + for (let i = 0; i < 10; i++) { + bench(create_computations_2to1, COUNT / 2, COUNT); + } + }); + destroy(); + }); + + return { + benchmark: 'sbench_create_2to1', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function sbench_create_4to1() { + // Do 3 loops to warm up JIT + for (let i = 0; i < 3; i++) { + bench(create_computations_4to1, COUNT / 4, COUNT); + } + + const { timing } = await fastest_test(10, () => { + const destroy = $.effect_root(() => { + for (let i = 0; i < 10; i++) { + bench(create_computations_4to1, COUNT / 4, COUNT); + } + }); + destroy(); + }); + + return { + benchmark: 'sbench_create_4to1', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function sbench_create_1000to1() { + // Do 3 loops to warm up JIT + for (let i = 0; i < 3; i++) { + bench(create_computations_1000to1, COUNT / 1000, COUNT); + } + + const { timing } = await fastest_test(10, () => { + const destroy = $.effect_root(() => { + for (let i = 0; i < 10; i++) { + bench(create_computations_1000to1, COUNT / 1000, COUNT); + } + }); + destroy(); + }); + + return { + benchmark: 'sbench_create_1000to1', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function sbench_create_1to2() { + // Do 3 loops to warm up JIT + for (let i = 0; i < 3; i++) { + bench(create_computations_1to2, COUNT, COUNT / 2); + } + + const { timing } = await fastest_test(10, () => { + const destroy = $.effect_root(() => { + for (let i = 0; i < 10; i++) { + bench(create_computations_1to2, COUNT, COUNT / 2); + } + }); + destroy(); + }); + + return { + benchmark: 'sbench_create_1to2', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function sbench_create_1to4() { + // Do 3 loops to warm up JIT + for (let i = 0; i < 3; i++) { + bench(create_computations_1to4, COUNT, COUNT / 4); + } + + const { timing } = await fastest_test(10, () => { + const destroy = $.effect_root(() => { + for (let i = 0; i < 10; i++) { + bench(create_computations_1to4, COUNT, COUNT / 4); + } + }); + destroy(); + }); + + return { + benchmark: 'sbench_create_1to4', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function sbench_create_1to8() { + // Do 3 loops to warm up JIT + for (let i = 0; i < 3; i++) { + bench(create_computations_1to8, COUNT, COUNT / 8); + } + + const { timing } = await fastest_test(10, () => { + const destroy = $.effect_root(() => { + for (let i = 0; i < 10; i++) { + bench(create_computations_1to8, COUNT, COUNT / 8); + } + }); + destroy(); + }); + + return { + benchmark: 'sbench_create_1to8', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} + +export async function sbench_create_1to1000() { + // Do 3 loops to warm up JIT + for (let i = 0; i < 3; i++) { + bench(create_computations_1to1000, COUNT, COUNT / 1000); + } + + const { timing } = await fastest_test(10, () => { + const destroy = $.effect_root(() => { + for (let i = 0; i < 10; i++) { + bench(create_computations_1to1000, COUNT, COUNT / 1000); + } + }); + destroy(); + }); + + return { + benchmark: 'sbench_create_1to1000', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} diff --git a/benchmarking/benchmarks/ssr/index.js b/benchmarking/benchmarks/ssr/index.js new file mode 100644 index 000000000000..667137d1ed12 --- /dev/null +++ b/benchmarking/benchmarks/ssr/index.js @@ -0,0 +1,3 @@ +import { wrapper_bench } from './wrapper/wrapper_bench.js'; + +export const ssr_benchmarks = [wrapper_bench]; diff --git a/benchmarking/benchmarks/ssr/wrapper/App.svelte b/benchmarking/benchmarks/ssr/wrapper/App.svelte new file mode 100644 index 000000000000..93f031f2bc9a --- /dev/null +++ b/benchmarking/benchmarks/ssr/wrapper/App.svelte @@ -0,0 +1,31 @@ + + +
+ {#each tiles as { x, y }} +
+ {/each} +
diff --git a/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js b/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js new file mode 100644 index 000000000000..ba0457b80ea7 --- /dev/null +++ b/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js @@ -0,0 +1,36 @@ +import { render } from 'svelte/server'; +import { fastest_test, read_file, write } from '../../../utils.js'; +import { compile } from 'svelte/compiler'; + +const dir = `${process.cwd()}/benchmarking/benchmarks/ssr/wrapper`; + +async function compile_svelte() { + const output = compile(read_file(`${dir}/App.svelte`), { + generate: 'server' + }); + write(`${dir}/output/App.js`, output.js.code); + + const module = await import(`${dir}/output/App.js`); + + return module.default; +} + +export async function wrapper_bench() { + const App = await compile_svelte(); + // Do 3 loops to warm up JIT + for (let i = 0; i < 3; i++) { + render(App); + } + + const { timing } = await fastest_test(10, () => { + for (let i = 0; i < 100; i++) { + render(App); + } + }); + + return { + benchmark: 'wrapper_bench', + time: timing.time.toFixed(2), + gc_time: timing.gc_time.toFixed(2) + }; +} diff --git a/benchmarking/compare/index.js b/benchmarking/compare/index.js new file mode 100644 index 000000000000..9d8d279c353a --- /dev/null +++ b/benchmarking/compare/index.js @@ -0,0 +1,89 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { execSync, fork } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +// if (execSync('git status --porcelain').toString().trim()) { +// console.error('Working directory is not clean'); +// process.exit(1); +// } + +const filename = fileURLToPath(import.meta.url); +const runner = path.resolve(filename, '../runner.js'); +const outdir = path.resolve(filename, '../.results'); + +if (fs.existsSync(outdir)) fs.rmSync(outdir, { recursive: true }); +fs.mkdirSync(outdir); + +const branches = []; + +for (const arg of process.argv.slice(2)) { + if (arg.startsWith('--')) continue; + if (arg === filename) continue; + + branches.push(arg); +} + +if (branches.length === 0) { + branches.push( + execSync('git symbolic-ref --short -q HEAD || git rev-parse --short HEAD').toString().trim() + ); +} + +if (branches.length === 1) { + branches.push('main'); +} + +process.on('exit', () => { + execSync(`git checkout ${branches[0]}`); +}); + +for (const branch of branches) { + console.group(`Benchmarking ${branch}`); + + execSync(`git checkout ${branch}`); + + await new Promise((fulfil, reject) => { + const child = fork(runner); + + child.on('message', (results) => { + fs.writeFileSync(`${outdir}/${branch}.json`, JSON.stringify(results, null, ' ')); + fulfil(); + }); + + child.on('error', reject); + }); + + console.groupEnd(); +} + +const results = branches.map((branch) => { + return JSON.parse(fs.readFileSync(`${outdir}/${branch}.json`, 'utf-8')); +}); + +for (let i = 0; i < results[0].length; i += 1) { + console.group(`${results[0][i].benchmark}`); + + for (const metric of ['time', 'gc_time']) { + const times = results.map((result) => +result[i][metric]); + let min = Infinity; + let min_index = -1; + + for (let b = 0; b < times.length; b += 1) { + if (times[b] < min) { + min = times[b]; + min_index = b; + } + } + + if (min !== 0) { + console.group(`${metric}: fastest is ${branches[min_index]}`); + times.forEach((time, b) => { + console.log(`${branches[b]}: ${time.toFixed(2)}ms (${((time / min) * 100).toFixed(2)}%)`); + }); + console.groupEnd(); + } + } + + console.groupEnd(); +} diff --git a/benchmarking/compare/runner.js b/benchmarking/compare/runner.js new file mode 100644 index 000000000000..a2e864637969 --- /dev/null +++ b/benchmarking/compare/runner.js @@ -0,0 +1,10 @@ +import { reactivity_benchmarks } from '../benchmarks/reactivity/index.js'; + +const results = []; +for (const benchmark of reactivity_benchmarks) { + const result = await benchmark(); + console.error(result.benchmark); + results.push(result); +} + +process.send(results); diff --git a/benchmarking/run.js b/benchmarking/run.js new file mode 100644 index 000000000000..bd96b9c2dc5a --- /dev/null +++ b/benchmarking/run.js @@ -0,0 +1,55 @@ +import * as $ from '../packages/svelte/src/internal/client/index.js'; +import { reactivity_benchmarks } from './benchmarks/reactivity/index.js'; +import { ssr_benchmarks } from './benchmarks/ssr/index.js'; + +let total_time = 0; +let total_gc_time = 0; + +const suites = [ + { benchmarks: reactivity_benchmarks, name: 'reactivity benchmarks' }, + { benchmarks: ssr_benchmarks, name: 'server-side rendering benchmarks' } +]; + +// eslint-disable-next-line no-console +console.log('\x1b[1m', '-- Benchmarking Started --', '\x1b[0m'); +$.push({}, true); +try { + for (const { benchmarks, name } of suites) { + let suite_time = 0; + let suite_gc_time = 0; + // eslint-disable-next-line no-console + console.log(`\nRunning ${name}...\n`); + + for (const benchmark of benchmarks) { + const results = await benchmark(); + // eslint-disable-next-line no-console + console.log(results); + total_time += Number(results.time); + total_gc_time += Number(results.gc_time); + suite_time += Number(results.time); + suite_gc_time += Number(results.gc_time); + } + + console.log(`\nFinished ${name}.\n`); + + // eslint-disable-next-line no-console + console.log({ + suite_time: suite_time.toFixed(2), + suite_gc_time: suite_gc_time.toFixed(2) + }); + } +} catch (e) { + // eslint-disable-next-line no-console + console.log('\x1b[1m', '\n-- Benchmarking Failed --\n', '\x1b[0m'); + // eslint-disable-next-line no-console + console.error(e); + process.exit(1); +} +$.pop(); +// eslint-disable-next-line no-console +console.log('\x1b[1m', '\n-- Benchmarking Complete --\n', '\x1b[0m'); +// eslint-disable-next-line no-console +console.log({ + total_time: total_time.toFixed(2), + total_gc_time: total_gc_time.toFixed(2) +}); diff --git a/benchmarking/tsconfig.json b/benchmarking/tsconfig.json new file mode 100644 index 000000000000..81fe19744ac3 --- /dev/null +++ b/benchmarking/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "moduleResolution": "Bundler", + "target": "ESNext", + "module": "ESNext", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "resolveJsonModule": true, + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "checkJs": true + }, + "include": ["./run.js", "./utils.js", "./benchmarks"] +} diff --git a/benchmarking/utils.js b/benchmarking/utils.js new file mode 100644 index 000000000000..684d2ee02b4d --- /dev/null +++ b/benchmarking/utils.js @@ -0,0 +1,119 @@ +import { performance, PerformanceObserver } from 'node:perf_hooks'; +import v8 from 'v8-natives'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +// Credit to https://github.com/milomg/js-reactivity-benchmark for the logic for timing + GC tracking. + +class GarbageTrack { + track_id = 0; + observer = new PerformanceObserver((list) => this.perf_entries.push(...list.getEntries())); + perf_entries = []; + periods = []; + + watch(fn) { + this.track_id++; + const start = performance.now(); + const result = fn(); + const end = performance.now(); + this.periods.push({ track_id: this.track_id, start, end }); + + return { result, track_id: this.track_id }; + } + + /** + * @param {number} track_id + */ + async gcDuration(track_id) { + await promise_delay(10); + + const period = this.periods.find((period) => period.track_id === track_id); + if (!period) { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + return Promise.reject('no period found'); + } + + const entries = this.perf_entries.filter( + (e) => e.startTime >= period.start && e.startTime < period.end + ); + return entries.reduce((t, e) => e.duration + t, 0); + } + + destroy() { + this.observer.disconnect(); + } + + constructor() { + this.observer.observe({ entryTypes: ['gc'] }); + } +} + +function promise_delay(timeout = 0) { + return new Promise((resolve) => setTimeout(resolve, timeout)); +} + +/** + * @param {{ (): void; (): any; }} fn + */ +function run_timed(fn) { + const start = performance.now(); + const result = fn(); + const time = performance.now() - start; + return { result, time }; +} + +/** + * @param {() => void} fn + */ +async function run_tracked(fn) { + v8.collectGarbage(); + const gc_track = new GarbageTrack(); + const { result: wrappedResult, track_id } = gc_track.watch(() => run_timed(fn)); + const gc_time = await gc_track.gcDuration(track_id); + const { result, time } = wrappedResult; + gc_track.destroy(); + return { result, timing: { time, gc_time } }; +} + +/** + * @param {number} times + * @param {() => void} fn + */ +export async function fastest_test(times, fn) { + const results = []; + for (let i = 0; i < times; i++) { + const run = await run_tracked(fn); + results.push(run); + } + const fastest = results.reduce((a, b) => (a.timing.time < b.timing.time ? a : b)); + + return fastest; +} + +/** + * @param {boolean} a + */ +export function assert(a) { + if (!a) { + throw new Error('Assertion failed'); + } +} + +/** + * @param {string} file + */ +export function read_file(file) { + return fs.readFileSync(file, 'utf-8').replace(/\r\n/g, '\n'); +} + +/** + * @param {string} file + * @param {string} contents + */ +export function write(file, contents) { + try { + fs.mkdirSync(path.dirname(file), { recursive: true }); + } catch {} + + fs.writeFileSync(file, contents); +} diff --git a/documentation/blog/2016-11-26-frameworks-without-the-framework.md b/documentation/blog/2016-11-26-frameworks-without-the-framework.md deleted file mode 100644 index de7a22708281..000000000000 --- a/documentation/blog/2016-11-26-frameworks-without-the-framework.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: "Frameworks without the framework: why didn't we think of this sooner?" -description: You can't write serious applications in vanilla JavaScript without hitting a complexity wall. But a compiler can do it for you. -author: Rich Harris -authorURL: https://twitter.com/Rich_Harris ---- - -> Wait, this new framework has a _runtime_? Ugh. Thanks, I'll pass. -> **– front end developers in 2018** - -We're shipping too much code to our users. Like a lot of front end developers, I've been in denial about that fact, thinking that it was fine to serve 100kb of JavaScript on page load – just use [one less .jpg!](https://twitter.com/miketaylr/status/227056824275333120) – and that what _really_ mattered was performance once your app was already interactive. - -But I was wrong. 100kb of .js isn't equivalent to 100kb of .jpg. It's not just the network time that'll kill your app's startup performance, but the time spent parsing and evaluating your script, during which time the browser becomes completely unresponsive. On mobile, those milliseconds rack up very quickly. - -If you're not convinced that this is a problem, follow [Alex Russell](https://twitter.com/slightlylate) on Twitter. Alex [hasn't been making many friends in the framework community lately](https://twitter.com/slightlylate/status/728355959022587905), but he's not wrong. But the proposed alternative to using frameworks like Angular, React and Ember – [Polymer](https://www.polymer-project.org/1.0/) – hasn't yet gained traction in the front end world, and it's certainly not for a lack of marketing. - -Perhaps we need to rethink the whole thing. - -## What problem do frameworks _really_ solve? - -The common view is that frameworks make it easier to manage the complexity of your code: the framework abstracts away all the fussy implementation details with techniques like virtual DOM diffing. But that's not really true. At best, frameworks _move the complexity around_, away from code that you had to write and into code you didn't. - -Instead, the reason that ideas like React are so wildly and deservedly successful is that they make it easier to manage the complexity of your _concepts_. Frameworks are primarily a tool for structuring your thoughts, not your code. - -Given that, what if the framework _didn't actually run in the browser_? What if, instead, it converted your application into pure vanilla JavaScript, just like Babel converts ES2016+ to ES5? You'd pay no upfront cost of shipping a hefty runtime, and your app would get seriously fast, because there'd be no layers of abstraction between your app and the browser. - -## Introducing Svelte - -Svelte is a new framework that does exactly that. You write your components using HTML, CSS and JavaScript (plus a few extra bits you can [learn in under 5 minutes](https://v2.svelte.dev/guide)), and during your build process Svelte compiles them into tiny standalone JavaScript modules. By statically analysing the component template, we can make sure that the browser does as little work as possible. - -The [Svelte implementation of TodoMVC](https://svelte-todomvc.surge.sh/) weighs 3.6kb zipped. For comparison, React plus ReactDOM _without any app code_ weighs about 45kb zipped. It takes about 10x as long for the browser just to evaluate React as it does for Svelte to be up and running with an interactive TodoMVC. - -And once your app _is_ up and running, according to [js-framework-benchmark](https://github.com/krausest/js-framework-benchmark) **Svelte is fast as heck**. It's faster than React. It's faster than Vue. It's faster than Angular, or Ember, or Ractive, or Preact, or Riot, or Mithril. It's competitive with Inferno, which is probably the fastest UI framework in the world, for now, because [Dominic Gannaway](https://twitter.com/trueadm) is a wizard. (Svelte is slower at removing elements. We're [working on it](https://github.com/sveltejs/svelte/issues/26).) - -It's basically as fast as vanilla JS, which makes sense because it _is_ vanilla JS – just vanilla JS that you didn't have to write. - -## But that's not the important thing - -Well, it _is_ important – performance matters a great deal. What's really exciting about this approach, though, is that we can finally solve some of the thorniest problems in web development. - -Consider interoperability. Want to `npm install cool-calendar-widget` and use it in your app? Previously, you could only do that if you were already using (a correct version of) the framework that the widget was designed for – if `cool-calendar-widget` was built in React and you're using Angular then, well, hard cheese. But if the widget author used Svelte, apps that use it can be built using whatever technology you like. (On the TODO list: a way to convert Svelte components into web components.) - -Or [code splitting](https://twitter.com/samccone/status/797528710085652480). It's a great idea (only load the code the user needs for the initial view, then get the rest later), but there's a problem – even if you only initially serve one React component instead of 100, _you still have to serve React itself_. With Svelte, code splitting can be much more effective, because the framework is embedded in the component, and the component is tiny. - -Finally, something I've wrestled with a great deal as an open source maintainer: your users always want _their_ features prioritised, and underestimate the cost of those features to people who don't need them. A framework author must always balance the long-term health of the project with the desire to meet their users' needs. That's incredibly difficult, because it's hard to anticipate – much less articulate – the consequences of incremental bloat, and it takes serious soft skills to tell people (who may have been enthusiastically evangelising your tool up to that point) that their feature isn't important enough. But with an approach like Svelte's, many features can be added with absolutely no cost to people who don't use them, because the code that implements those features just doesn't get generated by the compiler if it's unnecessary. - -## We're just getting started - -Svelte is very new. There's a lot of work still left to do – creating build tool integrations, adding a server-side renderer, hot reloading, transitions, more documentation and examples, starter kits, and so on. - -But you can already build rich components with it, which is why we've gone straight to a stable 1.0.0 release. [Read the guide](https://v2.svelte.dev/guide), [try it out in the REPL](/repl), and head over to [GitHub](https://github.com/sveltejs/svelte) to help kickstart the next era of front end development. diff --git a/documentation/blog/2017-08-07-the-easiest-way-to-get-started.md b/documentation/blog/2017-08-07-the-easiest-way-to-get-started.md deleted file mode 100644 index 2bbb2d463916..000000000000 --- a/documentation/blog/2017-08-07-the-easiest-way-to-get-started.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: The easiest way to get started with Svelte -description: This'll only take a minute. -author: Rich Harris -authorURL: https://twitter.com/Rich_Harris ---- - -Svelte is a [new kind of framework](/blog/frameworks-without-the-framework). Rather than putting a ` diff --git a/documentation/blog/2019-04-15-setting-up-your-editor.md b/documentation/blog/2019-04-15-setting-up-your-editor.md deleted file mode 100644 index 6456397a4149..000000000000 --- a/documentation/blog/2019-04-15-setting-up-your-editor.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Setting up your editor -description: Instructions for configuring linting and syntax highlighting -author: Rich Harris -authorURL: https://twitter.com/Rich_Harris -draft: true ---- - -_**Coming soon**_ - -This post will walk you through setting up your editor so that it recognises Svelte files: - -- eslint-plugin-svelte3 -- svelte-vscode -- associating .svelte files with HTML in VSCode, Sublime, etc. - -## Atom - -To treat `*.svelte` files as HTML, open _**Edit → Config...**_ and add the following lines to your `core` section: - -```cson -"*": - core: - … - customFileTypes: - "text.html.basic": [ - "svelte" - ] -``` - -## Vim/Neovim - -You can use the [coc-svelte extension](https://github.com/coc-extensions/coc-svelte) which utilises the official language-server. - -As an alternative you can treat all `*.svelte` files as HTML. Add the following line to your `init.vim`: - -``` -au! BufNewFile,BufRead *.svelte set ft=html -``` - -To temporarily turn on HTML syntax highlighting for the current buffer, use: - -``` -:set ft=html -``` - -To set the filetype for a single file, use a [modeline](https://vim.fandom.com/wiki/Modeline_magic): - -``` - -``` - -## Visual Studio Code - -We recommend using the official [Svelte for VS Code extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). - -## JetBrains WebStorm - -The [Svelte Framework Integration](https://plugins.jetbrains.com/plugin/12375-svelte/) can be used to add support for Svelte to WebStorm, or other Jetbrains IDEs. Consult the [WebStorm plugin installation guide](https://www.jetbrains.com/help/webstorm/managing-plugins.html) on the JetBrains website for more details. - -## Sublime Text 3 - -Open any `.svelte` file. - -Go to _**View → Syntax → Open all with current extension as... → HTML**_. diff --git a/documentation/blog/2019-04-16-svelte-for-new-developers.md b/documentation/blog/2019-04-16-svelte-for-new-developers.md deleted file mode 100644 index c0ad2add2bd3..000000000000 --- a/documentation/blog/2019-04-16-svelte-for-new-developers.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -title: Svelte for new developers -description: Never used Node.js or the command line? No problem -author: Rich Harris -authorURL: https://twitter.com/Rich_Harris ---- - -This short guide is designed to help you — someone who has looked at the [tutorial](/tutorial) and wants to start creating Svelte apps, but doesn't have a ton of experience using JavaScript build tooling — get up and running. - -If there are things that don't make sense, or that we're glossing over, feel free to [raise an issue](https://github.com/sveltejs/svelte/issues) or [suggest edits to this page](https://github.com/sveltejs/svelte/blob/master/site/content/blog/2019-04-16-svelte-for-new-developers.md) that will help us help more people. - -If you get stuck at any point following this guide, the best place to ask for help is in the [chatroom](https://svelte.dev/chat). - -## First things first - -You'll be using the _command line_, also known as the terminal. On Windows, you can access it by running **Command Prompt** from the Start menu; on a Mac, hit `Cmd` and `Space` together to bring up **Spotlight**, then start typing `Terminal.app`. On most Linux systems, `Ctrl-Alt-T` brings up the command line. - -The command line is a way to interact with your computer (or another computer! but that's a topic for another time) with more power and control than the GUI (graphical user interface) that most people use day-to-day. - -Once on the command line, you can navigate the filesystem using `ls` (`dir` on Windows) to list the contents of your current directory, and `cd` to change the current directory. For example, if you had a `Development` directory of your projects inside your home directory, you would type - -```bash -cd Development -``` - -to go to it. From there, you could create a new project directory with the `mkdir` command: - -```bash -mkdir svelte-projects -cd svelte-projects -``` - -A full introduction to the command line is out of the scope of this guide, but here are a few more useful commands: - -- `cd ..` — navigates to the parent of the current directory -- `cat my-file.txt` — on Mac/Linux (`type my-file.txt` on Windows), lists the contents of `my-file.txt` -- `open .` (or `start .` on Windows) — opens the current directory in Finder or File Explorer - -## Installing Node.js - -[Node](https://nodejs.org/en/) is a way to run JavaScript on the command line. It's used by many tools, including Svelte. If you don't yet have it installed, the easiest way is to download the latest version straight from the [website](https://nodejs.org/en/). - -Once installed, you'll have access to three new commands: - -- `node my-file.js` — runs the JavaScript in `my-file.js` -- `npm [subcommand]` — [npm](https://www.npmjs.com/) is a way to install 'packages' that your application depends on, such as the [svelte](https://www.npmjs.com/package/svelte) package -- `npx [subcommand]` — a convenient way to run programs available on npm without permanently installing them - -## Installing a text editor - -To write code, you need a good editor. The most popular choice is [Visual Studio Code](https://code.visualstudio.com/) or VSCode, and justifiably so — it's well-designed and fully-featured, and has a wealth of extensions ([including one for Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode), which provides syntax highlighting and diagnostic messages when you're writing components). - -## Creating a project - -We're going to use the [Svelte + Vite](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-svelte) template. You don't have to use a project template, but it means you have to do a lot less setup work. - -On the command line, navigate to where you want to create a new project, then type the following lines (you can paste the whole lot, but you'll develop better muscle memory if you get into the habit of writing each line out one at a time then running it): - -```bash -npm create vite@latest my-svelte-project -- --template svelte -cd my-svelte-project -npm install -``` - -> You can replace `--template svelte` with `--template svelte-ts`, if you prefer TypeScript. - -This creates a new directory, `my-svelte-project`, adds files from the [create-vite/template-svelte](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-svelte) template, and installs a number of packages from npm. Open the directory in your text editor and take a look around. The app's 'source code' lives in the `src` directory, while the files your app can load are in `public`. - -In the `package.json` file, there is a section called `"scripts"`. These scripts define shortcuts for working with your application — `dev`, `build` and `preview`. To launch your app in development mode, type the following: - -```bash -npm run dev -``` - -Running the `dev` script starts a program called [Vite](https://vitejs.dev/). Vite's job is to take your application's source files, pass them to other programs (including Svelte, in our case) and convert them into the code that will actually run when you open the application in a browser. - -Speaking of which, open a browser and navigate to http://localhost:5173. This is your application running on a local _web server_ (hence 'localhost') on port 5173. - -Try changing `src/App.svelte` and saving it. The application will update with your changes. - -## Building your app - -In the last step, we were running the app in 'development mode'. In dev mode, Svelte adds extra code that helps with debugging, and Vite skips the final step where your app's JavaScript is compressed. - -When you share your app with the world, you want to build it in 'production mode', so that it's as small and efficient as possible for end users. To do that, use the `build` command: - -```bash -npm run build -``` - -Your `dist` directory now contains an optimised version of your app. You can run it like so: - -```bash -npm run preview -``` diff --git a/documentation/blog/2019-04-20-write-less-code.md b/documentation/blog/2019-04-20-write-less-code.md deleted file mode 100644 index 8ccc2c04391f..000000000000 --- a/documentation/blog/2019-04-20-write-less-code.md +++ /dev/null @@ -1,163 +0,0 @@ ---- -title: Write less code -description: The most important metric you're not paying attention to -author: Rich Harris -authorURL: https://twitter.com/Rich_Harris ---- - -All code is buggy. It stands to reason, therefore, that the more code you have to write the buggier your apps will be. - -Writing more code also takes more time, leaving less time for other things like optimisation, nice-to-have features, or being outdoors instead of hunched over a laptop. - -In fact it's widely acknowledged that [project development time](https://blog.codinghorror.com/diseconomies-of-scale-and-lines-of-code/) and [bug count](https://www.mayerdan.com/ruby/2012/11/11/bugs-per-line-of-code-ratio) grow _quadratically_, not linearly, with the size of a codebase. That tracks with our intuitions: a ten-line pull request will get a level of scrutiny rarely applied to a 100-line one. And once a given module becomes too big to fit on a single screen, the cognitive effort required to understand it increases significantly. We compensate by refactoring and adding comments — activities that almost always result in _more_ code. It's a vicious cycle. - -Yet while we obsess — rightly! — over performance numbers, bundle size and anything else we can measure, we rarely pay attention to the amount of code we're writing. - -## Readability is important - -I'm certainly not claiming that we should use clever tricks to scrunch our code into the most compact form possible at the expense of readability. Nor am I claiming that reducing _lines_ of code is necessarily a worthwhile goal, since it encourages turning readable code like this... - -```js -for (let i = 0; i <= 100; i += 1) { - if (i % 2 === 0) { - console.log(`${i} is even`); - } -} -``` - -...into something much harder to parse: - -```js -for (let i = 0; i <= 100; i += 1) if (i % 2 === 0) console.log(`${i} is even`); -``` - -Instead, I'm claiming that we should favour languages and patterns that allow us to naturally write less code. - -## Yes, I'm talking about Svelte - -Reducing the amount of code you have to write is an explicit goal of Svelte. To illustrate, let's look at a very simple component implemented in React, Vue and Svelte. First, the Svelte version: - -
- -
- -How would we build this in React? It would probably look something like this: - -```js -// @noErrors -import React, { useState } from 'react'; - -export default () => { - const [a, setA] = useState(1); - const [b, setB] = useState(2); - - function handleChangeA(event) { - setA(+event.target.value); - } - - function handleChangeB(event) { - setB(+event.target.value); - } - - return ( -
- - - -

- {a} + {b} = {a + b} -

-
- ); -}; -``` - -Here's an equivalent component in Vue: - -```svelte - - - -``` - - - -In other words, it takes 442 characters in React, and 263 characters in Vue, to achieve something that takes 145 characters in Svelte. The React version is literally three times larger! - -It's unusual for the difference to be _quite_ so obvious — in my experience, a React component is typically around 40% larger than its Svelte equivalent. Let's look at the features of Svelte's design that enable you to express ideas more concisely: - -### Top-level elements - -In Svelte, a component can have as many top-level elements as you like. In React and Vue, a component must have a single top-level element — in React's case, trying to return two top-level elements from a component function would result in syntactically invalid code. (You can use a fragment — `<>` — instead of a `
`, but it's the same basic idea, and still results in an extra level of indentation). - -In Vue, your markup must be wrapped in a `