Skip to content
36 changes: 20 additions & 16 deletions packages/uncheck/src/checks/tsc.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { posix } from 'node:path'
import process from 'node:process'

import { Effect, FileSystem, Option, Path, Predicate } from 'effect'
import { parse as parseJsonc } from 'jsonc-parser'
Expand Down Expand Up @@ -240,7 +239,7 @@ const readReferences = Effect.fn(function* (configPath: string) {

interface InputPattern {
readonly spec: string
readonly regex: RegExp
readonly matches: (file: string) => boolean
}

interface TsconfigInputs {
Expand Down Expand Up @@ -326,10 +325,10 @@ const loadTsconfigInputs = Effect.fn(function* (configPath: string) {
configs: chain.map(({ file }) => file),
files: resolve(files),
include: includeSpecs.flatMap((spec) => {
const regex = compileGlob(spec, 'files')
return regex === undefined ? [] : [{ spec, regex }]
const matches = compileGlob(spec, 'files')
return matches === undefined ? [] : [{ spec, matches }]
}),
exclude: excludeSpecs.map((spec) => ({ spec, regex: compileGlob(spec, 'exclude')! })),
exclude: excludeSpecs.map((spec) => ({ spec, matches: compileGlob(spec, 'exclude')! })),
extensions: new Set(allowJs ? [...TS_EXTENSIONS, ...JS_EXTENSIONS] : TS_EXTENSIONS),
}
})
Expand All @@ -348,13 +347,13 @@ function includesFile(inputs: TsconfigInputs, file: string): boolean {
return false
}

if (inputs.exclude.some((pattern) => pattern.regex.test(target))) {
if (inputs.exclude.some((pattern) => pattern.matches(target))) {
return false
}

// JSON files only come in through an `include` that names the extension explicitly.
return inputs.include.some(
(pattern) => pattern.regex.test(target) && (!json || pattern.spec.endsWith('.json')),
(pattern) => pattern.matches(target) && (!json || pattern.spec.endsWith('.json')),
)
}

Expand Down Expand Up @@ -461,7 +460,6 @@ const IMPLICIT_EXCLUDE = '(?!(?:node_modules|bower_components|jspm_packages)(?:/
const FILES_ASTERISK = '(?:[^./]|(?:\\.(?!min\\.js$))?)*'
const FILES_DOUBLE_ASTERISK = `(?:/${IMPLICIT_EXCLUDE}[^/.][^/]*)*?`
const EXCLUDE_DOUBLE_ASTERISK = '(?:/.+?)?'
const CASE_INSENSITIVE = process.platform === 'win32' || process.platform === 'darwin'

/**
* Turns an absolute `include` or `exclude` pattern into the regular expression `tsc` uses for it:
Expand All @@ -470,7 +468,10 @@ const CASE_INSENSITIVE = process.platform === 'win32' || process.platform === 'd
* In `include`, `*` never matches a name ending in `.min.js`; `exclude` patterns also match every
* path below them.
*/
function compileGlob(pattern: string, usage: 'files' | 'exclude'): RegExp | undefined {
function compileGlob(
pattern: string,
usage: 'files' | 'exclude',
): ((file: string) => boolean) | undefined {
const components = pattern.replace(/\/+$/, '').split('/')
const last = components[components.length - 1]!

Expand Down Expand Up @@ -499,10 +500,15 @@ function compileGlob(pattern: string, usage: 'files' | 'exclude'): RegExp | unde
written = true
}

return new RegExp(
`^${source}${usage === 'exclude' ? '(?:$|/)' : '$'}`,
CASE_INSENSITIVE ? 'i' : '',
)
// tsc ignores case on file systems that do, even in the lookaheads that keep node_modules and .min.js
// out, so `include` matches the way tsc does on either kind and `exclude` keeps case: on any file
// system that selects every project tsc would check, and at worst one more.
const regexes =
usage === 'files'
? [new RegExp(`^${source}$`), new RegExp(`^${source}$`, 'i')]
: [new RegExp(`^${source}(?:$|/)`)]

return (file) => regexes.some((regex) => regex.test(file))
}

function filesComponent(component: string): string {
Expand Down Expand Up @@ -546,9 +552,7 @@ const EXPORT_CONDITIONS = new Set(['node', 'require', 'types', 'default'])
*/
function resolveExports(exports: unknown, subpath: string): string | undefined {
const map: Record<string, unknown> =
Predicate.isObject(exports) &&
!Array.isArray(exports) &&
Object.keys(exports).some((key) => key.startsWith('.'))
Predicate.isObject(exports) && Object.keys(exports).some((key) => key.startsWith('.'))
? exports
: { '.': exports }

Expand Down
9 changes: 4 additions & 5 deletions packages/uncheck/src/commands/hooks/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,10 @@ export const run = Command.make(

if (alreadyContinued) {
if (payload.hook_event_name === 'Stop') {
const summary =
report
.split('\n')
.filter((line) => line.startsWith('✘ '))
.at(-1) ?? ''
const summary = report
.split('\n')
.filter((line) => line.startsWith('✘ '))
.at(-1)!

yield* Console.log(
JSON.stringify({ systemMessage: `uncheck still fails: ${summary.slice(2)}` }),
Expand Down
4 changes: 2 additions & 2 deletions packages/uncheck/src/commands/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export const prepare = Command.make(

const {
prefix,
paths: [hooks = ''],
paths: [hooks],
} = repository.value
const inside = prefix.replace(/\/$/, '')
const exec = yield* detectExec(cwd)
Expand All @@ -161,7 +161,7 @@ export const prepare = Command.make(

// husky 9 and Vite+ point core.hooksPath at a `_` folder of generated shims that source the `h`
// dispatcher, which exits before any line appended to a shim and runs the hook in the folder above.
const configured = path.resolve(cwd, hooks)
const configured = path.resolve(cwd, hooks!)
const dispatched =
path.basename(configured) === '_' &&
(yield* fs.exists(path.join(configured, 'h')).pipe(Effect.orElseSucceed(() => false)))
Expand Down
22 changes: 10 additions & 12 deletions packages/uncheck/src/commands/staged.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,13 @@ export const staged = Command.make(
const [
{
prefix,
paths: [folder = '', indexLock = ''],
paths: [folder, indexLock],
},
unstaged,
] = yield* Effect.all([gitLocation(cwd, ['uncheck-unstaged', 'index.lock']), rawDiff(cwd)], {
concurrency: 'unbounded',
})
const saved = path.resolve(cwd, folder)
const saved = path.resolve(cwd, folder!)
const aside = { cwd, saved, prefix }

// A run that was killed, or could not put them back, left the only copy of unstaged changes.
Expand Down Expand Up @@ -129,7 +129,7 @@ export const staged = Command.make(
const active = process.env.GIT_INDEX_FILE

if (active?.endsWith('.lock') === true) {
const lock = path.resolve(cwd, indexLock)
const lock = path.resolve(cwd, indexLock!)

if (path.resolve(cwd, active) !== lock && (yield* fs.exists(lock))) {
yield* stage({ GIT_INDEX_FILE: lock })
Expand Down Expand Up @@ -218,9 +218,9 @@ const rawDiff = (cwd: string, ...args: ReadonlyArray<string>) =>
const fields = output.split('\0')

return Array.from({ length: Math.floor(fields.length / 2) }, (_, index) => {
const [fromMode = '', toMode = ''] = fields[index * 2]!.slice(1).split(' ')
const [fromMode, toMode] = fields[index * 2]!.slice(1).split(' ')

return { file: fields[index * 2 + 1]!, fromMode, toMode }
return { file: fields[index * 2 + 1]!, fromMode: fromMode!, toMode: toMode! }
})
},
)
Expand Down Expand Up @@ -274,18 +274,16 @@ interface SetAside {
readonly base: string
}

/** Runs `git <args> -- <files>` in batches; with no files, some commands would act on every path. */
/** Runs `git <args> -- <files>` in batches, and nothing without files. */
function gitEach(
cwd: string,
args: ReadonlyArray<string>,
files: ReadonlyArray<string>,
env?: Readonly<Record<string, string>>,
) {
return Effect.forEach(
files.length === 0 ? [] : argvBatches(files),
(batch) => git(cwd, [...args, '--', ...batch], env),
{ discard: true },
)
return Effect.forEach(argvBatches(files), (batch) => git(cwd, [...args, '--', ...batch], env), {
discard: true,
})
}

const setAside = Effect.fn(function* ({ cwd, saved, prefix }: Aside, files: ReadonlyArray<string>) {
Expand Down Expand Up @@ -402,7 +400,7 @@ const merge = Effect.fn(function* (

// git keeps a CRLF blob as it is under text=auto, so merging what hash-object stores and writing it
// back through the filters would turn every line ending of the file to LF.
const [staged = '', stored = ''] = (yield* git(cwd, [
const [staged, stored] = (yield* git(cwd, [
'rev-parse',
`:0:${prefix}${file}`,
`${before}:${prefix}${file}`,
Expand Down
2 changes: 1 addition & 1 deletion packages/uncheck/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ export function userError(userMessage: string): Effect.Effect<never, CliError.Us
}

export function platformMessage(error: PlatformError.PlatformError): string {
return error.cause instanceof Error ? error.cause.message : error.message
return (error.cause as Error).message
Comment thread
dinwwwh marked this conversation as resolved.
}
7 changes: 4 additions & 3 deletions packages/uncheck/src/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,16 @@ export const resolveBin = Effect.fn(function* (pkg: string, cwd: string, binName
})

/** Windows caps a whole command line, node and the tool path included, at 32,767 characters. */
const MAX_ARGV_LENGTH = process.platform === 'win32' ? 30_000 : 65_536
const MAX_ARGV_LENGTH = 30_000

/** No arguments make no batch: a tool or git command run without paths acts on every file. */
export function argvBatches(args: ReadonlyArray<string>): ReadonlyArray<ReadonlyArray<string>> {
const batches: string[][] = [[]]
const batches: string[][] = []
let length = 0

for (const arg of args) {
// Room for the separator and the quotes around an argument with a space.
if (length + arg.length + 3 > MAX_ARGV_LENGTH) {
if (batches.length === 0 || length + arg.length + 3 > MAX_ARGV_LENGTH) {
batches.push([])
length = 0
}
Expand Down
Loading
Loading