1515
1616import { execSync } from "node:child_process" ;
1717import { readFileSync , writeFileSync } from "node:fs" ;
18- import { extname } from "node:path" ;
18+ import { extname , basename } from "node:path" ;
1919
2020const args = parseArgs ( process . argv . slice ( 2 ) , {
2121 owner : { type : "string" , required : true } ,
22- year : { type : "string" } , // force single year
23- "default-start" : { type : "string" } , // fallback start year if git history missing
22+ year : { type : "string" } , // force single year
23+ "default-start" : { type : "string" } , // fallback start year if git history missing
2424 "spdx-id" : { type : "string" , default : "GPL-3.0-or-later" } ,
2525 "no-range" : { type : "boolean" , default : false } , // when true, only use single year
26- check : { type : "boolean" , default : false } // dry-run: report missing/mismatch
26+ check : { type : "boolean" , default : false } // dry-run: report missing/mismatch
2727} ) ;
2828
2929const OWNER = args . owner ;
@@ -33,6 +33,7 @@ const DEFAULT_START = args["default-start"] ? String(args["default-start"]) : nu
3333const USE_RANGE = ! args [ "no-range" ] ;
3434const CURRENT_YEAR = new Date ( ) . getFullYear ( ) ;
3535
36+ /** Comment styles per extension (NO markdown here) */
3637const EXT_STYLE = {
3738 ".js" : "block" ,
3839 ".jsx" : "block" ,
@@ -52,9 +53,27 @@ const EXT_STYLE = {
5253 ".env" : "line" ,
5354 ".env.local" : "line" ,
5455 ".env.example" : "line" ,
55- ".txt" : "line" ,
56+ ".txt" : "line"
5657} ;
5758
59+ /** Never touch these extensions / files */
60+ const SKIP_EXT = new Set ( [ ".json" , ".jsonc" , ".json5" , ".md" ] ) ;
61+ const SKIP_FILES = new Set ( [
62+ // common JSON configs
63+ ".eslintrc.json" ,
64+ "package.json" ,
65+ "package-lock.json" ,
66+ "tsconfig.json" ,
67+ "jsconfig.json" ,
68+ ".prettierrc" ,
69+ ".prettierrc.json" ,
70+ ".babelrc" ,
71+ ".babelrc.json" ,
72+ // generated or locky bits (even if extless)
73+ "pnpm-lock.yaml" ,
74+ "yarn.lock"
75+ ] ) ;
76+
5877function main ( ) {
5978 const files = listTrackedFiles ( ) . filter ( includeFile ) ;
6079 let missing = 0 , updated = 0 , unchanged = 0 , skipped = 0 ;
@@ -95,17 +114,30 @@ function listTrackedFiles() {
95114
96115function includeFile ( file ) {
97116 if ( / ^ ( n o d e _ m o d u l e s | b u i l d | d i s t ) \/ / . test ( file ) ) return false ;
117+
98118 const ext = extname ( file ) . toLowerCase ( ) ;
119+ const base = basename ( file ) ;
120+
121+ // hard skips
122+ if ( SKIP_EXT . has ( ext ) ) return false ;
123+ if ( SKIP_FILES . has ( base ) ) return false ;
124+
125+ // known styles
99126 if ( EXT_STYLE [ ext ] ) return true ;
100127
101- // dotfiles without ext (e.g., .gitignore, .env)
102- if ( / ^ \. / . test ( file ) && ! / \. ( p n g | j p g | j p e g | g i f | i c o | l o c k | m a p | p d f | z i p | g z | s v g z ) $ / i. test ( file ) ) return true ;
128+ // dotfiles without ext (e.g., .gitignore, .env) — allow texty ones
129+ if ( / ^ \. / . test ( file ) && ! / \. ( p n g | j p e ? g | g i f | i c o | l o c k | m a p | p d f | z i p | g z | s v g z | w o f f 2 ? | t t f | e o t | b i n | e x e ) $ / i. test ( file ) ) {
130+ // but don't let dotfile JSON/MD sneak through
131+ if ( SKIP_EXT . has ( ext ) || SKIP_FILES . has ( base ) ) return false ;
132+ return true ;
133+ }
103134
104135 return false ;
105136}
106137
107138function pickStyle ( file ) {
108139 const ext = extname ( file ) . toLowerCase ( ) ;
140+ if ( SKIP_EXT . has ( ext ) ) return null ;
109141 return EXT_STYLE [ ext ] || ( file . startsWith ( "." ) ? "line" : null ) ;
110142}
111143
@@ -147,16 +179,14 @@ function formatHeader(lines, style) {
147179 return "<!--\n " + lines . join ( "\n " ) + "\n-->\n" ;
148180 case "xml" :
149181 return "<!--\n " + lines . join ( "\n " ) + "\n-->\n" ;
150- case "md" :
151- return "> " + lines [ 0 ] + " — " + lines [ 1 ] + "\n\n" ;
152182 case "line" :
153183 default :
154184 return "# " + lines [ 0 ] + "\n# " + lines [ 1 ] + "\n" ;
155185 }
156186}
157187
158188function applyHeader ( file , content , header , style ) {
159- // Special-case XML prolog: insert after <?xml ...?>
189+ // XML prolog: insert after <?xml ...?>
160190 if ( style === "xml" ) {
161191 const trimmed = content . trimStart ( ) ;
162192 if ( / ^ < \? x m l \b / . test ( trimmed ) ) {
@@ -166,12 +196,12 @@ function applyHeader(file, content, header, style) {
166196 const withoutHeader = stripExistingHeader ( existing ) ;
167197 const newContent = content . slice ( 0 , after ) + "\n" + header + withoutHeader ;
168198 const hasHeader = existing !== withoutHeader ;
169- const needsUpdate = hasHeader ; // we rewrote it
199+ const needsUpdate = hasHeader ;
170200 return { hasHeader, needsUpdate, newContent } ;
171201 }
172202 }
173203
174- // Special-case shebang for shell or scripts : keep shebang on top
204+ // Shebang : keep it first
175205 if ( style === "line" && content . startsWith ( "#!" ) ) {
176206 const nl = content . indexOf ( "\n" ) ;
177207 const shebang = nl >= 0 ? content . slice ( 0 , nl + 1 ) : content + "\n" ;
@@ -185,22 +215,17 @@ function applyHeader(file, content, header, style) {
185215
186216 const withoutHeader = stripExistingHeader ( content ) ;
187217 const hasHeader = withoutHeader . length !== content . length ;
188- const needsUpdate = hasHeader ; // simplest: if header exists, we replace to be sure
218+ const needsUpdate = hasHeader ; // replace if present to normalize
189219 const newContent = header + withoutHeader ;
190220 return { hasHeader, needsUpdate, newContent } ;
191221}
192222
193223function stripExistingHeader ( content ) {
194- // Remove our recognizable headers at the very top only
224+ // Remove recognizable headers only at the very top
195225 const patterns = [
196- // block comment
197- / ^ \/ \* [ \s \S ] * ?\* \/ \s * / ,
198- // HTML/XML comment
199- / ^ < ! - - [ \s \S ] * ?- - > \s * / ,
200- // line comments (# ...)
201- / ^ (?: # .* \n ) + / ,
202- // markdown quote
203- / ^ (?: > .* \n ) + /
226+ / ^ \/ \* [ \s \S ] * ?\* \/ \s * / , // block comment
227+ / ^ < ! - - [ \s \S ] * ?- - > \s * / , // HTML/XML comment
228+ / ^ (?: # .* \n ) + / // line comments (# ...)
204229 ] ;
205230
206231 let out = content ;
@@ -244,7 +269,7 @@ function parseArgs(argv, schema) {
244269 return res ;
245270}
246271
247- // Top-level execution with try/catch (avoids .catch on undefined if main is sync)
272+ // Top-level execution (works if main is sync or async )
248273try {
249274 await main ( ) ;
250275} catch ( e ) {
0 commit comments