@saeeol/core 7.3.1
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/package.json +52 -0
- package/src/cross-spawn-process.ts +273 -0
- package/src/cross-spawn-spawner.ts +505 -0
- package/src/cross-spawn-utils.ts +74 -0
- package/src/effect/logger.ts +73 -0
- package/src/effect/memo-map.ts +3 -0
- package/src/effect/observability.ts +107 -0
- package/src/effect/runtime.ts +21 -0
- package/src/filesystem.ts +262 -0
- package/src/flag/flag.ts +107 -0
- package/src/global.ts +91 -0
- package/src/installation/version.ts +11 -0
- package/src/npm-config.ts +40 -0
- package/src/npm.ts +271 -0
- package/src/saeeol/global.ts +23 -0
- package/src/saeeol/kilocode/global.ts +23 -0
- package/src/saeeol/kilocode/spotlight.ts +23 -0
- package/src/saeeol/spotlight.ts +23 -0
- package/src/util/array.ts +10 -0
- package/src/util/binary.ts +41 -0
- package/src/util/effect-flock.ts +283 -0
- package/src/util/encode.ts +52 -0
- package/src/util/error.ts +60 -0
- package/src/util/flock.ts +358 -0
- package/src/util/glob.ts +34 -0
- package/src/util/hash.ts +7 -0
- package/src/util/identifier.ts +48 -0
- package/src/util/iife.ts +3 -0
- package/src/util/lazy.ts +11 -0
- package/src/util/log.ts +208 -0
- package/src/util/module.ts +10 -0
- package/src/util/path.ts +37 -0
- package/src/util/retry.ts +42 -0
- package/src/util/saeeol-process.ts +24 -0
- package/src/util/slug.ts +74 -0
- package/sst-env.d.ts +10 -0
- package/test/effect/cross-spawn-spawner.test.ts +423 -0
- package/test/effect/observability.test.ts +46 -0
- package/test/filesystem/filesystem.test.ts +338 -0
- package/test/fixture/effect-flock-worker.ts +60 -0
- package/test/fixture/flock-worker.ts +72 -0
- package/test/fixture/tmpdir.ts +13 -0
- package/test/global.test.ts +16 -0
- package/test/lib/effect.ts +53 -0
- package/test/npm-config.test.ts +51 -0
- package/test/npm.test.ts +91 -0
- package/test/saeeol/filesystem-containment.test.ts +13 -0
- package/test/saeeol/kilocode/filesystem-containment.test.ts +13 -0
- package/test/util/effect-flock.test.ts +386 -0
- package/test/util/flock.test.ts +426 -0
- package/tsconfig.json +8 -0
package/src/npm.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
export * as Npm from "./npm"
|
|
2
|
+
|
|
3
|
+
import path from "path"
|
|
4
|
+
import npa from "npm-package-arg"
|
|
5
|
+
import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect"
|
|
6
|
+
import { NodeFileSystem } from "@effect/platform-node"
|
|
7
|
+
import { AppFileSystem } from "./filesystem"
|
|
8
|
+
import { Global } from "./global"
|
|
9
|
+
import { EffectFlock } from "./util/effect-flock"
|
|
10
|
+
import { makeRuntime } from "./effect/runtime"
|
|
11
|
+
import { NpmConfig } from "./npm-config"
|
|
12
|
+
|
|
13
|
+
export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedError>()("NpmInstallFailedError", {
|
|
14
|
+
add: Schema.Array(Schema.String).pipe(Schema.optional),
|
|
15
|
+
dir: Schema.String,
|
|
16
|
+
cause: Schema.optional(Schema.Defect),
|
|
17
|
+
}) {}
|
|
18
|
+
|
|
19
|
+
export interface EntryPoint {
|
|
20
|
+
readonly directory: string
|
|
21
|
+
readonly entrypoint: Option.Option<string>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Interface {
|
|
25
|
+
readonly add: (pkg: string) => Effect.Effect<EntryPoint, InstallFailedError | EffectFlock.LockError>
|
|
26
|
+
readonly install: (
|
|
27
|
+
dir: string,
|
|
28
|
+
input?: {
|
|
29
|
+
add: {
|
|
30
|
+
name: string
|
|
31
|
+
version?: string
|
|
32
|
+
}[]
|
|
33
|
+
},
|
|
34
|
+
) => Effect.Effect<void, EffectFlock.LockError | InstallFailedError>
|
|
35
|
+
readonly which: (pkg: string, bin?: string) => Effect.Effect<Option.Option<string>>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class Service extends Context.Service<Service, Interface>()("@saeeol/Npm") {}
|
|
39
|
+
|
|
40
|
+
const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined
|
|
41
|
+
|
|
42
|
+
export function sanitize(pkg: string) {
|
|
43
|
+
if (!illegal) return pkg
|
|
44
|
+
return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const resolveEntryPoint = (name: string, dir: string): EntryPoint => {
|
|
48
|
+
let entrypoint: Option.Option<string>
|
|
49
|
+
try {
|
|
50
|
+
const resolved = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir)
|
|
51
|
+
entrypoint = Option.some(resolved)
|
|
52
|
+
} catch {
|
|
53
|
+
entrypoint = Option.none()
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
directory: dir,
|
|
57
|
+
entrypoint,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ArboristNode {
|
|
62
|
+
name: string
|
|
63
|
+
path: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ArboristTree {
|
|
67
|
+
edgesOut: Map<string, { to?: ArboristNode }>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const layer = Layer.effect(
|
|
71
|
+
Service,
|
|
72
|
+
Effect.gen(function* () {
|
|
73
|
+
const afs = yield* AppFileSystem.Service
|
|
74
|
+
const global = yield* Global.Service
|
|
75
|
+
const fs = yield* FileSystem.FileSystem
|
|
76
|
+
const flock = yield* EffectFlock.Service
|
|
77
|
+
const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg))
|
|
78
|
+
const reify = (input: { dir: string; add?: string[] }) =>
|
|
79
|
+
Effect.gen(function* () {
|
|
80
|
+
yield* flock.acquire(`npm-install:${input.dir}`)
|
|
81
|
+
const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist"))
|
|
82
|
+
const add = input.add ?? []
|
|
83
|
+
const npmOptions = yield* NpmConfig.load(input.dir)
|
|
84
|
+
const arborist = new Arborist({
|
|
85
|
+
...npmOptions,
|
|
86
|
+
path: input.dir,
|
|
87
|
+
binLinks: true,
|
|
88
|
+
progress: false,
|
|
89
|
+
savePrefix: "",
|
|
90
|
+
ignoreScripts: true,
|
|
91
|
+
})
|
|
92
|
+
return yield* Effect.tryPromise({
|
|
93
|
+
try: () =>
|
|
94
|
+
arborist.reify({
|
|
95
|
+
...npmOptions,
|
|
96
|
+
add,
|
|
97
|
+
save: true,
|
|
98
|
+
saveType: "prod",
|
|
99
|
+
}),
|
|
100
|
+
catch: (cause) =>
|
|
101
|
+
new InstallFailedError({
|
|
102
|
+
cause,
|
|
103
|
+
add,
|
|
104
|
+
dir: input.dir,
|
|
105
|
+
}),
|
|
106
|
+
}) as Effect.Effect<ArboristTree, InstallFailedError>
|
|
107
|
+
}).pipe(
|
|
108
|
+
Effect.withSpan("Npm.reify", {
|
|
109
|
+
attributes: input,
|
|
110
|
+
}),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const add = Effect.fn("Npm.add")(function* (pkg: string) {
|
|
114
|
+
const dir = directory(pkg)
|
|
115
|
+
const name = (() => {
|
|
116
|
+
try {
|
|
117
|
+
return npa(pkg).name ?? pkg
|
|
118
|
+
} catch {
|
|
119
|
+
return pkg
|
|
120
|
+
}
|
|
121
|
+
})()
|
|
122
|
+
|
|
123
|
+
if (yield* afs.existsSafe(path.join(dir, "node_modules", name))) {
|
|
124
|
+
return resolveEntryPoint(name, path.join(dir, "node_modules", name))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const tree = yield* reify({ dir, add: [pkg] })
|
|
128
|
+
const first = tree.edgesOut.values().next().value?.to
|
|
129
|
+
if (!first) {
|
|
130
|
+
const result = resolveEntryPoint(name, path.join(dir, "node_modules", name))
|
|
131
|
+
if (Option.isSome(result.entrypoint)) return result
|
|
132
|
+
return yield* new InstallFailedError({ add: [pkg], dir })
|
|
133
|
+
}
|
|
134
|
+
return resolveEntryPoint(first.name, first.path)
|
|
135
|
+
}, Effect.scoped)
|
|
136
|
+
|
|
137
|
+
const install: Interface["install"] = Effect.fn("Npm.install")(function* (dir, input) {
|
|
138
|
+
const canWrite = yield* afs.access(dir, { writable: true }).pipe(
|
|
139
|
+
Effect.as(true),
|
|
140
|
+
Effect.orElseSucceed(() => false),
|
|
141
|
+
)
|
|
142
|
+
if (!canWrite) return
|
|
143
|
+
|
|
144
|
+
const add = input?.add.map((pkg) => [pkg.name, pkg.version].filter(Boolean).join("@")) ?? []
|
|
145
|
+
if (
|
|
146
|
+
yield* Effect.gen(function* () {
|
|
147
|
+
const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules"))
|
|
148
|
+
if (!nodeModulesExists) {
|
|
149
|
+
yield* reify({ add, dir })
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
152
|
+
return false
|
|
153
|
+
}).pipe(Effect.withSpan("Npm.checkNodeModules"))
|
|
154
|
+
)
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
yield* Effect.gen(function* () {
|
|
158
|
+
const pkg = yield* afs.readJson(path.join(dir, "package.json")).pipe(Effect.orElseSucceed(() => ({})))
|
|
159
|
+
const lock = yield* afs.readJson(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => ({})))
|
|
160
|
+
|
|
161
|
+
const pkgAny = pkg as any
|
|
162
|
+
const lockAny = lock as any
|
|
163
|
+
const declared = new Set([
|
|
164
|
+
...Object.keys(pkgAny?.dependencies || {}),
|
|
165
|
+
...Object.keys(pkgAny?.devDependencies || {}),
|
|
166
|
+
...Object.keys(pkgAny?.peerDependencies || {}),
|
|
167
|
+
...Object.keys(pkgAny?.optionalDependencies || {}),
|
|
168
|
+
...(input?.add || []).map((pkg) => pkg.name),
|
|
169
|
+
])
|
|
170
|
+
|
|
171
|
+
const root = lockAny?.packages?.[""] || {}
|
|
172
|
+
const locked = new Set([
|
|
173
|
+
...Object.keys(root?.dependencies || {}),
|
|
174
|
+
...Object.keys(root?.devDependencies || {}),
|
|
175
|
+
...Object.keys(root?.peerDependencies || {}),
|
|
176
|
+
...Object.keys(root?.optionalDependencies || {}),
|
|
177
|
+
])
|
|
178
|
+
|
|
179
|
+
for (const name of declared) {
|
|
180
|
+
if (!locked.has(name)) {
|
|
181
|
+
yield* reify({ dir, add })
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}).pipe(Effect.withSpan("Npm.checkDirty"))
|
|
186
|
+
|
|
187
|
+
return
|
|
188
|
+
}, Effect.scoped)
|
|
189
|
+
|
|
190
|
+
const which = Effect.fn("Npm.which")(function* (pkg: string, bin?: string) {
|
|
191
|
+
const dir = directory(pkg)
|
|
192
|
+
const binDir = path.join(dir, "node_modules", ".bin")
|
|
193
|
+
|
|
194
|
+
const pick = Effect.fnUntraced(function* () {
|
|
195
|
+
const files = yield* fs.readDirectory(binDir).pipe(Effect.catch(() => Effect.succeed([] as string[])))
|
|
196
|
+
|
|
197
|
+
if (files.length === 0) return Option.none<string>()
|
|
198
|
+
// Caller picked a specific bin (e.g. pyright exposes both `pyright` and
|
|
199
|
+
// `pyright-langserver`); trust the hint if the package provides it.
|
|
200
|
+
if (bin) return files.includes(bin) ? Option.some(bin) : Option.none<string>()
|
|
201
|
+
if (files.length === 1) return Option.some(files[0])
|
|
202
|
+
|
|
203
|
+
const pkgJson = yield* afs.readJson(path.join(dir, "node_modules", pkg, "package.json")).pipe(Effect.option)
|
|
204
|
+
|
|
205
|
+
if (Option.isSome(pkgJson)) {
|
|
206
|
+
const parsed = pkgJson.value as { bin?: string | Record<string, string> }
|
|
207
|
+
if (parsed?.bin) {
|
|
208
|
+
const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg
|
|
209
|
+
const parsedBin = parsed.bin
|
|
210
|
+
if (typeof parsedBin === "string") return Option.some(unscoped)
|
|
211
|
+
const keys = Object.keys(parsedBin)
|
|
212
|
+
if (keys.length === 1) return Option.some(keys[0])
|
|
213
|
+
return parsedBin[unscoped] ? Option.some(unscoped) : Option.some(keys[0])
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return Option.some(files[0])
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
return yield* Effect.gen(function* () {
|
|
221
|
+
const bin = yield* pick()
|
|
222
|
+
if (Option.isSome(bin)) {
|
|
223
|
+
return Option.some(path.join(binDir, bin.value))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {}))
|
|
227
|
+
|
|
228
|
+
yield* add(pkg)
|
|
229
|
+
|
|
230
|
+
const resolved = yield* pick()
|
|
231
|
+
if (Option.isNone(resolved)) return Option.none<string>()
|
|
232
|
+
return Option.some(path.join(binDir, resolved.value))
|
|
233
|
+
}).pipe(
|
|
234
|
+
Effect.scoped,
|
|
235
|
+
Effect.orElseSucceed(() => Option.none<string>()),
|
|
236
|
+
)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
return Service.of({
|
|
240
|
+
add,
|
|
241
|
+
install,
|
|
242
|
+
which,
|
|
243
|
+
})
|
|
244
|
+
}),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
export const defaultLayer = layer.pipe(
|
|
248
|
+
Layer.provide(EffectFlock.layer),
|
|
249
|
+
Layer.provide(AppFileSystem.layer),
|
|
250
|
+
Layer.provide(Global.layer),
|
|
251
|
+
Layer.provide(NodeFileSystem.layer),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
const { runPromise } = makeRuntime(Service, defaultLayer)
|
|
255
|
+
|
|
256
|
+
export async function install(...args: Parameters<Interface["install"]>) {
|
|
257
|
+
return runPromise((svc) => svc.install(...args))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function add(...args: Parameters<Interface["add"]>) {
|
|
261
|
+
const entry = await runPromise((svc) => svc.add(...args))
|
|
262
|
+
return {
|
|
263
|
+
directory: entry.directory,
|
|
264
|
+
entrypoint: Option.getOrUndefined(entry.entrypoint),
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function which(...args: Parameters<Interface["which"]>) {
|
|
269
|
+
const resolved = await runPromise((svc) => svc.which(...args))
|
|
270
|
+
return Option.getOrUndefined(resolved)
|
|
271
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from "fs/promises"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Like `fs.mkdir({ recursive: true })` but also repairs broken symlinks and
|
|
5
|
+
* junctions whose target no longer exists (a Windows edge-case where the user
|
|
6
|
+
* had a junction at e.g. `~/.saeeol` pointing to a deleted directory).
|
|
7
|
+
*
|
|
8
|
+
* `fs.mkdir({ recursive: true })` silently no-ops when a junction exists even
|
|
9
|
+
* if its target is gone, so subsequent writes inside that path fail with ENOENT.
|
|
10
|
+
* We detect this by calling `fs.stat` (which follows the symlink/junction) after
|
|
11
|
+
* mkdir: if stat fails the entry is broken and we remove + recreate it.
|
|
12
|
+
*/
|
|
13
|
+
export async function ensureRealDir(p: string) {
|
|
14
|
+
await fs.mkdir(p, { recursive: true })
|
|
15
|
+
const ok = await fs
|
|
16
|
+
.stat(p)
|
|
17
|
+
.then(() => true)
|
|
18
|
+
.catch(() => false)
|
|
19
|
+
if (!ok) {
|
|
20
|
+
await fs.rm(p, { force: true })
|
|
21
|
+
await fs.mkdir(p, { recursive: true })
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from "fs/promises"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Like `fs.mkdir({ recursive: true })` but also repairs broken symlinks and
|
|
5
|
+
* junctions whose target no longer exists (a Windows edge-case where the user
|
|
6
|
+
* had a junction at e.g. `~/.saeeol` pointing to a deleted directory).
|
|
7
|
+
*
|
|
8
|
+
* `fs.mkdir({ recursive: true })` silently no-ops when a junction exists even
|
|
9
|
+
* if its target is gone, so subsequent writes inside that path fail with ENOENT.
|
|
10
|
+
* We detect this by calling `fs.stat` (which follows the symlink/junction) after
|
|
11
|
+
* mkdir: if stat fails the entry is broken and we remove + recreate it.
|
|
12
|
+
*/
|
|
13
|
+
export async function ensureRealDir(p: string) {
|
|
14
|
+
await fs.mkdir(p, { recursive: true })
|
|
15
|
+
const ok = await fs
|
|
16
|
+
.stat(p)
|
|
17
|
+
.then(() => true)
|
|
18
|
+
.catch(() => false)
|
|
19
|
+
if (!ok) {
|
|
20
|
+
await fs.rm(p, { force: true })
|
|
21
|
+
await fs.mkdir(p, { recursive: true })
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from "fs/promises"
|
|
2
|
+
import path from "path"
|
|
3
|
+
|
|
4
|
+
const marker = ".metadata_never_index"
|
|
5
|
+
|
|
6
|
+
function exists(err: unknown): boolean {
|
|
7
|
+
if (typeof err !== "object" || err === null) return false
|
|
8
|
+
return "code" in err && err.code === "EEXIST"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function message(err: unknown): string {
|
|
12
|
+
if (err instanceof Error) return err.message
|
|
13
|
+
return String(err)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function markNoIndex(dir: string): Promise<void> {
|
|
17
|
+
if (process.platform !== "darwin") return
|
|
18
|
+
const file = path.join(dir, marker)
|
|
19
|
+
await fs.writeFile(file, "", { flag: "wx" }).catch((err) => {
|
|
20
|
+
if (exists(err)) return
|
|
21
|
+
process.emitWarning(`Failed to mark ${dir} as Spotlight-excluded: ${message(err)}`)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from "fs/promises"
|
|
2
|
+
import path from "path"
|
|
3
|
+
|
|
4
|
+
const marker = ".metadata_never_index"
|
|
5
|
+
|
|
6
|
+
function exists(err: unknown): boolean {
|
|
7
|
+
if (typeof err !== "object" || err === null) return false
|
|
8
|
+
return "code" in err && err.code === "EEXIST"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function message(err: unknown): string {
|
|
12
|
+
if (err instanceof Error) return err.message
|
|
13
|
+
return String(err)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function markNoIndex(dir: string): Promise<void> {
|
|
17
|
+
if (process.platform !== "darwin") return
|
|
18
|
+
const file = path.join(dir, marker)
|
|
19
|
+
await fs.writeFile(file, "", { flag: "wx" }).catch((err) => {
|
|
20
|
+
if (exists(err)) return
|
|
21
|
+
process.emitWarning(`Failed to mark ${dir} as Spotlight-excluded: ${message(err)}`)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function findLast<T>(
|
|
2
|
+
items: readonly T[],
|
|
3
|
+
predicate: (item: T, index: number, items: readonly T[]) => boolean,
|
|
4
|
+
): T | undefined {
|
|
5
|
+
for (let i = items.length - 1; i >= 0; i -= 1) {
|
|
6
|
+
const item = items[i]
|
|
7
|
+
if (predicate(item, i, items)) return item
|
|
8
|
+
}
|
|
9
|
+
return undefined
|
|
10
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export namespace Binary {
|
|
2
|
+
export function search<T>(array: T[], id: string, compare: (item: T) => string): { found: boolean; index: number } {
|
|
3
|
+
let left = 0
|
|
4
|
+
let right = array.length - 1
|
|
5
|
+
|
|
6
|
+
while (left <= right) {
|
|
7
|
+
const mid = Math.floor((left + right) / 2)
|
|
8
|
+
const midId = compare(array[mid])
|
|
9
|
+
|
|
10
|
+
if (midId === id) {
|
|
11
|
+
return { found: true, index: mid }
|
|
12
|
+
} else if (midId < id) {
|
|
13
|
+
left = mid + 1
|
|
14
|
+
} else {
|
|
15
|
+
right = mid - 1
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return { found: false, index: left }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function insert<T>(array: T[], item: T, compare: (item: T) => string): T[] {
|
|
23
|
+
const id = compare(item)
|
|
24
|
+
let left = 0
|
|
25
|
+
let right = array.length
|
|
26
|
+
|
|
27
|
+
while (left < right) {
|
|
28
|
+
const mid = Math.floor((left + right) / 2)
|
|
29
|
+
const midId = compare(array[mid])
|
|
30
|
+
|
|
31
|
+
if (midId < id) {
|
|
32
|
+
left = mid + 1
|
|
33
|
+
} else {
|
|
34
|
+
right = mid
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
array.splice(left, 0, item)
|
|
39
|
+
return array
|
|
40
|
+
}
|
|
41
|
+
}
|