fix: reflect SvelteURLSearchParams changes to SvelteURL (#12285)

* separated url-search params and url classes and added more tests

* making URL aware of SvelteURLSearchParams changes so they are in sync

* added changeset

* generated types

* bail out if url.sp and SvelteSp are already in sync

* sync search on searchParams change because of how node18 handles it

* use ts-expect-error instead of ts-ignore

* remove a bit of indirection

* tweak

* fix

* regenerate types

* short-circuit in both directions

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/12387/head
FoHoOV 4 months ago committed by GitHub
parent d9860941eb
commit b2d106b67d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: reflect SvelteURLSearchParams changes to SvelteURL

@ -1,7 +1,8 @@
export { SvelteDate } from './date.js';
export { SvelteSet } from './set.js';
export { SvelteMap } from './map.js';
export { SvelteURL, SvelteURLSearchParams } from './url.js';
export { SvelteURL } from './url.js';
export { SvelteURLSearchParams } from './url-search-params.js';
/** @deprecated Use `SvelteDate` instead */
export function Date() {

@ -0,0 +1,146 @@
import { source } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js';
import { get_current_url } from './url.js';
import { increment } from './utils.js';
export const REPLACE = Symbol();
export class SvelteURLSearchParams extends URLSearchParams {
#version = source(0);
#url = get_current_url();
#updating = false;
#update_url() {
if (!this.#url || this.#updating) return;
this.#updating = true;
const search = this.toString();
this.#url.search = search && `?${search}`;
this.#updating = false;
}
/**
* @param {URLSearchParams} params
*/
[REPLACE](params) {
if (this.#updating) return;
this.#updating = true;
for (const key of [...super.keys()]) {
super.delete(key);
}
for (const [key, value] of params) {
super.append(key, value);
}
increment(this.#version);
this.#updating = false;
}
/**
* @param {string} name
* @param {string} value
* @returns {void}
*/
append(name, value) {
super.append(name, value);
this.#update_url();
increment(this.#version);
}
/**
* @param {string} name
* @param {string=} value
* @returns {void}
*/
delete(name, value) {
var has_value = super.has(name, value);
super.delete(name, value);
if (has_value) {
this.#update_url();
increment(this.#version);
}
}
/**
* @param {string} name
* @returns {string|null}
*/
get(name) {
get(this.#version);
return super.get(name);
}
/**
* @param {string} name
* @returns {string[]}
*/
getAll(name) {
get(this.#version);
return super.getAll(name);
}
/**
* @param {string} name
* @param {string=} value
* @returns {boolean}
*/
has(name, value) {
get(this.#version);
return super.has(name, value);
}
keys() {
get(this.#version);
return super.keys();
}
/**
* @param {string} name
* @param {string} value
* @returns {void}
*/
set(name, value) {
var previous = super.getAll(name).join('');
super.set(name, value);
// can't use has(name, value), because for something like https://svelte.dev?foo=1&bar=2&foo=3
// if you set `foo` to 1, then foo=3 gets deleted whilst `has("foo", "1")` returns true
if (previous !== super.getAll(name).join('')) {
this.#update_url();
increment(this.#version);
}
}
sort() {
super.sort();
this.#update_url();
increment(this.#version);
}
toString() {
get(this.#version);
return super.toString();
}
values() {
get(this.#version);
return super.values();
}
entries() {
get(this.#version);
return super.entries();
}
[Symbol.iterator]() {
return this.entries();
}
get size() {
get(this.#version);
return super.size;
}
}

@ -0,0 +1,208 @@
import { render_effect, effect_root } from '../internal/client/reactivity/effects.js';
import { flushSync } from '../index-client.js';
import { assert, test } from 'vitest';
import { SvelteURLSearchParams } from './url-search-params';
test('new URLSearchParams', () => {
const params = new SvelteURLSearchParams('a=b');
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(params.toString());
});
});
flushSync(() => {
params.set('a', 'c');
});
flushSync(() => {
// nothing should happen here
params.set('a', 'c');
});
assert.deepEqual(log, ['a=b', 'a=c']);
cleanup();
});
test('URLSearchParams.set', () => {
const params = new SvelteURLSearchParams();
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(params.toString());
});
});
flushSync(() => {
params.set('a', 'b');
});
flushSync(() => {
params.set('a', 'c');
});
flushSync(() => {
// nothing should happen here
params.set('a', 'c');
});
assert.deepEqual(log, ['', 'a=b', 'a=c']);
cleanup();
});
test('URLSearchParams.append', () => {
const params = new SvelteURLSearchParams();
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(params.toString());
});
});
flushSync(() => {
params.append('a', 'b');
});
flushSync(() => {
// nothing should happen here
params.set('a', 'b');
});
flushSync(() => {
params.append('a', 'c');
});
assert.deepEqual(log, ['', 'a=b', 'a=b&a=c']);
cleanup();
});
test('URLSearchParams.delete', () => {
const params = new SvelteURLSearchParams('a=b&c=d');
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(params.toString());
});
});
flushSync(() => {
params.delete('a');
});
flushSync(() => {
// nothing should happen here
params.delete('a');
});
flushSync(() => {
params.set('a', 'b');
});
assert.deepEqual(log, ['a=b&c=d', 'c=d', 'c=d&a=b']);
cleanup();
});
test('URLSearchParams.get', () => {
const params = new SvelteURLSearchParams('a=b&c=d');
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(params.get('a'));
});
render_effect(() => {
log.push(params.get('c'));
});
render_effect(() => {
log.push(params.get('e'));
});
});
flushSync(() => {
params.set('a', 'b');
});
flushSync(() => {
params.set('a', 'new-b');
});
flushSync(() => {
params.delete('a');
});
assert.deepEqual(log, ['b', 'd', null, 'new-b', 'd', null, null, 'd', null]);
cleanup();
});
test('URLSearchParams.getAll', () => {
const params = new SvelteURLSearchParams('a=b&c=d');
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(params.getAll('a'));
});
render_effect(() => {
log.push(params.getAll('q'));
});
});
flushSync(() => {
params.append('a', 'b1');
});
flushSync(() => {
params.append('q', 'z');
});
assert.deepEqual(log, [
// initial
['b'],
[],
// first flush
['b', 'b1'],
[],
// second flush
['b', 'b1'],
['z']
]);
cleanup();
});
test('URLSearchParams.toString', () => {
const params = new SvelteURLSearchParams();
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(params.toString());
});
});
flushSync(() => {
params.set('a', 'b');
});
flushSync(() => {
params.append('a', 'c');
});
assert.deepEqual(log, ['', 'a=b', 'a=b&a=c']);
cleanup();
});
test('SvelteURLSearchParams instanceof URLSearchParams', () => {
assert.ok(new SvelteURLSearchParams() instanceof URLSearchParams);
});

@ -1,8 +1,14 @@
import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js';
import { increment } from './utils.js';
import { REPLACE, SvelteURLSearchParams } from './url-search-params.js';
const REPLACE = Symbol();
/** @type {SvelteURL | null} */
let current_url = null;
export function get_current_url() {
// ideally we'd just export `current_url` directly, but it seems Vitest doesn't respect live bindings
return current_url;
}
export class SvelteURL extends URL {
#protocol = source(super.protocol);
@ -12,7 +18,8 @@ export class SvelteURL extends URL {
#port = source(super.port);
#pathname = source(super.pathname);
#hash = source(super.hash);
#searchParams = new SvelteURLSearchParams();
#search = source(super.search);
#searchParams;
/**
* @param {string | URL} url
@ -21,7 +28,10 @@ export class SvelteURL extends URL {
constructor(url, base) {
url = new URL(url, base);
super(url);
this.#searchParams[REPLACE](url.searchParams);
current_url = this;
this.#searchParams = new SvelteURLSearchParams(url.searchParams);
current_url = null;
}
get hash() {
@ -62,7 +72,7 @@ export class SvelteURL extends URL {
get(this.#port);
get(this.#pathname);
get(this.#hash);
this.#searchParams.toString();
get(this.#search);
return super.href;
}
@ -75,6 +85,7 @@ export class SvelteURL extends URL {
set(this.#port, super.port);
set(this.#pathname, super.pathname);
set(this.#hash, super.hash);
set(this.#search, super.search);
this.#searchParams[REPLACE](super.searchParams);
}
@ -115,13 +126,13 @@ export class SvelteURL extends URL {
}
get search() {
const search = this.#searchParams?.toString();
return search ? `?${search}` : '';
return get(this.#search);
}
set search(value) {
super.search = value;
this.#searchParams[REPLACE](new URLSearchParams(value.replace(/^\?/, '')));
set(this.#search, value);
this.#searchParams[REPLACE](super.searchParams);
}
get username() {
@ -152,114 +163,3 @@ export class SvelteURL extends URL {
return this.href;
}
}
export class SvelteURLSearchParams extends URLSearchParams {
#version = source(0);
/**
* @param {URLSearchParams} params
*/
[REPLACE](params) {
for (const key of [...super.keys()]) {
super.delete(key);
}
for (const [key, value] of params) {
super.append(key, value);
}
increment(this.#version);
}
/**
* @param {string} name
* @param {string} value
* @returns {void}
*/
append(name, value) {
increment(this.#version);
return super.append(name, value);
}
/**
* @param {string} name
* @param {string=} value
* @returns {void}
*/
delete(name, value) {
increment(this.#version);
return super.delete(name, value);
}
/**
* @param {string} name
* @returns {string|null}
*/
get(name) {
get(this.#version);
return super.get(name);
}
/**
* @param {string} name
* @returns {string[]}
*/
getAll(name) {
get(this.#version);
return super.getAll(name);
}
/**
* @param {string} name
* @param {string=} value
* @returns {boolean}
*/
has(name, value) {
get(this.#version);
return super.has(name, value);
}
keys() {
get(this.#version);
return super.keys();
}
/**
* @param {string} name
* @param {string} value
* @returns {void}
*/
set(name, value) {
increment(this.#version);
return super.set(name, value);
}
sort() {
increment(this.#version);
return super.sort();
}
toString() {
get(this.#version);
return super.toString();
}
values() {
get(this.#version);
return super.values();
}
entries() {
get(this.#version);
return super.entries();
}
[Symbol.iterator]() {
return this.entries();
}
get size() {
get(this.#version);
return super.size;
}
}

@ -1,10 +1,10 @@
import { render_effect, effect_root } from '../internal/client/reactivity/effects.js';
import { flushSync } from '../index-client.js';
import { SvelteURL, SvelteURLSearchParams } from './url.js';
import { SvelteURL } from './url.js';
import { assert, test } from 'vitest';
test('url.hash', () => {
const url = new SvelteURL('http://google.com');
const url = new SvelteURL('https://svelte.dev');
const log: any = [];
const cleanup = effect_root(() => {
@ -18,7 +18,7 @@ test('url.hash', () => {
});
flushSync(() => {
url.href = 'http://google.com/a/b/c#def';
url.href = 'https://svelte.dev/a/b/c#def';
});
flushSync(() => {
@ -31,6 +31,44 @@ test('url.hash', () => {
cleanup();
});
test('url.href', () => {
const url = new SvelteURL('https://svelte.dev?foo=bar&t=123');
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(url.href);
});
});
flushSync(() => {
url.search = '?q=kit&foo=baz';
});
flushSync(() => {
// changes from searchParams should be synced to URL instance as well
url.searchParams.append('foo', 'qux');
});
flushSync(() => {
url.searchParams.delete('foo');
});
flushSync(() => {
url.searchParams.set('love', 'svelte5');
});
assert.deepEqual(log, [
'https://svelte.dev/?foo=bar&t=123',
'https://svelte.dev/?q=kit&foo=baz',
'https://svelte.dev/?q=kit&foo=baz&foo=qux',
'https://svelte.dev/?q=kit',
'https://svelte.dev/?q=kit&love=svelte5'
]);
cleanup();
});
test('url.searchParams', () => {
const url = new SvelteURL('https://svelte.dev?foo=bar&t=123');
const log: any = [];
@ -77,25 +115,6 @@ test('url.searchParams', () => {
cleanup();
});
test('URLSearchParams', () => {
const params = new SvelteURLSearchParams();
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(params.toString());
});
});
flushSync(() => {
params.set('a', 'b');
});
flushSync(() => {
params.append('a', 'c');
});
assert.deepEqual(log, ['', 'a=b', 'a=b&a=c']);
cleanup();
test('SvelteURL instanceof URL', () => {
assert.ok(new SvelteURL('https://svelte.dev') instanceof URL);
});

@ -2146,12 +2146,12 @@ declare module 'svelte/reactivity' {
get searchParams(): SvelteURLSearchParams;
#private;
}
const REPLACE: unique symbol;
export class SvelteURLSearchParams extends URLSearchParams {
[REPLACE](params: URLSearchParams): void;
#private;
}
const REPLACE: unique symbol;
export { Date_1 as Date, Set_1 as Set, Map_1 as Map, URL_1 as URL, URLSearchParams_1 as URLSearchParams };
}

Loading…
Cancel
Save