@steinnes/snippets 0.1.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/README.md +130 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +404 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# @ses/snippets-lean
|
|
2
|
+
|
|
3
|
+
An in-memory store for named text snippets, with an editing API shaped for
|
|
4
|
+
use by LLMs and tooling. Snippets are created, retrieved, and mutated by
|
|
5
|
+
name or id; edits specify `oldText` / `newText` pairs and apply with
|
|
6
|
+
forward-cursor, fuzzy-matched semantics. Also supports single-snippet patches
|
|
7
|
+
for multi-chunk updates.
|
|
8
|
+
|
|
9
|
+
This package is Node ESM, no runtime dependencies, and has no persistence
|
|
10
|
+
— all state lives in the `SnippetStore` instance.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @steinnes/snippets
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { SnippetStore } from '@steinnes/snippets';
|
|
22
|
+
|
|
23
|
+
const store = new SnippetStore();
|
|
24
|
+
|
|
25
|
+
// Create
|
|
26
|
+
const snippet = store.create({ name: 'greeting', content: 'hello world\n' });
|
|
27
|
+
|
|
28
|
+
// Edit (oldText → newText, with a forward cursor across multiple edits)
|
|
29
|
+
const edited = store.edit(snippet.id, [
|
|
30
|
+
{ oldText: 'world', newText: 'universe' },
|
|
31
|
+
]);
|
|
32
|
+
console.log(edited.content); // 'hello universe\n'
|
|
33
|
+
|
|
34
|
+
// Patch (single-snippet, one or more @@ chunks)
|
|
35
|
+
const patchText = [
|
|
36
|
+
'*** Begin Patch',
|
|
37
|
+
'*** Update File: greeting',
|
|
38
|
+
'@@',
|
|
39
|
+
'-universe',
|
|
40
|
+
'+everyone',
|
|
41
|
+
'*** End Patch',
|
|
42
|
+
'',
|
|
43
|
+
].join('\n');
|
|
44
|
+
const patched = store.patch(snippet.id, patchText);
|
|
45
|
+
console.log(patched.content); // 'hello everyone\n'
|
|
46
|
+
|
|
47
|
+
// List all snippets
|
|
48
|
+
const all = store.list();
|
|
49
|
+
|
|
50
|
+
// Rename
|
|
51
|
+
const renamed = store.rename(snippet.id, 'new-name');
|
|
52
|
+
|
|
53
|
+
// Replace all content
|
|
54
|
+
const fresh = store.replace(snippet.id, 'brand new content');
|
|
55
|
+
|
|
56
|
+
// Delete
|
|
57
|
+
store.delete(snippet.id);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Errors
|
|
61
|
+
|
|
62
|
+
All errors extend `Error` and carry typed context fields:
|
|
63
|
+
|
|
64
|
+
- `SnippetNotFoundError` — the given snippet id is not in the store.
|
|
65
|
+
Fields: `snippetId`.
|
|
66
|
+
- `SnippetNameConflictError` — creating or renaming to a name that is
|
|
67
|
+
already taken. Fields: `conflictName`.
|
|
68
|
+
- `OldTextNotFoundError` — `oldText` was not found in the snippet during
|
|
69
|
+
an edit. Fields: `snippetId`, `oldText`, `editIndex` (for batch edits,
|
|
70
|
+
the 0-based index of the failing edit).
|
|
71
|
+
- `PatchParseError` — the patch text is malformed (missing sentinels,
|
|
72
|
+
multi-file patches, or structurally invalid chunks).
|
|
73
|
+
- `PatchApplyError` — a chunk's `oldText` could not be matched against
|
|
74
|
+
the snippet during patch application. Fields: `snippetId`, `chunkIndex`,
|
|
75
|
+
`detail` (the raw mismatch message).
|
|
76
|
+
- `PatchTargetMismatchError` — the patch's `*** Update File:` header
|
|
77
|
+
names a snippet that does not match the target id. Fields:
|
|
78
|
+
`snippetId`, `snippetName`, `patchTargetName`.
|
|
79
|
+
|
|
80
|
+
## Fuzzy matching
|
|
81
|
+
|
|
82
|
+
Text matching for `edit` and `patch` runs a four-tier cascade until the
|
|
83
|
+
first tier matches (or all fail):
|
|
84
|
+
|
|
85
|
+
1. **Exact** — literal substring match.
|
|
86
|
+
2. **Rstrip** — line-wise, trailing whitespace ignored on both sides.
|
|
87
|
+
3. **Trim** — line-wise, leading+trailing whitespace ignored on both sides.
|
|
88
|
+
4. **NFC** — line-wise, Unicode-normalized (e.g. `é` as `\u00e9` matches
|
|
89
|
+
`e\u0301`).
|
|
90
|
+
|
|
91
|
+
Returned offsets always point at the original (un-normalized) content, so
|
|
92
|
+
replacement is lossless. Pass `{ fuzzy: false }` to `edit` / `patch` (or
|
|
93
|
+
set `fuzzy: false` on the `SnippetStore` constructor options) to restrict
|
|
94
|
+
matching to tier 1 only.
|
|
95
|
+
|
|
96
|
+
## Patch format
|
|
97
|
+
|
|
98
|
+
The patch format is a simple single-snippet text-patching format:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
*** Begin Patch
|
|
102
|
+
*** Update File: <snippet-name> (optional; triggers strict name validation)
|
|
103
|
+
@@
|
|
104
|
+
- line to remove
|
|
105
|
+
+ line to add
|
|
106
|
+
context line (unchanged)
|
|
107
|
+
@@
|
|
108
|
+
...more chunks...
|
|
109
|
+
*** End Patch
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**What IS supported:**
|
|
113
|
+
|
|
114
|
+
- `*** Begin Patch` / `*** End Patch` sentinels
|
|
115
|
+
- Optional `*** Update File: <name>` header — when present, `store.patch`
|
|
116
|
+
validates that `<name>` matches the snippet's current name and throws
|
|
117
|
+
`PatchTargetMismatchError` if it does not
|
|
118
|
+
- One or more `@@` chunk markers
|
|
119
|
+
- Lines prefixed with `-` (removed), `+` (added), or ` ` (space; context)
|
|
120
|
+
- Blank lines within a chunk (represented as empty strings in both oldText
|
|
121
|
+
and newText)
|
|
122
|
+
- Fuzzy matching (optional; default on)
|
|
123
|
+
|
|
124
|
+
**What is NOT supported:**
|
|
125
|
+
|
|
126
|
+
- `@@ <context-string>` navigation (the `@@` line takes no arguments)
|
|
127
|
+
- `*** End of File` sentinel
|
|
128
|
+
- `*** Add File` / `*** Delete File` multi-file headers
|
|
129
|
+
- Multi-file patches (a single patch with multiple `*** Update File:` headers
|
|
130
|
+
throws `PatchParseError`)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
type Snippet = {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly content: string;
|
|
5
|
+
readonly createdAt: Date;
|
|
6
|
+
readonly updatedAt: Date;
|
|
7
|
+
};
|
|
8
|
+
type Edit = {
|
|
9
|
+
oldText: string;
|
|
10
|
+
newText: string;
|
|
11
|
+
};
|
|
12
|
+
type EditOptions = {
|
|
13
|
+
fuzzy?: boolean;
|
|
14
|
+
};
|
|
15
|
+
type SnippetStoreOptions = {
|
|
16
|
+
fuzzy?: boolean;
|
|
17
|
+
};
|
|
18
|
+
type CreateInput = {
|
|
19
|
+
name: string;
|
|
20
|
+
content?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
declare class SnippetStore {
|
|
24
|
+
private readonly _snippets;
|
|
25
|
+
private readonly _byName;
|
|
26
|
+
readonly fuzzy: boolean;
|
|
27
|
+
constructor(options?: SnippetStoreOptions);
|
|
28
|
+
create(input: CreateInput): Snippet;
|
|
29
|
+
get(id: string): Snippet | undefined;
|
|
30
|
+
getByName(name: string): Snippet | undefined;
|
|
31
|
+
has(id: string): boolean;
|
|
32
|
+
list(): Snippet[];
|
|
33
|
+
rename(id: string, newName: string): Snippet;
|
|
34
|
+
delete(id: string): boolean;
|
|
35
|
+
replace(id: string, content: string): Snippet;
|
|
36
|
+
edit(id: string, edits: Edit | readonly Edit[], options?: EditOptions): Snippet;
|
|
37
|
+
patch(id: string, patchText: string, options?: EditOptions): Snippet;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
declare class SnippetNotFoundError extends Error {
|
|
41
|
+
readonly name = "SnippetNotFoundError";
|
|
42
|
+
readonly snippetId: string;
|
|
43
|
+
constructor(snippetId: string);
|
|
44
|
+
}
|
|
45
|
+
declare class SnippetNameConflictError extends Error {
|
|
46
|
+
readonly name = "SnippetNameConflictError";
|
|
47
|
+
readonly conflictName: string;
|
|
48
|
+
constructor(name: string);
|
|
49
|
+
}
|
|
50
|
+
declare class OldTextNotFoundError extends Error {
|
|
51
|
+
readonly name = "OldTextNotFoundError";
|
|
52
|
+
readonly snippetId: string;
|
|
53
|
+
readonly oldText: string;
|
|
54
|
+
readonly editIndex: number;
|
|
55
|
+
constructor(snippetId: string, oldText: string, editIndex: number);
|
|
56
|
+
}
|
|
57
|
+
declare class PatchParseError extends Error {
|
|
58
|
+
readonly name = "PatchParseError";
|
|
59
|
+
constructor(message: string);
|
|
60
|
+
}
|
|
61
|
+
declare class PatchApplyError extends Error {
|
|
62
|
+
readonly name = "PatchApplyError";
|
|
63
|
+
readonly snippetId: string;
|
|
64
|
+
readonly chunkIndex: number;
|
|
65
|
+
readonly detail: string;
|
|
66
|
+
constructor(snippetId: string, chunkIndex: number, detail: string);
|
|
67
|
+
}
|
|
68
|
+
declare class PatchTargetMismatchError extends Error {
|
|
69
|
+
readonly name = "PatchTargetMismatchError";
|
|
70
|
+
readonly snippetId: string;
|
|
71
|
+
readonly snippetName: string;
|
|
72
|
+
readonly patchTargetName: string;
|
|
73
|
+
constructor(snippetId: string, snippetName: string, patchTargetName: string);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { type Edit, type EditOptions, OldTextNotFoundError, PatchApplyError, PatchParseError, PatchTargetMismatchError, type Snippet, SnippetNameConflictError, SnippetNotFoundError, SnippetStore, type SnippetStoreOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// src/store.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var SnippetNotFoundError = class extends Error {
|
|
6
|
+
name = "SnippetNotFoundError";
|
|
7
|
+
snippetId;
|
|
8
|
+
constructor(snippetId) {
|
|
9
|
+
super(`Snippet not found: ${snippetId}`);
|
|
10
|
+
this.snippetId = snippetId;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var SnippetNameConflictError = class extends Error {
|
|
14
|
+
name = "SnippetNameConflictError";
|
|
15
|
+
conflictName;
|
|
16
|
+
constructor(name) {
|
|
17
|
+
super(`Snippet name already taken: ${name}`);
|
|
18
|
+
this.conflictName = name;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var OldTextNotFoundError = class extends Error {
|
|
22
|
+
name = "OldTextNotFoundError";
|
|
23
|
+
snippetId;
|
|
24
|
+
oldText;
|
|
25
|
+
editIndex;
|
|
26
|
+
constructor(snippetId, oldText, editIndex) {
|
|
27
|
+
super(`Snippet ${snippetId}: oldText "${oldText}" not found at edit index ${editIndex}`);
|
|
28
|
+
this.snippetId = snippetId;
|
|
29
|
+
this.oldText = oldText;
|
|
30
|
+
this.editIndex = editIndex;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var PatchParseError = class extends Error {
|
|
34
|
+
name = "PatchParseError";
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super(message);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var PatchApplyError = class extends Error {
|
|
40
|
+
name = "PatchApplyError";
|
|
41
|
+
snippetId;
|
|
42
|
+
chunkIndex;
|
|
43
|
+
detail;
|
|
44
|
+
constructor(snippetId, chunkIndex, detail) {
|
|
45
|
+
super(`Snippet ${snippetId}: chunk ${chunkIndex} \u2014 ${detail}`);
|
|
46
|
+
this.snippetId = snippetId;
|
|
47
|
+
this.chunkIndex = chunkIndex;
|
|
48
|
+
this.detail = detail;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var PatchTargetMismatchError = class extends Error {
|
|
52
|
+
name = "PatchTargetMismatchError";
|
|
53
|
+
snippetId;
|
|
54
|
+
snippetName;
|
|
55
|
+
patchTargetName;
|
|
56
|
+
constructor(snippetId, snippetName, patchTargetName) {
|
|
57
|
+
super(`Patch targets '${patchTargetName}' but snippet ${snippetId} is named '${snippetName}'.`);
|
|
58
|
+
this.snippetId = snippetId;
|
|
59
|
+
this.snippetName = snippetName;
|
|
60
|
+
this.patchTargetName = patchTargetName;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// src/findMatch.ts
|
|
65
|
+
function splitIntoSpans(content) {
|
|
66
|
+
const spans = [];
|
|
67
|
+
let start = 0;
|
|
68
|
+
for (let i = 0; i < content.length; i++) {
|
|
69
|
+
if (content[i] === "\n") {
|
|
70
|
+
spans.push({ start, length: i - start, text: content.slice(start, i) });
|
|
71
|
+
start = i + 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
spans.push({ start, length: content.length - start, text: content.slice(start) });
|
|
75
|
+
return spans;
|
|
76
|
+
}
|
|
77
|
+
function rstripEquals(a, b) {
|
|
78
|
+
return a.replace(/\s+$/u, "") === b.replace(/\s+$/u, "");
|
|
79
|
+
}
|
|
80
|
+
function trimEquals(a, b) {
|
|
81
|
+
return a.trim() === b.trim();
|
|
82
|
+
}
|
|
83
|
+
function nfcEquals(a, b) {
|
|
84
|
+
return a.normalize("NFC") === b.normalize("NFC");
|
|
85
|
+
}
|
|
86
|
+
function findMatch(content, oldText, options) {
|
|
87
|
+
const searchOffset = options?.searchOffset ?? 0;
|
|
88
|
+
const fuzzy = options?.fuzzy ?? true;
|
|
89
|
+
if (oldText === "") {
|
|
90
|
+
return { start: searchOffset, end: searchOffset };
|
|
91
|
+
}
|
|
92
|
+
const exactStart = content.indexOf(oldText, searchOffset);
|
|
93
|
+
if (exactStart !== -1) {
|
|
94
|
+
return { start: exactStart, end: exactStart + oldText.length };
|
|
95
|
+
}
|
|
96
|
+
if (!fuzzy) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const contentSpans = splitIntoSpans(content);
|
|
100
|
+
const patternLines = oldText.split("\n");
|
|
101
|
+
let lineIdx = 0;
|
|
102
|
+
while (lineIdx < contentSpans.length && contentSpans[lineIdx].start + contentSpans[lineIdx].length < searchOffset) {
|
|
103
|
+
lineIdx++;
|
|
104
|
+
}
|
|
105
|
+
const tiers = [
|
|
106
|
+
rstripEquals,
|
|
107
|
+
trimEquals,
|
|
108
|
+
nfcEquals
|
|
109
|
+
];
|
|
110
|
+
for (const equals of tiers) {
|
|
111
|
+
const normalizedPattern = patternLines.map((line) => {
|
|
112
|
+
if (equals === rstripEquals) return line.replace(/\s+$/u, "");
|
|
113
|
+
if (equals === trimEquals) return line.trim();
|
|
114
|
+
if (equals === nfcEquals) return line.normalize("NFC");
|
|
115
|
+
return line;
|
|
116
|
+
});
|
|
117
|
+
for (let hit = lineIdx; hit < contentSpans.length - patternLines.length + 1; hit++) {
|
|
118
|
+
let allMatch = true;
|
|
119
|
+
for (let j = 0; j < patternLines.length; j++) {
|
|
120
|
+
const contentLine = contentSpans[hit + j].text;
|
|
121
|
+
const patternLine = normalizedPattern[j];
|
|
122
|
+
if (!equals(contentLine, patternLine)) {
|
|
123
|
+
allMatch = false;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (allMatch) {
|
|
128
|
+
const start = contentSpans[hit].start;
|
|
129
|
+
const lastSpan = contentSpans[hit + patternLines.length - 1];
|
|
130
|
+
const end = lastSpan.start + lastSpan.length;
|
|
131
|
+
return { start, end };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/applyEdits.ts
|
|
139
|
+
function applyEdits(content, edits, options) {
|
|
140
|
+
let working = content;
|
|
141
|
+
let searchOffset = 0;
|
|
142
|
+
for (let i = 0; i < edits.length; i++) {
|
|
143
|
+
const edit = edits[i];
|
|
144
|
+
const match = findMatch(working, edit.oldText, {
|
|
145
|
+
fuzzy: options?.fuzzy,
|
|
146
|
+
searchOffset
|
|
147
|
+
});
|
|
148
|
+
if (match === null) {
|
|
149
|
+
throw new OldTextNotFoundError("", edit.oldText, i);
|
|
150
|
+
}
|
|
151
|
+
working = working.slice(0, match.start) + edit.newText + working.slice(match.end);
|
|
152
|
+
searchOffset = match.start + edit.newText.length;
|
|
153
|
+
}
|
|
154
|
+
return working;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/applyPatch.ts
|
|
158
|
+
function applyPatch(content, chunks, options) {
|
|
159
|
+
let working = content;
|
|
160
|
+
let searchOffset = 0;
|
|
161
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
162
|
+
const chunk = chunks[i];
|
|
163
|
+
const match = findMatch(working, chunk.oldText, {
|
|
164
|
+
fuzzy: options?.fuzzy,
|
|
165
|
+
searchOffset
|
|
166
|
+
});
|
|
167
|
+
if (match === null) {
|
|
168
|
+
throw new PatchApplyError("", i, `oldText "${chunk.oldText}" not found`);
|
|
169
|
+
}
|
|
170
|
+
working = working.slice(0, match.start) + chunk.newText + working.slice(match.end);
|
|
171
|
+
searchOffset = match.start + chunk.newText.length;
|
|
172
|
+
}
|
|
173
|
+
return working;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/parsePatch.ts
|
|
177
|
+
var BEGIN_SENTINEL = "*** Begin Patch";
|
|
178
|
+
var END_SENTINEL = "*** End Patch";
|
|
179
|
+
var UPDATE_FILE_PREFIX = "*** Update File:";
|
|
180
|
+
var CHUNK_START = "@@";
|
|
181
|
+
function parsePatch(patchText) {
|
|
182
|
+
const lines = patchText.split("\n");
|
|
183
|
+
if (lines.length < 2) {
|
|
184
|
+
throw new PatchParseError("Patch is empty or invalid");
|
|
185
|
+
}
|
|
186
|
+
if (lines[0].trim() !== BEGIN_SENTINEL) {
|
|
187
|
+
throw new PatchParseError(`Patch must start with "${BEGIN_SENTINEL}"`);
|
|
188
|
+
}
|
|
189
|
+
let i = 1;
|
|
190
|
+
const chunks = [];
|
|
191
|
+
while (i < lines.length && lines[i].trim() === "") {
|
|
192
|
+
i++;
|
|
193
|
+
}
|
|
194
|
+
let targetName;
|
|
195
|
+
if (i < lines.length && lines[i].trim().startsWith(UPDATE_FILE_PREFIX)) {
|
|
196
|
+
const raw = lines[i].trim();
|
|
197
|
+
targetName = raw.slice(UPDATE_FILE_PREFIX.length).trim();
|
|
198
|
+
i++;
|
|
199
|
+
while (i < lines.length && lines[i].trim() === "") {
|
|
200
|
+
i++;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
while (i < lines.length) {
|
|
204
|
+
const line = lines[i];
|
|
205
|
+
const trimmed = line.trim();
|
|
206
|
+
if (trimmed === "") {
|
|
207
|
+
i++;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (trimmed === END_SENTINEL) {
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
if (trimmed.startsWith(UPDATE_FILE_PREFIX)) {
|
|
214
|
+
throw new PatchParseError(
|
|
215
|
+
`Multi-file patches are not supported (line ${i + 1})`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (trimmed !== CHUNK_START && !trimmed.startsWith(CHUNK_START + " ")) {
|
|
219
|
+
throw new PatchParseError(
|
|
220
|
+
`Expected chunk start "${CHUNK_START}" at line ${i + 1}, got "${trimmed}"`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
i++;
|
|
224
|
+
const oldLines = [];
|
|
225
|
+
const newLines = [];
|
|
226
|
+
while (i < lines.length) {
|
|
227
|
+
const raw = lines[i];
|
|
228
|
+
const trimmedLine = raw.trim();
|
|
229
|
+
if (raw === "") {
|
|
230
|
+
oldLines.push("");
|
|
231
|
+
newLines.push("");
|
|
232
|
+
i++;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (trimmedLine.startsWith(CHUNK_START) || trimmedLine.startsWith(UPDATE_FILE_PREFIX) || trimmedLine === END_SENTINEL) {
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
const marker = raw[0];
|
|
239
|
+
if (marker !== " " && marker !== "+" && marker !== "-") {
|
|
240
|
+
throw new PatchParseError(
|
|
241
|
+
`Invalid line marker "${marker}" at line ${i + 1}; expected " ", "+", or "-"`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
const body = raw.slice(1);
|
|
245
|
+
if (marker === " ") {
|
|
246
|
+
oldLines.push(body);
|
|
247
|
+
newLines.push(body);
|
|
248
|
+
} else if (marker === "-") {
|
|
249
|
+
oldLines.push(body);
|
|
250
|
+
} else {
|
|
251
|
+
newLines.push(body);
|
|
252
|
+
}
|
|
253
|
+
i++;
|
|
254
|
+
}
|
|
255
|
+
const chunk = {
|
|
256
|
+
oldText: oldLines.join("\n"),
|
|
257
|
+
newText: newLines.join("\n")
|
|
258
|
+
};
|
|
259
|
+
chunks.push(chunk);
|
|
260
|
+
}
|
|
261
|
+
let lastNonEmpty = lines.length - 1;
|
|
262
|
+
while (lastNonEmpty >= 0 && lines[lastNonEmpty].trim() === "") {
|
|
263
|
+
lastNonEmpty--;
|
|
264
|
+
}
|
|
265
|
+
if (lastNonEmpty < 0 || lines[lastNonEmpty].trim() !== END_SENTINEL) {
|
|
266
|
+
throw new PatchParseError(
|
|
267
|
+
`Patch must end with "${END_SENTINEL}" (at line ${lastNonEmpty + 1})`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return { targetName, chunks };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/store.ts
|
|
274
|
+
var _seq = 0;
|
|
275
|
+
function nextDate() {
|
|
276
|
+
return new Date(Date.now() + _seq++);
|
|
277
|
+
}
|
|
278
|
+
var SnippetStore = class {
|
|
279
|
+
_snippets = /* @__PURE__ */ new Map();
|
|
280
|
+
_byName = /* @__PURE__ */ new Map();
|
|
281
|
+
// name → id
|
|
282
|
+
fuzzy;
|
|
283
|
+
constructor(options = {}) {
|
|
284
|
+
this.fuzzy = options.fuzzy ?? true;
|
|
285
|
+
}
|
|
286
|
+
create(input) {
|
|
287
|
+
const name = input.name;
|
|
288
|
+
if (!name) throw new Error("name is required");
|
|
289
|
+
if (this._byName.has(name)) {
|
|
290
|
+
throw new SnippetNameConflictError(name);
|
|
291
|
+
}
|
|
292
|
+
const id = randomUUID();
|
|
293
|
+
const now = nextDate();
|
|
294
|
+
const snippet = Object.freeze({
|
|
295
|
+
id,
|
|
296
|
+
name,
|
|
297
|
+
content: input.content ?? "",
|
|
298
|
+
createdAt: now,
|
|
299
|
+
updatedAt: now
|
|
300
|
+
});
|
|
301
|
+
this._snippets.set(id, snippet);
|
|
302
|
+
this._byName.set(name, id);
|
|
303
|
+
return snippet;
|
|
304
|
+
}
|
|
305
|
+
get(id) {
|
|
306
|
+
return this._snippets.get(id);
|
|
307
|
+
}
|
|
308
|
+
getByName(name) {
|
|
309
|
+
const id = this._byName.get(name);
|
|
310
|
+
if (id === void 0) return void 0;
|
|
311
|
+
return this._snippets.get(id);
|
|
312
|
+
}
|
|
313
|
+
has(id) {
|
|
314
|
+
return this._snippets.has(id);
|
|
315
|
+
}
|
|
316
|
+
list() {
|
|
317
|
+
return Array.from(this._snippets.values());
|
|
318
|
+
}
|
|
319
|
+
rename(id, newName) {
|
|
320
|
+
const snippet = this._snippets.get(id);
|
|
321
|
+
if (snippet === void 0) {
|
|
322
|
+
throw new SnippetNotFoundError(id);
|
|
323
|
+
}
|
|
324
|
+
if (newName !== snippet.name && this._byName.has(newName)) {
|
|
325
|
+
throw new SnippetNameConflictError(newName);
|
|
326
|
+
}
|
|
327
|
+
this._byName.delete(snippet.name);
|
|
328
|
+
this._byName.set(newName, id);
|
|
329
|
+
const updated = Object.freeze({ ...snippet, name: newName, updatedAt: nextDate() });
|
|
330
|
+
this._snippets.set(id, updated);
|
|
331
|
+
return updated;
|
|
332
|
+
}
|
|
333
|
+
delete(id) {
|
|
334
|
+
const snippet = this._snippets.get(id);
|
|
335
|
+
if (snippet === void 0) return false;
|
|
336
|
+
this._byName.delete(snippet.name);
|
|
337
|
+
this._snippets.delete(id);
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
replace(id, content) {
|
|
341
|
+
const snippet = this._snippets.get(id);
|
|
342
|
+
if (snippet === void 0) {
|
|
343
|
+
throw new SnippetNotFoundError(id);
|
|
344
|
+
}
|
|
345
|
+
const updated = Object.freeze({ ...snippet, content, updatedAt: nextDate() });
|
|
346
|
+
this._snippets.set(id, updated);
|
|
347
|
+
return updated;
|
|
348
|
+
}
|
|
349
|
+
edit(id, edits, options) {
|
|
350
|
+
const snippet = this._snippets.get(id);
|
|
351
|
+
if (snippet === void 0) {
|
|
352
|
+
throw new SnippetNotFoundError(id);
|
|
353
|
+
}
|
|
354
|
+
const editsArray = Array.isArray(edits) ? edits : [edits];
|
|
355
|
+
try {
|
|
356
|
+
const newContent = applyEdits(snippet.content, editsArray, {
|
|
357
|
+
fuzzy: options?.fuzzy ?? this.fuzzy
|
|
358
|
+
});
|
|
359
|
+
const updated = Object.freeze({ ...snippet, content: newContent, updatedAt: nextDate() });
|
|
360
|
+
this._snippets.set(id, updated);
|
|
361
|
+
return updated;
|
|
362
|
+
} catch (err) {
|
|
363
|
+
if (err instanceof OldTextNotFoundError) {
|
|
364
|
+
const enriched = new OldTextNotFoundError(id, err.oldText, err.editIndex);
|
|
365
|
+
throw enriched;
|
|
366
|
+
}
|
|
367
|
+
throw err;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
patch(id, patchText, options) {
|
|
371
|
+
const snippet = this._snippets.get(id);
|
|
372
|
+
if (snippet === void 0) {
|
|
373
|
+
throw new SnippetNotFoundError(id);
|
|
374
|
+
}
|
|
375
|
+
const { targetName, chunks } = parsePatch(patchText);
|
|
376
|
+
if (targetName !== void 0 && targetName !== snippet.name) {
|
|
377
|
+
throw new PatchTargetMismatchError(snippet.id, snippet.name, targetName);
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
const newContent = applyPatch(snippet.content, chunks, {
|
|
381
|
+
fuzzy: options?.fuzzy ?? this.fuzzy
|
|
382
|
+
});
|
|
383
|
+
const updated = Object.freeze({ ...snippet, content: newContent, updatedAt: nextDate() });
|
|
384
|
+
this._snippets.set(id, updated);
|
|
385
|
+
return updated;
|
|
386
|
+
} catch (err) {
|
|
387
|
+
if (err instanceof PatchApplyError) {
|
|
388
|
+
const enriched = new PatchApplyError(id, err.chunkIndex, err.detail);
|
|
389
|
+
throw enriched;
|
|
390
|
+
}
|
|
391
|
+
throw err;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
export {
|
|
396
|
+
OldTextNotFoundError,
|
|
397
|
+
PatchApplyError,
|
|
398
|
+
PatchParseError,
|
|
399
|
+
PatchTargetMismatchError,
|
|
400
|
+
SnippetNameConflictError,
|
|
401
|
+
SnippetNotFoundError,
|
|
402
|
+
SnippetStore
|
|
403
|
+
};
|
|
404
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store.ts","../src/errors.ts","../src/findMatch.ts","../src/applyEdits.ts","../src/applyPatch.ts","../src/parsePatch.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { SnippetNotFoundError, SnippetNameConflictError, OldTextNotFoundError, PatchApplyError, PatchTargetMismatchError } from './errors.js';\nimport { applyEdits } from './applyEdits.js';\nimport { applyPatch } from './applyPatch.js';\nimport { parsePatch } from './parsePatch.js';\nimport type { Snippet, SnippetStoreOptions, CreateInput, Edit, EditOptions } from './types.js';\n\n// Millisecond timestamps can collide when two mutations happen in quick\n// succession. We pair each timestamp with a monotonic sequence number so that\n// every bump is strictly later regardless of wall-clock resolution.\nlet _seq = 0;\nfunction nextDate (): Date {\n return new Date(Date.now() + _seq++);\n}\n\nexport class SnippetStore {\n private readonly _snippets = new Map<string, Snippet>();\n private readonly _byName = new Map<string, string>(); // name → id\n readonly fuzzy: boolean;\n\n constructor (options: SnippetStoreOptions = {}) {\n this.fuzzy = options.fuzzy ?? true;\n }\n\n create (input: CreateInput): Snippet {\n const name = input.name;\n if (!name) throw new Error('name is required');\n if (this._byName.has(name)) {\n throw new SnippetNameConflictError(name);\n }\n const id = randomUUID();\n const now = nextDate();\n const snippet: Snippet = Object.freeze({\n id,\n name,\n content: input.content ?? '',\n createdAt: now,\n updatedAt: now,\n });\n this._snippets.set(id, snippet);\n this._byName.set(name, id);\n return snippet;\n }\n\n get (id: string): Snippet | undefined {\n return this._snippets.get(id);\n }\n\n getByName (name: string): Snippet | undefined {\n const id = this._byName.get(name);\n if (id === undefined) return undefined;\n return this._snippets.get(id);\n }\n\n has (id: string): boolean {\n return this._snippets.has(id);\n }\n\n list (): Snippet[] {\n return Array.from(this._snippets.values());\n }\n\n rename (id: string, newName: string): Snippet {\n const snippet = this._snippets.get(id);\n if (snippet === undefined) {\n throw new SnippetNotFoundError(id);\n }\n // Allow renaming to the current name (no-op that still bumps updatedAt).\n // Only throw for a genuinely different name that is already taken.\n if (newName !== snippet.name && this._byName.has(newName)) {\n throw new SnippetNameConflictError(newName);\n }\n this._byName.delete(snippet.name);\n this._byName.set(newName, id);\n const updated: Snippet = Object.freeze({ ...snippet, name: newName, updatedAt: nextDate() });\n this._snippets.set(id, updated);\n return updated;\n }\n\n delete (id: string): boolean {\n const snippet = this._snippets.get(id);\n if (snippet === undefined) return false;\n this._byName.delete(snippet.name);\n this._snippets.delete(id);\n return true;\n }\n\n replace (id: string, content: string): Snippet {\n const snippet = this._snippets.get(id);\n if (snippet === undefined) {\n throw new SnippetNotFoundError(id);\n }\n const updated: Snippet = Object.freeze({ ...snippet, content, updatedAt: nextDate() });\n this._snippets.set(id, updated);\n return updated;\n }\n\n edit (id: string, edits: Edit | readonly Edit[], options?: EditOptions): Snippet {\n const snippet = this._snippets.get(id);\n if (snippet === undefined) {\n throw new SnippetNotFoundError(id);\n }\n\n // Normalize single Edit to array.\n const editsArray = Array.isArray(edits) ? edits : [ edits ];\n\n try {\n const newContent = applyEdits(snippet.content, editsArray, {\n fuzzy: options?.fuzzy ?? this.fuzzy,\n });\n const updated: Snippet = Object.freeze({ ...snippet, content: newContent, updatedAt: nextDate() });\n this._snippets.set(id, updated);\n return updated;\n }\n catch (err) {\n // Re-throw OldTextNotFoundError with snippetId populated.\n if (err instanceof OldTextNotFoundError) {\n const enriched = new OldTextNotFoundError(id, err.oldText, err.editIndex);\n throw enriched;\n }\n throw err;\n }\n }\n\n patch (id: string, patchText: string, options?: EditOptions): Snippet {\n const snippet = this._snippets.get(id);\n if (snippet === undefined) {\n throw new SnippetNotFoundError(id);\n }\n\n // Parse the patch — PatchParseError propagates unchanged if malformed.\n const { targetName, chunks } = parsePatch(patchText);\n\n // Strict target-name validation: if the patch carries *** Update File: <name>,\n // it must match the current snippet name.\n if (targetName !== undefined && targetName !== snippet.name) {\n throw new PatchTargetMismatchError(snippet.id, snippet.name, targetName);\n }\n\n try {\n const newContent = applyPatch(snippet.content, chunks, {\n fuzzy: options?.fuzzy ?? this.fuzzy,\n });\n const updated: Snippet = Object.freeze({ ...snippet, content: newContent, updatedAt: nextDate() });\n this._snippets.set(id, updated);\n return updated;\n }\n catch (err) {\n // Re-throw PatchApplyError with snippetId populated.\n if (err instanceof PatchApplyError) {\n const enriched = new PatchApplyError(id, err.chunkIndex, err.detail);\n throw enriched;\n }\n throw err;\n }\n }\n}\n","export class SnippetNotFoundError extends Error {\n readonly name = 'SnippetNotFoundError';\n readonly snippetId: string;\n\n constructor (snippetId: string) {\n super(`Snippet not found: ${snippetId}`);\n this.snippetId = snippetId;\n }\n}\n\nexport class SnippetNameConflictError extends Error {\n readonly name = 'SnippetNameConflictError';\n readonly conflictName: string;\n\n constructor (name: string) {\n super(`Snippet name already taken: ${name}`);\n this.conflictName = name;\n }\n}\n\nexport class OldTextNotFoundError extends Error {\n readonly name = 'OldTextNotFoundError';\n readonly snippetId: string;\n readonly oldText: string;\n readonly editIndex: number;\n\n constructor (snippetId: string, oldText: string, editIndex: number) {\n super(`Snippet ${snippetId}: oldText \"${oldText}\" not found at edit index ${editIndex}`);\n this.snippetId = snippetId;\n this.oldText = oldText;\n this.editIndex = editIndex;\n }\n}\n\nexport class PatchParseError extends Error {\n readonly name = 'PatchParseError';\n\n constructor (message: string) {\n super(message);\n }\n}\n\nexport class PatchApplyError extends Error {\n readonly name = 'PatchApplyError';\n readonly snippetId: string;\n readonly chunkIndex: number;\n readonly detail: string;\n\n constructor (snippetId: string, chunkIndex: number, detail: string) {\n super(`Snippet ${snippetId}: chunk ${chunkIndex} — ${detail}`);\n this.snippetId = snippetId;\n this.chunkIndex = chunkIndex;\n this.detail = detail;\n }\n}\n\nexport class PatchTargetMismatchError extends Error {\n readonly name = 'PatchTargetMismatchError';\n readonly snippetId: string;\n readonly snippetName: string;\n readonly patchTargetName: string;\n\n constructor (snippetId: string, snippetName: string, patchTargetName: string) {\n super(`Patch targets '${patchTargetName}' but snippet ${snippetId} is named '${snippetName}'.`);\n this.snippetId = snippetId;\n this.snippetName = snippetName;\n this.patchTargetName = patchTargetName;\n }\n}\n","export type FindMatchResult = {\n readonly start: number;\n readonly end: number;\n};\n\nexport type FindMatchOptions = {\n readonly fuzzy?: boolean; // default true; false = exact only\n readonly searchOffset?: number; // default 0\n};\n\n// --- Representation anchor ---\ntype LineSpan = {\n readonly start: number; // char offset in original content where line starts\n readonly length: number; // line text length, excluding the trailing '\\n'\n readonly text: string; // the line text, excluding the trailing '\\n'\n};\n\nfunction splitIntoSpans (content: string): LineSpan[] {\n const spans: LineSpan[] = [];\n let start = 0;\n for (let i = 0; i < content.length; i++) {\n if (content[i] === '\\n') {\n spans.push({ start, length: i - start, text: content.slice(start, i) });\n start = i + 1;\n }\n }\n spans.push({ start, length: content.length - start, text: content.slice(start) });\n return spans;\n}\n\n// --- Fuzzy tier comparators ---\nfunction rstripEquals (a: string, b: string): boolean {\n return a.replace(/\\s+$/u, '') === b.replace(/\\s+$/u, '');\n}\n\nfunction trimEquals (a: string, b: string): boolean {\n return a.trim() === b.trim();\n}\n\nfunction nfcEquals (a: string, b: string): boolean {\n return a.normalize('NFC') === b.normalize('NFC');\n}\n\nexport function findMatch (\n content: string,\n oldText: string,\n options?: FindMatchOptions,\n): FindMatchResult | null {\n const searchOffset = options?.searchOffset ?? 0;\n const fuzzy = options?.fuzzy ?? true;\n\n // Empty oldText: match at the search cursor position (half-open span)\n if (oldText === '') {\n return { start: searchOffset, end: searchOffset };\n }\n\n // Tier 1: exact substring match\n const exactStart = content.indexOf(oldText, searchOffset);\n if (exactStart !== -1) {\n return { start: exactStart, end: exactStart + oldText.length };\n }\n\n // Fuzzy tiers only when fuzzy !== false\n if (!fuzzy) {\n return null;\n }\n\n const contentSpans = splitIntoSpans(content);\n const patternLines = oldText.split('\\n');\n\n // Find the first span whose end is >= searchOffset (skip spans entirely before searchOffset)\n let lineIdx = 0;\n while (\n lineIdx < contentSpans.length &&\n contentSpans[lineIdx].start + contentSpans[lineIdx].length < searchOffset\n ) {\n lineIdx++;\n }\n\n // Try each fuzzy tier in priority order\n const tiers: readonly ((a: string, b: string) => boolean)[] = [\n rstripEquals,\n trimEquals,\n nfcEquals,\n ];\n\n for (const equals of tiers) {\n // Normalize pattern lines for this tier\n const normalizedPattern = patternLines.map(line => {\n if (equals === rstripEquals) return line.replace(/\\s+$/u, '');\n if (equals === trimEquals) return line.trim();\n if (equals === nfcEquals) return line.normalize('NFC');\n return line; // should never reach\n });\n\n // Try to match starting from each possible line\n // Last valid hit: contentSpans.length - patternLines.length\n // Need hit < contentSpans.length - patternLines.length + 1\n for (\n let hit = lineIdx;\n hit < contentSpans.length - patternLines.length + 1;\n hit++\n ) {\n let allMatch = true;\n for (let j = 0; j < patternLines.length; j++) {\n const contentLine = contentSpans[hit + j].text;\n const patternLine = normalizedPattern[j];\n if (!equals(contentLine, patternLine)) {\n allMatch = false;\n break;\n }\n }\n if (allMatch) {\n const start = contentSpans[hit].start;\n const lastSpan = contentSpans[hit + patternLines.length - 1];\n const end = lastSpan.start + lastSpan.length;\n return { start, end };\n }\n }\n }\n\n return null;\n}\n","import { findMatch } from './findMatch.js';\nimport type { Edit } from './types.js';\nimport { OldTextNotFoundError } from './errors.js';\n\nexport type ApplyEditsOptions = {\n readonly fuzzy?: boolean;\n};\n\nexport function applyEdits (\n content: string,\n edits: readonly Edit[],\n options?: ApplyEditsOptions,\n): string {\n // Work on a local string — atomic by construction.\n // No changes escape on failure.\n let working = content;\n let searchOffset = 0;\n\n for (let i = 0; i < edits.length; i++) {\n const edit = edits[i];\n\n const match = findMatch(working, edit.oldText, {\n fuzzy: options?.fuzzy,\n searchOffset,\n });\n\n if (match === null) {\n throw new OldTextNotFoundError('', edit.oldText, i);\n }\n\n // Splice the replacement into the working string.\n working =\n working.slice(0, match.start) +\n edit.newText +\n working.slice(match.end);\n\n // Advance cursor past the replacement in the post-splice content.\n // No-op edits (oldText === newText) still advance the cursor.\n searchOffset = match.start + edit.newText.length;\n }\n\n return working;\n}\n","import { findMatch } from './findMatch.js';\nimport type { Chunk } from './parsePatch.js';\nimport { PatchApplyError } from './errors.js';\n\nexport type ApplyPatchOptions = {\n readonly fuzzy?: boolean;\n};\n\n/**\n * Apply a sequence of patches to content.\n *\n * Each chunk's oldText is located via findMatch with a forward-only cursor:\n * search begins at the current cursor position, which advances past each\n * replacement. This means later chunks cannot match text that appears before\n * the cursor, even if it exists in the original content.\n *\n * Atomic: on any chunk failure, no partial result escapes — the function\n * either returns the fully-patched content or throws PatchApplyError.\n *\n * @param content - The original content string.\n * @param chunks - Array of Patch Chunks to apply in order.\n * @param options - Optional fuzzy matching flag (default: true).\n * @returns The patched content string.\n * @throws PatchApplyError with chunkIndex on first unmatchable chunk,\n * with snippetId = '' (empty string sentinel — store layer\n * re-throws with the real snippetId populated).\n */\nexport function applyPatch (\n content: string,\n chunks: readonly Chunk[],\n options?: ApplyPatchOptions,\n): string {\n // Work on a local string — atomic by construction.\n // No changes escape on failure.\n let working = content;\n let searchOffset = 0;\n\n for (let i = 0; i < chunks.length; i++) {\n const chunk = chunks[i];\n\n const match = findMatch(working, chunk.oldText, {\n fuzzy: options?.fuzzy,\n searchOffset,\n });\n\n if (match === null) {\n throw new PatchApplyError('', i, `oldText \"${chunk.oldText}\" not found`);\n }\n\n // Splice the newText into the working string.\n working =\n working.slice(0, match.start) +\n chunk.newText +\n working.slice(match.end);\n\n // Advance cursor past the replacement in the post-splice content.\n searchOffset = match.start + chunk.newText.length;\n }\n\n return working;\n}\n","import { PatchParseError } from './errors.js';\n\nexport type Chunk = {\n /** Context+removed lines stitched with '\\n', ready to pass to findMatch. */\n oldText: string;\n /** Context+added lines stitched with '\\n', ready to splice in. */\n newText: string;\n};\n\nconst BEGIN_SENTINEL = '*** Begin Patch';\nconst END_SENTINEL = '*** End Patch';\nconst UPDATE_FILE_PREFIX = '*** Update File:';\nconst CHUNK_START = '@@';\n\n/**\n * Parse a single-snippet patch string into the target name and an array of\n * Chunks.\n *\n * Grammar:\n * patch = BEGIN_SENTINEL [UPDATE_FILE] chunk+ END_SENTINEL\n * UPDATE_FILE = \"*** Update File:\" SP name EOL\n * chunk = CHUNK_START line* (CHUNK_START | UPDATE_FILE | END_SENTINEL)\n * line = \" \" content (context — appears in both oldText and newText)\n * | \"-\" content (removed — only in oldText)\n * | \"+\" content (added — only in newText)\n *\n * Multi-file patches (multiple *** Update File: headers) are rejected.\n * Lines without a valid marker inside a chunk are rejected.\n *\n * @returns An object carrying `targetName` (undefined if the *** Update File\n * header was absent) and `chunks`.\n */\nexport function parsePatch (patchText: string): { targetName: string | undefined; chunks: Chunk[] } {\n const lines = patchText.split('\\n');\n\n if (lines.length < 2) {\n throw new PatchParseError('Patch is empty or invalid');\n }\n\n if (lines[0].trim() !== BEGIN_SENTINEL) {\n throw new PatchParseError(`Patch must start with \"${BEGIN_SENTINEL}\"`);\n }\n\n let i = 1;\n const chunks: Chunk[] = [];\n\n // Skip blank lines after the header\n while (i < lines.length && lines[i].trim() === '') {\n i++;\n }\n\n // Optional: *** Update File: <name>\n let targetName: string | undefined;\n if (i < lines.length && lines[i].trim().startsWith(UPDATE_FILE_PREFIX)) {\n const raw = lines[i].trim();\n targetName = raw.slice(UPDATE_FILE_PREFIX.length).trim();\n i++;\n // Skip blank lines after file header\n while (i < lines.length && lines[i].trim() === '') {\n i++;\n }\n }\n\n // Parse chunks until we hit *** End Patch\n while (i < lines.length) {\n const line = lines[i];\n const trimmed = line.trim();\n\n // Skip blank lines between chunks\n if (trimmed === '') {\n i++;\n continue;\n }\n\n // Reached end of patch\n if (trimmed === END_SENTINEL) {\n break;\n }\n\n // Encountered another *** Update File: — multi-file patch\n if (trimmed.startsWith(UPDATE_FILE_PREFIX)) {\n throw new PatchParseError(\n `Multi-file patches are not supported (line ${i + 1})`,\n );\n }\n\n // Must start a chunk\n if (trimmed !== CHUNK_START && !trimmed.startsWith(CHUNK_START + ' ')) {\n throw new PatchParseError(\n `Expected chunk start \"${CHUNK_START}\" at line ${i + 1}, got \"${trimmed}\"`,\n );\n }\n\n // Consume the @@ marker\n i++;\n\n // Collect lines for this chunk, tracking original order\n const oldLines: string[] = [];\n const newLines: string[] = [];\n\n while (i < lines.length) {\n const raw = lines[i];\n const trimmedLine = raw.trim();\n\n // Empty line within chunk — include as empty string in both\n if (raw === '') {\n oldLines.push('');\n newLines.push('');\n i++;\n continue;\n }\n\n // End conditions\n if (\n trimmedLine.startsWith(CHUNK_START) ||\n trimmedLine.startsWith(UPDATE_FILE_PREFIX) ||\n trimmedLine === END_SENTINEL\n ) {\n break;\n }\n\n // Detect invalid lines (lines not starting with space, +, or -)\n const marker = raw[0];\n if (marker !== ' ' && marker !== '+' && marker !== '-') {\n throw new PatchParseError(\n `Invalid line marker \"${marker}\" at line ${i + 1}; expected \" \", \"+\", or \"-\"`,\n );\n }\n\n const body = raw.slice(1);\n\n if (marker === ' ') {\n // Context line — appears in both\n oldLines.push(body);\n newLines.push(body);\n }\n else if (marker === '-') {\n // Removed line — only in oldText\n oldLines.push(body);\n }\n else {\n // marker === '+'\n // Added line — only in newText\n newLines.push(body);\n }\n\n i++;\n }\n\n // Build stitched text for this chunk\n // Join with \\n but DO NOT add a trailing newline\n // (The holy tests pin this: oldText: 'keep\\nold' has no trailing newline)\n const chunk: Chunk = {\n oldText: oldLines.join('\\n'),\n newText: newLines.join('\\n'),\n };\n chunks.push(chunk);\n }\n\n // Validate end sentinel (may be followed by empty lines from trailing newline)\n let lastNonEmpty = lines.length - 1;\n while (lastNonEmpty >= 0 && lines[lastNonEmpty].trim() === '') {\n lastNonEmpty--;\n }\n if (lastNonEmpty < 0 || lines[lastNonEmpty].trim() !== END_SENTINEL) {\n throw new PatchParseError(\n `Patch must end with \"${END_SENTINEL}\" (at line ${lastNonEmpty + 1})`,\n );\n }\n\n return { targetName, chunks };\n}\n"],"mappings":";AAAA,SAAS,kBAAkB;;;ACApB,IAAM,uBAAN,cAAmC,MAAM;AAAA,EACrC,OAAO;AAAA,EACP;AAAA,EAET,YAAa,WAAmB;AAC9B,UAAM,sBAAsB,SAAS,EAAE;AACvC,SAAK,YAAY;AAAA,EACnB;AACF;AAEO,IAAM,2BAAN,cAAuC,MAAM;AAAA,EACzC,OAAO;AAAA,EACP;AAAA,EAET,YAAa,MAAc;AACzB,UAAM,+BAA+B,IAAI,EAAE;AAC3C,SAAK,eAAe;AAAA,EACtB;AACF;AAEO,IAAM,uBAAN,cAAmC,MAAM;AAAA,EACrC,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAa,WAAmB,SAAiB,WAAmB;AAClE,UAAM,WAAW,SAAS,cAAc,OAAO,6BAA6B,SAAS,EAAE;AACvF,SAAK,YAAY;AACjB,SAAK,UAAU;AACf,SAAK,YAAY;AAAA,EACnB;AACF;AAEO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EAChC,OAAO;AAAA,EAEhB,YAAa,SAAiB;AAC5B,UAAM,OAAO;AAAA,EACf;AACF;AAEO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EAChC,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAa,WAAmB,YAAoB,QAAgB;AAClE,UAAM,WAAW,SAAS,WAAW,UAAU,WAAM,MAAM,EAAE;AAC7D,SAAK,YAAY;AACjB,SAAK,aAAa;AAClB,SAAK,SAAS;AAAA,EAChB;AACF;AAEO,IAAM,2BAAN,cAAuC,MAAM;AAAA,EACzC,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAa,WAAmB,aAAqB,iBAAyB;AAC5E,UAAM,kBAAkB,eAAe,iBAAiB,SAAS,cAAc,WAAW,IAAI;AAC9F,SAAK,YAAY;AACjB,SAAK,cAAc;AACnB,SAAK,kBAAkB;AAAA,EACzB;AACF;;;ACnDA,SAAS,eAAgB,SAA6B;AACpD,QAAM,QAAoB,CAAC;AAC3B,MAAI,QAAQ;AACZ,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,QAAI,QAAQ,CAAC,MAAM,MAAM;AACvB,YAAM,KAAK,EAAE,OAAO,QAAQ,IAAI,OAAO,MAAM,QAAQ,MAAM,OAAO,CAAC,EAAE,CAAC;AACtE,cAAQ,IAAI;AAAA,IACd;AAAA,EACF;AACA,QAAM,KAAK,EAAE,OAAO,QAAQ,QAAQ,SAAS,OAAO,MAAM,QAAQ,MAAM,KAAK,EAAE,CAAC;AAChF,SAAO;AACT;AAGA,SAAS,aAAc,GAAW,GAAoB;AACpD,SAAO,EAAE,QAAQ,SAAS,EAAE,MAAM,EAAE,QAAQ,SAAS,EAAE;AACzD;AAEA,SAAS,WAAY,GAAW,GAAoB;AAClD,SAAO,EAAE,KAAK,MAAM,EAAE,KAAK;AAC7B;AAEA,SAAS,UAAW,GAAW,GAAoB;AACjD,SAAO,EAAE,UAAU,KAAK,MAAM,EAAE,UAAU,KAAK;AACjD;AAEO,SAAS,UACd,SACA,SACA,SACwB;AACxB,QAAM,eAAe,SAAS,gBAAgB;AAC9C,QAAM,QAAQ,SAAS,SAAS;AAGhC,MAAI,YAAY,IAAI;AAClB,WAAO,EAAE,OAAO,cAAc,KAAK,aAAa;AAAA,EAClD;AAGA,QAAM,aAAa,QAAQ,QAAQ,SAAS,YAAY;AACxD,MAAI,eAAe,IAAI;AACrB,WAAO,EAAE,OAAO,YAAY,KAAK,aAAa,QAAQ,OAAO;AAAA,EAC/D;AAGA,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAEA,QAAM,eAAe,eAAe,OAAO;AAC3C,QAAM,eAAe,QAAQ,MAAM,IAAI;AAGvC,MAAI,UAAU;AACd,SACE,UAAU,aAAa,UACvB,aAAa,OAAO,EAAE,QAAQ,aAAa,OAAO,EAAE,SAAS,cAC7D;AACA;AAAA,EACF;AAGA,QAAM,QAAwD;AAAA,IAC5D;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,aAAW,UAAU,OAAO;AAE1B,UAAM,oBAAoB,aAAa,IAAI,UAAQ;AACjD,UAAI,WAAW,aAAc,QAAO,KAAK,QAAQ,SAAS,EAAE;AAC5D,UAAI,WAAW,WAAY,QAAO,KAAK,KAAK;AAC5C,UAAI,WAAW,UAAW,QAAO,KAAK,UAAU,KAAK;AACrD,aAAO;AAAA,IACT,CAAC;AAKD,aACM,MAAM,SACV,MAAM,aAAa,SAAS,aAAa,SAAS,GAClD,OACA;AACA,UAAI,WAAW;AACf,eAAS,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK;AAC5C,cAAM,cAAc,aAAa,MAAM,CAAC,EAAE;AAC1C,cAAM,cAAc,kBAAkB,CAAC;AACvC,YAAI,CAAC,OAAO,aAAa,WAAW,GAAG;AACrC,qBAAW;AACX;AAAA,QACF;AAAA,MACF;AACA,UAAI,UAAU;AACZ,cAAM,QAAQ,aAAa,GAAG,EAAE;AAChC,cAAM,WAAW,aAAa,MAAM,aAAa,SAAS,CAAC;AAC3D,cAAM,MAAM,SAAS,QAAQ,SAAS;AACtC,eAAO,EAAE,OAAO,IAAI;AAAA,MACtB;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;;;AClHO,SAAS,WACd,SACA,OACA,SACQ;AAGR,MAAI,UAAU;AACd,MAAI,eAAe;AAEnB,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,OAAO,MAAM,CAAC;AAEpB,UAAM,QAAQ,UAAU,SAAS,KAAK,SAAS;AAAA,MAC7C,OAAO,SAAS;AAAA,MAChB;AAAA,IACF,CAAC;AAED,QAAI,UAAU,MAAM;AAClB,YAAM,IAAI,qBAAqB,IAAI,KAAK,SAAS,CAAC;AAAA,IACpD;AAGA,cACE,QAAQ,MAAM,GAAG,MAAM,KAAK,IAC5B,KAAK,UACL,QAAQ,MAAM,MAAM,GAAG;AAIzB,mBAAe,MAAM,QAAQ,KAAK,QAAQ;AAAA,EAC5C;AAEA,SAAO;AACT;;;ACfO,SAAS,WACd,SACA,QACA,SACQ;AAGR,MAAI,UAAU;AACd,MAAI,eAAe;AAEnB,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,UAAM,QAAQ,OAAO,CAAC;AAEtB,UAAM,QAAQ,UAAU,SAAS,MAAM,SAAS;AAAA,MAC9C,OAAO,SAAS;AAAA,MAChB;AAAA,IACF,CAAC;AAED,QAAI,UAAU,MAAM;AAClB,YAAM,IAAI,gBAAgB,IAAI,GAAG,YAAY,MAAM,OAAO,aAAa;AAAA,IACzE;AAGA,cACE,QAAQ,MAAM,GAAG,MAAM,KAAK,IAC5B,MAAM,UACN,QAAQ,MAAM,MAAM,GAAG;AAGzB,mBAAe,MAAM,QAAQ,MAAM,QAAQ;AAAA,EAC7C;AAEA,SAAO;AACT;;;ACnDA,IAAM,iBAAiB;AACvB,IAAM,eAAe;AACrB,IAAM,qBAAqB;AAC3B,IAAM,cAAc;AAoBb,SAAS,WAAY,WAAwE;AAClG,QAAM,QAAQ,UAAU,MAAM,IAAI;AAElC,MAAI,MAAM,SAAS,GAAG;AACpB,UAAM,IAAI,gBAAgB,2BAA2B;AAAA,EACvD;AAEA,MAAI,MAAM,CAAC,EAAE,KAAK,MAAM,gBAAgB;AACtC,UAAM,IAAI,gBAAgB,0BAA0B,cAAc,GAAG;AAAA,EACvE;AAEA,MAAI,IAAI;AACR,QAAM,SAAkB,CAAC;AAGzB,SAAO,IAAI,MAAM,UAAU,MAAM,CAAC,EAAE,KAAK,MAAM,IAAI;AACjD;AAAA,EACF;AAGA,MAAI;AACJ,MAAI,IAAI,MAAM,UAAU,MAAM,CAAC,EAAE,KAAK,EAAE,WAAW,kBAAkB,GAAG;AACtE,UAAM,MAAM,MAAM,CAAC,EAAE,KAAK;AAC1B,iBAAa,IAAI,MAAM,mBAAmB,MAAM,EAAE,KAAK;AACvD;AAEA,WAAO,IAAI,MAAM,UAAU,MAAM,CAAC,EAAE,KAAK,MAAM,IAAI;AACjD;AAAA,IACF;AAAA,EACF;AAGA,SAAO,IAAI,MAAM,QAAQ;AACvB,UAAM,OAAO,MAAM,CAAC;AACpB,UAAM,UAAU,KAAK,KAAK;AAG1B,QAAI,YAAY,IAAI;AAClB;AACA;AAAA,IACF;AAGA,QAAI,YAAY,cAAc;AAC5B;AAAA,IACF;AAGA,QAAI,QAAQ,WAAW,kBAAkB,GAAG;AAC1C,YAAM,IAAI;AAAA,QACR,8CAA8C,IAAI,CAAC;AAAA,MACrD;AAAA,IACF;AAGA,QAAI,YAAY,eAAe,CAAC,QAAQ,WAAW,cAAc,GAAG,GAAG;AACrE,YAAM,IAAI;AAAA,QACR,yBAAyB,WAAW,aAAa,IAAI,CAAC,UAAU,OAAO;AAAA,MACzE;AAAA,IACF;AAGA;AAGA,UAAM,WAAqB,CAAC;AAC5B,UAAM,WAAqB,CAAC;AAE5B,WAAO,IAAI,MAAM,QAAQ;AACvB,YAAM,MAAM,MAAM,CAAC;AACnB,YAAM,cAAc,IAAI,KAAK;AAG7B,UAAI,QAAQ,IAAI;AACd,iBAAS,KAAK,EAAE;AAChB,iBAAS,KAAK,EAAE;AAChB;AACA;AAAA,MACF;AAGA,UACE,YAAY,WAAW,WAAW,KAClC,YAAY,WAAW,kBAAkB,KACzC,gBAAgB,cAChB;AACA;AAAA,MACF;AAGA,YAAM,SAAS,IAAI,CAAC;AACpB,UAAI,WAAW,OAAO,WAAW,OAAO,WAAW,KAAK;AACtD,cAAM,IAAI;AAAA,UACR,wBAAwB,MAAM,aAAa,IAAI,CAAC;AAAA,QAClD;AAAA,MACF;AAEA,YAAM,OAAO,IAAI,MAAM,CAAC;AAExB,UAAI,WAAW,KAAK;AAElB,iBAAS,KAAK,IAAI;AAClB,iBAAS,KAAK,IAAI;AAAA,MACpB,WACS,WAAW,KAAK;AAEvB,iBAAS,KAAK,IAAI;AAAA,MACpB,OACK;AAGH,iBAAS,KAAK,IAAI;AAAA,MACpB;AAEA;AAAA,IACF;AAKA,UAAM,QAAe;AAAA,MACnB,SAAS,SAAS,KAAK,IAAI;AAAA,MAC3B,SAAS,SAAS,KAAK,IAAI;AAAA,IAC7B;AACA,WAAO,KAAK,KAAK;AAAA,EACnB;AAGA,MAAI,eAAe,MAAM,SAAS;AAClC,SAAO,gBAAgB,KAAK,MAAM,YAAY,EAAE,KAAK,MAAM,IAAI;AAC7D;AAAA,EACF;AACA,MAAI,eAAe,KAAK,MAAM,YAAY,EAAE,KAAK,MAAM,cAAc;AACnE,UAAM,IAAI;AAAA,MACR,wBAAwB,YAAY,cAAc,eAAe,CAAC;AAAA,IACpE;AAAA,EACF;AAEA,SAAO,EAAE,YAAY,OAAO;AAC9B;;;ALjKA,IAAI,OAAO;AACX,SAAS,WAAkB;AACzB,SAAO,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM;AACrC;AAEO,IAAM,eAAN,MAAmB;AAAA,EACP,YAAY,oBAAI,IAAqB;AAAA,EACrC,UAAU,oBAAI,IAAoB;AAAA;AAAA,EAC1C;AAAA,EAET,YAAa,UAA+B,CAAC,GAAG;AAC9C,SAAK,QAAQ,QAAQ,SAAS;AAAA,EAChC;AAAA,EAEA,OAAQ,OAA6B;AACnC,UAAM,OAAO,MAAM;AACnB,QAAI,CAAC,KAAM,OAAM,IAAI,MAAM,kBAAkB;AAC7C,QAAI,KAAK,QAAQ,IAAI,IAAI,GAAG;AAC1B,YAAM,IAAI,yBAAyB,IAAI;AAAA,IACzC;AACA,UAAM,KAAK,WAAW;AACtB,UAAM,MAAM,SAAS;AACrB,UAAM,UAAmB,OAAO,OAAO;AAAA,MACrC;AAAA,MACA;AAAA,MACA,SAAS,MAAM,WAAW;AAAA,MAC1B,WAAW;AAAA,MACX,WAAW;AAAA,IACb,CAAC;AACD,SAAK,UAAU,IAAI,IAAI,OAAO;AAC9B,SAAK,QAAQ,IAAI,MAAM,EAAE;AACzB,WAAO;AAAA,EACT;AAAA,EAEA,IAAK,IAAiC;AACpC,WAAO,KAAK,UAAU,IAAI,EAAE;AAAA,EAC9B;AAAA,EAEA,UAAW,MAAmC;AAC5C,UAAM,KAAK,KAAK,QAAQ,IAAI,IAAI;AAChC,QAAI,OAAO,OAAW,QAAO;AAC7B,WAAO,KAAK,UAAU,IAAI,EAAE;AAAA,EAC9B;AAAA,EAEA,IAAK,IAAqB;AACxB,WAAO,KAAK,UAAU,IAAI,EAAE;AAAA,EAC9B;AAAA,EAEA,OAAmB;AACjB,WAAO,MAAM,KAAK,KAAK,UAAU,OAAO,CAAC;AAAA,EAC3C;AAAA,EAEA,OAAQ,IAAY,SAA0B;AAC5C,UAAM,UAAU,KAAK,UAAU,IAAI,EAAE;AACrC,QAAI,YAAY,QAAW;AACzB,YAAM,IAAI,qBAAqB,EAAE;AAAA,IACnC;AAGA,QAAI,YAAY,QAAQ,QAAQ,KAAK,QAAQ,IAAI,OAAO,GAAG;AACzD,YAAM,IAAI,yBAAyB,OAAO;AAAA,IAC5C;AACA,SAAK,QAAQ,OAAO,QAAQ,IAAI;AAChC,SAAK,QAAQ,IAAI,SAAS,EAAE;AAC5B,UAAM,UAAmB,OAAO,OAAO,EAAE,GAAG,SAAS,MAAM,SAAS,WAAW,SAAS,EAAE,CAAC;AAC3F,SAAK,UAAU,IAAI,IAAI,OAAO;AAC9B,WAAO;AAAA,EACT;AAAA,EAEA,OAAQ,IAAqB;AAC3B,UAAM,UAAU,KAAK,UAAU,IAAI,EAAE;AACrC,QAAI,YAAY,OAAW,QAAO;AAClC,SAAK,QAAQ,OAAO,QAAQ,IAAI;AAChC,SAAK,UAAU,OAAO,EAAE;AACxB,WAAO;AAAA,EACT;AAAA,EAEA,QAAS,IAAY,SAA0B;AAC7C,UAAM,UAAU,KAAK,UAAU,IAAI,EAAE;AACrC,QAAI,YAAY,QAAW;AACzB,YAAM,IAAI,qBAAqB,EAAE;AAAA,IACnC;AACA,UAAM,UAAmB,OAAO,OAAO,EAAE,GAAG,SAAS,SAAS,WAAW,SAAS,EAAE,CAAC;AACrF,SAAK,UAAU,IAAI,IAAI,OAAO;AAC9B,WAAO;AAAA,EACT;AAAA,EAEA,KAAM,IAAY,OAA+B,SAAgC;AAC/E,UAAM,UAAU,KAAK,UAAU,IAAI,EAAE;AACrC,QAAI,YAAY,QAAW;AACzB,YAAM,IAAI,qBAAqB,EAAE;AAAA,IACnC;AAGA,UAAM,aAAa,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAE,KAAM;AAE1D,QAAI;AACF,YAAM,aAAa,WAAW,QAAQ,SAAS,YAAY;AAAA,QACzD,OAAO,SAAS,SAAS,KAAK;AAAA,MAChC,CAAC;AACD,YAAM,UAAmB,OAAO,OAAO,EAAE,GAAG,SAAS,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;AACjG,WAAK,UAAU,IAAI,IAAI,OAAO;AAC9B,aAAO;AAAA,IACT,SACO,KAAK;AAEV,UAAI,eAAe,sBAAsB;AACvC,cAAM,WAAW,IAAI,qBAAqB,IAAI,IAAI,SAAS,IAAI,SAAS;AACxE,cAAM;AAAA,MACR;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAO,IAAY,WAAmB,SAAgC;AACpE,UAAM,UAAU,KAAK,UAAU,IAAI,EAAE;AACrC,QAAI,YAAY,QAAW;AACzB,YAAM,IAAI,qBAAqB,EAAE;AAAA,IACnC;AAGA,UAAM,EAAE,YAAY,OAAO,IAAI,WAAW,SAAS;AAInD,QAAI,eAAe,UAAa,eAAe,QAAQ,MAAM;AAC3D,YAAM,IAAI,yBAAyB,QAAQ,IAAI,QAAQ,MAAM,UAAU;AAAA,IACzE;AAEA,QAAI;AACF,YAAM,aAAa,WAAW,QAAQ,SAAS,QAAQ;AAAA,QACrD,OAAO,SAAS,SAAS,KAAK;AAAA,MAChC,CAAC;AACD,YAAM,UAAmB,OAAO,OAAO,EAAE,GAAG,SAAS,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;AACjG,WAAK,UAAU,IAAI,IAAI,OAAO;AAC9B,aAAO;AAAA,IACT,SACO,KAAK;AAEV,UAAI,eAAe,iBAAiB;AAClC,cAAM,WAAW,IAAI,gBAAgB,IAAI,IAAI,YAAY,IAAI,MAAM;AACnE,cAAM;AAAA,MACR;AACA,YAAM;AAAA,IACR;AAAA,EACF;AACF;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@steinnes/snippets",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "In-memory snippet store with an LLM-friendly editing API for Office.js and similar generated-code workflows.",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"lint": "eslint src",
|
|
23
|
+
"lint:fix": "eslint --fix src",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"check": "npm run lint && npm run typecheck && npm test",
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"build:watch": "tsup --watch"
|
|
30
|
+
},
|
|
31
|
+
"tsup": {
|
|
32
|
+
"entry": [
|
|
33
|
+
"src/index.ts"
|
|
34
|
+
],
|
|
35
|
+
"target": "node20",
|
|
36
|
+
"format": [
|
|
37
|
+
"esm"
|
|
38
|
+
],
|
|
39
|
+
"splitting": false,
|
|
40
|
+
"sourcemap": true,
|
|
41
|
+
"clean": true,
|
|
42
|
+
"dts": true
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@borgar/eslint-config": "^4.0.1",
|
|
46
|
+
"@eslint/js": "~10.0.1",
|
|
47
|
+
"eslint": "^10.0.2",
|
|
48
|
+
"globals": "~17.4.0",
|
|
49
|
+
"tsup": "~8.5.1",
|
|
50
|
+
"typescript": "^5.9.3",
|
|
51
|
+
"typescript-eslint": "~8.58.0",
|
|
52
|
+
"vitest": "^4.0.18"
|
|
53
|
+
}
|
|
54
|
+
}
|