mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge remote-tracking branch 'origin/master' into codex/web-queue-actions
# Conflicts: # packages/core/agent-loop/src/agent.ts
This commit is contained in:
98
scripts/lint-rule-fingerprint.spec.ts
Normal file
98
scripts/lint-rule-fingerprint.spec.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { flattenDiagnosticMessageText, parseConfigFileTextToJson } from 'typescript'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
type Rules = Record<string, unknown>
|
||||
|
||||
interface Profile {
|
||||
readonly count: number
|
||||
readonly indexes: readonly number[]
|
||||
readonly sha256: string
|
||||
}
|
||||
|
||||
// A one-time audit against eslint.config.mjs blob 696b08282885296830189fdafe7051a356806fc2
|
||||
// mapped @typescript-eslint/* to typescript/* and four extension rules to their
|
||||
// Oxlint core equivalents. These fingerprints pin the resulting repository
|
||||
// contract; they do not re-evaluate that deleted baseline or track its preset.
|
||||
const profiles = {
|
||||
source: {
|
||||
count: 88,
|
||||
indexes: [0, 1, 4, 5],
|
||||
sha256: 'da1dfd77cb6eb66be93d8d3820f9b9b68b7aa391c24680f8851c0910298f9e3b',
|
||||
},
|
||||
example: {
|
||||
count: 87,
|
||||
indexes: [0, 1, 2, 4, 5],
|
||||
sha256: '6a2606053bc1ec1de3b02611de88ea51d201dac13a1f193e4934d33c08b95f08',
|
||||
},
|
||||
test: {
|
||||
count: 83,
|
||||
indexes: [0, 3, 4, 5],
|
||||
sha256: '7995e14926a36c40bd65c474637735222a95fb030395681685f03060e50a7b78',
|
||||
},
|
||||
} as const satisfies Record<string, Profile>
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function isUnknownArray(value: unknown): value is unknown[] {
|
||||
return Array.isArray(value)
|
||||
}
|
||||
|
||||
function severity(value: unknown): 0 | 1 | 2 {
|
||||
const level = isUnknownArray(value) ? value[0] : value
|
||||
if (level === 'off' || level === 0) return 0
|
||||
if (level === 'warn' || level === 'warning' || level === 1) return 1
|
||||
if (level === 'error' || level === 2) return 2
|
||||
throw new Error(`unsupported lint severity: ${JSON.stringify(level)}`)
|
||||
}
|
||||
|
||||
function normalizedRules(rules: Rules): Rules {
|
||||
return Object.fromEntries(Object.entries(rules)
|
||||
.filter(([, value]) => severity(value) > 0)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([name, value]) => {
|
||||
const options = isUnknownArray(value) ? value.slice(1) : []
|
||||
return [name, [severity(value), ...options]]
|
||||
}))
|
||||
}
|
||||
|
||||
function mergedRules(overrides: readonly unknown[], indexes: readonly number[]): Rules {
|
||||
const merged: Rules = {}
|
||||
for (const index of indexes) {
|
||||
const override = overrides[index]
|
||||
if (!isRecord(override) || !isRecord(override.rules)) {
|
||||
throw new Error(`.oxlintrc.json override ${index} must contain a rules object`)
|
||||
}
|
||||
Object.assign(merged, override.rules)
|
||||
}
|
||||
return normalizedRules(merged)
|
||||
}
|
||||
|
||||
describe('Oxlint repository rule fingerprint', () => {
|
||||
const path = fileURLToPath(new URL('../.oxlintrc.json', import.meta.url))
|
||||
const result = parseConfigFileTextToJson(path, readFileSync(path, 'utf8'))
|
||||
if (result.error !== undefined) {
|
||||
throw new Error(flattenDiagnosticMessageText(result.error.messageText, '\n'))
|
||||
}
|
||||
const parsed: unknown = result.config
|
||||
if (!isRecord(parsed) || !Array.isArray(parsed.overrides)) {
|
||||
throw new Error('.oxlintrc.json must contain an overrides array')
|
||||
}
|
||||
const overrides: readonly unknown[] = parsed.overrides
|
||||
|
||||
it('pins the complete override shape', () => {
|
||||
expect(overrides).toHaveLength(6)
|
||||
})
|
||||
|
||||
it.each(Object.entries(profiles))('pins the %s rule profile', (_name, profile) => {
|
||||
const rules = mergedRules(overrides, profile.indexes)
|
||||
const fingerprint = createHash('sha256').update(JSON.stringify(rules)).digest('hex')
|
||||
|
||||
expect(Object.keys(rules)).toHaveLength(profile.count)
|
||||
expect(fingerprint).toBe(profile.sha256)
|
||||
})
|
||||
})
|
||||
250
scripts/oxlint-contract.spec.ts
Normal file
250
scripts/oxlint-contract.spec.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { join, relative } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { flattenDiagnosticMessageText, parseConfigFileTextToJson } from 'typescript'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const repositoryRoot = fileURLToPath(new URL('..', import.meta.url))
|
||||
const eslintCli = fileURLToPath(new URL('../node_modules/eslint/bin/eslint.js', import.meta.url))
|
||||
const oxlintCli = fileURLToPath(new URL('../node_modules/oxlint/bin/oxlint', import.meta.url))
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function isUnknownArray(value: unknown): value is unknown[] {
|
||||
return Array.isArray(value)
|
||||
}
|
||||
|
||||
function runStagedFormatter(paths: readonly string[]) {
|
||||
return spawnSync(process.execPath, [eslintCli, '--config', 'eslint.format.config.mjs', '--fix', '--no-warn-ignored', ...paths], {
|
||||
cwd: repositoryRoot,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, NO_COLOR: '1' },
|
||||
})
|
||||
}
|
||||
|
||||
function runOxlint(args: readonly string[], env: NodeJS.ProcessEnv = {}) {
|
||||
return spawnSync(process.execPath, [oxlintCli, ...args], {
|
||||
cwd: repositoryRoot,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, NO_COLOR: '1', ...env },
|
||||
})
|
||||
}
|
||||
|
||||
function normalizedOutput(result: ReturnType<typeof runOxlint>): string {
|
||||
return `${result.stdout}${result.stderr}`.replaceAll('\\', '/')
|
||||
}
|
||||
|
||||
async function writeContractConfig(suffix: string): Promise<string> {
|
||||
const path = join(repositoryRoot, `.oxlintrc.contract-${suffix}.json`)
|
||||
await writeFile(path, JSON.stringify({ extends: ['./.oxlintrc.json'], ignorePatterns: [] }))
|
||||
return path
|
||||
}
|
||||
|
||||
describe('Oxlint executable contract', () => {
|
||||
it('discovers the owning TypeScript project for every file class', async () => {
|
||||
const suffix = randomUUID()
|
||||
const configPath = await writeContractConfig(suffix)
|
||||
const probes = [
|
||||
['host package source', 'packages/fs/fs-policy/src', 'packages/fs/fs-policy/tsconfig.json'],
|
||||
['host package test', 'packages/fs/fs-policy/tests', 'tsconfig.host.json'],
|
||||
['client package source', 'packages/client/ui-primitives/src', 'packages/client/ui-primitives/tsconfig.json'],
|
||||
['client package test', 'packages/client/ui-trajectory/tests', 'tsconfig.client.json'],
|
||||
['example', 'examples/headless-agent/tests', 'tsconfig.host.json'],
|
||||
['website', 'website', 'tsconfig.host.json'],
|
||||
] as const
|
||||
const source = `export function probePromise(): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
probePromise()
|
||||
`
|
||||
|
||||
try {
|
||||
const paths: Array<readonly [label: string, path: string, tsconfig: string]> = []
|
||||
for (const [label, parent, tsconfig] of probes) {
|
||||
const path = join(repositoryRoot, parent, `oxlint-contract-${suffix}.ts`)
|
||||
await writeFile(path, source)
|
||||
paths.push([label, relative(repositoryRoot, path), tsconfig])
|
||||
}
|
||||
const clientScript = 'scripts/client-bundle-purity.spec.ts'
|
||||
|
||||
const result = runOxlint([
|
||||
'--config',
|
||||
relative(repositoryRoot, configPath),
|
||||
'--format',
|
||||
'unix',
|
||||
...paths.map(([, path]) => path),
|
||||
clientScript,
|
||||
], { OXC_LOG: 'debug' })
|
||||
const output = normalizedOutput(result)
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.status, output).toBe(1)
|
||||
for (const [label, path, tsconfig] of paths) {
|
||||
expect(output, label).toContain(`${path.replaceAll('\\', '/')}:5:1: Promises must be awaited`)
|
||||
expect(output, `${label} project`).toContain(
|
||||
`Got tsconfig for file ${join(repositoryRoot, path).replaceAll('\\', '/')}: ${join(repositoryRoot, tsconfig).replaceAll('\\', '/')}`,
|
||||
)
|
||||
}
|
||||
expect(output.match(/typescript\(no-floating-promises\)/g)).toHaveLength(probes.length)
|
||||
expect(output, 'client aggregate script project').toContain(
|
||||
`Got tsconfig for file ${join(repositoryRoot, clientScript).replaceAll('\\', '/')}: ${join(repositoryRoot, 'tsconfig.client.json').replaceAll('\\', '/')}`,
|
||||
)
|
||||
expect(output).not.toContain('Unmatched file:')
|
||||
} finally {
|
||||
await Promise.all([
|
||||
...probes.map(([, parent]) => rm(join(repositoryRoot, parent, `oxlint-contract-${suffix}.ts`), { force: true })),
|
||||
rm(configPath, { force: true }),
|
||||
])
|
||||
}
|
||||
}, 20_000)
|
||||
|
||||
it('runs JavaScript compatibility and nursery rules', async () => {
|
||||
const suffix = randomUUID()
|
||||
const configPath = await writeContractConfig(suffix)
|
||||
const path = join(repositoryRoot, 'scripts', `oxlint-contract-${suffix}.ts`)
|
||||
const source = `export function firstProbe(): number {
|
||||
const first = 1
|
||||
const second = 2
|
||||
return first + second
|
||||
}
|
||||
|
||||
export function secondProbe(): number {
|
||||
const first = 1
|
||||
const second = 2
|
||||
return first + second
|
||||
}
|
||||
|
||||
export function hasValue(value: string): boolean {
|
||||
return value !== undefined
|
||||
}
|
||||
|
||||
export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
|
||||
`
|
||||
|
||||
try {
|
||||
await writeFile(path, source)
|
||||
const result = runOxlint([
|
||||
'--config',
|
||||
relative(repositoryRoot, configPath),
|
||||
'--format',
|
||||
'unix',
|
||||
relative(repositoryRoot, path),
|
||||
])
|
||||
const output = normalizedOutput(result)
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.status, output).toBe(1)
|
||||
expect(output).toContain('@stylistic(max-len)')
|
||||
expect(output).toContain('sonarjs(no-identical-functions)')
|
||||
expect(output).toContain('typescript(no-unnecessary-condition)')
|
||||
} finally {
|
||||
await Promise.all([
|
||||
rm(path, { force: true }),
|
||||
rm(configPath, { force: true }),
|
||||
])
|
||||
}
|
||||
}, 20_000)
|
||||
|
||||
it('keeps formatter rules aligned with Oxlint validation', async () => {
|
||||
const oxlintPath = join(repositoryRoot, '.oxlintrc.json')
|
||||
const result = parseConfigFileTextToJson(oxlintPath, await readFile(oxlintPath, 'utf8'))
|
||||
if (result.error !== undefined) {
|
||||
throw new Error(flattenDiagnosticMessageText(result.error.messageText, '\n'))
|
||||
}
|
||||
const parsed = result.config as unknown
|
||||
if (!isRecord(parsed) || !isUnknownArray(parsed.overrides)) {
|
||||
throw new Error('.oxlintrc.json must contain an overrides array')
|
||||
}
|
||||
const stylisticOverride = parsed.overrides.find((value: unknown) =>
|
||||
isRecord(value) && isRecord(value.rules) && '@stylistic/max-len' in value.rules)
|
||||
if (!isRecord(stylisticOverride) || !isRecord(stylisticOverride.rules)) {
|
||||
throw new Error('.oxlintrc.json must contain the @stylistic validator override')
|
||||
}
|
||||
const validatorRules = { ...stylisticOverride.rules }
|
||||
const maxLen = validatorRules['@stylistic/max-len']
|
||||
delete validatorRules['@stylistic/max-len']
|
||||
|
||||
const formatterUrl = pathToFileURL(join(repositoryRoot, 'eslint.format.config.mjs')).href
|
||||
const formatterModule = await import(formatterUrl) as unknown
|
||||
if (!isRecord(formatterModule) || !isUnknownArray(formatterModule.default)) {
|
||||
throw new Error('eslint.format.config.mjs must default-export a config array')
|
||||
}
|
||||
const formatterOverride = formatterModule.default.find((value: unknown) => isRecord(value) && isRecord(value.rules))
|
||||
if (!isRecord(formatterOverride) || !isRecord(formatterOverride.rules)) {
|
||||
throw new Error('eslint.format.config.mjs must contain a rules object')
|
||||
}
|
||||
|
||||
expect(validatorRules).toStrictEqual(formatterOverride.rules)
|
||||
expect(maxLen).toStrictEqual(['error', { code: 140, ignoreUrls: true, ignoreStrings: true, ignoreTemplateLiterals: true }])
|
||||
})
|
||||
|
||||
it('reports an unused suppression', async () => {
|
||||
const suffix = randomUUID()
|
||||
const configPath = await writeContractConfig(suffix)
|
||||
const path = join(repositoryRoot, 'scripts', `oxlint-contract-${suffix}.ts`)
|
||||
|
||||
try {
|
||||
await writeFile(path, '// oxlint-disable-next-line no-console\nexport const value = 1\n')
|
||||
const result = runOxlint([
|
||||
'--config',
|
||||
relative(repositoryRoot, configPath),
|
||||
'--format',
|
||||
'unix',
|
||||
relative(repositoryRoot, path),
|
||||
])
|
||||
const output = normalizedOutput(result)
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.status, output).toBe(0)
|
||||
expect(output).toContain('Unused oxlint-disable directive')
|
||||
} finally {
|
||||
await Promise.all([
|
||||
rm(path, { force: true }),
|
||||
rm(configPath, { force: true }),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
it('accepts an ignored-only staged selection', () => {
|
||||
const result = runOxlint([
|
||||
'--fix',
|
||||
'--no-error-on-unmatched-pattern',
|
||||
'scripts/install-lefthook.mjs',
|
||||
])
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.status, normalizedOutput(result)).toBe(0)
|
||||
})
|
||||
|
||||
it('applies staged stylistic fixes before Oxlint validation', async () => {
|
||||
const suffix = randomUUID()
|
||||
const configPath = await writeContractConfig(suffix)
|
||||
const directory = join(repositoryRoot, 'scripts', `.oxlint-contract-${suffix}`)
|
||||
const path = join(directory, 'fix.ts')
|
||||
|
||||
try {
|
||||
await mkdir(directory, { recursive: true })
|
||||
await writeFile(path, 'const value={answer:1}; \nconsole.log(value)\n')
|
||||
|
||||
const relativePath = relative(repositoryRoot, path)
|
||||
const formatResult = runStagedFormatter([relativePath])
|
||||
const lintResult = runOxlint(['--config', relative(repositoryRoot, configPath), '--fix', relativePath])
|
||||
|
||||
expect(formatResult.error).toBeUndefined()
|
||||
expect(formatResult.status, normalizedOutput(formatResult)).toBe(0)
|
||||
expect(lintResult.error).toBeUndefined()
|
||||
expect(lintResult.status, normalizedOutput(lintResult)).toBe(0)
|
||||
await expect(readFile(path, 'utf8')).resolves.toBe('const value={ answer:1 }\nconsole.log(value)\n')
|
||||
} finally {
|
||||
await Promise.all([
|
||||
rm(directory, { recursive: true, force: true }),
|
||||
rm(configPath, { force: true }),
|
||||
])
|
||||
}
|
||||
}, 20_000)
|
||||
})
|
||||
@@ -42,6 +42,18 @@ function withPnpmEntrypoint<T>(action: () => T): T {
|
||||
}
|
||||
}
|
||||
|
||||
function withEnv<T>(name: string, value: string | undefined, action: () => T): T {
|
||||
const previous = process.env[name]
|
||||
if (value === undefined) Reflect.deleteProperty(process.env, name)
|
||||
else process.env[name] = value
|
||||
try {
|
||||
return action()
|
||||
} finally {
|
||||
if (previous === undefined) Reflect.deleteProperty(process.env, name)
|
||||
else process.env[name] = previous
|
||||
}
|
||||
}
|
||||
|
||||
describe('gate graph validation', () => {
|
||||
it.each([
|
||||
'ci-primary',
|
||||
@@ -96,6 +108,32 @@ describe('gate graph validation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Oxlint gate', () => {
|
||||
it('uses the package script when no worker bound is configured', () => {
|
||||
const subject = withEnv('DSH_OXLINT_THREADS', undefined, () =>
|
||||
withPnpmEntrypoint(() => gatesForMode('ci-lint')[0]))
|
||||
|
||||
expect(subject).toMatchObject({
|
||||
id: 'lint',
|
||||
displayCommand: 'pnpm run lint',
|
||||
command: process.execPath,
|
||||
args: ['/private/pnpm.cjs', 'run', 'lint'],
|
||||
})
|
||||
})
|
||||
|
||||
it('surfaces the configured worker bound on the shared package script', () => {
|
||||
const subject = withEnv('DSH_OXLINT_THREADS', '4', () =>
|
||||
withPnpmEntrypoint(() => gatesForMode('ci-lint')[0]))
|
||||
|
||||
expect(subject).toMatchObject({
|
||||
id: 'lint',
|
||||
displayCommand: 'DSH_OXLINT_THREADS=4 pnpm run lint',
|
||||
command: process.execPath,
|
||||
args: ['/private/pnpm.cjs', 'run', 'lint'],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Node 24 consumer graph', () => {
|
||||
it('owns the seven-command pool and orders restored-artifact consumers', () => {
|
||||
const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers'))
|
||||
|
||||
@@ -181,10 +181,6 @@ function pnpmInvocation(args: string[]): Pick<Gate, 'command' | 'args'> {
|
||||
return { command: process.execPath, args: [entrypoint, ...args] }
|
||||
}
|
||||
|
||||
function nodeOptions(...options: string[]): string {
|
||||
return [process.env.NODE_OPTIONS, ...options].filter(option => option !== undefined && option !== '').join(' ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the complete gate list for a named aggregate.
|
||||
* @param selected - aggregate mode to construct.
|
||||
@@ -380,43 +376,11 @@ function ciWindowsObservationalGates(): Gate[] {
|
||||
]
|
||||
}
|
||||
|
||||
function lintGate(eslintTargets: readonly string[] = ['.']): Gate {
|
||||
const concurrencyArgs = eslintConcurrencyArgs()
|
||||
if (process.env.DSH_ESLINT_CACHE === '1') {
|
||||
return pnpmExec('lint', [
|
||||
'eslint',
|
||||
...eslintTargets,
|
||||
...concurrencyArgs,
|
||||
'--cache',
|
||||
'--cache-location',
|
||||
'.cache/eslint/',
|
||||
'--cache-strategy',
|
||||
'content',
|
||||
], {
|
||||
label: 'lint',
|
||||
env: { NODE_OPTIONS: nodeOptions('--max-old-space-size=8192') },
|
||||
})
|
||||
}
|
||||
if (concurrencyArgs.length > 0) {
|
||||
return pnpmExec('lint', ['eslint', ...eslintTargets, ...concurrencyArgs], {
|
||||
label: 'lint',
|
||||
env: { NODE_OPTIONS: nodeOptions('--max-old-space-size=8192') },
|
||||
})
|
||||
}
|
||||
return pnpmScript('lint', 'lint', {
|
||||
env: { NODE_OPTIONS: nodeOptions('--max-old-space-size=8192') },
|
||||
})
|
||||
}
|
||||
|
||||
function eslintConcurrencyArgs(): string[] {
|
||||
const raw = process.env.DSH_ESLINT_CONCURRENCY
|
||||
if (raw === undefined || raw === '') return []
|
||||
if (raw === 'auto') return ['--concurrency=auto']
|
||||
const parsed = Number.parseInt(raw, 10)
|
||||
if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
|
||||
throw new Error(`run-gates: DSH_ESLINT_CONCURRENCY must be a positive integer or auto, got ${JSON.stringify(raw)}.`)
|
||||
}
|
||||
return [`--concurrency=${raw}`]
|
||||
function lintGate(): Gate {
|
||||
const raw = process.env.DSH_OXLINT_THREADS
|
||||
return pnpmScript('lint', 'lint', raw === undefined || raw === ''
|
||||
? {}
|
||||
: { displayCommand: `DSH_OXLINT_THREADS=${raw} pnpm run lint` })
|
||||
}
|
||||
|
||||
function coverageGate(): Gate {
|
||||
|
||||
28
scripts/run-oxlint.spec.ts
Normal file
28
scripts/run-oxlint.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { resolveOxlintInvocation } from './run-oxlint.ts'
|
||||
|
||||
describe('Oxlint invocation', () => {
|
||||
it('preserves the ordinary default invocation', () => {
|
||||
expect(resolveOxlintInvocation(['.'], { PATH: '/bin' })).toEqual({
|
||||
args: ['.'],
|
||||
env: { PATH: '/bin' },
|
||||
})
|
||||
})
|
||||
|
||||
it('bounds both worker pools from one setting', () => {
|
||||
expect(resolveOxlintInvocation(['.', '--fix'], { DSH_OXLINT_THREADS: '4', GOMAXPROCS: '12' })).toEqual({
|
||||
args: ['.', '--fix', '--threads=4'],
|
||||
env: { DSH_OXLINT_THREADS: '4', GOMAXPROCS: '4' },
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['0', '-1', '1.5', 'auto'])('rejects invalid worker bound %s', (value) => {
|
||||
expect(() => resolveOxlintInvocation(['.'], { DSH_OXLINT_THREADS: value }))
|
||||
.toThrow('DSH_OXLINT_THREADS must be a positive integer')
|
||||
})
|
||||
|
||||
it('rejects a competing direct worker bound', () => {
|
||||
expect(() => resolveOxlintInvocation(['.', '--threads=2'], { DSH_OXLINT_THREADS: '4' }))
|
||||
.toThrow('use DSH_OXLINT_THREADS instead')
|
||||
})
|
||||
})
|
||||
46
scripts/run-oxlint.ts
Normal file
46
scripts/run-oxlint.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const oxlintCli = fileURLToPath(new URL('../node_modules/oxlint/bin/oxlint', import.meta.url))
|
||||
|
||||
/** Complete Oxlint child-process arguments and environment. */
|
||||
export interface OxlintInvocation {
|
||||
readonly args: readonly string[]
|
||||
readonly env: NodeJS.ProcessEnv
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the repository worker bound to both Oxlint backends.
|
||||
* @param args - Oxlint CLI arguments requested by the caller.
|
||||
* @param env - Environment inherited by the Oxlint process.
|
||||
* @returns the complete CLI arguments and child environment.
|
||||
*/
|
||||
export function resolveOxlintInvocation(args: readonly string[], env: NodeJS.ProcessEnv): OxlintInvocation {
|
||||
const raw = env.DSH_OXLINT_THREADS
|
||||
if (raw === undefined || raw === '') return { args: [...args], env: { ...env } }
|
||||
const parsed = Number.parseInt(raw, 10)
|
||||
if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
|
||||
throw new Error(`run-oxlint: DSH_OXLINT_THREADS must be a positive integer, got ${JSON.stringify(raw)}.`)
|
||||
}
|
||||
if (args.some(arg => arg === '--threads' || arg.startsWith('--threads='))) {
|
||||
throw new Error('run-oxlint: use DSH_OXLINT_THREADS instead of passing --threads directly.')
|
||||
}
|
||||
return {
|
||||
args: [...args, `--threads=${raw}`],
|
||||
env: { ...env, GOMAXPROCS: raw },
|
||||
}
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const invocation = resolveOxlintInvocation(process.argv.slice(2), process.env)
|
||||
const result = spawnSync(process.execPath, [oxlintCli, ...invocation.args], {
|
||||
env: invocation.env,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
if (result.error !== undefined) throw result.error
|
||||
process.exitCode = result.status ?? 1
|
||||
}
|
||||
|
||||
const entrypoint = process.argv[1]
|
||||
if (entrypoint !== undefined && resolve(entrypoint) === fileURLToPath(import.meta.url)) main()
|
||||
File diff suppressed because one or more lines are too long
@@ -44,7 +44,7 @@ interface InvariantHost {
|
||||
type PluginFiber = ReturnType<RegistryService['plugin']>
|
||||
|
||||
const hosts = new WeakMap<Context, InvariantHost>()
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method -- every call below supplies its RegistryService receiver explicitly.
|
||||
// oxlint-disable-next-line typescript/unbound-method -- every call below supplies its RegistryService receiver explicitly.
|
||||
const originalPlugin = RegistryService.prototype.plugin
|
||||
|
||||
RegistryService.prototype.plugin = function(plugin: Plugin, config?: unknown, getOuterStack?: () => string[]) {
|
||||
|
||||
@@ -126,7 +126,7 @@ function heritageExemption(
|
||||
returnType = d.type.type
|
||||
} else continue
|
||||
baseParams ??= new Set()
|
||||
// Leading underscores are the deliberately-unused marker (eslint
|
||||
// Leading underscores are the deliberately-unused marker (lint
|
||||
// argsIgnorePattern), not a rename: `_cwd` overriding `cwd` is the
|
||||
// same parameter, so compare underscore-stripped on both sides.
|
||||
for (const p of params) if (ts.isIdentifier(p.name)) baseParams.add(p.name.text.replace(/^_+/, ''))
|
||||
|
||||
Reference in New Issue
Block a user