diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 00a9d1fab8..2fdc54a09f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -437,6 +437,11 @@ export function client_component(source, analysis, options) { } } + if (options.dev && state.options.filename) { + body.unshift(b.stmt(b.call(b.id('$.push_module'), b.literal(state.options.filename)))); + body.push(b.stmt(b.call(b.id('$.pop_module'), b.literal(state.options.filename)))); + } + return { type: 'Program', sourceType: 'module', diff --git a/packages/svelte/src/internal/client/dev.js b/packages/svelte/src/internal/client/dev.js new file mode 100644 index 0000000000..1fcb7bb15a --- /dev/null +++ b/packages/svelte/src/internal/client/dev.js @@ -0,0 +1,74 @@ +/** @typedef {{ file: string, line: number, column: number }} Location */ + +/** @type {Record>} */ +const boundaries = {}; + +const chrome_pattern = /\((.+):(\d+):(\d+)\)$/; +const firefox_pattern = /@(.+):(\d+):(\d+)$/; + +export function get_stack() { + const stack = new Error().stack; + if (!stack) return null; + + const entries = []; + + for (const line of stack.split('\n').slice(1)) { + let match = chrome_pattern.exec(line) ?? firefox_pattern.exec(line); + + if (match) { + entries.push({ + file: match[1], + line: +match[2], + column: +match[3] + }); + } + } + + return entries; +} + +export function get_module() { + const stack = get_stack(); + if (!stack) return null; + + for (const entry of stack) { + if (entry) { + const modules = boundaries[entry.file]; + for (const module of modules) { + if (module.start.line < entry.line && module.end.line > entry.line) { + return module.filename; + } + } + } + } + + return null; +} + +/** + * @param {string} filename The original `path/to/Blah.svelte` filename + */ +export function push_module(filename) { + const start = get_stack()?.[1]; + + if (start) { + (boundaries[start.file] ??= []).push({ + start, + // @ts-expect-error + end: null, + filename + }); + } +} + +/** + * @param {string} filename + */ +export function pop_module(filename) { + const end = get_stack()?.[1]; + + if (end) { + // @ts-expect-error + boundaries[end.file].at(-1).end = end; + } +} diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 2d76c78a5f..dfc76dcdef 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -20,6 +20,7 @@ import { is_frozen, object_prototype } from './utils.js'; +import { get_module } from './dev.js'; export const STATE_SYMBOL = Symbol('$state'); @@ -52,7 +53,8 @@ export function proxy(value, immutable = true) { a: is_array(value), i: immutable, p: proxy, - t: value + t: value, + o: DEV ? get_module() : '' }), writable: true, enumerable: false @@ -249,6 +251,13 @@ const state_proxy_handler = { // @ts-ignore target[prop] = value; + if (DEV) { + const site = get_module(); + if (site !== metadata.o) { + console.error(`mutating state outside the component where it was created: ${site}`); + } + } + if (not_has) { // If we have mutated an array directly, we might need to // signal that length has also changed. Do it before updating metadata diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 8996e92e40..823e8a749d 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -406,6 +406,8 @@ export interface ProxyMetadata> { p: ProxyStateObject; /** The original target this proxy was created for */ t: T; + /** The component that 'owns' this state, if any */ + o: string; } export type ProxyStateObject> = T & { diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js index 855fa43ced..49baca43de 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -40,6 +40,7 @@ export { freeze, init } from './client/runtime.js'; +export * from './client/dev.js'; export * from './client/each.js'; export * from './client/render.js'; export * from './client/validate.js';