Merge branch 'main' into fix-tests

pull/11738/head
Dominic Gannaway 2 years ago
commit e95fad3b3b

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: allow comments after last selector in css

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't add scoping modifier to nesting selectors

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: migrate derivations without semicolons

@ -0,0 +1,5 @@
---
"svelte": patch
---
chore: speedup hydration around input and select values

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: check for invalid bindings on window and document

@ -60,6 +60,7 @@
"chilly-snakes-scream",
"clean-eels-beg",
"clever-chefs-relate",
"clever-maps-travel",
"clever-rockets-burn",
"clever-sloths-push",
"cold-birds-own",
@ -208,6 +209,7 @@
"hungry-tips-unite",
"hungry-trees-travel",
"itchy-beans-melt",
"itchy-beds-kneel",
"itchy-bulldogs-tan",
"itchy-eels-marry",
"itchy-kings-deliver",
@ -227,6 +229,7 @@
"kind-dots-sort",
"kind-eagles-join",
"kind-rings-flash",
"kind-snakes-drive",
"kind-spoons-return",
"large-clouds-carry",
"large-turkeys-deny",
@ -267,6 +270,7 @@
"lucky-toes-begin",
"many-rockets-give",
"many-trees-fix",
"mean-jokes-exist",
"metal-clouds-raise",
"metal-lobsters-burn",
"mighty-cooks-scream",
@ -318,6 +322,7 @@
"pink-bikes-agree",
"pink-goats-promise",
"pink-mayflies-tie",
"plenty-elephants-fry",
"plenty-starfishes-dress",
"plenty-zoos-fix",
"polite-dolphins-care",
@ -338,7 +343,9 @@
"purple-dragons-peel",
"quiet-apricots-dream",
"quiet-berries-end",
"quiet-berries-explode",
"quiet-camels-mate",
"quiet-cobras-smile",
"quiet-crabs-nail",
"quiet-timers-speak",
"rare-mirrors-act",
@ -431,6 +438,7 @@
"soft-tigers-wink",
"sour-bags-fail",
"sour-forks-stare",
"sour-jeans-collect",
"sour-rules-march",
"sour-weeks-fix",
"spicy-jeans-deliver",
@ -465,6 +473,7 @@
"tall-shrimps-worry",
"tall-tigers-wait",
"tame-cycles-kneel",
"tame-dots-battle",
"tame-spies-drum",
"tasty-cheetahs-appear",
"tasty-numbers-perform",
@ -503,6 +512,7 @@
"tiny-meals-deliver",
"tiny-moose-kiss",
"tough-radios-punch",
"twelve-beans-drive",
"twelve-dragons-join",
"twelve-onions-juggle",
"twelve-worms-jog",

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: inline pointer events now correctly work in Chrome

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: update value like attributes in a separate template_effect

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

1
.gitignore vendored

@ -16,7 +16,6 @@ coverage
.env.test
# build output
dist
.vercel
# OS-specific

@ -1,5 +1,33 @@
# svelte
## 5.0.0-next.138
### Patch Changes
- fix: allow comments after last selector in css ([#11723](https://github.com/sveltejs/svelte/pull/11723))
- fix: don't add scoping modifier to nesting selectors ([#11713](https://github.com/sveltejs/svelte/pull/11713))
- chore: speedup hydration around input and select values ([#11717](https://github.com/sveltejs/svelte/pull/11717))
- fix: update value like attributes in a separate template_effect ([#11720](https://github.com/sveltejs/svelte/pull/11720))
- fix: improve handling of unowned derived signal ([#11712](https://github.com/sveltejs/svelte/pull/11712))
## 5.0.0-next.137
### Patch Changes
- fix: migrate derivations without semicolons ([#11704](https://github.com/sveltejs/svelte/pull/11704))
- fix: check for invalid bindings on window and document ([#11676](https://github.com/sveltejs/svelte/pull/11676))
- fix: more efficient spread attributes in SSR output ([#11660](https://github.com/sveltejs/svelte/pull/11660))
- fix: inline pointer events now correctly work in Chrome ([#11695](https://github.com/sveltejs/svelte/pull/11695))
- fix: don't require warning codes to be separated by commas in non-runes mode ([#11669](https://github.com/sveltejs/svelte/pull/11669))
## 5.0.0-next.136
### Patch Changes

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

@ -369,11 +369,15 @@ const instance_script = {
/** @type {number} */ (node.body.expression.right.start),
'$derived('
);
state.str.update(
/** @type {number} */ (node.body.expression.right.end),
/** @type {number} */ (node.end),
');'
);
if (node.body.expression.right.end !== node.end) {
state.str.update(
/** @type {number} */ (node.body.expression.right.end),
/** @type {number} */ (node.end),
');'
);
} else {
state.str.appendRight(/** @type {number} */ (node.end), ');');
}
return;
} else {
for (const binding of reassigned_bindings) {

@ -139,7 +139,7 @@ function read_selector_list(parser, inside_pseudo_class = false) {
const end = parser.index;
parser.allow_whitespace();
allow_comment_or_whitespace(parser);
if (inside_pseudo_class ? parser.match(')') : parser.match('{')) {
return {
@ -324,7 +324,7 @@ function read_selector(parser, inside_pseudo_class = false) {
}
const index = parser.index;
parser.allow_whitespace();
allow_comment_or_whitespace(parser);
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
// rewind, so we know whether to continue building the selector list

@ -217,7 +217,7 @@ export default function tag(parser) {
while ((attribute = read(parser))) {
if (attribute.type === 'Attribute' || attribute.type === 'BindDirective') {
if (unique_names.includes(attribute.name)) {
e.attribute_duplicate(attribute.start);
e.attribute_duplicate(attribute);
// <svelte:element bind:this this=..> is allowed
} else if (attribute.name !== 'this') {
unique_names.push(attribute.name);

@ -393,6 +393,23 @@ const validation = {
);
}
if (property.invalid_elements && property.invalid_elements.includes(parent.name)) {
const valid_bindings = Object.entries(binding_properties)
.filter(([_, binding_property]) => {
return (
binding_property.valid_elements?.includes(parent.name) ||
(!binding_property.valid_elements &&
!binding_property.invalid_elements?.includes(parent.name))
);
})
.map(([property_name]) => property_name);
e.bind_invalid_name(
node,
node.name,
`Possible bindings for <${parent.name}> are ${valid_bindings.join(', ')}`
);
}
if (parent.name === 'input' && node.name !== 'this') {
const type = /** @type {import('#compiler').Attribute | undefined} */ (
parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'type')

@ -506,6 +506,10 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
value
)
);
} else if (name === 'value') {
update = b.stmt(b.call('$.set_value', node_id, value));
} else if (name === 'checked') {
update = b.stmt(b.call('$.set_checked', node_id, value));
} else if (DOMProperties.includes(name)) {
update = b.stmt(b.assignment('=', b.member(node_id, b.id(name)), value));
} else {
@ -1990,7 +1994,7 @@ export const template_visitors = {
child_metadata.bound_contenteditable = true;
}
if (needs_input_reset && (node.name === 'input' || node.name === 'select')) {
if (needs_input_reset && node.name === 'input') {
context.state.init.push(b.stmt(b.call('$.remove_input_attr_defaults', context.state.node)));
}

@ -238,7 +238,7 @@ const visitors = {
}
}
if (relative_selector.selectors.every((s) => s.type === 'NestingSelector')) {
if (relative_selector.selectors.some((s) => s.type === 'NestingSelector')) {
continue;
}

@ -1851,18 +1851,27 @@ function serialize_element_attributes(node, context) {
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
if (attribute.name === 'value' && node.name === 'textarea') {
if (
attribute.value !== true &&
attribute.value[0].type === 'Text' &&
regex_starts_with_newline.test(attribute.value[0].data)
) {
// Two or more leading newlines are required to restore the leading newline immediately after `<textarea>`.
// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
// also see related code in analysis phase
attribute.value[0].data = '\n' + attribute.value[0].data;
if (attribute.name === 'value') {
if (node.name === 'textarea') {
if (
attribute.value !== true &&
attribute.value[0].type === 'Text' &&
regex_starts_with_newline.test(attribute.value[0].data)
) {
// Two or more leading newlines are required to restore the leading newline immediately after `<textarea>`.
// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
// also see related code in analysis phase
attribute.value[0].data = '\n' + attribute.value[0].data;
}
content = {
escape: true,
expression: serialize_attribute_value(attribute.value, context)
};
} else if (node.name !== 'select') {
// omit value attribute for select elements, it's irrelevant for the initially selected value and has no
// effect on the selected value after the user interacts with the select element (the value _property_ does, but not the attribute)
attributes.push(attribute);
}
content = { escape: true, expression: serialize_attribute_value(attribute.value, context) };
// omit event handlers except for special cases
} else if (is_event_attribute(attribute)) {

@ -5,6 +5,7 @@
* @property {string} [type] Set this to `set` if updates are written to the dom property
* @property {boolean} [omit_in_ssr] Set this to true if the binding should not be included in SSR
* @property {string[]} [valid_elements] If this is set, the binding is only valid on the given elements
* @property {string[]} [invalid_elements] If this is set, the binding is invalid on the given elements
*/
/**
@ -131,28 +132,36 @@ export const binding_properties = {
},
// dimensions
clientWidth: {
omit_in_ssr: true
omit_in_ssr: true,
invalid_elements: ['svelte:window', 'svelte:document']
},
clientHeight: {
omit_in_ssr: true
omit_in_ssr: true,
invalid_elements: ['svelte:window', 'svelte:document']
},
offsetWidth: {
omit_in_ssr: true
omit_in_ssr: true,
invalid_elements: ['svelte:window', 'svelte:document']
},
offsetHeight: {
omit_in_ssr: true
omit_in_ssr: true,
invalid_elements: ['svelte:window', 'svelte:document']
},
contentRect: {
omit_in_ssr: true
omit_in_ssr: true,
invalid_elements: ['svelte:window', 'svelte:document']
},
contentBoxSize: {
omit_in_ssr: true
omit_in_ssr: true,
invalid_elements: ['svelte:window', 'svelte:document']
},
borderBoxSize: {
omit_in_ssr: true
omit_in_ssr: true,
invalid_elements: ['svelte:window', 'svelte:document']
},
devicePixelContentBoxSize: {
omit_in_ssr: true
omit_in_ssr: true,
invalid_elements: ['svelte:window', 'svelte:document']
},
// checkbox/radio
indeterminate: {
@ -171,9 +180,15 @@ export const binding_properties = {
this: {
omit_in_ssr: true
},
innerText: {},
innerHTML: {},
textContent: {},
innerText: {
invalid_elements: ['svelte:window', 'svelte:document']
},
innerHTML: {
invalid_elements: ['svelte:window', 'svelte:document']
},
textContent: {
invalid_elements: ['svelte:window', 'svelte:document']
},
open: {
event: 'toggle',
type: 'set',

@ -40,7 +40,7 @@ export const DelegatedEvents = [
'contextmenu',
'focusin',
'focusout',
// 'input', This conflicts with bind:input
'input',
'keydown',
'keyup',
'mousedown',

@ -2,29 +2,67 @@ import { DEV } from 'esm-env';
import { hydrating } from '../hydration.js';
import { get_descriptors, get_prototype_of, map_get, map_set } from '../../utils.js';
import { AttributeAliases, DelegatedEvents, namespace_svg } from '../../../../constants.js';
import { delegate } from './events.js';
import { autofocus } from './misc.js';
import { create_event, delegate } from './events.js';
import { add_form_reset_listener, autofocus } from './misc.js';
import { effect, effect_root } from '../../reactivity/effects.js';
import * as w from '../../warnings.js';
import { LOADING_ATTR_SYMBOL } from '../../constants.js';
import { queue_idle_task } from '../task.js';
/**
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
* to remove it upon hydration to avoid a bug when someone resets the form value.
* @param {HTMLInputElement | HTMLSelectElement} dom
* @param {HTMLInputElement} dom
* @returns {void}
*/
export function remove_input_attr_defaults(dom) {
if (hydrating) {
// using getAttribute instead of dom.value allows us to have
// null instead of "on" if the user didn't set a value
const value = dom.getAttribute('value');
set_attribute(dom, 'value', null);
set_attribute(dom, 'checked', null);
if (value) dom.value = value;
let already_removed = false;
// We try and remove the default attributes later, rather than sync during hydration.
// Doing it sync during hydration has a negative impact on performance, but deferring the
// work in an idle task alleviates this greatly. If a form reset event comes in before
// the idle callback, then we ensure the input defaults are cleared just before.
const remove_defaults = () => {
if (already_removed) return;
already_removed = true;
const value = dom.getAttribute('value');
set_attribute(dom, 'value', null);
set_attribute(dom, 'checked', null);
if (value) dom.value = value;
};
// @ts-expect-error
dom.__on_r = remove_defaults;
queue_idle_task(remove_defaults);
add_form_reset_listener();
}
}
/**
* @param {Element} element
* @param {any} value
*/
export function set_value(element, value) {
// @ts-expect-error
var attributes = (element.__attributes ??= {});
if (attributes.value === (attributes.value = value)) return;
// @ts-expect-error
element.value = value;
}
/**
* @param {Element} element
* @param {boolean} checked
*/
export function set_checked(element, checked) {
// @ts-expect-error
var attributes = (element.__attributes ??= {});
if (attributes.checked === (attributes.checked = checked)) return;
// @ts-expect-error
element.checked = checked;
}
/**
* @param {Element} element
* @param {string} attribute
@ -151,9 +189,13 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha
if (!delegated) {
// we use `addEventListener` here because these events are not delegated
if (!prev) {
events.push([key, value, () => element.addEventListener(event_name, value, opts)]);
events.push([
key,
value,
() => (next[key] = create_event(event_name, element, value, opts))
]);
} else {
element.addEventListener(event_name, value, opts);
next[key] = create_event(event_name, element, value, opts);
}
} else {
// @ts-ignore

@ -1,4 +1,5 @@
import { render_effect } from '../../../reactivity/effects.js';
import { add_form_reset_listener } from '../misc.js';
/**
* Fires the handler once immediately (unless corresponding arg is set to `false`),
@ -26,8 +27,6 @@ export function listen(target, events, handler, call_handler_immediately = true)
});
}
let listening_to_form_reset = false;
/**
* Listen to the given event, and then instantiate a global form reset listener if not already done,
* to notify all bindings when the form is reset
@ -52,24 +51,5 @@ export function listen_to_event_and_reset_event(element, event, handler, on_rese
element.__on_r = on_reset;
}
if (!listening_to_form_reset) {
listening_to_form_reset = true;
document.addEventListener(
'reset',
(evt) => {
// Needs to happen one tick later or else the dom properties of the form
// elements have not updated to their reset values yet
Promise.resolve().then(() => {
if (!evt.defaultPrevented) {
for (const e of /**@type {HTMLFormElement} */ (evt.target).elements) {
// @ts-expect-error
e.__on_r?.();
}
}
});
},
// In the capture phase to guarantee we get noticed of it (no possiblity of stopPropagation)
{ capture: true }
);
}
add_form_reset_listener();
}

@ -1,7 +1,7 @@
import { STATE_SYMBOL } from '../../../constants.js';
import { effect, render_effect } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js';
import { queue_task } from '../../task.js';
import { queue_micro_task } from '../../task.js';
/**
* @param {any} bound_value
@ -49,7 +49,7 @@ export function bind_this(element_or_component, update, get_value, get_parts) {
return () => {
// We cannot use effects in the teardown phase, we we use a microtask instead.
queue_task(() => {
queue_micro_task(() => {
if (parts && is_bound_this(get_value(...parts), element_or_component)) {
update(null, ...parts);
}

@ -2,6 +2,7 @@ import { render_effect } from '../../reactivity/effects.js';
import { all_registered_events, root_event_handles } from '../../render.js';
import { define_property, is_array } from '../../utils.js';
import { hydrating } from '../hydration.js';
import { queue_micro_task } from '../task.js';
/**
* SSR adds onload and onerror attributes to catch those events before the hydration.
@ -34,18 +35,14 @@ export function replay_events(dom) {
* @param {string} event_name
* @param {Element} dom
* @param {EventListener} handler
* @param {boolean} capture
* @param {boolean} [passive]
* @returns {void}
* @param {AddEventListenerOptions} options
*/
export function event(event_name, dom, handler, capture, passive) {
var options = { capture, passive };
export function create_event(event_name, dom, handler, options) {
/**
* @this {EventTarget}
*/
function target_handler(/** @type {Event} */ event) {
if (!capture) {
if (!options.capture) {
// Only call in the bubble phase, else delegated events would be called before the capturing events
handle_event_propagation(dom, event);
}
@ -54,7 +51,32 @@ export function event(event_name, dom, handler, capture, passive) {
}
}
dom.addEventListener(event_name, target_handler, options);
// Chrome has a bug where pointer events don't work when attached to a DOM element that has been cloned
// with cloneNode() and the DOM element is disconnected from the document. To ensure the event works, we
// defer the attachment till after it's been appended to the document. TODO: remove this once Chrome fixes
// this bug.
if (event_name.startsWith('pointer')) {
queue_micro_task(() => {
dom.addEventListener(event_name, target_handler, options);
});
} else {
dom.addEventListener(event_name, target_handler, options);
}
return target_handler;
}
/**
* @param {string} event_name
* @param {Element} dom
* @param {EventListener} handler
* @param {boolean} capture
* @param {boolean} [passive]
* @returns {void}
*/
export function event(event_name, dom, handler, capture, passive) {
var options = { capture, passive };
var target_handler = create_event(event_name, dom, handler, options);
// @ts-ignore
if (dom === document.body || dom === window || dom === document) {

@ -31,3 +31,28 @@ export function remove_textarea_child(dom) {
clear_text_content(dom);
}
}
let listening_to_form_reset = false;
export function add_form_reset_listener() {
if (!listening_to_form_reset) {
listening_to_form_reset = true;
document.addEventListener(
'reset',
(evt) => {
// Needs to happen one tick later or else the dom properties of the form
// elements have not updated to their reset values yet
Promise.resolve().then(() => {
if (!evt.defaultPrevented) {
for (const e of /**@type {HTMLFormElement} */ (evt.target).elements) {
// @ts-expect-error
e.__on_r?.();
}
}
});
},
// In the capture phase to guarantee we get noticed of it (no possiblity of stopPropagation)
{ capture: true }
);
}
}

@ -1,33 +1,63 @@
import { run_all } from '../../shared/utils.js';
let is_task_queued = false;
// Fallback for when requestIdleCallback is not available
const request_idle_callback =
typeof requestIdleCallback === 'undefined'
? (/** @type {() => void} */ cb) => setTimeout(cb, 1)
: requestIdleCallback;
let is_micro_task_queued = false;
let is_idle_task_queued = false;
/** @type {Array<() => void>} */
let current_queued_miro_tasks = [];
/** @type {Array<() => void>} */
let current_queued_tasks = [];
let current_queued_idle_tasks = [];
function process_micro_tasks() {
is_micro_task_queued = false;
const tasks = current_queued_miro_tasks.slice();
current_queued_miro_tasks = [];
run_all(tasks);
}
function process_task() {
is_task_queued = false;
const tasks = current_queued_tasks.slice();
current_queued_tasks = [];
function process_idle_tasks() {
is_idle_task_queued = false;
const tasks = current_queued_idle_tasks.slice();
current_queued_idle_tasks = [];
run_all(tasks);
}
/**
* @param {() => void} fn
*/
export function queue_task(fn) {
if (!is_task_queued) {
is_task_queued = true;
queueMicrotask(process_task);
export function queue_micro_task(fn) {
if (!is_micro_task_queued) {
is_micro_task_queued = true;
queueMicrotask(process_micro_tasks);
}
current_queued_miro_tasks.push(fn);
}
/**
* @param {() => void} fn
*/
export function queue_idle_task(fn) {
if (!is_idle_task_queued) {
is_idle_task_queued = true;
request_idle_callback(process_idle_tasks);
}
current_queued_tasks.push(fn);
current_queued_idle_tasks.push(fn);
}
/**
* Synchronously run any queued tasks.
*/
export function flush_tasks() {
if (is_task_queued) {
process_task();
if (is_micro_task_queued) {
process_micro_tasks();
}
if (is_idle_task_queued) {
process_idle_tasks();
}
}

@ -27,7 +27,9 @@ export {
set_custom_element_data,
set_dynamic_element_attributes,
set_xlink_attribute,
handle_lazy_img
handle_lazy_img,
set_value,
set_checked
} from './dom/elements/attributes.js';
export { set_class, set_svg_class, set_mathml_class, toggle_class } from './dom/elements/class.js';
export { event, delegate, replay_events } from './dom/elements/events.js';

@ -420,8 +420,8 @@ function remove_reaction(signal, dependency) {
}
}
if (reactions_length === 0 && (dependency.f & UNOWNED) !== 0) {
// If the signal is unowned then we need to make sure to change it to dirty.
set_signal_status(dependency, DIRTY);
// If the signal is unowned then we need to make sure to change it to maybe dirty.
set_signal_status(dependency, MAYBE_DIRTY);
remove_reactions(/** @type {import('#client').Derived} **/ (dependency), 0);
}
}

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

@ -4,6 +4,6 @@ export default test({
error: {
code: 'attribute_duplicate',
message: 'Attributes need to be unique',
position: [17, 17]
position: [17, 25]
}
});

@ -4,6 +4,6 @@ export default test({
error: {
code: 'attribute_duplicate',
message: 'Attributes need to be unique',
position: [17, 17]
position: [17, 24]
}
});

@ -4,6 +4,6 @@ export default test({
error: {
code: 'attribute_duplicate',
message: 'Attributes need to be unique',
position: [17, 17]
position: [17, 28]
}
});

@ -0,0 +1,6 @@
.foo.svelte-xyz, /* some comment */
.bar.svelte-xyz /* some other comment */
{
color: red;
}

@ -0,0 +1,10 @@
<div class="foo">foo</div>
<div class="bar">bar</div>
<style>
.foo, /* some comment */
.bar /* some other comment */
{
color: red;
}
</style>

@ -75,29 +75,29 @@ export default test({
{
code: 'css_unused_selector',
end: {
character: 634,
character: 668,
column: 5,
line: 66
line: 70
},
message: 'Unused CSS selector "&.b"',
start: {
character: 631,
character: 665,
column: 2,
line: 66
line: 70
}
},
{
code: 'css_unused_selector',
end: {
character: 666,
character: 700,
column: 9,
line: 70
line: 74
},
message: 'Unused CSS selector ".unused"',
start: {
character: 659,
character: 693,
column: 2,
line: 70
line: 74
}
}
]

@ -36,6 +36,10 @@
}*/
}
&:hover {
color: green;
}
& & {
color: green;
}

@ -50,6 +50,10 @@
}
}
&:hover {
color: green;
}
& & {
color: green;
}

@ -0,0 +1,7 @@
<script>
let count = 0;
$: doubled = count * 2
$: ({ quadrupled } = { quadrupled: count * 4 })
</script>
{count} / {doubled} / {quadrupled}

@ -0,0 +1,7 @@
<script>
let count = 0;
let doubled = $derived(count * 2);
let { quadrupled } = $derived({ quadrupled: count * 4 });
</script>
{count} / {doubled} / {quadrupled}

@ -14,11 +14,6 @@ export default test({
<p>selected: two</p>
`,
ssrHtml: `
<select value="two"></select>
<p>selected: two</p>
`,
async test({ assert, component, target }) {
component.items = ['one', 'two', 'three'];

@ -0,0 +1,34 @@
import { test, ok } from '../../test';
import { flushSync } from 'svelte';
export default test({
mode: ['client'],
async test({ assert, target }) {
/**
* @type {HTMLInputElement | null}
*/
const input = target.querySelector('input[type=text]');
const button = target.querySelector('button');
/**
* @type {HTMLInputElement | null}
*/
const checkbox = target.querySelector('input[type=checkbox]');
const textarea = target.querySelector('textarea');
ok(input);
ok(button);
ok(checkbox);
ok(textarea);
flushSync(() => {
input.value = 'foo';
checkbox.click();
textarea.innerHTML = 'bar';
button.click();
});
assert.equal(input.value, 'foo');
assert.equal(checkbox.checked, true);
assert.equal(textarea.innerHTML, 'bar');
}
});

@ -0,0 +1,13 @@
<script>
let count = 0;
let value = { value: "" };
let checked = { value: false };
</script>
<input type="text" value={value.value} />
<textarea value={value.value}></textarea>
<input type="checkbox" checked={checked.value} />
<button on:click={()=>count++}>{count}</button>

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
async test({ assert, target, logs }) {
let [btn1] = target.querySelectorAll('button');
flushSync(() => {
btn1.click();
});
flushSync(() => {
btn1.click();
});
flushSync(() => {
btn1.click();
});
assert.deepEqual(logs, ['recalculating']);
}
});

@ -0,0 +1,18 @@
<script context="module">
let visible = $state(true);
function toggleVisibility() {
visible = !visible;
}
let unchangedState = $state("unchanged state");
let derived = $derived.by(() => {
console.log("recalculating");
return unchangedState;
});
</script>
<button onclick={toggleVisibility}>Toggle Visibility</button>
{#if visible}
<p>{derived}</p>
{/if}

@ -0,0 +1,34 @@
import { test, ok } from '../../test';
import { flushSync } from 'svelte';
export default test({
mode: ['client'],
async test({ assert, target }) {
/**
* @type {HTMLInputElement | null}
*/
const input = target.querySelector('input[type=text]');
const button = target.querySelector('button');
/**
* @type {HTMLInputElement | null}
*/
const checkbox = target.querySelector('input[type=checkbox]');
const textarea = target.querySelector('textarea');
ok(input);
ok(button);
ok(checkbox);
ok(textarea);
flushSync(() => {
input.value = 'foo';
checkbox.click();
textarea.innerHTML = 'bar';
button.click();
});
assert.equal(input.value, 'foo');
assert.equal(checkbox.checked, true);
assert.equal(textarea.innerHTML, 'bar');
}
});

@ -0,0 +1,17 @@
<script>
let count = $state(0);
let value = $state({
value: "",
});
let checked = $state({
checked: false,
});
</script>
<input type="text" {...value} />
<textarea {...value}></textarea>
<input type="checkbox" {...checked} />
<button onclick={()=>count++}>{count}</button>

@ -0,0 +1,34 @@
import { test, ok } from '../../test';
import { flushSync } from 'svelte';
export default test({
mode: ['client'],
async test({ assert, target }) {
/**
* @type {HTMLInputElement | null}
*/
const input = target.querySelector('input[type=text]');
const button = target.querySelector('button');
/**
* @type {HTMLInputElement | null}
*/
const checkbox = target.querySelector('input[type=checkbox]');
const textarea = target.querySelector('textarea');
ok(input);
ok(button);
ok(checkbox);
ok(textarea);
flushSync(() => {
input.value = 'foo';
checkbox.click();
textarea.innerHTML = 'bar';
button.click();
});
assert.equal(input.value, 'foo');
assert.equal(checkbox.checked, true);
assert.equal(textarea.innerHTML, 'bar');
}
});

@ -0,0 +1,13 @@
<script>
let count = $state(0);
let value = $state("");
let checked = $state(false);
</script>
<input type="text" {value} />
<textarea {value}></textarea>
<input type="checkbox" {checked} />
<button onclick={()=>count++}>{count}</button>

@ -0,0 +1,14 @@
[
{
"code": "bind_invalid_name",
"message": "`bind:clientWidth` is not a valid binding. Possible bindings for <svelte:document> are focused, fullscreenElement, visibilityState, this",
"start": {
"line": 5,
"column": 17
},
"end": {
"line": 5,
"column": 39
}
}
]

@ -0,0 +1,5 @@
<script>
let foo;
</script>
<svelte:document bind:clientWidth={foo} />

@ -0,0 +1,14 @@
[
{
"code": "bind_invalid_name",
"message": "`bind:clientWidth` is not a valid binding. Possible bindings for <svelte:window> are focused, innerWidth, innerHeight, outerWidth, outerHeight, scrollX, scrollY, online, devicePixelRatio, this",
"start": {
"line": 5,
"column": 15
},
"end": {
"line": 5,
"column": 37
}
}
]

@ -0,0 +1,5 @@
<script>
let foo;
</script>
<svelte:window bind:clientWidth={foo} />

@ -1,4 +1,5 @@
src/*
dist/*
dist/client/*
dist/server/*
!src/entry-client.ts
!src/entry-server.ts

@ -0,0 +1,22 @@
import fs from 'node:fs';
import path from 'node:path';
import express from 'express';
import { head, html } from './server/entry-server.js';
const rendered = fs
.readFileSync(path.resolve('./dist/client/index.html'), 'utf-8')
.replace(`<!--ssr-html-->`, html)
.replace(`<!--ssr-head-->`, head);
express()
.use('*', async (req, res) => {
if (req.originalUrl !== '/') {
res.sendFile(path.resolve('./dist/client' + req.originalUrl));
return;
}
res.status(200).set({ 'Content-Type': 'text/html' }).end(rendered);
})
.listen('3000');
console.log('listening on http://localhost:3000');

@ -7,7 +7,8 @@
"prepare": "node scripts/create-app-svelte.js",
"dev": "vite --host",
"ssr": "node ./server.js",
"build": "vite build",
"build": "vite build --outDir dist/client && vite build --outDir dist/server --ssr src/entry-server.ts",
"prod": "npm run build && node dist",
"preview": "vite preview"
},
"devDependencies": {

@ -3,6 +3,9 @@ import inspect from 'vite-plugin-inspect';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
build: {
minify: false
},
plugins: [inspect(), svelte()],
optimizeDeps: {
// svelte is a local workspace package, optimizing it would require dev server restarts with --force for every change

@ -223,8 +223,8 @@ importers:
specifier: ^6.0.0
version: 6.0.0(@codemirror/autocomplete@6.12.0)(@codemirror/lang-css@6.2.1)(@codemirror/lang-html@6.4.9)(@codemirror/lang-javascript@6.2.1)(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.24.0)(@lezer/common@1.2.1)(@lezer/highlight@1.2.0)(@lezer/javascript@1.4.15)(@lezer/lr@1.4.0)
'@rich_harris/svelte-split-pane':
specifier: ^1.1.2
version: 1.1.2(svelte@packages+svelte)
specifier: ^1.1.3
version: 1.1.3(svelte@packages+svelte)
'@rollup/browser':
specifier: ^3.28.0
version: 3.29.4
@ -2320,16 +2320,16 @@ packages:
'@resvg/resvg-js-win32-x64-msvc': 2.6.0
dev: true
/@rich_harris/svelte-split-pane@1.1.1(svelte@4.2.9):
resolution: {integrity: sha512-y2RRLyrN6DCeIgwA423aAIv/T5JqQeOl2XogBQ/21DvA2IF7oyrLUtXMxmQL2va2NFdeJO6MDx6nDX5X7kau7A==}
/@rich_harris/svelte-split-pane@1.1.2(svelte@4.2.9):
resolution: {integrity: sha512-O601UlgGzrn6Nva7uLAF25h63wvrI2rxDh5KTEXd0pdTP7LetHR/rqgi8jyPIgRDCL86eoopEdC3ejOOQoWGWQ==}
peerDependencies:
svelte: ^3.54.0
svelte: ^3.54.0 || ^4.0.0 || ^5.0.0-next.0
dependencies:
svelte: 4.2.9
dev: false
/@rich_harris/svelte-split-pane@1.1.2(svelte@packages+svelte):
resolution: {integrity: sha512-O601UlgGzrn6Nva7uLAF25h63wvrI2rxDh5KTEXd0pdTP7LetHR/rqgi8jyPIgRDCL86eoopEdC3ejOOQoWGWQ==}
/@rich_harris/svelte-split-pane@1.1.3(svelte@packages+svelte):
resolution: {integrity: sha512-eziKez1ncDfLqJQsViwLG2rYNfMEa3pYBKFUBfNTChgT5lUnofm5IDHxupAKklKvRpTXCVhQXb1MxLUfj5UgFQ==}
peerDependencies:
svelte: ^3.54.0 || ^4.0.0 || ^5.0.0-next.0
dependencies:
@ -2739,7 +2739,7 @@ packages:
'@neocodemirror/svelte': 0.0.15(@codemirror/autocomplete@6.12.0)(@codemirror/commands@6.3.3)(@codemirror/language@6.10.1)(@codemirror/lint@6.5.0)(@codemirror/search@6.5.6)(@codemirror/state@6.4.0)(@codemirror/view@6.24.0)
'@replit/codemirror-lang-svelte': 6.0.0(@codemirror/autocomplete@6.12.0)(@codemirror/lang-css@6.2.1)(@codemirror/lang-html@6.4.9)(@codemirror/lang-javascript@6.2.1)(@codemirror/language@6.10.1)(@codemirror/state@6.4.0)(@codemirror/view@6.24.0)(@lezer/common@1.2.1)(@lezer/highlight@1.2.0)(@lezer/javascript@1.4.15)(@lezer/lr@1.4.0)
'@replit/codemirror-vim': 6.1.0(@codemirror/commands@6.3.3)(@codemirror/language@6.10.1)(@codemirror/search@6.5.6)(@codemirror/state@6.4.0)(@codemirror/view@6.24.0)
'@rich_harris/svelte-split-pane': 1.1.1(svelte@4.2.9)
'@rich_harris/svelte-split-pane': 1.1.2(svelte@4.2.9)
'@rollup/browser': 3.29.4
'@sveltejs/site-kit': 5.2.2(@sveltejs/kit@2.4.3)(svelte@4.2.9)
acorn: 8.11.3

@ -46,7 +46,7 @@
"@lezer/highlight": "^1.1.6",
"@neocodemirror/svelte": "0.0.15",
"@replit/codemirror-lang-svelte": "^6.0.0",
"@rich_harris/svelte-split-pane": "^1.1.2",
"@rich_harris/svelte-split-pane": "^1.1.3",
"@rollup/browser": "^3.28.0",
"acorn": "^8.10.0",
"codemirror": "^6.0.1",

@ -246,6 +246,8 @@
<style>
.codemirror-container {
--warning: hsl(40 100% 70%);
--error: hsl(0 100% 90%);
position: relative;
width: 100%;
height: 100%;
@ -254,26 +256,134 @@
overflow: hidden;
}
.codemirror-container :global(.mark-text) {
background-color: var(--sk-selection-color);
backdrop-filter: opacity(40%);
:global(.dark) .codemirror-container {
--warning: hsl(40 100% 50%);
--error: hsl(0 100% 70%);
}
.codemirror-container :global(.cm-editor) {
height: 100%;
}
.codemirror-container :global {
* {
font: 400 var(--sk-text-xs) / 1.7 var(--sk-font-mono);
}
.codemirror-container :global(*) {
font: 400 var(--sk-text-xs) / 1.7 var(--sk-font-mono) !important;
}
.mark-text {
background-color: var(--sk-selection-color);
backdrop-filter: opacity(40%);
}
.codemirror-container :global(.error-loc) {
position: relative;
border-bottom: 2px solid #da106e;
}
.cm-editor {
height: 100%;
}
.error-loc {
position: relative;
border-bottom: 2px solid #da106e;
}
.error-line {
background-color: rgba(200, 0, 0, 0.05);
}
.codemirror-container :global(.error-line) {
background-color: rgba(200, 0, 0, 0.05);
.cm-tooltip {
border: none;
background: var(--sk-back-3);
font-family: var(--sk-font);
max-width: calc(100vw - 10em);
position: relative;
filter: drop-shadow(2px 4px 6px rgba(0, 0, 0, 0.1));
}
.cm-tooltip-section {
position: relative;
padding: 0.5em;
left: -13px;
background: var(--bg);
border-radius: 2px;
max-width: 64em;
}
.cm-tooltip-section::before {
content: '';
position: absolute;
left: 10px;
width: 8px;
height: 8px;
transform: rotate(45deg);
background-color: var(--bg);
border-radius: 2px;
}
.cm-tooltip-below .cm-tooltip-section {
top: 10px;
}
.cm-tooltip-above .cm-tooltip-section {
bottom: 10px;
}
.cm-tooltip-below .cm-tooltip-section::before {
top: -4px;
}
.cm-tooltip-above .cm-tooltip-section::before {
bottom: -4px;
}
.cm-tooltip:has(.cm-diagnostic) {
background: transparent;
}
.cm-tooltip:has(.cm-diagnostic-warning) {
--bg: var(--warning);
--fg: #222;
}
.cm-tooltip:has(.cm-diagnostic-error) {
--bg: var(--error);
--fg: #222;
}
.cm-diagnostic {
padding: 0.2em 0.4em;
position: relative;
border: none;
border-radius: 2px;
}
.cm-diagnostic:not(:last-child) {
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.cm-diagnostic-error {
border: none;
filter: drop-shadow(0px 0px 6px var(--error-bg));
}
.cm-diagnostic :not(code) {
font-family: var(--sk-font);
}
.cm-diagnosticText {
color: var(--fg);
position: relative;
z-index: 2;
}
.cm-diagnosticText code {
color: inherit;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 2px;
top: 0;
padding: 0.2em;
font-size: 0.9em;
}
.cm-diagnosticText strong {
font-size: 0.9em;
/* font-weight: 700; */
font-family: var(--sk-font-mono);
opacity: 0.7;
}
}
pre {

@ -22,7 +22,6 @@
<div class="editor notranslate" translate="no">
<CodeMirror
bind:this={$module_editor}
{autocomplete}
diagnostics={() => {
if (error) {
return [
@ -30,7 +29,16 @@
severity: 'error',
from: error.position[0],
to: error.position[1],
message: error.message
message: error.message,
renderMessage: () => {
// TODO expose error codes, so we can link to docs in future
const span = document.createElement('span');
span.innerHTML = `${error.message
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/`(.+?)`/g, `<code>$1</code>`)}`;
return span;
}
}
];
}
@ -40,7 +48,15 @@
severity: 'warning',
from: warning.start.character,
to: warning.end.character,
message: warning.message
message: warning.message,
renderMessage: () => {
const span = document.createElement('span');
span.innerHTML = `${warning.message
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/`(.+?)`/g, `<code>$1</code>`)} <strong>(${warning.code})</strong>`;
return span;
}
}));
}

@ -5,7 +5,7 @@ import { tags as t } from '@lezer/highlight';
const ERROR_HUE = 0;
const WARNING_HUE = 40;
const WARNING_FG = `hsl(${WARNING_HUE} 100% 40%)`;
const WARNING_FG = `hsl(${WARNING_HUE} 100% 60%)`;
const WARNING_BG = `hsl(${WARNING_HUE} 100% 40% / 0.5)`;
const ERROR_FG = `hsl(${ERROR_HUE} 100% 40%)`;
@ -80,28 +80,6 @@ const svelteThemeStyles = EditorView.theme(
color: '#ddd'
},
'.cm-tooltip': {
border: 'none',
backgroundColor: 'var(--sk-back-3)'
},
'.cm-diagnostic': {
padding: '0.2em 0.4em',
backgroundColor: 'var(--sk-back-3)',
color: 'var(--sk-text-1)',
border: 'none',
borderRadius: '2px',
position: 'relative',
top: '2px',
zIndex: 2
},
'.cm-diagnostic-error': {
border: `1px solid ${ERROR_FG}`,
filter: `drop-shadow(0px 0px 6px ${ERROR_BG})`
},
'.cm-diagnostic-warning': {
border: `1px solid ${WARNING_FG}`,
filter: `drop-shadow(0px 0px 6px ${WARNING_BG})`
},
// https://github.com/codemirror/lint/blob/271b35f5d31a7e3645eaccbfec608474022098e1/src/lint.ts#L620
'.cm-lintRange': {
backgroundPosition: 'left bottom',

@ -3,24 +3,26 @@
import '@sveltejs/site-kit/styles/index.css';
import Repl from '$lib/Repl.svelte';
import { onMount } from 'svelte';
import { default_files } from './defaults.js';
import { compress_and_encode_text, decode_and_decompress_text } from './gzip.js';
import { afterNavigate } from '$app/navigation';
/** @type {Repl} */
let repl;
let setting_hash = false;
let started = false;
let navigating = false;
onMount(change_from_hash);
afterNavigate(change_from_hash);
async function change_from_hash() {
navigating = true;
const hash = location.hash.slice(1);
if (!hash) {
repl.set({
files: default_files
files: default_files()
});
return;
@ -56,8 +58,8 @@
/** @param {CustomEvent<any>} e*/
async function change_from_editor(e) {
if (!started) {
started = true; // ignore initial change caused by the repl.set in change_from_hash
if (navigating) {
navigating = false;
return;
}
@ -76,7 +78,7 @@
</script>
<svelte:window
on:hashchange={(e) => {
on:hashchange={() => {
if (!setting_hash) {
change_from_hash();
}

@ -1,4 +1,4 @@
export const default_files = [
export const default_files = () => [
{
name: 'App',
type: 'svelte',

Loading…
Cancel
Save