generated-signature 0.1.0 → 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/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.
@@ -91,7 +91,7 @@ export class AutoGenerated {
91
91
  }
92
92
  ```
93
93
 
94
- - **Signature token**: `@generated-signature v1:` followed by 32 lowercase hex characters. It must appear exactly once, anywhere in the file. `sign()` inserts it as the first line (after a shebang or XML declaration) unless the text already has one, so a generator with its own header can place it there with [`signatureToken()`](#signaturetoken).
94
+ - **Signature token**: `@generated-signature v1:` followed by 32 lowercase hex characters. It must appear exactly once, anywhere in the file. `sign()` inserts it as the first line (after a shebang, an XML declaration, or Markdown front matter) unless the text already has one, so a generator with its own header can place it there with [`signatureToken()`](#signaturetoken).
95
95
  - **Manual sections**: a line containing `@manual-start <id>` and a later line containing `@manual-end <id>`. Ids are letters, digits, `_`, `.` and `-`, unique per file. Sections do not nest. The marker must be at the start of the line or after whitespace.
96
96
  - **Hash**: SHA-256 (first 128 bits) of the file with `\r\n` normalized to `\n`, the token's hash replaced by zeros, and every section body removed. The marker lines are hashed, so adding, removing or renaming a section is detected. `v1` versions this algorithm.
97
97
 
@@ -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.',
@@ -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: 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));
@@ -1,5 +1,8 @@
1
1
  import type { CommentOptions } from './comment-style.js';
2
2
  import type { ManualSection } from './sections.js';
3
+ /**
4
+ * @import { GeneratedSignatureMalformedError } from './errors.js'
5
+ */
3
6
  /**
4
7
  * Find the manual sections in a file.
5
8
  *
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$/u, '').split('\n');
27
+ const lines = body === '' ? [] : body.replace(/\n$/v, '').split('\n');
28
28
  return [
29
29
  renderComment(style, `@manual-start ${id}`),
30
30
  ...lines,
@@ -1,6 +1,6 @@
1
1
  import { GeneratedSignatureMalformedError } from './errors.js';
2
- const markerPattern = /(?:^|\s)@manual-(?<kind>start|end)\b(?:\s+(?<id>\S+))?/u;
3
- const idPattern = /^[\w.-]+$/u;
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 = [];
@@ -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}})`, 'gu');
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 findToken = (text)=>{
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 normalized = text.replaceAll('\r\n', '\n');
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 = ()=>`${TOKEN_NAME} v${VERSION}:${PLACEHOLDER}`;
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,28 +46,34 @@ 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
  }
49
+ const frontMatter = style.start === '<!--' ? /^---\r?\n(?:[^\n]*\n)*?---\r?(?:\n|$)/v.exec(text) : null;
50
+ if (frontMatter) {
51
+ const [fenced] = frontMatter;
52
+ return fenced.endsWith('\n') ? fenced + header + text.slice(fenced.length) : fenced + lineBreak + header;
53
+ }
54
54
  return header + text;
55
55
  };
56
56
  export const sign = (text, options)=>{
57
57
  let withToken = text;
58
- if (findToken(text) === null) {
58
+ if (findHash(text) === null) {
59
59
  if (!options) {
60
60
  throw new TypeError('Text has no signature token, provide `comment` or `filePath`');
61
61
  }
62
62
  withToken = insertHeader(text, options);
63
63
  }
64
- return replaceHash(withToken, findToken(withToken), computeHash(withToken));
64
+ const hash = computeHash(withToken);
65
+ return withToken.replaceAll(tokenPattern, ()=>renderToken(hash));
65
66
  };
66
67
  export const verify = (text)=>{
67
68
  try {
68
- const token = findToken(text.replaceAll('\r\n', '\n'));
69
- if (token === null) {
69
+ const hash = findHash(text.replaceAll('\r\n', '\n'));
70
+ if (hash === null) {
70
71
  return {
71
72
  status: 'unsigned'
72
73
  };
73
74
  }
74
75
  return {
75
- status: computeHash(text) === token.hash ? 'valid' : 'invalid'
76
+ status: computeHash(text) === hash ? 'valid' : 'invalid'
76
77
  };
77
78
  } catch (err) {
78
79
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "generated-signature",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Sign generated files so hand edits are detected, while keeping manual sections editable",
5
5
  "engines": {
6
6
  "node": ">=24"