Merge branch 'main' into check-generated-types

pull/11068/head
Rich Harris 2 years ago
commit 2201bf227a

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: use keys for hmr modules

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: further improvements to hmr component key generation

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: introduce `$host` rune, deprecate `createEventDispatcher`

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix missing classes after dynamic expressions in class attribute

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: execute sole static script tag

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: loosen proxy signal creation heuristics

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: make static `element` property available for the SvelteComponent type

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: simplify HMR implementation

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve internal proxied state signal heuristic

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: add validation around disallowed sequence expressions to element attributes

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: keep sibling selectors when dealing with slots/render tags/`svelte:element` tags

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure top level snippets are defined when binding to component prop

@ -0,0 +1,5 @@
---
"svelte": patch
---
breaking: robustify interop of exports and props in runes mode

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: use correct meta property for hmr key

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure deep mutation ownership widening

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: hot module reloading support for Svelte 5

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve compiled output of multiple call expression in single text node

@ -26,11 +26,13 @@
"blue-ants-raise",
"blue-rules-juggle",
"blue-timers-film",
"brave-gorillas-end",
"brave-points-sleep",
"brave-shrimps-kiss",
"brave-walls-destroy",
"brave-walls-flow",
"breezy-carrots-flash",
"breezy-rules-beg",
"bright-peas-juggle",
"bright-snakes-sing",
"brown-houses-obey",
@ -43,6 +45,7 @@
"chatty-taxis-juggle",
"chilled-pumas-invite",
"chilly-dolphins-lick",
"chilly-rocks-hug",
"chilly-snakes-scream",
"clean-eels-beg",
"clever-chefs-relate",
@ -50,10 +53,12 @@
"cold-birds-own",
"cold-masks-learn",
"cool-ants-leave",
"cool-comics-marry",
"cool-peas-lick",
"cool-rabbits-tickle",
"cool-roses-trade",
"cuddly-pianos-drop",
"cuddly-points-tickle",
"curly-lizards-dream",
"curvy-buses-laugh",
"curvy-cups-cough",
@ -74,6 +79,8 @@
"dull-pots-add",
"dull-roses-relate",
"early-ads-tie",
"early-months-whisper",
"eight-hornets-punch",
"eight-steaks-shout",
"eighty-bikes-camp",
"eighty-days-cheat",
@ -83,6 +90,7 @@
"empty-bulldogs-exercise",
"empty-crabs-think",
"empty-tools-share",
"fair-bags-smoke",
"fair-crabs-check",
"fair-pianos-talk",
"fair-spies-repeat",
@ -104,6 +112,8 @@
"forty-peaches-unite",
"forty-suns-smile",
"four-flies-hammer",
"four-mice-hammer",
"fresh-dots-destroy",
"fresh-impalas-bow",
"fresh-weeks-trade",
"friendly-candles-relate",
@ -116,7 +126,9 @@
"gentle-spies-happen",
"giant-moons-own",
"giant-planets-shake",
"giant-plants-grin",
"giant-roses-press",
"gold-tools-nail",
"good-buses-reply",
"good-cars-visit",
"good-pianos-jump",
@ -132,6 +144,7 @@
"happy-suits-film",
"healthy-planes-vanish",
"heavy-comics-move",
"heavy-ducks-leave",
"heavy-ears-rule",
"hip-balloons-begin",
"honest-buses-add",
@ -167,6 +180,7 @@
"lazy-spiders-think",
"lemon-geese-call",
"lemon-geese-drum",
"light-badgers-glow",
"light-days-clean",
"light-humans-hang",
"light-pens-watch",
@ -190,6 +204,7 @@
"metal-lobsters-burn",
"mighty-cooks-scream",
"mighty-files-hammer",
"mighty-frogs-obey",
"moody-carrots-lay",
"moody-frogs-exist",
"moody-houses-argue",
@ -218,6 +233,7 @@
"old-oranges-compete",
"olive-kangaroos-brake",
"olive-mice-fix",
"olive-moons-act",
"olive-seals-sell",
"olive-shirts-complain",
"olive-socks-kick",
@ -233,7 +249,9 @@
"poor-seahorses-flash",
"popular-ligers-perform",
"popular-mangos-rest",
"popular-walls-hunt",
"pretty-ties-help",
"proud-queens-sniff",
"purple-dragons-peel",
"quiet-apricots-dream",
"quiet-berries-end",
@ -250,6 +268,7 @@
"red-feet-worry",
"red-poets-study",
"rich-cobras-exist",
"rich-garlics-laugh",
"rich-olives-yell",
"rich-sheep-burn",
"rich-tables-sing",
@ -366,6 +385,7 @@
"three-icons-trade",
"three-lions-visit",
"three-papayas-buy",
"three-rice-tie",
"three-suits-grin",
"tidy-buses-whisper",
"tidy-starfishes-allow",
@ -399,6 +419,7 @@
"witty-steaks-dream",
"witty-tomatoes-care",
"witty-years-crash",
"yellow-taxis-double"
"yellow-taxis-double",
"young-masks-refuse"
]
}

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve hydration of svelte head blocks

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve handled of unowned derived signals

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: bundle CSS types

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: addresses reactive Set bug in certain engines

@ -11,5 +11,5 @@ trim_trailing_whitespace = true
[test/**/expected.css]
insert_final_newline = false
[{package.json,.travis.yml,.eslintrc.json}]
[package.json]
indent_style = space

@ -1,24 +0,0 @@
# NOTE: In general this should be kept in sync with .eslintignore
**/dist/**
**/config/**
**/build/**
**/playgrounds/sandbox/**
**/npm/**
**/*.js.flow
**/*.d.ts
**/playwright*/**
**/vite.config.js
**/vite.prod.config.js
**/node_modules
**/compiler/index.js
**/tests/**
# documentation can contain invalid examples
documentation/**
# contains a fork of the REPL which doesn't adhere to eslint rules
sites/svelte-5-preview/**
# Wasn't checked previously, reenable at some point
sites/svelte.dev/**

@ -1,54 +0,0 @@
module.exports = {
extends: ['@sveltejs'],
// TODO: add runes to eslint-plugin-svelte
globals: {
$state: true,
$derived: true,
$effect: true,
$props: true
},
overrides: [
{
// scripts and playground should be console logging so don't lint against them
files: ['playgrounds/**/*', 'scripts/**/*'],
rules: {
'no-console': 'off'
}
},
{
// the playgrounds can use public naming conventions since they're examples
files: ['playgrounds/**/*'],
rules: {
'lube/svelte-naming-convention': 'off'
}
},
{
files: ['packages/svelte/src/compiler/**/*'],
rules: {
'no-var': 'error'
}
}
],
plugins: ['lube'],
rules: {
'no-console': 'error',
'lube/svelte-naming-convention': ['error', { fixSameNames: true }],
// eslint isn't that well-versed with JSDoc to know that `foo: /** @type{..} */ (foo)` isn't a violation of this rule, so turn it off
'object-shorthand': 'off',
'no-var': 'off',
// TODO: enable these rules and run `pnpm lint:fix`
// skipping that for now so as to avoid impacting real work
'@typescript-eslint/array-type': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'prefer-const': 'off',
'svelte/valid-compile': 'off',
quotes: 'off'
}
};

@ -1,5 +1,3 @@
# NOTE: In general this should be kept in sync with .eslintignore
packages/**/dist/*.js
packages/**/build/*.js
packages/**/npm/**/*
@ -31,7 +29,6 @@ sites/svelte.dev/src/lib/generated
**/.vercel
.github/CODEOWNERS
.prettierignore
.eslintignore
.changeset
pnpm-lock.yaml
pnpm-workspace.yaml

@ -0,0 +1,45 @@
import svelte_config from '@sveltejs/eslint-config';
import lube from 'eslint-plugin-lube';
/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
...svelte_config,
{
plugins: {
lube
},
rules: {
'no-console': 'error',
'lube/svelte-naming-convention': ['error', { fixSameNames: true }],
// eslint isn't that well-versed with JSDoc to know that `foo: /** @type{..} */ (foo)` isn't a violation of this rule, so turn it off
'object-shorthand': 'off',
'no-var': 'off',
// TODO: enable these rules and run `pnpm lint:fix`
// skipping that for now so as to avoid impacting real work
'@stylistic/quotes': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'prefer-const': 'off'
}
},
{
files: ['playgrounds/**/*'],
rules: {
'lube/svelte-naming-convention': 'off',
'no-console': 'off'
}
},
{
ignores: [
'**/*.d.ts',
'**/tests',
'packages/svelte/compiler/index.js',
// documentation can contain invalid examples
'documentation',
// contains a fork of the REPL which doesn't adhere to eslint rules
'sites/svelte-5-preview/**',
// wasn't checked previously, reenable at some point
'sites/svelte.dev/**'
]
}
];

@ -19,28 +19,28 @@
"preview-site": "npm run build --prefix sites/svelte-5-preview",
"check": "cd packages/svelte && pnpm build && cd ../../ && pnpm -r check",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ./",
"test": "vitest run --coverage",
"test-output": "vitest run --reporter=json --outputFile=sites/svelte-5-preview/src/routes/status/results.json",
"lint": "prettier --check . && eslint",
"test": "vitest run",
"test-output": "vitest run --coverage --reporter=json --outputFile=sites/svelte-5-preview/src/routes/status/results.json",
"changeset:version": "changeset version && pnpm -r generate:version && git add --all",
"changeset:publish": "changeset publish"
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
"@sveltejs/eslint-config": "^6.0.4",
"@sveltejs/eslint-config": "^7.0.1",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@vitest/coverage-v8": "^1.2.1",
"concurrently": "^8.2.2",
"cross-env": "^7.0.3",
"eslint": "^8.56.0",
"eslint": "^9.0.0",
"eslint-plugin-lube": "^0.4.3",
"jsdom": "22.0.0",
"playwright": "^1.41.1",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2",
"typescript": "^5.3.3",
"typescript-eslint": "^7.6.0",
"vitest": "^1.2.1"
},
"pnpm": {

@ -1,5 +1,75 @@
# svelte
## 5.0.0-next.101
### Patch Changes
- fix missing classes after dynamic expressions in class attribute ([#11134](https://github.com/sveltejs/svelte/pull/11134))
- feat: simplify HMR implementation ([#11132](https://github.com/sveltejs/svelte/pull/11132))
- fix: add validation around disallowed sequence expressions to element attributes ([#11149](https://github.com/sveltejs/svelte/pull/11149))
## 5.0.0-next.100
### Patch Changes
- fix: further improvements to hmr component key generation ([#11129](https://github.com/sveltejs/svelte/pull/11129))
## 5.0.0-next.99
### Patch Changes
- fix: use correct meta property for hmr key ([#11125](https://github.com/sveltejs/svelte/pull/11125))
## 5.0.0-next.98
### Patch Changes
- fix: use keys for hmr modules ([#11123](https://github.com/sveltejs/svelte/pull/11123))
- fix: addresses reactive Set bug in certain engines ([#11120](https://github.com/sveltejs/svelte/pull/11120))
## 5.0.0-next.97
### Patch Changes
- fix: loosen proxy signal creation heuristics ([#11109](https://github.com/sveltejs/svelte/pull/11109))
- fix: ensure top level snippets are defined when binding to component prop ([#11104](https://github.com/sveltejs/svelte/pull/11104))
- feat: hot module reloading support for Svelte 5 ([#11106](https://github.com/sveltejs/svelte/pull/11106))
## 5.0.0-next.96
### Patch Changes
- feat: introduce `$host` rune, deprecate `createEventDispatcher` ([#11059](https://github.com/sveltejs/svelte/pull/11059))
- fix: execute sole static script tag ([#11095](https://github.com/sveltejs/svelte/pull/11095))
- fix: make static `element` property available for the SvelteComponent type ([#11079](https://github.com/sveltejs/svelte/pull/11079))
- fix: improve internal proxied state signal heuristic ([#11102](https://github.com/sveltejs/svelte/pull/11102))
- fix: keep sibling selectors when dealing with slots/render tags/`svelte:element` tags ([#11096](https://github.com/sveltejs/svelte/pull/11096))
- fix: ensure deep mutation ownership widening ([#11094](https://github.com/sveltejs/svelte/pull/11094))
- fix: improve compiled output of multiple call expression in single text node ([#11097](https://github.com/sveltejs/svelte/pull/11097))
- fix: improve hydration of svelte head blocks ([#11099](https://github.com/sveltejs/svelte/pull/11099))
## 5.0.0-next.95
### Patch Changes
- breaking: robustify interop of exports and props in runes mode ([#11064](https://github.com/sveltejs/svelte/pull/11064))
- fix: improve handled of unowned derived signals ([#11077](https://github.com/sveltejs/svelte/pull/11077))
- fix: bundle CSS types ([#11067](https://github.com/sveltejs/svelte/pull/11067))
## 5.0.0-next.94
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.94",
"version": "5.0.0-next.101",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -116,7 +116,7 @@
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-virtual": "^3.0.2",
"@types/aria-query": "^5.0.4",
"dts-buddy": "^0.4.4",
"dts-buddy": "^0.4.6",
"esbuild": "^0.19.11",
"knip": "^4.2.1",
"rollup": "^4.9.5",

@ -211,3 +211,24 @@ declare function $bindable<T>(t?: T): T;
declare function $inspect<T extends any[]>(
...values: T
): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void };
/**
* Retrieves the `this` reference of the custom element that contains this component. Example:
*
* ```svelte
* <svelte:options customElement="my-element" />
*
* <script>
* function greet(greeting) {
* $host().dispatchEvent(new CustomEvent('greeting', { detail: greeting }))
* }
* </script>
*
* <button onclick={() => greet('hello')}>say hello</button>
* ```
*
* Only available inside custom element components, and only on the client-side.
*
* https://svelte-5-preview.vercel.app/docs/runes#$host
*/
declare function $host<El extends HTMLElement = HTMLElement>(): El;

@ -187,6 +187,8 @@ const runes = {
'invalid-state-location': (rune) =>
`${rune}(...) can only be used as a variable declaration initializer or a class field`,
'invalid-effect-location': () => `$effect() can only be used as an expression statement`,
'invalid-host-location': () =>
`$host() can only be used inside custom element component instances`,
/**
* @param {boolean} is_binding
* @param {boolean} show_details
@ -212,7 +214,9 @@ const runes = {
'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')`,
'invalid-derived-call': () => `$derived.call(...) has been replaced with $derived.by(...)`
'invalid-derived-call': () => `$derived.call(...) has been replaced with $derived.by(...)`,
'conflicting-property-name': () =>
`Cannot have a property and a component export with the same name`
};
/** @satisfies {Errors} */

@ -36,7 +36,8 @@ export default function read_style(parser, start, attributes) {
content: {
start: content_start,
end: content_end,
styles: parser.template.slice(content_start, content_end)
styles: parser.template.slice(content_start, content_end),
comment: null
}
};
}

@ -283,25 +283,26 @@ export default function tag(parser) {
if (is_top_level_script_or_style) {
parser.eat('>', true);
if (name === 'script') {
const content = read_script(parser, start, element.attributes);
/** @type {import('#compiler').Comment | null} */
let prev_comment = null;
for (let i = current.fragment.nodes.length - 1; i >= 0; i--) {
const node = current.fragment.nodes[i];
/** @type {import('#compiler').Comment | null} */
let prev_comment = null;
for (let i = current.fragment.nodes.length - 1; i >= 0; i--) {
const node = current.fragment.nodes[i];
if (i === current.fragment.nodes.length - 1 && node.end !== start) {
break;
}
if (i === current.fragment.nodes.length - 1 && node.end !== start) {
break;
}
if (node.type === 'Comment') {
prev_comment = node;
break;
} else if (node.type !== 'Text' || node.data.trim()) {
break;
}
if (node.type === 'Comment') {
prev_comment = node;
break;
} else if (node.type !== 'Text' || node.data.trim()) {
break;
}
}
if (name === 'script') {
const content = read_script(parser, start, element.attributes);
if (prev_comment) {
// We take advantage of the fact that the root will never have leadingComments set,
// and set the previous comment to it so that the warning mechanism can later
@ -318,6 +319,7 @@ export default function tag(parser) {
}
} else {
const content = read_style(parser, start, element.attributes);
content.content.comment = prev_comment;
if (current.css) error(start, 'duplicate-style-element');
current.css = content;

@ -175,7 +175,13 @@ function apply_selector(relative_selectors, rule, element, stylesheet) {
let sibling_matched = false;
for (const possible_sibling of siblings.keys()) {
if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) {
if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') {
// `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) {
mark(relative_selector, element);
sibling_matched = true;
}
} else if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) {
mark(relative_selector, element);
sibling_matched = true;
}
@ -564,38 +570,39 @@ function get_element_parent(node) {
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;
while (
// @ts-expect-error TODO
!current_node.prev &&
// @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?.type === 'SlotElement') {
const slot_children = current_node.fragment.nodes;
if (slot_children.length > 0) {
current_node = slot_children.slice(-1)[0];
} else {
break;
}
// @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>}
* @returns {Map<import('#compiler').RegularElement | import('#compiler').SvelteElement | import('#compiler').SlotElement | import('#compiler').RenderTag, NodeExistsValue>}
*/
function get_possible_element_siblings(node, adjacent_only) {
/** @type {Map<import('#compiler').RegularElement, NodeExistsValue>} */
/** @type {Map<import('#compiler').RegularElement | import('#compiler').SvelteElement | import('#compiler').SlotElement | import('#compiler').RenderTag, NodeExistsValue>} */
const result = new Map();
/** @type {import('#compiler').SvelteNode} */
@ -618,6 +625,14 @@ function get_possible_element_siblings(node, adjacent_only) {
if (adjacent_only && has_definite_elements(possible_last_child)) {
return result;
}
} else if (
prev.type === 'SlotElement' ||
prev.type === 'RenderTag' ||
prev.type === 'SvelteElement'
) {
result.set(prev, NODE_PROBABLY_EXISTS);
// Special case: slots, render tags and svelte:element tags could resolve to no siblings,
// so we want to continue until we find a definite sibling even with the adjacent-only combinator
}
}
@ -720,7 +735,7 @@ function get_possible_last_child(relative_selector, adjacent_only) {
}
/**
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} result
* @param {Map<unknown, NodeExistsValue>} result
* @returns {boolean}
*/
function has_definite_elements(result) {
@ -734,8 +749,9 @@ function has_definite_elements(result) {
}
/**
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} from
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} to
* @template T
* @param {Map<T, NodeExistsValue>} from
* @param {Map<T, NodeExistsValue>} to
* @returns {void}
*/
function add_to_map(from, to) {

@ -0,0 +1,34 @@
import { walk } from 'zimmerframe';
import { warn } from '../../../warnings.js';
import { is_keyframes_node } from '../../css.js';
/**
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @param {import('../../types.js').RawWarning[]} warnings
*/
export function warn_unused(stylesheet, warnings) {
walk(stylesheet, { warnings, stylesheet }, visitors);
}
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, { warnings: import('../../types.js').RawWarning[], stylesheet: import('#compiler').Css.StyleSheet }>} */
const visitors = {
Atrule(node, context) {
if (!is_keyframes_node(node)) {
context.next();
}
},
PseudoClassSelector(node, context) {
if (node.name === 'is' || node.name === 'where') {
context.next();
}
},
ComplexSelector(node, context) {
if (!node.metadata.used) {
const content = context.state.stylesheet.content;
const text = content.styles.substring(node.start - content.start, node.end - content.start);
warn(context.state.warnings, node, context.path, 'css-unused-selector', text);
}
context.next();
}
};

@ -23,6 +23,7 @@ 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';
import { warn_unused } from './css/css-warn.js';
/**
* @param {import('#compiler').Script | null} script
@ -437,6 +438,20 @@ export function analyze_component(root, source, options) {
merge(set_scope(scopes), validation_runes, runes_scope_tweaker, common_visitors)
);
}
if (analysis.exports.length > 0) {
for (const [_, binding] of instance.scope.declarations) {
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
if (
analysis.exports.some(
({ alias, name }) => (binding.prop_alias ?? binding.node.name) === (alias ?? name)
)
) {
error(binding.node, 'conflicting-property-name');
}
}
}
}
} else {
instance.scope.declare(b.id('$$props'), 'bindable_prop', 'synthetic');
instance.scope.declare(b.id('$$restProps'), 'rest_prop', 'synthetic');
@ -534,6 +549,7 @@ export function analyze_component(root, source, options) {
for (const element of analysis.elements) {
prune(analysis.css.ast, element);
}
warn_unused(analysis.css.ast, analysis.warnings);
outer: for (const element of analysis.elements) {
if (element.metadata.scoped) {

@ -98,7 +98,12 @@ function validate_element(node, context) {
if (context.state.analysis.runes && is_expression) {
const expression = attribute.value[0].expression;
if (expression.type === 'SequenceExpression') {
error(expression, 'invalid-sequence-expression');
let i = /** @type {number} */ (expression.start);
while (--i > 0) {
const char = context.state.analysis.source[i];
if (char === '(') break; // parenthesized sequence expressions are ok
if (char === '{') error(expression, 'invalid-sequence-expression');
}
}
}
@ -896,6 +901,9 @@ export const validation_runes_js = {
}
},
CallExpression(node, { state, path }) {
if (get_rune(node, state.scope) === '$host') {
error(node, 'invalid-host-location');
}
validate_call_expression(node, state.scope, path);
},
VariableDeclarator(node, { state }) {
@ -1063,9 +1071,17 @@ export const validation_runes = merge(validation, a11y_validators, {
}
},
CallExpression(node, { state, path }) {
if (get_rune(node, state.scope) === '$bindable' && node.arguments.length > 1) {
const rune = get_rune(node, state.scope);
if (rune === '$bindable' && node.arguments.length > 1) {
error(node, 'invalid-rune-args-length', '$bindable', [0, 1]);
} else if (rune === '$host') {
if (node.arguments.length > 0) {
error(node, 'invalid-rune-args-length', '$host', [0]);
} else if (state.ast_type === 'module' || !state.analysis.custom_element) {
error(node, 'invalid-host-location');
}
}
validate_call_expression(node, state.scope, path);
},
EachBlock(node, { next, state }) {

@ -255,8 +255,7 @@ export function client_component(source, analysis, options) {
);
if (analysis.runes && options.dev) {
/** @type {import('estree').Literal[]} */
const bindable = [];
const bindable = analysis.exports.map(({ name, alias }) => b.literal(alias ?? name));
for (const [name, binding] of properties) {
if (binding.kind === 'bindable_prop') {
bindable.push(b.literal(binding.prop_alias ?? name));
@ -382,7 +381,6 @@ export function client_component(source, analysis, options) {
);
if (analysis.uses_rest_props) {
/** @type {string[]} */
const named_props = analysis.exports.map(({ name, alias }) => alias ?? name);
for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind === 'bindable_prop') named_props.push(binding.prop_alias ?? name);
@ -401,15 +399,12 @@ export function client_component(source, analysis, options) {
}
if (analysis.uses_props || analysis.uses_rest_props) {
const to_remove = [b.literal('children'), b.literal('$$slots'), b.literal('$$events')];
if (analysis.custom_element) {
to_remove.push(b.literal('$$host'));
}
component_block.body.unshift(
b.const(
'$$sanitized_props',
b.call(
'$.rest_props',
b.id('$$props'),
b.array([b.literal('children'), b.literal('$$slots'), b.literal('$$events')])
)
)
b.const('$$sanitized_props', b.call('$.rest_props', b.id('$$props'), b.array(to_remove)))
);
}
@ -420,15 +415,38 @@ export function client_component(source, analysis, options) {
const body = [
...state.hoisted,
...module.body,
b.export_default(
b.function_declaration(
b.id(analysis.name),
[b.id('$$anchor'), b.id('$$props')],
component_block
)
b.function_declaration(
b.id(analysis.name),
[b.id('$$anchor'), b.id('$$props')],
component_block
)
];
if (options.hmr) {
body.push(
b.if(
b.id('import.meta.hot'),
b.block([
b.const(b.id('s'), b.call('$.source', b.id(analysis.name))),
b.stmt(b.assignment('=', b.id(analysis.name), b.call('$.hmr', b.id('s')))),
b.stmt(
b.call(
'import.meta.hot.accept',
b.arrow(
[b.id('module')],
b.block([
b.stmt(b.call('$.set', b.id('s'), b.member(b.id('module'), b.id('default'))))
])
)
)
)
])
)
);
}
body.push(b.export_default(b.id(analysis.name)));
if (options.dev) {
if (options.filename) {
let filename = options.filename;

@ -195,8 +195,7 @@ export const javascript_visitors_runes = {
if (rune === '$props') {
assert.equal(declarator.id.type, 'ObjectPattern');
/** @type {string[]} */
const seen = [];
const seen = state.analysis.exports.map(({ name, alias }) => alias ?? name);
for (const property of declarator.id.properties) {
if (property.type === 'Property') {
@ -381,6 +380,10 @@ export const javascript_visitors_runes = {
CallExpression(node, context) {
const rune = get_rune(node, context.state.scope);
if (rune === '$host') {
return b.id('$$props.$$host');
}
if (rune === '$effect.active') {
return b.call('$.effect_active');
}

@ -1396,7 +1396,7 @@ function process_children(nodes, expression, is_element, { visit, state }) {
state.template.push(' ');
const [contains_call_expression, value] = serialize_template_literal(sequence, visit);
const [contains_call_expression, value] = serialize_template_literal(sequence, visit, state);
const update = b.stmt(b.call('$.set_text', text_id, value));
@ -1511,25 +1511,39 @@ function serialize_attribute_value(attribute_value, context) {
}
}
return serialize_template_literal(attribute_value, context.visit);
return serialize_template_literal(attribute_value, context.visit, context.state);
}
/**
* @param {Array<import('#compiler').Text | import('#compiler').ExpressionTag>} values
* @param {(node: import('#compiler').SvelteNode) => any} visit
* @param {import("../types.js").ComponentClientTransformState} state
* @returns {[boolean, import('estree').TemplateLiteral]}
*/
function serialize_template_literal(values, visit) {
function serialize_template_literal(values, visit, state) {
/** @type {import('estree').TemplateElement[]} */
const quasis = [];
/** @type {import('estree').Expression[]} */
const expressions = [];
let contains_call_expression = false;
let contains_multiple_call_expression = false;
quasis.push(b.quasi(''));
for (let i = 0; i < values.length; i++) {
const node = values[i];
if (node.type === 'ExpressionTag' && node.metadata.contains_call_expression) {
if (contains_call_expression) {
contains_multiple_call_expression = true;
}
contains_call_expression = true;
}
}
for (let i = 0; i < values.length; i++) {
const node = values[i];
if (node.type === 'Text') {
const last = /** @type {import('estree').TemplateElement} */ (quasis.at(-1));
last.value.raw += sanitize_template_string(node.data);
@ -1539,11 +1553,23 @@ function serialize_template_literal(values, visit) {
last.value.raw += sanitize_template_string(node.expression.value + '');
}
} else {
if (node.type === 'ExpressionTag' && node.metadata.contains_call_expression) {
contains_call_expression = true;
}
if (contains_multiple_call_expression) {
const id = b.id(state.scope.generate('stringified_text'));
expressions.push(b.call('$.stringify', visit(node.expression)));
state.init.push(
b.const(
id,
b.call(
// In runes mode, we want things to be fine-grained - but not in legacy mode
state.analysis.runes ? '$.derived' : '$.derived_safe_equal',
b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression)))
)
)
);
expressions.push(b.call('$.get', id));
} else {
expressions.push(b.call('$.stringify', visit(node.expression)));
}
quasis.push(b.quasi('', i + 1 === values.length));
}
}
@ -1586,7 +1612,7 @@ export const template_visitors = {
declaration.id,
b.call(
// In runes mode, we want things to be fine-grained - but not in legacy mode
state.options.runes ? '$.derived' : '$.derived_safe_equal',
state.analysis.runes ? '$.derived' : '$.derived_safe_equal',
b.thunk(/** @type {import('estree').Expression} */ (visit(declaration.init)))
)
)
@ -1623,7 +1649,7 @@ export const template_visitors = {
state.init.push(
// In runes mode, we want things to be fine-grained - but not in legacy mode
b.const(tmp, b.call(state.options.runes ? '$.derived' : '$.derived_safe_equal', fn))
b.const(tmp, b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', fn))
);
// we need to eagerly evaluate the expression in order to hit any
@ -2289,7 +2315,7 @@ export const template_visitors = {
)
: /** @type {import('estree').Expression} */ (context.visit(node.key))
)
: b.literal(null);
: b.id('$.index');
if (node.index && each_node_meta.contains_group_binding) {
// We needed to create a unique identifier for the index above, but we want to use the
@ -2297,36 +2323,21 @@ export const template_visitors = {
declarations.push(b.let(node.index, index));
}
let callee = '$.each_indexed';
/** @type {import('estree').Expression[]} */
const args = [];
if ((each_type & EACH_KEYED) !== 0) {
if (context.state.options.dev && key_function.type !== 'Literal') {
context.state.init.push(
b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function))
);
}
callee = '$.each_keyed';
args.push(
context.state.node,
b.literal(each_type),
each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
key_function,
b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(children)))
);
} else {
args.push(
context.state.node,
b.literal(each_type),
each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(children)))
if (context.state.options.dev && (each_type & EACH_KEYED) !== 0) {
context.state.init.push(
b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function))
);
}
/** @type {import('estree').Expression[]} */
const args = [
context.state.node,
b.literal(each_type),
each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
key_function,
b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(children)))
];
if (node.fallback) {
args.push(
b.arrow(
@ -2336,7 +2347,7 @@ export const template_visitors = {
);
}
context.state.init.push(b.stmt(b.call(callee, ...args)));
context.state.init.push(b.stmt(b.call('$.each', ...args)));
},
IfBlock(node, context) {
context.state.template.push('<!>');
@ -2972,7 +2983,7 @@ export const template_visitors = {
b.assignment(
'=',
b.member(b.id('$.document'), b.id('title')),
serialize_template_literal(/** @type {any} */ (node.fragment.nodes), visit)[1]
serialize_template_literal(/** @type {any} */ (node.fragment.nodes), visit, state)[1]
)
)
);

@ -691,7 +691,8 @@ const javascript_visitors_runes = {
}
if (rune === '$props') {
// remove $bindable() from props declaration
// remove $bindable() from props declaration and handle rest props
let uses_rest_props = false;
const id = walk(declarator.id, null, {
AssignmentPattern(node) {
if (
@ -703,9 +704,26 @@ const javascript_visitors_runes = {
: b.id('undefined');
return b.assignment_pattern(node.left, right);
}
},
RestElement(node, { path }) {
if (path.at(-1) === declarator.id) {
uses_rest_props = true;
}
}
});
declarations.push(b.declarator(id, b.id('$$props')));
const exports = /** @type {import('../../types').ComponentAnalysis} */ (
state.analysis
).exports.map(({ name, alias }) => b.literal(alias ?? name));
declarations.push(
b.declarator(
id,
uses_rest_props && exports.length > 0
? b.call('$.rest_props', b.id('$$props'), b.array(exports))
: b.id('$$props')
)
);
continue;
}
@ -767,6 +785,10 @@ const javascript_visitors_runes = {
CallExpression(node, context) {
const rune = get_rune(node, context.state.scope);
if (rune === '$host') {
return b.id('undefined');
}
if (rune === '$effect.active') {
return b.literal(false);
}
@ -831,9 +853,7 @@ function serialize_attribute_value(
/** @type {import('estree').Expression[]} */
const expressions = [];
if (attribute_value[0].type !== 'Text') {
quasis.push(b.quasi('', false));
}
quasis.push(b.quasi('', false));
let i = 0;
for (const node of attribute_value) {
@ -844,7 +864,8 @@ function serialize_attribute_value(
// don't trim, space could be important to separate from expression tag
data = data.replace(regex_whitespaces_strict, ' ');
}
quasis.push(b.quasi(data, i === attribute_value.length));
const last = /** @type {import('estree').TemplateElement} */ (quasis.at(-1));
last.value.raw += data;
} else {
expressions.push(
b.call(
@ -852,9 +873,7 @@ function serialize_attribute_value(
/** @type {import('estree').Expression} */ (context.visit(node.expression))
)
);
if (i === attribute_value.length || attribute_value[i]?.type !== 'Text') {
quasis.push(b.quasi('', true));
}
quasis.push(b.quasi('', i + 1 === attribute_value.length));
}
}
@ -1592,14 +1611,15 @@ const template_visitors = {
state.template.push(block_close);
},
SnippetBlock(node, context) {
// TODO hoist where possible
context.state.init.push(
b.function_declaration(
node.expression,
[b.id('$$payload'), ...node.parameters],
/** @type {import('estree').BlockStatement} */ (context.visit(node.body))
)
const fn = b.function_declaration(
node.expression,
[b.id('$$payload'), ...node.parameters],
/** @type {import('estree').BlockStatement} */ (context.visit(node.body))
);
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true;
// TODO hoist where possible
context.state.init.push(fn);
if (context.state.options.dev) {
context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression)));
@ -2199,14 +2219,27 @@ export function server_component(analysis, options) {
// If the component binds to a child, we need to put the template in a loop and repeat until legacy bindings are stable.
// We can remove this once the legacy syntax is gone.
if (analysis.uses_component_bindings) {
const snippets = template.body.filter(
(node) =>
node.type === 'FunctionDeclaration' &&
// @ts-expect-error
node.___snippet
);
const rest = template.body.filter(
(node) =>
node.type !== 'FunctionDeclaration' ||
// @ts-expect-error
!node.___snippet
);
template.body = [
...snippets,
b.let('$$settled', b.true),
b.let('$$inner_payload'),
b.stmt(
b.function(
b.id('$$render_inner'),
[b.id('$$payload')],
b.block(/** @type {import('estree').Statement[]} */ (template.body))
b.block(/** @type {import('estree').Statement[]} */ (rest))
)
),
b.do_while(

@ -40,7 +40,8 @@ export const Runes = /** @type {const} */ ([
'$effect.active',
'$effect.root',
'$inspect',
'$inspect().with'
'$inspect().with',
'$host'
]);
/**

@ -1,147 +1,153 @@
export interface BaseNode {
start: number;
end: number;
}
import type { Comment } from '#compiler';
export interface StyleSheet extends BaseNode {
type: 'StyleSheet';
attributes: any[]; // TODO
children: Array<Atrule | Rule>;
content: {
export namespace Css {
export interface BaseNode {
start: number;
end: number;
styles: string;
};
}
export interface Atrule extends BaseNode {
type: 'Atrule';
name: string;
prelude: string;
block: Block | null;
}
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: ComplexSelector[];
}
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 {
type: 'TypeSelector';
name: string;
}
export interface IdSelector extends BaseNode {
type: 'IdSelector';
name: string;
}
export interface ClassSelector extends BaseNode {
type: 'ClassSelector';
name: string;
}
export interface AttributeSelector extends BaseNode {
type: 'AttributeSelector';
name: string;
matcher: string | null;
value: string | null;
flags: string | null;
}
export interface PseudoElementSelector extends BaseNode {
type: 'PseudoElementSelector';
name: string;
}
export interface PseudoClassSelector extends BaseNode {
type: 'PseudoClassSelector';
name: string;
args: SelectorList | null;
}
export interface Percentage extends BaseNode {
type: 'Percentage';
value: string;
}
export interface NestingSelector extends BaseNode {
type: 'NestingSelector';
name: '&';
}
export interface StyleSheet extends BaseNode {
type: 'StyleSheet';
attributes: any[]; // TODO
children: Array<Atrule | Rule>;
content: {
start: number;
end: number;
styles: string;
/** Possible comment atop the style tag */
comment: Comment | null;
};
}
export interface Atrule extends BaseNode {
type: 'Atrule';
name: string;
prelude: string;
block: Block | null;
}
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: ComplexSelector[];
}
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 {
type: 'TypeSelector';
name: string;
}
export interface IdSelector extends BaseNode {
type: 'IdSelector';
name: string;
}
export interface ClassSelector extends BaseNode {
type: 'ClassSelector';
name: string;
}
export interface AttributeSelector extends BaseNode {
type: 'AttributeSelector';
name: string;
matcher: string | null;
value: string | null;
flags: string | null;
}
export interface PseudoElementSelector extends BaseNode {
type: 'PseudoElementSelector';
name: string;
}
export interface PseudoClassSelector extends BaseNode {
type: 'PseudoClassSelector';
name: string;
args: SelectorList | null;
}
export interface Percentage extends BaseNode {
type: 'Percentage';
value: string;
}
export interface NestingSelector extends BaseNode {
type: 'NestingSelector';
name: '&';
}
export interface Nth extends BaseNode {
type: 'Nth';
value: string;
}
export type SimpleSelector =
| TypeSelector
| IdSelector
| ClassSelector
| AttributeSelector
| PseudoElementSelector
| PseudoClassSelector
| Percentage
| Nth
| NestingSelector;
export interface Combinator extends BaseNode {
type: 'Combinator';
name: string;
}
export interface Block extends BaseNode {
type: 'Block';
children: Array<Declaration | Rule | Atrule>;
}
export interface Declaration extends BaseNode {
type: 'Declaration';
property: string;
value: string;
}
// for zimmerframe
export type Node =
| StyleSheet
| Rule
| Atrule
| SelectorList
| Block
| ComplexSelector
| RelativeSelector
| Combinator
| SimpleSelector
| Declaration;
}
export interface Nth extends BaseNode {
type: 'Nth';
value: string;
}
export type SimpleSelector =
| TypeSelector
| IdSelector
| ClassSelector
| AttributeSelector
| PseudoElementSelector
| PseudoClassSelector
| Percentage
| Nth
| NestingSelector;
export interface Combinator extends BaseNode {
type: 'Combinator';
name: string;
}
export interface Block extends BaseNode {
type: 'Block';
children: Array<Declaration | Rule | Atrule>;
}
export interface Declaration extends BaseNode {
type: 'Declaration';
property: string;
value: string;
}
// for zimmerframe
export type Node =
| StyleSheet
| Rule
| Atrule
| SelectorList
| Block
| ComplexSelector
| RelativeSelector
| Combinator
| SimpleSelector
| Declaration;

@ -10,7 +10,7 @@ import type { Location } from 'locate-character';
import type { SourceMap } from 'magic-string';
import type { Context } from 'zimmerframe';
import type { Scope } from '../phases/scope.js';
import * as Css from './css.js';
import type { Css } from './css.js';
import type { EachBlock, Namespace, SvelteNode, SvelteOptions } from './template.js';
/** The return value of `compile` from `svelte/compiler` */
@ -178,6 +178,12 @@ export interface CompileOptions extends ModuleCompileOptions {
* @default null
*/
cssOutputFilename?: string;
/**
* If `true`, compiles components with hot reloading support.
*
* @default false
*/
hmr?: boolean;
}
export interface ModuleCompileOptions {
@ -225,6 +231,7 @@ export type ValidatedCompileOptions = ValidatedModuleCompileOptions &
legacy: Required<Required<CompileOptions>['legacy']>;
runes: CompileOptions['runes'];
customElementOptions: SvelteOptions['customElement'];
hmr: CompileOptions['hmr'];
};
export type DeclarationKind =

@ -95,6 +95,8 @@ export const validate_component_options =
runes: boolean(undefined),
hmr: boolean(false),
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

@ -7,7 +7,8 @@ import {
/** @satisfies {Warnings} */
const css = {
'unused-selector': () => 'Unused CSS selector'
/** @param {string} name */
'css-unused-selector': (name) => `Unused CSS selector "${name}"`
};
/** @satisfies {Warnings} */
@ -300,6 +301,11 @@ export function warn(array, node, path, code, ...args) {
)
);
}
// Style nodes
if (current.type === 'StyleSheet' && current.content.comment) {
ignores.push(...current.content.comment.ignores);
}
}
if (ignores.includes(code)) return;

@ -80,6 +80,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false
* ```
*
* https://svelte.dev/docs/svelte#createeventdispatcher
* @deprecated Use callback props and/or the `$host()` rune instead see https://svelte-5-preview.vercel.app/docs/deprecations#createeventdispatcher
* @template {Record<string, any>} [EventMap = any]
* @returns {import('./index.js').EventDispatcher<EventMap>}
*/

@ -59,6 +59,9 @@ export class SvelteComponent<
Events extends Record<string, any> = any,
Slots extends Record<string, any> = any
> {
/** The custom element version of the component. Only present if compiled with the `customElement` compiler option */
static element?: typeof HTMLElement;
[prop: string]: any;
/**
* @deprecated This constructor only exists when using the `asClassComponent` compatibility helper, which

@ -0,0 +1,38 @@
import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { set_should_intro } from '../render.js';
import { get } from '../runtime.js';
/**
* @template {(anchor: Comment, props: any) => any} Component
* @param {import("#client").Source<Component>} source
*/
export function hmr(source) {
/**
* @param {Comment} anchor
* @param {any} props
*/
return (anchor, props) => {
let instance = {};
/** @type {import("#client").Effect} */
let effect;
block(() => {
const component = get(source);
if (effect) {
// @ts-ignore
for (var k in instance) delete instance[k];
destroy_effect(effect);
}
effect = branch(() => {
set_should_intro(false);
Object.assign(instance, component(anchor, props));
set_should_intro(true);
});
});
return instance;
};
}

@ -2,6 +2,7 @@
import { STATE_SYMBOL } from '../constants.js';
import { untrack } from '../runtime.js';
import { get_descriptors } from '../utils.js';
/** @type {Record<string, Array<{ start: Location, end: Location, component: Function }>>} */
const boundaries = {};
@ -91,49 +92,107 @@ export function mark_module_end() {
}
}
let add_owner_visited = new Set();
/**
*
* @param {any} object
* @param {any} owner
*/
export function add_owner(object, owner) {
untrack(() => {
add_owner_to_object(object, owner);
});
// Needed because ownership addition can invoke getters on a proxy,
// calling add_owner anew, so just keeping the set as part of
// add_owner_to_object would not be enough.
const prev = add_owner_visited;
try {
add_owner_visited = new Set(add_owner_visited);
untrack(() => {
add_owner_to_object(object, owner, add_owner_visited);
});
} finally {
add_owner_visited = prev;
}
}
/**
* @param {any} object
* @param {Function} owner
* @param {Set<any>} visited
*/
function add_owner_to_object(object, owner) {
function add_owner_to_object(object, owner, visited) {
if (visited.has(object)) return;
visited.add(object);
if (object?.[STATE_SYMBOL]?.o && !object[STATE_SYMBOL].o.has(owner)) {
object[STATE_SYMBOL].o.add(owner);
for (const key in object) {
add_owner_to_object(object[key], owner);
}
}
// Not inside previous if-block; there could be normal objects in-between
traverse_for_owners(object, (nested) => add_owner_to_object(nested, owner, visited));
}
let strip_owner_visited = new Set();
/**
* @param {any} object
*/
export function strip_owner(object) {
untrack(() => {
strip_owner_from_object(object);
});
// Needed because ownership stripping can invoke getters on a proxy,
// calling strip_owner anew, so just keeping the set as part of
// strip_owner_from_object would not be enough.
const prev = strip_owner_visited;
try {
untrack(() => {
strip_owner_from_object(object, strip_owner_visited);
});
} finally {
strip_owner_visited = prev;
}
}
/**
* @param {any} object
* @param {Set<any>} visited
*/
function strip_owner_from_object(object) {
function strip_owner_from_object(object, visited) {
if (visited.has(object)) return;
visited.add(object);
if (object?.[STATE_SYMBOL]?.o) {
object[STATE_SYMBOL].o = null;
}
// Not inside previous if-block; there could be normal objects in-between
traverse_for_owners(object, (nested) => strip_owner_from_object(nested, visited));
}
/**
* @param {any} object
* @param {(obj: any) => void} cb
*/
function traverse_for_owners(object, cb) {
if (typeof object === 'object' && object !== null && !(object instanceof EventTarget)) {
for (const key in object) {
strip_owner(object[key]);
cb(object[key]);
}
// deal with state on classes
const proto = Object.getPrototypeOf(object);
if (
proto !== Object.prototype &&
proto !== Array.prototype &&
proto !== Map.prototype &&
proto !== Set.prototype &&
proto !== Date.prototype
) {
const descriptors = get_descriptors(proto);
for (let key in descriptors) {
const get = descriptors[key].get;
if (get) {
try {
cb(object[key]);
} catch (e) {
// continue
}
}
}
}
}
}

@ -26,9 +26,6 @@ import { source, mutable_source, set } from '../../reactivity/sources.js';
import { is_array, is_frozen, map_get, map_set } from '../../utils.js';
import { STATE_SYMBOL } from '../../constants.js';
var NEW_ITEM = -1;
var LIS_ITEM = -2;
/**
* The row of a keyed each block that is currently updating. We track this
* so that `animate:` directives have something to attach themselves to
@ -41,25 +38,33 @@ export function set_current_each_item(item) {
current_each_item = item;
}
/**
* @param {any} _
* @param {number} i
*/
export function index(_, i) {
return i;
}
/**
* Pause multiple effects simultaneously, and coordinate their
* subsequent destruction. Used in each blocks
* @param {import('#client').Effect[]} effects
* @param {import('#client').EachItem[]} items
* @param {null | Node} controlled_anchor
* @param {() => void} [callback]
*/
function pause_effects(effects, controlled_anchor, callback) {
function pause_effects(items, controlled_anchor, callback) {
/** @type {import('#client').TransitionManager[]} */
var transitions = [];
var length = effects.length;
var length = items.length;
for (var i = 0; i < length; i++) {
pause_children(effects[i], transitions, true);
pause_children(items[i].e, transitions, true);
}
// If we have a controlled anchor, it means that the each block is inside a single
// DOM element, so we can apply a fast-path for clearing the contents of the element.
if (effects.length > 0 && transitions.length === 0 && controlled_anchor !== null) {
if (length > 0 && transitions.length === 0 && controlled_anchor !== null) {
var parent_node = /** @type {Element} */ (controlled_anchor.parentNode);
parent_node.textContent = '';
parent_node.append(controlled_anchor);
@ -67,7 +72,7 @@ function pause_effects(effects, controlled_anchor, callback) {
run_out_transitions(transitions, () => {
for (var i = 0; i < length; i++) {
destroy_effect(effects[i]);
destroy_effect(items[i].e);
}
if (callback !== undefined) callback();
@ -79,15 +84,14 @@ function pause_effects(effects, controlled_anchor, callback) {
* @param {Element | Comment} anchor The next sibling node, or the parent node if this is a 'controlled' block
* @param {number} flags
* @param {() => V[]} get_collection
* @param {null | ((item: V) => string)} get_key
* @param {(value: V, index: number) => any} get_key
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn
* @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn
* @returns {void}
*/
function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, reconcile_fn) {
export function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn = null) {
/** @type {import('#client').EachState} */
var state = { flags, items: [] };
var state = { flags, items: new Map(), next: null };
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
@ -113,8 +117,6 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
? []
: Array.from(collection);
var keys = get_key === null ? array : array.map(get_key);
var length = array.length;
// If we are working with an array that isn't proxied or frozen, then remove strict equality and ensure the items
@ -145,11 +147,15 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
// this is separate to the previous block because `hydrating` might change
if (hydrating) {
var b_items = [];
/** @type {Node} */
var child_anchor = hydrate_nodes[0];
/** @type {import('#client').EachItem | import('#client').EachState} */
var prev = state;
/** @type {import('#client').EachItem} */
var item;
for (var i = 0; i < length; i++) {
if (
child_anchor.nodeType !== 8 ||
@ -163,8 +169,13 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
}
child_anchor = hydrate_anchor(child_anchor);
b_items[i] = create_item(child_anchor, array[i], keys?.[i], i, render_fn, flags);
var value = array[i];
var key = get_key(value, i);
item = create_item(child_anchor, prev, null, value, key, i, render_fn, flags);
state.items.set(key, item);
child_anchor = /** @type {Comment} */ (child_anchor.nextSibling);
prev = item;
}
// remove excess nodes
@ -175,12 +186,10 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
child_anchor = next;
}
}
state.items = b_items;
}
if (!hydrating) {
reconcile_fn(array, state, anchor, render_fn, flags, keys);
reconcile(array, state, anchor, render_fn, flags, get_key);
}
if (fallback_fn !== null) {
@ -204,33 +213,6 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
});
}
/**
* @template V
* @param {Element | Comment} anchor
* @param {number} flags
* @param {() => V[]} get_collection
* @param {null | ((item: V) => string)} get_key
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} [fallback_fn]
* @returns {void}
*/
export function each_keyed(anchor, flags, get_collection, get_key, render_fn, fallback_fn = null) {
each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, reconcile_tracked_array);
}
/**
* @template V
* @param {Element | Comment} anchor
* @param {number} flags
* @param {() => V[]} get_collection
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} [fallback_fn]
* @returns {void}
*/
export function each_indexed(anchor, flags, get_collection, render_fn, fallback_fn = null) {
each(anchor, flags, get_collection, null, render_fn, fallback_fn, reconcile_indexed_array);
}
/**
* @template V
* @param {Array<V>} array
@ -238,308 +220,182 @@ export function each_indexed(anchor, flags, get_collection, render_fn, fallback_
* @param {Element | Comment | Text} anchor
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: number | import('#client').Source<number>) => void} render_fn
* @param {number} flags
* @param {(value: V, index: number) => any} get_key
* @returns {void}
*/
function reconcile_indexed_array(array, state, anchor, render_fn, flags) {
var a_items = state.items;
function reconcile(array, state, anchor, render_fn, flags, get_key) {
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
var a = a_items.length;
var b = array.length;
var min = Math.min(a, b);
var length = array.length;
var items = state.items;
var first = state.next;
var current = first;
/** @type {typeof a_items} */
var b_items = Array(b);
/** @type {Set<import('#client').EachItem>} */
var seen = new Set();
var item;
var value;
// update items
for (var i = 0; i < min; i += 1) {
value = array[i];
item = a_items[i];
b_items[i] = item;
update_item(item, value, i, flags);
resume_effect(item.e);
}
/** @type {import('#client').EachState | import('#client').EachItem} */
var prev = state;
if (b > a) {
// add items
for (; i < b; i += 1) {
value = array[i];
item = create_item(anchor, value, null, i, render_fn, flags);
b_items[i] = item;
}
state.items = b_items;
} else if (a > b) {
// remove items
var effects = [];
for (i = b; i < a; i += 1) {
effects.push(a_items[i].e);
}
var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && b === 0 ? anchor : null;
/** @type {import('#client').EachItem[]} */
var to_animate = [];
pause_effects(effects, controlled_anchor, () => {
state.items.length = b;
});
}
}
/** @type {import('#client').EachItem[]} */
var matched = [];
/**
* Reconcile arrays by the equality of the elements in the array. This algorithm
* is based on Ivi's reconcilation logic:
* https://github.com/localvoid/ivi/blob/9f1bd0918f487da5b131941228604763c5d8ef56/packages/ivi/src/client/core.ts#L968
* @template V
* @param {Array<V>} array
* @param {import('#client').EachState} state
* @param {Element | Comment | Text} anchor
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: number | import('#client').Source<number>) => void} render_fn
* @param {number} flags
* @param {any[]} keys
* @returns {void}
*/
function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) {
var a_items = state.items;
/** @type {import('#client').EachItem[]} */
var stashed = [];
var a = a_items.length;
var b = array.length;
/** @type {V} */
var value;
/** @type {Array<import('#client').EachItem>} */
var b_items = Array(b);
/** @type {any} */
var key;
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
var start = 0;
/** @type {import('#client').EachItem | undefined} */
var item;
/** @type {import('#client').Effect[]} */
var to_destroy = [];
/** @type {Array<import('#client').EachItem>} */
var to_animate = [];
// Step 1 — trim common suffix
while (a > 0 && b > 0 && a_items[a - 1].k === keys[b - 1]) {
item = b_items[--b] = a_items[--a];
anchor = get_first_child(item);
/** @type {number} */
var i;
resume_effect(item.e);
if (is_animated) {
for (i = 0; i < length; i += 1) {
value = array[i];
key = get_key(value, i);
item = items.get(key);
if (should_update) {
update_item(item, array[b], b, flags);
}
if (is_animated) {
item.a?.measure();
to_animate.push(item);
if (item !== undefined) {
item.a?.measure();
to_animate.push(item);
}
}
}
// Step 2 — trim common prefix
while (start < a && start < b && a_items[start].k === keys[start]) {
item = b_items[start] = a_items[start];
resume_effect(item.e);
for (i = 0; i < length; i += 1) {
value = array[i];
key = get_key(value, i);
item = items.get(key);
if (item === undefined) {
prev = create_item(
current ? get_first_child(current) : anchor,
prev,
prev.next,
value,
key,
i,
render_fn,
flags
);
items.set(key, prev);
matched = [];
stashed = [];
current = prev.next;
continue;
}
if (should_update) {
update_item(item, array[start], start, flags);
update_item(item, value, i, flags);
}
if (is_animated) {
item.a?.measure();
to_animate.push(item);
}
start += 1;
}
// Step 3 — update
if (start === a) {
// add only
while (start < b) {
item = create_item(anchor, array[start], keys[start], start, render_fn, flags);
b_items[start++] = item;
}
} else if (start === b) {
// remove only
while (start < a) {
to_destroy.push(a_items[start++].e);
}
} else {
// reconcile
var moved = false;
var sources = new Int32Array(b - start);
var indexes = new Map();
var i;
var index;
var last_item;
// store the indexes of each item in the new world
for (i = start; i < b; i += 1) {
sources[i - start] = NEW_ITEM;
map_set(indexes, keys[i], i);
}
resume_effect(item.e);
if (is_animated) {
// for all items that were in both the old and the new list,
// measure them and store them in `to_animate` so we can
// apply animations once the DOM has been updated
for (i = 0; i < a_items.length; i += 1) {
item = a_items[i];
if (indexes.has(item.k)) {
item.a?.measure();
to_animate.push(item);
}
}
}
if (item !== current) {
if (seen.has(item)) {
if (matched.length < stashed.length) {
// more efficient to move later items to the front
var start = stashed[0];
var local_anchor = get_first_child(start);
var j;
// populate the `sources` array for each old item with
// its new index, so that we can calculate moves
for (i = start; i < a; i += 1) {
item = a_items[i];
index = map_get(indexes, item.k);
prev = start.prev;
resume_effect(item.e);
var a = matched[0];
var b = matched[matched.length - 1];
if (index === undefined) {
to_destroy.push(item.e);
} else {
moved = true;
sources[index - start] = i;
b_items[index] = item;
link(a.prev, b.next);
link(prev, a);
link(b, start);
if (is_animated) {
to_animate.push(item);
}
}
}
for (j = 0; j < matched.length; j += 1) {
move(matched[j], local_anchor);
}
// if we need to move items (as opposed to just adding/removing),
// figure out how to do so efficiently (I would be lying if I said
// I fully understand this part)
if (moved) {
mark_lis(sources);
} else if (is_controlled && to_destroy.length === a_items.length) {
// We can optimize the case in which all items are replaced —
// destroy everything first, then append new items
pause_effects(to_destroy, anchor);
to_destroy = [];
}
for (j = 0; j < stashed.length; j += 1) {
seen.delete(stashed[j]);
}
// working from the back, insert new or moved items
while (b-- > start) {
index = sources[b - start];
var should_insert = index === NEW_ITEM;
if (should_insert) {
if (last_item !== undefined) anchor = get_first_child(last_item);
item = create_item(anchor, array[b], keys[b], b, render_fn, flags);
} else {
item = b_items[b];
if (should_update) {
update_item(item, array[b], b, flags);
}
current = start;
prev = b;
i -= 1;
if (moved && index !== LIS_ITEM) {
if (last_item !== undefined) anchor = get_first_child(last_item);
move(/** @type {import('#client').Dom} */ (item.e.dom), anchor);
}
}
matched = [];
stashed = [];
} else {
// more efficient to move earlier items to the back
seen.delete(item);
move(item, current ? get_first_child(current) : anchor);
last_item = b_items[b] = item;
}
}
link(item.prev, item.next);
link(item, prev.next);
link(prev, item);
if (to_animate.length > 0) {
// TODO we need to briefly take any outroing elements out of the flow, so that
// we can figure out the eventual destination of the animating elements
// - https://github.com/sveltejs/svelte/pull/10798#issuecomment-2013681778
// - https://svelte.dev/repl/6e891305e9644a7ca7065fa95c79d2d2?version=4.2.9
effect(() => {
untrack(() => {
for (item of to_animate) {
item.a?.apply();
prev = item;
}
});
});
}
var controlled_anchor = is_controlled && b_items.length === 0 ? anchor : null;
continue;
}
pause_effects(to_destroy, controlled_anchor, () => {
state.items = b_items;
});
}
matched = [];
stashed = [];
/**
* Longest Increased Subsequence algorithm
* @param {Int32Array} a
* @returns {void}
*/
function mark_lis(a) {
var length = a.length;
var parent = new Int32Array(length);
var index = new Int32Array(length);
var index_length = 0;
var i = 0;
/** @type {number} */
var j;
/** @type {number} */
var k;
while (current !== null && current.k !== key) {
seen.add(current);
stashed.push(current);
current = current.next;
}
/** @type {number} */
var lo;
if (current === null) {
continue;
}
/** @type {number} */
var hi;
item = current;
}
// Skip -1 values at the start of the input array `a`.
for (; a[i] === NEW_ITEM; ++i) {
/**/
matched.push(item);
prev = item;
current = item.next;
}
index[0] = i++;
for (; i < length; ++i) {
k = a[i];
if (k !== NEW_ITEM) {
// Ignore -1 values.
j = index[index_length];
const to_destroy = Array.from(seen);
if (a[j] < k) {
parent[i] = j;
index[++index_length] = i;
} else {
lo = 0;
hi = index_length;
while (current) {
to_destroy.push(current);
current = current.next;
}
while (lo < hi) {
j = (lo + hi) >> 1;
if (a[index[j]] < k) {
lo = j + 1;
} else {
hi = j;
}
}
var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && length === 0 ? anchor : null;
if (k < a[index[lo]]) {
if (lo > 0) {
parent[i] = index[lo - 1];
}
index[lo] = i;
}
}
pause_effects(to_destroy, controlled_anchor, () => {
for (var i = 0; i < to_destroy.length; i += 1) {
var item = to_destroy[i];
items.delete(item.k);
link(item.prev, item.next);
}
}
// Mutate input array `a` and assign -2 value to all nodes that are part of LIS.
j = index[index_length];
});
while (index_length-- >= 0) {
a[j] = LIS_ITEM;
j = parent[j];
if (is_animated) {
effect(() => {
untrack(() => {
for (item of to_animate) {
item.a?.apply();
}
});
});
}
}
@ -579,6 +435,8 @@ function update_item(item, value, index, type) {
/**
* @template V
* @param {Node} anchor
* @param {import('#client').EachItem | import('#client').EachState} prev
* @param {import('#client').EachItem | null} next
* @param {V} value
* @param {unknown} key
* @param {number} index
@ -586,7 +444,7 @@ function update_item(item, value, index, type) {
* @param {number} flags
* @returns {import('#client').EachItem}
*/
function create_item(anchor, value, key, index, render_fn, flags) {
function create_item(anchor, prev, next, value, key, index, render_fn, flags) {
var previous_each_item = current_each_item;
try {
@ -603,9 +461,14 @@ function create_item(anchor, value, key, index, render_fn, flags) {
k: key,
a: null,
// @ts-expect-error
e: null
e: null,
prev,
next
};
prev.next = item;
if (next !== null) next.prev = item;
current_each_item = item;
item.e = branch(() => render_fn(anchor, v, i));
@ -616,15 +479,29 @@ function create_item(anchor, value, key, index, render_fn, flags) {
}
/**
* @param {import('#client').Dom} current
* @param {import('#client').EachItem} item
* @param {Text | Element | Comment} anchor
*/
function move(current, anchor) {
if (is_array(current)) {
for (var i = 0; i < current.length; i++) {
anchor.before(current[i]);
function move(item, anchor) {
var dom = item.e.dom;
if (dom !== null) {
if (is_array(dom)) {
for (var i = 0; i < dom.length; i++) {
anchor.before(dom[i]);
}
} else {
anchor.before(dom);
}
} else {
anchor.before(current);
}
}
/**
*
* @param {import('#client').EachItem | import('#client').EachState} prev
* @param {import('#client').EachItem | null} next
*/
function link(prev, next) {
prev.next = next;
if (next !== null) next.prev = prev;
}

@ -1,7 +1,16 @@
import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js';
import { empty } from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { HYDRATION_START } from '../../../../constants.js';
import { HYDRATION_END, HYDRATION_START } from '../../../../constants.js';
/**
* @type {Node | undefined}
*/
let head_anchor;
export function reset_head_anchor() {
head_anchor = undefined;
}
/**
* @param {(anchor: Node) => import('#client').Dom | void} render_fn
@ -19,12 +28,20 @@ export function head(render_fn) {
if (hydrating) {
previous_hydrate_nodes = hydrate_nodes;
let anchor = /** @type {import('#client').TemplateNode} */ (document.head.firstChild);
while (anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START) {
anchor = /** @type {import('#client').TemplateNode} */ (anchor.nextSibling);
// There might be multiple head blocks in our app, so we need to account for each one needing independent hydration.
if (head_anchor === undefined) {
head_anchor = /** @type {import('#client').TemplateNode} */ (document.head.firstChild);
}
while (
head_anchor.nodeType !== 8 ||
/** @type {Comment} */ (head_anchor).data !== HYDRATION_START
) {
head_anchor = /** @type {import('#client').TemplateNode} */ (head_anchor.nextSibling);
}
anchor = /** @type {import('#client').TemplateNode} */ (hydrate_anchor(anchor));
head_anchor = /** @type {import('#client').TemplateNode} */ (hydrate_anchor(head_anchor));
head_anchor = /** @type {import('#client').TemplateNode} */ (head_anchor.nextSibling);
} else {
anchor = document.head.appendChild(empty());
}

@ -131,7 +131,6 @@ export function bind_checked(input, get_value, update) {
update(value);
});
// eslint-disable-next-line eqeqeq
if (get_value() == undefined) {
update(false);
}

@ -138,7 +138,8 @@ if (typeof HTMLElement === 'function') {
target: this.shadowRoot || this,
props: {
...this.$$d,
$$slots
$$slots,
$$host: this
}
});

@ -98,14 +98,14 @@ export function animation(element, get_fn, get_params) {
to = this.element.getBoundingClientRect();
const options = get_fn()(this.element, { from, to }, get_params?.());
if (
from.left !== to.left ||
from.right !== to.right ||
from.top !== to.top ||
from.bottom !== to.bottom
) {
const options = get_fn()(this.element, { from, to }, get_params?.());
animation = animate(this.element, options, undefined, 1, () => {
animation?.abort();
animation = undefined;

@ -3,6 +3,7 @@ import { clone_node, empty } from './operations.js';
import { create_fragment_from_html } from './reconciler.js';
import { current_effect } from '../runtime.js';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
import { effect } from '../reactivity/effects.js';
/**
* @param {string} content
@ -120,14 +121,29 @@ export function svg_template_with_script(content, flags) {
* @param {Element | DocumentFragment} node
*/
function run_scripts(node) {
for (const script of node.querySelectorAll('script')) {
// scripts were SSR'd, in which case they will run
if (hydrating) return;
const scripts =
/** @type {HTMLElement} */ (node).tagName === 'SCRIPT'
? [/** @type {HTMLScriptElement} */ (node)]
: node.querySelectorAll('script');
for (const script of scripts) {
var clone = document.createElement('script');
for (var attribute of script.attributes) {
clone.setAttribute(attribute.name, attribute.value);
}
clone.textContent = script.textContent;
script.replaceWith(clone);
// If node === script tag, replaceWith will do nothing because there's no parent yet,
// waiting until that's the case using an effect solves this.
// Don't do it in other circumstances or we could accidentally execute scripts
// in an adjacent @html tag that was instantiated in the meantime.
if (script === node) {
effect(() => script.replaceWith(clone));
} else {
script.replaceWith(clone);
}
}
}

@ -1,9 +1,10 @@
export { hmr } from './dev/hmr.js';
export { add_owner, mark_module_start, mark_module_end } from './dev/ownership.js';
export { await_block as await } from './dom/blocks/await.js';
export { if_block as if } from './dom/blocks/if.js';
export { key_block as key } from './dom/blocks/key.js';
export { css_props } from './dom/blocks/css-props.js';
export { each_keyed, each_indexed } from './dom/blocks/each.js';
export { index, each } from './dom/blocks/each.js';
export { html } from './dom/blocks/html.js';
export { snippet } from './dom/blocks/snippet.js';
export { component } from './dom/blocks/svelte-component.js';

@ -1,6 +1,11 @@
import { DEV } from 'esm-env';
import { get, batch_inspect, current_component_context, untrack } from './runtime.js';
import { effect_active } from './reactivity/effects.js';
import {
get,
batch_inspect,
current_component_context,
untrack,
current_effect
} from './runtime.js';
import {
array_prototype,
define_property,
@ -14,7 +19,6 @@ import {
import { add_owner, check_ownership, strip_owner } from './dev/ownership.js';
import { mutable_source, source, set } from './reactivity/sources.js';
import { STATE_SYMBOL } from './constants.js';
import { updating_derived } from './reactivity/deriveds.js';
import { UNINITIALIZED } from '../../constants.js';
/**
@ -202,13 +206,8 @@ const state_proxy_handler = {
const metadata = target[STATE_SYMBOL];
let s = metadata.s.get(prop);
// if we're reading a property in a reactive context, create a source,
// but only if it's an own property and not a prototype property
if (
s === undefined &&
(effect_active() || updating_derived) &&
(!(prop in target) || get_descriptor(target, prop)?.writable)
) {
// create a source, but only if it's an own property and not a prototype property
if (s === undefined && (!(prop in target) || get_descriptor(target, prop)?.writable)) {
s = (metadata.i ? source : mutable_source)(proxy(target[prop], metadata.i, metadata.o));
metadata.s.set(prop, s);
}
@ -250,7 +249,10 @@ const state_proxy_handler = {
const has = Reflect.has(target, prop);
let s = metadata.s.get(prop);
if (s !== undefined || (effect_active() && (!has || get_descriptor(target, prop)?.writable))) {
if (
s !== undefined ||
(current_effect !== null && (!has || get_descriptor(target, prop)?.writable))
) {
if (s === undefined) {
s = (metadata.i ? source : mutable_source)(
has ? proxy(target[prop], metadata.i, metadata.o) : UNINITIALIZED
@ -273,7 +275,7 @@ const state_proxy_handler = {
// we do so otherwise if we read it later, then the write won't be tracked and
// the heuristics of effects will be different vs if we had read the proxied
// object property before writing to that property.
if (s === undefined && effect_active()) {
if (s === undefined) {
// the read creates a signal
untrack(() => receiver[prop]);
s = metadata.s.get(prop);

@ -9,10 +9,8 @@ export function equals(value) {
* @returns {boolean}
*/
export function safe_not_equal(a, b) {
// eslint-disable-next-line eqeqeq
return a != a
? // eslint-disable-next-line eqeqeq
b == b
? b == b
: a !== b || (a !== null && typeof a === 'object') || typeof a === 'function';
}

@ -7,7 +7,7 @@ import {
init_operations
} from './dom/operations.js';
import { HYDRATION_START, PassiveDelegatedEvents } from '../../constants.js';
import { flush_sync, push, pop, current_component_context, untrack } from './runtime.js';
import { flush_sync, push, pop, current_component_context } from './runtime.js';
import { effect_root, branch } from './reactivity/effects.js';
import {
hydrate_anchor,
@ -18,6 +18,7 @@ import {
} from './dom/hydration.js';
import { array_from } from './utils.js';
import { handle_event_propagation } from './dom/elements/events.js';
import { reset_head_anchor } from './dom/blocks/svelte-head.js';
/** @type {Set<string>} */
export const all_registered_events = new Set();
@ -175,6 +176,7 @@ export function hydrate(component, options) {
} finally {
set_hydrating(!!previous_hydrate_nodes);
set_hydrate_nodes(previous_hydrate_nodes);
reset_head_anchor();
}
}

@ -194,9 +194,21 @@ export function check_dirtiness(reaction) {
// is also dirty.
var version = dependency.version;
if (is_unowned && version > /** @type {import('#client').Derived} */ (reaction).version) {
/** @type {import('#client').Derived} */ (reaction).version = version;
return true;
if (is_unowned) {
if (version > /** @type {import('#client').Derived} */ (reaction).version) {
/** @type {import('#client').Derived} */ (reaction).version = version;
return true;
} else if (!current_skip_reaction && !dependency?.reactions?.includes(reaction)) {
// If we are working with an unowned signal as part of an effect (due to !current_skip_reaction)
// and the version hasn't changed, we still need to check that this reaction
// if linked to the dependency source otherwise future updates will not be caught.
var reactions = dependency.reactions;
if (reactions === null) {
dependency.reactions = [reaction];
} else {
reactions.push(reaction);
}
}
}
}
}

@ -50,8 +50,10 @@ export type Dom = TemplateNode | TemplateNode[];
export type EachState = {
/** flags */
flags: number;
/** items */
items: EachItem[];
/** a key -> item lookup */
items: Map<any, EachItem>;
/** head of the linked list of items */
next: EachItem | null;
};
export type EachItem = {
@ -65,6 +67,8 @@ export type EachItem = {
i: number | Source<number>;
/** key */
k: unknown;
prev: EachItem | EachState;
next: EachItem | null;
};
export interface TransitionManager {

@ -207,10 +207,7 @@ export function render(component, options) {
on_destroy = prev_on_destroy;
return {
head:
payload.head.out || payload.head.title
? payload.head.title + BLOCK_OPEN + payload.head.out + BLOCK_CLOSE
: '',
head: payload.head.out || payload.head.title ? payload.head.out + payload.head.title : '',
html: payload.out
};
}
@ -247,7 +244,9 @@ export function escape(value, is_attr = false) {
*/
export function head(payload, fn) {
const head_payload = payload.head;
payload.head.out += BLOCK_OPEN;
fn(head_payload);
payload.head.out += BLOCK_CLOSE;
}
/**

@ -1,4 +1,3 @@
// eslint-disable-next-line @typescript-eslint/no-empty-function
export const noop = () => {};
// Adapted from https://github.com/then/is-promise/blob/master/index.js

@ -53,7 +53,7 @@ export function asClassComponent(component) {
class Svelte4Component {
/** @type {any} */
#events = {};
#events;
/** @type {Record<string, any>} */
#instance;
@ -70,7 +70,7 @@ class Svelte4Component {
// Using proxy state here isn't completely mirroring the Svelte 4 behavior, because mutations to a property
// cause fine-grained updates to only the places where that property is used, and not the entire property.
// Reactive statements and actions (the things where this matters) are handling this properly regardless, so it should be fine in practise.
const props = proxy({ ...(options.props || {}), $$events: this.#events }, false);
const props = proxy({ ...(options.props || {}), $$events: {} }, false);
this.#instance = (options.hydrate ? hydrate : mount)(options.component, {
target: options.target,
props,
@ -79,6 +79,8 @@ class Svelte4Component {
recover: options.recover
});
this.#events = props.$$events;
for (const key of Object.keys(this.#instance)) {
if (key === '$set' || key === '$destroy' || key === '$on') continue;
define_property(this, key, {

@ -47,10 +47,7 @@ export class ReactiveSet extends Set {
var proto = ReactiveSet.prototype;
var set_proto = Set.prototype;
/** @type {string} */
var method;
for (method of read_methods) {
for (const method of read_methods) {
// @ts-ignore
proto[method] = function (...v) {
get(this.#version);
@ -59,7 +56,7 @@ export class ReactiveSet extends Set {
};
}
for (method of set_like_methods) {
for (const method of set_like_methods) {
// @ts-ignore
proto[method] = function (...v) {
get(this.#version);

@ -27,11 +27,7 @@ export function readable(value, start) {
* @returns {boolean}
*/
export function safe_not_equal(a, b) {
// eslint-disable-next-line eqeqeq
return a != a
? // eslint-disable-next-line eqeqeq
b == b
: a !== b || (a && typeof a === 'object') || typeof a === 'function';
return a != a ? b == b : a !== b || (a && typeof a === 'object') || typeof a === 'function';
}
/**

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

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
error: {
code: 'invalid-sequence-expression',
message:
'Sequence expressions are not allowed as attribute/directive values in runes mode, unless wrapped in parentheses',
position: [124, 131]
}
});

@ -0,0 +1,9 @@
<script>
let { x, y, z } = $props();
</script>
<!-- allowed -->
<span foo={(x, y, z)} />
<!-- not allowed -->
<span foo={x, y, z} />

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
error: {
code: 'conflicting-property-name',
message: 'Cannot have a property and a component export with the same name'
}
});

@ -0,0 +1,4 @@
<script>
let { x: y } = $props();
export function x() {}
</script>

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
error: {
code: 'invalid-host-location',
message: '$host() can only be used inside custom element component instances'
}
});

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
end: {
character: 44,
column: 14,
line: 4
},
message: 'Unused CSS selector "p[type=\'B\' s]"',
start: {
character: 31,
column: 1,
line: 4
}
}
]
});

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
end: {
character: 33,
column: 6,
line: 6
},
message: 'Unused CSS selector "x y z"',
start: {
character: 28,
column: 1,
line: 6
}
}
]
});

@ -5,8 +5,8 @@ export default test({
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".b ~ .c"',
start: { character: 199, column: 1, line: 13 },
end: { character: 206, column: 8, line: 13 }
start: { character: 198, column: 1, line: 13 },
end: { character: 205, column: 8, line: 13 }
}
]
});

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
end: {
character: 479,
column: 19,
line: 22
},
message: 'Unused CSS selector ":global(.x) + .bar"',
start: {
character: 461,
column: 1,
line: 22
}
}
]
});

@ -0,0 +1,13 @@
.before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; }
.before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; }
.before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
.x + .foo.svelte-xyz { color: green; }
.x + .foo.svelte-xyz span:where(.svelte-xyz) { color: green; }
.x ~ .foo.svelte-xyz { color: green; }
.x ~ .foo.svelte-xyz span:where(.svelte-xyz) { color: green; }
.x ~ .bar.svelte-xyz { color: green; }
/* no match */
/* (unused) :global(.x) + .bar { color: green; }*/

@ -0,0 +1,23 @@
<div>
<p class="before">before</p>
{@render children()}
<p class="foo">
<span>foo</span>
</p>
<p class="bar">bar</p>
</div>
<style>
.before + .foo { color: green; }
.before ~ .foo { color: green; }
.before ~ .bar { color: green; }
:global(.x) + .foo { color: green; }
:global(.x) + .foo span { color: green; }
:global(.x) ~ .foo { color: green; }
:global(.x) ~ .foo span { color: green; }
:global(.x) ~ .bar { color: green; }
/* no match */
:global(.x) + .bar { color: green; }
</style>

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
end: {
character: 472,
column: 19,
line: 22
},
message: 'Unused CSS selector ":global(.x) + .bar"',
start: {
character: 454,
column: 1,
line: 22
}
}
]
});

@ -0,0 +1,13 @@
.before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; }
.before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; }
.before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
.x + .foo.svelte-xyz { color: green; }
.x + .foo.svelte-xyz span:where(.svelte-xyz) { color: green; }
.x ~ .foo.svelte-xyz { color: green; }
.x ~ .foo.svelte-xyz span:where(.svelte-xyz) { color: green; }
.x ~ .bar.svelte-xyz { color: green; }
/* no match */
/* (unused) :global(.x) + .bar { color: green; }*/

@ -0,0 +1,23 @@
<div>
<p class="before">before</p>
<slot></slot>
<p class="foo">
<span>foo</span>
</p>
<p class="bar">bar</p>
</div>
<style>
.before + .foo { color: green; }
.before ~ .foo { color: green; }
.before ~ .bar { color: green; }
:global(.x) + .foo { color: green; }
:global(.x) + .foo span { color: green; }
:global(.x) ~ .foo { color: green; }
:global(.x) ~ .foo span { color: green; }
:global(.x) ~ .bar { color: green; }
/* no match */
:global(.x) + .bar { color: green; }
</style>

@ -5,38 +5,38 @@ export default test({
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".a ~ .b"',
start: { character: 111, column: 1, line: 10 },
end: { character: 118, column: 8, line: 10 }
start: { character: 110, column: 1, line: 10 },
end: { character: 117, column: 8, line: 10 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".b ~ .c"',
start: { character: 138, column: 1, line: 11 },
end: { character: 145, column: 8, line: 11 }
start: { character: 137, column: 1, line: 11 },
end: { character: 144, column: 8, line: 11 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".c ~ .f"',
start: { character: 165, column: 1, line: 12 },
end: { character: 172, column: 8, line: 12 }
start: { character: 164, column: 1, line: 12 },
end: { character: 171, column: 8, line: 12 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".f ~ .g"',
start: { character: 192, column: 1, line: 13 },
end: { character: 199, column: 8, line: 13 }
start: { character: 191, column: 1, line: 13 },
end: { character: 198, column: 8, line: 13 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".b ~ .f"',
start: { character: 219, column: 1, line: 14 },
end: { character: 226, column: 8, line: 14 }
start: { character: 218, column: 1, line: 14 },
end: { character: 225, column: 8, line: 14 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".b ~ .g"',
start: { character: 246, column: 1, line: 15 },
end: { character: 253, column: 8, line: 15 }
start: { character: 245, column: 1, line: 15 },
end: { character: 252, column: 8, line: 15 }
}
]
});

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
end: {
character: 496,
column: 10,
line: 26
},
message: 'Unused CSS selector ".x + .bar"',
start: {
character: 487,
column: 1,
line: 26
}
}
]
});

@ -0,0 +1,13 @@
.before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; }
.before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; }
.before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
.x.svelte-xyz + .foo:where(.svelte-xyz) { color: green; }
.x.svelte-xyz + .foo:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; }
.x.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; }
.x.svelte-xyz ~ .foo:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; }
.x.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
/* no match */
/* (unused) .x + .bar { color: green; }*/

@ -0,0 +1,27 @@
<script>
let tag = 'div'
</script>
<div>
<p class="before">before</p>
<svelte:element class="x" this={tag}></svelte:element>
<p class="foo">
<span>foo</span>
</p>
<p class="bar">bar</p>
</div>
<style>
.before + .foo { color: green; }
.before ~ .foo { color: green; }
.before ~ .bar { color: green; }
.x + .foo { color: green; }
.x + .foo span { color: green; }
.x ~ .foo { color: green; }
.x ~ .foo span { color: green; }
.x ~ .bar { color: green; }
/* no match */
.x + .bar { color: green; }
</style>

@ -6,12 +6,12 @@ export default test({
code: 'css-unused-selector',
message: 'Unused CSS selector ":host > span"',
start: {
character: 147,
character: 145,
column: 1,
line: 18
},
end: {
character: 159,
character: 157,
column: 13,
line: 18
}

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
end: {
character: 38,
column: 11,
line: 6
},
message: 'Unused CSS selector "z"',
start: {
character: 37,
column: 10,
line: 6
}
}
]
});

@ -0,0 +1,104 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
end: {
character: 239,
column: 13,
line: 20
},
message: 'Unused CSS selector ".unused"',
start: {
character: 232,
column: 6,
line: 20
}
},
{
code: 'css-unused-selector',
end: {
character: 302,
column: 10,
line: 27
},
message: 'Unused CSS selector ".unused"',
start: {
character: 295,
column: 3,
line: 27
}
},
{
code: 'css-unused-selector',
end: {
character: 328,
column: 6,
line: 30
},
message: 'Unused CSS selector ".c"',
start: {
character: 326,
column: 4,
line: 30
}
},
{
code: 'css-unused-selector',
end: {
character: 381,
column: 10,
line: 37
},
message: 'Unused CSS selector ".unused"',
start: {
character: 374,
column: 3,
line: 37
}
},
{
code: 'css-unused-selector',
end: {
character: 471,
column: 7,
line: 47
},
message: 'Unused CSS selector "& &"',
start: {
character: 468,
column: 4,
line: 47
}
},
{
code: 'css-unused-selector',
end: {
character: 634,
column: 5,
line: 66
},
message: 'Unused CSS selector "&.b"',
start: {
character: 631,
column: 2,
line: 66
}
},
{
code: 'css-unused-selector',
end: {
character: 666,
column: 9,
line: 70
},
message: 'Unused CSS selector ".unused"',
start: {
character: 659,
column: 2,
line: 70
}
}
]
});

@ -5,62 +5,62 @@ export default test({
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".a + .c"',
start: { character: 479, column: 1, line: 23 },
end: { character: 486, column: 8, line: 23 }
start: { character: 478, column: 1, line: 23 },
end: { character: 485, column: 8, line: 23 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".a + .g"',
start: { character: 506, column: 1, line: 24 },
end: { character: 513, column: 8, line: 24 }
start: { character: 505, column: 1, line: 24 },
end: { character: 512, column: 8, line: 24 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".b + .e"',
start: { character: 533, column: 1, line: 25 },
end: { character: 540, column: 8, line: 25 }
start: { character: 532, column: 1, line: 25 },
end: { character: 539, column: 8, line: 25 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".c + .g"',
start: { character: 560, column: 1, line: 26 },
end: { character: 567, column: 8, line: 26 }
start: { character: 559, column: 1, line: 26 },
end: { character: 566, column: 8, line: 26 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".c + .k"',
start: { character: 587, column: 1, line: 27 },
end: { character: 594, column: 8, line: 27 }
start: { character: 586, column: 1, line: 27 },
end: { character: 593, column: 8, line: 27 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".d + .d"',
start: { character: 614, column: 1, line: 28 },
end: { character: 621, column: 8, line: 28 }
start: { character: 613, column: 1, line: 28 },
end: { character: 620, column: 8, line: 28 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".e + .f"',
start: { character: 641, column: 1, line: 29 },
end: { character: 648, column: 8, line: 29 }
start: { character: 640, column: 1, line: 29 },
end: { character: 647, column: 8, line: 29 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".f + .f"',
start: { character: 668, column: 1, line: 30 },
end: { character: 675, column: 8, line: 30 }
start: { character: 667, column: 1, line: 30 },
end: { character: 674, column: 8, line: 30 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".g + .j"',
start: { character: 695, column: 1, line: 31 },
end: { character: 702, column: 8, line: 31 }
start: { character: 694, column: 1, line: 31 },
end: { character: 701, column: 8, line: 31 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".g + .h + .i + .j"',
start: { character: 722, column: 1, line: 32 },
end: { character: 739, column: 18, line: 32 }
start: { character: 721, column: 1, line: 32 },
end: { character: 738, column: 18, line: 32 }
}
]
});

@ -5,14 +5,14 @@ export default test({
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".a + .d"',
start: { character: 172, column: 1, line: 12 },
end: { character: 179, column: 8, line: 12 }
start: { character: 171, column: 1, line: 12 },
end: { character: 178, column: 8, line: 12 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".b + .c"',
start: { character: 199, column: 1, line: 13 },
end: { character: 206, column: 8, line: 13 }
start: { character: 198, column: 1, line: 13 },
end: { character: 205, column: 8, line: 13 }
}
]
});

@ -5,20 +5,20 @@ export default test({
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".a + .b"',
start: { character: 84, column: 1, line: 9 },
end: { character: 91, column: 8, line: 9 }
start: { character: 83, column: 1, line: 9 },
end: { character: 90, column: 8, line: 9 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".b + .c"',
start: { character: 111, column: 1, line: 10 },
end: { character: 118, column: 8, line: 10 }
start: { character: 110, column: 1, line: 10 },
end: { character: 117, column: 8, line: 10 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".c + .f"',
start: { character: 138, column: 1, line: 11 },
end: { character: 145, column: 8, line: 11 }
start: { character: 137, column: 1, line: 11 },
end: { character: 144, column: 8, line: 11 }
}
]
});

@ -5,20 +5,20 @@ export default test({
{
code: 'css-unused-selector',
message: 'Unused CSS selector "article > *"',
start: { character: 10, column: 1, line: 2 },
end: { character: 21, column: 12, line: 2 }
start: { character: 9, column: 1, line: 2 },
end: { character: 20, column: 12, line: 2 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector "article *"',
start: { character: 49, column: 1, line: 6 },
end: { character: 58, column: 10, line: 6 }
start: { character: 47, column: 1, line: 6 },
end: { character: 56, column: 10, line: 6 }
},
{
code: 'css-unused-selector',
message: 'Unused CSS selector ".article > *"',
start: { character: 86, column: 1, line: 10 },
end: { character: 98, column: 13, line: 10 }
start: { character: 83, column: 1, line: 10 },
end: { character: 95, column: 13, line: 10 }
}
]
});

@ -1,6 +1,6 @@
/* (unused) article > * {
font-size: 36px;
}*/
}*/
/* (unused) article * {
font-size: 36px;

@ -1,7 +1,7 @@
<style>
article > * {
font-size: 36px;
}
}
article * {
font-size: 36px;

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
end: {
character: 32,
column: 3,
line: 5
},
message: 'Unused CSS selector "h2"',
start: {
character: 30,
column: 1,
line: 5
}
}
]
});

@ -8,12 +8,17 @@ import { mount, unmount } from 'svelte';
import { suite, type BaseTest } from '../suite.js';
import type { CompileOptions, Warning } from '#compiler';
// function normalize_warning(warning) {
// warning.frame = warning.frame.replace(/^\n/, '').replace(/^\t+/gm, '').replace(/\s+$/gm, '');
// delete warning.filename;
// delete warning.toString;
// return warning;
// }
function normalize_warning(warning: Warning) {
delete warning.filename;
return warning;
}
function load_warnings(path: string) {
if (!fs.existsSync(path)) {
return [];
}
return JSON.parse(fs.readFileSync(path, 'utf-8')).map(normalize_warning);
}
interface CssTest extends BaseTest {
compileOptions?: Partial<CompileOptions>;
@ -22,9 +27,6 @@ interface CssTest extends BaseTest {
}
const { test, run } = suite<CssTest>(async (config, cwd) => {
// TODO
// const expected_warnings = (config.warnings || []).map(normalize_warning);
await compile_directory(cwd, 'client', { cssHash: () => 'svelte-xyz', ...config.compileOptions });
await compile_directory(cwd, 'server', { cssHash: () => 'svelte-xyz', ...config.compileOptions });
@ -33,11 +35,11 @@ const { test, run } = suite<CssTest>(async (config, cwd) => {
assert.equal(dom_css, ssr_css);
// TODO reenable
// const dom_warnings = dom.warnings.map(normalize_warning);
// const ssr_warnings = ssr.warnings.map(normalize_warning);
// assert.deepEqual(dom_warnings, ssr_warnings);
// assert.deepEqual(dom_warnings.map(normalize_warning), expected_warnings);
const dom_warnings = load_warnings(`${cwd}/_output/client/input.svelte.warnings.json`);
const ssr_warnings = load_warnings(`${cwd}/_output/server/input.svelte.warnings.json`);
const expected_warnings = (config.warnings || []).map(normalize_warning);
assert.deepEqual(dom_warnings, ssr_warnings);
assert.deepEqual(dom_warnings.map(normalize_warning), expected_warnings);
const expected = {
html: try_read_file(`${cwd}/expected.html`),

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

Loading…
Cancel
Save