sugar-high 0.9.4 → 1.0.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 CHANGED
@@ -1,19 +1,19 @@
1
1
  # Sugar High
2
2
 
3
- [![Build][build-badge]][build]
3
+ [![npm][npm-badge]][npm]
4
4
 
5
- ### Introduction
5
+ Super lightweight syntax highlighter for JavaScript and JSX—about **1 kB** minified and gzipped. Works in the browser or any JS runtime that can set HTML strings.
6
6
 
7
- Super lightweight JSX syntax highlighter, around 1KB after minified and gzipped
7
+ ![Sugar High preview](https://repository-images.githubusercontent.com/453236442/e9c853e9-4af6-45c4-aa4a-c27afbf16a0d)
8
8
 
9
- ![img](https://repository-images.githubusercontent.com/453236442/aa0db684-bad3-4cd3-a420-f4e53b8c6757)
10
-
11
- ### Usage
9
+ ## Install
12
10
 
13
11
  ```sh
14
- npm install --save sugar-high
12
+ npm install sugar-high
15
13
  ```
16
14
 
15
+ ## Usage
16
+
17
17
  ```js
18
18
  import { highlight } from 'sugar-high'
19
19
 
@@ -22,26 +22,26 @@ const codeHTML = highlight(code)
22
22
  document.querySelector('pre > code').innerHTML = codeHTML
23
23
  ```
24
24
 
25
- ### Highlight with CSS
25
+ ### Language presets
26
+
27
+ The core highlighter targets JavaScript and JSX. For CSS, Rust, Python, and similar, import a preset from [`sugar-high/presets`](https://github.com/huozhi/sugar-high/tree/main/packages/sugar-high/lib/presets) and pass it into `highlight`:
28
+
29
+ ```js
30
+ import { highlight } from 'sugar-high'
31
+ import { rust } from 'sugar-high/presets'
32
+
33
+ const html = highlight(source, { ...rust })
34
+ ```
35
+
36
+ For more language presets and syntax color themes, see **[sugar-high.vercel.app](https://sugar-high.vercel.app/)**.
26
37
 
27
- Then make your own theme with customized colors by token type and put in global CSS. The corresponding class names start with `--sh-` prefix.
38
+ ## Styling
39
+
40
+ Each line is wrapped in `sh__line`. Set **CSS custom properties** `--sh-*` on an ancestor (for example `:root`) to pick colors—inspect the output or the example below for the variable names you need.
41
+
42
+ Example theme:
28
43
 
29
44
  ```css
30
- /**
31
- * Types that sugar-high have:
32
- *
33
- * identifier
34
- * keyword
35
- * string
36
- * Class, number and null
37
- * property
38
- * entity
39
- * jsx literals
40
- * sign
41
- * comment
42
- * break
43
- * space
44
- */
45
45
  :root {
46
46
  --sh-class: #2d5e9d;
47
47
  --sh-identifier: #354150;
@@ -55,11 +55,9 @@ Then make your own theme with customized colors by token type and put in global
55
55
  }
56
56
  ```
57
57
 
58
- ### Features
59
-
60
- #### Line number
58
+ ### Line numbers
61
59
 
62
- Sugar high provide `.sh_line` class name for each line. To display line number, define the `.sh_line::before` element with CSS will enable line numbers automatically.
60
+ Use a `::before` counter on `.sh__line` for gutter numbers:
63
61
 
64
62
  ```css
65
63
  pre code {
@@ -75,9 +73,9 @@ pre code {
75
73
  }
76
74
  ```
77
75
 
78
- ### Line Highlight
76
+ ### Highlighting a line
79
77
 
80
- Use `.sh__line:nth-child(<line number>)` to highlight specific line.
78
+ Target a line with `.sh__line:nth-child(<n>)` (1-based):
81
79
 
82
80
  ```css
83
81
  .sh__line:nth-child(5) {
@@ -85,36 +83,16 @@ Use `.sh__line:nth-child(<line number>)` to highlight specific line.
85
83
  }
86
84
  ```
87
85
 
88
- #### CSS Class Names
86
+ ## Remark
89
87
 
90
- You can use `.sh__token--<token type>` to customize the output node of each token.
88
+ Use the [remark plugin](https://sugar-high.vercel.app/remark) to highlight fenced code blocks when processing Markdown with [remark](https://remark.js.org/). Details: [`packages/remark-sugar-high`](https://github.com/huozhi/sugar-high/tree/main/packages/remark-sugar-high).
91
89
 
92
- ```css
93
- .sh__token--keyword {
94
- background: #f47067;
95
- }
96
- ```
97
-
98
- ### Use With Remark.js
99
-
100
- [Remark.js](https://remark.js.org/) is a powerful markdown processor, you can use the [sugar-high remark plugin](https://sugar-high.vercel.app/remark) with remark.js to highlight code blocks in markdown.
101
-
102
- Check out the [readme](https://github.com/huozhi/sugar-high/tree/main/packages/remark-sugar-high) for more details.
103
-
104
- ### LICENSE
90
+ ## License
105
91
 
106
92
  MIT
107
93
 
108
94
  <!-- Definitions -->
109
95
 
110
- [build-badge]: https://github.com/huozhi/sugar-high/workflows/Test/badge.svg
111
-
112
- [build]: https://github.com/huozhi/sugar-high/actions
113
-
114
- [coverage-badge]: https://badge.fury.io/js/sugar-high.svg
115
-
116
- [coverage]: https://codecov.io/github/huozhi/sugar-high
117
-
118
- [downloads-badge]: https://img.shields.io/npm/dm/sugar-high.svg
96
+ [npm-badge]: https://img.shields.io/npm/v/sugar-high.svg
119
97
 
120
- [downloads]: https://www.npmjs.com/package/sugar-high
98
+ [npm]: https://www.npmjs.com/package/sugar-high
package/lib/index.d.ts CHANGED
@@ -2,6 +2,11 @@ type HighlightOptions = {
2
2
  keywords?: Set<string>
3
3
  onCommentStart?: (curr: string, next: string) => number | boolean
4
4
  onCommentEnd?: (curr: string, prev: string) => number | boolean
5
+ /**
6
+ * At `code[i] === "'"`: return how many code units to consume from `i` as one token,
7
+ * or null/undefined or a number below 1 for default JS single-quoted string rules.
8
+ */
9
+ onQuote?: (curr: string, i: number, code: string) => number | null | undefined
5
10
  }
6
11
 
7
12
  export function highlight(code: string, options?: HighlightOptions): string
package/lib/index.js CHANGED
@@ -45,6 +45,32 @@ const Keywords_Js = new Set([
45
45
  'static',
46
46
  ])
47
47
 
48
+ const Keywords_Ts = new Set([
49
+ ...Keywords_Js,
50
+ 'type',
51
+ 'interface',
52
+ 'enum',
53
+ 'implements',
54
+ 'readonly',
55
+ 'abstract',
56
+ 'declare',
57
+ 'namespace',
58
+ 'module',
59
+ 'private',
60
+ 'protected',
61
+ 'public',
62
+ 'override',
63
+ 'keyof',
64
+ 'infer',
65
+ 'is',
66
+ 'asserts',
67
+ 'satisfies',
68
+ 'as',
69
+ 'unknown',
70
+ 'never',
71
+ 'any',
72
+ ])
73
+
48
74
  const Signs = new Set([
49
75
  '+',
50
76
  '-',
@@ -82,6 +108,76 @@ const DefaultOptions = {
82
108
  onCommentEnd: isCommentEnd_Js,
83
109
  }
84
110
 
111
+ /**
112
+ * Fast, heuristic TS detection. It intentionally prefers speed over full parsing.
113
+ * @param {string} code
114
+ * @returns {boolean}
115
+ */
116
+ function isLikelyTypeScript(code) {
117
+ let tsScore = 0
118
+
119
+ // TS-only declarations and operators.
120
+ if (/\binterface\s+[A-Za-z_$][\w$]*/.test(code)) tsScore += 2
121
+ if (/\btype\s+[A-Za-z_$][\w$]*\s*=/.test(code)) tsScore += 2
122
+ if (/\benum\s+[A-Za-z_$][\w$]*/.test(code)) tsScore += 2
123
+ if (/\b(?:implements|readonly|declare|namespace|satisfies|infer|keyof|asserts)\b/.test(code)) tsScore += 2
124
+
125
+ // Common TS annotations/signatures.
126
+ if (/:\s*[A-Za-z_$][\w$]*(?:<[^>\n]+>)?(?:\[\])?(?=\s*[,)=;{])/m.test(code)) tsScore += 1
127
+ if (/\b(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*:\s*/.test(code)) tsScore += 1
128
+ if (/\)\s*:\s*[A-Za-z_$][\w$]*(?:<[^>\n]+>)?(?:\[\])?\s*(?:=>|\{)/.test(code)) tsScore += 1
129
+
130
+ return tsScore >= 2
131
+ }
132
+
133
+ /**
134
+ * Detects `<T, U = ...>(` style generic parameter lists so they are not
135
+ * treated as JSX tags.
136
+ * @param {string} code
137
+ * @param {number} startIndex
138
+ * @returns {boolean}
139
+ */
140
+ function isTypeParameterListStart(code, startIndex) {
141
+ if (code[startIndex] !== '<') return false
142
+
143
+ let depth = 0
144
+ let sawIdentifierStart = false
145
+
146
+ for (let i = startIndex; i < code.length; i++) {
147
+ const ch = code[i]
148
+
149
+ if (ch === '<') {
150
+ depth++
151
+ continue
152
+ }
153
+ if (ch === '>') {
154
+ depth--
155
+ if (depth === 0) {
156
+ let next = i + 1
157
+ while (next < code.length && /\s/.test(code[next])) next++
158
+ if (!(sawIdentifierStart && code[next] === '(')) return false
159
+
160
+ // Focus this heuristic on generic arrow functions:
161
+ // const fn = <T>(arg) => ...
162
+ const tail = code.slice(next, next + 320)
163
+ return /\)\s*(?::[\s\S]{0,120}?)?=>/.test(tail)
164
+ }
165
+ continue
166
+ }
167
+ if (depth === 0) continue
168
+
169
+ if (/[$A-Za-z_]/.test(ch)) {
170
+ sawIdentifierStart = true
171
+ continue
172
+ }
173
+ if (/[\s,\.\=\?\:\|\&\[\]]/.test(ch)) continue
174
+
175
+ return false
176
+ }
177
+
178
+ return false
179
+ }
180
+
85
181
  /**
86
182
  *
87
183
  * 0 - identifier
@@ -200,16 +296,29 @@ function isRegexStart(str) {
200
296
 
201
297
  /**
202
298
  * @param {string} code
203
- * @param {{ keywords: Set<string> }} options
299
+ * @param {{
300
+ * keywords?: Set<string>
301
+ * onCommentStart?: (curr: string, next: string) => number | boolean
302
+ * onCommentEnd?: (prev: string, curr: string) => number | boolean
303
+ * onQuote?: (curr: string, i: number, code: string) => number | null | undefined
304
+ * } | undefined} options
305
+ * Optional `onQuote(curr, i, code)` at `code[i] === "'"`: return length to consume from `i` (>= 1),
306
+ * or null/undefined/below 1 for default JS single-quoted strings. No substring allocation.
204
307
  * @return {Array<[number, string]>}
205
308
  */
206
309
  function tokenize(code, options) {
310
+ const mergedOptions = { ...DefaultOptions, ...options }
311
+ const hasCustomKeywords = !!(options && options.keywords instanceof Set)
312
+ const isTs = isLikelyTypeScript(code)
313
+ const resolvedKeywords = hasCustomKeywords
314
+ ? mergedOptions.keywords
315
+ : (isTs ? Keywords_Ts : Keywords_Js)
316
+
207
317
  const {
208
- keywords,
209
318
  onCommentStart,
210
319
  onCommentEnd,
211
- } = { ...DefaultOptions, ...options }
212
-
320
+ } = mergedOptions
321
+
213
322
  let current = ''
214
323
  let type = -1
215
324
  /** @type {[number, string]} */
@@ -219,20 +328,22 @@ function tokenize(code, options) {
219
328
  /** @type {Array<[number, string]>} */
220
329
  const tokens = []
221
330
 
222
- /** @type boolean if entered jsx tag, inside <open tag> or </close tag> */
223
- let __jsxEnter = false
224
331
  /**
225
- * @type {0 | 1 | 2}
226
- * @example
227
- * 0 for not in jsx;
228
- * 1 for open jsx tag;
229
- * 2 for closing jsx tag;
230
- **/
332
+ * TS generics (`Map<string>`) and JSX (`<div>`) share the same `<Name …>` lexical shape. We use
333
+ * one tag-lexer mode (__jsxTag + __jsxStack) for both when we enter it; isTsTypeArgStart and
334
+ * isTypeParameterListStart only decide when *not* to enter (e.g. `foo<T>`, `<T>(x)=>`).
335
+ * __jsxEnter gates that mode so a latched __jsxTag (from `<` in `"a<b"`) does not run the tag
336
+ * lexer until we are in real JSX/TSX surface (not inside strings).
337
+ * @type {boolean}
338
+ */
339
+ let __jsxEnter = false
340
+ /** @type {0 | 1 | 2} 0 = none; 1 = inside `<open`; 2 = inside `</close` */
231
341
  let __jsxTag = 0
232
342
  let __jsxExpr = false
233
343
 
234
- // only match paired (open + close) tags, not self-closing tags
344
+ /** Nested `<open>…</open>` depth (content between tags, including nested elements). */
235
345
  let __jsxStack = 0
346
+
236
347
  const __jsxChild = () => __jsxEnter && !__jsxExpr && !__jsxTag
237
348
  // < __content__ >
238
349
  const inJsxTag = () => __jsxTag && !__jsxChild()
@@ -266,7 +377,7 @@ function tokenize(code, options) {
266
377
  const [, lastToken] = last
267
378
  if (isIdentifier(token)) {
268
379
  // classify jsx open tag
269
- if ((lastToken === '<' || lastToken === '</'))
380
+ if ((lastToken === '<' || lastToken === '</'))
270
381
  return T_ENTITY
271
382
  }
272
383
  }
@@ -277,7 +388,7 @@ function tokenize(code, options) {
277
388
  // Determine strings first before other types
278
389
  if (inStringQuotes() || inStrTemplateLiterals()) {
279
390
  return T_STRING
280
- } else if (keywords.has(token)) {
391
+ } else if (resolvedKeywords.has(token)) {
281
392
  return last[1] === '.' ? T_IDENTIFIER : T_KEYWORD
282
393
  } else if (isLineBreak) {
283
394
  return T_BREAK
@@ -299,16 +410,16 @@ function tokenize(code, options) {
299
410
  }
300
411
 
301
412
  /**
302
- *
303
- * @param {number} type_
304
- * @param {string} token_
413
+ *
414
+ * @param {number | undefined} [type_]
415
+ * @param {string | undefined} [token_]
305
416
  */
306
417
  const append = (type_, token_) => {
307
418
  if (token_) {
308
419
  current = token_
309
420
  }
310
421
  if (current) {
311
- type = type_ || classify(current)
422
+ type = typeof type_ === 'number' ? type_ : classify(current)
312
423
  /** @type [number, string] */
313
424
  const pair = [type, current]
314
425
  if (type !== T_SPACE && type !== T_BREAK) {
@@ -326,6 +437,30 @@ function tokenize(code, options) {
326
437
  const p_c = prev + curr // previous and current
327
438
  const c_n = curr + next // current and next
328
439
 
440
+ // onQuote(curr, i, code): length from i; end = i + len (capped).
441
+ if (
442
+ typeof mergedOptions.onQuote === 'function' &&
443
+ curr === "'" &&
444
+ !inStringQuotes() &&
445
+ !inJsxLiterals() &&
446
+ !inStrTemplateLiterals()
447
+ ) {
448
+ const rawLen = mergedOptions.onQuote(curr, i, code)
449
+ if (
450
+ typeof rawLen === 'number' &&
451
+ rawLen >= 1 &&
452
+ !Number.isNaN(rawLen)
453
+ ) {
454
+ const len = Math.min(rawLen, code.length - i)
455
+ const end = i + len
456
+ append()
457
+ current = code.slice(i, end)
458
+ append(T_IDENTIFIER)
459
+ i = end - 1
460
+ continue
461
+ }
462
+ }
463
+
329
464
  // Determine string quotation outside of jsx literals and template literals.
330
465
  // Inside jsx literals or template literals, string quotation is still part of it.
331
466
  if (isSingleQuotes(curr) && !inJsxLiterals() && !inStrTemplateLiterals()) {
@@ -469,7 +604,7 @@ function tokenize(code, options) {
469
604
  if (isSpaces(current)) {
470
605
  append()
471
606
  }
472
-
607
+
473
608
  // Now check if the accumulated token is a property
474
609
  const prop = current + curr
475
610
  if (isIdentifier(prop)) {
@@ -483,12 +618,45 @@ function tokenize(code, options) {
483
618
 
484
619
  // if it's not in a jsx tag declaration or a string, close child if next is jsx close tag
485
620
  if (!__jsxTag && (curr === '<' && isIdentifierChar(next) || c_n === '</')) {
486
- __jsxTag = next === '/' ? 2 : 1
621
+ let prevNonSpace = i - 1
622
+ while (prevNonSpace >= 0 && /\s/.test(code[prevNonSpace])) prevNonSpace--
623
+ const prevChar = prevNonSpace >= 0 ? code[prevNonSpace] : ''
624
+
625
+ const [lastType, lastTok] = last
626
+ // Without a space before `<`, the LHS is often still in `current` (not flushed), so `last` is stale.
627
+ let typeArgFromPending = false
628
+ let jsxFromPending = false
629
+ if (current && !isSpaces(current)) {
630
+ const w = current
631
+ // Unflushed LHS before `<`: numbers/null/Upper (isCls), and booleans, behave like `foo<` (type
632
+ // args / `<`). Any other keyword (`return`, `void`, …) is JSX-friendly — no keyword allowlist.
633
+ if (isCls(w) || w === 'true' || w === 'false') {
634
+ typeArgFromPending = true
635
+ } else if (resolvedKeywords.has(w) && isIdentifier(w)) {
636
+ jsxFromPending = true
637
+ } else if (isIdentifier(w)) {
638
+ typeArgFromPending = true
639
+ }
640
+ }
641
+
642
+ const isTsTypeArgStart =
643
+ curr === '<' &&
644
+ /[$\w\]\)]/.test(prevChar) &&
645
+ (typeArgFromPending ||
646
+ (!jsxFromPending &&
647
+ (lastType === T_IDENTIFIER ||
648
+ lastType === T_CLS_NUMBER ||
649
+ (lastType === T_SIGN && (lastTok === ')' || lastTok === ']')))))
650
+ const isTsGenericStart = curr === '<' && isTypeParameterListStart(code, i)
651
+ if (!isTsTypeArgStart && !isTsGenericStart) {
652
+ __jsxTag = next === '/' ? 2 : 1
653
+ }
487
654
 
488
- // current and next char can form a jsx open or close tag
489
655
  if (curr === '<' && (next === '/' || isAlpha(next))) {
490
656
  if (
491
- !inStringContent() &&
657
+ !isTsTypeArgStart &&
658
+ !isTsGenericStart &&
659
+ !inStringContent() &&
492
660
  !inJsxLiterals() &&
493
661
  !inRegexQuotes()
494
662
  ) {
@@ -532,7 +700,7 @@ function tokenize(code, options) {
532
700
  const isEol = () => isEof() || code[i] === '\n'
533
701
 
534
702
  let foundClose = false
535
-
703
+
536
704
  // traverse to find closing regex slash
537
705
  for (; !isEol(); i++) {
538
706
  if (code[i] === '/' && code[i - 1] !== '\\') {
@@ -640,7 +808,7 @@ function generate(tokens) {
640
808
  * @param {any} children
641
809
  * @return {{type: string, tagName: string, children: any[], properties: Record<string, string>}}
642
810
  */
643
- const createLine = (children) =>
811
+ const createLine = (children) =>
644
812
  ({
645
813
  type: 'element',
646
814
  tagName: 'span',
@@ -678,9 +846,13 @@ function generate(tokens) {
678
846
  }
679
847
  /** @type {Array<[number, string]>} */
680
848
  const lineTokens = []
849
+ let lastWasBreak = false
850
+
681
851
  for (let i = 0; i < tokens.length; i++) {
682
852
  const token = tokens[i]
683
853
  const [type, value] = token
854
+ const isLastToken = i === tokens.length - 1
855
+
684
856
  if (type !== T_BREAK) {
685
857
  // Divide multi-line token into multi-line code
686
858
  if (value.includes('\n')) {
@@ -695,20 +867,35 @@ function generate(tokens) {
695
867
  } else {
696
868
  lineTokens.push(token)
697
869
  }
870
+ lastWasBreak = false
698
871
  } else {
699
- lineTokens.push([type, ''])
700
- flushLine(lineTokens)
701
- lineTokens.length = 0
872
+ if (lastWasBreak) {
873
+ // Consecutive break - create empty line
874
+ flushLine([])
875
+ } else {
876
+ // First break after content - flush current line
877
+ flushLine(lineTokens)
878
+ lineTokens.length = 0
879
+ }
880
+
881
+ // If this is the last token and it's a break, create an empty line
882
+ if (isLastToken) {
883
+ flushLine([])
884
+ }
885
+
886
+ lastWasBreak = true
702
887
  }
703
888
  }
704
889
 
705
- if (lineTokens.length)
890
+ // Flush remaining tokens if any
891
+ if (lineTokens.length) {
706
892
  flushLine(lineTokens)
893
+ }
707
894
 
708
895
  return lines
709
896
  }
710
897
 
711
- /** @param { className: string, style?: Record<string, string> } */
898
+ /** @param {{ className: string, style?: Record<string, string> }} props */
712
899
  const propsToString = (props) => {
713
900
  let str = `class="${props.className}"`
714
901
 
@@ -739,12 +926,13 @@ function toHtml(lines) {
739
926
  /**
740
927
  *
741
928
  * @param {string} code
742
- * @param {
743
- * {
744
- * keywords: Set<string>
929
+ * @param {{
930
+ * keywords?: Set<string>
745
931
  * onCommentStart?: (curr: string, next: string) => number | boolean
746
932
  * onCommentEnd?: (curr: string, prev: string) => number | boolean
933
+ * onQuote?: (curr: string, i: number, code: string) => number | null | undefined
747
934
  * } | undefined} options
935
+ * `onQuote` same as `tokenize`.
748
936
  * @returns {string}
749
937
  */
750
938
  function highlight(code, options) {
@@ -1,7 +1,8 @@
1
1
  type LanguageConfig = {
2
2
  keywords: Set<string>
3
- onCommentStart(curr: string, next: string): 0 | 1 | 2
4
- onCommentEnd(prev: string, curr: string): 0 | 1 | 2
3
+ onCommentStart?(curr: string, next: string): 0 | 1 | 2
4
+ onCommentEnd?(prev: string, curr: string): 0 | 1 | 2
5
+ onQuote?(curr: string, i: number, code: string): number | null | undefined
5
6
  }
6
7
 
7
8
  export const css: LanguageConfig
@@ -6,3 +6,38 @@ export const keywords = new Set([
6
6
  'async', 'await', 'dyn', 'abstract', 'become', 'box', 'do', 'final', 'macro', 'override',
7
7
  'priv', 'typeof', 'unsized', 'virtual', 'yield', 'try',
8
8
  ])
9
+
10
+ /**
11
+ * @param {string} curr `code[i]` (apostrophe)
12
+ * @param {number} i
13
+ * @param {string} code
14
+ * @returns {number} code units to consume from `i`
15
+ */
16
+ export function onQuote(curr, i, code) {
17
+ if (curr !== "'" || i + 1 >= code.length) return 1
18
+ const n1 = code[i + 1]
19
+ if (n1 === '\\') {
20
+ let j = i + 2
21
+ while (j < code.length) {
22
+ if (code[j] === '\\') {
23
+ j += 2
24
+ continue
25
+ }
26
+ if (code[j] === "'") return j - i + 1
27
+ j++
28
+ }
29
+ return code.length - i
30
+ }
31
+ if (n1 === '_') {
32
+ return 2
33
+ }
34
+ if (/[a-zA-Z]/.test(n1)) {
35
+ let j = i + 2
36
+ while (j < code.length && /[a-zA-Z0-9_]/.test(code[j])) j++
37
+ if (j < code.length && code[j] === "'") {
38
+ return j - i + 1
39
+ }
40
+ return j - i
41
+ }
42
+ return 1
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sugar-high",
3
- "version": "0.9.4",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "types": "./lib/index.d.ts",
6
6
  "main": "./lib/index.js",
@@ -19,14 +19,13 @@
19
19
  "lib"
20
20
  ],
21
21
  "license": "MIT",
22
+ "scripts": {
23
+ "test": "vitest",
24
+ "build": "echo 'package requires no build'"
25
+ },
22
26
  "devDependencies": {
23
27
  "@types/node": "22.12.0",
24
28
  "typescript": "5.7.3",
25
29
  "vitest": "^3.0.2"
26
- },
27
- "packageManager": "pnpm@8.7.1",
28
- "scripts": {
29
- "test": "vitest",
30
- "build": "echo 'package requires no build'"
31
30
  }
32
- }
31
+ }