1
+ #!/usr/bin/env node
2
+
3
+ const fs = require ( 'fs' ) ;
4
+ const path = require ( 'path' ) ;
5
+
6
+ // Dynamic path resolution to work from any directory
7
+ const scriptDir = __dirname ;
8
+ const gitHubDiffParserPath = path . join ( scriptDir , 'github-diff-parser' ) ;
9
+ const validateGitHubPath = path . join ( scriptDir , 'validate-content-style-github' ) ;
10
+ const ruleParserPath = path . join ( scriptDir , 'rule-parser' ) ;
11
+ const styleRulesPath = path . join ( scriptDir , 'style-rules.yml' ) ;
12
+
13
+ const GitHubURLDiffParser = require ( gitHubDiffParserPath ) ;
14
+ const GitHubDocumentationValidator = require ( validateGitHubPath ) ;
15
+
16
+ /**
17
+ * Local test script for validating documentation with a diff file
18
+ * Usage: node test-local-diff.js path/to/diff-file.patch [PR_NUMBER]
19
+ */
20
+ class LocalDiffTester {
21
+ constructor ( diffFilePath , prNumber = null ) {
22
+ this . diffFilePath = diffFilePath ;
23
+ this . prNumber = prNumber ;
24
+ this . diffParser = new GitHubURLDiffParser ( { verbose : true } ) ;
25
+ }
26
+
27
+ async testValidation ( ) {
28
+ try {
29
+ console . log ( '🎯 Local Diff Validation Test' ) ;
30
+ console . log ( '============================' ) ;
31
+ console . log ( `📄 Diff file: ${ this . diffFilePath } ` ) ;
32
+ if ( this . prNumber ) {
33
+ console . log ( `🔍 PR number: #${ this . prNumber } ` ) ;
34
+ }
35
+ console . log ( '' ) ;
36
+
37
+ // Read the diff file
38
+ if ( ! fs . existsSync ( this . diffFilePath ) ) {
39
+ throw new Error ( `Diff file not found: ${ this . diffFilePath } ` ) ;
40
+ }
41
+
42
+ const diffContent = fs . readFileSync ( this . diffFilePath , 'utf8' ) ;
43
+ console . log ( `✅ Diff file loaded (${ diffContent . length } characters)` ) ;
44
+
45
+ // Parse the diff
46
+ const parsedDiff = this . diffParser . parsePRDiff ( diffContent ) ;
47
+ console . log ( `📊 Found ${ parsedDiff . totalFiles } markdown files in diff` ) ;
48
+
49
+ if ( parsedDiff . totalFiles === 0 ) {
50
+ console . log ( '📝 No markdown files found in diff' ) ;
51
+ return ;
52
+ }
53
+
54
+ // Display files found
55
+ console . log ( '\n📋 Files to validate:' ) ;
56
+ Object . keys ( parsedDiff . files ) . forEach ( filePath => {
57
+ const info = parsedDiff . files [ filePath ] ;
58
+ if ( info . isNewFile ) {
59
+ console . log ( ` 📄 ${ filePath } (new file)` ) ;
60
+ } else if ( info . isDeletedFile ) {
61
+ console . log ( ` 🗑️ ${ filePath } (deleted)` ) ;
62
+ } else {
63
+ console . log ( ` 📝 ${ filePath } (${ info . addedLines . size } added, ${ info . deletedLines . size } deleted lines)` ) ;
64
+ }
65
+ } ) ;
66
+
67
+ // Create a mock validator to test our logic
68
+ const validator = new MockGitHubValidator ( {
69
+ prNumber : this . prNumber ,
70
+ verbose : true
71
+ } ) ;
72
+
73
+ // Process each file
74
+ let totalIssues = 0 ;
75
+ for ( const [ filePath , diffInfo ] of Object . entries ( parsedDiff . files ) ) {
76
+ console . log ( `\n🔍 Validating: ${ filePath } ` ) ;
77
+
78
+ const issues = await validator . validateFileWithDiff ( filePath , diffInfo ) ;
79
+ totalIssues += issues . length ;
80
+
81
+ if ( issues . length === 0 ) {
82
+ console . log ( ` ✅ No issues found` ) ;
83
+ } else {
84
+ console . log ( ` ⚠️ ${ issues . length } issues found:` ) ;
85
+ issues . forEach ( issue => {
86
+ console . log ( ` - Line ${ issue . line } : ${ issue . message } ` ) ;
87
+ } ) ;
88
+ }
89
+ }
90
+
91
+ // Generate mock GitHub comment
92
+ console . log ( '\n📝 Mock GitHub Comment:' ) ;
93
+ console . log ( '========================' ) ;
94
+ const mockComment = this . generateMockGitHubComment ( parsedDiff , totalIssues ) ;
95
+ console . log ( mockComment ) ;
96
+
97
+ // Save results to file
98
+ const results = {
99
+ timestamp : new Date ( ) . toISOString ( ) ,
100
+ prNumber : this . prNumber ,
101
+ mode : 'local-diff-test' ,
102
+ summary : {
103
+ filesProcessed : parsedDiff . totalFiles ,
104
+ totalIssues : totalIssues
105
+ } ,
106
+ diffFile : this . diffFilePath
107
+ } ;
108
+
109
+ fs . writeFileSync ( 'local-test-results.json' , JSON . stringify ( results , null , 2 ) ) ;
110
+ console . log ( '\n💾 Results saved to: local-test-results.json' ) ;
111
+
112
+ } catch ( error ) {
113
+ console . error ( '❌ Test failed:' , error . message ) ;
114
+ process . exit ( 1 ) ;
115
+ }
116
+ }
117
+
118
+ generateMockGitHubComment ( parsedDiff , totalIssues ) {
119
+ let comment = `## 🎯 Strapi Documentation Style Review (LOCAL TEST)\n\n` ;
120
+ comment += `*Automated analysis using Strapi's 12 Rules of Technical Writing*\n\n` ;
121
+ comment += `**🚀 Test Mode:** Local diff file analysis\n` ;
122
+ comment += `**📊 Source:** ${ this . diffFilePath } \n` ;
123
+ if ( this . prNumber ) {
124
+ comment += `**🎯 PR:** #${ this . prNumber } \n` ;
125
+ }
126
+ comment += '\n' ;
127
+
128
+ comment += '### 📊 Analysis Results\n' ;
129
+ comment += `- **Files analyzed:** ${ parsedDiff . totalFiles } \n` ;
130
+ comment += `- **Total issues:** ${ totalIssues } \n\n` ;
131
+
132
+ if ( totalIssues === 0 ) {
133
+ comment += '🎉 **Perfect!** Your documentation changes follow all 12 technical writing rules.\n\n' ;
134
+ } else {
135
+ comment += `⚠️ Found ${ totalIssues } issues that should be addressed.\n\n` ;
136
+ }
137
+
138
+ comment += '**📚 Resources:**\n' ;
139
+ comment += '- [Strapi\'s 12 Rules of Technical Writing](https://strapi.notion.site/12-Rules-of-Technical-Writing-c75e080e6b19432287b3dd61c2c9fa04)\n' ;
140
+ comment += '- [Documentation Style Guide](https://github.com/strapi/documentation/blob/main/STYLE_GUIDE.pdf)\n\n' ;
141
+ comment += '*🧪 This is a local test simulation of what would be posted on GitHub.*\n' ;
142
+
143
+ return comment ;
144
+ }
145
+ }
146
+
147
+ // Mock validator for local testing
148
+ class MockGitHubValidator {
149
+ constructor ( options ) {
150
+ this . options = options ;
151
+ // Import the real rule parser with correct path
152
+ const Strapi12RulesParser = require ( ruleParserPath ) ;
153
+ this . ruleParser = new Strapi12RulesParser ( styleRulesPath ) ;
154
+ this . diffParser = new GitHubURLDiffParser ( { verbose : options . verbose } ) ;
155
+ }
156
+
157
+ async validateFileWithDiff ( filePath , diffInfo ) {
158
+ try {
159
+ if ( diffInfo . isDeletedFile ) {
160
+ return [ ] ;
161
+ }
162
+
163
+ if ( ! fs . existsSync ( filePath ) ) {
164
+ console . log ( ` ⚠️ File not found locally: ${ filePath } ` ) ;
165
+ return [ {
166
+ file : filePath ,
167
+ line : 1 ,
168
+ message : `File not found locally (this is normal for local testing)` ,
169
+ severity : 'warning'
170
+ } ] ;
171
+ }
172
+
173
+ const originalContent = fs . readFileSync ( filePath , 'utf8' ) ;
174
+
175
+ // Generate filtered content
176
+ const { content : filteredContent , lineMapping, changedLines } =
177
+ this . diffParser . generateFilteredContent ( originalContent , diffInfo ) ;
178
+
179
+ // Apply all rules
180
+ const allIssues = this . applyAllRules ( filteredContent , filePath , { lineMapping, changedLines, diffInfo } ) ;
181
+
182
+ // Filter for changed lines only
183
+ return this . filterIssuesForChangedLines ( allIssues , changedLines , lineMapping , diffInfo ) ;
184
+
185
+ } catch ( error ) {
186
+ return [ {
187
+ file : filePath ,
188
+ line : 1 ,
189
+ message : `Validation error: ${ error . message } ` ,
190
+ severity : 'error'
191
+ } ] ;
192
+ }
193
+ }
194
+
195
+ applyAllRules ( content , filePath , diffContext ) {
196
+ const allIssues = [ ] ;
197
+ const allRules = this . ruleParser . getAllRules ( ) ;
198
+
199
+ allRules . forEach ( rule => {
200
+ try {
201
+ const issues = rule . validator ( content , filePath ) ;
202
+ issues . forEach ( issue => {
203
+ issue . ruleId = rule . id ;
204
+ // Map line numbers if we have diff context
205
+ if ( diffContext && diffContext . lineMapping ) {
206
+ const originalLine = diffContext . lineMapping [ issue . line ] ;
207
+ if ( originalLine ) {
208
+ issue . line = originalLine ;
209
+ }
210
+ }
211
+ } ) ;
212
+ allIssues . push ( ...issues ) ;
213
+ } catch ( error ) {
214
+ // Ignore rule errors for simplicity
215
+ }
216
+ } ) ;
217
+
218
+ return allIssues ;
219
+ }
220
+
221
+ filterIssuesForChangedLines ( issues , changedLines , lineMapping , diffInfo ) {
222
+ if ( ! changedLines || diffInfo . isNewFile ) {
223
+ return issues ;
224
+ }
225
+
226
+ const actuallyChangedLines = new Set ( [
227
+ ...changedLines . added ,
228
+ ...changedLines . modified
229
+ ] ) ;
230
+
231
+ return issues . filter ( issue => {
232
+ if ( actuallyChangedLines . has ( issue . line ) ) {
233
+ return true ;
234
+ }
235
+
236
+ // Simple proximity check for contextual rules
237
+ for ( const changedLine of actuallyChangedLines ) {
238
+ if ( Math . abs ( issue . line - changedLine ) <= 2 ) {
239
+ return true ;
240
+ }
241
+ }
242
+
243
+ return false ;
244
+ } ) ;
245
+ }
246
+ }
247
+
248
+ // Command line interface
249
+ async function main ( ) {
250
+ const args = process . argv . slice ( 2 ) ;
251
+
252
+ if ( args . length === 0 ) {
253
+ console . log ( 'Usage: node test-local-diff.js <diff-file> [pr-number]' ) ;
254
+ console . log ( '' ) ;
255
+ console . log ( 'Examples:' ) ;
256
+ console . log ( ' node test-local-diff.js pr-2439.diff' ) ;
257
+ console . log ( ' node test-local-diff.js pr-2439.diff 2439' ) ;
258
+ console . log ( '' ) ;
259
+ console . log ( 'To get a diff file:' ) ;
260
+ console . log ( ' curl https://patch-diff.githubusercontent.com/raw/strapi/documentation/pull/2439.diff > pr-2439.diff' ) ;
261
+ process . exit ( 1 ) ;
262
+ }
263
+
264
+ const diffFile = args [ 0 ] ;
265
+ const prNumber = args [ 1 ] ? parseInt ( args [ 1 ] ) : null ;
266
+
267
+ const tester = new LocalDiffTester ( diffFile , prNumber ) ;
268
+ await tester . testValidation ( ) ;
269
+ }
270
+
271
+ if ( require . main === module ) {
272
+ main ( ) . catch ( console . error ) ;
273
+ }
274
+
275
+ module . exports = LocalDiffTester ;
0 commit comments