diff --git a/.changeset/create-element-fast-path.md b/.changeset/create-element-fast-path.md
new file mode 100644
index 0000000000..8b88a14163
--- /dev/null
+++ b/.changeset/create-element-fast-path.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+perf: use `createElement` instead of `createElementNS` for HTML elements
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index df9f755874..4f72b6c23e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -98,6 +98,8 @@ jobs:
- 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); }
+ - name: check browser-support docs page is up to date
+ run: '{ [ "`git status --porcelain=v1 documentation/docs/07-misc/05-browser-support.md`" == "" ] || (echo "The browser-support docs page is out of date — please regenerate it locally with \`cd packages/svelte && pnpm generate:browser-support\` and commit the changes"; git diff documentation/docs/07-misc/05-browser-support.md; exit 1); }'
Benchmarks:
permissions: {}
runs-on: ubuntu-latest
diff --git a/.gitignore b/.gitignore
index d3c1819bd5..556cae6344 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,7 @@ coverage
.DS_Store
tmp
+packages/svelte/scripts/_baseline/
benchmarking/.profiles
benchmarking/compare/.results
diff --git a/documentation/docs/07-misc/.generated/browser-support-features.md b/documentation/docs/07-misc/.generated/browser-support-features.md
new file mode 100644
index 0000000000..1e1a00d4d3
--- /dev/null
+++ b/documentation/docs/07-misc/.generated/browser-support-features.md
@@ -0,0 +1,7 @@
+
+
+| Feature | Chrome/Edge | Firefox | Safari |
+| --- | ---: | ---: | ---: |
+| `$state.snapshot` | 98 | 94 | 15.4 |
+| `bind:devicePixelContentBoxSize` | — | 93 | not supported |
+| `flip` from `svelte/animate` | — | 126 | — |
\ No newline at end of file
diff --git a/documentation/docs/07-misc/.generated/browser-support.md b/documentation/docs/07-misc/.generated/browser-support.md
new file mode 100644
index 0000000000..8c58588960
--- /dev/null
+++ b/documentation/docs/07-misc/.generated/browser-support.md
@@ -0,0 +1,13 @@
+
+
+| Browser | Minimum version |
+| ---------------- | --------------- |
+| Chrome/Edge | 87 |
+| Firefox | 83 |
+| Safari | 14 |
+| Opera | 73 |
+| Opera (Android) | 62 |
+| Samsung Internet | 14.0 |
+| Android WebView | 87 |
+
+> [!NOTE] This equates to a Baseline target of 2020.
\ No newline at end of file
diff --git a/documentation/docs/07-misc/05-browser-support.md b/documentation/docs/07-misc/05-browser-support.md
new file mode 100644
index 0000000000..1a8830bdb9
--- /dev/null
+++ b/documentation/docs/07-misc/05-browser-support.md
@@ -0,0 +1,30 @@
+---
+title: Browser support
+---
+
+The table below shows the minimum browser versions Svelte's runtime and compiled output are expected to work in.
+
+@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.
+
+## What is covered
+
+- **Svelte's runtime.** Everything you import from `svelte` or its subpackages, in the form your bundler ships to the browser.
+- **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 ``,
+ '$state.raw': ``,
+ '$state.eager': ``,
+ '$state.snapshot': ``,
+ $derived: ``,
+ '$derived.by': ``,
+ $props: ``,
+ '$props.id': ``,
+ $bindable: ``,
+ $effect: ``,
+ '$effect.pre': ``,
+ '$effect.tracking': ``,
+ '$effect.root': ``,
+ '$effect.pending': ``,
+ $inspect: ``,
+ '$inspect().with': ``,
+ '$inspect.trace': ``,
+ $host: `\n`
+};
+
+function rune_fixture(rune: (typeof RUNES)[number]): string {
+ if (!Object.hasOwn(rune_fixtures, rune)) {
+ throw new Error(`Fixture missing for ${rune}`);
+ }
+
+ return rune_fixtures[rune];
+}
+
+/**
+ * Compiled-fixture sources for directives. Bindings are covered by the
+ * `binding_properties` enumeration; transitions, animate, actions, and
+ * `@attach` need explicit fixtures because they require accompanying
+ * imports or surrounding markup.
+ */
+const TESTED_DIRECTIVES = [
+ {
+ name: '`transition:` / `in:` / `out:`',
+ source: `{#if show}
{/if}`
+ },
+ {
+ name: '`animate:`',
+ source: `{#each items as item (item)}{item}
{/each}`
+ },
+ {
+ name: '`use:` actions',
+ source: ``
+ },
+ {
+ name: '`@attach`',
+ source: ``
+ },
+ {
+ name: '`{@html ...}`',
+ source: `{@html html}`
+ },
+ {
+ name: 'Custom elements (``)',
+ source: `\n`
+ }
+];
+
+/**
+ * Filesystem-safe identifier for an importee like `svelte/internal/client`.
+ */
+function safe_name(importee: string): string {
+ return importee.replace(/[^a-z0-9]+/gi, '_');
+}
+
+/**
+ * Bundle an entry the way users receive it, so we scan the same code the
+ * browser does. Mirrors `check-treeshakeability.js`.
+ *
+ * `entry_code` is virtual module source: typically `export * from
+ * 'svelte/...'` for a runtime entry, or compiled fixture JS for a per-feature
+ * scan. `silent` suppresses rollup's circular-dependency warnings — used for
+ * fixture bundles where they're known and noisy.
+ */
+async function bundle(entry_code: string, options: { silent?: boolean } = {}): Promise {
+ const built = await rollup({
+ input: '__entry__',
+ plugins: [
+ virtual({ __entry__: entry_code }),
+ {
+ name: 'resolve-svelte',
+ resolveId(id: string) {
+ if (id.startsWith('svelte')) {
+ const entry = pkg.exports[id.replace('svelte', '.')];
+ if (!entry) return;
+ if (typeof entry === 'string') return path.resolve(pkg_dir, entry);
+ const file = entry.browser ?? entry.default;
+ if (file) return path.resolve(pkg_dir, file);
+ }
+ }
+ },
+ nodeResolve({ exportConditions: ['production', 'import', 'browser', 'default'] })
+ ],
+ // Treat optional peers / Node-only branches as external so we only scan
+ // code that actually runs in the browser.
+ external: ['esm-env'],
+ onwarn: options.silent
+ ? () => {}
+ : (warning, handler) => {
+ if (warning.code === 'CIRCULAR_DEPENDENCY') return;
+ handler(warning);
+ }
+ });
+
+ const { output } = await built.generate({ format: 'esm' });
+ await built.close();
+
+ return output
+ .filter((chunk): chunk is OutputChunk => chunk.type === 'chunk')
+ .map((chunk) => chunk.code)
+ .join('\n');
+}
+
+/**
+ * Read every compiler-emitted client file from the snapshot tests. These
+ * fixtures cover the full range of patterns the compiler emits — bindings,
+ * transitions, ``, async derived, hydration markers, etc.
+ */
+function load_compiler_output_fixtures(): CompilerFixture[] {
+ const fixtures: CompilerFixture[] = [];
+
+ for (const sample of fs.readdirSync(snapshot_dir)) {
+ const client_dir = path.join(snapshot_dir, sample, '_expected/client');
+ if (!fs.existsSync(client_dir)) continue;
+
+ for (const file of fs.readdirSync(client_dir)) {
+ if (!file.endsWith('.js')) continue;
+ fixtures.push({
+ filename: `${sample}/${file}`,
+ code: fs.readFileSync(path.join(client_dir, file), 'utf-8')
+ });
+ }
+ }
+
+ return fixtures;
+}
+
+/**
+ * Combine per-feature version data into a single Record. Takes the max
+ * (strictest) version per browser across the input feature IDs.
+ *
+ * `null` propagates as "not supported" — if any contributing feature
+ * marks a browser unsupported, the merged record does too.
+ *
+ * Returns `null` if NONE of the IDs have versions in `web-features` or
+ * supplemental rules, so callers can fall back to year-based mapping.
+ */
+function versions_from_features(ids: Iterable): BrowserVersions | null {
+ const merged: BrowserVersions = {};
+ let any_found = false;
+
+ for (const id of ids) {
+ const support = versions_for_feature(id);
+ if (!support) continue;
+ any_found = true;
+ for (const [browser, version] of Object.entries(support)) {
+ const current = merged[browser];
+ // `null` means "not supported"; propagate it directly.
+ if (version === null) {
+ merged[browser] = null;
+ continue;
+ }
+ if (current === null) continue; // already known unsupported
+ if (current === undefined || Number(version) > Number(current)) {
+ merged[browser] = version;
+ }
+ }
+ }
+
+ return any_found ? merged : null;
+}
+
+/**
+ * Highest baseline year among `detected`, and the set of feature IDs that
+ * drove it. Used both for the aggregate runtime floor and for per-fixture
+ * scans — the two differ only in their ignore set.
+ */
+function compute_floor(
+ detected: Iterable,
+ ignore: Set
+): { year: number; drivers: Set } {
+ let year = 0;
+ const drivers = new Set();
+ for (const id of detected) {
+ if (ignore.has(id)) continue;
+ const y = baseline_year_for_feature(id);
+ if (!y) continue;
+ if (y > year) {
+ year = y;
+ drivers.clear();
+ }
+ if (y === year) drivers.add(id);
+ }
+ return { year, drivers };
+}
+
+/**
+ * Run the TS-based detector across the runtime bundles and the compiler-
+ * output fixtures, then compute the highest baseline year among the
+ * detected features (after subtracting `AGGREGATE_IGNORE`).
+ *
+ * `runtime_files` are absolute paths to runtime bundle files. Returns the
+ * minimum Baseline year the combined code satisfies.
+ */
+function find_minimum_target(
+ runtime_files: string[],
+ compiler_fixtures: CompilerFixture[]
+): number {
+ // Type-aware walk over the runtime bundles.
+ const detected = detect_features(runtime_files);
+
+ // Syntax-only walk over the compiler-output fixtures (text only, no
+ // program context; the bare TS source-file parser handles the syntax
+ // features the compiler emits).
+ for (const fixture of compiler_fixtures) {
+ for (const id of detect_features_in_text(fixture.code)) detected.add(id);
+ }
+
+ const { year, drivers } = compute_floor(detected, AGGREGATE_IGNORE);
+ // Floor at 2015 so the docs never claim a pre-ES6 target if every
+ // detected feature happens to lack a Baseline year.
+ const final_year = Math.max(year, 2015);
+
+ // eslint-disable-next-line no-console
+ console.log(` → ${final_year} (features that drove the floor:)`);
+ for (const id of [...drivers].sort()) {
+ // eslint-disable-next-line no-console
+ console.log(` - ${id}`);
+ }
+
+ return final_year;
+}
+
+/**
+ * Verify every entry in `BEHAVIORAL_IGNORE` is actually used by the
+ * runtime. Without this, a behavioural suppression can outlive the API
+ * it suppresses — the comment stays in the config pointing at code that
+ * no longer exists.
+ *
+ * `SAFE_TO_IGNORE` entries are exempt: they're safe to carry regardless
+ * of whether the runtime currently uses the API.
+ *
+ */
+function validate_ignore_features(runtime_files: string[]): void {
+ if (BEHAVIORAL_IGNORE.size === 0) return;
+ const detected = detect_features(runtime_files);
+ const stale = [...BEHAVIORAL_IGNORE].filter((id) => !detected.has(id));
+ if (stale.length > 0) {
+ throw new Error(
+ `BEHAVIORAL_IGNORE contains entries that the detector does not flag — ` +
+ `they can be removed:\n` +
+ stale.map((id) => ` - ${id}`).join('\n') +
+ `\n\nEdit \`packages/svelte/scripts/generate-browser-support.js\` ` +
+ `and delete the stale entries. If the API was removed from the runtime ` +
+ `as part of this change, that is exactly the intended signal.`
+ );
+ }
+}
+
+/**
+ * Build the full list of user-facing features to test for conditional
+ * floor bumps. Each feature gets a self-contained fixture, compiled and
+ * bundled like real user code, then scanned. If the bundle's floor
+ * exceeds the runtime floor, a row is auto-emitted in the docs.
+ *
+ * `subpackage_exports` maps subpath → list of exported symbols, produced by
+ * `enumerate_subpackage_exports`. Passed in rather than computed here so the
+ * dynamic-import discovery can happen once in `main`.
+ */
+function enumerate_features(subpackage_exports: Record): Feature[] {
+ const features: Feature[] = [];
+
+ // Every `bind:*` accepted by the compiler. Element selection respects
+ // the `valid_elements` constraint declared in `binding_properties`.
+ for (const [name, props] of Object.entries(binding_properties)) {
+ const fixture = binding_fixture(name, props);
+ if (fixture) {
+ features.push({
+ name: `\`bind:${name}\``,
+ kind: 'svelte',
+ source: fixture
+ });
+ }
+ }
+
+ for (const [module, exports] of Object.entries(subpackage_exports)) {
+ for (const exp of exports) {
+ features.push({
+ name: `\`${exp}\` from \`${module}\``,
+ kind: 'js',
+ source: `import { ${exp} } from '${module}'; export const _ = ${exp};`
+ });
+ }
+ }
+
+ for (const rune of RUNES) {
+ features.push({
+ name: `\`${rune}\``,
+ kind: 'svelte',
+ source: rune_fixture(rune)
+ });
+ }
+
+ for (const directive of TESTED_DIRECTIVES) {
+ features.push({ name: directive.name, kind: 'svelte', source: directive.source });
+ }
+
+ return features;
+}
+
+/**
+ * Produce the `.svelte` source for a single binding fixture. Returns
+ * `null` for bindings the compiler treats as elements rather than
+ * properties (none currently, but defensive).
+ *
+ */
+function binding_fixture(name: string, props: BindingProperty): string {
+ // Map declared `valid_elements` to a concrete element + minimal attrs
+ // so the compiler accepts the binding.
+ const tag = (props.valid_elements ?? ['div'])[0];
+
+ // Pick a sensible initial value and attribute set per binding.
+ const reactive = `let v = $state();`;
+
+ if (tag === 'svelte:window') {
+ return ``;
+ }
+ if (tag === 'svelte:document') {
+ return ``;
+ }
+ if (tag === 'input') {
+ // `bind:checked` requires type="checkbox"|"radio"; `bind:group` too.
+ const type =
+ name === 'checked' || name === 'indeterminate'
+ ? ' type="checkbox"'
+ : name === 'group'
+ ? ' type="radio" value="a"'
+ : name === 'files'
+ ? ' type="file"'
+ : '';
+ return ``;
+ }
+ if (tag === 'details') {
+ return `x
`;
+ }
+
+ return `<${tag} bind:${name}={v}>${tag}>`;
+}
+
+/**
+ * Compile a `.svelte` fixture to JS (no-op for `.js` fixtures), then
+ * bundle the result through the shared `bundle` helper. Fixtures are tiny
+ * so circular-dep warnings from the Svelte runtime are silenced.
+ *
+ */
+async function bundle_fixture(feature: Feature): Promise {
+ const entry_code =
+ feature.kind === 'svelte'
+ ? svelte_compile(feature.source, {
+ generate: 'client',
+ filename: 'Fixture.svelte',
+ dev: false
+ }).js.code
+ : feature.source;
+ return bundle(entry_code, { silent: true });
+}
+
+/**
+ * Detect features in a single fixture bundle and report the per-fixture
+ * floor year along with the IDs that drove it. Used for the per-feature
+ * conditional table.
+ *
+ * `fixture_file` is the absolute path to the `.ts` bundle.
+ */
+function scan_fixture(fixture_file: string): {
+ year: number;
+ driving_ids: string[];
+} {
+ const { year, drivers } = compute_floor(detect_features([fixture_file]), SAFE_TO_IGNORE);
+
+ return {
+ year,
+ driving_ids: [...drivers]
+ };
+}
+
+/**
+ * Iterate every feature, bundle its fixture, scan it. Return the rows
+ * that need to appear in the conditional-features table.
+ */
+async function find_all_conditional_features(
+ runtime_floor: RuntimeFloor,
+ subpackage_exports: Record
+): Promise {
+ const runtime_year = typeof runtime_floor === 'number' ? runtime_floor : Infinity;
+ const features = enumerate_features(subpackage_exports);
+ const rows: ConditionalRow[] = [];
+
+ for (let i = 0; i < features.length; i++) {
+ const feature = features[i];
+ process.stdout.write(`\r ${i + 1}/${features.length} ${feature.name}`.padEnd(80));
+
+ let bundle_code;
+ try {
+ bundle_code = await bundle_fixture(feature);
+ } catch {
+ continue; // some fixtures (rare element combos) may fail to compile
+ }
+
+ // Write the bundle so the type-aware scanner can resolve its types.
+ const fixture_file = path.join(tmp_dir, `fixture_${i}.ts`);
+ fs.writeFileSync(fixture_file, bundle_code);
+
+ const scanned = scan_fixture(fixture_file);
+ const final_year = scanned.year;
+
+ // Skip features at or below the runtime floor — they don't need a row.
+ if (final_year <= runtime_year || final_year === 0) continue;
+
+ // Use exact per-feature versions where available (from web-features
+ // or supplemental rules), falling back to the conservative year
+ // mapping only if no feature has explicit version data.
+ let versions = versions_from_features(scanned.driving_ids);
+ if (!versions) {
+ try {
+ versions = browser_versions_for(final_year);
+ } catch {
+ continue;
+ }
+ }
+
+ rows.push({
+ name: feature.name,
+ versions,
+ baseline_year: final_year
+ });
+ }
+ process.stdout.write('\n');
+
+ return rows;
+}
+
+/**
+ * Render the per-feature browser-requirements table from the auto-detected
+ * 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._';
+ }
+
+ const browsers = [
+ ['chrome', 'Chrome/Edge'],
+ ['firefox', 'Firefox'],
+ ['safari', 'Safari']
+ ] as const;
+
+ const runtime_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 cells = browsers.map(([key]) => {
+ const v = entry.versions[key];
+ if (v === null) return 'not supported';
+ if (v === undefined) return '—';
+ const floor_v = runtime_versions[key];
+ return floor_v && Number(v) <= Number(floor_v)
+ ? '—'
+ : v;
+ });
+ return `| ${entry.name} | ${cells.join(' | ')} |`;
+ });
+
+ return [header, sep, ...body].join('\n');
+}
+
+function browser_versions_for(target: RuntimeFloor): Record {
+ // `targetYear` returns the minimum versions in which every feature that
+ // reached Baseline by the end of that year is supported. If the lint
+ // search fell through to `'newly'`, we use the current year — that gives
+ // the most recent Newly-available cutoff, which is the strongest
+ // statement `baseline-browser-mapping` is able to make.
+ const target_year = typeof target === 'number' ? target : new Date().getFullYear();
+
+ const versions = getCompatibleVersions({
+ targetYear: target_year,
+ includeDownstreamBrowsers: true
+ });
+
+ // The core Baseline browsers plus the downstream browsers worth listing
+ // in the docs. Downstream browsers come from `baseline-browser-mapping`'s
+ // dataset and represent the highest-traffic Chromium derivatives; the
+ // long tail (UC, QQ, Yandex, in-app Facebook/Instagram browsers, etc.)
+ // is omitted to keep the table focused.
+ const visible_browsers = new Set([
+ 'chrome',
+ 'chrome_android',
+ 'edge',
+ 'firefox',
+ 'firefox_android',
+ 'safari',
+ 'safari_ios',
+ 'opera',
+ 'opera_android',
+ 'samsunginternet_android',
+ 'webview_android'
+ ]);
+
+ const suffixes = ['_android', '_ios'];
+
+ const lookup: Record = {};
+ outer: for (const { browser, version } of versions) {
+ if (visible_browsers.has(browser)) {
+ for (const suffix of suffixes) {
+ // skip e.g. 'Chrome (Android)' if it matches Chrome
+ if (browser.endsWith(suffix) && version === lookup[browser.replace(suffix, '')]) {
+ continue outer;
+ }
+ }
+
+ lookup[browser] = version;
+ }
+ }
+
+ return lookup;
+}
+
+function render_table(versions: Record, target: RuntimeFloor): string {
+ // Chrome and Edge ship from the same engine and historically resolve to the
+ // same Baseline version. Collapse them into one row when they match, but
+ // fall back to listing them separately if they ever drift.
+ const chrome_edge: [string, string] | null =
+ versions.chrome && versions.chrome === versions.edge ? ['Chrome/Edge', versions.chrome] : null;
+
+ const base_rows: Array<[string, string]> = chrome_edge
+ ? [chrome_edge, ['Chrome (Android)', versions.chrome_android]]
+ : [
+ ['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));
+
+ const header = `| ${headings.map((heading, i) => pad(heading, widths[i])).join(' | ')} |`;
+ const sep = `| ${widths.map((width) => '-'.repeat(width)).join(' | ')} |`;
+ const body = rows
+ .map(([a, b]) => `| ${pad(a, widths[0])} | ${pad(String(b), widths[1])} |`)
+ .join('\n');
+
+ const target_label = target === 'newly' ? '"newly available"' : target;
+
+ return `${header}\n${sep}\n${body}\n\n> [!NOTE] This equates to a Baseline target of ${target_label}.`;
+}
+
+/* eslint-disable no-console */
+async function main() {
+ console.log('Preparing scratch directory…');
+ // Wipe and recreate so stale bundles can't leak into the next scan.
+ fs.rmSync(tmp_dir, { recursive: true, force: true });
+ fs.mkdirSync(tmp_dir, { recursive: true });
+
+ try {
+ console.log('Bundling runtime entries…');
+ const runtime_files: string[] = [];
+ for (const importee of browser_subpaths().map(importee_for)) {
+ // `import * as` + re-export keeps default and named exports
+ // alive, so flag modules (only a default export) don't produce
+ // empty chunks but their code still ends up in the scan.
+ const code = await bundle(`import * as __ns from '${importee}'; export default __ns;`);
+ const file = path.join(tmp_dir, `${safe_name(importee)}.ts`);
+ fs.writeFileSync(file, code);
+ runtime_files.push(file);
+ }
+
+ console.log('Loading compiler-output fixtures…');
+ const compiler_fixtures = load_compiler_output_fixtures();
+ console.log(` (${compiler_fixtures.length} fixtures found)`);
+
+ console.log('Searching for the minimum Baseline target (type-aware)…');
+ const target = find_minimum_target(runtime_files, compiler_fixtures);
+
+ console.log('Checking BEHAVIORAL_IGNORE for stale entries…');
+ validate_ignore_features(runtime_files);
+ console.log(' no stale entries');
+
+ console.log('Enumerating subpackage exports…');
+ const subpackage_exports = await enumerate_subpackage_exports();
+ const total_exports = Object.values(subpackage_exports).reduce((n, list) => n + list.length, 0);
+ console.log(
+ ` ${total_exports} export(s) across ${Object.keys(subpackage_exports).length} subpackage(s)`
+ );
+
+ console.log('Scanning per-feature fixtures for conditional requirements…');
+ const conditional_rows = await find_all_conditional_features(target, subpackage_exports);
+ console.log(
+ ` ${conditional_rows.length} feature(s) require browsers newer than the runtime floor`
+ );
+
+ console.log('Resolving browser versions…');
+ const versions = browser_versions_for(target);
+
+ console.log('Rewriting docs page…');
+ generate('browser-support.md', render_table(versions, target));
+ generate('browser-support-features.md', render_conditional_table(conditional_rows, target));
+
+ console.log('Done.');
+ } 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 });
+ }
+}
+
+function generate(file: string, content: string): void {
+ const filename = path.join(docs_dir, file);
+
+ try {
+ fs.mkdirSync(path.dirname(file), { recursive: true });
+ } catch {}
+
+ const backlink = path.relative(filename, fileURLToPath(import.meta.url));
+
+ fs.writeFileSync(filename, `\n\n${content}`);
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js
index f6d05162ca..bc12a0b07e 100644
--- a/packages/svelte/src/internal/client/dom/operations.js
+++ b/packages/svelte/src/internal/client/dom/operations.js
@@ -233,6 +233,12 @@ export function should_defer_append() {
}
/**
+ * Branching here is intentional and load-bearing for perf. `createElement(tag)`
+ * hits a fast path in Blink that `createElementNS(NAMESPACE_HTML, tag)` doesn't,
+ * and passing an explicit `undefined` as the trailing options arg measurably
+ * slows both APIs. Funnelling every case through a single `createElementNS(ns,
+ * tag, options)` call would be smaller but slower on the HTML path.
+ *
* @template {keyof HTMLElementTagNameMap | string} T
* @param {T} tag
* @param {string} [namespace]
@@ -240,9 +246,13 @@ export function should_defer_append() {
* @returns {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element}
*/
export function create_element(tag, namespace, is) {
- let options = is ? { is } : undefined;
+ if (namespace == null || namespace === NAMESPACE_HTML) {
+ return /** @type {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element} */ (
+ is ? document.createElement(tag, { is }) : document.createElement(tag)
+ );
+ }
return /** @type {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element} */ (
- document.createElementNS(namespace ?? NAMESPACE_HTML, tag, options)
+ is ? document.createElementNS(namespace, tag, { is }) : document.createElementNS(namespace, tag)
);
}
diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js
index 54757a6f13..40b558d0f9 100644
--- a/packages/svelte/src/utils.js
+++ b/packages/svelte/src/utils.js
@@ -434,7 +434,7 @@ const STATE_CREATION_RUNES = /** @type {const} */ ([
'$derived.by'
]);
-const RUNES = /** @type {const} */ ([
+export const RUNES = /** @type {const} */ ([
...STATE_CREATION_RUNES,
'$state.eager',
'$state.snapshot',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 25899b297d..4c077f0f6b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -103,7 +103,7 @@ importers:
version: 1.2.1
esrap:
specifier: ^2.2.9
- version: 2.2.9(@typescript-eslint/types@8.56.0)
+ version: 2.2.9(@typescript-eslint/types@8.59.4)
is-reference:
specifier: ^3.0.3
version: 3.0.3
@@ -141,6 +141,9 @@ importers:
'@types/node':
specifier: ^20.11.5
version: 20.19.17
+ baseline-browser-mapping:
+ specifier: ^2.10.32
+ version: 2.10.32
dts-buddy:
specifier: ^0.5.5
version: 0.5.5(typescript@5.5.4)
@@ -162,6 +165,9 @@ importers:
vitest:
specifier: ^2.1.9
version: 2.1.9(@types/node@20.19.17)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)
+ web-features:
+ specifier: ^3.29.0
+ version: 3.29.0
playgrounds/sandbox:
devDependencies:
@@ -1153,6 +1159,10 @@ packages:
resolution: {integrity: sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@typescript-eslint/types@8.59.4':
+ resolution: {integrity: sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
'@typescript-eslint/typescript-estree@8.56.0':
resolution: {integrity: sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1281,6 +1291,11 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+ baseline-browser-mapping@2.10.32:
+ resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
better-path-resolve@1.0.0:
resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
engines: {node: '>=4'}
@@ -2660,6 +2675,9 @@ packages:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
+ web-features@3.29.0:
+ resolution: {integrity: sha512-r8m0Xj77/PSHXEbv2U3UuLhw5gE4U+YAPek5hCor5DMsS9Qnwtxl5FSfam3dXQo7Gl0gxK9BRrcQLlOBZZZXpw==}
+
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@@ -3530,6 +3548,9 @@ snapshots:
'@typescript-eslint/types@8.56.0': {}
+ '@typescript-eslint/types@8.59.4':
+ optional: true
+
'@typescript-eslint/typescript-estree@8.56.0(typescript@5.5.4)':
dependencies:
'@typescript-eslint/project-service': 8.56.0(typescript@5.5.4)
@@ -3676,6 +3697,8 @@ snapshots:
balanced-match@1.0.2: {}
+ baseline-browser-mapping@2.10.32: {}
+
better-path-resolve@1.0.0:
dependencies:
is-windows: 1.0.2
@@ -4051,11 +4074,11 @@ snapshots:
dependencies:
estraverse: 5.3.0
- esrap@2.2.9(@typescript-eslint/types@8.56.0):
+ esrap@2.2.9(@typescript-eslint/types@8.59.4):
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
optionalDependencies:
- '@typescript-eslint/types': 8.56.0
+ '@typescript-eslint/types': 8.59.4
esrecurse@4.3.0:
dependencies:
@@ -5036,6 +5059,8 @@ snapshots:
dependencies:
xml-name-validator: 5.0.0
+ web-features@3.29.0: {}
+
webidl-conversions@3.0.1: {}
webidl-conversions@7.0.0: {}