declapract-typescript-ehmpathy 0.47.30 → 0.47.32

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.
@@ -18,7 +18,12 @@ export const check: FileCheckFunction = async (contents, context) => {
18
18
  { template: context.declaredFileContents },
19
19
  context,
20
20
  );
21
- if (contents === expected) throw new Error('file matches expected content');
21
+
22
+ // if contents don't match expected, best practice is violated
23
+ if (contents !== expected)
24
+ throw new Error('file does not match expected content with apikey secrets');
25
+
26
+ // return = file matches expected (best practice followed)
22
27
  };
23
28
 
24
29
  /**
@@ -18,7 +18,12 @@ export const check: FileCheckFunction = async (contents, context) => {
18
18
  { template: context.declaredFileContents },
19
19
  context,
20
20
  );
21
- if (contents === expected) throw new Error('file matches expected content');
21
+
22
+ // if contents don't match expected, best practice is violated
23
+ if (contents !== expected)
24
+ throw new Error('file does not match expected content with apikey secrets');
25
+
26
+ // return = file matches expected (best practice followed)
22
27
  };
23
28
 
24
29
  /**
@@ -18,7 +18,12 @@ export const check: FileCheckFunction = async (contents, context) => {
18
18
  { template: context.declaredFileContents },
19
19
  context,
20
20
  );
21
- if (contents === expected) throw new Error('file matches expected content');
21
+
22
+ // if contents don't match expected, best practice is violated
23
+ if (contents !== expected)
24
+ throw new Error('file does not match expected content with apikey secrets');
25
+
26
+ // return = file matches expected (best practice followed)
22
27
  };
23
28
 
24
29
  /**
@@ -8,53 +8,126 @@ export const check: FileCheckType = FileCheckType.CONTAINS;
8
8
  /**
9
9
  * sections of gitattributes entries to ensure exist
10
10
  */
11
- const SECTIONS = [
11
+ const SECTIONS: {
12
+ header: { latest: string; priors: string[] };
13
+ entries: { latest: string[]; priors: string[] };
14
+ }[] = [
12
15
  {
13
- header:
14
- '# exclude package locks from git diff; https://stackoverflow.com/a/72834452/3068233',
15
- entries: ['pnpm-lock.yaml -diff', 'package-lock.json -diff'],
16
+ header: {
17
+ latest:
18
+ '# exclude package locks from git diff; https://stackoverflow.com/a/72834452/3068233',
19
+ priors: [
20
+ '# exclude package-lock from git diff; https://stackoverflow.com/a/72834452/3068233',
21
+ ],
22
+ },
23
+ entries: {
24
+ latest: ['pnpm-lock.yaml -diff', 'package-lock.json -diff'],
25
+ priors: ['pnpm-lock.json -diff'],
26
+ },
16
27
  },
17
28
  {
18
- header:
19
- '# auto-resolve lock file conflicts by taking theirs; run install after merge',
20
- entries: ['pnpm-lock.yaml merge=theirs', 'package-lock.json merge=theirs'],
29
+ header: {
30
+ latest:
31
+ '# auto-resolve lock file conflicts by taking theirs; run install after merge',
32
+ priors: [],
33
+ },
34
+ entries: {
35
+ latest: ['pnpm-lock.yaml merge=theirs', 'package-lock.json merge=theirs'],
36
+ priors: ['pnpm-lock.json merge=theirs'],
37
+ },
21
38
  },
22
39
  ];
23
40
 
41
+ /**
42
+ * all latest entries managed by this practice (for deduplication)
43
+ */
44
+ const ALL_LATEST_ENTRIES = SECTIONS.flatMap((s) => s.entries.latest);
45
+
46
+ /**
47
+ * all prior entries that should be removed
48
+ */
49
+ const ALL_PRIOR_ENTRIES = SECTIONS.flatMap((s) => s.entries.priors);
50
+
51
+ /**
52
+ * all prior headers that should be removed
53
+ */
54
+ const ALL_PRIOR_HEADERS = SECTIONS.flatMap((s) => s.header.priors);
55
+
56
+ /**
57
+ * removes legacy headers, duplicate entries, and cleans up empty lines
58
+ */
59
+ const cleanContent = (content: string): string => {
60
+ const lines = content.split('\n');
61
+ const seenEntries = new Set<string>();
62
+ const cleanedLines: string[] = [];
63
+
64
+ for (const line of lines) {
65
+ const trimmed = line.trim();
66
+
67
+ // skip prior headers
68
+ if (ALL_PRIOR_HEADERS.includes(trimmed)) continue;
69
+
70
+ // skip prior entries
71
+ if (ALL_PRIOR_ENTRIES.includes(trimmed)) continue;
72
+
73
+ // skip duplicate latest entries (keep first occurrence)
74
+ if (ALL_LATEST_ENTRIES.includes(trimmed)) {
75
+ if (seenEntries.has(trimmed)) continue;
76
+ seenEntries.add(trimmed);
77
+ }
78
+
79
+ cleanedLines.push(line);
80
+ }
81
+
82
+ // collapse multiple consecutive empty lines into one
83
+ const collapsedLines: string[] = [];
84
+ let prevWasEmpty = false;
85
+ for (const line of cleanedLines) {
86
+ const isEmpty = line.trim() === '';
87
+ if (isEmpty && prevWasEmpty) continue;
88
+ collapsedLines.push(line);
89
+ prevWasEmpty = isEmpty;
90
+ }
91
+
92
+ return collapsedLines.join('\n').trim();
93
+ };
94
+
24
95
  /**
25
96
  * ensures a section with header and entries exists in the content
26
97
  */
27
98
  const ensureSection = (
28
99
  content: string,
29
- section: { header: string; entries: string[] },
100
+ section: (typeof SECTIONS)[number],
30
101
  ): string => {
31
102
  const lines = content.split('\n');
32
103
 
33
104
  // find the header line index
34
- const headerIndex = lines.findIndex((line) => line.trim() === section.header);
105
+ const headerIndex = lines.findIndex(
106
+ (line) => line.trim() === section.header.latest,
107
+ );
35
108
 
36
109
  // if header not found, append the whole section at the end
37
110
  if (headerIndex === -1) {
38
111
  return (
39
112
  content.trimEnd() +
40
113
  '\n\n' +
41
- [section.header, ...section.entries].join('\n') +
114
+ [section.header.latest, ...section.entries.latest].join('\n') +
42
115
  '\n'
43
116
  );
44
117
  }
45
118
 
46
- // find which entries are missing
47
- const missingEntries = section.entries.filter(
119
+ // find which entries are absent
120
+ const entriesAbsent = section.entries.latest.filter(
48
121
  (entry) => !content.includes(entry),
49
122
  );
50
123
 
51
- // if no missing entries, nothing to fix
52
- if (missingEntries.length === 0) return content;
124
+ // if all entries present, no fix required
125
+ if (entriesAbsent.length === 0) return content;
53
126
 
54
- // insert missing entries right after the header
127
+ // insert absent entries right after the header
55
128
  const newLines = [
56
129
  ...lines.slice(0, headerIndex + 1),
57
- ...missingEntries,
130
+ ...entriesAbsent,
58
131
  ...lines.slice(headerIndex + 1),
59
132
  ];
60
133
 
@@ -62,22 +135,25 @@ const ensureSection = (
62
135
  };
63
136
 
64
137
  /**
65
- * fix by ensuring all sections are present with their entries
138
+ * fix by cleanup of legacy content and ensure all sections are present
66
139
  */
67
140
  export const fix: FileFixFunction = (contents) => {
68
141
  // if no contents, create the file with all sections
69
142
  if (!contents) {
70
143
  const allSections = SECTIONS.map((s) =>
71
- [s.header, ...s.entries].join('\n'),
144
+ [s.header.latest, ...s.entries.latest].join('\n'),
72
145
  ).join('\n\n');
73
146
  return { contents: allSections + '\n' };
74
147
  }
75
148
 
149
+ // clean up legacy headers and duplicates first
150
+ let result = cleanContent(contents);
151
+
76
152
  // ensure each section exists
77
- let result = contents;
78
153
  for (const section of SECTIONS) {
79
154
  result = ensureSection(result, section);
80
155
  }
81
156
 
82
- return { contents: result };
157
+ // normalize to exactly one final newline
158
+ return { contents: result.trimEnd() + '\n' };
83
159
  };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "declapract-typescript-ehmpathy",
3
3
  "author": "ehmpathy",
4
4
  "description": "declapract best practices declarations for typescript",
5
- "version": "0.47.30",
5
+ "version": "0.47.32",
6
6
  "license": "MIT",
7
7
  "main": "src/index.js",
8
8
  "repository": "ehmpathy/declapract-typescript-ehmpathy",