Skip to content

Commit 0c0f371

Browse files
committed
chore(sync): cascade logger-guardrails check from socket-repo-template
Adds _shared/scripts/logger-guardrails.mts. Custom validator that bans status-symbol emoji literals, console.log/error/warn/info, inline getDefaultLogger().<method>() calls, and dynamic import() outside bundled trees. Source: socket-repo-template@9071a2a — 'feat(lint): logger-guardrails + stricter typescript rules'.
1 parent 90bb01f commit 0c0f371

1 file changed

Lines changed: 228 additions & 0 deletions

File tree

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/**
2+
* Lint guardrails the fleet enforces beyond what oxlint covers natively.
3+
*
4+
* Five checks, one pass:
5+
*
6+
* 1. **Status-symbol emoji** (✓ ✔ ❌ ✗ ⚠ ⚠️ ❗ ✅ ❎ ☑) — banned.
7+
* The `@socketsecurity/lib/logger` package owns the visual prefix
8+
* via `logger.success()` / `logger.fail()` / `logger.warn()` etc.
9+
* Hand-rolling the symbols fragments the visual style and bypasses
10+
* theme-aware color.
11+
* 2. **`console.log` / `console.error` / `console.warn` / `console.info`
12+
* / `console.debug` / `console.trace`** — banned. Use the logger.
13+
* 3. **Inline `getDefaultLogger().<method>()`** — banned. The logger
14+
* must be hoisted at the top of the file:
15+
* `const logger = getDefaultLogger()`
16+
* then `logger.success(...)`. Inline calls re-resolve the logger
17+
* every invocation and read inconsistently.
18+
* 4. **Dynamic `import()` in non-bundled code** — banned. Scripts under
19+
* `scripts/` run directly via `node`; nothing bundles them, so a
20+
* dynamic import only adds a runtime async hop for no resolution win.
21+
* Use static ES6 imports. Allowed inside `src/` (which gets bundled)
22+
* and inside `.config/` bundler configs.
23+
*
24+
* (TypeScript `any` is enforced by oxlint's `typescript/no-explicit-any`
25+
* rule — kept in `.oxlintrc.json` so it benefits from the language-aware
26+
* AST. Don't duplicate that here.)
27+
*
28+
* Why a custom check instead of oxlint plugins: the rules above need
29+
* either custom matchers (the inline-logger hoist requirement) or
30+
* conditional scope (dynamic-import bans only outside the bundled tree)
31+
* that oxlint's built-in rule set doesn't express. A small TS scanner
32+
* is cheaper than a full oxlint plugin and runs in the existing
33+
* scripts/check.mts pipeline.
34+
*
35+
* Usage:
36+
* import { checkLoggerGuardrails } from '.../_shared/scripts/logger-guardrails.mts'
37+
* const { violations } = await checkLoggerGuardrails({ cwd: process.cwd() })
38+
* if (violations.length) { process.exitCode = 1 }
39+
*/
40+
import { existsSync, readFileSync } from 'node:fs'
41+
import path from 'node:path'
42+
43+
import fastGlob from 'fast-glob'
44+
45+
export type GuardrailReason =
46+
| 'status-emoji'
47+
| 'console-call'
48+
| 'inline-logger'
49+
| 'dynamic-import'
50+
51+
export type GuardrailViolation = {
52+
readonly file: string
53+
readonly line: number
54+
readonly column: number
55+
readonly snippet: string
56+
readonly reason: GuardrailReason
57+
}
58+
59+
export type CheckLoggerGuardrailsOptions = {
60+
/** Repo root. Defaults to process.cwd(). */
61+
readonly cwd?: string
62+
/** Globs to scan, relative to cwd. */
63+
readonly include?: readonly string[]
64+
/** Globs to skip. */
65+
readonly exclude?: readonly string[]
66+
/** File extensions to scan. */
67+
readonly extensions?: readonly string[]
68+
/**
69+
* Globs that ARE bundled. Dynamic `import()` is allowed inside these
70+
* (the bundler resolves the import statically at build time). Default
71+
* is `src/**` + `.config/**` (bundler configs).
72+
*/
73+
readonly bundledRoots?: readonly string[]
74+
}
75+
76+
export type CheckLoggerGuardrailsResult = {
77+
readonly violations: readonly GuardrailViolation[]
78+
readonly fileCount: number
79+
}
80+
81+
const DEFAULT_INCLUDE = ['scripts/**/*', 'src/**/*', 'lib/**/*', '.config/**/*']
82+
const DEFAULT_EXCLUDE = [
83+
'**/dist/**',
84+
'**/node_modules/**',
85+
'**/coverage/**',
86+
'**/.cache/**',
87+
'**/test/fixtures/**',
88+
'**/test/packages/**',
89+
'**/*.d.ts',
90+
'**/*.d.mts',
91+
'**/upstream/**',
92+
]
93+
const DEFAULT_EXTENSIONS = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs']
94+
const DEFAULT_BUNDLED_ROOTS = ['src/', '.config/']
95+
96+
const STATUS_EMOJI = ['✓', '✔', '❌', '✗', '⚠', '⚠️', '❗', '✅', '❎', '☑']
97+
98+
const CONSOLE_CALL_RE =
99+
/\bconsole\s*\.\s*(?:log|error|warn|info|debug|trace)\s*\(/g
100+
101+
const INLINE_LOGGER_RE = /\bgetDefaultLogger\s*\(\s*\)\s*\.\s*[a-zA-Z_$]/g
102+
103+
const DYNAMIC_IMPORT_RE = /(?<![a-zA-Z_$])import\s*\(/g
104+
105+
function isInBundledRoot(
106+
relativePath: string,
107+
bundledRoots: readonly string[],
108+
): boolean {
109+
const normalized = relativePath.split(path.sep).join('/')
110+
return bundledRoots.some(root => normalized.startsWith(root))
111+
}
112+
113+
function isCommentLine(trimmed: string): boolean {
114+
return (
115+
trimmed.startsWith('//') ||
116+
trimmed.startsWith('*') ||
117+
trimmed.startsWith('/*')
118+
)
119+
}
120+
121+
export async function checkLoggerGuardrails(
122+
options: CheckLoggerGuardrailsOptions = {},
123+
): Promise<CheckLoggerGuardrailsResult> {
124+
const cwd = options.cwd ?? process.cwd()
125+
const include = options.include ?? DEFAULT_INCLUDE
126+
const exclude = options.exclude ?? DEFAULT_EXCLUDE
127+
const extensions = options.extensions ?? DEFAULT_EXTENSIONS
128+
const bundledRoots = options.bundledRoots ?? DEFAULT_BUNDLED_ROOTS
129+
130+
const files = await fastGlob(include as string[], {
131+
absolute: true,
132+
cwd,
133+
ignore: exclude as string[],
134+
onlyFiles: true,
135+
})
136+
137+
const matched = files.filter(file =>
138+
extensions.some(ext => file.endsWith(ext)),
139+
)
140+
141+
const violations: GuardrailViolation[] = []
142+
143+
for (const file of matched) {
144+
if (!existsSync(file)) {
145+
continue
146+
}
147+
const relative = path.relative(cwd, file)
148+
const inBundled = isInBundledRoot(relative, bundledRoots)
149+
const content = readFileSync(file, 'utf8')
150+
const lines = content.split('\n')
151+
152+
for (const [index, line] of lines.entries()) {
153+
const trimmed = line.trimStart()
154+
if (isCommentLine(trimmed)) {
155+
continue
156+
}
157+
158+
// (1) Status-symbol emoji.
159+
for (const emoji of STATUS_EMOJI) {
160+
const col = line.indexOf(emoji)
161+
if (col >= 0) {
162+
violations.push({
163+
column: col + 1,
164+
file: relative,
165+
line: index + 1,
166+
reason: 'status-emoji',
167+
snippet: line.trim(),
168+
})
169+
break
170+
}
171+
}
172+
173+
// (2) console.* calls.
174+
CONSOLE_CALL_RE.lastIndex = 0
175+
const consoleMatch = CONSOLE_CALL_RE.exec(line)
176+
if (consoleMatch) {
177+
violations.push({
178+
column: consoleMatch.index + 1,
179+
file: relative,
180+
line: index + 1,
181+
reason: 'console-call',
182+
snippet: line.trim(),
183+
})
184+
}
185+
186+
// (3) Inline getDefaultLogger().
187+
INLINE_LOGGER_RE.lastIndex = 0
188+
const inlineMatch = INLINE_LOGGER_RE.exec(line)
189+
if (inlineMatch) {
190+
violations.push({
191+
column: inlineMatch.index + 1,
192+
file: relative,
193+
line: index + 1,
194+
reason: 'inline-logger',
195+
snippet: line.trim(),
196+
})
197+
}
198+
199+
// (4) Dynamic import in non-bundled code.
200+
if (!inBundled) {
201+
DYNAMIC_IMPORT_RE.lastIndex = 0
202+
const dynamicMatch = DYNAMIC_IMPORT_RE.exec(line)
203+
if (dynamicMatch) {
204+
violations.push({
205+
column: dynamicMatch.index + 1,
206+
file: relative,
207+
line: index + 1,
208+
reason: 'dynamic-import',
209+
snippet: line.trim(),
210+
})
211+
}
212+
}
213+
}
214+
}
215+
216+
return { fileCount: matched.length, violations }
217+
}
218+
219+
export const GUARDRAIL_FIX_HINTS: Readonly<Record<GuardrailReason, string>> = {
220+
'console-call':
221+
'Use logger from @socketsecurity/lib/logger: import { getDefaultLogger } from "@socketsecurity/lib/logger"; const logger = getDefaultLogger(); then logger.success(...) / logger.fail(...) / logger.warn(...) / logger.info(...) / logger.log(...).',
222+
'dynamic-import':
223+
"Use a static `import` statement at the top of the file. Dynamic `import()` is only allowed inside bundled code (src/ or bundler configs); script files run directly via `node` and don't need lazy resolution.",
224+
'inline-logger':
225+
'Hoist the logger: `const logger = getDefaultLogger()` near the top of the file. Inline `getDefaultLogger().<method>()` re-resolves on every call.',
226+
'status-emoji':
227+
'Remove the literal symbol and use the matching logger method: ✓/✔/✅ → logger.success(...), ❌/✗ → logger.fail(...), ⚠/⚠️ → logger.warn(...), ℹ → logger.info(...).',
228+
}

0 commit comments

Comments
 (0)