generated-signature 0.1.1 → 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.
@@ -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,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|$)/u.exec(text) : null;
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 (findToken(text) === null) {
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
- return replaceHash(withToken, findToken(withToken), computeHash(withToken));
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 token = findToken(text.replaceAll('\r\n', '\n'));
74
- if (token === null) {
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) === token.hash ? 'valid' : 'invalid'
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.1",
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"