@rm-industries/oxlint-plugin-headstamp 0.0.0-stage → 0.1.2
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 +344 -2
- package/dist/index.d.ts +12 -0
- package/dist/index.js +166 -0
- package/dist/template.d.ts +22 -0
- package/dist/template.js +191 -0
- package/package.json +56 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RM Industries
|
|
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
CHANGED
|
@@ -1,3 +1,345 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Headstamp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An oxlint-first plugin that validates templated source headers and inserts missing
|
|
4
|
+
ones safely. Supports JavaScript, TypeScript, JSX, and TSX.
|
|
5
|
+
|
|
6
|
+
Supports complete literal templates and named, single-line placeholders with regex
|
|
7
|
+
validation, safe repair of invalid fields, and incomplete scaffolds requiring human
|
|
8
|
+
input.
|
|
9
|
+
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
- Node.js 22.18+ on the 22.x line, or Node.js 24+.
|
|
13
|
+
- The Node-based oxlint package, version `^1.88.0` (1.88.0 or later within 1.x).
|
|
14
|
+
|
|
15
|
+
Headstamp uses oxlint's `createOnce` API with a `Program` visitor. JavaScript plugins
|
|
16
|
+
are still an alpha oxlint feature. CI verifies the lockfile's exact versions,
|
|
17
|
+
currently oxlint 1.93.0. No benchmarked performance improvement is claimed.
|
|
18
|
+
|
|
19
|
+
Development and peer dependencies use caret ranges. The committed lockfile and
|
|
20
|
+
`npm ci` keep installs reproducible; dependency updates must pass the same checks.
|
|
21
|
+
|
|
22
|
+
## Tested source formats
|
|
23
|
+
|
|
24
|
+
Real CLI tests verify `.js`, `.mjs`, `.cjs`, `.ts`, `.mts`, `.cts`, `.jsx`, and
|
|
25
|
+
`.tsx`, using module, CommonJS, typed, and JSX syntax as appropriate. For every
|
|
26
|
+
extension, tests require missing-header diagnostics, exact header insertion,
|
|
27
|
+
valid-header acceptance, unchanged source bodies, and repeated-fix convergence.
|
|
28
|
+
They also cover LF/CRLF, BOM-only and shebang-only prefixes, and mixed-file
|
|
29
|
+
configuration overrides, disabled rules, and ignores.
|
|
30
|
+
|
|
31
|
+
Use oxlint's existing file discovery, overrides, and ignores; Headstamp adds no
|
|
32
|
+
file selector or parser. Physical-file headers in Vue, Svelte, and Astro are
|
|
33
|
+
deferred on oxlint 1.88.0: scriptless files do not invoke the plugin, and scripts
|
|
34
|
+
expose separate source/edit regions. Enabling the rule there can insert per-script
|
|
35
|
+
banners rather than one physical-file header. Exclude those formats with oxlint
|
|
36
|
+
overrides/ignores when enforcing physical headers. See the [framework investigation](docs/framework-upstream-research.md)
|
|
37
|
+
for separate decisions, reproducible CLI probes, and pinned upstream references.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npm install --save-dev @rm-industries/oxlint-plugin-headstamp oxlint@^1.88.0
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Configure
|
|
46
|
+
|
|
47
|
+
In your existing `oxlint.config.ts`:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import type { HeaderOptions } from '@rm-industries/oxlint-plugin-headstamp';
|
|
51
|
+
import { defineConfig } from 'oxlint';
|
|
52
|
+
|
|
53
|
+
const header = {
|
|
54
|
+
template: `/*
|
|
55
|
+
* Copyright {{years}} {{owner}}
|
|
56
|
+
*/`,
|
|
57
|
+
values: {
|
|
58
|
+
years: {
|
|
59
|
+
pattern: '\\d{4}(?:-\\d{4})?',
|
|
60
|
+
replacement: String(new Date().getUTCFullYear()),
|
|
61
|
+
},
|
|
62
|
+
owner: { pattern: 'Acme', replacement: 'Acme' },
|
|
63
|
+
},
|
|
64
|
+
} satisfies HeaderOptions;
|
|
65
|
+
|
|
66
|
+
export default defineConfig({
|
|
67
|
+
jsPlugins: [{ name: 'headstamp', specifier: '@rm-industries/oxlint-plugin-headstamp' }],
|
|
68
|
+
overrides: [
|
|
69
|
+
{
|
|
70
|
+
files: ['src/**/*.{js,ts,jsx,tsx}'],
|
|
71
|
+
rules: { 'headstamp/header': ['error', header] },
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Use ordinary oxlint overrides and ignores to choose files. To store the template in
|
|
78
|
+
`HEADER.txt`, read it in configuration code:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { readFileSync } from 'node:fs';
|
|
82
|
+
|
|
83
|
+
const header = {
|
|
84
|
+
template: readFileSync(new URL('./HEADER.txt', import.meta.url), 'utf8'),
|
|
85
|
+
} satisfies HeaderOptions;
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Text files contain the same comment template, including placeholders; their
|
|
89
|
+
extension has no meaning to Headstamp. Keep the `values` map alongside the template
|
|
90
|
+
property when reading from a file. Configuration can compute replacement values or
|
|
91
|
+
import shared options, but the rule receives strings, not executable functions.
|
|
92
|
+
JSON oxlint configuration also works with inline template and value strings.
|
|
93
|
+
|
|
94
|
+
## SPDX header example
|
|
95
|
+
|
|
96
|
+
Use this in `oxlint.config.ts`. Change `MIT` and `Acme` to your project's
|
|
97
|
+
license identifier and copyright holder.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { defineConfig } from 'oxlint';
|
|
101
|
+
|
|
102
|
+
const year = String(new Date().getUTCFullYear());
|
|
103
|
+
|
|
104
|
+
export default defineConfig({
|
|
105
|
+
jsPlugins: [{ name: 'headstamp', specifier: '@rm-industries/oxlint-plugin-headstamp' }],
|
|
106
|
+
overrides: [
|
|
107
|
+
{
|
|
108
|
+
files: ['src/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
|
109
|
+
rules: {
|
|
110
|
+
'headstamp/header': [
|
|
111
|
+
'error',
|
|
112
|
+
{
|
|
113
|
+
template: '// SPDX-License-Identifier: {{license}}\n// SPDX-FileCopyrightText: {{years}} Acme',
|
|
114
|
+
values: {
|
|
115
|
+
license: { pattern: 'MIT', replacement: 'MIT' },
|
|
116
|
+
years: { pattern: '\\d{4}(?:-\\d{4})?', replacement: year },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
With `oxlint --fix`, a missing header gets the configured license and year.
|
|
127
|
+
An existing header with the wrong license identifier can be repaired without
|
|
128
|
+
changing a valid year or year range.
|
|
129
|
+
|
|
130
|
+
### Optional Git-derived year
|
|
131
|
+
|
|
132
|
+
To use the latest commit's year as the default, replace the `year` declaration
|
|
133
|
+
above with this snippet. This runs Git once when the config loads. It uses the
|
|
134
|
+
same year for every file; it does not infer per-file history. If Git is missing
|
|
135
|
+
or the checkout has no commits, it falls back to the current UTC year.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { spawnSync } from 'node:child_process';
|
|
139
|
+
|
|
140
|
+
const commitDate = spawnSync('git', ['log', '-1', '--format=%cs'], {
|
|
141
|
+
cwd: new URL('.', import.meta.url),
|
|
142
|
+
encoding: 'utf8',
|
|
143
|
+
timeout: 10000,
|
|
144
|
+
});
|
|
145
|
+
const gitYear = commitDate.status === 0 ? /^(\d{4})-/.exec(commitDate.stdout)?.[1] : undefined;
|
|
146
|
+
const year = gitYear ?? String(new Date().getUTCFullYear());
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Placeholders
|
|
150
|
+
|
|
151
|
+
Use `{{name}}` and define each name once in `values`, with a regex `pattern` string
|
|
152
|
+
and a literal `replacement` string. Names use `[A-Za-z_][A-Za-z0-9_]*`. Repeated,
|
|
153
|
+
undefined, or malformed placeholders and unused definitions are configuration errors.
|
|
154
|
+
|
|
155
|
+
Patterns must match the entire field value. Existing `2020` and `2020-2024` years
|
|
156
|
+
remain valid with the example above, even when the generated replacement is `2026`.
|
|
157
|
+
Regex capture groups and backreferences are local to each pattern. Replacement
|
|
158
|
+
strings are inserted literally, including dollar signs and backslashes.
|
|
159
|
+
|
|
160
|
+
Placeholders capture single-line values and require literal text between them. A
|
|
161
|
+
separator may occur inside a value if the patterns identify exactly one valid split.
|
|
162
|
+
Multiple valid splits produce a diagnostic without autofix. A configuration whose
|
|
163
|
+
otherwise-valid generated header has ambiguous field boundaries is rejected.
|
|
164
|
+
|
|
165
|
+
Invalid existing fields are reported by name when their boundaries are clear.
|
|
166
|
+
`--fix` replaces a field only when its configured replacement matches the entire
|
|
167
|
+
pattern, preserving every valid field, including historical years and ranges. If a
|
|
168
|
+
replacement fails its pattern, existing text in that field remains unchanged; other
|
|
169
|
+
safe fields may still be repaired. A repair is refused if it would make boundaries
|
|
170
|
+
ambiguous, reinterpret another field, or break the comment. Literal text identifies
|
|
171
|
+
the header structure; changed wording or ambiguous extraction requires manual review.
|
|
172
|
+
|
|
173
|
+
Replacements may intentionally fail their pattern; this is not a configuration error.
|
|
174
|
+
They must still be single-line and preserve the complete comment. Missing-header
|
|
175
|
+
generation uses replacement strings, while validation uses field patterns.
|
|
176
|
+
|
|
177
|
+
## Incomplete scaffolds
|
|
178
|
+
|
|
179
|
+
A field can require human input by providing an intentionally invalid replacement:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
const header = {
|
|
183
|
+
template: '// Owner: {{owner}}\n// Description: {{description}}',
|
|
184
|
+
values: {
|
|
185
|
+
owner: { pattern: 'Acme', replacement: 'Acme' },
|
|
186
|
+
description: { pattern: '\\S.{9,}', replacement: '' },
|
|
187
|
+
},
|
|
188
|
+
} satisfies HeaderOptions;
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The description must start with a non-whitespace character and contain at least ten
|
|
192
|
+
characters. Running `oxlint --fix` on a file without a header inserts:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// Owner: Acme
|
|
196
|
+
// Description:
|
|
197
|
+
|
|
198
|
+
export {};
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The generated description line retains the template’s space after the colon.
|
|
202
|
+
Successful insertion still leaves a lint error for `description`, so the CLI exits
|
|
203
|
+
unsuccessfully. Fill in a valid description, then lint again to pass. An invalid
|
|
204
|
+
nonempty replacement such as `TODO` works the same way.
|
|
205
|
+
|
|
206
|
+
Repeated fixes preserve the scaffold and keep reporting the incomplete field
|
|
207
|
+
without offering a no-op edit. Existing unfinished text such as `WIP` is preserved
|
|
208
|
+
rather than reset to the replacement. Other independently safe repairs, such as
|
|
209
|
+
correcting the owner, still apply while the description remains incomplete. Valid
|
|
210
|
+
fields, including historical years, stay unchanged. Ambiguous field boundaries
|
|
211
|
+
still require manual review.
|
|
212
|
+
|
|
213
|
+
## Permitted leading comments
|
|
214
|
+
|
|
215
|
+
Use `allowedLeadingComments` to preserve explicitly permitted directives or pragmas
|
|
216
|
+
before the required header:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const header = {
|
|
220
|
+
template: '/* Acme */',
|
|
221
|
+
allowedLeadingComments: ['// @ts-check', { pattern: '/\\*\\* @jest-environment [\\s\\S]*?\\*/' }],
|
|
222
|
+
} satisfies HeaderOptions;
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The default is an empty allowlist. Entries are complete literal comments or objects
|
|
226
|
+
with a regex `pattern` string. Patterns match the entire comment, including its
|
|
227
|
+
delimiters, with no flags; use explicit multiline expressions when needed. Literal
|
|
228
|
+
and source CRLF are normalized to LF for matching, while original prefix bytes are
|
|
229
|
+
preserved. Malformed literals, regexes, or option shapes are configuration errors.
|
|
230
|
+
|
|
231
|
+
A matching unit is one block comment or a consecutive `//` group. Headstamp never
|
|
232
|
+
splits a group to accept only some of its lines. Separate an existing line-comment
|
|
233
|
+
prefix and line-comment header with a blank line. Insertion adds the newline(s)
|
|
234
|
+
needed to keep these groups separate on subsequent runs.
|
|
235
|
+
|
|
236
|
+
Only the opening prefix after any BOM/shebang is considered. The required header
|
|
237
|
+
takes precedence over a matching allowlist entry, including when its fields need
|
|
238
|
+
repair. An ambiguous header remains diagnostic-only. Unknown comments stop the
|
|
239
|
+
search without autofix; source code ends the prefix, and later comments are not
|
|
240
|
+
searched for a header.
|
|
241
|
+
|
|
242
|
+
Permitted comments are never generated, repaired, or reordered. Their text and
|
|
243
|
+
spacing are preserved, including whitespace before an existing or inserted header.
|
|
244
|
+
Insertion adds a separator only when needed. Prefix-only files receive a header
|
|
245
|
+
with one final newline. Source indentation, historical fields, and unfinished
|
|
246
|
+
human input remain protected, and repeated fixes converge.
|
|
247
|
+
|
|
248
|
+
The allowlist does not change spacing before the header; `blankLinesAfter` controls
|
|
249
|
+
only the boundary after it.
|
|
250
|
+
This rule preserves source LF/CRLF; it does not read EditorConfig or convert the
|
|
251
|
+
rest of a file's line endings.
|
|
252
|
+
|
|
253
|
+
## Spacing after the header
|
|
254
|
+
|
|
255
|
+
Set `blankLinesAfter` to the exact number of empty lines required before subsequent
|
|
256
|
+
content. It defaults to `1`; values must be nonnegative safe integers.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
const header = {
|
|
260
|
+
template: '/* Acme */',
|
|
261
|
+
blankLinesAfter: 0,
|
|
262
|
+
} satisfies HeaderOptions;
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Zero places source on the following line without an empty line. A value of `3`
|
|
266
|
+
requires three empty lines. Fixes add missing or remove excess lines, preserving
|
|
267
|
+
source indentation and LF/CRLF. Empty and header-only files always end with one
|
|
268
|
+
final newline, regardless of the configured count. Prefix spacing stays unchanged.
|
|
269
|
+
|
|
270
|
+
A zero count cannot safely join a `//` header to following `//` documentation:
|
|
271
|
+
that would merge separate comment groups on the next lint run. Headstamp reports
|
|
272
|
+
this conflict without changing that separator. Use a block header or at least one
|
|
273
|
+
blank line. Independently safe field repairs still apply.
|
|
274
|
+
|
|
275
|
+
Spacing fixes can also apply while required scaffold fields are incomplete. The
|
|
276
|
+
unfinished text and its lint errors remain until valid human input is supplied.
|
|
277
|
+
Repeated fixes converge and do not offer no-op edits.
|
|
278
|
+
|
|
279
|
+
## Header behavior
|
|
280
|
+
|
|
281
|
+
- Templates contain exactly one complete `/* ... */` comment or consecutive `//`
|
|
282
|
+
lines. Include the delimiters and desired internal whitespace. Trailing template
|
|
283
|
+
newlines are ignored; LF and CRLF templates match either source convention.
|
|
284
|
+
- Literal header text must match exactly, including internal whitespace. Placeholder
|
|
285
|
+
values must match their patterns. Recognized invalid fields may be repaired as
|
|
286
|
+
described above. Changed literal text or an unrecognized opening comment produces
|
|
287
|
+
a diagnostic without autofix.
|
|
288
|
+
Consecutive opening `//` lines form one comment group: an extra line in that group
|
|
289
|
+
requires manual review.
|
|
290
|
+
- `oxlint --fix` inserts a missing header after any shebang and permitted opening
|
|
291
|
+
comments, preserving a BOM, source code, and separate later comments.
|
|
292
|
+
- Headers get exactly `blankLinesAfter` empty lines before subsequent content
|
|
293
|
+
(default `1`) when the edit preserves separate comment groups. Indentation of
|
|
294
|
+
subsequent content is retained. Without a permitted prefix, leading whitespace
|
|
295
|
+
before the header is removed; prefix spacing is preserved when present.
|
|
296
|
+
- Empty, whitespace-only, and header-only files end with the header and exactly one
|
|
297
|
+
final newline. They do not receive an empty trailing paragraph.
|
|
298
|
+
- Edits use the first LF or CRLF convention present in the source, defaulting to LF
|
|
299
|
+
when none exists. They do not rewrite the rest of the file.
|
|
300
|
+
- Repeated fixes converge and never offer an edit that makes no change.
|
|
301
|
+
- Invalid options or malformed templates fail rather than inserting source code
|
|
302
|
+
from an invalid template.
|
|
303
|
+
|
|
304
|
+
Oxlint 1.88.0 rejects a BOM immediately followed by a shebang during parsing, before
|
|
305
|
+
plugins run. BOM-only and shebang-only inputs are supported and tested separately;
|
|
306
|
+
Headstamp cannot fix a file that oxlint cannot parse.
|
|
307
|
+
|
|
308
|
+
## Development
|
|
309
|
+
|
|
310
|
+
```sh
|
|
311
|
+
npm run typecheck
|
|
312
|
+
npm run test:rule
|
|
313
|
+
npm test
|
|
314
|
+
npm run lint
|
|
315
|
+
npm run format:check
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Run `npm run format` to apply Oxfmt formatting across the repository. EditorConfig
|
|
319
|
+
sets UTF-8, LF, final newlines, two-space indentation, and a 120-column target.
|
|
320
|
+
The typed Oxfmt config adds single quotes, trailing commas, and import/package
|
|
321
|
+
sorting. Generated files and local state are excluded through the Toptal-generated
|
|
322
|
+
Git ignore rules and the project's additional exclusions.
|
|
323
|
+
|
|
324
|
+
GitHub Actions checks formatting and lint on pull requests and pushes to the main
|
|
325
|
+
branch. A Node 22.18, 24, and 26 matrix runs strict typechecking, the package build,
|
|
326
|
+
all tests, and package-content inspection. CI checks formatting without rewriting
|
|
327
|
+
files. Publishing a GitHub release triggers a separate npm release workflow after
|
|
328
|
+
those same checks; manual release-workflow runs prepare an artifact without
|
|
329
|
+
publishing. See [RELEASING.md](RELEASING.md) for bootstrap, trusted-publisher setup,
|
|
330
|
+
and version/tag requirements.
|
|
331
|
+
|
|
332
|
+
Tests use oxlint's public rule-testing API and real CLI runs. CLI tests exercise
|
|
333
|
+
TypeScript configuration and exported types, external templates, supported source
|
|
334
|
+
languages, overrides/ignores, distinct configurations, and repeated fixes.
|
|
335
|
+
Placeholder tests cover historical values, whole-value matching, literal replacement
|
|
336
|
+
characters, malformed configuration, ambiguity, and source preservation.
|
|
337
|
+
|
|
338
|
+
Implementation and tests are written in TypeScript. Both test commands strictly
|
|
339
|
+
typecheck source and tests, then run the TypeScript tests with Node's built-in type
|
|
340
|
+
stripping. The full suite builds the package first for CLI integration. Only plugin
|
|
341
|
+
source is emitted to the package; tests are never included in the build output.
|
|
342
|
+
|
|
343
|
+
Package output is ESM JavaScript plus TypeScript declarations. Build artifacts and
|
|
344
|
+
development-only files are excluded from the published package surface; inspect it
|
|
345
|
+
without publishing with `npm pack --dry-run`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RuleTester } from 'oxlint/plugins-dev';
|
|
2
|
+
export type { HeaderOptions, HeaderValue } from './template.ts';
|
|
3
|
+
type Rule = Parameters<RuleTester['run']>[1];
|
|
4
|
+
declare const plugin: {
|
|
5
|
+
meta: {
|
|
6
|
+
name: string;
|
|
7
|
+
};
|
|
8
|
+
rules: {
|
|
9
|
+
header: Rule;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export default plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { prepareHeader } from './template.js';
|
|
2
|
+
const header = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: 'layout',
|
|
5
|
+
fixable: 'code',
|
|
6
|
+
schema: [
|
|
7
|
+
{
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
template: { type: 'string' },
|
|
11
|
+
blankLinesAfter: { type: 'integer', minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
|
|
12
|
+
allowedLeadingComments: {
|
|
13
|
+
type: 'array',
|
|
14
|
+
items: {
|
|
15
|
+
anyOf: [
|
|
16
|
+
{ type: 'string' },
|
|
17
|
+
{
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: { pattern: { type: 'string' } },
|
|
20
|
+
required: ['pattern'],
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
values: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
additionalProperties: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: { pattern: { type: 'string' }, replacement: { type: 'string' } },
|
|
31
|
+
required: ['pattern', 'replacement'],
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
required: ['template'],
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
messages: {
|
|
41
|
+
missing: 'Missing required header.',
|
|
42
|
+
mismatch: 'Opening comment does not match the required header; review it manually.',
|
|
43
|
+
spacing: 'Normalize spacing around the required header.',
|
|
44
|
+
unsafeSpacing: 'Zero blank lines would merge the header with following line comments; use a block header or at least one blank line.',
|
|
45
|
+
invalidValue: 'Header field(s) {{fields}} do not match their patterns.',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
createOnce(context) {
|
|
49
|
+
const templates = new Map();
|
|
50
|
+
return {
|
|
51
|
+
Program(node) {
|
|
52
|
+
const hasOneOptionsArgument = context.options.length === 1;
|
|
53
|
+
if (!hasOneOptionsArgument)
|
|
54
|
+
throw new Error('Headstamp requires exactly one options object.');
|
|
55
|
+
const optionsKey = JSON.stringify(context.options);
|
|
56
|
+
let prepared = templates.get(optionsKey);
|
|
57
|
+
if (prepared === undefined) {
|
|
58
|
+
prepared = prepareHeader(context.options[0]);
|
|
59
|
+
templates.set(optionsKey, prepared);
|
|
60
|
+
}
|
|
61
|
+
let headerText = prepared.rendered;
|
|
62
|
+
let invalidFields = [];
|
|
63
|
+
let hasFieldRepairs = false;
|
|
64
|
+
const source = context.sourceCode;
|
|
65
|
+
const text = source.text;
|
|
66
|
+
const eol = /\r\n|\n/.exec(text)?.[0] ?? '\n';
|
|
67
|
+
const shebang = /^#![^\r\n]*(?:\r\n|\n|$)/.exec(text)?.[0] ?? '';
|
|
68
|
+
let anchor = shebang.length;
|
|
69
|
+
let start = anchor + /^\s*/.exec(text.slice(anchor))[0].length;
|
|
70
|
+
const comments = source.getAllComments();
|
|
71
|
+
let commentIndex = comments.findIndex((comment) => comment.range[0] === start);
|
|
72
|
+
let first = commentIndex < 0 ? undefined : comments[commentIndex];
|
|
73
|
+
let end = anchor;
|
|
74
|
+
let hasLeadingComments = false;
|
|
75
|
+
let prefixWhitespace = '';
|
|
76
|
+
let lastLeadingCommentIsLine = false;
|
|
77
|
+
while (first) {
|
|
78
|
+
end = first.range[1];
|
|
79
|
+
if (first.type === 'Line') {
|
|
80
|
+
for (let nextIndex = commentIndex + 1; nextIndex < comments.length; nextIndex++) {
|
|
81
|
+
const comment = comments[nextIndex];
|
|
82
|
+
const isLineComment = comment.type === 'Line';
|
|
83
|
+
const followsOnNextLine = /^(?:\r\n|\n)[\t ]*$/.test(text.slice(end, comment.range[0]));
|
|
84
|
+
if (!isLineComment || !followsOnNextLine)
|
|
85
|
+
break;
|
|
86
|
+
end = comment.range[1];
|
|
87
|
+
commentIndex = nextIndex;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
headerText = text.slice(start, end).replace(/\r\n/g, '\n');
|
|
91
|
+
const inspection = prepared.inspect(headerText);
|
|
92
|
+
if (inspection === null && prepared.isAllowedLeadingComment(headerText)) {
|
|
93
|
+
hasLeadingComments = true;
|
|
94
|
+
lastLeadingCommentIsLine = first.type === 'Line';
|
|
95
|
+
prefixWhitespace = /^\s*/.exec(text.slice(end))[0];
|
|
96
|
+
anchor = end + prefixWhitespace.length;
|
|
97
|
+
start = anchor;
|
|
98
|
+
end = anchor;
|
|
99
|
+
first = comments[++commentIndex];
|
|
100
|
+
if (first?.range[0] !== start)
|
|
101
|
+
first = undefined;
|
|
102
|
+
headerText = prepared.rendered;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (inspection === null || inspection === 'ambiguous') {
|
|
106
|
+
context.report({ node, messageId: 'mismatch' });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
invalidFields = inspection.invalidFields;
|
|
110
|
+
hasFieldRepairs = inspection.repaired !== headerText;
|
|
111
|
+
headerText = inspection.repaired;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
const whitespace = /^\s*/.exec(text.slice(end))[0];
|
|
115
|
+
const contentStart = end + whitespace.length;
|
|
116
|
+
const hasContent = contentStart < text.length;
|
|
117
|
+
const sourceWhitespace = hasLeadingComments && !first ? prefixWhitespace : whitespace;
|
|
118
|
+
const indent = hasContent ? sourceWhitespace.slice(sourceWhitespace.lastIndexOf('\n') + 1) : '';
|
|
119
|
+
const needsShebangNewline = shebang.length > 0 && !shebang.endsWith('\n');
|
|
120
|
+
let prefix = needsShebangNewline ? eol : '';
|
|
121
|
+
if (hasLeadingComments && !first) {
|
|
122
|
+
// A blank line prevents two line-comment groups from merging on the next lint run.
|
|
123
|
+
const needsLineGroupBoundary = lastLeadingCommentIsLine && headerText.startsWith('//');
|
|
124
|
+
const requiredNewlines = needsLineGroupBoundary ? 2 : 1;
|
|
125
|
+
const existingNewlines = prefixWhitespace.match(/\n/g)?.length ?? 0;
|
|
126
|
+
prefix = eol.repeat(Math.max(0, requiredNewlines - existingNewlines));
|
|
127
|
+
}
|
|
128
|
+
const wouldMergeLineGroups = first?.type === 'Line' && prepared.blankLinesAfter === 0 && text.startsWith('//', contentStart);
|
|
129
|
+
if (wouldMergeLineGroups)
|
|
130
|
+
context.report({ node, messageId: 'unsafeSpacing' });
|
|
131
|
+
const separator = wouldMergeLineGroups
|
|
132
|
+
? whitespace
|
|
133
|
+
: eol + (hasContent ? eol.repeat(prepared.blankLinesAfter) + indent : '');
|
|
134
|
+
const replacement = prefix + headerText.replace(/\n/g, eol) + separator;
|
|
135
|
+
let remainingInvalidFields = prepared.invalidReplacementFields;
|
|
136
|
+
if (first) {
|
|
137
|
+
remainingInvalidFields = hasFieldRepairs
|
|
138
|
+
? invalidFields.filter((name) => prepared.invalidReplacementFields.includes(name))
|
|
139
|
+
: invalidFields;
|
|
140
|
+
}
|
|
141
|
+
const repairedFields = invalidFields.filter((name) => !remainingInvalidFields.includes(name));
|
|
142
|
+
const hasChanges = text.slice(anchor, contentStart) !== replacement;
|
|
143
|
+
if (hasChanges) {
|
|
144
|
+
let messageId = first ? 'spacing' : 'missing';
|
|
145
|
+
if (repairedFields.length > 0)
|
|
146
|
+
messageId = 'invalidValue';
|
|
147
|
+
context.report({
|
|
148
|
+
node,
|
|
149
|
+
messageId,
|
|
150
|
+
data: { fields: repairedFields.join(', ') },
|
|
151
|
+
fix: (fixer) => fixer.replaceTextRange([anchor, contentStart], replacement),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// Oxlint removes fixed diagnostics without re-linting; retain errors needing human input.
|
|
155
|
+
if (remainingInvalidFields.length > 0) {
|
|
156
|
+
context.report({ node, messageId: 'invalidValue', data: { fields: remainingInvalidFields.join(', ') } });
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
const plugin = {
|
|
163
|
+
meta: { name: '@rm-industries/oxlint-plugin-headstamp' },
|
|
164
|
+
rules: { header },
|
|
165
|
+
};
|
|
166
|
+
export default plugin;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface HeaderValue {
|
|
2
|
+
pattern: string;
|
|
3
|
+
replacement: string;
|
|
4
|
+
}
|
|
5
|
+
export interface HeaderOptions {
|
|
6
|
+
template: string;
|
|
7
|
+
values?: Record<string, HeaderValue>;
|
|
8
|
+
allowedLeadingComments?: Array<string | {
|
|
9
|
+
pattern: string;
|
|
10
|
+
}>;
|
|
11
|
+
blankLinesAfter?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function prepareHeader(options: unknown): {
|
|
14
|
+
rendered: string;
|
|
15
|
+
blankLinesAfter: number;
|
|
16
|
+
inspect: (comment: string) => {
|
|
17
|
+
invalidFields: string[];
|
|
18
|
+
repaired: string;
|
|
19
|
+
} | 'ambiguous' | null;
|
|
20
|
+
invalidReplacementFields: string[];
|
|
21
|
+
isAllowedLeadingComment: (comment: string) => boolean;
|
|
22
|
+
};
|
package/dist/template.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function isCompleteComment(text) {
|
|
5
|
+
const isBlockComment = text.length >= 4 && text.startsWith('/*') && text.indexOf('*/') === text.length - 2;
|
|
6
|
+
const isLineCommentGroup = text.split('\n').every((line) => line.startsWith('//'));
|
|
7
|
+
const hasSupportedCommentStyle = isBlockComment || isLineCommentGroup;
|
|
8
|
+
const hasUnsupportedLineEndings = /[\r\u2028\u2029]/.test(text);
|
|
9
|
+
return hasSupportedCommentStyle && !hasUnsupportedLineEndings;
|
|
10
|
+
}
|
|
11
|
+
function fullValuePattern(pattern) {
|
|
12
|
+
const expression = new RegExp(pattern);
|
|
13
|
+
return new RegExp(`^(?:${expression.source})$`);
|
|
14
|
+
}
|
|
15
|
+
function matchValues(comment, parts, fields, checkPatterns) {
|
|
16
|
+
if (!comment.startsWith(parts[0]))
|
|
17
|
+
return null;
|
|
18
|
+
const memo = new Map();
|
|
19
|
+
function visit(index, start) {
|
|
20
|
+
if (index === fields.length)
|
|
21
|
+
return { count: start === comment.length ? 1 : 0, values: [] };
|
|
22
|
+
const key = `${index}:${start}`;
|
|
23
|
+
const cached = memo.get(key);
|
|
24
|
+
if (cached)
|
|
25
|
+
return cached;
|
|
26
|
+
const field = fields[index];
|
|
27
|
+
const delimiter = parts[index + 1];
|
|
28
|
+
const nextLineBreak = comment.slice(start).search(/[\r\n\u2028\u2029]/);
|
|
29
|
+
const lineEnd = nextLineBreak < 0 ? comment.length : start + nextLineBreak;
|
|
30
|
+
let end = delimiter.length === 0 ? comment.length : comment.indexOf(delimiter, start);
|
|
31
|
+
const result = { count: 0, values: [] };
|
|
32
|
+
while (end >= 0 && end <= lineEnd) {
|
|
33
|
+
const value = comment.slice(start, end);
|
|
34
|
+
const isAllowedValue = !checkPatterns || field.pattern.test(value);
|
|
35
|
+
if (isAllowedValue) {
|
|
36
|
+
const tail = visit(index + 1, end + delimiter.length);
|
|
37
|
+
if (tail.count > 0) {
|
|
38
|
+
if (result.count === 0)
|
|
39
|
+
result.values = [value, ...tail.values];
|
|
40
|
+
result.count += tail.count;
|
|
41
|
+
// Two matches already prove ambiguity; do not enumerate further splits.
|
|
42
|
+
if (result.count >= 2)
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
end = delimiter.length === 0 ? -1 : comment.indexOf(delimiter, end + 1);
|
|
47
|
+
}
|
|
48
|
+
memo.set(key, result);
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
const result = visit(0, parts[0].length);
|
|
52
|
+
if (result.count === 0)
|
|
53
|
+
return null;
|
|
54
|
+
return result.count === 1 ? result.values : 'ambiguous';
|
|
55
|
+
}
|
|
56
|
+
export function prepareHeader(options) {
|
|
57
|
+
if (!isRecord(options))
|
|
58
|
+
throw new Error('Headstamp requires a template string and optional values map.');
|
|
59
|
+
const configuredTemplate = options.template;
|
|
60
|
+
const hasTemplateString = typeof configuredTemplate === 'string';
|
|
61
|
+
const hasKnownOptions = Object.keys(options).every((key) => key === 'template' || key === 'values' || key === 'allowedLeadingComments' || key === 'blankLinesAfter');
|
|
62
|
+
if (!hasTemplateString || !hasKnownOptions)
|
|
63
|
+
throw new Error('Headstamp requires a template string and optional values map.');
|
|
64
|
+
const blankLinesAfter = options.blankLinesAfter === undefined ? 1 : options.blankLinesAfter;
|
|
65
|
+
const hasValidBlankLineCount = typeof blankLinesAfter === 'number' && Number.isSafeInteger(blankLinesAfter) && blankLinesAfter >= 0;
|
|
66
|
+
if (!hasValidBlankLineCount)
|
|
67
|
+
throw new Error('Headstamp blankLinesAfter must be a nonnegative safe integer.');
|
|
68
|
+
const leadingComments = options.allowedLeadingComments === undefined ? [] : options.allowedLeadingComments;
|
|
69
|
+
if (!Array.isArray(leadingComments))
|
|
70
|
+
throw new Error('Headstamp allowedLeadingComments must be an array.');
|
|
71
|
+
const allowedLeadingComments = leadingComments.map((comment) => {
|
|
72
|
+
if (typeof comment === 'string') {
|
|
73
|
+
const normalized = comment.replace(/\r\n/g, '\n').replace(/\n+$/, '');
|
|
74
|
+
if (!isCompleteComment(normalized))
|
|
75
|
+
throw new Error('Headstamp permitted literals must be complete comments.');
|
|
76
|
+
return normalized;
|
|
77
|
+
}
|
|
78
|
+
if (!isRecord(comment))
|
|
79
|
+
throw new Error('Headstamp permitted comments require a literal comment or pattern string.');
|
|
80
|
+
const pattern = comment.pattern;
|
|
81
|
+
const hasPatternString = typeof pattern === 'string';
|
|
82
|
+
const hasOnlyPattern = Object.keys(comment).length === 1;
|
|
83
|
+
if (!hasPatternString || !hasOnlyPattern)
|
|
84
|
+
throw new Error('Headstamp permitted comments require a literal comment or pattern string.');
|
|
85
|
+
try {
|
|
86
|
+
return fullValuePattern(pattern);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw new Error('Headstamp permitted comment has an invalid regex.');
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
const values = options.values === undefined ? {} : options.values;
|
|
93
|
+
if (!isRecord(values))
|
|
94
|
+
throw new Error('Headstamp values must be an object.');
|
|
95
|
+
const template = configuredTemplate.replace(/\r\n/g, '\n').replace(/\n+$/, '');
|
|
96
|
+
if (!isCompleteComment(template))
|
|
97
|
+
throw new Error('Headstamp template must be one complete block comment or consecutive // comments using LF or CRLF.');
|
|
98
|
+
const parts = [];
|
|
99
|
+
const fields = [];
|
|
100
|
+
const names = new Set();
|
|
101
|
+
let cursor = 0;
|
|
102
|
+
for (const match of template.matchAll(/\{\{([A-Za-z_]\w*)\}\}/g)) {
|
|
103
|
+
const name = match[1];
|
|
104
|
+
const literal = template.slice(cursor, match.index);
|
|
105
|
+
const isAdjacentPlaceholder = fields.length > 0 && literal.length === 0;
|
|
106
|
+
if (isAdjacentPlaceholder)
|
|
107
|
+
throw new Error('Headstamp placeholders must be separated by literal text.');
|
|
108
|
+
if (names.has(name))
|
|
109
|
+
throw new Error(`Headstamp placeholder "${name}" must occur once.`);
|
|
110
|
+
if (!Object.hasOwn(values, name))
|
|
111
|
+
throw new Error(`Headstamp placeholder "${name}" has no definition.`);
|
|
112
|
+
const definition = values[name];
|
|
113
|
+
if (!isRecord(definition))
|
|
114
|
+
throw new Error(`Headstamp placeholder "${name}" requires pattern and replacement strings.`);
|
|
115
|
+
const pattern = definition.pattern;
|
|
116
|
+
const replacement = definition.replacement;
|
|
117
|
+
const hasPatternString = typeof pattern === 'string';
|
|
118
|
+
const hasReplacementString = typeof replacement === 'string';
|
|
119
|
+
const hasOnlyValueProperties = Object.keys(definition).length === 2;
|
|
120
|
+
if (!hasPatternString || !hasReplacementString || !hasOnlyValueProperties)
|
|
121
|
+
throw new Error(`Headstamp placeholder "${name}" requires pattern and replacement strings.`);
|
|
122
|
+
const hasMultilineReplacement = /[\r\n\u2028\u2029]/.test(replacement);
|
|
123
|
+
if (hasMultilineReplacement)
|
|
124
|
+
throw new Error(`Headstamp placeholder "${name}" replacement must be single-line.`);
|
|
125
|
+
let validator;
|
|
126
|
+
try {
|
|
127
|
+
validator = fullValuePattern(pattern);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new Error(`Headstamp placeholder "${name}" has an invalid regex.`);
|
|
131
|
+
}
|
|
132
|
+
parts.push(literal);
|
|
133
|
+
fields.push({ name, pattern: validator, replacement });
|
|
134
|
+
names.add(name);
|
|
135
|
+
cursor = match.index + match[0].length;
|
|
136
|
+
}
|
|
137
|
+
parts.push(template.slice(cursor));
|
|
138
|
+
const hasMalformedPlaceholder = parts.some((part) => part.includes('{{') || part.includes('}}'));
|
|
139
|
+
if (hasMalformedPlaceholder)
|
|
140
|
+
throw new Error('Headstamp placeholders must use {{name}} with an identifier name.');
|
|
141
|
+
if (Object.keys(values).length !== fields.length)
|
|
142
|
+
throw new Error('Headstamp values must define exactly the template placeholders.');
|
|
143
|
+
function render(fieldValues) {
|
|
144
|
+
let comment = parts[0];
|
|
145
|
+
for (const [index, value] of fieldValues.entries())
|
|
146
|
+
comment += value + parts[index + 1];
|
|
147
|
+
return comment;
|
|
148
|
+
}
|
|
149
|
+
const rendered = render(fields.map((field) => field.replacement));
|
|
150
|
+
if (!isCompleteComment(rendered))
|
|
151
|
+
throw new Error('Headstamp replacement values must preserve the complete comment.');
|
|
152
|
+
function inspect(comment) {
|
|
153
|
+
if (fields.length === 0)
|
|
154
|
+
return comment === template ? { invalidFields: [], repaired: comment } : null;
|
|
155
|
+
const values = matchValues(comment, parts, fields, true) ?? matchValues(comment, parts, fields, false);
|
|
156
|
+
if (values === null || values === 'ambiguous')
|
|
157
|
+
return values;
|
|
158
|
+
const invalidFields = [];
|
|
159
|
+
const repairedValues = values.map((value, index) => {
|
|
160
|
+
const field = fields[index];
|
|
161
|
+
if (field.pattern.test(value))
|
|
162
|
+
return value;
|
|
163
|
+
invalidFields.push(field.name);
|
|
164
|
+
const hasValidReplacement = field.pattern.test(field.replacement);
|
|
165
|
+
return hasValidReplacement ? field.replacement : value;
|
|
166
|
+
});
|
|
167
|
+
const repaired = render(repairedValues);
|
|
168
|
+
if (repaired === comment)
|
|
169
|
+
return { invalidFields, repaired };
|
|
170
|
+
const resultingValues = matchValues(repaired, parts, fields, true) ?? matchValues(repaired, parts, fields, false);
|
|
171
|
+
const preservesFieldBoundaries = resultingValues !== null &&
|
|
172
|
+
resultingValues !== 'ambiguous' &&
|
|
173
|
+
resultingValues.every((value, index) => value === repairedValues[index]);
|
|
174
|
+
const hasSafeRepair = isCompleteComment(repaired) && preservesFieldBoundaries;
|
|
175
|
+
return { invalidFields, repaired: hasSafeRepair ? repaired : comment };
|
|
176
|
+
}
|
|
177
|
+
const invalidReplacementFields = fields
|
|
178
|
+
.filter((field) => !field.pattern.test(field.replacement))
|
|
179
|
+
.map((field) => field.name);
|
|
180
|
+
const replacementsMatchPatterns = invalidReplacementFields.length === 0;
|
|
181
|
+
const hasAmbiguousGeneratedHeader = replacementsMatchPatterns && inspect(rendered) === 'ambiguous';
|
|
182
|
+
if (hasAmbiguousGeneratedHeader)
|
|
183
|
+
throw new Error('Headstamp generated header has ambiguous placeholder boundaries.');
|
|
184
|
+
return {
|
|
185
|
+
rendered,
|
|
186
|
+
blankLinesAfter,
|
|
187
|
+
inspect,
|
|
188
|
+
invalidReplacementFields,
|
|
189
|
+
isAllowedLeadingComment: (comment) => allowedLeadingComments.some((allowed) => typeof allowed === 'string' ? allowed === comment : allowed.test(comment)),
|
|
190
|
+
};
|
|
191
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rm-industries/oxlint-plugin-headstamp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Oxlint-first header templates, regex validation, and safe insertion.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"autofix",
|
|
7
|
+
"copyright-header",
|
|
8
|
+
"file-header",
|
|
9
|
+
"header-template",
|
|
10
|
+
"javascript",
|
|
11
|
+
"license-header",
|
|
12
|
+
"oxlint",
|
|
13
|
+
"oxlint-plugin",
|
|
14
|
+
"typescript"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/rm-industries/headstamp.git"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"registry": "https://registry.npmjs.org"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc",
|
|
38
|
+
"typecheck": "tsc --project tsconfig.test.json",
|
|
39
|
+
"test": "npm run typecheck && npm run build && node --test test/*.test.ts",
|
|
40
|
+
"test:rule": "npm run typecheck && node --test test/header.test.ts",
|
|
41
|
+
"lint": "oxlint src test scripts oxfmt.config.ts",
|
|
42
|
+
"prepack": "npm run build",
|
|
43
|
+
"format:check": "oxfmt --check .",
|
|
44
|
+
"format": "oxfmt --write ."
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^22.20.5",
|
|
48
|
+
"oxfmt": "^0.78.0",
|
|
49
|
+
"oxlint": "^1.93.0",
|
|
50
|
+
"typescript": "^7.0.2"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"oxlint": "^1.88.0"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": "^22.18.0 || >=24.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|