generated-signature 0.1.1 → 0.1.3
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 +13 -5
- package/dist/eslint/rules/valid.js +1 -1
- package/dist/lib/manifest.js +3 -1
- package/dist/lib/merge.d.ts +3 -0
- package/dist/lib/merge.js +1 -1
- package/dist/lib/sections.js +2 -2
- package/dist/lib/signature.js +13 -17
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Sign generated files so hand edits are detected, while keeping manual sections e
|
|
|
8
8
|
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
|
-
Guides and the full reference are on the [docs site](https://jacobley.github.io/leyman/generated-signature/).
|
|
11
|
+
Guides and the full reference are on the [docs site](https://jacobley.github.io/leyman/packages/generated-signature/).
|
|
12
12
|
|
|
13
13
|
- **Offline check**: `verify()` needs only the file, not the generator, so it is cheap enough for a lint rule or a pre-commit hook.
|
|
14
14
|
- **Manual sections**: mark regions a person may edit. Their content is left out of the signature and carried over when the file is regenerated.
|
|
@@ -186,7 +186,15 @@ Carries manual sections from an existing file into freshly generated text, match
|
|
|
186
186
|
|
|
187
187
|
### `commentStyle(filePath)`
|
|
188
188
|
|
|
189
|
-
**Returns** the line comment syntax for a file, as `CommentStyle` (`{ start: string, end?: string }`), looked up by extension or file name (`Dockerfile`, `.gitignore`, …). `null` if unknown, or for formats without comments like `.json`.
|
|
189
|
+
**Returns** the line comment syntax for a file, as `CommentStyle` (`{ start: string, end?: string }`), looked up by extension or file name (`Dockerfile`, `.gitignore`, …). `null` if unknown, or for formats without comments like `.json`. For any other file, pass `comment` instead of `filePath`.
|
|
190
|
+
|
|
191
|
+
| `CommentStyle` | Files |
|
|
192
|
+
|----------------|-------|
|
|
193
|
+
| `//` | `.c`, `.cc`, `.cjs`, `.cpp`, `.cs`, `.cts`, `.go`, `.h`, `.hpp`, `.java`, `.js`, `.json5`, `.jsonc`, `.jsx`, `.kt`, `.less`, `.mjs`, `.mts`, `.rs`, `.scss`, `.swift`, `.ts`, `.tsx` |
|
|
194
|
+
| `#` | `.bash`, `.cfg`, `.conf`, `.env`, `.ini`, `.pl`, `.properties`, `.py`, `.r`, `.rb`, `.sh`, `.toml`, `.yaml`, `.yml`, `.zsh`, and `.gitattributes`, `.gitignore`, `.npmignore`, `Dockerfile`, `Makefile` |
|
|
195
|
+
| `/* */` | `.css` |
|
|
196
|
+
| `<!-- -->` | `.htm`, `.html`, `.md`, `.mdx`, `.svg`, `.vue`, `.xml` |
|
|
197
|
+
| `--` | `.hs`, `.lua`, `.sql` |
|
|
190
198
|
|
|
191
199
|
---
|
|
192
200
|
|
|
@@ -211,7 +219,7 @@ A JSON file recording generated files that don't carry their own token, e.g. JSO
|
|
|
211
219
|
| Function | Description |
|
|
212
220
|
|----------|-------------|
|
|
213
221
|
| <a id="contenthashcontent"></a>`contentHash(content: string \| Uint8Array): string` | `v1:` and 32 hex characters of SHA-256. `\r\n` counts as `\n`, also in bytes. Manual sections are **not** excluded, so a file with manual sections should be signed inline. |
|
|
214
|
-
| `verifyManifestEntry(content, entry): VerifyResult` | `valid` or `invalid` for a `hash` entry; [`verify()`](#verifytext)'s result for an `inline` entry. |
|
|
222
|
+
| `verifyManifestEntry(content: string \| Uint8Array, entry): VerifyResult` | `valid` or `invalid` for a `hash` entry; [`verify()`](#verifytext)'s result for an `inline` entry. |
|
|
215
223
|
| `parseManifest(text): Manifest` | Parses and validates. Throws `GeneratedSignatureManifestError` if the text is not a valid manifest. |
|
|
216
224
|
| `serializeManifest(files): string` | Manifest text, with paths and inputs sorted so the output doesn't depend on write order. |
|
|
217
225
|
|
|
@@ -239,11 +247,11 @@ import generatedSignature from 'generated-signature/eslint';
|
|
|
239
247
|
export default [generatedSignature.configs.recommended];
|
|
240
248
|
```
|
|
241
249
|
|
|
242
|
-
`configs.recommended` registers the plugin (namespace `generated-signature`) and turns on every rule below as an error.
|
|
250
|
+
`configs.recommended` registers the plugin (namespace `generated-signature`) and turns on every rule below as an error. `eslint` is an optional peer dependency (`^10.13.0`), needed only for this entry point. To choose rules yourself, register the plugin: `{ plugins: { 'generated-signature': generatedSignature }, rules: { 'generated-signature/valid': 'warn' } }`.
|
|
243
251
|
|
|
244
252
|
### `generated-signature/valid`
|
|
245
253
|
|
|
246
|
-
Reports a file whose signature doesn't match (it was edited outside its manual sections), or whose token or manual sections are malformed. Unsigned files are ignored. There is no autofix: re-signing would just approve the edit. Regenerate the file instead, or move the change into a manual section.
|
|
254
|
+
Reports a file whose signature doesn't match (it was edited outside its manual sections; reported on the token), or whose token or manual sections are malformed (`Generated file is malformed: <reason>`, reported on the line at fault). Unsigned files are ignored. There is no autofix: re-signing would just approve the edit. Regenerate the file instead, or move the change into a manual section.
|
|
247
255
|
|
|
248
256
|
```ts
|
|
249
257
|
// @generated-signature v1:… Generated file. Do not edit outside of manual sections.
|
|
@@ -4,7 +4,7 @@ export const valid = {
|
|
|
4
4
|
type: 'problem',
|
|
5
5
|
docs: {
|
|
6
6
|
description: 'Disallow editing generated files outside of their manual sections',
|
|
7
|
-
url: 'https://jacobley.github.io/leyman/generated-signature/guides/eslint#generated-signature-valid'
|
|
7
|
+
url: 'https://jacobley.github.io/leyman/packages/generated-signature/guides/eslint#generated-signature-valid'
|
|
8
8
|
},
|
|
9
9
|
messages: {
|
|
10
10
|
invalid: 'Generated file was edited outside of its manual sections. Regenerate it instead.',
|
package/dist/lib/manifest.js
CHANGED
|
@@ -59,14 +59,16 @@ export const parseManifest = (text)=>{
|
|
|
59
59
|
if (typeof files !== 'object' || files === null || Array.isArray(files)) {
|
|
60
60
|
throw new GeneratedSignatureManifestError('files is not an object');
|
|
61
61
|
}
|
|
62
|
+
const entries = {};
|
|
62
63
|
for (const [path, entry] of Object.entries(files)){
|
|
63
64
|
if (!isEntry(entry)) {
|
|
64
65
|
throw new GeneratedSignatureManifestError(`invalid entry for ${path}`);
|
|
65
66
|
}
|
|
67
|
+
entries[path] = entry;
|
|
66
68
|
}
|
|
67
69
|
return {
|
|
68
70
|
version,
|
|
69
|
-
files:
|
|
71
|
+
files: entries
|
|
70
72
|
};
|
|
71
73
|
};
|
|
72
74
|
const sortKeys = (record)=>Object.fromEntries(Object.entries(record).toSorted(([a], [b])=>a < b ? -1 : 1));
|
package/dist/lib/merge.d.ts
CHANGED
package/dist/lib/merge.js
CHANGED
|
@@ -24,7 +24,7 @@ export const manualSection = ({ id, body = '', indent = '', ...options })=>{
|
|
|
24
24
|
throw new GeneratedSignatureSectionIdError(id);
|
|
25
25
|
}
|
|
26
26
|
const style = resolveCommentStyle(options);
|
|
27
|
-
const lines = body === '' ? [] : body.replace(/\n$/
|
|
27
|
+
const lines = body === '' ? [] : body.replace(/\n$/v, '').split('\n');
|
|
28
28
|
return [
|
|
29
29
|
renderComment(style, `@manual-start ${id}`),
|
|
30
30
|
...lines,
|
package/dist/lib/sections.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GeneratedSignatureMalformedError } from './errors.js';
|
|
2
|
-
const markerPattern = /(?:^|\s)@manual-(?<kind>start
|
|
3
|
-
const idPattern = /^[\w
|
|
2
|
+
const markerPattern = /(?:^|\s)@manual-(?<kind>end|start)\b(?:\s+(?<id>\S+))?/v;
|
|
3
|
+
const idPattern = /^[\w\-.]+$/v;
|
|
4
4
|
export const isValidSectionId = (id)=>idPattern.test(id);
|
|
5
5
|
export const scanSections = (text)=>{
|
|
6
6
|
const sections = [];
|
package/dist/lib/signature.js
CHANGED
|
@@ -6,10 +6,11 @@ const VERSION = '1';
|
|
|
6
6
|
const HASH_LENGTH = 32;
|
|
7
7
|
const PLACEHOLDER = '0'.repeat(HASH_LENGTH);
|
|
8
8
|
const TOKEN_NAME = '@generated-signature';
|
|
9
|
-
const tokenPattern = new RegExp(String.raw`${TOKEN_NAME} v(?<version>\d+):(?<hash>[\da-f]{${HASH_LENGTH}})`, '
|
|
9
|
+
const tokenPattern = new RegExp(String.raw`${TOKEN_NAME} v(?<version>\d+):(?<hash>[\da-f]{${HASH_LENGTH}})`, 'gv');
|
|
10
10
|
const DEFAULT_MESSAGE = 'Generated file. Do not edit outside of manual sections.';
|
|
11
|
+
const renderToken = (hash)=>`${TOKEN_NAME} v${VERSION}:${hash}`;
|
|
11
12
|
const lineOf = (text, offset)=>text.slice(0, offset).split('\n').length;
|
|
12
|
-
const
|
|
13
|
+
const findHash = (text)=>{
|
|
13
14
|
const [first, second] = text.matchAll(tokenPattern);
|
|
14
15
|
if (!first) {
|
|
15
16
|
return null;
|
|
@@ -29,20 +30,14 @@ const findToken = (text)=>{
|
|
|
29
30
|
detail: `v${version} is not supported, expected v${VERSION}`
|
|
30
31
|
});
|
|
31
32
|
}
|
|
32
|
-
return
|
|
33
|
-
hash,
|
|
34
|
-
hashStart: first.index + first[0].length - HASH_LENGTH
|
|
35
|
-
};
|
|
33
|
+
return hash;
|
|
36
34
|
};
|
|
37
|
-
const replaceHash = (text, token, hash)=>text.slice(0, token.hashStart) + hash + text.slice(token.hashStart + HASH_LENGTH);
|
|
38
35
|
const computeHash = (text)=>{
|
|
39
|
-
const
|
|
40
|
-
const token = findToken(normalized);
|
|
41
|
-
const withPlaceholder = replaceHash(normalized, token, PLACEHOLDER);
|
|
36
|
+
const withPlaceholder = text.replaceAll('\r\n', '\n').replaceAll(tokenPattern, ()=>renderToken(PLACEHOLDER));
|
|
42
37
|
const hashable = replaceBodies(withPlaceholder, scanSections(withPlaceholder), ()=>'');
|
|
43
38
|
return createHash('sha256').update(hashable).digest('hex').slice(0, HASH_LENGTH);
|
|
44
39
|
};
|
|
45
|
-
export const signatureToken = ()
|
|
40
|
+
export const signatureToken = ()=>renderToken(PLACEHOLDER);
|
|
46
41
|
const insertHeader = (text, options)=>{
|
|
47
42
|
const style = resolveCommentStyle(options);
|
|
48
43
|
const lineBreak = text.includes('\r\n') ? '\r\n' : '\n';
|
|
@@ -51,7 +46,7 @@ const insertHeader = (text, options)=>{
|
|
|
51
46
|
const firstLineEnd = text.indexOf('\n');
|
|
52
47
|
return firstLineEnd === -1 ? text + lineBreak + header : text.slice(0, firstLineEnd + 1) + header + text.slice(firstLineEnd + 1);
|
|
53
48
|
}
|
|
54
|
-
const frontMatter = style.start === '<!--' ? /^---\r?\n(?:[^\n]*\n)*?---\r?(?:\n|$)/
|
|
49
|
+
const frontMatter = style.start === '<!--' ? /^---\r?\n(?:[^\n]*\n)*?---\r?(?:\n|$)/v.exec(text) : null;
|
|
55
50
|
if (frontMatter) {
|
|
56
51
|
const [fenced] = frontMatter;
|
|
57
52
|
return fenced.endsWith('\n') ? fenced + header + text.slice(fenced.length) : fenced + lineBreak + header;
|
|
@@ -60,24 +55,25 @@ const insertHeader = (text, options)=>{
|
|
|
60
55
|
};
|
|
61
56
|
export const sign = (text, options)=>{
|
|
62
57
|
let withToken = text;
|
|
63
|
-
if (
|
|
58
|
+
if (findHash(text) === null) {
|
|
64
59
|
if (!options) {
|
|
65
60
|
throw new TypeError('Text has no signature token, provide `comment` or `filePath`');
|
|
66
61
|
}
|
|
67
62
|
withToken = insertHeader(text, options);
|
|
68
63
|
}
|
|
69
|
-
|
|
64
|
+
const hash = computeHash(withToken);
|
|
65
|
+
return withToken.replaceAll(tokenPattern, ()=>renderToken(hash));
|
|
70
66
|
};
|
|
71
67
|
export const verify = (text)=>{
|
|
72
68
|
try {
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
69
|
+
const hash = findHash(text.replaceAll('\r\n', '\n'));
|
|
70
|
+
if (hash === null) {
|
|
75
71
|
return {
|
|
76
72
|
status: 'unsigned'
|
|
77
73
|
};
|
|
78
74
|
}
|
|
79
75
|
return {
|
|
80
|
-
status: computeHash(text) ===
|
|
76
|
+
status: computeHash(text) === hash ? 'valid' : 'invalid'
|
|
81
77
|
};
|
|
82
78
|
} catch (err) {
|
|
83
79
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "generated-signature",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Sign generated files so hand edits are detected, while keeping manual sections editable",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"url": "git+https://github.com/JacobLey/leyman.git",
|
|
14
14
|
"directory": "tools/generated-signature/generated-signature"
|
|
15
15
|
},
|
|
16
|
-
"homepage": "https://github.
|
|
16
|
+
"homepage": "https://jacobley.github.io/leyman/packages/generated-signature/",
|
|
17
17
|
"bugs": {
|
|
18
18
|
"url": "https://github.com/JacobLey/leyman/issues"
|
|
19
19
|
},
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"!dist/tsconfig.tsbuildinfo"
|
|
37
37
|
],
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"eslint": "^10.
|
|
39
|
+
"eslint": "^10.13.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependenciesMeta": {
|
|
42
42
|
"eslint": {
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
"@leyman/eslint-config": "^0.0.1",
|
|
49
49
|
"@leyman/expect": "^0.0.1",
|
|
50
50
|
"@swc/cli": "^0.8.1",
|
|
51
|
-
"@swc/core": "^1.16.
|
|
51
|
+
"@swc/core": "^1.16.18",
|
|
52
52
|
"@types/mocha": "^10.0.10",
|
|
53
53
|
"@types/node": "^24.19.0",
|
|
54
54
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
55
55
|
"c8": "^12.0.0",
|
|
56
|
-
"eslint": "^10.
|
|
56
|
+
"eslint": "^10.13.0",
|
|
57
57
|
"expect-type": "^1.4.0",
|
|
58
|
-
"mocha": "^12.0.
|
|
59
|
-
"mocha-chain": "^1.0.
|
|
58
|
+
"mocha": "^12.0.3",
|
|
59
|
+
"mocha-chain": "^1.0.2",
|
|
60
60
|
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
|
61
61
|
},
|
|
62
62
|
"imports": {
|