docs: desloppify browser support page (#18333)

The text in https://svelte.dev/docs/svelte/browser-support is quite
redundant and has a lot of AI smell. I did my best to make it more
concise and remove some slop comments in the generation script.

Requires a companion PR in svelte.dev:
https://github.com/sveltejs/svelte.dev/pull/2013

<img width="620" height="809" alt="image"
src="https://github.com/user-attachments/assets/7be9d5ff-1623-4897-a2d4-9a601b20f54d"
/>

### 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
- [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`.
- [x] This message body should clearly illustrate what problems it
solves.
- [ ] Ideally, include a test that fails without this PR but passes with
it.
- [x] If this PR changes code within `packages/svelte/src`, add a
changeset (`npx changeset`).

### Tests and linting

- [x] Run the tests with `pnpm test` and lint the project with `pnpm
lint`

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/18348/head
ottomated 4 months ago committed by GitHub
parent 05a3bce6bb
commit 11985c020f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -8,6 +8,7 @@ packages/**/config/*.js
# packages/svelte # packages/svelte
packages/svelte/messages/**/*.md packages/svelte/messages/**/*.md
packages/svelte/scripts/_bundle.js packages/svelte/scripts/_bundle.js
packages/svelte/scripts/_baseline/*.ts
packages/svelte/src/compiler/errors.js packages/svelte/src/compiler/errors.js
packages/svelte/src/compiler/warnings.js packages/svelte/src/compiler/warnings.js
packages/svelte/src/internal/client/errors.js packages/svelte/src/internal/client/errors.js

@ -1,7 +1,7 @@
<!-- generated in ../../../../../packages/svelte/scripts/generate-browser-support.ts. do not edit --> <!-- generated in ../../../../../packages/svelte/scripts/generate-browser-support.ts. do not edit -->
| Feature | Chrome/Edge | Firefox | Safari | | Feature | Chrome/Edge | Firefox | Safari |
| --- | ---: | ---: | ---: | | - | - | - | - |
| `$state.snapshot` | 98 | 94 | 15.4 | | [`$state.snapshot`](/docs/svelte/$state#$state.snapshot) | 98 | 94 | 15.4 |
| `bind:devicePixelContentBoxSize` | <span style="color: var(--sk-fg-4)">—</span> | 93 | not supported | | [`bind:devicePixelContentBoxSize`](/docs/svelte/bind#Dimensions) | <span style="color: var(--sk-fg-4)">—</span> | 93 | not supported |
| `flip` from `svelte/animate` | <span style="color: var(--sk-fg-4)">—</span> | 126 | <span style="color: var(--sk-fg-4)">—</span> | | [`flip` from `svelte/animate`](/docs/svelte/svelte-animate#flip) | <span style="color: var(--sk-fg-4)">—</span> | 126 | <span style="color: var(--sk-fg-4)">—</span> |

@ -1,13 +1,15 @@
<!-- generated in ../../../../../packages/svelte/scripts/generate-browser-support.ts. do not edit --> <!-- generated in ../../../../../packages/svelte/scripts/generate-browser-support.ts. do not edit -->
| Browser | Minimum version | | Browser | Minimum version |
| ---------------- | --------------- | | - | - |
| Chrome/Edge | 87 | | Chrome/Edge | 87 |
| Firefox | 83 | | Firefox | 83 |
| Safari | 14 | | Safari | 14 |
| Opera | 73 | | Opera | 73 |
| Opera (Android) | 62 | | Opera (Android) | 62 |
| Samsung Internet | 14.0 | | Samsung Internet | 14.0 |
| Android WebView | 87 | | Android WebView | 87 |
| Internet Explorer | not supported |
> [!NOTE] This equates to a <a href="https://web-platform-dx.github.io/baseline/">Baseline</a> target of 2020. > [!NOTE] This equates to a <a href="https://web-platform-dx.github.io/baseline/">Baseline</a> target of 2020.

@ -2,29 +2,14 @@
title: Browser support title: Browser support
--- ---
The table below shows the minimum browser versions Svelte's runtime and compiled output are expected to work in. The table below shows the minimum browser versions Svelte is expected to work in, derived from the browser APIs used by Svelte's internal code.
@include .generated/browser-support.md @include .generated/browser-support.md
These numbers describe what Svelte's output _requires_ in order to run — they're derived from the APIs the code uses, not from a list of browsers the team commits to testing. This table only covers Svelte itself. It does not include [SvelteKit](/docs/kit), other Svelte libraries, or your own code.
## What is covered ## Exceptions
- **Svelte's runtime.** Everything you import from `svelte` or its subpackages, in the form your bundler ships to the browser. A few Svelte features require a higher minimum browser version. You'll only need to take the following table into consideration if you use these specific features.
- **Compiler output.** The JavaScript the Svelte compiler emits from your `.svelte` files, including the DOM operations, bindings and transitions used in your components.
## What is not covered
- **Your own code** inside `<script>` blocks or `.svelte.js` files. If you use newer browser APIs the table will not reflect them — configure your own [browserslist](https://github.com/browserslist/browserslist) and polyfills accordingly.
- **SvelteKit**, adapters and build tooling. See the [SvelteKit docs](https://svelte.dev/docs/kit) for the browser support story there.
- **Internet Explorer 11.** Svelte's runtime relies on `Proxy`, which cannot be polyfilled. IE11 is not supported and there is no path to making it work.
## Per-feature browser requirements
Some Svelte features rely on browser APIs that exceed the floor above. The runtime still loads on older browsers — modern bundlers tree-shake the affected code when the feature is unused — but if you use one of these features, you need the higher minimum version listed here.
@include .generated/browser-support-features.md @include .generated/browser-support-features.md
## How this page stays accurate
The minimum versions can only move forward in a minor or major release, and any change is recorded in the [changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md). Every Svelte feature — bindings, runes, directives, and module exports — is checked against the [web-features](https://www.npmjs.com/package/web-features) Baseline dataset on every pull request, and the build fails if a change requires newer browsers than the page reflects.

@ -1,3 +1,4 @@
/* eslint-disable no-console */
// Regenerates `documentation/docs/07-misc/05-browser-support.md`. // Regenerates `documentation/docs/07-misc/05-browser-support.md`.
// //
// Pipeline: // Pipeline:
@ -46,10 +47,17 @@ type BrowserVersions = Record<string, string | null>;
type RuntimeFloor = number | 'newly'; type RuntimeFloor = number | 'newly';
type ConditionalRow = { type ConditionalRow = {
name: string; name: string;
doc_link: string | null;
versions: BrowserVersions; versions: BrowserVersions;
baseline_year: RuntimeFloor; baseline_year: RuntimeFloor;
}; };
const doc_links: Record<string, string | null> = {
'`$state.snapshot`': '/docs/svelte/$state#$state.snapshot',
'`bind:devicePixelContentBoxSize`': '/docs/svelte/bind#Dimensions',
'`flip` from `svelte/animate`': '/docs/svelte/svelte-animate#flip'
};
// Supplemental detection rules for APIs `web-features` doesn't track // Supplemental detection rules for APIs `web-features` doesn't track
// yet. Each rule is checked with full TS type-aware precision — the // yet. Each rule is checked with full TS type-aware precision — the
// only reason it lives here instead of being auto-derived is that no // only reason it lives here instead of being auto-derived is that no
@ -125,13 +133,7 @@ const SAFE_TO_IGNORE = new Set(['devicepixelratio', 'trusted-types']);
/** /**
* Suppressions for features that DO live in the runtime but are reached * Suppressions for features that DO live in the runtime but are reached
* only via a specific code path documented in the per-feature table on * only via a specific code path documented in the per-feature table on
* the docs page. The aggregate scan hides them so the headline floor * the docs page.
* reflects "load Svelte and use the basic runtime", not "use every
* conditional feature".
*
* Each entry MUST appear in the conditional-features table. The
* staleness check below also verifies the entry is still present in the
* runtime — if the detector doesn't flag it, the entry can be removed.
*/ */
const BEHAVIORAL_IGNORE = new Set([ const BEHAVIORAL_IGNORE = new Set([
'structured-clone', 'structured-clone',
@ -203,7 +205,6 @@ async function enumerate_subpackage_exports(): Promise<Record<string, string[]>>
if (names.length > 0) result[module_id] = names; if (names.length > 0) result[module_id] = names;
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : String(err); const message = err instanceof Error ? err.message : String(err);
// eslint-disable-next-line no-console
console.warn(` (could not enumerate ${module_id}: ${message.split('\n')[0]})`); console.warn(` (could not enumerate ${module_id}: ${message.split('\n')[0]})`);
} }
} }
@ -437,10 +438,8 @@ function find_minimum_target(
// detected feature happens to lack a Baseline year. // detected feature happens to lack a Baseline year.
const final_year = Math.max(year, 2015); const final_year = Math.max(year, 2015);
// eslint-disable-next-line no-console
console.log(` → ${final_year} (features that drove the floor:)`); console.log(` → ${final_year} (features that drove the floor:)`);
for (const id of [...drivers].sort()) { for (const id of [...drivers].sort()) {
// eslint-disable-next-line no-console
console.log(` - ${id}`); console.log(` - ${id}`);
} }
@ -455,7 +454,6 @@ function find_minimum_target(
* *
* `SAFE_TO_IGNORE` entries are exempt: they're safe to carry regardless * `SAFE_TO_IGNORE` entries are exempt: they're safe to carry regardless
* of whether the runtime currently uses the API. * of whether the runtime currently uses the API.
*
*/ */
function validate_ignore_features(runtime_files: string[]): void { function validate_ignore_features(runtime_files: string[]): void {
if (BEHAVIORAL_IGNORE.size === 0) return; if (BEHAVIORAL_IGNORE.size === 0) return;
@ -528,14 +526,12 @@ function enumerate_features(subpackage_exports: Record<string, string[]>): Featu
* Produce the `.svelte` source for a single binding fixture. Returns * Produce the `.svelte` source for a single binding fixture. Returns
* `null` for bindings the compiler treats as elements rather than * `null` for bindings the compiler treats as elements rather than
* properties (none currently, but defensive). * properties (none currently, but defensive).
*
*/ */
function binding_fixture(name: string, props: BindingProperty): string { function binding_fixture(name: string, props: BindingProperty): string {
// Map declared `valid_elements` to a concrete element + minimal attrs // Map declared `valid_elements` to a concrete element + minimal attrs
// so the compiler accepts the binding. // so the compiler accepts the binding.
const tag = (props.valid_elements ?? ['div'])[0]; const tag = (props.valid_elements ?? ['div'])[0];
// Pick a sensible initial value and attribute set per binding.
const reactive = `let v = $state();`; const reactive = `let v = $state();`;
if (tag === 'svelte:window') { if (tag === 'svelte:window') {
@ -545,7 +541,7 @@ function binding_fixture(name: string, props: BindingProperty): string {
return `<script>${reactive}</script><svelte:document bind:${name}={v} />`; return `<script>${reactive}</script><svelte:document bind:${name}={v} />`;
} }
if (tag === 'input') { if (tag === 'input') {
// `bind:checked` requires type="checkbox"|"radio"; `bind:group` too. // `bind:checked` and `bind:group` require type="checkbox" | "radio"
const type = const type =
name === 'checked' || name === 'indeterminate' name === 'checked' || name === 'indeterminate'
? ' type="checkbox"' ? ' type="checkbox"'
@ -567,7 +563,6 @@ function binding_fixture(name: string, props: BindingProperty): string {
* Compile a `.svelte` fixture to JS (no-op for `.js` fixtures), then * Compile a `.svelte` fixture to JS (no-op for `.js` fixtures), then
* bundle the result through the shared `bundle` helper. Fixtures are tiny * bundle the result through the shared `bundle` helper. Fixtures are tiny
* so circular-dep warnings from the Svelte runtime are silenced. * so circular-dep warnings from the Svelte runtime are silenced.
*
*/ */
async function bundle_fixture(feature: Feature): Promise<string> { async function bundle_fixture(feature: Feature): Promise<string> {
const entry_code = const entry_code =
@ -612,6 +607,8 @@ async function find_all_conditional_features(
const features = enumerate_features(subpackage_exports); const features = enumerate_features(subpackage_exports);
const rows: ConditionalRow[] = []; const rows: ConditionalRow[] = [];
const missing_doc_links: string[] = [];
for (let i = 0; i < features.length; i++) { for (let i = 0; i < features.length; i++) {
const feature = features[i]; const feature = features[i];
process.stdout.write(`\r ${i + 1}/${features.length} ${feature.name}`.padEnd(80)); process.stdout.write(`\r ${i + 1}/${features.length} ${feature.name}`.padEnd(80));
@ -645,25 +642,35 @@ async function find_all_conditional_features(
} }
} }
let doc_link = doc_links[feature.name];
if (doc_link === undefined) {
doc_link = null;
missing_doc_links.push(feature.name);
}
rows.push({ rows.push({
name: feature.name, name: feature.name,
doc_link,
versions, versions,
baseline_year: final_year baseline_year: final_year
}); });
} }
process.stdout.write('\n'); process.stdout.write('\n');
if (missing_doc_links.length) {
throw new Error(`Missing documentation url for some features.
Add them to the \`doc_links\` map in \`scripts/generate-browser-support.ts\`, or add an explicit \`null\` if they don't have a documentation url.
${missing_doc_links.map((name) => ` - "${name}"`).join('\n')}`);
}
return rows; return rows;
} }
/** function render_conditional_table(features: ConditionalRow[], runtime_floor: RuntimeFloor): string {
* Render the per-feature browser-requirements table from the auto-detected if (features.length === 0) {
* rows. Sorted by Safari floor descending, then alphabetically.
*/
function render_conditional_table(rows: ConditionalRow[], runtime_floor: RuntimeFloor): string {
if (rows.length === 0) {
return '_No features currently require browser versions newer than the runtime floor._'; return '_No features currently require browser versions newer than the runtime floor._';
} }
features.sort((a, b) => a.name.localeCompare(b.name));
const browsers = [ const browsers = [
['chrome', 'Chrome/Edge'], ['chrome', 'Chrome/Edge'],
@ -671,32 +678,25 @@ function render_conditional_table(rows: ConditionalRow[], runtime_floor: Runtime
['safari', 'Safari'] ['safari', 'Safari']
] as const; ] as const;
const runtime_versions = browser_versions_for(runtime_floor); const floor_versions = browser_versions_for(runtime_floor);
const sorted = [...rows].sort((a, b) => {
const sa = Number(a.versions.safari ?? '0');
const sb = Number(b.versions.safari ?? '0');
if (sb !== sa) return sb - sa;
return a.name.localeCompare(b.name);
});
const header = '| Feature | ' + browsers.map(([, label]) => `${label}`).join(' | ') + ' |';
const sep = '| --- |' + browsers.map(() => ' ---: |').join('');
const body = sorted.map((entry) => { const rows: string[][] = [];
const cells = browsers.map(([key]) => { for (const row of features) {
const v = entry.versions[key]; const name_cell = row.doc_link ? `[${row.name}](${row.doc_link})` : row.name;
const versions = browsers.map(([key]) => {
const v = row.versions[key];
if (v === null) return 'not supported'; if (v === null) return 'not supported';
if (v === undefined) return '<span style="color: var(--sk-fg-4)">—</span>'; if (v === undefined) return '<span style="color: var(--sk-fg-4)">—</span>';
const floor_v = runtime_versions[key]; const floor_v = floor_versions[key];
return floor_v && Number(v) <= Number(floor_v) if (floor_v && Number(v) <= Number(floor_v))
? '<span style="color: var(--sk-fg-4)">—</span>' return '<span style="color: var(--sk-fg-4)">—</span>';
: v;
return v;
}); });
return `| ${entry.name} | ${cells.join(' | ')} |`; rows.push([name_cell, ...versions]);
}); }
return [header, sep, ...body].join('\n'); return render_markdown_table(['Feature', ...browsers.map(([, label]) => `${label}`)], rows);
} }
function browser_versions_for(target: RuntimeFloor): Record<string, string> { function browser_versions_for(target: RuntimeFloor): Record<string, string> {
@ -750,53 +750,59 @@ function browser_versions_for(target: RuntimeFloor): Record<string, string> {
return lookup; return lookup;
} }
function render_table(versions: Record<string, string>, target: RuntimeFloor): string { const BROWSER = {
// Chrome and Edge ship from the same engine and historically resolve to the chrome: 'Chrome',
// same Baseline version. Collapse them into one row when they match, but edge: 'Edge',
// fall back to listing them separately if they ever drift. firefox: 'Firefox',
const chrome_edge: [string, string] | null = safari: 'Safari',
versions.chrome && versions.chrome === versions.edge ? ['Chrome/Edge', versions.chrome] : null; opera: 'Opera',
samsung_internet: 'Samsung Internet',
const base_rows: Array<[string, string]> = chrome_edge webview_android: 'Android WebView',
? [chrome_edge, ['Chrome (Android)', versions.chrome_android]] internet_explorer: 'Internet Explorer'
: [ };
['Chrome', versions.chrome],
['Chrome (Android)', versions.chrome_android],
['Edge', versions.edge]
];
const rows = [
...base_rows,
['Firefox', versions.firefox],
['Firefox (Android)', versions.firefox_android],
['Safari', versions.safari],
['Safari (iOS)', versions.safari_ios],
['Opera', versions.opera],
['Opera (Android)', versions.opera_android],
['Samsung Internet', versions.samsunginternet_android],
['Android WebView', versions.webview_android]
].filter(([label, version]) => version !== undefined) as Array<[string, string]>;
const headings = ['Browser', 'Minimum version'];
const widths = headings.map((heading, i) =>
Math.max(heading.length, ...rows.map((r) => String(r[i]).length))
);
const pad = (s: string, n: number) => s + ' '.repeat(Math.max(0, n - s.length)); function render_browser_table(versions: Record<string, string>, target: RuntimeFloor): string {
const rows: Array<[string, string]> = [
[BROWSER.chrome, versions.chrome],
[`${BROWSER.chrome} (Android)`, versions.chrome_android]
];
const header = `| ${headings.map((heading, i) => pad(heading, widths[i])).join(' | ')} |`; if (versions.chrome === versions.edge) {
const sep = `| ${widths.map((width) => '-'.repeat(width)).join(' | ')} |`; rows[0][0] += `/${BROWSER.edge}`;
const body = rows } else {
.map(([a, b]) => `| ${pad(a, widths[0])} | ${pad(String(b), widths[1])} |`) rows.push([BROWSER.edge, versions.edge]);
.join('\n'); }
rows.push(
[BROWSER.firefox, versions.firefox],
[`${BROWSER.firefox} (Android)`, versions.firefox_android],
[BROWSER.safari, versions.safari],
[`${BROWSER.safari} (iOS)`, versions.safari_ios],
[BROWSER.opera, versions.opera],
[`${BROWSER.opera} (Android)`, versions.opera_android],
[BROWSER.samsung_internet, versions.samsunginternet_android],
[BROWSER.webview_android, versions.webview_android],
[BROWSER.internet_explorer, 'not supported']
);
const target_label = target === 'newly' ? '"newly available"' : target; const target_label = target === 'newly' ? '"newly available"' : target;
return `${header}\n${sep}\n${body}\n\n> [!NOTE] This equates to a <a href="https://web-platform-dx.github.io/baseline/">Baseline</a> target of ${target_label}.`; return (
render_markdown_table(
['Browser', 'Minimum version'],
rows.filter(([, version]) => version !== undefined)
) +
`\n\n> [!NOTE] This equates to a <a href="https://web-platform-dx.github.io/baseline/">Baseline</a> target of ${target_label}.`
);
}
function render_markdown_table(headers: string[], rows: string[][]): string {
return `| ${headers.join(' | ')} |
| ${headers.map(() => '-').join(' | ')} |
${rows.map((row) => `| ${row.join(' | ')} |`).join('\n')}
`;
} }
/* eslint-disable no-console */
async function main() { async function main() {
console.log('Preparing scratch directory…'); console.log('Preparing scratch directory…');
// Wipe and recreate so stale bundles can't leak into the next scan. // Wipe and recreate so stale bundles can't leak into the next scan.
@ -844,14 +850,11 @@ async function main() {
const versions = browser_versions_for(target); const versions = browser_versions_for(target);
console.log('Rewriting docs page…'); console.log('Rewriting docs page…');
generate('browser-support.md', render_table(versions, target)); generate('browser-support.md', render_browser_table(versions, target));
generate('browser-support-features.md', render_conditional_table(conditional_rows, target)); generate('browser-support-features.md', render_conditional_table(conditional_rows, target));
console.log('Done.'); console.log('Done.');
} finally { } finally {
// Ensure cleanup happens even on failure — otherwise leftover bundle
// files in `scripts/_baseline/` get picked up by `pnpm lint` on the
// next CI step and produce spurious naming/no-console errors.
fs.rmSync(tmp_dir, { recursive: true, force: true }); fs.rmSync(tmp_dir, { recursive: true, force: true });
} }
} }

Loading…
Cancel
Save