fcis 0.1.0 → 0.2.0
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/.plans/003-code-cleanup-consolidation.md +242 -0
- package/.plans/004-directory-depth-rollup.md +408 -0
- package/.plans/005-code-refinements.md +210 -0
- package/.plans/006-minor-refinements.md +149 -0
- package/.plans/007-compositional-function-scoring.md +514 -0
- package/README.md +38 -3
- package/TECHNICAL.md +125 -2
- package/dist/cli.js +595 -327
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +15 -2
- package/dist/index.js +409 -240
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/cli-utils.ts +201 -0
- package/src/cli.ts +99 -117
- package/src/detection/markers.ts +0 -222
- package/src/extraction/extract-functions.ts +106 -2
- package/src/extraction/extractor.ts +35 -74
- package/src/reporting/report-console.ts +188 -102
- package/src/reporting/report-json.ts +26 -3
- package/src/scoring/scorer.ts +425 -160
- package/src/types.ts +9 -2
- package/tests/classifier.test.ts +0 -1
- package/tests/cli.test.ts +356 -0
- package/tests/detect-markers.test.ts +1 -3
- package/tests/extractor.test.ts +95 -1
- package/tests/integration.test.ts +344 -0
- package/tests/report-console.test.ts +92 -0
- package/tests/scorer.test.ts +886 -0
package/src/types.ts
CHANGED
|
@@ -31,7 +31,7 @@ export type ExtractedFunction = {
|
|
|
31
31
|
bodyLineCount: number
|
|
32
32
|
statementCount: number
|
|
33
33
|
hasConditionals: boolean
|
|
34
|
-
parentContext: string | null // e.g. class name, variable name
|
|
34
|
+
parentContext: string | null // e.g. class name, variable name, or HOF method name
|
|
35
35
|
callSites: CallSite[] // enriched call expressions with position and await context
|
|
36
36
|
hasAwait: boolean // convenience flag: true if any callSite.isAwaited
|
|
37
37
|
propertyAccessChains: string[] // e.g. ["process.env.NODE_ENV", "db.user"]
|
|
@@ -42,6 +42,11 @@ export type ExtractedFunction = {
|
|
|
42
42
|
| 'function-expression'
|
|
43
43
|
| 'getter'
|
|
44
44
|
| 'setter'
|
|
45
|
+
// Compositional scoring fields (Plan 007)
|
|
46
|
+
// Reference to enclosing function for inline callbacks; null if at module scope or not a callback
|
|
47
|
+
enclosingFunctionStartLine: number | null
|
|
48
|
+
// True if this is an inline callback passed to a known HOF (map, filter, forEach, etc.)
|
|
49
|
+
isInlineCallback: boolean
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
/**
|
|
@@ -65,7 +70,6 @@ export type MarkerType =
|
|
|
65
70
|
| 'database-call'
|
|
66
71
|
| 'network-fetch'
|
|
67
72
|
| 'network-http'
|
|
68
|
-
| 'fs-import'
|
|
69
73
|
| 'fs-call'
|
|
70
74
|
| 'env-access'
|
|
71
75
|
| 'console-log'
|
|
@@ -187,6 +191,8 @@ export type ProjectScore = {
|
|
|
187
191
|
filesGlob?: string // the glob pattern used for --files
|
|
188
192
|
// Errors encountered during analysis
|
|
189
193
|
errors?: AnalysisError[]
|
|
194
|
+
// Rolled-up directories (when --dir-depth is used)
|
|
195
|
+
rolledUpDirectories?: DirectoryScore[]
|
|
190
196
|
}
|
|
191
197
|
|
|
192
198
|
/**
|
|
@@ -210,6 +216,7 @@ export type AnalyzerConfig = {
|
|
|
210
216
|
outputPath?: string
|
|
211
217
|
quiet?: boolean
|
|
212
218
|
verbose?: boolean
|
|
219
|
+
dirDepth?: number
|
|
213
220
|
}
|
|
214
221
|
|
|
215
222
|
/**
|
package/tests/classifier.test.ts
CHANGED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the extracted pure functions from the CLI module.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest'
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
validateCliFlags,
|
|
11
|
+
validateGlobPattern,
|
|
12
|
+
buildAnalyzerConfig,
|
|
13
|
+
buildThresholdConfig,
|
|
14
|
+
type CliFlags,
|
|
15
|
+
} from '../src/cli-utils.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Helper to create default CLI flags
|
|
19
|
+
*/
|
|
20
|
+
function createDefaultFlags(overrides: Partial<CliFlags> = {}): CliFlags {
|
|
21
|
+
return {
|
|
22
|
+
json: false,
|
|
23
|
+
output: undefined,
|
|
24
|
+
minHealth: undefined,
|
|
25
|
+
minPurity: undefined,
|
|
26
|
+
minQuality: undefined,
|
|
27
|
+
files: undefined,
|
|
28
|
+
format: 'console',
|
|
29
|
+
quiet: false,
|
|
30
|
+
verbose: false,
|
|
31
|
+
dirDepth: undefined,
|
|
32
|
+
version: undefined,
|
|
33
|
+
help: undefined,
|
|
34
|
+
...overrides,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('validateCliFlags', () => {
|
|
39
|
+
describe('threshold validation', () => {
|
|
40
|
+
it('should accept valid minHealth', () => {
|
|
41
|
+
const flags = createDefaultFlags({ minHealth: 70 })
|
|
42
|
+
const result = validateCliFlags(flags)
|
|
43
|
+
expect(result).toEqual({ valid: true })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should accept minHealth of 0', () => {
|
|
47
|
+
const flags = createDefaultFlags({ minHealth: 0 })
|
|
48
|
+
const result = validateCliFlags(flags)
|
|
49
|
+
expect(result).toEqual({ valid: true })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should accept minHealth of 100', () => {
|
|
53
|
+
const flags = createDefaultFlags({ minHealth: 100 })
|
|
54
|
+
const result = validateCliFlags(flags)
|
|
55
|
+
expect(result).toEqual({ valid: true })
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should reject minHealth > 100', () => {
|
|
59
|
+
const flags = createDefaultFlags({ minHealth: 101 })
|
|
60
|
+
const result = validateCliFlags(flags)
|
|
61
|
+
expect(result.valid).toBe(false)
|
|
62
|
+
if (!result.valid) {
|
|
63
|
+
expect(result.error).toContain('--min-health')
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should reject minHealth < 0', () => {
|
|
68
|
+
const flags = createDefaultFlags({ minHealth: -1 })
|
|
69
|
+
const result = validateCliFlags(flags)
|
|
70
|
+
expect(result.valid).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should reject minPurity > 100', () => {
|
|
74
|
+
const flags = createDefaultFlags({ minPurity: 150 })
|
|
75
|
+
const result = validateCliFlags(flags)
|
|
76
|
+
expect(result.valid).toBe(false)
|
|
77
|
+
if (!result.valid) {
|
|
78
|
+
expect(result.error).toContain('--min-purity')
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should reject minQuality > 100', () => {
|
|
83
|
+
const flags = createDefaultFlags({ minQuality: 200 })
|
|
84
|
+
const result = validateCliFlags(flags)
|
|
85
|
+
expect(result.valid).toBe(false)
|
|
86
|
+
if (!result.valid) {
|
|
87
|
+
expect(result.error).toContain('--min-quality')
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should accept all thresholds at once', () => {
|
|
92
|
+
const flags = createDefaultFlags({
|
|
93
|
+
minHealth: 70,
|
|
94
|
+
minPurity: 50,
|
|
95
|
+
minQuality: 60,
|
|
96
|
+
})
|
|
97
|
+
const result = validateCliFlags(flags)
|
|
98
|
+
expect(result).toEqual({ valid: true })
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('format validation', () => {
|
|
103
|
+
it('should accept console format', () => {
|
|
104
|
+
const flags = createDefaultFlags({ format: 'console' })
|
|
105
|
+
const result = validateCliFlags(flags)
|
|
106
|
+
expect(result).toEqual({ valid: true })
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should accept json format', () => {
|
|
110
|
+
const flags = createDefaultFlags({ format: 'json' })
|
|
111
|
+
const result = validateCliFlags(flags)
|
|
112
|
+
expect(result).toEqual({ valid: true })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should accept summary format', () => {
|
|
116
|
+
const flags = createDefaultFlags({ format: 'summary' })
|
|
117
|
+
const result = validateCliFlags(flags)
|
|
118
|
+
expect(result).toEqual({ valid: true })
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should reject invalid format', () => {
|
|
122
|
+
const flags = createDefaultFlags({ format: 'xml' })
|
|
123
|
+
const result = validateCliFlags(flags)
|
|
124
|
+
expect(result.valid).toBe(false)
|
|
125
|
+
if (!result.valid) {
|
|
126
|
+
expect(result.error).toContain('--format')
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('--dir-depth validation', () => {
|
|
132
|
+
it('should accept positive integer', () => {
|
|
133
|
+
const flags = createDefaultFlags({ dirDepth: 2 })
|
|
134
|
+
const result = validateCliFlags(flags)
|
|
135
|
+
expect(result).toEqual({ valid: true })
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('should accept depth of 1', () => {
|
|
139
|
+
const flags = createDefaultFlags({ dirDepth: 1 })
|
|
140
|
+
const result = validateCliFlags(flags)
|
|
141
|
+
expect(result).toEqual({ valid: true })
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('should reject zero', () => {
|
|
145
|
+
const flags = createDefaultFlags({ dirDepth: 0 })
|
|
146
|
+
const result = validateCliFlags(flags)
|
|
147
|
+
expect(result.valid).toBe(false)
|
|
148
|
+
if (!result.valid) {
|
|
149
|
+
expect(result.error).toContain('--dir-depth')
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should reject negative numbers', () => {
|
|
154
|
+
const flags = createDefaultFlags({ dirDepth: -1 })
|
|
155
|
+
const result = validateCliFlags(flags)
|
|
156
|
+
expect(result.valid).toBe(false)
|
|
157
|
+
if (!result.valid) {
|
|
158
|
+
expect(result.error).toContain('--dir-depth')
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should reject non-integers', () => {
|
|
163
|
+
const flags = createDefaultFlags({ dirDepth: 1.5 })
|
|
164
|
+
const result = validateCliFlags(flags)
|
|
165
|
+
expect(result.valid).toBe(false)
|
|
166
|
+
if (!result.valid) {
|
|
167
|
+
expect(result.error).toContain('--dir-depth')
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should accept undefined (flag not provided)', () => {
|
|
172
|
+
const flags = createDefaultFlags({ dirDepth: undefined })
|
|
173
|
+
const result = validateCliFlags(flags)
|
|
174
|
+
expect(result).toEqual({ valid: true })
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('validateGlobPattern', () => {
|
|
180
|
+
describe('valid patterns', () => {
|
|
181
|
+
it('should accept simple glob', () => {
|
|
182
|
+
const result = validateGlobPattern('src/**/*.ts')
|
|
183
|
+
expect(result).toEqual({ valid: true })
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should accept pattern with brackets', () => {
|
|
187
|
+
const result = validateGlobPattern('src/[abc]/*.ts')
|
|
188
|
+
expect(result).toEqual({ valid: true })
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should accept pattern with braces', () => {
|
|
192
|
+
const result = validateGlobPattern('src/{a,b,c}/*.ts')
|
|
193
|
+
expect(result).toEqual({ valid: true })
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should accept relative path', () => {
|
|
197
|
+
const result = validateGlobPattern('./src/**/*.ts')
|
|
198
|
+
expect(result).toEqual({ valid: true })
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('invalid patterns', () => {
|
|
203
|
+
it('should reject empty pattern', () => {
|
|
204
|
+
const result = validateGlobPattern('')
|
|
205
|
+
expect(result.valid).toBe(false)
|
|
206
|
+
if (!result.valid) {
|
|
207
|
+
expect(result.error).toContain('empty')
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('should reject whitespace-only pattern', () => {
|
|
212
|
+
const result = validateGlobPattern(' ')
|
|
213
|
+
expect(result.valid).toBe(false)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('should reject absolute path', () => {
|
|
217
|
+
const result = validateGlobPattern('/Users/test/src/**/*.ts')
|
|
218
|
+
expect(result.valid).toBe(false)
|
|
219
|
+
if (!result.valid) {
|
|
220
|
+
expect(result.error).toContain('absolute')
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('should reject unbalanced opening bracket', () => {
|
|
225
|
+
const result = validateGlobPattern('src/[incomplete/*.ts')
|
|
226
|
+
expect(result.valid).toBe(false)
|
|
227
|
+
if (!result.valid) {
|
|
228
|
+
expect(result.error).toContain('unbalanced')
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should reject unbalanced closing bracket', () => {
|
|
233
|
+
const result = validateGlobPattern('src/incomplete]/*.ts')
|
|
234
|
+
expect(result.valid).toBe(false)
|
|
235
|
+
if (!result.valid) {
|
|
236
|
+
expect(result.error).toContain('unbalanced')
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('should reject unbalanced opening brace', () => {
|
|
241
|
+
const result = validateGlobPattern('src/{a,b/*.ts')
|
|
242
|
+
expect(result.valid).toBe(false)
|
|
243
|
+
if (!result.valid) {
|
|
244
|
+
expect(result.error).toContain('unbalanced')
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('should reject unbalanced closing brace', () => {
|
|
249
|
+
const result = validateGlobPattern('src/a,b}/*.ts')
|
|
250
|
+
expect(result.valid).toBe(false)
|
|
251
|
+
if (!result.valid) {
|
|
252
|
+
expect(result.error).toContain('unbalanced')
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
describe('buildAnalyzerConfig', () => {
|
|
259
|
+
it('should build minimal config', () => {
|
|
260
|
+
const flags = createDefaultFlags()
|
|
261
|
+
const config = buildAnalyzerConfig('/path/to/tsconfig.json', flags)
|
|
262
|
+
|
|
263
|
+
expect(config.tsconfigPath).toBe('/path/to/tsconfig.json')
|
|
264
|
+
expect(config.format).toBe('console')
|
|
265
|
+
expect(config.quiet).toBe(false)
|
|
266
|
+
expect(config.verbose).toBe(false)
|
|
267
|
+
expect(config.filesGlob).toBeUndefined()
|
|
268
|
+
expect(config.minHealth).toBeUndefined()
|
|
269
|
+
expect(config.dirDepth).toBeUndefined()
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('should include optional properties when provided', () => {
|
|
273
|
+
const flags = createDefaultFlags({
|
|
274
|
+
files: 'src/**/*.ts',
|
|
275
|
+
minHealth: 70,
|
|
276
|
+
minPurity: 50,
|
|
277
|
+
minQuality: 60,
|
|
278
|
+
output: 'report.json',
|
|
279
|
+
format: 'json',
|
|
280
|
+
quiet: true,
|
|
281
|
+
verbose: true,
|
|
282
|
+
dirDepth: 2,
|
|
283
|
+
})
|
|
284
|
+
const config = buildAnalyzerConfig('/path/to/tsconfig.json', flags)
|
|
285
|
+
|
|
286
|
+
expect(config.tsconfigPath).toBe('/path/to/tsconfig.json')
|
|
287
|
+
expect(config.filesGlob).toBe('src/**/*.ts')
|
|
288
|
+
expect(config.minHealth).toBe(70)
|
|
289
|
+
expect(config.minPurity).toBe(50)
|
|
290
|
+
expect(config.minQuality).toBe(60)
|
|
291
|
+
expect(config.outputPath).toBe('report.json')
|
|
292
|
+
expect(config.format).toBe('json')
|
|
293
|
+
expect(config.quiet).toBe(true)
|
|
294
|
+
expect(config.verbose).toBe(true)
|
|
295
|
+
expect(config.dirDepth).toBe(2)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('should include dirDepth when provided', () => {
|
|
299
|
+
const flags = createDefaultFlags({ dirDepth: 1 })
|
|
300
|
+
const config = buildAnalyzerConfig('/path/to/tsconfig.json', flags)
|
|
301
|
+
|
|
302
|
+
expect(config.dirDepth).toBe(1)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('should not include dirDepth when undefined', () => {
|
|
306
|
+
const flags = createDefaultFlags({ dirDepth: undefined })
|
|
307
|
+
const config = buildAnalyzerConfig('/path/to/tsconfig.json', flags)
|
|
308
|
+
|
|
309
|
+
expect(config.dirDepth).toBeUndefined()
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
describe('buildThresholdConfig', () => {
|
|
314
|
+
it('should return empty object when no thresholds set', () => {
|
|
315
|
+
const flags = createDefaultFlags()
|
|
316
|
+
const config = buildThresholdConfig(flags)
|
|
317
|
+
|
|
318
|
+
expect(config).toEqual({})
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('should include minHealth when set', () => {
|
|
322
|
+
const flags = createDefaultFlags({ minHealth: 70 })
|
|
323
|
+
const config = buildThresholdConfig(flags)
|
|
324
|
+
|
|
325
|
+
expect(config).toEqual({ minHealth: 70 })
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('should include minPurity when set', () => {
|
|
329
|
+
const flags = createDefaultFlags({ minPurity: 50 })
|
|
330
|
+
const config = buildThresholdConfig(flags)
|
|
331
|
+
|
|
332
|
+
expect(config).toEqual({ minPurity: 50 })
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('should include minQuality when set', () => {
|
|
336
|
+
const flags = createDefaultFlags({ minQuality: 60 })
|
|
337
|
+
const config = buildThresholdConfig(flags)
|
|
338
|
+
|
|
339
|
+
expect(config).toEqual({ minQuality: 60 })
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('should include all thresholds when all set', () => {
|
|
343
|
+
const flags = createDefaultFlags({
|
|
344
|
+
minHealth: 70,
|
|
345
|
+
minPurity: 50,
|
|
346
|
+
minQuality: 60,
|
|
347
|
+
})
|
|
348
|
+
const config = buildThresholdConfig(flags)
|
|
349
|
+
|
|
350
|
+
expect(config).toEqual({
|
|
351
|
+
minHealth: 70,
|
|
352
|
+
minPurity: 50,
|
|
353
|
+
minQuality: 60,
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
})
|
|
@@ -271,9 +271,7 @@ describe('detectMarkers', () => {
|
|
|
271
271
|
|
|
272
272
|
const markers = detectMarkers(fn, context)
|
|
273
273
|
|
|
274
|
-
expect(
|
|
275
|
-
markers.filter(m => m.type === 'fs-import' || m.type === 'fs-call'),
|
|
276
|
-
).toHaveLength(0)
|
|
274
|
+
expect(markers.filter(m => m.type === 'fs-call')).toHaveLength(0)
|
|
277
275
|
})
|
|
278
276
|
|
|
279
277
|
it('should detect fs.readFile calls', () => {
|
package/tests/extractor.test.ts
CHANGED
|
@@ -10,7 +10,10 @@ import * as fs from 'node:fs'
|
|
|
10
10
|
import * as path from 'node:path'
|
|
11
11
|
import * as os from 'node:os'
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
loadProject,
|
|
15
|
+
validateTsconfigContent,
|
|
16
|
+
} from '../src/extraction/extractor.js'
|
|
14
17
|
|
|
15
18
|
describe('loadProject', () => {
|
|
16
19
|
let tempDir: string
|
|
@@ -126,6 +129,97 @@ describe('loadProject', () => {
|
|
|
126
129
|
|
|
127
130
|
expect(() => loadProject({ tsconfigPath })).not.toThrow()
|
|
128
131
|
})
|
|
132
|
+
|
|
133
|
+
it('should parse tsconfig with trailing commas', () => {
|
|
134
|
+
const tsconfigContent = `{
|
|
135
|
+
"compilerOptions": {
|
|
136
|
+
"typeRoots": [
|
|
137
|
+
"./node_modules/@types",
|
|
138
|
+
]
|
|
139
|
+
},
|
|
140
|
+
"include": [
|
|
141
|
+
"./src/**/*.ts",
|
|
142
|
+
],
|
|
143
|
+
"exclude": [
|
|
144
|
+
"node_modules",
|
|
145
|
+
],
|
|
146
|
+
}`
|
|
147
|
+
|
|
148
|
+
const tsconfigPath = path.join(tempDir, 'tsconfig.json')
|
|
149
|
+
fs.writeFileSync(tsconfigPath, tsconfigContent)
|
|
150
|
+
|
|
151
|
+
// Create a simple source file
|
|
152
|
+
fs.writeFileSync(path.join(tempDir, 'index.ts'), 'export const foo = 1;')
|
|
153
|
+
|
|
154
|
+
expect(() => loadProject({ tsconfigPath })).not.toThrow()
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should parse tsconfig with extends and trailing comma in array (like packages/chat)', () => {
|
|
158
|
+
// This matches the exact format of packages/chat/tsconfig.json
|
|
159
|
+
const tsconfigContent = `{
|
|
160
|
+
"extends": "@sai/tsconfig/pkg.json",
|
|
161
|
+
"compilerOptions": {
|
|
162
|
+
"typeRoots": [
|
|
163
|
+
"./node_modules/@types",
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
"include": [
|
|
167
|
+
"./src/**/*.ts"
|
|
168
|
+
],
|
|
169
|
+
"exclude": [
|
|
170
|
+
"node_modules"
|
|
171
|
+
]
|
|
172
|
+
}`
|
|
173
|
+
|
|
174
|
+
const tsconfigPath = path.join(tempDir, 'tsconfig.json')
|
|
175
|
+
fs.writeFileSync(tsconfigPath, tsconfigContent)
|
|
176
|
+
|
|
177
|
+
// Create a simple source file
|
|
178
|
+
fs.writeFileSync(path.join(tempDir, 'index.ts'), 'export const foo = 1;')
|
|
179
|
+
|
|
180
|
+
expect(() => loadProject({ tsconfigPath })).not.toThrow()
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe('validateTsconfigContent', () => {
|
|
185
|
+
it('should return valid for correct JSON', () => {
|
|
186
|
+
const result = validateTsconfigContent('{ "compilerOptions": {} }')
|
|
187
|
+
expect(result).toEqual({ valid: true })
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should return valid for JSONC with comments', () => {
|
|
191
|
+
const content = `{
|
|
192
|
+
// This is a comment
|
|
193
|
+
"compilerOptions": {
|
|
194
|
+
"target": "es2022" /* inline */
|
|
195
|
+
}
|
|
196
|
+
}`
|
|
197
|
+
const result = validateTsconfigContent(content)
|
|
198
|
+
expect(result).toEqual({ valid: true })
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('should return valid for JSONC with trailing commas', () => {
|
|
202
|
+
const content = `{
|
|
203
|
+
"compilerOptions": {
|
|
204
|
+
"lib": ["es2023",],
|
|
205
|
+
},
|
|
206
|
+
}`
|
|
207
|
+
const result = validateTsconfigContent(content)
|
|
208
|
+
expect(result).toEqual({ valid: true })
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('should return error for invalid JSON', () => {
|
|
212
|
+
const result = validateTsconfigContent('{ invalid }')
|
|
213
|
+
expect(result.valid).toBe(false)
|
|
214
|
+
if (!result.valid) {
|
|
215
|
+
expect(result.error).toContain('Parse error')
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should return error for missing closing brace', () => {
|
|
220
|
+
const result = validateTsconfigContent('{ "a": 1')
|
|
221
|
+
expect(result.valid).toBe(false)
|
|
222
|
+
})
|
|
129
223
|
})
|
|
130
224
|
|
|
131
225
|
describe('error handling', () => {
|