Merge branch 'main' into stack-trace-based-readonly

pull/10464/head
Rich Harris 3 years ago
commit 9da0abbd70

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly scope CSS selectors with descendant combinators

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: implement support for `:is(...)` and `:where(...)`

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: treeshake unused store subscriptions in SSR mode

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: warn against accidental global event referenced

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve bind:this support for each blocks

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve global transition handling of effect cleardown

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: replace `$derived.call` with `$derived.by`

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: implement nested CSS support

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: derive destructured derived objects values

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve global transition outro handling

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: add hydrate method, make hydration treeshakeable

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: prevent infinite loop when writing to store using shorthand

@ -14,31 +14,39 @@
"angry-books-jam",
"angry-plums-punch",
"beige-flies-wash",
"beige-mirrors-listen",
"beige-rabbits-shave",
"big-cars-serve",
"big-eggs-flash",
"big-eyes-carry",
"big-geese-act",
"blue-timers-film",
"brave-shrimps-kiss",
"brave-walls-destroy",
"breezy-carrots-flash",
"bright-peas-juggle",
"bright-snakes-sing",
"brown-months-fry",
"brown-spoons-boil",
"chatty-cups-drop",
"chatty-taxis-juggle",
"chilled-pumas-invite",
"chilly-dolphins-lick",
"chilly-snakes-scream",
"clean-eels-beg",
"clever-chefs-relate",
"clever-rockets-burn",
"cold-birds-own",
"cool-ants-leave",
"cool-rabbits-tickle",
"cuddly-pianos-drop",
"curly-lizards-dream",
"curvy-cups-cough",
"curvy-ties-shout",
"cyan-flowers-destroy",
"cyan-spies-grin",
"dirty-bats-punch",
"dirty-donuts-yell",
"dirty-garlics-design",
"dirty-tips-add",
"dry-clocks-grow",
@ -64,6 +72,8 @@
"fifty-steaks-float",
"five-tigers-search",
"flat-melons-protect",
"fluffy-dolls-share",
"fluffy-humans-worry",
"forty-comics-invent",
"forty-dolls-wave",
"forty-peaches-unite",
@ -73,12 +83,14 @@
"friendly-candles-relate",
"friendly-lies-camp",
"funny-wombats-argue",
"gentle-dolls-juggle",
"gentle-sheep-hug",
"gentle-spies-happen",
"giant-moons-own",
"giant-roses-press",
"good-cars-visit",
"good-pianos-jump",
"gorgeous-singers-rest",
"great-fans-unite",
"great-icons-retire",
"green-eggs-approve",
@ -141,12 +153,14 @@
"odd-taxis-retire",
"old-flies-jog",
"old-houses-drum",
"old-jokes-deliver",
"old-mails-sneeze",
"old-oranges-compete",
"olive-kangaroos-brake",
"olive-seals-sell",
"olive-shirts-complain",
"olive-socks-kick",
"orange-crews-rescue",
"orange-dingos-poke",
"pink-mayflies-tie",
"polite-dolphins-care",
@ -157,6 +171,7 @@
"popular-mangos-rest",
"pretty-ties-help",
"purple-dragons-peel",
"quiet-apricots-dream",
"quiet-camels-mate",
"quiet-crabs-nail",
"quiet-timers-speak",
@ -187,6 +202,7 @@
"shiny-shrimps-march",
"short-buses-camp",
"silent-apes-report",
"silly-laws-happen",
"silver-points-approve",
"sixty-items-crash",
"slimy-clouds-talk",
@ -205,11 +221,14 @@
"sour-forks-stare",
"sour-rules-march",
"sour-weeks-fix",
"spicy-jeans-deliver",
"spicy-plums-admire",
"spotty-pens-agree",
"spotty-rocks-destroy",
"spotty-spiders-compare",
"stale-books-perform",
"stale-comics-look",
"stale-jeans-refuse",
"strange-apricots-happen",
"strong-gifts-smoke",
"strong-lemons-provide",
@ -235,6 +254,7 @@
"ten-worms-reflect",
"thick-cycles-rule",
"thick-pans-tell",
"thick-shirts-deliver",
"thin-foxes-lick",
"thirty-flowers-sit",
"thirty-ghosts-fix",

@ -0,0 +1,5 @@
---
"svelte": patch
---
breaking: remove `createRoot`, adjust `mount`/`hydrate` APIs, introduce `unmount`

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: add proper source map support

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure inspect fires on prop changes

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: makes keyed each blocks consistent between dev and prod

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: subscribe to stores in `transition`,`animation`,`use` directives

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: encapsulate/remove selectors inside `:is(...)` and `:where(...)`

@ -51,7 +51,7 @@ We use [GitHub issues](https://github.com/sveltejs/svelte/issues) for our public
If you have questions about using Svelte, contact us on Discord at [svelte.dev/chat](https://svelte.dev/chat), and we will do our best to answer your questions.
If you see anything you'd like to be implemented, create a [feature request issue](https://github.com/sveltejs/svelte/issues/new?template=feature_request.md)
If you see anything you'd like to be implemented, create a [feature request issue](https://github.com/sveltejs/svelte/issues/new?template=feature_request.yml)
### Reporting new issues

@ -92,14 +92,16 @@ Some resources for getting started with testing:
The official routing library is [SvelteKit](https://kit.svelte.dev/). SvelteKit provides a filesystem router, server-side rendering (SSR), and hot module reloading (HMR) in one easy-to-use package. It shares similarities with Next.js for React.
However, you can use any router library. A lot of people use [page.js](https://github.com/visionmedia/page.js). There's also [navaid](https://github.com/lukeed/navaid), which is very similar. And [universal-router](https://github.com/kriasoft/universal-router), which is isomorphic with child routes, but without built-in history support.
However, you can use any router library. Some framework-agnostic libraries include [navaid](https://github.com/lukeed/navaid); [universal-router](https://github.com/kriasoft/universal-router), which is isomorphic; [Hono](https://hono.dev/), which supports multiple JS runtimes; and [TanStack Router](https://github.com/TanStack/router/tree/beta-agnostic), which is typesafe and supports caching.
If you prefer a declarative HTML approach, there's the isomorphic [svelte-routing](https://github.com/EmilTholin/svelte-routing) library and a fork of it called [svelte-navigator](https://github.com/mefechoel/svelte-navigator) containing some additional functionality.
If you prefer a declarative HTML approach, the isomorphic [svelte-routing](https://github.com/EmilTholin/svelte-routing) and [svelte-pilot](https://svelte-pilot.github.io/en/link#toc) libraries are available.
If you need hash-based routing on the client side, check out [svelte-spa-router](https://github.com/ItalyPaleAle/svelte-spa-router) or [abstract-state-router](https://github.com/TehShrike/abstract-state-router/).
[Routify](https://routify.dev) is another filesystem-based router, similar to SvelteKit's router. Version 3 supports Svelte's native SSR.
[Vike](https://vike.dev/svelte) is another Vite-based router, like SvelteKit's router, but is still experimental.
You can see a [community-maintained list of routers on sveltesociety.dev](https://sveltesociety.dev/components#routers).
## Can I tell Svelte not to remove my unused styles?

@ -30,7 +30,7 @@
"@sveltejs/eslint-config": "^6.0.4",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@vitest/coverage-v8": "^1.2.1",
"concurrently": "^8.2.2",
"cross-env": "^7.0.3",

@ -1,5 +1,69 @@
# svelte
## 5.0.0-next.58
### Patch Changes
- breaking: remove `createRoot`, adjust `mount`/`hydrate` APIs, introduce `unmount` ([#10516](https://github.com/sveltejs/svelte/pull/10516))
## 5.0.0-next.57
### Patch Changes
- fix: correctly scope CSS selectors with descendant combinators ([#10490](https://github.com/sveltejs/svelte/pull/10490))
- feat: implement support for `:is(...)` and `:where(...)` ([#10490](https://github.com/sveltejs/svelte/pull/10490))
- chore: treeshake unused store subscriptions in SSR mode ([#10506](https://github.com/sveltejs/svelte/pull/10506))
- fix: warn against accidental global event referenced ([#10442](https://github.com/sveltejs/svelte/pull/10442))
- fix: improve bind:this support for each blocks ([#10510](https://github.com/sveltejs/svelte/pull/10510))
- feat: implement nested CSS support ([#10490](https://github.com/sveltejs/svelte/pull/10490))
- breaking: encapsulate/remove selectors inside `:is(...)` and `:where(...)` ([#10490](https://github.com/sveltejs/svelte/pull/10490))
## 5.0.0-next.56
### Patch Changes
- feat: add hydrate method, make hydration treeshakeable ([#10497](https://github.com/sveltejs/svelte/pull/10497))
- fix: makes keyed each blocks consistent between dev and prod ([#10500](https://github.com/sveltejs/svelte/pull/10500))
- fix: subscribe to stores in `transition`,`animation`,`use` directives ([#10481](https://github.com/sveltejs/svelte/pull/10481))
## 5.0.0-next.55
### Patch Changes
- feat: derive destructured derived objects values ([#10488](https://github.com/sveltejs/svelte/pull/10488))
- fix: prevent infinite loop when writing to store using shorthand ([#10477](https://github.com/sveltejs/svelte/pull/10477))
- fix: add proper source map support ([#10459](https://github.com/sveltejs/svelte/pull/10459))
## 5.0.0-next.54
### Patch Changes
- breaking: replace `$derived.call` with `$derived.by` ([#10445](https://github.com/sveltejs/svelte/pull/10445))
- fix: improve global transition outro handling ([#10474](https://github.com/sveltejs/svelte/pull/10474))
## 5.0.0-next.53
### Patch Changes
- fix: only throw bind error when not passing a value ([#10090](https://github.com/sveltejs/svelte/pull/10090))
- fix: improve global transition handling of effect cleardown ([#10469](https://github.com/sveltejs/svelte/pull/10469))
- fix: improve handling of object property deletions ([#10456](https://github.com/sveltejs/svelte/pull/10456))
- fix: ensure inspect fires on prop changes ([#10468](https://github.com/sveltejs/svelte/pull/10468))
## 5.0.0-next.52
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.52",
"version": "5.0.0-next.58",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -3,6 +3,34 @@ import path from 'node:path';
import { rollup } from 'rollup';
import virtual from '@rollup/plugin-virtual';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import { compile } from 'svelte/compiler';
async function bundle_code(entry) {
const bundle = await rollup({
input: '__entry__',
plugins: [
virtual({
__entry__: entry
}),
nodeResolve({
exportConditions: ['production', 'import', 'browser', 'default']
})
],
onwarn: (warning, handle) => {
if (warning.code !== 'EMPTY_BUNDLE' && warning.code !== 'CIRCULAR_DEPENDENCY') {
handle(warning);
}
}
});
const { output } = await bundle.generate({});
if (output.length > 1) {
throw new Error('errr what');
}
return output[0].code.trim();
}
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
@ -20,31 +48,8 @@ for (const key in pkg.exports) {
if (!pkg.exports[key][type]) continue;
const subpackage = path.join(pkg.name, key);
const resolved = path.resolve(pkg.exports[key][type]);
const bundle = await rollup({
input: '__entry__',
plugins: [
virtual({
__entry__: `import ${JSON.stringify(resolved)}`
}),
nodeResolve({
exportConditions: ['production', 'import', 'browser', 'default']
})
],
onwarn: (warning, handle) => {
// if (warning.code !== 'EMPTY_BUNDLE') handle(warning);
}
});
const { output } = await bundle.generate({});
if (output.length > 1) {
throw new Error('errr what');
}
const code = output[0].code.trim();
const code = await bundle_code(`import ${JSON.stringify(resolved)}`);
if (code === '') {
// eslint-disable-next-line no-console
@ -59,6 +64,52 @@ for (const key in pkg.exports) {
}
}
const client_main = path.resolve(pkg.exports['.'].browser);
const without_hydration = await bundle_code(
// Use all features which contain hydration code to ensure it's treeshakeable
compile(
`
<script>
import { mount } from ${JSON.stringify(client_main)}; mount();
let foo;
</script>
<svelte:head><title>hi</title></svelte:head>
<a href={foo} class={foo}>a</a>
<a {...foo}>a</a>
<svelte:component this={foo} />
<svelte:element this={foo} />
<C {foo} />
{#if foo}
{/if}
{#each foo as bar}
{/each}
{#await foo}
{/await}
{#key foo}
{/key}
{#snippet x()}
{/snippet}
{@render x()}
{@html foo}
`,
{ filename: 'App.svelte' }
).js.code
);
if (!without_hydration.includes('current_hydration_fragment')) {
// eslint-disable-next-line no-console
console.error(`✅ Hydration code treeshakeable`);
} else {
// eslint-disable-next-line no-console
console.error(without_hydration);
// eslint-disable-next-line no-console
console.error(`❌ Hydration code not treeshakeable`);
failed = true;
}
// eslint-disable-next-line no-console
console.groupEnd();

@ -107,7 +107,8 @@ const css = {
'invalid-css-global-selector-list': () =>
`:global(...) must not contain type or universal selectors when used in a compound selector`,
'invalid-css-selector': () => `Invalid selector`,
'invalid-css-identifier': () => 'Expected a valid CSS identifier'
'invalid-css-identifier': () => 'Expected a valid CSS identifier',
'invalid-nesting-selector': () => `Nesting selectors can only be used inside a rule`
};
/** @satisfies {Errors} */
@ -208,7 +209,8 @@ const runes = {
'invalid-runes-mode-import': (name) => `${name} cannot be used in runes mode`,
'duplicate-props-rune': () => `Cannot use $props() more than once`,
'invalid-each-assignment': () =>
`Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. 'array[i] = value' instead of 'entry = value')`
`Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. 'array[i] = value' instead of 'entry = value')`,
'invalid-derived-call': () => `$derived.call(...) has been replaced with $derived.by(...)`
};
/** @satisfies {Errors} */

@ -102,14 +102,7 @@ export function convert(source, ast) {
},
instance,
module,
css: ast.css
? walk(ast.css, null, {
_(node) {
// @ts-ignore
delete node.parent;
}
})
: undefined
css: ast.css ? visit(ast.css) : undefined
};
},
AnimateDirective(node) {
@ -192,6 +185,24 @@ export function convert(source, ast) {
ClassDirective(node) {
return { ...node, type: 'Class' };
},
ComplexSelector(node, { visit }) {
const children = [];
for (const child of node.children) {
if (child.combinator) {
children.push(child.combinator);
}
children.push(...child.selectors);
}
return {
type: 'Selector',
start: node.start,
end: node.end,
children
};
},
Component(node, { visit }) {
return {
type: 'InlineComponent',
@ -389,6 +400,13 @@ export function convert(source, ast) {
SpreadAttribute(node) {
return { ...node, type: 'Spread' };
},
StyleSheet(node, context) {
return {
...node,
...context.next(),
type: 'Style'
};
},
SvelteBody(node, { visit }) {
return {
type: 'Body',

@ -7,6 +7,7 @@ import full_char_code_at from './utils/full_char_code_at.js';
import { error } from '../../errors.js';
import { create_fragment } from './utils/create.js';
import read_options from './read/options.js';
import { getLocator } from 'locate-character';
const regex_position_indicator = / \(\d+:\d+\)$/;
@ -41,13 +42,16 @@ export class Parser {
/** @type {LastAutoClosedTag | undefined} */
last_auto_closed_tag;
locate;
/** @param {string} template */
constructor(template) {
if (typeof template !== 'string') {
throw new TypeError('Template must be a string');
}
this.template = template.trimRight();
this.template = template.trimEnd();
this.locate = getLocator(this.template, { offsetLine: 1 });
let match_lang;
@ -133,6 +137,18 @@ export class Parser {
}
}
/**
* offset -> line/column
* @param {number} start
* @param {number} end
*/
get_location(start, end) {
return {
start: /** @type {import('locate-character').Location_1} */ (this.locate(start)),
end: /** @type {import('locate-character').Location_1} */ (this.locate(end))
};
}
current() {
return this.stack[this.stack.length - 1];
}
@ -147,9 +163,9 @@ export class Parser {
/**
* @param {string} str
* @param {boolean} [required]
* @param {boolean} required
*/
eat(str, required) {
eat(str, required = false) {
if (this.match(str)) {
this.index += str.length;
return true;
@ -297,7 +313,6 @@ export class Parser {
*/
export function parse(template) {
const parser = new Parser(template);
return parser.root;
}

@ -28,6 +28,7 @@ export default function read_pattern(parser) {
type: 'Identifier',
name,
start,
loc: parser.get_location(start, parser.index),
end: parser.index,
typeAnnotation: annotation
};

@ -18,7 +18,7 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/;
* @param {import('../index.js').Parser} parser
* @param {number} start
* @param {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute | import('#compiler').Directive>} attributes
* @returns {import('#compiler').Style}
* @returns {import('#compiler').Css.StyleSheet}
*/
export default function read_style(parser, start, attributes) {
const content_start = parser.index;
@ -28,7 +28,7 @@ export default function read_style(parser, start, attributes) {
parser.read(/^<\/style\s*>/);
return {
type: 'Style',
type: 'StyleSheet',
start,
end: parser.index,
attributes,
@ -37,8 +37,7 @@ export default function read_style(parser, start, attributes) {
start: content_start,
end: content_end,
styles: parser.template.slice(content_start, content_end)
},
parent: null
}
};
}
@ -84,36 +83,10 @@ function read_at_rule(parser) {
let block = null;
if (parser.match('{')) {
// if the parser could easily distinguish between rules and declarations, this wouldn't be necessary.
// but this approach is much simpler. in future, when we support CSS nesting, the parser _will_ need
// to be able to distinguish between them, but since we'll also need other changes to support that
// this remains a TODO
const contains_declarations = [
'color-profile',
'counter-style',
'font-face',
'font-palette-values',
'page',
'property'
].includes(name);
if (contains_declarations) {
block = read_block(parser);
} else {
const start = parser.index;
parser.eat('{', true);
const children = read_body(parser, '}');
parser.eat('}', true);
block = {
type: 'Block',
start,
end: parser.index,
children
};
}
// e.g. `@media (...) {...}`
block = read_block(parser);
} else {
// e.g. `@import '...'`
parser.eat(';', true);
}
@ -139,7 +112,11 @@ function read_rule(parser) {
prelude: read_selector_list(parser),
block: read_block(parser),
start,
end: parser.index
end: parser.index,
metadata: {
parent_rule: null,
has_local_selectors: false
}
};
}
@ -149,7 +126,7 @@ function read_rule(parser) {
* @returns {import('#compiler').Css.SelectorList}
*/
function read_selector_list(parser, inside_pseudo_class = false) {
/** @type {import('#compiler').Css.Selector[]} */
/** @type {import('#compiler').Css.ComplexSelector[]} */
const children = [];
allow_comment_or_whitespace(parser);
@ -182,47 +159,78 @@ function read_selector_list(parser, inside_pseudo_class = false) {
/**
* @param {import('../index.js').Parser} parser
* @param {boolean} [inside_pseudo_class]
* @returns {import('#compiler').Css.Selector}
* @returns {import('#compiler').Css.ComplexSelector}
*/
function read_selector(parser, inside_pseudo_class = false) {
const list_start = parser.index;
/** @type {Array<import('#compiler').Css.SimpleSelector | import('#compiler').Css.Combinator>} */
/** @type {import('#compiler').Css.RelativeSelector[]} */
const children = [];
/**
* @param {import('#compiler').Css.Combinator | null} combinator
* @param {number} start
* @returns {import('#compiler').Css.RelativeSelector}
*/
function create_selector(combinator, start) {
return {
type: 'RelativeSelector',
combinator,
selectors: [],
start,
end: -1,
metadata: {
is_global: false,
is_host: false,
is_root: false,
scoped: false
}
};
}
/** @type {import('#compiler').Css.RelativeSelector} */
let relative_selector = create_selector(null, parser.index);
while (parser.index < parser.template.length) {
const start = parser.index;
let start = parser.index;
if (parser.eat('*')) {
if (parser.eat('&')) {
relative_selector.selectors.push({
type: 'NestingSelector',
name: '&',
start,
end: parser.index
});
} else if (parser.eat('*')) {
let name = '*';
if (parser.match('|')) {
if (parser.eat('|')) {
// * is the namespace (which we ignore)
parser.index++;
name = read_identifier(parser);
}
children.push({
relative_selector.selectors.push({
type: 'TypeSelector',
name,
start,
end: parser.index
});
} else if (parser.eat('#')) {
children.push({
relative_selector.selectors.push({
type: 'IdSelector',
name: read_identifier(parser),
start,
end: parser.index
});
} else if (parser.eat('.')) {
children.push({
relative_selector.selectors.push({
type: 'ClassSelector',
name: read_identifier(parser),
start,
end: parser.index
});
} else if (parser.eat('::')) {
children.push({
relative_selector.selectors.push({
type: 'PseudoElementSelector',
name: read_identifier(parser),
start,
@ -247,7 +255,7 @@ function read_selector(parser, inside_pseudo_class = false) {
error(parser.index, 'invalid-css-global-selector');
}
children.push({
relative_selector.selectors.push({
type: 'PseudoClassSelector',
name,
args,
@ -276,7 +284,7 @@ function read_selector(parser, inside_pseudo_class = false) {
parser.allow_whitespace();
parser.eat(']', true);
children.push({
relative_selector.selectors.push({
type: 'AttributeSelector',
start,
end: parser.index,
@ -288,37 +296,28 @@ function read_selector(parser, inside_pseudo_class = false) {
} else if (inside_pseudo_class && parser.match_regex(REGEX_NTH_OF)) {
// nth of matcher must come before combinator matcher to prevent collision else the '+' in '+2n-1' would be parsed as a combinator
children.push({
relative_selector.selectors.push({
type: 'Nth',
value: /**@type {string} */ (parser.read(REGEX_NTH_OF)),
start,
end: parser.index
});
} else if (parser.match_regex(REGEX_COMBINATOR_WHITESPACE)) {
parser.allow_whitespace();
const start = parser.index;
children.push({
type: 'Combinator',
name: /** @type {string} */ (parser.read(REGEX_COMBINATOR)),
start,
end: parser.index
});
parser.allow_whitespace();
} else if (parser.match_regex(REGEX_PERCENTAGE)) {
children.push({
relative_selector.selectors.push({
type: 'Percentage',
value: /** @type {string} */ (parser.read(REGEX_PERCENTAGE)),
start,
end: parser.index
});
} else {
} else if (!parser.match_regex(REGEX_COMBINATOR)) {
let name = read_identifier(parser);
if (parser.match('|')) {
if (parser.eat('|')) {
// we ignore the namespace when trying to find matching element classes
parser.index++;
name = read_identifier(parser);
}
children.push({
relative_selector.selectors.push({
type: 'TypeSelector',
name,
start,
@ -330,29 +329,86 @@ function read_selector(parser, inside_pseudo_class = false) {
parser.allow_whitespace();
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
// rewind, so we know whether to continue building the selector list
parser.index = index;
relative_selector.end = index;
children.push(relative_selector);
return {
type: 'Selector',
type: 'ComplexSelector',
start: list_start,
end: index,
children
children,
metadata: {
rule: null,
used: false
}
};
}
if (parser.index !== index && !parser.match_regex(REGEX_COMBINATOR)) {
children.push({
type: 'Combinator',
name: ' ',
start: index,
end: parser.index
});
parser.index = index;
const combinator = read_combinator(parser);
if (combinator) {
if (relative_selector.selectors.length === 0) {
if (!inside_pseudo_class) {
error(start, 'invalid-css-selector');
}
} else {
relative_selector.end = index;
children.push(relative_selector);
}
// ...and start a new one
relative_selector = create_selector(combinator, combinator.start);
parser.allow_whitespace();
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
error(parser.index, 'invalid-css-selector');
}
}
}
error(parser.template.length, 'unexpected-eof');
}
/**
* @param {import('../index.js').Parser} parser
* @returns {import('#compiler').Css.Combinator | null}
*/
function read_combinator(parser) {
const start = parser.index;
parser.allow_whitespace();
const index = parser.index;
const name = parser.read(REGEX_COMBINATOR);
if (name) {
const end = parser.index;
parser.allow_whitespace();
return {
type: 'Combinator',
name,
start: index,
end
};
}
if (parser.index !== start) {
return {
type: 'Combinator',
name: ' ',
start,
end: parser.index
};
}
return null;
}
/**
* @param {import('../index.js').Parser} parser
* @returns {import('#compiler').Css.Block}
@ -362,7 +418,7 @@ function read_block(parser) {
parser.eat('{', true);
/** @type {Array<import('#compiler').Css.Declaration | import('#compiler').Css.Rule>} */
/** @type {Array<import('#compiler').Css.Declaration | import('#compiler').Css.Rule | import('#compiler').Css.Atrule>} */
const children = [];
while (parser.index < parser.template.length) {
@ -371,7 +427,7 @@ function read_block(parser) {
if (parser.match('}')) {
break;
} else {
children.push(read_declaration(parser));
children.push(read_block_item(parser));
}
}
@ -385,6 +441,27 @@ function read_block(parser) {
};
}
/**
* Reads a declaration, rule or at-rule
*
* @param {import('../index.js').Parser} parser
* @returns {import('#compiler').Css.Declaration | import('#compiler').Css.Rule | import('#compiler').Css.Atrule}
*/
function read_block_item(parser) {
if (parser.match('@')) {
return read_at_rule(parser);
}
// read ahead to understand whether we're dealing with a declaration or a nested rule.
// this involves some duplicated work, but avoids a try-catch that would disguise errors
const start = parser.index;
read_value(parser);
const char = parser.template[parser.index];
parser.index = start;
return char === '{' ? read_rule(parser) : read_declaration(parser);
}
/**
* @param {import('../index.js').Parser} parser
* @returns {import('#compiler').Css.Declaration}

@ -1,424 +0,0 @@
// @ts-nocheck TODO this has a bunch of type errors in strict mode which may or may not hint at bugs - check at some point
import remapping from '@ampproject/remapping';
import { push_array } from './push_array.js';
/** @param {string} s */
function last_line_length(s) {
return s.length - s.lastIndexOf('\n') - 1;
}
// mutate map in-place
/**
* @param {import('@ampproject/remapping').DecodedSourceMap} map
* @param {SourceLocation} offset
* @param {number} source_index
*/
export function sourcemap_add_offset(map, offset, source_index) {
if (map.mappings.length === 0) return;
for (let line = 0; line < map.mappings.length; line++) {
const segment_list = map.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[segment];
// shift only segments that belong to component source file
if (seg[1] === source_index) {
// also ensures that seg.length >= 4
// shift column if it points at the first line
if (seg[2] === 0) {
seg[3] += offset.column;
}
// shift line
seg[2] += offset.line;
}
}
}
}
/**
* @template T
* @param {T[]} this_table
* @param {T[]} other_table
* @returns {[T[], number[], boolean, boolean]}
*/
function merge_tables(this_table, other_table) {
const new_table = this_table.slice();
const idx_map = [];
other_table = other_table || [];
let val_changed = false;
for (const [other_idx, other_val] of other_table.entries()) {
const this_idx = this_table.indexOf(other_val);
if (this_idx >= 0) {
idx_map[other_idx] = this_idx;
} else {
const new_idx = new_table.length;
new_table[new_idx] = other_val;
idx_map[other_idx] = new_idx;
val_changed = true;
}
}
let idx_changed = val_changed;
if (val_changed) {
if (
idx_map.find(
/**
* @param {any} val
* @param {any} idx
*/ (val, idx) => val !== idx
) === undefined
) {
// idx_map is identity map [0, 1, 2, 3, 4, ....]
idx_changed = false;
}
}
return [new_table, idx_map, val_changed, idx_changed];
}
const regex_line_token = /([^\d\w\s]|\s+)/g;
export class MappedCode {
/** @type {string} */
string;
/** @type {import('@ampproject/remapping').DecodedSourceMap} */
map;
/**
* @param {any} string
* @param {import('@ampproject/remapping').DecodedSourceMap} map
*/
constructor(string = '', map = null) {
this.string = string;
if (map) {
this.map = /** @type {import('@ampproject/remapping').DecodedSourceMap} */ (map);
} else {
this.map = {
version: 3,
mappings: [],
sources: [],
names: []
};
}
}
/**
* concat in-place (mutable), return this (chainable)
* will also mutate the `other` object
* @param {MappedCode} other
* @returns {import("C:/repos/svelte/svelte-octane/mapped_code.ts-to-jsdoc").MappedCode}
*/
concat(other) {
// noop: if one is empty, return the other
if (other.string === '') return this;
if (this.string === '') {
this.string = other.string;
this.map = other.map;
return this;
}
// compute last line length before mutating
const column_offset = last_line_length(this.string);
this.string += other.string;
const m1 = this.map;
const m2 = other.map;
if (m2.mappings.length === 0) return this;
// combine sources and names
const [sources, new_source_idx, sources_changed, sources_idx_changed] = merge_tables(
m1.sources,
m2.sources
);
const [names, new_name_idx, names_changed, names_idx_changed] = merge_tables(
m1.names,
m2.names
);
if (sources_changed) m1.sources = sources;
if (names_changed) m1.names = names;
// unswitched loops are faster
if (sources_idx_changed && names_idx_changed) {
for (let line = 0; line < m2.mappings.length; line++) {
const segment_list = m2.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[segment];
if (seg[1] >= 0) seg[1] = new_source_idx[seg[1]];
if (seg[4] >= 0) seg[4] = new_name_idx[seg[4]];
}
}
} else if (sources_idx_changed) {
for (let line = 0; line < m2.mappings.length; line++) {
const segment_list = m2.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[segment];
if (seg[1] >= 0) seg[1] = new_source_idx[seg[1]];
}
}
} else if (names_idx_changed) {
for (let line = 0; line < m2.mappings.length; line++) {
const segment_list = m2.mappings[line];
for (let segment = 0; segment < segment_list.length; segment++) {
const seg = segment_list[segment];
if (seg[4] >= 0) seg[4] = new_name_idx[seg[4]];
}
}
}
// combine the mappings
// combine
// 1. last line of first map
// 2. first line of second map
// columns of 2 must be shifted
if (m2.mappings.length > 0 && column_offset > 0) {
const first_line = m2.mappings[0];
for (let i = 0; i < first_line.length; i++) {
first_line[i][0] += column_offset;
}
}
// combine last line + first line
push_array(m1.mappings[m1.mappings.length - 1], m2.mappings.shift());
// append other lines
push_array(m1.mappings, m2.mappings);
return this;
}
/**
* @static
* @param {string} string
* @param {import('@ampproject/remapping').DecodedSourceMap} [map]
* @returns {import("C:/repos/svelte/svelte-octane/mapped_code.ts-to-jsdoc").MappedCode}
*/
static from_processed(string, map) {
const line_count = string.split('\n').length;
if (map) {
// ensure that count of source map mappings lines
// is equal to count of generated code lines
// (some tools may produce less)
const missing_lines = line_count - map.mappings.length;
for (let i = 0; i < missing_lines; i++) {
map.mappings.push([]);
}
return new MappedCode(string, map);
}
if (string === '') return new MappedCode();
map = { version: 3, names: [], sources: [], mappings: [] };
// add empty SourceMapSegment[] for every line
for (let i = 0; i < line_count; i++) map.mappings.push([]);
return new MappedCode(string, map);
}
/**
* @static
* @param {import('../preprocess/types.js').Source}params_0
* @returns {import("C:/repos/svelte/svelte-octane/mapped_code.ts-to-jsdoc").MappedCode}
*/
static from_source({ source, file_basename, get_location }) {
/** @type {SourceLocation} */
let offset = get_location(0);
if (!offset) offset = { line: 0, column: 0 };
/** @type {import('@ampproject/remapping').DecodedSourceMap} */
const map = { version: 3, names: [], sources: [file_basename], mappings: [] };
if (source === '') return new MappedCode(source, map);
// we create a high resolution identity map here,
// we know that it will eventually be merged with svelte's map,
// at which stage the resolution will decrease.
const line_list = source.split('\n');
for (let line = 0; line < line_list.length; line++) {
map.mappings.push([]);
const token_list = line_list[line].split(regex_line_token);
for (let token = 0, column = 0; token < token_list.length; token++) {
if (token_list[token] === '') continue;
map.mappings[line].push([column, 0, offset.line + line, column]);
column += token_list[token].length;
}
}
// shift columns in first line
const segment_list = map.mappings[0];
for (let segment = 0; segment < segment_list.length; segment++) {
segment_list[segment][3] += offset.column;
}
return new MappedCode(source, map);
}
}
/**
* @param {string} filename
* @param {Array<import('@ampproject/remapping').DecodedSourceMap | import('@ampproject/remapping').RawSourceMap>} sourcemap_list
* @returns {import('@ampproject/remapping').RawSourceMap}
*/
export function combine_sourcemaps(filename, sourcemap_list) {
if (sourcemap_list.length === 0) return null;
let map_idx = 1;
/** @type {import('@ampproject/remapping').RawSourceMap} */
const map =
sourcemap_list.slice(0, -1).find(/** @param {any} m */ (m) => m.sources.length !== 1) ===
undefined
? remapping(
// use array interface
// only the oldest sourcemap can have multiple sources
sourcemap_list,
() => null,
true // skip optional field `sourcesContent`
)
: remapping(
// use loader interface
sourcemap_list[0], // last map
/** @type {import('@ampproject/remapping').SourceMapLoader} */ (
(sourcefile) => {
if (sourcefile === filename && sourcemap_list[map_idx]) {
return sourcemap_list[map_idx++]; // idx 1, 2, ...
// bundle file = branch node
} else {
return null; // source file = leaf node
}
}
),
true
);
if (!map.file) delete map.file; // skip optional field `file`
// When source maps are combined and the leading map is empty, sources is not set.
// Add the filename to the empty array in this case.
// Further improvements to remapping may help address this as well https://github.com/ampproject/remapping/issues/116
if (!map.sources.length) map.sources = [filename];
return map;
}
// browser vs node.js
const b64enc =
typeof btoa === 'function'
? btoa /** @param {any} b */
: (b) => Buffer.from(b).toString('base64');
const b64dec =
typeof atob === 'function'
? atob /** @param {any} a */
: (a) => Buffer.from(a, 'base64').toString();
/**
* @param {string} filename
* @param {import('magic-string').SourceMap} svelte_map
* @param {string | import('@ampproject/remapping').DecodedSourceMap | import('@ampproject/remapping').RawSourceMap} preprocessor_map_input
* @returns {import('magic-string').SourceMap}
*/
export function apply_preprocessor_sourcemap(filename, svelte_map, preprocessor_map_input) {
if (!svelte_map || !preprocessor_map_input) return svelte_map;
const preprocessor_map =
typeof preprocessor_map_input === 'string'
? JSON.parse(preprocessor_map_input)
: preprocessor_map_input;
const result_map = /** @type {import('@ampproject/remapping').RawSourceMap} */ (
combine_sourcemaps(filename, [
/** @type {import('@ampproject/remapping').RawSourceMap} */ (svelte_map),
preprocessor_map
])
);
// Svelte expects a SourceMap which includes toUrl and toString. Instead of wrapping our output in a class,
// we just tack on the extra properties.
Object.defineProperties(result_map, {
toString: {
enumerable: false,
value: function toString() {
return JSON.stringify(this);
}
},
toUrl: {
enumerable: false,
value: function toUrl() {
return 'data:application/json;charset=utf-8;base64,' + b64enc(this.toString());
}
}
});
return /** @type {import('magic-string').SourceMap} */ (result_map);
}
const regex_data_uri = /data:(?:application|text)\/json;(?:charset[:=]\S+?;)?base64,(\S*)/;
// parse attached sourcemap in processed.code
/**
* @param {import('../preprocess/types.js').Processed} processed
* @param {'script' | 'style'} tag_name
* @returns {void}
*/
export function parse_attached_sourcemap(processed, tag_name) {
const r_in = '[#@]\\s*sourceMappingURL\\s*=\\s*(\\S*)';
const regex =
tag_name === 'script'
? new RegExp('(?://' + r_in + ')|(?:/\\*' + r_in + '\\s*\\*/)$')
: new RegExp('/\\*' + r_in + '\\s*\\*/$');
/** @param {any} message */
function log_warning(message) {
// code_start: help to find preprocessor
const code_start =
processed.code.length < 100 ? processed.code : processed.code.slice(0, 100) + ' [...]';
// eslint-disable-next-line no-console
console.warn(`warning: ${message}. processed.code = ${JSON.stringify(code_start)}`);
}
processed.code = processed.code.replace(
regex,
/**
* @param {any} _
* @param {any} match1
* @param {any} match2
*/ (_, match1, match2) => {
const map_url = tag_name === 'script' ? match1 || match2 : match1;
const map_data = (map_url.match(regex_data_uri) || [])[1];
if (map_data) {
// sourceMappingURL is data URL
if (processed.map) {
log_warning(
'Not implemented. ' +
'Found sourcemap in both processed.code and processed.map. ' +
'Please update your preprocessor to return only one sourcemap.'
);
// ignore attached sourcemap
return '';
}
processed.map = b64dec(map_data); // use attached sourcemap
return ''; // remove from processed.code
}
// sourceMappingURL is path or URL
if (!processed.map) {
log_warning(
`Found sourcemap path ${JSON.stringify(
map_url
)} in processed.code, but no sourcemap data. ` +
'Please update your preprocessor to return sourcemap data directly.'
);
}
// ignore sourcemap path
return ''; // remove from processed.code
}
);
}
/**
* @typedef {{
* line: number;
* column: number;
* }} SourceLocation
*/

@ -1,845 +0,0 @@
import { get_possible_values } from './gather_possible_values.js';
import { regex_starts_with_whitespace, regex_ends_with_whitespace } from '../../patterns.js';
import { error } from '../../../errors.js';
const NO_MATCH = 'NO_MATCH';
const POSSIBLE_MATCH = 'POSSIBLE_MATCH';
const UNKNOWN_SELECTOR = 'UNKNOWN_SELECTOR';
const NodeExist = /** @type {const} */ ({
Probably: 0,
Definitely: 1
});
/** @typedef {typeof NodeExist[keyof typeof NodeExist]} NodeExistsValue */
const whitelist_attribute_selector = new Map([
['details', new Set(['open'])],
['dialog', new Set(['open'])]
]);
export default class Selector {
/** @type {import('#compiler').Css.Selector} */
node;
/** @type {import('./Stylesheet.js').default} */
stylesheet;
/** @type {Block[]} */
blocks;
/** @type {Block[]} */
local_blocks;
/** @type {boolean} */
used;
/**
* @param {import('#compiler').Css.Selector} node
* @param {import('./Stylesheet.js').default} stylesheet
*/
constructor(node, stylesheet) {
this.node = node;
this.stylesheet = stylesheet;
this.blocks = group_selectors(node);
// take trailing :global(...) selectors out of consideration
let i = this.blocks.length;
while (i > 0) {
if (!this.blocks[i - 1].global) break;
i -= 1;
}
this.local_blocks = this.blocks.slice(0, i);
const host_only = this.blocks.length === 1 && this.blocks[0].host;
const root_only = this.blocks.length === 1 && this.blocks[0].root;
this.used = this.local_blocks.length === 0 || host_only || root_only;
}
/** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */
apply(node) {
/** @type {Array<{ node: import('#compiler').RegularElement | import('#compiler').SvelteElement; block: Block }>} */
const to_encapsulate = [];
apply_selector(this.local_blocks.slice(), node, to_encapsulate);
if (to_encapsulate.length > 0) {
to_encapsulate.forEach(({ node, block }) => {
this.stylesheet.nodes_with_css_class.add(node);
block.should_encapsulate = true;
});
this.used = true;
}
}
/**
* @param {import('magic-string').default} code
* @param {string} modifier
*/
transform(code, modifier) {
/** @param {import('#compiler').Css.SimpleSelector} selector */
function remove_global_pseudo_class(selector) {
code
.remove(selector.start, selector.start + ':global('.length)
.remove(selector.end - 1, selector.end);
}
/**
* @param {Block} block
* @param {string} modifier
*/
function encapsulate_block(block, modifier) {
for (const selector of block.selectors) {
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
remove_global_pseudo_class(selector);
}
}
let i = block.selectors.length;
while (i--) {
const selector = block.selectors[i];
if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') {
if (selector.name !== 'root' && selector.name !== 'host') {
if (i === 0) code.prependRight(selector.start, modifier);
}
continue;
}
if (selector.type === 'TypeSelector' && selector.name === '*') {
code.update(selector.start, selector.end, modifier);
} else {
code.appendLeft(selector.end, modifier);
}
break;
}
}
let first = true;
for (const block of this.blocks) {
if (block.global) {
remove_global_pseudo_class(block.selectors[0]);
}
if (block.should_encapsulate) {
// for the first occurrence, we use a classname selector, so that every
// encapsulated selector gets a +0-1-0 specificity bump. thereafter,
// we use a `:where` selector, which does not affect specificity
encapsulate_block(block, first ? modifier : `:where(${modifier})`);
first = false;
}
}
}
/** @param {import('../../types.js').ComponentAnalysis} analysis */
validate(analysis) {
let start = 0;
let end = this.blocks.length;
for (; start < end; start += 1) {
if (!this.blocks[start].global) break;
}
for (; end > start; end -= 1) {
if (!this.blocks[end - 1].global) break;
}
for (let i = start; i < end; i += 1) {
if (this.blocks[i].global) {
error(this.blocks[i].selectors[0], 'invalid-css-global-placement');
}
}
this.validate_global_with_multiple_selectors();
this.validate_global_compound_selector();
this.validate_invalid_combinator_without_selector(analysis);
}
validate_global_with_multiple_selectors() {
if (this.blocks.length === 1 && this.blocks[0].selectors.length === 1) {
// standalone :global() with multiple selectors is OK
return;
}
for (const block of this.blocks) {
for (const selector of block.selectors) {
if (
selector.type === 'PseudoClassSelector' &&
selector.name === 'global' &&
selector.args !== null &&
selector.args.children.length > 1
) {
error(selector, 'invalid-css-global-selector');
}
}
}
}
/** @param {import('../../types.js').ComponentAnalysis} analysis */
validate_invalid_combinator_without_selector(analysis) {
for (let i = 0; i < this.blocks.length; i++) {
const block = this.blocks[i];
if (block.selectors.length === 0) {
error(this.node, 'invalid-css-selector');
}
}
}
validate_global_compound_selector() {
for (const block of this.blocks) {
if (block.selectors.length === 1) continue;
for (let i = 0; i < block.selectors.length; i++) {
const selector = block.selectors[i];
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
const child = selector.args?.children[0].children[0];
if (
child?.type === 'TypeSelector' &&
!/[.:#]/.test(child.name[0]) &&
(i !== 0 ||
block.selectors
.slice(1)
.some(
(s) => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector'
))
) {
error(selector, 'invalid-css-global-selector-list');
}
}
}
}
}
}
/**
* @param {Block[]} blocks
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} node
* @param {Array<{ node: import('#compiler').RegularElement | import('#compiler').SvelteElement; block: Block }>} to_encapsulate
* @returns {boolean}
*/
function apply_selector(blocks, node, to_encapsulate) {
const block = blocks.pop();
if (!block) return false;
if (!node) {
return (
(block.global && blocks.every((block) => block.global)) || (block.host && blocks.length === 0)
);
}
const applies = block_might_apply_to_node(block, node);
if (applies === NO_MATCH) {
return false;
}
if (applies === UNKNOWN_SELECTOR) {
to_encapsulate.push({ node, block });
return true;
}
if (block.combinator) {
if (block.combinator.type === 'Combinator' && block.combinator.name === ' ') {
for (const ancestor_block of blocks) {
if (ancestor_block.global) {
continue;
}
if (ancestor_block.host) {
to_encapsulate.push({ node, block });
return true;
}
/** @type {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} */
let parent = node;
while ((parent = get_element_parent(parent))) {
if (block_might_apply_to_node(ancestor_block, parent) !== NO_MATCH) {
to_encapsulate.push({ node: parent, block: ancestor_block });
}
}
if (to_encapsulate.length) {
to_encapsulate.push({ node, block });
return true;
}
}
if (blocks.every((block) => block.global)) {
to_encapsulate.push({ node, block });
return true;
}
return false;
} else if (block.combinator.name === '>') {
const has_global_parent = blocks.every((block) => block.global);
if (has_global_parent || apply_selector(blocks, get_element_parent(node), to_encapsulate)) {
to_encapsulate.push({ node, block });
return true;
}
return false;
} else if (block.combinator.name === '+' || block.combinator.name === '~') {
const siblings = get_possible_element_siblings(node, block.combinator.name === '+');
let has_match = false;
// NOTE: if we have :global(), we couldn't figure out what is selected within `:global` due to the
// css-tree limitation that does not parse the inner selector of :global
// so unless we are sure there will be no sibling to match, we will consider it as matched
const has_global = blocks.some((block) => block.global);
if (has_global) {
if (siblings.size === 0 && get_element_parent(node) !== null) {
return false;
}
to_encapsulate.push({ node, block });
return true;
}
for (const possible_sibling of siblings.keys()) {
if (apply_selector(blocks.slice(), possible_sibling, to_encapsulate)) {
to_encapsulate.push({ node, block });
has_match = true;
}
}
return has_match;
}
// TODO other combinators
to_encapsulate.push({ node, block });
return true;
}
to_encapsulate.push({ node, block });
return true;
}
const regex_backslash_and_following_character = /\\(.)/g;
/**
* @param {Block} block
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @returns {NO_MATCH | POSSIBLE_MATCH | UNKNOWN_SELECTOR}
*/
function block_might_apply_to_node(block, node) {
if (block.host || block.root) return NO_MATCH;
let i = block.selectors.length;
while (i--) {
const selector = block.selectors[i];
if (selector.type === 'Percentage' || selector.type === 'Nth') continue;
const name = selector.name.replace(regex_backslash_and_following_character, '$1');
if (selector.type === 'PseudoClassSelector' && (name === 'host' || name === 'root')) {
return NO_MATCH;
}
if (
block.selectors.length === 1 &&
selector.type === 'PseudoClassSelector' &&
name === 'global'
) {
return NO_MATCH;
}
if (selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector') {
continue;
}
if (selector.type === 'AttributeSelector') {
const whitelisted = whitelist_attribute_selector.get(node.name.toLowerCase());
if (
!whitelisted?.has(selector.name.toLowerCase()) &&
!attribute_matches(
node,
selector.name,
selector.value && unquote(selector.value),
selector.matcher,
selector.flags?.includes('i') ?? false
)
) {
return NO_MATCH;
}
} else {
if (selector.type === 'ClassSelector') {
if (
!attribute_matches(node, 'class', name, '~=', false) &&
!node.attributes.some(
(attribute) => attribute.type === 'ClassDirective' && attribute.name === name
)
) {
return NO_MATCH;
}
} else if (selector.type === 'IdSelector') {
if (!attribute_matches(node, 'id', name, '=', false)) return NO_MATCH;
} else if (selector.type === 'TypeSelector') {
if (
node.name.toLowerCase() !== name.toLowerCase() &&
name !== '*' &&
node.type !== 'SvelteElement'
) {
return NO_MATCH;
}
} else {
return UNKNOWN_SELECTOR;
}
}
}
return POSSIBLE_MATCH;
}
/**
* @param {any} operator
* @param {any} expected_value
* @param {any} case_insensitive
* @param {any} value
*/
function test_attribute(operator, expected_value, case_insensitive, value) {
if (case_insensitive) {
expected_value = expected_value.toLowerCase();
value = value.toLowerCase();
}
switch (operator) {
case '=':
return value === expected_value;
case '~=':
return value.split(/\s/).includes(expected_value);
case '|=':
return `${value}-`.startsWith(`${expected_value}-`);
case '^=':
return value.startsWith(expected_value);
case '$=':
return value.endsWith(expected_value);
case '*=':
return value.includes(expected_value);
default:
throw new Error("this shouldn't happen");
}
}
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @param {string} name
* @param {string | null} expected_value
* @param {string | null} operator
* @param {boolean} case_insensitive
*/
function attribute_matches(node, name, expected_value, operator, case_insensitive) {
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') return true;
if (attribute.type === 'BindDirective' && attribute.name === name) return true;
if (attribute.type !== 'Attribute') continue;
if (attribute.name.toLowerCase() !== name.toLowerCase()) continue;
if (attribute.value === true) return operator === null;
if (expected_value === null) return true;
const chunks = attribute.value;
if (chunks.length === 1) {
const value = chunks[0];
if (value.type === 'Text') {
return test_attribute(operator, expected_value, case_insensitive, value.data);
}
}
const possible_values = new Set();
/** @type {string[]} */
let prev_values = [];
for (const chunk of chunks) {
const current_possible_values = get_possible_values(chunk);
// impossible to find out all combinations
if (!current_possible_values) return true;
if (prev_values.length > 0) {
/** @type {string[]} */
const start_with_space = [];
/** @type {string[]} */
const remaining = [];
current_possible_values.forEach((current_possible_value) => {
if (regex_starts_with_whitespace.test(current_possible_value)) {
start_with_space.push(current_possible_value);
} else {
remaining.push(current_possible_value);
}
});
if (remaining.length > 0) {
if (start_with_space.length > 0) {
prev_values.forEach((prev_value) => possible_values.add(prev_value));
}
/** @type {string[]} */
const combined = [];
prev_values.forEach((prev_value) => {
remaining.forEach((value) => {
combined.push(prev_value + value);
});
});
prev_values = combined;
start_with_space.forEach((value) => {
if (regex_ends_with_whitespace.test(value)) {
possible_values.add(value);
} else {
prev_values.push(value);
}
});
continue;
} else {
prev_values.forEach((prev_value) => possible_values.add(prev_value));
prev_values = [];
}
}
current_possible_values.forEach((current_possible_value) => {
if (regex_ends_with_whitespace.test(current_possible_value)) {
possible_values.add(current_possible_value);
} else {
prev_values.push(current_possible_value);
}
});
if (prev_values.length < current_possible_values.size) {
prev_values.push(' ');
}
if (prev_values.length > 20) {
// might grow exponentially, bail out
return true;
}
}
prev_values.forEach((prev_value) => possible_values.add(prev_value));
for (const value of possible_values) {
if (test_attribute(operator, expected_value, case_insensitive, value)) return true;
}
}
return false;
}
/** @param {string} str */
function unquote(str) {
if ((str[0] === str[str.length - 1] && str[0] === "'") || str[0] === '"') {
return str.slice(1, str.length - 1);
}
return str;
}
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @returns {import('#compiler').RegularElement | import('#compiler').SvelteElement | null}
*/
function get_element_parent(node) {
/** @type {import('#compiler').SvelteNode | null} */
let parent = node;
while (
// @ts-expect-error TODO figure out a more elegant solution
(parent = parent.parent) &&
parent.type !== 'RegularElement' &&
parent.type !== 'SvelteElement'
);
return parent ?? null;
}
/**
* Finds the given node's previous sibling in the DOM
*
* The Svelte `<slot>` is just a placeholder and is not actually real. Any children nodes
* in `<slot>` are 'flattened' and considered as the same level as the `<slot>`'s siblings
*
* e.g.
* ```html
* <h1>Heading 1</h1>
* <slot>
* <h2>Heading 2</h2>
* </slot>
* ```
*
* is considered to look like:
* ```html
* <h1>Heading 1</h1>
* <h2>Heading 2</h2>
* ```
* @param {import('#compiler').SvelteNode} node
* @returns {import('#compiler').SvelteNode}
*/
function find_previous_sibling(node) {
/** @type {import('#compiler').SvelteNode} */
let current_node = node;
do {
if (current_node.type === 'SlotElement') {
const slot_children = current_node.fragment.nodes;
if (slot_children.length > 0) {
current_node = slot_children.slice(-1)[0]; // go to its last child first
continue;
}
}
while (
// @ts-expect-error TODO
!current_node.prev &&
// @ts-expect-error TODO
current_node.parent &&
// @ts-expect-error TODO
current_node.parent.type === 'SlotElement'
) {
// @ts-expect-error TODO
current_node = current_node.parent;
}
// @ts-expect-error
current_node = current_node.prev;
} while (current_node && current_node.type === 'SlotElement');
return current_node;
}
/**
* @param {import('#compiler').SvelteNode} node
* @param {boolean} adjacent_only
* @returns {Map<import('#compiler').RegularElement, NodeExistsValue>}
*/
function get_possible_element_siblings(node, adjacent_only) {
/** @type {Map<import('#compiler').RegularElement, NodeExistsValue>} */
const result = new Map();
/** @type {import('#compiler').SvelteNode} */
let prev = node;
while ((prev = find_previous_sibling(prev))) {
if (prev.type === 'RegularElement') {
if (
!prev.attributes.find(
(attr) => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot'
)
) {
result.set(prev, NodeExist.Definitely);
}
if (adjacent_only) {
break;
}
} else if (prev.type === 'EachBlock' || prev.type === 'IfBlock' || prev.type === 'AwaitBlock') {
const possible_last_child = get_possible_last_child(prev, adjacent_only);
add_to_map(possible_last_child, result);
if (adjacent_only && has_definite_elements(possible_last_child)) {
return result;
}
}
}
if (!prev || !adjacent_only) {
/** @type {import('#compiler').SvelteNode | null} */
let parent = node;
while (
// @ts-expect-error TODO
(parent = parent?.parent) &&
(parent.type === 'EachBlock' || parent.type === 'IfBlock' || parent.type === 'AwaitBlock')
) {
const possible_siblings = get_possible_element_siblings(parent, adjacent_only);
add_to_map(possible_siblings, result);
// @ts-expect-error
if (parent.type === 'EachBlock' && !parent.fallback?.nodes.includes(node)) {
// `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />`
add_to_map(get_possible_last_child(parent, adjacent_only), result);
}
if (adjacent_only && has_definite_elements(possible_siblings)) {
break;
}
}
}
return result;
}
/**
* @param {import('#compiler').EachBlock | import('#compiler').IfBlock | import('#compiler').AwaitBlock} block
* @param {boolean} adjacent_only
* @returns {Map<import('#compiler').RegularElement, NodeExistsValue>}
*/
function get_possible_last_child(block, adjacent_only) {
/** @typedef {Map<import('#compiler').RegularElement, NodeExistsValue>} NodeMap */
/** @type {NodeMap} */
const result = new Map();
if (block.type === 'EachBlock') {
/** @type {NodeMap} */
const each_result = loop_child(block.body.nodes, adjacent_only);
/** @type {NodeMap} */
const else_result = block.fallback
? loop_child(block.fallback.nodes, adjacent_only)
: new Map();
const not_exhaustive = !has_definite_elements(else_result);
if (not_exhaustive) {
mark_as_probably(each_result);
mark_as_probably(else_result);
}
add_to_map(each_result, result);
add_to_map(else_result, result);
} else if (block.type === 'IfBlock') {
/** @type {NodeMap} */
const if_result = loop_child(block.consequent.nodes, adjacent_only);
/** @type {NodeMap} */
const else_result = block.alternate
? loop_child(block.alternate.nodes, adjacent_only)
: new Map();
const not_exhaustive = !has_definite_elements(if_result) || !has_definite_elements(else_result);
if (not_exhaustive) {
mark_as_probably(if_result);
mark_as_probably(else_result);
}
add_to_map(if_result, result);
add_to_map(else_result, result);
} else if (block.type === 'AwaitBlock') {
/** @type {NodeMap} */
const pending_result = block.pending
? loop_child(block.pending.nodes, adjacent_only)
: new Map();
/** @type {NodeMap} */
const then_result = block.then ? loop_child(block.then.nodes, adjacent_only) : new Map();
/** @type {NodeMap} */
const catch_result = block.catch ? loop_child(block.catch.nodes, adjacent_only) : new Map();
const not_exhaustive =
!has_definite_elements(pending_result) ||
!has_definite_elements(then_result) ||
!has_definite_elements(catch_result);
if (not_exhaustive) {
mark_as_probably(pending_result);
mark_as_probably(then_result);
mark_as_probably(catch_result);
}
add_to_map(pending_result, result);
add_to_map(then_result, result);
add_to_map(catch_result, result);
}
return result;
}
/**
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} result
* @returns {boolean}
*/
function has_definite_elements(result) {
if (result.size === 0) return false;
for (const exist of result.values()) {
if (exist === NodeExist.Definitely) {
return true;
}
}
return false;
}
/**
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} from
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} to
* @returns {void}
*/
function add_to_map(from, to) {
from.forEach((exist, element) => {
to.set(element, higher_existence(exist, to.get(element)));
});
}
/**
* @param {NodeExistsValue | undefined} exist1
* @param {NodeExistsValue | undefined} exist2
* @returns {NodeExistsValue}
*/
function higher_existence(exist1, exist2) {
// @ts-expect-error TODO figure out if this is a bug
if (exist1 === undefined || exist2 === undefined) return exist1 || exist2;
return exist1 > exist2 ? exist1 : exist2;
}
/** @param {Map<import('#compiler').RegularElement, NodeExistsValue>} result */
function mark_as_probably(result) {
for (const key of result.keys()) {
result.set(key, NodeExist.Probably);
}
}
/**
* @param {import('#compiler').SvelteNode[]} children
* @param {boolean} adjacent_only
*/
function loop_child(children, adjacent_only) {
/** @type {Map<import('#compiler').RegularElement, NodeExistsValue>} */
const result = new Map();
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (child.type === 'RegularElement') {
result.set(child, NodeExist.Definitely);
if (adjacent_only) {
break;
}
} else if (
child.type === 'EachBlock' ||
child.type === 'IfBlock' ||
child.type === 'AwaitBlock'
) {
const child_result = get_possible_last_child(child, adjacent_only);
add_to_map(child_result, result);
if (adjacent_only && has_definite_elements(child_result)) {
break;
}
}
}
return result;
}
class Block {
/** @type {boolean} */
host;
/** @type {boolean} */
root;
/** @type {import('#compiler').Css.Combinator | null} */
combinator;
/** @type {import('#compiler').Css.SimpleSelector[]} */
selectors;
/** @type {number} */
start;
/** @type {number} */
end;
/** @type {boolean} */
should_encapsulate;
/** @param {import('#compiler').Css.Combinator | null} combinator */
constructor(combinator) {
this.combinator = combinator;
this.host = false;
this.root = false;
this.selectors = [];
this.start = -1;
this.end = -1;
this.should_encapsulate = false;
}
/** @param {import('#compiler').Css.SimpleSelector} selector */
add(selector) {
if (this.selectors.length === 0) {
this.start = selector.start;
this.host = selector.type === 'PseudoClassSelector' && selector.name === 'host';
}
this.root = this.root || (selector.type === 'PseudoClassSelector' && selector.name === 'root');
this.selectors.push(selector);
this.end = selector.end;
}
get global() {
return (
this.selectors.length >= 1 &&
this.selectors[0].type === 'PseudoClassSelector' &&
this.selectors[0].name === 'global' &&
this.selectors.every(
(selector) =>
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
)
);
}
}
/** @param {import('#compiler').Css.Selector} selector */
function group_selectors(selector) {
let block = new Block(null);
const blocks = [block];
selector.children.forEach((child) => {
if (child.type === 'Combinator') {
block = new Block(child);
blocks.push(block);
} else {
block.add(child);
}
});
return blocks;
}

@ -1,550 +0,0 @@
import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
import Selector from './Selector.js';
import hash from '../utils/hash.js';
// import compiler_warnings from '../compiler_warnings.js';
// import { extract_ignores_above_position } from '../utils/extract_svelte_ignore.js';
import { push_array } from '../utils/push_array.js';
import { create_attribute } from '../../nodes.js';
const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/;
const regex_name_boundary = /^[\s,;}]$/;
/**
* @param {string} name
* @returns {string}
*/
function remove_css_prefix(name) {
return name.replace(regex_css_browser_prefix, '');
}
/** @param {import('#compiler').Css.Atrule} node */
const is_keyframes_node = (node) => remove_css_prefix(node.name) === 'keyframes';
/**
*
* @param {import('#compiler').Css.Rule} node
* @param {MagicString} code
*/
function escape_comment_close(node, code) {
let escaped = false;
let in_comment = false;
for (let i = node.start; i < node.end; i++) {
if (escaped) {
escaped = false;
} else {
const char = code.original[i];
if (in_comment) {
if (char === '*' && code.original[i + 1] === '/') {
code.prependRight(++i, '\\');
in_comment = false;
}
} else if (char === '\\') {
escaped = true;
} else if (char === '/' && code.original[++i] === '*') {
in_comment = true;
}
}
}
}
class Rule {
/** @type {import('./Selector.js').default[]} */
selectors;
/** @type {Declaration[]} */
declarations;
/** @type {import('#compiler').Css.Rule} */
node;
/** @type {Atrule | undefined} */
parent;
/**
* @param {import('#compiler').Css.Rule} node
* @param {any} stylesheet
* @param {Atrule | undefined} parent
*/
constructor(node, stylesheet, parent) {
this.node = node;
this.parent = parent;
this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet));
this.declarations = /** @type {import('#compiler').Css.Declaration[]} */ (
node.block.children
).map((node) => new Declaration(node));
}
/** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */
apply(node) {
this.selectors.forEach((selector) => selector.apply(node)); // TODO move the logic in here?
}
/** @param {boolean} dev */
is_used(dev) {
if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node))
return true;
// keep empty rules in dev, because it's convenient to
// see them in devtools
if (this.declarations.length === 0) return dev;
return this.selectors.some((s) => s.used);
}
/**
* @param {import('magic-string').default} code
* @param {string} id
* @param {Map<string, string>} keyframes
*/
transform(code, id, keyframes) {
if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) {
return;
}
const modifier = `.${id}`;
this.selectors.forEach((selector) => selector.transform(code, modifier));
this.declarations.forEach((declaration) => declaration.transform(code, keyframes));
}
/** @param {import('../../types.js').ComponentAnalysis} analysis */
validate(analysis) {
this.selectors.forEach((selector) => {
selector.validate(analysis);
});
}
/** @param {(selector: import('./Selector.js').default) => void} handler */
warn_on_unused_selector(handler) {
this.selectors.forEach((selector) => {
if (!selector.used) handler(selector);
});
}
/**
* @param {MagicString} code
* @param {boolean} dev
*/
prune(code, dev) {
if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) {
return;
}
// keep empty rules in dev, because it's convenient to
// see them in devtools
if (this.declarations.length === 0) {
if (!dev) {
code.prependRight(this.node.start, '/* (empty) ');
code.appendLeft(this.node.end, '*/');
escape_comment_close(this.node, code);
}
return;
}
const used = this.selectors.filter((s) => s.used);
if (used.length === 0) {
code.prependRight(this.node.start, '/* (unused) ');
code.appendLeft(this.node.end, '*/');
escape_comment_close(this.node, code);
return;
}
if (used.length < this.selectors.length) {
let pruning = false;
let last = this.selectors[0].node.start;
for (let i = 0; i < this.selectors.length; i += 1) {
const selector = this.selectors[i];
if (selector.used === pruning) {
if (pruning) {
let i = selector.node.start;
while (code.original[i] !== ',') i--;
code.overwrite(i, i + 1, '*/');
} else {
if (i === 0) {
code.prependRight(selector.node.start, '/* (unused) ');
} else {
code.overwrite(last, selector.node.start, ' /* (unused) ');
}
}
pruning = !pruning;
}
last = selector.node.end;
}
if (pruning) {
code.appendLeft(last, '*/');
}
}
}
}
class Declaration {
/** @type {import('#compiler').Css.Declaration} */
node;
/** @param {import('#compiler').Css.Declaration} node */
constructor(node) {
this.node = node;
}
/**
* @param {import('magic-string').default} code
* @param {Map<string, string>} keyframes
*/
transform(code, keyframes) {
const property = this.node.property && remove_css_prefix(this.node.property.toLowerCase());
if (property === 'animation' || property === 'animation-name') {
let index = this.node.start + this.node.property.length + 1;
let name = '';
while (index < code.original.length) {
const character = code.original[index];
if (regex_name_boundary.test(character)) {
const keyframe = keyframes.get(name);
if (keyframe) {
code.update(index - name.length, index, keyframe);
}
if (character === ';' || character === '}') {
break;
}
name = '';
} else {
name += character;
}
index++;
}
}
}
}
class Atrule {
/** @type {import('#compiler').Css.Atrule} */
node;
/** @type {Array<Atrule | Rule>} */
children;
/** @type {Declaration[]} */
declarations;
/** @param {import('#compiler').Css.Atrule} node */
constructor(node) {
this.node = node;
this.children = [];
this.declarations = [];
}
/** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */
apply(node) {
if (
this.node.name === 'container' ||
this.node.name === 'media' ||
this.node.name === 'supports' ||
this.node.name === 'layer'
) {
this.children.forEach((child) => {
child.apply(node);
});
} else if (is_keyframes_node(this.node)) {
/** @type {Rule[]} */ (this.children).forEach((rule) => {
rule.selectors.forEach((selector) => {
selector.used = true;
});
});
}
}
/** @param {boolean} _dev */
is_used(_dev) {
return true; // TODO
}
/**
* @param {import('magic-string').default} code
* @param {string} id
* @param {Map<string, string>} keyframes
*/
transform(code, id, keyframes) {
if (is_keyframes_node(this.node)) {
let start = this.node.start + this.node.name.length + 1;
while (code.original[start] === ' ') start += 1;
let end = start;
while (code.original[end] !== '{' && code.original[end] !== ' ') end += 1;
if (this.node.prelude.startsWith('-global-')) {
code.remove(start, start + 8);
/** @type {Rule[]} */ (this.children).forEach((rule) => {
rule.selectors.forEach((selector) => {
selector.used = true;
});
});
} else {
const keyframe = /** @type {string} */ (keyframes.get(this.node.prelude));
code.update(start, end, keyframe);
}
}
this.children.forEach((child) => {
child.transform(code, id, keyframes);
});
}
/** @param {import('../../types.js').ComponentAnalysis} analysis */
validate(analysis) {
this.children.forEach((child) => {
child.validate(analysis);
});
}
/** @param {(selector: import('./Selector.js').default) => void} handler */
warn_on_unused_selector(handler) {
if (this.node.name !== 'media') return;
this.children.forEach((child) => {
child.warn_on_unused_selector(handler);
});
}
/**
* @param {MagicString} code
* @param {boolean} dev
*/
prune(code, dev) {
// TODO prune children
}
}
export default class Stylesheet {
/** @type {import('#compiler').Style | null} */
ast;
/** @type {string} */
filename;
/** @type {boolean} */
has_styles;
/** @type {string} */
id;
/** @type {Array<Rule | Atrule>} */
children = [];
/** @type {Map<string, string>} */
keyframes = new Map();
/** @type {Set<import('#compiler').RegularElement | import('#compiler').SvelteElement>} */
nodes_with_css_class = new Set();
/**
* @param {{
* ast: import('#compiler').Style | null;
* filename: string;
* component_name: string;
* get_css_hash: import('#compiler').CssHashGetter;
* }} params
*/
constructor({ ast, component_name, filename, get_css_hash }) {
this.ast = ast;
this.filename = filename;
if (!ast || ast.children.length === 0) {
this.has_styles = false;
this.id = '';
return;
}
this.id = get_css_hash({
filename,
name: component_name,
css: ast.content.styles,
hash
});
this.has_styles = true;
const state = {
/** @type {Atrule | undefined} */
atrule: undefined
};
walk(/** @type {import('#compiler').Css.Node} */ (ast), state, {
Atrule: (node, context) => {
const atrule = new Atrule(node);
if (context.state.atrule) {
context.state.atrule.children.push(atrule);
} else {
this.children.push(atrule);
}
if (is_keyframes_node(node)) {
if (!node.prelude.startsWith('-global-')) {
this.keyframes.set(node.prelude, `${this.id}-${node.prelude}`);
}
} else if (node.block) {
/** @type {Declaration[]} */
const declarations = [];
for (const child of node.block.children) {
if (child.type === 'Declaration') {
declarations.push(new Declaration(child));
}
}
if (declarations.length > 0) {
push_array(atrule.declarations, declarations);
}
}
context.next({
...context.state,
atrule
});
},
Rule: (node, context) => {
const rule = new Rule(node, this, context.state.atrule);
if (context.state.atrule) {
context.state.atrule.children.push(rule);
} else {
this.children.push(rule);
}
context.next();
}
});
}
/** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */
apply(node) {
if (!this.has_styles) return;
for (let i = 0; i < this.children.length; i += 1) {
const child = this.children[i];
child.apply(node);
}
}
/** @param {boolean} is_dom_mode */
reify(is_dom_mode) {
nodes: for (const node of this.nodes_with_css_class) {
// Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them
// TODO this happens during the analysis phase, which shouldn't know anything about client vs server
if (node.type === 'SvelteElement' && is_dom_mode) continue;
/** @type {import('#compiler').Attribute | undefined} */
let class_attribute = undefined;
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') {
// The spread method appends the hash to the end of the class attribute on its own
continue nodes;
}
if (attribute.type !== 'Attribute') continue;
if (attribute.name.toLowerCase() !== 'class') continue;
class_attribute = attribute;
}
if (class_attribute && class_attribute.value !== true) {
const chunks = class_attribute.value;
if (chunks.length === 1 && chunks[0].type === 'Text') {
chunks[0].data += ` ${this.id}`;
} else {
chunks.push({
type: 'Text',
data: ` ${this.id}`,
raw: ` ${this.id}`,
start: -1,
end: -1,
parent: null
});
}
} else {
node.attributes.push(
create_attribute('class', -1, -1, [
{ type: 'Text', data: this.id, raw: this.id, parent: null, start: -1, end: -1 }
])
);
}
}
}
/**
* @param {string} file
* @param {string} source
* @param {boolean} dev
*/
render(file, source, dev) {
// TODO neaten this up
if (!this.ast) throw new Error('Unexpected error');
const code = new MagicString(source);
walk(/** @type {import('#compiler').Css.Node} */ (this.ast), null, {
_: (node) => {
code.addSourcemapLocation(node.start);
code.addSourcemapLocation(node.end);
}
});
for (const child of this.children) {
child.transform(code, this.id, this.keyframes);
}
code.remove(0, this.ast.content.start);
for (const child of this.children) {
child.prune(code, dev);
}
code.remove(/** @type {number} */ (this.ast.content.end), source.length);
return {
code: code.toString(),
map: code.generateMap({
includeContent: true,
source: this.filename,
file
})
};
}
/** @param {import('../../types.js').ComponentAnalysis} analysis */
validate(analysis) {
this.children.forEach((child) => {
child.validate(analysis);
});
}
/** @param {import('../../types.js').ComponentAnalysis} analysis */
warn_on_unused_selectors(analysis) {
// const ignores = !this.ast
// ? []
// : extract_ignores_above_position(this.ast.css.start, this.ast.html.children);
// analysis.push_ignores(ignores);
// this.children.forEach((child) => {
// child.warn_on_unused_selector((selector) => {
// analysis.warn(selector.node, {
// code: 'css-unused-selector',
// message: `Unused CSS selector "${this.source.slice(
// selector.node.start,
// selector.node.end
// )}"`
// });
// });
// });
// analysis.pop_ignores();
}
}

@ -0,0 +1,158 @@
import { walk } from 'zimmerframe';
import { error } from '../../../errors.js';
import { is_keyframes_node } from '../../css.js';
import { merge } from '../../visitors.js';
/**
* @typedef {import('zimmerframe').Visitors<
* import('#compiler').Css.Node,
* {
* keyframes: string[];
* rule: import('#compiler').Css.Rule | null;
* }
* >} Visitors
*/
/** @param {import('#compiler').Css.RelativeSelector} relative_selector */
function is_global(relative_selector) {
const first = relative_selector.selectors[0];
return (
first.type === 'PseudoClassSelector' &&
first.name === 'global' &&
relative_selector.selectors.every(
(selector) =>
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
)
);
}
/** @type {Visitors} */
const analysis_visitors = {
Atrule(node, context) {
if (is_keyframes_node(node)) {
if (!node.prelude.startsWith('-global-')) {
context.state.keyframes.push(node.prelude);
}
}
},
ComplexSelector(node, context) {
context.next(); // analyse relevant selectors first
node.metadata.rule = context.state.rule;
node.metadata.used = node.children.every(
({ metadata }) => metadata.is_global || metadata.is_host || metadata.is_root
);
},
RelativeSelector(node, context) {
node.metadata.is_global =
node.selectors.length >= 1 &&
node.selectors[0].type === 'PseudoClassSelector' &&
node.selectors[0].name === 'global' &&
node.selectors.every(
(selector) =>
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
);
if (node.selectors.length === 1) {
const first = node.selectors[0];
node.metadata.is_host = first.type === 'PseudoClassSelector' && first.name === 'host';
}
node.metadata.is_root = !!node.selectors.find(
(child) => child.type === 'PseudoClassSelector' && child.name === 'root'
);
context.next();
},
Rule(node, context) {
node.metadata.parent_rule = context.state.rule;
context.next({
...context.state,
rule: node
});
node.metadata.has_local_selectors = node.prelude.children.some((selector) => {
return selector.children.some(
({ metadata }) => !metadata.is_global && !metadata.is_host && !metadata.is_root
);
});
}
};
/** @type {Visitors} */
const validation_visitors = {
ComplexSelector(node, context) {
// ensure `:global(...)` is not used in the middle of a selector
{
const a = node.children.findIndex((child) => !is_global(child));
const b = node.children.findLastIndex((child) => !is_global(child));
if (a !== b) {
for (let i = a; i <= b; i += 1) {
if (is_global(node.children[i])) {
error(node.children[i].selectors[0], 'invalid-css-global-placement');
}
}
}
}
// ensure `:global(...)`contains a single selector
// (standalone :global() with multiple selectors is OK)
if (node.children.length > 1 || node.children[0].selectors.length > 1) {
for (const relative_selector of node.children) {
for (const selector of relative_selector.selectors) {
if (
selector.type === 'PseudoClassSelector' &&
selector.name === 'global' &&
selector.args !== null &&
selector.args.children.length > 1
) {
error(selector, 'invalid-css-global-selector');
}
}
}
}
// ensure `:global(...)` is not part of a larger compound selector
for (const relative_selector of node.children) {
for (let i = 0; i < relative_selector.selectors.length; i++) {
const selector = relative_selector.selectors[i];
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
const child = selector.args?.children[0].children[0];
if (
child?.selectors[0].type === 'TypeSelector' &&
!/[.:#]/.test(child.selectors[0].name[0]) &&
(i !== 0 ||
relative_selector.selectors
.slice(1)
.some(
(s) => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector'
))
) {
error(selector, 'invalid-css-global-selector-list');
}
}
}
}
},
NestingSelector(node, context) {
const rule = /** @type {import('#compiler').Css.Rule} */ (context.state.rule);
if (!rule.metadata.parent_rule) {
error(node, 'invalid-nesting-selector');
}
}
};
const css_visitors = merge(analysis_visitors, validation_visitors);
/**
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @param {import('../../types.js').ComponentAnalysis} analysis
*/
export function analyze_css(stylesheet, analysis) {
walk(stylesheet, { keyframes: analysis.css.keyframes, rule: null }, css_visitors);
}

@ -0,0 +1,792 @@
import { walk } from 'zimmerframe';
import { get_possible_values } from './utils.js';
import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
import { error } from '../../../errors.js';
/**
* @typedef {{
* stylesheet: import('#compiler').Css.StyleSheet;
* element: import('#compiler').RegularElement | import('#compiler').SvelteElement;
* }} State
*/
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
const NODE_PROBABLY_EXISTS = 0;
const NODE_DEFINITELY_EXISTS = 1;
const whitelist_attribute_selector = new Map([
['details', ['open']],
['dialog', ['open']]
]);
/** @type {import('#compiler').Css.Combinator} */
const descendant_combinator = {
type: 'Combinator',
name: ' ',
start: -1,
end: -1
};
/** @type {import('#compiler').Css.RelativeSelector} */
const nesting_selector = {
type: 'RelativeSelector',
start: -1,
end: -1,
combinator: null,
selectors: [
{
type: 'NestingSelector',
name: '&',
start: -1,
end: -1
}
],
metadata: {
is_global: false,
is_host: false,
is_root: false,
scoped: false
}
};
/**
*
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
*/
export function prune(stylesheet, element) {
walk(stylesheet, { stylesheet, element }, visitors);
}
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, State>} */
const visitors = {
ComplexSelector(node, context) {
const selectors = truncate(node);
const inner = selectors[selectors.length - 1];
if (node.metadata.rule?.metadata.parent_rule) {
const has_explicit_nesting_selector = selectors.some((selector) =>
selector.selectors.some((s) => s.type === 'NestingSelector')
);
if (!has_explicit_nesting_selector) {
selectors[0] = {
...selectors[0],
combinator: descendant_combinator
};
selectors.unshift(nesting_selector);
}
}
if (
apply_selector(
selectors,
/** @type {import('#compiler').Css.Rule} */ (node.metadata.rule),
context.state.element,
context.state.stylesheet
)
) {
mark(inner, context.state.element);
node.metadata.used = true;
}
// note: we don't call context.next() here, we only recurse into
// selectors that don't belong to rules (i.e. inside `:is(...)` etc)
// when we encounter them below
}
};
/**
* Discard trailing `:global(...)` selectors, these are unused for scoping purposes
* @param {import('#compiler').Css.ComplexSelector} node
*/
function truncate(node) {
const i = node.children.findLastIndex(({ metadata }) => {
return !metadata.is_global && !metadata.is_host && !metadata.is_root;
});
return node.children.slice(0, i + 1);
}
/**
* @param {import('#compiler').Css.RelativeSelector[]} relative_selectors
* @param {import('#compiler').Css.Rule} rule
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @returns {boolean}
*/
function apply_selector(relative_selectors, rule, element, stylesheet) {
const parent_selectors = relative_selectors.slice();
const relative_selector = parent_selectors.pop();
if (!relative_selector) return false;
const possible_match = relative_selector_might_apply_to_node(
relative_selector,
rule,
element,
stylesheet
);
if (!possible_match) {
return false;
}
if (relative_selector.combinator) {
const name = relative_selector.combinator.name;
switch (name) {
case ' ':
case '>': {
let parent = /** @type {import('#compiler').TemplateNode | null} */ (element.parent);
let parent_matched = false;
let crossed_component_boundary = false;
while (parent) {
if (parent.type === 'Component' || parent.type === 'SvelteComponent') {
crossed_component_boundary = true;
}
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
if (apply_selector(parent_selectors, rule, parent, stylesheet)) {
// TODO the `name === ' '` causes false positives, but removing it causes false negatives...
if (name === ' ' || crossed_component_boundary) {
mark(parent_selectors[parent_selectors.length - 1], parent);
}
parent_matched = true;
}
if (name === '>') return parent_matched;
}
parent = /** @type {import('#compiler').TemplateNode | null} */ (parent.parent);
}
return parent_matched || parent_selectors.every((selector) => is_global(selector, rule));
}
case '+':
case '~': {
const siblings = get_possible_element_siblings(element, name === '+');
let sibling_matched = false;
for (const possible_sibling of siblings.keys()) {
if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) {
mark(relative_selector, element);
sibling_matched = true;
}
}
return (
sibling_matched ||
(get_element_parent(element) === null &&
parent_selectors.every((selector) => is_global(selector, rule)))
);
}
default:
// TODO other combinators
return true;
}
}
// if this is the left-most non-global selector, mark it — we want
// `x y z {...}` to become `x.blah y z.blah {...}`
const parent = parent_selectors[parent_selectors.length - 1];
if (!parent || is_global(parent, rule)) {
mark(relative_selector, element);
}
return true;
}
/**
* Mark both the compound selector and the node it selects as encapsulated,
* for transformation in a later step
* @param {import('#compiler').Css.RelativeSelector} relative_selector
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
*/
function mark(relative_selector, element) {
relative_selector.metadata.scoped = true;
element.metadata.scoped = true;
}
/**
* Returns `true` if the relative selector is global, meaning
* it's a `:global(...)` or `:host` or `:root` selector, or
* is an `:is(...)` or `:where(...)` selector that contains
* a global selector
* @param {import('#compiler').Css.RelativeSelector} selector
* @param {import('#compiler').Css.Rule} rule
*/
function is_global(selector, rule) {
if (selector.metadata.is_global || selector.metadata.is_host || selector.metadata.is_root) {
return true;
}
for (const s of selector.selectors) {
/** @type {import('#compiler').Css.SelectorList | null} */
let selector_list = null;
let owner = rule;
if (s.type === 'PseudoClassSelector') {
if ((s.name === 'is' || s.name === 'where') && s.args) {
selector_list = s.args;
}
}
if (s.type === 'NestingSelector') {
owner = /** @type {import('#compiler').Css.Rule} */ (rule.metadata.parent_rule);
selector_list = owner.prelude;
}
const has_global_selectors = selector_list?.children.some((complex_selector) => {
return complex_selector.children.every((relative_selector) =>
is_global(relative_selector, owner)
);
});
if (!has_global_selectors) {
return false;
}
}
return true;
}
const regex_backslash_and_following_character = /\\(.)/g;
/**
* Ensure that `element` satisfies each simple selector in `relative_selector`
*
* @param {import('#compiler').Css.RelativeSelector} relative_selector
* @param {import('#compiler').Css.Rule} rule
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @returns {boolean}
*/
function relative_selector_might_apply_to_node(relative_selector, rule, element, stylesheet) {
for (const selector of relative_selector.selectors) {
if (selector.type === 'Percentage' || selector.type === 'Nth') continue;
const name = selector.name.replace(regex_backslash_and_following_character, '$1');
switch (selector.type) {
case 'PseudoClassSelector': {
if (name === 'host' || name === 'root') {
return false;
}
if (name === 'global' && relative_selector.selectors.length === 1) {
const args = /** @type {import('#compiler').Css.SelectorList} */ (selector.args);
const complex_selector = args.children[0];
return apply_selector(complex_selector.children, rule, element, stylesheet);
}
if ((name === 'is' || name === 'where') && selector.args) {
let matched = false;
for (const complex_selector of selector.args.children) {
if (apply_selector(truncate(complex_selector), rule, element, stylesheet)) {
complex_selector.metadata.used = true;
matched = true;
}
}
if (!matched) {
return false;
}
}
break;
}
case 'PseudoElementSelector': {
break;
}
case 'AttributeSelector': {
const whitelisted = whitelist_attribute_selector.get(element.name.toLowerCase());
if (
!whitelisted?.includes(selector.name.toLowerCase()) &&
!attribute_matches(
element,
selector.name,
selector.value && unquote(selector.value),
selector.matcher,
selector.flags?.includes('i') ?? false
)
) {
return false;
}
break;
}
case 'ClassSelector': {
if (
!attribute_matches(element, 'class', name, '~=', false) &&
!element.attributes.some(
(attribute) => attribute.type === 'ClassDirective' && attribute.name === name
)
) {
return false;
}
break;
}
case 'IdSelector': {
if (!attribute_matches(element, 'id', name, '=', false)) {
return false;
}
break;
}
case 'TypeSelector': {
if (
element.name.toLowerCase() !== name.toLowerCase() &&
name !== '*' &&
element.type !== 'SvelteElement'
) {
return false;
}
break;
}
case 'NestingSelector': {
let matched = false;
const parent = /** @type {import('#compiler').Css.Rule} */ (rule.metadata.parent_rule);
for (const complex_selector of parent.prelude.children) {
if (apply_selector(truncate(complex_selector), parent, element, stylesheet)) {
complex_selector.metadata.used = true;
matched = true;
}
}
if (!matched) {
return false;
}
break;
}
}
}
// possible match
return true;
}
/**
* @param {any} operator
* @param {any} expected_value
* @param {any} case_insensitive
* @param {any} value
*/
function test_attribute(operator, expected_value, case_insensitive, value) {
if (case_insensitive) {
expected_value = expected_value.toLowerCase();
value = value.toLowerCase();
}
switch (operator) {
case '=':
return value === expected_value;
case '~=':
return value.split(/\s/).includes(expected_value);
case '|=':
return `${value}-`.startsWith(`${expected_value}-`);
case '^=':
return value.startsWith(expected_value);
case '$=':
return value.endsWith(expected_value);
case '*=':
return value.includes(expected_value);
default:
throw new Error("this shouldn't happen");
}
}
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @param {string} name
* @param {string | null} expected_value
* @param {string | null} operator
* @param {boolean} case_insensitive
*/
function attribute_matches(node, name, expected_value, operator, case_insensitive) {
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') return true;
if (attribute.type === 'BindDirective' && attribute.name === name) return true;
if (attribute.type !== 'Attribute') continue;
if (attribute.name.toLowerCase() !== name.toLowerCase()) continue;
if (attribute.value === true) return operator === null;
if (expected_value === null) return true;
const chunks = attribute.value;
if (chunks.length === 1) {
const value = chunks[0];
if (value.type === 'Text') {
return test_attribute(operator, expected_value, case_insensitive, value.data);
}
}
const possible_values = new Set();
/** @type {string[]} */
let prev_values = [];
for (const chunk of chunks) {
const current_possible_values = get_possible_values(chunk);
// impossible to find out all combinations
if (!current_possible_values) return true;
if (prev_values.length > 0) {
/** @type {string[]} */
const start_with_space = [];
/** @type {string[]} */
const remaining = [];
current_possible_values.forEach((current_possible_value) => {
if (regex_starts_with_whitespace.test(current_possible_value)) {
start_with_space.push(current_possible_value);
} else {
remaining.push(current_possible_value);
}
});
if (remaining.length > 0) {
if (start_with_space.length > 0) {
prev_values.forEach((prev_value) => possible_values.add(prev_value));
}
/** @type {string[]} */
const combined = [];
prev_values.forEach((prev_value) => {
remaining.forEach((value) => {
combined.push(prev_value + value);
});
});
prev_values = combined;
start_with_space.forEach((value) => {
if (regex_ends_with_whitespace.test(value)) {
possible_values.add(value);
} else {
prev_values.push(value);
}
});
continue;
} else {
prev_values.forEach((prev_value) => possible_values.add(prev_value));
prev_values = [];
}
}
current_possible_values.forEach((current_possible_value) => {
if (regex_ends_with_whitespace.test(current_possible_value)) {
possible_values.add(current_possible_value);
} else {
prev_values.push(current_possible_value);
}
});
if (prev_values.length < current_possible_values.size) {
prev_values.push(' ');
}
if (prev_values.length > 20) {
// might grow exponentially, bail out
return true;
}
}
prev_values.forEach((prev_value) => possible_values.add(prev_value));
for (const value of possible_values) {
if (test_attribute(operator, expected_value, case_insensitive, value)) return true;
}
}
return false;
}
/** @param {string} str */
function unquote(str) {
if ((str[0] === str[str.length - 1] && str[0] === "'") || str[0] === '"') {
return str.slice(1, str.length - 1);
}
return str;
}
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @returns {import('#compiler').RegularElement | import('#compiler').SvelteElement | null}
*/
function get_element_parent(node) {
/** @type {import('#compiler').SvelteNode | null} */
let parent = node;
while (
// @ts-expect-error TODO figure out a more elegant solution
(parent = parent.parent) &&
parent.type !== 'RegularElement' &&
parent.type !== 'SvelteElement'
);
return parent ?? null;
}
/**
* Finds the given node's previous sibling in the DOM
*
* The Svelte `<slot>` is just a placeholder and is not actually real. Any children nodes
* in `<slot>` are 'flattened' and considered as the same level as the `<slot>`'s siblings
*
* e.g.
* ```html
* <h1>Heading 1</h1>
* <slot>
* <h2>Heading 2</h2>
* </slot>
* ```
*
* is considered to look like:
* ```html
* <h1>Heading 1</h1>
* <h2>Heading 2</h2>
* ```
* @param {import('#compiler').SvelteNode} node
* @returns {import('#compiler').SvelteNode}
*/
function find_previous_sibling(node) {
/** @type {import('#compiler').SvelteNode} */
let current_node = node;
do {
if (current_node.type === 'SlotElement') {
const slot_children = current_node.fragment.nodes;
if (slot_children.length > 0) {
current_node = slot_children.slice(-1)[0]; // go to its last child first
continue;
}
}
while (
// @ts-expect-error TODO
!current_node.prev &&
// @ts-expect-error TODO
current_node.parent &&
// @ts-expect-error TODO
current_node.parent.type === 'SlotElement'
) {
// @ts-expect-error TODO
current_node = current_node.parent;
}
// @ts-expect-error
current_node = current_node.prev;
} while (current_node && current_node.type === 'SlotElement');
return current_node;
}
/**
* @param {import('#compiler').SvelteNode} node
* @param {boolean} adjacent_only
* @returns {Map<import('#compiler').RegularElement, NodeExistsValue>}
*/
function get_possible_element_siblings(node, adjacent_only) {
/** @type {Map<import('#compiler').RegularElement, NodeExistsValue>} */
const result = new Map();
/** @type {import('#compiler').SvelteNode} */
let prev = node;
while ((prev = find_previous_sibling(prev))) {
if (prev.type === 'RegularElement') {
if (
!prev.attributes.find(
(attr) => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot'
)
) {
result.set(prev, NODE_DEFINITELY_EXISTS);
}
if (adjacent_only) {
break;
}
} else if (prev.type === 'EachBlock' || prev.type === 'IfBlock' || prev.type === 'AwaitBlock') {
const possible_last_child = get_possible_last_child(prev, adjacent_only);
add_to_map(possible_last_child, result);
if (adjacent_only && has_definite_elements(possible_last_child)) {
return result;
}
}
}
if (!prev || !adjacent_only) {
/** @type {import('#compiler').SvelteNode | null} */
let parent = node;
while (
// @ts-expect-error TODO
(parent = parent?.parent) &&
(parent.type === 'EachBlock' || parent.type === 'IfBlock' || parent.type === 'AwaitBlock')
) {
const possible_siblings = get_possible_element_siblings(parent, adjacent_only);
add_to_map(possible_siblings, result);
// @ts-expect-error
if (parent.type === 'EachBlock' && !parent.fallback?.nodes.includes(node)) {
// `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />`
add_to_map(get_possible_last_child(parent, adjacent_only), result);
}
if (adjacent_only && has_definite_elements(possible_siblings)) {
break;
}
}
}
return result;
}
/**
* @param {import('#compiler').EachBlock | import('#compiler').IfBlock | import('#compiler').AwaitBlock} relative_selector
* @param {boolean} adjacent_only
* @returns {Map<import('#compiler').RegularElement, NodeExistsValue>}
*/
function get_possible_last_child(relative_selector, adjacent_only) {
/** @typedef {Map<import('#compiler').RegularElement, NodeExistsValue>} NodeMap */
/** @type {NodeMap} */
const result = new Map();
if (relative_selector.type === 'EachBlock') {
/** @type {NodeMap} */
const each_result = loop_child(relative_selector.body.nodes, adjacent_only);
/** @type {NodeMap} */
const else_result = relative_selector.fallback
? loop_child(relative_selector.fallback.nodes, adjacent_only)
: new Map();
const not_exhaustive = !has_definite_elements(else_result);
if (not_exhaustive) {
mark_as_probably(each_result);
mark_as_probably(else_result);
}
add_to_map(each_result, result);
add_to_map(else_result, result);
} else if (relative_selector.type === 'IfBlock') {
/** @type {NodeMap} */
const if_result = loop_child(relative_selector.consequent.nodes, adjacent_only);
/** @type {NodeMap} */
const else_result = relative_selector.alternate
? loop_child(relative_selector.alternate.nodes, adjacent_only)
: new Map();
const not_exhaustive = !has_definite_elements(if_result) || !has_definite_elements(else_result);
if (not_exhaustive) {
mark_as_probably(if_result);
mark_as_probably(else_result);
}
add_to_map(if_result, result);
add_to_map(else_result, result);
} else if (relative_selector.type === 'AwaitBlock') {
/** @type {NodeMap} */
const pending_result = relative_selector.pending
? loop_child(relative_selector.pending.nodes, adjacent_only)
: new Map();
/** @type {NodeMap} */
const then_result = relative_selector.then
? loop_child(relative_selector.then.nodes, adjacent_only)
: new Map();
/** @type {NodeMap} */
const catch_result = relative_selector.catch
? loop_child(relative_selector.catch.nodes, adjacent_only)
: new Map();
const not_exhaustive =
!has_definite_elements(pending_result) ||
!has_definite_elements(then_result) ||
!has_definite_elements(catch_result);
if (not_exhaustive) {
mark_as_probably(pending_result);
mark_as_probably(then_result);
mark_as_probably(catch_result);
}
add_to_map(pending_result, result);
add_to_map(then_result, result);
add_to_map(catch_result, result);
}
return result;
}
/**
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} result
* @returns {boolean}
*/
function has_definite_elements(result) {
if (result.size === 0) return false;
for (const exist of result.values()) {
if (exist === NODE_DEFINITELY_EXISTS) {
return true;
}
}
return false;
}
/**
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} from
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} to
* @returns {void}
*/
function add_to_map(from, to) {
from.forEach((exist, element) => {
to.set(element, higher_existence(exist, to.get(element)));
});
}
/**
* @param {NodeExistsValue | undefined} exist1
* @param {NodeExistsValue | undefined} exist2
* @returns {NodeExistsValue}
*/
function higher_existence(exist1, exist2) {
// @ts-expect-error TODO figure out if this is a bug
if (exist1 === undefined || exist2 === undefined) return exist1 || exist2;
return exist1 > exist2 ? exist1 : exist2;
}
/** @param {Map<import('#compiler').RegularElement, NodeExistsValue>} result */
function mark_as_probably(result) {
for (const key of result.keys()) {
result.set(key, NODE_PROBABLY_EXISTS);
}
}
/**
* @param {import('#compiler').SvelteNode[]} children
* @param {boolean} adjacent_only
*/
function loop_child(children, adjacent_only) {
/** @type {Map<import('#compiler').RegularElement, NodeExistsValue>} */
const result = new Map();
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (child.type === 'RegularElement') {
result.set(child, NODE_DEFINITELY_EXISTS);
if (adjacent_only) {
break;
}
} else if (
child.type === 'EachBlock' ||
child.type === 'IfBlock' ||
child.type === 'AwaitBlock'
) {
const child_result = get_possible_last_child(child, adjacent_only);
add_to_map(child_result, result);
if (adjacent_only && has_definite_elements(child_result)) {
break;
}
}
}
return result;
}

@ -13,7 +13,6 @@ import * as b from '../../utils/builders.js';
import { ReservedKeywords, Runes, SVGElements } from '../constants.js';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import { merge } from '../visitors.js';
import Stylesheet from './css/Stylesheet.js';
import { validation_legacy, validation_runes, validation_runes_js } from './validation.js';
import { warn } from '../../warnings.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
@ -21,6 +20,9 @@ import { regex_starts_with_newline } from '../patterns.js';
import { create_attribute, is_element_node } from '../nodes.js';
import { DelegatedEvents, namespace_svg } from '../../../constants.js';
import { should_proxy_or_freeze } from '../3-transform/client/utils.js';
import { analyze_css } from './css/css-analyze.js';
import { prune } from './css/css-prune.js';
import { hash } from './utils.js';
/**
* @param {import('#compiler').Script | null} script
@ -340,13 +342,6 @@ export function analyze_component(root, options) {
instance,
template,
elements: [],
stylesheet: new Stylesheet({
ast: root.css,
// TODO are any of these necessary or can we just pass in the whole `analysis` object later?
filename: options.filename ?? '<unknown>',
component_name,
get_css_hash: options.cssHash
}),
runes,
immutable: runes || options.immutable,
exports: [],
@ -360,7 +355,19 @@ export function analyze_component(root, options) {
reactive_statements: new Map(),
binding_groups: new Map(),
slot_names: new Set(),
warnings
warnings,
css: {
ast: root.css,
hash: root.css
? options.cssHash({
css: root.css.content.styles,
filename: options.filename ?? '<unknown>',
name: component_name,
hash
})
: '',
keyframes: []
}
};
if (analysis.runes) {
@ -452,13 +459,67 @@ export function analyze_component(root, options) {
}
}
analysis.stylesheet.validate(analysis);
if (analysis.css.ast) {
analyze_css(analysis.css.ast, analysis);
for (const element of analysis.elements) {
analysis.stylesheet.apply(element);
}
// mark nodes as scoped/unused/empty etc
for (const element of analysis.elements) {
prune(analysis.css.ast, element);
}
outer: for (const element of analysis.elements) {
if (element.metadata.scoped) {
// Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them
// TODO this happens during the analysis phase, which shouldn't know anything about client vs server
if (element.type === 'SvelteElement' && options.generate === 'client') continue;
/** @type {import('#compiler').Attribute | undefined} */
let class_attribute = undefined;
for (const attribute of element.attributes) {
if (attribute.type === 'SpreadAttribute') {
// The spread method appends the hash to the end of the class attribute on its own
continue outer;
}
analysis.stylesheet.reify(options.generate === 'client');
if (attribute.type !== 'Attribute') continue;
if (attribute.name.toLowerCase() !== 'class') continue;
class_attribute = attribute;
}
if (class_attribute && class_attribute.value !== true) {
const chunks = class_attribute.value;
if (chunks.length === 1 && chunks[0].type === 'Text') {
chunks[0].data += ` ${analysis.css.hash}`;
} else {
chunks.push({
type: 'Text',
data: ` ${analysis.css.hash}`,
raw: ` ${analysis.css.hash}`,
start: -1,
end: -1,
parent: null
});
}
} else {
element.attributes.push(
create_attribute('class', -1, -1, [
{
type: 'Text',
data: analysis.css.hash,
raw: analysis.css.hash,
parent: null,
start: -1,
end: -1
}
])
);
}
}
}
}
// TODO
// analysis.stylesheet.warn_on_unused_selectors(analysis);
@ -681,7 +742,7 @@ const runes_scope_js_tweaker = {
rune !== '$state' &&
rune !== '$state.frozen' &&
rune !== '$derived' &&
rune !== '$derived.call'
rune !== '$derived.by'
)
return;
@ -717,7 +778,7 @@ const runes_scope_tweaker = {
rune !== '$state' &&
rune !== '$state.frozen' &&
rune !== '$derived' &&
rune !== '$derived.call' &&
rune !== '$derived.by' &&
rune !== '$props'
)
return;
@ -730,7 +791,7 @@ const runes_scope_tweaker = {
? 'state'
: rune === '$state.frozen'
? 'frozen_state'
: rune === '$derived' || rune === '$derived.call'
: rune === '$derived' || rune === '$derived.by'
? 'derived'
: path.is_rest
? 'rest_prop'

@ -4,7 +4,7 @@ const regex_return_characters = /\r/g;
* @param {string} str
* @returns {string}
*/
export default function hash(str) {
export function hash(str) {
str = str.replace(regex_return_characters, '');
let hash = 5381;
let i = str.length;

@ -1,5 +1,11 @@
import { error } from '../../errors.js';
import { extract_identifiers, get_parent, is_text_attribute, object } from '../../utils/ast.js';
import {
extract_identifiers,
get_parent,
is_expression_attribute,
is_text_attribute,
object
} from '../../utils/ast.js';
import { warn } from '../../warnings.js';
import fuzzymatch from '../1-parse/utils/fuzzymatch.js';
import { disallowed_parapgraph_contents, interactive_elements } from '../1-parse/utils/html.js';
@ -66,12 +72,23 @@ function validate_element(node, context) {
}
if (attribute.name.startsWith('on') && attribute.name.length > 2) {
if (!is_expression_attribute(attribute)) {
error(attribute, 'invalid-event-attribute-value');
}
const value = attribute.value[0].expression;
if (
attribute.value === true ||
is_text_attribute(attribute) ||
attribute.value.length > 1
value.type === 'Identifier' &&
value.name === attribute.name &&
!context.state.scope.get(value.name)
) {
error(attribute, 'invalid-event-attribute-value');
warn(
context.state.analysis.warnings,
attribute,
context.path,
'global-event-reference',
attribute.name
);
}
}
@ -746,7 +763,7 @@ function validate_call_expression(node, scope, path) {
error(node, 'invalid-props-location');
}
if (rune === '$state' || rune === '$derived' || rune === '$derived.call') {
if (rune === '$state' || rune === '$derived' || rune === '$derived.by') {
if (parent.type === 'VariableDeclarator') return;
if (parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) return;
error(node, 'invalid-state-location', rune);
@ -817,7 +834,7 @@ export const validation_runes_js = {
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
if ((rune === '$derived' || rune === '$derived.call') && args.length !== 1) {
if ((rune === '$derived' || rune === '$derived.by') && args.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]);
} else if (rune === '$state' && args.length > 1) {
error(node, 'invalid-rune-args-length', rune, [0, 1]);
@ -842,7 +859,7 @@ export const validation_runes_js = {
definition.value?.type === 'CallExpression'
) {
const rune = get_rune(definition.value, context.state.scope);
if (rune === '$derived' || rune === '$derived.call') {
if (rune === '$derived' || rune === '$derived.by') {
private_derived_state.push(definition.key.name);
}
}
@ -988,7 +1005,7 @@ export const validation_runes = merge(validation, a11y_validators, {
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
// TODO some of this is duplicated with above, seems off
if ((rune === '$derived' || rune === '$derived.call') && args.length !== 1) {
if ((rune === '$derived' || rune === '$derived.by') && args.length !== 1) {
error(node, 'invalid-rune-args-length', rune, [1]);
} else if (rune === '$state' && args.length > 1) {
error(node, 'invalid-rune-args-length', rune, [0, 1]);

@ -8,6 +8,7 @@ import { javascript_visitors } from './visitors/javascript.js';
import { javascript_visitors_runes } from './visitors/javascript-runes.js';
import { javascript_visitors_legacy } from './visitors/javascript-legacy.js';
import { is_state_source, serialize_get_binding } from './utils.js';
import { render_stylesheet } from '../css/index.js';
/**
* This function ensures visitor sets don't accidentally clobber each other
@ -271,15 +272,15 @@ export function client_component(source, analysis, options) {
]);
const append_styles =
analysis.inject_styles && analysis.stylesheet.has_styles
analysis.inject_styles && analysis.css.ast
? () =>
component_block.body.push(
b.stmt(
b.call(
'$.append_styles',
b.id('$$anchor'),
b.literal(analysis.stylesheet.id),
b.literal(analysis.stylesheet.render(analysis.name, source, options.dev).code)
b.literal(analysis.css.hash),
b.literal(render_stylesheet(source, analysis, options).code)
)
)
)

@ -75,7 +75,7 @@ export function serialize_get_binding(node, state) {
}
if (binding.expression) {
return binding.expression;
return typeof binding.expression === 'function' ? binding.expression(node) : binding.expression;
}
if (binding.kind === 'prop') {
@ -418,15 +418,44 @@ export function serialize_set_binding(node, context, fallback, options) {
}
} else {
if (is_store) {
// If we are assigning to a store property, we need to ensure we don't
// capture the read for the store as part of the member expression to
// keep consistency with how store $ shorthand reads work in Svelte 4.
/**
*
* @param {import("estree").Expression | import("estree").Pattern} node
* @returns {import("estree").Expression}
*/
function visit_node(node) {
if (node.type === 'MemberExpression') {
return {
...node,
object: visit_node(/** @type {import("estree").Expression} */ (node.object)),
property: /** @type {import("estree").Expression} */ (visit(node.property))
};
}
if (node.type === 'Identifier') {
const binding = state.scope.get(node.name);
if (binding !== null && binding.kind === 'store_sub') {
return b.call(
'$.untrack',
b.thunk(/** @type {import('estree').Expression} */ (visit(node)))
);
}
}
return /** @type {import("estree").Expression} */ (visit(node));
}
return b.call(
'$.mutate_store',
serialize_get_binding(b.id(left_name), state),
b.assignment(
node.operator,
/** @type {import('estree').Pattern} */ (visit(node.left)),
/** @type {import("estree").Pattern}} */ (visit_node(node.left)),
value
),
b.call('$' + left_name)
b.call('$.untrack', b.id('$' + left_name))
);
} else if (!state.analysis.runes) {
if (binding.kind === 'prop') {
@ -521,6 +550,7 @@ function get_hoistable_params(node, context) {
} else if (
// If it's a destructured derived binding, then we can extract the derived signal reference and use that.
binding.expression !== null &&
typeof binding.expression !== 'function' &&
binding.expression.type === 'MemberExpression' &&
binding.expression.object.type === 'CallExpression' &&
binding.expression.object.callee.type === 'Identifier' &&
@ -668,3 +698,17 @@ export function should_proxy_or_freeze(node, scope) {
}
return true;
}
/**
* Port over the location information from the source to the target identifier.
* but keep the target as-is (i.e. a new id is created).
* This ensures esrap can generate accurate source maps.
* @param {import('estree').Identifier} target
* @param {import('estree').Identifier} source
*/
export function with_loc(target, source) {
if (source.loc) {
return { ...target, loc: source.loc };
}
return target;
}

@ -33,7 +33,7 @@ export const javascript_visitors_runes = {
rune === '$state' ||
rune === '$state.frozen' ||
rune === '$derived' ||
rune === '$derived.call'
rune === '$derived.by'
) {
/** @type {import('../types.js').StateField} */
const field = {
@ -42,7 +42,7 @@ export const javascript_visitors_runes = {
? 'state'
: rune === '$state.frozen'
? 'frozen_state'
: rune === '$derived.call'
: rune === '$derived.by'
? 'derived_call'
: 'derived',
// @ts-expect-error this is set in the next pass
@ -289,12 +289,12 @@ export const javascript_visitors_runes = {
continue;
}
if (rune === '$derived' || rune === '$derived.call') {
if (rune === '$derived' || rune === '$derived.by') {
if (declarator.id.type === 'Identifier') {
declarations.push(
b.declarator(
declarator.id,
b.call('$.derived', rune === '$derived.call' ? value : b.thunk(value))
b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value))
)
);
} else {
@ -307,7 +307,7 @@ export const javascript_visitors_runes = {
'$.derived',
b.thunk(
b.block([
b.let(declarator.id, rune === '$derived.call' ? b.call(value) : value),
b.let(declarator.id, rune === '$derived.by' ? b.call(value) : value),
b.return(b.array(bindings.map((binding) => binding.node)))
])
)
@ -315,7 +315,16 @@ export const javascript_visitors_runes = {
)
);
for (let i = 0; i < bindings.length; i++) {
bindings[i].expression = b.member(b.call('$.get', b.id(id)), b.literal(i), true);
const binding = bindings[i];
declarations.push(
b.declarator(
binding.node,
b.call(
'$.derived',
b.thunk(b.member(b.call('$.get', b.id(id)), b.literal(i), true))
)
)
);
}
}
continue;

@ -17,6 +17,7 @@ import { is_custom_element_node, is_element_node } from '../../../nodes.js';
import * as b from '../../../../utils/builders.js';
import { error } from '../../../../errors.js';
import {
with_loc,
function_visitor,
get_assignment_value,
serialize_get_binding,
@ -321,7 +322,7 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
element_id,
b.thunk(b.array(values)),
lowercase_attributes,
b.literal(context.state.analysis.stylesheet.id)
b.literal(context.state.analysis.css.hash)
)
);
@ -344,7 +345,7 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
b.id(id),
b.array(values),
lowercase_attributes,
b.literal(context.state.analysis.stylesheet.id)
b.literal(context.state.analysis.css.hash)
)
)
)
@ -363,9 +364,9 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
*/
function serialize_dynamic_element_attributes(attributes, context, element_id) {
if (attributes.length === 0) {
if (context.state.analysis.stylesheet.id) {
if (context.state.analysis.css.hash) {
context.state.init.push(
b.stmt(b.call('$.class_name', element_id, b.literal(context.state.analysis.stylesheet.id)))
b.stmt(b.call('$.class_name', element_id, b.literal(context.state.analysis.css.hash)))
);
}
return false;
@ -398,7 +399,7 @@ function serialize_dynamic_element_attributes(attributes, context, element_id) {
'$.spread_dynamic_element_attributes_effect',
element_id,
b.thunk(b.array(values)),
b.literal(context.state.analysis.stylesheet.id)
b.literal(context.state.analysis.css.hash)
)
);
@ -419,7 +420,7 @@ function serialize_dynamic_element_attributes(attributes, context, element_id) {
element_id,
b.id(id),
b.array(values),
b.literal(context.state.analysis.stylesheet.id)
b.literal(context.state.analysis.css.hash)
)
)
)
@ -433,7 +434,7 @@ function serialize_dynamic_element_attributes(attributes, context, element_id) {
element_id,
b.literal(null),
b.array(values),
b.literal(context.state.analysis.stylesheet.id)
b.literal(context.state.analysis.css.hash)
)
)
);
@ -978,7 +979,11 @@ function serialize_inline_component(node, component_name, context) {
const assignment = b.assignment('=', bind_this, b.id('$$value'));
const bind_this_id = /** @type {import('estree').Expression} */ (
// if expression is not an identifier, we know it can't be a signal
bind_this.type === 'Identifier' ? bind_this : undefined
bind_this.type === 'Identifier'
? bind_this
: bind_this.type === 'MemberExpression' && bind_this.object.type === 'Identifier'
? bind_this.object
: undefined
);
fn = (node_id) =>
b.call(
@ -2321,14 +2326,20 @@ export const template_visitors = {
each_node_meta.contains_group_binding || !node.index
? each_node_meta.index
: b.id(node.index);
const item = b.id(each_node_meta.item_name);
const item = each_node_meta.item;
const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(item.name));
binding.expression = each_item_is_reactive ? b.call('$.unwrap', item) : item;
binding.expression = (id) => {
const item_with_loc = with_loc(item, id);
return each_item_is_reactive ? b.call('$.unwrap', item_with_loc) : item_with_loc;
};
if (node.index) {
const index_binding = /** @type {import('#compiler').Binding} */ (
context.state.scope.get(node.index)
);
index_binding.expression = each_item_is_reactive ? b.call('$.unwrap', index) : index;
index_binding.expression = (id) => {
const index_with_loc = with_loc(index, id);
return each_item_is_reactive ? b.call('$.unwrap', index_with_loc) : index_with_loc;
};
}
/** @type {import('estree').Statement[]} */
@ -2343,7 +2354,7 @@ export const template_visitors = {
)
);
} else {
const unwrapped = binding.expression;
const unwrapped = binding.expression(binding.node);
const paths = extract_paths(node.context);
for (const path of paths) {
@ -2741,7 +2752,11 @@ export const template_visitors = {
setter,
/** @type {import('estree').Expression} */ (
// if expression is not an identifier, we know it can't be a signal
expression.type === 'Identifier' ? expression : undefined
expression.type === 'Identifier'
? expression
: expression.type === 'MemberExpression' && expression.object.type === 'Identifier'
? expression.object
: undefined
)
);
break;

@ -0,0 +1,334 @@
import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
import { is_keyframes_node, regex_css_name_boundary, remove_css_prefix } from '../../css.js';
import { merge_with_preprocessor_map } from '../../../utils/mapped_code.js';
/**
* @typedef {{
* code: MagicString;
* dev: boolean;
* hash: string;
* selector: string;
* keyframes: string[];
* specificity: {
* bumped: boolean
* }
* }} State
*/
/**
*
* @param {string} source
* @param {import('../../types.js').ComponentAnalysis} analysis
* @param {import('#compiler').ValidatedCompileOptions} options
*/
export function render_stylesheet(source, analysis, options) {
const code = new MagicString(source);
/** @type {State} */
const state = {
code,
dev: options.dev,
hash: analysis.css.hash,
selector: `.${analysis.css.hash}`,
keyframes: analysis.css.keyframes,
specificity: {
bumped: false
}
};
const ast = /** @type {import('#compiler').Css.StyleSheet} */ (analysis.css.ast);
walk(/** @type {import('#compiler').Css.Node} */ (ast), state, visitors);
code.remove(0, ast.content.start);
code.remove(/** @type {number} */ (ast.content.end), source.length);
const css = {
code: code.toString(),
map: code.generateMap({
// include source content; makes it easier/more robust looking up the source map code
includeContent: true,
// generateMap takes care of calculating source relative to file
source: options.filename,
file: options.cssOutputFilename || options.filename
})
};
merge_with_preprocessor_map(css, options, css.map.sources[0]);
if (options.dev && options.css === 'injected' && css.code) {
css.code += `\n/*# sourceMappingURL=${css.map.toUrl()} */`;
}
return css;
}
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, State>} */
const visitors = {
_: (node, context) => {
context.state.code.addSourcemapLocation(node.start);
context.state.code.addSourcemapLocation(node.end);
context.next();
},
Atrule(node, { state, next }) {
if (is_keyframes_node(node)) {
let start = node.start + node.name.length + 1;
while (state.code.original[start] === ' ') start += 1;
let end = start;
while (state.code.original[end] !== '{' && state.code.original[end] !== ' ') end += 1;
if (node.prelude.startsWith('-global-')) {
state.code.remove(start, start + 8);
} else {
state.code.prependRight(start, `${state.hash}-`);
}
return; // don't transform anything within
}
next();
},
Declaration(node, { state, next }) {
const property = node.property && remove_css_prefix(node.property.toLowerCase());
if (property === 'animation' || property === 'animation-name') {
let index = node.start + node.property.length + 1;
let name = '';
while (index < state.code.original.length) {
const character = state.code.original[index];
if (regex_css_name_boundary.test(character)) {
if (state.keyframes.includes(name)) {
state.code.prependRight(index - name.length, `${state.hash}-`);
}
if (character === ';' || character === '}') {
break;
}
name = '';
} else {
name += character;
}
index++;
}
}
},
Rule(node, { state, next }) {
// keep empty rules in dev, because it's convenient to
// see them in devtools
if (!state.dev && is_empty(node)) {
state.code.prependRight(node.start, '/* (empty) ');
state.code.appendLeft(node.end, '*/');
escape_comment_close(node, state.code);
return;
}
if (!is_used(node)) {
state.code.prependRight(node.start, '/* (unused) ');
state.code.appendLeft(node.end, '*/');
escape_comment_close(node, state.code);
return;
}
next();
},
SelectorList(node, { state, next, path }) {
let pruning = false;
let last = node.children[0].start;
for (let i = 0; i < node.children.length; i += 1) {
const selector = node.children[i];
if (selector.metadata.used === pruning) {
if (pruning) {
let i = selector.start;
while (state.code.original[i] !== ',') i--;
state.code.overwrite(i, i + 1, '*/');
} else {
if (i === 0) {
state.code.prependRight(selector.start, '/* (unused) ');
} else {
state.code.overwrite(last, selector.start, ' /* (unused) ');
}
}
pruning = !pruning;
}
last = selector.end;
}
if (pruning) {
state.code.appendLeft(last, '*/');
}
// if we're in a `:is(...)` or whatever, keep existing specificity bump state
let specificity = state.specificity;
// if this selector list belongs to a rule, require a specificity bump for the
// first scoped selector but only if we're at the top level
let parent = path.at(-1);
if (parent?.type === 'Rule') {
specificity = { bumped: false };
/** @type {import('#compiler').Css.Rule | null} */
let rule = parent.metadata.parent_rule;
while (rule) {
if (rule.metadata.has_local_selectors) {
specificity = { bumped: true };
break;
}
rule = rule.metadata.parent_rule;
}
}
next({ ...state, specificity });
},
ComplexSelector(node, context) {
/** @param {import('#compiler').Css.SimpleSelector} selector */
function remove_global_pseudo_class(selector) {
context.state.code
.remove(selector.start, selector.start + ':global('.length)
.remove(selector.end - 1, selector.end);
}
for (const relative_selector of node.children) {
if (relative_selector.metadata.is_global) {
remove_global_pseudo_class(relative_selector.selectors[0]);
continue;
}
if (relative_selector.metadata.scoped) {
if (relative_selector.selectors.length === 1) {
// skip standalone :is/:where/& selectors
const selector = relative_selector.selectors[0];
if (
selector.type === 'PseudoClassSelector' &&
(selector.name === 'is' || selector.name === 'where')
) {
continue;
}
}
if (relative_selector.selectors.every((s) => s.type === 'NestingSelector')) {
continue;
}
// for the first occurrence, we use a classname selector, so that every
// encapsulated selector gets a +0-1-0 specificity bump. thereafter,
// we use a `:where` selector, which does not affect specificity
let modifier = context.state.selector;
if (context.state.specificity.bumped) modifier = `:where(${modifier})`;
context.state.specificity.bumped = true;
// TODO err... can this happen?
for (const selector of relative_selector.selectors) {
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
remove_global_pseudo_class(selector);
}
}
let i = relative_selector.selectors.length;
while (i--) {
const selector = relative_selector.selectors[i];
if (
selector.type === 'PseudoElementSelector' ||
selector.type === 'PseudoClassSelector'
) {
if (selector.name !== 'root' && selector.name !== 'host') {
if (i === 0) context.state.code.prependRight(selector.start, modifier);
}
continue;
}
if (selector.type === 'TypeSelector' && selector.name === '*') {
context.state.code.update(selector.start, selector.end, modifier);
} else {
context.state.code.appendLeft(selector.end, modifier);
}
break;
}
}
}
context.next();
},
PseudoClassSelector(node, context) {
if (node.name === 'is' || node.name === 'where') {
context.next();
}
}
};
/** @param {import('#compiler').Css.Rule} rule */
function is_empty(rule) {
for (const child of rule.block.children) {
if (child.type === 'Declaration') {
return false;
}
if (child.type === 'Rule') {
if (is_used(child) && !is_empty(child)) return false;
}
if (child.type === 'Atrule') {
return false; // TODO
}
}
return true;
}
/** @param {import('#compiler').Css.Rule} rule */
function is_used(rule) {
for (const selector of rule.prelude.children) {
if (selector.metadata.used) return true;
}
for (const child of rule.block.children) {
if (child.type === 'Rule' && is_used(child)) return true;
if (child.type === 'Atrule') {
return true; // TODO
}
}
return false;
}
/**
*
* @param {import('#compiler').Css.Rule} node
* @param {MagicString} code
*/
function escape_comment_close(node, code) {
let escaped = false;
let in_comment = false;
for (let i = node.start; i < node.end; i++) {
if (escaped) {
escaped = false;
} else {
const char = code.original[i];
if (in_comment) {
if (char === '*' && code.original[i + 1] === '/') {
code.prependRight(++i, '\\');
in_comment = false;
}
} else if (char === '\\') {
escaped = true;
} else if (char === '/' && code.original[++i] === '*') {
in_comment = true;
}
}
}
}

@ -3,6 +3,8 @@ import { VERSION } from '../../../version.js';
import { server_component, server_module } from './server/transform-server.js';
import { client_component, client_module } from './client/transform-client.js';
import { getLocator } from 'locate-character';
import { render_stylesheet } from './css/index.js';
import { merge_with_preprocessor_map, get_source_name } from '../../utils/mapped_code.js';
/**
* @param {import('../types').ComponentAnalysis} analysis
@ -41,13 +43,23 @@ export function transform_component(analysis, source, options) {
];
}
const js_source_name = get_source_name(options.filename, options.outputFilename, 'input.svelte');
const js = print(program, {
// include source content; makes it easier/more robust looking up the source map code
sourceMapContent: source,
sourceMapSource: js_source_name
});
merge_with_preprocessor_map(js, options, js_source_name);
const css =
analysis.css.ast && !analysis.inject_styles
? render_stylesheet(source, analysis, options)
: null;
return {
js: print(program, { sourceMapSource: options.filename }), // TODO needs more logic to apply map from preprocess
css:
analysis.stylesheet.has_styles && !analysis.inject_styles
? analysis.stylesheet.render(options.filename ?? 'TODO', source, options.dev)
: null,
warnings: transform_warnings(source, options.filename, analysis.warnings),
js,
css,
warnings: transform_warnings(source, options.filename, analysis.warnings), // TODO apply preprocessor sourcemap
metadata: {
runes: analysis.runes
}

@ -321,15 +321,15 @@ function serialize_get_binding(node, state) {
if (binding.kind === 'store_sub') {
const store_id = b.id(node.name.slice(1));
return b.call(
state.options.dev ? '$.store_get_dev' : '$.store_get',
b.id('$$store_subs'),
'$.store_get',
b.assignment('??=', b.id('$$store_subs'), b.object([])),
b.literal(node.name),
serialize_get_binding(store_id, state)
);
}
if (binding.expression) {
return binding.expression;
return typeof binding.expression === 'function' ? binding.expression(node) : binding.expression;
}
return node;
@ -460,7 +460,7 @@ function serialize_set_binding(node, context, fallback) {
} else if (is_store) {
return b.call(
'$.mutate_store',
b.id('$$store_subs'),
b.assignment('??=', b.id('$$store_subs'), b.object([])),
b.literal(left.name),
b.id(left_name),
b.assignment(node.operator, /** @type {import('estree').Pattern} */ (visit(node.left)), value)
@ -506,7 +506,11 @@ const global_visitors = {
if (node.prefix) fn += '_pre';
/** @type {import('estree').Expression[]} */
const args = [b.id('$$store_subs'), b.literal(argument.name), b.id(argument.name.slice(1))];
const args = [
b.assignment('??=', b.id('$$store_subs'), b.object([])),
b.literal(argument.name),
b.id(argument.name.slice(1))
];
if (node.operator === '--') {
args.push(b.literal(-1));
}
@ -548,7 +552,7 @@ const javascript_visitors_runes = {
: /** @type {import('estree').Expression} */ (visit(node.value.arguments[0]))
};
}
if (rune === '$derived.call') {
if (rune === '$derived.by') {
return {
...node,
value:
@ -582,7 +586,7 @@ const javascript_visitors_runes = {
? b.id('undefined')
: /** @type {import('estree').Expression} */ (visit(args[0]));
if (rune === '$derived.call') {
if (rune === '$derived.by') {
declarations.push(
b.declarator(
/** @type {import('estree').Pattern} */ (visit(declarator.id)),
@ -760,7 +764,7 @@ function serialize_element_spread_attributes(
b.array(values),
lowercase_attributes,
is_svg,
b.literal(context.state.analysis.stylesheet.id)
b.literal(context.state.analysis.css.hash)
];
if (style_directives.length > 0 || class_directives.length > 0) {
@ -1311,7 +1315,7 @@ const template_visitors = {
const each_node_meta = node.metadata;
const collection = /** @type {import('estree').Expression} */ (context.visit(node.expression));
const item = b.id(each_node_meta.item_name);
const item = each_node_meta.item;
const index =
each_node_meta.contains_group_binding || !node.index
? each_node_meta.index
@ -2094,10 +2098,11 @@ export function server_component(analysis, options) {
(binding) => binding.kind === 'store_sub'
)
) {
instance.body.unshift(b.const('$$store_subs', b.object([])));
template.body.push(b.stmt(b.call('$.unsubscribe_stores', b.id('$$store_subs'))));
instance.body.unshift(b.var('$$store_subs'));
template.body.push(
b.if(b.id('$$store_subs'), b.stmt(b.call('$.unsubscribe_stores', b.id('$$store_subs'))))
);
}
// Propagate values of bound props upwards if they're undefined in the parent and have a value.
// Don't do this as part of the props retrieval because people could eagerly mutate the prop in the instance script.
/** @type {import('estree').Property[]} */

@ -33,7 +33,7 @@ export const Runes = /** @type {const} */ ([
'$state.frozen',
'$props',
'$derived',
'$derived.call',
'$derived.by',
'$effect',
'$effect.pre',
'$effect.active',

@ -0,0 +1,13 @@
const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/;
export const regex_css_name_boundary = /^[\s,;}]$/;
/**
* @param {string} name
* @returns {string}
*/
export function remove_css_prefix(name) {
return name.replace(regex_css_browser_prefix, '');
}
/** @param {import('#compiler').Css.Atrule} node */
export const is_keyframes_node = (node) => remove_css_prefix(node.name) === 'keyframes';

@ -185,6 +185,7 @@ export class Scope {
reference(node, path) {
path = [...path]; // ensure that mutations to path afterwards don't affect this reference
let references = this.references.get(node.name);
if (!references) this.references.set(node.name, (references = []));
references.push({ node, path });
@ -329,6 +330,13 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
return /** @type {const} */ ([scope, is_default_slot]);
}
/**
* @type {import('zimmerframe').Visitor<import('#compiler').Directive, State, import('#compiler').SvelteNode>}
*/
const SvelteDirective = (node, context) => {
context.state.scope.reference(b.id(node.name), context.path);
};
walk(ast, state, {
// references
Identifier(node, { path, state }) {
@ -556,7 +564,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
contains_group_binding: false,
array_name: needs_array_deduplication ? state.scope.root.unique('$$array') : null,
index: scope.root.unique('$$index'),
item_name: node.context.type === 'Identifier' ? node.context.name : '$$item',
item: node.context.type === 'Identifier' ? node.context : b.id('$$item'),
declarations: scope.declarations,
references: [...references_within]
.map((id) => /** @type {import('#compiler').Binding} */ (state.scope.get(id.name)))
@ -625,7 +633,11 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
)
]);
context.next();
}
},
TransitionDirective: SvelteDirective,
AnimateDirective: SvelteDirective,
UseDirective: SvelteDirective
// TODO others
});
@ -717,6 +729,8 @@ export function get_rune(node, scope) {
if (n.type !== 'Identifier') return null;
joined = n.name + joined;
if (joined === '$derived.call') error(node, 'invalid-derived-call');
if (!Runes.includes(/** @type {any} */ (joined))) return null;
const binding = scope.get(n.name);

@ -1,5 +1,6 @@
import type {
Binding,
Css,
Fragment,
RegularElement,
SvelteElement,
@ -7,7 +8,6 @@ import type {
SvelteOptions
} from '#compiler';
import type { Identifier, LabeledStatement, Program } from 'estree';
import type Stylesheet from './2-analyze/css/Stylesheet.js';
import type { Scope, ScopeRoot } from './scope.js';
export interface Js {
@ -51,7 +51,6 @@ export interface ComponentAnalysis extends Analysis {
root: ScopeRoot;
instance: Js;
template: Template;
stylesheet: Stylesheet;
elements: Array<RegularElement | SvelteElement>;
runes: boolean;
exports: Array<{ name: string; alias: string | null }>;
@ -69,6 +68,11 @@ export interface ComponentAnalysis extends Analysis {
/** Identifiers that make up the `bind:group` expression -> internal group binding name */
binding_groups: Map<[key: string, bindings: Array<Binding | null>], Identifier>;
slot_names: Set<string>;
css: {
ast: Css.StyleSheet | null;
hash: string;
keyframes: string[];
};
}
declare module 'estree' {

@ -3,27 +3,21 @@ import {
MappedCode,
parse_attached_sourcemap,
sourcemap_add_offset,
combine_sourcemaps
combine_sourcemaps,
get_basename
} from '../utils/mapped_code.js';
import { decode_map } from './decode_sourcemap.js';
import { replace_in_code, slice_source } from './replace_in_code.js';
const regex_filepath_separator = /[/\\]/;
/**
* @param {string} filename
*/
function get_file_basename(filename) {
return /** @type {string} */ (filename.split(regex_filepath_separator).pop());
}
/**
* Represents intermediate states of the preprocessing.
* Implements the Source interface.
*/
class PreprocessResult {
/** @type {string} */
source;
/** @type {string | undefined} */
/** @type {string | undefined} The filename passed as-is to preprocess */
filename;
// sourcemap_list is sorted in reverse order from last map (index 0) to first map (index -1)
@ -43,7 +37,7 @@ class PreprocessResult {
dependencies = [];
/**
* @type {string | null }
* @type {string | null} last part of the filename, as used for `sources` in sourcemaps
*/
file_basename = /** @type {any} */ (undefined);
@ -61,7 +55,7 @@ class PreprocessResult {
this.filename = filename;
this.update_source({ string: source });
// preprocess source must be relative to itself or equal null
this.file_basename = filename == null ? null : get_file_basename(filename);
this.file_basename = filename == null ? null : get_basename(filename);
}
/**

@ -1,10 +1,19 @@
import type { Style } from './template';
export interface BaseNode {
start: number;
end: number;
}
export interface StyleSheet extends BaseNode {
type: 'StyleSheet';
attributes: any[]; // TODO
children: Array<Atrule | Rule>;
content: {
start: number;
end: number;
styles: string;
};
}
export interface Atrule extends BaseNode {
type: 'Atrule';
name: string;
@ -16,16 +25,36 @@ export interface Rule extends BaseNode {
type: 'Rule';
prelude: SelectorList;
block: Block;
metadata: {
parent_rule: null | Rule;
has_local_selectors: boolean;
};
}
export interface SelectorList extends BaseNode {
type: 'SelectorList';
children: Selector[];
children: ComplexSelector[];
}
export interface Selector extends BaseNode {
type: 'Selector';
children: Array<SimpleSelector | Combinator>;
export interface ComplexSelector extends BaseNode {
type: 'ComplexSelector';
children: RelativeSelector[];
metadata: {
rule: null | Rule;
used: boolean;
};
}
export interface RelativeSelector extends BaseNode {
type: 'RelativeSelector';
combinator: null | Combinator;
selectors: SimpleSelector[];
metadata: {
is_global: boolean;
is_host: boolean;
is_root: boolean;
scoped: boolean;
};
}
export interface TypeSelector extends BaseNode {
@ -67,6 +96,11 @@ export interface Percentage extends BaseNode {
value: string;
}
export interface NestingSelector extends BaseNode {
type: 'NestingSelector';
name: '&';
}
export interface Nth extends BaseNode {
type: 'Nth';
value: string;
@ -80,7 +114,8 @@ export type SimpleSelector =
| PseudoElementSelector
| PseudoClassSelector
| Percentage
| Nth;
| Nth
| NestingSelector;
export interface Combinator extends BaseNode {
type: 'Combinator';
@ -99,4 +134,14 @@ export interface Declaration extends BaseNode {
}
// for zimmerframe
export type Node = Style | Rule | Atrule;
export type Node =
| StyleSheet
| Rule
| Atrule
| SelectorList
| Block
| ComplexSelector
| RelativeSelector
| Combinator
| SimpleSelector
| Declaration;

@ -178,10 +178,6 @@ export interface CompileOptions extends ModuleCompileOptions {
* @default null
*/
cssOutputFilename?: string;
// Other Svelte 4 compiler options:
// enableSourcemap?: EnableSourcemap; // TODO bring back? https://github.com/sveltejs/svelte/pull/6835
// legacy?: boolean; // TODO compiler error noting the new purpose?
}
export interface ModuleCompileOptions {
@ -285,8 +281,11 @@ export interface Binding {
legacy_dependencies: Binding[];
/** Legacy props: the `class` in `{ export klass as class}` */
prop_alias: string | null;
/** If this is set, all references should use this expression instead of the identifier name */
expression: Expression | null;
/**
* If this is set, all references should use this expression instead of the identifier name.
* If a function is given, it will be called with the identifier at that location and should return the new expression.
*/
expression: Expression | ((id: Identifier) => Expression) | null;
/** If this is set, all mutations should use this expression */
mutation: ((assignment: AssignmentExpression, context: Context<any, any>) => Expression) | null;
}

@ -1,4 +1,4 @@
import type { StyleDirective as LegacyStyleDirective, Text } from '#compiler';
import type { StyleDirective as LegacyStyleDirective, Text, Css } from '#compiler';
import type {
ArrayExpression,
AssignmentExpression,
@ -227,9 +227,28 @@ export type LegacyElementLike =
| LegacyTitle
| LegacyWindow;
export interface LegacyStyle extends BaseNode {
type: 'Style';
attributes: any[];
content: {
start: number;
end: number;
styles: string;
};
children: any[];
}
export interface LegacySelector extends BaseNode {
type: 'Selector';
children: Array<Css.Combinator | Css.SimpleSelector>;
}
export type LegacyCssNode = LegacyStyle | LegacySelector;
export type LegacySvelteNode =
| LegacyConstTag
| LegacyElementLike
| LegacyAttributeLike
| LegacyAttributeShorthand
| LegacyCssNode
| Text;

@ -1,8 +1,7 @@
import type { Binding } from '#compiler';
import type { Binding, Css } from '#compiler';
import type {
ArrayExpression,
ArrowFunctionExpression,
ArrayPattern,
VariableDeclaration,
VariableDeclarator,
Expression,
@ -16,6 +15,7 @@ import type {
Program,
SpreadElement
} from 'estree';
import type { Atrule, Rule } from './css';
export interface BaseNode {
type: string;
@ -53,7 +53,7 @@ export interface Root extends BaseNode {
options: SvelteOptions | null;
fragment: Fragment;
/** The parsed `<style>` element, if exists */
css: Style | null;
css: Css.StyleSheet | null;
/** The parsed `<script>` element, if exists */
instance: Script | null;
/** The parsed `<script context="module">` element, if exists */
@ -291,6 +291,7 @@ export interface RegularElement extends BaseElement {
svg: boolean;
/** `true` if contains a SpreadAttribute */
has_spread: boolean;
scoped: boolean;
};
}
@ -320,6 +321,7 @@ export interface SvelteElement extends BaseElement {
* `null` means we can't know statically.
*/
svg: boolean | null;
scoped: boolean;
};
}
@ -378,7 +380,7 @@ export interface EachBlock extends BaseNode {
/** Set if something in the array expression is shadowed within the each block */
array_name: Identifier | null;
index: Identifier;
item_name: string;
item: Identifier;
declarations: Map<string, Binding>;
/** List of bindings that are referenced within the expression */
references: Binding[];
@ -460,7 +462,7 @@ export type TemplateNode =
| Comment
| Block;
export type SvelteNode = Node | TemplateNode | Fragment;
export type SvelteNode = Node | TemplateNode | Fragment | Css.Node;
export interface Script extends BaseNode {
type: 'Script';
@ -468,17 +470,6 @@ export interface Script extends BaseNode {
content: Program;
}
export interface Style extends BaseNode {
type: 'Style';
attributes: any[]; // TODO
children: any[]; // TODO add CSS node types
content: {
start: number;
end: number;
styles: string;
};
}
declare module 'estree' {
export interface BaseNode {
/** Added by the Svelte parser */

@ -36,7 +36,7 @@ export function is_text_attribute(attribute) {
* @param {import('#compiler').Attribute} attribute
* @returns {attribute is import('#compiler').Attribute & { value: [import('#compiler').ExpressionTag] }}
*/
function is_expression_attribute(attribute) {
export function is_expression_attribute(attribute) {
return (
attribute.value !== true &&
attribute.value.length === 1 &&

@ -243,8 +243,18 @@ export class MappedCode {
}
}
// browser vs node.js
const b64enc =
typeof window !== 'undefined' && typeof btoa === 'function'
? /** @param {string} str */ (str) => btoa(unescape(encodeURIComponent(str)))
: /** @param {string} str */ (str) => Buffer.from(str).toString('base64');
const b64dec =
typeof window !== 'undefined' && typeof atob === 'function'
? atob
: /** @param {any} a */ (a) => Buffer.from(a, 'base64').toString();
/**
* @param {string} filename
* @param {string} filename Basename of the input file
* @param {Array<import('@ampproject/remapping').DecodedSourceMap | import('@ampproject/remapping').RawSourceMap>} sourcemap_list
*/
export function combine_sourcemaps(filename, sourcemap_list) {
@ -263,6 +273,10 @@ export function combine_sourcemaps(filename, sourcemap_list) {
// use loader interface
sourcemap_list[0], // last map
(sourcefile) => {
// TODO the equality check assumes that the preprocessor map has the input file as a relative path in sources,
// e.g. when the input file is `src/foo/bar.svelte`, then sources is expected to contain just `bar.svelte`.
// Therefore filename also needs to be the basename of the path. This feels brittle, investigate how we can
// harden this (without breaking other tooling that assumes this behavior).
if (sourcefile === filename && sourcemap_list[map_idx]) {
return sourcemap_list[map_idx++]; // idx 1, 2, ...
// bundle file = branch node
@ -286,7 +300,7 @@ export function combine_sourcemaps(filename, sourcemap_list) {
* @param {string | import('@ampproject/remapping').DecodedSourceMap | import('@ampproject/remapping').RawSourceMap} preprocessor_map_input
* @returns {import('magic-string').SourceMap}
*/
export function apply_preprocessor_sourcemap(filename, svelte_map, preprocessor_map_input) {
function apply_preprocessor_sourcemap(filename, svelte_map, preprocessor_map_input) {
if (!svelte_map || !preprocessor_map_input) return svelte_map;
const preprocessor_map =
typeof preprocessor_map_input === 'string'
@ -305,18 +319,7 @@ export function apply_preprocessor_sourcemap(filename, svelte_map, preprocessor_
toUrl: {
enumerable: false,
value: function toUrl() {
let b64 = '';
if (typeof window !== 'undefined' && window.btoa) {
// btoa doesn't support multi-byte characters
b64 = window.btoa(unescape(encodeURIComponent(this.toString())));
} else if (typeof Buffer !== 'undefined') {
b64 = Buffer.from(this.toString(), 'utf8').toString('base64');
} else {
throw new Error(
'Unsupported environment: `window.btoa` or `Buffer` should be present to use toUrl.'
);
}
return 'data:application/json;charset=utf-8;base64,' + b64;
return 'data:application/json;charset=utf-8;base64,' + b64enc(this.toString());
}
}
});
@ -361,7 +364,7 @@ export function parse_attached_sourcemap(processed, tag_name) {
// ignore attached sourcemap
return '';
}
processed.map = atob(map_data); // use attached sourcemap
processed.map = b64dec(map_data); // use attached sourcemap
return ''; // remove from processed.code
}
// sourceMappingURL is path or URL
@ -377,3 +380,68 @@ export function parse_attached_sourcemap(processed, tag_name) {
return ''; // remove from processed.code
});
}
/**
* @param {{ code: string, map: import('magic-string').SourceMap}} result
* @param {import('#compiler').ValidatedCompileOptions} options
* @param {string} source_name
*/
export function merge_with_preprocessor_map(result, options, source_name) {
if (options.sourcemap) {
const file_basename = get_basename(options.filename || 'input.svelte');
// The preprocessor map is expected to contain `sources: [basename_of_filename]`, but our own
// map may contain a different file name. Patch our map beforehand to align sources so merging
// with the preprocessor map works correctly.
result.map.sources = [file_basename];
result.map = apply_preprocessor_sourcemap(
file_basename,
result.map,
/** @type {any} */ (options.sourcemap)
);
// After applying the preprocessor map, we need to do the inverse and make the sources
// relative to the input file again in case the output code is in a different directory.
if (file_basename !== source_name) {
result.map.sources = result.map.sources.map(
/** @param {string} source */ (source) => get_relative_path(source_name, source)
);
}
}
}
/**
* @param {string} from
* @param {string} to
*/
export function get_relative_path(from, to) {
// Don't use node's utils here to ensure the compiler is usable in a browser environment
const from_parts = from.split(/[/\\]/);
const to_parts = to.split(/[/\\]/);
from_parts.pop(); // get dirname
while (from_parts[0] === to_parts[0]) {
from_parts.shift();
to_parts.shift();
}
if (from_parts.length) {
let i = from_parts.length;
while (i--) from_parts[i] = '..';
}
return from_parts.concat(to_parts).join('/');
}
/**
* Like node's `basename`, but doesn't use it to ensure the compiler is usable in a browser environment
* @param {string} filename
*/
export function get_basename(filename) {
return /** @type {string} */ (filename.split(/[/\\]/).pop());
}
/**
* @param {string | undefined} filename
* @param {string | undefined} output_filename
* @param {string} fallback
*/
export function get_source_name(filename, output_filename, fallback) {
if (!filename) return fallback;
return output_filename ? get_relative_path(output_filename, filename) : get_basename(filename);
}

@ -87,7 +87,6 @@ export const validate_component_options =
namespace: list(['html', 'svg', 'foreign']),
// TODO this is a sourcemap option, would be good to put under a sourcemap namespace
outputFilename: string(undefined),
preserveComments: boolean(false),
@ -96,16 +95,15 @@ export const validate_component_options =
runes: boolean(undefined),
sourcemap: validator(undefined, (input, keypath) => {
// TODO
return input;
}),
enableSourcemap: validator(undefined, (input, keypath) => {
// TODO decide if we want to keep this
sourcemap: validator(undefined, (input) => {
// Source maps can take on a variety of values, including string, JSON, map objects from magic-string and source-map,
// so there's no good way to check type validity here
return input;
}),
enableSourcemap: warn_removed(
'The enableSourcemap option has been removed. Source maps are always generated now, and tooling can choose to ignore them.'
),
hydratable: warn_removed(
'The hydratable option has been removed. Svelte components are always hydratable now.'
),

@ -12,7 +12,10 @@ const css = {
/** @satisfies {Warnings} */
const attributes = {
'avoid-is': () => 'The "is" attribute is not supported cross-browser and should be avoided'
'avoid-is': () => 'The "is" attribute is not supported cross-browser and should be avoided',
/** @param {string} name */
'global-event-reference': (name) =>
`You are referencing globalThis.${name}. Did you forget to declare a variable with that name?`
};
/** @satisfies {Warnings} */
@ -24,8 +27,7 @@ const runes = {
/** @param {string} name */
'non-state-reference': (name) =>
`${name} is updated, but is not declared with $state(...). Changing its value will not correctly trigger updates.`,
'derived-iife': () =>
`Use \`$derived.call(() => {...})\` instead of \`$derived((() => {...})());\``
'derived-iife': () => `Use \`$derived.by(() => {...})\` instead of \`$derived((() => {...})());\``
};
/** @satisfies {Warnings} */

@ -89,7 +89,7 @@ if (typeof HTMLElement === 'function') {
if (!this.$$c) {
// We wait one tick to let possible child slot elements be created/mounted
await Promise.resolve();
if (!this.$$cn) {
if (!this.$$cn || this.$$c) {
return;
}
/** @param {string} name */

@ -11,6 +11,7 @@ import {
current_hydration_fragment,
get_hydration_fragment,
hydrate_block_anchor,
hydrating,
set_current_hydration_fragment
} from './hydration.js';
import { clear_text_content, empty, map_get, map_set } from './operations.js';
@ -61,7 +62,10 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
/** @type {null | import('./types.js').EffectSignal} */
let render = null;
/** Whether or not there was a "rendered fallback but want to render items" (or vice versa) hydration mismatch */
/**
* Whether or not there was a "rendered fallback but want to render items" (or vice versa) hydration mismatch.
* Needs to be a `let` or else it isn't treeshaken out
*/
let mismatch = false;
block.r =
@ -107,7 +111,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
// If the each block is controlled, then the anchor node will be the surrounding
// element in which the each block is rendered, which requires certain handling
// depending on whether we're in hydration mode or not
if (current_hydration_fragment === null) {
if (!hydrating) {
// Create a new anchor on the fly because there's none due to the optimization
anchor = empty();
block.a.appendChild(anchor);
@ -153,13 +157,13 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
const length = array.length;
if (current_hydration_fragment !== null) {
if (hydrating) {
const is_each_else_comment =
/** @type {Comment} */ (current_hydration_fragment?.[0])?.data === 'ssr:each_else';
// Check for hydration mismatch which can happen if the server renders the each fallback
// but the client has items, or vice versa. If so, remove everything inside the anchor and start fresh.
if ((is_each_else_comment && length) || (!is_each_else_comment && !length)) {
remove(/** @type {import('./types.js').TemplateNode[]} */ (current_hydration_fragment));
remove(current_hydration_fragment);
set_current_hydration_fragment(null);
mismatch = true;
} else if (is_each_else_comment) {
@ -306,22 +310,22 @@ function reconcile_indexed_array(
}
} else {
var item;
var is_hydrating = current_hydration_fragment !== null;
/** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false;
b_blocks = Array(b);
if (is_hydrating) {
if (hydrating) {
// Hydrate block
var hydration_list = /** @type {import('./types.js').TemplateNode[]} */ (
current_hydration_fragment
);
var hydrating_node = hydration_list[0];
for (; index < length; index++) {
var fragment = /** @type {Array<Text | Comment | Element>} */ (
get_hydration_fragment(hydrating_node)
);
var fragment = get_hydration_fragment(hydrating_node);
set_current_hydration_fragment(fragment);
if (!fragment) {
// If fragment is null, then that means that the server rendered less items than what
// the client code specifies -> break out and continue with client-side node creation
mismatch = true;
break;
}
@ -357,7 +361,7 @@ function reconcile_indexed_array(
}
}
if (is_hydrating && current_hydration_fragment === null) {
if (mismatch) {
// Server rendered less nodes than the client -> set empty array so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
}
@ -425,9 +429,10 @@ function reconcile_tracked_array(
var key;
var item;
var idx;
var is_hydrating = current_hydration_fragment !== null;
/** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false;
b_blocks = Array(b);
if (is_hydrating) {
if (hydrating) {
// Hydrate block
var fragment;
var hydration_list = /** @type {import('./types.js').TemplateNode[]} */ (
@ -435,13 +440,12 @@ function reconcile_tracked_array(
);
var hydrating_node = hydration_list[0];
while (b > 0) {
fragment = /** @type {Array<Text | Comment | Element>} */ (
get_hydration_fragment(hydrating_node)
);
fragment = get_hydration_fragment(hydrating_node);
set_current_hydration_fragment(fragment);
if (!fragment) {
// If fragment is null, then that means that the server rendered less items than what
// the client code specifies -> break out and continue with client-side node creation
mismatch = true;
break;
}
@ -594,7 +598,7 @@ function reconcile_tracked_array(
}
}
if (is_hydrating && current_hydration_fragment === null) {
if (mismatch) {
// Server rendered less nodes than the client -> set empty array so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
}

@ -3,25 +3,37 @@
import { empty } from './operations.js';
import { schedule_task } from './runtime.js';
/** @type {null | Array<import('./types.js').TemplateNode>} */
export let current_hydration_fragment = null;
/**
* Use this variable to guard everything related to hydration code so it can be treeshaken out
* if the user doesn't use the `hydrate` method and these code paths are therefore not needed.
*/
export let hydrating = false;
/**
* Array of nodes to traverse for hydration. This will be null if we're not hydrating, but for
* the sake of simplicity we're not going to use `null` checks everywhere and instead rely on
* the `hydrating` flag to tell whether or not we're in hydration mode at which point this is set.
* @type {import('./types.js').TemplateNode[]}
*/
export let current_hydration_fragment = /** @type {any} */ (null);
/**
* @param {null | Array<import('./types.js').TemplateNode>} fragment
* @param {null | import('./types.js').TemplateNode[]} fragment
* @returns {void}
*/
export function set_current_hydration_fragment(fragment) {
current_hydration_fragment = fragment;
hydrating = fragment !== null;
current_hydration_fragment = /** @type {import('./types.js').TemplateNode[]} */ (fragment);
}
/**
* Returns all nodes between the first `<!--ssr:...-->` comment tag pair encountered.
* @param {Node | null} node
* @param {boolean} [insert_text] Whether to insert an empty text node if the fragment is empty
* @returns {Array<import('./types.js').TemplateNode> | null}
* @returns {import('./types.js').TemplateNode[] | null}
*/
export function get_hydration_fragment(node, insert_text = false) {
/** @type {Array<import('./types.js').TemplateNode>} */
/** @type {import('./types.js').TemplateNode[]} */
const fragment = [];
/** @type {null | Node} */
@ -66,9 +78,10 @@ export function get_hydration_fragment(node, insert_text = false) {
* @returns {void}
*/
export function hydrate_block_anchor(anchor_node, is_controlled) {
/** @type {Node} */
let target_node = anchor_node;
if (current_hydration_fragment !== null) {
if (hydrating) {
/** @type {Node} */
let target_node = anchor_node;
if (is_controlled) {
target_node = /** @type {Node} */ (target_node.firstChild);
}

@ -1,4 +1,4 @@
import { current_hydration_fragment, get_hydration_fragment } from './hydration.js';
import { current_hydration_fragment, get_hydration_fragment, hydrating } from './hydration.js';
import { get_descriptor } from './utils.js';
// We cache the Node and Element prototype methods, so that we can avoid doing
@ -171,7 +171,7 @@ export function empty() {
/*#__NO_SIDE_EFFECTS__*/
export function child(node) {
const child = first_child_get.call(node);
if (current_hydration_fragment !== null) {
if (hydrating) {
// Child can be null if we have an element with a single child, like `<p>{text}</p>`, where `text` is empty
if (child === null) {
const text = empty();
@ -192,7 +192,7 @@ export function child(node) {
*/
/*#__NO_SIDE_EFFECTS__*/
export function child_frag(node, is_text) {
if (current_hydration_fragment !== null) {
if (hydrating) {
const first_node = /** @type {Node[]} */ (node)[0];
// if an {expression} is empty during SSR, there might be no
@ -225,7 +225,7 @@ export function child_frag(node, is_text) {
/*#__NO_SIDE_EFFECTS__*/
export function sibling(node, is_text = false) {
const next_sibling = next_sibling_get.call(node);
if (current_hydration_fragment !== null) {
if (hydrating) {
// if a sibling {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (is_text && next_sibling?.nodeType !== 3) {
@ -276,6 +276,7 @@ export function create_element(name) {
}
/**
* Expects to only be called in hydration mode
* @param {Node} node
* @returns {Node}
*/
@ -283,7 +284,7 @@ function capture_fragment_from_node(node) {
if (
node.nodeType === 8 &&
/** @type {Comment} */ (node).data.startsWith('ssr:') &&
/** @type {Array<Element | Text | Comment>} */ (current_hydration_fragment).at(-1) !== node
current_hydration_fragment.at(-1) !== node
) {
const fragment = /** @type {Array<Element | Text | Comment>} */ (get_hydration_fragment(node));
const last_child = fragment.at(-1) || node;

@ -1,5 +1,5 @@
import { append_child } from './operations.js';
import { current_hydration_fragment, hydrate_block_anchor } from './hydration.js';
import { current_hydration_fragment, hydrate_block_anchor, hydrating } from './hydration.js';
import { is_array } from './utils.js';
/** @param {string} html */
@ -92,7 +92,7 @@ export function remove(current) {
*/
export function reconcile_html(target, value, svg) {
hydrate_block_anchor(target);
if (current_hydration_fragment !== null) {
if (hydrating) {
return current_hydration_fragment;
}
var html = value + '';

@ -43,6 +43,7 @@ import {
untrack,
effect,
flushSync,
flush_sync,
safe_not_equal,
current_block,
managed_effect,
@ -54,6 +55,7 @@ import {
current_hydration_fragment,
get_hydration_fragment,
hydrate_block_anchor,
hydrating,
set_current_hydration_fragment
} from './hydration.js';
import {
@ -63,12 +65,11 @@ import {
get_descriptors,
is_array,
is_function,
object_assign,
object_keys
object_assign
} from './utils.js';
import { is_promise } from '../common.js';
import { bind_transition, trigger_transitions } from './transitions.js';
import { proxy } from './proxy.js';
import { STATE_SYMBOL } from './proxy.js';
/** @type {Set<string>} */
const all_registerd_events = new Set();
@ -166,7 +167,7 @@ export function svg_replace(node) {
* @returns {Element | DocumentFragment | Node[]}
*/
function open_template(is_fragment, use_clone_node, anchor, template_element_fn) {
if (current_hydration_fragment !== null) {
if (hydrating) {
if (anchor !== null) {
hydrate_block_anchor(anchor, false);
}
@ -217,7 +218,7 @@ export function space(anchor) {
// if an {expression} is empty during SSR, there might be no
// text node to hydrate (or an anchor comment is falsely detected instead)
// — we must therefore create one
if (current_hydration_fragment !== null && node?.nodeType !== 3) {
if (hydrating && node?.nodeType !== 3) {
node = empty();
// @ts-ignore in this case the anchor should always be a comment,
// if not something more fundamental is wrong and throwing here is better to bail out early
@ -251,10 +252,8 @@ function close_template(dom, is_fragment, anchor) {
? dom
: /** @type {import('./types.js').TemplateNode[]} */ (Array.from(dom.childNodes))
: dom;
if (anchor !== null) {
if (current_hydration_fragment === null) {
insert(current, null, anchor);
}
if (!hydrating && anchor !== null) {
insert(current, null, anchor);
}
block.d = current;
}
@ -415,14 +414,13 @@ export function class_name(dom, value) {
// @ts-expect-error need to add __className to patched prototype
const prev_class_name = dom.__className;
const next_class_name = to_class(value);
const is_hydrating = current_hydration_fragment !== null;
if (is_hydrating && dom.className === next_class_name) {
if (hydrating && dom.className === next_class_name) {
// In case of hydration don't reset the class as it's already correct.
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
} else if (
prev_class_name !== next_class_name ||
(is_hydrating && dom.className !== next_class_name)
(hydrating && dom.className !== next_class_name)
) {
if (next_class_name === '') {
dom.removeAttribute('class');
@ -452,7 +450,7 @@ export function text(dom, value) {
// @ts-expect-error need to add __value to patched prototype
const prev_node_value = dom.__nodeValue;
const next_node_value = stringify(value);
if (current_hydration_fragment !== null && dom.nodeValue === next_node_value) {
if (hydrating && dom.nodeValue === next_node_value) {
// In case of hydration don't reset the nodeValue as it's already correct.
// @ts-expect-error need to add __nodeValue to patched prototype
dom.__nodeValue = next_node_value;
@ -739,7 +737,7 @@ export function bind_playback_rate(media, get_value, update) {
* @param {(paused: boolean) => void} update
*/
export function bind_paused(media, get_value, update) {
let mounted = current_hydration_fragment !== null;
let mounted = hydrating;
let paused = get_value();
const callback = () => {
if (paused !== media.paused) {
@ -1297,6 +1295,13 @@ export function bind_prop(props, prop, value) {
}
}
/**
* @param {unknown} value
*/
function is_state_object(value) {
return value != null && typeof value === 'object' && STATE_SYMBOL in value;
}
/**
* @param {Element} element_or_component
* @param {(value: unknown) => void} update
@ -1304,9 +1309,15 @@ export function bind_prop(props, prop, value) {
* @returns {void}
*/
export function bind_this(element_or_component, update, binding) {
untrack(() => {
update(element_or_component);
render_effect(() => () => {
render_effect(() => {
// If we are reading from a proxied state binding, then we don't need to untrack
// the update function as it will be fine-grain.
if (is_state_object(binding) || (is_signal(binding) && is_state_object(binding.v))) {
update(element_or_component);
} else {
untrack(() => update(element_or_component));
}
return () => {
// Defer to the next tick so that all updates can be reconciled first.
// This solves the case where one variable is shared across multiple this-bindings.
render_effect(() => {
@ -1316,7 +1327,7 @@ export function bind_this(element_or_component, update, binding) {
}
});
});
});
};
});
}
@ -1452,7 +1463,8 @@ export function slot(anchor_node, slot_fn, slot_props, fallback_fn) {
function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) {
const block = create_if_block();
hydrate_block_anchor(anchor_node);
const previous_hydration_fragment = current_hydration_fragment;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false;
/** @type {null | import('./types.js').TemplateNode | Array<import('./types.js').TemplateNode>} */
let consequent_dom = null;
@ -1495,7 +1507,7 @@ function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) {
trigger_transitions(alternate_transitions, 'in');
}
}
} else if (current_hydration_fragment !== null) {
} else if (hydrating) {
const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data;
if (
!comment_text ||
@ -1506,6 +1518,7 @@ function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) {
// This could happen using when `{#if browser} .. {/if}` in SvelteKit.
remove(current_hydration_fragment);
set_current_hydration_fragment(null);
mismatch = true;
} else {
// Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm
current_hydration_fragment.shift();
@ -1530,9 +1543,9 @@ function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) {
}
if (result && current_branch_effect !== consequent_effect) {
consequent_fn(anchor_node);
if (current_branch_effect === null) {
// Restore previous fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment(previous_hydration_fragment);
if (mismatch && current_branch_effect === null) {
// Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
}
current_branch_effect = consequent_effect;
consequent_dom = block.d;
@ -1558,9 +1571,9 @@ function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) {
if (alternate_fn !== null) {
alternate_fn(anchor_node);
}
if (current_branch_effect === null) {
// Restore previous fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment(previous_hydration_fragment);
if (mismatch && current_branch_effect === null) {
// Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
}
current_branch_effect = alternate_effect;
alternate_dom = block.d;
@ -1593,10 +1606,15 @@ export function head(render_fn) {
const block = create_head_block();
// The head function may be called after the first hydration pass and ssr comment nodes may still be present,
// therefore we need to skip that when we detect that we're not in hydration mode.
const hydration_fragment =
current_hydration_fragment !== null ? get_hydration_fragment(document.head.firstChild) : null;
const previous_hydration_fragment = current_hydration_fragment;
set_current_hydration_fragment(hydration_fragment);
let hydration_fragment = null;
let previous_hydration_fragment = null;
let is_hydrating = hydrating;
if (is_hydrating) {
hydration_fragment = get_hydration_fragment(document.head.firstChild);
previous_hydration_fragment = current_hydration_fragment;
set_current_hydration_fragment(hydration_fragment);
}
try {
const head_effect = render_effect(
() => {
@ -1606,7 +1624,7 @@ export function head(render_fn) {
block.d = null;
}
let anchor = null;
if (current_hydration_fragment === null) {
if (!hydrating) {
anchor = empty();
document.head.appendChild(anchor);
}
@ -1623,7 +1641,9 @@ export function head(render_fn) {
});
block.e = head_effect;
} finally {
set_current_hydration_fragment(previous_hydration_fragment);
if (is_hydrating) {
set_current_hydration_fragment(previous_hydration_fragment);
}
}
}
@ -1688,7 +1708,7 @@ export function element(anchor_node, tag_fn, is_svg, render_fn) {
? null
: anchor_node.parentElement?.namespaceURI ?? null;
const next_element = tag
? current_hydration_fragment !== null
? hydrating
? /** @type {Element} */ (current_hydration_fragment[0])
: ns
? document.createElementNS(ns, tag)
@ -1701,7 +1721,7 @@ export function element(anchor_node, tag_fn, is_svg, render_fn) {
element = next_element;
if (element !== null && render_fn !== undefined) {
let anchor;
if (current_hydration_fragment !== null) {
if (hydrating) {
// Use the existing ssr comment as the anchor so that the inner open and close
// methods can pick up the existing nodes correctly
anchor = /** @type {Comment} */ (element.firstChild);
@ -1765,7 +1785,9 @@ export function component(anchor_node, component_fn, render_fn) {
transition.f(() => {
transitions.delete(transition);
if (transitions.size === 0) {
if (render.e !== null) {
// If the current render has changed since, then we can remove the old render
// effect as it's stale.
if (current_render !== render && render.e !== null) {
if (render.d !== null) {
remove(render.d);
render.d = null;
@ -1891,7 +1913,9 @@ function await_block(anchor_node, input, pending_fn, then_fn, catch_fn) {
transition.f(() => {
transitions.delete(transition);
if (transitions.size === 0) {
if (render.e !== null) {
// If the current render has changed since, then we can remove the old render
// effect as it's stale.
if (current_render !== render && render.e !== null) {
if (render.d !== null) {
remove(render.d);
render.d = null;
@ -2051,7 +2075,9 @@ export function key(anchor_node, key, render_fn) {
transition.f(() => {
transitions.delete(transition);
if (transitions.size === 0) {
if (render.e !== null) {
// If the current render has changed since, then we can remove the old render
// effect as it's stale.
if (current_render !== render && render.e !== null) {
if (render.d !== null) {
remove(render.d);
render.d = null;
@ -2152,7 +2178,7 @@ export function cssProps(anchor, is_html, props, component) {
/** @type {Text | Comment} */
let component_anchor;
if (current_hydration_fragment !== null) {
if (hydrating) {
// Hydration: css props element is surrounded by a ssr comment ...
tag = /** @type {HTMLElement | SVGElement} */ (current_hydration_fragment[0]);
// ... and the child(ren) of the css props element is also surround by a ssr comment
@ -2318,7 +2344,7 @@ export function action(dom, action, value_fn) {
* @returns {void}
*/
export function remove_input_attr_defaults(dom) {
if (current_hydration_fragment !== null) {
if (hydrating) {
attr(dom, 'value', null);
attr(dom, 'checked', null);
}
@ -2330,7 +2356,7 @@ export function remove_input_attr_defaults(dom) {
* @returns {void}
*/
export function remove_textarea_child(dom) {
if (current_hydration_fragment !== null && dom.firstChild !== null) {
if (hydrating && dom.firstChild !== null) {
dom.textContent = '';
}
}
@ -2360,7 +2386,7 @@ export function attr(dom, attribute, value) {
}
if (
current_hydration_fragment === null ||
!hydrating ||
(dom.getAttribute(attribute) !== value &&
// If we reset those, they would result in another network request, which we want to avoid.
// We assume they are the same between client and server as checking if they are equal is expensive
@ -2423,7 +2449,7 @@ export function srcset_url_equal(element, srcset) {
* @param {string | null} value
*/
function check_src_in_dev_hydration(dom, attribute, value) {
if (!current_hydration_fragment) return;
if (!hydrating) return;
if (attribute !== 'src' && attribute !== 'href' && attribute !== 'srcset') return;
if (attribute === 'srcset' && srcset_url_equal(dom, value)) return;
@ -2636,7 +2662,7 @@ export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_ha
check_src_in_dev_hydration(dom, name, value);
}
if (
current_hydration_fragment === null ||
!hydrating ||
// @ts-ignore see attr method for an explanation of src/srcset
(dom[name] !== value && name !== 'src' && name !== 'href' && name !== 'srcset')
) {
@ -2799,14 +2825,21 @@ export function spread_props(...props) {
return new Proxy({ props }, spread_props_handler);
}
// TODO 5.0 remove this
/**
* Mounts the given component to the given target and returns a handle to the component's public accessors
* as well as a `$set` and `$destroy` method to update the props of the component or destroy it.
*
* If you don't need to interact with the component after mounting, use `mount` instead to save some bytes.
* @deprecated Use `mount` or `hydrate` instead
*/
export function createRoot() {
throw new Error(
'`createRoot` has been removed. Use `mount` or `hydrate` instead. See the updated docs for more info: https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes'
);
}
/**
* Mounts a component to the given target and returns the exports and potentially the accessors (if compiled with `accessors: true`) of the component
*
* @template {Record<string, any>} Props
* @template {Record<string, any> | undefined} Exports
* @template {Record<string, any>} Exports
* @template {Record<string, any>} Events
* @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component
* @param {{
@ -2815,48 +2848,22 @@ export function spread_props(...props) {
* events?: Events;
* context?: Map<any, any>;
* intro?: boolean;
* recover?: false;
* }} options
* @returns {Exports & { $destroy: () => void; $set: (props: Partial<Props>) => void; }}
* @returns {Exports}
*/
export function createRoot(component, options) {
const props = proxy(/** @type {any} */ (options.props) || {}, false);
let [accessors, $destroy] = mount(component, { ...options, props });
const result =
/** @type {Exports & { $destroy: () => void; $set: (props: Partial<Props>) => void; }} */ ({
$set: (next) => {
object_assign(props, next);
},
$destroy
});
for (const key of object_keys(accessors || {})) {
define_property(result, key, {
get() {
// @ts-expect-error TS doesn't know key exists on accessor
return accessors[key];
},
/** @param {any} value */
set(value) {
// @ts-expect-error TS doesn't know key exists on accessor
flushSync(() => (accessors[key] = value));
},
enumerable: true
});
}
return result;
export function mount(component, options) {
init_operations();
const anchor = empty();
options.target.appendChild(anchor);
// Don't flush previous effects to ensure order of outer effects stays consistent
return flush_sync(() => _mount(component, { ...options, anchor }), false);
}
/**
* Mounts the given component to the given target and returns the accessors of the component and a function to destroy it.
*
* If you need to interact with the component after mounting, use `createRoot` instead.
* Hydrates a component on the given target and returns the exports and potentially the accessors (if compiled with `accessors: true`) of the component
*
* @template {Record<string, any>} Props
* @template {Record<string, any> | undefined} Exports
* @template {Record<string, any>} Exports
* @template {Record<string, any>} Events
* @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component
* @param {{
@ -2867,51 +2874,39 @@ export function createRoot(component, options) {
* intro?: boolean;
* recover?: false;
* }} options
* @returns {[Exports, () => void]}
* @returns {Exports}
*/
export function mount(component, options) {
export function hydrate(component, options) {
init_operations();
const registered_events = new Set();
const container = options.target;
const block = create_root_block(options.intro || false);
const first_child = /** @type {ChildNode} */ (container.firstChild);
// Call with insert_text == true to prevent empty {expressions} resulting in an empty
// fragment array, resulting in a hydration error down the line
const hydration_fragment = get_hydration_fragment(first_child, true);
const previous_hydration_fragment = current_hydration_fragment;
set_current_hydration_fragment(hydration_fragment);
/** @type {Exports} */
// @ts-expect-error will be defined because the render effect runs synchronously
let accessors = undefined;
/** @type {null | Text} */
let anchor = null;
if (hydration_fragment === null) {
anchor = empty();
container.appendChild(anchor);
}
let finished_hydrating = false;
try {
/** @type {null | Text} */
let anchor = null;
if (hydration_fragment === null) {
anchor = empty();
container.appendChild(anchor);
}
set_current_hydration_fragment(hydration_fragment);
const effect = render_effect(
() => {
if (options.context) {
push({});
/** @type {import('../client/types.js').ComponentContext} */ (
current_component_context
).c = options.context;
}
// @ts-expect-error the public typings are not what the actual function looks like
accessors = component(anchor, options.props || {});
if (options.context) {
pop();
}
},
block,
true
);
block.e = effect;
// Don't flush previous effects to ensure order of outer effects stays consistent
return flush_sync(() => {
const instance = _mount(component, { ...options, anchor });
// flush_sync will run this callback and then synchronously run any pending effects,
// which don't belong to the hydration phase anymore - therefore reset it here
set_current_hydration_fragment(null);
finished_hydrating = true;
return instance;
}, false);
} catch (error) {
if (options.recover !== false && hydration_fragment !== null) {
if (!finished_hydrating && options.recover !== false && hydration_fragment !== null) {
// eslint-disable-next-line no-console
console.error(
'ERR_SVELTE_HYDRATION_MISMATCH' +
@ -2923,6 +2918,7 @@ export function mount(component, options) {
remove(hydration_fragment);
first_child.remove();
hydration_fragment.at(-1)?.nextSibling?.remove();
set_current_hydration_fragment(null);
return mount(component, options);
} else {
throw error;
@ -2930,6 +2926,50 @@ export function mount(component, options) {
} finally {
set_current_hydration_fragment(previous_hydration_fragment);
}
}
/**
* @template {Record<string, any>} Props
* @template {Record<string, any>} Exports
* @template {Record<string, any>} Events
* @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component
* @param {{
* target: Node;
* anchor: null | Text;
* props?: Props;
* events?: Events;
* context?: Map<any, any>;
* intro?: boolean;
* recover?: false;
* }} options
* @returns {Exports}
*/
function _mount(component, options) {
const registered_events = new Set();
const container = options.target;
const block = create_root_block(options.intro || false);
/** @type {Exports} */
// @ts-expect-error will be defined because the render effect runs synchronously
let accessors = undefined;
const effect = render_effect(
() => {
if (options.context) {
push({});
/** @type {import('../client/types.js').ComponentContext} */ (current_component_context).c =
options.context;
}
// @ts-expect-error the public typings are not what the actual function looks like
accessors = component(options.anchor, options.props || {}) || {};
if (options.context) {
pop();
}
},
block,
true
);
block.e = effect;
const bound_event_listener = handle_event_propagation.bind(null, container);
const bound_document_event_listener = handle_event_propagation.bind(null, document);
@ -2968,23 +3008,38 @@ export function mount(component, options) {
event_handle(array_from(all_registerd_events));
root_event_handles.add(event_handle);
return [
accessors,
() => {
for (const event_name of registered_events) {
container.removeEventListener(event_name, bound_event_listener);
}
root_event_handles.delete(event_handle);
const dom = block.d;
if (dom !== null) {
remove(dom);
}
if (hydration_fragment !== null) {
remove(hydration_fragment);
}
destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.e));
mounted_components.set(accessors, () => {
for (const event_name of registered_events) {
container.removeEventListener(event_name, bound_event_listener);
}
];
root_event_handles.delete(event_handle);
const dom = block.d;
if (dom !== null) {
remove(dom);
}
destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.e));
});
return accessors;
}
/**
* References of the accessors of all components that were `mount`ed or `hydrate`d.
* Uses a `WeakMap` to avoid memory leaks.
*/
let mounted_components = new WeakMap();
/**
* Unmounts a component that was previously mounted using `mount` or `hydrate`.
* @param {Record<string, any>} component
*/
export function unmount(component) {
const destroy = mounted_components.get(component);
if (DEV && !destroy) {
// eslint-disable-next-line no-console
console.warn('Tried to unmount a component that was not mounted.');
}
destroy?.();
}
/**

@ -75,7 +75,11 @@ export let current_effect = null;
/** @type {null | import('./types.js').Signal[]} */
let current_dependencies = null;
let current_dependencies_index = 0;
/** @type {null | import('./types.js').Signal[]} */
/**
* Tracks writes that the effect it's executed in doesn't listen to yet,
* so that the dependency can be added to the effect later on if it then reads it
* @type {null | import('./types.js').Signal[]}
*/
let current_untracked_writes = null;
/** @type {null | import('./types.js').SignalDebug} */
let last_inspected_signal = null;
@ -766,9 +770,22 @@ export function flush_local_pre_effects(context) {
* @returns {void}
*/
export function flushSync(fn) {
flush_sync(fn);
}
/**
* Internal version of `flushSync` with the option to not flush previous effects.
* Returns the result of the passed function, if given.
* @param {() => any} [fn]
* @param {boolean} [flush_previous]
* @returns {any}
*/
export function flush_sync(fn, flush_previous = true) {
const previous_scheduler_mode = current_scheduler_mode;
const previous_queued_pre_and_render_effects = current_queued_pre_and_render_effects;
const previous_queued_effects = current_queued_effects;
let result;
try {
infinite_loop_guard();
/** @type {import('./types.js').EffectSignal[]} */
@ -779,10 +796,12 @@ export function flushSync(fn) {
current_scheduler_mode = FLUSH_SYNC;
current_queued_pre_and_render_effects = pre_and_render_effects;
current_queued_effects = effects;
flush_queued_effects(previous_queued_pre_and_render_effects);
flush_queued_effects(previous_queued_effects);
if (flush_previous) {
flush_queued_effects(previous_queued_pre_and_render_effects);
flush_queued_effects(previous_queued_effects);
}
if (fn !== undefined) {
fn();
result = fn();
}
if (current_queued_pre_and_render_effects.length > 0 || effects.length > 0) {
flushSync();
@ -799,6 +818,8 @@ export function flushSync(fn) {
current_queued_pre_and_render_effects = previous_queued_pre_and_render_effects;
current_queued_effects = previous_queued_effects;
}
return result;
}
/**
@ -1683,7 +1704,8 @@ export function prop(props, key, flags, initial) {
var current = get(current_value);
// legacy nonsense — need to ensure the source is invalidated when necessary
if (is_signals_recorded) {
// also needed for when handling inspect logic so we can inspect the correct source signal
if (is_signals_recorded || (DEV && inspect_fn)) {
// set this so that we don't reset to the parent value if `d`
// is invalidated because of `invalidate_inner_signals` (rather
// than because the parent or child value changed)

@ -691,7 +691,8 @@ function if_block_transition(transition) {
transition.f(() => {
const c = /** @type {Set<import('./types.js').Transition>} */ (consequent_transitions);
c.delete(transition);
if (c.size === 0) {
// If the block has changed to falsy and has transitions
if (!block.v && c.size === 0) {
const consequent_effect = block.ce;
execute_effect(/** @type {import('./types.js').EffectSignal} */ (consequent_effect));
}
@ -702,7 +703,8 @@ function if_block_transition(transition) {
transition.f(() => {
const a = /** @type {Set<import('./types.js').Transition>} */ (alternate_transitions);
a.delete(transition);
if (a.size === 0) {
// If the block has changed to truthy and has transitions
if (block.v && a.size === 0) {
const alternate_effect = block.ae;
execute_effect(/** @type {import('./types.js').EffectSignal} */ (alternate_effect));
}

@ -2,6 +2,7 @@ import * as $ from '../client/runtime.js';
import { is_promise, noop } from '../common.js';
import { subscribe_to_store } from '../../store/utils.js';
import { DOMBooleanAttributes } from '../../constants.js';
import { DEV } from 'esm-env';
export * from '../client/validate.js';
@ -340,6 +341,10 @@ export function merge_styles(style_attribute, style_directive) {
* @returns {V}
*/
export function store_get(store_values, store_name, store) {
if (DEV) {
validate_store(store, store_name.slice(1));
}
// it could be that someone eagerly updates the store in the instance script, so
// we should only reuse the store value in the template
if (store_name in store_values && store_values[store_name][0] === store) {
@ -356,18 +361,6 @@ export function store_get(store_values, store_name, store) {
return store_values[store_name][2];
}
/**
* @template V
* @param {Record<string, [any, any, any]>} store_values
* @param {string} store_name
* @param {import('../client/types.js').Store<V> | null | undefined} store
* @returns {V}
*/
export function store_get_dev(store_values, store_name, store) {
validate_store(store, store_name.slice(1));
return store_get(store_values, store_name, store);
}
/**
* @param {any} store
* @param {string} name

@ -14,6 +14,7 @@ import * as $ from '../internal/index.js';
* @param {import('../main/public.js').ComponentConstructorOptions<Props> & {
* component: import('../main/public.js').SvelteComponent<Props, Events, Slots>;
* immutable?: boolean;
* hydrate?: boolean;
* recover?: false;
* }} options
* @returns {import('../main/public.js').SvelteComponent<Props, Events, Slots> & Exports}
@ -53,28 +54,28 @@ class Svelte4Component {
/** @type {any} */
#events = {};
/** @type {ReturnType<typeof $.createRoot>} */
/** @type {Record<string, any>} */
#instance;
/**
* @param {import('../main/public.js').ComponentConstructorOptions & {
* component: any;
* immutable?: boolean;
* hydrate?: boolean;
* recover?: false;
* }} options
*/
constructor(options) {
this.#instance = $.createRoot(options.component, {
const props = $.proxy({ ...(options.props || {}), $$events: this.#events }, false);
this.#instance = (options.hydrate ? $.hydrate : $.mount)(options.component, {
target: options.target,
props: { ...options.props, $$events: this.#events },
props,
context: options.context,
intro: options.intro,
recover: options.recover
});
for (const key of Object.keys(this.#instance)) {
if (key === '$set' || key === '$destroy') continue;
define_property(this, key, {
get() {
return this.#instance[key];
@ -86,6 +87,13 @@ class Svelte4Component {
enumerable: true
});
}
this.#instance.$set = /** @param {Record<string, any>} next */ (next) => {
Object.assign(props, next);
};
this.#instance.$destroy = () => {
$.unmount(this.#instance);
};
}
/** @param {Record<string, any>} props */

@ -62,11 +62,11 @@ declare function $derived<T>(expression: T): T;
declare namespace $derived {
/**
* Sometimes you need to create complex derivations that don't fit inside a short expression.
* In these cases, you can use `$derived.call` which accepts a function as its argument.
* In these cases, you can use `$derived.by` which accepts a function as its argument.
*
* Example:
* ```ts
* let total = $derived.call(() => {
* let total = $derived.by(() => {
* let result = 0;
* for (const n of numbers) {
* result += n;
@ -75,9 +75,9 @@ declare namespace $derived {
* });
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$derived-call
* https://svelte-5-preview.vercel.app/docs/runes#$derived-by
*/
export function call<T>(fn: () => T): T;
export function by<T>(fn: () => T): T;
}
/**

@ -233,4 +233,13 @@ function init_update_callbacks(context) {
// TODO bring implementations in here
// (except probably untrack — do we want to expose that, if there's also a rune?)
export { flushSync, createRoot, mount, tick, untrack, unstate } from '../internal/index.js';
export {
flushSync,
mount,
hydrate,
tick,
unmount,
untrack,
unstate,
createRoot
} from '../internal/index.js';

@ -1,16 +1,18 @@
import { on_destroy } from '../internal/server/index.js';
export {
createRoot,
createEventDispatcher,
flushSync,
getAllContexts,
getContext,
hasContext,
mount,
hydrate,
setContext,
tick,
untrack
unmount,
untrack,
createRoot
} from './main-client.js';
/** @returns {void} */

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string}
*/
export const VERSION = '5.0.0-next.52';
export const VERSION = '5.0.0-next.58';
export const PUBLIC_VERSION = '5';

@ -70,7 +70,11 @@ class Animation {
_update() {
if (this.#reversed) {
this.currentTime = this.#timeline_offset + (this.#timeline_offset - raf.time);
if (this.#timeline_offset === 0) {
this.currentTime = this.#duration - raf.time;
} else {
this.currentTime = this.#timeline_offset + (this.#timeline_offset - raf.time);
}
} else {
this.currentTime = raf.time - this.#timeline_offset;
}
@ -130,6 +134,9 @@ class Animation {
}
reverse() {
if (this.#paused && !raf.animations.has(this)) {
raf.animations.add(this);
}
this.#timeline_offset = this.currentTime;
this.#reversed = !this.#reversed;
this.playState = 'running';

@ -2,6 +2,6 @@
background-color: red;
}
main.svelte-xyz div:where(.svelte-xyz) > button:where(.svelte-xyz) {
main.svelte-xyz div > button:where(.svelte-xyz) {
background-color: blue;
}

@ -0,0 +1,9 @@
<x>
<z></z>
</x>
<style>
x y z {
color: red;
}
</style>

@ -7,10 +7,10 @@
h2.svelte-xyz span:where(.svelte-xyz) {
color: red;
}
h2.svelte-xyz > span:where(.svelte-xyz) > b:where(.svelte-xyz) {
h2.svelte-xyz > span > b:where(.svelte-xyz) {
color: red;
}
h2.svelte-xyz span b:where(.svelte-xyz) {
h2.svelte-xyz span:where(.svelte-xyz) b:where(.svelte-xyz) {
color: red;
}
h2.svelte-xyz b:where(.svelte-xyz) {

@ -1,3 +1,3 @@
/* (empty) .foo.svelte-xyz {
/* (empty) .foo {
/* empty *\/
}*/

@ -1,4 +1,4 @@
.match.svelte-xyz > :where(.svelte-xyz) ~ :where(.svelte-xyz) {
.match.svelte-xyz > * ~ :where(.svelte-xyz) {
margin-left: 4px;
}
/* (unused) .not-match > * ~ * {

@ -2,6 +2,6 @@
<div></div>
</div>
<div class="match svelte-xyz">
<div></div>
<div class="svelte-xyz"></div>
<div class="svelte-xyz"></div>
</div>
</div>

@ -1,6 +1,6 @@
div.svelte-xyz ~ article:where(.svelte-xyz) { color: green; }
span.svelte-xyz ~ b:where(.svelte-xyz) { color: green; }
div.svelte-xyz span:where(.svelte-xyz) ~ b:where(.svelte-xyz) { color: green; }
div.svelte-xyz span ~ b:where(.svelte-xyz) { color: green; }
.a.svelte-xyz ~ article:where(.svelte-xyz) { color: green; }
div.svelte-xyz ~ .b:where(.svelte-xyz) { color: green; }
.a.svelte-xyz ~ .c:where(.svelte-xyz) { color: green; }

@ -1,12 +1,5 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
message: 'Unused CSS selector "a:global(.foo) > div"',
start: { character: 91, column: 1, line: 8 },
end: { character: 111, column: 21, line: 8 }
}
]
warnings: []
});

@ -1,3 +1,3 @@
div > div.svelte-xyz {
a > b > div.svelte-xyz {
color: red;
}

@ -1,3 +1,3 @@
<div class="svelte-xyz">
<div class="svelte-xyz"></div>
</div>
<div></div>
</div>

@ -1,9 +1,9 @@
<style>
:global(div) > div {
:global(a) > :global(b) > div {
color: red;
}
</style>
<div>
<div />
</div>
</div>

@ -1,3 +0,0 @@
<div class="svelte-xyz">
<div class="svelte-xyz"></div>
</div>

@ -1,9 +0,0 @@
<style>
:global(a) > :global(b) > div {
color: red;
}
</style>
<div>
<div />
</div>

@ -1,3 +1,3 @@
/* (unused) .foo .bar {
/* (unused) :global(.foo) .bar {
color: red;
}*/

@ -0,0 +1,4 @@
x.svelte-xyz :is(y:where(.svelte-xyz) /* (unused) z*/) {
color: purple;
}

@ -0,0 +1,9 @@
<x>
<y></y>
</x>
<style>
x :is(y, z) {
color: purple;
}
</style>

@ -0,0 +1,65 @@
.a.svelte-xyz {
color: green;
/* implicit & */
.b:where(.svelte-xyz) /* (unused) .unused*/ {
color: green;
.c:where(.svelte-xyz) {
color: green;
}
/* (unused) .unused {
color: red;
.c {
color: red;
}
}*/
}
/* (empty) .d {
.unused {
color: red;
}
}*/
/* explicit & */
& .b:where(.svelte-xyz) {
color: green;
/* (empty) .c {
& & {
color: red;
}
}*/
}
& & {
color: green;
}
/* silly but valid */
&& {
color: green;
}
.container:where(.svelte-xyz) & {
color: green;
}
/* (unused) &.b {
color: red;
}*/
/* (unused) .unused {
color: red;
}*/
}
blah {
.a.svelte-xyz {
color: green;
}
}

@ -0,0 +1,80 @@
<div class="a">
<div class="a"></div>
<div class="b">
<div class="c"></div>
</div>
<div class="d"></div>
</div>
<div class="container">
<div class="a"></div>
</div>
<style>
.a {
color: green;
/* implicit & */
.b, .unused {
color: green;
.c {
color: green;
}
.unused {
color: red;
.c {
color: red;
}
}
}
.d {
.unused {
color: red;
}
}
/* explicit & */
& .b {
color: green;
.c {
& & {
color: red;
}
}
}
& & {
color: green;
}
/* silly but valid */
&& {
color: green;
}
.container & {
color: green;
}
&.b {
color: red;
}
.unused {
color: red;
}
}
:global(blah) {
.a {
color: green;
}
}
</style>

@ -1,3 +1,3 @@
div.svelte-xyz section p:where(.svelte-xyz) {
div.svelte-xyz section:where(.svelte-xyz) p:where(.svelte-xyz) {
color: red;
}

@ -1 +1 @@
<div class="svelte-xyz"><section><p class="svelte-xyz">this is styled</p></section></div>
<div class="svelte-xyz"><section class="svelte-xyz"><p class="svelte-xyz">this is styled</p></section></div>

@ -1,4 +1,4 @@
a.svelte-xyz b c span:where(.svelte-xyz) {
a.svelte-xyz b:where(.svelte-xyz) c:where(.svelte-xyz) span:where(.svelte-xyz) {
color: red;
font-size: 2em;
font-family: 'Comic Sans MS';

@ -1,12 +1,12 @@
<a class="svelte-xyz">
<b>
<c>
<b class="svelte-xyz">
<c class="svelte-xyz">
<span class="svelte-xyz">
Big red Comic Sans
</span>
</span>
<span class="foo svelte-xyz">
Big red Comic Sans
</span>
</c>
</b>
</a>
</a>

@ -5,5 +5,5 @@
input + p.svelte-xyz { color: red; }
input ~ p.svelte-xyz { color: red; }
/* (unused) input + span { color: red; }*/
/* (unused) input ~ span { color: red; }*/
/* (unused) :global(input) + span { color: red; }*/
/* (unused) :global(input) ~ span { color: red; }*/

@ -1,4 +1,4 @@
.match.svelte-xyz > :where(.svelte-xyz) + :where(.svelte-xyz) {
.match.svelte-xyz > * + :where(.svelte-xyz) {
margin-left: 4px;
}
/* (unused) .not-match > * + * {

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save