Compare commits

...

131 Commits

Author SHA1 Message Date
renovate[bot]
95543b1f9d chore(deps): update dependency oxlint-tsgolint to ^0.5.0 (#875)
[skip ci]

This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [oxlint-tsgolint](https://redirect.github.com/oxc-project/tsgolint) |
[`^0.3.0` ->
`^0.5.0`](https://renovatebot.com/diffs/npm/oxlint-tsgolint/0.3.0/0.5.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/oxlint-tsgolint/0.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxlint-tsgolint/0.3.0/0.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>oxc-project/tsgolint (oxlint-tsgolint)</summary>

###
[`v0.5.0`](https://redirect.github.com/oxc-project/tsgolint/releases/tag/v0.5.0)

[Compare
Source](https://redirect.github.com/oxc-project/tsgolint/compare/v0.4.0...v0.5.0)

#### What's Changed

- chore(deps): update typescript-go digest to
[`b278afd`](https://redirect.github.com/oxc-project/tsgolint/commit/b278afd)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;344](https://redirect.github.com/oxc-project/tsgolint/pull/344)
- perf: `no-unnecessary-boolean-literal-compare`: only build fixes when
needed by [@&#8203;camchenry](https://redirect.github.com/camchenry) in
[#&#8203;332](https://redirect.github.com/oxc-project/tsgolint/pull/332)
- perf: `no-unnecessary-type-arguments`: only compute remove range when
creating fixes by
[@&#8203;camchenry](https://redirect.github.com/camchenry) in
[#&#8203;333](https://redirect.github.com/oxc-project/tsgolint/pull/333)
- chore(deps): update typescript-go digest to
[`24b38de`](https://redirect.github.com/oxc-project/tsgolint/commit/24b38de)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;345](https://redirect.github.com/oxc-project/tsgolint/pull/345)
- chore(deps): update typescript-go digest to
[`d891e4f`](https://redirect.github.com/oxc-project/tsgolint/commit/d891e4f)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;348](https://redirect.github.com/oxc-project/tsgolint/pull/348)
- chore(deps): update typescript-go digest to
[`8ac3092`](https://redirect.github.com/oxc-project/tsgolint/commit/8ac3092)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;350](https://redirect.github.com/oxc-project/tsgolint/pull/350)
- chore(deps): update typescript-go digest to
[`4705d38`](https://redirect.github.com/oxc-project/tsgolint/commit/4705d38)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;352](https://redirect.github.com/oxc-project/tsgolint/pull/352)
- chore(deps): update crate-ci/typos action to v1.39.0 by
[@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;354](https://redirect.github.com/oxc-project/tsgolint/pull/354)
- chore(deps): update typescript-go digest to
[`71a622f`](https://redirect.github.com/oxc-project/tsgolint/commit/71a622f)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;353](https://redirect.github.com/oxc-project/tsgolint/pull/353)
- chore(deps): update typescript-go digest to
[`240b101`](https://redirect.github.com/oxc-project/tsgolint/commit/240b101)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;355](https://redirect.github.com/oxc-project/tsgolint/pull/355)
- chore(deps): update typescript-go digest to
[`6fb55b7`](https://redirect.github.com/oxc-project/tsgolint/commit/6fb55b7)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;357](https://redirect.github.com/oxc-project/tsgolint/pull/357)
- chore(deps): update dependency dprint-typescript to v0.95.12 by
[@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;359](https://redirect.github.com/oxc-project/tsgolint/pull/359)
- chore(deps): update taiki-e/install-action action to v2.62.45 by
[@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;360](https://redirect.github.com/oxc-project/tsgolint/pull/360)
- chore(deps): update github.com/go-json-experiment/json digest to
[`4849db3`](https://redirect.github.com/oxc-project/tsgolint/commit/4849db3)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;358](https://redirect.github.com/oxc-project/tsgolint/pull/358)
- chore(deps): lock file maintenance npm packages by
[@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;362](https://redirect.github.com/oxc-project/tsgolint/pull/362)
- chore(deps): update typescript-go digest to
[`82039b6`](https://redirect.github.com/oxc-project/tsgolint/commit/82039b6)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;361](https://redirect.github.com/oxc-project/tsgolint/pull/361)
- chore(dpes): update golang to ^1.25.0 by
[@&#8203;sunrabbit123](https://redirect.github.com/sunrabbit123) in
[#&#8203;363](https://redirect.github.com/oxc-project/tsgolint/pull/363)
- docs: mark `strict-boolean-expressions` as implemented by
[@&#8203;camc314](https://redirect.github.com/camc314) in
[#&#8203;364](https://redirect.github.com/oxc-project/tsgolint/pull/364)
- feat: improve invalid config diagnostics by
[@&#8203;camc314](https://redirect.github.com/camc314) in
[#&#8203;365](https://redirect.github.com/oxc-project/tsgolint/pull/365)
- feat: link to github issue for deprecated tsconfig options by
[@&#8203;camc314](https://redirect.github.com/camc314) in
[#&#8203;366](https://redirect.github.com/oxc-project/tsgolint/pull/366)
- perf: `no-unnecessary-type-assertion`: only do scanning when fixes are
generated by [@&#8203;camchenry](https://redirect.github.com/camchenry)
in
[#&#8203;334](https://redirect.github.com/oxc-project/tsgolint/pull/334)
- chore(deps): update typescript-go digest to
[`13d3e19`](https://redirect.github.com/oxc-project/tsgolint/commit/13d3e19)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;367](https://redirect.github.com/oxc-project/tsgolint/pull/367)
- fix(justfile): fix `just shim` command by
[@&#8203;camc314](https://redirect.github.com/camc314) in
[#&#8203;369](https://redirect.github.com/oxc-project/tsgolint/pull/369)
- feat: implement `typescript/no-deprecated` by
[@&#8203;camc314](https://redirect.github.com/camc314) in
[#&#8203;368](https://redirect.github.com/oxc-project/tsgolint/pull/368)

#### New Contributors

- [@&#8203;sunrabbit123](https://redirect.github.com/sunrabbit123) made
their first contribution in
[#&#8203;363](https://redirect.github.com/oxc-project/tsgolint/pull/363)

**Full Changelog**:
<https://github.com/oxc-project/tsgolint/compare/v0.4.0...v0.5.0>

###
[`v0.4.0`](https://redirect.github.com/oxc-project/tsgolint/releases/tag/v0.4.0)

[Compare
Source](https://redirect.github.com/oxc-project/tsgolint/compare/v0.3.0...v0.4.0)

#### What's Changed

- chore(deps): update typescript-go digest to
[`def283d`](https://redirect.github.com/oxc-project/tsgolint/commit/def283d)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;315](https://redirect.github.com/oxc-project/tsgolint/pull/315)
- chore(deps): update typescript-go digest to
[`d461fad`](https://redirect.github.com/oxc-project/tsgolint/commit/d461fad)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;323](https://redirect.github.com/oxc-project/tsgolint/pull/323)
- chore(deps): update taiki-e/install-action action to v2.62.38 by
[@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;327](https://redirect.github.com/oxc-project/tsgolint/pull/327)
- chore(deps): update github.com/go-json-experiment/json digest to
[`8a0206d`](https://redirect.github.com/oxc-project/tsgolint/commit/8a0206d)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;325](https://redirect.github.com/oxc-project/tsgolint/pull/325)
- chore(deps): update github-actions (major) by
[@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;328](https://redirect.github.com/oxc-project/tsgolint/pull/328)
- chore(deps): lock file maintenance npm packages by
[@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;329](https://redirect.github.com/oxc-project/tsgolint/pull/329)
- chore(deps): update typescript-go digest to
[`b7840c2`](https://redirect.github.com/oxc-project/tsgolint/commit/b7840c2)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;326](https://redirect.github.com/oxc-project/tsgolint/pull/326)
- chore(deps): update typescript-go digest to
[`33eeaf3`](https://redirect.github.com/oxc-project/tsgolint/commit/33eeaf3)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;330](https://redirect.github.com/oxc-project/tsgolint/pull/330)
- chore(deps): update typescript-go digest to
[`f0ca632`](https://redirect.github.com/oxc-project/tsgolint/commit/f0ca632)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;331](https://redirect.github.com/oxc-project/tsgolint/pull/331)
- chore(deps): update typescript-go digest to
[`a4fa408`](https://redirect.github.com/oxc-project/tsgolint/commit/a4fa408)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;337](https://redirect.github.com/oxc-project/tsgolint/pull/337)
- chore(deps): update typescript-go digest to
[`ca68e0b`](https://redirect.github.com/oxc-project/tsgolint/commit/ca68e0b)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;339](https://redirect.github.com/oxc-project/tsgolint/pull/339)
- chore(deps): update typescript-go digest to
[`4037f3a`](https://redirect.github.com/oxc-project/tsgolint/commit/4037f3a)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;340](https://redirect.github.com/oxc-project/tsgolint/pull/340)
- refactor: allow suggestions/fixes to be generated lazily by
[@&#8203;camchenry](https://redirect.github.com/camchenry) in
[#&#8203;313](https://redirect.github.com/oxc-project/tsgolint/pull/313)
- chore(deps): update typescript-go digest to
[`6642b0a`](https://redirect.github.com/oxc-project/tsgolint/commit/6642b0a)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;342](https://redirect.github.com/oxc-project/tsgolint/pull/342)
- feat(overlayfs): implement overlay filesystem with source overrides
support by [@&#8203;camc314](https://redirect.github.com/camc314) in
[#&#8203;291](https://redirect.github.com/oxc-project/tsgolint/pull/291)
- feat!: make fixes/suggestions opt-in by
[@&#8203;camchenry](https://redirect.github.com/camchenry) in
[#&#8203;317](https://redirect.github.com/oxc-project/tsgolint/pull/317)
- perf: `no-array-delete`: only get token ranges when fixing by
[@&#8203;camchenry](https://redirect.github.com/camchenry) in
[#&#8203;320](https://redirect.github.com/oxc-project/tsgolint/pull/320)
- perf: `no-confusing-void-expression`: only generate fixes and
suggestions when fixing by
[@&#8203;camchenry](https://redirect.github.com/camchenry) in
[#&#8203;321](https://redirect.github.com/oxc-project/tsgolint/pull/321)
- perf: `no-duplicate-type-constituents`: only build fixes when needed
by [@&#8203;camchenry](https://redirect.github.com/camchenry) in
[#&#8203;324](https://redirect.github.com/oxc-project/tsgolint/pull/324)
- feat: expose program diagnostics by
[@&#8203;camc314](https://redirect.github.com/camc314) in
[#&#8203;343](https://redirect.github.com/oxc-project/tsgolint/pull/343)

**Full Changelog**:
<https://github.com/oxc-project/tsgolint/compare/v0.3.0...v0.4.0>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/cnpm/cnpmcore).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTkuNCIsInVwZGF0ZWRJblZlciI6IjQxLjE1OS40IiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 21:17:43 +08:00
时瑾
d7de1cded8 feat: add ffmpeg-builds binary mirror (#879)
[skip ci]

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* FFmpeg builds are now available as a supported binary source, offering
static builds for Windows and Linux platforms (x86_64). Both master
branch and latest release versions are accessible.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-06 21:12:56 +08:00
renovate[bot]
0f11e7730a fix(deps): update dependency ssri to v13 (#870)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [ssri](https://redirect.github.com/npm/ssri) | [`^8.0.1` ->
`^13.0.0`](https://renovatebot.com/diffs/npm/ssri/8.0.1/13.0.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/ssri/13.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/ssri/8.0.1/13.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>npm/ssri (ssri)</summary>

###
[`v13.0.0`](https://redirect.github.com/npm/ssri/blob/HEAD/CHANGELOG.md#1300-2025-10-22)

[Compare
Source](https://redirect.github.com/npm/ssri/compare/v12.0.0...v13.0.0)

##### ⚠️ BREAKING CHANGES

- `ssri` now supports node `^20.17.0 || >=22.9.0`

##### Bug Fixes

-
[`46a2520`](46a2520214)
[#&#8203;155](https://redirect.github.com/npm/ssri/pull/155) align to
npm 11 node engine range
([#&#8203;155](https://redirect.github.com/npm/ssri/issues/155))
([@&#8203;owlstronaut](https://redirect.github.com/owlstronaut))
-
[`8f0bbf2`](8f0bbf2717)
[#&#8203;151](https://redirect.github.com/npm/ssri/pull/151) improve
`SRI_REGEX`
([#&#8203;151](https://redirect.github.com/npm/ssri/issues/151))
([@&#8203;ericcornelissen](https://redirect.github.com/ericcornelissen))

##### Chores

-
[`79e0018`](79e0018941)
[#&#8203;146](https://redirect.github.com/npm/ssri/pull/146) postinstall
workflow updates
([#&#8203;146](https://redirect.github.com/npm/ssri/issues/146))
([@&#8203;owlstronaut](https://redirect.github.com/owlstronaut))
-
[`89b775a`](89b775a9cf)
[#&#8203;154](https://redirect.github.com/npm/ssri/pull/154) bump
[@&#8203;npmcli/template-oss](https://redirect.github.com/npmcli/template-oss)
from 4.26.0 to 4.27.1
([#&#8203;154](https://redirect.github.com/npm/ssri/issues/154))
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot],
[@&#8203;npm-cli-bot](https://redirect.github.com/npm-cli-bot))

###
[`v12.0.0`](https://redirect.github.com/npm/ssri/blob/HEAD/CHANGELOG.md#1200-2024-09-24)

[Compare
Source](https://redirect.github.com/npm/ssri/compare/v11.0.0...v12.0.0)

##### ⚠️ BREAKING CHANGES

- `ssri` now supports node `^18.17.0 || >=20.5.0`

##### Bug Fixes

-
[`b7a3f9a`](b7a3f9ad35)
[#&#8203;141](https://redirect.github.com/npm/ssri/pull/141) align to
npm 10 node engine range
([@&#8203;hashtagchris](https://redirect.github.com/hashtagchris))

##### Chores

-
[`f8121e9`](f8121e9e5e)
[#&#8203;141](https://redirect.github.com/npm/ssri/pull/141) run
template-oss-apply
([@&#8203;hashtagchris](https://redirect.github.com/hashtagchris))

###
[`v11.0.0`](https://redirect.github.com/npm/ssri/blob/HEAD/CHANGELOG.md#1100-2024-09-03)

[Compare
Source](https://redirect.github.com/npm/ssri/compare/v10.0.6...v11.0.0)

##### ⚠️ BREAKING CHANGES

- `ssri` is now compatible with the following semver range for node:
`^16.14.0 || >=18.0.0`

##### Bug Fixes

-
[`29a6e2c`](29a6e2c2e0)
Address breaking engine change in dependency
([@&#8203;hashtagchris](https://redirect.github.com/hashtagchris))

##### Chores

-
[`db4219f`](db4219f28a)
bump
[@&#8203;npmcli/eslint-config](https://redirect.github.com/npmcli/eslint-config)
from 4.0.5 to 5.0.0
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot])
-
[`f2dd012`](f2dd0125e4)
template-oss-apply
([@&#8203;hashtagchris](https://redirect.github.com/hashtagchris))
-
[`f2a2a9d`](f2a2a9d09f)
postinstall for dependabot template-oss PR
([@&#8203;hashtagchris](https://redirect.github.com/hashtagchris))
-
[`4508f71`](4508f71d8a)
bump
[@&#8203;npmcli/template-oss](https://redirect.github.com/npmcli/template-oss)
from 4.22.0 to 4.23.3
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot])

###
[`v10.0.6`](https://redirect.github.com/npm/ssri/blob/HEAD/CHANGELOG.md#1006-2024-05-04)

[Compare
Source](https://redirect.github.com/npm/ssri/compare/v10.0.5...v10.0.6)

##### Bug Fixes

-
[`f3773e2`](f3773e21f9)
[#&#8203;127](https://redirect.github.com/npm/ssri/pull/127) linting:
no-unused-vars
([@&#8203;lukekarrys](https://redirect.github.com/lukekarrys))

##### Chores

-
[`55e7dfb`](55e7dfbe3f)
[#&#8203;127](https://redirect.github.com/npm/ssri/pull/127) bump
[@&#8203;npmcli/template-oss](https://redirect.github.com/npmcli/template-oss)
to 4.22.0 ([@&#8203;lukekarrys](https://redirect.github.com/lukekarrys))
-
[`96d1795`](96d1795b93)
[#&#8203;127](https://redirect.github.com/npm/ssri/pull/127) postinstall
for dependabot template-oss PR
([@&#8203;lukekarrys](https://redirect.github.com/lukekarrys))
-
[`10d5e8a`](10d5e8ae49)
[#&#8203;125](https://redirect.github.com/npm/ssri/pull/125) bump
[@&#8203;npmcli/template-oss](https://redirect.github.com/npmcli/template-oss)
from 4.21.3 to 4.21.4
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot])

###
[`v10.0.5`](https://redirect.github.com/npm/ssri/blob/HEAD/CHANGELOG.md#1005-2023-08-14)

[Compare
Source](https://redirect.github.com/npm/ssri/compare/v10.0.4...v10.0.5)

##### Dependencies

-
[`00dacfd`](00dacfd5e9)
[#&#8203;94](https://redirect.github.com/npm/ssri/pull/94) bump minipass
from 5.0.0 to 7.0.3

###
[`v10.0.4`](https://redirect.github.com/npm/ssri/blob/HEAD/CHANGELOG.md#1004-2023-04-26)

[Compare
Source](https://redirect.github.com/npm/ssri/compare/v10.0.3...v10.0.4)

##### Dependencies

-
[`152e2bc`](152e2bce43)
[#&#8203;78](https://redirect.github.com/npm/ssri/pull/78) bump minipass
from 4.2.7 to 5.0.0
([#&#8203;78](https://redirect.github.com/npm/ssri/issues/78))

###
[`v10.0.3`](https://redirect.github.com/npm/ssri/blob/HEAD/CHANGELOG.md#1003-2023-04-11)

[Compare
Source](https://redirect.github.com/npm/ssri/compare/v10.0.2...v10.0.3)

##### Bug Fixes

-
[`7fef846`](7fef8463d4)
[#&#8203;79](https://redirect.github.com/npm/ssri/pull/79) optimize
adding this.algorithm to algorithms list
([@&#8203;wraithgar](https://redirect.github.com/wraithgar))
-
[`d90f674`](d90f674cd5)
[#&#8203;79](https://redirect.github.com/npm/ssri/pull/79) prevent
DEFAULT\_ALGORITHM mutation
([@&#8203;wraithgar](https://redirect.github.com/wraithgar))
-
[`4e94d15`](4e94d15dd9)
[#&#8203;79](https://redirect.github.com/npm/ssri/pull/79)
Integrity#match prioritizes overlapping hashes
([@&#8203;wraithgar](https://redirect.github.com/wraithgar))
-
[`dce3dab`](dce3dab846)
[#&#8203;79](https://redirect.github.com/npm/ssri/pull/79) faster stream
verification ([@&#8203;H4ad](https://redirect.github.com/H4ad))

###
[`v10.0.2`](https://redirect.github.com/npm/ssri/blob/HEAD/CHANGELOG.md#1002-2023-04-03)

[Compare
Source](https://redirect.github.com/npm/ssri/compare/v10.0.1...v10.0.2)

##### Bug Fixes

-
[`8e80eca`](8e80eca497)
[#&#8203;74](https://redirect.github.com/npm/ssri/pull/74) move from
symbols to private methods
([#&#8203;74](https://redirect.github.com/npm/ssri/issues/74))
([@&#8203;wraithgar](https://redirect.github.com/wraithgar))
-
[`a316b12`](a316b12e0c)
[#&#8203;75](https://redirect.github.com/npm/ssri/pull/75) faster
toString for integrity
([#&#8203;75](https://redirect.github.com/npm/ssri/issues/75))
([@&#8203;H4ad](https://redirect.github.com/H4ad))
-
[`6e6877d`](6e6877d55c)
[#&#8203;72](https://redirect.github.com/npm/ssri/pull/72) remove spread
of defaultOpts
([#&#8203;72](https://redirect.github.com/npm/ssri/issues/72))
([@&#8203;H4ad](https://redirect.github.com/H4ad))

###
[`v10.0.1`](https://redirect.github.com/npm/ssri/blob/HEAD/CHANGELOG.md#1001-2022-12-07)

[Compare
Source](https://redirect.github.com/npm/ssri/compare/v10.0.0...v10.0.1)

##### Dependencies

-
[`4f6ba1e`](4f6ba1e5cc)
[#&#8203;64](https://redirect.github.com/npm/ssri/pull/64) bump minipass
from 3.3.6 to 4.0.0

###
[`v10.0.0`](https://redirect.github.com/npm/ssri/blob/HEAD/CHANGELOG.md#1000-2022-10-10)

[Compare
Source](https://redirect.github.com/npm/ssri/compare/v9.0.1...v10.0.0)

##### ⚠️ BREAKING CHANGES

- `ssri` is now compatible with the following semver range for node:
`^14.17.0 || ^16.13.0 || >=18.0.0`

##### Features

-
[`3de0c45`](3de0c4502d)
[#&#8203;52](https://redirect.github.com/npm/ssri/pull/52) postinstall
for dependabot template-oss PR
([@&#8203;lukekarrys](https://redirect.github.com/lukekarrys))

##### Bug Fixes

-
[`2e876d1`](2e876d12a6)
[#&#8203;48](https://redirect.github.com/npm/ssri/pull/48) properly
handle missing algorithm type
([#&#8203;48](https://redirect.github.com/npm/ssri/issues/48))
([@&#8203;ahmedwelhakim](https://redirect.github.com/ahmedwelhakim))

#####
[9.0.1](https://redirect.github.com/npm/ssri/compare/v9.0.0...v9.0.1)
(2022-05-19)

##### Bug Fixes

- store emitted events and re-emit them for late listeners
([#&#8203;39](https://redirect.github.com/npm/ssri/issues/39))
([c5421f1](c5421f1fb4))

###
[`v9.0.1`](https://redirect.github.com/npm/ssri/blob/HEAD/CHANGELOG.md#1000-2022-10-10)

[Compare
Source](https://redirect.github.com/npm/ssri/compare/v9.0.0...v9.0.1)

##### ⚠️ BREAKING CHANGES

- `ssri` is now compatible with the following semver range for node:
`^14.17.0 || ^16.13.0 || >=18.0.0`

##### Features

-
[`3de0c45`](3de0c4502d)
[#&#8203;52](https://redirect.github.com/npm/ssri/pull/52) postinstall
for dependabot template-oss PR
([@&#8203;lukekarrys](https://redirect.github.com/lukekarrys))

##### Bug Fixes

-
[`2e876d1`](2e876d12a6)
[#&#8203;48](https://redirect.github.com/npm/ssri/pull/48) properly
handle missing algorithm type
([#&#8203;48](https://redirect.github.com/npm/ssri/issues/48))
([@&#8203;ahmedwelhakim](https://redirect.github.com/ahmedwelhakim))

#####
[9.0.1](https://redirect.github.com/npm/ssri/compare/v9.0.0...v9.0.1)
(2022-05-19)

##### Bug Fixes

- store emitted events and re-emit them for late listeners
([#&#8203;39](https://redirect.github.com/npm/ssri/issues/39))
([c5421f1](c5421f1fb4))

###
[`v9.0.0`](https://redirect.github.com/npm/ssri/blob/HEAD/CHANGELOG.md#1000-2022-10-10)

[Compare
Source](https://redirect.github.com/npm/ssri/compare/v8.0.1...v9.0.0)

##### ⚠️ BREAKING CHANGES

- `ssri` is now compatible with the following semver range for node:
`^14.17.0 || ^16.13.0 || >=18.0.0`

##### Features

-
[`3de0c45`](3de0c4502d)
[#&#8203;52](https://redirect.github.com/npm/ssri/pull/52) postinstall
for dependabot template-oss PR
([@&#8203;lukekarrys](https://redirect.github.com/lukekarrys))

##### Bug Fixes

-
[`2e876d1`](2e876d12a6)
[#&#8203;48](https://redirect.github.com/npm/ssri/pull/48) properly
handle missing algorithm type
([#&#8203;48](https://redirect.github.com/npm/ssri/issues/48))
([@&#8203;ahmedwelhakim](https://redirect.github.com/ahmedwelhakim))

#####
[9.0.1](https://redirect.github.com/npm/ssri/compare/v9.0.0...v9.0.1)
(2022-05-19)

##### Bug Fixes

- store emitted events and re-emit them for late listeners
([#&#8203;39](https://redirect.github.com/npm/ssri/issues/39))
([c5421f1](c5421f1fb4))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/cnpm/cnpmcore).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTkuNCIsInVwZGF0ZWRJblZlciI6IjQxLjE1OS40IiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-29 23:36:35 +08:00
MK (fengmk2)
0d32146562 chore: add deployment test (#874)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Added an npm build script to simplify project compilation.

* **Chores**
  * Simplified CI dependency installation to use standard npm install.
* Added a new deployment test job that runs multi-service integration
checks, build, health checks, and graceful shutdown.
* Introduced an environment-variable gate to allow opting into local
filesystem behavior in production.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-29 23:35:06 +08:00
renovate[bot]
6c29f084b2 fix(deps): update dependency validate-npm-package-name to v7 [skip ci] (#873)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
|
[validate-npm-package-name](https://redirect.github.com/npm/validate-npm-package-name)
| [`^6.0.0` ->
`^7.0.0`](https://renovatebot.com/diffs/npm/validate-npm-package-name/6.0.2/7.0.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/validate-npm-package-name/7.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/validate-npm-package-name/6.0.2/7.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>npm/validate-npm-package-name
(validate-npm-package-name)</summary>

###
[`v7.0.0`](https://redirect.github.com/npm/validate-npm-package-name/blob/HEAD/CHANGELOG.md#700-2025-10-22)

[Compare
Source](https://redirect.github.com/npm/validate-npm-package-name/compare/v6.0.2...v7.0.0)

##### ⚠️ BREAKING CHANGES

- align to npm 11 node engine range
([#&#8203;147](https://redirect.github.com/npm/validate-npm-package-name/issues/147))

##### Bug Fixes

-
[`69e0cbb`](69e0cbbd77)
[#&#8203;147](https://redirect.github.com/npm/validate-npm-package-name/pull/147)
align to npm 11 node engine range
([#&#8203;147](https://redirect.github.com/npm/validate-npm-package-name/issues/147))
([@&#8203;owlstronaut](https://redirect.github.com/owlstronaut))

##### Chores

-
[`c6866b9`](c6866b9d96)
[#&#8203;146](https://redirect.github.com/npm/validate-npm-package-name/pull/146)
bump
[@&#8203;npmcli/template-oss](https://redirect.github.com/npmcli/template-oss)
from 4.26.0 to 4.27.1
([#&#8203;146](https://redirect.github.com/npm/validate-npm-package-name/issues/146))
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot],
[@&#8203;npm-cli-bot](https://redirect.github.com/npm-cli-bot))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/cnpm/cnpmcore).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTkuNCIsInVwZGF0ZWRJblZlciI6IjQxLjE1OS40IiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-29 21:19:12 +08:00
renovate[bot]
01385f4954 chore(deps): update dependency type-fest to v5 [skip ci] (#871)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [type-fest](https://redirect.github.com/sindresorhus/type-fest) |
[`^2.5.3` ->
`^5.0.0`](https://renovatebot.com/diffs/npm/type-fest/2.19.0/5.1.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/type-fest/5.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/type-fest/2.19.0/5.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>sindresorhus/type-fest (type-fest)</summary>

###
[`v5.1.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v5.1.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v5.0.1...v5.1.0)

##### New types

-
[`TupleOf`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/tuple-of.d.ts)
([#&#8203;1247](https://redirect.github.com/sindresorhus/type-fest/issues/1247))
[`7fb2f75`](https://redirect.github.com/sindresorhus/type-fest/commit/7fb2f75)
-
[`Xor`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/xor.d.ts)
([#&#8203;1254](https://redirect.github.com/sindresorhus/type-fest/issues/1254))
[`ad04bc5`](https://redirect.github.com/sindresorhus/type-fest/commit/ad04bc5)
-
[`SplitOnRestElement`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/split-on-rest-element.d.ts)
([#&#8203;1166](https://redirect.github.com/sindresorhus/type-fest/issues/1166))
[`34b8fad`](https://redirect.github.com/sindresorhus/type-fest/commit/34b8fad)
-
[`ExtractRestElement`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/extract-rest-element.d.ts)
([#&#8203;1166](https://redirect.github.com/sindresorhus/type-fest/issues/1166))
[`34b8fad`](https://redirect.github.com/sindresorhus/type-fest/commit/34b8fad)
-
[`ExcludeRestElement`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/exclude-rest-element.d.ts)
([#&#8203;1166](https://redirect.github.com/sindresorhus/type-fest/issues/1166))
[`34b8fad`](https://redirect.github.com/sindresorhus/type-fest/commit/34b8fad)

##### Improvements

- `ReadonlyTuple`: Deprecate in favor of `TupleOf`
([#&#8203;1256](https://redirect.github.com/sindresorhus/type-fest/issues/1256))
[`af4bebc`](https://redirect.github.com/sindresorhus/type-fest/commit/af4bebc)
- `TsConfigJson`: Add missing lib enum values
([#&#8203;1263](https://redirect.github.com/sindresorhus/type-fest/issues/1263))
[`72f491f`](https://redirect.github.com/sindresorhus/type-fest/commit/72f491f)
- `TsConfigJson`: Add `rewriteRelativeImportExtensions`
([#&#8203;1262](https://redirect.github.com/sindresorhus/type-fest/issues/1262))
[`7d011ce`](https://redirect.github.com/sindresorhus/type-fest/commit/7d011ce)

##### Fixes

- `PartialDeep`: Fix behavior with functions containing multiple call
signatures
([#&#8203;1259](https://redirect.github.com/sindresorhus/type-fest/issues/1259))
[`3bd9de6`](https://redirect.github.com/sindresorhus/type-fest/commit/3bd9de6)
- `IsEqual`: Fix behaviour when instantiated with `never` and `unknown`
([#&#8203;1251](https://redirect.github.com/sindresorhus/type-fest/issues/1251))
[`785549f`](https://redirect.github.com/sindresorhus/type-fest/commit/785549f)
- `FixedLengthArray`: Fix element type
([#&#8203;1246](https://redirect.github.com/sindresorhus/type-fest/issues/1246))
[`ee29ef7`](https://redirect.github.com/sindresorhus/type-fest/commit/ee29ef7)
- `is-equal`: Fix handling with intersecting wrapped types
([#&#8203;1231](https://redirect.github.com/sindresorhus/type-fest/issues/1231))
[`5af60a1`](https://redirect.github.com/sindresorhus/type-fest/commit/5af60a1)

***

###
[`v5.0.1`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v5.0.1)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v5.0.0...v5.0.1)

- Add missing exports
[`4f9c248`](https://redirect.github.com/sindresorhus/type-fest/commit/4f9c248)

***

###
[`v5.0.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v5.0.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.41.0...v5.0.0)

##### Breaking

- This package is now pure ESM. **Please [read
this](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c).**
- Require TypeScript 5.9
[`b5b0214`](https://redirect.github.com/sindresorhus/type-fest/commit/b5b0214)
- Require Node.js 20
[`cc2b0f2`](https://redirect.github.com/sindresorhus/type-fest/commit/cc2b0f2)
- Reminder: `type-fest` requires `strict: true` in your tsconfig.
- `StringKeyOf`: Rename to
[`KeyAsString`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/key-as-string.d.ts)
[`e492c9c`](https://redirect.github.com/sindresorhus/type-fest/commit/e492c9c)
-
[`ArrayTail`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/array-tail.d.ts):
Enable
[`preserveReadonly`](68469722a9/source/array-tail.d.ts (L8-L30))
by default and remove the option
[`b34b1d8`](https://redirect.github.com/sindresorhus/type-fest/commit/b34b1d8)
-
[`CamelCase`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/camel-case.d.ts)
/
[`CamelCasedProperties`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/camel-cased-properties.d.ts)
/
[`CamelCasedPropertiesDeep`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/camel-cased-properties-deep.d.ts)
/
[`PascalCase`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/pascal-case.d.ts)
/
[`PascalCasedProperties`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/pascal-cased-properties.d.ts)
/
[`PascalCasedPropertiesDeep`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/pascal-cased-properties-deep.d.ts):
Disable `preserveConsecutiveUppercase` by default
[`8226c1b`](https://redirect.github.com/sindresorhus/type-fest/commit/8226c1b)
  - This aligns it with the general JavaScript naming convention.
-
[`PartialDeep`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/partial-deep.d.ts):
Disable `allowUndefinedInNonTupleArrays` by default
[`b3c4524`](https://redirect.github.com/sindresorhus/type-fest/commit/b3c4524)
-
[`Split`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/split.d.ts):
Enable `strictLiteralChecks` by default
[`544a846`](https://redirect.github.com/sindresorhus/type-fest/commit/544a846)
-
[`Paths`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/paths.d.ts):
Default `maxRecursionDepth` 5 (was 10)
[`2ab5dec`](https://redirect.github.com/sindresorhus/type-fest/commit/2ab5dec)
-
[`ObservableLike`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/globals/observable-like.d.ts):
Move to sub-export
[`2a1072e`](https://redirect.github.com/sindresorhus/type-fest/commit/2a1072e)
- Deprecate `If*` types in favor of a single
[`If`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/if.d.ts)
[`4c2151a`](https://redirect.github.com/sindresorhus/type-fest/commit/4c2151a)

##### New types

-
[`Alphanumeric`](fc14f87e7f/source/characters.d.ts)
— Single alphanumeric character (`A–Z`, `a–z`, `0–9`).
[`484e030`](https://redirect.github.com/sindresorhus/type-fest/commit/484e030)
-
[`AllExtend`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/all-extend.d.ts)
— Evaluates to `true` if every element of a tuple/array extends `U`.
[`c8c6d55`](https://redirect.github.com/sindresorhus/type-fest/commit/c8c6d55)
-
[`ConditionalSimplify`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/conditional-simplify.d.ts)
— Simplifies a type’s unions/intersections with opt-in controls.
[`b7a4771`](https://redirect.github.com/sindresorhus/type-fest/commit/b7a4771)
-
[`ConditionalSimplifyDeep`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/conditional-simplify-deep.d.ts)
— Deep version of `ConditionalSimplify` that recurses into objects.
[`b7a4771`](https://redirect.github.com/sindresorhus/type-fest/commit/b7a4771)
-
[`DigitCharacter`](fc14f87e7f/source/characters.d.ts)
— Single ASCII digit character (`0–9`).
[`484e030`](https://redirect.github.com/sindresorhus/type-fest/commit/484e030)
-
[`ExcludeStrict`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/exclude-strict.d.ts)
— Non-distributive, stricter variant of `Exclude<T, U>`.
[`e6f62a2`](https://redirect.github.com/sindresorhus/type-fest/commit/e6f62a2)
-
[`ExtendsStrict`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/extends-strict.d.ts)
— Non-distributive `A extends B` check.
[`d71242a`](https://redirect.github.com/sindresorhus/type-fest/commit/d71242a)
-
[`ExtractStrict`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/extract-strict.d.ts)
— Non-distributive, stricter variant of `Extract<T, U>`.
[`98d24fa`](https://redirect.github.com/sindresorhus/type-fest/commit/98d24fa)
-
[`IsLowercase`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-lowercase.d.ts)
— Evaluates to `true` if a string literal is all lowercase.
[`afe132c`](https://redirect.github.com/sindresorhus/type-fest/commit/afe132c)
-
[`IsNullable`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-nullable.d.ts)
— Evaluates to `true` if `T` includes `null`.
[`5067e25`](https://redirect.github.com/sindresorhus/type-fest/commit/5067e25)
-
[`IsOptional`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-optional.d.ts)
— Evaluates to `true` if `T` includes `undefined`.
[`5067e25`](https://redirect.github.com/sindresorhus/type-fest/commit/5067e25)
-
[`IsOptionalKeyOf`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-optional-key-of.d.ts)
— Evaluates to `true` if property `K` of `T` is optional.
[`93728b5`](https://redirect.github.com/sindresorhus/type-fest/commit/93728b5)
-
[`IsReadonlyKeyOf`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-readonly-key-of.d.ts)
— Evaluates to `true` if property `K` of `T` is readonly.
[`93728b5`](https://redirect.github.com/sindresorhus/type-fest/commit/93728b5)
-
[`IsRequiredKeyOf`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-required-key-of.d.ts)
— Evaluates to `true` if property `K` of `T` is required.
[`93728b5`](https://redirect.github.com/sindresorhus/type-fest/commit/93728b5)
-
[`IsUnion`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-union.d.ts)
— Evaluates to `true` if `T` is a union type.
[`b3d92ed`](https://redirect.github.com/sindresorhus/type-fest/commit/b3d92ed)
-
[`IsUndefined`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-undefined.d.ts)
— Evaluates to `true` if the type is exactly `undefined`.
[`f7bc576`](https://redirect.github.com/sindresorhus/type-fest/commit/f7bc576)
-
[`IsUppercase`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-uppercase.d.ts)
— Evaluates to `true` if a string literal is all uppercase.
[`afe132c`](https://redirect.github.com/sindresorhus/type-fest/commit/afe132c)
-
[`LowercaseLetter`](fc14f87e7f/source/characters.d.ts)
— Single lowercase Latin letter (`a–z`).
[`484e030`](https://redirect.github.com/sindresorhus/type-fest/commit/484e030)
-
[`RemovePrefix`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/remove-prefix.d.ts)
— Removes a specified prefix from a string literal.
[`18a1c04`](https://redirect.github.com/sindresorhus/type-fest/commit/18a1c04)
-
[`UppercaseLetter`](fc14f87e7f/source/characters.d.ts)
— Single uppercase Latin letter (`A–Z`).
[`484e030`](https://redirect.github.com/sindresorhus/type-fest/commit/484e030)

##### Improvements

-
[`Jsonify`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/jsonify.d.ts):
Handle `unknown` as `JsonValue`
[`642bb13`](https://redirect.github.com/sindresorhus/type-fest/commit/642bb13)
-
[`SetRequired`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/set-required.d.ts)
/
[`SetOptional`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/set-optional.d.ts)
/
[`SetReadonly`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/set-readonly.d.ts):
Handle functions with properties
[`a5e45d4`](https://redirect.github.com/sindresorhus/type-fest/commit/a5e45d4)
-
[`Schema`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/schema.d.ts):
Preserve arrays/remove extraneous unions
[`8a96def`](https://redirect.github.com/sindresorhus/type-fest/commit/8a96def);
drop `undefined` for `recurseIntoArrays`
[`1cb955b`](https://redirect.github.com/sindresorhus/type-fest/commit/1cb955b)
-
[`ReadonlyKeysOf`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/readonly-keys-of.d.ts)
/
[`WritableKeysOf`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/writable-keys-of.d.ts):
Add `object` constraint
[`a6efbe0`](https://redirect.github.com/sindresorhus/type-fest/commit/a6efbe0)
-
[`TsConfigJson`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/tsconfig-json.d.ts):
Add TypeScript 5.9 fields
[`d2bda94`](https://redirect.github.com/sindresorhus/type-fest/commit/d2bda94)

##### Fixes

-
[`Or`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/or.d.ts):
Fix with `boolean`, `never`, `any`
[`42d6106`](https://redirect.github.com/sindresorhus/type-fest/commit/42d6106)
-
[`And`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/and.d.ts):
Fix with `boolean`, `never`, `any`
[`b38ac60`](https://redirect.github.com/sindresorhus/type-fest/commit/b38ac60)
-
[`IsStringLiteral`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-literal.d.ts):
Fix uncollapsed unions, and tagged types
[`eb37799`](https://redirect.github.com/sindresorhus/type-fest/commit/eb37799)
/
[`d1b35c7`](https://redirect.github.com/sindresorhus/type-fest/commit/d1b35c7)
-
[`Paths`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/paths.d.ts):
Fix behavior with index signatures
[`9926e5d`](https://redirect.github.com/sindresorhus/type-fest/commit/9926e5d)
-
[`ConditionalKeys`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/conditional-keys.d.ts):
Fix behavior with arrays and unions
[`4d7cc50`](https://redirect.github.com/sindresorhus/type-fest/commit/4d7cc50)
-
[`RequiredDeep`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/required-deep.d.ts):
Fix with `undefined`
[`bfcdbc4`](https://redirect.github.com/sindresorhus/type-fest/commit/bfcdbc4)
-
[`Split`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/split.d.ts):
Fix template strings ending with interpolation
[`853b881`](https://redirect.github.com/sindresorhus/type-fest/commit/853b881)
-
[`ArrayTail`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/array-tail.d.ts):
Fix fix non-tuple arrays
[`f3aabd8`](https://redirect.github.com/sindresorhus/type-fest/commit/f3aabd8)
- Fix `UnionMin` and `UnionMax`
[`d52d5e7`](https://redirect.github.com/sindresorhus/type-fest/commit/d52d5e7)

##### Meta

Huge thanks to all the contributors to this release, especially
[@&#8203;som-sm](https://redirect.github.com/som-sm) 🙌

***

###
[`v4.41.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.41.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.40.1...v4.41.0)

- Add
[`SetNonNullableDeep`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/set-non-nullable-deep.d.ts)
type
([#&#8203;1117](https://redirect.github.com/sindresorhus/type-fest/issues/1117))
[`b9606e7`](https://redirect.github.com/sindresorhus/type-fest/commit/b9606e7)
- `LessThan` / `GreaterThan` / `GreaterThanOrEqual`: Fix behaviour with
unions
([#&#8203;1116](https://redirect.github.com/sindresorhus/type-fest/issues/1116))
[`afd809a`](https://redirect.github.com/sindresorhus/type-fest/commit/afd809a)
- `RequireAllOrNone` / `RequireAtLeastOne` / `RequireExactlyOne` /
`RequireOneOrNone`: Fix behaviour with `any` and `never`
([#&#8203;1113](https://redirect.github.com/sindresorhus/type-fest/issues/1113))
[`8c154e9`](https://redirect.github.com/sindresorhus/type-fest/commit/8c154e9)

***

###
[`v4.40.1`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.40.1)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.40.0...v4.40.1)

- `PartialDeep`: Fix behaviour with functions containing properties
([#&#8203;1108](https://redirect.github.com/sindresorhus/type-fest/issues/1108))
[`86a3a69`](https://redirect.github.com/sindresorhus/type-fest/commit/86a3a69)
- `CamelCasedPropertiesDeep` / `DelimiterCasedPropertiesDeep` /
`KebabCasedPropertiesDeep` / `PascalCasedPropertiesDeep` /
`SnakeCasedPropertiesDeep`: Fix behaviour when property value is
`unknown`
([#&#8203;1112](https://redirect.github.com/sindresorhus/type-fest/issues/1112))
[`cfcf9ec`](https://redirect.github.com/sindresorhus/type-fest/commit/cfcf9ec)

***

###
[`v4.40.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.40.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.39.1...v4.40.0)

- Add
[`NonEmptyString`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/non-empty-string.d.ts)
type
([#&#8203;1103](https://redirect.github.com/sindresorhus/type-fest/issues/1103))
[`19a9c37`](https://redirect.github.com/sindresorhus/type-fest/commit/19a9c37)
- Add
[`UnknownMap`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/unknown-map.d.ts)
type
([#&#8203;1106](https://redirect.github.com/sindresorhus/type-fest/issues/1106))
[`b4ace2d`](https://redirect.github.com/sindresorhus/type-fest/commit/b4ace2d)
- Add
[`UnknownSet`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/unknown-set.d.ts)
type
([#&#8203;1106](https://redirect.github.com/sindresorhus/type-fest/issues/1106))
[`b4ace2d`](https://redirect.github.com/sindresorhus/type-fest/commit/b4ace2d)
- `IsFloat` / `IsInteger`: Fix instantiations with numbers represented
using exponential notation
([#&#8203;1101](https://redirect.github.com/sindresorhus/type-fest/issues/1101))
[`21a92f6`](https://redirect.github.com/sindresorhus/type-fest/commit/21a92f6)

***

###
[`v4.39.1`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.39.1)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.39.0...v4.39.1)

- `OptionalKeysOf` / `WritableKeysOf`: Fix generic assignability with
`keyof T`
([#&#8203;1098](https://redirect.github.com/sindresorhus/type-fest/issues/1098))
[`1b41ed3`](https://redirect.github.com/sindresorhus/type-fest/commit/1b41ed3)

***

###
[`v4.39.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.39.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.38.0...v4.39.0)

- `ArrayTail`: Add
[`preserveReadonly`](07cb87075f/source/array-tail.d.ts (L8-L30))
option
([#&#8203;1091](https://redirect.github.com/sindresorhus/type-fest/issues/1091))
[`544271e`](https://redirect.github.com/sindresorhus/type-fest/commit/544271e)
- `PartialDeep `: Fix behaviour when `strictNullChecks` is disabled
([#&#8203;1096](https://redirect.github.com/sindresorhus/type-fest/issues/1096))
[`7536bae`](https://redirect.github.com/sindresorhus/type-fest/commit/7536bae)
- `OptionalKeysOf` / `RequiredKeysOf`: Fix instantiations with unions
and arrays
([#&#8203;1089](https://redirect.github.com/sindresorhus/type-fest/issues/1089))
[`e1ac7b2`](https://redirect.github.com/sindresorhus/type-fest/commit/e1ac7b2)
- `WritableKeysOf` / `ReadonlyKeysOf`: Fix behavior with unions and
optional properties
([#&#8203;1088](https://redirect.github.com/sindresorhus/type-fest/issues/1088))
[`bbf9137`](https://redirect.github.com/sindresorhus/type-fest/commit/bbf9137)

***

###
[`v4.38.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.38.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.37.0...v4.38.0)

- `AsyncReturnType`: Add support for `PromiseLike`
([#&#8203;1082](https://redirect.github.com/sindresorhus/type-fest/issues/1082))
[`72ccde9`](https://redirect.github.com/sindresorhus/type-fest/commit/72ccde9)
- `DelimiterCase` / `SnakeCase` / `ScreamingSnakeCase` / `KebabCase`:
Fix instantiations containing punctuations
([#&#8203;1080](https://redirect.github.com/sindresorhus/type-fest/issues/1080))
[`063e28d`](https://redirect.github.com/sindresorhus/type-fest/commit/063e28d)
- `DelimiterCase`: Pass `Options` generic to all related types
([#&#8203;1078](https://redirect.github.com/sindresorhus/type-fest/issues/1078))
[`1974944`](https://redirect.github.com/sindresorhus/type-fest/commit/1974944)
- `CamelCasedPropertiesDeep`: Make nested array objects respect the
options
([#&#8203;1077](https://redirect.github.com/sindresorhus/type-fest/issues/1077))
[`c11c9ca`](https://redirect.github.com/sindresorhus/type-fest/commit/c11c9ca)

***

###
[`v4.37.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.37.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.36.0...v4.37.0)

- `Sum`: Add negative return value support
([#&#8203;1068](https://redirect.github.com/sindresorhus/type-fest/issues/1068))
[`af5bfb7`](https://redirect.github.com/sindresorhus/type-fest/commit/af5bfb7)
- `Subtract`: Add negative return value support
([#&#8203;1061](https://redirect.github.com/sindresorhus/type-fest/issues/1061))
[`2b85ae2`](https://redirect.github.com/sindresorhus/type-fest/commit/2b85ae2)
- `Split`: Add
[`strictLiteralChecks`](ed8c987129/source/split.d.ts (L12-L34))
option
([#&#8203;1067](https://redirect.github.com/sindresorhus/type-fest/issues/1067))
[`cc93f85`](https://redirect.github.com/sindresorhus/type-fest/commit/cc93f85)
- `Split`: Fix instantiations with unions
([#&#8203;1067](https://redirect.github.com/sindresorhus/type-fest/issues/1067))
[`cc93f85`](https://redirect.github.com/sindresorhus/type-fest/commit/cc93f85)
- `Replace`: Fix instantiations with unions
([#&#8203;1065](https://redirect.github.com/sindresorhus/type-fest/issues/1065))
[`a733698`](https://redirect.github.com/sindresorhus/type-fest/commit/a733698)
- `DelimiterCase` / `SnakeCase` / `ScreamingSnakeCase` / `KebabCase`:
Fix default value for `splitOnNumbers` option
([#&#8203;1073](https://redirect.github.com/sindresorhus/type-fest/issues/1073))
[`e462e72`](https://redirect.github.com/sindresorhus/type-fest/commit/e462e72)

***

###
[`v4.36.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.36.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.35.0...v4.36.0)

- `TsConfigJson`: Add TypeScript 5.8 fields
([#&#8203;1064](https://redirect.github.com/sindresorhus/type-fest/issues/1064))
[`918156a`](https://redirect.github.com/sindresorhus/type-fest/commit/918156a)
- `Replace`: Add support for generating longer strings
([#&#8203;1060](https://redirect.github.com/sindresorhus/type-fest/issues/1060))
[`3c03a0d`](https://redirect.github.com/sindresorhus/type-fest/commit/3c03a0d)
- `DelimiterCase`: Internal improvements
([#&#8203;930](https://redirect.github.com/sindresorhus/type-fest/issues/930))
[`a463c30`](https://redirect.github.com/sindresorhus/type-fest/commit/a463c30)

***

###
[`v4.35.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.35.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.34.1...v4.35.0)

- Add
[`TupleToObject`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/tuple-to-object.d.ts)
type
([#&#8203;1055](https://redirect.github.com/sindresorhus/type-fest/issues/1055))
[`c8149ec`](https://redirect.github.com/sindresorhus/type-fest/commit/c8149ec)
- `Paths`: Add
[`depth`](db3403a4b3/source/paths.d.ts (L95-L128))
option
([#&#8203;1058](https://redirect.github.com/sindresorhus/type-fest/issues/1058))
[`2633e5b`](https://redirect.github.com/sindresorhus/type-fest/commit/2633e5b)

***

###
[`v4.34.1`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.34.1)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.34.0...v4.34.1)

- `OmitDeep`: Fix import statement
([#&#8203;1052](https://redirect.github.com/sindresorhus/type-fest/issues/1052))
[`e5b66a4`](https://redirect.github.com/sindresorhus/type-fest/commit/e5b66a4)

***

###
[`v4.34.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.34.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.33.0...v4.34.0)

- Add
[`AllUnionFields`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/all-union-fields.d.ts)
type
([#&#8203;997](https://redirect.github.com/sindresorhus/type-fest/issues/997))
[`9aba4c3`](https://redirect.github.com/sindresorhus/type-fest/commit/9aba4c3)
- `Paths`: Add
[`leavesOnly`](00c2210831/source/paths.d.ts (L54-L93))
option
([#&#8203;1050](https://redirect.github.com/sindresorhus/type-fest/issues/1050))
[`7dfb307`](https://redirect.github.com/sindresorhus/type-fest/commit/7dfb307)
- `OmitDeep`: Fix removal of multiple paths within arrays
([#&#8203;1049](https://redirect.github.com/sindresorhus/type-fest/issues/1049))
[`fa6e31b`](https://redirect.github.com/sindresorhus/type-fest/commit/fa6e31b)

***

###
[`v4.33.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.33.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.32.0...v4.33.0)

- `StringRepeat`: Add support for generating longer strings & fix
instantiations with unions
([#&#8203;1046](https://redirect.github.com/sindresorhus/type-fest/issues/1046))
[`fbccaab`](https://redirect.github.com/sindresorhus/type-fest/commit/fbccaab)
- `Split`: Add support for longer strings
([#&#8203;1042](https://redirect.github.com/sindresorhus/type-fest/issues/1042))
[`49605b9`](https://redirect.github.com/sindresorhus/type-fest/commit/49605b9)
- `Get`: Optimize performance
([#&#8203;1031](https://redirect.github.com/sindresorhus/type-fest/issues/1031))
[`cfb5947`](https://redirect.github.com/sindresorhus/type-fest/commit/cfb5947)
- `KeysOfUnion`: Fix assignability with `keyof`
([#&#8203;1009](https://redirect.github.com/sindresorhus/type-fest/issues/1009))
[`4789c7c`](https://redirect.github.com/sindresorhus/type-fest/commit/4789c7c)
- `IsStringLiteral`: Fix instantiations with infinite string types
([#&#8203;1044](https://redirect.github.com/sindresorhus/type-fest/issues/1044))
[`e7800af`](https://redirect.github.com/sindresorhus/type-fest/commit/e7800af)
- `SetRequiredDeep`: Fix handling of unions in nested keys
([#&#8203;1037](https://redirect.github.com/sindresorhus/type-fest/issues/1037))
[`bf5ce3c`](https://redirect.github.com/sindresorhus/type-fest/commit/bf5ce3c)
- `StringSlice`: Fix return type when passing in non-literal
([#&#8203;1036](https://redirect.github.com/sindresorhus/type-fest/issues/1036))
[`979eccf`](https://redirect.github.com/sindresorhus/type-fest/commit/979eccf)
- `Sum`/`Subtract`: Fix instantiations with unions
([#&#8203;1034](https://redirect.github.com/sindresorhus/type-fest/issues/1034))
[`69bfd51`](https://redirect.github.com/sindresorhus/type-fest/commit/69bfd51)

***

###
[`v4.32.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.32.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.31.0...v4.32.0)

##### New types

-
[`IsTuple`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-tuple.d.ts)
([#&#8203;1024](https://redirect.github.com/sindresorhus/type-fest/issues/1024))
[`1e0872d`](https://redirect.github.com/sindresorhus/type-fest/commit/1e0872d)
-
[`SetRequiredDeep`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/set-required-deep.d.ts)
([#&#8203;939](https://redirect.github.com/sindresorhus/type-fest/issues/939))
[`3d54627`](https://redirect.github.com/sindresorhus/type-fest/commit/3d54627)

##### Improvements

- `PartialDeep`: Add
[`allowUndefinedInNonTupleArrays`](20f1995b4a/source/partial-deep.d.ts (L14-L38))
option
([#&#8203;1019](https://redirect.github.com/sindresorhus/type-fest/issues/1019))
[`278df80`](https://redirect.github.com/sindresorhus/type-fest/commit/278df80)

##### Fixes

- `SetRequired`: Fix support for removal of optional modifiers from
tuples
([#&#8203;1030](https://redirect.github.com/sindresorhus/type-fest/issues/1030))
[`c897aad`](https://redirect.github.com/sindresorhus/type-fest/commit/c897aad)

***

###
[`v4.31.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.31.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.30.2...v4.31.0)

- `SetFieldType`: Add option to preserve property modifiers
([#&#8203;1017](https://redirect.github.com/sindresorhus/type-fest/issues/1017))
[`59517cb`](https://redirect.github.com/sindresorhus/type-fest/commit/59517cb)
- `IsEqual`: Fix identity issue
([#&#8203;1012](https://redirect.github.com/sindresorhus/type-fest/issues/1012))
[`591700a`](https://redirect.github.com/sindresorhus/type-fest/commit/591700a)
- `SetOptional`: Fix instantiations with index signatures
([#&#8203;1014](https://redirect.github.com/sindresorhus/type-fest/issues/1014))
[`cb269ff`](https://redirect.github.com/sindresorhus/type-fest/commit/cb269ff)
- `SetRequired`: Fix instantiations with index signatures
([#&#8203;1014](https://redirect.github.com/sindresorhus/type-fest/issues/1014))
[`cb269ff`](https://redirect.github.com/sindresorhus/type-fest/commit/cb269ff)
- `SetReadonly`: Fix instantiations with index signatures
([#&#8203;1014](https://redirect.github.com/sindresorhus/type-fest/issues/1014))
[`cb269ff`](https://redirect.github.com/sindresorhus/type-fest/commit/cb269ff)

###
[`v4.30.2`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.30.2)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.30.1...v4.30.2)

- Fix missing export
([#&#8203;1011](https://redirect.github.com/sindresorhus/type-fest/issues/1011))
[`527d600`](https://redirect.github.com/sindresorhus/type-fest/commit/527d600)
- `SetOptional`/`SetRequired`/`SetReadonly`: Fix when the second
argument is `any`
([#&#8203;1007](https://redirect.github.com/sindresorhus/type-fest/issues/1007))
[`fdbcb11`](https://redirect.github.com/sindresorhus/type-fest/commit/fdbcb11)
- `SetOptional`: Fix when instantiated with unions
([#&#8203;1007](https://redirect.github.com/sindresorhus/type-fest/issues/1007))
[`fdbcb11`](https://redirect.github.com/sindresorhus/type-fest/commit/fdbcb11)

###
[`v4.30.1`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.30.1)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.30.0...v4.30.1)

- `Arrayable`: Make it more usable by removing `readonly`
([#&#8203;1003](https://redirect.github.com/sindresorhus/type-fest/issues/1003))
[`a128f69`](https://redirect.github.com/sindresorhus/type-fest/commit/a128f69)

###
[`v4.30.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.30.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.29.1...v4.30.0)

- Add
[`SharedUnionFields`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/shared-union-fields.d.ts)
type
([#&#8203;994](https://redirect.github.com/sindresorhus/type-fest/issues/994))
[`a716c29`](https://redirect.github.com/sindresorhus/type-fest/commit/a716c29)
- `ArrayTail`: Fix support for optional parameters
([#&#8203;977](https://redirect.github.com/sindresorhus/type-fest/issues/977))
[`f6b1387`](https://redirect.github.com/sindresorhus/type-fest/commit/f6b1387)

###
[`v4.29.1`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.29.1)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.29.0...v4.29.1)

- `IsAny`: Fix circular constraint error on TypeScript 5.4+
([#&#8203;993](https://redirect.github.com/sindresorhus/type-fest/issues/993))
[`32d94dd`](https://redirect.github.com/sindresorhus/type-fest/commit/32d94dd)

###
[`v4.29.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.29.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.28.1...v4.29.0)

- Add
[`IntClosedRange`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/int-closed-range.d.ts)
type
([#&#8203;992](https://redirect.github.com/sindresorhus/type-fest/issues/992))
[`d7b692b`](https://redirect.github.com/sindresorhus/type-fest/commit/d7b692b)
- `Schema`: Add
[`recurseIntoArrays`](1d44863d3a/source/schema.d.ts (L89-L113))
option
([#&#8203;960](https://redirect.github.com/sindresorhus/type-fest/issues/960))
[`fbbb8ba`](https://redirect.github.com/sindresorhus/type-fest/commit/fbbb8ba)

###
[`v4.28.1`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.28.1)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.28.0...v4.28.1)

- `SharedUnionFieldsDeep`: Fix support for optional fields
([#&#8203;988](https://redirect.github.com/sindresorhus/type-fest/issues/988))
[`4b49b93`](https://redirect.github.com/sindresorhus/type-fest/commit/4b49b93)

###
[`v4.28.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.28.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.27.1...v4.28.0)

- `TsConfigJson`: Add TypeScript 5.7 fields
([#&#8203;987](https://redirect.github.com/sindresorhus/type-fest/issues/987))
[`9d6fff3`](https://redirect.github.com/sindresorhus/type-fest/commit/9d6fff3)

###
[`v4.27.1`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.27.1)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.27.0...v4.27.1)

- `SharedUnionFieldsDeep`: Fix propagation for non union root types
([#&#8203;984](https://redirect.github.com/sindresorhus/type-fest/issues/984))
[`f215f9f`](https://redirect.github.com/sindresorhus/type-fest/commit/f215f9f)
- `ArraySlice`: Fix support for union input
([#&#8203;986](https://redirect.github.com/sindresorhus/type-fest/issues/986))
[`0efbae3`](https://redirect.github.com/sindresorhus/type-fest/commit/0efbae3)

###
[`v4.27.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.27.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.26.1...v4.27.0)

- Add
[`Words`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/words.d.ts)
type
([#&#8203;975](https://redirect.github.com/sindresorhus/type-fest/issues/975))
[`13c0c83`](https://redirect.github.com/sindresorhus/type-fest/commit/13c0c83)
- `Get`: Fix handling of paths with number template literal
([#&#8203;968](https://redirect.github.com/sindresorhus/type-fest/issues/968))
[`b93f54a`](https://redirect.github.com/sindresorhus/type-fest/commit/b93f54a)
- `TsConfigJson`: Add `noCheck` to `compilerOptions`
([#&#8203;981](https://redirect.github.com/sindresorhus/type-fest/issues/981))
[`a470913`](https://redirect.github.com/sindresorhus/type-fest/commit/a470913)

###
[`v4.26.1`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.26.1)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.26.0...v4.26.1)

- `Exact`: Fix usage with recursive types and unions
([#&#8203;949](https://redirect.github.com/sindresorhus/type-fest/issues/949))
[`91f6d39`](https://redirect.github.com/sindresorhus/type-fest/commit/91f6d39)

###
[`v4.26.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.26.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.25.0...v4.26.0)

- Add
[`UnionToTuple`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/union-to-tuple.d.ts)
type
([#&#8203;945](https://redirect.github.com/sindresorhus/type-fest/issues/945))
[`1f4f7a1`](https://redirect.github.com/sindresorhus/type-fest/commit/1f4f7a1)

###
[`v4.25.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.25.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.24.0...v4.25.0)

- Add `StringRepeat` type
([#&#8203;938](https://redirect.github.com/sindresorhus/type-fest/issues/938))
[`a83e87e`](https://redirect.github.com/sindresorhus/type-fest/commit/a83e87e)
- Add `Arrayable` type
[#&#8203;270](https://redirect.github.com/sindresorhus/type-fest/issues/270)
([#&#8203;935](https://redirect.github.com/sindresorhus/type-fest/issues/935))
[`9aabcb9`](https://redirect.github.com/sindresorhus/type-fest/commit/9aabcb9)

###
[`v4.24.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.24.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.23.0...v4.24.0)

- `Path`: Add `bracketNotation` option
([#&#8203;926](https://redirect.github.com/sindresorhus/type-fest/issues/926))
[`3b15a94`](https://redirect.github.com/sindresorhus/type-fest/commit/3b15a94)

###
[`v4.23.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.23.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.22.1...v4.23.0)

- `Paths`: Add `maxRecursionDepth` option
([#&#8203;920](https://redirect.github.com/sindresorhus/type-fest/issues/920))
[`052e887`](https://redirect.github.com/sindresorhus/type-fest/commit/052e887)

###
[`v4.22.1`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.22.1)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.22.0...v4.22.1)

- Fix missing exported internal types
([#&#8203;918](https://redirect.github.com/sindresorhus/type-fest/issues/918))
[`4b74444`](https://redirect.github.com/sindresorhus/type-fest/commit/4b74444)

###
[`v4.22.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.22.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.21.0...v4.22.0)

##### New types

-
[`ArrayTail`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/array-tail.d.ts)
([#&#8203;913](https://redirect.github.com/sindresorhus/type-fest/issues/913))
[`128b21e`](https://redirect.github.com/sindresorhus/type-fest/commit/128b21e)
-
[`NonEmptyTuple`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/non-empty-tuple.d.ts)
([#&#8203;915](https://redirect.github.com/sindresorhus/type-fest/issues/915))
[`bb57638`](https://redirect.github.com/sindresorhus/type-fest/commit/bb57638)
-
[`FindGlobalType`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/find-global-type.d.ts)
([#&#8203;908](https://redirect.github.com/sindresorhus/type-fest/issues/908))
[`0086cd6`](https://redirect.github.com/sindresorhus/type-fest/commit/0086cd6)
-
[`FindGlobalInstanceType`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/find-global-type.d.ts)
([#&#8203;908](https://redirect.github.com/sindresorhus/type-fest/issues/908))
[`0086cd6`](https://redirect.github.com/sindresorhus/type-fest/commit/0086cd6)

##### Improvements

- Ensure all `RequireX` types' second parameter is optional
([#&#8203;907](https://redirect.github.com/sindresorhus/type-fest/issues/907))
[`fee4e04`](https://redirect.github.com/sindresorhus/type-fest/commit/fee4e04)
-
[`StructuredCloneable`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/find-global-type.d.ts):
Include web-specific types when available
([#&#8203;908](https://redirect.github.com/sindresorhus/type-fest/issues/908))
[`0086cd6`](https://redirect.github.com/sindresorhus/type-fest/commit/0086cd6)

##### Fixes

- `Exact`: Fix type when class is present
([#&#8203;911](https://redirect.github.com/sindresorhus/type-fest/issues/911))
[`bf85819`](https://redirect.github.com/sindresorhus/type-fest/commit/bf85819)

###
[`v4.21.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.21.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.20.1...v4.21.0)

##### New types

-
[`StructuredCloneable`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/structured-cloneable.d.ts)
([#&#8203;897](https://redirect.github.com/sindresorhus/type-fest/issues/897))
[`737550b`](https://redirect.github.com/sindresorhus/type-fest/commit/737550b)

##### Fixes

- `Jsonify`: Convert `undefined` to `null` in union element of array
([#&#8203;901](https://redirect.github.com/sindresorhus/type-fest/issues/901))
[`60c1024`](https://redirect.github.com/sindresorhus/type-fest/commit/60c1024)
- `Exact`: Fix support for `Date` in union
([#&#8203;902](https://redirect.github.com/sindresorhus/type-fest/issues/902))
[`d89a709`](https://redirect.github.com/sindresorhus/type-fest/commit/d89a709)
- `CamelCasedPropertiesDeep`: Fix handling of non-recursive types inside
target type
([#&#8203;890](https://redirect.github.com/sindresorhus/type-fest/issues/890))
[`476024d`](https://redirect.github.com/sindresorhus/type-fest/commit/476024d)

###
[`v4.20.1`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.20.1)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.20.0...v4.20.1)

- `Schema`: Fix handling of arrays
([#&#8203;887](https://redirect.github.com/sindresorhus/type-fest/issues/887))
[`c570ec2`](https://redirect.github.com/sindresorhus/type-fest/commit/c570ec2)
- `Paths`: Prevent infinite recursion
([#&#8203;891](https://redirect.github.com/sindresorhus/type-fest/issues/891))
[`7d4e875`](https://redirect.github.com/sindresorhus/type-fest/commit/7d4e875)

###
[`v4.20.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.20.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.19.0...v4.20.0)

- `SimplifyDeep`: Support array
([#&#8203;888](https://redirect.github.com/sindresorhus/type-fest/issues/888))
[`a6ab051`](https://redirect.github.com/sindresorhus/type-fest/commit/a6ab051)
- `IsLiteral`: Return `false` for tagged types
([#&#8203;886](https://redirect.github.com/sindresorhus/type-fest/issues/886))
[`587380c`](https://redirect.github.com/sindresorhus/type-fest/commit/587380c)

###
[`v4.19.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.19.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.18.3...v4.19.0)

- Add
[`SimplifyDeep`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/simplify-deep.d.ts)
type
([#&#8203;882](https://redirect.github.com/sindresorhus/type-fest/issues/882))
[`3a04fcf`](https://redirect.github.com/sindresorhus/type-fest/commit/3a04fcf)

###
[`v4.18.3`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.18.3)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.18.2...v4.18.3)

- `ConditionalKeys`: Fix filtering out never type
([#&#8203;881](https://redirect.github.com/sindresorhus/type-fest/issues/881))
[`863511d`](https://redirect.github.com/sindresorhus/type-fest/commit/863511d)

###
[`v4.18.2`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.18.2)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.18.1...v4.18.2)

- `CamelCasedPropertiesDeep`: Fix tuple being incorrectly turned into
array
([#&#8203;818](https://redirect.github.com/sindresorhus/type-fest/issues/818))
[`4e7bb18`](https://redirect.github.com/sindresorhus/type-fest/commit/4e7bb18)

###
[`v4.18.1`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.18.1)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.18.0...v4.18.1)

- Fix missing exports
([#&#8203;876](https://redirect.github.com/sindresorhus/type-fest/issues/876))
[`ed860e9`](https://redirect.github.com/sindresorhus/type-fest/commit/ed860e9)

###
[`v4.18.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.18.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.17.0...v4.18.0)

##### New types

-
[`Or`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/or.d.ts)
[`909c38e`](https://redirect.github.com/sindresorhus/type-fest/commit/909c38e)
-
[`And`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/and.d.ts)
[`9d628aa`](https://redirect.github.com/sindresorhus/type-fest/commit/9d628aa)

##### Improvements

- `TsConfigJson`: Add `preserve` module type and `ES2022` lib types
([#&#8203;874](https://redirect.github.com/sindresorhus/type-fest/issues/874))
[`7096613`](https://redirect.github.com/sindresorhus/type-fest/commit/7096613)
- `Opaque`: Mark as deprecated
([#&#8203;867](https://redirect.github.com/sindresorhus/type-fest/issues/867))
[`ef7b580`](https://redirect.github.com/sindresorhus/type-fest/commit/ef7b580)
- `UnwrapOpaque`: Mark as deprecated
([#&#8203;867](https://redirect.github.com/sindresorhus/type-fest/issues/867))
[`ef7b580`](https://redirect.github.com/sindresorhus/type-fest/commit/ef7b580)

###
[`v4.17.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.17.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.16.0...v4.17.0)

##### New types

-
[`IsNull`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-null.d.ts)
[`d639574`](https://redirect.github.com/sindresorhus/type-fest/commit/d639574)

##### Fixes

- `Zero`: Fix missing export
([#&#8203;870](https://redirect.github.com/sindresorhus/type-fest/issues/870))
[`91a2b1e`](https://redirect.github.com/sindresorhus/type-fest/commit/91a2b1e)

###
[`v4.16.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.16.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.15.0...v4.16.0)

##### New types

-
[`IsInteger`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-integer.d.ts)
-
[`IsFloat`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-float.d.ts)

##### Fixes

- `Integer`: Fix handling of some edge-cases
([#&#8203;857](https://redirect.github.com/sindresorhus/type-fest/issues/857))
[`f5b09de`](https://redirect.github.com/sindresorhus/type-fest/commit/f5b09de)
- `Float`: Fix handling of some edge-cases
([#&#8203;857](https://redirect.github.com/sindresorhus/type-fest/issues/857))
[`f5b09de`](https://redirect.github.com/sindresorhus/type-fest/commit/f5b09de)

###
[`v4.15.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.15.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.14.0...v4.15.0)

##### New types

-
[`SingleKeyObject`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/single-key-object.d.ts)
([#&#8203;849](https://redirect.github.com/sindresorhus/type-fest/issues/849))
[`fa1c3f3`](https://redirect.github.com/sindresorhus/type-fest/commit/fa1c3f3)
-
[`IfEmptyObject`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/if-empty-object.d.ts)
([#&#8203;849](https://redirect.github.com/sindresorhus/type-fest/issues/849))
[`fa1c3f3`](https://redirect.github.com/sindresorhus/type-fest/commit/fa1c3f3)

##### Fixes

- `ConditionalKeys`: Fix TypeScript 5.4 compatibility
([#&#8203;852](https://redirect.github.com/sindresorhus/type-fest/issues/852))
[`0fb2d62`](https://redirect.github.com/sindresorhus/type-fest/commit/0fb2d62)
- `WritableDeep`: Fix TypeScript 5.4 compatibility
([#&#8203;839](https://redirect.github.com/sindresorhus/type-fest/issues/839))
[`2878773`](https://redirect.github.com/sindresorhus/type-fest/commit/2878773)
- `ReadonlyDeep`: Fix TypeScript 5.4 compatibility
([#&#8203;839](https://redirect.github.com/sindresorhus/type-fest/issues/839))
[`2878773`](https://redirect.github.com/sindresorhus/type-fest/commit/2878773)

###
[`v4.14.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.14.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.13.1...v4.14.0)

- Add
[`DistributedPick`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/distributed-pick.d.ts)
type
([#&#8203;841](https://redirect.github.com/sindresorhus/type-fest/issues/841))
[`fa4099c`](https://redirect.github.com/sindresorhus/type-fest/commit/fa4099c)

###
[`v4.13.1`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.13.1)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.13.0...v4.13.1)

- `SetParameterType`: Properly fix compatibility with TypeScript 5.4
([#&#8203;836](https://redirect.github.com/sindresorhus/type-fest/issues/836))
[`a186adb`](https://redirect.github.com/sindresorhus/type-fest/commit/a186adb)

###
[`v4.13.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.13.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.12.0...v4.13.0)

##### New types

-
[`DistributedOmit`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/distributed-omit.d.ts)
([#&#8203;820](https://redirect.github.com/sindresorhus/type-fest/issues/820))
[`bc49577`](https://redirect.github.com/sindresorhus/type-fest/commit/bc49577)

##### Improvements

- `ArraySlice`: Support array spread
([#&#8203;832](https://redirect.github.com/sindresorhus/type-fest/issues/832))
[`d2d0d01`](https://redirect.github.com/sindresorhus/type-fest/commit/d2d0d01)

##### Fixes

- `SetParameterType`: Fix compatibility with TypeScript 5.4
([#&#8203;835](https://redirect.github.com/sindresorhus/type-fest/issues/835))
[`2bc451e`](https://redirect.github.com/sindresorhus/type-fest/commit/2bc451e)
- `OmitDeep`: Fix handling for when the given path is not matched
([#&#8203;834](https://redirect.github.com/sindresorhus/type-fest/issues/834))
[`4f14bff`](https://redirect.github.com/sindresorhus/type-fest/commit/4f14bff)

###
[`v4.12.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.12.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.11.1...v4.12.0)

##### New types

-
[`ArraySlice`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/array-slice.d.ts)
-
[`StringSlice`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/string-slice.d.ts)
-
[`IsNegative`](85221aa12f/source/numeric.d.ts (L172-L187))
-
[`GreaterThan`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/greater-than.d.ts)
-
[`GreaterThanOrEqual`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/greater-than-or-equal.d.ts)
-
[`LessThan`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/less-than.d.ts)
-
[`LessThanOrEqual`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/less-than-or-equal.d.ts)
-
[`Sum`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/sum.d.ts)
-
[`Subtract`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/subtract.d.ts)

###
[`v4.11.1`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.11.1)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.11.0...v4.11.1)

- `OmitDeep`: Fix internally imported type
([#&#8203;824](https://redirect.github.com/sindresorhus/type-fest/issues/824))
[`2061925`](https://redirect.github.com/sindresorhus/type-fest/commit/2061925)

###
[`v4.11.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.11.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.10.3...v4.11.0)

##### New types

-
[`OmitDeep`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/omit-deep.d.ts)
([#&#8203;816](https://redirect.github.com/sindresorhus/type-fest/issues/816))
[`0e196aa`](https://redirect.github.com/sindresorhus/type-fest/commit/0e196aa)
-
[`ArraySplice`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/array-splice.d.ts)
([#&#8203;816](https://redirect.github.com/sindresorhus/type-fest/issues/816))
[`0e196aa`](https://redirect.github.com/sindresorhus/type-fest/commit/0e196aa)

##### Improvements

- `Tagged`: Add metadata support
([#&#8203;723](https://redirect.github.com/sindresorhus/type-fest/issues/723))
[`3ec8dba`](https://redirect.github.com/sindresorhus/type-fest/commit/3ec8dba)

###
[`v4.10.3`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.10.3)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.10.2...v4.10.3)

- `PartialOnUndefinedDeep`: Fix it incorrectly removing non-optional
properties when the input type contains an index signature
([#&#8203;764](https://redirect.github.com/sindresorhus/type-fest/issues/764))
[`2f4b55a`](https://redirect.github.com/sindresorhus/type-fest/commit/2f4b55a)

###
[`v4.10.2`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.10.2)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.10.1...v4.10.2)

- `MergeDeep`: Fix compatibility with TypeScript 5.4
([#&#8203;807](https://redirect.github.com/sindresorhus/type-fest/issues/807))
[`5f6165a`](https://redirect.github.com/sindresorhus/type-fest/commit/5f6165a)

###
[`v4.10.1`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.10.1)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.10.0...v4.10.1)

- Fix support for `exactOptionalPropertyTypes: true` tsconfig
([#&#8203;804](https://redirect.github.com/sindresorhus/type-fest/issues/804))
[`a54e313`](https://redirect.github.com/sindresorhus/type-fest/commit/a54e313)

###
[`v4.10.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.10.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.9.0...v4.10.0)

- `Get`: Fix handling of readonly array
([#&#8203;799](https://redirect.github.com/sindresorhus/type-fest/issues/799))
[`4a38651`](https://redirect.github.com/sindresorhus/type-fest/commit/4a38651)
- `SharedUnionFieldsDeep`: Skip if input type is not a union type
([#&#8203;798](https://redirect.github.com/sindresorhus/type-fest/issues/798))
[`6f1db93`](https://redirect.github.com/sindresorhus/type-fest/commit/6f1db93)
- `DelimiterCasedPropertiesDeep`: Don't recurse into intersection type
that include primitive value
([#&#8203;789](https://redirect.github.com/sindresorhus/type-fest/issues/789))
[`eb96609`](https://redirect.github.com/sindresorhus/type-fest/commit/eb96609)
- `Merge`: Don't turn undefined into optional key
([#&#8203;787](https://redirect.github.com/sindresorhus/type-fest/issues/787))
[`0aec247`](https://redirect.github.com/sindresorhus/type-fest/commit/0aec247)

###
[`v4.9.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.9.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.8.3...v4.9.0)

##### New types

-
[`SharedUnionFieldsDeep`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/shared-union-fields-deep.d.ts)
([#&#8203;783](https://redirect.github.com/sindresorhus/type-fest/issues/783))
[`94bb3d3`](https://redirect.github.com/sindresorhus/type-fest/commit/94bb3d3)

##### Improvements

- `ReadonlyDeep`: Fix usage with properties defined with `void`
([#&#8203;782](https://redirect.github.com/sindresorhus/type-fest/issues/782))
[`a919f93`](https://redirect.github.com/sindresorhus/type-fest/commit/a919f93)
- `ConditionalPickDeep`: Support interface
([#&#8203;776](https://redirect.github.com/sindresorhus/type-fest/issues/776))
[`ebb7a59`](https://redirect.github.com/sindresorhus/type-fest/commit/ebb7a59)

###
[`v4.8.3`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v4.8.3)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.8.2...v4.8.3)

- `MergeDeep`: Fix optional key when value type is `any` or `never`
([#&#8203;777](https://redirect.github.com/sindresorhus/type-fest/issues/777))
[`609c097`](https://redirect.github.com/sindresorhus/type-fest/commit/609c097)
- `Paths`: Ensure it doesn'

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/cnpm/cnpmcore).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTkuNCIsInVwZGF0ZWRJblZlciI6IjQxLjE1OS40IiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-29 21:18:28 +08:00
renovate[bot]
098c75a4ca fix(deps): update dependency read-env-value to v2 (#869)
[skip ci]

This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
|
[read-env-value](https://redirect.github.com/node-modules/read-env-value)
| [`^1.0.0` ->
`^2.0.0`](https://renovatebot.com/diffs/npm/read-env-value/1.1.0/2.0.2)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/read-env-value/2.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/read-env-value/1.1.0/2.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>node-modules/read-env-value (read-env-value)</summary>

###
[`v2.0.2`](https://redirect.github.com/node-modules/read-env-value/blob/HEAD/CHANGELOG.md#small202-2025-08-08-small)

[Compare
Source](https://redirect.github.com/node-modules/read-env-value/compare/v2.0.1...v2.0.2)

- fix: remove src on publish package
([#&#8203;9](https://redirect.github.com/node-modules/read-env-value/issues/9))
([4a2f39c](https://redirect.github.com/node-modules/read-env-value/commit/4a2f39c)),
closes
[#&#8203;9](https://redirect.github.com/node-modules/read-env-value/issues/9)
- test: remove experimental-strip-types flag
([#&#8203;8](https://redirect.github.com/node-modules/read-env-value/issues/8))
([983da2f](https://redirect.github.com/node-modules/read-env-value/commit/983da2f)),
closes
[#&#8203;8](https://redirect.github.com/node-modules/read-env-value/issues/8)
- chore: change jsr name to
[@&#8203;nw/read-env-value](https://redirect.github.com/nw/read-env-value)
([fbbd50b](https://redirect.github.com/node-modules/read-env-value/commit/fbbd50b))

###
[`v2.0.1`](https://redirect.github.com/node-modules/read-env-value/blob/HEAD/CHANGELOG.md#201-2025-07-31)

[Compare
Source](https://redirect.github.com/node-modules/read-env-value/compare/v2.0.0...v2.0.1)

##### Bug Fixes

- duel publish to npm and jsr
([7cd320e](7cd320e410))

###
[`v2.0.0`](https://redirect.github.com/node-modules/read-env-value/blob/HEAD/CHANGELOG.md#200-2025-07-30)

[Compare
Source](https://redirect.github.com/node-modules/read-env-value/compare/v1.1.0...v2.0.0)

##### ⚠ BREAKING CHANGES

- drop Node.js < 22.17.0 support

##### Features

- esm only
([#&#8203;6](https://redirect.github.com/node-modules/read-env-value/issues/6))
([e7f4dad](e7f4dad3f4))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/cnpm/cnpmcore).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTkuNCIsInVwZGF0ZWRJblZlciI6IjQxLjE1OS40IiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-29 21:17:46 +08:00
renovate[bot]
c3059c7138 fix(deps): update dependency npm-package-arg to v13 (#866)
[skip ci]

This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [npm-package-arg](https://redirect.github.com/npm/npm-package-arg) |
[`^10.1.0` ->
`^13.0.0`](https://renovatebot.com/diffs/npm/npm-package-arg/10.1.0/13.0.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/npm-package-arg/13.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/npm-package-arg/10.1.0/13.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>npm/npm-package-arg (npm-package-arg)</summary>

###
[`v13.0.1`](https://redirect.github.com/npm/npm-package-arg/blob/HEAD/CHANGELOG.md#1301-2025-10-06)

[Compare
Source](https://redirect.github.com/npm/npm-package-arg/compare/v13.0.0...v13.0.1)

##### Bug Fixes

-
[`f00dea0`](f00dea08e9)
[#&#8203;211](https://redirect.github.com/npm/npm-package-arg/pull/211)
Correct tarball regex to detect literal dots
([@&#8203;markovejnovic](https://redirect.github.com/markovejnovic))

###
[`v13.0.0`](https://redirect.github.com/npm/npm-package-arg/blob/HEAD/CHANGELOG.md#1300-2025-07-24)

[Compare
Source](https://redirect.github.com/npm/npm-package-arg/compare/v12.0.2...v13.0.0)

##### ⚠️ BREAKING CHANGES

- `npm-package-arg` now supports node `^20.17.0 || >=22.9.0`

##### Bug Fixes

-
[`aa3ed29`](aa3ed290c5)
[#&#8203;207](https://redirect.github.com/npm/npm-package-arg/pull/207)
align to npm 11 node engine range
([@&#8203;owlstronaut](https://redirect.github.com/owlstronaut))

##### Dependencies

-
[`fb6ea64`](fb6ea6416f)
[#&#8203;207](https://redirect.github.com/npm/npm-package-arg/pull/207)
`hosted-git-info@9.0.0`

###
[`v12.0.2`](https://redirect.github.com/npm/npm-package-arg/blob/HEAD/CHANGELOG.md#1202-2025-02-05)

[Compare
Source](https://redirect.github.com/npm/npm-package-arg/compare/v12.0.1...v12.0.2)

##### Bug Fixes

-
[`14cb8a1`](14cb8a18b3)
[#&#8203;200](https://redirect.github.com/npm/npm-package-arg/pull/200)
properly parse non-url encoded file specs
([#&#8203;200](https://redirect.github.com/npm/npm-package-arg/issues/200))
([@&#8203;wraithgar](https://redirect.github.com/wraithgar))

##### Chores

-
[`1343a54`](1343a54064)
[#&#8203;199](https://redirect.github.com/npm/npm-package-arg/pull/199)
bump
[@&#8203;npmcli/template-oss](https://redirect.github.com/npmcli/template-oss)
from 4.23.4 to 4.23.5
([#&#8203;199](https://redirect.github.com/npm/npm-package-arg/issues/199))
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot],
[@&#8203;npm-cli-bot](https://redirect.github.com/npm-cli-bot))

###
[`v12.0.1`](https://redirect.github.com/npm/npm-package-arg/blob/HEAD/CHANGELOG.md#1201-2024-12-10)

[Compare
Source](https://redirect.github.com/npm/npm-package-arg/compare/v12.0.0...v12.0.1)

##### Bug Fixes

-
[`ea07a6e`](ea07a6edc7)
[#&#8203;197](https://redirect.github.com/npm/npm-package-arg/pull/197)
allow for git usernames that start with a number
([#&#8203;197](https://redirect.github.com/npm/npm-package-arg/issues/197))
([@&#8203;wraithgar](https://redirect.github.com/wraithgar))

##### Chores

-
[`41aa799`](41aa799ee5)
[#&#8203;196](https://redirect.github.com/npm/npm-package-arg/pull/196)
bump
[@&#8203;npmcli/template-oss](https://redirect.github.com/npmcli/template-oss)
from 4.23.3 to 4.23.4
([#&#8203;196](https://redirect.github.com/npm/npm-package-arg/issues/196))
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot],
[@&#8203;npm-cli-bot](https://redirect.github.com/npm-cli-bot))

###
[`v12.0.0`](https://redirect.github.com/npm/npm-package-arg/blob/HEAD/CHANGELOG.md#1200-2024-09-25)

[Compare
Source](https://redirect.github.com/npm/npm-package-arg/compare/v11.0.3...v12.0.0)

##### ⚠️ BREAKING CHANGES

- `npm-package-arg` now supports node `^18.17.0 || >=20.5.0`

##### Bug Fixes

-
[`6bf84db`](6bf84db8c3)
[#&#8203;194](https://redirect.github.com/npm/npm-package-arg/pull/194)
align to npm 10 node engine range
([@&#8203;reggi](https://redirect.github.com/reggi))

##### Dependencies

-
[`3361e59`](3361e59441)
[#&#8203;194](https://redirect.github.com/npm/npm-package-arg/pull/194)
`validate-npm-package-name@6.0.0`
-
[`06e3bd6`](06e3bd64cd)
[#&#8203;194](https://redirect.github.com/npm/npm-package-arg/pull/194)
`proc-log@5.0.0`
-
[`96dd671`](96dd671a06)
[#&#8203;194](https://redirect.github.com/npm/npm-package-arg/pull/194)
`hosted-git-info@8.0.0`

##### Chores

-
[`163925e`](163925e693)
[#&#8203;194](https://redirect.github.com/npm/npm-package-arg/pull/194)
run template-oss-apply
([@&#8203;reggi](https://redirect.github.com/reggi))
-
[`a8a9bdd`](a8a9bddc72)
[#&#8203;190](https://redirect.github.com/npm/npm-package-arg/pull/190)
bump
[@&#8203;npmcli/eslint-config](https://redirect.github.com/npmcli/eslint-config)
from 4.0.5 to 5.0.0
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot])
-
[`f8d32ec`](f8d32ec3d0)
[#&#8203;188](https://redirect.github.com/npm/npm-package-arg/pull/188)
postinstall for dependabot template-oss PR
([@&#8203;hashtagchris](https://redirect.github.com/hashtagchris))
-
[`a867f96`](a867f96444)
[#&#8203;188](https://redirect.github.com/npm/npm-package-arg/pull/188)
bump
[@&#8203;npmcli/template-oss](https://redirect.github.com/npmcli/template-oss)
from 4.23.1 to 4.23.3
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot])

###
[`v11.0.3`](https://redirect.github.com/npm/npm-package-arg/blob/HEAD/CHANGELOG.md#1103-2024-07-22)

[Compare
Source](https://redirect.github.com/npm/npm-package-arg/compare/v11.0.2...v11.0.3)

##### Bug Fixes

-
[`59d53b3`](59d53b3ea2)
[#&#8203;184](https://redirect.github.com/npm/npm-package-arg/pull/184)
throws an err when alias is without name
([#&#8203;184](https://redirect.github.com/npm/npm-package-arg/issues/184))
([@&#8203;milaninfy](https://redirect.github.com/milaninfy))

##### Chores

-
[`911661e`](911661e2bd)
[#&#8203;176](https://redirect.github.com/npm/npm-package-arg/pull/176)
bump
[@&#8203;npmcli/template-oss](https://redirect.github.com/npmcli/template-oss)
to 4.22.0 ([@&#8203;lukekarrys](https://redirect.github.com/lukekarrys))
-
[`575012e`](575012e190)
[#&#8203;186](https://redirect.github.com/npm/npm-package-arg/pull/186)
bump
[@&#8203;npmcli/template-oss](https://redirect.github.com/npmcli/template-oss)
from 4.22.0 to 4.23.1
([#&#8203;186](https://redirect.github.com/npm/npm-package-arg/issues/186))
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot],
[@&#8203;wraithgar](https://redirect.github.com/wraithgar))
-
[`74d06ae`](74d06ae66d)
[#&#8203;176](https://redirect.github.com/npm/npm-package-arg/pull/176)
postinstall for dependabot template-oss PR
([@&#8203;lukekarrys](https://redirect.github.com/lukekarrys))

###
[`v11.0.2`](https://redirect.github.com/npm/npm-package-arg/blob/HEAD/CHANGELOG.md#1102-2024-04-12)

[Compare
Source](https://redirect.github.com/npm/npm-package-arg/compare/v11.0.1...v11.0.2)

##### Documentation

-
[`1765111`](17651118e1)
[#&#8203;171](https://redirect.github.com/npm/npm-package-arg/pull/171)
readme: fix broken badge URL
([#&#8203;171](https://redirect.github.com/npm/npm-package-arg/issues/171))
([@&#8203;10xLaCroixDrinker](https://redirect.github.com/10xLaCroixDrinker))

##### Dependencies

-
[`4ccd080`](4ccd08087e)
[#&#8203;173](https://redirect.github.com/npm/npm-package-arg/pull/173)
`proc-log@4.0.0`
([#&#8203;173](https://redirect.github.com/npm/npm-package-arg/issues/173))

##### Chores

-
[`207ba7d`](207ba7d5cf)
[#&#8203;168](https://redirect.github.com/npm/npm-package-arg/pull/168)
postinstall for dependabot template-oss PR
([@&#8203;lukekarrys](https://redirect.github.com/lukekarrys))
-
[`604c1d2`](604c1d2011)
[#&#8203;168](https://redirect.github.com/npm/npm-package-arg/pull/168)
bump
[@&#8203;npmcli/template-oss](https://redirect.github.com/npmcli/template-oss)
from 4.21.1 to 4.21.3
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot])
-
[`82273b5`](82273b59ba)
[#&#8203;165](https://redirect.github.com/npm/npm-package-arg/pull/165)
postinstall for dependabot template-oss PR
([@&#8203;lukekarrys](https://redirect.github.com/lukekarrys))
-
[`4228b37`](4228b378a4)
[#&#8203;165](https://redirect.github.com/npm/npm-package-arg/pull/165)
bump
[@&#8203;npmcli/template-oss](https://redirect.github.com/npmcli/template-oss)
from 4.19.0 to 4.21.1
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot])
-
[`d4b1447`](d4b144726c)
[#&#8203;147](https://redirect.github.com/npm/npm-package-arg/pull/147)
postinstall for dependabot template-oss PR
([@&#8203;lukekarrys](https://redirect.github.com/lukekarrys))
-
[`c5920a9`](c5920a9545)
[#&#8203;147](https://redirect.github.com/npm/npm-package-arg/pull/147)
bump
[@&#8203;npmcli/template-oss](https://redirect.github.com/npmcli/template-oss)
from 4.18.1 to 4.19.0
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot])
-
[`ee68f93`](ee68f93f7b)
[#&#8203;146](https://redirect.github.com/npm/npm-package-arg/pull/146)
postinstall for dependabot template-oss PR
([@&#8203;lukekarrys](https://redirect.github.com/lukekarrys))
-
[`7901052`](79010526af)
[#&#8203;146](https://redirect.github.com/npm/npm-package-arg/pull/146)
bump
[@&#8203;npmcli/template-oss](https://redirect.github.com/npmcli/template-oss)
from 4.18.0 to 4.18.1
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot])

###
[`v11.0.1`](https://redirect.github.com/npm/npm-package-arg/blob/HEAD/CHANGELOG.md#1101-2023-09-05)

[Compare
Source](https://redirect.github.com/npm/npm-package-arg/compare/v11.0.0...v11.0.1)

##### Bug Fixes

-
[`74b3c7e`](74b3c7e34a)
[#&#8203;141](https://redirect.github.com/npm/npm-package-arg/pull/141)
use URL instead of url.parse
([#&#8203;141](https://redirect.github.com/npm/npm-package-arg/issues/141))
([@&#8203;wraithgar](https://redirect.github.com/wraithgar))

##### Documentation

-
[`ea00495`](ea00495783)
[#&#8203;142](https://redirect.github.com/npm/npm-package-arg/pull/142)
fix readme typo
([#&#8203;142](https://redirect.github.com/npm/npm-package-arg/issues/142))
([@&#8203;rotu](https://redirect.github.com/rotu))
-
[`26705c5`](26705c5fef)
[#&#8203;143](https://redirect.github.com/npm/npm-package-arg/pull/143)
Fix citations to RFC 8089 (not 8909) for file: url
([#&#8203;143](https://redirect.github.com/npm/npm-package-arg/issues/143))
([@&#8203;rotu](https://redirect.github.com/rotu))

###
[`v11.0.0`](https://redirect.github.com/npm/npm-package-arg/blob/HEAD/CHANGELOG.md#1100-2023-08-15)

[Compare
Source](https://redirect.github.com/npm/npm-package-arg/compare/v10.1.0...v11.0.0)

##### ⚠️ BREAKING CHANGES

- the strict RFC 8089 mode has been removed
- support for node 14 has been removed

##### Bug Fixes

-
[`9344167`](934416709c)
[#&#8203;135](https://redirect.github.com/npm/npm-package-arg/pull/135)
remove strict 8909 mode
([@&#8203;wraithgar](https://redirect.github.com/wraithgar))
-
[`5042ff2`](5042ff2bba)
[#&#8203;139](https://redirect.github.com/npm/npm-package-arg/pull/139)
drop node14 support
([@&#8203;lukekarrys](https://redirect.github.com/lukekarrys))

##### Dependencies

-
[`d2ab7ba`](d2ab7bade1)
[#&#8203;138](https://redirect.github.com/npm/npm-package-arg/pull/138)
bump hosted-git-info from 6.1.1 to 7.0.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/cnpm/cnpmcore).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTYuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE1Ni4xIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 23:30:04 +08:00
renovate[bot]
0c987a7225 chore(deps): update dependency oxlint-tsgolint to ^0.3.0 (#863)
[skip ci]

This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [oxlint-tsgolint](https://redirect.github.com/oxc-project/tsgolint) |
[`^0.2.0` ->
`^0.3.0`](https://renovatebot.com/diffs/npm/oxlint-tsgolint/0.2.1/0.3.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/oxlint-tsgolint/0.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxlint-tsgolint/0.2.1/0.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>oxc-project/tsgolint (oxlint-tsgolint)</summary>

###
[`v0.3.0`](https://redirect.github.com/oxc-project/tsgolint/releases/tag/v0.3.0)

[Compare
Source](https://redirect.github.com/oxc-project/tsgolint/compare/v0.2.1...v0.3.0)

#### What's Changed

- chore(deps): update typescript-go digest to
[`48b739c`](https://redirect.github.com/oxc-project/tsgolint/commit/48b739c)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;311](https://redirect.github.com/oxc-project/tsgolint/pull/311)
- chore(deps): update typescript-go digest to
[`06a7b84`](https://redirect.github.com/oxc-project/tsgolint/commit/06a7b84)
by [@&#8203;renovate](https://redirect.github.com/renovate)\[bot] in
[#&#8203;312](https://redirect.github.com/oxc-project/tsgolint/pull/312)
- chore(deps): update vitest to v4 by
[@&#8203;camc314](https://redirect.github.com/camc314) in
[#&#8203;314](https://redirect.github.com/oxc-project/tsgolint/pull/314)
- feat(rules): Implement `strict_boolean_expressions` by
[@&#8203;nnnnoel](https://redirect.github.com/nnnnoel) in
[#&#8203;222](https://redirect.github.com/oxc-project/tsgolint/pull/222)
- fix: resolve panic when compiling wildcard directory patterns and add
tests for non-ASCII characters by
[@&#8203;camc314](https://redirect.github.com/camc314) in
[#&#8203;319](https://redirect.github.com/oxc-project/tsgolint/pull/319)
- fix(strict-boolean-expressions): fix memory leak by
[@&#8203;camc314](https://redirect.github.com/camc314) in
[#&#8203;316](https://redirect.github.com/oxc-project/tsgolint/pull/316)

#### New Contributors

- [@&#8203;nnnnoel](https://redirect.github.com/nnnnoel) made their
first contribution in
[#&#8203;222](https://redirect.github.com/oxc-project/tsgolint/pull/222)

**Full Changelog**:
<https://github.com/oxc-project/tsgolint/compare/v0.2.1...v0.3.0>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/cnpm/cnpmcore).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTYuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE1Ni4xIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 23:29:18 +08:00
MK (fengmk2)
89f1250927 refactor: use all in one egg v4 (#855)
required https://github.com/eggjs/egg/pull/5654

---------

Signed-off-by: MK (fengmk2) <fengmk2@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-28 22:17:01 +08:00
renovate[bot]
e1848c71ec chore(deps): update dependency lint-staged to v16 (#853)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [lint-staged](https://redirect.github.com/lint-staged/lint-staged) |
[`^15.5.0` ->
`^16.0.0`](https://renovatebot.com/diffs/npm/lint-staged/15.5.2/16.2.6)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/lint-staged/16.2.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/lint-staged/15.5.2/16.2.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>lint-staged/lint-staged (lint-staged)</summary>

###
[`v16.2.6`](https://redirect.github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1626)

[Compare
Source](https://redirect.github.com/lint-staged/lint-staged/compare/v16.2.5...v16.2.6)

##### Patch Changes

-
[#&#8203;1693](https://redirect.github.com/lint-staged/lint-staged/pull/1693)
[`33d4502`](33d4502ef9)
Thanks
[@&#8203;Adrian-Baran-GY](https://redirect.github.com/Adrian-Baran-GY)!
- Fix problems with `--continue-on-error` option, where tasks might have
still been killed (`SIGINT`) when one of them failed.

###
[`v16.2.5`](https://redirect.github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1625)

[Compare
Source](https://redirect.github.com/lint-staged/lint-staged/compare/v16.2.4...v16.2.5)

##### Patch Changes

-
[#&#8203;1687](https://redirect.github.com/lint-staged/lint-staged/pull/1687)
[`9e02d9d`](9e02d9dc8a)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - Fix
unhandled promise rejection when spawning tasks (*instead of the tasks
themselves failing*). Previously when a task failed to spawn,
*lint-staged* also failed and the backup stash might not have been
automatically restored.

###
[`v16.2.4`](https://redirect.github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1624)

[Compare
Source](https://redirect.github.com/lint-staged/lint-staged/compare/v16.2.3...v16.2.4)

##### Patch Changes

-
[#&#8203;1682](https://redirect.github.com/lint-staged/lint-staged/pull/1682)
[`0176038`](01760380e5)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - Update
dependencies, including
[`nano-spawn@2.0.0`](https://redirect.github.com/sindresorhus/nano-spawn/releases/tag/v2.0.0)
with bug fixes.

-
[#&#8203;1671](https://redirect.github.com/lint-staged/lint-staged/pull/1671)
[`581a54e`](581a54eea6)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - Speed up
execution by only importing the `yaml` depedency if using YAML
configuration files.

###
[`v16.2.3`](https://redirect.github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1623)

[Compare
Source](https://redirect.github.com/lint-staged/lint-staged/compare/v16.2.2...v16.2.3)

##### Patch Changes

-
[#&#8203;1669](https://redirect.github.com/lint-staged/lint-staged/pull/1669)
[`27cd541`](27cd5413d9)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - When using
`--fail-on-changes`, automatically hidden (partially) unstaged changes
are no longer counted to make *lint-staged* fail.

###
[`v16.2.2`](https://redirect.github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1622)

[Compare
Source](https://redirect.github.com/lint-staged/lint-staged/compare/v16.2.1...v16.2.2)

##### Patch Changes

-
[#&#8203;1667](https://redirect.github.com/lint-staged/lint-staged/pull/1667)
[`699f95d`](699f95df8f)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - The backup
stash will not be dropped when using `--fail-on-changes` and there are
errors. When reverting to original state is disabled (via `--no-revert`
or `--fail-on-changes`), hidden (partially) unstaged changes are still
restored automatically so that it's easier to resolve the situation
manually.

Additionally, the example for using the backup stash manually now uses
the correct backup hash, if available:

  ```shell
  % npx lint-staged --fail-on-changes
  ✔ Backed up original state in git stash (c18d55a3)
  ✔ Running tasks for staged files...
  ✖ Tasks modified files and --fail-on-changes was used!
  ↓ Cleaning up temporary files...

  ✖ lint-staged failed because `--fail-on-changes` was used.

  Any lost modifications can be restored from a git stash:

    > git stash list --format="%h %s"
    c18d55a3 On main: lint-staged automatic backup
    > git apply --index c18d55a3
  ```

###
[`v16.2.1`](https://redirect.github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1621)

[Compare
Source](https://redirect.github.com/lint-staged/lint-staged/compare/v16.2.0...v16.2.1)

##### Patch Changes

-
[#&#8203;1664](https://redirect.github.com/lint-staged/lint-staged/pull/1664)
[`8277b3b`](8277b3b298)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - The
built-in TypeScript types have been updated to more closely match the
implementation. Notably, the list of staged files supplied to task
functions is `readonly string[]` and can't be mutated. Thanks
[@&#8203;outslept](https://redirect.github.com/outslept)!

  ```diff
  export default {
  ---  "*": (files: string[]) => void console.log('staged files', files)
+++ "*": (files: readonly string[]) => void console.log('staged files',
files)
  }
  ```

-
[#&#8203;1654](https://redirect.github.com/lint-staged/lint-staged/pull/1654)
[`70b9af3`](70b9af3ac3)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - This
version has been published from GitHub Actions using [Trusted Publishing
for npm packages](https://docs.npmjs.com/trusted-publishers).

-
[#&#8203;1659](https://redirect.github.com/lint-staged/lint-staged/pull/1659)
[`4996817`](49968170ab)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - Fix
searching configuration files when the working directory is a
subdirectory of a git repository, and there are `package.json` files in
the working directory. This situation might happen when running
*lint-staged* for a single package in a monorepo.

-
[#&#8203;1654](https://redirect.github.com/lint-staged/lint-staged/pull/1654)
[`7021f0a`](7021f0af40)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - Return the
caret semver range (`^`) to direct dependencies so that future patch and
minor versions are allowed. This enables projects to better maintain and
deduplicate their own transitive dependencies while not requiring direct
updates to *lint-staged*. This was changed in
[16.2.0](https://redirect.github.com/lint-staged/lint-staged/releases/tag/v16.2.0)
after the vulnerability issues with `chalk` and `debug`, which were also
removed in the same version.

Given the recent vulnerabilities in the *npm* ecosystem, it's best to be
very careful when updating dependencies.

###
[`v16.2.0`](https://redirect.github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1620)

[Compare
Source](https://redirect.github.com/lint-staged/lint-staged/compare/v16.1.6...v16.2.0)

##### Minor Changes

-
[#&#8203;1615](https://redirect.github.com/lint-staged/lint-staged/pull/1615)
[`99eb742`](99eb74200e)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - Added a new
option `--fail-on-changes` to make *lint-staged* exit with code 1 when
tasks modify any files, making the `precommit` hook fail. This is
similar to the `git diff --exit-code` option. Using this flag also
implies the `--no-revert` flag which means any changes made by tasks
will be left in the working tree after failing, so that they can be
manually staged and the commit tried again.

-
[#&#8203;1611](https://redirect.github.com/lint-staged/lint-staged/pull/1611)
[`cd05fd3`](cd05fd3495)
Thanks [@&#8203;rlorenzo](https://redirect.github.com/rlorenzo)! - Added
a new option `--continue-on-error` so that *lint-staged* will run all
tasks to completion even if some of them fail. By default, *lint-staded*
will exit early on the first failure.

-
[#&#8203;1637](https://redirect.github.com/lint-staged/lint-staged/pull/1637)
[`82fcc07`](82fcc0789c)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - Internal
*lint-staged* errors are now thrown and visible in the console output.
Previously they were caught with the process exit code set to 1, but not
logged. This happens when, for example, there's a syntax error in the
*lint-staged* configuration file.

-
[#&#8203;1647](https://redirect.github.com/lint-staged/lint-staged/pull/1647)
[`a5ecc06`](a5ecc0605d)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - Remove
[debug](https://redirect.github.com/debug-js/debug) as a dependency due
to recent malware issue; read more at
[debug-js/debug#1005](https://redirect.github.com/debug-js/debug/issues/1005).
Because of this, the `DEBUG` environment variable is no longer supported
— use the `--debug` to enable debugging

-
[#&#8203;1636](https://redirect.github.com/lint-staged/lint-staged/pull/1636)
[`8db2717`](8db2717574)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - Added a new
option `--hide-unstaged` so that *lint-staged* will hide all unstaged
changes to tracked files before running tasks. The changes will be
applied back after running the tasks. Note that the combination of flags
`--hide-unstaged --no-hide-partially-staged` isn't meaningful and
behaves the same as just `--hide-unstaged`.

Thanks to
[@&#8203;ItsNickBarry](https://redirect.github.com/ItsNickBarry) for the
idea and initial implementation in
[#&#8203;1552](https://redirect.github.com/lint-staged/lint-staged/pull/1552).

-
[#&#8203;1648](https://redirect.github.com/lint-staged/lint-staged/pull/1648)
[`7900b3b`](7900b3b79c)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - Remove
[lilconfig](https://redirect.github.com/antonk52/lilconfig) to reduce
reliance on third-party dependencies. It was used to find possible
config files outside of those tracked in Git, including from the parent
directories. This behavior has been moved directly into *lint-staged*
and should work about the same.

##### Patch Changes

-
[#&#8203;1633](https://redirect.github.com/lint-staged/lint-staged/pull/1633)
[`7f9e485`](7f9e485a98)
Thanks
[@&#8203;dependabot](https://redirect.github.com/apps/dependabot)! -
Bumps [listr2](https://redirect.github.com/listr2/listr2) from 9.0.3 to
9.0.4.

-
[#&#8203;1626](https://redirect.github.com/lint-staged/lint-staged/pull/1626)
[`99d5a9b`](99d5a9b0dd)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - Due to
recent phishing attacks, for example
[chalk@5.6.1](https://redirect.github.com/chalk/chalk/issues/656) was
released with malware. To avoid *lint-staged*'s users being at risk the
**direct dependencies are pinned to exact versions**, instead of
allowing future patch versions with the [caret (`^`)
range](https://docs.npmjs.com/cli/v6/using-npm/semver#caret-ranges-123-025-004).

-
[#&#8203;1588](https://redirect.github.com/lint-staged/lint-staged/pull/1588)
[`035bbf2`](035bbf268a)
Thanks [@&#8203;outslept](https://redirect.github.com/outslept)! -
Increase performance by listing staged files and searching for
configuration concurrently.

-
[#&#8203;1645](https://redirect.github.com/lint-staged/lint-staged/pull/1645)
[`deba3ad`](deba3ad835)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - Remove
[chalk](https://redirect.github.com/chalk/chalk) as a dependency due to
recent malware issue; read more at
[chalk/chalk#656](https://redirect.github.com/chalk/chalk/issues/656).

If you are having trouble with ANSI color codes when using
*lint-staged*, you can try setting either `FORCE_COLOR=true` or
`NO_COLOR=true` env variables.

###
[`v16.1.6`](https://redirect.github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1616)

[Compare
Source](https://redirect.github.com/lint-staged/lint-staged/compare/v16.1.5...v16.1.6)

##### Patch Changes

-
[#&#8203;1610](https://redirect.github.com/lint-staged/lint-staged/pull/1610)
[`e93578e`](e93578e265)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - Try to
improve terminating of subprocess of tasks by using `SIGKILL`, and only
calling `pidtree` when the the main task process has a known pid.

###
[`v16.1.5`](https://redirect.github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1615)

[Compare
Source](https://redirect.github.com/lint-staged/lint-staged/compare/v16.1.4...v16.1.5)

##### Patch Changes

-
[#&#8203;1608](https://redirect.github.com/lint-staged/lint-staged/pull/1608)
[`4e3ce22`](4e3ce225b3)
Thanks [@&#8203;srsatt](https://redirect.github.com/srsatt)! - Detect
the git repo's top-level directory correctly when in a worktree.

###
[`v16.1.4`](https://redirect.github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1614)

[Compare
Source](https://redirect.github.com/lint-staged/lint-staged/compare/v16.1.3...v16.1.4)

##### Patch Changes

-
[#&#8203;1604](https://redirect.github.com/lint-staged/lint-staged/pull/1604)
[`90b37b0`](90b37b00c2)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - Add another
`types` field to `package.json` to make even more sure NPM detects that
*lint-staged* includes built-in TypeScript type definitions.

###
[`v16.1.3`](https://redirect.github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1613)

[Compare
Source](https://redirect.github.com/lint-staged/lint-staged/compare/v16.1.2...v16.1.3)

##### Patch Changes

-
[#&#8203;1602](https://redirect.github.com/lint-staged/lint-staged/pull/1602)
[`7ea700b`](7ea700bcf3)
Thanks [@&#8203;dword-design](https://redirect.github.com/dword-design)!
- Add the `types` field to `package.json` to make sure NPM detects
*lint-staged* includes built-in TypeScript type definitions.

###
[`v16.1.2`](https://redirect.github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1612)

[Compare
Source](https://redirect.github.com/lint-staged/lint-staged/compare/v16.1.1...v16.1.2)

##### Patch Changes

-
[#&#8203;1570](https://redirect.github.com/lint-staged/lint-staged/pull/1570)
[`a7c0c88`](a7c0c88bcf)
Thanks [@&#8203;ItsNickBarry](https://redirect.github.com/ItsNickBarry)!
- When using `--diff-filter` with the `D` option to include deleted
staged files, *lint-staged* no longer tries to stage the deleted files,
unless they're no longer deleted. Previously this caused an error from
`git add` like `fatal: pathspec 'deleted-file' did not match any files`.

-
[`38f942e`](38f942ecc4)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - Removed an
extraneous log entry that printed `shouldHidePArtiallyStagedFiles` to
console output.

###
[`v16.1.1`](https://redirect.github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1611)

[Compare
Source](https://redirect.github.com/lint-staged/lint-staged/compare/v16.1.0...v16.1.1)

##### Patch Changes

-
[#&#8203;1565](https://redirect.github.com/lint-staged/lint-staged/pull/1565)
[`3686977`](3686977ccd)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! -
*Lint-staged* now explicitly warns about potential data loss when using
`--no-stash`.

-
[#&#8203;1571](https://redirect.github.com/lint-staged/lint-staged/pull/1571)
[`02299a9`](02299a9e4f)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - Function
tasks (introduced in v16.0.0) only receive the staged files matching the
configured glob, instead of all staged files.

-
[#&#8203;1563](https://redirect.github.com/lint-staged/lint-staged/pull/1563)
[`bc61c74`](bc61c74383)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - This
version fixes incorrect behavior where unstaged changes were committed
when using the `--no-stash` option. This happened because `--no-stash`
implied `--no-hide-partially-staged`, meaning unstaged changes to files
which also had other staged changes were added to the commit by
*lint-staged*; this is no longer the case.

The previous (incorrect) behavior can still be achieved by using both
options `--no-stash --no-hide-partially-staged` at the same time.

###
[`v16.1.0`](https://redirect.github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1610)

[Compare
Source](https://redirect.github.com/lint-staged/lint-staged/compare/v16.0.0...v16.1.0)

##### Minor Changes

-
[#&#8203;1536](https://redirect.github.com/lint-staged/lint-staged/pull/1536)
[`e729daa`](e729daa3b3)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - A new flag
`--no-revert` has been introduced for when task modifications should be
applied to the index before aborting the commit in case of errors. By
default, *lint-staged* will clear all task modifications and revert to
the original state.

-
[#&#8203;1550](https://redirect.github.com/lint-staged/lint-staged/pull/1550)
[`b27fa3f`](b27fa3fecb)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! -
*Lint-staged* now ignores symlinks and leaves them out from the list of
staged files.

##### Patch Changes

-
[#&#8203;1558](https://redirect.github.com/lint-staged/lint-staged/pull/1558)
[`c37dc38`](c37dc38ddd)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - The minimum
required Node.js version is lowered to `20.17` following
[nano-spawn@1.0.2](https://redirect.github.com/sindresorhus/nano-spawn/releases/tag/v1.0.2).

###
[`v16.0.0`](https://redirect.github.com/lint-staged/lint-staged/blob/HEAD/CHANGELOG.md#1600)

[Compare
Source](https://redirect.github.com/lint-staged/lint-staged/compare/v15.5.2...v16.0.0)

##### Major Changes

-
[#&#8203;1546](https://redirect.github.com/lint-staged/lint-staged/pull/1546)
[`158d15c`](158d15c9ae)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - Processes
are spawned using
[nano-spawn](https://redirect.github.com/sindresorhus/nano-spawn)
instead of [execa](https://redirect.github.com/sindresorhus/execa). If
you are using Node.js scripts as tasks, you might need to explicitly run
them with `node`, especially when using Windows:

  ```json
  {
    "*.js": "node my-js-linter.js"
  }
  ```

-
[#&#8203;1546](https://redirect.github.com/lint-staged/lint-staged/pull/1546)
[`158d15c`](158d15c9ae)
Thanks [@&#8203;iiroj](https://redirect.github.com/iiroj)! - The
`--shell` flag has been removed and *lint-staged* no longer supports
evaluating commands directly via a shell. To migrate existing commands,
you can create a shell script and invoke it instead. Lint-staged will
pass matched staged files as a list of arguments, accessible via
`"$@&#8203;"`:

  ```shell
  ```

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/cnpm/cnpmcore).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTYuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE1Ni4xIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 17:43:27 +00:00
renovate[bot]
e8a3ee5208 chore(deps): update actions/checkout action to v5 (#849)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/checkout](https://redirect.github.com/actions/checkout) |
action | major | `v4` -> `v5` |

---

### Release Notes

<details>
<summary>actions/checkout (actions/checkout)</summary>

### [`v5`](https://redirect.github.com/actions/checkout/compare/v4...v5)

[Compare
Source](https://redirect.github.com/actions/checkout/compare/v4...v5)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/cnpm/cnpmcore).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTYuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE1Ni4xIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 17:09:52 +00:00
renovate[bot]
3671c52513 chore(deps): update actions/setup-node action to v6 (#851)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/setup-node](https://redirect.github.com/actions/setup-node) |
action | major | `v4` -> `v6` |

---

### Release Notes

<details>
<summary>actions/setup-node (actions/setup-node)</summary>

###
[`v6`](https://redirect.github.com/actions/setup-node/compare/v5...v6)

[Compare
Source](https://redirect.github.com/actions/setup-node/compare/v5...v6)

###
[`v5`](https://redirect.github.com/actions/setup-node/compare/v4...v5)

[Compare
Source](https://redirect.github.com/actions/setup-node/compare/v4...v5)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/cnpm/cnpmcore).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTYuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE1Ni4xIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 17:09:10 +00:00
renovate[bot]
35a7be34e8 chore(deps): update actions/first-interaction action to v3 (#850)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[actions/first-interaction](https://redirect.github.com/actions/first-interaction)
| action | major | `v1` -> `v3` |

---

### Release Notes

<details>
<summary>actions/first-interaction (actions/first-interaction)</summary>

###
[`v3`](https://redirect.github.com/actions/first-interaction/compare/v2...v3)

[Compare
Source](https://redirect.github.com/actions/first-interaction/compare/v2...v3)

###
[`v2`](https://redirect.github.com/actions/first-interaction/compare/v1...v2)

[Compare
Source](https://redirect.github.com/actions/first-interaction/compare/v1...v2)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/cnpm/cnpmcore).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTYuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE1Ni4xIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 23:12:06 +08:00
renovate[bot]
957f43dab1 chore(deps): update actions/attest-build-provenance action to v3 (#847)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[actions/attest-build-provenance](https://redirect.github.com/actions/attest-build-provenance)
| action | major | `v2` -> `v3` |

---

### Release Notes

<details>
<summary>actions/attest-build-provenance
(actions/attest-build-provenance)</summary>

###
[`v3`](https://redirect.github.com/actions/attest-build-provenance/compare/v2...v3)

[Compare
Source](https://redirect.github.com/actions/attest-build-provenance/compare/v2...v3)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/cnpm/cnpmcore).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTYuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE1Ni4xIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 23:10:32 +08:00
renovate[bot]
4213bd8823 chore(deps): update dependency mocha to v11.7.4 (#846)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [mocha](https://mochajs.org/)
([source](https://redirect.github.com/mochajs/mocha)) | [`11.6.0` ->
`11.7.4`](https://renovatebot.com/diffs/npm/mocha/11.6.0/11.7.4) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/mocha/11.7.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/mocha/11.6.0/11.7.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>mochajs/mocha (mocha)</summary>

###
[`v11.7.4`](https://redirect.github.com/mochajs/mocha/blob/HEAD/CHANGELOG.md#1174-2025-10-01)

[Compare
Source](https://redirect.github.com/mochajs/mocha/compare/v11.7.3...v11.7.4)

##### 🩹 Fixes

- watch mode using chokidar v4
([#&#8203;5379](https://redirect.github.com/mochajs/mocha/issues/5379))
([c2667c3](c2667c3b3f))

##### 📚 Documentation

- migrate remaining legacy wiki pages to main documentation
([#&#8203;5465](https://redirect.github.com/mochajs/mocha/issues/5465))
([bff9166](bff9166073))

##### 🧹 Chores

- remove trailing spaces
([#&#8203;5475](https://redirect.github.com/mochajs/mocha/issues/5475))
([7f68e5c](7f68e5c156))

###
[`v11.7.3`](https://redirect.github.com/mochajs/mocha/blob/HEAD/CHANGELOG.md#1173-2025-09-30)

[Compare
Source](https://redirect.github.com/mochajs/mocha/compare/v11.7.2...v11.7.3)

##### 🩹 Fixes

- use original require() error for TS files if
ERR\_UNKNOWN\_FILE\_EXTENSION
([#&#8203;5408](https://redirect.github.com/mochajs/mocha/issues/5408))
([ebdbc48](ebdbc48769))

##### 📚 Documentation

- add security escalation policy
([#&#8203;5466](https://redirect.github.com/mochajs/mocha/issues/5466))
([4122c7d](4122c7d13d))
- fix duplicate global leak documentation
([#&#8203;5461](https://redirect.github.com/mochajs/mocha/issues/5461))
([1164b9d](1164b9da89))
- migrate third party UIs wiki page to docs
([#&#8203;5434](https://redirect.github.com/mochajs/mocha/issues/5434))
([6654704](66547045cb))
- update maintainer release notes for release-please
([#&#8203;5453](https://redirect.github.com/mochajs/mocha/issues/5453))
([185ae1e](185ae1eabe))

##### 🤖 Automation

- **deps:** bump actions/setup-node in the github-actions group
([#&#8203;5459](https://redirect.github.com/mochajs/mocha/issues/5459))
([48c6f40](48c6f4068b))

###
[`v11.7.2`](https://redirect.github.com/mochajs/mocha/blob/HEAD/CHANGELOG.md#1172-2025-09-01)

[Compare
Source](https://redirect.github.com/mochajs/mocha/compare/v11.7.1...v11.7.2)

##### 🩹 Fixes

- fail with an informative error message on a file with a broken default
import
([#&#8203;5413](https://redirect.github.com/mochajs/mocha/issues/5413))
([b0e6135](b0e6135059))
- load mjs files correctly
([#&#8203;5429](https://redirect.github.com/mochajs/mocha/issues/5429))
([a947b9b](a947b9b955))

##### 📚 Documentation

- add banner from old site to new site, link from new to old
([#&#8203;5414](https://redirect.github.com/mochajs/mocha/issues/5414))
([dedef11](dedef110a2))
- add info on spies to legacy docs
([#&#8203;5421](https://redirect.github.com/mochajs/mocha/issues/5421))
([21f5544](21f554459c))
- explain node import swallowing error
([#&#8203;5401](https://redirect.github.com/mochajs/mocha/issues/5401))
([09f5b2c](09f5b2c9de))
- fix links in new site
([#&#8203;5416](https://redirect.github.com/mochajs/mocha/issues/5416))
([b2bc769](b2bc769c6c))
- migrate assertion libraries wiki link to main docs
([#&#8203;5442](https://redirect.github.com/mochajs/mocha/issues/5442))
([95f3ca8](95f3ca8bc3))
- migrate count assertions wiki page to docs
([#&#8203;5438](https://redirect.github.com/mochajs/mocha/issues/5438))
([02a306c](02a306c6cb))
- migrate shared behaviours to docs-next
([#&#8203;5432](https://redirect.github.com/mochajs/mocha/issues/5432))
([1dc4aa9](1dc4aa98eb))
- migrate Spies wiki page to explainers
([#&#8203;5420](https://redirect.github.com/mochajs/mocha/issues/5420))
([cbcf007](cbcf007c5a))
- Migrate tagging wiki page to docs
([#&#8203;5435](https://redirect.github.com/mochajs/mocha/issues/5435))
([876247a](876247a8a6))
- migrate third party reporters wiki page to docs
([#&#8203;5433](https://redirect.github.com/mochajs/mocha/issues/5433))
([f70764c](f70764c9a5))
- migrate to global leak wiki page to docs
([#&#8203;5437](https://redirect.github.com/mochajs/mocha/issues/5437))
([8a6fdca](8a6fdcafcc))
- update /next bug report link to be docs issue template
([#&#8203;5424](https://redirect.github.com/mochajs/mocha/issues/5424))
([668cb66](668cb66e12))

##### 🧹 Chores

- add issue form for ️ Performance
([#&#8203;5406](https://redirect.github.com/mochajs/mocha/issues/5406))
([a908b3b](a908b3b866))
- add test for `-R import-only-loader`
([#&#8203;5391](https://redirect.github.com/mochajs/mocha/issues/5391))
([6ee5b48](6ee5b483b8))
- also test Node.js 24 in CI
([#&#8203;5405](https://redirect.github.com/mochajs/mocha/issues/5405))
([15f5980](15f5980528))
- bump CI to use 20.19.4, 22.18.0, 24.6.0
([#&#8203;5430](https://redirect.github.com/mochajs/mocha/issues/5430))
([ace5eb4](ace5eb47a7))
- bump Knip to 5.61.2
([#&#8203;5394](https://redirect.github.com/mochajs/mocha/issues/5394))
([f3d7430](f3d743061d))
- cleanup references of --opts
([#&#8203;5402](https://redirect.github.com/mochajs/mocha/issues/5402))
([1096b37](1096b376c3))
- enabled ESLint's no-unused-vars
([#&#8203;5399](https://redirect.github.com/mochajs/mocha/issues/5399))
([d4168ae](d4168aef4c))
- move callback and object typedefs to a new types.d.ts
([#&#8203;5351](https://redirect.github.com/mochajs/mocha/issues/5351))
([3300d21](3300d2155a))
- rewrite base path instead of copy-pasting
([#&#8203;5431](https://redirect.github.com/mochajs/mocha/issues/5431))
([c6c6740](c6c6740fb4))
- unify caught errors as err
([#&#8203;5439](https://redirect.github.com/mochajs/mocha/issues/5439))
([d4912e7](d4912e705c))
- Update experimental module detection test and pin exact Node versions
([#&#8203;5417](https://redirect.github.com/mochajs/mocha/issues/5417))
([2489090](2489090223))

##### 🤖 Automation

- **deps:** bump actions/checkout in the github-actions group
([#&#8203;5419](https://redirect.github.com/mochajs/mocha/issues/5419))
([03ac2d0](03ac2d0e6e))

###
[`v11.7.1`](https://redirect.github.com/mochajs/mocha/blob/HEAD/CHANGELOG.md#1171-2025-06-24)

[Compare
Source](https://redirect.github.com/mochajs/mocha/compare/v11.7.0...v11.7.1)

##### 🩹 Fixes

- always fallback to import() if require() fails
([#&#8203;5384](https://redirect.github.com/mochajs/mocha/issues/5384))
([295c168](295c168628))

##### 🧹 Chores

- add esm loader test
([#&#8203;5383](https://redirect.github.com/mochajs/mocha/issues/5383))
([f58e49f](f58e49f08d))

###
[`v11.7.0`](https://redirect.github.com/mochajs/mocha/blob/HEAD/CHANGELOG.md#1170-2025-06-18)

[Compare
Source](https://redirect.github.com/mochajs/mocha/compare/v11.6.0...v11.7.0)

##### 🌟 Features

- use require to load esm
([#&#8203;5366](https://redirect.github.com/mochajs/mocha/issues/5366))
([41e24a2](41e24a2429))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/cnpm/cnpmcore).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNTYuMSIsInVwZGF0ZWRJblZlciI6IjQxLjE1Ni4xIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 23:09:20 +08:00
renovate[bot]
f3f3584d62 chore: Configure Renovate (#845)
Welcome to [Renovate](https://redirect.github.com/renovatebot/renovate)!
This is an onboarding PR to help you understand and configure settings
before regular Pull Requests begin.

🚦 To activate Renovate, merge this Pull Request. To disable Renovate,
simply close this Pull Request unmerged.



---
### Detected Package Files

 * `docker-compose-es.yml` (docker-compose)
 * `docker-compose-postgres.yml` (docker-compose)
 * `docker-compose.yml` (docker-compose)
 * `.docker/alpine/Dockerfile` (dockerfile)
 * `.docker/debian/Dockerfile` (dockerfile)
 * `.github/workflows/greetings.yml` (github-actions)
 * `.github/workflows/nodejs.yml` (github-actions)
 * `.github/workflows/release-image.yml` (github-actions)
 * `.github/workflows/release.yml` (github-actions)
 * `app/common/package.json` (npm)
 * `app/core/package.json` (npm)
 * `app/infra/package.json` (npm)
 * `app/port/package.json` (npm)
 * `app/repository/package.json` (npm)
 * `package.json` (npm)

### Configuration Summary

Based on the default config's presets, Renovate will:

  - Start dependency updates only once this onboarding PR is merged
  - Hopefully safe environment variables to allow users to configure.
  - Show all Merge Confidence badges for pull requests.
  - Enable Renovate Dependency Dashboard creation.
- Use semantic commit type `fix` for dependencies and `chore` for all
others if semantic commits are in use.
- Ignore `node_modules`, `bower_components`, `vendor` and various
test/tests (except for nuget) directories.
  - Group known monorepo packages together.
  - Use curated list of recommended non-monorepo package groupings.
- Show only the Age and Confidence Merge Confidence badges for pull
requests.
  - Apply crowd-sourced package replacement rules.
  - Apply crowd-sourced workarounds for known problems with packages.

🔡 Do you want to change how Renovate upgrades your dependencies? Add
your custom config to `renovate.json` in this branch. Renovate will
update the Pull Request description the next time it runs.

---

### What to Expect

With your current configuration, Renovate will create 21 Pull Requests:

<details>
<summary>chore(deps): update dependency mocha to v11.7.4</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/mocha-11.x`
  - Merge into: `master`
- Upgrade [mocha](https://redirect.github.com/mochajs/mocha) to `11.7.4`


</details>

<details>
<summary>chore(deps): update actions/attest-build-provenance action to
v3</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/actions-attest-build-provenance-3.x`
  - Merge into: `master`
- Upgrade
[actions/attest-build-provenance](https://redirect.github.com/actions/attest-build-provenance)
to `v3`


</details>

<details>
<summary>chore(deps): update actions/checkout action to v5</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/actions-checkout-5.x`
  - Merge into: `master`
- Upgrade
[actions/checkout](https://redirect.github.com/actions/checkout) to `v5`


</details>

<details>
<summary>chore(deps): update actions/first-interaction action to
v3</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/actions-first-interaction-3.x`
  - Merge into: `master`
- Upgrade
[actions/first-interaction](https://redirect.github.com/actions/first-interaction)
to `v3`


</details>

<details>
<summary>chore(deps): update actions/setup-node action to v6</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/actions-setup-node-6.x`
  - Merge into: `master`
- Upgrade
[actions/setup-node](https://redirect.github.com/actions/setup-node) to
`v6`


</details>

<details>
<summary>chore(deps): update dependency
@&#8203;simplewebauthn/typescript-types to v8</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/simplewebauthn-typescript-types-8.x`
  - Merge into: `master`
- Upgrade
[@simplewebauthn/typescript-types](https://redirect.github.com/MasterKale/SimpleWebAuthn)
to `^8.0.0`


</details>

<details>
<summary>chore(deps): update dependency lint-staged to v16</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/lint-staged-16.x`
  - Merge into: `master`
- Upgrade
[lint-staged](https://redirect.github.com/lint-staged/lint-staged) to
`^16.0.0`


</details>

<details>
<summary>chore(deps): update mysql docker tag to v9</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/mysql-9.x`
  - Merge into: `master`
  - Upgrade mysql to `9.5`


</details>

<details>
<summary>chore(deps): update postgres docker tag to v18</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/postgres-18.x`
  - Merge into: `master`
  - Upgrade postgres to `18`


</details>

<details>
<summary>chore(deps): update redis docker tag to v8</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/redis-8.x`
  - Merge into: `master`
  - Upgrade redis to `8-alpine`


</details>

<details>
<summary>fix(deps): update dependency @&#8203;elastic/elasticsearch to
v9</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/elastic-elasticsearch-9.x`
  - Merge into: `master`
- Upgrade
[@elastic/elasticsearch](https://redirect.github.com/elastic/elasticsearch-js)
to `^9.0.0`


</details>

<details>
<summary>fix(deps): update dependency @&#8203;simplewebauthn/server to
v13</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/simplewebauthn-server-13.x`
  - Merge into: `master`
- Upgrade
[@simplewebauthn/server](https://redirect.github.com/MasterKale/SimpleWebAuthn)
to `^13.0.0`


</details>

<details>
<summary>fix(deps): update dependency base-x to v5</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/base-x-5.x`
  - Merge into: `master`
- Upgrade [base-x](https://redirect.github.com/cryptocoinjs/base-x) to
`^5.0.0`


</details>

<details>
<summary>fix(deps): update dependency mime-types to v3</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/mime-types-3.x`
  - Merge into: `master`
- Upgrade [mime-types](https://redirect.github.com/jshttp/mime-types) to
`^3.0.0`
- Upgrade
[@types/mime-types](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped)
to `^3.0.0`


</details>

<details>
<summary>fix(deps): update dependency npm-package-arg to v13</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/npm-package-arg-13.x`
  - Merge into: `master`
- Upgrade
[npm-package-arg](https://redirect.github.com/npm/npm-package-arg) to
`^13.0.0`


</details>

<details>
<summary>fix(deps): update dependency p-map to v7</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/p-map-7.x`
  - Merge into: `master`
- Upgrade [p-map](https://redirect.github.com/sindresorhus/p-map) to
`^7.0.0`


</details>

<details>
<summary>fix(deps): update dependency read-env-value to v2</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/read-env-value-2.x`
  - Merge into: `master`
- Upgrade
[read-env-value](https://redirect.github.com/node-modules/read-env-value)
to `^2.0.0`


</details>

<details>
<summary>fix(deps): update dependency ssri to v13</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/ssri-13.x`
  - Merge into: `master`
  - Upgrade [ssri](https://redirect.github.com/npm/ssri) to `^13.0.0`


</details>

<details>
<summary>fix(deps): update dependency type-fest to v5</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/type-fest-5.x`
  - Merge into: `master`
- Upgrade
[type-fest](https://redirect.github.com/sindresorhus/type-fest) to
`^5.0.0`


</details>

<details>
<summary>fix(deps): update dependency ua-parser-js to v2</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/ua-parser-js-2.x`
  - Merge into: `master`
- Upgrade
[ua-parser-js](https://redirect.github.com/faisalman/ua-parser-js) to
`^2.0.0`


</details>

<details>
<summary>fix(deps): update dependency validate-npm-package-name to
v7</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/validate-npm-package-name-7.x`
  - Merge into: `master`
- Upgrade
[validate-npm-package-name](https://redirect.github.com/npm/validate-npm-package-name)
to `^7.0.0`


</details>



🚸 Branch creation will be limited to maximum 2 per hour, so it doesn't
swamp any CI resources or overwhelm the project. See docs for
`prhourlylimit` for details.


---

 Got questions? Check out Renovate's
[Docs](https://docs.renovatebot.com/), particularly the Getting Started
section.
If you need any further assistance then you can also [request help
here](https://redirect.github.com/renovatebot/renovate/discussions).


---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/cnpm/cnpmcore).


<!--renovate-config-hash:e80b4e42a3043bc12fa0640db4bac392d2bf770acf841360d7c8ceeeac2ec1a9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 04:54:57 +00:00
semantic-release-bot
80663505cb Release 4.11.1
[skip ci]

## <small>4.11.1 (2025-10-22)</small>

* fix: improve TypeScript type definitions across codebase (#844) ([e5162f2](https://github.com/cnpm/cnpmcore/commit/e5162f2)), closes [#844](https://github.com/cnpm/cnpmcore/issues/844)
2025-10-22 17:01:17 +00:00
MK (fengmk2)
e5162f20aa fix: improve TypeScript type definitions across codebase (#844)
This commit enhances type safety and fixes type-related issues
throughout the project including:
- Updated type definitions in entities, repositories, and models
- Improved type annotations in services and controllers
- Fixed type issues in adapters and utilities
- Enhanced test file type definitions
- Added typings/index.d.ts for global type declarations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-23 00:58:59 +08:00
semantic-release-bot
b6c96defa4 Release 4.11.0
[skip ci]

## 4.11.0 (2025-10-15)

* feat: perpage config (#843) ([3e1dbd8](https://github.com/cnpm/cnpmcore/commit/3e1dbd8)), closes [#843](https://github.com/cnpm/cnpmcore/issues/843)
2025-10-15 16:02:23 +00:00
elrrrrrrr
3e1dbd819c feat: perpage config (#843) 2025-10-16 00:00:21 +08:00
semantic-release-bot
faee3b96f1 Release 4.10.0
[skip ci]

## 4.10.0 (2025-10-15)

* feat: Add time field to abbreviated metadata for pnpm time-based resolution (#834) ([2e51399](https://github.com/cnpm/cnpmcore/commit/2e51399)), closes [#834](https://github.com/cnpm/cnpmcore/issues/834)
* feat: mirror microsoft/ripgrep-prebuilt binary (#842) ([606c983](https://github.com/cnpm/cnpmcore/commit/606c983)), closes [#842](https://github.com/cnpm/cnpmcore/issues/842) [cnpm/cnpmcore#841](https://github.com/cnpm/cnpmcore/issues/841)
* docs: Add comprehensive internal API documentation for direct HTTP requests (#832) ([5b1da74](https://github.com/cnpm/cnpmcore/commit/5b1da74)), closes [#832](https://github.com/cnpm/cnpmcore/issues/832) [#709](https://github.com/cnpm/cnpmcore/issues/709)
* docs: Enhance Copilot instructions with DDD architecture [skip ci] (#838) ([cb93141](https://github.com/cnpm/cnpmcore/commit/cb93141)), closes [#838](https://github.com/cnpm/cnpmcore/issues/838) [cnpm/cnpmcore#837](https://github.com/cnpm/cnpmcore/issues/837)
* test: enable test shard (#839) ([324511d](https://github.com/cnpm/cnpmcore/commit/324511d)), closes [#839](https://github.com/cnpm/cnpmcore/issues/839)
* refactor: use tegg and egg v4 beta (#836) ([c7df471](https://github.com/cnpm/cnpmcore/commit/c7df471)), closes [#836](https://github.com/cnpm/cnpmcore/issues/836)
2025-10-15 08:15:50 +00:00
Copilot
606c983363 feat: mirror microsoft/ripgrep-prebuilt binary (#842)
- [x] Add `ripgrep-prebuilt` configuration to `config/binaries.ts`
- [x] Create test file for `ripgrep-prebuilt` binary
- [x] Create test fixture data for releases
- [x] Run and validate tests
- [x] Verify linting passes
- [x] Address code review feedback - use assert.equal instead of
assert.ok

<!-- START COPILOT CODING AGENT SUFFIX -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>mirror microsoft/ripgrep-prebuilt</issue_title>
>
<issue_description>https://github.com/microsoft/ripgrep-prebuilt/releases
> 
> part of
https://github.com/cnpm/binary-mirror-config/issues/57</issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>

Fixes cnpm/cnpmcore#841

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 Share your feedback on Copilot coding agent for the chance to win a
$200 gift card! Click
[here](https://survey3.medallia.com/?EAHeSx-AP01bZqG0Ld9QLQ) to start
the survey.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: fengmk2 <156269+fengmk2@users.noreply.github.com>
Co-authored-by: MK <fengmk2@gmail.com>
2025-10-15 08:10:04 +00:00
Copilot
cb931417c1 docs: Enhance Copilot instructions with DDD architecture [skip ci] (#838)
## Overview

This PR significantly enhances the `.github/copilot-instructions.md`
file to provide comprehensive guidance for GitHub Copilot when assisting
with cnpmcore development. The instructions have been expanded from 281
lines to 564 lines, adding critical sections that align with GitHub's
best practices for coding agents.

## What Changed

### New Sections Added

**1. Code Style and Conventions**
- Detailed Oxlint and Prettier configuration rules
- TypeScript conventions (strict typing, avoiding `any`, ES modules)
- Testing conventions with naming patterns and mock usage
- Complete code examples for test structure

**2. Domain-Driven Design (DDD) Architecture**
- Visual layer architecture showing dependency flow
- Detailed responsibilities for each layer:
  - Controller: HTTP interface, validation, authentication
  - Service: Business logic orchestration
  - Repository: Data access and persistence
  - Entity: Domain models with business behavior
  - Model: ORM definitions
- Repository method naming conventions (`findX`, `saveX`, `removeX`,
`listXs`)
- Request validation trilogy workflow (params → auth → authorization)
- Database model modification guidelines (update all 3 locations)

**3. Infrastructure Adapters**
- Documentation of enterprise customization points
- Adapter types: NFSClientAdapter, QueueAdapter, AuthAdapter,
BinaryAdapter

**4. Semantic Commit Messages**
- Conventional commit format standards
- Real-world examples for feat, fix, docs, chore, test, refactor, perf

### Sections Enhanced

**Adding New Features** (7 lines → 69 lines)
- Step-by-step bottom-up implementation workflow
- Layer-by-layer implementation guidance
- Complete TypeScript controller code example with decorators:
  ```typescript
  @HTTPController()
  export class YourController extends AbstractController {
    @Inject()
    private readonly yourService: YourService;

    @HTTPMethod({ path: '/api/path', method: 'GET' })
    async yourMethod(@HTTPQuery() params: YourQueryType) {
      // 1. Validate params, 2. Authenticate, 3. Authorize, 4. Execute
    }
  }
  ```

**Testing** (15 lines → 42 lines)
- Testing philosophy (test all features, test at right layer, mock
dependencies)
- Common test patterns with setup/execute/assert structure
- Examples for both success and error cases

**Performance Notes**
- Added individual test file timing (~12 seconds)
- Added linting speed (<1 second)
- Comprehensive timing reference for CI/CD planning

## Quality Validation

 **Markdown Syntax**: All 48 code blocks properly balanced and closed  
 **Completeness**: All 13 key sections present with practical examples
 **Code Examples**: 21 TypeScript code examples throughout  
 **Best Practices**: Follows GitHub's recommended structure for Copilot
instructions

## Benefits

With these enhancements, GitHub Copilot can now:
- Generate code following cnpmcore's DDD architecture patterns
- Apply correct code style (Prettier + Oxlint rules)
- Create properly structured tests with appropriate mocking
- Follow repository naming conventions
- Implement the 3-step request validation pattern
- Suggest appropriate architectural layers for new features
- Generate semantic commit messages

## Related

Closes #[issue_number]

The instructions now provide comprehensive, actionable guidance that
enables GitHub Copilot to be a more effective development assistant
while maintaining code quality and architectural consistency across the
cnpmcore project.

<!-- START COPILOT CODING AGENT SUFFIX -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title> Set up Copilot instructions</issue_title>
> <issue_description>Configure instructions for this repository as
documented in [Best practices for Copilot coding agent in your
repository](https://gh.io/copilot-coding-agent-tips).
> 
> <Onboard this repo></issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>
Fixes cnpm/cnpmcore#837

<!-- START COPILOT CODING AGENT TIPS -->
---

 Let Copilot coding agent [set things up for
you](https://github.com/cnpm/cnpmcore/issues/new?title=+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)
— coding agent works faster and does higher quality work when set up for
your repo.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: fengmk2 <156269+fengmk2@users.noreply.github.com>
Co-authored-by: MK (fengmk2) <fengmk2@gmail.com>
2025-10-07 22:15:30 +08:00
MK (fengmk2)
324511d159 test: enable test shard (#839)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Improved CI parallelization and shard-aware job naming for faster,
clearer test runs.

* **Bug Fixes**
* More robust handling of binary paths and change-stream edge cases to
reduce rare failures.
* Prevented unintended data mutation and tightened minor
version-handling resilience.

* **Chores**
  * Streamlined test scripts and updated dependency resolution.
* Added Prettier ignore rules, relaxed selected lint rules, and small
build script formatting cleanup.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-07 22:14:14 +08:00
Copilot
2e51399db1 feat: Add time field to abbreviated metadata for pnpm time-based resolution (#834)
- [x] Add time field to abbreviated manifests in
`_listPackageAbbreviatedManifests` method
- [x] Fix cache update logic to populate time field when adding versions
to abbreviated manifests
- [x] Fix cache update logic to remove time field when removing versions
from abbreviated manifests
- [x] Add comprehensive test coverage for time field in abbreviated
manifests
- [x] All tests passing (30/30 in ShowPackageController.test.ts)

closes https://github.com/cnpm/cnpmcore/issues/609

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Package metadata now consistently includes time information (created,
modified, and per-version publish timestamps) in both full and
abbreviated manifests.
* Removing a version also removes its corresponding time entry to keep
metadata accurate.
* Added guards to prevent writing invalid time data, improving
stability.

* **Tests**
* Added tests to verify presence and correctness of time fields in
abbreviated manifests, including created/modified and per-version
timestamps.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: fengmk2 <156269+fengmk2@users.noreply.github.com>
Co-authored-by: MK (fengmk2) <fengmk2@gmail.com>
2025-10-06 15:37:26 +08:00
Copilot
5b1da74746 docs: Add comprehensive internal API documentation for direct HTTP requests (#832)
This PR adds comprehensive documentation for cnpmcore's internal APIs,
enabling users to make direct HTTP requests without needing the cnpm
client with outdated dependencies.

## Background

Users requested documentation for internal APIs to avoid using the cnpm
client, which has outdated dependencies like `bagpipe` (11 years old)
and `npm-request` (9 years old). The primary use case is package
synchronization functionality that was previously only accessible
through the cnpm CLI.

## Changes

### New Documentation: `docs/internal-api.md`

Created a comprehensive 988-line API reference covering:

**Package Sync API** - The primary requested functionality:
- `PUT /-/package/:fullname/syncs` - Modern sync API
- `PUT /:fullname/sync` - Legacy sync API (cnpmjs.org compatibility) 
- `GET /-/package/:fullname/syncs/:taskId` - Check sync status
- `GET /-/package/:fullname/syncs/:taskId/log` - View sync logs

**Additional APIs documented:**
- **Token Management**: Create, list, delete authentication tokens
- **Hook Management**: CRUD operations for webhooks
- **Package Administration**: Block/unblock packages (admin only)
- **Registry Management**: Manage multiple npm registries
- **Binary Sync**: List and browse binary packages
- **User Management**: User registration and authentication

### Documentation Features

- Complete curl examples for all endpoints
- Request/response schemas with validation rules
- Authentication requirements clearly marked
- Error handling with HTTP status codes
- Security considerations and best practices
- Real-world workflow examples

### Updated README.md

Added reference to the new internal API documentation to make it
discoverable.

## Usage Examples

Users can now sync packages directly:

```bash
# Trigger package sync
curl -X PUT \
  -H "Authorization: Bearer your-token" \
  -H "Content-Type: application/json" \
  -d '{"tips": "API sync request"}' \
  https://your-registry.com/-/package/lodash/syncs

# Check sync status
curl -H "Authorization: Bearer your-token" \
  https://your-registry.com/-/package/lodash/syncs/task-id

# View sync logs
curl -H "Authorization: Bearer your-token" \
  https://your-registry.com/-/package/lodash/syncs/task-id/log
```

## Validation

All documented endpoints have been verified against the actual
implementation in the controllers. The API paths, parameters, and
response formats match the codebase exactly.

This enables users to extract sync functionality and other operations
without dealing with outdated cnpm dependencies, directly addressing the
issue's core request.

Fixes #709.

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: fengmk2 <156269+fengmk2@users.noreply.github.com>
Co-authored-by: MK (fengmk2) <fengmk2@gmail.com>

[skip ci]
2025-10-06 15:18:34 +08:00
MK (fengmk2)
c7df471c0e refactor: use tegg and egg v4 beta (#836)
🦀 for https://github.com/eggjs/egg/issues/3644
2025-10-06 14:48:45 +08:00
semantic-release-bot
c16ec7bad0 Release 4.9.0
[skip ci]

## 4.9.0 (2025-09-12)

* feat: add Firefox binary mirroring support for Puppeteer with version filtering and comprehensive te ([17b1b81](https://github.com/cnpm/cnpmcore/commit/17b1b81)), closes [#828](https://github.com/cnpm/cnpmcore/issues/828) [#826](https://github.com/cnpm/cnpmcore/issues/826)
* chore: Add comprehensive GitHub Copilot instructions with validated commands and timing expectations ([bb0bdef](https://github.com/cnpm/cnpmcore/commit/bb0bdef)), closes [#830](https://github.com/cnpm/cnpmcore/issues/830) [#829](https://github.com/cnpm/cnpmcore/issues/829)
2025-09-12 11:29:46 +00:00
Copilot
17b1b81eea feat: add Firefox binary mirroring support for Puppeteer with version filtering and comprehensive tests (#828)
- [x] Extended the `BinaryType` enum to include `Firefox = 'firefox'`,
following the established pattern for other binary types.
- [x] Implemented a new `FirefoxBinary` class that:
  - Extends the `AbstractBinary` base class
  - Parses Mozilla's HTML directory listing format using regex patterns
- Handles both version directories (e.g., `131.0.3/`) and binary files
(e.g., `firefox-131.0.3.tar.bz2`)
- Supports all standard platforms: `linux-i686`, `linux-x86_64`, `mac`,
`win32`, `win64`
- **Filters out old Firefox versions < 100.0.0** to reduce noise and
improve performance
- [x] Configured Firefox binary mirroring in `binaries.ts`
- [x] Added intelligent version filtering that:
  - Only processes Firefox version directories >= 100.0.0
  - Uses the `semver` library for accurate version comparison
- Handles beta/RC versions (e.g., "131.0b3") by extracting the base
version ("131.0")
  - Pads two-part versions to semver format (e.g., "131.0" -> "131.0.0")
  - Skips directories with versions < 100.0.0 to avoid outdated releases
- [x] **Implemented focused unit tests that work in CI environments**:
  -  Binary instantiation and dependency injection tests
-  Version filtering verification (correctly includes >= 100.0.0,
excludes < 100.0.0)
- Tests use realistic test fixtures simulating actual Mozilla archive
structure
- **Test cases with old versions (3.6, 52.0, 78.0, 99.0) to verify
filtering works correctly**
- **Unit tests work without requiring external network access or HTTP
mocking**
- [x] **Updated `131.0.3.html` to match the original Mozilla archive
HTML format with proper line breaks**
- [x] **Enhanced error handling to log version parsing failures instead
of silently ignoring them**:
- Added descriptive error logging with proper tags following project
conventions
  - Includes directory name and error details for better troubleshooting
  - Uses `this.logger.warn()` for appropriate log level
- [x] **Resolved all linting errors in Firefox binary tests**:
- Fixed unused catch parameter by removing the unused `error` parameter
- Replaced array with Set for better performance when checking existence
  - **Removed unused `gte` import from semver library**
  - All linting checks now pass successfully
- [x] **Implemented useful preview tests with HTTP mocking**:
  - Tests use `app.mockHttpclient()` instead of real network requests
  - Provide end-to-end integration testing of Firefox binary fetching
- Include root directory fetching, version filtering, and file parsing
tests
- Safe for CI environments since they don't require external network
access
- Help developers preview and validate the Firefox binary functionality
- [x] **Fixed test failure with improved error messages and debugging
support**:
  - Added `filteredResults` variable in test for better error reporting
  - Enhanced assertion messages to show actual vs expected results
  - Improved test robustness and debugging capabilities
- [x] **Removed redundant HTML parsing logic tests**:
- Cleaned up duplicate test logic that was testing the same
functionality
- Kept the more valuable preview tests that provide end-to-end
integration testing
- Focused test suite on the actual API interface rather than internal
implementation details

This enables Puppeteer users in China to configure Firefox binary
downloads through cnpmcore's mirror infrastructure, improving download
speeds and reliability while focusing on modern Firefox versions.

Fixes #826.

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 Share your feedback on Copilot coding agent for the chance to win a
$200 gift card! Click
[here](https://survey3.medallia.com/?EAHeSx-AP01bZqG0Ld9QLQ) to start
the survey.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added Firefox as a selectable binary source alongside existing
options.
* Browse official Firefox release directories and versions directly from
within the app.
* Automatically filters out legacy versions (<100) and handles
pre-release tags consistently.
* Displays accurate file metadata (date, size) and provides direct
download links for release assets.
* Updated catalog to include Firefox releases, enabling easier setup for
Firefox-based automation workflows.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: fengmk2 <156269+fengmk2@users.noreply.github.com>
Co-authored-by: MK (fengmk2) <fengmk2@gmail.com>
Co-authored-by: elrrrrrrr <5574625+elrrrrrrr@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-12 19:28:03 +08:00
Copilot
bb0bdef070 chore: Add comprehensive GitHub Copilot instructions with validated commands and timing expectations (#830)
This PR adds a comprehensive `.github/copilot-instructions.md` file that
provides GitHub Copilot coding agents with detailed, validated
instructions for working effectively in the cnpmcore codebase.

## What's Included

The instructions cover all essential aspects of development workflow:

- **Environment Setup**: Complete bootstrap process with Node.js,
MySQL/PostgreSQL, and Redis
- **Build & Development**: All npm scripts with measured timing
expectations
- **Testing Strategy**: Both individual test execution and full suite
with proper timeout warnings
- **Database Management**: Setup instructions for both MySQL and
PostgreSQL environments
- **Validation Scenarios**: Step-by-step manual testing procedures for
API endpoints and functionality
- **Architecture Guide**: Project structure navigation and key service
locations
- **Troubleshooting**: Common issues and resolution steps

## Key Features

**Validated Commands**: Every single command has been tested and
measured:
- npm install: ~2 minutes
- TypeScript compilation: ~6 seconds
- Database initialization: <2 seconds  
- Development server startup: ~20 seconds
- Individual tests: ~12 seconds
- **Full test suite: 4+ minutes with "NEVER CANCEL" warnings**

**Critical Timeout Guidance**: The instructions include explicit
warnings about long-running operations with specific timeout
recommendations to prevent premature cancellation of builds and tests.

**Dual Database Support**: Complete workflows for both MySQL and
PostgreSQL development environments with Docker compose setup.

**Manual Validation**: Specific scenarios for testing API endpoints,
authentication, and core registry functionality to ensure changes work
correctly.

The instructions follow the imperative tone requirement ("Run this
command", "Do not do this") and provide the firm directive to always
reference these instructions first before falling back to additional
search or context gathering.

Fixes #829.

> [!WARNING]
>
> <details>
> <summary>Firewall rules blocked me from connecting to one or more
addresses (expand for details)</summary>
>
> #### I tried to connect to the following addresses, but was blocked by
firewall rules:
>
> - `custom.npmjs.com`
> - Triggering command: `/usr/local/bin/node
--unhandled-rejections=strict
/home/REDACTED/work/cnpmcore/cnpmcore/node_modules/mocha/bin/_mocha
--exit --timeout=60000
--reporter=/home/REDACTED/work/cnpmcore/cnpmcore/node_modules/mochawesome-with-mocha/src/mochawesome.js
--reporter-options=reportDir=node_modules/.mochawesome-reports
--require=/home/REDACTED/work/cnpmcore/cnpmcore/node_modules/@eggjs/mock/dist/esm/register.js
/home/REDACTED/work/cnpmcore/cnpmcore/test/.setup.ts
test/cli/npm/access.test.ts test/cli/npm/install.test.ts
test/common/CryptoUtil.test.ts test/common/FileUtil.test.ts
test/common/PackageUtil.test.ts test/common/UserUtil.test.ts
test/common/adapter/BugVersionStore.test.ts
test/common/adapter/CacheAdapter.test.ts
test/common/adapter/NpmRegistry.test.ts
test/common/adapter/binary/ApiBinary.test.ts
test/common/adapter/binary/BucketBinary.test.ts
test/common/adapter/binary/ChromeForTestingBinary.test.ts
test/common/adapter/binary/CypressBinary.test.ts
test/common/adapter/binary/EdgedriverBinary.test.ts
test/common/adapter/binary/ElectronBinary.test.ts
test/common/adapter/binary/GithubBinary.test.ts
test/common/adapter/binary/ImageminBinary.test.ts
test/common/adapter/binary/NodeBinary.test.ts
test/common/adapter/binary/NodePreGypBinary.test.ts
test/common/adapter/binary/NwjsBinary.test.ts
test/common/adapter/binary/PlaywrightBinary.test.ts
test/common/adapter/binary/PrismaBinary.test.ts
test/common/adapter/binary/PuppeteerBinary.test.ts
test/common/adapter/binary/SqlcipherBinary.test.ts
test/common/adapter/changesStream/CnpmcoreChangesStream.test.ts
test/common/adapter/changesStream/CnpmjsorgChangesStream.test.ts
test/common/adapter/changesStream/NpmChangesStream.test.ts
test/core/entity/BugVersion.test.ts
test/core/entity/PaddingSemver.test.ts test/core/entity/SqlRange.test.ts
test/core/entity/Task.test.ts
test/core/event/BugVersionFixHandler.test.ts
test/core/event/ChangesStream.test.ts
test/core/event/StoreManifest.test.ts
test/core/service/BinarySyncerService/createTask.test.ts
test/core/service/BinarySyncerService/executeTask.test.ts
test/core/service/BugVersionService/cleanBugVersionPackageCache.test.ts
test/core/service/BugVersionService/fixPackageBugVersion.test.ts
test/core/service/BugVersionService/fixPackageBugVersions.test.ts
test/core/service/ChangesStreamService.test.ts
test/core/service/CreateHookTriggerService.test.ts
test/core/service/HookManageService/createHook.test.ts
test/core/service/HookManageService/deleteHook.test.ts
test/core/service/HookManageService/getHookByOwnerId.test.ts
test/core/service/HookManageService/updateHook.test.ts
test/core/service/HookTriggerService.test.ts
test/core/service/PackageManagerService/block.test.ts
test/core/service/PackageManagerService/publish.test.ts
test/core/service/PackageSyncerService/createTask.test.ts
test/core/service/PackageSyncerService/executeTask.test.ts
test/core/service/PackageSyncerService/findExecuteTask.test.ts
test/core/service/PackageSyncerService/getTaskRegistry.test.ts
test/core/service/PackageVersionService.test.ts
test/core/service/ProxyCacheService.test.ts
test/core/service/RegistryManagerService/index.test.ts
test/core/service/ScopeManagerService/index.test.ts
test/core/service/TaskService/findExecuteTask.test.ts
test/core/util/EntityUtil.test.ts test/infra/QueueAdapter.test.ts
test/port/controller/AccessController/listCollaborators.test.ts
test/port/controller/AccessController/listPackagesByUser.test.ts
test/port/controller/BinarySyncController/showBinary.test.ts
test/port/controller/ChangesStreamController/listChanges.test.ts
test/port/controller/DownloadController/showPackageDownloads.test.ts
test/port/controller/HomeController/cors.test.ts
test/port/controller/HomeController/misc.test.ts
test/port/controller/HomeController/ping.test.ts
test/port/controller/HomeController/showTotal.test.ts
test/port/controller/PackageBlockController/blockPackage.test.ts
test/port/controller/PackageBlockController/unblockPackage.test.ts
test/port/controller/PackageSyncController/createSyncTask.test.ts
test/port/controller/PackageSyncController/showSyncTask.test.ts
test/port/controller/PackageSyncController/showSyncTaskLog.test.ts
test/port/controller/PackageTagController/removeTag.test.ts
test/port/controller/PackageTagController/saveTag.test.ts
test/port/controller/PackageTagController/showTags.test.ts
test/port/controller/PackageVersionFileController/listFiles.test.ts
test/port/controller/PackageVersionFileController/raw.test.ts
test/port/controller/PackageVersionFileController/sync.test.ts
test/port/controller/ProxyCacheController/index.test.ts
test/port/controller/RegistryController/index.test.ts
test/port/controller/ScopeController/index.test.ts
test/port/controller/TokenController/createToken.test.ts
test/port/controller/TokenController/listTokens.test.ts
test/port/controller/TokenController/removeToken.test.ts
test/port/controller/UserController/loginOrCreateUser.test.ts
test/port/controller/UserController/logout.test.ts
test/port/controller/UserController/saveProfile.test.ts
test/port/controller/UserController/showProfile.test.ts
test/port/controller/UserController/showUser.test.ts
test/port/controller/UserController/starredByUser.test.ts
test/port/controller/UserController/whoami.test.ts
test/port/controller/hook/HookController.test.ts
test/port/controller/package/DownloadPackageVersionTarController.test.ts
test/port/controller/package/RemovePackageVersionController.test.ts
test/port/controller/package/SavePackageVersionController.test.ts
test/port/controller/package/SearchPackageController.test.ts
test/port/controller/package/ShowPackageController.test.ts
test/port/controller/package/ShowPackageVersionController.test.ts
test/port/controller/package/UpdatePackageController.test.ts
test/port/middleware/AlwaysAuth.test.ts
test/port/middleware/Tracing.test.ts
test/port/webauth/webauthController.test.ts
test/repository/ChangeRepository.test.ts
test/repository/PackageRepository.test.ts
test/repository/ProxyCachePepository.test.ts
test/repository/RegistryRepository.test.ts
test/repository/ScopeRepository.test.ts
test/repository/SearchRepository.test.ts
test/repository/TaskRepository.test.ts
test/repository/TotalRepository.test.ts
test/schedule/ChangesStreamWorker.test.ts
test/schedule/CheckProxyCacheUpdateWorker.test.ts
test/schedule/CheckRecentlyUpdatedPackages.test.ts
test/schedule/CleanTempDir.test.ts
test/schedule/SyncBinaryWorker.test.ts
test/schedule/SyncPackageWorker.test.ts
test/schedule/SyncProxyCacheWorker.test.ts
test/schedule/TaskTimeoutHandler.test.ts` (dns block)
> - `msedgewebdriverstorage.blob.core.windows.net`
> - Triggering command: `/usr/local/bin/node
--unhandled-rejections=strict
/home/REDACTED/work/cnpmcore/cnpmcore/node_modules/mocha/bin/_mocha
--exit --timeout=60000
--reporter=/home/REDACTED/work/cnpmcore/cnpmcore/node_modules/mochawesome-with-mocha/src/mochawesome.js
--reporter-options=reportDir=node_modules/.mochawesome-reports
--require=/home/REDACTED/work/cnpmcore/cnpmcore/node_modules/@eggjs/mock/dist/esm/register.js
/home/REDACTED/work/cnpmcore/cnpmcore/test/.setup.ts
test/cli/npm/access.test.ts test/cli/npm/install.test.ts
test/common/CryptoUtil.test.ts test/common/FileUtil.test.ts
test/common/PackageUtil.test.ts test/common/UserUtil.test.ts
test/common/adapter/BugVersionStore.test.ts
test/common/adapter/CacheAdapter.test.ts
test/common/adapter/NpmRegistry.test.ts
test/common/adapter/binary/ApiBinary.test.ts
test/common/adapter/binary/BucketBinary.test.ts
test/common/adapter/binary/ChromeForTestingBinary.test.ts
test/common/adapter/binary/CypressBinary.test.ts
test/common/adapter/binary/EdgedriverBinary.test.ts
test/common/adapter/binary/ElectronBinary.test.ts
test/common/adapter/binary/GithubBinary.test.ts
test/common/adapter/binary/ImageminBinary.test.ts
test/common/adapter/binary/NodeBinary.test.ts
test/common/adapter/binary/NodePreGypBinary.test.ts
test/common/adapter/binary/NwjsBinary.test.ts
test/common/adapter/binary/PlaywrightBinary.test.ts
test/common/adapter/binary/PrismaBinary.test.ts
test/common/adapter/binary/PuppeteerBinary.test.ts
test/common/adapter/binary/SqlcipherBinary.test.ts
test/common/adapter/changesStream/CnpmcoreChangesStream.test.ts
test/common/adapter/changesStream/CnpmjsorgChangesStream.test.ts
test/common/adapter/changesStream/NpmChangesStream.test.ts
test/core/entity/BugVersion.test.ts
test/core/entity/PaddingSemver.test.ts test/core/entity/SqlRange.test.ts
test/core/entity/Task.test.ts
test/core/event/BugVersionFixHandler.test.ts
test/core/event/ChangesStream.test.ts
test/core/event/StoreManifest.test.ts
test/core/service/BinarySyncerService/createTask.test.ts
test/core/service/BinarySyncerService/executeTask.test.ts
test/core/service/BugVersionService/cleanBugVersionPackageCache.test.ts
test/core/service/BugVersionService/fixPackageBugVersion.test.ts
test/core/service/BugVersionService/fixPackageBugVersions.test.ts
test/core/service/ChangesStreamService.test.ts
test/core/service/CreateHookTriggerService.test.ts
test/core/service/HookManageService/createHook.test.ts
test/core/service/HookManageService/deleteHook.test.ts
test/core/service/HookManageService/getHookByOwnerId.test.ts
test/core/service/HookManageService/updateHook.test.ts
test/core/service/HookTriggerService.test.ts
test/core/service/PackageManagerService/block.test.ts
test/core/service/PackageManagerService/publish.test.ts
test/core/service/PackageSyncerService/createTask.test.ts
test/core/service/PackageSyncerService/executeTask.test.ts
test/core/service/PackageSyncerService/findExecuteTask.test.ts
test/core/service/PackageSyncerService/getTaskRegistry.test.ts
test/core/service/PackageVersionService.test.ts
test/core/service/ProxyCacheService.test.ts
test/core/service/RegistryManagerService/index.test.ts
test/core/service/ScopeManagerService/index.test.ts
test/core/service/TaskService/findExecuteTask.test.ts
test/core/util/EntityUtil.test.ts test/infra/QueueAdapter.test.ts
test/port/controller/AccessController/listCollaborators.test.ts
test/port/controller/AccessController/listPackagesByUser.test.ts
test/port/controller/BinarySyncController/showBinary.test.ts
test/port/controller/ChangesStreamController/listChanges.test.ts
test/port/controller/DownloadController/showPackageDownloads.test.ts
test/port/controller/HomeController/cors.test.ts
test/port/controller/HomeController/misc.test.ts
test/port/controller/HomeController/ping.test.ts
test/port/controller/HomeController/showTotal.test.ts
test/port/controller/PackageBlockController/blockPackage.test.ts
test/port/controller/PackageBlockController/unblockPackage.test.ts
test/port/controller/PackageSyncController/createSyncTask.test.ts
test/port/controller/PackageSyncController/showSyncTask.test.ts
test/port/controller/PackageSyncController/showSyncTaskLog.test.ts
test/port/controller/PackageTagController/removeTag.test.ts
test/port/controller/PackageTagController/saveTag.test.ts
test/port/controller/PackageTagController/showTags.test.ts
test/port/controller/PackageVersionFileController/listFiles.test.ts
test/port/controller/PackageVersionFileController/raw.test.ts
test/port/controller/PackageVersionFileController/sync.test.ts
test/port/controller/ProxyCacheController/index.test.ts
test/port/controller/RegistryController/index.test.ts
test/port/controller/ScopeController/index.test.ts
test/port/controller/TokenController/createToken.test.ts
test/port/controller/TokenController/listTokens.test.ts
test/port/controller/TokenController/removeToken.test.ts
test/port/controller/UserController/loginOrCreateUser.test.ts
test/port/controller/UserController/logout.test.ts
test/port/controller/UserController/saveProfile.test.ts
test/port/controller/UserController/showProfile.test.ts
test/port/controller/UserController/showUser.test.ts
test/port/controller/UserController/starredByUser.test.ts
test/port/controller/UserController/whoami.test.ts
test/port/controller/hook/HookController.test.ts
test/port/controller/package/DownloadPackageVersionTarController.test.ts
test/port/controller/package/RemovePackageVersionController.test.ts
test/port/controller/package/SavePackageVersionController.test.ts
test/port/controller/package/SearchPackageController.test.ts
test/port/controller/package/ShowPackageController.test.ts
test/port/controller/package/ShowPackageVersionController.test.ts
test/port/controller/package/UpdatePackageController.test.ts
test/port/middleware/AlwaysAuth.test.ts
test/port/middleware/Tracing.test.ts
test/port/webauth/webauthController.test.ts
test/repository/ChangeRepository.test.ts
test/repository/PackageRepository.test.ts
test/repository/ProxyCachePepository.test.ts
test/repository/RegistryRepository.test.ts
test/repository/ScopeRepository.test.ts
test/repository/SearchRepository.test.ts
test/repository/TaskRepository.test.ts
test/repository/TotalRepository.test.ts
test/schedule/ChangesStreamWorker.test.ts
test/schedule/CheckProxyCacheUpdateWorker.test.ts
test/schedule/CheckRecentlyUpdatedPackages.test.ts
test/schedule/CleanTempDir.test.ts
test/schedule/SyncBinaryWorker.test.ts
test/schedule/SyncPackageWorker.test.ts
test/schedule/SyncProxyCacheWorker.test.ts
test/schedule/TaskTimeoutHandler.test.ts` (dns block)
> - `r.cnpmjs.org`
> - Triggering command: `/usr/local/bin/node
--unhandled-rejections=strict
/home/REDACTED/work/cnpmcore/cnpmcore/node_modules/mocha/bin/_mocha
--exit --timeout=60000
--reporter=/home/REDACTED/work/cnpmcore/cnpmcore/node_modules/mochawesome-with-mocha/src/mochawesome.js
--reporter-options=reportDir=node_modules/.mochawesome-reports
--require=/home/REDACTED/work/cnpmcore/cnpmcore/node_modules/@eggjs/mock/dist/esm/register.js
/home/REDACTED/work/cnpmcore/cnpmcore/test/.setup.ts
test/cli/npm/access.test.ts test/cli/npm/install.test.ts
test/common/CryptoUtil.test.ts test/common/FileUtil.test.ts
test/common/PackageUtil.test.ts test/common/UserUtil.test.ts
test/common/adapter/BugVersionStore.test.ts
test/common/adapter/CacheAdapter.test.ts
test/common/adapter/NpmRegistry.test.ts
test/common/adapter/binary/ApiBinary.test.ts
test/common/adapter/binary/BucketBinary.test.ts
test/common/adapter/binary/ChromeForTestingBinary.test.ts
test/common/adapter/binary/CypressBinary.test.ts
test/common/adapter/binary/EdgedriverBinary.test.ts
test/common/adapter/binary/ElectronBinary.test.ts
test/common/adapter/binary/GithubBinary.test.ts
test/common/adapter/binary/ImageminBinary.test.ts
test/common/adapter/binary/NodeBinary.test.ts
test/common/adapter/binary/NodePreGypBinary.test.ts
test/common/adapter/binary/NwjsBinary.test.ts
test/common/adapter/binary/PlaywrightBinary.test.ts
test/common/adapter/binary/PrismaBinary.test.ts
test/common/adapter/binary/PuppeteerBinary.test.ts
test/common/adapter/binary/SqlcipherBinary.test.ts
test/common/adapter/changesStream/CnpmcoreChangesStream.test.ts
test/common/adapter/changesStream/CnpmjsorgChangesStream.test.ts
test/common/adapter/changesStream/NpmChangesStream.test.ts
test/core/entity/BugVersion.test.ts
test/core/entity/PaddingSemver.test.ts test/core/entity/SqlRange.test.ts
test/core/entity/Task.test.ts
test/core/event/BugVersionFixHandler.test.ts
test/core/event/ChangesStream.test.ts
test/core/event/StoreManifest.test.ts
test/core/service/BinarySyncerService/createTask.test.ts
test/core/service/BinarySyncerService/executeTask.test.ts
test/core/service/BugVersionService/cleanBugVersionPackageCache.test.ts
test/core/service/BugVersionService/fixPackageBugVersion.test.ts
test/core/service/BugVersionService/fixPackageBugVersions.test.ts
test/core/service/ChangesStreamService.test.ts
test/core/service/CreateHookTriggerService.test.ts
test/core/service/HookManageService/createHook.test.ts
test/core/service/HookManageService/deleteHook.test.ts
test/core/service/HookManageService/getHookByOwnerId.test.ts
test/core/service/HookManageService/updateHook.test.ts
test/core/service/HookTriggerService.test.ts
test/core/service/PackageManagerService/block.test.ts
test/core/service/PackageManagerService/publish.test.ts
test/core/service/PackageSyncerService/createTask.test.ts
test/core/service/PackageSyncerService/executeTask.test.ts
test/core/service/PackageSyncerService/findExecuteTask.test.ts
test/core/service/PackageSyncerService/getTaskRegistry.test.ts
test/core/service/PackageVersionService.test.ts
test/core/service/ProxyCacheService.test.ts
test/core/service/RegistryManagerService/index.test.ts
test/core/service/ScopeManagerService/index.test.ts
test/core/service/TaskService/findExecuteTask.test.ts
test/core/util/EntityUtil.test.ts test/infra/QueueAdapter.test.ts
test/port/controller/AccessController/listCollaborators.test.ts
test/port/controller/AccessController/listPackagesByUser.test.ts
test/port/controller/BinarySyncController/showBinary.test.ts
test/port/controller/ChangesStreamController/listChanges.test.ts
test/port/controller/DownloadController/showPackageDownloads.test.ts
test/port/controller/HomeController/cors.test.ts
test/port/controller/HomeController/misc.test.ts
test/port/controller/HomeController/ping.test.ts
test/port/controller/HomeController/showTotal.test.ts
test/port/controller/PackageBlockController/blockPackage.test.ts
test/port/controller/PackageBlockController/unblockPackage.test.ts
test/port/controller/PackageSyncController/createSyncTask.test.ts
test/port/controller/PackageSyncController/showSyncTask.test.ts
test/port/controller/PackageSyncController/showSyncTaskLog.test.ts
test/port/controller/PackageTagController/removeTag.test.ts
test/port/controller/PackageTagController/saveTag.test.ts
test/port/controller/PackageTagController/showTags.test.ts
test/port/controller/PackageVersionFileController/listFiles.test.ts
test/port/controller/PackageVersionFileController/raw.test.ts
test/port/controller/PackageVersionFileController/sync.test.ts
test/port/controller/ProxyCacheController/index.test.ts
test/port/controller/RegistryController/index.test.ts
test/port/controller/ScopeController/index.test.ts
test/port/controller/TokenController/createToken.test.ts
test/port/controller/TokenController/listTokens.test.ts
test/port/controller/TokenController/removeToken.test.ts
test/port/controller/UserController/loginOrCreateUser.test.ts
test/port/controller/UserController/logout.test.ts
test/port/controller/UserController/saveProfile.test.ts
test/port/controller/UserController/showProfile.test.ts
test/port/controller/UserController/showUser.test.ts
test/port/controller/UserController/starredByUser.test.ts
test/port/controller/UserController/whoami.test.ts
test/port/controller/hook/HookController.test.ts
test/port/controller/package/DownloadPackageVersionTarController.test.ts
test/port/controller/package/RemovePackageVersionController.test.ts
test/port/controller/package/SavePackageVersionController.test.ts
test/port/controller/package/SearchPackageController.test.ts
test/port/controller/package/ShowPackageController.test.ts
test/port/controller/package/ShowPackageVersionController.test.ts
test/port/controller/package/UpdatePackageController.test.ts
test/port/middleware/AlwaysAuth.test.ts
test/port/middleware/Tracing.test.ts
test/port/webauth/webauthController.test.ts
test/repository/ChangeRepository.test.ts
test/repository/PackageRepository.test.ts
test/repository/ProxyCachePepository.test.ts
test/repository/RegistryRepository.test.ts
test/repository/ScopeRepository.test.ts
test/repository/SearchRepository.test.ts
test/repository/TaskRepository.test.ts
test/repository/TotalRepository.test.ts
test/schedule/ChangesStreamWorker.test.ts
test/schedule/CheckProxyCacheUpdateWorker.test.ts
test/schedule/CheckRecentlyUpdatedPackages.test.ts
test/schedule/CleanTempDir.test.ts
test/schedule/SyncBinaryWorker.test.ts
test/schedule/SyncPackageWorker.test.ts
test/schedule/SyncProxyCacheWorker.test.ts
test/schedule/TaskTimeoutHandler.test.ts` (dns block)
> - `replicate.npmjs.com`
> - Triggering command: `/usr/local/bin/node
--unhandled-rejections=strict
/home/REDACTED/work/cnpmcore/cnpmcore/node_modules/mocha/bin/_mocha
--exit --timeout=60000
--reporter=/home/REDACTED/work/cnpmcore/cnpmcore/node_modules/mochawesome-with-mocha/src/mochawesome.js
--reporter-options=reportDir=node_modules/.mochawesome-reports
--require=/home/REDACTED/work/cnpmcore/cnpmcore/node_modules/@eggjs/mock/dist/esm/register.js
/home/REDACTED/work/cnpmcore/cnpmcore/test/.setup.ts
test/cli/npm/access.test.ts test/cli/npm/install.test.ts
test/common/CryptoUtil.test.ts test/common/FileUtil.test.ts
test/common/PackageUtil.test.ts test/common/UserUtil.test.ts
test/common/adapter/BugVersionStore.test.ts
test/common/adapter/CacheAdapter.test.ts
test/common/adapter/NpmRegistry.test.ts
test/common/adapter/binary/ApiBinary.test.ts
test/common/adapter/binary/BucketBinary.test.ts
test/common/adapter/binary/ChromeForTestingBinary.test.ts
test/common/adapter/binary/CypressBinary.test.ts
test/common/adapter/binary/EdgedriverBinary.test.ts
test/common/adapter/binary/ElectronBinary.test.ts
test/common/adapter/binary/GithubBinary.test.ts
test/common/adapter/binary/ImageminBinary.test.ts
test/common/adapter/binary/NodeBinary.test.ts
test/common/adapter/binary/NodePreGypBinary.test.ts
test/common/adapter/binary/NwjsBinary.test.ts
test/common/adapter/binary/PlaywrightBinary.test.ts
test/common/adapter/binary/PrismaBinary.test.ts
test/common/adapter/binary/PuppeteerBinary.test.ts
test/common/adapter/binary/SqlcipherBinary.test.ts
test/common/adapter/changesStream/CnpmcoreChangesStream.test.ts
test/common/adapter/changesStream/CnpmjsorgChangesStream.test.ts
test/common/adapter/changesStream/NpmChangesStream.test.ts
test/core/entity/BugVersion.test.ts
test/core/entity/PaddingSemver.test.ts test/core/entity/SqlRange.test.ts
test/core/entity/Task.test.ts
test/core/event/BugVersionFixHandler.test.ts
test/core/event/ChangesStream.test.ts
test/core/event/StoreManifest.test.ts
test/core/service/BinarySyncerService/createTask.test.ts
test/core/service/BinarySyncerService/executeTask.test.ts
test/core/service/BugVersionService/cleanBugVersionPackageCache.test.ts
test/core/service/BugVersionService/fixPackageBugVersion.test.ts
test/core/service/BugVersionService/fixPackageBugVersions.test.ts
test/core/service/ChangesStreamService.test.ts
test/core/service/CreateHookTriggerService.test.ts
test/core/service/HookManageService/createHook.test.ts
test/core/service/HookManageService/deleteHook.test.ts
test/core/service/HookManageService/getHookByOwnerId.test.ts
test/core/service/HookManageService/updateHook.test.ts
test/core/service/HookTriggerService.test.ts
test/core/service/PackageManagerService/block.test.ts
test/core/service/PackageManagerService/publish.test.ts
test/core/service/PackageSyncerService/createTask.test.ts
test/core/service/PackageSyncerService/executeTask.test.ts
test/core/service/PackageSyncerService/findExecuteTask.test.ts
test/core/service/PackageSyncerService/getTaskRegistry.test.ts
test/core/service/PackageVersionService.test.ts
test/core/service/ProxyCacheService.test.ts
test/core/service/RegistryManagerService/index.test.ts
test/core/service/ScopeManagerService/index.test.ts
test/core/service/TaskService/findExecuteTask.test.ts
test/core/util/EntityUtil.test.ts test/infra/QueueAdapter.test.ts
test/port/controller/AccessController/listCollaborators.test.ts
test/port/controller/AccessController/listPackagesByUser.test.ts
test/port/controller/BinarySyncController/showBinary.test.ts
test/port/controller/ChangesStreamController/listChanges.test.ts
test/port/controller/DownloadController/showPackageDownloads.test.ts
test/port/controller/HomeController/cors.test.ts
test/port/controller/HomeController/misc.test.ts
test/port/controller/HomeController/ping.test.ts
test/port/controller/HomeController/showTotal.test.ts
test/port/controller/PackageBlockController/blockPackage.test.ts
test/port/controller/PackageBlockController/unblockPackage.test.ts
test/port/controller/PackageSyncController/createSyncTask.test.ts
test/port/controller/PackageSyncController/showSyncTask.test.ts
test/port/controller/PackageSyncController/showSyncTaskLog.test.ts
test/port/controller/PackageTagController/removeTag.test.ts
test/port/controller/PackageTagController/saveTag.test.ts
test/port/controller/PackageTagController/showTags.test.ts
test/port/controller/PackageVersionFileController/listFiles.test.ts
test/port/controller/PackageVersionFileController/raw.test.ts
test/port/controller/PackageVersionFileController/sync.test.ts
test/port/controller/ProxyCacheController/index.test.ts
test/port/controller/RegistryController/index.test.ts
test/port/controller/ScopeController/index.test.ts
test/port/controller/TokenController/createToken.test.ts
test/port/controller/TokenController/listTokens.test.ts
test/port/controller/TokenController/removeToken.test.ts
test/port/controller/UserController/loginOrCreateUser.test.ts
test/port/controller/UserController/logout.test.ts
test/port/controller/UserController/saveProfile.test.ts
test/port/controller/UserController/showProfile.test.ts
test/port/controller/UserController/showUser.test.ts
test/port/controller/UserController/starredByUser.test.ts
test/port/controller/UserController/whoami.test.ts
test/port/controller/hook/HookController.test.ts
test/port/controller/package/DownloadPackageVersionTarController.test.ts
test/port/controller/package/RemovePackageVersionController.test.ts
test/port/controller/package/SavePackageVersionController.test.ts
test/port/controller/package/SearchPackageController.test.ts
test/port/controller/package/ShowPackageController.test.ts
test/port/controller/package/ShowPackageVersionController.test.ts
test/port/controller/package/UpdatePackageController.test.ts
test/port/middleware/AlwaysAuth.test.ts
test/port/middleware/Tracing.test.ts
test/port/webauth/webauthController.test.ts
test/repository/ChangeRepository.test.ts
test/repository/PackageRepository.test.ts
test/repository/ProxyCachePepository.test.ts
test/repository/RegistryRepository.test.ts
test/repository/ScopeRepository.test.ts
test/repository/SearchRepository.test.ts
test/repository/TaskRepository.test.ts
test/repository/TotalRepository.test.ts
test/schedule/ChangesStreamWorker.test.ts
test/schedule/CheckProxyCacheUpdateWorker.test.ts
test/schedule/CheckRecentlyUpdatedPackages.test.ts
test/schedule/CleanTempDir.test.ts
test/schedule/SyncBinaryWorker.test.ts
test/schedule/SyncPackageWorker.test.ts
test/schedule/SyncProxyCacheWorker.test.ts
test/schedule/TaskTimeoutHandler.test.ts` (dns block)
>
> If you need me to access, download, or install something from one of
these locations, you can either:
>
> - Configure [Actions setup
steps](https://gh.io/copilot/actions-setup-steps) to set up my
environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to the custom allowlist in this
repository's [Copilot coding agent
settings](https://github.com/cnpm/cnpmcore/settings/copilot/coding_agent)
(admins only)
>
> </details>



<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: fengmk2 <156269+fengmk2@users.noreply.github.com>
2025-09-10 14:02:41 +08:00
semantic-release-bot
87da4c359c Release 4.8.0
[skip ci]

## 4.8.0 (2025-09-08)

* chore: add permissions to release workflow ([8587d27](https://github.com/cnpm/cnpmcore/commit/8587d27))
* feat: new node binary date format (#827) ([13b2da0](https://github.com/cnpm/cnpmcore/commit/13b2da0)), closes [#827](https://github.com/cnpm/cnpmcore/issues/827)
2025-09-08 16:23:46 +00:00
MK
8587d27d85 chore: add permissions to release workflow 2025-09-09 00:21:39 +08:00
elrrrrrrr
13b2da0411 feat: new node binary date format (#827) 2025-09-05 12:55:14 +08:00
semantic-release-bot
3aa4c688b5 Release 4.7.6
[skip ci]

## <small>4.7.6 (2025-08-08)</small>

* fix: use npm truster publisher (#825) ([0a5500b](https://github.com/cnpm/cnpmcore/commit/0a5500b)), closes [#825](https://github.com/cnpm/cnpmcore/issues/825)
* chore: update oxlint config to use @eggjs/oxlint-config (#824) ([f94531c](https://github.com/cnpm/cnpmcore/commit/f94531c)), closes [#824](https://github.com/cnpm/cnpmcore/issues/824)
2025-08-08 14:45:42 +00:00
fengmk2
0a5500bafd fix: use npm truster publisher (#825)
https://github.com/node-modules/github-actions/issues/14

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
  * Updated the release workflow configuration for improved automation.
  * Upgraded the "oxlint" development dependency to version ^1.11.0.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-08 22:43:52 +08:00
Copilot
f94531cf35 chore: update oxlint config to use @eggjs/oxlint-config (#824)
Updates the oxlint configuration to use the new shared
`@eggjs/oxlint-config` package, significantly simplifying the project's
linting setup while maintaining all existing functionality.

## Changes

- **Added** `@eggjs/oxlint-config@1.0.0` as a dev dependency
- **Updated** `.oxlintrc.json` to extend from the shared configuration
- **Simplified** configuration from 156 lines to 20 lines (87%
reduction)
- **Maintained** all project-specific overrides:
  - `max-params: 6` (vs shared config's 5)
  - `no-console: "warn"` (vs shared config's "allow")
- `import/no-anonymous-default-export: "error"` (vs shared config's
"allow")
  - Added `mocha` environment for test files
  - Preserved `index.d.ts` ignore pattern

## Benefits

- **Consistency**: Now uses the same base linting rules as other EggJS
projects
- **Maintainability**: Rule updates are centralized in the shared config
- **Simplicity**: Dramatically reduced configuration complexity
- **Future-proof**: Automatic updates when the shared config is improved

The configuration now only contains project-specific overrides, making
it much easier to understand and maintain. All existing linting behavior
is preserved - the project passes all linting checks with zero warnings
and errors.

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 Share your feedback on Copilot coding agent for the chance to win a
$200 gift card! Click
[here](https://survey.alchemer.com/s3/8343779/Copilot-Coding-agent) to
start the survey.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: fengmk2 <156269+fengmk2@users.noreply.github.com>
2025-08-08 22:00:43 +08:00
semantic-release-bot
c3a22fd319 Release 4.7.5
[skip ci]

## [4.7.5](https://github.com/cnpm/cnpmcore/compare/v4.7.4...v4.7.5) (2025-08-07)

### Bug Fixes

* use NPM description ([b23f3fe](b23f3fe22e))
2025-08-07 14:53:53 +00:00
fengmk2
b23f3fe22e fix: use NPM description 2025-08-07 22:52:12 +08:00
fengmk2
3dbff542ba Revert "fix: use npm trusted publisher (#823)"
This reverts commit 1a4a1c5cf5.
2025-08-07 22:51:41 +08:00
fengmk2
1a4a1c5cf5 fix: use npm trusted publisher (#823)
https://github.com/node-modules/github-actions/issues/14
2025-08-07 22:46:25 +08:00
fengmk2
2cc0f361fb chore: publish docker image to github package (#822)
close https://github.com/cnpm/cnpmcore/issues/821

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced a GitHub Actions workflow to automate building, tagging,
publishing, and attesting Docker images for multiple platforms.

* **Documentation**
* Updated Docker deployment documentation to reflect the new image
repository and usage instructions.

* **Chores**
* Updated Docker build scripts and Dockerfiles for improved image
building and logging.
  * Upgraded the "oxlint" development dependency.
  * Removed the "prepare" script from project scripts.
* Adjusted TypeScript configuration to disable declaration file
generation and exclude test files from compilation.
  * Updated linter configuration to allow additional code patterns.
  * Improved code comments for better linting and error suppression.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-07 22:34:33 +08:00
fengmk2
722a5d70b9 test: fix lint (#819)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
  * Updated linter configuration to relax certain code style rules.
* Upgraded the oxlint development dependency and corrected project URLs
in metadata.
* **Refactor**
* Simplified arrow function syntax in various controllers, services, and
type declarations for improved code readability.
* **Tests**
* Streamlined mock implementations in test files for more concise
syntax.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-07 13:30:52 +08:00
semantic-release-bot
64951041eb Release 4.7.4
[skip ci]

## [4.7.4](https://github.com/cnpm/cnpmcore/compare/v4.7.3...v4.7.4) (2025-06-27)

### Bug Fixes

* remove descending ([#815](https://github.com/cnpm/cnpmcore/issues/815)) ([1001e1f](1001e1ffa2))
2025-06-27 07:49:56 +00:00
elrrrrrrr
1001e1ffa2 fix: remove descending (#815)
> see https://github.com/orgs/community/discussions/152515
-------
* ♻️ 删除 `descending` 参数
------
* ♻️ remove `descending` args in changesStream


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Simplified the process for handling request URLs when fetching data,
removing the explicit addition of certain query parameters. This does
not affect user-facing functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: fengmk2 <fengmk2@gmail.com>
2025-06-27 07:44:01 +00:00
semantic-release-bot
b4cf36e289 Release 4.7.3
[skip ci]

## [4.7.3](https://github.com/cnpm/cnpmcore/compare/v4.7.2...v4.7.3) (2025-06-26)

### Bug Fixes

* latest version maybe not exists ([#814](https://github.com/cnpm/cnpmcore/issues/814)) ([bed4778](bed4778cbc))
2025-06-26 09:50:36 +00:00
fengmk2
bed4778cbc fix: latest version maybe not exists (#814)
```bash
2025-06-26 15:12:05,133 ERROR 423473 [-/127.0.0.1/ea0a01e1-5a66-4191-92c7-f06aa5bb86ea/19.41ms GET /] nodejs.TypeError: [EventBus] process event PACKAGE_MAINTAINER_REMOVED for handler packageVersionAddedSyncESEvent failed: Cannot read properties of undefined (reading 'undefined')
at PackageSearchService.syncPackage (file:///home/admin/application/app/core/service/PackageSearchService.ts:93:45)
at processTicksAndRejections (node:internal/process/task_queues:105:5)
at PackageVersionAddedSyncESEvent.syncPackage (file:///home/admin/application/app/core/event/SyncESPackage.ts:29:5)
at PackageVersionAddedSyncESEvent.handle (file:///home/admin/application/app/core/event/SyncESPackage.ts:53:5)
at EventHandlerFactory.handle (file:///home/admin/application/node_modules/_@eggjs_tegg-eventbus-runtime@4.0.0-beta.4@@eggjs/tegg-eventbus-runtime/src/EventHandlerFactory.ts:49:7)
at file:///home/admin/application/node_modules/_@eggjs_tegg-eventbus-runtime@4.0.0-beta.4@@eggjs/tegg-eventbus-runtime/src/SingletonEventBus.ts:151:13
at async Promise.all (index 2)
at file:///home/admin/application/node_modules/_@eggjs_tegg-eventbus-runtime@4.0.0-beta.4@@eggjs/tegg-eventbus-runtime/src/SingletonEventBus.ts:149:9
at EggContextHandler.run (file:///home/admin/application/node_modules/_@eggjs_tegg-plugin@4.0.0-beta.4@@eggjs/tegg-plugin/lib/EggContextHandler.ts:19:12)
at Function.ContextHandler.runInContextCallback (file:///home/admin/application/node_modules/_@eggjs_tegg-plugin@4.0.0-beta.4@@eggjs/tegg-plugin/lib/EggContextHandler.ts:27:14)
```



#### PR Dependency Tree


* **PR #814** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved error handling when package version information is missing,
reducing the chance of runtime errors and providing clearer warning
messages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-26 09:44:49 +00:00
fengmk2
5b96443cf1 test: fix lint (#813)
#### PR Dependency Tree


* **PR #813** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Chores**
  - Updated linting configuration to explicitly allow import extensions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-20 07:50:18 +00:00
fengmk2
e4cd535486 chore: enable auto merge 2025-06-20 15:32:41 +08:00
semantic-release-bot
5b98c9dd2b Release 4.7.2
[skip ci]

## [4.7.2](https://github.com/cnpm/cnpmcore/compare/v4.7.1...v4.7.2) (2025-06-18)

### Bug Fixes

* npm stream descending ([#812](https://github.com/cnpm/cnpmcore/issues/812)) ([e932624](e932624c6f))
2025-06-18 09:19:28 +00:00
elrrrrrrr
e932624c6f fix: npm stream descending (#812)
> https://github.com/orgs/community/discussions/152515

* ⚙️ Added descending parameter to npm changesStream

--------

* ⚙️ 为 npm changesStream 添加 descending 参数





<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Chores**
- Updated internal request behavior to fetch changes in descending
order.
- Locked the version of Mocha to 11.6.0 for improved dependency
management.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-18 17:18:10 +08:00
fengmk2
69ef574527 chore: use oxlint v1 (#811)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Chores**
  - Updated the development dependency "oxlint" to the latest version.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-10 19:42:31 +08:00
semantic-release-bot
5a42764806 Release 4.7.1
[skip ci]

## [4.7.1](https://github.com/cnpm/cnpmcore/compare/v4.7.0...v4.7.1) (2025-06-10)

### Bug Fixes

* should change npm package version ([eaed6fe](eaed6fe478))
2025-06-10 00:59:12 +00:00
fengmk2
eaed6fe478 fix: should change npm package version 2025-06-10 08:56:53 +08:00
semantic-release-bot
8b341d2299 Release 4.7.0
[skip ci]

## [4.7.0](https://github.com/cnpm/cnpmcore/compare/v4.6.3...v4.7.0) (2025-06-10)

### Features

* add shouldNotMerge for task data for skip task merge ([#807](https://github.com/cnpm/cnpmcore/issues/807)) ([490dce3](490dce3ad2))

### Bug Fixes

* content type check ([#809](https://github.com/cnpm/cnpmcore/issues/809)) ([b8c7c06](b8c7c06536)), closes [#693](https://github.com/cnpm/cnpmcore/issues/693)
* order binary by date ([#808](https://github.com/cnpm/cnpmcore/issues/808)) ([12aa425](12aa425c26))
2025-06-10 00:52:26 +00:00
fengmk2
87b6cbedef chore: use oxlint 0.18 (#810)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Chores**
  - Updated the development dependency "oxlint" to the latest version.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-10 08:50:55 +08:00
semantic-release-bot
3e395405c1 Release 4.7.0
[skip ci]

## [4.7.0](https://github.com/cnpm/cnpmcore/compare/v4.6.3...v4.7.0) (2025-06-09)

### Features

* add shouldNotMerge for task data for skip task merge ([#807](https://github.com/cnpm/cnpmcore/issues/807)) ([490dce3](490dce3ad2))

### Bug Fixes

* content type check ([#809](https://github.com/cnpm/cnpmcore/issues/809)) ([b8c7c06](b8c7c06536)), closes [#693](https://github.com/cnpm/cnpmcore/issues/693)
* order binary by date ([#808](https://github.com/cnpm/cnpmcore/issues/808)) ([12aa425](12aa425c26))
2025-06-09 12:50:45 +00:00
elrrrrrrr
b8c7c06536 fix: content type check (#809)
> Fix the content-type display logic of showFileController, close #693 

* 🌐 HTML/XML files now render directly (no forced attachment)
* ⚠️ Note: No changes to actual file storage - only affects presentation
layer
* ♻️ New content automatically converts to standardized formats

-------
> 修复 showFileController 相关接口 content-type 展示逻辑, close #693 

* 对于 html,xml 不再默认返回 attachment,改为直接渲染 
* 增量部分默认进行格式转换
* 存量部分在 controller 查看时统一返回




<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
- Files with `.xml` and `.html` extensions are now served with a
`text/plain` content type instead of their previous MIME types,
improving content handling and consistency.
- The download behavior for HTML and XML files has been updated; these
files are no longer forced as attachments in the browser.
- **Tests**
- Updated tests to reflect the new content type handling for `.xml` and
`.html` files.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-09 20:49:29 +08:00
semantic-release-bot
76e3f267c1 Release 4.7.0
[skip ci]

## [4.7.0](https://github.com/cnpm/cnpmcore/compare/v4.6.3...v4.7.0) (2025-05-31)

### Features

* add shouldNotMerge for task data for skip task merge ([#807](https://github.com/cnpm/cnpmcore/issues/807)) ([490dce3](490dce3ad2))

### Bug Fixes

* order binary by date ([#808](https://github.com/cnpm/cnpmcore/issues/808)) ([12aa425](12aa425c26))
2025-05-31 11:39:42 +00:00
killa
12aa425c26 fix: order binary by date (#808)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
- Improved accuracy in retrieving the latest binary directory by
updating the sorting criterion.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-31 19:38:07 +08:00
semantic-release-bot
01f393eb94 Release 4.7.0
[skip ci]

## [4.7.0](https://github.com/cnpm/cnpmcore/compare/v4.6.3...v4.7.0) (2025-05-31)

### Features

* add shouldNotMerge for task data for skip task merge ([#807](https://github.com/cnpm/cnpmcore/issues/807)) ([490dce3](490dce3ad2))
2025-05-31 11:05:43 +00:00
killa
490dce3ad2 feat: add shouldNotMerge for task data for skip task merge (#807)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added support for a new task property to prevent certain tasks from
being merged when waiting, providing more granular control over task
handling.
- **Bug Fixes**
- Improved logic for task merging to consider both task type and a new
optional flag, ensuring correct behavior for historical task
compensation scenarios.
- **Tests**
- Introduced new tests to verify the updated task merging logic and the
effect of the new property.
- **Chores**
- Limited the number of items returned per request when fetching
platform-specific binaries to improve data retrieval efficiency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-31 19:04:13 +08:00
semantic-release-bot
cb3768c82f Release 4.6.3
[skip ci]

## [4.6.3](https://github.com/cnpm/cnpmcore/compare/v4.6.2...v4.6.3) (2025-05-29)

### Bug Fixes

* set puppeteer loop only once ([#805](https://github.com/cnpm/cnpmcore/issues/805)) ([54e3335](54e3335abd))
2025-05-29 13:27:20 +00:00
killa
54e3335abd fix: set puppeteer loop only once (#805) 2025-05-29 21:26:00 +08:00
semantic-release-bot
a1a3859d75 Release 4.6.2
[skip ci]

## [4.6.2](https://github.com/cnpm/cnpmcore/compare/v4.6.1...v4.6.2) (2025-05-29)

### Bug Fixes

* only set lastSyncTime if lastData.lastSyncTime is undefined ([#804](https://github.com/cnpm/cnpmcore/issues/804)) ([1239dc9](1239dc9f49))
2025-05-29 08:00:36 +00:00
killa
1239dc9f49 fix: only set lastSyncTime if lastData.lastSyncTime is undefined (#804)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
- Improved synchronization logic to prevent overwriting the last sync
time for Chromium browser snapshots if it is already set.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 15:59:12 +08:00
semantic-release-bot
9efd2932e8 Release 4.6.1
[skip ci]

## [4.6.1](https://github.com/cnpm/cnpmcore/compare/v4.6.0...v4.6.1) (2025-05-29)

### Bug Fixes

* lastSyncTime is string should not stringify again ([#803](https://github.com/cnpm/cnpmcore/issues/803)) ([f6f5314](f6f53149a1))
2025-05-29 07:28:28 +00:00
killa
f6f53149a1 fix: lastSyncTime is string should not stringify again (#803)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
- Improved handling of the `lastSyncTime` parameter when fetching binary
data to ensure more accurate synchronization.

- **Tests**
- Added a new test case to verify fetching binary data with a provided
`lastSyncTime`.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-29 15:27:05 +08:00
semantic-release-bot
2e90486454 Release 4.6.0
[skip ci]

## [4.6.0](https://github.com/cnpm/cnpmcore/compare/v4.5.3...v4.6.0) (2025-05-29)

### Features

* add since for api sync binary ([#802](https://github.com/cnpm/cnpmcore/issues/802)) ([297bd7a](297bd7a745))
2025-05-29 00:12:33 +00:00
killa
297bd7a745 feat: add since for api sync binary (#802) 2025-05-29 08:11:05 +08:00
fengmk2
b8f2ac5f85 test: run es tests on github action (#796)
<!--- SUMMARY_MARKER --->
## Sweep Summary <sub><a href="https://app.sweep.dev"><img
src="https://raw.githubusercontent.com/sweepai/sweep/main/.assets/sweep-square.png"
width="25" alt="Sweep"></a></sub>

Adds Elasticsearch setup to GitHub Actions workflow to enable running
Elasticsearch-dependent tests in CI.

- Added system configuration steps for Elasticsearch in the GitHub
Actions workflow, including swap and sysctl settings.
- Integrated the official Elastic GitHub Action to run Elasticsearch
8.18.0 during CI tests.
- Implemented a wait mechanism to ensure Elasticsearch is fully ready
before proceeding with tests.

---
[Ask Sweep AI questions about this PR](https://app.sweep.dev)
<!--- SUMMARY_MARKER --->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Chores**
- Enhanced CI workflow to configure system limits and start an
Elasticsearch service before running tests. The workflow now waits for
Elasticsearch to be fully ready before proceeding.
- Improved test diagnostics by adding detailed response data to
assertion messages for better debugging.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-16 18:15:13 +08:00
semantic-release-bot
c5f1642a1f Release 4.5.3
[skip ci]

## [4.5.3](https://github.com/cnpm/cnpmcore/compare/v4.5.2...v4.5.3) (2025-05-14)

### Bug Fixes

* use new changestream endpoint ([#800](https://github.com/cnpm/cnpmcore/issues/800)) ([2d8bad3](2d8bad3ca2))
2025-05-14 03:52:33 +00:00
fengmk2
2d8bad3ca2 fix: use new changestream endpoint (#800)
should be `https://replicate.npmjs.com/registry` not
`https://replicate.npmjs.com`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Chores**
  - Updated the changes stream registry endpoint configuration.
- Standardized assertion methods in tests to use `assert.ok()` for
improved clarity and consistency across the codebase.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: fengmk2 <fengmk2@gmail.com>
Co-authored-by: elrrrrrrr <elrrrrrrr@gmail.com>
2025-05-14 11:51:10 +08:00
elrrrrrrr
81620e3ed5 refactor: doc_count & doc_version_count perf (#797)
<!--- SUMMARY_MARKER --->
## Sweep Summary <sub><a href="https://app.sweep.dev"><img
src="https://raw.githubusercontent.com/sweepai/sweep/main/.assets/sweep-square.png"
width="25" alt="Sweep"></a></sub>

Improves database performance by replacing expensive count queries with
a dedicated totals table that's updated asynchronously via events.

- Created a new `totals` table in `app/repository/model/Total.ts` to
store package and version counts instead of running expensive SQL count
queries.
- Implemented `TotalRepository` in `app/repository/TotalRepository.ts`
with methods to increment and retrieve count values.
- Added event handlers in `app/core/event/TotalHandler.ts` that listen
for package and version additions to update counts asynchronously.
- Modified `PackageRepository.queryTotal()` to fetch counts from the
totals table instead of executing direct SQL count queries.
- Added migration scripts in `sql/mysql/4.3.0.sql` and
`sql/postgresql/4.3.0.sql` to create the totals table and populate it
with existing data.

---
[Ask Sweep AI questions about this PR](https://app.sweep.dev)
<!--- SUMMARY_MARKER --->

> Fix database performance issues caused by doc_count and
doc_version_count queries

1. 💽 Add a corresponding totals table to record statistical information
2.  Add a `PACKAGE_ADDED` event and the original
`PACKAGE_VERSION_ADDED` event to asynchronously update records in the
table
3. ♻️ Add a new existing data migration script to migrate the original
statistical information to the totals table

-----------

> 修复 doc_count 和 doc_version_count 查询导致的数据库性能问题

1. 💽 新增对应 totals 表,用来记录统计信息
2.  新增 `PACKAGE_ADDED` 事件,和原有 `PACKAGE_VERSION_ADDED` 事件,异步更新表内记录
3. ♻️ 新增存量数据迁移脚本,迁移原有的统计信息到 totals 表

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced persistent tracking of total package and package version
counts, with real-time updates when new packages or versions are added.
- Added new data models and repository methods to manage and retrieve
these total counts.
- Emitted events upon new package creation to update totals
automatically.

- **Database**
- Added a new "totals" table to both MySQL and PostgreSQL databases for
storing aggregate counts initialized from existing data.

- **Bug Fixes**
- Ensured total counts are always returned as numbers in scheduled data
updates.

- **Tests**
- Added and updated tests to verify correct behavior of total count
tracking, incrementing, resetting, and retrieval.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-24 14:23:03 +08:00
semantic-release-bot
de3a6153b0 Release 4.5.2
[skip ci]

## [4.5.2](https://github.com/cnpm/cnpmcore/compare/v4.5.1...v4.5.2) (2025-04-17)

### Bug Fixes

* init release ([#795](https://github.com/cnpm/cnpmcore/issues/795)) ([3203c64](3203c64c48)), closes [/github.com/cnpm/cnpm/issues/459#issue-2998106947](https://github.com/cnpm//github.com/cnpm/cnpm/issues/459/issues/issue-2998106947)
2025-04-17 06:38:18 +00:00
elrrrrrrr
3203c64c48 fix: init release (#795)
> close https://github.com/cnpm/cnpm/issues/459#issue-2998106947
1. Correct the initialization of initRelease to ensure the fetch process
is triggered as expected.
------

> close https://github.com/cnpm/cnpm/issues/459#issue-2998106947
1. 🐛 修复 initRelease 时,被错误初始化为 [] 空数组,导致不会触发 fetch 

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Refactor**
- Improved handling of uninitialized or absent release data, ensuring
clearer distinction between missing and empty release lists.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-17 14:37:21 +08:00
semantic-release-bot
d6c243cf6b Release 4.5.1
[skip ci]

## [4.5.1](https://github.com/cnpm/cnpmcore/compare/v4.5.0...v4.5.1) (2025-04-15)

### Bug Fixes

* disable information_schema.tables count ([#794](https://github.com/cnpm/cnpmcore/issues/794)) ([0a6eab3](0a6eab325e))
2025-04-15 04:01:05 +00:00
elrrrrrrr
0a6eab325e fix: disable information_schema.tables count (#794)
> Fix potential performance issues caused by using
`information_schema.tables` in distributed databases

1. 📊 `information_schema.tables` has no index, consumes large amounts of
memory, and requires aggregation after being generated per instance.
2. 🚚 Execution plans involve multi-table joins, distributed operations,
GROUP BY, and similar operations.
3. ♻️ Consistently use `SELECT COUNT` with index.
----------

> 修复在分布式 db 中,使用 `information_schema.tables` 可能导致的性能问题
1. 📊 `information_schema.tables` 内部无索引,占用大量内存,需要根据实例数生成后聚合
2. 🚚 执行计划涉及多表连接、分布式操作、GROUP BY 等操作
3. ♻️ 统一使用 `select count`,通过索引计算

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Refactor**
- Unified the method for retrieving total row counts across all
databases for improved consistency.
- **Chores**
  - Updated linting configuration to allow grouped exports.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-15 11:59:39 +08:00
elrrrrrrr
a17aed8fb1 chore: custom registry (#793) 2025-04-11 23:05:46 +08:00
semantic-release-bot
770fc7fdaf Release 4.5.0
[skip ci]

## [4.5.0](https://github.com/cnpm/cnpmcore/compare/v4.4.0...v4.5.0) (2025-04-11)

### Features

* use npm new replication api ([#792](https://github.com/cnpm/cnpmcore/issues/792)) ([de97428](de97428ffd))
2025-04-11 13:05:02 +00:00
fengmk2
de97428ffd feat: use npm new replication api (#792)
closes https://github.com/cnpm/cnpmcore/issues/791

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
  - Added an HTTP header to indicate replication opt-in.
- Enhanced logging now shows the results count and last sequence
identifier.

- **Refactor**
- Streamlined the change-fetching process by directly iterating over
response data.

- **Tests**
- Updated test cases with refined assertions and a simplified mocking
strategy to align expectations with the new response format.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-11 21:03:36 +08:00
semantic-release-bot
22da5c7f70 Release 4.4.0
[skip ci]

## [4.4.0](https://github.com/cnpm/cnpmcore/compare/v4.3.1...v4.4.0) (2025-04-09)

### Features

* add onnxruntime binary mirror ([#790](https://github.com/cnpm/cnpmcore/issues/790)) ([f4f09c2](f4f09c2d66))
2025-04-09 09:02:40 +00:00
Feng Yu
f4f09c2d66 feat: add onnxruntime binary mirror (#790) 2025-04-09 17:01:12 +08:00
semantic-release-bot
3dce867d3a Release 4.3.1
[skip ci]

## [4.3.1](https://github.com/cnpm/cnpmcore/compare/v4.3.0...v4.3.1) (2025-04-08)

### Bug Fixes

* execute SyncBinaryTask should with data ([#788](https://github.com/cnpm/cnpmcore/issues/788)) ([af3672e](af3672ebdb))
2025-04-08 05:55:10 +00:00
killa
af3672ebdb fix: execute SyncBinaryTask should with data (#788)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Enhanced the task management system to support binary data
synchronization tasks, enabling more precise handling of binary workflow
processes.
- Improved support for binary task execution and logging to ensure data
accuracy during synchronization.

- **Tests**
- Added new test cases to validate the proper execution and data
handling for binary synchronization tasks.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-08 13:53:45 +08:00
semantic-release-bot
e89d8528df Release 4.3.0
[skip ci]

## [4.3.0](https://github.com/cnpm/cnpmcore/compare/v4.2.0...v4.3.0) (2025-04-07)

### Features

* mirror astral-sh/python-build-standalone ([#787](https://github.com/cnpm/cnpmcore/issues/787)) ([58a1a9c](58a1a9c2af))
2025-04-07 06:28:18 +00:00
fengmk2
58a1a9c2af feat: mirror astral-sh/python-build-standalone (#787)
closes https://github.com/cnpm/cnpmcore/issues/784

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced a new option for obtaining redistributable Python builds,
making it easier for users to access pre-packaged releases via GitHub.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-07 14:26:54 +08:00
semantic-release-bot
ce0fd9d9c2 Release 4.2.0
[skip ci]

## [4.2.0](https://github.com/cnpm/cnpmcore/compare/v4.1.6...v4.2.0) (2025-04-03)

### Features

* **sql:** add gmt_create for binaries index idx_category_parent_gmt_create ([#786](https://github.com/cnpm/cnpmcore/issues/786)) ([080035f](080035f7bc))
2025-04-03 06:48:31 +00:00
killa
080035f7bc feat(sql): add gmt_create for binaries index idx_category_parent_gmt_create (#786)
Include sql change.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Enhanced data processing with additional safeguards to limit
iterations during data retrieval, improving stability and preventing
resource overuse.

- **Chores**
- Refined database indexing by removing an old index and adding a new
one, optimizing query performance for faster data access and a smoother
overall experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-03 14:47:12 +08:00
semantic-release-bot
ffd90473f7 Release 4.1.6
[skip ci]

## [4.1.6](https://github.com/cnpm/cnpmcore/compare/v4.1.5...v4.1.6) (2025-04-02)

### Bug Fixes

* iterator chromium bucket to get all versions ([#785](https://github.com/cnpm/cnpmcore/issues/785)) ([facb26d](facb26d192))
2025-04-02 14:46:56 +00:00
killa
facb26d192 fix: iterator chromium bucket to get all versions (#785)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced an admin-protected endpoint for binary synchronization with
support for additional data.
- Added enhanced logic to dynamically retrieve and update the latest
binary versions for multiple platforms.
  
- **Improvements**
- Optimized binary data fetching by migrating to a more dynamic,
cloud-based data source.
  
- **Dependency Management**
- Updated project dependencies to improve XML parsing capabilities while
removing outdated tools.
  
- **Tests**
- Revised test workflows to verify the new synchronization parameters
and data structures.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-02 22:45:18 +08:00
zhangyuantao
53060c817a test: vscode test config error (#783) 2025-03-29 19:40:59 +08:00
semantic-release-bot
9f4b8ebb00 Release 4.1.5
[skip ci]

## [4.1.5](https://github.com/cnpm/cnpmcore/compare/v4.1.4...v4.1.5) (2025-03-27)

### Bug Fixes

* 🐛 oss config assert ([#780](https://github.com/cnpm/cnpmcore/issues/780)) ([5c132b8](5c132b882b)), closes [#779](https://github.com/cnpm/cnpmcore/issues/779)
2025-03-27 11:23:53 +00:00
AN Meng
5c132b882b fix: 🐛 oss config assert (#780)
 Closes: #779

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Chores**
- Updated configuration validation to require the OSS bucket setting
instead of a CDN URL, with no changes to public interfaces.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: ANMeng <littleliar.im@qq.com>
2025-03-27 19:22:26 +08:00
fengmk2
a1e1072d1a test: enable more oxlint rules (#778)
- unicorn/prefer-number-properties
- typescript/no-import-type-side-effects
- unicorn/no-array-for-each
- typescript/no-dynamic-delete
- no-empty-function
- import/unambiguous
- max-depth 6
- unicorn/prefer-array-some
- unicorn/prefer-string-slice
- no-lonely-if
- max-nested-callbacks 5
- unicorn/explicit-length-check
- unicorn/no-lonely-if
- no-negated-condition
- no-else-return
- unicorn/prefer-date-now
- typescript/prefer-ts-expect-error
- typescript/ban-ts-comment
- no-throw-literal
- typescript/prefer-enum-initializers
- unicorn/no-typeof-undefined

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Chores**  
- Upgraded dependency version of `oxlint` to `^0.16.0` for improved code
quality.

- **Refactor**  
- Streamlined internal logic for error handling, string manipulation,
and data parsing, enhancing maintainability and performance.
- Simplified conditional logic and iteration methods in various services
and controllers for improved clarity.
- Updated import statements for type clarity and consistency across the
codebase.

- **Tests**  
- Improved test assertions and iteration methods to provide clearer
verification of system reliability.
- Enhanced logging functionality in test cases for better visibility of
output and error handling.
- Modified assertions to ensure more precise requirements in test cases.

These behind-the-scenes enhancements contribute to a more robust and
stable application, ensuring a smoother experience for end-users.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-18 23:48:00 +08:00
fengmk2
5d5f40ba26 chore: more oxlint rules (#774) 2025-03-18 00:38:32 +08:00
semantic-release-bot
b322f2c9ec Release 4.1.4
[skip ci]

## [4.1.4](https://github.com/cnpm/cnpmcore/compare/v4.1.3...v4.1.4) (2025-03-16)

### Bug Fixes

* use tegg beta ([#776](https://github.com/cnpm/cnpmcore/issues/776)) ([dd5ee4f](dd5ee4ff30))
2025-03-16 02:50:34 +00:00
fengmk2
dd5ee4ff30 fix: use tegg beta (#776)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Chores**
- Updated dependency versioning where specific beta numbers were
replaced with a broader beta designation. This change streamlines
dependency management and supports smoother integrations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-16 10:49:07 +08:00
semantic-release-bot
0c9a515874 Release 4.1.3
[skip ci]

## [4.1.3](https://github.com/cnpm/cnpmcore/compare/v4.1.2...v4.1.3) (2025-03-16)

### Performance Improvements

* refactor AsyncTimer advice to Singleton ([#775](https://github.com/cnpm/cnpmcore/issues/775)) ([884ff50](884ff50a2f))
2025-03-16 01:50:28 +00:00
killa
884ff50a2f perf: refactor AsyncTimer advice to Singleton (#775)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Refactor**
- Streamlined asynchronous operation tracking for improved performance
monitoring.
- Enhanced event handling configuration to provide more consistent and
reliable system behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-16 09:48:56 +08:00
fengmk2
1517750513 test: disable oss tests 2025-03-14 20:53:57 +08:00
semantic-release-bot
64beee1ba1 Release 4.1.2
[skip ci]

## [4.1.2](https://github.com/cnpm/cnpmcore/compare/v4.1.1...v4.1.2) (2025-03-14)

### Bug Fixes

* use tsconfig.json for tsc ([#773](https://github.com/cnpm/cnpmcore/issues/773)) ([acb988b](acb988b553))
2025-03-14 12:09:48 +00:00
fengmk2
acb988b553 fix: use tsconfig.json for tsc (#773)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Chores**
- Updated the build command to use revised TypeScript configuration
settings, ensuring a streamlined and consistent build process.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-14 20:08:49 +08:00
fengmk2
ffe723e65f refactor: use oxlint instead of eslint (#772)
say goodbye to eslint
2025-03-13 23:31:13 +08:00
semantic-release-bot
ed4d5d07ad Release 4.1.1
[skip ci]

## [4.1.1](https://github.com/cnpm/cnpmcore/compare/v4.1.0...v4.1.1) (2025-03-11)

### Bug Fixes

* don't block tgz ([#770](https://github.com/cnpm/cnpmcore/issues/770)) ([b92354d](b92354d280))
2025-03-11 14:42:50 +00:00
fengmk2
b92354d280 fix: don't block tgz (#770)
revert https://github.com/cnpm/cnpmcore/pull/763

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Refactor**
- Streamlined the package download process by removing outdated package
blocking checks, leading to a smoother and more direct download
experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-11 22:41:16 +08:00
semantic-release-bot
f51f6028ac Release 4.1.0
[skip ci]

## [4.1.0](https://github.com/cnpm/cnpmcore/compare/v4.0.3...v4.1.0) (2025-03-09)

### Features

* remove codeql-analysis.yml ([3f3751b](3f3751b2d5))
* use tegg v4 ([#769](https://github.com/cnpm/cnpmcore/issues/769)) ([7938919](7938919d81))
2025-03-09 09:30:39 +00:00
fengmk2
3f3751b2d5 feat: remove codeql-analysis.yml 2025-03-09 17:28:49 +08:00
fengmk2
7938919d81 feat: use tegg v4 (#769)
[skip ci]

---------

Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net>
2025-03-09 17:20:44 +08:00
fengmk2
52c2494355 test: fix unstable tests 2025-03-09 17:11:20 +08:00
fengmk2
0d4fec90dd test: remove oxlint quiet 2025-03-09 16:53:52 +08:00
fengmk2
b8d055a74e test: remove test/common/EnvUtil.test.ts 2025-03-09 16:50:12 +08:00
fengmk2
5c8b024a0a Merge branch 'next' into merge-next 2025-03-09 16:45:43 +08:00
semantic-release-bot
914aee2560 Release 3.75.1
[skip ci]

## [3.75.1](https://github.com/cnpm/cnpmcore/compare/v3.75.0...v3.75.1) (2025-03-09)

### Bug Fixes

* only get packageId from database ([#768](https://github.com/cnpm/cnpmcore/issues/768)) ([bc068d1](bc068d165c))
2025-03-09 02:10:36 +00:00
fengmk2
bc068d165c fix: only get packageId from database (#768)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Refactor**
- Enhanced the internal process for retrieving package version details,
resulting in more accurate and reliable version information.
- Streamlined the handling of missing package data to reduce unexpected
issues.
- Optimized overall data processing, contributing to improved stability
and a smoother user experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-09 10:09:03 +08:00
semantic-release-bot
9ffb09eaa8 Release 3.75.0
[skip ci]

## [3.75.0](https://github.com/cnpm/cnpmcore/compare/v3.74.2...v3.75.0) (2025-03-09)

### Features

* mirror node-pty-prebuilt-multiarch ([#767](https://github.com/cnpm/cnpmcore/issues/767)) ([cbefb5c](cbefb5c6d0))
2025-03-09 01:56:37 +00:00
时瑾
cbefb5c6d0 feat: mirror node-pty-prebuilt-multiarch (#767)
closes https://github.com/cnpm/cnpmcore/issues/766

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Expanded our binary support to include prebuilt binaries for the
node-pty package, enhancing integration possibilities and deployment
options for users relying on these binaries.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-09 09:55:02 +08:00
fengmk2
0a64698ec0 refactor: use tegg v4 (#765) 2025-03-06 00:04:09 +08:00
elrrrrrrr
1922bf2f76 chore: disbale public registration by default (#764)
> The 'allowPublicRegistration' is enabled by default, which my cause
unexpected users registering arbitrarily
1. ⚙ Modify the default configuration 'allowPublicRegistration' to
'false'`

-------------
> 目前默认开启了 `allowPublicRegistration` 配置,公网部署可能会导致预期外的用户任意注册

1. ⚙ 修改默认配置 `allowPublicRegistration` 为 `false`


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Public registration can now be enabled through a new configuration
option, allowing for more flexible user onboarding.
  
- **Bug Fixes**
- Updated the configuration to disallow public registration by default,
ensuring only administrators can log in unless changed.

- **Documentation**
- Added an informational note in the developer documentation regarding
public registration settings.

- **Tests**
- Introduced a setup method to enable public registration before each
test case runs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-28 09:34:11 +08:00
semantic-release-bot
47da2f40cf Release 3.74.2
[skip ci]

## [3.74.2](https://github.com/cnpm/cnpmcore/compare/v3.74.1...v3.74.2) (2025-02-27)

### Bug Fixes

* block tgz ([#763](https://github.com/cnpm/cnpmcore/issues/763)) ([3054577](305457777e))
2025-02-27 06:44:47 +00:00
elrrrrrrr
305457777e fix: block tgz (#763)
> The tgz download interface does not check if the package is blocked,
which may pose additional risks for parsing package-lock.json or other
lock files.
[exp](https://registry.npmmirror.com/joker-su/-/joker-su-1.0.0.tgz)
1. 🛡️ Add validation logic for
DownloadPackageVersionTarController#download to check if the package is
allowed to be downloaded.
2. 🧶 Add PackageVersionService#findBlockInfo to check if the
corresponding package is blocked.
3. ♻️ When a single version is blocked, skip check as per the current
manifest logic.

---------

> tgz 下载接口没有判断包是否被 block,对于 package-lock.json
或者其他依赖锁文件解析可能会有额外风险,[exp](https://registry.npmmirror.com/joker-su/-/joker-su-1.0.0.tgz)

1. 🛡️ `DownloadPackageVersionTarController#download` 接口新增校验逻辑,判断是否允许下载
2. 🧶 新增 PackageVersionService#findBlockInfo 判断对应包是否被全局拦截
3. ♻️ 单版本被 block 时,考虑到误封场景,按目前 manifest 逻辑,不在 tgz 下载时进行拦截操作

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Enhanced the package download process with an additional block check.
Now, if a package is flagged, the download will be halted and a clear
error response is returned to inform users of the block.
- Introduced a method to retrieve block information related to package
versions, improving the service's capabilities.

- **Tests**
- Added new test cases to verify the blocking functionality for package
downloads, ensuring the application correctly handles requests for
blocked packages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-27 06:43:28 +00:00
Kevin Cui
ae88145317 docs(elk): add missing version field (#760) 2025-02-26 18:01:05 +08:00
semantic-release-bot
069afb98cc Release 4.0.2
[skip ci]

## [4.0.2](https://github.com/cnpm/cnpmcore/compare/v4.0.1...v4.0.2) (2025-02-10)

### Bug Fixes

* should return default value when env is empty string ([#759](https://github.com/cnpm/cnpmcore/issues/759)) ([b6c781e](b6c781ec25))
2025-02-10 11:47:32 +00:00
fengmk2
b6c781ec25 fix: should return default value when env is empty string (#759)
pick from https://github.com/cnpm/cnpmcore/pull/754
2025-02-10 19:46:06 +08:00
semantic-release-bot
05b3b798b6 Release 3.74.1
[skip ci]

## [3.74.1](https://github.com/cnpm/cnpmcore/compare/v3.74.0...v3.74.1) (2025-02-10)

### Bug Fixes

* should return default value when env is empty string ([#758](https://github.com/cnpm/cnpmcore/issues/758)) ([e72e396](e72e396e3c))
2025-02-10 01:28:21 +00:00
fengmk2
e72e396e3c fix: should return default value when env is empty string (#758)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- Refactor
- Enhanced environment variable handling to trim extra whitespace and
improve default value checks for more robust configuration processing.
  
- Tests
- Expanded test coverage to validate default behavior, type conversions,
and error handling for various environment variable scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-10 09:26:47 +08:00
fengmk2
d095d3f48c chore: fix docker demo url 2025-02-09 23:25:05 +08:00
fengmk2
b0f7bf0967 chore: enable elasticsearch on local dev env (#757) 2025-02-09 23:21:49 +08:00
fengmk2
02a1ee35d7 chore: support alpine docker image (#755) 2025-02-09 21:30:37 +08:00
semantic-release-bot
1e9d710b0f Release 4.0.1
[skip ci]

## [4.0.1](https://github.com/cnpm/cnpmcore/compare/v4.0.0...v4.0.1) (2025-02-09)

### Bug Fixes

* remove npm-cli-login for ssri security ([#754](https://github.com/cnpm/cnpmcore/issues/754)) ([d18981e](d18981e658))
2025-02-09 08:40:22 +00:00
fengmk2
d18981e658 fix: remove npm-cli-login for ssri security (#754)
https://github.com/advisories/GHSA-vx3p-948g-6vhq

![image](https://github.com/user-attachments/assets/4eaf45a2-f3da-45ba-9613-c3bcb62f31f2)

https://hub.docker.com/repository/docker/fengmk2/cnpmcore/tags/v4/sha256-38b310438907eec1f00da6f110f6f24c81c85e983833fddd08a31598510154f3
2025-02-09 16:38:55 +08:00
365 changed files with 39757 additions and 11704 deletions

36
.docker/alpine/Dockerfile Normal file
View File

@@ -0,0 +1,36 @@
FROM node:22-alpine
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY . .
RUN .docker/build.sh
ENV NODE_ENV=production \
EGG_SERVER_ENV=prod \
CNPMCORE_CONFIG_REGISTRY= \
CNPMCORE_CONFIG_SOURCE_REGISTRY=https://registry.npmmirror.com \
CNPMCORE_CONFIG_SOURCE_REGISTRY_IS_CNPM=true \
CNPMCORE_DATABASE_TYPE= \
CNPMCORE_DATABASE_NAME= \
CNPMCORE_DATABASE_HOST= \
CNPMCORE_DATABASE_PORT=3306 \
CNPMCORE_DATABASE_USER= \
CNPMCORE_DATABASE_PASSWORD= \
CNPMCORE_REDIS_HOST= \
CNPMCORE_REDIS_PORT=6379 \
CNPMCORE_REDIS_PASSWORD= \
CNPMCORE_REDIS_DB= \
CNPMCORE_NFS_TYPE=s3 \
CNPMCORE_NFS_S3_CLIENT_ENDPOINT= \
CNPMCORE_NFS_S3_CLIENT_BUCKET= \
CNPMCORE_NFS_S3_CLIENT_ID= \
CNPMCORE_NFS_S3_CLIENT_SECRET= \
CNPMCORE_NFS_S3_CLIENT_FORCE_PATH_STYLE=true \
CNPMCORE_NFS_S3_CLIENT_DISABLE_URL=true \
TZ=Asia/Shanghai
EXPOSE 7001
CMD ["npm", "run", "start:foreground"]

7
.docker/build.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
node -v && npm -v \
&& npm install -g npminstall --registry=https://registry.npmmirror.com \
&& npminstall -c \
&& npm run tsc \
&& npmupdate -c --production

36
.docker/debian/Dockerfile Normal file
View File

@@ -0,0 +1,36 @@
FROM node:22-bookworm-slim
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY . .
RUN .docker/build.sh
ENV NODE_ENV=production \
EGG_SERVER_ENV=prod \
CNPMCORE_CONFIG_REGISTRY= \
CNPMCORE_CONFIG_SOURCE_REGISTRY=https://registry.npmmirror.com \
CNPMCORE_CONFIG_SOURCE_REGISTRY_IS_CNPM=true \
CNPMCORE_DATABASE_TYPE= \
CNPMCORE_DATABASE_NAME= \
CNPMCORE_DATABASE_HOST= \
CNPMCORE_DATABASE_PORT=3306 \
CNPMCORE_DATABASE_USER= \
CNPMCORE_DATABASE_PASSWORD= \
CNPMCORE_REDIS_HOST= \
CNPMCORE_REDIS_PORT=6379 \
CNPMCORE_REDIS_PASSWORD= \
CNPMCORE_REDIS_DB= \
CNPMCORE_NFS_TYPE=s3 \
CNPMCORE_NFS_S3_CLIENT_ENDPOINT= \
CNPMCORE_NFS_S3_CLIENT_BUCKET= \
CNPMCORE_NFS_S3_CLIENT_ID= \
CNPMCORE_NFS_S3_CLIENT_SECRET= \
CNPMCORE_NFS_S3_CLIENT_FORCE_PATH_STYLE=true \
CNPMCORE_NFS_S3_CLIENT_DISABLE_URL=true \
TZ=Asia/Shanghai
EXPOSE 7001
CMD ["npm", "run", "start:foreground"]

View File

@@ -7,3 +7,44 @@
# CNPMCORE_DATABASE_USER=postgres
# CNPMCORE_DATABASE_PASSWORD=postgres
# CNPMCORE_DATABASE_NAME=cnpmcore
# CNPMCORE_CONFIG_ENABLE_ES=true
# CNPMCORE_CONFIG_ES_CLIENT_NODE=http://localhost:9200
# CNPMCORE_CONFIG_ES_CLIENT_AUTH_USERNAME=elastic
# CNPMCORE_CONFIG_ES_CLIENT_AUTH_PASSWORD=abcdef
# https://github.com/cnpm/cnpmcore/blob/next/docs/elasticsearch-setup.md#%E6%96%B0%E5%BB%BA-env-%E6%96%87%E4%BB%B6
# Password for the 'elastic' user (at least 6 characters)
ELASTIC_PASSWORD="abcdef"
# Password for the 'kibana_system' user (at least 6 characters)
KIBANA_PASSWORD="abcdef"
# Version of Elastic products
STACK_VERSION=8.7.1
# enable for arm64
# STACK_VERSION_ARM64=-arm64
# STACK_PLATFORM=linux/arm64
# Set the cluster name
CLUSTER_NAME=docker-cluster
# Set to 'basic' or 'trial' to automatically start the 30-day trial
LICENSE=basic
#LICENSE=trial
# Port to expose Elasticsearch HTTP API to the host
ES_PORT=9200
#ES_PORT=127.0.0.1:9200
# Port to expose Kibana to the host
KIBANA_PORT=5601
#KIBANA_PORT=80
# Increase or decrease based on the available host memory (in bytes)
ES_MEM_LIMIT=1073741824
KB_MEM_LIMIT=1073741824
LS_MEM_LIMIT=1073741824
# SAMPLE Predefined Key only to be used in POC environments
ENCRYPTION_KEY=c34d38b3a14956121ff2170e5030b471551370178f43e5626eec58b04a30fae2

View File

@@ -1,7 +0,0 @@
app/proxy*
**/*.d.ts
node_modules/
dist/
coverage/
mocks/
.react_entries/

View File

@@ -1,6 +0,0 @@
{
"extends": [
"eslint-config-egg/typescript",
"eslint-config-egg/lib/rules/enforce-node-prefix"
]
}

564
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,564 @@
# cnpmcore - Private NPM Registry for Enterprise
cnpmcore is a TypeScript-based private NPM registry implementation built with Egg.js framework. It provides enterprise-grade package management with support for MySQL/PostgreSQL databases, Redis caching, and optional Elasticsearch.
**ALWAYS reference these instructions first** and fallback to search or bash commands only when you encounter unexpected information that does not match the information here.
## Code Style and Conventions
### Linting and Formatting
- **Linter**: Oxlint (fast Rust-based linter)
- **Formatter**: Prettier with specific configuration
- **Pre-commit hooks**: Husky + lint-staged automatically format and lint on commit
**Code Style Rules:**
```javascript
// From .prettierrc
{
"singleQuote": true, // Use single quotes
"trailingComma": "es5", // ES5 trailing commas
"tabWidth": 2, // 2-space indentation
"printWidth": 120, // 120 character line width
"arrowParens": "avoid" // Avoid parens when possible
}
// From .oxlintrc.json
{
"max-params": 6, // Maximum 6 function parameters
"no-console": "warn", // Warn on console usage
"import/no-anonymous-default-export": "error"
}
```
**Linting Commands:**
```bash
npm run lint # Check for linting errors
npm run lint:fix # Auto-fix linting issues
npm run typecheck # TypeScript type checking without build
```
### TypeScript Conventions
- Use strict TypeScript with comprehensive type definitions
- Avoid `any` types - use proper typing or `unknown`
- Export types and interfaces for reusability
- Use ES modules (`import/export`) syntax throughout
### Testing Conventions
- Test files use `.test.ts` suffix
- Use `@eggjs/mock` for mocking and testing
- Tests organized to mirror source structure in `test/` directory
- Use `assert` from `node:assert/strict` for assertions
- Mock external dependencies using `mock()` from `@eggjs/mock`
**Test Naming Pattern:**
```typescript
describe('test/path/to/SourceFile.test.ts', () => {
describe('[HTTP_METHOD /api/path] functionName()', () => {
it('should handle expected behavior', async () => {
// Test implementation
});
});
});
```
## Domain-Driven Design (DDD) Architecture
cnpmcore follows **Domain-Driven Design** principles with clear separation of concerns:
### Layer Architecture (Dependency Flow)
```
Controller (HTTP Interface Layer)
↓ depends on
Service (Business Logic Layer)
↓ depends on
Repository (Data Access Layer)
↓ depends on
Model (ORM/Database Layer)
Entity (Domain Models - no dependencies, pure business logic)
Common (Utilities and Adapters - available to all layers)
```
### Layer Responsibilities
**Controller Layer** (`app/port/controller/`):
- HTTP request/response handling
- Request validation using `@eggjs/typebox-validate`
- User authentication and authorization
- **NO business logic** - delegate to Services
- Inheritance: `YourController extends AbstractController extends MiddlewareController`
**Service Layer** (`app/core/service/`):
- Core business logic implementation
- Orchestration of multiple repositories and entities
- Transaction management
- Event publishing
- NO HTTP concerns, NO direct database access
**Repository Layer** (`app/repository/`):
- Data access and persistence
- CRUD operations on Models
- Query building and optimization
- NO business logic
**Entity Layer** (`app/core/entity/`):
- Domain models with business behavior
- Pure business logic (no infrastructure dependencies)
- Immutable data structures where possible
- Rich domain objects (not anemic models)
**Model Layer** (`app/repository/model/`):
- ORM definitions using Leoric
- Database schema mapping
- Table and column definitions
- NO business logic
### Repository Method Naming Convention
**ALWAYS follow these naming patterns:**
- `findSomething` - Query a single model/entity
- `saveSomething` - Save (create or update) a model
- `removeSomething` - Delete a model
- `listSomethings` - Query multiple models (use plural)
### Request Validation Trilogy
**ALWAYS validate requests in this exact order:**
1. **Request Parameter Validation** - First line of defense
```typescript
// Use @eggjs/typebox-validate for type-safe validation
// See app/port/typebox.ts for examples
```
2. **User Authentication & Token Permissions**
```typescript
// Token roles: 'read' | 'publish' | 'setting'
const authorizedUser = await this.userRoleManager.requiredAuthorizedUser(ctx, 'publish');
```
3. **Resource Authorization** - Prevent horizontal privilege escalation
```typescript
// Example: Ensure user is package maintainer
await this.userRoleManager.requiredPackageMaintainer(pkg, authorizedUser);
// Or use convenience method
const { pkg } = await this.ensurePublishAccess(ctx, fullname);
```
### Modifying Database Models
When changing a Model, update **all 3 locations**:
1. SQL migration files: `sql/mysql/*.sql` AND `sql/postgresql/*.sql`
2. ORM Model: `app/repository/model/*.ts`
3. Domain Entity: `app/core/entity/*.ts`
**NEVER auto-generate SQL migrations** - manual review is required for safety.
## Prerequisites and Environment Setup
- **Node.js**: Version 20.18.0 or higher (required by engines field in package.json)
- **Database**: MySQL 5.7+ or PostgreSQL 17+
- **Cache**: Redis 6+
- **Optional**: Elasticsearch 8.x for enhanced search capabilities
## Working Effectively
### Bootstrap and Build
```bash
# Install dependencies (takes ~2 minutes)
npm install
# Copy environment configuration
cp .env.example .env
# Lint code (very fast, <1 second)
npm run lint
# Fix linting issues
npm run lint:fix
# Build TypeScript (takes ~6 seconds)
npm run tsc
# Production build (takes ~6 seconds)
npm run tsc:prod
```
### Database Setup - MySQL (Recommended for Development)
```bash
# Start MySQL + Redis services via Docker (takes ~1 minute to pull images initially)
docker compose -f docker-compose.yml up -d
# Verify services are running
docker compose ps
# Initialize database (takes <2 seconds)
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-mysql.sh
# For tests, create test database
mysql -h 127.0.0.1 -P 3306 -u root -e "CREATE DATABASE cnpmcore_unittest;"
```
### Database Setup - PostgreSQL (Alternative)
```bash
# Start PostgreSQL + Redis services via Docker
docker compose -f docker-compose-postgres.yml up -d
# Initialize database (takes <1 second)
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-postgresql.sh
```
### Development Server
```bash
# MySQL development server (starts in ~20 seconds)
npm run dev
# Server runs on http://127.0.0.1:7001
# PostgreSQL development server
npm run dev:postgresql
# Server runs on http://127.0.0.1:7001
```
### Testing
```bash
# Run full test suite with MySQL - NEVER CANCEL: Takes 4+ minutes. Set timeout to 10+ minutes.
npm run test
# Run full test suite with PostgreSQL - NEVER CANCEL: Takes 4+ minutes. Set timeout to 10+ minutes.
npm run test:postgresql
# Run single test file (for faster iteration, takes ~12 seconds)
npm run test:local test/common/CryptoUtil.test.ts
# Test coverage with MySQL - NEVER CANCEL: Takes 5+ minutes. Set timeout to 15+ minutes.
npm run cov
# Test coverage with PostgreSQL - NEVER CANCEL: Takes 5+ minutes. Set timeout to 15+ minutes.
npm run cov:postgresql
```
**CRITICAL TESTING NOTES:**
- **NEVER CANCEL** build or test commands - they may take 4-15 minutes to complete
- Individual test files run much faster (~12 seconds) for development iteration
- Full test suite processes 100+ test files and requires database initialization
- Test failures may occur in CI environment; use individual test files for validation
**Testing Philosophy:**
- **Write tests for all new features** - No feature is complete without tests
- **Test at the right layer** - Controller tests for HTTP, Service tests for business logic
- **Mock external dependencies** - Use `mock()` from `@eggjs/mock`
- **Use realistic test data** - Create through `TestUtil` helper methods
- **Clean up after tests** - Database is reset between test files
- **Test both success and failure cases** - Error paths are equally important
**Common Test Patterns:**
```typescript
import { app, mock } from '@eggjs/mock/bootstrap';
import { TestUtil } from '../../../test/TestUtil';
describe('test/path/to/YourController.test.ts', () => {
describe('[GET /api/endpoint] methodName()', () => {
it('should return expected result', async () => {
// Setup
const { authorization } = await TestUtil.createUser();
// Execute
const res = await app
.httpRequest()
.get('/api/endpoint')
.set('authorization', authorization)
.expect(200);
// Assert
assert.equal(res.body.someField, expectedValue);
});
it('should handle unauthorized access', async () => {
const res = await app
.httpRequest()
.get('/api/endpoint')
.expect(401);
assert.equal(res.body.error, '[UNAUTHORIZED] Login first');
});
});
});
```
### Production Commands
```bash
# CI pipeline commands - NEVER CANCEL: Takes 5+ minutes. Set timeout to 15+ minutes.
npm run ci # MySQL CI (includes lint, test, coverage, build)
npm run ci:postgresql # PostgreSQL CI
# Production start/stop
npm run start # Start as daemon
npm run stop # Stop daemon
npm run start:foreground # Start in foreground for debugging
```
## Validation Scenarios
**ALWAYS manually validate changes** by running through these scenarios:
### Basic API Validation
```bash
# Start development server
npm run dev
# Test registry root endpoint
curl http://127.0.0.1:7001
# Should return JSON with app metadata and stats
# Test authentication endpoint
curl http://127.0.0.1:7001/-/whoami
# Should return authentication error (expected when not logged in)
# Test package listing (initially empty)
curl http://127.0.0.1:7001/-/all
```
### Admin User Setup and Package Publishing
```bash
# Register admin user (cnpmcore_admin) - requires allowPublicRegistration=true in config
npm login --registry=http://127.0.0.1:7001
# Verify login
npm whoami --registry=http://127.0.0.1:7001
# Test package publishing
npm publish --registry=http://127.0.0.1:7001
```
## Architecture and Navigation
### Project Structure
```
app/
├── common/ # Global utilities and adapters
│ ├── adapter/ # External service adapters (NpmRegistry, Binary, etc.)
│ └── enum/ # Shared enumerations
├── core/ # Business logic layer
│ ├── entity/ # Core domain models
│ ├── event/ # Event handlers and async processing
│ ├── service/ # Core business services
│ └── util/ # Internal utilities
├── port/ # Interface layer
│ ├── controller/ # HTTP controllers
│ ├── middleware/ # Express middleware
│ ├── schedule/ # Background job schedulers
│ └── webauth/ # WebAuth integration
├── repository/ # Data access layer
│ ├── model/ # ORM models
│ └── util/ # Repository utilities
└── infra/ # Infrastructure adapters
```
### Key Services and Controllers
- **PackageController**: Main package CRUD operations
- **PackageManagerService**: Core package management business logic
- **BinarySyncerService**: Binary package synchronization
- **ChangesStreamService**: NPM registry change stream processing
- **UserController**: User authentication and profile management
### Infrastructure Adapters (`app/infra/`)
Enterprise customization layer for PaaS integration. cnpmcore provides default implementations, but enterprises should implement their own based on their infrastructure:
- **NFSClientAdapter**: File storage abstraction (local/S3/OSS)
- **QueueAdapter**: Message queue integration
- **AuthAdapter**: Authentication system integration
- **BinaryAdapter**: Binary package storage adapter
These adapters allow cnpmcore to integrate with different cloud providers and enterprise systems without modifying core business logic.
### Configuration Files
- `config/config.default.ts`: Main application configuration
- `config/database.ts`: Database connection settings
- `config/binaries.ts`: Binary package mirror configurations
- `.env`: Environment-specific variables
- `tsconfig.json`: TypeScript compilation settings
- `tsconfig.prod.json`: Production build settings
## Common Development Tasks
### Adding New Features
**ALWAYS follow this workflow:**
1. **Plan the change** - Identify which layers need modification
2. **Run linter** - `npm run lint:fix` to establish clean baseline
3. **Bottom-up implementation** - Build from data layer up to controller:
a. **Model Layer** (if new data structure needed):
- Add SQL migrations: `sql/mysql/*.sql` AND `sql/postgresql/*.sql`
- Create Model: `app/repository/model/YourModel.ts`
- Run database migration scripts
b. **Entity Layer** (domain models):
- Create Entity: `app/core/entity/YourEntity.ts`
- Implement business logic and behavior
- Keep entities pure (no infrastructure dependencies)
c. **Repository Layer** (data access):
- Create Repository: `app/repository/YourRepository.ts`
- Follow naming: `findX`, `saveX`, `removeX`, `listXs`
- Inject dependencies using `@Inject()`
d. **Service Layer** (business logic):
- Create Service: `app/core/service/YourService.ts`
- Orchestrate repositories and entities
- Use `@SingletonProto()` for service lifecycle
e. **Controller Layer** (HTTP endpoints):
- Create Controller: `app/port/controller/YourController.ts`
- Extend `AbstractController`
- Add HTTP method decorators: `@HTTPMethod()`, `@HTTPBody()`, etc.
- Implement 3-step validation (params → auth → authorization)
4. **Add tests** - Create test file: `test/path/matching/source/YourFile.test.ts`
5. **Lint and test** - `npm run lint:fix && npm run test:local test/your/test.test.ts`
6. **Type check** - `npm run typecheck`
7. **Commit** - Use semantic commit messages (feat/fix/chore/docs/test)
**Example Controller Implementation:**
```typescript
import { AbstractController } from './AbstractController';
import { HTTPController, HTTPMethod, HTTPQuery, Inject } from 'egg';
@HTTPController()
export class YourController extends AbstractController {
@Inject()
private readonly yourService: YourService;
@HTTPMethod({ path: '/api/path', method: 'GET' })
async yourMethod(@HTTPQuery() params: YourQueryType) {
// 1. Validate params (done by @HTTPQuery with typebox)
// 2. Authenticate user
const user = await this.userRoleManager.requiredAuthorizedUser(this.ctx, 'read');
// 3. Authorize resource access (if needed)
// 4. Delegate to service
return await this.yourService.doSomething(params);
}
}
```
### Database Migrations
- SQL files are in `sql/mysql/` and `sql/postgresql/`
- Migration scripts automatically run during database preparation
- **NEVER** modify existing migration files - only add new ones
### Background Jobs
- Schedulers are in `app/port/schedule/`
- Include sync workers, cleanup tasks, and stream processors
- Jobs run automatically when development server starts
## Troubleshooting
### Database Connection Issues
```bash
# Check if services are running
docker compose ps
# Reset MySQL environment
docker compose -f docker-compose.yml down
docker compose -f docker-compose.yml up -d
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-mysql.sh
# Reset PostgreSQL environment
docker compose -f docker-compose-postgres.yml down
docker compose -f docker-compose-postgres.yml up -d
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-postgresql.sh
```
### Build Issues
```bash
# Clean and rebuild
npm run clean
npm run tsc
# Check TypeScript configuration
npx tsc --noEmit
```
### Test Issues
```bash
# Create missing test database
mysql -h 127.0.0.1 -P 3306 -u root -e "CREATE DATABASE cnpmcore_unittest;"
# Run single test for debugging
npm run test:local test/common/CryptoUtil.test.ts
```
## CI/CD Integration
The project uses GitHub Actions with workflows in `.github/workflows/`:
- `nodejs.yml`: Main CI pipeline with MySQL, PostgreSQL, and Elasticsearch testing
- Multiple Node.js versions tested: 20, 22, 24
- **CRITICAL**: CI jobs include long-running tests that can take 15+ minutes per database type
### Pre-commit Validation
**ALWAYS run before committing:**
```bash
npm run lint:fix # Fix linting issues
npm run tsc # Verify TypeScript compilation
npm run test:local test/path/to/relevant.test.ts # Run relevant tests
```
## Docker Support
### Development Environments
- `docker-compose.yml`: MySQL + Redis + phpMyAdmin
- `docker-compose-postgres.yml`: PostgreSQL + Redis + pgAdmin
- `docker-compose-es.yml`: Elasticsearch integration
### Production Images
```bash
# Build Alpine image
npm run images:alpine
# Build Debian image
npm run images:debian
```
## External Dependencies
- **Database**: MySQL 9.x or PostgreSQL 17+
- **Cache**: Redis 6+
- **Search**: Elasticsearch 8.x (optional)
- **Storage**: Local filesystem or S3-compatible storage
- **Framework**: Egg.js with extensive TypeScript integration
## Performance Notes
Command execution times (for timeout planning):
- **Startup Time**: ~20 seconds for development server
- **Build Time**: ~6 seconds for TypeScript compilation
- **Test Time**: 4-15 minutes for full suite (database dependent)
- **Individual Test**: ~12 seconds for single test file
- **Package Installation**: ~2 minutes for npm install
- **Database Init**: <2 seconds for either MySQL or PostgreSQL
- **Linting**: <1 second (oxlint is very fast)
Always account for these timings when setting timeouts for automated processes.
## Semantic Commit Messages
Use conventional commit format for all commits:
- `feat:` - New features
- `fix:` - Bug fixes
- `docs:` - Documentation changes
- `chore:` - Maintenance tasks
- `test:` - Test additions or modifications
- `refactor:` - Code refactoring
- `perf:` - Performance improvements
Examples:
```bash
feat: add support for GitHub binary mirroring
fix: resolve authentication token expiration issue
docs: update API documentation for sync endpoints
test: add tests for package publication workflow
```

View File

@@ -1,67 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript', 'typescript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -9,7 +9,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/first-interaction@v1
- uses: actions/first-interaction@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: '我们已经看到你的反馈,如果是功能缺陷,可以提供一下重现该问题的方式;如果是新功能需求,我们会尽快加入讨论。同时我们非常期待你可以加入我们的贡献者行列,让项目可以长期可持续发展。'

View File

@@ -5,12 +5,128 @@ name: Node.js CI
on:
push:
branches: [ master, next ]
branches: [master]
pull_request:
branches: [ master, next ]
branches: [master]
merge_group:
jobs:
typecheck:
runs-on: ubuntu-latest
concurrency:
group: typecheck-${{ github.workflow }}-#${{ github.event.pull_request.number || github.head_ref || github.ref }}
cancel-in-progress: true
steps:
- name: Checkout Git Source
uses: actions/checkout@v5
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: 22
- name: Install Dependencies
run: npm i
- name: Lint
run: npm run lint
- name: Typecheck
run: npm run typecheck
- name: Build
run: npm run tsc && npm run tsc:prod
test-deployment:
runs-on: ubuntu-latest
concurrency:
group: test-deployment-${{ github.workflow }}-#${{ github.event.pull_request.number || github.head_ref || github.ref }}
cancel-in-progress: true
services:
mysql:
image: mysql:5.7
env:
MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: cnpmcore
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
redis:
# https://docs.github.com/en/actions/using-containerized-services/about-service-containers#example-mapping-redis-ports
image: redis
ports:
# Opens tcp port 6379 on the host and service container
- 6379:6379
steps:
- name: Checkout Git Source
uses: actions/checkout@v5
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: 22
- name: Install Dependencies
run: npm i
- name: Test Deployment
run: |
npm run build
echo "Preparing database..."
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-mysql.sh
echo "Starting cnpmcore..."
CNPMCORE_FORCE_LOCAL_FS=true npm run start:foreground &
sleep 5
echo "Checking cnpmcore is ready..."
set -Eeuo pipefail
URL="http://127.0.0.1:7001"
PATTERN="instance_start_time"
TIMEOUT=60
TMP="$(mktemp)"
echo "🔎 Health check $URL, expect 200 & body contains: $PATTERN"
deadline=$((SECONDS + TIMEOUT))
last_status=""
while (( SECONDS < deadline )); do
last_status="$(curl -sS -o "$TMP" -w '%{http_code}' "$URL" || true)"
echo "last_status=$last_status"
echo "body=$(cat $TMP)"
if [[ "$last_status" == "200" ]] && grep -q "$PATTERN" "$TMP"; then
echo "✅ OK"
rm -f "$TMP"
npx eggctl stop
exit 0
fi
sleep 1
done
echo "::error::❌ Health check failed: status=$last_status"
echo "---- Response body (last try) ----"
cat "$TMP" || true
rm -f "$TMP"
exit 1
test-postgresql-fs-nfs:
strategy:
fail-fast: false
matrix:
node-version: [20, 22, 24]
os: [ubuntu-latest]
# 0-based index
shardIndex: [0, 1, 2]
shardTotal: [3]
name: test on postgresql (node@${{ matrix.node-version }}, shard@${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
concurrency:
group: test-postgresql-fs-nfs-${{ github.workflow }}-#${{ github.event.pull_request.number || github.head_ref || github.ref }}-${{ matrix.node-version }}-${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
cancel-in-progress: true
runs-on: ${{ matrix.os }}
services:
@@ -38,40 +154,75 @@ jobs:
# Opens tcp port 6379 on the host and service container
- 6379:6379
steps:
- name: Checkout Git Source
uses: actions/checkout@v5
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- name: Install Dependencies
run: npm i
# https://github.com/elastic/elastic-github-actions/blob/master/elasticsearch/README.md
- name: Configure sysctl limits
run: |
sudo swapoff -a
sudo sysctl -w vm.swappiness=1
sudo sysctl -w fs.file-max=262144
sudo sysctl -w vm.max_map_count=262144
- name: Runs Elasticsearch
uses: elastic/elastic-github-actions/elasticsearch@master
with:
stack-version: 8.18.0
security-enabled: false
- name: Wait for Elasticsearch to be ready
run: |
curl -v http://localhost:9200
while ! curl -s http://localhost:9200 | grep -q "elasticsearch"; do
echo "Waiting for Elasticsearch to be ready..."
sleep 1
done
- name: Continuous Integration
run: npm run ci:postgresql
env:
# The hostname used to communicate with the PostgreSQL service container
POSTGRES_HOST: localhost
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
# The default PostgreSQL port
POSTGRES_PORT: 5432
CNPMCORE_CONFIG_ENABLE_ES: true
CNPMCORE_CONFIG_ES_CLIENT_NODES: http://localhost:9200
# https://github.com/jamiebuilds/ci-parallel-vars
CI_NODE_INDEX: ${{ matrix.shardIndex }}
CI_NODE_TOTAL: ${{ matrix.shardTotal }}
- name: Code Coverage
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
test-mysql57-fs-nfs:
strategy:
fail-fast: false
matrix:
node-version: [20.18.0, 20, 22]
node-version: [20, 22, 24]
os: [ubuntu-latest]
# 0-based index
shardIndex: [0, 1, 2]
shardTotal: [3]
steps:
- name: Checkout Git Source
uses: actions/checkout@v4
name: test on mysql (node@${{ matrix.node-version }}, shard@${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
concurrency:
group: test-mysql57-fs-nfs-${{ github.workflow }}-#${{ github.event.pull_request.number || github.head_ref || github.ref }}-${{ matrix.node-version }}-${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
cancel-in-progress: true
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install Dependencies
run: npm i -g npminstall && npminstall
- name: Continuous Integration
run: npm run ci:postgresql
env:
# The hostname used to communicate with the PostgreSQL service container
POSTGRES_HOST: localhost
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
# The default PostgreSQL port
POSTGRES_PORT: 5432
- name: Code Coverage
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
test-mysql57-fs-nfs:
runs-on: ${{ matrix.os }}
services:
@@ -90,88 +241,42 @@ jobs:
# Opens tcp port 6379 on the host and service container
- 6379:6379
strategy:
fail-fast: false
matrix:
node-version: [20.18.0, 20, 22]
os: [ubuntu-latest]
steps:
- name: Checkout Git Source
uses: actions/checkout@v4
- name: Checkout Git Source
uses: actions/checkout@v5
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- name: Install Dependencies
run: npm i -g npminstall && npminstall
- name: Install Dependencies
run: npm i
- name: Continuous Integration
run: npm run ci
- name: Code Coverage
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
test-mysql57-oss-nfs:
runs-on: ${{ matrix.os }}
if: |
contains('
refs/heads/master-skip-oss
refs/heads/dev-skip-oss
', github.ref)
services:
mysql:
image: mysql:5.7
- name: Continuous Integration
run: npm run ci
env:
MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: cnpmcore_unittest
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5
# https://github.com/jamiebuilds/ci-parallel-vars
CI_NODE_INDEX: ${{ matrix.shardIndex }}
CI_NODE_TOTAL: ${{ matrix.shardTotal }}
redis:
image: redis
ports:
- 6379:6379
- name: Code Coverage
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
strategy:
fail-fast: false
matrix:
node-version: [20, 22]
os: [ubuntu-latest]
steps:
- name: Checkout Git Source
uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install Dependencies
run: npm i
- name: Continuous Integration
run: npm run ci
env:
CNPMCORE_NFS_TYPE: oss
CNPMCORE_NFS_OSS_BUCKET: cnpmcore-unittest-github-nodejs-${{ matrix.node-version }}
CNPMCORE_NFS_OSS_ENDPOINT: https://oss-us-west-1.aliyuncs.com
CNPMCORE_NFS_OSS_ID: ${{ secrets.CNPMCORE_NFS_OSS_ID }}
CNPMCORE_NFS_OSS_SECRET: ${{ secrets.CNPMCORE_NFS_OSS_SECRET }}
- name: Code Coverage
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
test-mysql57-s3-nfs:
if: ${{ github.ref_name == 'master' }}
strategy:
fail-fast: false
matrix:
node-version: [20, 22]
os: [ubuntu-latest]
concurrency:
group: test-mysql57-s3-nfs-${{ github.workflow }}-#${{ github.event.pull_request.number || github.head_ref || github.ref }}-${{ matrix.node-version }}
cancel-in-progress: true
runs-on: ${{ matrix.os }}
services:
@@ -189,37 +294,41 @@ jobs:
ports:
- 6379:6379
strategy:
fail-fast: false
matrix:
node-version: [20, 22]
os: [ubuntu-latest]
steps:
- name: Checkout Git Source
uses: actions/checkout@v4
- name: Checkout Git Source
uses: actions/checkout@v5
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- name: Install Dependencies
run: npm i
- name: Install Dependencies
run: npm i
- name: Continuous Integration
run: npm run ci "test/cli/npm/install.test.ts"
env:
CNPMCORE_NFS_TYPE: s3
CNPMCORE_NFS_REMOVE_BEFORE_UPLOAD: true
CNPMCORE_NFS_S3_CLIENT_BUCKET: cnpmcore-unittest-github-nodejs-${{ matrix.node-version }}
CNPMCORE_NFS_S3_CLIENT_ENDPOINT: ${{ secrets.CNPMCORE_NFS_S3_ENDPOINT }}
CNPMCORE_NFS_S3_CLIENT_ID: ${{ secrets.CNPMCORE_NFS_S3_ID }}
CNPMCORE_NFS_S3_CLIENT_SECRET: ${{ secrets.CNPMCORE_NFS_S3_SECRET }}
CNPMCORE_NFS_S3_CLIENT_FORCE_PATH_STYLE: true
# CNPMCORE_NFS_S3_CLIENT_DISABLE_URL: true
- name: Continuous Integration
run: npm run ci "test/cli/npm/install.test.ts"
env:
CNPMCORE_NFS_TYPE: s3
CNPMCORE_NFS_REMOVE_BEFORE_UPLOAD: true
CNPMCORE_NFS_S3_CLIENT_BUCKET: cnpmcore-unittest-github-nodejs-${{ matrix.node-version }}
CNPMCORE_NFS_S3_CLIENT_ENDPOINT: ${{ secrets.CNPMCORE_NFS_S3_ENDPOINT }}
CNPMCORE_NFS_S3_CLIENT_ID: ${{ secrets.CNPMCORE_NFS_S3_ID }}
CNPMCORE_NFS_S3_CLIENT_SECRET: ${{ secrets.CNPMCORE_NFS_S3_SECRET }}
CNPMCORE_NFS_S3_CLIENT_FORCE_PATH_STYLE: true
# CNPMCORE_NFS_S3_CLIENT_DISABLE_URL: true
- name: Code Coverage
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Code Coverage
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
done:
runs-on: ubuntu-latest
needs:
- test-postgresql-fs-nfs
- test-mysql57-fs-nfs
- typecheck
steps:
- run: exit 1
if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}

75
.github/workflows/release-image.yml vendored Normal file
View File

@@ -0,0 +1,75 @@
# https://docs.github.com/en/actions/tutorials/publish-packages/publish-docker-images#publishing-images-to-github-packages
name: Create and publish a Docker image
# Configures this workflow to run manually
on:
workflow_dispatch:
pull_request:
branches: [master]
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
jobs:
build-and-push-image:
runs-on: ubuntu-latest
concurrency:
group: build-and-push-image-${{ github.workflow }}-#${{ github.event.pull_request.number || github.head_ref || github.ref }}
cancel-in-progress: true
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Log in to the Container registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see [Usage](https://github.com/docker/build-push-action#usage) in the README of the `docker/build-push-action` repository.
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
file: .docker/debian/Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# This step generates an artifact attestation for the image, which is a tamper-proof statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see [Using artifact attestations to establish provenance for builds](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds).
- name: Generate artifact attestation
if: github.event_name != 'pull_request'
uses: actions/attest-build-provenance@v3
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

View File

@@ -1,12 +1,18 @@
name: Release
on:
push:
branches: [ master, next ]
branches: [master]
permissions:
contents: write
deployments: write
issues: write
pull-requests: write
id-token: write
jobs:
release:
name: Node.js
uses: cnpm/github-actions/.github/workflows/node-release.yml@master
name: NPM
uses: cnpm/github-actions/.github/workflows/npm-release.yml@master
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
npx lint-staged

24
.oxlintrc.json Normal file
View File

@@ -0,0 +1,24 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
// FIXME: @eggjs/oxlint-config too strict, disable it for now, will fix it later
// "extends": ["./node_modules/@eggjs/oxlint-config/.oxlintrc.json"],
"env": {
"node": true,
"mocha": true
},
"rules": {
// Project-specific overrides
"max-params": ["error", 6],
"no-console": "warn",
"import/no-anonymous-default-export": "error",
"no-unassigned-import": "allow",
"new-cap": "allow",
"class-methods-use-this": "allow",
"import/no-named-export": "allow",
"unicorn/no-array-sort": "allow",
"no-param-reassign": "allow",
"unicorn/prefer-at": "allow",
"no-process-env": "allow"
},
"ignorePatterns": ["index.d.ts"]
}

4
.prettierignore Normal file
View File

@@ -0,0 +1,4 @@
CHANGELOG.md
__snapshots__
pnpm-lock.yaml
node_modules

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"printWidth": 120,
"arrowParens": "avoid"
}

14
.vscode/launch.json vendored
View File

@@ -9,12 +9,7 @@
"request": "launch",
"name": "Egg Debug",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev",
"--",
"--inspect-brk"
],
"runtimeArgs": ["run", "dev", "--", "--inspect-brk"],
"console": "integratedTerminal",
"restart": true,
"autoAttachChildProcesses": true
@@ -24,12 +19,7 @@
"request": "launch",
"name": "Egg Test",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"test-local",
"--",
"--inspect-brk"
],
"runtimeArgs": ["run", "test:local", "--", "--inspect-brk"],
"autoAttachChildProcesses": true
}
]

File diff suppressed because it is too large Load Diff

270
CLAUDE.md Normal file
View File

@@ -0,0 +1,270 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
cnpmcore is a TypeScript-based private NPM registry implementation for enterprise use. It's built on the Egg.js framework using Domain-Driven Design (DDD) architecture principles and supports both MySQL and PostgreSQL databases.
## Essential Commands
### Development
```bash
# Start development server (MySQL)
npm run dev
# Start development server (PostgreSQL)
npm run dev:postgresql
# Lint code
npm run lint
# Fix linting issues
npm run lint:fix
# TypeScript type checking
npm run typecheck
```
### Testing
```bash
# Run all tests with MySQL (takes 4+ minutes)
npm run test
# Run all tests with PostgreSQL (takes 4+ minutes)
npm run test:postgresql
# Run single test file (faster iteration, ~12 seconds)
npm run test:local test/path/to/file.test.ts
# Generate coverage report
npm run cov
```
### Database Setup
```bash
# MySQL setup
docker compose -f docker-compose.yml up -d
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-mysql.sh
# PostgreSQL setup
docker compose -f docker-compose-postgres.yml up -d
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-postgresql.sh
```
### Build
```bash
# Clean build artifacts
npm run clean
# Development build
npm run tsc
# Production build
npm run tsc:prod
```
## Architecture - Domain-Driven Design (DDD)
The codebase follows strict DDD layering with clear separation of concerns:
```
Controller (app/port/controller/) ← HTTP interface, validation, auth
↓ depends on
Service (app/core/service/) ← Business logic orchestration
↓ depends on
Repository (app/repository/) ← Data access layer
↓ depends on
Model (app/repository/model/) ← ORM/Database mapping
Entity (app/core/entity/) ← Pure domain models (no dependencies)
Common (app/common/) ← Utilities and adapters (all layers)
```
### Layer Responsibilities
**Controller Layer** (`app/port/controller/`):
- Handle HTTP requests/responses
- Validate inputs using `@eggjs/typebox-validate`
- Authenticate users and verify authorization
- Delegate business logic to Services
- All controllers extend `AbstractController`
**Service Layer** (`app/core/service/`):
- Implement core business logic
- Orchestrate multiple repositories
- Publish domain events
- Manage transactions
**Repository Layer** (`app/repository/`):
- CRUD operations on Models
- Data access and persistence
- Query building and optimization
- Methods named: `findX`, `saveX`, `removeX`, `listXs`
**Entity Layer** (`app/core/entity/`):
- Pure domain models with business behavior
- No infrastructure dependencies
- Immutable data structures preferred
**Model Layer** (`app/repository/model/`):
- ORM definitions using Leoric
- Database schema mapping
- No business logic
### Infrastructure Adapters (`app/infra/`)
Enterprise customization layer for PaaS integration:
- **NFSClientAdapter**: File storage (local/S3/OSS)
- **QueueAdapter**: Message queue integration
- **AuthAdapter**: Authentication system
- **BinaryAdapter**: Binary package storage
## Key Development Patterns
### Request Validation Trilogy
Always validate requests in this exact order:
1. **Parameter Validation** - Use `@eggjs/typebox-validate` for type-safe validation
2. **Authentication** - Get authorized user with token role verification
3. **Authorization** - Check resource-level permissions to prevent privilege escalation
```typescript
// Example controller method
async someMethod(@HTTPQuery() params: QueryType) {
// 1. Params already validated by @HTTPQuery with typebox
// 2. Authenticate
const user = await this.userRoleManager.requiredAuthorizedUser(this.ctx, 'publish');
// 3. Authorize (if needed)
const { pkg } = await this.ensurePublishAccess(this.ctx, fullname);
// 4. Execute business logic
return await this.service.doSomething(params);
}
```
### Repository Method Naming
- `findSomething` - Query single entity
- `saveSomething` - Create or update entity
- `removeSomething` - Delete entity
- `listSomethings` - Query multiple entities (plural)
### Modifying Database Models
When changing a Model, update all 3 locations:
1. SQL migrations: `sql/mysql/*.sql` AND `sql/postgresql/*.sql`
2. ORM Model: `app/repository/model/*.ts`
3. Domain Entity: `app/core/entity/*.ts`
## Code Style
### Linting
- **Linter**: Oxlint (Rust-based, very fast)
- **Formatter**: Prettier
- **Pre-commit**: Husky + lint-staged (auto-format on commit)
Style rules:
- Single quotes (`'`)
- 2-space indentation
- 120 character line width
- ES5 trailing commas
- Max 6 function parameters
- No console statements (use logger)
### TypeScript
- Strict TypeScript enabled
- Avoid `any` types - use proper typing or `unknown`
- ES modules (`import/export`) throughout
- Comprehensive type definitions in all files
### Testing
- Test files use `.test.ts` suffix
- Tests mirror source structure in `test/` directory
- Use `@eggjs/mock` for mocking
- Use `assert` from `node:assert/strict`
- Test both success and error cases
Pattern:
```typescript
describe('test/path/to/SourceFile.test.ts', () => {
describe('[HTTP_METHOD /api/path] functionName()', () => {
it('should handle expected behavior', async () => {
// Test implementation
});
});
});
```
## Project Structure
```
app/
├── common/ # Global utilities and adapters
│ ├── adapter/ # External service adapters
│ └── enum/ # Shared enumerations
├── core/ # Business logic layer
│ ├── entity/ # Domain models
│ ├── event/ # Event handlers
│ ├── service/ # Business services
│ └── util/ # Internal utilities
├── port/ # Interface layer
│ ├── controller/ # HTTP controllers
│ ├── middleware/ # Middleware
│ └── schedule/ # Background jobs
├── repository/ # Data access layer
│ └── model/ # ORM models
└── infra/ # Infrastructure adapters
config/ # Configuration files
sql/ # Database migrations
├── mysql/ # MySQL migrations
└── postgresql/ # PostgreSQL migrations
test/ # Test files (mirrors app/ structure)
```
## Important Configuration
- `config/config.default.ts` - Main application configuration
- `config/database.ts` - Database connection settings
- `config/binaries.ts` - Binary package mirror configurations
- `.env` - Environment-specific variables (copy from `.env.example`)
- `tsconfig.json` - TypeScript settings (target: ES2021 for Leoric compatibility)
## Development Workflow
1. **Setup**: Copy `.env.example` to `.env`, start Docker services, initialize database
2. **Feature Development**: Follow bottom-up approach (Model → Entity → Repository → Service → Controller)
3. **Testing**: Write tests at appropriate layer, run individual tests for fast iteration
4. **Validation**: Run linter, typecheck, relevant tests before committing
5. **Commit**: Use semantic commit messages (feat/fix/docs/test/chore)
## Integration as NPM Package
cnpmcore can be integrated into Egg.js/Tegg applications as an NPM package, allowing enterprises to:
- Customize infrastructure adapters (storage, auth, queue)
- Override default behavior while receiving updates
- Integrate with existing enterprise systems
See INTEGRATE.md for detailed integration guide.
## Performance Notes
Typical command execution times:
- Development server startup: ~20 seconds
- TypeScript build: ~6 seconds
- Full test suite: 4-15 minutes
- Single test file: ~12 seconds
- Linting: <1 second
- Database initialization: <2 seconds
## Prerequisites
- Node.js: 20.18.0+ or 22.18.0+
- Database: MySQL 5.7+ or PostgreSQL 17+
- Cache: Redis 6+
- Optional: Elasticsearch 8.x
## Key Services & Controllers
Core components to understand:
- **PackageController**: Package CRUD operations
- **PackageManagerService**: Core package management logic
- **BinarySyncerService**: Binary package synchronization
- **ChangesStreamService**: NPM registry change stream processing
- **UserController**: User authentication and profiles

View File

@@ -70,6 +70,9 @@ curl -v http://127.0.0.1:7001
### 登录和测试发包
> cnpmcore 默认不开放注册,可以通过 `config.default.ts` 中的 `allowPublicRegistration` 配置开启,否则只有管理员可以登录
注册 cnpmcore_admin 管理员
```bash
@@ -233,7 +236,7 @@ private async getPackageEntity(scope: string, name: string) {
#### 1、请求参数校验
使用 [egg-typebox-validate](https://github.com/xiekw2010/egg-typebox-validate) 来做请求参数校验,只需要定义一次参数类型和规则,就能同时拥有参数校验和类型定义。
使用 [@eggjs/typebox-validate](https://github.com/eggjs/egg/tree/next/plugins/typebox-validate) 来做请求参数校验,只需要定义一次参数类型和规则,就能同时拥有参数校验和类型定义。
详细使用方式可以参考 [PR#12](https://github.com/cnpm/cnpmcore/pull/12)。
使用方式请直接参考 `app/port/typebox.ts` 代码。

View File

@@ -1,17 +0,0 @@
FROM node:22
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY . .
RUN npm install -g npminstall --registry=https://registry.npmmirror.com \
&& npminstall -c \
&& npm run tsc
ENV NODE_ENV=production \
EGG_SERVER_ENV=prod
EXPOSE 7001
CMD ["npm", "run", "start:foreground"]

View File

@@ -1,14 +1,14 @@
# 🥚 如何在 [tegg](https://github.com/eggjs/tegg) 中集成 cnpmcore
# 🥚 如何在 [tegg](https://github.com/eggjs/egg/blob/next/tegg) 中集成 cnpmcore
> 文档中的示例项目可以在 [这里](https://github.com/eggjs/examples/commit/bed580fe053ae573f8b63f6788002ff9c6e7a142) 查看,在开始前请确保已阅读 [DEVELOPER.md](DEVELOPER.md) 中的相关文档,完成本地开发环境搭建。
在生产环境中,我们也可以直接部署 cnpmcore 系统,实现完整的 Registry 镜像功能。
但通常,在企业内部会有一些内部的中间件服务或限制,例如文件存储、缓存服务、登录鉴权流程等。
除了源码部署、二次开发的方式,我们还提供了 npm 包的方式,便于 [tegg](https://github.com/eggjs/tegg) 应用集成。
除了源码部署、二次开发的方式,我们还提供了 npm 包的方式,便于 [tegg](https://github.com/eggjs/egg/blob/next/tegg) 应用集成。
这样既可以享受到丰富的自定义扩展能力,又可以享受到 cnpmcore 持续迭代的功能演进。
下面,让我们以 [tegg](https://github.com/eggjs/tegg) 初始化的应用为例,以 npm 包的方式集成 cnpmcore并扩展登录功能以支持企业内 [SSO](https://en.wikipedia.org/wiki/Single_sign-on) 登录。
下面,让我们以 [tegg](https://github.com/eggjs/egg/blob/next/tegg) 初始化的应用为例,以 npm 包的方式集成 cnpmcore并扩展登录功能以支持企业内 [SSO](https://en.wikipedia.org/wiki/Single_sign-on) 登录。
## 🚀 快速开始
@@ -34,51 +34,36 @@
### 📦︎ 安装 cnpmcore 修改对应配置
```shell
npm i cnpmcore -S
```
```shell
npm i cnpmcore
```
1. 修改 `ts-config.json` 配置,这是因为 cnpmcore 使用了 [subPath](https://nodejs.org/api/packages.html#subpath-exports)
```json
{
"extends": "@eggjs/tsconfig",
"compilerOptions": {
"baseUrl": "./",
"moduleResolution": "NodeNext",
"target": "ES2020",
"module": "Node16"
}
}
```
```json
{
"extends": "@eggjs/tsconfig",
"compilerOptions": {
"baseUrl": "./",
"target": "ES2021"
}
}
```
2. 修改 `config/plugin.ts` 文件,开启 cnpmcore 依赖的一些插件
```typescript
// 开启如下插件
{
redis: {
enable: true,
package: '@eggjs/redis',
},
teggOrm: {
enable: true,
package: '@eggjs/tegg-orm-plugin',
},
eventbusModule: {
enable: true,
package: '@eggjs/tegg-eventbus-plugin',
},
tracer: {
enable: true,
package: '@eggjs/tracer',
},
typeboxValidate: {
enable: true,
package: 'egg-typebox-validate',
},
}
```
```typescript
import tracerPlugin from '@eggjs/tracer';
import typeboxValidatePlugin from '@eggjs/typebox-validate';
import redisPlugin from '@eggjs/redis';
// 开启如下插件
export default {
...redisPlugin(),
...tracerPlugin(),
...typeboxValidatePlugin(),
}
```
3. 修改 `config.default.ts` 文件,可以直接覆盖默认配置
@@ -92,6 +77,8 @@ export default () => {
...cnpmcoreConfig,
enableChangesStream: false,
syncMode: SyncMode.all,
allowPublicRegistration: true,
// 放开注册配置
};
return config;
}
@@ -101,63 +88,63 @@ export default () => {
1. 创建文件夹,用于存放自定义的 infra module这里以 app/infra 为例
```shell
├── infra
│   ├── AuthAdapter.ts
│   ├── NFSAdapter.ts
│   ├── QueueAdapter.ts
│   └── package.json
```
```shell
├── infra
│   ├── AuthAdapter.ts
│   ├── NFSAdapter.ts
│   ├── QueueAdapter.ts
│   └── package.json
```
* 添加 `package.json` ,声明 infra 作为一个 eggModule 单元
```JSON
{
"name": "infra",
"eggModule": {
"name": "infra"
}
}
```
```JSON
{
"name": "infra",
"eggModule": {
"name": "infra"
}
}
```
* 添加 `XXXAdapter.ts` 在对应的 Adapter 中继承 cnpmcore 默认的 Adapter以 AuthAdapter 为例
```typescript
import { AccessLevel, SingletonProto } from '@eggjs/tegg';
import { AuthAdapter } from 'cnpmcore/infra/AuthAdapter';
```typescript
import { AccessLevel, SingletonProto } from 'egg';
import { AuthAdapter } from 'cnpmcore/infra/AuthAdapter';
@SingletonProto({
name: 'authAdapter',
accessLevel: AccessLevel.PUBLIC,
})
export class MyAuthAdapter extends AuthAdapter {
}
```
@SingletonProto({
name: 'authAdapter',
accessLevel: AccessLevel.PUBLIC,
})
export class MyAuthAdapter extends AuthAdapter {
}
```
2. 添加 `config/module.json`,将 cnpmcore 作为一个 module 集成进我们新增的 tegg 应用中
```json
[
{
"path": "../app/biz"
},
{
"path": "../app/infra"
},
{
"package": "cnpmcore/common"
},
{
"package": "cnpmcore/core"
},
{
"package": "cnpmcore/port"
},
{
"package": "cnpmcore/repository"
}
]
```
```json
[
{
"path": "../app/biz"
},
{
"path": "../app/infra"
},
{
"package": "cnpmcore/common"
},
{
"package": "cnpmcore/core"
},
{
"package": "cnpmcore/port"
},
{
"package": "cnpmcore/repository"
}
]
```
### ✍🏻 重载 AuthAdapter 实现
@@ -173,10 +160,10 @@ export default () => {
修改 AuthAdapter.ts 文件
```typescript
import { AccessLevel, EggContext, SingletonProto } from '@eggjs/tegg';
import { AccessLevel, Context, SingletonProto } from 'egg';
import { AuthAdapter } from 'cnpmcore/infra/AuthAdapter';
import { randomUUID } from 'crypto';
import { AuthUrlResult, userResult } from 'node_modules/cnpmcore/dist/app/common/typing';
import { AuthUrlResult, userResult } from 'cnpmcore/dist/app/common/typing';
const ONE_DAY = 3600 * 24;
@@ -185,7 +172,7 @@ const ONE_DAY = 3600 * 24;
accessLevel: AccessLevel.PUBLIC,
})
export class MyAuthAdapter extends AuthAdapter {
async getAuthUrl(ctx: EggContext): Promise<AuthUrlResult> {
async getAuthUrl(ctx: Context): Promise<AuthUrlResult> {
const sessionId = randomUUID();
await this.redis.setex(sessionId, ONE_DAY, '');
return {
@@ -208,33 +195,33 @@ export class MyAuthAdapter extends AuthAdapter {
修改 HelloController 的实现,实际也可以通过登录中心回调、页面确认等方式实现
```typescript
// 触发回调接口,会自动完成用户创建
await this.httpclient.request(`${ctx.origin}/-/v1/login/sso/${name}`, { method: 'POST' });
// 触发回调接口,会自动完成用户创建
await this.httpclient.request(`${ctx.origin}/-/v1/login/sso/${name}`, { method: 'POST' });
```
## 🎉 功能验证
1. 在命令行输入 `npm login --registry=http://127.0.0.1:7001`
```shell
npm login --registry=http://127.0.0.1:7001
npm notice Log in on http://127.0.0.1:7001/
Login at:
http://127.0.0.1:7001/hello?name=e44e8c43-211a-4bcd-ae78-c4cbb1a78ae7
Press ENTER to open in the browser...
```
```shell
npm login --registry=http://127.0.0.1:7001
npm notice Log in on http://127.0.0.1:7001/
Login at:
http://127.0.0.1:7001/hello?name=e44e8c43-211a-4bcd-ae78-c4cbb1a78ae7
Press ENTER to open in the browser...
```
2. 界面提示回车打开浏览器访问登录中心,也就是我们在 getAuthUrl返回的 loginUrl 配置
3. 由于我们 mock 了对应实现,界面会直接显示登录成功
```shell
Logged in on http://127.0.0.1:7001/.
```
```shell
Logged in on http://127.0.0.1:7001/.
```
4. 在命令行输入 `npm whoami --registry=http://127.0.0.1:7001` 验证
```shell
npm whoami --registry=http://127.0.0.1:7001
hello
```
```shell
npm whoami --registry=http://127.0.0.1:7001
hello
```

View File

@@ -1,9 +1,14 @@
# Private NPM Registry for Enterprise
[![Node.js CI](https://github.com/cnpm/cnpmcore/actions/workflows/nodejs.yml/badge.svg)](https://github.com/cnpm/cnpmcore/actions/workflows/nodejs.yml)
[![codecov](https://codecov.io/gh/cnpm/cnpmcore/branch/main/graph/badge.svg)](https://codecov.io/gh/cnpm/cnpmcore)
[![CodeQL](https://github.com/cnpm/cnpmcore/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/cnpm/cnpmcore/actions/workflows/codeql-analysis.yml)
[![Node.js CI](https://github.com/cnpm/cnpmcore/actions/workflows/nodejs.yml/badge.svg?branch=master)](https://github.com/cnpm/cnpmcore/actions/workflows/nodejs.yml)
[![codecov](https://codecov.io/gh/cnpm/cnpmcore/master/main/graph/badge.svg)](https://app.codecov.io/gh/cnpm/cnpmcore/tree/master)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fcnpm%2Fcnpmcore.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fcnpm%2Fcnpmcore?ref=badge_shield)
[![Node.js Version](https://img.shields.io/node/v/cnpmcore.svg?style=flat)](https://nodejs.org/en/download/)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com)
![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/cnpm/cnpmcore)
[![NPM Version](https://img.shields.io/npm/v/cnpmcore)](https://www.npmjs.com/package/cnpmcore)
[![NPM Downloads](https://img.shields.io/npm/dm/cnpmcore)](https://www.npmjs.com/package/cnpmcore)
[![NPM License](https://img.shields.io/npm/l/cnpmcore)](https://github.com/cnpm/cnpmcore/blob/master/LICENSE)
Reimplement based on [cnpmjs.org](https://github.com/cnpm/cnpmjs.org) with TypeScript.
@@ -11,6 +16,10 @@ Reimplement based on [cnpmjs.org](https://github.com/cnpm/cnpmjs.org) with TypeS
See [registry-api.md](docs/registry-api.md)
## Internal API for Direct HTTP Requests
See [internal-api.md](docs/internal-api.md) for comprehensive documentation of cnpmcore's internal APIs that allow direct HTTP requests for package synchronization, administration, and other advanced operations.
## How to contribute
See [DEVELOPER.md](DEVELOPER.md)

21
app.ts
View File

@@ -1,7 +1,8 @@
import path from 'node:path';
import { readFile } from 'node:fs/promises';
import { Application } from 'egg';
import { ChangesStreamService } from './app/core/service/ChangesStreamService';
import type { Application, ILifecycleBoot } from 'egg';
import { ChangesStreamService } from './app/core/service/ChangesStreamService.ts';
declare module 'egg' {
interface Application {
@@ -9,7 +10,7 @@ declare module 'egg' {
}
}
export default class CnpmcoreAppHook {
export default class CnpmcoreAppHook implements ILifecycleBoot {
private readonly app: Application;
constructor(app: Application) {
@@ -17,9 +18,9 @@ export default class CnpmcoreAppHook {
this.app.binaryHTML = '';
}
async configWillLoad() {
configWillLoad() {
const app = this.app;
// https://github.com/eggjs/tegg/blob/master/plugin/orm/app.ts#L37
// https://github.com/eggjs/egg/blob/next/tegg/plugin/orm/src/app.ts#L37
// store query sql to log
app.config.orm.logger = {
...app.config.orm.logger,
@@ -33,14 +34,18 @@ export default class CnpmcoreAppHook {
async didReady() {
// ready binary.html and replace registry
const filepath = path.join(this.app.baseDir, 'app/port/binary.html');
const text = await readFile(filepath, 'utf-8');
this.app.binaryHTML = text.replace('{{registry}}', this.app.config.cnpmcore.registry);
const text = await readFile(filepath, 'utf8');
this.app.binaryHTML = text.replace(
'{{registry}}',
this.app.config.cnpmcore.registry
);
}
// 应用退出时执行
// 需要暂停当前执行的 changesStream task
async beforeClose() {
const changesStreamService = await this.app.getEggObject(ChangesStreamService);
const changesStreamService =
await this.app.getEggObject(ChangesStreamService);
await changesStreamService.suspendSync(true);
}
}

View File

@@ -1,14 +1,8 @@
import {
Inject,
} from '@eggjs/tegg';
import {
EggAppConfig,
EggLogger,
} from 'egg';
import { EggAppConfig, Logger, Inject } from 'egg';
export abstract class AbstractService {
@Inject()
protected readonly config: EggAppConfig;
@Inject()
protected readonly logger: EggLogger;
protected readonly logger: Logger;
}

View File

@@ -1,37 +0,0 @@
export type ValueType = 'string' | 'boolean' | 'number';
export function env(key: string, valueType: ValueType, defaultValue: string): string;
export function env(key: string, valueType: ValueType, defaultValue: boolean): boolean;
export function env(key: string, valueType: ValueType, defaultValue: number): number;
export function env(key: string, valueType: ValueType, defaultValue: string | boolean | number): string | boolean | number {
const value = process.env[key];
if (value === undefined) {
return defaultValue;
}
if (valueType === 'string') {
return value;
}
if (valueType === 'boolean') {
let booleanValue = false;
if (value === 'true' || value === '1') {
booleanValue = true;
} else if (value === 'false' || value === '0') {
booleanValue = false;
} else {
throw new TypeError(`Invalid boolean value: ${value} on process.env.${key}`);
}
return booleanValue;
}
if (valueType === 'number') {
const numberValue = Number(value);
if (isNaN(numberValue)) {
throw new TypeError(`Invalid number value: ${value} on process.env.${key}`);
}
return numberValue;
}
throw new TypeError(`Invalid value type: ${valueType}`);
}

View File

@@ -1,27 +1,29 @@
const TimeoutErrorNames = [
const TimeoutErrorNames = new Set([
'HttpClientRequestTimeoutError',
'HttpClientConnectTimeoutError',
'ConnectionError',
'ConnectTimeoutError',
'BodyTimeoutError',
'ResponseTimeoutError',
];
]);
export function isTimeoutError(err: Error) {
if (TimeoutErrorNames.includes(err.name)) {
if (TimeoutErrorNames.has(err.name)) {
return true;
}
if (err instanceof AggregateError && err.errors) {
for (const subError of err.errors) {
if (TimeoutErrorNames.includes(subError.name)) {
if (TimeoutErrorNames.has(subError.name)) {
return true;
}
}
}
if ('cause' in err && err.cause instanceof Error) {
if (TimeoutErrorNames.includes(err.cause.name)) {
return true;
}
if (
'cause' in err &&
err.cause instanceof Error &&
TimeoutErrorNames.has(err.cause.name)
) {
return true;
}
return false;
}

View File

@@ -1,64 +1,20 @@
// oxlint-disable import/exports-last
import { mkdir, rm } from 'node:fs/promises';
import { createWriteStream } from 'node:fs';
import { setTimeout } from 'node:timers/promises';
import path from 'node:path';
import url from 'node:url';
import { randomBytes } from 'node:crypto';
import { EggContextHttpClient, HttpClientResponse } from 'egg';
import type { EggContextHttpClient, HttpClientResponse } from 'egg';
import mime from 'mime-types';
import dayjs from './dayjs';
import dayjs from './dayjs.ts';
interface DownloadToTempfileOptionalConfig {
retries?: number,
ignoreDownloadStatuses?: number[],
remoteAuthToken?: string
}
export async function createTempDir(dataDir: string, dirname?: string) {
// will auto clean on CleanTempDir Schedule
let tmpdir = path.join(dataDir, 'downloads', dayjs().format('YYYY/MM/DD'));
if (dirname) {
tmpdir = path.join(tmpdir, dirname);
}
await mkdir(tmpdir, { recursive: true });
return tmpdir;
}
export async function createTempfile(dataDir: string, filename: string) {
const tmpdir = await createTempDir(dataDir);
// The filename is a URL (from dist.tarball), which needs to be truncated, (`getconf NAME_MAX /` # max filename length: 255 bytes)
// https://github.com/cnpm/cnpmjs.org/pull/1345
const tmpfile = path.join(tmpdir, `${randomBytes(10).toString('hex')}-${path.basename(url.parse(filename).pathname!)}`);
return tmpfile;
}
export async function downloadToTempfile(httpclient: EggContextHttpClient,
dataDir: string, url: string, optionalConfig?: DownloadToTempfileOptionalConfig) {
let retries = optionalConfig?.retries || 3;
let lastError: any;
while (retries > 0) {
try {
return await _downloadToTempfile(httpclient, dataDir, url, optionalConfig);
} catch (err: any) {
if (err.name === 'DownloadNotFoundError') throw err;
lastError = err;
}
retries--;
if (retries > 0) {
// sleep 1s ~ 4s in random
const delay = process.env.NODE_ENV === 'test' ? 1 : 1000 + Math.random() * 4000;
await setTimeout(delay);
}
}
throw lastError;
}
export interface Tempfile {
tmpfile: string;
headers: HttpClientResponse['res']['headers'];
timing: HttpClientResponse['res']['timing'];
}
async function _downloadToTempfile(httpclient: EggContextHttpClient,
dataDir: string, url: string, optionalConfig?: DownloadToTempfileOptionalConfig): Promise<Tempfile> {
async function _downloadToTempfile(
httpclient: EggContextHttpClient,
dataDir: string,
url: string,
optionalConfig?: DownloadToTempfileOptionalConfig
): Promise<Tempfile> {
const tmpfile = await createTempfile(dataDir, url);
const writeStream = createWriteStream(tmpfile);
try {
@@ -68,14 +24,18 @@ async function _downloadToTempfile(httpclient: EggContextHttpClient,
if (optionalConfig?.remoteAuthToken) {
requestHeaders.authorization = `Bearer ${optionalConfig.remoteAuthToken}`;
}
const { status, headers, res } = await httpclient.request(url, {
timeout: 60000 * 10,
const { status, headers, res } = (await httpclient.request(url, {
timeout: 60_000 * 10,
headers: requestHeaders,
writeStream,
timing: true,
followRedirect: true,
}) as HttpClientResponse;
if (status === 404 || (optionalConfig?.ignoreDownloadStatuses && optionalConfig.ignoreDownloadStatuses.includes(status))) {
})) as HttpClientResponse;
if (
status === 404 ||
(optionalConfig?.ignoreDownloadStatuses &&
optionalConfig.ignoreDownloadStatuses.includes(status))
) {
const err = new Error(`Not found, status(${status})`);
err.name = 'DownloadNotFoundError';
throw err;
@@ -96,6 +56,71 @@ async function _downloadToTempfile(httpclient: EggContextHttpClient,
}
}
export interface DownloadToTempfileOptionalConfig {
retries?: number;
ignoreDownloadStatuses?: number[];
remoteAuthToken?: string;
}
export async function createTempDir(dataDir: string, dirname?: string) {
// will auto clean on CleanTempDir Schedule
let tmpdir = path.join(dataDir, 'downloads', dayjs().format('YYYY/MM/DD'));
if (dirname) {
tmpdir = path.join(tmpdir, dirname);
}
await mkdir(tmpdir, { recursive: true });
return tmpdir;
}
export async function createTempfile(dataDir: string, filename: string) {
const tmpdir = await createTempDir(dataDir);
// The filename is a URL (from dist.tarball), which needs to be truncated, (`getconf NAME_MAX /` # max filename length: 255 bytes)
// https://github.com/cnpm/cnpmjs.org/pull/1345
const tmpfile = path.join(
tmpdir,
// oxlint-disable-next-line typescript-eslint/no-non-null-assertion
`${randomBytes(10).toString('hex')}-${path.basename(url.parse(filename).pathname!)}`
);
return tmpfile;
}
export async function downloadToTempfile(
httpclient: EggContextHttpClient,
dataDir: string,
url: string,
optionalConfig?: DownloadToTempfileOptionalConfig
) {
let retries = optionalConfig?.retries || 3;
let lastError: Error | undefined;
while (retries > 0) {
try {
return await _downloadToTempfile(
httpclient,
dataDir,
url,
optionalConfig
);
} catch (err) {
if (err.name === 'DownloadNotFoundError') throw err;
lastError = err;
}
retries--;
if (retries > 0) {
// sleep 1s ~ 4s in random
const delay =
process.env.NODE_ENV === 'test' ? 1 : 1000 + Math.random() * 4000;
await setTimeout(delay);
}
}
// oxlint-disable-next-line no-throw-literal
throw lastError;
}
export interface Tempfile {
tmpfile: string;
headers: HttpClientResponse['res']['headers'];
timing: HttpClientResponse['res']['timing'];
}
const DEFAULT_CONTENT_TYPE = 'application/octet-stream';
const PLAIN_TEXT = 'text/plain';
const WHITE_FILENAME_CONTENT_TYPES = {
@@ -110,11 +135,27 @@ const WHITE_FILENAME_CONTENT_TYPES = {
'.eslintrc': 'application/json',
} as const;
const CONTENT_TYPE_BLACKLIST = new Set(['application/xml', 'text/html']);
export function ensureContentType(contentType: string) {
if (CONTENT_TYPE_BLACKLIST.has(contentType)) {
return 'text/plain';
}
return contentType;
}
export function mimeLookup(filepath: string) {
const filename = path.basename(filepath).toLowerCase();
if (filename.endsWith('.ts')) return PLAIN_TEXT;
if (filename.endsWith('.lock')) return PLAIN_TEXT;
return mime.lookup(filename) ||
WHITE_FILENAME_CONTENT_TYPES[filename as keyof typeof WHITE_FILENAME_CONTENT_TYPES] ||
const defaultContentType = mime.lookup(filename);
// https://github.com/cnpm/cnpmcore/issues/693#issuecomment-2955268229
const contentType =
defaultContentType ||
WHITE_FILENAME_CONTENT_TYPES[
filename as keyof typeof WHITE_FILENAME_CONTENT_TYPES
] ||
DEFAULT_CONTENT_TYPE;
return ensureContentType(contentType);
}

View File

@@ -1,10 +1,15 @@
import { createReadStream } from 'node:fs';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import * as ssri from 'ssri';
import tar from '@fengmk2/tar';
import { AuthorType, PackageJSONType } from '../repository/PackageRepository';
import { fromData, fromStream, type HashLike } from 'ssri';
// @ts-expect-error no types available
import tar from '@fengmk2/tar';
import type {
AuthorType,
PackageJSONType,
} from '../repository/PackageRepository.ts';
// /@cnpm%2ffoo
// /@cnpm%2Ffoo
@@ -12,13 +17,14 @@ import { AuthorType, PackageJSONType } from '../repository/PackageRepository';
// /foo
// name max length is 214 chars
// https://www.npmjs.com/package/path-to-regexp#custom-matching-parameters
export const FULLNAME_REG_STRING = '@[^/]{1,220}\/[^/]{1,220}|@[^%]+\%2[fF][^/]{1,220}|[^@/]{1,220}';
export const FULLNAME_REG_STRING =
'@[^/]{1,220}/[^/]{1,220}|@[^%]+%2[fF][^/]{1,220}|[^@/]{1,220}';
export function getScopeAndName(fullname: string): string[] {
if (fullname.startsWith('@')) {
return fullname.split('/', 2);
}
return [ '', fullname ];
return ['', fullname];
}
export function getFullname(scope: string, name: string): string {
@@ -33,15 +39,22 @@ export function getPrefixedName(prefix: string, username: string): string {
return prefix ? `${prefix}${username}` : username;
}
export async function calculateIntegrity(contentOrFile: Uint8Array | string) {
let integrityObj;
export interface Integrity {
integrity: string;
shasum: string;
}
export async function calculateIntegrity(
contentOrFile: Uint8Array | string
): Promise<Integrity> {
let integrityObj: HashLike;
if (typeof contentOrFile === 'string') {
integrityObj = await ssri.fromStream(createReadStream(contentOrFile), {
algorithms: [ 'sha512', 'sha1' ],
integrityObj = await fromStream(createReadStream(contentOrFile), {
algorithms: ['sha512', 'sha1'],
});
} else {
integrityObj = ssri.fromData(contentOrFile, {
algorithms: [ 'sha512', 'sha1' ],
integrityObj = fromData(contentOrFile, {
algorithms: ['sha512', 'sha1'],
});
}
const integrity = integrityObj.sha512[0].toString() as string;
@@ -49,26 +62,33 @@ export async function calculateIntegrity(contentOrFile: Uint8Array | string) {
return { integrity, shasum };
}
export function formatTarball(registry: string, scope: string, name: string, version: string) {
export function formatTarball(
registry: string,
scope: string,
name: string,
version: string
) {
const fullname = getFullname(scope, name);
return `${registry}/${fullname}/-/${name}-${version}.tgz`;
}
export function detectInstallScript(manifest: any) {
export function detectInstallScript(manifest: {
scripts?: Record<string, string>;
}) {
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
let hasInstallScript = false;
const scripts = manifest.scripts;
if (scripts) {
// https://www.npmjs.com/package/fix-has-install-script
if (scripts.install || scripts.preinstall || scripts.postinstall) {
hasInstallScript = true;
}
// https://www.npmjs.com/package/fix-has-install-script
if (scripts?.install || scripts?.preinstall || scripts?.postinstall) {
hasInstallScript = true;
}
return hasInstallScript;
}
/** 判断一个版本压缩包中是否包含 npm-shrinkwrap.json */
export async function hasShrinkWrapInTgz(contentOrFile: Uint8Array | string): Promise<boolean> {
export async function hasShrinkWrapInTgz(
contentOrFile: Uint8Array | string
): Promise<boolean> {
let readable: Readable;
if (typeof contentOrFile === 'string') {
readable = createReadStream(contentOrFile);
@@ -86,7 +106,8 @@ export async function hasShrinkWrapInTgz(contentOrFile: Uint8Array | string): Pr
const parser = tar.t({
// options.strict 默认为 false会忽略 Recoverable errors例如 tar 解析失败
// 详见 https://github.com/isaacs/node-tar#warnings-and-errors
onentry(entry) {
// oxlint-disable-next-line typescript-eslint/no-explicit-any
onentry(entry: any) {
if (entry.path === 'package/npm-shrinkwrap.json') {
hasShrinkWrap = true;
abortController.abort();
@@ -101,12 +122,17 @@ export async function hasShrinkWrapInTgz(contentOrFile: Uint8Array | string): Pr
if (e.code === 'ABORT_ERR') {
return hasShrinkWrap;
}
throw Object.assign(new Error('[hasShrinkWrapInTgz] Fail to parse input file'), { cause: e });
throw Object.assign(
new Error('[hasShrinkWrapInTgz] Fail to parse input file'),
{ cause: e }
);
}
}
/** 写入 ES 时,格式化 author */
export function formatAuthor(author: string | AuthorType | undefined): AuthorType | undefined {
export function formatAuthor(
author: string | AuthorType | undefined
): AuthorType | undefined {
if (author === undefined) {
return author;
}
@@ -118,12 +144,15 @@ export function formatAuthor(author: string | AuthorType | undefined): AuthorTyp
return author;
}
export async function extractPackageJSON(tarballBytes: Buffer): Promise<PackageJSONType> {
export async function extractPackageJSON(
tarballBytes: Buffer
): Promise<PackageJSONType> {
// oxlint-disable-next-line promise/avoid-new
return new Promise((resolve, reject) => {
Readable.from(tarballBytes)
.pipe(tar.t({
filter: name => name === 'package/package.json',
onentry: async entry => {
Readable.from(tarballBytes).pipe(
tar.t({
filter: (name: string) => name === 'package/package.json',
onentry: async (entry: Readable) => {
const chunks: Buffer[] = [];
for await (const chunk of entry) {
chunks.push(chunk);
@@ -131,10 +160,11 @@ export async function extractPackageJSON(tarballBytes: Buffer): Promise<PackageJ
try {
const data = Buffer.concat(chunks);
return resolve(JSON.parse(data.toString()));
} catch (err) {
} catch {
reject(new Error('Error parsing package.json'));
}
},
}));
})
);
});
}

View File

@@ -1,6 +1,6 @@
import { EggContext } from '@eggjs/tegg';
import type { Context } from 'egg';
export function isSyncWorkerRequest(ctx: EggContext) {
export function isSyncWorkerRequest(ctx: Context) {
// sync request will contain this query params
let isSyncWorkerRequest = ctx.query.cache === '0';
if (!isSyncWorkerRequest) {

View File

@@ -1,7 +1,7 @@
import crypto from 'node:crypto';
import base from 'base-x';
import { crc32 } from '@node-rs/crc32';
import * as ssri from 'ssri';
import { checkData, create } from 'ssri';
import UAParser from 'ua-parser-js';
const base62 = base('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
@@ -29,12 +29,12 @@ export function checkToken(token: string, prefix: string): boolean {
}
export function integrity(plain: string): string {
return ssri.create().update(plain).digest()
return create().update(plain).digest()
.toString();
}
export function checkIntegrity(plain: string, expectedIntegrity: string): boolean {
return !!ssri.checkData(plain, expectedIntegrity);
return !!checkData(plain, expectedIntegrity);
}
export function sha512(plain: string): string {

View File

@@ -1,5 +1,6 @@
import { AccessLevel, SingletonProto } from '@eggjs/tegg';
import { BugVersion } from '../../core/entity/BugVersion';
import { AccessLevel, SingletonProto } from 'egg';
import type { BugVersion } from '../../core/entity/BugVersion.ts';
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,

View File

@@ -1,8 +1,4 @@
import {
SingletonProto,
AccessLevel,
Inject,
} from '@eggjs/tegg';
import { AccessLevel, Inject, SingletonProto } from 'egg';
// FIXME: @eggjs/redis should use ioredis v5
// https://github.com/eggjs/redis/issues/35
import type { Redis } from 'ioredis';
@@ -40,7 +36,7 @@ export class CacheAdapter {
const lockName = this.getLockName(key);
const existsTimestamp = await this.redis.get(lockName);
if (existsTimestamp) {
if (Date.now() - parseInt(existsTimestamp) < seconds * 1000) {
if (Date.now() - Number.parseInt(existsTimestamp) < seconds * 1000) {
return null;
}
// lock timeout, delete it

View File

@@ -1,14 +1,11 @@
import { Readable } from 'node:stream';
import { IncomingHttpHeaders } from 'node:http';
import {
SingletonProto,
AccessLevel,
Inject,
} from '@eggjs/tegg';
import { Pointcut } from '@eggjs/tegg/aop';
import { EggLogger } from 'egg';
import { AsyncTimer } from '../aop/AsyncTimer';
import { NFSClient } from '../typing';
import type { Readable } from 'node:stream';
import type { IncomingHttpHeaders } from 'node:http';
import { AccessLevel, Inject, SingletonProto, Logger } from 'egg';
import { Pointcut } from 'egg/aop';
import { AsyncTimer } from '../aop/AsyncTimer.ts';
import type { NFSClient } from '../typing.ts';
const INSTANCE_NAME = 'nfsAdapter';
@@ -21,17 +18,27 @@ export class NFSAdapter {
private readonly nfsClient: NFSClient;
@Inject()
private readonly logger: EggLogger;
private readonly logger: Logger;
@Pointcut(AsyncTimer)
async uploadBytes(storeKey: string, bytes: Uint8Array) {
this.logger.info('[%s:uploadBytes] key: %s, bytes: %d', INSTANCE_NAME, storeKey, bytes.length);
this.logger.info(
'[%s:uploadBytes] key: %s, bytes: %d',
INSTANCE_NAME,
storeKey,
bytes.length
);
await this.nfsClient.uploadBytes(bytes, { key: storeKey });
}
// will return next store position
@Pointcut(AsyncTimer)
async appendBytes(storeKey: string, bytes: Uint8Array, position?: string, headers?: IncomingHttpHeaders) {
async appendBytes(
storeKey: string,
bytes: Uint8Array,
position?: string,
headers?: IncomingHttpHeaders
) {
// make sure position is undefined by the first time
if (!position) position = undefined;
const options = {
@@ -45,14 +52,24 @@ export class NFSAdapter {
@Pointcut(AsyncTimer)
async uploadFile(storeKey: string, file: string) {
this.logger.info('[%s:uploadFile] key: %s, file: %s', INSTANCE_NAME, storeKey, file);
this.logger.info(
'[%s:uploadFile] key: %s, file: %s',
INSTANCE_NAME,
storeKey,
file
);
await this.nfsClient.upload(file, { key: storeKey });
}
@Pointcut(AsyncTimer)
async downloadFile(storeKey: string, file: string, timeout: number) {
this.logger.info('[%s:downloadFile] key: %s, file: %s, timeout: %s',
INSTANCE_NAME, storeKey, file, timeout);
this.logger.info(
'[%s:downloadFile] key: %s, file: %s, timeout: %s',
INSTANCE_NAME,
storeKey,
file,
timeout
);
await this.nfsClient.download(storeKey, file, { timeout });
}
@@ -79,7 +96,9 @@ export class NFSAdapter {
}
}
async getDownloadUrlOrStream(storeKey: string): Promise<string | Readable | undefined> {
async getDownloadUrlOrStream(
storeKey: string
): Promise<string | Readable | undefined> {
const downloadUrl = await this.getDownloadUrl(storeKey);
if (downloadUrl) {
return downloadUrl;

View File

@@ -1,18 +1,17 @@
import { setTimeout } from 'node:timers/promises';
import {
ContextProto,
AccessLevel,
Inject,
} from '@eggjs/tegg';
import {
EggLogger,
EggContextHttpClient,
AccessLevel, Inject,
EggAppConfig,
HttpClient,
Logger,
HttpClientRequestOptions,
HttpClientResponse,
} from 'egg';
import { PackageManifestType } from '../../repository/PackageRepository';
import { isTimeoutError } from '../ErrorUtil';
import type { PackageManifestType } from '../../repository/PackageRepository.ts';
import { isTimeoutError } from '../ErrorUtil.ts';
type HttpMethod = HttpClientRequestOptions['method'];
@@ -26,12 +25,12 @@ export type RegistryResponse = { method: HttpMethod } & HttpClientResponse;
})
export class NPMRegistry {
@Inject()
private readonly logger: EggLogger;
private readonly logger: Logger;
@Inject()
private readonly httpclient: EggContextHttpClient;
private readonly httpClient: HttpClient;
@Inject()
private config: EggAppConfig;
private timeout = 10000;
private timeout = 10_000;
public registryHost: string;
get registry(): string {
@@ -42,22 +41,27 @@ export class NPMRegistry {
this.registryHost = registryHost;
}
public async getFullManifests(fullname: string, optionalConfig?: { retries?: number, remoteAuthToken?: string }): Promise<{ method: HttpMethod } & HttpClientResponse<PackageManifestType>> {
public async getFullManifests(
fullname: string,
optionalConfig?: { retries?: number; remoteAuthToken?: string }
): Promise<{ method: HttpMethod } & HttpClientResponse<PackageManifestType>> {
let retries = optionalConfig?.retries || 3;
// set query t=timestamp, make sure CDN cache disable
// cache=0 is sync worker request flag
const url = `${this.registry}/${encodeURIComponent(fullname)}?t=${Date.now()}&cache=0`;
let lastError: any;
let lastError: Error | undefined;
while (retries > 0) {
try {
// large package: https://r.cnpmjs.org/%40procore%2Fcore-icons
// https://r.cnpmjs.org/intraactive-sdk-ui 44s
const authorization = this.genAuthorizationHeader(optionalConfig?.remoteAuthToken);
const authorization = this.genAuthorizationHeader(
optionalConfig?.remoteAuthToken
);
return await this.request('GET', url, undefined, {
timeout: 120000,
timeout: 120_000,
headers: { authorization },
});
} catch (err: any) {
} catch (err) {
if (isTimeoutError(err)) {
throw err;
}
@@ -66,16 +70,23 @@ export class NPMRegistry {
retries--;
if (retries > 0) {
// sleep 1s ~ 4s in random
const delay = process.env.NODE_ENV === 'test' ? 1 : 1000 + Math.random() * 4000;
const delay =
process.env.NODE_ENV === 'test' ? 1 : 1000 + Math.random() * 4000;
await setTimeout(delay);
}
}
// oxlint-disable-next-line no-throw-literal
throw lastError;
}
// app.put('/:name/sync', sync.sync);
public async createSyncTask(fullname: string, optionalConfig?: { remoteAuthToken?:string}): Promise<RegistryResponse> {
const authorization = this.genAuthorizationHeader(optionalConfig?.remoteAuthToken);
public async createSyncTask(
fullname: string,
optionalConfig?: { remoteAuthToken?: string }
): Promise<RegistryResponse> {
const authorization = this.genAuthorizationHeader(
optionalConfig?.remoteAuthToken
);
const url = `${this.registry}/${encodeURIComponent(fullname)}/sync?sync_upstream=true&nodeps=true`;
// {
// ok: true,
@@ -85,21 +96,41 @@ export class NPMRegistry {
}
// app.get('/:name/sync/log/:id', sync.getSyncLog);
public async getSyncTask(fullname: string, id: string, offset: number, optionalConfig?:{ remoteAuthToken?:string }): Promise<RegistryResponse> {
const authorization = this.genAuthorizationHeader(optionalConfig?.remoteAuthToken);
public async getSyncTask(
fullname: string,
id: string,
offset: number,
optionalConfig?: { remoteAuthToken?: string }
): Promise<RegistryResponse> {
const authorization = this.genAuthorizationHeader(
optionalConfig?.remoteAuthToken
);
const url = `${this.registry}/${encodeURIComponent(fullname)}/sync/log/${id}?offset=${offset}`;
// { ok: true, syncDone: syncDone, log: log }
return await this.request('GET', url, undefined, { authorization });
}
public async getDownloadRanges(registry: string, fullname: string, start: string, end: string, optionalConfig?:{ remoteAuthToken?:string }): Promise<RegistryResponse> {
const authorization = this.genAuthorizationHeader(optionalConfig?.remoteAuthToken);
public async getDownloadRanges(
registry: string,
fullname: string,
start: string,
end: string,
optionalConfig?: { remoteAuthToken?: string }
): Promise<RegistryResponse> {
const authorization = this.genAuthorizationHeader(
optionalConfig?.remoteAuthToken
);
const url = `${registry}/downloads/range/${start}:${end}/${encodeURIComponent(fullname)}`;
return await this.request('GET', url, undefined, { authorization });
}
private async request(method: HttpMethod, url: string, params?: object, options?: object): Promise<RegistryResponse> {
const res = await this.httpclient.request(url, {
private async request(
method: HttpMethod,
url: string,
params?: object,
options?: object
): Promise<RegistryResponse> {
const res = (await this.httpClient.request(url, {
method,
data: params,
dataType: 'json',
@@ -109,15 +140,20 @@ export class NPMRegistry {
followRedirect: true,
gzip: true,
...options,
}) as HttpClientResponse;
this.logger.info('[NPMRegistry:request] %s %s, status: %s', method, url, res.status);
})) as HttpClientResponse;
this.logger.info(
'[NPMRegistry:request] %s %s, status: %s',
method,
url,
res.status
);
return {
method,
...res,
};
}
public genAuthorizationHeader(remoteAuthToken?:string) {
public genAuthorizationHeader(remoteAuthToken?: string) {
return remoteAuthToken ? `Bearer ${remoteAuthToken}` : '';
}
}

View File

@@ -1,35 +1,48 @@
import { ImplDecorator, Inject, QualifierImplDecoratorUtil } from '@eggjs/tegg';
import { BinaryType } from '../../enum/Binary';
import { EggHttpClient, EggLogger } from 'egg';
import { BinaryName, BinaryTaskConfig } from '../../../../config/binaries';
import {
Inject,
QualifierImplDecoratorUtil,
type ImplDecorator,
HttpClient,
Logger,
} from 'egg';
export type BinaryItem = {
import type { BinaryType } from '../../enum/Binary.ts';
import type {
BinaryName,
BinaryTaskConfig,
} from '../../../../config/binaries.ts';
const platforms = ['darwin', 'linux', 'win32'] as const;
export interface BinaryItem {
name: string;
isDir: boolean;
url: string;
size: string | number;
date: string;
ignoreDownloadStatuses?: number[];
};
}
export type FetchResult = {
export interface FetchResult {
items: BinaryItem[];
// oxlint-disable-next-line typescript-eslint/no-explicit-any
nextParams?: any;
};
const platforms = [ 'darwin', 'linux', 'win32' ] as const;
}
export const BINARY_ADAPTER_ATTRIBUTE = Symbol('BINARY_ADAPTER_ATTRIBUTE');
export abstract class AbstractBinary {
@Inject()
protected logger: EggLogger;
protected logger: Logger;
@Inject()
protected httpclient: EggHttpClient;
protected httpclient: HttpClient;
abstract initFetch(binaryName: BinaryName): Promise<void>;
abstract fetch(dir: string, binaryName: BinaryName): Promise<FetchResult | undefined>;
abstract fetch(
dir: string,
binaryName: BinaryName,
lastData?: Record<string, unknown>
): Promise<FetchResult | undefined>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async finishFetch(_success: boolean, _binaryName: BinaryName): Promise<void> {
@@ -38,40 +51,57 @@ export abstract class AbstractBinary {
protected async requestXml(url: string) {
const { status, data, headers } = await this.httpclient.request(url, {
timeout: 30000,
timeout: 30_000,
followRedirect: true,
gzip: true,
});
const xml = data.toString() as string;
if (status !== 200) {
this.logger.warn('[AbstractBinary.requestXml:non-200-status] url: %s, status: %s, headers: %j, xml: %j', url, status, headers, xml);
this.logger.warn(
'[AbstractBinary.requestXml:non-200-status] url: %s, status: %s, headers: %j, xml: %j',
url,
status,
headers,
xml
);
return '';
}
return xml;
}
protected async requestJSON(url: string, requestHeaders?: Record<string, string>) {
// oxlint-disable-next-line typescript-eslint/no-explicit-any
protected async requestJSON<T = any>(
url: string,
requestHeaders?: Record<string, string>
): Promise<T> {
const { status, data, headers } = await this.httpclient.request(url, {
timeout: 30000,
timeout: 30_000,
dataType: 'json',
followRedirect: true,
gzip: true,
headers: requestHeaders,
});
if (status !== 200) {
this.logger.warn('[AbstractBinary.requestJSON:non-200-status] url: %s, status: %s, headers: %j', url, status, headers);
return data;
this.logger.warn(
'[AbstractBinary.requestJSON:non-200-status] url: %s, status: %s, headers: %j',
url,
status,
headers
);
return data as T;
}
return data;
return data as T;
}
// https://nodejs.org/api/n-api.html#n_api_node_api_version_matrix
protected async listNodeABIVersions() {
const nodeABIVersions: number[] = [];
const versions = await this.requestJSON('https://nodejs.org/dist/index.json');
const versions = await this.requestJSON(
'https://nodejs.org/dist/index.json'
);
for (const version of versions) {
if (!version.modules) continue;
const modulesVersion = parseInt(version.modules);
const modulesVersion = Number.parseInt(version.modules);
// node v6.0.0 modules 48 min
if (modulesVersion >= 48 && !nodeABIVersions.includes(modulesVersion)) {
nodeABIVersions.push(modulesVersion);
@@ -89,21 +119,24 @@ export abstract class AbstractBinary {
if (binaryConfig?.options?.nodeArchs) return binaryConfig.options.nodeArchs;
// https://nodejs.org/api/os.html#osarch
return {
linux: [ 'arm', 'arm64', 's390x', 'ia32', 'x64' ],
darwin: [ 'arm64', 'ia32', 'x64' ],
win32: [ 'ia32', 'x64' ],
linux: ['arm', 'arm64', 's390x', 'ia32', 'x64'],
darwin: ['arm64', 'ia32', 'x64'],
win32: ['ia32', 'x64'],
};
}
protected listNodeLibcs(): Record<typeof platforms[number], string[]> {
protected listNodeLibcs(): Record<(typeof platforms)[number], string[]> {
// https://github.com/lovell/detect-libc/blob/master/lib/detect-libc.js#L42
return {
darwin: [ 'unknown' ],
linux: [ 'glibc', 'musl' ],
win32: [ 'unknown' ],
darwin: ['unknown'],
linux: ['glibc', 'musl'],
win32: ['unknown'],
};
}
}
export const BinaryAdapter: ImplDecorator<AbstractBinary, typeof BinaryType> =
QualifierImplDecoratorUtil.generatorDecorator(AbstractBinary, BINARY_ADAPTER_ATTRIBUTE);
QualifierImplDecoratorUtil.generatorDecorator(
AbstractBinary,
BINARY_ADAPTER_ATTRIBUTE
);

View File

@@ -1,7 +1,12 @@
import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './AbstractBinary';
import { Inject, SingletonProto } from '@eggjs/tegg';
import { BinaryType } from '../../enum/Binary';
import { EggAppConfig } from 'egg';
import { Inject, SingletonProto, EggAppConfig } from 'egg';
import { BinaryType } from '../../enum/Binary.ts';
import {
AbstractBinary,
BinaryAdapter,
type BinaryItem,
type FetchResult,
} from './AbstractBinary.ts';
@SingletonProto()
@BinaryAdapter(BinaryType.Api)
@@ -14,12 +19,25 @@ export class ApiBinary extends AbstractBinary {
return;
}
async fetch(dir: string, binaryName: string): Promise<FetchResult | undefined> {
const apiUrl = this.config.cnpmcore.syncBinaryFromAPISource || `${this.config.cnpmcore.sourceRegistry}/-/binary`;
const url = `${apiUrl}/${binaryName}${dir}`;
async fetch(
dir: string,
binaryName: string,
lastData?: Record<string, unknown>
): Promise<FetchResult | undefined> {
const apiUrl =
this.config.cnpmcore.syncBinaryFromAPISource ||
`${this.config.cnpmcore.sourceRegistry}/-/binary`;
let url = `${apiUrl}/${binaryName}${dir}`;
if (lastData && lastData.lastSyncTime) {
url += `?since=${lastData.lastSyncTime}&limit=100`;
}
const data = await this.requestJSON(url);
if (!Array.isArray(data)) {
this.logger.warn('[ApiBinary.fetch:response-data-not-array] data: %j', data);
this.logger.warn(
'[ApiBinary.fetch:response-data-not-array] data: %j',
data
);
return;
}
const items: BinaryItem[] = [];
@@ -28,6 +46,7 @@ export class ApiBinary extends AbstractBinary {
name: item.name,
isDir: item.type === 'dir',
url: item.url,
// oxlint-disable-next-line unicorn/explicit-length-check
size: item.size || '-',
date: item.date,
});

View File

@@ -1,8 +1,18 @@
import { SingletonProto } from '@eggjs/tegg';
import { BinaryType } from '../../enum/Binary';
import binaries, { BinaryName, BinaryTaskConfig } from '../../../../config/binaries';
import path from 'node:path';
import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './AbstractBinary';
import { SingletonProto } from 'egg';
import binaries, {
type BinaryName,
type BinaryTaskConfig,
} from '../../../../config/binaries.ts';
import { BinaryType } from '../../enum/Binary.ts';
import {
AbstractBinary,
BinaryAdapter,
type BinaryItem,
type FetchResult,
} from './AbstractBinary.ts';
@SingletonProto()
@BinaryAdapter(BinaryType.Bucket)
@@ -12,22 +22,30 @@ export class BucketBinary extends AbstractBinary {
return;
}
async fetch(dir: string, binaryName: BinaryName): Promise<FetchResult | undefined> {
async fetch(
dir: string,
binaryName: BinaryName
): Promise<FetchResult | undefined> {
// /foo/ => foo/
const binaryConfig = binaries[binaryName];
const subDir = dir.substring(1);
const subDir = dir.slice(1);
const url = `${binaryConfig.distUrl}?delimiter=/&prefix=${encodeURIComponent(subDir)}`;
const xml = await this.requestXml(url);
return { items: this.parseItems(xml, dir, binaryConfig), nextParams: null };
}
protected parseItems(xml: string, dir: string, binaryConfig: BinaryTaskConfig): BinaryItem[] {
protected parseItems(
xml: string,
dir: string,
binaryConfig: BinaryTaskConfig
): BinaryItem[] {
const items: BinaryItem[] = [];
// https://nwjs2.s3.amazonaws.com/?prefix=v0.59.0%2Fx64%2F
// https://chromedriver.storage.googleapis.com/?delimiter=/&prefix=
// <Contents><Key>2.0/chromedriver_linux32.zip</Key><Generation>1380149859530000</Generation><MetaGeneration>2</MetaGeneration><LastModified>2013-09-25T22:57:39.349Z</LastModified><ETag>"c0d96102715c4916b872f91f5bf9b12c"</ETag><Size>7262134</Size><Owner/></Contents><Contents>
// <Contents><Key>v0.59.0/nwjs-v0.59.0-linux-ia32.tar.gz</Key><LastModified>2015-11-02T02:34:18.000Z</LastModified><ETag>&quot;b1b7a52928e9f874bad0cabf7f74ba8e&quot;</ETag><Size>22842</Size><StorageClass>STANDARD</StorageClass></Contents>
const fileRe = /<Contents><Key>([^<]+?)<\/Key>(?:<Generation>\d+?<\/Generation>)?(?:<MetaGeneration>\d+?<\/MetaGeneration>)?<LastModified>([^<]+?)<\/LastModified><ETag>[^<]+?<\/ETag><Size>(\d+?)<\/Size>/g;
const fileRe =
/<Contents><Key>([^<]+?)<\/Key>(?:<Generation>\d+?<\/Generation>)?(?:<MetaGeneration>\d+?<\/MetaGeneration>)?<LastModified>([^<]+?)<\/LastModified><ETag>[^<]+?<\/ETag><Size>(\d+?)<\/Size>/g;
let matchs = xml.matchAll(fileRe);
for (const m of matchs) {
const fullname = m[1].trim();
@@ -42,7 +60,7 @@ export class BucketBinary extends AbstractBinary {
const name = path.basename(fullname);
const date = m[2].trim();
const size = parseInt(m[3].trim());
const size = Number.parseInt(m[3].trim());
items.push({
name,
isDir: false,
@@ -52,7 +70,8 @@ export class BucketBinary extends AbstractBinary {
});
}
// <CommonPrefixes><Prefix>v0.59.0/x64/</Prefix></CommonPrefixes>
const dirRe = /<CommonPrefixes><Prefix>([^<]+?)<\/Prefix><\/CommonPrefixes>/g;
const dirRe =
/<CommonPrefixes><Prefix>([^<]+?)<\/Prefix><\/CommonPrefixes>/g;
matchs = xml.matchAll(dirRe);
for (const m of matchs) {
// <Prefix>AWSLogs/</Prefix>
@@ -65,7 +84,7 @@ export class BucketBinary extends AbstractBinary {
let date = '-';
// root dir children, should set date to '2022-04-19T01:00:00Z', sync per hour
if (dir === '/') {
date = new Date().toISOString().split(':', 1)[0] + ':00:00Z';
date = `${new Date().toISOString().split(':', 1)[0]}:00:00Z`;
}
items.push({
name,

View File

@@ -1,7 +1,14 @@
import { basename } from 'node:path';
import { SingletonProto } from '@eggjs/tegg';
import { BinaryType } from '../../enum/Binary';
import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './AbstractBinary';
import { SingletonProto } from 'egg';
import { BinaryType } from '../../enum/Binary.ts';
import {
AbstractBinary,
BinaryAdapter,
type BinaryItem,
type FetchResult,
} from './AbstractBinary.ts';
@SingletonProto()
@BinaryAdapter(BinaryType.ChromeForTesting)
@@ -18,7 +25,11 @@ export class ChromeForTestingBinary extends AbstractBinary {
}
async finishFetch(success: boolean) {
if (success && this.#timestamp && ChromeForTestingBinary.lastTimestamp !== this.#timestamp) {
if (
success &&
this.#timestamp &&
ChromeForTestingBinary.lastTimestamp !== this.#timestamp
) {
ChromeForTestingBinary.lastTimestamp = this.#timestamp;
}
}
@@ -26,22 +37,35 @@ export class ChromeForTestingBinary extends AbstractBinary {
async #syncDirItems() {
this.dirItems = {};
this.dirItems['/'] = [];
const jsonApiEndpoint = 'https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json';
const { data, status, headers } = await this.httpclient.request(jsonApiEndpoint, {
dataType: 'json',
timeout: 30000,
followRedirect: true,
gzip: true,
});
const jsonApiEndpoint =
'https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json';
const { data, status, headers } = await this.httpclient.request(
jsonApiEndpoint,
{
dataType: 'json',
timeout: 30_000,
followRedirect: true,
gzip: true,
}
);
if (status !== 200) {
this.logger.warn('[ChromeForTestingBinary.request:non-200-status] url: %s, status: %s, headers: %j, data: %j',
jsonApiEndpoint, status, headers, data);
this.logger.warn(
'[ChromeForTestingBinary.request:non-200-status] url: %s, status: %s, headers: %j, data: %j',
jsonApiEndpoint,
status,
headers,
data
);
return;
}
this.#timestamp = data.timestamp;
const hasNewData = this.#timestamp !== ChromeForTestingBinary.lastTimestamp;
this.logger.info('[ChromeForTestingBinary] remote data timestamp: %j, last timestamp: %j, hasNewData: %s',
this.#timestamp, ChromeForTestingBinary.lastTimestamp, hasNewData);
this.logger.info(
'[ChromeForTestingBinary] remote data timestamp: %j, last timestamp: %j, hasNewData: %s',
this.#timestamp,
ChromeForTestingBinary.lastTimestamp,
hasNewData
);
if (!hasNewData) {
return;
}
@@ -151,6 +175,6 @@ export class ChromeForTestingBinary extends AbstractBinary {
if (!this.dirItems) {
await this.#syncDirItems();
}
return { items: this.dirItems![dir], nextParams: null };
return { items: this.dirItems?.[dir] ?? [], nextParams: null };
}
}

View File

@@ -1,6 +1,12 @@
import { SingletonProto } from '@eggjs/tegg';
import { BinaryType } from '../../enum/Binary';
import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './AbstractBinary';
import { SingletonProto } from 'egg';
import { BinaryType } from '../../enum/Binary.ts';
import {
AbstractBinary,
BinaryAdapter,
type BinaryItem,
type FetchResult,
} from './AbstractBinary.ts';
@SingletonProto()
@BinaryAdapter(BinaryType.Cypress)
@@ -20,7 +26,7 @@ export class CypressBinary extends AbstractBinary {
this.dirItems = {};
this.dirItems['/'] = [];
for (const version in data.versions) {
const major = parseInt(version.split('.', 1)[0]);
const major = Number.parseInt(version.split('.', 1)[0]);
// need >= 4.0.0
// https://npmmirror.com/mirrors/cypress/4.0.0/
if (major < 4) continue;
@@ -53,8 +59,10 @@ export class CypressBinary extends AbstractBinary {
// { platform: 'win32', arch: 'x64' },
// ]
const platforms = [
'darwin-x64', 'darwin-arm64',
'linux-x64', 'linux-arm64',
'darwin-x64',
'darwin-arm64',
'linux-x64',
'linux-arm64',
'win32-x64',
];
for (const platform of platforms) {

View File

@@ -1,9 +1,14 @@
import path from 'node:path';
import { SingletonProto } from '@eggjs/tegg';
import { SingletonProto } from 'egg';
import {
AbstractBinary, FetchResult, BinaryItem, BinaryAdapter,
} from './AbstractBinary';
import { BinaryType } from '../../enum/Binary';
AbstractBinary,
BinaryAdapter,
type BinaryItem,
type FetchResult,
} from './AbstractBinary.ts';
import { BinaryType } from '../../enum/Binary.ts';
@SingletonProto()
@BinaryAdapter(BinaryType.Edgedriver)
@@ -20,15 +25,23 @@ export class EdgedriverBinary extends AbstractBinary {
this.dirItems = {};
this.dirItems['/'] = [];
const jsonApiEndpoint = 'https://edgeupdates.microsoft.com/api/products';
const { data, status, headers } = await this.httpclient.request(jsonApiEndpoint, {
dataType: 'json',
timeout: 30000,
followRedirect: true,
gzip: true,
});
const { data, status, headers } = await this.httpclient.request(
jsonApiEndpoint,
{
dataType: 'json',
timeout: 30_000,
followRedirect: true,
gzip: true,
}
);
if (status !== 200) {
this.logger.warn('[EdgedriverBinary.request:non-200-status] url: %s, status: %s, headers: %j, data: %j',
jsonApiEndpoint, status, headers, data);
this.logger.warn(
'[EdgedriverBinary.request:non-200-status] url: %s, status: %s, headers: %j, data: %j',
jsonApiEndpoint,
status,
headers,
data
);
return;
}
this.logger.info('[EdgedriverBinary] remote data length: %s', data.length);
@@ -160,12 +173,12 @@ export class EdgedriverBinary extends AbstractBinary {
}
// fetch root dir
if (dir === '/') {
return { items: this.dirItems![dir], nextParams: null };
return { items: this.dirItems?.[dir] ?? [], nextParams: null };
}
// fetch sub dir
// /foo/ => foo/
const subDir = dir.substring(1);
const subDir = dir.slice(1);
// https://msedgewebdriverstorage.blob.core.windows.net/edgewebdriver?prefix=124.0.2478.97/&delimiter=/&maxresults=100&restype=container&comp=list
const url = `https://msedgewebdriverstorage.blob.core.windows.net/edgewebdriver?prefix=${encodeURIComponent(subDir)}&delimiter=/&maxresults=100&restype=container&comp=list`;
const xml = await this.requestXml(url);
@@ -175,7 +188,8 @@ export class EdgedriverBinary extends AbstractBinary {
#parseItems(xml: string): BinaryItem[] {
const items: BinaryItem[] = [];
// <Blob><Name>124.0.2478.97/edgedriver_arm64.zip</Name><Url>https://msedgewebdriverstorage.blob.core.windows.net/edgewebdriver/124.0.2478.97/edgedriver_arm64.zip</Url><Properties><Last-Modified>Fri, 10 May 2024 18:35:44 GMT</Last-Modified><Etag>0x8DC712000713C13</Etag><Content-Length>9191362</Content-Length><Content-Type>application/octet-stream</Content-Type><Content-Encoding /><Content-Language /><Content-MD5>1tjPTf5JU6KKB06Qf1JOGw==</Content-MD5><Cache-Control /><BlobType>BlockBlob</BlobType><LeaseStatus>unlocked</LeaseStatus></Properties></Blob>
const fileRe = /<Blob><Name>([^<]+?)<\/Name><Url>([^<]+?)<\/Url><Properties><Last\-Modified>([^<]+?)<\/Last\-Modified><Etag>(?:[^<]+?)<\/Etag><Content\-Length>(\d+)<\/Content\-Length>/g;
const fileRe =
/<Blob><Name>([^<]+?)<\/Name><Url>([^<]+?)<\/Url><Properties><Last-Modified>([^<]+?)<\/Last-Modified><Etag>(?:[^<]+?)<\/Etag><Content-Length>(\d+)<\/Content-Length>/g;
const matchItems = xml.matchAll(fileRe);
for (const m of matchItems) {
const fullname = m[1].trim();
@@ -199,7 +213,7 @@ export class EdgedriverBinary extends AbstractBinary {
const name = path.basename(fullname);
const url = m[2].trim();
const date = m[3].trim();
const size = parseInt(m[4].trim());
const size = Number.parseInt(m[4].trim());
items.push({
name,
isDir: false,

View File

@@ -1,13 +1,21 @@
import { SingletonProto } from '@eggjs/tegg';
import { BinaryType } from '../../enum/Binary';
import binaries, { BinaryName } from '../../../../config/binaries';
import { BinaryAdapter, BinaryItem, FetchResult } from './AbstractBinary';
import { GithubBinary } from './GithubBinary';
import { SingletonProto } from 'egg';
import binaries, { type BinaryName } from '../../../../config/binaries.ts';
import { BinaryType } from '../../enum/Binary.ts';
import { GithubBinary } from './GithubBinary.ts';
import {
BinaryAdapter,
type BinaryItem,
type FetchResult,
} from './AbstractBinary.ts';
@SingletonProto()
@BinaryAdapter(BinaryType.Electron)
export class ElectronBinary extends GithubBinary {
async fetch(dir: string, binaryName: BinaryName = 'electron'): Promise<FetchResult | undefined> {
async fetch(
dir: string,
binaryName: BinaryName = 'electron'
): Promise<FetchResult | undefined> {
const releases = await this.initReleases(binaryName, binaries.electron);
if (!releases) return;
@@ -24,7 +32,7 @@ export class ElectronBinary extends GithubBinary {
// v14.2.6 => 14.2.6
if (/^v\d+?\./.test(item.tag_name)) {
items.push({
name: `${item.tag_name.substring(1)}/`,
name: `${item.tag_name.slice(1)}/`,
isDir: true,
url: item.url,
size: '-',
@@ -34,7 +42,10 @@ export class ElectronBinary extends GithubBinary {
}
} else {
for (const item of releases) {
if (dir === `/${item.tag_name}/` || dir === `/${item.tag_name.substring(1)}/`) {
if (
dir === `/${item.tag_name}/` ||
dir === `/${item.tag_name.slice(1)}/`
) {
items = this.formatItems(item, binaries.electron);
break;
}

View File

@@ -0,0 +1,145 @@
import { basename } from 'node:path';
import { SingletonProto } from 'egg';
import binaries, { type BinaryName } from '../../../../config/binaries.ts';
import { BinaryType } from '../../enum/Binary.ts';
import {
AbstractBinary,
BinaryAdapter,
type BinaryItem,
type FetchResult,
} from './AbstractBinary.ts';
@SingletonProto()
@BinaryAdapter(BinaryType.Firefox)
export class FirefoxBinary extends AbstractBinary {
async initFetch() {
// do nothing
return;
}
// Only fetch Firefox versions >= 100.0.0 to avoid too old versions
async fetch(
dir: string,
binaryName: BinaryName
): Promise<FetchResult | undefined> {
const binaryConfig = binaries[binaryName];
const url = `${binaryConfig.distUrl}${dir}`;
const html = await this.requestXml(url);
// Mozilla archive has format like:
// <tr>
// <td>Dir</td>
// <td><a href="/pub/firefox/releases/131.0.3/update/">update/</a></td>
// <td></td>
// <td></td>
// </tr>
// <tr>
// <td>File</td>
// <td><a href="/pub/firefox/releases/131.0.3/SHA256SUMS.asc">SHA256SUMS.asc</a></td>
// <td>833</td>
// <td>12-Apr-2025 08:52</td>
// </tr>
// Parse Mozilla directory listing format - handles two different formats:
// Format 1 (main index): <td><a href="/path/">name/</a></td>
// Format 2 (version dir): <td>Type</td><td><a href="/path/">name</a></td><td>size</td><td>date</td>
// Try the detailed format first (with Type/Size/Date columns)
const detailedRe = /<tr>\s*<td>(Dir|File)<\/td>\s*<td><a href="([^"]+?)"[^>]*?>[^<]+?<\/a><\/td>\s*<td>([^<]*?)<\/td>\s*<td>([^<]*?)<\/td>\s*<\/tr>/gi;
const detailedMatches = Array.from(html.matchAll(detailedRe));
let matchs: RegExpMatchArray[];
let useDetailedFormat = false;
if (detailedMatches.length > 0) {
// Use detailed format
matchs = detailedMatches;
useDetailedFormat = true;
} else {
// Fallback to simple format
const simpleRe = /<td><a href="([^"]+?)"[^>]*?>[^<]+?<\/a><\/td>/gi;
matchs = Array.from(html.matchAll(simpleRe));
}
const items: BinaryItem[] = [];
for (const m of matchs) {
let href: string;
let isDir: boolean;
let size: string;
let date: string;
if (useDetailedFormat) {
// Detailed format: [fullMatch, type, href, size, date]
const type = m[1]; // "Dir" or "File"
href = m[2];
size = m[3].trim() || '-';
date = m[4].trim() || '-';
isDir = type === 'Dir';
} else {
// Simple format: [fullMatch, href]
href = m[1];
isDir = href.endsWith('/');
size = '-';
date = '-';
}
// Extract the name from the href path
// href could be "/pub/firefox/releases/130.0/" or just "130.0/"
let name = href;
if (href.startsWith('/')) {
// Extract the last part of the path
const parts = href.split('/').filter(Boolean);
name = parts[parts.length - 1] ?? '';
if (href.endsWith('/')) {
name += '/';
}
}
if (!isDir) {
// Keep the full name for files
name = basename(name);
}
// Skip parent directory links
if (name === '../' || href === '/pub/firefox/' || href.endsWith('/..') || href === '/pub/firefox/releases/') continue;
// Filter out old Firefox versions (< 100.0.0) for directories - apply to main index (root directory)
if (isDir && name !== '../' && dir === '/') {
const versionName = name.slice(0, -1); // Remove trailing '/'
// Skip non-version directories that are just special names
if (/^\d+\.\d+/.test(versionName)) {
try {
const major = Number.parseInt(versionName.split('.')[0]);
if (major < 100) {
continue; // Skip versions < 100.0.0
}
} catch {
// If version parsing fails, skip this directory
continue;
}
}
// Also skip named directories that aren't version numbers
else if (!['latest', 'latest-beta', 'latest-esr'].includes(versionName)) {
continue;
}
}
const fileUrl = isDir ? '' : `${url}${name}`;
if (binaryConfig.ignoreFiles?.includes(`${dir}${name}`)) continue;
const item = {
name,
isDir,
url: fileUrl,
size,
date,
ignoreDownloadStatuses: binaryConfig.options?.ignoreDownloadStatuses,
};
items.push(item);
}
return { items, nextParams: null };
}
}

View File

@@ -1,15 +1,17 @@
import { SingletonProto } from '@eggjs/tegg';
import { BinaryType } from '../../enum/Binary';
import binaries, { BinaryName, BinaryTaskConfig } from '../../../../config/binaries';
import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './AbstractBinary';
import { SingletonProto } from 'egg';
import binaries, { type BinaryName, type BinaryTaskConfig } from '../../../../config/binaries.ts';
import { BinaryType } from '../../enum/Binary.ts';
import { AbstractBinary, BinaryAdapter, type BinaryItem, type FetchResult } from './AbstractBinary.ts';
@SingletonProto()
@BinaryAdapter(BinaryType.GitHub)
export class GithubBinary extends AbstractBinary {
private releases: Record<string, any[]> = {};
// oxlint-disable-next-line typescript-eslint/no-explicit-any
private releases: Record<string, any[] | undefined> = {};
async initFetch(binaryName: BinaryName) {
delete this.releases[binaryName];
this.releases[binaryName] = undefined;
}
protected async initReleases(binaryName: BinaryName, binaryConfig: BinaryTaskConfig) {
@@ -17,10 +19,12 @@ export class GithubBinary extends AbstractBinary {
// https://docs.github.com/en/rest/reference/releases get three pages
// https://api.github.com/repos/electron/electron/releases
// https://api.github.com/repos/electron/electron/releases?per_page=100&page=3
// oxlint-disable-next-line typescript-eslint/no-explicit-any
let releases: any[] = [];
const maxPage = binaryConfig.options?.maxPage || 1;
const perPage = binaryConfig.options?.perPage || 100;
for (let i = 0; i < maxPage; i++) {
const url = `https://api.github.com/repos/${binaryConfig.repo}/releases?per_page=100&page=${i + 1}`;
const url = `https://api.github.com/repos/${binaryConfig.repo}/releases?per_page=${perPage}&page=${i + 1}`;
const requestHeaders: Record<string, string> = {};
if (process.env.GITHUB_TOKEN) {
requestHeaders.Authorization = `token ${process.env.GITHUB_TOKEN}`;
@@ -42,13 +46,17 @@ export class GithubBinary extends AbstractBinary {
return this.releases[binaryName];
}
// oxlint-disable-next-line typescript-eslint/no-explicit-any
protected formatItems(releaseItem: any, binaryConfig: BinaryTaskConfig) {
const items: BinaryItem[] = [];
// 250MB
const maxFileSize = 1024 * 1024 * 250;
for (const asset of releaseItem.assets) {
if (asset.size > maxFileSize) {
this.logger.info('[GithubBinary.formatItems] asset reach max file size(> 250MB), ignore download it, asset: %j', asset);
this.logger.info(
'[GithubBinary.formatItems] asset reach max file size(> 250MB), ignore download it, asset: %j',
asset
);
continue;
}
items.push({

View File

@@ -1,7 +1,13 @@
import { SingletonProto } from '@eggjs/tegg';
import { BinaryType } from '../../enum/Binary';
import binaries, { BinaryName } from '../../../../config/binaries';
import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './AbstractBinary';
import { SingletonProto } from 'egg';
import binaries, { type BinaryName } from '../../../../config/binaries.ts';
import { BinaryType } from '../../enum/Binary.ts';
import {
AbstractBinary,
BinaryAdapter,
type BinaryItem,
type FetchResult,
} from './AbstractBinary.ts';
@SingletonProto()
@BinaryAdapter(BinaryType.Imagemin)
@@ -11,7 +17,10 @@ export class ImageminBinary extends AbstractBinary {
return;
}
async fetch(dir: string, binaryName: BinaryName): Promise<FetchResult | undefined> {
async fetch(
dir: string,
binaryName: BinaryName
): Promise<FetchResult | undefined> {
const binaryConfig = binaries[binaryName];
const dirItems: {
[key: string]: BinaryItem[];
@@ -24,7 +33,7 @@ export class ImageminBinary extends AbstractBinary {
// https://github.com/imagemin/jpegtran-bin/blob/v4.0.0/lib/index.js
// https://github.com/imagemin/pngquant-bin/blob/v4.0.0/lib/index.js
for (const version in data.versions) {
const major = parseInt(version.split('.', 1)[0]);
const major = Number.parseInt(version.split('.', 1)[0]);
if (major < 4) continue;
// >= 4.0.0
const date = data.time[version];
@@ -47,7 +56,7 @@ export class ImageminBinary extends AbstractBinary {
});
const versionVendorDir = `/v${version}/vendor/`;
dirItems[versionVendorDir] = [];
for (const platform of binaryConfig.options!.nodePlatforms!) {
for (const platform of binaryConfig.options?.nodePlatforms ?? []) {
dirItems[versionVendorDir].push({
name: `${platform}/`,
date,
@@ -57,16 +66,16 @@ export class ImageminBinary extends AbstractBinary {
});
const platformDir = `/v${version}/vendor/${platform}/`;
dirItems[platformDir] = [];
const archs = binaryConfig.options!.nodeArchs![platform];
const archs = binaryConfig.options?.nodeArchs?.[platform] ?? [];
if (archs.length === 0) {
for (const name of binaryConfig.options!.binFiles![platform]) {
for (const name of binaryConfig.options?.binFiles?.[platform] ?? []) {
dirItems[platformDir].push({
name,
date,
size: '-',
isDir: false,
url: `${binaryConfig.distUrl}/${binaryConfig.repo}${platformDir}${name}`,
ignoreDownloadStatuses: [ 404 ],
ignoreDownloadStatuses: [404],
});
}
} else {
@@ -81,14 +90,15 @@ export class ImageminBinary extends AbstractBinary {
const platformArchDir = `/v${version}/vendor/${platform}/${arch}/`;
dirItems[platformArchDir] = [];
for (const name of binaryConfig.options!.binFiles![platform]) {
for (const name of binaryConfig.options?.binFiles?.[platform] ??
[]) {
dirItems[platformArchDir].push({
name,
date,
size: '-',
isDir: false,
url: `${binaryConfig.distUrl}/${binaryConfig.repo}${platformArchDir}${name}`,
ignoreDownloadStatuses: [ 404 ],
ignoreDownloadStatuses: [404],
});
}
}

View File

@@ -1,8 +1,16 @@
import { basename } from 'node:path';
import { SingletonProto } from '@eggjs/tegg';
import { BinaryType } from '../../enum/Binary';
import binaries, { BinaryName } from '../../../../config/binaries';
import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './AbstractBinary';
import { SingletonProto } from 'egg';
import dayjs from 'dayjs';
import binaries, { type BinaryName } from '../../../../config/binaries.ts';
import { BinaryType } from '../../enum/Binary.ts';
import {
AbstractBinary,
BinaryAdapter,
type BinaryItem,
type FetchResult,
} from './AbstractBinary.ts';
@SingletonProto()
@BinaryAdapter(BinaryType.Node)
@@ -12,10 +20,14 @@ export class NodeBinary extends AbstractBinary {
return;
}
async fetch(dir: string, binaryName: BinaryName): Promise<FetchResult | undefined> {
async fetch(
dir: string,
binaryName: BinaryName
): Promise<FetchResult | undefined> {
const binaryConfig = binaries[binaryName];
const url = `${binaryConfig.distUrl}${dir}`;
const html = await this.requestXml(url);
// <a href="v9.8.0/">v9.8.0/</a> 08-Mar-2018 01:55 -
// <a href="v9.9.0/">v9.9.0/</a> 21-Mar-2018 15:47 -
// <a href="index.json">index.json</a> 17-Dec-2021 23:16 219862
@@ -30,7 +42,43 @@ export class NodeBinary extends AbstractBinary {
// <a href="/dist/v18.15.0/SHASUMS256.txt.asc">SHASUMS256.txt.asc</a> 04-Nov-2024 17:29 3.7 KB
// <a href="/dist/v18.15.0/SHASUMS256.txt.sig">SHASUMS256.txt.sig</a> 04-Nov-2024 17:29 310 B
// <a href="/dist/v18.15.0/SHASUMS256.txt">SHASUMS256.txt</a> 04-Nov-2024 17:29 3.2 KB
const re = /<a href="([^\"]+?)"[^>]*?>[^<]+?<\/a>\s+?((?:[\w\-]+? \w{2}\:\d{2})|\-)\s+?([\d\.\-\s\w]+)/ig;
// <a href="/dist/latest-v20.x/SHASUMS256.txt.asc">SHASUMS256.txt.asc</a> 03 Sept 2025, 18:20 4.7 KB
// <a href="/dist/latest-v20.x/SHASUMS256.txt.sig">SHASUMS256.txt.sig</a> 03 Sept 2025, 18:20 566 B
// <a href="/dist/latest-v20.x/SHASUMS256.txt">SHASUMS256.txt</a> 03 Sept 2025, 18:19 3.8 KB
// <a href="/dist/latest-v20.x/node-v20.19.5-aix-ppc64.tar.gz">node-v20.19.5-aix-ppc64.tar.gz</a> 03 Sept 2025, 18:19 60 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-arm64.msi">node-v20.19.5-arm64.msi</a> 03 Sept 2025, 18:19 24 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-darwin-arm64.tar.gz">node-v20.19.5-darwin-arm64.tar.gz</a> 03 Sept 2025, 18:19 41 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-darwin-arm64.tar.xz">node-v20.19.5-darwin-arm64.tar.xz</a> 03 Sept 2025, 18:19 21 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-darwin-x64.tar.gz">node-v20.19.5-darwin-x64.tar.gz</a> 03 Sept 2025, 18:19 43 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-darwin-x64.tar.xz">node-v20.19.5-darwin-x64.tar.xz</a> 03 Sept 2025, 18:19 23 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-headers.tar.gz">node-v20.19.5-headers.tar.gz</a> 03 Sept 2025, 18:19 8.7 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-headers.tar.xz">node-v20.19.5-headers.tar.xz</a> 03 Sept 2025, 18:19 524 KB
// <a href="/dist/latest-v20.x/node-v20.19.5-linux-arm64.tar.gz">node-v20.19.5-linux-arm64.tar.gz</a> 03 Sept 2025, 18:19 47 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-linux-arm64.tar.xz">node-v20.19.5-linux-arm64.tar.xz</a> 03 Sept 2025, 18:19 25 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-linux-armv7l.tar.gz">node-v20.19.5-linux-armv7l.tar.gz</a> 03 Sept 2025, 18:19 43 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-linux-armv7l.tar.xz">node-v20.19.5-linux-armv7l.tar.xz</a> 03 Sept 2025, 18:19 22 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-linux-ppc64le.tar.gz">node-v20.19.5-linux-ppc64le.tar.gz</a> 03 Sept 2025, 18:19 49 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-linux-ppc64le.tar.xz">node-v20.19.5-linux-ppc64le.tar.xz</a> 03 Sept 2025, 18:19 26 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-linux-s390x.tar.gz">node-v20.19.5-linux-s390x.tar.gz</a> 03 Sept 2025, 18:19 47 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-linux-s390x.tar.xz">node-v20.19.5-linux-s390x.tar.xz</a> 03 Sept 2025, 18:19 25 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-linux-x64.tar.gz">node-v20.19.5-linux-x64.tar.gz</a> 03 Sept 2025, 18:19 47 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-linux-x64.tar.xz">node-v20.19.5-linux-x64.tar.xz</a> 03 Sept 2025, 18:19 26 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-win-arm64.7z">node-v20.19.5-win-arm64.7z</a> 03 Sept 2025, 18:19 17 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-win-arm64.zip">node-v20.19.5-win-arm64.zip</a> 03 Sept 2025, 18:19 26 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-win-x64.7z">node-v20.19.5-win-x64.7z</a> 03 Sept 2025, 18:19 19 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-win-x64.zip">node-v20.19.5-win-x64.zip</a> 03 Sept 2025, 18:19 30 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-win-x86.7z">node-v20.19.5-win-x86.7z</a> 03 Sept 2025, 18:19 18 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-win-x86.zip">node-v20.19.5-win-x86.zip</a> 03 Sept 2025, 18:19 28 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-x64.msi">node-v20.19.5-x64.msi</a> 03 Sept 2025, 18:19 27 MB
// <a href="/dist/latest-v20.x/node-v20.19.5-x86.msi">node-v20.19.5-x86.msi</a> 03 Sept 2025, 18:19 25 MB
// <a href="/dist/latest-v20.x/node-v20.19.5.pkg">node-v20.19.5.pkg</a> 03 Sept 2025, 18:19 72 MB
// <a href="/dist/latest-v20.x/node-v20.19.5.tar.gz">node-v20.19.5.tar.gz</a> 03 Sept 2025, 18:19 89 MB
// <a href="/dist/latest-v20.x/node-v20.19.5.tar.xz">node-v20.19.5.tar.xz</a> 03 Sept 2025, 18:19 43 MB
// date format: 19-Jan-2020 06:07 or 03 Sept 2025, 18:19
const re =
/<a href="([^"]+?)"[^>]*?>[^<]+?<\/a>\s+?((?:[\w-]+? \w{2}:\d{2})|(?:\d{2} [A-Za-z]{3,9} \d{4}, \d{2}:\d{2})|-)\s+?([\d.\-\s\w]+)/gi;
const matchs = html.matchAll(re);
const items: BinaryItem[] = [];
for (const m of matchs) {
@@ -41,7 +89,7 @@ export class NodeBinary extends AbstractBinary {
name = basename(name);
}
const fileUrl = isDir ? '' : `${url}${name}`;
const date = m[2];
const date = m[2] === '-' ? '-' : dayjs(m[2]).format('DD-MMM-YYYY HH:mm');
const size = m[3].trim();
if (size === '0') continue;
if (binaryConfig.ignoreFiles?.includes(`${dir}${name}`)) continue;

View File

@@ -1,8 +1,15 @@
import { SingletonProto } from '@eggjs/tegg';
import { BinaryType } from '../../enum/Binary';
import binaries, { BinaryName } from '../../../../config/binaries';
import { join } from 'node:path';
import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './AbstractBinary';
import { SingletonProto } from 'egg';
import binaries, { type BinaryName } from '../../../../config/binaries.ts';
import { BinaryType } from '../../enum/Binary.ts';
import {
AbstractBinary,
BinaryAdapter,
type BinaryItem,
type FetchResult,
} from './AbstractBinary.ts';
@SingletonProto()
@BinaryAdapter(BinaryType.NodePreGyp)
@@ -13,7 +20,10 @@ export class NodePreGypBinary extends AbstractBinary {
}
// https://github.com/mapbox/node-pre-gyp
async fetch(dir: string, binaryName: BinaryName): Promise<FetchResult | undefined> {
async fetch(
dir: string,
binaryName: BinaryName
): Promise<FetchResult | undefined> {
const binaryConfig = binaries[binaryName];
const npmPackageName = binaryConfig.options?.npmPackageName ?? binaryName;
const pkgUrl = `https://registry.npmjs.com/${npmPackageName}`;
@@ -33,20 +43,28 @@ export class NodePreGypBinary extends AbstractBinary {
if (!pkgVersion.binary) continue;
// https://github.com/mapbox/node-pre-gyp#package_name
// defaults to {module_name}-v{version}-{node_abi}-{platform}-{arch}.tar.gz
let binaryFile = pkgVersion.binary.package_name
|| '{module_name}-v{version}-{node_abi}-{platform}-{arch}.tar.gz';
let binaryFile =
pkgVersion.binary.package_name ||
'{module_name}-v{version}-{node_abi}-{platform}-{arch}.tar.gz';
if (!binaryFile) continue;
const moduleName = pkgVersion.binary.module_name || pkgVersion.name;
binaryFile = binaryFile.replace('{version}', version)
binaryFile = binaryFile
.replace('{version}', version)
.replace('{module_name}', moduleName);
let currentDir = dirItems['/'];
let versionPrefix = '';
let remotePath = pkgVersion.binary.remote_path;
const napiVersions = pkgVersion.binary.napi_versions ?? [];
if (binaryConfig.options?.requiredNapiVersions && napiVersions.length === 0) continue;
if (
binaryConfig.options?.requiredNapiVersions &&
napiVersions.length === 0
)
continue;
if (remotePath?.includes('{version}')) {
const dirName = remotePath.includes('v{version}') ? `v${version}` : version;
const dirName = remotePath.includes('v{version}')
? `v${version}`
: version;
versionPrefix = `/${dirName}`;
dirItems['/'].push({
name: `${dirName}/`,
@@ -55,7 +73,8 @@ export class NodePreGypBinary extends AbstractBinary {
isDir: true,
url: '',
});
currentDir = dirItems[`/${dirName}/`] = [];
currentDir = [];
dirItems[`/${dirName}/`] = currentDir;
}
// https://node-precompiled-binaries.grpc.io/?delimiter=/&prefix=grpc/v1.24.11/
@@ -67,17 +86,20 @@ export class NodePreGypBinary extends AbstractBinary {
// "remote_path": "{name}/v{version}",
// "package_name": "{node_abi}-{platform}-{arch}-{libc}.tar.gz"
// },
if (binaryFile.includes('{node_abi}')
&& binaryFile.includes('{platform}')
&& binaryFile.includes('{arch}')
&& binaryFile.includes('{libc}')) {
if (
binaryFile.includes('{node_abi}') &&
binaryFile.includes('{platform}') &&
binaryFile.includes('{arch}') &&
binaryFile.includes('{libc}')
) {
for (const nodeAbi of nodeABIVersions) {
for (const platform of nodePlatforms) {
const archs = nodeArchs[platform];
const libcs = nodeLibcs[platform];
for (const arch of archs) {
for (const libc of libcs) {
const name = binaryFile.replace('{node_abi}', `node-v${nodeAbi}`)
const name = binaryFile
.replace('{node_abi}', `node-v${nodeAbi}`)
.replace('{platform}', platform)
.replace('{arch}', arch)
.replace('{libc}', libc);
@@ -87,20 +109,23 @@ export class NodePreGypBinary extends AbstractBinary {
size: '-',
isDir: false,
url: `${binaryConfig.distUrl}/${binaryName}${versionPrefix}/${name}`,
ignoreDownloadStatuses: [ 404 ],
ignoreDownloadStatuses: [404],
});
}
}
}
}
} else if (binaryFile.includes('{node_abi}')
&& binaryFile.includes('{platform}')
&& binaryFile.includes('{arch}')) {
} else if (
binaryFile.includes('{node_abi}') &&
binaryFile.includes('{platform}') &&
binaryFile.includes('{arch}')
) {
for (const nodeAbi of nodeABIVersions) {
for (const platform of nodePlatforms) {
const archs = nodeArchs[platform];
for (const arch of archs) {
const name = binaryFile.replace('{node_abi}', `node-v${nodeAbi}`)
const name = binaryFile
.replace('{node_abi}', `node-v${nodeAbi}`)
.replace('{platform}', platform)
.replace('{arch}', arch);
currentDir.push({
@@ -109,12 +134,15 @@ export class NodePreGypBinary extends AbstractBinary {
size: '-',
isDir: false,
url: `${binaryConfig.distUrl}/${binaryName}${versionPrefix}/${name}`,
ignoreDownloadStatuses: [ 404 ],
ignoreDownloadStatuses: [404],
});
}
}
}
} else if (binaryFile.includes('{platform}-{arch}-{node_napi_label}-{libc}') && napiVersions.length > 0) {
} else if (
binaryFile.includes('{platform}-{arch}-{node_napi_label}-{libc}') &&
napiVersions.length > 0
) {
// https://skia-canvas.s3.us-east-1.amazonaws.com/v0.9.30/darwin-arm64-napi-v6-unknown.tar.gz
// https://github.com/samizdatco/skia-canvas/blob/2a75801d7cce3b4e4e6ad015a173daefaa8465e6/package.json#L48
// "binary": {
@@ -133,7 +161,8 @@ export class NodePreGypBinary extends AbstractBinary {
for (const arch of archs) {
for (const libc of libcs) {
for (const napiVersion of napiVersions) {
const name = binaryFile.replace('{platform}', platform)
const name = binaryFile
.replace('{platform}', platform)
.replace('{arch}', arch)
.replace('{node_napi_label}', `napi-v${napiVersion}`)
.replace('{libc}', libc);
@@ -143,7 +172,7 @@ export class NodePreGypBinary extends AbstractBinary {
size: '-',
isDir: false,
url: `${binaryConfig.distUrl}${versionPrefix}/${name}`,
ignoreDownloadStatuses: [ 404, 403 ],
ignoreDownloadStatuses: [404, 403],
});
}
}
@@ -165,10 +194,12 @@ export class NodePreGypBinary extends AbstractBinary {
const archs = nodeArchs[platform];
for (const arch of archs) {
for (const napiVersion of napiVersions) {
const binaryFileName = binaryFile.replace('{platform}', platform)
const binaryFileName = binaryFile
.replace('{platform}', platform)
.replace('{arch}', arch)
.replace('{node_napi_label}', napiVersion);
remotePath = remotePath.replace('{module_name}', moduleName)
remotePath = remotePath
.replace('{module_name}', moduleName)
.replace('{name}', binaryName)
.replace('{version}', version)
.replace('{configuration}', 'Release');
@@ -180,12 +211,15 @@ export class NodePreGypBinary extends AbstractBinary {
size: '-',
isDir: false,
url: remoteUrl,
ignoreDownloadStatuses: [ 404 ],
ignoreDownloadStatuses: [404],
});
}
}
}
} else if (binaryFile.includes('{platform}') && binaryFile.includes('{arch}')) {
} else if (
binaryFile.includes('{platform}') &&
binaryFile.includes('{arch}')
) {
// https://github.com/grpc/grpc-node/blob/master/packages/grpc-tools/package.json#L29
// "binary": {
// "module_name": "grpc_tools",
@@ -205,9 +239,11 @@ export class NodePreGypBinary extends AbstractBinary {
for (const platform of nodePlatforms) {
const archs = nodeArchs[platform];
for (const arch of archs) {
const binaryFileName = binaryFile.replace('{platform}', platform)
const binaryFileName = binaryFile
.replace('{platform}', platform)
.replace('{arch}', arch);
remotePath = remotePath.replace('{module_name}', moduleName)
remotePath = remotePath
.replace('{module_name}', moduleName)
.replace('{name}', binaryName)
.replace('{version}', version)
.replace('{configuration}', 'Release');
@@ -219,7 +255,7 @@ export class NodePreGypBinary extends AbstractBinary {
size: '-',
isDir: false,
url: remoteUrl,
ignoreDownloadStatuses: [ 404 ],
ignoreDownloadStatuses: [404],
});
}
}

View File

@@ -1,8 +1,13 @@
import { SingletonProto } from '@eggjs/tegg';
import { BinaryType } from '../../enum/Binary';
import binaries from '../../../../config/binaries';
import { FetchResult, BinaryItem, BinaryAdapter } from './AbstractBinary';
import { BucketBinary } from './BucketBinary';
import { SingletonProto } from 'egg';
import binaries from '../../../../config/binaries.ts';
import { BinaryType } from '../../enum/Binary.ts';
import {
BinaryAdapter,
type BinaryItem,
type FetchResult,
} from './AbstractBinary.ts';
import { BucketBinary } from './BucketBinary.ts';
@SingletonProto()
@BinaryAdapter(BinaryType.Nwjs)
@@ -13,8 +18,10 @@ export class NwjsBinary extends BucketBinary {
const binaryConfig = binaries.nwjs;
const isRootDir = dir === '/';
// /foo/ => foo/
const subDir = dir.substring(1);
const url = isRootDir ? binaryConfig.distUrl : `${this.s3Url}${encodeURIComponent(subDir)}`;
const subDir = dir.slice(1);
const url = isRootDir
? binaryConfig.distUrl
: `${this.s3Url}${encodeURIComponent(subDir)}`;
const xml = await this.requestXml(url);
if (!xml) return;
@@ -25,7 +32,8 @@ export class NwjsBinary extends BucketBinary {
// <tr><td valign="top"><img src="/icons/folder.gif" alt="[DIR]"></td><td><a href="v0.15.0-rc1/">v0.15.0-rc1/</a></td><td align="right">06-May-2016 12:24 </td><td align="right"> - </td><td>&nbsp;</td></tr>
// <tr><td valign="top"><img src="/icons/folder.gif" alt="[DIR]"></td><td><a href="v0.15.0-rc2/">v0.15.0-rc2/</a></td><td align="right">13-May-2016 20:13 </td><td align="right"> - </td><td>&nbsp;</td></tr>
const items: BinaryItem[] = [];
const re = /<td><a [^>]+?>([^<]+?\/)<\/a><\/td><td [^>]+?>([^>]+?)<\/td>/ig;
const re =
/<td><a [^>]+?>([^<]+?\/)<\/a><\/td><td [^>]+?>([^>]+?)<\/td>/gi;
const matchs = xml.matchAll(re);
for (const m of matchs) {
const name = m[1].trim();

View File

@@ -1,9 +1,15 @@
import { AbstractBinary, BinaryAdapter, BinaryItem, FetchResult } from './AbstractBinary';
import util from 'node:util';
import path from 'node:path';
import { SingletonProto } from '@eggjs/tegg';
import { BinaryType } from '../../enum/Binary';
import { SingletonProto } from 'egg';
import { BinaryType } from '../../enum/Binary.ts';
import {
AbstractBinary,
BinaryAdapter,
type BinaryItem,
type FetchResult,
} from './AbstractBinary.ts';
const PACKAGE_URL = 'https://registry.npmjs.com/playwright-core';
const DOWNLOAD_HOST = 'https://playwright.azureedge.net/';
@@ -11,7 +17,7 @@ const DOWNLOAD_HOST = 'https://playwright.azureedge.net/';
// https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/registry/index.ts
/* eslint-disable quote-props */
const DOWNLOAD_PATHS = {
'chromium': {
chromium: {
'<unknown>': undefined,
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': 'builds/chromium/%s/chromium-linux.zip',
@@ -28,17 +34,17 @@ const DOWNLOAD_PATHS = {
'mac10.13': 'builds/chromium/%s/chromium-mac.zip',
'mac10.14': 'builds/chromium/%s/chromium-mac.zip',
'mac10.15': 'builds/chromium/%s/chromium-mac.zip',
'mac11': 'builds/chromium/%s/chromium-mac.zip',
mac11: 'builds/chromium/%s/chromium-mac.zip',
'mac11-arm64': 'builds/chromium/%s/chromium-mac-arm64.zip',
'mac12': 'builds/chromium/%s/chromium-mac.zip',
mac12: 'builds/chromium/%s/chromium-mac.zip',
'mac12-arm64': 'builds/chromium/%s/chromium-mac-arm64.zip',
'mac13': 'builds/chromium/%s/chromium-mac.zip',
mac13: 'builds/chromium/%s/chromium-mac.zip',
'mac13-arm64': 'builds/chromium/%s/chromium-mac-arm64.zip',
'mac14': 'builds/chromium/%s/chromium-mac.zip',
mac14: 'builds/chromium/%s/chromium-mac.zip',
'mac14-arm64': 'builds/chromium/%s/chromium-mac-arm64.zip',
'mac15': 'builds/chromium/%s/chromium-mac.zip',
mac15: 'builds/chromium/%s/chromium-mac.zip',
'mac15-arm64': 'builds/chromium/%s/chromium-mac-arm64.zip',
'win64': 'builds/chromium/%s/chromium-win64.zip',
win64: 'builds/chromium/%s/chromium-win64.zip',
},
'chromium-headless-shell': {
'<unknown>': undefined,
@@ -47,87 +53,128 @@ const DOWNLOAD_PATHS = {
'ubuntu22.04-x64': 'builds/chromium/%s/chromium-headless-shell-linux.zip',
'ubuntu24.04-x64': 'builds/chromium/%s/chromium-headless-shell-linux.zip',
'ubuntu18.04-arm64': undefined,
'ubuntu20.04-arm64': 'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip',
'ubuntu22.04-arm64': 'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip',
'ubuntu24.04-arm64': 'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip',
'ubuntu20.04-arm64':
'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip',
'ubuntu22.04-arm64':
'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip',
'ubuntu24.04-arm64':
'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip',
'debian11-x64': 'builds/chromium/%s/chromium-headless-shell-linux.zip',
'debian11-arm64': 'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip',
'debian11-arm64':
'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip',
'debian12-x64': 'builds/chromium/%s/chromium-headless-shell-linux.zip',
'debian12-arm64': 'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip',
'debian12-arm64':
'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip',
'mac10.13': undefined,
'mac10.14': undefined,
'mac10.15': undefined,
'mac11': 'builds/chromium/%s/chromium-headless-shell-mac.zip',
mac11: 'builds/chromium/%s/chromium-headless-shell-mac.zip',
'mac11-arm64': 'builds/chromium/%s/chromium-headless-shell-mac-arm64.zip',
'mac12': 'builds/chromium/%s/chromium-headless-shell-mac.zip',
mac12: 'builds/chromium/%s/chromium-headless-shell-mac.zip',
'mac12-arm64': 'builds/chromium/%s/chromium-headless-shell-mac-arm64.zip',
'mac13': 'builds/chromium/%s/chromium-headless-shell-mac.zip',
mac13: 'builds/chromium/%s/chromium-headless-shell-mac.zip',
'mac13-arm64': 'builds/chromium/%s/chromium-headless-shell-mac-arm64.zip',
'mac14': 'builds/chromium/%s/chromium-headless-shell-mac.zip',
mac14: 'builds/chromium/%s/chromium-headless-shell-mac.zip',
'mac14-arm64': 'builds/chromium/%s/chromium-headless-shell-mac-arm64.zip',
'mac15': 'builds/chromium/%s/chromium-headless-shell-mac.zip',
mac15: 'builds/chromium/%s/chromium-headless-shell-mac.zip',
'mac15-arm64': 'builds/chromium/%s/chromium-headless-shell-mac-arm64.zip',
'win64': 'builds/chromium/%s/chromium-headless-shell-win64.zip',
win64: 'builds/chromium/%s/chromium-headless-shell-win64.zip',
},
'chromium-tip-of-tree': {
'<unknown>': undefined,
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip',
'ubuntu22.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip',
'ubuntu24.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip',
'ubuntu20.04-x64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip',
'ubuntu22.04-x64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip',
'ubuntu24.04-x64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip',
'ubuntu18.04-arm64': undefined,
'ubuntu20.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip',
'ubuntu22.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip',
'ubuntu24.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip',
'debian11-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip',
'debian11-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip',
'debian12-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip',
'debian12-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip',
'ubuntu20.04-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip',
'ubuntu22.04-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip',
'ubuntu24.04-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip',
'debian11-x64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip',
'debian11-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip',
'debian12-x64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip',
'debian12-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip',
'mac10.13': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac.zip',
'mac10.14': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac.zip',
'mac10.15': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac.zip',
'mac11': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac.zip',
'mac11-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac-arm64.zip',
'mac12': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac.zip',
'mac12-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac-arm64.zip',
'mac13': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac.zip',
'mac13-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac-arm64.zip',
'mac14': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac.zip',
'mac14-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac-arm64.zip',
'mac15': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac.zip',
'mac15-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac-arm64.zip',
'win64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-win64.zip',
mac11: 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac.zip',
'mac11-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac-arm64.zip',
mac12: 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac.zip',
'mac12-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac-arm64.zip',
mac13: 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac.zip',
'mac13-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac-arm64.zip',
mac14: 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac.zip',
'mac14-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac-arm64.zip',
mac15: 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac.zip',
'mac15-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac-arm64.zip',
win64: 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-win64.zip',
},
'chromium-tip-of-tree-headless-shell': {
'<unknown>': undefined,
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'ubuntu22.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'ubuntu24.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'ubuntu20.04-x64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'ubuntu22.04-x64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'ubuntu24.04-x64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'ubuntu18.04-arm64': undefined,
'ubuntu20.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'ubuntu22.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'ubuntu24.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'debian11-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'debian11-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'debian12-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'debian12-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'ubuntu20.04-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'ubuntu22.04-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'ubuntu24.04-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'debian11-x64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'debian11-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'debian12-x64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'debian12-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'mac10.13': undefined,
'mac10.14': undefined,
'mac10.15': undefined,
'mac11': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac11-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
'mac12': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac12-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
'mac13': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac13-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
'mac14': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac14-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
'mac15': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac15-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
'win64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-win64.zip',
mac11:
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac11-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
mac12:
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac12-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
mac13:
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac13-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
mac14:
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac14-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
mac15:
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac15-arm64':
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
win64:
'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-win64.zip',
},
'firefox': {
firefox: {
'<unknown>': undefined,
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': 'builds/firefox/%s/firefox-ubuntu-20.04.zip',
@@ -144,17 +191,17 @@ const DOWNLOAD_PATHS = {
'mac10.13': 'builds/firefox/%s/firefox-mac.zip',
'mac10.14': 'builds/firefox/%s/firefox-mac.zip',
'mac10.15': 'builds/firefox/%s/firefox-mac.zip',
'mac11': 'builds/firefox/%s/firefox-mac.zip',
mac11: 'builds/firefox/%s/firefox-mac.zip',
'mac11-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip',
'mac12': 'builds/firefox/%s/firefox-mac.zip',
mac12: 'builds/firefox/%s/firefox-mac.zip',
'mac12-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip',
'mac13': 'builds/firefox/%s/firefox-mac.zip',
mac13: 'builds/firefox/%s/firefox-mac.zip',
'mac13-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip',
'mac14': 'builds/firefox/%s/firefox-mac.zip',
mac14: 'builds/firefox/%s/firefox-mac.zip',
'mac14-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip',
'mac15': 'builds/firefox/%s/firefox-mac.zip',
mac15: 'builds/firefox/%s/firefox-mac.zip',
'mac15-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip',
'win64': 'builds/firefox/%s/firefox-win64.zip',
win64: 'builds/firefox/%s/firefox-win64.zip',
},
'firefox-beta': {
'<unknown>': undefined,
@@ -164,8 +211,10 @@ const DOWNLOAD_PATHS = {
'ubuntu24.04-x64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-24.04.zip',
'ubuntu18.04-arm64': undefined,
'ubuntu20.04-arm64': undefined,
'ubuntu22.04-arm64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-22.04-arm64.zip',
'ubuntu24.04-arm64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-24.04-arm64.zip',
'ubuntu22.04-arm64':
'builds/firefox-beta/%s/firefox-beta-ubuntu-22.04-arm64.zip',
'ubuntu24.04-arm64':
'builds/firefox-beta/%s/firefox-beta-ubuntu-24.04-arm64.zip',
'debian11-x64': 'builds/firefox-beta/%s/firefox-beta-debian-11.zip',
'debian11-arm64': 'builds/firefox-beta/%s/firefox-beta-debian-11-arm64.zip',
'debian12-x64': 'builds/firefox-beta/%s/firefox-beta-debian-12.zip',
@@ -173,19 +222,19 @@ const DOWNLOAD_PATHS = {
'mac10.13': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
'mac10.14': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
'mac10.15': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
'mac11': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
mac11: 'builds/firefox-beta/%s/firefox-beta-mac.zip',
'mac11-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip',
'mac12': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
mac12: 'builds/firefox-beta/%s/firefox-beta-mac.zip',
'mac12-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip',
'mac13': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
mac13: 'builds/firefox-beta/%s/firefox-beta-mac.zip',
'mac13-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip',
'mac14': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
mac14: 'builds/firefox-beta/%s/firefox-beta-mac.zip',
'mac14-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip',
'mac15': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
mac15: 'builds/firefox-beta/%s/firefox-beta-mac.zip',
'mac15-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip',
'win64': 'builds/firefox-beta/%s/firefox-beta-win64.zip',
win64: 'builds/firefox-beta/%s/firefox-beta-win64.zip',
},
'webkit': {
webkit: {
'<unknown>': undefined,
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': 'builds/webkit/%s/webkit-ubuntu-20.04.zip',
@@ -200,21 +249,23 @@ const DOWNLOAD_PATHS = {
'debian12-x64': 'builds/webkit/%s/webkit-debian-12.zip',
'debian12-arm64': 'builds/webkit/%s/webkit-debian-12-arm64.zip',
'mac10.13': undefined,
'mac10.14': 'builds/deprecated-webkit-mac-10.14/%s/deprecated-webkit-mac-10.14.zip',
'mac10.15': 'builds/deprecated-webkit-mac-10.15/%s/deprecated-webkit-mac-10.15.zip',
'mac11': 'builds/webkit/%s/webkit-mac-11.zip',
'mac10.14':
'builds/deprecated-webkit-mac-10.14/%s/deprecated-webkit-mac-10.14.zip',
'mac10.15':
'builds/deprecated-webkit-mac-10.15/%s/deprecated-webkit-mac-10.15.zip',
mac11: 'builds/webkit/%s/webkit-mac-11.zip',
'mac11-arm64': 'builds/webkit/%s/webkit-mac-11-arm64.zip',
'mac12': 'builds/webkit/%s/webkit-mac-12.zip',
mac12: 'builds/webkit/%s/webkit-mac-12.zip',
'mac12-arm64': 'builds/webkit/%s/webkit-mac-12-arm64.zip',
'mac13': 'builds/webkit/%s/webkit-mac-13.zip',
mac13: 'builds/webkit/%s/webkit-mac-13.zip',
'mac13-arm64': 'builds/webkit/%s/webkit-mac-13-arm64.zip',
'mac14': 'builds/webkit/%s/webkit-mac-14.zip',
mac14: 'builds/webkit/%s/webkit-mac-14.zip',
'mac14-arm64': 'builds/webkit/%s/webkit-mac-14-arm64.zip',
'mac15': 'builds/webkit/%s/webkit-mac-15.zip',
mac15: 'builds/webkit/%s/webkit-mac-15.zip',
'mac15-arm64': 'builds/webkit/%s/webkit-mac-15-arm64.zip',
'win64': 'builds/webkit/%s/webkit-win64.zip',
win64: 'builds/webkit/%s/webkit-win64.zip',
},
'ffmpeg': {
ffmpeg: {
'<unknown>': undefined,
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': 'builds/ffmpeg/%s/ffmpeg-linux.zip',
@@ -231,19 +282,19 @@ const DOWNLOAD_PATHS = {
'mac10.13': 'builds/ffmpeg/%s/ffmpeg-mac.zip',
'mac10.14': 'builds/ffmpeg/%s/ffmpeg-mac.zip',
'mac10.15': 'builds/ffmpeg/%s/ffmpeg-mac.zip',
'mac11': 'builds/ffmpeg/%s/ffmpeg-mac.zip',
mac11: 'builds/ffmpeg/%s/ffmpeg-mac.zip',
'mac11-arm64': 'builds/ffmpeg/%s/ffmpeg-mac-arm64.zip',
'mac12': 'builds/ffmpeg/%s/ffmpeg-mac.zip',
mac12: 'builds/ffmpeg/%s/ffmpeg-mac.zip',
'mac12-arm64': 'builds/ffmpeg/%s/ffmpeg-mac-arm64.zip',
'mac13': 'builds/ffmpeg/%s/ffmpeg-mac.zip',
mac13: 'builds/ffmpeg/%s/ffmpeg-mac.zip',
'mac13-arm64': 'builds/ffmpeg/%s/ffmpeg-mac-arm64.zip',
'mac14': 'builds/ffmpeg/%s/ffmpeg-mac.zip',
mac14: 'builds/ffmpeg/%s/ffmpeg-mac.zip',
'mac14-arm64': 'builds/ffmpeg/%s/ffmpeg-mac-arm64.zip',
'mac15': 'builds/ffmpeg/%s/ffmpeg-mac.zip',
mac15: 'builds/ffmpeg/%s/ffmpeg-mac.zip',
'mac15-arm64': 'builds/ffmpeg/%s/ffmpeg-mac-arm64.zip',
'win64': 'builds/ffmpeg/%s/ffmpeg-win64.zip',
win64: 'builds/ffmpeg/%s/ffmpeg-win64.zip',
},
'winldd': {
winldd: {
'<unknown>': undefined,
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': undefined,
@@ -260,19 +311,19 @@ const DOWNLOAD_PATHS = {
'mac10.13': undefined,
'mac10.14': undefined,
'mac10.15': undefined,
'mac11': undefined,
mac11: undefined,
'mac11-arm64': undefined,
'mac12': undefined,
mac12: undefined,
'mac12-arm64': undefined,
'mac13': undefined,
mac13: undefined,
'mac13-arm64': undefined,
'mac14': undefined,
mac14: undefined,
'mac14-arm64': undefined,
'mac15': undefined,
mac15: undefined,
'mac15-arm64': undefined,
'win64': 'builds/winldd/%s/winldd-win64.zip',
win64: 'builds/winldd/%s/winldd-win64.zip',
},
'android': {
android: {
'<unknown>': 'builds/android/%s/android.zip',
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': 'builds/android/%s/android.zip',
@@ -289,17 +340,17 @@ const DOWNLOAD_PATHS = {
'mac10.13': 'builds/android/%s/android.zip',
'mac10.14': 'builds/android/%s/android.zip',
'mac10.15': 'builds/android/%s/android.zip',
'mac11': 'builds/android/%s/android.zip',
mac11: 'builds/android/%s/android.zip',
'mac11-arm64': 'builds/android/%s/android.zip',
'mac12': 'builds/android/%s/android.zip',
mac12: 'builds/android/%s/android.zip',
'mac12-arm64': 'builds/android/%s/android.zip',
'mac13': 'builds/android/%s/android.zip',
mac13: 'builds/android/%s/android.zip',
'mac13-arm64': 'builds/android/%s/android.zip',
'mac14': 'builds/android/%s/android.zip',
mac14: 'builds/android/%s/android.zip',
'mac14-arm64': 'builds/android/%s/android.zip',
'mac15': 'builds/android/%s/android.zip',
mac15: 'builds/android/%s/android.zip',
'mac15-arm64': 'builds/android/%s/android.zip',
'win64': 'builds/android/%s/android.zip',
win64: 'builds/android/%s/android.zip',
},
} as const;
@@ -317,17 +368,37 @@ export class PlaywrightBinary extends AbstractBinary {
const nowDateISO = new Date().toISOString();
const buildDirs: BinaryItem[] = [];
for (const browserName of Object.keys(DOWNLOAD_PATHS)) {
if (browserName === 'chromium-headless-shell' || browserName === 'chromium-tip-of-tree-headless-shell') {
if (
browserName === 'chromium-headless-shell' ||
browserName === 'chromium-tip-of-tree-headless-shell'
) {
continue;
}
buildDirs.push({ name: `${browserName}/`, isDir: true, url: '', size: '-', date: nowDateISO });
buildDirs.push({
name: `${browserName}/`,
isDir: true,
url: '',
size: '-',
date: nowDateISO,
});
}
this.dirItems = {
'/': [{ name: 'builds/', isDir: true, url: '', size: '-', date: nowDateISO }],
'/': [
{
name: 'builds/',
isDir: true,
url: '',
size: '-',
date: nowDateISO,
},
],
'/builds/': buildDirs,
};
for (const browserName of Object.keys(DOWNLOAD_PATHS)) {
if (browserName === 'chromium-headless-shell' || browserName === 'chromium-tip-of-tree-headless-shell') {
if (
browserName === 'chromium-headless-shell' ||
browserName === 'chromium-tip-of-tree-headless-shell'
) {
continue;
}
this.dirItems[`/builds/${browserName}/`] = [];
@@ -338,11 +409,16 @@ export class PlaywrightBinary extends AbstractBinary {
.filter(version => version.match(/^(?:\d+\.\d+\.\d+)(?:-beta-\d+)?$/))
// select recently update 20 items
.slice(-20);
const browsers: { name: keyof typeof DOWNLOAD_PATHS; revision: string; browserVersion: string; revisionOverrides?: Record<string, string> }[] = [];
const browsers: {
name: keyof typeof DOWNLOAD_PATHS;
revision: string;
browserVersion: string;
revisionOverrides?: Record<string, string>;
}[] = [];
await Promise.all(
packageVersions.map(version =>
this.requestJSON(
`https://unpkg.com/playwright-core@${version}/browsers.json`,
`https://unpkg.com/playwright-core@${version}/browsers.json`
)
.then(data => {
// browsers: [
@@ -355,16 +431,22 @@ export class PlaywrightBinary extends AbstractBinary {
// },
// ]
browsers.push(...data.browsers);
return data;
})
.catch(err => {
/* c8 ignore next 2 */
this.logger.warn('[PlaywrightBinary.fetch:error] Playwright version %s browser data request failed: %s',
version, err);
}),
),
this.logger.warn(
'[PlaywrightBinary.fetch:error] Playwright version %s browser data request failed: %s',
version,
err
);
})
)
);
// if chromium-headless-shell not exists on browsers, copy chromium to chromium-headless-shell
if (!browsers.find(browser => browser.name === 'chromium-headless-shell')) {
if (
!browsers.some(browser => browser.name === 'chromium-headless-shell')
) {
const chromium = browsers.find(browser => browser.name === 'chromium');
// {
// "name": "chromium",
@@ -380,8 +462,14 @@ export class PlaywrightBinary extends AbstractBinary {
}
}
// if chromium-tip-of-tree-headless-shell not exists on browsers, copy chromium-tip-of-tree to chromium-tip-of-tree-headless-shell
if (!browsers.find(browser => browser.name === 'chromium-tip-of-tree-headless-shell')) {
const chromiumTipOfTree = browsers.find(browser => browser.name === 'chromium-tip-of-tree');
if (
!browsers.some(
browser => browser.name === 'chromium-tip-of-tree-headless-shell'
)
) {
const chromiumTipOfTree = browsers.find(
browser => browser.name === 'chromium-tip-of-tree'
);
if (chromiumTipOfTree) {
browsers.push({
...chromiumTipOfTree,
@@ -403,9 +491,10 @@ export class PlaywrightBinary extends AbstractBinary {
// https://playwright.azureedge.net/builds/chromium-tip-of-tree/1293/chromium-tip-of-tree-headless-shell-mac-arm64.zip
browserDirname = 'chromium-tip-of-tree';
}
for (const [ platform, remotePath ] of Object.entries(downloadPaths)) {
for (const [platform, remotePath] of Object.entries(downloadPaths)) {
if (typeof remotePath !== 'string') continue;
const revision = browser.revisionOverrides?.[platform] ?? browser.revision;
const revision =
browser.revisionOverrides?.[platform] ?? browser.revision;
const itemDate = browser.browserVersion || revision;
const url = DOWNLOAD_HOST + util.format(remotePath, revision);
const name = path.basename(remotePath);
@@ -420,8 +509,14 @@ export class PlaywrightBinary extends AbstractBinary {
});
this.dirItems[dir] = [];
}
if (!this.dirItems[dir].find(item => item.name === name)) {
this.dirItems[dir].push({ name, isDir: false, url, size: '-', date: itemDate });
if (!this.dirItems[dir].some(item => item.name === name)) {
this.dirItems[dir].push({
name,
isDir: false,
url,
size: '-',
date: itemDate,
});
}
}
}

View File

@@ -1,8 +1,15 @@
import path from 'node:path';
import { SingletonProto } from '@eggjs/tegg';
import { BinaryType } from '../../enum/Binary';
import binaries, { BinaryName } from '../../../../config/binaries';
import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './AbstractBinary';
import { SingletonProto } from 'egg';
import binaries, { type BinaryName } from '../../../../config/binaries.ts';
import { BinaryType } from '../../enum/Binary.ts';
import {
AbstractBinary,
BinaryAdapter,
type BinaryItem,
type FetchResult,
} from './AbstractBinary.ts';
@SingletonProto()
@BinaryAdapter(BinaryType.Prisma)
@@ -30,15 +37,17 @@ export class PrismaBinary extends AbstractBinary {
const commitIdMap: Record<string, boolean> = {};
// https://list-binaries.prisma-orm.workers.dev/?delimiter=/&prefix=all_commits/61023c35d2c8762f66f09bc4183d2f630b541d08/
for (const version in data.versions) {
const major = parseInt(version.split('.', 1)[0]);
const major = Number.parseInt(version.split('.', 1)[0]);
// need >= 3.0.0
if (major < 3) continue;
const date = data.time[version];
const pkg = data.versions[version];
// https://registry.npmjs.com/@prisma/engines/4.14.1
// https://registry.npmjs.com/@prisma/engines/5.7.0 should read from dependencies
const enginesVersion = pkg.devDependencies?.['@prisma/engines-version']
|| pkg.dependencies?.['@prisma/engines-version'] || '';
const enginesVersion =
pkg.devDependencies?.['@prisma/engines-version'] ||
pkg.dependencies?.['@prisma/engines-version'] ||
'';
// "@prisma/engines-version": "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c"
// "@prisma/engines-version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9"
const matched = /\.(\w{30,})$/.exec(enginesVersion);
@@ -56,19 +65,23 @@ export class PrismaBinary extends AbstractBinary {
}
}
async fetch(dir: string, binaryName: BinaryName): Promise<FetchResult | undefined> {
async fetch(
dir: string,
binaryName: BinaryName
): Promise<FetchResult | undefined> {
const existsItems = this.dirItems[dir];
if (existsItems) {
return { items: existsItems, nextParams: null };
}
// /foo/ => foo/
const binaryConfig = binaries[binaryName];
const subDir = dir.substring(1);
const subDir = dir.slice(1);
const url = `${binaryConfig.distUrl}?delimiter=/&prefix=${encodeURIComponent(subDir)}`;
const result = await this.requestJSON(url);
return { items: this.#parseItems(result), nextParams: null };
}
// oxlint-disable-next-line typescript-eslint/no-explicit-any
#parseItems(result: any): BinaryItem[] {
const items: BinaryItem[] = [];
// objects": [

View File

@@ -1,6 +1,17 @@
import { SingletonProto } from '@eggjs/tegg';
import { BinaryType } from '../../enum/Binary';
import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './AbstractBinary';
import { SingletonProto } from 'egg';
import { XMLParser } from 'fast-xml-parser';
import { BinaryType } from '../../enum/Binary.ts';
import {
AbstractBinary,
BinaryAdapter,
type BinaryItem,
type FetchResult,
} from './AbstractBinary.ts';
export const platforms = ['Linux_x64', 'Mac', 'Mac_Arm', 'Win', 'Win_x64'];
const MAX_DEPTH = 1;
@SingletonProto()
@BinaryAdapter(BinaryType.Puppeteer)
@@ -13,72 +24,28 @@ export class PuppeteerBinary extends AbstractBinary {
this.dirItems = undefined;
}
async fetch(dir: string): Promise<FetchResult | undefined> {
async fetch(
dir: string,
_binaryName: string,
lastData?: Record<string, unknown>
): Promise<FetchResult | undefined> {
if (!this.dirItems) {
const pkgUrl = 'https://registry.npmjs.com/puppeteer';
const data = await this.requestJSON(pkgUrl);
const s3Url = 'https://chromium-browser-snapshots.storage.googleapis.com';
const chromiumRevisions = new Map<string, string>();
this.dirItems = {};
this.dirItems['/'] = [];
const chromiumRevisions = new Map<string, string>();
for (const version in data.versions) {
// find chromium versions
const pkg = data.versions[version];
const revision = pkg.puppeteer?.chromium_revision ? String(pkg.puppeteer.chromium_revision) : '';
if (revision && !chromiumRevisions.has(revision)) {
chromiumRevisions.set(revision, data.time[version]);
}
}
// https://unpkg.com/puppeteer@5.1.0/lib/cjs/revisions.js
// https://unpkg.com/puppeteer@latest/lib/cjs/puppeteer/revisions.js
// exports.PUPPETEER_REVISIONS = {
// chromium: '768783',
// firefox: 'latest',
// };
const unpkgURL = 'https://unpkg.com/puppeteer-core@latest/lib/cjs/puppeteer/revisions.js';
const text = await this.requestXml(unpkgURL);
const m = /chromium:\s+\'(\d+)\'\,/.exec(text);
if (m && !chromiumRevisions.has(m[1])) {
chromiumRevisions.set(m[1], new Date().toISOString());
}
// download LAST_CHANGE
// https://github.com/chaopeng/chromium-downloader/blob/master/get-chromium#L28
const LAST_CHANGE_URL = 'https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2FLAST_CHANGE?alt=media';
const lastRevision = await this.requestXml(LAST_CHANGE_URL);
if (lastRevision) {
chromiumRevisions.set(lastRevision, new Date().toISOString());
}
// old versions
// v5.0.0
chromiumRevisions.set('756035', data.time['5.0.0']);
// v5.2.0
chromiumRevisions.set('768783', data.time['5.2.0']);
// v5.2.1
chromiumRevisions.set('782078', data.time['5.2.1']);
// v5.3.0
chromiumRevisions.set('800071', data.time['5.3.0']);
// v5.4.0
chromiumRevisions.set('809590', data.time['5.4.0']);
// v5.5.0
chromiumRevisions.set('818858', data.time['5.5.0']);
// v6.0.0
chromiumRevisions.set('843427', data.time['6.0.0']);
// "7.0.0"
chromiumRevisions.set('848005', data.time['7.0.0']);
// https://github.com/puppeteer/puppeteer/blob/v8.0.0/src/revisions.ts#L23
// "8.0.0":"2021-02-26T08:36:50.107Z"
chromiumRevisions.set('856583', data.time['8.0.0']);
// "9.0.0":"2021-04-21T11:27:32.513Z"
chromiumRevisions.set('869685', data.time['9.0.0']);
// "10.0.0":"2021-05-31T12:42:27.486Z"
chromiumRevisions.set('884014', data.time['10.0.0']);
// "11.0.0":"2021-11-03T09:29:12.751Z"
chromiumRevisions.set('901912', data.time['11.0.0']);
const platforms = [ 'Linux_x64', 'Mac', 'Mac_Arm', 'Win', 'Win_x64' ];
for (const platform of platforms) {
const revision = lastData?.[platform] as string;
if (!revision) {
// 丢弃库中历史不带 lastData 的任务,防止遍历任务过多
this.logger.info(
'drop puppeteer task if has no last data for platform %s, lastPlatform',
platform,
lastData
);
return;
}
let marker = revision ? `${platform}/${revision}/REVISIONS` : undefined;
this.dirItems['/'].push({
name: `${platform}/`,
date: new Date().toISOString(),
@@ -87,8 +54,35 @@ export class PuppeteerBinary extends AbstractBinary {
url: '',
});
this.dirItems[`/${platform}/`] = [];
let i = 0;
do {
let requestUrl = `${s3Url}?prefix=${platform}&max-keys=100`;
if (marker) {
requestUrl += `&marker=${marker}`;
}
const xml = await this.requestXml(requestUrl);
const parser = new XMLParser();
const obj = parser.parse(xml);
if (
obj.ListBucketResult.IsTruncated === true &&
obj.ListBucketResult.NextMarker
) {
marker = obj.ListBucketResult.NextMarker;
} else {
marker = undefined;
}
for (const content of obj.ListBucketResult.Contents) {
// /Linux_x64/1041455/REVISIONS
if (content.Key.endsWith('/REVISIONS')) {
const revision = content.Key.split('/')[1].trim();
chromiumRevisions.set(revision, content.LastModified);
}
}
// 最多遍历 100 次防止内存爆炸,下次同步任务会继续
} while (i++ < MAX_DEPTH && marker !== undefined);
}
for (const [ revision, date ] of chromiumRevisions.entries()) {
for (const [revision, date] of chromiumRevisions.entries()) {
// https://github.com/puppeteer/puppeteer/blob/eebf452d38b79bb2ea1a1ba84c3d2ea6f2f9f899/src/node/BrowserFetcher.ts#L40
// chrome: {
// linux: '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip',
@@ -113,7 +107,7 @@ export class PuppeteerBinary extends AbstractBinary {
size: '-',
isDir: false,
url: `https://storage.googleapis.com/chromium-browser-snapshots/${platform}/${revision}/${name}`,
ignoreDownloadStatuses: [ 404 ],
ignoreDownloadStatuses: [404],
},
];
}
@@ -124,15 +118,14 @@ export class PuppeteerBinary extends AbstractBinary {
}
// https://github.com/puppeteer/puppeteer/blob/eebf452d38b79bb2ea1a1ba84c3d2ea6f2f9f899/src/node/BrowserFetcher.ts#L72
private archiveName(
platform: string,
revision: string,
): string {
private archiveName(platform: string, revision: string): string {
if (platform === 'Linux_x64') return 'chrome-linux';
if (platform === 'Mac' || platform === 'Mac_Arm') return 'chrome-mac';
if (platform === 'Win' || platform === 'Win_x64') {
// Windows archive name changed at r591479.
return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32';
return Number.parseInt(revision, 10) > 591_479
? 'chrome-win'
: 'chrome-win32';
}
return '';
}

View File

@@ -1,6 +1,12 @@
import { SingletonProto } from '@eggjs/tegg';
import { BinaryType } from '../../enum/Binary';
import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './AbstractBinary';
import { SingletonProto } from 'egg';
import { BinaryType } from '../../enum/Binary.ts';
import {
AbstractBinary,
BinaryAdapter,
type BinaryItem,
type FetchResult,
} from './AbstractBinary.ts';
@SingletonProto()
@BinaryAdapter(BinaryType.Sqlcipher)
@@ -16,7 +22,8 @@ export class SqlcipherBinary extends AbstractBinary {
} = {
'/': [],
};
const s3Url = 'https://journeyapps-node-binary.s3.amazonaws.com/@journeyapps/sqlcipher';
const s3Url =
'https://journeyapps-node-binary.s3.amazonaws.com/@journeyapps/sqlcipher';
const pkgUrl = 'https://registry.npmjs.com/@journeyapps/sqlcipher';
const data = await this.requestJSON(pkgUrl);
// https://github.com/journeyapps/node-sqlcipher/blob/master/.circleci/config.yml#L407
@@ -44,11 +51,12 @@ export class SqlcipherBinary extends AbstractBinary {
'win32-ia32',
];
for (const version in data.versions) {
const major = parseInt(version.split('.', 1)[0]);
const major = Number.parseInt(version.split('.', 1)[0]);
if (major < 5) continue;
// >= 5.0.0
const pkgVersion = data.versions[version];
const napiVersions = pkgVersion.binary && pkgVersion.binary.napi_versions || [];
const napiVersions =
(pkgVersion.binary && pkgVersion.binary.napi_versions) || [];
const date = data.time[version];
dirItems['/'].push({
name: `v${version}/`,
@@ -74,7 +82,7 @@ export class SqlcipherBinary extends AbstractBinary {
size: '-',
isDir: false,
url: `${s3Url}/v${version}/${name}`,
ignoreDownloadStatuses: [ 404, 403 ],
ignoreDownloadStatuses: [404, 403],
});
}
}

View File

@@ -1,32 +1,38 @@
import {
ImplDecorator,
Inject,
QualifierImplDecoratorUtil,
} from '@eggjs/tegg';
import { RegistryType } from '../../../common/enum/Registry';
import { Registry } from '../../../core/entity/Registry';
import {
EggHttpClient,
EggLogger,
type ImplDecorator,
Logger,
HttpClient,
} from 'egg';
import type { RegistryType } from '../../../common/enum/Registry.ts';
import type { Registry } from '../../../core/entity/Registry.ts';
export const CHANGE_STREAM_ATTRIBUTE = 'CHANGE_STREAM_ATTRIBUTE';
export type ChangesStreamChange = {
export interface ChangesStreamChange {
seq: string;
fullname: string;
};
}
export abstract class AbstractChangeStream {
@Inject()
protected logger: EggLogger;
protected logger: Logger;
@Inject()
protected httpclient: EggHttpClient;
protected httpClient: HttpClient;
abstract getInitialSince(registry: Registry): Promise<string>;
abstract fetchChanges(registry: Registry, since: string): AsyncGenerator<ChangesStreamChange>;
abstract fetchChanges(
registry: Registry,
since: string
): AsyncGenerator<ChangesStreamChange>;
getChangesStreamUrl(registry: Registry, since: string, limit?: number): string {
getChangesStreamUrl(
registry: Registry,
since: string,
limit?: number
): string {
const url = new URL(registry.changeStream);
url.searchParams.set('since', since);
if (limit) {
@@ -36,5 +42,10 @@ export abstract class AbstractChangeStream {
}
}
export const RegistryChangesStream: ImplDecorator<AbstractChangeStream, typeof RegistryType> =
QualifierImplDecoratorUtil.generatorDecorator(AbstractChangeStream, CHANGE_STREAM_ATTRIBUTE);
export const RegistryChangesStream: ImplDecorator<
AbstractChangeStream,
typeof RegistryType
> = QualifierImplDecoratorUtil.generatorDecorator(
AbstractChangeStream,
CHANGE_STREAM_ATTRIBUTE
);

View File

@@ -1,35 +1,43 @@
import { SingletonProto } from '@eggjs/tegg';
import { RegistryType } from '../../../common/enum/Registry';
import { Registry } from '../../../core/entity/Registry';
import { E500 } from 'egg-errors';
import { AbstractChangeStream, RegistryChangesStream } from './AbstractChangesStream';
import { SingletonProto } from 'egg';
import { E500 } from 'egg/errors';
import { RegistryType } from '../../../common/enum/Registry.ts';
import type { Registry } from '../../../core/entity/Registry.ts';
import {
AbstractChangeStream,
RegistryChangesStream,
} from './AbstractChangesStream.ts';
@SingletonProto()
@RegistryChangesStream(RegistryType.Cnpmcore)
export class CnpmcoreChangesStream extends AbstractChangeStream {
async getInitialSince(registry: Registry): Promise<string> {
const db = (new URL(registry.changeStream)).origin;
const { status, data } = await this.httpclient.request(db, {
const db = new URL(registry.changeStream).origin;
const { status, data } = await this.httpClient.request(db, {
followRedirect: true,
timeout: 10000,
timeout: 10_000,
dataType: 'json',
});
if (!data.update_seq) {
throw new E500(`get getInitialSince failed: ${data.update_seq}`);
}
const since = String(data.update_seq - 10);
this.logger.warn('[NpmChangesStream.getInitialSince:firstSeq] GET %s status: %s, data: %j, since: %s',
registry.name, status, data, since);
this.logger.warn(
'[NpmChangesStream.getInitialSince:firstSeq] GET %s status: %s, data: %j, since: %s',
registry.name,
status,
data,
since
);
return since;
}
async* fetchChanges(registry: Registry, since: string) {
async *fetchChanges(registry: Registry, since: string) {
const db = this.getChangesStreamUrl(registry, since);
// json mode
const { data } = await this.httpclient.request(db, {
const { data } = await this.httpClient.request(db, {
followRedirect: true,
timeout: 30000,
timeout: 30_000,
dataType: 'json',
gzip: true,
});

View File

@@ -1,50 +1,61 @@
import { SingletonProto } from '@eggjs/tegg';
import { RegistryType } from '../../../common/enum/Registry';
import { Registry } from '../../../core/entity/Registry';
import { E500 } from 'egg-errors';
import { AbstractChangeStream, RegistryChangesStream } from './AbstractChangesStream';
import { SingletonProto } from 'egg';
import { E500 } from 'egg/errors';
const MAX_LIMIT = 10000;
import { RegistryType } from '../../../common/enum/Registry.ts';
import type { Registry } from '../../../core/entity/Registry.ts';
import {
AbstractChangeStream,
RegistryChangesStream,
} from './AbstractChangesStream.ts';
type FetchResults = {
const MAX_LIMIT = 10_000;
interface FetchResults {
results: {
seq: number;
type: string;
id: string;
changes: Record<string, string>[];
gmt_modified: Date,
gmt_modified: Date;
}[];
};
}
@SingletonProto()
@RegistryChangesStream(RegistryType.Cnpmjsorg)
export class CnpmjsorgChangesStream extends AbstractChangeStream {
// cnpmjsorg 未实现 update_seq 字段
// 默认返回当前时间戳字符串
async getInitialSince(registry: Registry): Promise<string> {
const since = String((new Date()).getTime());
this.logger.warn(`[CnpmjsorgChangesStream.getInitialSince] since: ${since}, skip query ${registry.changeStream}`);
const since = String(Date.now());
this.logger.warn(
`[CnpmjsorgChangesStream.getInitialSince] since: ${since}, skip query ${registry.changeStream}`
);
return since;
}
private async tryFetch(registry: Registry, since: string, limit = 1000): Promise<{ data: FetchResults }> {
private async tryFetch(
registry: Registry,
since: string,
limit = 1000
): Promise<{ data: FetchResults }> {
if (limit > MAX_LIMIT) {
throw new E500(`limit too large, current since: ${since}, limit: ${limit}`);
throw new E500(
`limit too large, current since: ${since}, limit: ${limit}`
);
}
const db = this.getChangesStreamUrl(registry, since, limit);
// json mode
const res = await this.httpclient.request<FetchResults>(db, {
const res = await this.httpClient.request<FetchResults>(db, {
followRedirect: true,
timeout: 30000,
timeout: 30_000,
dataType: 'json',
gzip: true,
});
const { results = [] } = res.data;
if (results?.length >= limit) {
const [ first ] = results;
const [first] = results;
const last = results[results.length - 1];
if (first.gmt_modified === last.gmt_modified) {
if (first.gmt_modified === last?.gmt_modified) {
return await this.tryFetch(registry, since, limit + 1000);
}
}
@@ -52,7 +63,7 @@ export class CnpmjsorgChangesStream extends AbstractChangeStream {
return res;
}
async* fetchChanges(registry: Registry, since: string) {
async *fetchChanges(registry: Registry, since: string) {
// ref: https://github.com/cnpm/cnpmjs.org/pull/1734
// 由于 cnpmjsorg 无法计算准确的 seq
// since 是一个时间戳,需要确保一次返回的结果中首尾两个 gmtModified 不相等
@@ -60,7 +71,7 @@ export class CnpmjsorgChangesStream extends AbstractChangeStream {
if (data.results?.length > 0) {
for (const change of data.results) {
const seq = new Date(change.gmt_modified).getTime() + '';
const seq = `${new Date(change.gmt_modified).getTime()}`;
const fullname = change.id;
if (seq && fullname && seq !== since) {
const change = {

View File

@@ -1,55 +1,84 @@
import { SingletonProto } from '@eggjs/tegg';
import { E500 } from 'egg-errors';
import { RegistryType } from '../../../common/enum/Registry';
import { Registry } from '../../../core/entity/Registry';
import { AbstractChangeStream, ChangesStreamChange, RegistryChangesStream } from './AbstractChangesStream';
import { SingletonProto } from 'egg';
import { E500 } from 'egg/errors';
import { RegistryType } from '../../../common/enum/Registry.ts';
import type { Registry } from '../../../core/entity/Registry.ts';
import {
AbstractChangeStream,
RegistryChangesStream,
type ChangesStreamChange,
} from './AbstractChangesStream.ts';
@SingletonProto()
@RegistryChangesStream(RegistryType.Npm)
export class NpmChangesStream extends AbstractChangeStream {
async getInitialSince(registry: Registry): Promise<string> {
const db = (new URL(registry.changeStream)).origin;
const { status, data } = await this.httpclient.request(db, {
const db = new URL(registry.changeStream).origin;
const { status, data } = await this.httpClient.request(db, {
followRedirect: true,
timeout: 10000,
timeout: 10_000,
dataType: 'json',
headers: {
'npm-replication-opt-in': 'true',
},
});
const since = String(data.update_seq - 10);
if (!data.update_seq) {
throw new E500(`get getInitialSince failed: ${data.update_seq}`);
}
this.logger.warn('[NpmChangesStream.getInitialSince] GET %s status: %s, data: %j, since: %s',
registry.name, registry.changeStream, status, data, since);
this.logger.warn(
'[NpmChangesStream.getInitialSince] GET %s status: %s, data: %j, since: %s',
registry.name,
registry.changeStream,
status,
data,
since
);
return since;
}
async* fetchChanges(registry: Registry, since: string) {
async *fetchChanges(
registry: Registry,
since: string
): AsyncGenerator<ChangesStreamChange> {
// https://github.com/orgs/community/discussions/152515
const db = this.getChangesStreamUrl(registry, since);
const { res } = await this.httpclient.request(db, {
streaming: true,
timeout: 60000,
const { data, headers } = await this.httpClient.request(db, {
timeout: 60_000,
headers: {
'npm-replication-opt-in': 'true',
},
dataType: 'json',
gzip: true,
});
const count = data.results?.length;
const last_seq = data.last_seq;
this.logger.info(
'[NpmChangesStream.fetchChanges] %s, count: %s, last_seq: %s, headers: %j',
db,
count,
last_seq,
headers
);
let buf = '';
for await (const chunk of res) {
const text = chunk.toString();
const lines = text.split('\n');
for (const line of lines) {
const content = buf + line;
const match = /"seq":(\d+),"id":"([^"]+)"/g.exec(content);
const seq = match?.[1];
const fullname = match?.[2];
if (seq && fullname) {
buf = '';
const change: ChangesStreamChange = { fullname, seq };
if (data.results?.length > 0) {
for (const change of data.results) {
// {
// seq: 2495018,
// id: 'ng-create-all-project',
// changes: [ { rev: '3-be3a014aab8e379ba28a28adb8e10142' }, [length]: 1 ],
// deleted: true
// },
const seq = String(change.seq);
const fullname = change.id;
if (seq && fullname && seq !== since) {
const change = {
fullname,
seq,
};
yield change;
} else {
buf += line;
}
}
}
}
}

View File

@@ -1,27 +1,34 @@
import { performance } from 'node:perf_hooks';
import { Advice, AdviceContext, IAdvice } from '@eggjs/tegg/aop';
import { Inject } from '@eggjs/tegg';
import { EggLogger } from 'egg';
import { Advice, type AdviceContext, type IAdvice } from 'egg/aop';
import { Inject, Logger } from 'egg';
const START = Symbol('AsyncTimer#start');
const SUCCEED = Symbol('AsyncTimer#succeed');
// auto print async function call performance timer log into logger
@Advice()
export class AsyncTimer implements IAdvice {
@Inject()
private readonly logger: EggLogger;
private start: number;
private succeed = true;
private readonly logger: Logger;
async beforeCall() {
this.start = performance.now();
async beforeCall(ctx: AdviceContext) {
ctx.set(START, performance.now());
ctx.set(SUCCEED, true);
}
async afterThrow() {
this.succeed = false;
async afterThrow(ctx: AdviceContext) {
ctx.set(SUCCEED, false);
}
async afterFinally(ctx: AdviceContext) {
const ms = Math.floor((performance.now() - this.start) * 1000) / 1000;
this.logger.info('[%s] [%s:%s|%s]',
ms, ctx.that.constructor.name, ctx.method, this.succeed ? 'T' : 'F');
const ms = Math.floor((performance.now() - ctx.get(START)) * 1000) / 1000;
this.logger.info(
'[%s] [%s:%s|%s]',
ms,
ctx.that.constructor.name,
ctx.method,
ctx.get(SUCCEED) ? 'T' : 'F'
);
}
}

View File

@@ -1,5 +1,5 @@
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import customParseFormat from 'dayjs/plugin/customParseFormat.js';
dayjs.extend(customParseFormat);
export default dayjs;

View File

@@ -3,6 +3,7 @@ export enum BinaryType {
Bucket = 'bucket',
Cypress = 'cypress',
Electron = 'electron',
Firefox = 'firefox',
GitHub = 'github',
Imagemin = 'imagemin',
Node = 'node',

4
app/common/enum/Total.ts Normal file
View File

@@ -0,0 +1,4 @@
export enum TotalType {
PackageCount = 'packageCount',
PackageVersionCount = 'packageVersionCount',
}

View File

@@ -1,11 +1,11 @@
export enum LoginResultCode {
UserNotFound,
Success,
Fail,
UserNotFound = 0,
Success = 1,
Fail = 2,
}
export enum WanStatusCode {
UserNotFound,
Unbound,
Bound,
UserNotFound = 0,
Unbound = 1,
Bound = 2,
}

View File

@@ -2,5 +2,6 @@
"name": "cnpmcore-common",
"eggModule": {
"name": "cnpmcoreCommon"
}
},
"type": "module"
}

View File

@@ -1,8 +1,9 @@
import { Readable } from 'node:stream';
import { IncomingHttpHeaders } from 'node:http';
import { EggContext } from '@eggjs/tegg';
import { estypes } from '@elastic/elasticsearch';
import { CnpmcoreConfig } from '../port/config';
import type { Readable } from 'node:stream';
import type { IncomingHttpHeaders } from 'node:http';
import type { Context } from 'egg';
import type { estypes } from '@elastic/elasticsearch';
import type { CnpmcoreConfig } from '../port/config.ts';
export interface UploadResult {
key: string;
@@ -19,8 +20,8 @@ export interface UploadOptions {
export interface AppendOptions {
key: string;
position?: string,
headers?: IncomingHttpHeaders,
position?: string;
headers?: IncomingHttpHeaders;
}
export interface DownloadOptions {
@@ -40,7 +41,11 @@ export interface NFSClient {
createDownloadStream(key: string): Promise<Readable | undefined>;
download(key: string, filepath: string, options: DownloadOptions): Promise<void>;
download(
key: string,
filepath: string,
options: DownloadOptions
): Promise<void>;
url?(key: string): string;
}
@@ -52,6 +57,7 @@ export interface QueueAdapter {
}
export interface SearchAdapter {
// oxlint-disable-next-line typescript-eslint/no-explicit-any
search<T>(query: any): Promise<estypes.SearchHitsMetadata<T>>;
upsert<T>(id: string, document: T): Promise<string>;
delete(id: string): Promise<string>;
@@ -67,14 +73,11 @@ export interface userResult {
email: string;
}
export interface AuthClient {
getAuthUrl(ctx: EggContext): Promise<AuthUrlResult>;
getAuthUrl(ctx: Context): Promise<AuthUrlResult>;
ensureCurrentUser(): Promise<userResult | null>;
}
declare module 'egg' {
// eslint-disable-next-line
// @ts-ignore
// avoid TS2310 Type 'EggAppConfig' recursively references itself as a base type.
interface EggAppConfig {
cnpmcore: CnpmcoreConfig;
}

View File

@@ -1,5 +1,5 @@
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import { Entity, type EntityData } from './Entity.ts';
import { EntityUtil, type EasyData } from '../util/EntityUtil.ts';
interface BinaryData extends EntityData {
binaryId: string;
@@ -21,7 +21,7 @@ export class Binary extends Entity {
isDir: boolean;
size: number;
date: string;
sourceUrl?: string;
sourceUrl: string;
ignoreDownloadStatuses?: number[];
constructor(data: BinaryData) {

View File

@@ -12,11 +12,11 @@ export class BugVersion {
this.data = data;
}
listAllPackagesHasBugs(): Array<string> {
listAllPackagesHasBugs(): string[] {
return Object.keys(this.data);
}
listBugVersions(pkgName: string): Array<string> {
listBugVersions(pkgName: string): string[] {
const bugVersionPackage = this.data[pkgName];
if (!bugVersionPackage) {
return [];
@@ -31,18 +31,24 @@ export class BugVersion {
}
// TODO manifest typing
// oxlint-disable-next-line typescript-eslint/no-explicit-any
fixManifest(bugVersionManifest: any, fixVersionManifest: any): any {
// If the tarball is same, manifest has fixed.
if (bugVersionManifest.dist.tarball === fixVersionManifest.dist.tarball) {
return;
}
const advice = this.fixVersion(bugVersionManifest.name, bugVersionManifest.version);
const advice = this.fixVersion(
bugVersionManifest.name,
bugVersionManifest.version
);
if (!advice) {
return;
}
const newManifest = JSON.parse(JSON.stringify(fixVersionManifest));
const newManifest = structuredClone(fixVersionManifest);
const hotfixDeprecated = `[WARNING] Use ${advice.version} instead of ${bugVersionManifest.version}, reason: ${advice.reason}`;
newManifest.deprecated = bugVersionManifest.deprecated ? `${bugVersionManifest.deprecated} (${hotfixDeprecated})` : hotfixDeprecated;
newManifest.deprecated = bugVersionManifest.deprecated
? `${bugVersionManifest.deprecated} (${hotfixDeprecated})`
: hotfixDeprecated;
// don't change version
newManifest.version = bugVersionManifest.version;
return newManifest;

View File

@@ -1,10 +1,11 @@
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import { Entity, type EntityData } from './Entity.ts';
import { EntityUtil, type EasyData } from '../util/EntityUtil.ts';
interface ChangeData extends EntityData {
changeId: string;
type: string;
targetName: string;
// oxlint-disable-next-line typescript-eslint/no-explicit-any
data: any;
}
@@ -12,6 +13,7 @@ export class Change extends Entity {
changeId: string;
type: string;
targetName: string;
// oxlint-disable-next-line typescript-eslint/no-explicit-any
data: any;
constructor(data: ChangeData) {

View File

@@ -1,5 +1,5 @@
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import { Entity, type EntityData } from './Entity.ts';
import { EntityUtil, type EasyData } from '../util/EntityUtil.ts';
interface DistData extends EntityData {
distId: string;

View File

@@ -1,9 +1,13 @@
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import { HookType } from '../../common/enum/Hook';
import crypto from 'node:crypto';
export type CreateHookData = Omit<EasyData<HookData, 'hookId'>, 'enable' | 'latestTaskId'>;
import { Entity, type EntityData } from './Entity.ts';
import { EntityUtil, type EasyData } from '../util/EntityUtil.ts';
import type { HookType } from '../../common/enum/Hook.ts';
export type CreateHookData = Omit<
EasyData<HookData, 'hookId'>,
'enable' | 'latestTaskId'
>;
export interface HookData extends EntityData {
hookId: string;
@@ -39,10 +43,11 @@ export class Hook extends Entity {
}
static create(data: CreateHookData): Hook {
const hookData: EasyData<HookData, 'hookId'> = Object.assign({}, data, {
const hookData: EasyData<HookData, 'hookId'> = {
...data,
enable: true,
latestTaskId: undefined,
});
};
const newData = EntityUtil.defaultData(hookData, 'hookId');
return new Hook(newData);
}
@@ -50,7 +55,8 @@ export class Hook extends Entity {
// payload 可能会特别大,如果做多次 stringify 浪费太多 cpu
signPayload(payload: object) {
const payloadStr = JSON.stringify(payload);
const digest = crypto.createHmac('sha256', this.secret)
const digest = crypto
.createHmac('sha256', this.secret)
.update(JSON.stringify(payload))
.digest('hex');
return {

View File

@@ -1,4 +1,4 @@
import { HookEventType } from '../../common/enum/Hook';
import { HookEventType } from '../../common/enum/Hook.ts';
export interface PublishChangePayload {
'dist-tag'?: string;

View File

@@ -1,7 +1,7 @@
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import { Dist } from './Dist';
import { getFullname } from '../../common/PackageUtil';
import { Entity, type EntityData } from './Entity.ts';
import { EntityUtil, type EasyData } from '../util/EntityUtil.ts';
import { Dist } from './Dist.ts';
import { getFullname } from '../../common/PackageUtil.ts';
interface PackageData extends EntityData {
scope: string;
@@ -22,6 +22,13 @@ export enum DIST_NAMES {
ABBREVIATED_MANIFESTS = 'abbreviated_manifests.json',
}
export function isPkgManifest(fileType: DIST_NAMES) {
return (
fileType === DIST_NAMES.FULL_MANIFESTS ||
fileType === DIST_NAMES.ABBREVIATED_MANIFESTS
);
}
interface FileInfo {
size: number;
shasum: string;

View File

@@ -1,5 +1,5 @@
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import { Entity, type EntityData } from './Entity.ts';
import { EntityUtil, type EasyData } from '../util/EntityUtil.ts';
interface PackageTagData extends EntityData {
packageId: string;

View File

@@ -1,7 +1,7 @@
import { Dist } from './Dist';
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import { PaddingSemVer } from './PaddingSemVer';
import type { Dist } from './Dist.ts';
import { Entity, type EntityData } from './Entity.ts';
import { EntityUtil, type EasyData } from '../util/EntityUtil.ts';
import { PaddingSemVer } from './PaddingSemVer.ts';
interface PackageVersionData extends EntityData {
packageId: string;
@@ -48,7 +48,9 @@ export class PackageVersion extends Entity {
}
}
static create(data: EasyData<PackageVersionData, 'packageVersionId'>): PackageVersion {
static create(
data: EasyData<PackageVersionData, 'packageVersionId'>
): PackageVersion {
const newData = EntityUtil.defaultData(data, 'packageVersionId');
return new PackageVersion(newData);
}

View File

@@ -1,5 +1,5 @@
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import { Entity, type EntityData } from './Entity.ts';
import { EntityUtil, type EasyData } from '../util/EntityUtil.ts';
interface PackageVersionBlockData extends EntityData {
packageVersionBlockId: string;
@@ -22,7 +22,9 @@ export class PackageVersionBlock extends Entity {
this.reason = data.reason;
}
static create(data: EasyData<PackageVersionBlockData, 'packageVersionBlockId'>): PackageVersionBlock {
static create(
data: EasyData<PackageVersionBlockData, 'packageVersionBlockId'>
): PackageVersionBlock {
const newData = EntityUtil.defaultData(data, 'packageVersionBlockId');
return new PackageVersionBlock(newData);
}

View File

@@ -1,6 +1,6 @@
import { Dist } from './Dist';
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import { Entity, type EntityData } from './Entity.ts';
import { EntityUtil, type EasyData } from '../util/EntityUtil.ts';
import type { Dist } from './Dist.ts';
interface PackageVersionFileData extends EntityData {
packageVersionFileId: string;
@@ -33,10 +33,14 @@ export class PackageVersionFile extends Entity {
}
get path() {
return this.directory === '/' ? `/${this.name}` : `${this.directory}/${this.name}`;
return this.directory === '/'
? `/${this.name}`
: `${this.directory}/${this.name}`;
}
static create(data: EasyData<PackageVersionFileData, 'packageVersionFileId'>): PackageVersionFile {
static create(
data: EasyData<PackageVersionFileData, 'packageVersionFileId'>
): PackageVersionFile {
const newData = EntityUtil.defaultData(data, 'packageVersionFileId');
return new PackageVersionFile(newData);
}

View File

@@ -1,10 +1,11 @@
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import { Entity, type EntityData } from './Entity.ts';
import { EntityUtil, type EasyData } from '../util/EntityUtil.ts';
interface PackageVersionManifestData extends EntityData {
packageId: string;
packageVersionId: string;
packageVersionManifestId: string;
// oxlint-disable-next-line typescript-eslint/no-explicit-any
manifest: any;
}
@@ -12,6 +13,7 @@ export class PackageVersionManifest extends Entity {
packageId: string;
packageVersionId: string;
packageVersionManifestId: string;
// oxlint-disable-next-line typescript-eslint/no-explicit-any
manifest: any;
constructor(data: PackageVersionManifestData) {
@@ -22,7 +24,9 @@ export class PackageVersionManifest extends Entity {
this.manifest = data.manifest;
}
static create(data: EasyData<PackageVersionManifestData, 'packageVersionManifestId'>): PackageVersionManifest {
static create(
data: EasyData<PackageVersionManifestData, 'packageVersionManifestId'>
): PackageVersionManifest {
const newData = EntityUtil.defaultData(data, 'packageVersionManifestId');
return new PackageVersionManifest(newData);
}

View File

@@ -14,9 +14,10 @@ export class PaddingSemVer {
return;
}
this.semver = new SemVer(semver);
if ((this.semver as any).includePrerelease) {
// @ts-expect-error type definition is not correct
if (this.semver.includePrerelease) {
this.isPreRelease = true;
} else if (this.semver.prerelease && this.semver.prerelease.length) {
} else if (this.semver.prerelease && this.semver.prerelease.length > 0) {
this.isPreRelease = true;
} else {
this.isPreRelease = false;
@@ -25,9 +26,10 @@ export class PaddingSemVer {
get paddingVersion(): string {
if (!this._paddingVersion) {
this._paddingVersion = PaddingSemVer.paddingVersion(this.semver.major)
+ PaddingSemVer.paddingVersion(this.semver.minor)
+ PaddingSemVer.paddingVersion(this.semver.patch);
this._paddingVersion =
PaddingSemVer.paddingVersion(this.semver.major) +
PaddingSemVer.paddingVersion(this.semver.minor) +
PaddingSemVer.paddingVersion(this.semver.patch);
}
return this._paddingVersion;
}
@@ -37,7 +39,8 @@ export class PaddingSemVer {
static paddingVersion(v: number) {
const t = String(v);
if (t.length <= 16) {
const padding = new Array(16 - t.length).fill(0)
const padding = Array.from({ length: 16 - t.length })
.fill(0)
.join('');
return padding + t;
}

View File

@@ -1,15 +1,17 @@
import { Entity, EntityData } from './Entity';
import { EasyData } from '../util/EntityUtil';
import { DIST_NAMES } from './Package';
import { isPkgManifest } from '../service/ProxyCacheService';
import { PROXY_CACHE_DIR_NAME } from '../../common/constants';
import { Entity, type EntityData } from './Entity.ts';
import { isPkgManifest, type DIST_NAMES } from './Package.ts';
import type { EasyData } from '../util/EntityUtil.ts';
import { PROXY_CACHE_DIR_NAME } from '../../common/constants.ts';
interface ProxyCacheData extends EntityData {
fullname: string;
fileType: DIST_NAMES;
version?: string;
}
export type CreateProxyCacheData = Omit<EasyData<ProxyCacheData, 'id'>, 'id'| 'filePath'>;
export type CreateProxyCacheData = Omit<
EasyData<ProxyCacheData, 'id'>,
'id' | 'filePath'
>;
export class ProxyCache extends Entity {
readonly fullname: string;
@@ -38,5 +40,4 @@ export class ProxyCache extends Entity {
data.updatedAt = new Date();
return data;
}
}

View File

@@ -1,6 +1,6 @@
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import type { RegistryType } from '../../common/enum/Registry';
import { Entity, type EntityData } from './Entity.ts';
import { EntityUtil, type EasyData } from '../util/EntityUtil.ts';
import type { RegistryType } from '../../common/enum/Registry.ts';
interface RegistryData extends EntityData {
name: string;
@@ -12,7 +12,10 @@ interface RegistryData extends EntityData {
authToken?: string;
}
export type CreateRegistryData = Omit<EasyData<RegistryData, 'registryId'>, 'id'>;
export type CreateRegistryData = Omit<
EasyData<RegistryData, 'registryId'>,
'id'
>;
export class Registry extends Entity {
name: string;
@@ -35,7 +38,10 @@ export class Registry extends Entity {
}
public static create(data: CreateRegistryData): Registry {
const newData = EntityUtil.defaultData<RegistryData, 'registryId'>(data, 'registryId');
const newData = EntityUtil.defaultData<RegistryData, 'registryId'>(
data,
'registryId'
);
return new Registry(newData);
}
}

View File

@@ -1,5 +1,5 @@
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import { Entity, type EntityData } from './Entity.ts';
import { EntityUtil, type EasyData } from '../util/EntityUtil.ts';
interface ScopeData extends EntityData {
name: string;

View File

@@ -1,5 +1,5 @@
import { Range, Comparator } from 'semver';
import { PaddingSemVer } from './PaddingSemVer';
import { Comparator, Range } from 'semver';
import { PaddingSemVer } from './PaddingSemVer.ts';
const OPERATOR_MAP = {
'<': '$lt',
@@ -21,7 +21,8 @@ export class SqlRange {
}
private comparatorToSql(comparator: Comparator) {
if (comparator.semver === (Comparator as any).ANY) {
// @ts-expect-error type definition is not correct
if (comparator.semver === Comparator.ANY) {
return {
$and: [
{
@@ -38,11 +39,13 @@ export class SqlRange {
};
}
const paddingSemver = new PaddingSemVer(comparator.semver);
const operator = OPERATOR_MAP[comparator.operator as keyof typeof OPERATOR_MAP];
const operator =
OPERATOR_MAP[comparator.operator as keyof typeof OPERATOR_MAP];
if (!operator) {
throw new Error(`unknown operator ${comparator.operator}`);
}
this._containPreRelease = this._containPreRelease || paddingSemver.isPreRelease;
this._containPreRelease =
this._containPreRelease || paddingSemver.isPreRelease;
return {
$and: [
{
@@ -59,8 +62,8 @@ export class SqlRange {
};
}
private comparatorSetToSql(comparatorSet: Array<Comparator>) {
const condition: Array<object> = [];
private comparatorSetToSql(comparatorSet: Comparator[]) {
const condition: object[] = [];
for (const comparator of comparatorSet) {
condition.push(this.comparatorToSql(comparator));
}
@@ -68,7 +71,7 @@ export class SqlRange {
}
private generateWhere() {
const conditions: Array<object> = [];
const conditions: object[] = [];
for (const rangeSet of this.range.set) {
conditions.push(this.comparatorSetToSql(rangeSet as Comparator[]));
}

View File

@@ -1,20 +1,22 @@
import os from 'node:os';
import path from 'node:path';
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import { TaskType, TaskState } from '../../common/enum/Task';
import { PROXY_CACHE_DIR_NAME } from '../../common/constants';
import dayjs from '../../common/dayjs';
import { HookEvent } from './HookEvent';
import { DIST_NAMES } from './Package';
import { isPkgManifest } from '../service/ProxyCacheService';
import { InternalServerError } from 'egg-errors';
import { InternalServerError } from 'egg/errors';
import { Entity, type EntityData } from './Entity.ts';
import { EntityUtil, type EasyData } from '../util/EntityUtil.ts';
import { TaskState, TaskType } from '../../common/enum/Task.ts';
import { PROXY_CACHE_DIR_NAME } from '../../common/constants.ts';
import dayjs from '../../common/dayjs.ts';
import type { HookEvent } from './HookEvent.ts';
import { isPkgManifest, type DIST_NAMES } from './Package.ts';
export const HOST_NAME = os.hostname();
export const PID = process.pid;
export interface TaskBaseData {
taskWorker: string;
shouldNotMerge?: boolean;
}
export interface TaskData<T = TaskBaseData> extends EntityData {
@@ -32,7 +34,7 @@ export interface TaskData<T = TaskBaseData> extends EntityData {
bizId?: string;
}
export type SyncPackageTaskOptions = {
export interface SyncPackageTaskOptions {
authorId?: string;
authorIp?: string;
tips?: string;
@@ -41,14 +43,14 @@ export type SyncPackageTaskOptions = {
// force sync history version
forceSyncHistory?: boolean;
registryId?: string;
specificVersions?: Array<string>;
};
specificVersions?: string[];
}
export type UpdateProxyCacheTaskOptions = {
fullname: string,
version?: string,
fileType: DIST_NAMES,
};
export interface UpdateProxyCacheTaskOptions {
fullname: string;
version?: string;
fileType: DIST_NAMES;
}
export interface CreateHookTaskData extends TaskBaseData {
hookEvent: HookEvent;
@@ -65,22 +67,24 @@ export interface CreateSyncPackageTaskData extends TaskBaseData {
skipDependencies?: boolean;
syncDownloadData?: boolean;
forceSyncHistory?: boolean;
specificVersions?: Array<string>;
specificVersions?: string[];
}
export interface CreateUpdateProxyCacheTaskData extends TaskBaseData {
fullname: string,
version?: string,
fileType: DIST_NAMES,
filePath: string
fullname: string;
version?: string;
fileType: DIST_NAMES;
filePath: string;
}
export type SyncBinaryTaskData = Record<string, unknown> & TaskBaseData;
export interface ChangesStreamTaskData extends TaskBaseData {
since: string;
last_package?: string,
last_package_created?: Date,
task_count?: number,
registryId?: string,
last_package?: string;
last_package_created?: Date;
task_count?: number;
registryId?: string;
}
export interface TaskUpdateCondition {
@@ -93,6 +97,7 @@ export type TriggerHookTask = Task<TriggerHookTaskData>;
export type CreateSyncPackageTask = Task<CreateSyncPackageTaskData>;
export type ChangesStreamTask = Task<ChangesStreamTaskData>;
export type CreateUpdateProxyCacheTask = Task<CreateUpdateProxyCacheTaskData>;
export type SyncBinaryTask = Task<SyncBinaryTaskData>;
export class Task<T extends TaskBaseData = TaskBaseData> extends Entity {
taskId: string;
@@ -134,12 +139,17 @@ export class Task<T extends TaskBaseData = TaskBaseData> extends Entity {
this.data.taskWorker = `${HOST_NAME}:${PID}`;
}
private static create<T extends TaskBaseData>(data: EasyData<TaskData<T>, 'taskId'>): Task<T> {
private static create<T extends TaskBaseData>(
data: EasyData<TaskData<T>, 'taskId'>
): Task<T> {
const newData = EntityUtil.defaultData(data, 'taskId');
return new Task(newData);
}
public static createSyncPackage(fullname: string, options?: SyncPackageTaskOptions): CreateSyncPackageTask {
public static createSyncPackage(
fullname: string,
options?: SyncPackageTaskOptions
): CreateSyncPackageTask {
const data = {
type: TaskType.SyncPackage,
state: TaskState.Waiting,
@@ -162,7 +172,11 @@ export class Task<T extends TaskBaseData = TaskBaseData> extends Entity {
return task;
}
public static createChangesStream(targetName: string, registryId = '', since = ''): ChangesStreamTask {
public static createChangesStream(
targetName: string,
registryId = '',
since = ''
): ChangesStreamTask {
const data = {
type: TaskType.ChangesStream,
state: TaskState.Waiting,
@@ -210,7 +224,10 @@ export class Task<T extends TaskBaseData = TaskBaseData> extends Entity {
return task;
}
public static createTriggerHookTask(hookEvent: HookEvent, hookId: string): TriggerHookTask {
public static createTriggerHookTask(
hookEvent: HookEvent,
hookId: string
): TriggerHookTask {
const data = {
type: TaskType.TriggerHook,
state: TaskState.Waiting,
@@ -230,7 +247,10 @@ export class Task<T extends TaskBaseData = TaskBaseData> extends Entity {
return task;
}
public static createSyncBinary(targetName: string, lastData: any): Task {
public static createSyncBinary(
targetName: string,
lastData?: Record<string, unknown>
): Task {
const data = {
type: TaskType.SyncBinary,
state: TaskState.Waiting,
@@ -249,13 +269,24 @@ export class Task<T extends TaskBaseData = TaskBaseData> extends Entity {
return task;
}
public static needMergeWhenWaiting(type: TaskType) {
return [ TaskType.SyncBinary, TaskType.SyncPackage ].includes(type);
needMergeWhenWaiting(): boolean {
// 历史任务补偿时,将 shouldNotMerge 设置为 true避免合并
// 补偿任务单独执行
if (this.data.shouldNotMerge === true) {
return false;
}
// 仅合并二进制镜像与 npm 包
return [TaskType.SyncBinary, TaskType.SyncPackage].includes(this.type);
}
public static createUpdateProxyCache(targetName: string, options: UpdateProxyCacheTaskOptions):CreateUpdateProxyCacheTask {
public static createUpdateProxyCache(
targetName: string,
options: UpdateProxyCacheTaskOptions
): CreateUpdateProxyCacheTask {
if (!isPkgManifest(options.fileType)) {
throw new InternalServerError('should not update package version manifest.');
throw new InternalServerError(
'should not update package version manifest.'
);
}
const filePath = `/${PROXY_CACHE_DIR_NAME}/${options.fullname}/${options.fileType}`;
const data = {
@@ -289,8 +320,8 @@ export class Task<T extends TaskBaseData = TaskBaseData> extends Entity {
}
}
export type SyncInfo = {
export interface SyncInfo {
lastSince: string;
taskCount: number;
lastPackage?: string;
};
}

View File

@@ -1,6 +1,7 @@
import dayjs from 'dayjs';
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import { Entity, type EntityData } from './Entity.ts';
import { EntityUtil, type EasyData } from '../util/EntityUtil.ts';
export enum TokenType {
granular = 'granular',
@@ -17,7 +18,7 @@ interface BaseTokenData extends EntityData {
lastUsedAt?: Date;
}
interface ClassicTokenData extends BaseTokenData{
interface ClassicTokenData extends BaseTokenData {
isAutomation?: boolean;
}
interface GranularTokenData extends BaseTokenData {
@@ -31,7 +32,9 @@ interface GranularTokenData extends BaseTokenData {
type TokenData = ClassicTokenData | GranularTokenData;
export function isGranularToken(data: TokenData | Token): data is GranularTokenData {
export function isGranularToken(
data: TokenData | Token
): data is GranularTokenData {
return data.type === TokenType.granular;
}
@@ -51,7 +54,7 @@ export class Token extends Entity {
readonly expires?: number;
lastUsedAt: Date | null;
allowedPackages?: string[];
token?: string;
token: string;
constructor(data: TokenData) {
super(data);
@@ -79,9 +82,10 @@ export class Token extends Entity {
static create(data: EasyData<TokenData, 'tokenId'>): Token {
const newData = EntityUtil.defaultData(data, 'tokenId');
if (isGranularToken(newData) && !newData.expiredAt) {
newData.expiredAt = dayjs(newData.createdAt).add(newData.expires, 'days').toDate();
newData.expiredAt = dayjs(newData.createdAt)
.add(newData.expires, 'days')
.toDate();
}
return new Token(newData);
}
}

View File

@@ -1,6 +1,6 @@
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import { cleanUserPrefix } from '../../common/PackageUtil';
import { Entity, type EntityData } from './Entity.ts';
import { EntityUtil, type EasyData } from '../util/EntityUtil.ts';
import { cleanUserPrefix } from '../../common/PackageUtil.ts';
interface UserData extends EntityData {
userId: string;

View File

@@ -1,5 +1,5 @@
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import { Entity, type EntityData } from './Entity.ts';
import { EntityUtil, type EasyData } from '../util/EntityUtil.ts';
interface WebauthnCredentialData extends EntityData {
wancId: string;
@@ -25,7 +25,9 @@ export class WebauthnCredential extends Entity {
this.browserType = data.browserType;
}
static create(data: EasyData<WebauthnCredentialData, 'wancId'>): WebauthnCredential {
static create(
data: EasyData<WebauthnCredentialData, 'wancId'>
): WebauthnCredential {
const newData = EntityUtil.defaultData(data, 'wancId');
return new WebauthnCredential(newData);
}

View File

@@ -1,8 +1,8 @@
import { Event, Inject } from '@eggjs/tegg';
import { EggLogger } from 'egg';
import { PACKAGE_VERSION_ADDED } from './index';
import { BUG_VERSIONS } from '../../common/constants';
import { BugVersionService } from '../service/BugVersionService';
import { Event, Inject, Logger } from 'egg';
import { PACKAGE_VERSION_ADDED } from './index.ts';
import { BUG_VERSIONS } from '../../common/constants.ts';
import type { BugVersionService } from '../service/BugVersionService.ts';
@Event(PACKAGE_VERSION_ADDED)
export class BugVersionFixHandler {
@@ -10,7 +10,7 @@ export class BugVersionFixHandler {
private readonly bugVersionService: BugVersionService;
@Inject()
private readonly logger: EggLogger;
private readonly logger: Logger;
async handle(fullname: string) {
if (fullname !== BUG_VERSIONS) return;

View File

@@ -1,18 +1,19 @@
import { Event, Inject } from '@eggjs/tegg';
import { Event, Inject } from 'egg';
import {
PACKAGE_UNPUBLISHED,
PACKAGE_BLOCKED,
PACKAGE_UNBLOCKED,
PACKAGE_VERSION_ADDED,
PACKAGE_VERSION_REMOVED,
PACKAGE_TAG_ADDED,
PACKAGE_TAG_CHANGED,
PACKAGE_TAG_REMOVED,
PACKAGE_MAINTAINER_CHANGED,
PACKAGE_MAINTAINER_REMOVED,
PACKAGE_META_CHANGED,
} from './index';
import { CacheService } from '../../core/service/CacheService';
PACKAGE_TAG_ADDED,
PACKAGE_TAG_CHANGED,
PACKAGE_TAG_REMOVED,
PACKAGE_UNBLOCKED,
PACKAGE_UNPUBLISHED,
PACKAGE_VERSION_ADDED,
PACKAGE_VERSION_REMOVED,
} from './index.ts';
import type { CacheService } from '../../core/service/CacheService.ts';
class CacheCleanerEvent {
@Inject()

View File

@@ -1,22 +1,24 @@
import { EggAppConfig } from 'egg';
import { Event, Inject } from '@eggjs/tegg';
import { Event, Inject, Config } from 'egg';
import {
PACKAGE_UNPUBLISHED,
PACKAGE_VERSION_ADDED,
PACKAGE_VERSION_REMOVED,
type PackageMetaChange,
PACKAGE_MAINTAINER_CHANGED,
PACKAGE_MAINTAINER_REMOVED,
PACKAGE_META_CHANGED,
PACKAGE_TAG_ADDED,
PACKAGE_TAG_CHANGED,
PACKAGE_TAG_REMOVED,
PACKAGE_MAINTAINER_CHANGED,
PACKAGE_MAINTAINER_REMOVED,
PACKAGE_META_CHANGED, PackageMetaChange,
} from './index';
import { ChangeRepository } from '../../repository/ChangeRepository';
import { Change } from '../entity/Change';
import { HookEvent } from '../entity/HookEvent';
import { Task } from '../entity/Task';
import { User } from '../entity/User';
import { TaskService } from '../service/TaskService';
PACKAGE_UNPUBLISHED,
PACKAGE_VERSION_ADDED,
PACKAGE_VERSION_REMOVED,
} from './index.ts';
import type { ChangeRepository } from '../../repository/ChangeRepository.ts';
import { Change } from '../entity/Change.ts';
import { HookEvent } from '../entity/HookEvent.ts';
import { Task } from '../entity/Task.ts';
import type { User } from '../entity/User.ts';
import type { TaskService } from '../service/TaskService.ts';
class ChangesStreamEvent {
@Inject()
@@ -26,13 +28,17 @@ class ChangesStreamEvent {
protected readonly taskService: TaskService;
@Inject()
protected readonly config: EggAppConfig;
protected readonly config: Config;
protected get hookEnable() {
return this.config.cnpmcore.hookEnable;
}
protected async addChange(type: string, fullname: string, data: object): Promise<Change> {
protected async addChange(
type: string,
fullname: string,
data: object
): Promise<Change> {
const change = Change.create({
type,
targetName: fullname,
@@ -48,7 +54,9 @@ export class PackageUnpublishedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string) {
const change = await this.addChange(PACKAGE_UNPUBLISHED, fullname, {});
if (this.hookEnable) {
const task = Task.createCreateHookTask(HookEvent.createUnpublishEvent(fullname, change.changeId));
const task = Task.createCreateHookTask(
HookEvent.createUnpublishEvent(fullname, change.changeId)
);
await this.taskService.createTask(task, true);
}
}
@@ -57,9 +65,13 @@ export class PackageUnpublishedChangesStreamEvent extends ChangesStreamEvent {
@Event(PACKAGE_VERSION_ADDED)
export class PackageVersionAddedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string, version: string, tag?: string) {
const change = await this.addChange(PACKAGE_VERSION_ADDED, fullname, { version });
const change = await this.addChange(PACKAGE_VERSION_ADDED, fullname, {
version,
});
if (this.hookEnable) {
const task = Task.createCreateHookTask(HookEvent.createPublishEvent(fullname, change.changeId, version, tag));
const task = Task.createCreateHookTask(
HookEvent.createPublishEvent(fullname, change.changeId, version, tag)
);
await this.taskService.createTask(task, true);
}
}
@@ -68,9 +80,13 @@ export class PackageVersionAddedChangesStreamEvent extends ChangesStreamEvent {
@Event(PACKAGE_VERSION_REMOVED)
export class PackageVersionRemovedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string, version: string, tag?: string) {
const change = await this.addChange(PACKAGE_VERSION_REMOVED, fullname, { version });
const change = await this.addChange(PACKAGE_VERSION_REMOVED, fullname, {
version,
});
if (this.hookEnable) {
const task = Task.createCreateHookTask(HookEvent.createUnpublishEvent(fullname, change.changeId, version, tag));
const task = Task.createCreateHookTask(
HookEvent.createUnpublishEvent(fullname, change.changeId, version, tag)
);
await this.taskService.createTask(task, true);
}
}
@@ -81,7 +97,9 @@ export class PackageTagAddedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string, tag: string) {
const change = await this.addChange(PACKAGE_TAG_ADDED, fullname, { tag });
if (this.hookEnable) {
const task = Task.createCreateHookTask(HookEvent.createDistTagEvent(fullname, change.changeId, tag));
const task = Task.createCreateHookTask(
HookEvent.createDistTagEvent(fullname, change.changeId, tag)
);
await this.taskService.createTask(task, true);
}
}
@@ -92,7 +110,9 @@ export class PackageTagChangedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string, tag: string) {
const change = await this.addChange(PACKAGE_TAG_CHANGED, fullname, { tag });
if (this.hookEnable) {
const task = Task.createCreateHookTask(HookEvent.createDistTagEvent(fullname, change.changeId, tag));
const task = Task.createCreateHookTask(
HookEvent.createDistTagEvent(fullname, change.changeId, tag)
);
await this.taskService.createTask(task, true);
}
}
@@ -103,7 +123,9 @@ export class PackageTagRemovedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string, tag: string) {
const change = await this.addChange(PACKAGE_TAG_REMOVED, fullname, { tag });
if (this.hookEnable) {
const task = Task.createCreateHookTask(HookEvent.createDistTagRmEvent(fullname, change.changeId, tag));
const task = Task.createCreateHookTask(
HookEvent.createDistTagRmEvent(fullname, change.changeId, tag)
);
await this.taskService.createTask(task, true);
}
}
@@ -112,11 +134,17 @@ export class PackageTagRemovedChangesStreamEvent extends ChangesStreamEvent {
@Event(PACKAGE_MAINTAINER_CHANGED)
export class PackageMaintainerChangedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string, maintainers: User[]) {
const change = await this.addChange(PACKAGE_MAINTAINER_CHANGED, fullname, {});
const change = await this.addChange(
PACKAGE_MAINTAINER_CHANGED,
fullname,
{}
);
// TODO 应该比较差值,而不是全量推送
if (this.hookEnable) {
for (const maintainer of maintainers) {
const task = Task.createCreateHookTask(HookEvent.createOwnerEvent(fullname, change.changeId, maintainer.name));
const task = Task.createCreateHookTask(
HookEvent.createOwnerEvent(fullname, change.changeId, maintainer.name)
);
await this.taskService.createTask(task, true);
}
}
@@ -126,9 +154,13 @@ export class PackageMaintainerChangedChangesStreamEvent extends ChangesStreamEve
@Event(PACKAGE_MAINTAINER_REMOVED)
export class PackageMaintainerRemovedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string, maintainer: string) {
const change = await this.addChange(PACKAGE_MAINTAINER_REMOVED, fullname, { maintainer });
const change = await this.addChange(PACKAGE_MAINTAINER_REMOVED, fullname, {
maintainer,
});
if (this.hookEnable) {
const task = Task.createCreateHookTask(HookEvent.createOwnerRmEvent(fullname, change.changeId, maintainer));
const task = Task.createCreateHookTask(
HookEvent.createOwnerRmEvent(fullname, change.changeId, maintainer)
);
await this.taskService.createTask(task, true);
}
}
@@ -137,11 +169,19 @@ export class PackageMaintainerRemovedChangesStreamEvent extends ChangesStreamEve
@Event(PACKAGE_META_CHANGED)
export class PackageMetaChangedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string, meta: PackageMetaChange) {
const change = await this.addChange(PACKAGE_META_CHANGED, fullname, { ...meta });
const change = await this.addChange(PACKAGE_META_CHANGED, fullname, {
...meta,
});
const { deprecateds } = meta;
if (this.hookEnable) {
for (const deprecated of deprecateds || []) {
const task = Task.createCreateHookTask(HookEvent.createDeprecatedEvent(fullname, change.changeId, deprecated.version));
const task = Task.createCreateHookTask(
HookEvent.createDeprecatedEvent(
fullname,
change.changeId,
deprecated.version
)
);
await this.taskService.createTask(task, true);
}
}

View File

@@ -1,30 +1,38 @@
import { Event, Inject } from '@eggjs/tegg';
import {
EggAppConfig,
} from 'egg';
import { PACKAGE_VERSION_ADDED } from './index';
import { getScopeAndName } from '../../common/PackageUtil';
import { PackageVersionManifest as PackageVersionManifestEntity } from '../entity/PackageVersionManifest';
import { PackageRepository } from '../../repository/PackageRepository';
import { DistRepository } from '../../repository/DistRepository';
import { Config, Event, Inject } from 'egg';
import { PACKAGE_VERSION_ADDED } from './index.ts';
import { getScopeAndName } from '../../common/PackageUtil.ts';
import { PackageVersionManifest as PackageVersionManifestEntity } from '../entity/PackageVersionManifest.ts';
import type { PackageRepository } from '../../repository/PackageRepository.ts';
import type { DistRepository } from '../../repository/DistRepository.ts';
class StoreManifestEvent {
@Inject()
protected readonly config: EggAppConfig;
protected readonly config: Config;
@Inject()
private readonly packageRepository: PackageRepository;
@Inject()
private readonly distRepository: DistRepository;
protected async savePackageVersionManifest(fullname: string, version: string) {
if (!this.config.cnpmcore.enableStoreFullPackageVersionManifestsToDatabase) return;
protected async savePackageVersionManifest(
fullname: string,
version: string
) {
if (!this.config.cnpmcore.enableStoreFullPackageVersionManifestsToDatabase)
return;
const [ scope, name ] = getScopeAndName(fullname);
const [scope, name] = getScopeAndName(fullname);
const packageId = await this.packageRepository.findPackageId(scope, name);
if (!packageId) return;
const packageVersion = await this.packageRepository.findPackageVersion(packageId, version);
const packageVersion = await this.packageRepository.findPackageVersion(
packageId,
version
);
if (!packageVersion) return;
const manifest = await this.distRepository.findPackageVersionManifest(packageId, version);
const manifest = await this.distRepository.findPackageVersionManifest(
packageId,
version
);
if (!manifest) return;
const entity = PackageVersionManifestEntity.create({
packageId,

View File

@@ -1,29 +1,26 @@
// TODO sync event
/* eslint-disable @typescript-eslint/no-unused-vars */
import { EggAppConfig } from 'egg';
import { Event, Inject } from '@eggjs/tegg';
import { Config, Event, Inject } from 'egg';
import {
PACKAGE_UNPUBLISHED,
PACKAGE_VERSION_ADDED,
PACKAGE_VERSION_REMOVED,
PACKAGE_TAG_ADDED,
PACKAGE_TAG_CHANGED,
PACKAGE_TAG_REMOVED,
PACKAGE_BLOCKED,
PACKAGE_MAINTAINER_CHANGED,
PACKAGE_MAINTAINER_REMOVED,
PACKAGE_META_CHANGED,
PACKAGE_BLOCKED,
PACKAGE_TAG_ADDED,
PACKAGE_TAG_CHANGED,
PACKAGE_TAG_REMOVED,
PACKAGE_UNBLOCKED,
} from './index';
import { PackageSearchService } from '../service/PackageSearchService';
PACKAGE_UNPUBLISHED,
PACKAGE_VERSION_ADDED,
PACKAGE_VERSION_REMOVED,
} from './index.ts';
import type { PackageSearchService } from '../service/PackageSearchService.ts';
class SyncESPackage {
@Inject()
protected readonly packageSearchService: PackageSearchService;
@Inject()
protected readonly config: EggAppConfig;
protected readonly config: Config;
protected async syncPackage(fullname: string) {
if (!this.config.cnpmcore.enableElasticsearch) return;

View File

@@ -1,18 +1,20 @@
import { Event, Inject } from '@eggjs/tegg';
import { Event, Inject, Config, Logger } from 'egg';
import { ForbiddenError } from 'egg/errors';
import {
EggAppConfig, EggLogger,
} from 'egg';
import { ForbiddenError } from 'egg-errors';
import { PACKAGE_VERSION_ADDED, PACKAGE_TAG_ADDED, PACKAGE_TAG_CHANGED } from './index';
import { getScopeAndName } from '../../common/PackageUtil';
import { PackageManagerService } from '../service/PackageManagerService';
import { PackageVersionFileService } from '../service/PackageVersionFileService';
PACKAGE_TAG_ADDED,
PACKAGE_TAG_CHANGED,
PACKAGE_VERSION_ADDED,
} from './index.ts';
import { getScopeAndName } from '../../common/PackageUtil.ts';
import type { PackageManagerService } from '../service/PackageManagerService.ts';
import type { PackageVersionFileService } from '../service/PackageVersionFileService.ts';
class SyncPackageVersionFileEvent {
@Inject()
protected readonly config: EggAppConfig;
protected readonly config: Config;
@Inject()
protected readonly logger: EggLogger;
protected readonly logger: Logger;
@Inject()
private readonly packageManagerService: PackageManagerService;
@Inject()
@@ -23,17 +25,28 @@ class SyncPackageVersionFileEvent {
if (!this.config.cnpmcore.enableUnpkg) return;
if (!this.config.cnpmcore.enableSyncUnpkgFiles) return;
// ignore sync on unittest
if (this.config.env === 'unittest' && fullname !== '@cnpm/unittest-unpkg-demo') return;
const [ scope, name ] = getScopeAndName(fullname);
const { packageVersion } = await this.packageManagerService.showPackageVersionByVersionOrTag(
scope, name, version);
if (
this.config.env === 'unittest' &&
fullname !== '@cnpm/unittest-unpkg-demo'
)
return;
const [scope, name] = getScopeAndName(fullname);
const { packageVersion } =
await this.packageManagerService.showPackageVersionByVersionOrTag(
scope,
name,
version
);
if (!packageVersion) return;
try {
await this.packageVersionFileService.syncPackageVersionFiles(packageVersion);
await this.packageVersionFileService.syncPackageVersionFiles(
packageVersion
);
} catch (err) {
if (err instanceof ForbiddenError) {
this.logger.info('[SyncPackageVersionFileEvent.syncPackageVersionFile] ignore sync files, cause: %s',
err.message,
this.logger.info(
'[SyncPackageVersionFileEvent.syncPackageVersionFile] ignore sync files, cause: %s',
err.message
);
return;
}
@@ -42,9 +55,13 @@ class SyncPackageVersionFileEvent {
}
protected async syncPackageReadmeToLatestVersion(fullname: string) {
const [ scope, name ] = getScopeAndName(fullname);
const { pkg, packageVersion } = await this.packageManagerService.showPackageVersionByVersionOrTag(
scope, name, 'latest');
const [scope, name] = getScopeAndName(fullname);
const { pkg, packageVersion } =
await this.packageManagerService.showPackageVersionByVersionOrTag(
scope,
name,
'latest'
);
if (!pkg || !packageVersion) return;
await this.packageVersionFileService.syncPackageReadme(pkg, packageVersion);
}

View File

@@ -0,0 +1,23 @@
import { Event, Inject } from 'egg';
import { PACKAGE_ADDED, PACKAGE_VERSION_ADDED } from './index.ts';
import type { TotalRepository } from '../../repository/TotalRepository.ts';
class TotalHandlerEvent {
@Inject()
protected readonly totalRepository: TotalRepository;
}
@Event(PACKAGE_ADDED)
export class PackageAddedTotalHandlerEvent extends TotalHandlerEvent {
async handle() {
await this.totalRepository.incrementPackageCount();
}
}
@Event(PACKAGE_VERSION_ADDED)
export class PackageVersionAddedTotalHandlerEvent extends TotalHandlerEvent {
async handle() {
await this.totalRepository.incrementPackageVersionCount();
}
}

View File

@@ -1,6 +1,6 @@
import '@eggjs/tegg';
import { User } from '../entity/User';
import type { User } from '../entity/User.ts';
export const PACKAGE_ADDED = 'PACKAGE_ADDED';
export const PACKAGE_UNPUBLISHED = 'PACKAGE_UNPUBLISHED';
export const PACKAGE_BLOCKED = 'PACKAGE_BLOCKED';
export const PACKAGE_UNBLOCKED = 'PACKAGE_UNBLOCKED';
@@ -15,26 +15,43 @@ export const PACKAGE_META_CHANGED = 'PACKAGE_META_CHANGED';
export interface PackageDeprecated {
version: string;
deprecated: string;
deprecated?: string;
}
export interface PackageMetaChange {
deprecateds?: Array<PackageDeprecated>;
deprecateds?: PackageDeprecated[];
}
declare module '@eggjs/tegg' {
declare module 'egg' {
interface Events {
[PACKAGE_ADDED]: (fullname: string) => Promise<void>;
[PACKAGE_UNPUBLISHED]: (fullname: string) => Promise<void>;
[PACKAGE_BLOCKED]: (fullname: string) => Promise<void>;
[PACKAGE_UNBLOCKED]: (fullname: string) => Promise<void>;
[PACKAGE_VERSION_ADDED]: (fullname: string, version: string, tag?: string) => Promise<void>;
[PACKAGE_VERSION_REMOVED]: (fullname: string, version: string, tag?: string) => Promise<void>;
[PACKAGE_VERSION_ADDED]: (
fullname: string,
version: string,
tag?: string
) => Promise<void>;
[PACKAGE_VERSION_REMOVED]: (
fullname: string,
version: string,
tag?: string
) => Promise<void>;
[PACKAGE_TAG_ADDED]: (fullname: string, tag: string) => Promise<void>;
[PACKAGE_TAG_CHANGED]: (fullname: string, tag: string) => Promise<void>;
[PACKAGE_TAG_REMOVED]: (fullname: string, tag: string) => Promise<void>;
[PACKAGE_MAINTAINER_CHANGED]: (fullname: string, maintainers: User[]) => Promise<void>;
[PACKAGE_MAINTAINER_REMOVED]: (fullname: string, maintainer: string) => Promise<void>;
[PACKAGE_META_CHANGED]: (fullname: string, meta: PackageMetaChange) => Promise<void>;
[PACKAGE_MAINTAINER_CHANGED]: (
fullname: string,
maintainers: User[]
) => Promise<void>;
[PACKAGE_MAINTAINER_REMOVED]: (
fullname: string,
maintainer: string
) => Promise<void>;
[PACKAGE_META_CHANGED]: (
fullname: string,
meta: PackageMetaChange
) => Promise<void>;
}
}

View File

@@ -2,5 +2,6 @@
"name": "cnpmcore-core",
"eggModule": {
"name": "cnpmcoreCore"
}
},
"type": "module"
}

View File

@@ -1,26 +1,33 @@
import fs from 'node:fs/promises';
import {
AccessLevel,
SingletonProto,
Inject,
EggObjectFactory,
} from '@eggjs/tegg';
import {
EggHttpClient,
SingletonProto,
type EggObjectFactory,
HttpClient,
} from 'egg';
import fs from 'node:fs/promises';
import { sortBy } from 'lodash';
import binaries, { BinaryName, CategoryName } from '../../../config/binaries';
import { BinaryRepository } from '../../repository/BinaryRepository';
import { Task } from '../entity/Task';
import { Binary } from '../entity/Binary';
import { TaskService } from './TaskService';
import { NFSAdapter } from '../../common/adapter/NFSAdapter';
import { downloadToTempfile } from '../../common/FileUtil';
import { isTimeoutError } from '../../common/ErrorUtil';
import { AbstractBinary, BinaryItem } from '../../common/adapter/binary/AbstractBinary';
import { AbstractService } from '../../common/AbstractService';
import { BinaryType } from '../../common/enum/Binary';
import { TaskType, TaskState } from '../../common/enum/Task';
import { sortBy } from 'lodash-es';
import binaries, {
type BinaryName,
type CategoryName,
} from '../../../config/binaries.ts';
import type { BinaryRepository } from '../../repository/BinaryRepository.ts';
import { Task, type SyncBinaryTask } from '../entity/Task.ts';
import { Binary } from '../entity/Binary.ts';
import type { TaskService } from './TaskService.ts';
import type { NFSAdapter } from '../../common/adapter/NFSAdapter.ts';
import { downloadToTempfile } from '../../common/FileUtil.ts';
import { isTimeoutError } from '../../common/ErrorUtil.ts';
import {
AbstractBinary,
type BinaryItem,
} from '../../common/adapter/binary/AbstractBinary.ts';
import { AbstractService } from '../../common/AbstractService.ts';
import { BinaryType } from '../../common/enum/Binary.ts';
import { TaskState, TaskType } from '../../common/enum/Task.ts';
import { platforms } from '../../common/adapter/binary/PuppeteerBinary.ts';
function isoNow() {
return new Date().toISOString();
@@ -35,7 +42,7 @@ export class BinarySyncerService extends AbstractService {
@Inject()
private readonly taskService: TaskService;
@Inject()
private readonly httpclient: EggHttpClient;
private readonly httpClient: HttpClient;
@Inject()
private readonly nfsAdapter: NFSAdapter;
@Inject()
@@ -44,40 +51,49 @@ export class BinarySyncerService extends AbstractService {
// canvas/v2.6.1/canvas-v2.6.1-node-v57-linux-glibc-x64.tar.gz
// -> node-canvas-prebuilt/v2.6.1/node-canvas-prebuilt-v2.6.1-node-v57-linux-glibc-x64.tar.gz
// canvas 历史版本的 targetName 可能是 category 需要兼容
public async findBinary(targetName: BinaryName | CategoryName, parent: string, name: string) {
public async findBinary(
targetName: BinaryName | CategoryName,
parent: string,
name: string
) {
return await this.binaryRepository.findBinary(targetName, parent, name);
}
public async listDirBinaries(binary: Binary) {
return await this.binaryRepository.listBinaries(binary.category, `${binary.parent}${binary.name}`);
public async listDirBinaries(
binary: Binary,
options?: {
limit: number;
since: string;
}
) {
return await this.binaryRepository.listBinaries(
binary.category,
`${binary.parent}${binary.name}`,
options
);
}
public async listRootBinaries(binaryName: BinaryName) {
// 通常 binaryName 和 category 是一样的,但是有些特殊的 binaryName 会有多个 category比如 canvas
// 所以查询 canvas 的时候,需要将 binaryName 和 category 的数据都查出来
const {
category,
} = binaries[binaryName];
const reqs = [
this.binaryRepository.listBinaries(binaryName, '/'),
];
const { category } = binaries[binaryName];
const reqs = [this.binaryRepository.listBinaries(binaryName, '/')];
if (category && category !== binaryName) {
reqs.push(this.binaryRepository.listBinaries(category, '/'));
}
const [
rootBinary,
categoryBinary,
] = await Promise.all(reqs);
const [rootBinary, categoryBinary] = await Promise.all(reqs);
const versions = rootBinary.map(b => b.name);
categoryBinary?.forEach(b => {
const version = b.name;
// 只将没有的版本添加进去
if (!versions.includes(version)) {
rootBinary.push(b);
const versions = new Set(rootBinary.map(b => b.name));
if (categoryBinary) {
for (const b of categoryBinary) {
const version = b.name;
// 只将没有的版本添加进去
if (!versions.has(version)) {
rootBinary.push(b);
}
}
});
}
return rootBinary;
}
@@ -86,66 +102,131 @@ export class BinarySyncerService extends AbstractService {
return await this.nfsAdapter.getDownloadUrlOrStream(binary.storePath);
}
public async createTask(binaryName: BinaryName, lastData?: any) {
public async createTask(
binaryName: BinaryName,
lastData?: Record<string, unknown>
) {
// chromium-browser-snapshots 产物极大,完整遍历 s3 bucket 耗时会太长
// 必须从上次同步的 revision 之后开始遍历
// 如果需要补偿数据,可以
if (binaryName === 'chromium-browser-snapshots') {
lastData = lastData || {};
for (const platform of platforms) {
if (lastData[platform]) continue;
const binaryDir = await this.binaryRepository.findLatestBinaryDir(
'chromium-browser-snapshots',
`/${platform}/`
);
if (binaryDir) {
lastData[platform] = binaryDir.name.slice(0, -1);
}
}
const latestBinary = await this.binaryRepository.findLatestBinary(
'chromium-browser-snapshots'
);
if (latestBinary && !lastData.lastSyncTime) {
lastData.lastSyncTime = latestBinary.date;
}
}
try {
return await this.taskService.createTask(Task.createSyncBinary(binaryName, lastData), false);
return await this.taskService.createTask(
Task.createSyncBinary(binaryName, lastData),
false
);
} catch (e) {
this.logger.error('[BinarySyncerService.createTask] binaryName: %s, error: %s', binaryName, e);
this.logger.error(
'[BinarySyncerService.createTask] binaryName: %s, error: %s',
binaryName,
e
);
}
}
public async findTask(taskId: string) {
return await this.taskService.findTask(taskId);
public async findTask(taskId: string): Promise<SyncBinaryTask | null> {
return (await this.taskService.findTask(taskId)) as SyncBinaryTask;
}
public async findTaskLog(task: Task) {
public async findTaskLog(task: SyncBinaryTask) {
return await this.taskService.findTaskLog(task);
}
public async findExecuteTask() {
return await this.taskService.findExecuteTask(TaskType.SyncBinary);
public async findExecuteTask(): Promise<SyncBinaryTask | null> {
return (await this.taskService.findExecuteTask(
TaskType.SyncBinary
)) as SyncBinaryTask;
}
public async executeTask(task: Task) {
public async executeTask(task: SyncBinaryTask) {
const binaryName = task.targetName as BinaryName;
const binaryAdapter = await this.getBinaryAdapter(binaryName);
const logUrl = `${this.config.cnpmcore.registry}/-/binary/${binaryName}/syncs/${task.taskId}/log`;
let logs: string[] = [];
logs.push(`[${isoNow()}] 🚧🚧🚧🚧🚧 Start sync binary "${binaryName}" 🚧🚧🚧🚧🚧`);
logs.push(
`[${isoNow()}] 🚧🚧🚧🚧🚧 Start sync binary "${binaryName}" 🚧🚧🚧🚧🚧`
);
if (!binaryAdapter) {
task.error = 'unknow binaryName';
logs.push(`[${isoNow()}] ❌ Synced "${binaryName}" fail, ${task.error}, log: ${logUrl}`);
logs.push(
`[${isoNow()}] ❌ Synced "${binaryName}" fail, ${task.error}, log: ${logUrl}`
);
logs.push(`[${isoNow()}] ❌❌❌❌❌ "${binaryName}" ❌❌❌❌❌`);
this.logger.error('[BinarySyncerService.executeTask:fail] taskId: %s, targetName: %s, %s',
task.taskId, task.targetName, task.error);
this.logger.error(
'[BinarySyncerService.executeTask:fail] taskId: %s, targetName: %s, %s',
task.taskId,
task.targetName,
task.error
);
await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n'));
return;
}
await this.taskService.appendTaskLog(task, logs.join('\n'));
logs = [];
this.logger.info('[BinarySyncerService.executeTask:start] taskId: %s, targetName: %s, log: %s',
task.taskId, task.targetName, logUrl);
this.logger.info(
'[BinarySyncerService.executeTask:start] taskId: %s, targetName: %s, log: %s',
task.taskId,
task.targetName,
logUrl
);
try {
const [ hasDownloadError ] = await this.syncDir(binaryAdapter, task, '/');
const [hasDownloadError] = await this.syncDir(binaryAdapter, task, '/');
logs.push(`[${isoNow()}] 🟢 log: ${logUrl}`);
logs.push(`[${isoNow()}] 🟢🟢🟢🟢🟢 "${binaryName}" 🟢🟢🟢🟢🟢`);
await this.taskService.finishTask(task, TaskState.Success, logs.join('\n'));
await this.taskService.finishTask(
task,
TaskState.Success,
logs.join('\n')
);
// 确保没有下载异常才算 success
await binaryAdapter.finishFetch(!hasDownloadError, binaryName);
this.logger.info('[BinarySyncerService.executeTask:success] taskId: %s, targetName: %s, log: %s, hasDownloadError: %s',
task.taskId, task.targetName, logUrl, hasDownloadError);
} catch (err: any) {
this.logger.info(
'[BinarySyncerService.executeTask:success] taskId: %s, targetName: %s, log: %s, hasDownloadError: %s',
task.taskId,
task.targetName,
logUrl,
hasDownloadError
);
} catch (err) {
task.error = `${err.name}: ${err.message}`;
logs.push(`[${isoNow()}] ❌ Synced "${binaryName}" fail, ${task.error}, log: ${logUrl}`);
logs.push(
`[${isoNow()}] ❌ Synced "${binaryName}" fail, ${task.error}, log: ${logUrl}`
);
logs.push(`[${isoNow()}] ❌❌❌❌❌ "${binaryName}" ❌❌❌❌❌`);
if (isTimeoutError(err)) {
this.logger.warn('[BinarySyncerService.executeTask:fail] taskId: %s, targetName: %s, %s',
task.taskId, task.targetName, task.error);
this.logger.warn(
'[BinarySyncerService.executeTask:fail] taskId: %s, targetName: %s, %s',
task.taskId,
task.targetName,
task.error
);
this.logger.warn(err);
} else {
this.logger.error('[BinarySyncerService.executeTask:fail] taskId: %s, targetName: %s, %s',
task.taskId, task.targetName, task.error);
this.logger.error(
'[BinarySyncerService.executeTask:fail] taskId: %s, targetName: %s, %s',
task.taskId,
task.targetName,
task.error
);
this.logger.error(err);
}
await binaryAdapter.finishFetch(false, binaryName);
@@ -153,62 +234,102 @@ export class BinarySyncerService extends AbstractService {
}
}
private async syncDir(binaryAdapter: AbstractBinary, task: Task, dir: string, parentIndex = '', latestVersionParent = '/') {
private async syncDir(
binaryAdapter: AbstractBinary,
task: SyncBinaryTask,
dir: string,
parentIndex = '',
latestVersionParent = '/'
) {
const binaryName = task.targetName as BinaryName;
const result = await binaryAdapter.fetch(dir, binaryName);
const result = await binaryAdapter.fetch(dir, binaryName, task.data);
let hasDownloadError = false;
let hasItems = false;
if (result && result.items.length > 0) {
hasItems = true;
let logs: string[] = [];
const { newItems, latestVersionDir } = await this.diff(binaryName, dir, result.items, latestVersionParent);
logs.push(`[${isoNow()}][${dir}] 🚧 Syncing diff: ${result.items.length} => ${newItems.length}, Binary class: ${binaryAdapter.constructor.name}`);
const { newItems, latestVersionDir } = await this.diff(
binaryName,
dir,
result.items,
latestVersionParent
);
logs.push(
`[${isoNow()}][${dir}] 🚧 Syncing diff: ${result.items.length} => ${newItems.length}, Binary class: ${binaryAdapter.constructor.name}`
);
// re-check latest version
for (const [ index, { item, reason }] of newItems.entries()) {
for (const [index, { item, reason }] of newItems.entries()) {
if (item.isDir) {
logs.push(`[${isoNow()}][${dir}] 🚧 [${parentIndex}${index}] Start sync dir ${JSON.stringify(item)}, reason: ${reason}`);
logs.push(
`[${isoNow()}][${dir}] 🚧 [${parentIndex}${index}] Start sync dir ${JSON.stringify(item)}, reason: ${reason}`
);
await this.taskService.appendTaskLog(task, logs.join('\n'));
logs = [];
const [ hasError, hasSubItems ] = await this.syncDir(binaryAdapter, task, `${dir}${item.name}`, `${parentIndex}${index}.`, latestVersionDir);
const [hasError, hasSubItems] = await this.syncDir(
binaryAdapter,
task,
`${dir}${item.name}`,
`${parentIndex}${index}.`,
latestVersionDir
);
if (hasError) {
hasDownloadError = true;
} else {
} else if (hasSubItems) {
// if any file download error, let dir sync again next time
// if empty dir, don't save it
if (hasSubItems) {
await this.saveBinaryItem(item);
}
await this.saveBinaryItem(item);
}
} else {
// download to nfs
logs.push(`[${isoNow()}][${dir}] 🚧 [${parentIndex}${index}] Downloading ${JSON.stringify(item)}, reason: ${reason}`);
logs.push(
`[${isoNow()}][${dir}] 🚧 [${parentIndex}${index}] Downloading ${JSON.stringify(item)}, reason: ${reason}`
);
// skip exists binary file
const existsBinary = await this.binaryRepository.findBinary(item.category, item.parent, item.name);
const existsBinary = await this.binaryRepository.findBinary(
item.category,
item.parent,
item.name
);
if (existsBinary && existsBinary.date === item.date) {
logs.push(`[${isoNow()}][${dir}] 🟢 [${parentIndex}${index}] binary file exists, skip download, binaryId: ${existsBinary.binaryId}`);
this.logger.info('[BinarySyncerService.syncDir:skipDownload] binaryId: %s exists, storePath: %s',
existsBinary.binaryId, existsBinary.storePath);
logs.push(
`[${isoNow()}][${dir}] 🟢 [${parentIndex}${index}] binary file exists, skip download, binaryId: ${existsBinary.binaryId}`
);
this.logger.info(
'[BinarySyncerService.syncDir:skipDownload] binaryId: %s exists, storePath: %s',
existsBinary.binaryId,
existsBinary.storePath
);
continue;
}
await this.taskService.appendTaskLog(task, logs.join('\n'));
logs = [];
let localFile = '';
try {
const { tmpfile, headers, timing } =
await downloadToTempfile(
this.httpclient, this.config.dataDir, item.sourceUrl!, { ignoreDownloadStatuses: item.ignoreDownloadStatuses });
const { tmpfile, headers, timing } = await downloadToTempfile(
this.httpClient,
this.config.dataDir,
item.sourceUrl,
{ ignoreDownloadStatuses: item.ignoreDownloadStatuses }
);
const log = `[${isoNow()}][${dir}] 🟢 [${parentIndex}${index}] HTTP content-length: ${headers['content-length']}, timing: ${JSON.stringify(timing)}, ${item.sourceUrl} => ${tmpfile}`;
logs.push(log);
this.logger.info('[BinarySyncerService.syncDir:downloadToTempfile] %s', log);
this.logger.info(
'[BinarySyncerService.syncDir:downloadToTempfile] %s',
log
);
localFile = tmpfile;
const binary = await this.saveBinaryItem(item, tmpfile);
logs.push(`[${isoNow()}][${dir}] 🟢 [${parentIndex}${index}] Synced file success, binaryId: ${binary.binaryId}`);
logs.push(
`[${isoNow()}][${dir}] 🟢 [${parentIndex}${index}] Synced file success, binaryId: ${binary.binaryId}`
);
await this.taskService.appendTaskLog(task, logs.join('\n'));
logs = [];
} catch (err: any) {
} catch (err) {
if (err.name === 'DownloadNotFoundError') {
this.logger.info('Not found %s, skip it', item.sourceUrl);
logs.push(`[${isoNow()}][${dir}] 🧪️ [${parentIndex}${index}] Download ${item.sourceUrl} not found, skip it`);
logs.push(
`[${isoNow()}][${dir}] 🧪️ [${parentIndex}${index}] Download ${item.sourceUrl} not found, skip it`
);
} else {
if (err.name === 'DownloadStatusInvalidError') {
this.logger.warn('Download binary %s %s', item.sourceUrl, err);
@@ -216,7 +337,9 @@ export class BinarySyncerService extends AbstractService {
this.logger.error('Download binary %s %s', item.sourceUrl, err);
}
hasDownloadError = true;
logs.push(`[${isoNow()}][${dir}] ❌ [${parentIndex}${index}] Download ${item.sourceUrl} error: ${err}`);
logs.push(
`[${isoNow()}][${dir}] ❌ [${parentIndex}${index}] Download ${item.sourceUrl} error: ${err}`
);
}
await this.taskService.appendTaskLog(task, logs.join('\n'));
logs = [];
@@ -230,20 +353,29 @@ export class BinarySyncerService extends AbstractService {
if (hasDownloadError) {
logs.push(`[${isoNow()}][${dir}] ❌ Synced dir fail`);
} else {
logs.push(`[${isoNow()}][${dir}] 🟢 Synced dir success, hasItems: ${hasItems}`);
logs.push(
`[${isoNow()}][${dir}] 🟢 Synced dir success, hasItems: ${hasItems}`
);
}
await this.taskService.appendTaskLog(task, logs.join('\n'));
}
return [ hasDownloadError, hasItems ];
return [hasDownloadError, hasItems];
}
// see https://github.com/cnpm/cnpmcore/issues/556
// 上游可能正在发布新版本、同步流程中断,导致同步的时候,文件列表不一致
// 如果的当前目录命中 latestVersionParent 父目录,那么就再校验一下当前目录
// 如果 existsItems 为空或者经过修改,那么就不需要 revalidate 了
private async diff(binaryName: BinaryName, dir: string, fetchItems: BinaryItem[], latestVersionParent = '/') {
const existsItems = await this.binaryRepository.listBinaries(binaryName, dir);
private async diff(
binaryName: BinaryName,
dir: string,
fetchItems: BinaryItem[],
latestVersionParent = '/'
) {
const existsItems = await this.binaryRepository.listBinaries(
binaryName,
dir
);
const existsMap = new Map<string, Binary>();
for (const item of existsItems) {
existsMap.set(item.name, item);
@@ -276,7 +408,7 @@ export class BinarySyncerService extends AbstractService {
existsItem.date = item.date;
} else if (dir.endsWith(latestVersionParent)) {
if (!latestItem) {
latestItem = sortBy(fetchItems, [ 'date' ]).pop();
latestItem = sortBy(fetchItems, ['date']).pop();
}
const isLatestItem = latestItem?.name === item.name;
if (isLatestItem && existsItem.isDir) {
@@ -289,7 +421,6 @@ export class BinarySyncerService extends AbstractService {
}
}
return {
newItems: diffItems,
latestVersionDir: latestVersionParent,
@@ -301,22 +432,35 @@ export class BinarySyncerService extends AbstractService {
const stat = await fs.stat(tmpfile);
binary.size = stat.size;
await this.nfsAdapter.uploadFile(binary.storePath, tmpfile);
this.logger.info('[BinarySyncerService.saveBinaryItem:uploadFile] binaryId: %s, size: %d, %s => %s',
binary.binaryId, stat.size, tmpfile, binary.storePath);
this.logger.info(
'[BinarySyncerService.saveBinaryItem:uploadFile] binaryId: %s, size: %d, %s => %s',
binary.binaryId,
stat.size,
tmpfile,
binary.storePath
);
}
await this.binaryRepository.saveBinary(binary);
return binary;
}
private async getBinaryAdapter(binaryName: BinaryName): Promise<AbstractBinary | undefined> {
private async getBinaryAdapter(
binaryName: BinaryName
): Promise<AbstractBinary | undefined> {
const config = this.config.cnpmcore;
const binaryConfig = binaries[binaryName];
let binaryAdapter: AbstractBinary;
if (config.sourceRegistryIsCNpm) {
binaryAdapter = await this.eggObjectFactory.getEggObject(AbstractBinary, BinaryType.Api);
binaryAdapter = await this.eggObjectFactory.getEggObject(
AbstractBinary,
BinaryType.Api
);
} else {
binaryAdapter = await this.eggObjectFactory.getEggObject(AbstractBinary, binaryConfig.type);
binaryAdapter = await this.eggObjectFactory.getEggObject(
AbstractBinary,
binaryConfig.type
);
}
await binaryAdapter.initFetch(binaryName);
return binaryAdapter;

View File

@@ -1,13 +1,15 @@
import { AccessLevel, SingletonProto, Inject } from '@eggjs/tegg';
import { EggLogger } from 'egg';
import { AccessLevel, Inject, SingletonProto, Logger } from 'egg';
import pMap from 'p-map';
import { BugVersion } from '../entity/BugVersion';
import { PackageJSONType, PackageRepository } from '../../repository/PackageRepository';
import { DistRepository } from '../../repository/DistRepository';
import { getScopeAndName } from '../../common/PackageUtil';
import { CacheService } from './CacheService';
import { BUG_VERSIONS, LATEST_TAG } from '../../common/constants';
import { BugVersionStore } from '../../common/adapter/BugVersionStore';
import { BugVersion } from '../entity/BugVersion.ts';
import type {
PackageJSONType,
PackageRepository,
} from '../../repository/PackageRepository.ts';
import type { DistRepository } from '../../repository/DistRepository.ts';
import { getScopeAndName } from '../../common/PackageUtil.ts';
import type { CacheService } from './CacheService.ts';
import { BUG_VERSIONS, LATEST_TAG } from '../../common/constants.ts';
import type { BugVersionStore } from '../../common/adapter/BugVersionStore.ts';
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
@@ -20,7 +22,7 @@ export class BugVersionService {
private readonly distRepository: DistRepository;
@Inject()
private readonly logger: EggLogger;
private readonly logger: Logger;
@Inject()
private readonly cacheService: CacheService;
@@ -33,66 +35,109 @@ export class BugVersionService {
const pkg = await this.packageRepository.findPackage('', BUG_VERSIONS);
if (!pkg) return;
/* c8 ignore next 10 */
const tag = await this.packageRepository.findPackageTag(pkg!.packageId, LATEST_TAG);
const tag = await this.packageRepository.findPackageTag(
pkg.packageId,
LATEST_TAG
);
if (!tag) return;
let bugVersion = this.bugVersionStore.getBugVersion(tag!.version);
let bugVersion = this.bugVersionStore.getBugVersion(tag.version);
if (!bugVersion) {
const packageVersionJson = (await this.distRepository.findPackageVersionManifest(pkg!.packageId, tag!.version)) as PackageJSONType;
const packageVersionJson =
(await this.distRepository.findPackageVersionManifest(
pkg.packageId,
tag.version
)) as PackageJSONType;
if (!packageVersionJson) return;
const data = packageVersionJson.config?.['bug-versions'];
bugVersion = new BugVersion(data || {});
this.bugVersionStore.setBugVersion(bugVersion, tag!.version);
this.bugVersionStore.setBugVersion(bugVersion, tag.version);
}
return bugVersion;
}
async cleanBugVersionPackageCaches(bugVersion: BugVersion) {
const fullnames = bugVersion.listAllPackagesHasBugs();
await pMap(fullnames, async fullname => {
await this.cacheService.removeCache(fullname);
}, {
concurrency: 50,
stopOnError: false,
});
await pMap(
fullnames,
async fullname => {
await this.cacheService.removeCache(fullname);
},
{
concurrency: 50,
stopOnError: false,
}
);
}
async fixPackageBugVersions(bugVersion: BugVersion, fullname: string, manifests: Record<string, any>) {
async fixPackageBugVersions(
bugVersion: BugVersion,
fullname: string,
// oxlint-disable-next-line typescript-eslint/no-explicit-any
manifests: Record<string, any>
) {
// If package all version unpublished(like pinyin-tool), versions is undefined
if (!manifests) return;
for (const manifest of Object.values(manifests)) {
this.fixPackageBugVersionWithAllVersions(fullname, bugVersion, manifest, manifests);
this.fixPackageBugVersionWithAllVersions(
fullname,
bugVersion,
manifest,
manifests
);
}
}
async fixPackageBugVersion(bugVersion: BugVersion, fullname: string, manifest: any) {
async fixPackageBugVersion(
bugVersion: BugVersion,
fullname: string,
// oxlint-disable-next-line typescript-eslint/no-explicit-any
manifest: any
) {
const advice = bugVersion.fixVersion(fullname, manifest.version);
if (!advice) {
return manifest;
}
const [ scope, name ] = getScopeAndName(fullname);
const [scope, name] = getScopeAndName(fullname);
const pkg = await this.packageRepository.findPackage(scope, name);
if (!pkg) {
return manifest;
}
const packageVersion = await this.packageRepository.findPackageVersion(pkg.packageId, advice.version);
const packageVersion = await this.packageRepository.findPackageVersion(
pkg.packageId,
advice.version
);
if (!packageVersion) {
return manifest;
}
const fixedManifest = await this.distRepository.findPackageVersionManifest(packageVersion.packageId, advice.version);
const fixedManifest = await this.distRepository.findPackageVersionManifest(
packageVersion.packageId,
advice.version
);
if (!fixedManifest) {
return manifest;
}
return bugVersion.fixManifest(manifest, fixedManifest);
}
private fixPackageBugVersionWithAllVersions(fullname: string, bugVersion: BugVersion, manifest: any, manifests: Record<string, any>) {
private fixPackageBugVersionWithAllVersions(
fullname: string,
bugVersion: BugVersion,
// oxlint-disable-next-line typescript-eslint/no-explicit-any
manifest: any,
// oxlint-disable-next-line typescript-eslint/no-explicit-any
manifests: Record<string, any>
) {
const advice = bugVersion.fixVersion(fullname, manifest.version);
if (!advice) {
return;
}
const fixedManifest = manifests[advice.version];
if (!fixedManifest) {
this.logger.warn('[BugVersionService] not found pkg for %s@%s manifest', fullname, advice.version);
this.logger.warn(
'[BugVersionService] not found pkg for %s@%s manifest',
fullname,
advice.version
);
return;
}
const newManifest = bugVersion.fixManifest(manifest, fixedManifest);

View File

@@ -1,13 +1,10 @@
import {
AccessLevel,
SingletonProto,
Inject,
} from '@eggjs/tegg';
import { CacheAdapter } from '../../common/adapter/CacheAdapter';
import { AbstractService } from '../../common/AbstractService';
import { ChangesStreamTaskData } from '../entity/Task';
import { AccessLevel, Inject, SingletonProto } from 'egg';
type PackageCacheAttribe = 'etag' | 'manifests';
import type { CacheAdapter } from '../../common/adapter/CacheAdapter.ts';
import { AbstractService } from '../../common/AbstractService.ts';
import type { ChangesStreamTaskData } from '../entity/Task.ts';
type PackageCacheAttribute = 'etag' | 'manifests';
export type UpstreamRegistryInfo = {
registry_name: string;
@@ -15,7 +12,7 @@ export type UpstreamRegistryInfo = {
changes_stream_url: string;
} & ChangesStreamTaskData;
export type DownloadInfo = {
export interface DownloadInfo {
today: number;
yesterday: number;
samedayLastweek: number;
@@ -25,9 +22,9 @@ export type DownloadInfo = {
lastweek: number;
lastmonth: number;
lastyear: number;
};
}
export type TotalData = {
export interface TotalData {
packageCount: number;
packageVersionCount: number;
lastPackage: string;
@@ -37,7 +34,7 @@ export type TotalData = {
lastChangeId: number | bigint;
cacheTime: string;
upstreamRegistries: UpstreamRegistryInfo[];
};
}
const TOTAL_DATA_KEY = '__TOTAL_DATA__';
@SingletonProto({
@@ -57,52 +54,72 @@ export class CacheService extends AbstractService {
return await this.cacheAdapter.getBytes(key);
}
public async savePackageEtagAndManifests(fullname: string, isFullManifests: boolean, etag: string, manifests: Buffer) {
public async savePackageEtagAndManifests(
fullname: string,
isFullManifests: boolean,
etag: string,
manifests: Buffer
) {
await Promise.all([
await this.cacheAdapter.set(this.cacheKey(fullname, isFullManifests, 'etag'), etag),
await this.cacheAdapter.setBytes(this.cacheKey(fullname, isFullManifests, 'manifests'), manifests),
this.cacheAdapter.set(
this.cacheKey(fullname, isFullManifests, 'etag'),
etag
),
this.cacheAdapter.setBytes(
this.cacheKey(fullname, isFullManifests, 'manifests'),
manifests
),
]);
}
public async getTotalData() {
const value = await this.cacheAdapter.get(TOTAL_DATA_KEY);
const totalData: TotalData = value ? JSON.parse(value) : {
packageCount: 0,
packageVersionCount: 0,
lastPackage: '',
lastPackageVersion: '',
download: {
today: 0,
thisweek: 0,
thismonth: 0,
thisyear: 0,
lastday: 0,
lastweek: 0,
lastmonth: 0,
lastyear: 0,
},
changesStream: {},
upstreamRegistries: [],
lastChangeId: 0,
cacheTime: '',
};
const totalData: TotalData = value
? JSON.parse(value)
: {
packageCount: 0,
packageVersionCount: 0,
lastPackage: '',
lastPackageVersion: '',
download: {
today: 0,
thisweek: 0,
thismonth: 0,
thisyear: 0,
lastday: 0,
lastweek: 0,
lastmonth: 0,
lastyear: 0,
},
changesStream: {},
upstreamRegistries: [],
lastChangeId: 0,
cacheTime: '',
};
return totalData;
}
public async saveTotalData(totalData: TotalData) {
return await this.cacheAdapter.set(TOTAL_DATA_KEY, JSON.stringify(totalData));
return await this.cacheAdapter.set(
TOTAL_DATA_KEY,
JSON.stringify(totalData)
);
}
public async removeCache(fullname: string) {
await Promise.all([
await this.cacheAdapter.delete(this.cacheKey(fullname, true, 'etag')),
await this.cacheAdapter.delete(this.cacheKey(fullname, true, 'manifests')),
await this.cacheAdapter.delete(this.cacheKey(fullname, false, 'etag')),
await this.cacheAdapter.delete(this.cacheKey(fullname, false, 'manifests')),
this.cacheAdapter.delete(this.cacheKey(fullname, true, 'etag')),
this.cacheAdapter.delete(this.cacheKey(fullname, true, 'manifests')),
this.cacheAdapter.delete(this.cacheKey(fullname, false, 'etag')),
this.cacheAdapter.delete(this.cacheKey(fullname, false, 'manifests')),
]);
}
private cacheKey(fullname: string, isFullManifests: boolean, attribute: PackageCacheAttribe) {
private cacheKey(
fullname: string,
isFullManifests: boolean,
attribute: PackageCacheAttribute
) {
return `${fullname}|${isFullManifests ? 'full' : 'abbr'}:${attribute}`;
}
}

View File

@@ -1,26 +1,31 @@
import os from 'node:os';
import { setTimeout } from 'node:timers/promises';
import {
AccessLevel,
SingletonProto,
EggObjectFactory,
Inject,
} from '@eggjs/tegg';
import { E500 } from 'egg-errors';
import { PackageSyncerService, RegistryNotMatchError } from './PackageSyncerService';
import { TaskService } from './TaskService';
import { RegistryManagerService } from './RegistryManagerService';
import { ScopeManagerService } from './ScopeManagerService';
import { PackageRepository } from '../../repository/PackageRepository';
import { TaskRepository } from '../../repository/TaskRepository';
import { HOST_NAME, ChangesStreamTask, Task } from '../entity/Task';
import { Registry } from '../entity/Registry';
import { AbstractChangeStream } from '../../common/adapter/changesStream/AbstractChangesStream';
import { getScopeAndName } from '../../common/PackageUtil';
import { isTimeoutError } from '../../common/ErrorUtil';
import { GLOBAL_WORKER } from '../../common/constants';
import { TaskState, TaskType } from '../../common/enum/Task';
import { AbstractService } from '../../common/AbstractService';
SingletonProto,
type EggObjectFactory,
} from 'egg';
import { E500 } from 'egg/errors';
import {
RegistryNotMatchError,
type PackageSyncerService,
} from './PackageSyncerService.ts';
import type { TaskService } from './TaskService.ts';
import type { RegistryManagerService } from './RegistryManagerService.ts';
import type { ScopeManagerService } from './ScopeManagerService.ts';
import type { PackageRepository } from '../../repository/PackageRepository.ts';
import type { TaskRepository } from '../../repository/TaskRepository.ts';
import { HOST_NAME, Task, type ChangesStreamTask } from '../entity/Task.ts';
import type { Registry } from '../entity/Registry.ts';
import { AbstractChangeStream } from '../../common/adapter/changesStream/AbstractChangesStream.ts';
import { getScopeAndName } from '../../common/PackageUtil.ts';
import { isTimeoutError } from '../../common/ErrorUtil.ts';
import { GLOBAL_WORKER } from '../../common/constants.ts';
import { TaskState, TaskType } from '../../common/enum/Task.ts';
import { AbstractService } from '../../common/AbstractService.ts';
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
@@ -33,9 +38,9 @@ export class ChangesStreamService extends AbstractService {
@Inject()
private readonly taskService: TaskService;
@Inject()
private readonly registryManagerService : RegistryManagerService;
private readonly registryManagerService: RegistryManagerService;
@Inject()
private readonly scopeManagerService : ScopeManagerService;
private readonly scopeManagerService: ScopeManagerService;
@Inject()
private readonly eggObjectFactory: EggObjectFactory;
@Inject()
@@ -46,14 +51,22 @@ export class ChangesStreamService extends AbstractService {
// `{registryName}_WORKER`: 自定义 scope 的同步源
public async findExecuteTask(): Promise<ChangesStreamTask | null> {
const targetName = GLOBAL_WORKER;
const globalRegistryTask = await this.taskRepository.findTaskByTargetName(targetName, TaskType.ChangesStream);
const globalRegistryTask = await this.taskRepository.findTaskByTargetName(
targetName,
TaskType.ChangesStream
);
// 如果没有配置默认同步源,先进行初始化
if (!globalRegistryTask) {
await this.taskService.createTask(Task.createChangesStream(targetName), false);
await this.taskService.createTask(
Task.createChangesStream(targetName),
false
);
}
// 自定义 scope 由 admin 手动创建
// 根据 TaskType.ChangesStream 从队列中获取
return await this.taskService.findExecuteTask(TaskType.ChangesStream) as ChangesStreamTask;
return (await this.taskService.findExecuteTask(
TaskType.ChangesStream
)) as ChangesStreamTask;
}
public async suspendSync(exit = false) {
@@ -65,10 +78,16 @@ export class ChangesStreamService extends AbstractService {
}
const authorIp = os.hostname();
// 暂停当前机器所有的 changesStream 任务
const tasks = await this.taskRepository.findTaskByAuthorIpAndType(authorIp, TaskType.ChangesStream);
const tasks = await this.taskRepository.findTaskByAuthorIpAndType(
authorIp,
TaskType.ChangesStream
);
for (const task of tasks) {
if (task.state === TaskState.Processing) {
this.logger.info('[ChangesStreamService.suspendSync:suspend] taskId: %s', task.taskId);
this.logger.info(
'[ChangesStreamService.suspendSync:suspend] taskId: %s',
task.taskId
);
// 1. 更新任务状态为 waiting
// 2. 重新推入任务队列供其他机器执行
await this.taskService.retryTask(task);
@@ -93,8 +112,14 @@ export class ChangesStreamService extends AbstractService {
// allow disable changesStream dynamic
while (since && this.config.cnpmcore.enableChangesStream) {
const { lastSince, taskCount } = await this.executeSync(since, task);
this.logger.info('[ChangesStreamService.executeTask:changes] since: %s => %s, %d new tasks, taskId: %s, updatedAt: %j',
since, lastSince, taskCount, task.taskId, task.updatedAt);
this.logger.info(
'[ChangesStreamService.executeTask:changes] since: %s => %s, %d new tasks, taskId: %s, updatedAt: %j',
since,
lastSince,
taskCount,
task.taskId,
task.updatedAt
);
since = lastSince;
if (taskCount === 0 && this.config.env === 'unittest') {
break;
@@ -102,7 +127,10 @@ export class ChangesStreamService extends AbstractService {
await setTimeout(this.config.cnpmcore.checkChangesStreamInterval);
}
} catch (err) {
this.logger.warn('[ChangesStreamService.executeTask:error] %s, exit now', err.message);
this.logger.warn(
'[ChangesStreamService.executeTask:error] %s, exit now',
err.message
);
if (isTimeoutError(err)) {
this.logger.warn(err);
} else {
@@ -119,9 +147,13 @@ export class ChangesStreamService extends AbstractService {
const { registryId } = task.data || {};
// 如果已有 registryId, 查询 DB 直接获取
if (registryId) {
const registry = await this.registryManagerService.findByRegistryId(registryId);
const registry =
await this.registryManagerService.findByRegistryId(registryId);
if (!registry) {
this.logger.error('[ChangesStreamService.getRegistry:error] registryId %s not found', registryId);
this.logger.error(
'[ChangesStreamService.getRegistry:error] registryId %s not found',
registryId
);
throw new E500(`invalid change stream registry: ${registryId}`);
}
return registry;
@@ -129,7 +161,7 @@ export class ChangesStreamService extends AbstractService {
const registry = await this.registryManagerService.ensureDefaultRegistry();
task.data = {
...(task.data || {}),
...task.data,
registryId: registry.registryId,
};
await this.taskRepository.saveTask(task);
@@ -141,9 +173,15 @@ export class ChangesStreamService extends AbstractService {
// 1. 如果该包已经指定了 registryId 则以 registryId 为准
// 1. 该包的 scope 在当前 registry 下
// 2. 如果 registry 下没有配置 scope (认为是通用 registry 地址) ,且该包的 scope 不在其他 registry 下
public async needSync(registry: Registry, fullname: string): Promise<boolean> {
const [ scopeName, name ] = getScopeAndName(fullname);
const packageEntity = await this.packageRepository.findPackage(scopeName, name);
public async needSync(
registry: Registry,
fullname: string
): Promise<boolean> {
const [scopeName, name] = getScopeAndName(fullname);
const packageEntity = await this.packageRepository.findPackage(
scopeName,
name
);
// 如果包不存在,且处在 exist 模式下,则不同步
if (this.config.cnpmcore.syncMode === 'exist' && !packageEntity) {
@@ -155,18 +193,24 @@ export class ChangesStreamService extends AbstractService {
}
const scope = await this.scopeManagerService.findByName(scopeName);
const inCurrentRegistry = scope && scope?.registryId === registry.registryId;
const inCurrentRegistry =
scope && scope?.registryId === registry.registryId;
if (inCurrentRegistry) {
return true;
}
const registryScopeCount = await this.scopeManagerService.countByRegistryId(registry.registryId);
const registryScopeCount = await this.scopeManagerService.countByRegistryId(
registry.registryId
);
// 当前包没有 scope 信息,且当前 registry 下没有 scope是通用 registry需要同步
return !scope && !registryScopeCount;
}
public async getInitialSince(task: ChangesStreamTask): Promise<string> {
const registry = await this.prepareRegistry(task);
const changesStreamAdapter = await this.eggObjectFactory.getEggObject(AbstractChangeStream, registry.type) as AbstractChangeStream;
const changesStreamAdapter = (await this.eggObjectFactory.getEggObject(
AbstractChangeStream,
registry.type
)) as AbstractChangeStream;
const since = await changesStreamAdapter.getInitialSince(registry);
return since;
}
@@ -175,7 +219,10 @@ export class ChangesStreamService extends AbstractService {
// 更新任务的 since 和 taskCount 相关字段
public async executeSync(since: string, task: ChangesStreamTask) {
const registry = await this.prepareRegistry(task);
const changesStreamAdapter = await this.eggObjectFactory.getEggObject(AbstractChangeStream, registry.type) as AbstractChangeStream;
const changesStreamAdapter = (await this.eggObjectFactory.getEggObject(
AbstractChangeStream,
registry.type
)) as AbstractChangeStream;
let taskCount = 0;
let lastSince = since;
@@ -201,17 +248,29 @@ export class ChangesStreamService extends AbstractService {
skipDependencies: true,
tips,
});
this.logger.info('[ChangesStreamService.createTask:success] fullname: %s, task: %s, tips: %s',
fullname, task.id, tips);
this.logger.info(
'[ChangesStreamService.createTask:success] fullname: %s, task: %s, tips: %s',
fullname,
task.id,
tips
);
} catch (err) {
if (err instanceof RegistryNotMatchError) {
this.logger.warn('[ChangesStreamService.executeSync:skip] fullname: %s, error: %s, tips: %s',
fullname, err, tips);
this.logger.warn(
'[ChangesStreamService.executeSync:skip] fullname: %s, error: %s, tips: %s',
fullname,
err,
tips
);
continue;
}
// only log error, make sure changes still reading
this.logger.error('[ChangesStreamService.executeSync:error] fullname: %s, error: %s, tips: %s',
fullname, err, tips);
this.logger.error(
'[ChangesStreamService.executeSync:error] fullname: %s, error: %s, tips: %s',
fullname,
err,
tips
);
this.logger.error(err);
continue;
}

View File

@@ -1,16 +1,17 @@
import { AccessLevel, SingletonProto, Inject } from '@eggjs/tegg';
import { AbstractService } from '../../common/AbstractService';
import { HookType } from '../../common/enum/Hook';
import { TaskState } from '../../common/enum/Task';
import { HookEvent } from '../entity/HookEvent';
import { CreateHookTask, Task } from '../entity/Task';
import { HookRepository } from '../../repository/HookRepository';
import { PackageRepository } from '../../repository/PackageRepository';
import { AccessLevel, Inject, SingletonProto } from 'egg';
import pMap from 'p-map';
import { Hook } from '../entity/Hook';
import { TaskService } from './TaskService';
import { isoNow } from '../../common/LogUtil';
import { getScopeAndName } from '../../common/PackageUtil';
import { AbstractService } from '../../common/AbstractService.ts';
import { HookType } from '../../common/enum/Hook.ts';
import { TaskState } from '../../common/enum/Task.ts';
import { Task, type CreateHookTask } from '../entity/Task.ts';
import type { HookEvent } from '../entity/HookEvent.ts';
import type { HookRepository } from '../../repository/HookRepository.ts';
import type { PackageRepository } from '../../repository/PackageRepository.ts';
import type { Hook } from '../entity/Hook.ts';
import type { TaskService } from './TaskService.ts';
import { isoNow } from '../../common/LogUtil.ts';
import { getScopeAndName } from '../../common/PackageUtil.ts';
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
@@ -27,10 +28,14 @@ export class CreateHookTriggerService extends AbstractService {
async executeTask(task: CreateHookTask): Promise<void> {
const { hookEvent } = task.data;
const [ scope, name ] = getScopeAndName(hookEvent.fullname);
const [scope, name] = getScopeAndName(hookEvent.fullname);
const pkg = await this.packageRepository.findPackage(scope, name);
if (!pkg) {
await this.taskService.finishTask(task, TaskState.Success, `[${isoNow()}][Hooks] package ${hookEvent.fullname} not exits`);
await this.taskService.finishTask(
task,
TaskState.Success,
`[${isoNow()}][Hooks] package ${hookEvent.fullname} not exits`
);
return;
}
@@ -38,41 +43,97 @@ export class CreateHookTriggerService extends AbstractService {
`[${isoNow()}][Hooks] Start Create Trigger for ${pkg.fullname} ${task.data.hookEvent.changeId}`,
`[${isoNow()}][Hooks] change content ${JSON.stringify(task.data.hookEvent.change)}`,
];
await this.taskService.finishTask(task, TaskState.Processing, startLog.join('\n'));
await this.taskService.finishTask(
task,
TaskState.Processing,
startLog.join('\n')
);
try {
await this.taskService.appendTaskLog(task, `[${isoNow()}][Hooks] PushHooks to ${HookType.Package} ${pkg.fullname}\n`);
await this.createTriggerByMethod(task, HookType.Package, pkg.fullname, hookEvent);
await this.taskService.appendTaskLog(task, `[${isoNow()}][Hooks] PushHooks to ${HookType.Scope} ${pkg.scope}\n`);
await this.createTriggerByMethod(task, HookType.Scope, pkg.scope, hookEvent);
await this.taskService.appendTaskLog(
task,
`[${isoNow()}][Hooks] PushHooks to ${HookType.Package} ${pkg.fullname}\n`
);
await this.createTriggerByMethod(
task,
HookType.Package,
pkg.fullname,
hookEvent
);
await this.taskService.appendTaskLog(
task,
`[${isoNow()}][Hooks] PushHooks to ${HookType.Scope} ${pkg.scope}\n`
);
await this.createTriggerByMethod(
task,
HookType.Scope,
pkg.scope,
hookEvent
);
const maintainers = await this.packageRepository.listPackageMaintainers(pkg.packageId);
const maintainers = await this.packageRepository.listPackageMaintainers(
pkg.packageId
);
for (const maintainer of maintainers) {
await this.taskService.appendTaskLog(task, `[${isoNow()}][Hooks] PushHooks to ${HookType.Owner} ${maintainer.name}\n`);
await this.createTriggerByMethod(task, HookType.Owner, maintainer.name, hookEvent);
await this.taskService.appendTaskLog(
task,
`[${isoNow()}][Hooks] PushHooks to ${HookType.Owner} ${maintainer.name}\n`
);
await this.createTriggerByMethod(
task,
HookType.Owner,
maintainer.name,
hookEvent
);
}
await this.taskService.finishTask(task, TaskState.Success, `[${isoNow()}][Hooks] create trigger succeed \n`);
await this.taskService.finishTask(
task,
TaskState.Success,
`[${isoNow()}][Hooks] create trigger succeed \n`
);
} catch (e) {
e.message = 'create trigger failed: ' + e.message;
await this.taskService.finishTask(task, TaskState.Fail, `[${isoNow()}][Hooks] ${e.stack} \n`);
e.message = `create trigger failed: ${e.message}`;
await this.taskService.finishTask(
task,
TaskState.Fail,
`[${isoNow()}][Hooks] ${e.stack} \n`
);
return;
}
}
private async createTriggerByMethod(task: Task, type: HookType, name: string, hookEvent: HookEvent) {
private async createTriggerByMethod(
task: Task,
type: HookType,
name: string,
hookEvent: HookEvent
) {
let hooks = await this.hookRepository.listHooksByTypeAndName(type, name);
while (hooks.length) {
while (hooks.length > 0) {
await this.createTriggerTasks(hooks, hookEvent);
hooks = await this.hookRepository.listHooksByTypeAndName(type, name, hooks[hooks.length - 1].id);
await this.taskService.appendTaskLog(task,
`[${isoNow()}][Hooks] PushHooks to ${type} ${name} ${hooks.length} \n`);
hooks = await this.hookRepository.listHooksByTypeAndName(
type,
name,
hooks[hooks.length - 1].id
);
await this.taskService.appendTaskLog(
task,
`[${isoNow()}][Hooks] PushHooks to ${type} ${name} ${hooks.length} \n`
);
}
}
private async createTriggerTasks(hooks: Array<Hook>, hookEvent: HookEvent) {
await pMap(hooks, async hook => {
const triggerHookTask = Task.createTriggerHookTask(hookEvent, hook.hookId);
await this.taskService.createTask(triggerHookTask, true);
}, { concurrency: 5 });
private async createTriggerTasks(hooks: Hook[], hookEvent: HookEvent) {
await pMap(
hooks,
async hook => {
const triggerHookTask = Task.createTriggerHookTask(
hookEvent,
hook.hookId
);
await this.taskService.createTask(triggerHookTask, true);
},
{ concurrency: 5 }
);
}
}

View File

@@ -1,7 +1,10 @@
import { ContextEventBus, Inject } from '@eggjs/tegg';
import { Advice, IAdvice } from '@eggjs/tegg/aop';
@Advice()
import { Inject, ObjectInitType, type ContextEventBus } from 'egg';
import { Advice, type IAdvice } from 'egg/aop';
@Advice({
initType: ObjectInitType.CONTEXT,
})
export class EventCorkAdvice implements IAdvice {
@Inject()
private eventBus: ContextEventBus;

View File

@@ -1,8 +1,8 @@
import { SingletonProto, AccessLevel, Inject } from '@eggjs/tegg';
import { EggLogger } from 'egg';
import { AccessLevel, Inject, SingletonProto, Logger } from 'egg';
import pMap from 'p-map';
import { PackageVersionRepository } from '../../repository/PackageVersionRepository';
import { PaddingSemVer } from '../entity/PaddingSemVer';
import type { PackageVersionRepository } from '../../repository/PackageVersionRepository.ts';
import { PaddingSemVer } from '../entity/PaddingSemVer.ts';
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
@@ -12,22 +12,35 @@ export class FixNoPaddingVersionService {
private readonly packageVersionRepository: PackageVersionRepository;
@Inject()
private readonly logger: EggLogger;
private readonly logger: Logger;
async fixPaddingVersion(id?: number): Promise<void> {
// eslint-disable-next-line no-constant-condition
while (true) {
const packageVersions = await this.packageVersionRepository.findHaveNotPaddingVersion(id);
const packageVersions =
await this.packageVersionRepository.findHaveNotPaddingVersion(id);
if (packageVersions.length === 0) {
break;
}
id = packageVersions[packageVersions.length - 1].id as unknown as number + 1;
this.logger.info('[FixNoPaddingVersionService] fix padding version ids %j', packageVersions.map(t => t.id));
const lastVersion = packageVersions[packageVersions.length - 1];
id =
(lastVersion.id as unknown as number) +
1;
this.logger.info(
'[FixNoPaddingVersionService] fix padding version ids %j',
packageVersions.map(t => t.id)
);
await pMap(packageVersions, async packageVersion => {
const paddingSemver = new PaddingSemVer(packageVersion.version);
await this.packageVersionRepository.fixPaddingVersion(packageVersion.packageVersionId, paddingSemver);
}, { concurrency: 30 });
await pMap(
packageVersions,
async packageVersion => {
const paddingSemver = new PaddingSemVer(packageVersion.version);
await this.packageVersionRepository.fixPaddingVersion(
packageVersion.packageVersionId,
paddingSemver
);
},
{ concurrency: 30 }
);
}
}
}

Some files were not shown because too many files have changed in this diff Show More