prettier-plugin-razor2 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/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +715 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Robert Macfie
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# prettier-plugin-razor2
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [Getting Started](#getting-started)
|
|
6
|
+
- [Known limitations](#known-limitations)
|
|
7
|
+
- [Composing with other plugins](#composing-with-other-plugins)
|
|
8
|
+
- [Configuration](#configuration)
|
|
9
|
+
- [Development](#development)
|
|
10
|
+
|
|
11
|
+
## Description
|
|
12
|
+
|
|
13
|
+
An opinionated formatter plugin for [Prettier](https://prettier.io) that adds
|
|
14
|
+
support for Razor files — `.razor` (Blazor components) and `.cshtml` (MVC views
|
|
15
|
+
and Razor Pages).
|
|
16
|
+
|
|
17
|
+
It formats the HTML in a Razor file by delegating to Prettier's own HTML
|
|
18
|
+
formatter, and the C# in `@code`/`@functions`/`@{ }` blocks by delegating to
|
|
19
|
+
[CSharpier](https://csharpier.com). Control-flow blocks (`@if`, `@foreach`, …)
|
|
20
|
+
are re-indented in Allman style. Please try it out and provide feedback.
|
|
21
|
+
|
|
22
|
+
# Getting Started
|
|
23
|
+
|
|
24
|
+
Install the plugin and Prettier itself.
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm install --save-dev prettier prettier-plugin-razor2
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Add to your Prettier config file:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"plugins": ["prettier-plugin-razor2"]
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Optionally, disable the CSharpier integration to only format the HTML parts.
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"plugins": ["prettier-plugin-razor2"],
|
|
43
|
+
"csharpierEnabled": false
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## C# formatting
|
|
48
|
+
|
|
49
|
+
C# formatting requires the [CSharpier](https://csharpier.com) CLI on your `PATH`
|
|
50
|
+
(`dotnet tool install csharpier`, invoked as `dotnet csharpier`). If it is not
|
|
51
|
+
available, C# is left untouched and a one-time warning is printed. Set
|
|
52
|
+
`csharpierEnabled: false` to disable C# formatting (and silence the warning), or
|
|
53
|
+
override the command with `csharpierCommand`.
|
|
54
|
+
|
|
55
|
+
# Known limitations
|
|
56
|
+
|
|
57
|
+
- `@switch` blocks with several cases: the bare `case X:` / `break;` lines are
|
|
58
|
+
plain text to the HTML formatter, so consecutive arms can end up glued onto
|
|
59
|
+
one line (`break; case Y:`). The output is stable and nothing is lost — it
|
|
60
|
+
just isn't pretty yet.
|
|
61
|
+
- Templated Razor delegates (`.cshtml`), e.g.
|
|
62
|
+
`@Repeat(items, @<li>@item.Name</li>)`, are preserved but not reformatted.
|
|
63
|
+
|
|
64
|
+
# Composing with other plugins
|
|
65
|
+
|
|
66
|
+
The HTML in your Razor files is formatted by Prettier's own HTML formatter, so
|
|
67
|
+
other plugins that hook the `html` parser work on it too — including inside
|
|
68
|
+
control blocks. For example,
|
|
69
|
+
[prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss)
|
|
70
|
+
sorts `class="…"` attributes. List both plugins (Tailwind last, per its docs):
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{ "plugins": ["prettier-plugin-razor2", "prettier-plugin-tailwindcss"] }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This applies only to the HTML: classes written inside C# (a `@code`/`@{ }` block
|
|
77
|
+
or a dynamic `class="@GetCss()"` value) are not sorted.
|
|
78
|
+
|
|
79
|
+
# Configuration
|
|
80
|
+
|
|
81
|
+
This library follows the same configuration format as Prettier, which is
|
|
82
|
+
documented [here](https://prettier.io/docs/en/configuration.html). It adds two
|
|
83
|
+
options:
|
|
84
|
+
|
|
85
|
+
| Option | Default | Description |
|
|
86
|
+
| ------------------ | -------------------- | --------------------------------------------------------------------- |
|
|
87
|
+
| `csharpierEnabled` | `true` | Format embedded C# with CSharpier. When `false`, C# is kept verbatim. |
|
|
88
|
+
| `csharpierCommand` | `"dotnet csharpier"` | Command used to invoke the CSharpier CLI. |
|
|
89
|
+
|
|
90
|
+
# Development
|
|
91
|
+
|
|
92
|
+
The plugin is written in TypeScript in `src/` and published as a single
|
|
93
|
+
[tsdown](https://tsdown.dev)-bundled file in `dist/`.
|
|
94
|
+
|
|
95
|
+
pnpm install # install dependencies
|
|
96
|
+
pnpm test # run the test suite (node --test)
|
|
97
|
+
pnpm typecheck # type-check without emitting
|
|
98
|
+
pnpm build # bundle src/ to dist/ with tsdown
|
|
99
|
+
|
|
100
|
+
Tests run the TypeScript sources directly via Node's native type stripping, so
|
|
101
|
+
no build step is required to develop or test. The C# tests need the CSharpier
|
|
102
|
+
CLI (`dotnet tool restore`); they skip automatically when it isn't available.
|
|
103
|
+
|
|
104
|
+
Releases: bump `version` in package.json, commit, tag `vX.Y.Z`, and
|
|
105
|
+
`git push --follow-tags` — GitHub Actions runs the checks and publishes to npm.
|
|
106
|
+
|
|
107
|
+
> **Fork notice** This project started as a fork of
|
|
108
|
+
> [prettier-plugin-razor](https://github.com/KristinaPlusPlus/prettier-plugin-razor),
|
|
109
|
+
> but has since been mostly rewritten.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Parser, Plugin, Printer, SupportLanguage } from "prettier";
|
|
2
|
+
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
/** Trivial AST: the whole document is formatted in one embedded pass. */
|
|
5
|
+
interface RazorRoot {
|
|
6
|
+
type: 'razor-root';
|
|
7
|
+
source: string;
|
|
8
|
+
}
|
|
9
|
+
declare const languages: SupportLanguage[];
|
|
10
|
+
declare const parsers: Record<string, Parser<RazorRoot>>;
|
|
11
|
+
declare const options: Plugin['options'];
|
|
12
|
+
declare const printers: Record<string, Printer<RazorRoot>>;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { languages, options, parsers, printers };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
import { doc } from "prettier";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
/** Build the block-level placeholder element for block index `n`. */
|
|
5
|
+
function blockPlaceholderFor(n) {
|
|
6
|
+
return `<div data-razor="${n}"></div>`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Matcher for {@link blockPlaceholderFor} output in formatted HTML; group 1 is
|
|
10
|
+
* the preceding indentation, group 2 the block index. Constructed fresh per
|
|
11
|
+
* call \u2014 restore recurses, so a shared /g regex's lastIndex would clash.
|
|
12
|
+
*/
|
|
13
|
+
function blockPlaceholderRE() {
|
|
14
|
+
return /([ \t]*)<div data-razor="(\d+)"><\/div>/g;
|
|
15
|
+
}
|
|
16
|
+
const HTML_ELEMENTS = new Set("a abbr address area article aside audio b base bdi bdo blockquote body br button canvas caption cite code col colgroup data datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup hr html i iframe img input ins kbd label legend li link main map mark menu meta meter nav noscript object ol optgroup option output p param picture pre progress q rp rt ruby s samp script search section select slot small source span strong style sub summary sup table tbody td template textarea tfoot th thead time title tr track u ul var video wbr".split(" "));
|
|
17
|
+
const DIRECTIVES = /* @__PURE__ */ new Set([
|
|
18
|
+
"page",
|
|
19
|
+
"using",
|
|
20
|
+
"inject",
|
|
21
|
+
"inherits",
|
|
22
|
+
"namespace",
|
|
23
|
+
"implements",
|
|
24
|
+
"attribute",
|
|
25
|
+
"layout",
|
|
26
|
+
"typeparam",
|
|
27
|
+
"model",
|
|
28
|
+
"rendermode",
|
|
29
|
+
"addTagHelper",
|
|
30
|
+
"removeTagHelper",
|
|
31
|
+
"tagHelperPrefix",
|
|
32
|
+
"preservewhitespace"
|
|
33
|
+
]);
|
|
34
|
+
const CONTROL = /* @__PURE__ */ new Set([
|
|
35
|
+
"if",
|
|
36
|
+
"for",
|
|
37
|
+
"foreach",
|
|
38
|
+
"while",
|
|
39
|
+
"switch",
|
|
40
|
+
"using",
|
|
41
|
+
"lock",
|
|
42
|
+
"do",
|
|
43
|
+
"try",
|
|
44
|
+
"section"
|
|
45
|
+
]);
|
|
46
|
+
const isWs = (c) => c === " " || c === " " || c === "\r" || c === "\n";
|
|
47
|
+
const isLetter = (c) => c !== void 0 && /[A-Za-z]/.test(c);
|
|
48
|
+
function skipWs(src, i) {
|
|
49
|
+
while (i < src.length && isWs(src[i])) i++;
|
|
50
|
+
return i;
|
|
51
|
+
}
|
|
52
|
+
function readIdent(src, i) {
|
|
53
|
+
let j = i;
|
|
54
|
+
while (j < src.length && isLetter(src[j])) j++;
|
|
55
|
+
return src.slice(i, j);
|
|
56
|
+
}
|
|
57
|
+
function readTagName(src, i) {
|
|
58
|
+
let j = i;
|
|
59
|
+
while (j < src.length && /[A-Za-z0-9]/.test(src[j])) j++;
|
|
60
|
+
return src.slice(i, j);
|
|
61
|
+
}
|
|
62
|
+
function atLineStart(src, i) {
|
|
63
|
+
let j = i - 1;
|
|
64
|
+
while (j >= 0 && (src[j] === " " || src[j] === " ")) j--;
|
|
65
|
+
return j < 0 || src[j] === "\n";
|
|
66
|
+
}
|
|
67
|
+
function skipUntil(src, from, needle) {
|
|
68
|
+
const idx = src.indexOf(needle, from);
|
|
69
|
+
return idx === -1 ? src.length : idx + needle.length;
|
|
70
|
+
}
|
|
71
|
+
function skipCSharpToken(src, i) {
|
|
72
|
+
const c = src[i];
|
|
73
|
+
if (c === "\"" && src[i + 1] === "\"" && src[i + 2] === "\"") {
|
|
74
|
+
let openLen = 0;
|
|
75
|
+
while (src[i + openLen] === "\"") openLen++;
|
|
76
|
+
let j = i + openLen;
|
|
77
|
+
while (j < src.length) {
|
|
78
|
+
if (src[j] !== "\"") {
|
|
79
|
+
j++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
let runLen = 0;
|
|
83
|
+
while (src[j + runLen] === "\"") runLen++;
|
|
84
|
+
if (runLen >= openLen) return j + openLen;
|
|
85
|
+
j += runLen;
|
|
86
|
+
}
|
|
87
|
+
return src.length;
|
|
88
|
+
}
|
|
89
|
+
if (c === "@" && src[i + 1] === "\"") {
|
|
90
|
+
let j = i + 2;
|
|
91
|
+
while (j < src.length) {
|
|
92
|
+
if (src[j] === "\"") {
|
|
93
|
+
if (src[j + 1] === "\"") {
|
|
94
|
+
j += 2;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
return j + 1;
|
|
98
|
+
}
|
|
99
|
+
j++;
|
|
100
|
+
}
|
|
101
|
+
return src.length;
|
|
102
|
+
}
|
|
103
|
+
if (c === "\"" || c === "'") {
|
|
104
|
+
let j = i + 1;
|
|
105
|
+
while (j < src.length) {
|
|
106
|
+
if (src[j] === "\\") {
|
|
107
|
+
j += 2;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (src[j] === c) return j + 1;
|
|
111
|
+
j++;
|
|
112
|
+
}
|
|
113
|
+
return src.length;
|
|
114
|
+
}
|
|
115
|
+
if (c === "/" && src[i + 1] === "/") {
|
|
116
|
+
let j = i + 2;
|
|
117
|
+
while (j < src.length && src[j] !== "\n") j++;
|
|
118
|
+
return j;
|
|
119
|
+
}
|
|
120
|
+
if (c === "/" && src[i + 1] === "*") return skipUntil(src, i + 2, "*/");
|
|
121
|
+
return -1;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 0-based indices of the lines in `text` that _start inside_ a multi-line C#
|
|
125
|
+
* string literal (verbatim or raw). Their leading whitespace is string content,
|
|
126
|
+
* so re-indentation must leave them untouched. Multi-line comments are not
|
|
127
|
+
* protected — shifting their interior is cosmetic, not a content change.
|
|
128
|
+
*/
|
|
129
|
+
function linesInsideCSharpStrings(text) {
|
|
130
|
+
const inside = /* @__PURE__ */ new Set();
|
|
131
|
+
let line = 0;
|
|
132
|
+
let i = 0;
|
|
133
|
+
while (i < text.length) {
|
|
134
|
+
if (text[i] === "\n") {
|
|
135
|
+
line++;
|
|
136
|
+
i++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const end = skipCSharpToken(text, i);
|
|
140
|
+
if (end === -1) {
|
|
141
|
+
i++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const isString = text[i] === "\"" || text[i] === "'" || text[i] === "@" && text[i + 1] === "\"";
|
|
145
|
+
for (let j = i; j < end; j++) if (text[j] === "\n") {
|
|
146
|
+
line++;
|
|
147
|
+
if (isString) inside.add(line);
|
|
148
|
+
}
|
|
149
|
+
i = end;
|
|
150
|
+
}
|
|
151
|
+
return inside;
|
|
152
|
+
}
|
|
153
|
+
function matchDelimited(src, i, open, close) {
|
|
154
|
+
let depth = 0;
|
|
155
|
+
let j = i;
|
|
156
|
+
while (j < src.length) {
|
|
157
|
+
const skipped = skipCSharpToken(src, j);
|
|
158
|
+
if (skipped !== -1) {
|
|
159
|
+
j = skipped;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const c = src[j];
|
|
163
|
+
if (c === open) depth++;
|
|
164
|
+
else if (c === close && --depth === 0) return j + 1;
|
|
165
|
+
j++;
|
|
166
|
+
}
|
|
167
|
+
return src.length;
|
|
168
|
+
}
|
|
169
|
+
const matchBraceCSharp = (src, i) => matchDelimited(src, i, "{", "}");
|
|
170
|
+
const matchParen = (src, i) => matchDelimited(src, i, "(", ")");
|
|
171
|
+
function skipMarkupQuote(src, i) {
|
|
172
|
+
const q = src[i];
|
|
173
|
+
let j = i + 1;
|
|
174
|
+
while (j < src.length && src[j] !== q) j++;
|
|
175
|
+
return j < src.length ? j + 1 : src.length;
|
|
176
|
+
}
|
|
177
|
+
function matchBraceMarkup(src, i) {
|
|
178
|
+
let depth = 0;
|
|
179
|
+
let j = i;
|
|
180
|
+
while (j < src.length) {
|
|
181
|
+
if (src[j] === "@" && src[j + 1] === "*") {
|
|
182
|
+
j = skipUntil(src, j + 2, "*@");
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (src.startsWith("<!--", j)) {
|
|
186
|
+
j = skipUntil(src, j + 4, "-->");
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (src[j] === "@" && src[j + 1] === "{") {
|
|
190
|
+
j = matchBraceCSharp(src, j + 1);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (src[j] === "@" && src[j + 1] === "(") {
|
|
194
|
+
j = matchParen(src, j + 1);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (src[j] === "\"" || src[j] === "'") {
|
|
198
|
+
j = skipMarkupQuote(src, j);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const c = src[j];
|
|
202
|
+
if (c === "{") depth++;
|
|
203
|
+
else if (c === "}" && --depth === 0) return j + 1;
|
|
204
|
+
j++;
|
|
205
|
+
}
|
|
206
|
+
return src.length;
|
|
207
|
+
}
|
|
208
|
+
function parseClause(src, headerStart, afterKeyword, hasCondition, requireCondition) {
|
|
209
|
+
let pos = skipWs(src, afterKeyword);
|
|
210
|
+
if (hasCondition) if (src[pos] !== "(") {
|
|
211
|
+
if (requireCondition) return null;
|
|
212
|
+
} else {
|
|
213
|
+
pos = matchParen(src, pos);
|
|
214
|
+
pos = skipWs(src, pos);
|
|
215
|
+
}
|
|
216
|
+
if (src[pos] !== "{") return null;
|
|
217
|
+
const bodyEnd = matchBraceMarkup(src, pos);
|
|
218
|
+
return {
|
|
219
|
+
clause: {
|
|
220
|
+
header: src.slice(headerStart, pos).trimEnd(),
|
|
221
|
+
body: src.slice(pos + 1, bodyEnd - 1)
|
|
222
|
+
},
|
|
223
|
+
end: bodyEnd
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function parseControl(src, at, keyword) {
|
|
227
|
+
const afterKw = at + 1 + keyword.length;
|
|
228
|
+
if (keyword === "section") {
|
|
229
|
+
let pos = skipWs(src, afterKw);
|
|
230
|
+
pos += readIdent(src, pos).length;
|
|
231
|
+
pos = skipWs(src, pos);
|
|
232
|
+
if (src[pos] !== "{") return null;
|
|
233
|
+
const bodyEnd = matchBraceMarkup(src, pos);
|
|
234
|
+
return {
|
|
235
|
+
block: {
|
|
236
|
+
kind: "control",
|
|
237
|
+
clauses: [{
|
|
238
|
+
header: src.slice(at, pos).trimEnd(),
|
|
239
|
+
body: src.slice(pos + 1, bodyEnd - 1)
|
|
240
|
+
}]
|
|
241
|
+
},
|
|
242
|
+
end: bodyEnd
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const first = parseClause(src, at, afterKw, keyword !== "do" && keyword !== "try", keyword !== "using" && keyword !== "lock");
|
|
246
|
+
if (!first) return null;
|
|
247
|
+
const clauses = [first.clause];
|
|
248
|
+
let end = first.end;
|
|
249
|
+
if (keyword === "do") {
|
|
250
|
+
let pos = skipWs(src, end);
|
|
251
|
+
if (readIdent(src, pos) === "while") {
|
|
252
|
+
pos = skipWs(src, pos + 5);
|
|
253
|
+
if (src[pos] === "(") {
|
|
254
|
+
pos = matchParen(src, pos);
|
|
255
|
+
let trailer = src.slice(skipWs(src, first.end), pos);
|
|
256
|
+
if (src[pos] === ";") {
|
|
257
|
+
pos++;
|
|
258
|
+
trailer += ";";
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
block: {
|
|
262
|
+
kind: "control",
|
|
263
|
+
clauses,
|
|
264
|
+
trailer
|
|
265
|
+
},
|
|
266
|
+
end: pos
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
block: {
|
|
272
|
+
kind: "control",
|
|
273
|
+
clauses
|
|
274
|
+
},
|
|
275
|
+
end
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const chained = keyword === "if" ? ["else"] : keyword === "try" ? ["catch", "finally"] : [];
|
|
279
|
+
for (;;) {
|
|
280
|
+
const pos = skipWs(src, end);
|
|
281
|
+
const next = readIdent(src, pos);
|
|
282
|
+
if (!chained.includes(next)) break;
|
|
283
|
+
if (next === "else") {
|
|
284
|
+
const afterElse = skipWs(src, pos + 4);
|
|
285
|
+
if (readIdent(src, afterElse) === "if") {
|
|
286
|
+
const arm = parseClause(src, pos, afterElse + 2, true, true);
|
|
287
|
+
if (!arm) break;
|
|
288
|
+
clauses.push(arm.clause);
|
|
289
|
+
end = arm.end;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const arm = parseClause(src, pos, pos + 4, false, false);
|
|
293
|
+
if (!arm) break;
|
|
294
|
+
clauses.push(arm.clause);
|
|
295
|
+
end = arm.end;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
const arm = parseClause(src, pos, pos + next.length, next === "catch", false);
|
|
299
|
+
if (!arm) break;
|
|
300
|
+
clauses.push(arm.clause);
|
|
301
|
+
end = arm.end;
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
block: {
|
|
305
|
+
kind: "control",
|
|
306
|
+
clauses
|
|
307
|
+
},
|
|
308
|
+
end
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/** Replace Razor block constructs with placeholders; see {@link MaskResult}. */
|
|
312
|
+
function mask(src) {
|
|
313
|
+
const blocks = [];
|
|
314
|
+
const tagAliases = [];
|
|
315
|
+
const blockPlaceholder = (block) => blockPlaceholderFor(blocks.push(block) - 1);
|
|
316
|
+
const inlinePlaceholder = (block) => `${blocks.push(block) - 1}`;
|
|
317
|
+
let out = "";
|
|
318
|
+
let i = 0;
|
|
319
|
+
const n = src.length;
|
|
320
|
+
while (i < n) {
|
|
321
|
+
const c = src[i];
|
|
322
|
+
if (c === "<" && !src.startsWith("<!--", i)) {
|
|
323
|
+
const nameStart = src[i + 1] === "/" ? i + 2 : i + 1;
|
|
324
|
+
const name = readTagName(src, nameStart);
|
|
325
|
+
const after = src[nameStart + name.length];
|
|
326
|
+
const nameEnds = after === void 0 || after === ">" || after === "/" || isWs(after);
|
|
327
|
+
if (name !== "" && nameEnds && /^[A-Z]/.test(name) && HTML_ELEMENTS.has(name.toLowerCase())) {
|
|
328
|
+
let idx = tagAliases.indexOf(name);
|
|
329
|
+
if (idx === -1) idx = tagAliases.push(name) - 1;
|
|
330
|
+
out += src.slice(i, nameStart) + `rz-${idx}`;
|
|
331
|
+
i = nameStart + name.length;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (c === "@" && src[i + 1] === "*") {
|
|
336
|
+
const end = skipUntil(src, i + 2, "*@");
|
|
337
|
+
out += inlinePlaceholder({
|
|
338
|
+
kind: "inline",
|
|
339
|
+
text: src.slice(i, end)
|
|
340
|
+
});
|
|
341
|
+
i = end;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (src.startsWith("<!--", i)) {
|
|
345
|
+
const end = skipUntil(src, i + 4, "-->");
|
|
346
|
+
out += src.slice(i, end);
|
|
347
|
+
i = end;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (c !== "@") {
|
|
351
|
+
out += c;
|
|
352
|
+
i++;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (src[i + 1] === "@") {
|
|
356
|
+
out += "@@";
|
|
357
|
+
i += 2;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (src[i + 1] === "(") {
|
|
361
|
+
const end = matchParen(src, i + 1);
|
|
362
|
+
out += inlinePlaceholder({
|
|
363
|
+
kind: "inline",
|
|
364
|
+
text: src.slice(i, end)
|
|
365
|
+
});
|
|
366
|
+
i = end;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (src[i + 1] === "{") {
|
|
370
|
+
const end = matchBraceCSharp(src, i + 1);
|
|
371
|
+
out += blockPlaceholder({
|
|
372
|
+
kind: "verbatim",
|
|
373
|
+
opener: "@{",
|
|
374
|
+
body: src.slice(i + 2, end - 1),
|
|
375
|
+
csharp: "statements",
|
|
376
|
+
raw: src.slice(i, end)
|
|
377
|
+
});
|
|
378
|
+
i = end;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const kw = readIdent(src, i + 1);
|
|
382
|
+
if (kw === "code" || kw === "functions") {
|
|
383
|
+
const bracePos = skipWs(src, i + 1 + kw.length);
|
|
384
|
+
if (src[bracePos] === "{") {
|
|
385
|
+
const end = matchBraceCSharp(src, bracePos);
|
|
386
|
+
out += blockPlaceholder({
|
|
387
|
+
kind: "verbatim",
|
|
388
|
+
opener: `@${kw} {`,
|
|
389
|
+
body: src.slice(bracePos + 1, end - 1),
|
|
390
|
+
csharp: "members",
|
|
391
|
+
raw: src.slice(i, end)
|
|
392
|
+
});
|
|
393
|
+
i = end;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (CONTROL.has(kw)) {
|
|
398
|
+
const parsed = parseControl(src, i, kw);
|
|
399
|
+
if (parsed) {
|
|
400
|
+
out += blockPlaceholder(parsed.block);
|
|
401
|
+
i = parsed.end;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (DIRECTIVES.has(kw) && atLineStart(src, i) && src[i + 1 + kw.length] !== "=") {
|
|
406
|
+
let end = src.indexOf("\n", i);
|
|
407
|
+
if (end === -1) end = n;
|
|
408
|
+
out += blockPlaceholder({
|
|
409
|
+
kind: "directive",
|
|
410
|
+
text: src.slice(i, end).trimEnd()
|
|
411
|
+
});
|
|
412
|
+
i = end;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
out += c;
|
|
416
|
+
i++;
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
masked: out,
|
|
420
|
+
blocks,
|
|
421
|
+
tagAliases
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
//#endregion
|
|
425
|
+
//#region src/csharp.ts
|
|
426
|
+
const DEFAULT_COMMAND = "dotnet csharpier";
|
|
427
|
+
const WRAPPER = "__CSharpierWrapper__";
|
|
428
|
+
const EOT = "";
|
|
429
|
+
const REQUEST_TIMEOUT_MS = 1e4;
|
|
430
|
+
/**
|
|
431
|
+
* A persistent `csharpier pipe-files` process. Spawning `dotnet csharpier`
|
|
432
|
+
* costs ~200 ms (runtime + Roslyn startup); keeping one warm process reduces
|
|
433
|
+
* every call after the first to a couple of milliseconds. Requests are
|
|
434
|
+
* serialized (the protocol is sequential over one pipe), and the child is
|
|
435
|
+
* unref'ed while idle so it never keeps the Node process alive.
|
|
436
|
+
*/
|
|
437
|
+
var CSharpierServer = class {
|
|
438
|
+
command;
|
|
439
|
+
child = null;
|
|
440
|
+
available = true;
|
|
441
|
+
stdoutBuf = "";
|
|
442
|
+
pendingResolve = null;
|
|
443
|
+
queue = Promise.resolve();
|
|
444
|
+
constructor(command) {
|
|
445
|
+
this.command = command;
|
|
446
|
+
}
|
|
447
|
+
/** Format one snippet; null when unavailable or the input was rejected. */
|
|
448
|
+
format(filePath, content) {
|
|
449
|
+
const task = this.queue.then(() => this.request(filePath, content));
|
|
450
|
+
this.queue = task.catch(() => null);
|
|
451
|
+
return task;
|
|
452
|
+
}
|
|
453
|
+
spawnChild() {
|
|
454
|
+
const parts = this.command.split(/\s+/);
|
|
455
|
+
const child = spawn(parts[0], [...parts.slice(1), "pipe-files"], { stdio: "pipe" });
|
|
456
|
+
this.child = child;
|
|
457
|
+
this.stdoutBuf = "";
|
|
458
|
+
child.stdout.on("data", (chunk) => {
|
|
459
|
+
this.stdoutBuf += chunk;
|
|
460
|
+
const end = this.stdoutBuf.indexOf(EOT);
|
|
461
|
+
if (end !== -1 && this.pendingResolve) {
|
|
462
|
+
const payload = this.stdoutBuf.slice(0, end);
|
|
463
|
+
this.stdoutBuf = this.stdoutBuf.slice(end + 1);
|
|
464
|
+
this.settle(payload === "" ? null : payload);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
child.stderr.on("data", () => {});
|
|
468
|
+
child.stdin.on("error", () => {});
|
|
469
|
+
child.on("error", () => {
|
|
470
|
+
this.available = false;
|
|
471
|
+
this.child = null;
|
|
472
|
+
warnUnavailable(this.command);
|
|
473
|
+
this.settle(null);
|
|
474
|
+
});
|
|
475
|
+
child.on("close", () => {
|
|
476
|
+
this.child = null;
|
|
477
|
+
this.settle(null);
|
|
478
|
+
});
|
|
479
|
+
this.idle();
|
|
480
|
+
process.on("exit", () => child.kill());
|
|
481
|
+
}
|
|
482
|
+
request(filePath, content) {
|
|
483
|
+
if (!this.available) return Promise.resolve(null);
|
|
484
|
+
if (!this.child) this.spawnChild();
|
|
485
|
+
const child = this.child;
|
|
486
|
+
if (!child || !this.available) return Promise.resolve(null);
|
|
487
|
+
return new Promise((resolve) => {
|
|
488
|
+
this.pendingResolve = resolve;
|
|
489
|
+
this.busy();
|
|
490
|
+
const timeout = setTimeout(() => {
|
|
491
|
+
child.kill();
|
|
492
|
+
}, REQUEST_TIMEOUT_MS);
|
|
493
|
+
timeout.unref();
|
|
494
|
+
const done = (payload) => {
|
|
495
|
+
clearTimeout(timeout);
|
|
496
|
+
resolve(payload);
|
|
497
|
+
};
|
|
498
|
+
this.pendingResolve = done;
|
|
499
|
+
child.stdin.write(filePath + EOT + content + EOT);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
settle(payload) {
|
|
503
|
+
const resolve = this.pendingResolve;
|
|
504
|
+
this.pendingResolve = null;
|
|
505
|
+
this.idle();
|
|
506
|
+
if (resolve) resolve(payload);
|
|
507
|
+
}
|
|
508
|
+
setLoopHold(hold) {
|
|
509
|
+
const child = this.child;
|
|
510
|
+
if (!child) return;
|
|
511
|
+
const handles = [
|
|
512
|
+
child,
|
|
513
|
+
child.stdout,
|
|
514
|
+
child.stdin,
|
|
515
|
+
child.stderr
|
|
516
|
+
];
|
|
517
|
+
for (const handle of handles) if (hold) handle.ref?.();
|
|
518
|
+
else handle.unref?.();
|
|
519
|
+
}
|
|
520
|
+
busy() {
|
|
521
|
+
this.setLoopHold(true);
|
|
522
|
+
}
|
|
523
|
+
idle() {
|
|
524
|
+
this.setLoopHold(false);
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
const servers = /* @__PURE__ */ new Map();
|
|
528
|
+
function serverFor(command) {
|
|
529
|
+
let server = servers.get(command);
|
|
530
|
+
if (!server) {
|
|
531
|
+
server = new CSharpierServer(command);
|
|
532
|
+
servers.set(command, server);
|
|
533
|
+
}
|
|
534
|
+
return server;
|
|
535
|
+
}
|
|
536
|
+
const warnedCommands = /* @__PURE__ */ new Set();
|
|
537
|
+
function warnUnavailable(command) {
|
|
538
|
+
if (warnedCommands.has(command)) return;
|
|
539
|
+
warnedCommands.add(command);
|
|
540
|
+
console.warn(`[prettier-plugin-razor2] Could not run CSharpier ("${command}"); leaving embedded C# unformatted. Install it (\`dotnet tool install csharpier\`) or set the "csharpierEnabled" option to false to disable C# formatting and silence this warning.`);
|
|
541
|
+
}
|
|
542
|
+
function detectIndentUnit(text) {
|
|
543
|
+
for (const line of text.split("\n")) {
|
|
544
|
+
const m = /^([ \t]+)\S/.exec(line);
|
|
545
|
+
if (m) return m[1];
|
|
546
|
+
}
|
|
547
|
+
return " ";
|
|
548
|
+
}
|
|
549
|
+
function resolveCommand(options) {
|
|
550
|
+
if (options.csharpierEnabled === false) return null;
|
|
551
|
+
const command = (options.csharpierCommand ?? DEFAULT_COMMAND).trim();
|
|
552
|
+
return command === "" ? null : command;
|
|
553
|
+
}
|
|
554
|
+
async function runCSharpier(code, options) {
|
|
555
|
+
const command = resolveCommand(options);
|
|
556
|
+
if (command === null) return null;
|
|
557
|
+
const dir = options.filepath ? path.resolve(path.dirname(options.filepath)) : process.cwd();
|
|
558
|
+
return serverFor(command).format(path.join(dir, "__csharpier__.cs"), code);
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Format a C# snippet. Returns the formatted code (indented from column zero),
|
|
562
|
+
* or `null` if CSharpier is unavailable/disabled or the code can't be parsed.
|
|
563
|
+
*/
|
|
564
|
+
async function formatCSharp(code, kind, options) {
|
|
565
|
+
const trimmed = code.trim();
|
|
566
|
+
if (trimmed === "") return "";
|
|
567
|
+
if (kind === "statements") {
|
|
568
|
+
const out = await runCSharpier(trimmed, options);
|
|
569
|
+
return out === null ? null : out.replace(/\s+$/, "");
|
|
570
|
+
}
|
|
571
|
+
const out = await runCSharpier(`class ${WRAPPER}\n{\n${trimmed}\n}\n`, options);
|
|
572
|
+
if (out === null) return null;
|
|
573
|
+
const trimmedOut = out.replace(/\n+$/, "");
|
|
574
|
+
const lines = trimmedOut.split("\n");
|
|
575
|
+
const open = lines.findIndex((line, i) => i > 0 && line.trim() === "{");
|
|
576
|
+
if (open === -1) return null;
|
|
577
|
+
let close = lines.length - 1;
|
|
578
|
+
while (close > open && lines[close].trim() !== "}") close--;
|
|
579
|
+
const insideString = linesInsideCSharpStrings(trimmedOut);
|
|
580
|
+
const unit = detectIndentUnit(out);
|
|
581
|
+
const body = lines.slice(open + 1, close).map((line, k) => !insideString.has(open + 1 + k) && line.startsWith(unit) ? line.slice(unit.length) : line);
|
|
582
|
+
while (body.length && body[0].trim() === "") body.shift();
|
|
583
|
+
while (body.length && body[body.length - 1].trim() === "") body.pop();
|
|
584
|
+
return body.join("\n");
|
|
585
|
+
}
|
|
586
|
+
//#endregion
|
|
587
|
+
//#region src/format.ts
|
|
588
|
+
const inlineRE = () => new RegExp(`(\\d+)`, "g");
|
|
589
|
+
function indentUnit(options) {
|
|
590
|
+
return options.useTabs ? " " : " ".repeat(options.tabWidth ?? 2);
|
|
591
|
+
}
|
|
592
|
+
function indentLines(text, prefix) {
|
|
593
|
+
return text.split("\n").map((line) => line === "" ? "" : prefix + line).join("\n");
|
|
594
|
+
}
|
|
595
|
+
function indentCSharpLines(text, prefix) {
|
|
596
|
+
const insideString = linesInsideCSharpStrings(text);
|
|
597
|
+
return text.split("\n").map((line, i) => line === "" || insideString.has(i) ? line : prefix + line).join("\n");
|
|
598
|
+
}
|
|
599
|
+
function reindentVerbatim(text, indent) {
|
|
600
|
+
if (indent === "") return text;
|
|
601
|
+
return indentCSharpLines(text, indent);
|
|
602
|
+
}
|
|
603
|
+
async function renderBlock(block, indent, textToDoc, options) {
|
|
604
|
+
switch (block.kind) {
|
|
605
|
+
case "inline": return indent + block.text;
|
|
606
|
+
case "directive": return indent + block.text;
|
|
607
|
+
case "verbatim": {
|
|
608
|
+
const formatted = await formatCSharp(block.body, block.csharp, options);
|
|
609
|
+
if (formatted === null) return reindentVerbatim(block.raw, indent);
|
|
610
|
+
if (formatted === "") return `${indent}${block.opener}\n${indent}}`;
|
|
611
|
+
const body = indentCSharpLines(formatted, indent + indentUnit(options));
|
|
612
|
+
return `${indent}${block.opener}\n${body}\n${indent}}`;
|
|
613
|
+
}
|
|
614
|
+
case "control": {
|
|
615
|
+
const inner = indent + indentUnit(options);
|
|
616
|
+
const parts = [];
|
|
617
|
+
for (const clause of block.clauses) {
|
|
618
|
+
parts.push(indent + clause.header);
|
|
619
|
+
parts.push(indent + "{");
|
|
620
|
+
const body = (await formatDocument(clause.body, textToDoc, options)).trimEnd();
|
|
621
|
+
if (body !== "") parts.push(indentLines(body, inner));
|
|
622
|
+
parts.push(indent + "}");
|
|
623
|
+
}
|
|
624
|
+
let result = parts.join("\n");
|
|
625
|
+
if (block.trailer) result += "\n" + indent + block.trailer;
|
|
626
|
+
return result;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
async function restore(html, blocks, textToDoc, options) {
|
|
631
|
+
const re = blockPlaceholderRE();
|
|
632
|
+
let out = "";
|
|
633
|
+
let last = 0;
|
|
634
|
+
let m;
|
|
635
|
+
while ((m = re.exec(html)) !== null) {
|
|
636
|
+
out += html.slice(last, m.index);
|
|
637
|
+
out += await renderBlock(blocks[Number(m[2])], m[1], textToDoc, options);
|
|
638
|
+
last = m.index + m[0].length;
|
|
639
|
+
}
|
|
640
|
+
out += html.slice(last);
|
|
641
|
+
return out;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Format Razor source into a formatted string: mask the Razor constructs,
|
|
645
|
+
* delegate the remaining markup to Prettier's HTML printer, then restore the
|
|
646
|
+
* constructs (recursing into control-block bodies).
|
|
647
|
+
*/
|
|
648
|
+
async function formatDocument(source, textToDoc, options) {
|
|
649
|
+
if (source.trim() === "") return "";
|
|
650
|
+
const { masked, blocks, tagAliases } = mask(source);
|
|
651
|
+
const htmlDoc = await textToDoc(masked, { parser: "html" });
|
|
652
|
+
let html = doc.printer.printDocToString(htmlDoc, {
|
|
653
|
+
printWidth: options.printWidth ?? 80,
|
|
654
|
+
tabWidth: options.tabWidth ?? 2,
|
|
655
|
+
useTabs: options.useTabs ?? false
|
|
656
|
+
}).formatted;
|
|
657
|
+
if (blocks.length > 0) {
|
|
658
|
+
html = await restore(html, blocks, textToDoc, options);
|
|
659
|
+
html = resolveInline(html, blocks);
|
|
660
|
+
}
|
|
661
|
+
return restoreTagAliases(html, tagAliases);
|
|
662
|
+
}
|
|
663
|
+
function restoreTagAliases(html, tagAliases) {
|
|
664
|
+
if (tagAliases.length === 0) return html;
|
|
665
|
+
return html.replace(/(<\/?)rz-(\d+)/g, (whole, open, id) => open + (tagAliases[Number(id)] ?? whole));
|
|
666
|
+
}
|
|
667
|
+
function resolveInline(text, blocks) {
|
|
668
|
+
return text.replace(inlineRE(), (whole, id) => {
|
|
669
|
+
const block = blocks[Number(id)];
|
|
670
|
+
return block?.kind === "inline" ? block.text : whole;
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
//#endregion
|
|
674
|
+
//#region src/index.ts
|
|
675
|
+
const languages = [{
|
|
676
|
+
name: "Razor",
|
|
677
|
+
parsers: ["razor"],
|
|
678
|
+
extensions: [".razor", ".cshtml"],
|
|
679
|
+
vscodeLanguageIds: ["razor", "aspnetcorerazor"]
|
|
680
|
+
}];
|
|
681
|
+
const parsers = { razor: {
|
|
682
|
+
parse: (text) => ({
|
|
683
|
+
type: "razor-root",
|
|
684
|
+
source: text
|
|
685
|
+
}),
|
|
686
|
+
astFormat: "razor-ast",
|
|
687
|
+
locStart: () => 0,
|
|
688
|
+
locEnd: () => 0
|
|
689
|
+
} };
|
|
690
|
+
const options = {
|
|
691
|
+
csharpierEnabled: {
|
|
692
|
+
type: "boolean",
|
|
693
|
+
category: "Razor",
|
|
694
|
+
default: true,
|
|
695
|
+
description: "Format embedded C# (@code/@functions/@{ } blocks) with CSharpier. When false, C# is kept verbatim."
|
|
696
|
+
},
|
|
697
|
+
csharpierCommand: {
|
|
698
|
+
type: "string",
|
|
699
|
+
category: "Razor",
|
|
700
|
+
default: "dotnet csharpier",
|
|
701
|
+
description: "Command used to invoke the CSharpier CLI."
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
const printers = { "razor-ast": {
|
|
705
|
+
print: () => "",
|
|
706
|
+
embed(path) {
|
|
707
|
+
const node = path.node;
|
|
708
|
+
if (node.type !== "razor-root") return null;
|
|
709
|
+
return async (textToDoc, _print, _embedPath, options) => {
|
|
710
|
+
return (await formatDocument(node.source, textToDoc, options)).replace(/\s+$/, "") + "\n";
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
} };
|
|
714
|
+
//#endregion
|
|
715
|
+
export { languages, options, parsers, printers };
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "prettier-plugin-razor2",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Prettier plugin for Razor (Blazor) files — a fork of prettier-plugin-razor",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"prettier",
|
|
7
|
+
"razor",
|
|
8
|
+
"blazor",
|
|
9
|
+
"plugin"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/rmacfie/prettier-plugin-razor2#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/rmacfie/prettier-plugin-razor2/issues"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/rmacfie/prettier-plugin-razor2.git"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": {
|
|
21
|
+
"name": "Robert Macfie",
|
|
22
|
+
"email": "robert@macfie.se"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"main": "dist/index.js",
|
|
32
|
+
"types": "dist/index.d.ts",
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "26.1.0",
|
|
38
|
+
"prettier": "3.9.4",
|
|
39
|
+
"prettier-plugin-jsdoc": "1.8.1",
|
|
40
|
+
"prettier-plugin-packagejson": "3.0.2",
|
|
41
|
+
"tsdown": "^0.22.3",
|
|
42
|
+
"typescript": "6.0.3",
|
|
43
|
+
"node": "runtime:26.4.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"prettier": "^3.0.0"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=20"
|
|
50
|
+
},
|
|
51
|
+
"devEngines": {
|
|
52
|
+
"runtime": {
|
|
53
|
+
"name": "node",
|
|
54
|
+
"version": "26.4.0",
|
|
55
|
+
"onFail": "download"
|
|
56
|
+
},
|
|
57
|
+
"packageManager": {
|
|
58
|
+
"name": "pnpm",
|
|
59
|
+
"version": "11.9.0",
|
|
60
|
+
"onFail": "download"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"bench": "node bench/bench.ts",
|
|
65
|
+
"build": "tsdown",
|
|
66
|
+
"clean": "rm -rf dist",
|
|
67
|
+
"csharpier": "dotnet csharpier",
|
|
68
|
+
"format": "prettier --write .",
|
|
69
|
+
"node": "node",
|
|
70
|
+
"test": "node --test \"tests/**/*.test.ts\"",
|
|
71
|
+
"typecheck": "tsc"
|
|
72
|
+
}
|
|
73
|
+
}
|