Skip to content

Commit ee10acf

Browse files
committed
chore: wip
1 parent bf6b29b commit ee10acf

File tree

12 files changed

+217
-21
lines changed

12 files changed

+217
-21
lines changed

deps.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
dependencies:
2-
bun.sh: 1.2.19
2+
bun.sh: 1.2.20

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
"build": "for dir in packages/*; do if [ -f \"$dir/package.json\" ]; then echo \"Building $dir\" && bun run --cwd $dir build; fi; done",
2222
"lint": "bunx --bun eslint .",
2323
"lint:fix": "bunx --bun eslint . --fix",
24+
"pickier": "./packages/pickier/pickier run . --mode auto",
25+
"pickier:lint": "./packages/pickier/pickier run . --mode lint",
26+
"pickier:lint:fix": "./packages/pickier/pickier run . --mode lint --fix",
27+
"pickier:format:check": "./packages/pickier/pickier run . --mode format --check",
28+
"pickier:format:write": "./packages/pickier/pickier run . --mode format --write",
2429
"fresh": "bunx rimraf node_modules/ bun.lock && bun i",
2530
"changelog": "bunx logsmith --verbose",
2631
"changelog:generate": "bunx logsmith --output CHANGELOG.md",

packages/pickier/src/config.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,26 @@ import type { PickierConfig } from './types'
22
import { loadConfig } from 'bunfig'
33

44
export const defaultConfig: PickierConfig = {
5-
ignores: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/vendor/**', '**/coverage/**'],
5+
ignores: [
6+
'**/node_modules/**',
7+
'**/.pnpm/**',
8+
'**/.yarn/**',
9+
'**/dist/**',
10+
'**/build/**',
11+
'**/out/**',
12+
'**/.output/**',
13+
'**/.next/**',
14+
'**/.nuxt/**',
15+
'**/.vite/**',
16+
'**/.turbo/**',
17+
'**/.cache/**',
18+
'**/coverage/**',
19+
'**/vendor/**',
20+
'**/tmp/**',
21+
'**/.git/**',
22+
'**/.idea/**',
23+
'**/.vscode/**',
24+
],
625
lint: {
726
extensions: ['ts', 'js', 'html', 'css', 'json', 'jsonc', 'md', 'yaml', 'yml', 'stx'],
827
reporter: 'stylish',

packages/pickier/src/formatter.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import process from 'node:process'
66
import { glob as tinyGlob } from 'tinyglobby'
77
import { formatCode } from './format'
88
import { getAllPlugins } from './plugins'
9-
import { colors, expandPatterns, loadConfigFromPath } from './utils'
9+
import { colors, expandPatterns, loadConfigFromPath, shouldIgnorePath } from './utils'
1010

1111
function trace(...args: any[]) {
1212
if (process.env.PICKIER_TRACE === '1')
@@ -128,6 +128,8 @@ export async function runFormat(globs: string[], options: FormatOptions): Promis
128128
for (const it of items) {
129129
const full = join(dir, it)
130130
const st = statSync(full)
131+
if (shouldIgnorePath(full, cfg.ignores))
132+
continue
131133
if (st.isDirectory())
132134
stack.push(full)
133135
else
@@ -156,6 +158,8 @@ export async function runFormat(globs: string[], options: FormatOptions): Promis
156158
trace('globbed entries', entries.length)
157159

158160
const files = entries.filter((f) => {
161+
if (shouldIgnorePath(f, cfg.ignores))
162+
return false
159163
const idx = f.lastIndexOf('.')
160164
if (idx < 0)
161165
return true // include files without extensions (edge-case test expects this)

packages/pickier/src/index.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
export { config, defaultConfig } from './config'
22
export * from './format'
33
export { runFormat } from './formatter'
4+
export { lintText, runLint, runLintProgrammatic } from './linter'
5+
export { runUnified as run } from './run'
46

5-
export { runLint } from './linter'
6-
export { lintText, runLintProgrammatic } from './linter'
7-
// Unified CLI API
8-
export { run } from './run'
97
export type { RunOptions } from './run'
108

119
export * from './types'

packages/pickier/src/linter.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/* eslint-disable no-console */
22
import type { LintIssue, LintOptions, PickierConfig, PickierPlugin, RuleContext, RulesConfigMap } from './types'
33
import { readFileSync, writeFileSync } from 'node:fs'
4-
import { relative } from 'node:path'
4+
import { isAbsolute, relative, resolve } from 'node:path'
55
import process from 'node:process'
66
import { glob as tinyGlob } from 'tinyglobby'
77
import { detectQuoteIssues, hasIndentIssue } from './format'
88
import { formatStylish } from './formatter'
99
import { getAllPlugins } from './plugins'
10-
import { colors, expandPatterns, isCodeFile, loadConfigFromPath } from './utils'
10+
import { colors, expandPatterns, isCodeFile, loadConfigFromPath, shouldIgnorePath } from './utils'
1111

1212
function trace(...args: any[]) {
1313
if (process.env.PICKIER_TRACE === '1')
@@ -79,19 +79,22 @@ export async function runLintProgrammatic(
7979
try {
8080
const { statSync } = await import('node:fs')
8181
const st = statSync(patterns[0])
82-
if (st.isFile())
83-
entries = [patterns[0]]
82+
if (st.isFile()) {
83+
const abs = isAbsolute(patterns[0]) ? patterns[0] : resolve(process.cwd(), patterns[0])
84+
entries = [abs]
85+
}
8486
}
8587
catch {}
8688
}
8789

8890
const simpleDirPattern = patterns.length === 1 && /\*\*\/*\*$/.test(patterns[0])
8991
if (!entries.length && simpleDirPattern) {
9092
const base = patterns[0].replace(/\/?\*\*\/*\*\*?$/, '')
93+
const rootBase = isAbsolute(base) ? base : resolve(process.cwd(), base)
9194
try {
9295
const { readdirSync, statSync } = await import('node:fs')
9396
const { join } = await import('node:path')
94-
const stack: string[] = [base]
97+
const stack: string[] = [rootBase]
9598
while (stack.length) {
9699
if (signal?.aborted)
97100
throw new Error('AbortError')
@@ -100,6 +103,8 @@ export async function runLintProgrammatic(
100103
for (const it of items) {
101104
const full = join(dir, it)
102105
const st = statSync(full)
106+
if (shouldIgnorePath(full, cfg.ignores))
107+
continue
103108
if (st.isDirectory())
104109
stack.push(full)
105110
else entries.push(full)
@@ -126,7 +131,19 @@ export async function runLintProgrammatic(
126131

127132
if (signal?.aborted)
128133
throw new Error('AbortError')
129-
const files = entries.filter((f: string) => isCodeFile(f, extSet))
134+
// filter with trace counters
135+
let cntTotal = 0; let cntIncluded = 0; let cntNodeModules = 0; let cntIgnored = 0; let cntWrongExt = 0
136+
const files: string[] = []
137+
for (const f of entries) {
138+
cntTotal++
139+
const p = f.replace(/\\/g, '/')
140+
if (p.includes('/node_modules/')) { cntNodeModules++; continue }
141+
if (shouldIgnorePath(f, cfg.ignores)) { cntIgnored++; continue }
142+
if (!isCodeFile(f, extSet)) { cntWrongExt++; continue }
143+
files.push(f)
144+
cntIncluded++
145+
}
146+
trace('filter:programmatic', { total: cntTotal, included: cntIncluded, node_modules: cntNodeModules, ignored: cntIgnored, wrongExt: cntWrongExt })
130147

131148
let allIssues: LintIssue[] = []
132149
for (const file of files) {
@@ -628,13 +645,16 @@ export async function runLint(globs: string[], options: LintOptions): Promise<nu
628645
try {
629646
const { readdirSync, statSync } = await import('node:fs')
630647
const { join } = await import('node:path')
631-
const stack: string[] = [base]
648+
const rootBase = isAbsolute(base) ? base : resolve(process.cwd(), base)
649+
const stack: string[] = [rootBase]
632650
while (stack.length) {
633651
const dir = stack.pop()!
634652
const items = readdirSync(dir)
635653
for (const it of items) {
636654
const full = join(dir, it)
637655
const st = statSync(full)
656+
if (shouldIgnorePath(full, cfg.ignores))
657+
continue
638658
if (st.isDirectory())
639659
stack.push(full)
640660
else
@@ -662,7 +682,19 @@ export async function runLint(globs: string[], options: LintOptions): Promise<nu
662682
}
663683

664684
trace('globbed entries', entries.length)
665-
const files = entries.filter((f: string) => isCodeFile(f, extSet))
685+
// filter with trace counters
686+
let cntTotal = 0; let cntIncluded = 0; let cntNodeModules = 0; let cntIgnored = 0; let cntWrongExt = 0
687+
const files: string[] = []
688+
for (const f of entries) {
689+
cntTotal++
690+
const p = f.replace(/\\/g, '/')
691+
if (p.includes('/node_modules/')) { cntNodeModules++; continue }
692+
if (shouldIgnorePath(f, cfg.ignores)) { cntIgnored++; continue }
693+
if (!isCodeFile(f, extSet)) { cntWrongExt++; continue }
694+
files.push(f)
695+
cntIncluded++
696+
}
697+
trace('filter:cli', { total: cntTotal, included: cntIncluded, node_modules: cntNodeModules, ignored: cntIgnored, wrongExt: cntWrongExt })
666698
trace('filtered files', files.length)
667699

668700
let allIssues: LintIssue[] = []

packages/pickier/src/plugins/pickier.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { PickierPlugin } from '../types'
2+
import { noUnusedVarsRule } from '../rules/general/no-unused-vars'
3+
import { preferConstRule } from '../rules/general/prefer-const'
24
import { importDedupeRule } from '../rules/imports/import-dedupe'
35
import { noImportDistRule } from '../rules/imports/no-import-dist'
46
import { noImportNodeModulesByPathRule } from '../rules/imports/no-import-node-modules-by-path'
5-
import { noUnusedVarsRule } from '../rules/general/no-unused-vars'
6-
import { preferConstRule } from '../rules/general/prefer-const'
77
import { sortExportsRule } from '../rules/sort/exports'
88
import { sortHeritageClausesRule } from '../rules/sort/heritage-clauses'
99
import { sortImportsRule } from '../rules/sort/imports'

packages/pickier/src/run.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export type RunOptions = (Partial<LintOptions> & Partial<FormatOptions>) & {
55
mode?: 'auto' | 'lint' | 'format'
66
}
77

8-
export async function run(globs: string[], options: RunOptions): Promise<number> {
8+
export async function runUnified(globs: string[], options: RunOptions): Promise<number> {
99
const mode = options.mode || 'auto'
1010
if (mode === 'lint')
1111
return runLint(globs, options as LintOptions)

packages/pickier/src/utils.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,41 @@ export function isCodeFile(file: string, allowedExts: Set<string>): boolean {
9898
const ext = file.slice(idx)
9999
return allowedExts.has(ext)
100100
}
101+
102+
// Basic POSIX-like normalization for matching
103+
function toPosixPath(p: string): string {
104+
return p.replace(/\\/g, '/').replace(/\/+/g, '/')
105+
}
106+
107+
/**
108+
* Lightweight ignore matcher supporting common patterns like double-star slash dir slash double-star.
109+
* (Example: patterns matching any path segment named "dir" recursively.)
110+
* Not a full glob engine; optimized for directory skip checks in manual traversal.
111+
*/
112+
export function shouldIgnorePath(absPath: string, ignoreGlobs: string[]): boolean {
113+
const rel = toPosixPath(absPath.startsWith(process.cwd())
114+
? absPath.slice(process.cwd().length)
115+
: absPath)
116+
// quick checks for typical patterns **/name/**
117+
for (const g of ignoreGlobs) {
118+
// normalize
119+
const gg = toPosixPath(g.trim())
120+
// handle patterns like any-depth/name/any-depth (including dot-prefixed names)
121+
const m = gg.match(/\*\*\/(.+?)\/\*\*$/)
122+
if (m) {
123+
const name = m[1]
124+
if (rel.includes(`/${name}/`) || rel.endsWith(`/${name}`))
125+
return true
126+
continue
127+
}
128+
// handle any-depth/name (no trailing any-depth)
129+
const m2 = gg.match(/\*\*\/(.+)$/)
130+
if (m2) {
131+
const name = m2[1].replace(/\/$/, '')
132+
if (rel.includes(`/${name}/`) || rel.endsWith(`/${name}`))
133+
return true
134+
continue
135+
}
136+
}
137+
return false
138+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, expect, it } from 'bun:test'
2+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
3+
import { tmpdir } from 'node:os'
4+
import { join } from 'node:path'
5+
import { runLint } from '../src/linter'
6+
7+
function tmpdirPickier(): string {
8+
return mkdtempSync(join(tmpdir(), 'pickier-ignore-cli-'))
9+
}
10+
11+
describe('ignored directories are not scanned (CLI path runLint)', () => {
12+
it('does not scan node_modules', async () => {
13+
const dir = tmpdirPickier()
14+
try {
15+
const nmFile = join(dir, 'node_modules', 'pako', 'index.ts')
16+
mkdirSync(join(dir, 'node_modules', 'pako'), { recursive: true })
17+
writeFileSync(nmFile, 'console.log(1)\ndebugger\n', 'utf8')
18+
19+
// nothing else; if node_modules were scanned, code would be 1 due to debugger
20+
const code = await runLint([dir], { reporter: 'json', maxWarnings: -1 })
21+
expect(code).toBe(0)
22+
}
23+
finally {
24+
rmSync(dir, { recursive: true, force: true })
25+
}
26+
})
27+
28+
it('does not scan dist', async () => {
29+
const dir = tmpdirPickier()
30+
try {
31+
const distFile = join(dir, 'dist', 'x.ts')
32+
mkdirSync(join(dir, 'dist'), { recursive: true })
33+
writeFileSync(distFile, 'console.log(1)\ndebugger\n', 'utf8')
34+
35+
const code = await runLint([dir], { reporter: 'json', maxWarnings: -1 })
36+
expect(code).toBe(0)
37+
}
38+
finally {
39+
rmSync(dir, { recursive: true, force: true })
40+
}
41+
})
42+
})

0 commit comments

Comments
 (0)