Merge branch 'main' into incremental-batches

incremental-batches
Rich Harris 3 weeks ago
commit e413dbf668

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: use `createElement` instead of `createElementNS` for HTML elements

@ -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

1
.gitignore vendored

@ -22,6 +22,7 @@ coverage
.DS_Store
tmp
packages/svelte/scripts/_baseline/
benchmarking/.profiles
benchmarking/compare/.results

@ -0,0 +1,7 @@
<!-- generated in ../../../../../packages/svelte/scripts/generate-browser-support.ts. do not edit -->
| Feature | Chrome/Edge | Firefox | Safari |
| --- | ---: | ---: | ---: |
| `$state.snapshot` | 98 | 94 | 15.4 |
| `bind:devicePixelContentBoxSize` | <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> |

@ -0,0 +1,13 @@
<!-- generated in ../../../../../packages/svelte/scripts/generate-browser-support.ts. do not edit -->
| 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 <a href="https://web-platform-dx.github.io/baseline/">Baseline</a> target of 2020.

@ -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 `<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
## 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.

@ -90,6 +90,7 @@ export default [
'**/tests',
'packages/svelte/scripts/process-messages/templates/*.js',
'packages/svelte/scripts/_bundle.js',
'packages/svelte/scripts/_baseline/**',
'packages/svelte/src/compiler/errors.js',
'packages/svelte/src/internal/client/errors.js',
'packages/svelte/src/internal/client/warnings.js',

@ -143,9 +143,10 @@
"check": "tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc",
"check:tsgo": "tsgo --project tsconfig.runtime.json --skipLibCheck && tsgo --skipLibCheck",
"check:watch": "tsc --watch",
"generate": "node scripts/process-messages && node ./scripts/generate-types.js",
"generate": "node scripts/process-messages && node ./scripts/generate-types.js && pnpm generate:browser-support",
"generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json",
"generate:browser-support": "node ./scripts/generate-browser-support.ts",
"prepublishOnly": "pnpm build",
"knip": "pnpm dlx knip"
},
@ -158,8 +159,10 @@
"@rollup/plugin-virtual": "^3.0.2",
"@types/aria-query": "^5.0.4",
"@types/node": "^20.11.5",
"baseline-browser-mapping": "^2.10.32",
"dts-buddy": "^0.5.5",
"esbuild": "^0.25.10",
"web-features": "^3.29.0",
"rollup": "^4.59.0",
"source-map": "^0.7.4",
"tinyglobby": "^0.2.12",

@ -0,0 +1,417 @@
import ts from 'typescript';
import { features } from 'web-features';
/**
* Maps compat-key suffixes under `javascript.operators` and
* `javascript.statements` (and a few other `javascript.*` subtrees) to
* detection callbacks. The callback receives a TS AST node and returns
* true if that node represents the operator or statement.
*
* @type {Record<string, (node: ts.Node) => boolean>}
*/
const SYNTAX_PREDICATES = {
nullish_coalescing: (node) =>
ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken,
nullish_coalescing_assignment: (node) =>
ts.isBinaryExpression(node) &&
node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionEqualsToken,
logical_or_assignment: (node) =>
ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.BarBarEqualsToken,
logical_and_assignment: (node) =>
ts.isBinaryExpression(node) &&
node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken,
optional_chaining: (node) =>
(ts.isPropertyAccessExpression(node) ||
ts.isElementAccessExpression(node) ||
ts.isCallExpression(node)) &&
node.questionDotToken !== undefined,
spread: (node) => ts.isSpreadElement(node) || ts.isSpreadAssignment(node),
destructuring: (node) => ts.isObjectBindingPattern(node) || ts.isArrayBindingPattern(node),
arrow_functions: (node) => ts.isArrowFunction(node),
try_catch_optional_binding: (node) =>
ts.isCatchClause(node) && node.variableDeclaration === undefined,
async_iteration: (node) => ts.isForOfStatement(node) && node.awaitModifier !== undefined,
for_await: (node) => ts.isForOfStatement(node) && node.awaitModifier !== undefined,
private_class_fields: (node) => ts.isPrivateIdentifier(node),
async_generator_function: (node) =>
(ts.isFunctionDeclaration(node) ||
ts.isFunctionExpression(node) ||
ts.isMethodDeclaration(node)) &&
node.asteriskToken !== undefined &&
(node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false),
generator_function: (node) =>
(ts.isFunctionDeclaration(node) ||
ts.isFunctionExpression(node) ||
ts.isMethodDeclaration(node)) &&
node.asteriskToken !== undefined &&
!(node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false),
async_function: (node) =>
(ts.isFunctionDeclaration(node) ||
ts.isFunctionExpression(node) ||
ts.isArrowFunction(node) ||
ts.isMethodDeclaration(node)) &&
(node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false) &&
node.asteriskToken === undefined,
classes: (node) => ts.isClassDeclaration(node) || ts.isClassExpression(node),
let_const: (node) =>
ts.isVariableDeclarationList(node) &&
((node.flags & ts.NodeFlags.Let) !== 0 || (node.flags & ts.NodeFlags.Const) !== 0),
template_literals: (node) =>
ts.isTemplateExpression(node) || ts.isNoSubstitutionTemplateLiteral(node)
};
/**
* Walk `web-features` once and partition every `compat_features` path
* into the lookup tables the AST walker uses.
*/
function build_detection_maps() {
/** @type {Map<string, string>} identifier name → feature_id */
const globals = new Map();
/** @type {Map<string, Map<string, string>>} type → member → feature_id */
const members = new Map();
/** @type {Map<string, string>} string-literal value → feature_id */
const string_literals = new Map();
/** @type {Array<{ predicate: (n: ts.Node) => boolean, feature_id: string }>} */
const syntax_predicates = [];
const add_member = (
/** @type {string} */ type,
/** @type {string} */ member,
/** @type {string} */ feature_id
) => {
let by_member = members.get(type);
if (!by_member) {
by_member = new Map();
members.set(type, by_member);
}
// Only set if not already present — the first feature claiming a
// (type, member) pair wins. (Multiple features can map to the same
// pair via duplicated compat paths; we just need any.)
if (!by_member.has(member)) by_member.set(member, feature_id);
};
for (const [feature_id, feature] of Object.entries(features)) {
if (!('compat_features' in feature) || !feature.compat_features) continue;
for (const path of feature.compat_features) {
const parts = path.split('.');
// `api.X` → global identifier (constructor or function), e.g.
// `api.ResizeObserver`, `api.structuredClone`.
if (parts[0] === 'api' && parts.length === 2) {
globals.set(parts[1], feature_id);
continue;
}
// `api.X.Y` → member access on type X. E.g.
// `api.HTMLElement.inert`, `api.ResizeObserverEntry.contentBoxSize`.
if (parts[0] === 'api' && parts.length === 3) {
add_member(parts[1], parts[2], feature_id);
continue;
}
// `javascript.builtins.X` → global like Promise, Symbol, Proxy.
if (parts[0] === 'javascript' && parts[1] === 'builtins' && parts.length === 3) {
globals.set(parts[2], feature_id);
continue;
}
// `javascript.builtins.X.Y` → method/property on type X.
// We also accept the `ArrayConstructor`-style mapping for static
// methods: when the AST walker sees `Array.from(...)`, the
// receiver type's symbol name is `ArrayConstructor`, not `Array`.
if (parts[0] === 'javascript' && parts[1] === 'builtins' && parts.length === 4) {
add_member(parts[2], parts[3], feature_id);
add_member(`${parts[2]}Constructor`, parts[3], feature_id);
continue;
}
// `javascript.*` syntax: try the second segment first (covers
// `javascript.operators.X` and `javascript.statements.X`), then
// the last segment (covers `javascript.classes.private_class_fields`
// and similar).
if (parts[0] === 'javascript' && parts.length >= 3) {
const candidates = [parts[2], parts[parts.length - 1]];
for (const key of candidates) {
if (Object.hasOwn(SYNTAX_PREDICATES, key)) {
syntax_predicates.push({ predicate: SYNTAX_PREDICATES[key], feature_id });
break;
}
}
continue;
}
}
}
return { globals, members, string_literals, syntax_predicates };
}
const MAPS = build_detection_maps();
/**
* Versions and friendly names for synthetic feature IDs registered via
* `register_extra_rules` APIs the type-aware walker can see but that
* `web-features` doesn't (yet) catalogue. `versions_for_feature` consults
* this map before falling back to `web-features` lookups.
*
* @type {Map<string, { name: string, versions: Record<string, string | null>, baseline_year: number | null }>}
*/
const EXTRA_FEATURE_INFO = new Map();
/**
* Register additional detection rules for APIs the `web-features` dataset
* doesn't track yet. Two rule shapes are accepted:
*
* - **Member access**: `{ receiver_type, member, ... }` flags
* `expr.member` when `expr`'s type resolves to `receiver_type`.
* Equivalent to a `api.X.Y` rule auto-derived from web-features.
*
* - **String literal**: `{ string_literal, ... }` flags any
* occurrence of the literal value in source. Used for API options
* that are string-typed (e.g. `{ box: 'device-pixel-content-box' }`).
* The walker can be tightened later with a contextual-type check if
* false positives ever surface; for now an exact match is enough
* (these strings are too specific to occur incidentally).
*
* Each rule contributes its `feature_id`, per-browser versions,
* baseline year, and display name to the shared lookup tables.
*
* @param {Array<{
* feature_id: string,
* name: string,
* baseline_year: number,
* versions: Record<string, string | null>,
* receiver_type?: string,
* member?: string,
* string_literal?: string
* }>} rules
*/
export function register_extra_rules(rules) {
for (const rule of rules) {
if (rule.receiver_type && rule.member) {
let by_member = MAPS.members.get(rule.receiver_type);
if (!by_member) {
by_member = new Map();
MAPS.members.set(rule.receiver_type, by_member);
}
// Don't overwrite an existing web-features rule; that's canonical.
if (!by_member.has(rule.member)) {
by_member.set(rule.member, rule.feature_id);
}
} else if (rule.string_literal) {
if (!MAPS.string_literals.has(rule.string_literal)) {
MAPS.string_literals.set(rule.string_literal, rule.feature_id);
}
}
EXTRA_FEATURE_INFO.set(rule.feature_id, {
name: rule.name,
baseline_year: rule.baseline_year,
versions: rule.versions
});
}
}
/**
* Compile bundle files into a single `ts.Program` so type-checking is
* amortised across all of them.
*
* @param {string[]} files
*/
function build_program(files) {
const program = ts.createProgram(files, {
allowJs: true,
checkJs: false,
target: ts.ScriptTarget.ESNext,
module: ts.ModuleKind.ESNext,
moduleResolution: ts.ModuleResolutionKind.Bundler,
lib: ['lib.esnext.d.ts', 'lib.dom.d.ts', 'lib.dom.iterable.d.ts'],
strict: false,
noEmit: true,
skipLibCheck: true,
isolatedModules: true,
noErrorTruncation: true
});
const checker = program.getTypeChecker();
return { program, checker };
}
/**
* Collect the names of a type and its base types so a `member` lookup
* keyed on (say) `HTMLElement` matches a receiver typed as
* `HTMLDivElement`. Also includes the apparent type to catch primitives
* (`'foo'` apparent-types to `String`).
*
* @param {ts.Type} type
* @param {ts.TypeChecker} checker
*/
function get_type_names(type, checker) {
const names = new Set();
const constituents = type.isUnionOrIntersection() ? type.types : [type];
for (const t of constituents) {
const symbol = t.getSymbol() ?? t.aliasSymbol;
if (symbol) names.add(symbol.getName());
for (const base of t.getBaseTypes?.() ?? []) {
const base_symbol = base.getSymbol() ?? base.aliasSymbol;
if (base_symbol) names.add(base_symbol.getName());
}
const apparent = checker.getApparentType(t);
if (apparent && apparent !== t) {
const apparent_symbol = apparent.getSymbol() ?? apparent.aliasSymbol;
if (apparent_symbol) names.add(apparent_symbol.getName());
}
}
return names;
}
/**
* Cheap test for whether a symbol refers to a global binding (declared
* in a lib.d.ts or ambient module) rather than a user-defined local.
*
* @param {ts.Symbol | undefined} symbol
*/
function is_global_binding(symbol) {
if (!symbol) return true; // unresolved → assume global
const declarations = symbol.getDeclarations() ?? [];
if (declarations.length === 0) return true;
for (const decl of declarations) {
const file_name = decl.getSourceFile().fileName;
if (file_name.includes('/lib.') && file_name.endsWith('.d.ts')) return true;
}
return false;
}
/**
* Walk a TS source file emitting feature IDs as they're discovered.
*
* @param {ts.SourceFile} source
* @param {ts.TypeChecker | null} checker
* When `null`, type-aware checks (member access) are skipped. Used for
* the compiler-output fixtures which are parsed without a Program.
* @param {(feature_id: string) => void} emit
*/
function walk_source(source, checker, emit) {
/** @param {ts.Node} node */
function visit(node) {
// Syntax predicates run regardless of whether we have a checker.
for (const { predicate, feature_id } of MAPS.syntax_predicates) {
if (predicate(node)) emit(feature_id);
}
// Global identifier detection.
if (
ts.isIdentifier(node) &&
!(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) &&
!ts.isPropertyAssignment(node.parent) &&
!ts.isMethodDeclaration(node.parent) &&
!ts.isPropertySignature(node.parent)
) {
const feature_id = MAPS.globals.get(node.text);
if (feature_id) {
if (!checker || is_global_binding(checker.getSymbolAtLocation(node))) {
emit(feature_id);
}
}
}
// String-literal detection. The walker matches by value alone;
// false positives are theoretically possible but the literal
// values we care about (e.g. `'device-pixel-content-box'`) are
// distinctive enough that one hasn't been observed.
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
const feature_id = MAPS.string_literals.get(node.text);
if (feature_id) emit(feature_id);
}
// Member access detection (requires the checker).
if (checker && ts.isPropertyAccessExpression(node)) {
const member_name = node.name.text;
const receiver_type = checker.getTypeAtLocation(node.expression);
const type_names = get_type_names(receiver_type, checker);
for (const type_name of type_names) {
const by_member = MAPS.members.get(type_name);
if (!by_member) continue;
const feature_id = by_member.get(member_name);
if (feature_id) {
emit(feature_id);
break;
}
}
}
ts.forEachChild(node, visit);
}
visit(source);
}
/**
* Detect features used in a set of bundle files. Returns the union of
* web-features IDs flagged across all files.
*
* @param {string[]} bundle_files Absolute paths to JS/TS files.
* @returns {Set<string>}
*/
export function detect_features(bundle_files) {
const { program, checker } = build_program(bundle_files);
const flagged = new Set();
for (const file of bundle_files) {
const source = program.getSourceFile(file);
if (!source) continue;
walk_source(source, checker, (id) => flagged.add(id));
}
return flagged;
}
/**
* Detect features used in a single in-memory source string. No type
* checker (so member-based rules silently skip) useful for the
* compiler-output fixtures, where syntax-level detection is sufficient.
*
* @param {string} source_text
* @returns {Set<string>}
*/
export function detect_features_in_text(source_text) {
const source = ts.createSourceFile(
'fixture.js',
source_text,
ts.ScriptTarget.ESNext,
true,
ts.ScriptKind.JS
);
const flagged = new Set();
walk_source(source, null, (id) => flagged.add(id));
return flagged;
}
/**
* Per-browser minimum versions for a feature ID. Consults
* supplemental rules first (from `register_extra_rules`), then falls
* back to `web-features`. Returns null when neither has data.
*
* @param {string} feature_id
*/
export function versions_for_feature(feature_id) {
const extra = EXTRA_FEATURE_INFO.get(feature_id);
if (extra) return extra.versions;
const feature = features[feature_id];
if (!feature || !('status' in feature)) return null;
return /** @type {Record<string, string> | null} */ (
/** @type {unknown} */ (feature.status.support)
);
}
/**
* Baseline year for a feature. Returns `null` for features without a
* Baseline date (limited availability, supplemental rules without a
* year, or absent from the dataset).
*
* @param {string} feature_id
*/
export function baseline_year_for_feature(feature_id) {
const extra = EXTRA_FEATURE_INFO.get(feature_id);
if (extra) return extra.baseline_year;
const feature = features[feature_id];
if (!feature || !('status' in feature)) return null;
const status = /** @type {{ baseline_low_date?: string }} */ (feature.status);
if (!status.baseline_low_date) return null;
return Number(status.baseline_low_date.slice(0, 4));
}

@ -0,0 +1,874 @@
// Regenerates `documentation/docs/07-misc/05-browser-support.md`.
//
// Pipeline:
// 1. Bundle each runtime entry point with rollup using production export
// conditions, then walk the resulting JS with TypeScript's compiler
// API + TypeChecker. The walker (see `browser-support.detector.js`)
// flags any web-features ID the runtime references.
// 2. Verify each entry in `BEHAVIORAL_IGNORE` is still flagged by the
// detector — if not, the entry can be removed.
// 3. Enumerate every user-facing feature (each `bind:*`, every public
// subpackage export, every rune from the compiler's `RUNES` array,
// and the handful of directives that need their own fixtures). For
// each: compile, bundle, walk. If the bundle requires browser
// versions newer than the runtime floor, emit a row in the
// conditional-features table. Blind-spot regexes pick up APIs the
// AST walker can't see (string-literal constructor options,
// `getComputedStyle(...).zoom` reads).
// 4. Translate floors into concrete browser versions via `web-features`
// data (exact per-feature versions), falling back to
// `baseline-browser-mapping` for year-only resolution.
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import { rollup, type OutputChunk } from 'rollup';
import virtual from '@rollup/plugin-virtual';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import { getCompatibleVersions } from 'baseline-browser-mapping';
import {
detect_features,
detect_features_in_text,
versions_for_feature,
baseline_year_for_feature,
register_extra_rules
} from './browser-support.detector.js';
import { binding_properties } from '../src/compiler/phases/bindings.js';
import { RUNES } from '../src/utils.js';
import { compile as svelte_compile } from '../src/compiler/index.js';
type BindingProperty = import('../src/compiler/phases/bindings.js').BindingProperty;
type PackageExport = string | { browser?: string; default?: string };
type CompilerFixture = { filename: string; code: string };
type Feature = { name: string; source: string; kind: 'svelte' | 'js' };
type BrowserVersions = Record<string, string | null>;
type RuntimeFloor = number | 'newly';
type ConditionalRow = {
name: string;
versions: BrowserVersions;
baseline_year: RuntimeFloor;
};
// Supplemental detection rules for APIs `web-features` doesn't track
// 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
// compat key in `web-features` covers the API.
register_extra_rules([
{
// `getComputedStyle(current).zoom` walk in `svelte/animate`'s
// `flip` fallback path. Firefox didn't expose `.zoom` on
// `CSSStyleDeclaration` until v126 (May 2024) — pre-126 the read
// yields an empty string, breaking the animation math. No entry
// for it exists in `web-features` (CSS `zoom` is an IDL accessor
// without its own Baseline feature record).
receiver_type: 'CSSStyleDeclaration',
member: 'zoom',
feature_id: 'extra:css-zoom-read',
name: 'CSS zoom property reads (getComputedStyle(...).zoom)',
baseline_year: 2024,
versions: { firefox: '126' }
},
{
// `box: 'device-pixel-content-box'` in `bind_resize_observer`
// (size.js). The TS DOM lib declares the option value as part of
// the `ResizeObserverBoxOptions` union — we could check the
// contextual type, but matching the literal value is enough since
// the string is too specific to occur incidentally. Per MDN BCD:
// - constructor option: Chrome 84, Firefox 93, Safari 15.4
// - `ResizeObserverEntry.devicePixelContentBoxSize`: Safari NOT
// SUPPORTED (`version_added: false`)
// Safari therefore silently accepts the option from 15.4 onwards
// but never exposes the matching entry property, so the binding
// reads `undefined` on any Safari.
string_literal: 'device-pixel-content-box',
feature_id: 'extra:device-pixel-content-box',
name: 'ResizeObserver `box: device-pixel-content-box` option + `entry.devicePixelContentBoxSize`',
baseline_year: 2023,
versions: {
chrome: '84',
edge: '84',
firefox: '93',
safari: null,
safari_ios: null
}
}
]);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const pkg_dir = path.resolve(__dirname, '..');
const repo_root = path.resolve(pkg_dir, '..', '..');
const docs_dir = path.join(repo_root, 'documentation/docs/07-misc/.generated');
const snapshot_dir = path.join(pkg_dir, 'tests/snapshot/samples');
const tmp_dir = path.join(__dirname, '_baseline');
const pkg = JSON.parse(fs.readFileSync(path.join(pkg_dir, 'package.json'), 'utf-8')) as {
exports: Record<string, PackageExport>;
};
/**
* Suppressions that should NEVER affect the floor regardless of whether
* the runtime currently uses the API. Two reasons an entry belongs here:
*
* - The `web-features` dataset misclassifies the API (e.g.
* `devicepixelratio` is marked Baseline `false` because Safari is
* missing from its `support` map, but the property has shipped in
* every Safari for over a decade).
* - Svelte feature-detects the API at runtime with `?.` and degrades
* gracefully when it's unavailable. Example: `trusted-types` in
* `src/internal/client/dom/reconciler.js`.
*
* These are exempt from the staleness check.
*/
const SAFE_TO_IGNORE = new Set(['devicepixelratio', 'trusted-types']);
/**
* 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
* the docs page. The aggregate scan hides them so the headline floor
* 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([
'structured-clone',
'extra:css-zoom-read',
'extra:device-pixel-content-box'
]);
/** Aggregate ignore set — used for the headline floor. */
const AGGREGATE_IGNORE = new Set([...SAFE_TO_IGNORE, ...BEHAVIORAL_IGNORE]);
/**
* Subpaths in `pkg.exports` whose runtime is Node-only. Can't be derived
* from the exports map alone `./compiler` has a `require:` field that
* hints at CJS, but `./server` and `./internal/server` are plain `default`
* entries indistinguishable from a browser module.
*/
const NODE_ONLY_EXPORTS = new Set(['./compiler', './server', './internal/server']);
/**
* Every subpath in `pkg.exports` that ships browser JS. Type-only entries
* (`./action`, `./elements`) and the `./package.json` re-export filter out
* naturally on the `.js` check; only Node-only subpaths need an explicit
* exception, so new browser exports are picked up automatically.
*/
function browser_subpaths(): string[] {
const subpaths: string[] = [];
for (const [subpath, conditions] of Object.entries(pkg.exports)) {
if (NODE_ONLY_EXPORTS.has(subpath)) continue;
if (typeof conditions !== 'object' || conditions === null) continue;
const file = conditions.browser ?? conditions.default;
if (typeof file !== 'string' || !file.endsWith('.js')) continue;
subpaths.push(subpath);
}
return subpaths;
}
/**
* `.` `svelte`, `./animate` `svelte/animate`, etc.
*/
function importee_for(subpath: string): string {
return subpath === '.' ? 'svelte' : `svelte${subpath.slice(1)}`;
}
/**
* True if a subpath represents a public, user-facing subpackage whose
* named exports should each get their own per-feature fixture. Excludes
* the main entry (covered by the aggregate scan), the `./legacy` shim,
* and everything under `./internal/`.
*/
function is_public_subpackage(subpath: string): boolean {
return subpath !== '.' && subpath !== './legacy' && !subpath.startsWith('./internal');
}
/**
* For each public subpackage, dynamically import the module and return
* its named exports. Driven entirely by `pkg.exports`, so a new
* subpackage is picked up the next time the script runs.
*/
async function enumerate_subpackage_exports(): Promise<Record<string, string[]>> {
const result: Record<string, string[]> = {};
for (const subpath of browser_subpaths()) {
if (!is_public_subpackage(subpath)) continue;
const module_id = importee_for(subpath);
try {
const ns = await import(module_id);
const names = Object.keys(ns)
.filter((k) => k !== 'default')
.sort();
if (names.length > 0) result[module_id] = names;
} catch (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]})`);
}
}
return result;
}
const rune_fixtures: Record<(typeof RUNES)[number], string> = {
$state: `<script>let v = $state(0); console.log(v);</script>`,
'$state.raw': `<script>let v = $state.raw({}); console.log(v);</script>`,
'$state.eager': `<script>let v = $state.eager(0); console.log(v);</script>`,
'$state.snapshot': `<script>const v = $state({}); const snap = $state.snapshot(v); console.log(snap);</script>`,
$derived: `<script>let a = $state(0); let d = $derived(a + 1); console.log(d);</script>`,
'$derived.by': `<script>let a = $state(0); let d = $derived.by(() => a + 1); console.log(d);</script>`,
$props: `<script>let { x } = $props(); console.log(x);</script>`,
'$props.id': `<script>const id = $props.id(); console.log(id);</script>`,
$bindable: `<script>let { v = $bindable() } = $props(); console.log(v);</script>`,
$effect: `<script>$effect(() => { console.log('e'); });</script>`,
'$effect.pre': `<script>$effect.pre(() => { console.log('p'); });</script>`,
'$effect.tracking': `<script>$effect(() => { console.log($effect.tracking()); });</script>`,
'$effect.root': `<script>const stop = $effect.root(() => () => {}); stop();</script>`,
'$effect.pending': `<script>$effect(() => { console.log($effect.pending()); });</script>`,
$inspect: `<script>let v = $state(0); $inspect(v);</script>`,
'$inspect().with': `<script>let v = $state(0); $inspect(v).with(() => {});</script>`,
'$inspect.trace': `<script>$effect(() => { $inspect.trace(); });</script>`,
$host: `<svelte:options customElement="x-y" />\n<script>const h = $host(); console.log(h);</script>`
};
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: `<script>import { fade } from 'svelte/transition'; let show = $state(false);</script>{#if show}<div transition:fade></div>{/if}`
},
{
name: '`animate:`',
source: `<script>const flip = () => {}; let items = $state([1,2,3]);</script>{#each items as item (item)}<div animate:flip>{item}</div>{/each}`
},
{
name: '`use:` actions',
source: `<script>function action(node){return {destroy(){}}}</script><div use:action></div>`
},
{
name: '`@attach`',
source: `<script>const attachment = (node) => () => {};</script><div {@attach attachment}></div>`
},
{
name: '`{@html ...}`',
source: `<script>let html = $state('<b>x</b>');</script>{@html html}`
},
{
name: 'Custom elements (`<svelte:options customElement>`)',
source: `<svelte:options customElement="my-el" />\n<div></div>`
}
];
/**
* 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<string> {
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, `<svelte:element>`, 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<string>): 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<string>,
ignore: Set<string>
): { year: number; drivers: Set<string> } {
let year = 0;
const drivers = new Set<string>();
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<string, string[]>): 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 `<script>${reactive}</script><svelte:window bind:${name}={v} />`;
}
if (tag === 'svelte:document') {
return `<script>${reactive}</script><svelte:document bind:${name}={v} />`;
}
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 `<script>${reactive}</script><input${type} bind:${name}={v} />`;
}
if (tag === 'details') {
return `<script>${reactive}</script><details bind:${name}={v}><summary>x</summary></details>`;
}
return `<script>${reactive}</script><${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<string> {
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<string, string[]>
): Promise<ConditionalRow[]> {
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 '<span style="color: var(--sk-fg-4)">—</span>';
const floor_v = runtime_versions[key];
return floor_v && Number(v) <= Number(floor_v)
? '<span style="color: var(--sk-fg-4)">—</span>'
: v;
});
return `| ${entry.name} | ${cells.join(' | ')} |`;
});
return [header, sep, ...body].join('\n');
}
function browser_versions_for(target: RuntimeFloor): Record<string, string> {
// `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<string, string> = {};
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<string, string>, 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 <a href="https://web-platform-dx.github.io/baseline/">Baseline</a> 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, `<!-- generated in ${backlink}. do not edit -->\n\n${content}`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});

@ -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)
);
}

@ -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',

@ -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: {}

Loading…
Cancel
Save