@nocturnium/svelte-ide 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/editor/CustomEditor.svelte +17 -0
- package/dist/components/editor/CustomEditor.svelte.d.ts +2 -1
- package/dist/components/editor/core/apply-extract-plan.d.ts +26 -0
- package/dist/components/editor/core/apply-extract-plan.js +151 -0
- package/dist/components/editor/core/extract-function.js +35 -2
- package/dist/components/editor/core/index.d.ts +1 -0
- package/dist/components/editor/core/index.js +1 -0
- package/dist/components/editor/core/state.d.ts +19 -0
- package/dist/components/editor/core/state.js +24 -0
- package/package.json +2 -1
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
createKeyboardHandler,
|
|
8
8
|
createDefaultKeybindings,
|
|
9
9
|
createFoldManager,
|
|
10
|
+
extractFunctionAt,
|
|
10
11
|
type EditorState,
|
|
12
|
+
type ExtractFunctionResult,
|
|
11
13
|
type Selection,
|
|
12
14
|
type SearchMatch,
|
|
13
15
|
type FoldManager,
|
|
@@ -590,6 +592,21 @@
|
|
|
590
592
|
hiddenInput?.focus();
|
|
591
593
|
}
|
|
592
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Extract the current selection into a new function (Track H). Analyzes the
|
|
597
|
+
* current content fresh (not the debounced complexityMetrics, which can lag a
|
|
598
|
+
* recent edit) so the enclosing region is always correct, regardless of the
|
|
599
|
+
* complexityHighlighting prop. The edit is applied as a single undo step; on
|
|
600
|
+
* refusal the editor is untouched. Returns the result so callers can surface
|
|
601
|
+
* the reason.
|
|
602
|
+
*/
|
|
603
|
+
export function extractFunction(): ExtractFunctionResult {
|
|
604
|
+
const metrics = complexityAnalyzer.analyze(editorState.lines, language);
|
|
605
|
+
const result = extractFunctionAt(editorState, metrics);
|
|
606
|
+
announce(result.ok ? 'Extracted function' : result.reason);
|
|
607
|
+
return result;
|
|
608
|
+
}
|
|
609
|
+
|
|
593
610
|
function handleFoldIndicatorClick(lineNumber: number, e: MouseEvent) {
|
|
594
611
|
e.preventDefault();
|
|
595
612
|
e.stopPropagation();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EditorPreferences } from '../../types';
|
|
2
|
-
import { type Cursor } from './core';
|
|
2
|
+
import { type ExtractFunctionResult, type Cursor } from './core';
|
|
3
3
|
import { type ComplexityMetrics, type ComplexityRegion } from './core/complexity-analyzer';
|
|
4
4
|
import { type FoldPreset } from './core/semantic-analyzer';
|
|
5
5
|
import type { AIAwareness } from './core/ai-awareness';
|
|
@@ -38,6 +38,7 @@ declare const CustomEditor: import("svelte").Component<Props, {
|
|
|
38
38
|
unfoldAll: () => void;
|
|
39
39
|
flashComplexityRegion: (region: ComplexityRegion) => void;
|
|
40
40
|
scrollToLine: (line: number, region?: ComplexityRegion) => Promise<void>;
|
|
41
|
+
extractFunction: () => ExtractFunctionResult;
|
|
41
42
|
}, "content">;
|
|
42
43
|
type CustomEditor = ReturnType<typeof CustomEditor>;
|
|
43
44
|
export default CustomEditor;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { EditorState } from './state';
|
|
2
|
+
import { type ExtractPlan, type ExtractRefusal } from './extract-function';
|
|
3
|
+
import type { ComplexityMetrics, ComplexityRegion } from './complexity-analyzer';
|
|
4
|
+
export type ExtractFunctionResult = {
|
|
5
|
+
ok: true;
|
|
6
|
+
} | ExtractRefusal;
|
|
7
|
+
/**
|
|
8
|
+
* Find the innermost enclosing region whose [startLine, endLine] (0-based,
|
|
9
|
+
* inclusive) contains the block. Prefers `function` regions; ties are broken by
|
|
10
|
+
* the smallest span. Returns undefined when no region encloses the block.
|
|
11
|
+
*/
|
|
12
|
+
export declare function findEnclosingFunctionRegion(metrics: ComplexityMetrics | null, blockStart: number, blockEnd: number): ComplexityRegion | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Apply a successful ExtractPlan to a live editor as a SINGLE undo step. Edits
|
|
15
|
+
* are issued bottom-up: the new function is inserted AFTER the enclosing
|
|
16
|
+
* function first (it sits below the block, so the block's line numbers stay
|
|
17
|
+
* valid), then the block is replaced by the call at its original indentation.
|
|
18
|
+
*/
|
|
19
|
+
export declare function applyExtractPlan(editor: EditorState, plan: ExtractPlan, blockStart: number, blockEnd: number): void;
|
|
20
|
+
/**
|
|
21
|
+
* Extract the editor's current line selection into a new function. Finds the
|
|
22
|
+
* enclosing function from the complexity metrics, runs the (pure) planner, and
|
|
23
|
+
* either applies the plan or returns the planner's refusal so the caller can
|
|
24
|
+
* surface the reason to the user.
|
|
25
|
+
*/
|
|
26
|
+
export declare function extractFunctionAt(editor: EditorState, metrics: ComplexityMetrics | null): ExtractFunctionResult;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { planExtractFunction } from './extract-function';
|
|
2
|
+
import { tokenize } from '../tokenizer';
|
|
3
|
+
// Tokens after which a `{` opens an object literal (a value position) rather than
|
|
4
|
+
// a statement block.
|
|
5
|
+
const OBJECT_LITERAL_PRECEDERS = new Set([
|
|
6
|
+
'=',
|
|
7
|
+
'(',
|
|
8
|
+
'[',
|
|
9
|
+
',',
|
|
10
|
+
':',
|
|
11
|
+
'return',
|
|
12
|
+
'=>',
|
|
13
|
+
'?',
|
|
14
|
+
'&&',
|
|
15
|
+
'||',
|
|
16
|
+
'??',
|
|
17
|
+
'!'
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* Would the new function — inserted as a sibling after `insertAfterLine` — land
|
|
21
|
+
* inside a container where a `function` declaration is NOT valid (a class body,
|
|
22
|
+
* or an object/array literal)? The applier writes a bare `function extracted()`
|
|
23
|
+
* there, which is a SyntaxError, so the orchestration refuses these. Scans the
|
|
24
|
+
* brace nesting through the insertion line, classifying each `{` as a class
|
|
25
|
+
* body, object literal, or statement block.
|
|
26
|
+
*/
|
|
27
|
+
function insertionInNonFunctionContainer(lines, language, insertAfterLine) {
|
|
28
|
+
const source = lines
|
|
29
|
+
.slice(0, insertAfterLine + 1)
|
|
30
|
+
.map((line) => line.text)
|
|
31
|
+
.join('\n');
|
|
32
|
+
const tokens = tokenize(source, language)
|
|
33
|
+
.flatMap((line) => line.tokens)
|
|
34
|
+
.filter((t) => {
|
|
35
|
+
if (t.type === 'text')
|
|
36
|
+
return t.text.trim().length > 0;
|
|
37
|
+
if (t.type === 'comment' || t.type.startsWith('comment.'))
|
|
38
|
+
return false;
|
|
39
|
+
if (t.type === 'string' || t.type.startsWith('string.'))
|
|
40
|
+
return false;
|
|
41
|
+
return true;
|
|
42
|
+
});
|
|
43
|
+
const stack = [];
|
|
44
|
+
let prev = '';
|
|
45
|
+
let pendingClass = false;
|
|
46
|
+
for (const t of tokens) {
|
|
47
|
+
const text = t.text;
|
|
48
|
+
if (text === 'class' && t.type.startsWith('keyword')) {
|
|
49
|
+
pendingClass = true;
|
|
50
|
+
}
|
|
51
|
+
else if (text === '{') {
|
|
52
|
+
stack.push(pendingClass ? 'class' : OBJECT_LITERAL_PRECEDERS.has(prev) ? 'object' : 'block');
|
|
53
|
+
pendingClass = false;
|
|
54
|
+
}
|
|
55
|
+
else if (text === '}') {
|
|
56
|
+
stack.pop();
|
|
57
|
+
}
|
|
58
|
+
else if (text === '[') {
|
|
59
|
+
stack.push('object'); // array/computed literal is not a statement container
|
|
60
|
+
}
|
|
61
|
+
else if (text === ']') {
|
|
62
|
+
stack.pop();
|
|
63
|
+
}
|
|
64
|
+
prev = text;
|
|
65
|
+
}
|
|
66
|
+
const top = stack[stack.length - 1];
|
|
67
|
+
return top === 'object' || top === 'class';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Find the innermost enclosing region whose [startLine, endLine] (0-based,
|
|
71
|
+
* inclusive) contains the block. Prefers `function` regions; ties are broken by
|
|
72
|
+
* the smallest span. Returns undefined when no region encloses the block.
|
|
73
|
+
*/
|
|
74
|
+
export function findEnclosingFunctionRegion(metrics, blockStart, blockEnd) {
|
|
75
|
+
if (!metrics)
|
|
76
|
+
return undefined;
|
|
77
|
+
const containing = metrics.regions.filter((r) => r.startLine <= blockStart && blockEnd <= r.endLine);
|
|
78
|
+
if (containing.length === 0)
|
|
79
|
+
return undefined;
|
|
80
|
+
const functions = containing.filter((r) => r.type === 'function');
|
|
81
|
+
const pool = functions.length > 0 ? functions : containing;
|
|
82
|
+
return pool.reduce((best, r) => r.endLine - r.startLine < best.endLine - best.startLine ? r : best);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Apply a successful ExtractPlan to a live editor as a SINGLE undo step. Edits
|
|
86
|
+
* are issued bottom-up: the new function is inserted AFTER the enclosing
|
|
87
|
+
* function first (it sits below the block, so the block's line numbers stay
|
|
88
|
+
* valid), then the block is replaced by the call at its original indentation.
|
|
89
|
+
*/
|
|
90
|
+
export function applyExtractPlan(editor, plan, blockStart, blockEnd) {
|
|
91
|
+
// Geometry captured from the pre-edit document.
|
|
92
|
+
const indent = editor.getLine(blockStart)?.text.match(/^[ \t]*/)?.[0] ?? '';
|
|
93
|
+
const insertLine = plan.insertAfterLine;
|
|
94
|
+
const insertCol = editor.getLine(insertLine)?.text.length ?? 0;
|
|
95
|
+
const blockEndCol = editor.getLine(blockEnd)?.text.length ?? 0;
|
|
96
|
+
const callText = indent + plan.callText;
|
|
97
|
+
editor.transact((tx) => {
|
|
98
|
+
// 1. New function after the enclosing function's last line.
|
|
99
|
+
tx.insert({ line: insertLine, column: insertCol }, `\n${plan.functionText}`);
|
|
100
|
+
// 2. Remove the block's text — the lines collapse to one empty line at blockStart.
|
|
101
|
+
tx.delete({ line: blockStart, column: 0 }, { line: blockEnd, column: blockEndCol });
|
|
102
|
+
// 3. Drop in the call at the block's original indent.
|
|
103
|
+
tx.insert({ line: blockStart, column: 0 }, callText);
|
|
104
|
+
});
|
|
105
|
+
// Caret at the end of the new call.
|
|
106
|
+
editor.setCursor({ line: blockStart, column: callText.length });
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Extract the editor's current line selection into a new function. Finds the
|
|
110
|
+
* enclosing function from the complexity metrics, runs the (pure) planner, and
|
|
111
|
+
* either applies the plan or returns the planner's refusal so the caller can
|
|
112
|
+
* surface the reason to the user.
|
|
113
|
+
*/
|
|
114
|
+
export function extractFunctionAt(editor, metrics) {
|
|
115
|
+
if (!editor.hasSelection) {
|
|
116
|
+
return { ok: false, reason: 'Select a block of statements to extract.' };
|
|
117
|
+
}
|
|
118
|
+
const { start, end } = editor.normalizedSelection;
|
|
119
|
+
const blockStart = start.line;
|
|
120
|
+
// A selection ending at column 0 of a later line covers the full lines above it.
|
|
121
|
+
const blockEnd = end.column === 0 && end.line > start.line ? end.line - 1 : end.line;
|
|
122
|
+
const region = findEnclosingFunctionRegion(metrics, blockStart, blockEnd);
|
|
123
|
+
if (!region) {
|
|
124
|
+
return { ok: false, reason: 'Place the selection inside a function to extract from.' };
|
|
125
|
+
}
|
|
126
|
+
// The applier inserts the new function as a sibling after the enclosing
|
|
127
|
+
// function's last line. The analyzer types class/object methods as `function`
|
|
128
|
+
// too, so that insertion can land inside a class body or object/array literal,
|
|
129
|
+
// where a bare `function extracted()` is a SyntaxError. Refuse rather than
|
|
130
|
+
// miswrite until method extraction is supported. (The region-based class check
|
|
131
|
+
// is kept as defense-in-depth alongside the brace-context scan.)
|
|
132
|
+
const inClassRegion = metrics?.regions.some((r) => r !== region &&
|
|
133
|
+
r.type === 'class' &&
|
|
134
|
+
r.startLine <= region.startLine &&
|
|
135
|
+
region.endLine <= r.endLine);
|
|
136
|
+
if (inClassRegion ||
|
|
137
|
+
insertionInNonFunctionContainer(editor.lines, editor.language, region.endLine)) {
|
|
138
|
+
return { ok: false, reason: 'Extracting from a class or object method is not supported yet.' };
|
|
139
|
+
}
|
|
140
|
+
const plan = planExtractFunction({
|
|
141
|
+
lines: editor.lines.map((line) => ({ text: line.text })),
|
|
142
|
+
language: editor.language,
|
|
143
|
+
region: { startLine: region.startLine, endLine: region.endLine, type: region.type },
|
|
144
|
+
blockStart,
|
|
145
|
+
blockEnd
|
|
146
|
+
});
|
|
147
|
+
if (!plan.ok)
|
|
148
|
+
return plan;
|
|
149
|
+
applyExtractPlan(editor, plan, blockStart, blockEnd);
|
|
150
|
+
return { ok: true };
|
|
151
|
+
}
|
|
@@ -123,7 +123,16 @@ function planExtractFunctionUnsafe(input) {
|
|
|
123
123
|
return { ok: false, reason: 'Could not safely determine the block inputs and outputs.' };
|
|
124
124
|
}
|
|
125
125
|
for (const decl of insideDeclarations) {
|
|
126
|
-
|
|
126
|
+
// A name declared at depth > 0 inside the block and used after it is only
|
|
127
|
+
// conditionally defined — extracting it would drop the after-use's binding.
|
|
128
|
+
// The ONE safe exception is a nested arrow/catch/function PARAM that merely
|
|
129
|
+
// shadows an outer var (params are excluded from outputs, and the after-use
|
|
130
|
+
// resolves to the always-defined outer binding). A real lexical
|
|
131
|
+
// let/const/for-of declaration that shadows an outer name is NOT safe: it
|
|
132
|
+
// becomes a declared return and the call site emits a duplicate `const`.
|
|
133
|
+
if (usedAfterB.has(decl.name) &&
|
|
134
|
+
decl.depth > 0 &&
|
|
135
|
+
!(decl.kind === 'param' && declaredBeforeB.has(decl.name))) {
|
|
127
136
|
return {
|
|
128
137
|
ok: false,
|
|
129
138
|
reason: 'A variable used after the block is only conditionally defined inside it.'
|
|
@@ -142,6 +151,18 @@ function planExtractFunctionUnsafe(input) {
|
|
|
142
151
|
const returnNames = returns.map((item) => item.name);
|
|
143
152
|
const declaredReturnNames = new Set(returnNames.filter((name) => declaredInsideB.has(name)));
|
|
144
153
|
const outerReturnNames = returnNames.filter((name) => !declaredInsideB.has(name));
|
|
154
|
+
// A `const`-emitted return (a name declared inside the block) that ALSO has an
|
|
155
|
+
// outer binding before the block would write a duplicate `const <name>` at the
|
|
156
|
+
// call site (SyntaxError). This is depth-independent, so it covers the
|
|
157
|
+
// for-/while-header lexical declarations the brace-depth guard above cannot see
|
|
158
|
+
// (a `for (const v of …)` binding sits before the loop body brace).
|
|
159
|
+
const duplicateConstReturn = [...declaredReturnNames].find((name) => declaredBeforeB.has(name));
|
|
160
|
+
if (duplicateConstReturn) {
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
reason: 'A variable declared in the block shadows an outer variable used after it.'
|
|
164
|
+
};
|
|
165
|
+
}
|
|
145
166
|
if (returnNames.length > 1 && outerReturnNames.length > 0) {
|
|
146
167
|
return {
|
|
147
168
|
ok: false,
|
|
@@ -633,7 +654,13 @@ function isIdentifierUse(tokens, index) {
|
|
|
633
654
|
return true;
|
|
634
655
|
}
|
|
635
656
|
function getAssignments(tokens, declarations) {
|
|
636
|
-
|
|
657
|
+
// Seed with the block's own let/const/var/function declarations (a declared
|
|
658
|
+
// value used after the block is a return). Nested fn/arrow/catch params are
|
|
659
|
+
// NOT outputs — they're bindings scoped to their construct; a real assignment
|
|
660
|
+
// to one is still picked up by the operator scan below.
|
|
661
|
+
const assigned = declarations
|
|
662
|
+
.filter((decl) => decl.kind !== 'param')
|
|
663
|
+
.map((decl) => ({
|
|
637
664
|
name: decl.name,
|
|
638
665
|
line: decl.line,
|
|
639
666
|
col: decl.col,
|
|
@@ -677,6 +704,12 @@ function identifierInIncrement(tokens, index) {
|
|
|
677
704
|
if (!areAdjacent(first, second))
|
|
678
705
|
return undefined;
|
|
679
706
|
const previous = tokens[index - 1];
|
|
707
|
+
// A run of 3+ identical operators (e.g. `a+++b` is `a++ + b`) yields overlapping
|
|
708
|
+
// pairs; only the run's leading pair is a real increment. Skip a pair whose
|
|
709
|
+
// previous token is the same operator adjacent to it, so the trailing operand
|
|
710
|
+
// (`b`) is never spuriously modeled as a mutation target.
|
|
711
|
+
if (previous && previous.text === first.text && areAdjacent(previous, first))
|
|
712
|
+
return undefined;
|
|
680
713
|
const next = tokens[index + 2];
|
|
681
714
|
const target = isIdentifierToken(previous)
|
|
682
715
|
? previous
|
|
@@ -18,5 +18,6 @@ export * from './quick-actions';
|
|
|
18
18
|
export * from './diagnostics';
|
|
19
19
|
export * from './breakpoints';
|
|
20
20
|
export * from './extract-function';
|
|
21
|
+
export * from './apply-extract-plan';
|
|
21
22
|
export type { Position } from './state';
|
|
22
23
|
export type { Diagnostic, Range } from './quick-actions';
|
|
@@ -25,6 +25,16 @@ export interface Selection {
|
|
|
25
25
|
/** End of selection (head/cursor position) */
|
|
26
26
|
head: Position;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Primitive edits available inside {@link EditorState.transact}. Every insert
|
|
30
|
+
* and delete is folded into a single undo step.
|
|
31
|
+
*/
|
|
32
|
+
export interface EditorTransaction {
|
|
33
|
+
/** Insert text at a position; returns the position after the inserted text. */
|
|
34
|
+
insert(position: Position, text: string): Position;
|
|
35
|
+
/** Delete the range [from, to) (end exclusive). */
|
|
36
|
+
delete(from: Position, to: Position): void;
|
|
37
|
+
}
|
|
28
38
|
/**
|
|
29
39
|
* A single line in the document
|
|
30
40
|
*/
|
|
@@ -271,6 +281,15 @@ export declare class EditorState {
|
|
|
271
281
|
* Insert text at all cursor positions
|
|
272
282
|
*/
|
|
273
283
|
insert(text: string): void;
|
|
284
|
+
/**
|
|
285
|
+
* Run several primitive edits as ONE undoable transaction. Inserts/deletes
|
|
286
|
+
* performed via the provided `tx` are folded into a single history entry, so
|
|
287
|
+
* one undo reverts the whole change and one redo reapplies it. Used by
|
|
288
|
+
* multi-edit refactors such as extract-function. Emits a single content +
|
|
289
|
+
* selection + cursor change after the body so subscribers (and `bind:content`)
|
|
290
|
+
* update once.
|
|
291
|
+
*/
|
|
292
|
+
transact(fn: (tx: EditorTransaction) => void): void;
|
|
274
293
|
/**
|
|
275
294
|
* Insert text at a specific position (internal, doesn't update cursor)
|
|
276
295
|
* @returns New position after insert
|
|
@@ -376,6 +376,30 @@ export class EditorState {
|
|
|
376
376
|
this.emitCursorChange();
|
|
377
377
|
this.commitHistory(history, 'insert');
|
|
378
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Run several primitive edits as ONE undoable transaction. Inserts/deletes
|
|
381
|
+
* performed via the provided `tx` are folded into a single history entry, so
|
|
382
|
+
* one undo reverts the whole change and one redo reapplies it. Used by
|
|
383
|
+
* multi-edit refactors such as extract-function. Emits a single content +
|
|
384
|
+
* selection + cursor change after the body so subscribers (and `bind:content`)
|
|
385
|
+
* update once.
|
|
386
|
+
*/
|
|
387
|
+
transact(fn) {
|
|
388
|
+
// Force a fresh, isolated history entry: don't merge into prior typing, and
|
|
389
|
+
// (after commit) don't let the next keystroke merge into this one.
|
|
390
|
+
this.lastHistoryType = null;
|
|
391
|
+
const history = this.beginHistory('insert');
|
|
392
|
+
fn({
|
|
393
|
+
insert: (position, text) => this.insertAtInternal(position, text, history.entry.changes),
|
|
394
|
+
delete: (from, to) => this.deleteRangeInternal(from, to, history.entry.changes)
|
|
395
|
+
});
|
|
396
|
+
// The internal primitives don't move cursors; clamp the primary so it stays
|
|
397
|
+
// valid against the new content (callers may set a more precise position).
|
|
398
|
+
this.setCursor(this.clampPosition(this.selection.head));
|
|
399
|
+
this.emitChange({ type: 'replace', from: { line: 0, column: 0 } });
|
|
400
|
+
this.commitHistory(history, 'insert');
|
|
401
|
+
this.lastHistoryType = null;
|
|
402
|
+
}
|
|
379
403
|
/**
|
|
380
404
|
* Insert text at a specific position (internal, doesn't update cursor)
|
|
381
405
|
* @returns New position after insert
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocturnium/svelte-ide",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Svelte 5 code editor and IDE building blocks — custom editor, syntax highlighting, code folding, multi-cursor, LSP client, and optional realtime collaboration.",
|
|
5
5
|
"author": "Nocturnium & Jordan Dziat <hello@nocturnium.ai> (https://nocturnium.ai)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -142,6 +142,7 @@
|
|
|
142
142
|
"@sveltejs/vite-plugin-svelte": "^5.1.0",
|
|
143
143
|
"@tailwindcss/vite": "^4.1.0",
|
|
144
144
|
"@types/node": "^22.15.0",
|
|
145
|
+
"acorn": "^8.16.0",
|
|
145
146
|
"eslint": "^9.39.0",
|
|
146
147
|
"eslint-plugin-svelte": "^3.9.0",
|
|
147
148
|
"globals": "^16.2.0",
|