Compare commits

...

234 Commits

Author SHA1 Message Date
semantic-release-bot
0a0c4e72ae Release 3.62.0
[skip ci]

## [3.62.0](https://github.com/cnpm/cnpmcore/compare/v3.61.6...v3.62.0) (2024-09-22)

### Features

* set allowH2 to true and require Node.js >= 18 ([#705](https://github.com/cnpm/cnpmcore/issues/705)) ([9a79940](9a7994090b))
2024-09-22 15:49:18 +00:00
fengmk2
9a7994090b feat: set allowH2 to true and require Node.js >= 18 (#705)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced a new configuration option allowing the use of HTTP/2 in
the HTTP client.
  
- **Updates**
- Updated the minimum required Node.js version to 18.20.0 for improved
performance and compatibility.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-09-22 23:47:37 +08:00
semantic-release-bot
c1de249445 Release 3.61.6
[skip ci]

## [3.61.6](https://github.com/cnpm/cnpmcore/compare/v3.61.5...v3.61.6) (2024-07-11)

### Bug Fixes

* ignore hook when disable ([#702](https://github.com/cnpm/cnpmcore/issues/702)) ([bd49917](bd49917b86))
2024-07-11 13:28:17 +00:00
elrrrrrrr
bd49917b86 fix: ignore hook when disable (#702)
> Currently, `triggerHookWorkerMaxConcurrentTasks` is 10 by defualt,
which can lead to some redis queries even hookEnable is not activated.
* ♻️ Follow `CreateTriggerHookWorker`, when hookEnable is not activated,
do not query task queue.
-------

> 目前 triggerHookWorkerMaxConcurrentTasks 默认为 10,在未开启 hookEnable
时会带来一些冗余的 redis 查询
* ♻️ 参照 `CreateTriggerHookWorker` 逻辑,hookEnable 关闭时,不进行存量任务轮训

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

## Summary by CodeRabbit

- **Bug Fixes**
- Added a check to ensure hooks are enabled before proceeding, improving
reliability and preventing errors when hooks are disabled.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-07-11 21:26:52 +08:00
semantic-release-bot
381a10cd6e Release 3.61.5
[skip ci]

## [3.61.5](https://github.com/cnpm/cnpmcore/compare/v3.61.4...v3.61.5) (2024-06-25)

### Bug Fixes

* auto remove blocked package on sync process ([#700](https://github.com/cnpm/cnpmcore/issues/700)) ([ca6ce4e](ca6ce4e860))
2024-06-25 12:30:37 +00:00
elrrrrrrr
ca6ce4e860 fix: auto remove blocked package on sync process (#700)
> remove es index when the target pkg has been blocked or deleted.
1.  update `SyncESPackage` logic for event register
2. ♻️ do the block check when `syncPackage` 
---------
> 包删除或被 block 时,es 索引同步删除
1.  修改 `SyncESPackage` 逻辑,更新事件注册
2. ♻️ `syncPackage` 时,重新判断包是否被 block,用于清理

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

## Summary by CodeRabbit

- **New Features**
- Introduced event types `PACKAGE_BLOCKED` and `PACKAGE_UNBLOCKED` for
better package management.

- **Bug Fixes**
- Improved logic to handle package blocks before syncing, ensuring
smoother operations.

- **Tests**
- Added test scenarios to cover new package blocking and unblocking
features, enhancing reliability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-06-25 20:28:55 +08:00
semantic-release-bot
571d265065 Release 3.61.4
[skip ci]

## [3.61.4](https://github.com/cnpm/cnpmcore/compare/v3.61.3...v3.61.4) (2024-06-14)

### Bug Fixes

* perf diff ([#699](https://github.com/cnpm/cnpmcore/issues/699)) ([753e519](753e519f17)), closes [#698](https://github.com/cnpm/cnpmcore/issues/698)
2024-06-14 01:43:06 +00:00
elrrrrrrr
753e519f17 fix: perf diff (#699)
> optimize binary sync perf , closes #698 
* ♻️ calculate the latestItem only once for the same fetchItems.
--------
> binary 同步性能优化,close #698

* ♻️ binary 最新版本比对时,相同 fetchItems 仅计算一次。

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

- **Bug Fixes**
- Improved logic for identifying the latest item in synchronization
processes, ensuring more accurate date comparisons.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-06-14 09:41:43 +08:00
elrrrrrrr
038736dd60 chore: trends api (#697)
* 📚 add doc for the trends API.
---------
* 📚 添加 trends api 相关文档


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

## Summary by CodeRabbit

- **Documentation**
- Added sections for `[Token]` and `[Trend]` in the Registry API
documentation.
- Introduced a new endpoint `GET /downloads/range/:start::end/:pkgName`
for fetching package download trends.
	- Included sample response data for the `Trend` endpoint.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-06-03 22:51:46 +08:00
semantic-release-bot
317e24da55 Release 3.61.3
[skip ci]

## [3.61.3](https://github.com/cnpm/cnpmcore/compare/v3.61.2...v3.61.3) (2024-05-30)

### Bug Fixes

* force detect unpkg-white-list ([#695](https://github.com/cnpm/cnpmcore/issues/695)) ([9664504](9664504151))
2024-05-30 06:22:04 +00:00
fengmk2
9664504151 fix: force detect unpkg-white-list (#695) 2024-05-30 14:20:48 +08:00
semantic-release-bot
dcc5509dac Release 3.61.2
[skip ci]

## [3.61.2](https://github.com/cnpm/cnpmcore/compare/v3.61.1...v3.61.2) (2024-05-30)

### Bug Fixes

* support + wildcast ([#694](https://github.com/cnpm/cnpmcore/issues/694)) ([c8f5ee8](c8f5ee82f1)), closes [#692](https://github.com/cnpm/cnpmcore/issues/692)
2024-05-30 03:53:37 +00:00
elrrrrrrr
c8f5ee82f1 fix: support + wildcast (#694)
> Update #692, Declare compatibility using + notation

1. 🤖 Ensure version matching by `semver`
---------

> 更新 #692 , 兼容版本声明为 `+` 的场景
1. 🤖 统一通过 `semver` 进行版本匹配判断

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

## Summary by CodeRabbit

- **New Features**
- Added support for prerelease versions when checking package version
compatibility.

- **Tests**
- Updated test cases to include operations related to a new object `baz`
with version `*`.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-05-30 11:51:56 +08:00
semantic-release-bot
cacf5e9da3 Release 3.61.1
[skip ci]

## [3.61.1](https://github.com/cnpm/cnpmcore/compare/v3.61.0...v3.61.1) (2024-05-30)

### Bug Fixes

* rc version should match `*` version ([#692](https://github.com/cnpm/cnpmcore/issues/692)) ([0b62238](0b6223882e))
2024-05-30 01:35:17 +00:00
fengmk2
0b6223882e fix: rc version should match * version (#692)
closes https://github.com/cnpm/unpkg-white-list/issues/63

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

## Summary by CodeRabbit

- **Bug Fixes**
- Improved package version checking to support wildcard (`*`) versions,
ensuring better compatibility and flexibility.
- Fixed issues in handling release candidate (rc) versions in package
version checks.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-05-30 09:33:51 +08:00
semantic-release-bot
9beaf4164c Release 3.61.0
[skip ci]

## [3.61.0](https://github.com/cnpm/cnpmcore/compare/v3.60.0...v3.61.0) (2024-05-25)

### Features

* sync missing acceptDependencies on abbreviated format ([#691](https://github.com/cnpm/cnpmcore/issues/691)) ([96648fd](96648fddaf))
2024-05-25 06:59:41 +00:00
fengmk2
96648fddaf feat: sync missing acceptDependencies on abbreviated format (#691)
closes https://github.com/cnpm/cnpmcore/issues/689

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

## Summary by CodeRabbit

- **New Features**
- Added support for `acceptDependencies`, `directories`, and `funding`
metadata in package management.

- **Bug Fixes**
  - Improved package metadata synchronization to include new keys.

- **Tests**
- Added new test cases to verify `acceptDependencies` metadata syncing.

- **Chores**
- Updated import statements to use the `node` namespace for better
compatibility.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-05-25 14:57:59 +08:00
semantic-release-bot
6f9f8abc16 Release 3.60.0
[skip ci]

## [3.60.0](https://github.com/cnpm/cnpmcore/compare/v3.59.1...v3.60.0) (2024-05-24)

### Features

* mirror homebrew-macos-cross-toolchains ([#690](https://github.com/cnpm/cnpmcore/issues/690)) ([a247065](a2470650d5))
2024-05-24 02:57:17 +00:00
fengmk2
a2470650d5 feat: mirror homebrew-macos-cross-toolchains (#690)
https://github.com/messense/homebrew-macos-cross-toolchains/releases

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

## Summary by CodeRabbit

- **New Features**
- Added support for `homebrew-macos-cross-toolchains` in the binaries
list, enhancing cross-toolchain capabilities for macOS users.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-05-24 10:55:27 +08:00
semantic-release-bot
8b1f526966 Release 3.59.1
[skip ci]

## [3.59.1](https://github.com/cnpm/cnpmcore/compare/v3.59.0...v3.59.1) (2024-05-19)

### Bug Fixes

* files meta only return currrent files and sub directory ([#687](https://github.com/cnpm/cnpmcore/issues/687)) ([e442580](e442580b81))
2024-05-19 02:00:04 +00:00
fengmk2
e442580b81 fix: files meta only return currrent files and sub directory (#687)
closes https://github.com/cnpm/cnpmcore/issues/680

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

## Summary by CodeRabbit

- **Bug Fixes**
- Improved file and directory handling to ensure accurate listing and
filtering of package version files.

- **Tests**
- Updated test cases to reflect the new logic for file and directory
handling, ensuring more reliable and accurate test results.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-05-19 09:58:14 +08:00
semantic-release-bot
8a927fcc2d Release 3.59.0
[skip ci]

## [3.59.0](https://github.com/cnpm/cnpmcore/compare/v3.58.1...v3.59.0) (2024-05-18)

### Features

* add unpkg-white-list to detect sync unpkg files or not ([#686](https://github.com/cnpm/cnpmcore/issues/686)) ([0530116](05301166a2))
2024-05-18 05:13:07 +00:00
fengmk2
05301166a2 feat: add unpkg-white-list to detect sync unpkg files or not (#686)
see https://github.com/cnpm/unpkg-white-list

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


- **New Features**
- Introduced a new configuration option `enableSyncUnpkgFilesWhiteList`
to enhance package version file synchronization.

- **Improvements**
- Enhanced logging in package version file operations for better
traceability.
- Simplified file redirection logic for improved performance and
readability.

- **Tests**
- Added test cases for the new `enableSyncUnpkgFilesWhiteList`
configuration to ensure reliability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-05-18 13:11:22 +08:00
fengmk2
c5c6145fda chore: update contributors
[skip ci]
2024-05-17 22:32:11 +08:00
semantic-release-bot
3383d7f403 Release 3.58.1
[skip ci]

## [3.58.1](https://github.com/cnpm/cnpmcore/compare/v3.58.0...v3.58.1) (2024-05-17)

### Bug Fixes

* remove CVE-2023-46809 revert config ([#683](https://github.com/cnpm/cnpmcore/issues/683)) ([ff00e42](ff00e42668))
2024-05-17 14:29:38 +00:00
fengmk2
ff00e42668 fix: remove CVE-2023-46809 revert config (#683)
revert https://github.com/cnpm/cnpmcore/pull/650

run test on Node.js 22

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

- **Chores**
  - Updated Node.js version support to 18, 20, and 22.
  - Removed unnecessary configuration from the release workflow.
- Cleaned up outdated security-related entries in the package
configuration.
  - Updated encryption functions in CryptoUtil.ts for improved security.
- Refactored import statements for ES module syntax in
webauthController.test.ts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-05-17 22:27:55 +08:00
semantic-release-bot
ade9305342 Release 3.58.0
[skip ci]

## [3.58.0](https://github.com/cnpm/cnpmcore/compare/v3.57.0...v3.58.0) (2024-05-16)

### Features

* support unpkg alias path access entry file [#674](https://github.com/cnpm/cnpmcore/issues/674) ([#675](https://github.com/cnpm/cnpmcore/issues/675)) ([a51891d](a51891d3b9))
2024-05-16 15:48:28 +00:00
chilingling
a51891d3b9 feat: support unpkg alias path access entry file #674 (#675)
closes https://github.com/cnpm/cnpmcore/issues/674

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

- **New Features**
- Implemented a feature to handle compatibility with unpkg by searching
for and redirecting to possible file entries if the requested file is
not found.

- **Tests**
- Added a new test case to ensure the redirection to possible file
entries functions correctly.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-05-16 23:46:51 +08:00
elrrrrrrr
65d6f4489f chore: add sync-setup (#666)
> close #664 Missing a startup document for sync pkgs.
* 📚 Add doc for sync-setup

-------

> close #664 目前缺少一个启动文档,开启包同步服务
* 📚 添加 sync-setup 使用文档

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

## Summary by CodeRabbit

- **Documentation**
- Added a new guide for setting up package synchronization services
using `cnpmcore`.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-05-16 13:26:33 +08:00
semantic-release-bot
8366ee70a4 Release 3.57.0
[skip ci]

## [3.57.0](https://github.com/cnpm/cnpmcore/compare/v3.56.2...v3.57.0) (2024-05-14)

### Features

* update playwright config ([#681](https://github.com/cnpm/cnpmcore/issues/681)) ([6bfbe35](6bfbe35c65))
2024-05-14 16:42:10 +00:00
elrrrrrrr
6bfbe35c65 feat: update playwright config (#681)
> follow
[ref](https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/registry/index.ts)
update
* 🔧 Modify Playwright `DOWNLOAD_PATHS` configuration 
* 🔧 Add `android` related configuration 
* ♻️ Remove `chromium-with-symbols` configuration and `ubuntu18.04`
related versions

---------

> 参照
[ref](https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/registry/index.ts)
变更
* 🔧 修改 playwright `DOWNLOAD_PATHS` 相关配置
* 🔧 添加 `android` 相关配置
* ♻️ 移除 `chromium-with-symbols` 配置,`ubuntu18.04` 相关版本




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

## Summary by CodeRabbit

- **New Features**
- Added support for `mac14` and `mac14-arm64` platforms for Chromium and
Chromium tip-of-tree downloads.

- **Bug Fixes**
- Updated download paths for various platforms to ensure compatibility
and correct file retrieval.

- **Tests**
- Adjusted test assertions to reflect changes in download paths and
platform support, ensuring accurate validation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-05-15 00:40:37 +08:00
semantic-release-bot
cedb959f65 Release 3.56.2
[skip ci]

## [3.56.2](https://github.com/cnpm/cnpmcore/compare/v3.56.1...v3.56.2) (2024-05-14)

### Bug Fixes

* allow to disable sync unpkg files ([#679](https://github.com/cnpm/cnpmcore/issues/679)) ([101c9b3](101c9b30b5))
2024-05-14 06:47:57 +00:00
fengmk2
101c9b30b5 fix: allow to disable sync unpkg files (#679)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Added a new configuration option `enableSyncUnpkgFiles` for enhanced
synchronization control.
  
- **Improvements**
- Improved synchronization logic to check both `enableUnpkg` and
`enableSyncUnpkgFiles` settings before proceeding.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-05-14 14:46:26 +08:00
semantic-release-bot
cdca770a0b Release 3.56.1
[skip ci]

## [3.56.1](https://github.com/cnpm/cnpmcore/compare/v3.56.0...v3.56.1) (2024-05-14)

### Bug Fixes

* only set err.status on statusCode >= 200 ([#677](https://github.com/cnpm/cnpmcore/issues/677)) ([668eed2](668eed2d50))
2024-05-14 02:30:02 +00:00
fengmk2
668eed2d50 fix: only set err.status on statusCode >= 200 (#677)
> nodejs.AssertionError: invalid status code: -1

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

## Summary by CodeRabbit

- **Bug Fixes**
- Improved error handling to ensure the HTTP status code is valid and
within the correct range.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-05-14 10:28:31 +08:00
semantic-release-bot
dbf5b5248a Release 3.56.0
[skip ci]

## [3.56.0](https://github.com/cnpm/cnpmcore/compare/v3.55.1...v3.56.0) (2024-05-11)

### Features

* mirror edgewebdriver ([#676](https://github.com/cnpm/cnpmcore/issues/676)) ([21cbc18](21cbc1849f))
2024-05-11 10:11:05 +00:00
fengmk2
21cbc1849f feat: mirror edgewebdriver (#676)
closes https://github.com/cnpm/cnpmcore/issues/594

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

## Summary by CodeRabbit

- **New Features**
- Introduced the EdgedriverBinary class to enhance the fetching and
management of Edgedriver binaries.
- Added support for the Edgedriver type in our binary management system.
- Updated the binaries configuration to include the new Edgedriver
category, complete with repository and distribution details.

- **Documentation**
- Updated documentation to reflect the addition of the Edgedriver
category and its functionalities.

- **Tests**
- Implemented new tests for the EdgedriverBinary class to ensure
functionality works as expected.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-05-11 18:09:27 +08:00
semantic-release-bot
67f1a2476d Release 3.55.1
[skip ci]

## [3.55.1](https://github.com/cnpm/cnpmcore/compare/v3.55.0...v3.55.1) (2024-04-17)

### Bug Fixes

* add logs fro aggregate errors ([#672](https://github.com/cnpm/cnpmcore/issues/672)) ([c0f96d7](c0f96d72e5))
2024-04-17 13:18:19 +00:00
elrrrrrrr
c0f96d72e5 fix: add logs fro aggregate errors (#672)
> add logs for sync tasks with the upstream registry errors
[ref](https://cdn.npmmirror.com/packages/%40eggjs/tegg-schedule-plugin/syncs/2024/04/171901-661fac1a613c4b7bd1e015e3.log)
* 📒 Log the corresponding errors for AggregateError

--------------

> 对 upstream registry 创建同步任务失败时,添加日志信息
[ref](https://cdn.npmmirror.com/packages/%40eggjs/tegg-schedule-plugin/syncs/2024/04/171901-661fac1a613c4b7bd1e015e3.log)
* 📒 针对 AggregateError 打印对应 errors



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


- **Documentation**
- Updated comments in the package synchronization service to include
information about handling `AggregateError` exceptions.
- **New Features**
- Added a retry mechanism with a limit of 3 attempts in the HTTP
requests for the NPMRegistry class.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-04-17 21:16:26 +08:00
semantic-release-bot
abad15b8e0 Release 3.55.0
[skip ci]

## [3.55.0](https://github.com/cnpm/cnpmcore/compare/v3.54.0...v3.55.0) (2024-04-15)

### Features

* use mysql2 ([#671](https://github.com/cnpm/cnpmcore/issues/671)) ([58d19b1](58d19b17f0))
2024-04-15 06:24:32 +00:00
fengmk2
58d19b17f0 feat: use mysql2 (#671)
https://github.com/cyjake/leoric/pull/419

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

## Summary by CodeRabbit

- **Refactor**
- Updated the database management system to enhance performance and
compatibility.
- **Chores**
	- Upgraded internal libraries to improve stability and security.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-04-15 14:22:53 +08:00
semantic-release-bot
b94c8efd6c Release 3.54.0
[skip ci]

## [3.54.0](https://github.com/cnpm/cnpmcore/compare/v3.53.4...v3.54.0) (2024-04-13)

### Features

* use mysql2@^3.9.4 ([#669](https://github.com/cnpm/cnpmcore/issues/669)) ([468f9e4](468f9e4e36))

### Bug Fixes

* remove unuse mysql2 deps ([#670](https://github.com/cnpm/cnpmcore/issues/670)) ([c71d185](c71d185ee1))
2024-04-13 14:24:37 +00:00
fengmk2
c71d185ee1 fix: remove unuse mysql2 deps (#670)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Chores**
	- Removed the `mysql2` dependency from the project.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-04-13 22:23:12 +08:00
fengmk2
468f9e4e36 feat: use mysql2@^3.9.4 (#669)
closes https://github.com/cnpm/cnpmcore/issues/668

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


- **Chores**
- Updated the database management package to enhance performance and
security.
	- Adjusted supported Node.js versions in the workflow configuration.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-04-13 21:51:16 +08:00
semantic-release-bot
ebc212c1c4 Release 3.53.4
[skip ci]

## [3.53.4](https://github.com/cnpm/cnpmcore/compare/v3.53.3...v3.53.4) (2024-03-28)

### Bug Fixes

* sync cypress linux-arm64 binary ([#662](https://github.com/cnpm/cnpmcore/issues/662)) ([049b186](049b186a0e))
2024-03-28 05:21:01 +00:00
fengmk2
049b186a0e fix: sync cypress linux-arm64 binary (#662)
closes https://github.com/cnpm/cnpmjs.org/issues/1560

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

## Summary by CodeRabbit

- **New Features**
- Added support for the `linux-arm64` platform in Cypress binary
handling.
- **Tests**
- Updated tests to include assertions for the newly supported
`linux-arm64` platform.
- **Chores**
- Updated Node.js version to 21 in workflow configuration for improved
job execution.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-03-28 13:19:44 +08:00
semantic-release-bot
6664189a91 Release 3.53.3
[skip ci]

## [3.53.3](https://github.com/cnpm/cnpmcore/compare/v3.53.2...v3.53.3) (2024-02-23)

### Bug Fixes

* dont set empty string authorization header ([#654](https://github.com/cnpm/cnpmcore/issues/654)) ([64bb78c](64bb78cf8a))
2024-02-23 17:48:31 +00:00
fengmk2
64bb78cf8a fix: dont set empty string authorization header (#654)
https://github.com/cnpm/cnpmcore/issues/652
2024-02-24 01:47:20 +08:00
semantic-release-bot
f7d9d49b4c Release 3.53.2
[skip ci]

## [3.53.2](https://github.com/cnpm/cnpmcore/compare/v3.53.1...v3.53.2) (2024-02-23)

### Bug Fixes

* only set chrome-for-testing data timestamp after sync task finish ([#653](https://github.com/cnpm/cnpmcore/issues/653)) ([4bc0c9c](4bc0c9ca59))
2024-02-23 17:18:58 +00:00
fengmk2
4bc0c9ca59 fix: only set chrome-for-testing data timestamp after sync task finish (#653)
closes https://github.com/cnpm/cnpmcore/issues/652
2024-02-24 01:17:48 +08:00
hljwkwm
ae83136e62 docs: add custom time zone example (#651)
使用 Docker 打出的镜像,默认会使用0区时区,会影响到日志、数据库等时间的显示,考虑到全球化,可在镜像运行时,
设置环境变量 `TZ` 即可解决该问题,TZ 列表可参考:[https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)。
2024-02-21 21:48:28 +08:00
semantic-release-bot
6b4f9af947 Release 3.53.1
[skip ci]

## [3.53.1](https://github.com/cnpm/cnpmcore/compare/v3.53.0...v3.53.1) (2024-02-20)

### Bug Fixes

* fix breaking change about RSA_PKCS1_PADDING ([#650](https://github.com/cnpm/cnpmcore/issues/650)) ([a6737e6](a6737e6150))
2024-02-20 14:10:53 +00:00
hljwkwm
a6737e6150 fix: fix breaking change about RSA_PKCS1_PADDING (#650)
Node.JS 安全性修复导致 RSA_PKCS1_PADDING 不可用,会报出以下错误:

> RSA_PKCS1_PADDING is no longer supported for private decryption, this
can be reverted with --security-revert=CVE-2023-46809。

参考链接:https://www.eggjs.org/zh-CN/core/security#revert-cve
2024-02-20 22:09:44 +08:00
semantic-release-bot
2ec6bd94b2 Release 3.53.0
[skip ci]

## [3.53.0](https://github.com/cnpm/cnpmcore/compare/v3.52.0...v3.53.0) (2024-01-27)

### Features

* mirror bun ([#645](https://github.com/cnpm/cnpmcore/issues/645)) ([26d2ef2](26d2ef2124))
2024-01-27 13:16:31 +00:00
fengmk2
26d2ef2124 feat: mirror bun (#645)
https://github.com/oven-sh/bun
2024-01-27 21:15:05 +08:00
semantic-release-bot
9004ce7a1c Release 3.52.0
[skip ci]

## [3.52.0](https://github.com/cnpm/cnpmcore/compare/v3.51.1...v3.52.0) (2024-01-25)

### Features

* mirror pkg-fetch from yao-pkg/pkg-fetch ([#644](https://github.com/cnpm/cnpmcore/issues/644)) ([6e32679](6e326790c4))
2024-01-25 10:10:11 +00:00
unbyte
6e326790c4 feat: mirror pkg-fetch from yao-pkg/pkg-fetch (#644)
**what:** pkg-fetch provides prebuilt node binaries used by pkg.

**why from yao-pkg:**
[vercel/pkg-fetch](https://github.com/vercel/pkg-fetch) was archived,
[yao-pkg/pkg-fetch](https://github.com/yao-pkg/pkg-fetch) is currently
the most actively maintained fork.

close: https://github.com/cnpm/cnpmcore/issues/543
2024-01-25 18:08:56 +08:00
semantic-release-bot
4644c1e788 Release 3.51.1
[skip ci]

## [3.51.1](https://github.com/cnpm/cnpmcore/compare/v3.51.0...v3.51.1) (2024-01-23)

### Bug Fixes

* hooks triggers ([#641](https://github.com/cnpm/cnpmcore/issues/641)) ([838eecf](838eecff2d))
2024-01-23 07:35:16 +00:00
elrrrrrrr
838eecff2d fix: hooks triggers (#641)
> Since the eventBus#cork , version & tag events are triggered at same
time, cause the abnormal triggers of different types of hooks.
* ~~🐞 Fix triggerHook type targetName to be `tagetName:changeId`~~
* 🤖 Only merge sync tasks (binary, package) which in waiting states
--------
> 由于 `eventBus#cork` 机制,版本事件同时触发,导致不同类型 hook 触发异常
* ~~🐞 修改 triggerHook 类型 targetName 为 `包名:changeId`~~
* 🤖 仅合并 waiting 状态下包同步任务
2024-01-23 15:34:09 +08:00
semantic-release-bot
08678c70db Release 3.51.0
[skip ci]

## [3.51.0](https://github.com/cnpm/cnpmcore/compare/v3.50.0...v3.51.0) (2024-01-13)

### Features

* support GITHUB_TOKEN to increae github api request rate ([#639](https://github.com/cnpm/cnpmcore/issues/639)) ([0f7aa4a](0f7aa4a50f))
2024-01-13 05:49:31 +00:00
fengmk2
0f7aa4a50f feat: support GITHUB_TOKEN to increae github api request rate (#639) 2024-01-13 13:48:10 +08:00
semantic-release-bot
05445b49c3 Release 3.50.0
[skip ci]

## [3.50.0](https://github.com/cnpm/cnpmcore/compare/v3.49.5...v3.50.0) (2024-01-13)

### Features

* mirror ant-design-charts releases ([#638](https://github.com/cnpm/cnpmcore/issues/638)) ([ad86be3](ad86be312e))
2024-01-13 05:11:12 +00:00
fengmk2
ad86be312e feat: mirror ant-design-charts releases (#638)
closes https://github.com/cnpm/cnpmcore/issues/635
2024-01-13 13:09:42 +08:00
windhc
039a56f471 chore: add registry api doc (#636)
closes https://github.com/cnpm/cnpmcore/issues/630
2024-01-13 12:52:10 +08:00
semantic-release-bot
3310b0e435 Release 3.49.5
[skip ci]

## [3.49.5](https://github.com/cnpm/cnpmcore/compare/v3.49.4...v3.49.5) (2024-01-06)

### Bug Fixes

* read @prisma/engines-version from dependencies ([#633](https://github.com/cnpm/cnpmcore/issues/633)) ([a009668](a0096685fc))
2024-01-06 13:28:01 +00:00
fengmk2
a0096685fc fix: read @prisma/engines-version from dependencies (#633)
closes https://github.com/cnpm/cnpm/issues/436
2024-01-06 21:26:59 +08:00
semantic-release-bot
b0e0a2d464 Release 3.49.4
[skip ci]

## [3.49.4](https://github.com/cnpm/cnpmcore/compare/v3.49.3...v3.49.4) (2024-01-06)

### Bug Fixes

* github file size limit increased to 250mb ([#632](https://github.com/cnpm/cnpmcore/issues/632)) ([d5bf9ce](d5bf9ceb1b))
2024-01-06 13:10:36 +00:00
fengmk2
d5bf9ceb1b fix: github file size limit increased to 250mb (#632)
closes https://github.com/cnpm/cnpm/issues/435
2024-01-06 21:08:41 +08:00
semantic-release-bot
10b97c8697 Release 3.49.3
[skip ci]

## [3.49.3](https://github.com/cnpm/cnpmcore/compare/v3.49.2...v3.49.3) (2023-12-25)

### Bug Fixes

* unpkg lock ([#629](https://github.com/cnpm/cnpmcore/issues/629)) ([5a8a4eb](5a8a4eb10c))
2023-12-25 03:07:09 +00:00
elrrrrrrr
5a8a4eb10c fix: unpkg lock (#629)
> When accessing the unpkg service, when the packages have not yet been
synchronized, will lead to multiple synchronization attempts
concurrently causing db insert errors.
* 🔒 Added a Redis lock for the `ensurePackageVersionFilesSync` function,
with a default timeout of 60 seconds.
* 🥸 Admin PUT requests and the package version auto sync process are not
restricted by this.

> 当访问 unpkg 服务时,如果访问存量未同步的包,可能导致多次同步并发报错
* 🔒 为 ensurePackageVersionFilesSync 添加 redis 锁,默认超时 60s
* 🥸 管理员手动 PUT 请求和包同步流程不受限制
2023-12-25 11:05:43 +08:00
semantic-release-bot
7e176f2f42 Release 3.49.2
[skip ci]

## [3.49.2](https://github.com/cnpm/cnpmcore/compare/v3.49.1...v3.49.2) (2023-12-19)

### Bug Fixes

* ignore db query error default logger ([#626](https://github.com/cnpm/cnpmcore/issues/626)) ([6cc2f2d](6cc2f2d830))
* should search default packages when text is empty ([#623](https://github.com/cnpm/cnpmcore/issues/623)) ([0c4a52d](0c4a52d220))
2023-12-19 09:17:04 +00:00
fengmk2
6cc2f2d830 fix: ignore db query error default logger (#626) 2023-12-19 17:15:55 +08:00
semantic-release-bot
dddb10e510 Release 3.49.2
[skip ci]

## [3.49.2](https://github.com/cnpm/cnpmcore/compare/v3.49.1...v3.49.2) (2023-12-18)

### Bug Fixes

* should search default packages when text is empty ([#623](https://github.com/cnpm/cnpmcore/issues/623)) ([0c4a52d](0c4a52d220))
2023-12-18 12:01:55 +00:00
Beace
0c4a52d220 fix: should search default packages when text is empty (#623)
当搜索关键词为空时,不进行 ES 关键词搜索拼接,返回一个默认的全量数据排名。 

close https://github.com/cnpm/cnpmcore/issues/622
2023-12-18 20:00:52 +08:00
semantic-release-bot
c3e481c5c4 Release 3.49.1
[skip ci]

## [3.49.1](https://github.com/cnpm/cnpmcore/compare/v3.49.0...v3.49.1) (2023-12-18)

### Bug Fixes

* use tar fork version to fix memory leak ([#625](https://github.com/cnpm/cnpmcore/issues/625)) ([6c519f7](6c519f73ce))
2023-12-18 03:04:36 +00:00
fengmk2
6c519f73ce fix: use tar fork version to fix memory leak (#625)
closes https://github.com/cnpm/cnpmcore/issues/624
2023-12-18 11:03:32 +08:00
semantic-release-bot
87ca86f1db Release 3.49.0
[skip ci]

## [3.49.0](https://github.com/cnpm/cnpmcore/compare/v3.48.4...v3.49.0) (2023-12-12)

### Features

* use egg-cors@3 ([#620](https://github.com/cnpm/cnpmcore/issues/620)) ([fcca3c3](fcca3c30ce))
2023-12-12 01:31:42 +00:00
fengmk2
fcca3c30ce feat: use egg-cors@3 (#620)
https://github.com/eggjs/egg-cors/pull/27
2023-12-12 09:08:33 +08:00
semantic-release-bot
37b50842fd Release 3.48.4
[skip ci]

## [3.48.4](https://github.com/cnpm/cnpmcore/compare/v3.48.3...v3.48.4) (2023-12-07)

### Bug Fixes

* incorrect latest tag in strict specific version ([#610](https://github.com/cnpm/cnpmcore/issues/610)) ([acfd667](acfd66748f))
2023-12-07 12:48:52 +00:00
elrrrrrrr
e62fa26788 chore: ts (#619)
* 锁定 typescript 依赖至 5.2.2 解决构建报错
2023-12-07 20:47:31 +08:00
semantic-release-bot
64dfcb35a4 Release 3.48.4
[skip ci]

## [3.48.4](https://github.com/cnpm/cnpmcore/compare/v3.48.3...v3.48.4) (2023-11-28)

### Bug Fixes

* incorrect latest tag in strict specific version ([#610](https://github.com/cnpm/cnpmcore/issues/610)) ([acfd667](acfd66748f))
2023-11-28 14:48:48 +00:00
hezhengxu2018
acfd66748f fix: incorrect latest tag in strict specific version (#610)
1. 添加新建任务时的重复版本校验,去除执行任务时的版本去重
2.
在 strictSyncSpecificVersion 开启时,修改为从可用版本中选择 latestTag。之前从任务的指定版本中生成 latestTag 逻辑不正确
2023-11-28 22:47:34 +08:00
semantic-release-bot
072e146e5b Release 3.48.3
[skip ci]

## [3.48.3](https://github.com/cnpm/cnpmcore/compare/v3.48.2...v3.48.3) (2023-11-06)

### Bug Fixes

* es query script score syntax fix and add error handler for 404 error ([#607](https://github.com/cnpm/cnpmcore/issues/607)) ([8e1f4ca](8e1f4ca880))
2023-11-06 06:29:09 +00:00
Beace
8e1f4ca880 fix: es query script score syntax fix and add error handler for 404 error (#607)
closes https://github.com/cnpm/cnpmcore/issues/598
2023-11-06 14:27:32 +08:00
semantic-release-bot
603bb82b1f Release 3.48.2
[skip ci]

## [3.48.2](https://github.com/cnpm/cnpmcore/compare/v3.48.1...v3.48.2) (2023-11-03)

### Bug Fixes

* should set OPTIONS on access-control-allow-methods ([#608](https://github.com/cnpm/cnpmcore/issues/608)) ([0179ef3](0179ef364a))
2023-11-03 11:13:45 +00:00
fengmk2
0179ef364a fix: should set OPTIONS on access-control-allow-methods (#608)
Access to fetch at
'https://registry.npmmirror.com/isstream/-/isstream-0.1.0.tgz' from
origin 'https://foo.com' has been blocked by CORS policy: Method OPTIONS
is not allowed by Access-Control-Allow-Methods in preflight response.
2023-11-03 19:12:25 +08:00
semantic-release-bot
f03d48e511 Release 3.48.1
[skip ci]

## [3.48.1](https://github.com/cnpm/cnpmcore/compare/v3.48.0...v3.48.1) (2023-11-03)

### Bug Fixes

* should set access-control-allow-origin and headers ([#606](https://github.com/cnpm/cnpmcore/issues/606)) ([18ef7f4](18ef7f49af))
2023-11-03 10:53:27 +00:00
fengmk2
18ef7f49af fix: should set access-control-allow-origin and headers (#606) 2023-11-03 18:52:08 +08:00
semantic-release-bot
9ea70088fb Release 3.48.0
[skip ci]

## [3.48.0](https://github.com/cnpm/cnpmcore/compare/v3.47.2...v3.48.0) (2023-11-03)

### Features

* allow OPTIONS request on tgz downlaod url ([#605](https://github.com/cnpm/cnpmcore/issues/605)) ([5bedb25](5bedb25f9d))
2023-11-03 10:24:08 +00:00
fengmk2
5bedb25f9d feat: allow OPTIONS request on tgz downlaod url (#605)
make webcontainer can run npm install on cnpmcore registry
2023-11-03 18:22:57 +08:00
semantic-release-bot
31946ba10e Release 3.47.2
[skip ci]

## [3.47.2](https://github.com/cnpm/cnpmcore/compare/v3.47.1...v3.47.2) (2023-10-28)

### Bug Fixes

* ignore BodyTimeoutError ([#603](https://github.com/cnpm/cnpmcore/issues/603)) ([cde4f03](cde4f03c30))
2023-10-28 14:29:05 +00:00
fengmk2
cde4f03c30 fix: ignore BodyTimeoutError (#603) 2023-10-28 22:27:42 +08:00
semantic-release-bot
c3c7b391c0 Release 3.47.1
[skip ci]

## [3.47.1](https://github.com/cnpm/cnpmcore/compare/v3.47.0...v3.47.1) (2023-10-26)

### Bug Fixes

* ignore HttpClientRequestTimeoutError on change stream worker ([#601](https://github.com/cnpm/cnpmcore/issues/601)) ([0791769](079176926d))
2023-10-26 09:43:17 +00:00
fengmk2
079176926d fix: ignore HttpClientRequestTimeoutError on change stream worker (#601) 2023-10-26 17:41:59 +08:00
semantic-release-bot
e01d39ef4e Release 3.47.0
[skip ci]

## [3.47.0](https://github.com/cnpm/cnpmcore/compare/v3.46.0...v3.47.0) (2023-10-26)

### Features

* ignore network error to error log ([#600](https://github.com/cnpm/cnpmcore/issues/600)) ([22d401e](22d401ee1f))
2023-10-26 07:39:36 +00:00
fengmk2
22d401ee1f feat: ignore network error to error log (#600) 2023-10-26 15:38:10 +08:00
killa
3cdb7cc9df mirrors: add fuse-t (#599) 2023-10-25 12:59:09 +08:00
semantic-release-bot
5ad775e411 Release 3.46.0
[skip ci]

## [3.46.0](https://github.com/cnpm/cnpmcore/compare/v3.45.1...v3.46.0) (2023-10-11)

### Features

* read remote auth token from database ([#595](https://github.com/cnpm/cnpmcore/issues/595)) ([707a1d3](707a1d3809))
2023-10-11 14:26:40 +00:00
hezhengxu2018
707a1d3809 feat: read remote auth token from database (#595)
closes https://github.com/cnpm/cnpmcore/issues/586
2023-10-11 09:25:17 -05:00
semantic-release-bot
9fcbb00406 Release 3.45.1
[skip ci]

## [3.45.1](https://github.com/cnpm/cnpmcore/compare/v3.45.0...v3.45.1) (2023-10-07)

### Bug Fixes

* use oss-cnpm@5.0.1 ([#597](https://github.com/cnpm/cnpmcore/issues/597)) ([413ec56](413ec5685e))
2023-10-07 10:51:42 +00:00
fengmk2
413ec5685e fix: use oss-cnpm@5.0.1 (#597)
https://github.com/node-modules/oss-client/pull/15
2023-10-07 18:50:13 +08:00
fengmk2
f66057794e Release 3.45.0
[skip ci]
2023-10-07 13:34:36 +08:00
semantic-release-bot
9a5e8c387a Release 3.44.0
[skip ci]

## [3.44.0](https://github.com/cnpm/cnpmcore/compare/v3.43.5...v3.44.0) (2023-10-07)

### Features

* sync all crhome for test binaries ([#592](https://github.com/cnpm/cnpmcore/issues/592)) ([4596b21](4596b21271))
* use oss-client v2 ([#596](https://github.com/cnpm/cnpmcore/issues/596)) ([d24e3bd](d24e3bd235))
2023-10-07 04:19:41 +00:00
fengmk2
d24e3bd235 feat: use oss-client v2 (#596)
https://github.com/cnpm/oss-cnpm/pull/29
https://github.com/node-modules/oss-client/pull/12
2023-10-07 12:18:00 +08:00
semantic-release-bot
d6d72650dd Release 3.44.0
[skip ci]

## [3.44.0](https://github.com/cnpm/cnpmcore/compare/v3.43.5...v3.44.0) (2023-09-19)

### Features

* sync all crhome for test binaries ([#592](https://github.com/cnpm/cnpmcore/issues/592)) ([4596b21](4596b21271))
2023-09-19 15:34:28 +00:00
fengmk2
4596b21271 feat: sync all crhome for test binaries (#592)
closes https://github.com/cnpm/cnpmcore/issues/591
2023-09-19 23:33:03 +08:00
semantic-release-bot
c33f10e0ab Release 3.43.5
[skip ci]

## [3.43.5](https://github.com/cnpm/cnpmcore/compare/v3.43.4...v3.43.5) (2023-09-05)

### Bug Fixes

* the license may be an object ([#587](https://github.com/cnpm/cnpmcore/issues/587)) ([88b6afb](88b6afb66e)), closes [/github.com/cnpm/cnpmcore/issues/585#issuecomment-1706009496](https://github.com/cnpm//github.com/cnpm/cnpmcore/issues/585/issues/issuecomment-1706009496)
2023-09-05 10:47:03 +00:00
Beace
88b6afb66e fix: the license may be an object (#587)
有些 package 的 license 是个对象,会导致 es 写入失败
https://github.com/cnpm/cnpmcore/issues/585#issuecomment-1706009496


![image](https://github.com/cnpm/cnpmcore/assets/13284978/4343a1e8-1fa5-4aed-950d-d5038534dad8)
2023-09-05 18:45:42 +08:00
semantic-release-bot
6d156a5c96 Release 3.43.4
[skip ci]

## [3.43.4](https://github.com/cnpm/cnpmcore/compare/v3.43.3...v3.43.4) (2023-09-02)

### Bug Fixes

* add CDN cache header on search api ([#583](https://github.com/cnpm/cnpmcore/issues/583)) ([89f6b98](89f6b989c1))
2023-09-02 02:42:58 +00:00
fengmk2
89f6b989c1 fix: add CDN cache header on search api (#583) 2023-09-02 10:41:39 +08:00
semantic-release-bot
5e4d988c2f Release 3.43.3
[skip ci]

## [3.43.3](https://github.com/cnpm/cnpmcore/compare/v3.43.2...v3.43.3) (2023-09-01)

### Bug Fixes

* author display in cli ([#582](https://github.com/cnpm/cnpmcore/issues/582)) ([9b2dc41](9b2dc41134))
2023-09-01 06:52:08 +00:00
elrrrrrrr
9b2dc41134 fix: author display in cli (#582)
> Revert ineffective changes from
https://github.com/cnpm/cnpmcore/pull/581, add username index
* 🧶 Add username field to maintainers in search results, handle
uniformly when writing
* 📚 Update index documentation
-----
> 回滚 https://github.com/cnpm/cnpmcore/pull/581 无效改动,添加 username 索引
* 🧶 搜索结果中的 maintainers 添加 username 字段,写入时统一处理
* 📚 更新索引文档说明
2023-09-01 14:50:54 +08:00
semantic-release-bot
3f95d0fadd Release 3.43.2
[skip ci]

## [3.43.2](https://github.com/cnpm/cnpmcore/compare/v3.43.1...v3.43.2) (2023-09-01)

### Bug Fixes

* author info ([#581](https://github.com/cnpm/cnpmcore/issues/581)) ([6dd241d](6dd241d690))
2023-09-01 03:25:14 +00:00
elrrrrrrr
6dd241d690 fix: author info (#581)
> in npm@8 cli, empty author information after executing npm search

* 🐞 Fix: npm-cli author field is empty, need to add publisher-related
fields
* 📚 Update API documentation
-----

> npm@8 命令行,执行 npm search 后,author 信息为空
* 🐞 修复 npm-cli author 字段为空,需添加 publisher 相关字段
* 📚 更新文档接口信息


![image](https://github.com/cnpm/cnpmcore/assets/5574625/f3a91d29-0bf4-498a-ae2f-c3dd4b47b22f)
2023-09-01 11:23:34 +08:00
semantic-release-bot
868c8d305e Release 3.43.1
[skip ci]

## [3.43.1](https://github.com/cnpm/cnpmcore/compare/v3.43.0...v3.43.1) (2023-09-01)

### Bug Fixes

* use env.CNPMCORE_CONFIG_ENABLE_ES to enable ([#580](https://github.com/cnpm/cnpmcore/issues/580)) ([bcf67c4](bcf67c4cea))
2023-09-01 02:07:12 +00:00
fengmk2
bcf67c4cea fix: use env.CNPMCORE_CONFIG_ENABLE_ES to enable (#580) 2023-09-01 10:05:58 +08:00
semantic-release-bot
941b277244 Release 3.43.0
[skip ci]

## [3.43.0](https://github.com/cnpm/cnpmcore/compare/v3.42.2...v3.43.0) (2023-09-01)

### Features

* support npm search command like npmio ([#513](https://github.com/cnpm/cnpmcore/issues/513)) ([7f85848](7f858482f7))
2023-09-01 01:22:37 +00:00
Beace
7f858482f7 feat: support npm search command like npmio (#513)
- [x] 找个合适的 eggjs es 插件,或者手撸个,看社区的几个版本都比较低
- [x] HTTP Server 新增 API
`/-/v1/search?text=react&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5`,第一版不一定能
qpm 都支持,先支持现有的下载量数据,即 popularity 的参考数据
- [x] 监听相关的 metadata 变更的 event,同步写入、删除 增量的 ES 数据,ES 有较强抗压能力,这块直接做成同步就好
- [x] 考虑可能同步也会丢部分数据(stream 不稳定时),HTTP Server 再追加一个手动同步 ES 的接口,传包名同步触发写
ES 即可
- [x] 提供全量一次性的初始化同步 ES 脚本
- [x] setting/mapping 参考
https://github.com/npms-io/npms-analyzer/blob/master/config/elasticsearch/npms.json5

---------

Co-authored-by: fengmk2 <fengmk2@gmail.com>
Co-authored-by: elrrrrrrr <elrrrrrrr@gmail.com>
2023-09-01 09:21:03 +08:00
semantic-release-bot
6e45ac5a63 Release 3.42.2
[skip ci]

## [3.42.2](https://github.com/cnpm/cnpmcore/compare/v3.42.1...v3.42.2) (2023-08-31)

### Bug Fixes

* should use NodeNext on module and moduleResolution tsconfig ([#578](https://github.com/cnpm/cnpmcore/issues/578)) ([10d7a84](10d7a8499e))
2023-08-31 01:16:34 +00:00
fengmk2
10d7a8499e fix: should use NodeNext on module and moduleResolution tsconfig (#578)
https://devblogs.microsoft.com/typescript/announcing-typescript-5-2/#module-and-moduleresolution-must-match-under-recent-node-js-settings

> Option 'moduleResolution' must be set to 'NodeNext' (or left
unspecified) when option 'module' is set to 'NodeNext'.
2023-08-31 09:15:04 +08:00
semantic-release-bot
2b2e13c01d Release 3.42.1
[skip ci]

## [3.42.1](https://github.com/cnpm/cnpmcore/compare/v3.42.0...v3.42.1) (2023-08-23)

### Bug Fixes

* default latest tag ([#575](https://github.com/cnpm/cnpmcore/issues/575)) ([ffe8fa7](ffe8fa7d19)), closes [#574](https://github.com/cnpm/cnpmcore/issues/574)
2023-08-23 06:12:39 +00:00
elrrrrrrr
ffe8fa7d19 fix: default latest tag (#575)
> closes #574 Fixed the issue where custom tags in publishConfig
prevented the default latest tag.
* 🧶 Modified the `savePackageVersion` API, automatically add latest tag
if no latest tag.
* 🧶 The publish tag parameter has been changed to tags, triggering
corresponding events in batches.
* ♻️ No changes to the package synchronization process.
------
> closes #574 修复 publishConfig 中自定义 tag,导致 latest tag 未设置的问题
* 🧶 修改 savePackageVersion 接口,如果当前包未配置 latest tag,则自动补全
* 🧶 publish tag 参数改为 tags,对应事件分批触发
* ♻️ 包同步流程不做修改
2023-08-23 14:11:23 +08:00
semantic-release-bot
39de1c7df2 Release 3.42.0
[skip ci]

## [3.42.0](https://github.com/cnpm/cnpmcore/compare/v3.41.0...v3.42.0) (2023-08-22)

### Features

* revalidate latest version ([#573](https://github.com/cnpm/cnpmcore/issues/573)) ([73b4383](73b4383f5c)), closes [#376](https://github.com/cnpm/cnpmcore/issues/376)
2023-08-22 07:42:19 +00:00
elrrrrrrr
73b4383f5c feat: revalidate latest version (#573)
> closes #376: fix issue with incomplete binary file after upstream
release or sync failure
* 🧶 Modify the binary `diff` method, adding latest version check method.
* ♻️ Perform additional comparison on the latest version, without
modifying existing data.

---------
> 修复 #376 ,兼容上游发布或同步失败后,产物同步不全的问题
* 🧶 调整 binary `diff` 方法,添加最新版本校验逻辑
* ♻️ 对最新版本的子目录进行额外比对,存量数据不做修改
2023-08-22 15:41:04 +08:00
semantic-release-bot
9916bd9ecf Release 3.41.0
[skip ci]

## [3.41.0](https://github.com/cnpm/cnpmcore/compare/v3.40.0...v3.41.0) (2023-08-17)

### Features

* improved readability of emoji in sync logs ([#572](https://github.com/cnpm/cnpmcore/issues/572)) ([0ac275a](0ac275a348))
2023-08-17 12:51:41 +00:00
一丝
0ac275a348 feat: improved readability of emoji in sync logs (#572) 2023-08-17 20:50:08 +08:00
semantic-release-bot
3f9c91c430 Release 3.40.0
[skip ci]

## [3.40.0](https://github.com/cnpm/cnpmcore/compare/v3.39.5...v3.40.0) (2023-08-08)

### Features

* signup on auth ([#567](https://github.com/cnpm/cnpmcore/issues/567)) ([c710600](c7106008d9))
2023-08-08 12:47:01 +00:00
elrrrrrrr
c7106008d9 feat: signup on auth (#567)
> Auto init the account when auth

* 🧶 Added `findOrCreateUser` method. Initialize account on both login
and authorization, as per the submitted GitHub."
------

> 授权时,默认进行账户初始化
* 🧶 新增 `findOrCreateUser` 方法,登录和授权时均初始化账户
2023-08-08 20:45:46 +08:00
semantic-release-bot
b102711adf Release 3.39.5
[skip ci]

## [3.39.5](https://github.com/cnpm/cnpmcore/compare/v3.39.4...v3.39.5) (2023-08-08)

### Bug Fixes

* noImplicitAny ts ([#568](https://github.com/cnpm/cnpmcore/issues/568)) ([1932bb9](1932bb9713))
2023-08-08 12:44:23 +00:00
elrrrrrrr
1932bb9713 fix: noImplicitAny ts (#568)
> Attempted to apply the `noImplicitAny`, parameter types should be
specified unless any is manually declared.
* 🐞 Fixed an issue in bugVersionAdvice where AbbreviatedManifest was
being set abnormally.
* 🤖 Added index.d.ts to store declarations for dependencies without
types.
* 🤔 skipLibCheck has no effect on leoric for now, so it cannot be
enabled temporarily.
--------
> 尝试应用 `noImplicitAny` 配置,除非手动声明 any,否则需要指定参数类型
* 🐞 修复 bugVersionAdvice 中,AbbreviatedManifest 设置异常
* 🤖 添加 index.d.ts 存放无类型依赖声明
* 🤔 skipLibCheck 对 leoric 失效,暂时无法开启 



![image](https://github.com/cnpm/cnpmcore/assets/5574625/7ed9d22e-cac8-4202-ba3c-d4c26eb7dc00)
2023-08-08 20:42:57 +08:00
semantic-release-bot
3297121b9f Release 3.39.4
[skip ci]

## [3.39.4](https://github.com/cnpm/cnpmcore/compare/v3.39.3...v3.39.4) (2023-08-04)

### Bug Fixes

* publisher info ([#565](https://github.com/cnpm/cnpmcore/issues/565)) ([94bcc1a](94bcc1a37e))
2023-08-04 05:47:12 +00:00
elrrrrrrr
94bcc1a37e fix: publisher info (#565)
> Fixed an issue with the `_npmUser` field setting during the initial
sync.
1. 🧶 Fixed _npmUser field setting issue during initial sync, should use
displayName.
2. 🧶 skip diff abbreviated meta for _npmUser
3. 🤖 Refined some TypeScript definitions, use isEqual to diff metas

-------

> 修复首次同步时,`_npmUser` 字段设置异常
1. 🧶 修复 publisher 匹配,获取逻辑,应当用 displayName 进行匹配
2. 🧶 精简 meta 信息,跳过比较不存在的 _npmUser 字段
2. 🤖 调整部分 ts 定义,使用 isEqual 来进行 diff 比较
2023-08-04 13:45:35 +08:00
semantic-release-bot
276b9511b8 Release 3.39.3
[skip ci]

## [3.39.3](https://github.com/cnpm/cnpmcore/compare/v3.39.2...v3.39.3) (2023-08-01)

### Bug Fixes

* don't log NotImplementedError to error log ([#563](https://github.com/cnpm/cnpmcore/issues/563)) ([bcf3547](bcf3547ff2))
2023-08-01 10:26:15 +00:00
fengmk2
bcf3547ff2 fix: don't log NotImplementedError to error log (#563)
![image](https://github.com/cnpm/cnpmcore/assets/156269/0a1de25e-47de-4c74-a934-566bd7a16f2f)
2023-08-01 18:24:46 +08:00
semantic-release-bot
9f6b44dfe9 Release 3.39.2
[skip ci]

## [3.39.2](https://github.com/cnpm/cnpmcore/compare/v3.39.1...v3.39.2) (2023-07-29)

### Bug Fixes

* rename libpg-query to libpg-query-node ([#561](https://github.com/cnpm/cnpmcore/issues/561)) ([9483a46](9483a460a3))
2023-07-29 08:22:32 +00:00
fengmk2
9483a460a3 fix: rename libpg-query to libpg-query-node (#561) 2023-07-29 16:21:03 +08:00
semantic-release-bot
3498ba221c Release 3.39.1
[skip ci]

## [3.39.1](https://github.com/cnpm/cnpmcore/compare/v3.39.0...v3.39.1) (2023-07-27)

### Bug Fixes

* body parser ignore ([#558](https://github.com/cnpm/cnpmcore/issues/558)) ([81d6455](81d6455ff8))
2023-07-27 08:13:00 +00:00
elrrrrrrr
81d6455ff8 fix: body parser ignore (#558)
> Closes https://github.com/cnpm/cnpmcore/issues/551 Some versions
encounter issues with audit requests using the GET method & gzip
* 🧶 Modified the NOT_IMPLEMENTED configuration to skip bodyParser
parsing by default.
* 🧶 Added homeService and implemented miscellaneous methods to handle
additional routes.
------
> Closes https://github.com/cnpm/cnpmcore/issues/551 部分版本 audit 请求为 get
且 参数解析异常
* 🧶 修改 NOT_IMPLEMENTED 配置,默认跳过 bodyParser 解析
* 🧶 添加 homeService,提供 misc 方法处理额外路由

---------

Co-authored-by: fengmk2 <fengmk2@gmail.com>
2023-07-27 16:11:47 +08:00
semantic-release-bot
7ba8dbb4a7 Release 3.39.0
[skip ci]

## [3.39.0](https://github.com/cnpm/cnpmcore/compare/v3.38.2...v3.39.0) (2023-07-27)

### Features

* Sync libpg-query binary ([#557](https://github.com/cnpm/cnpmcore/issues/557)) ([8556b5f](8556b5f92f))
2023-07-27 06:30:37 +00:00
wandergis
8556b5f92f feat: Sync libpg-query binary (#557) 2023-07-27 14:29:22 +08:00
semantic-release-bot
e4d44c68e5 Release 3.38.2
[skip ci]

## [3.38.2](https://github.com/cnpm/cnpmcore/compare/v3.38.1...v3.38.2) (2023-07-21)

### Bug Fixes

* _npmUser info in fullManifest ([#554](https://github.com/cnpm/cnpmcore/issues/554)) ([4455295](44552959eb)), closes [#553](https://github.com/cnpm/cnpmcore/issues/553)
2023-07-21 11:52:25 +00:00
elrrrrrrr
44552959eb fix: _npmUser info in fullManifest (#554)
> closes #553, fixing the issue introduced by
https://github.com/cnpm/cnpmcore/pull/491, which caused an abnormality
in the _npmUser field in fullManifest.
1. 🧶 Update the `publish` method to pass in the actual operating
publisher information.
2. 🧶 Update the diffMeta function to compare the _npmUser as well.
3. ♻️ Existing data needs to be resynchronized, from 6.2 to 7.20.
-----
> closes #553 , 修复由 https://github.com/cnpm/cnpmcore/pull/491 引入问题,导致
fullManifest 中 _npmUser 字段异常
1. 🧶 更新 .publish 方法,传入实际操作的 publisher 信息
2. 🧶 更新 diffMeta 算法,将 _npmUser 也进行比对
3. ♻️ 存量数据需要重新进行同步,6.2 -> 7.20
2023-07-21 19:50:55 +08:00
semantic-release-bot
9ac676772d Release 3.38.1
[skip ci]

## [3.38.1](https://github.com/cnpm/cnpmcore/compare/v3.38.0...v3.38.1) (2023-07-21)

### Bug Fixes

* publish lock ([#555](https://github.com/cnpm/cnpmcore/issues/555)) ([ec90ab8](ec90ab85fa))
2023-07-21 09:34:03 +00:00
elrrrrrrr
ec90ab85fa fix: publish lock (#555)
> When concurrently executing packet sending, there is a possibility of
version overwrite

* 🧶 Add `usingLock` logic to the publish interface to handle concurrent
execution, which will prevent version overwrite
* 🔨 Modify usingLock to include a return value indicating the success of
lock creation
------
> 并发执行发包时,可能出现版本覆盖问题
1. 🧶 在发布接口中,添加 usingLock 逻辑,包同步场景不涉及
2. 🔨 `usingLock` 添加返回值,标记是否创建锁成功
2023-07-21 17:32:39 +08:00
semantic-release-bot
9ca483cfa5 Release 3.38.0
[skip ci]

## [3.38.0](https://github.com/cnpm/cnpmcore/compare/v3.37.1...v3.38.0) (2023-07-17)

### Features

* misc router ([#552](https://github.com/cnpm/cnpmcore/issues/552)) ([e9e3a7b](e9e3a7b70f)), closes [#551](https://github.com/cnpm/cnpmcore/issues/551)
2023-07-17 06:53:39 +00:00
elrrrrrrr
e9e3a7b70f feat: misc router (#552)
![image](https://github.com/cnpm/cnpmcore/assets/5574625/d158e354-d390-4835-91de-7aa9f5104e49)


> close #551 , 针对未实现的 post 请求添加错误码

1. 🧶 拦截 audit 相关接口
[ref](https://docs.npmjs.com/cli/v9/commands/npm-audit)
2. ♻️ 默认 404 GET 接口不做额外处理

-----

> close #551, add error code for unimplemented post requests

1. 🧶 Intercept audit-related interfaces
[ref](https://docs.npmjs.com/cli/v9/commands/npm-audit)
2. ♻️ No additional processing for default 404 GET interfaces.
2023-07-17 14:52:21 +08:00
乐潇游
8a9412df4f chore: update tegg deps (#549) (#550) 2023-07-15 00:00:36 +08:00
semantic-release-bot
28deae4b70 Release 3.37.1
[skip ci]

## [3.37.1](https://github.com/cnpm/cnpmcore/compare/v3.37.0...v3.37.1) (2023-07-13)

### Bug Fixes

* playwright bianry sync config ([#548](https://github.com/cnpm/cnpmcore/issues/548)) ([166e334](166e3341f4))
2023-07-13 07:06:22 +00:00
elrrrrrrr
166e3341f4 fix: playwright bianry sync config (#548)
> Follow the Playwright-core configuration,
[ref](https://registry.npmmirror.com/playwright-core/1.36.0/files/lib/server/registry/index.js),
to update the binary download path information.

1. 🧶 Update Playwright configuration information.
--------

> Follow playwright-core 配置,
[ref](https://registry.npmmirror.com/playwright-core/1.36.0/files/lib/server/registry/index.js),更新
binary 下载路径信息
1. 🧶 更新 playwright 配置信息
2023-07-13 15:05:10 +08:00
semantic-release-bot
02d0d2b5a0 Release 3.37.0
[skip ci]

## [3.37.0](https://github.com/cnpm/cnpmcore/compare/v3.36.0...v3.37.0) (2023-07-12)

### Features

* add lastUsedAt for classic token ([#547](https://github.com/cnpm/cnpmcore/issues/547)) ([e061685](e0616859ff))
2023-07-12 07:12:37 +00:00
elrrrrrrr
e0616859ff feat: add lastUsedAt for classic token (#547)
> Follow https://github.com/cnpm/cnpmcore/pull/488 , add lastUsedAt for
classic tokens.
1. 🧶 Modify the `checkTokenExpired` method to `checkTokenStatus` which
update the token field internally.
2. ♻️ No compensation will be made for existing data, and it should be
updated by the consuming end.
---------
> Follow https://github.com/cnpm/cnpmcore/pull/488, 为 classic token 也添加
lastUsedAt 信息
1. 🧶 修改 `checkTokenExpired` 方法为 `checkTokenStatus`,内部进行 token 字段更新
2. ♻️ 存量数据不做补偿,由消费端控制
2023-07-12 15:11:20 +08:00
semantic-release-bot
f5da7e6c19 Release 3.36.0
[skip ci]

## [3.36.0](https://github.com/cnpm/cnpmcore/compare/v3.35.1...v3.36.0) (2023-07-09)

### Features

* support strictValidateTarballPkg ([#546](https://github.com/cnpm/cnpmcore/issues/546)) ([dd3438f](dd3438f470)), closes [#542](https://github.com/cnpm/cnpmcore/issues/542)
2023-07-09 15:34:17 +00:00
elrrrrrrr
dd3438f470 feat: support strictValidateTarballPkg (#546)
> Validate the manifest and tarball info to prevent contamination during
consumption, closes #542.
1. 🔨 Added the "strictValidateTarballPkg" mode to enable validation,
only applicable to the slef registry scenario.
2. 🧶 When the configuration is enabled, validate the relevant fields
during publishing, currently only validating the fields affecting
consumption.
3. ♻️ No corrective actions will be taken for existing scenario data.
-----

> 发布时校验 manifest 和 tarball 字段是否陪陪,防止消费时被污染 closes #542
1. 🔨 新增 strictValidateTarballPkg 配置,仅对在发布当前 registry 场景下生效
2. 🧶 配置开启时,发布时校验相关字段,目前仅校验影响消费相关字段
3. ♻️ 存量场景数据不做订正处理
2023-07-09 23:32:53 +08:00
semantic-release-bot
18af011a51 Release 3.35.1
[skip ci]

## [3.35.1](https://github.com/cnpm/cnpmcore/compare/v3.35.0...v3.35.1) (2023-06-29)

### Bug Fixes

* update source registry ([#537](https://github.com/cnpm/cnpmcore/issues/537)) ([ab2fde7](ab2fde7c80))
2023-06-29 07:51:11 +00:00
elrrrrrrr
ab2fde7c80 fix: update source registry (#537)
> the registryInfo in pkg fullManifest should be updated when the
package is migrated to another registry.

1. 🐞 when query from DB, dynamically add registry information.
2. ♻️ when hit cache, updating metadata should already trigger cache
modifications.
--------

> 当包从属 registryId 发生变化时,包 manifest 内的信息也需要同步更新
1. 🐞 从 db 读取元信息时,实时添加 registry 信息
2. ♻️ 从缓存读取元信息时,发布或修改元信息时已触发缓存修改
2023-06-29 15:49:56 +08:00
semantic-release-bot
ebcb65d27f Release 3.35.0
[skip ci]

## [3.35.0](https://github.com/cnpm/cnpmcore/compare/v3.34.10...v3.35.0) (2023-06-28)

### Features

* adaptive username ([#536](https://github.com/cnpm/cnpmcore/issues/536)) ([dd69606](dd69606365)), closes [/github.com/npm/cli/blob/latest/lib/commands/owner.js#L151](https://github.com/cnpm//github.com/npm/cli/blob/latest/lib/commands/owner.js/issues/L151)
2023-06-28 12:50:47 +00:00
elrrrrrrr
dd69606365 feat: adaptive username (#536)
> When executing npm cli owner add command,
[ref](https://github.com/npm/cli/blob/latest/lib/commands/owner.js#L151),
it causes errors due to duplicate additions,when the selfRegistry
configuration has the userPrefix option enabled.
1. 🧶 Modify the user query api to prioritize returning users from
selfRegistry
2. 🧶 Ensure that the query api uniformly returns displayName

---------

> npm cli 执行 owner add 时
[ref](https://github.com/npm/cli/blob/latest/lib/commands/owner.js#L151),依赖查询结果做去重,selfRegistry
配置 userPrefix 时会导致重复添加报错
1. 🧶 修改用户查询接口,优先返回 selfRegistry 内的用户
2. 🧶 查询接口统一返回 displayName
2023-06-28 20:49:32 +08:00
semantic-release-bot
a4a0f2df3a Release 3.34.10
[skip ci]

## [3.34.10](https://github.com/cnpm/cnpmcore/compare/v3.34.9...v3.34.10) (2023-06-28)

### Bug Fixes

* only syncUpstream in default registry ([#535](https://github.com/cnpm/cnpmcore/issues/535)) ([bb5d993](bb5d993030))
2023-06-28 05:58:08 +00:00
elrrrrrrr
bb5d993030 fix: only syncUpstream in default registry (#535)
> Only syncUpstream for default registry, to optimize the
synchronization speed.
* 🧶 Adjust the syncUpstream judgment process.

----

> 仅对公网包进行 syncUpstream 流程处理,内网包没有多级代理流程,优化同步速度。
* 🧶 调整 syncUpstream 判断流程
2023-06-28 13:56:54 +08:00
semantic-release-bot
506969615b Release 3.34.9
[skip ci]

## [3.34.9](https://github.com/cnpm/cnpmcore/compare/v3.34.8...v3.34.9) (2023-06-27)

### Bug Fixes

* console ([#534](https://github.com/cnpm/cnpmcore/issues/534)) ([4141003](4141003e13))
2023-06-27 04:30:55 +00:00
elrrrrrrr
4141003e13 fix: console (#534)
> remove console.log

----

去除 console.log
2023-06-27 12:29:14 +08:00
semantic-release-bot
241677687a Release 3.34.8
[skip ci]

## [3.34.8](https://github.com/cnpm/cnpmcore/compare/v3.34.7...v3.34.8) (2023-06-27)

### Bug Fixes

* legacy pkg publish ([#533](https://github.com/cnpm/cnpmcore/issues/533)) ([20ffba8](20ffba8d41))
2023-06-27 03:55:20 +00:00
elrrrrrrr
20ffba8d41 fix: legacy pkg publish (#533)
> pkgs sync from cnpmjs.org may contain uppercase characters.
1. 🧶 Update the validation rules, allow to publish existing packages.
---------
> 部分包从 cnpmjs.org 进行同步,可能含有大写字符
1. 🧶 更新校验规则,允许存量包修改
2023-06-27 11:54:02 +08:00
semantic-release-bot
c0415c01f7 Release 3.34.7
[skip ci]

## [3.34.7](https://github.com/cnpm/cnpmcore/compare/v3.34.6...v3.34.7) (2023-06-25)

### Bug Fixes

* sync self pkg ([#532](https://github.com/cnpm/cnpmcore/issues/532)) ([ada3e22](ada3e220a1))
2023-06-25 15:03:34 +00:00
elrrrrrrr
ada3e220a1 fix: sync self pkg (#532)
> During the syncUpstream process, it will attempt to create sync
repeatedly until it times out, when the pkg has been published in the
self registry.
1. 🐞 When executing the syncTask, filter out scenarios where the target
registry is the self registry.
-------

> 包迁移至当前 registry 时,收到同步请求会产生无效的同步任务,当 `syncUpstream` 时,会尝试重复创建 sync
直到超时。
1. 🐞 syncTask 执行时,先过滤目标 registry 是当前 registry 的场景。
2023-06-25 23:02:11 +08:00
semantic-release-bot
fece88201d Release 3.34.6
[skip ci]

## [3.34.6](https://github.com/cnpm/cnpmcore/compare/v3.34.5...v3.34.6) (2023-06-25)

### Bug Fixes

* only auto install s3-cnpmcore on Node.js 18+ ([#531](https://github.com/cnpm/cnpmcore/issues/531)) ([c9d9ce8](c9d9ce8205))
2023-06-25 09:49:58 +00:00
fengmk2
c9d9ce8205 fix: only auto install s3-cnpmcore on Node.js 18+ (#531)
closes https://github.com/cnpm/cnpmcore/issues/530
2023-06-25 17:48:39 +08:00
semantic-release-bot
ff8a81cde4 Release 3.34.5
[skip ci]

## [3.34.5](https://github.com/cnpm/cnpmcore/compare/v3.34.4...v3.34.5) (2023-06-21)

### Bug Fixes

* create sync task by 'GET /:fullname/-/:filenameWithVersion.tgz' ([#526](https://github.com/cnpm/cnpmcore/issues/526)) ([5ceaa6b](5ceaa6b8dd))
2023-06-21 00:48:33 +00:00
hezhengxu2018
5ceaa6b8dd fix: create sync task by 'GET /:fullname/-/:filenameWithVersion.tgz' (#526)
used by pnpm project with lock

closes https://github.com/cnpm/cnpmcore/issues/525
2023-06-21 08:46:48 +08:00
semantic-release-bot
110fdaef55 Release 3.34.4
[skip ci]

## [3.34.4](https://github.com/cnpm/cnpmcore/compare/v3.34.3...v3.34.4) (2023-06-20)

### Bug Fixes

* avoid db query on sync mode all ([#527](https://github.com/cnpm/cnpmcore/issues/527)) ([49855d9](49855d97e5)), closes [/github.com/cnpm/cnpmcore/pull/522/files#r1234655574](https://github.com/cnpm//github.com/cnpm/cnpmcore/pull/522/files/issues/r1234655574)
2023-06-20 02:16:40 +00:00
fengmk2
49855d97e5 fix: avoid db query on sync mode all (#527)
https://github.com/cnpm/cnpmcore/pull/522/files#r1234655574
2023-06-20 10:15:26 +08:00
semantic-release-bot
84499bc9f8 Release 3.34.3
[skip ci]

## [3.34.3](https://github.com/cnpm/cnpmcore/compare/v3.34.2...v3.34.3) (2023-06-17)

### Bug Fixes

* merge docker ENV into one layer ([#523](https://github.com/cnpm/cnpmcore/issues/523)) ([eb91b83](eb91b834c0))
2023-06-17 16:32:14 +00:00
fengmk2
eb91b834c0 fix: merge docker ENV into one layer (#523) 2023-06-18 00:29:46 +08:00
semantic-release-bot
182bc8b3e7 Release 3.34.2
[skip ci]

## [3.34.2](https://github.com/cnpm/cnpmcore/compare/v3.34.1...v3.34.2) (2023-06-17)

### Bug Fixes

* should redirect when nfs adapter support url ([#522](https://github.com/cnpm/cnpmcore/issues/522)) ([3d6864c](3d6864c713))
2023-06-17 16:13:29 +00:00
hezhengxu2018
3d6864c713 fix: should redirect when nfs adapter support url (#522)
will check package version on database before redirect to nfs store url

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

---------

Co-authored-by: fengmk2 <fengmk2@gmail.com>
2023-06-18 00:02:12 +08:00
semantic-release-bot
713c879af0 Release 3.34.1
[skip ci]

## [3.34.1](https://github.com/cnpm/cnpmcore/compare/v3.34.0...v3.34.1) (2023-06-14)

### Bug Fixes

* add block package by packageId and name function ([#514](https://github.com/cnpm/cnpmcore/issues/514)) ([b81b2a0](b81b2a03f8))
2023-06-14 09:08:42 +00:00
Ke Wu
b81b2a03f8 fix: add block package by packageId and name function (#514) 2023-06-14 17:07:23 +08:00
semantic-release-bot
f588e272b3 Release 3.34.0
[skip ci]

## [3.34.0](https://github.com/cnpm/cnpmcore/compare/v3.33.0...v3.34.0) (2023-06-13)

### Features

* sync package readme ([#512](https://github.com/cnpm/cnpmcore/issues/512)) ([f64e273](f64e273566))
* use unpkg README.md to update package version readme property ([#511](https://github.com/cnpm/cnpmcore/issues/511)) ([56d8e1a](56d8e1ad87))
2023-06-13 03:29:45 +00:00
fengmk2
f64e273566 feat: sync package readme (#512) 2023-06-13 11:28:31 +08:00
fengmk2
56d8e1ad87 feat: use unpkg README.md to update package version readme property (#511)
closes https://github.com/cnpm/cnpmcore/issues/481
2023-06-13 08:42:26 +08:00
semantic-release-bot
7df9648e6b Release 3.33.0
[skip ci]

## [3.33.0](https://github.com/cnpm/cnpmcore/compare/v3.32.0...v3.33.0) (2023-06-12)

### Features

* support Dockerfile and S3 nfs ([#509](https://github.com/cnpm/cnpmcore/issues/509)) ([f61ef1c](f61ef1c058))
2023-06-12 01:20:41 +00:00
fengmk2
f61ef1c058 feat: support Dockerfile and S3 nfs (#509)
closes https://github.com/cnpm/cnpmcore/issues/507
2023-06-12 09:19:03 +08:00
semantic-release-bot
428b1f3299 Release 3.32.0
[skip ci]

## [3.32.0](https://github.com/cnpm/cnpmcore/compare/v3.31.0...v3.32.0) (2023-06-11)

### Features

* sync specified versions ([#487](https://github.com/cnpm/cnpmcore/issues/487)) ([a9bb81a](a9bb81adfb))
2023-06-11 12:14:23 +00:00
hezhengxu2018
a9bb81adfb feat: sync specified versions (#487)
允许同步指定版本
---------
Allow to sync the specified versions
2023-06-11 20:12:59 +08:00
semantic-release-bot
1dbf481a11 Release 3.31.0
[skip ci]

## [3.31.0](https://github.com/cnpm/cnpmcore/compare/v3.30.2...v3.31.0) (2023-06-11)

### Features

* Support environment variable for config 🚀  ([#489](https://github.com/cnpm/cnpmcore/issues/489)) ([d4d7a3d](d4d7a3d7c8))
2023-06-11 07:08:07 +00:00
Nadeshiko Manju
d4d7a3d7c8 feat: Support environment variable for config 🚀 (#489)
closes https://github.com/cnpm/cnpmcore/issues/465

---------

Co-authored-by: fengmk2 <fengmk2@gmail.com>
2023-06-11 15:06:37 +08:00
semantic-release-bot
f79ac030c1 Release 3.30.2
[skip ci]

## [3.30.2](https://github.com/cnpm/cnpmcore/compare/v3.30.1...v3.30.2) (2023-06-09)

### Bug Fixes

* sync DOWNLOAD_PATHS code update ([#506](https://github.com/cnpm/cnpmcore/issues/506)) ([bf2bf64](bf2bf64532))
2023-06-09 06:48:38 +00:00
fengmk2
bf2bf64532 fix: sync DOWNLOAD_PATHS code update (#506)
closes https://github.com/cnpm/cnpmcore/issues/505
2023-06-09 14:47:01 +08:00
semantic-release-bot
816fa16526 Release 3.30.1
[skip ci]

## [3.30.1](https://github.com/cnpm/cnpmcore/compare/v3.30.0...v3.30.1) (2023-06-07)

### Bug Fixes

* type import ([#502](https://github.com/cnpm/cnpmcore/issues/502)) ([27ee3d6](27ee3d61a3))
2023-06-07 10:11:51 +00:00
elrrrrrrr
27ee3d61a3 fix: type import (#502)
> fix loading issues in the integrate mode
* 🧶 Modify the spec validation to only support `version` `tag` `range`
and not `alias` method.
* 🤖 Remove ts baseUrl config, use relative paths uniformly.

----

> 修复集成模式加载失败
* 🧶 修改 Spec 校验,仅支持 `version` `tag` `range`,不支持 `alias` 方式
* 🤖 去除 ts baseUrl 配置,统一使用相对路径
2023-06-07 18:10:36 +08:00
semantic-release-bot
2001168ab8 Release 3.30.0
[skip ci]

## [3.30.0](https://github.com/cnpm/cnpmcore/compare/v3.29.4...v3.30.0) (2023-06-07)

### Features

* bulk update maintainers ([#501](https://github.com/cnpm/cnpmcore/issues/501)) ([1b6dce6](1b6dce65ac))
2023-06-07 08:13:12 +00:00
elrrrrrrr
1b6dce65ac feat: bulk update maintainers (#501)
> Some dirty data of userPrefix in the maintainers cannot be cleared
during synchronization. Adjust the synchronization logic for
maintainers.
1. 🧶 When synchronizing packages, uniformly update the manifest dist.
2. 🛠️ `replacereplacePackageMaintainers` =>
`replacePackageMaintainersAndDist`
-----
> 部分旧包 maintainer 中的 userPrefix 脏数据同步时由于没有成员变更,无法清除,调整 maintainer 同步逻辑
1. 🧶 包同步时,统一更新 maintainer 对应的 manifest dist
2. 🛠️ `replacereplacePackageMaintainers` =>
`replacePackageMaintainersAndDist`
2023-06-07 16:12:11 +08:00
semantic-release-bot
f1bbd267b4 Release 3.29.4
[skip ci]

## [3.29.4](https://github.com/cnpm/cnpmcore/compare/v3.29.3...v3.29.4) (2023-06-07)

### Bug Fixes

* validate pkg@version spec ([#500](https://github.com/cnpm/cnpmcore/issues/500)) ([a9d2ff7](a9d2ff7e54))
2023-06-07 08:01:13 +00:00
elrrrrrrr
a9d2ff7e54 fix: validate pkg@version spec (#500)
> follow https://github.com/cnpm/cnpmcore/pull/495 after supporting
spec, adjust the parameter validation rules
1. 🆕 Add `Spec` validation rule, validating the spec by npa
2. 🛠️ Upgrade versionOrTag to versionSpec to support semver expressions,
such as `^2.x || > 3.x`
---------

> follow https://github.com/cnpm/cnpmcore/pull/495 支持 spec 后,调整参数校验规则
1. 🆕 新增 `Sepc` 校验规则,使用 npa 拼接包名进行验证
2. 🛠️ versionOrTag 升级为 versionSpec 支持 semver 表达式,例如 `^2.x || > 3.x`
2023-06-07 15:59:58 +08:00
semantic-release-bot
efb6412bac Release 3.29.3
[skip ci]

## [3.29.3](https://github.com/cnpm/cnpmcore/compare/v3.29.2...v3.29.3) (2023-06-07)

### Bug Fixes

* auto fix invalid version to any version ([#499](https://github.com/cnpm/cnpmcore/issues/499)) ([0cf6470](0cf6470993))
2023-06-07 02:02:51 +00:00
fengmk2
0cf6470993 fix: auto fix invalid version to any version (#499) 2023-06-07 10:01:40 +08:00
fengmk2
7fbf79f038 chore: print more error infomation 2023-06-07 09:18:58 +08:00
semantic-release-bot
06eaa13116 Release 3.29.2
[skip ci]

## [3.29.2](https://github.com/cnpm/cnpmcore/compare/v3.29.1...v3.29.2) (2023-06-07)

### Bug Fixes

* ignore fix invalid version ([#498](https://github.com/cnpm/cnpmcore/issues/498)) ([f5607c0](f5607c0a87))
2023-06-07 00:59:48 +00:00
fengmk2
f5607c0a87 fix: ignore fix invalid version (#498) 2023-06-07 08:58:35 +08:00
semantic-release-bot
cb5117aa72 Release 3.29.1
[skip ci]

## [3.29.1](https://github.com/cnpm/cnpmcore/compare/v3.29.0...v3.29.1) (2023-06-07)

### Bug Fixes

* fix encoded semver spec ([#497](https://github.com/cnpm/cnpmcore/issues/497)) ([aa39ced](aa39cedf35))
2023-06-07 00:36:25 +00:00
killa
aa39cedf35 fix: fix encoded semver spec (#497) 2023-06-07 08:34:37 +08:00
semantic-release-bot
183223b67d Release 3.29.0
[skip ci]

## [3.29.0](https://github.com/cnpm/cnpmcore/compare/v3.28.0...v3.29.0) (2023-06-06)

### Features

* infer userPrefix when update maintainers  ([#496](https://github.com/cnpm/cnpmcore/issues/496)) ([e02ea2a](e02ea2a3e5))
2023-06-06 13:00:58 +00:00
elrrrrrrr
e02ea2a3e5 feat: infer userPrefix when update maintainers (#496)
> When update members , infer the userPrefix by default to be compatible
with the default npm cli.
* 🧶 When adding a member, query the userPrefix corresponding to the
registry.
```
 $ npm owner add elrrrrrrr @cnpm/example --registry=http://127.0.0.1:7001
 $ npm owner add cnpm:elrrrrrrr @cnpm/example --registry=http://127.0.0.1:7001
```

-------
> 使用 cli 更新成员时,默认推导 userPrefix 信息,兼容 npm 客户端默认流程
* 🧶 添加成员时,查询 registry 对应的 userPrefix
```
 $ npm owner add elrrrrrrr @cnpm/example --registry=http://127.0.0.1:7001               
 $ npm owner add cnpm:elrrrrrrr @cnpm/example --registry=http://127.0.0.1:7001
```
2023-06-06 20:59:46 +08:00
semantic-release-bot
df8e114a5b Release 3.28.0
[skip ci]

## [3.28.0](https://github.com/cnpm/cnpmcore/compare/v3.27.0...v3.28.0) (2023-06-06)

### Features

* impl fast semver search ([#495](https://github.com/cnpm/cnpmcore/issues/495)) ([a7fd3a8](a7fd3a8c8a))
2023-06-06 12:57:26 +00:00
killa
a7fd3a8c8a feat: impl fast semver search (#495)
Including SQL change: `sql/3.28.0.sql`
2023-06-06 20:56:05 +08:00
semantic-release-bot
926d724793 Release 3.27.0
[skip ci]

## [3.27.0](https://github.com/cnpm/cnpmcore/compare/v3.26.0...v3.27.0) (2023-06-04)

### Features

* source registry manifest ([#493](https://github.com/cnpm/cnpmcore/issues/493)) ([bbec9a3](bbec9a38bd))
2023-06-04 08:58:24 +00:00
elrrrrrrr
bbec9a38bd feat: source registry manifest (#493)
> Add the `_source_registry_manifest` in pkgFullManifest.
* 🧶 Set registryInfo during reading because of the excessive existing
data.
* ♻️ The change takes effect when redis cache expired.
----------

> 在 pkgFullManifest 中添加 _source_registry_manifest 相关字段
1. 🧶 存量数据过多,在读取时统一设置,展示当前对应 registryId
2. ♻️ 在读取 db 时生效,需要等缓存过期
2023-06-04 16:57:19 +08:00
semantic-release-bot
3a423a2eb2 Release 3.26.0
[skip ci]

## [3.26.0](https://github.com/cnpm/cnpmcore/compare/v3.25.1...v3.26.0) (2023-06-04)

### Features

* whoami return granular token info ([#494](https://github.com/cnpm/cnpmcore/issues/494)) ([d0d2f78](d0d2f78d7b))
2023-06-04 08:56:19 +00:00
elrrrrrrr
d0d2f78d7b feat: whoami return granular token info (#494)
> add token info when invoke `whoami` to notify the caller about the
token's current status.

* 🧶 Add token information to the "whoami" interface.
* 🔨 Modify the query logic for allowedPackages uniformly within the
repository.
-----------

> 当使用 granularToken 调用 whoami 信息时,返回当前 token 信息,告知调用方当前 token 状态
* 🧶 在whoami 接口中添加 token 信息
* 🔨 修改 allowedPackages 查询逻辑,统一在 repository 中集成
2023-06-04 16:54:48 +08:00
semantic-release-bot
ad9adf7cd0 Release 3.25.1
[skip ci]

## [3.25.1](https://github.com/cnpm/cnpmcore/compare/v3.25.0...v3.25.1) (2023-06-03)

### Bug Fixes

* hook enable ([#492](https://github.com/cnpm/cnpmcore/issues/492)) ([40e2f92](40e2f92b94))
2023-06-03 11:12:00 +00:00
elrrrrrrr
40e2f92b94 fix: hook enable (#492)
> Fix hookEnable config does not take effect
* 🐞 Modify config key in changesStream event
* 🤖 Add corresponding unit tests
---------
> 修复 hookEnable 开关配置不生效的问题
* 🐞 修改 events 中配置获取异常
* 🤖 添加对应单测
2023-06-03 19:10:33 +08:00
semantic-release-bot
e88a610011 Release 3.25.0
[skip ci]

## [3.25.0](https://github.com/cnpm/cnpmcore/compare/v3.24.0...v3.25.0) (2023-06-02)

### Features

* add _npmUser ([#491](https://github.com/cnpm/cnpmcore/issues/491)) ([f7b5d5a](f7b5d5af12))
2023-06-02 04:38:14 +00:00
elrrrrrrr
f7b5d5af12 feat: add _npmUser (#491)
> For private packages published in the current registry, add the
"_npmUser" field to align with the npm registry.

* 🧶 Add the "_npmUser" field for new scenarios, without modifying the
abbreviated data. Use the following command: curl -H 'Accept:
application/vnd.npm.install-v1+json'
'https://registry.npmjs.org/cnpmcore'
* ♻️ Existing data cannot be traced and will not be compensated.
-----

> 对于在当前 registry 发布的私有包,添加 _npmUser 字段,和公网 registry 保持一致
* 🧶 新增 _npmUser 字段,abbreviated 场景不做修改, (via `curl -H 'Accept:
application/vnd.npm.install-v1+json'
'https://registry.npmjs.org/cnpmcore'`)
* ♻️ 存量数据无法回溯,不做补偿
2023-06-02 12:36:53 +08:00
semantic-release-bot
f30e517f9e Release 3.24.0
[skip ci]

## [3.24.0](https://github.com/cnpm/cnpmcore/compare/v3.23.2...v3.24.0) (2023-06-01)

### Features

* token last used at ([#488](https://github.com/cnpm/cnpmcore/issues/488)) ([3a8a91a](3a8a91ae0b))
2023-06-01 11:19:35 +00:00
elrrrrrrr
3a8a91ae0b feat: token last used at (#488)
> Add the lastUsedBy field to the Token model

* 🧶 Add `lastUsedBy` field to the token, for platform display.
* 🐞 Fix the issue where the graunlarToken is not expired in read-only
scenarios.
------
> 为 Token 模型添加 lastUsedBy 字段
* 🧶 新增 lastUsedBy 字段,记录 token 最近使用时间,用于平台展示
* 🐞 修复 graunlarToken 过期时,只读场景没有禁用的问题
2023-06-01 19:18:17 +08:00
semantic-release-bot
0c1e2ceb7f Release 3.23.2
[skip ci]

## [3.23.2](https://github.com/cnpm/cnpmcore/compare/v3.23.1...v3.23.2) (2023-05-31)

### Bug Fixes

* unpkg support non-npm publish tgz file ([#485](https://github.com/cnpm/cnpmcore/issues/485)) ([5fe883f](5fe883f878)), closes [/github.com/cnpm/cnpmcore/issues/452#issuecomment-1570077310](https://github.com/cnpm//github.com/cnpm/cnpmcore/issues/452/issues/issuecomment-1570077310)
2023-05-31 12:44:11 +00:00
fengmk2
5fe883f878 fix: unpkg support non-npm publish tgz file (#485)
https://github.com/cnpm/cnpmcore/issues/452#issuecomment-1570077310
2023-05-31 20:42:19 +08:00
semantic-release-bot
a7258aa7ec Release 3.23.1
[skip ci]

## [3.23.1](https://github.com/cnpm/cnpmcore/compare/v3.23.0...v3.23.1) (2023-05-30)

### Bug Fixes

* use package version publishTime instead of file mtime ([#483](https://github.com/cnpm/cnpmcore/issues/483)) ([68f6b6b](68f6b6b944))
2023-05-30 15:58:42 +00:00
fengmk2
68f6b6b944 fix: use package version publishTime instead of file mtime (#483)
closes https://github.com/cnpm/cnpmcore/issues/482
2023-05-30 23:57:34 +08:00
semantic-release-bot
9e5e555552 Release 3.23.0
[skip ci]

## [3.23.0](https://github.com/cnpm/cnpmcore/compare/v3.22.3...v3.23.0) (2023-05-29)

### Features

* export getUserAndToken ([#480](https://github.com/cnpm/cnpmcore/issues/480)) ([aa4fdd3](aa4fdd3545))
2023-05-29 17:39:20 +00:00
elrrrrrrr
aa4fdd3545 feat: export getUserAndToken (#480)
> Export `getUserAndToken` method for authorization parsing in integrate
mode.

1. 🧶 TokenService adds getUserAndToken method

----------

> 暴露 `getUserAndToken` 方法进行 authorization 解析,面向集成模式的场景
1. 🧶 TokenService 新增 `getUserAndToken` 方法

---------

Co-authored-by: fengmk2 <fengmk2@gmail.com>
2023-05-30 01:37:22 +08:00
semantic-release-bot
1b89b64356 Release 3.22.3
[skip ci]

## [3.22.3](https://github.com/cnpm/npmcore/compare/v3.22.2...v3.22.3) (2023-05-29)

### Bug Fixes

* unpkg redirect ([#479](https://github.com/cnpm/npmcore/issues/479)) ([c395c79](c395c7906b))
2023-05-29 10:03:47 +00:00
elrrrrrrr
c395c7906b fix: unpkg redirect (#479) 2023-05-29 18:02:23 +08:00
fengmk2
cc01398a16 refactor: use binaries.prisma.sh to download prisma files (#478) 2023-05-25 17:52:46 +08:00
semantic-release-bot
be228399d1 Release 3.22.2
[skip ci]

## [3.22.2](https://github.com/cnpm/npmcore/compare/v3.22.1...v3.22.2) (2023-05-25)

### Bug Fixes

* use S3 url to download file ([#477](https://github.com/cnpm/npmcore/issues/477)) ([9bed829](9bed829628)), closes [/github.com/cnpm/cnpmcore/issues/472#issuecomment-1562452369](https://github.com/cnpm//github.com/cnpm/cnpmcore/issues/472/issues/issuecomment-1562452369)
2023-05-25 08:11:26 +00:00
fengmk2
9bed829628 fix: use S3 url to download file (#477)
https://github.com/cnpm/cnpmcore/issues/472#issuecomment-1562452369
2023-05-25 16:10:18 +08:00
semantic-release-bot
9c60a597f2 Release 3.22.1
[skip ci]

## [3.22.1](https://github.com/cnpm/npmcore/compare/v3.22.0...v3.22.1) (2023-05-25)

### Bug Fixes

* refactor config type ([#476](https://github.com/cnpm/npmcore/issues/476)) ([ebc8c98](ebc8c98fa4))
2023-05-25 07:20:59 +00:00
elrrrrrrr
ebc8c98fa4 fix: refactor config type (#476)
> New CnpmcoreConfig type to handle compatibility issues
https://github.com/cnpm/cnpmcore/pull/475
1. 🆕 add a new CnpmcoreConfig in port/config, referenced in
config.default.ts
2. 🆕 add `ChangesStreamMode` enums.
-------------
> 新增 CnpmcoreConfig 类型处理兼容问题 https://github.com/cnpm/cnpmcore/pull/475
1. 🆕 新增 CnpmcoreConfig,config.default.ts 中进行引用
2. 🆕 新增 `ChangesStreamMode` 枚举


![image](https://github.com/cnpm/cnpmcore/assets/5574625/1fd4afd0-0739-4021-a134-2311bbf78713)
2023-05-25 15:19:51 +08:00
semantic-release-bot
517bb8e8d4 Release 3.22.0
[skip ci]

## [3.22.0](https://github.com/cnpm/npmcore/compare/v3.21.0...v3.22.0) (2023-05-25)

### Features

* sync prisma binary from R2 ([#474](https://github.com/cnpm/npmcore/issues/474)) ([ce4e868](ce4e8681ae))
2023-05-25 03:45:07 +00:00
fengmk2
ce4e8681ae feat: sync prisma binary from R2 (#474)
closes https://github.com/cnpm/cnpmcore/issues/473
2023-05-25 11:43:52 +08:00
fengmk2
26f5eaf438 refactor: jsencrypt and jquery use unpkg (#471) 2023-05-24 11:09:22 +08:00
semantic-release-bot
81865a1790 Release 3.21.0
[skip ci]

## [3.21.0](https://github.com/cnpm/npmcore/compare/v3.20.3...v3.21.0) (2023-05-21)

### Features

* easy config ([#468](https://github.com/cnpm/npmcore/issues/468)) ([9208392](92083924ea))
2023-05-21 13:30:29 +00:00
elrrrrrrr
92083924ea feat: easy config (#468)
> Make the method for tegg integration mode to be more user-friendly.

* 🤖 Automatically add config.cnpmcore type hints.
* 🧶 Export the default `cnpmcoreConfig` , which needs to be explicitly
declared for app config.
* 📚 Supplement the documentation and field definitions.
------

> 对于 egg 集成模式,提供更加友好的自定义配置方式。
* 🤖 自动添加 config.cnpmcore 类型提示
* 🧶 输出默认的 cnpmcoreConfig 对象,应用集成需显式声明,防止新增配置丢失
* 📚 补充文档及字段定义信息


![image](https://github.com/cnpm/cnpmcore/assets/5574625/98d3e0df-32f5-4de5-990a-bc1561cd73be)

---------

Co-authored-by: fengmk2 <fengmk2@gmail.com>
2023-05-21 21:27:50 +08:00
elrrrrrrr
80ab0548f2 refactor: unpublish logic (#467)
> Adjust the logic for unpublishing a package
* 🧶 Determine if a call to unpublish within the removePackageVersion
function
* ♻️ Remove`forceRefresh` in unpublishPackage
-------
> 调整 unpublish package 逻辑
* 🧶 removePackageVersion 内判断是否需要调用 unpublish
* ♻️ unpublishPackage 删除 forceRefresh 逻辑
2023-05-18 22:45:18 +08:00
240 changed files with 25920 additions and 1673 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
logs
node_modules
run
typings
.cnpmcore*
coverage

View File

@@ -1,23 +0,0 @@
name: 🤖 ChatGPT Code Review
permissions:
contents: read
pull-requests: write
on:
pull_request:
types: [opened, reopened, synchronize]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: anc95/ChatGPT-CodeReview@main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
# Optional
LANGUAGE: Chinese
MODEL:
top_p: 1
temperature: 1

View File

@@ -13,12 +13,9 @@ name: "CodeQL"
on:
push:
branches: [ main ]
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '41 13 * * 3'
branches: [ master ]
jobs:
analyze:

View File

@@ -3,7 +3,11 @@
name: Node.js CI
on: [push, pull_request]
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test-mysql57-fs-nfs:
@@ -28,7 +32,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [16, 18, 20]
node-version: [18, 20, 22]
os: [ubuntu-latest]
steps:
@@ -79,7 +83,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [16, 18, 20]
node-version: [18, 20, 22]
os: [ubuntu-latest]
steps:

View File

@@ -1,18 +1,12 @@
name: Release
on:
# 合并后自动发布
push:
branches: [ master, main ]
# 手动发布
workflow_dispatch: {}
branches: [ master ]
jobs:
release:
name: Node.js
uses: artusjs/github-actions/.github/workflows/node-release.yml@v1
uses: cnpm/github-actions/.github/workflows/node-release.yml@master
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
with:
checkTest: false

File diff suppressed because it is too large Load Diff

17
Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM node:18
# 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,317 +0,0 @@
2.9.0 / 2022-12-15
==================
**features**
* [[`c562645`](http://github.com/cnpm/cnpmcore/commit/c562645db7c88f9c3c5787fd450b457574d1cce6)] - feat: suspend task before app close (#365) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
2.8.1 / 2022-12-05
==================
**features**
* [[`fad30ad`](http://github.com/cnpm/cnpmcore/commit/fad30adc564c931c0bf63828d83bab84105aaef0)] - feat: npm command support npm v6 (#356) (laibao101 <<369632567@qq.com>>)
**fixes**
* [[`f961219`](http://github.com/cnpm/cnpmcore/commit/f961219dbe4676156e1766db82379ee40087bcd8)] - fix: Sync save ignore ER_DUP_ENTRY error (#364) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
**others**
* [[`7bc0fcc`](http://github.com/cnpm/cnpmcore/commit/7bc0fccaca880efe08228b4109953bd3974d2eb9)] - 🤖 TEST: Fix async function mock (fengmk2 <<fengmk2@gmail.com>>)
* [[`84ae9bc`](http://github.com/cnpm/cnpmcore/commit/84ae9bcfa06124255703b926f83fb5e6a6bf9d6b)] - 📖 DOC: Update contributors (fengmk2 <<fengmk2@gmail.com>>)
2.8.0 / 2022-11-29
==================
**others**
* [[`d55c680`](http://github.com/cnpm/cnpmcore/commit/d55c680ef906ecb27f7967782ad7d25987cef7d4)] - Event cork (#361) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
2.7.1 / 2022-11-25
==================
**fixes**
* [[`c6b8aec`](http://github.com/cnpm/cnpmcore/commit/c6b8aecfd0c2b0d454389e931747c431dac5742b)] - fix: request binary error (#360) (Ke Wu <<gemwuu@163.com>>)
2.7.0 / 2022-11-25
==================
**others**
* [[`5738d56`](http://github.com/cnpm/cnpmcore/commit/5738d569ea691c05c3f3b0b74a454a33fefb8fc7)] - refactor: binary sync task use binaryName by default (#358) (Ke Wu <<gemwuu@163.com>>)
2.6.1 / 2022-11-23
==================
**fixes**
* [[`0b35ead`](http://github.com/cnpm/cnpmcore/commit/0b35ead2a0cd73b89d2d961bafec13d7250fe805)] - 🐛 FIX: typo for canvas (fengmk2 <<fengmk2@gmail.com>>)
2.6.0 / 2022-11-23
==================
**features**
* [[`be8387d`](http://github.com/cnpm/cnpmcore/commit/be8387dfa48b9487156542000a93081fa823694a)] - feat: Support canvas sync from different binary (#357) (Ke Wu <<gemwuu@163.com>>)
**fixes**
* [[`d6c4cf5`](http://github.com/cnpm/cnpmcore/commit/d6c4cf5029ca6450064fc05696a8624b6c36f0b2)] - fix: duplicate binary task (#354) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
2.5.2 / 2022-11-11
==================
**fixes**
* [[`7eb209d`](http://github.com/cnpm/cnpmcore/commit/7eb209de1332417db2070846891d78f5afa0cd10)] - fix: create task when waiting (#352) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
2.5.1 / 2022-11-07
==================
**others**
* [[`e40c502`](http://github.com/cnpm/cnpmcore/commit/e40c5021bb2ba78f8879d19bc477883168560b85)] - 🐛 FIX: Mirror cypress arm64 binary (#351) (fengmk2 <<fengmk2@gmail.com>>)
2.5.0 / 2022-11-04
==================
**features**
* [[`43d77ee`](http://github.com/cnpm/cnpmcore/commit/43d77ee91e52bd74594d9d569b839c1a4b7fbac6)] - feat: long description (#349) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
2.4.1 / 2022-10-28
==================
**features**
* [[`92350a8`](http://github.com/cnpm/cnpmcore/commit/92350a864313ee42a048d9e83886ef42db3419de)] - 👌 IMPROVE: Show changes stream create task log (#347) (fengmk2 <<fengmk2@gmail.com>>)
**fixes**
* [[`28eeeaf`](http://github.com/cnpm/cnpmcore/commit/28eeeafd9870c6b1c5b4f4c23916f6ae73ddda12)] - fix: registry host config (#346) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
* [[`cd5bd92`](http://github.com/cnpm/cnpmcore/commit/cd5bd923b8d47bf90b5f077ce04777b38653b850)] - 🐛 FIX: Catch all error on changes stream handler (#344) (fengmk2 <<fengmk2@gmail.com>>)
2.4.0 / 2022-10-25
==================
**features**
* [[`6aa302d`](http://github.com/cnpm/cnpmcore/commit/6aa302d074f2c84f39e2065fa20853b007f6fa3b)] - 📦 NEW: Use oss-cnpm v4 (#340) (fengmk2 <<fengmk2@gmail.com>>)
* [[`a217fd0`](http://github.com/cnpm/cnpmcore/commit/a217fd07ccad3fe5058881654a13e0c69c758717)] - 👌 IMPROVE: Reduce warning log (#326) (fengmk2 <<fengmk2@gmail.com>>)
**fixes**
* [[`b19b0a0`](http://github.com/cnpm/cnpmcore/commit/b19b0a0496e35ac1c6b3de746b9221990ba9dc93)] - fix: Lazy set registryId when executeTask (#341) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
**others**
* [[`305175a`](http://github.com/cnpm/cnpmcore/commit/305175ab5fcdc3ad3b60055d45cfcacb23065a80)] - 🤖 TEST: Use enum define on unittest (#333) (fengmk2 <<fengmk2@gmail.com>>)
* [[`07f2eba`](http://github.com/cnpm/cnpmcore/commit/07f2eba137ba625b2d422677a465920617141b87)] - 🤖 TEST: Mock all binary http requests (#328) (fengmk2 <<fengmk2@gmail.com>>)
* [[`4b0c7dc`](http://github.com/cnpm/cnpmcore/commit/4b0c7dc6196960d34b2529bfde724e97f1af8444)] - 🤖 TEST: Mock all httpclient request (#327) (fengmk2 <<fengmk2@gmail.com>>)
2.3.1 / 2022-10-06
==================
**features**
* [[`bbc08fd`](http://github.com/cnpm/cnpmcore/commit/bbc08fd26887d55b98b70d1ed210caf81f9d5c22)] - 👌 IMPROVE: syncPackageWorkerMaxConcurrentTasks up to 20 (#322) (fengmk2 <<fengmk2@gmail.com>>)
* [[`5852f22`](http://github.com/cnpm/cnpmcore/commit/5852f22023525d857ff1ceea205e4315c8079877)] - feat: support sync exist mode (#275) (zhangyuantao <<zhangyuantao@163.com>>)
**fixes**
* [[`d79634e`](http://github.com/cnpm/cnpmcore/commit/d79634eea749fef1a420988a8599f156f28ee85a)] - 🐛 FIX: Should sync package when registry id is null (#324) (fengmk2 <<fengmk2@gmail.com>>)
* [[`24f920d`](http://github.com/cnpm/cnpmcore/commit/24f920d65b31f9eb83c1ecda36adf7f9e2c379c3)] - 🐛 FIX: Should run sync package on all worker (#323) (fengmk2 <<fengmk2@gmail.com>>)
2.3.0 / 2022-09-24
==================
**others**
* [[`bd83a19`](http://github.com/cnpm/cnpmcore/commit/bd83a19eca761c96bcee04e6ae91e68eac3cb6bf)] - 👌 IMPROVE: use urllib3 instead (#302) (fengmk2 <<fengmk2@gmail.com>>)
* [[`35e7d3a`](http://github.com/cnpm/cnpmcore/commit/35e7d3ad3c78712b507d522a0b72b5a6a5a4ec1c)] - 👌 IMPROVE: Enable phpmyadmin and DEBUG_LOCAL_SQL by default (#320) (fengmk2 <<fengmk2@gmail.com>>)
2.2.0 / 2022-09-22
==================
**features**
* [[`bca0fb3`](http://github.com/cnpm/cnpmcore/commit/bca0fb3c37b9f74f3c41ab181dd3113d9dab4c05)] - feat: only allow pkg sync from registry it belong (#317) (killa <<killa123@126.com>>)
**fixes**
* [[`7e9beea`](http://github.com/cnpm/cnpmcore/commit/7e9beead576a41de3aa042b92b788bde5d55f44a)] - fix: only append / if path is not empty and not ends with / (#316) (killa <<killa123@126.com>>)
* [[`4fe68cb`](http://github.com/cnpm/cnpmcore/commit/4fe68cbf38f303e797b80b88407f714ec76bfae0)] - fix: fix directory path (#313) (killa <<killa123@126.com>>)
**others**
* [[`e72ce35`](http://github.com/cnpm/cnpmcore/commit/e72ce3576f9a3cda095e3feac59eeb1d8c1e8033)] - 🤖 TEST: Skip unstable tests (#318) (fengmk2 <<fengmk2@gmail.com>>)
* [[`171b11f`](http://github.com/cnpm/cnpmcore/commit/171b11f7bba534c993af4088b00f8545216734a9)] - Revert "fix: fix directory path (#313)" (fengmk2 <<fengmk2@gmail.com>>)
2.1.1 / 2022-09-08
==================
**fixes**
* [[`8fb9dd8`](http://github.com/cnpm/cnpmcore/commit/8fb9dd8cf4800afe3f54aba9ee4c0ae05efb4f1d)] - fix: findExecuteTask only return waiting task (#312) (killa <<killa123@126.com>>)
2.1.0 / 2022-09-05
==================
**features**
* [[`c5d2b49`](http://github.com/cnpm/cnpmcore/commit/c5d2b49ab3a0ce0d67f6e7cc19e0be867c92d04c)] - feat: auto get next valid task (#311) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
2.0.0 / 2022-09-05
==================
**others**
* [[`fc4baff`](http://github.com/cnpm/cnpmcore/commit/fc4baff226540e7cfee9adc069e17a59f4050a43)] - chore: refactor schedule with @Schedule (#309) (killa <<killa123@126.com>>)
1.11.6 / 2022-09-04
==================
**fixes**
* [[`768f951`](http://github.com/cnpm/cnpmcore/commit/768f951b6f2509f14c30a70d86a6719107d963a4)] - fix: cnpmjsorg changesstream limit (#310) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
1.11.5 / 2022-09-02
==================
**fixes**
* [[`f673ab8`](http://github.com/cnpm/cnpmcore/commit/f673ab8ba1545909ff6b8e445364646511930891)] - fix: execute state check (#308) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
**others**
* [[`091420a`](http://github.com/cnpm/cnpmcore/commit/091420ae2677ecedd1a26a238921321c2a191675)] - 🤖 TEST: Add SQL Review Action (#307) (fengmk2 <<fengmk2@gmail.com>>)
1.11.4 / 2022-08-30
==================
**fixes**
* [[`f9210ca`](http://github.com/cnpm/cnpmcore/commit/f9210ca7e180e19bce08da9ef33e46e990b86ef1)] - fix: changes stream empty (#306) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
1.11.3 / 2022-08-29
==================
**fixes**
* [[`48f228d`](http://github.com/cnpm/cnpmcore/commit/48f228da447d8cde62849fa52cf43bae7754e2e3)] - fix: changes stream updatedAt (#304) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
* [[`87045ba`](http://github.com/cnpm/cnpmcore/commit/87045ba8b0e14547c93689600eb7e2c1de2a611b)] - fix: task updatedAt save (#305) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
1.11.2 / 2022-08-28
==================
**fixes**
* [[`4e8700c`](http://github.com/cnpm/cnpmcore/commit/4e8700c4f7c6fb5c4f4d4a2b9a9546096c5d10e2)] - fix: only create createHookTask if hook enable (#299) (killa <<killa123@126.com>>)
**others**
* [[`e06c841`](http://github.com/cnpm/cnpmcore/commit/e06c841537113fdb0c00beb22b0a55378c61ce80)] - 🐛 FIX: Should sync public package when registryName not exists (#303) (fengmk2 <<fengmk2@gmail.com>>)
* [[`f139444`](http://github.com/cnpm/cnpmcore/commit/f139444213403494ebe9bf073df62125413892d9)] - 📖 DOC: Update contributors (fengmk2 <<fengmk2@gmail.com>>)
* [[`c4a9de5`](http://github.com/cnpm/cnpmcore/commit/c4a9de598dce9a1b82bbcdd91968a15bbc5a4b6b)] - Create SECURITY.md (fengmk2 <<fengmk2@gmail.com>>)
* [[`709d65b`](http://github.com/cnpm/cnpmcore/commit/709d65bd0473856c9bfc4416ea2ca375136e354f)] - 🤖 TEST: Use diff bucket on OSS test (#301) (fengmk2 <<fengmk2@gmail.com>>)
* [[`9576699`](http://github.com/cnpm/cnpmcore/commit/95766990fa9c4c2c43d462f6b151557425b0c741)] - chore: use AsyncGenerator insteadof Transform stream (#300) (killa <<killa123@126.com>>)
* [[`3ed5269`](http://github.com/cnpm/cnpmcore/commit/3ed5269f1d22ca3aaca89a90a4fff90f293e2464)] - 📦 NEW: Mirror better-sqlite3 binary (#296) (fengmk2 <<fengmk2@gmail.com>>)
1.11.1 / 2022-08-24
==================
**fixes**
* [[`359a150`](http://github.com/cnpm/cnpmcore/commit/359a150eb450d69e6523b20efcc5c7cfe3efab4d)] - fix: changes stream (#297) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
1.11.0 / 2022-08-23
==================
**features**
* [[`a91c8ac`](http://github.com/cnpm/cnpmcore/commit/a91c8ac4d05dc903780fda516b09364a05a2b1e6)] - feat: sync package from spec regsitry (#293) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
* [[`de37008`](http://github.com/cnpm/cnpmcore/commit/de37008261b05845f392d66764cdfe14ae324756)] - feat: changesStream adapter & needSync() method (#292) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
* [[`4b506c8`](http://github.com/cnpm/cnpmcore/commit/4b506c8371697ddacdbe99a8ecb330bfc1911ec6)] - feat: init registry & scope (#286) (elrrrrrrr <<elrrrrrrr@gmail.com>>)
* [[`41c6e24`](http://github.com/cnpm/cnpmcore/commit/41c6e24c84d546eb9d5515cc0940cc3e4274687b)] - feat: impl trigger Hooks (#289) (killa <<killa123@126.com>>)
* [[`79cb826`](http://github.com/cnpm/cnpmcore/commit/79cb82615f04bdb3da3ccbe09bb6a861608b69c5)] - feat: impl migration sql (#290) (killa <<killa123@126.com>>)
* [[`4cfa8ed`](http://github.com/cnpm/cnpmcore/commit/4cfa8ed9d687ce7d950d7d20c0ea28221763ba5f)] - feat: impl hooks api (#287) (killa <<killa123@126.com>>)
* [[`47d53d2`](http://github.com/cnpm/cnpmcore/commit/47d53d22ad03c02ee9cb9035a38ae205a6d38381)] - feat: add bizId for task (#285) (killa <<killa123@126.com>>)
* [[`3b1536b`](http://github.com/cnpm/cnpmcore/commit/3b1536b070b2f9062bc2cc377db96d2f4a160efc)] - feat: add node-webrtc mirror (#274) (Opportunity <<opportunity@live.in>>)
**others**
* [[`7106807`](http://github.com/cnpm/cnpmcore/commit/710680742a078b2faf4cb18c3a39c0397308712e)] - 🐛 FIX: Should show queue size on logging (#280) (fengmk2 <<fengmk2@gmail.com>>)
* [[`3a41b21`](http://github.com/cnpm/cnpmcore/commit/3a41b2161cc99bb2f6f6dd7cbaa7abef25ff4393)] - 🐛 FIX: Handle binary configuration value (#278) (fengmk2 <<fengmk2@gmail.com>>)
1.10.0 / 2022-08-04
==================
**features**
* [[`c2b7d5a`](http://github.com/cnpm/cnpmcore/commit/c2b7d5aa98b5ba8649ec246c616574a22e9a74b8)] - feat: use sort set to impl queue (#277) (killa <<killa123@126.com>>)
1.9.1 / 2022-07-29
==================
**fixes**
* [[`c54aa21`](http://github.com/cnpm/cnpmcore/commit/c54aa2165c3938dcbb5a2b3b54e66a0d961cc813)] - fix: check executingCount after task is done (#276) (killa <<killa123@126.com>>)
**others**
* [[`3268d03`](http://github.com/cnpm/cnpmcore/commit/3268d030b620825c8c2e6331e1745c1788066c61)] - 🤖 TEST: show package not use cache if isSync (#273) (fengmk2 <<fengmk2@gmail.com>>)
1.9.0 / 2022-07-25
==================
**features**
* [[`af6a75a`](http://github.com/cnpm/cnpmcore/commit/af6a75af32ea04c90fda82be3a56c99ec77e5807)] - feat: add forceSyncHistory options (#271) (killa <<killa123@126.com>>)
1.8.0 / 2022-07-21
==================
**features**
* [[`b49a38c`](http://github.com/cnpm/cnpmcore/commit/b49a38c77e044c978e6de32a9d3e257cc90ea7c1)] - feat: use Model with inject (#269) (killa <<killa123@126.com>>)
1.7.1 / 2022-07-20
==================
**fixes**
* [[`52fca55`](http://github.com/cnpm/cnpmcore/commit/52fca55aa883865f0ae70bfc1ff274c313b8f76a)] - fix: show package not use cache if isSync (#268) (killa <<killa123@126.com>>)
1.7.0 / 2022-07-12
==================
**others**
* [[`4f7ce8b`](http://github.com/cnpm/cnpmcore/commit/4f7ce8b4b2a5806a225ce67228388e14388b7059)] - deps: upgrade leoric to 2.x (#262) (killa <<killa123@126.com>>)
1.6.0 / 2022-07-11
==================
**features**
* [[`1b9a9c7`](http://github.com/cnpm/cnpmcore/commit/1b9a9c70f66d8393e3b132f18713461a9243db73)] - feat: mirror nydus binaries (#261) (killa <<killa123@126.com>>)
**others**
* [[`c1256bf`](http://github.com/cnpm/cnpmcore/commit/c1256bf3807bcc9a5c8be2ec5bf5ca8a5eef112e)] - 🐛 FIX: Ignore 403 status on s3 download fail (#260) (fengmk2 <<fengmk2@gmail.com>>)
* [[`d685772`](http://github.com/cnpm/cnpmcore/commit/d6857724307fb0df0c4c118491784b30d19a9a15)] - 🐛 FIX: skia-canvas should use NodePreGypBinary (#259) (fengmk2 <<fengmk2@gmail.com>>)
1.5.0 / 2022-07-09
==================
**features**
* [[`b15b10c`](http://github.com/cnpm/cnpmcore/commit/b15b10c5c6cfb32bcc2b1d94434cdd16871ae565)] - feat(mirror): add skia-canvas mirror (#258) (Beace <<beaceshimin@gmail.com>>)
**others**
* [[`2bd6ed0`](http://github.com/cnpm/cnpmcore/commit/2bd6ed0e5dace1d8840c342ecf4c86e8973dc6b7)] - 👌 IMPROVE: use tegg@1.2.0 (fengmk2 <<fengmk2@gmail.com>>)
1.4.0 / 2022-06-28
==================
**features**
* [[`57da0a3`](http://github.com/cnpm/cnpmcore/commit/57da0a3c7e56d6613b57391948949ffea24ec058)] - feat: add configuration enableNopmClientAndVersionCheck (laibao101 <<369632567@qq.com>>)
**others**
* [[`bf62932`](http://github.com/cnpm/cnpmcore/commit/bf62932f2e5224de6e34b873bf690a6e887b94b0)] - 🤖 TEST: Fix unstable test cases on OSS env (#254) (fengmk2 <<fengmk2@gmail.com>>)
1.3.2 / 2022-06-27
==================
**fixes**
* [[`c63159d`](http://github.com/cnpm/cnpmcore/commit/c63159d8df804fe711b664606fe42be42010eb38)] - fix: valid npm client with correct pattern (#252) (TZ | 天猪 <<atian25@qq.com>>)
**others**
* [[`d578baf`](http://github.com/cnpm/cnpmcore/commit/d578bafff07a0f9d4dd75393492cffc7f5d2660b)] - 🐛 FIX: Ignore exists seq on changes worker (#253) (fengmk2 <<fengmk2@gmail.com>>)
1.3.1 / 2022-06-24
==================
**fixes**
* [[`4ea0ef6`](http://github.com/cnpm/cnpmcore/commit/4ea0ef63b7af9fd4dcc247c2c2ac8e4d579f941a)] - fix: query changes with order by id asc (#251) (killa <<killa123@126.com>>)
1.3.0 / 2022-06-24
==================
**features**
* [[`0948a71`](http://github.com/cnpm/cnpmcore/commit/0948a71a40ac4897d129ef56830665dc028f07c7)] - feat: read enableChangesStream when sync changes stream (#250) (killa <<killa123@126.com>>)
1.2.0 / 2022-06-20
==================
**others**
* [[`c0d8b52`](http://github.com/cnpm/cnpmcore/commit/c0d8b52ea09736ac11b0ef780aec781d172fb94c)] - refactor: move CacheAdapter to ContextProto (#249) (killa <<killa123@126.com>>)
1.1.0 / 2022-06-20
==================
**features**
* [[`66b411e`](http://github.com/cnpm/cnpmcore/commit/66b411ea5bf6192dc9509df408525078e7128a27)] - feat: add type for exports (#248) (killa <<killa123@126.com>>)
1.0.0 / 2022-06-17
==================
**others**
* [[`5cadbf4`](http://github.com/cnpm/cnpmcore/commit/5cadbf4b22bee7d85cd14526f5d6c6e2cd3a2e4b)] - refactor: add infra module (#245) (killa <<killa123@126.com>>),fatal: No names found, cannot describe anything.

View File

@@ -1,4 +1,5 @@
# 🥚 如何在 [tegg](https://github.com/eggjs/tegg) 中集成 cnpmcore
> 文档中的示例项目可以在 [这里](https://github.com/eggjs/examples/commit/bed580fe053ae573f8b63f6788002ff9c6e7a142) 查看,在开始前请确保已阅读 [DEVELOPER.md](DEVELOPER.md) 中的相关文档,完成本地开发环境搭建。
在生产环境中,我们也可以直接部署 cnpmcore 系统,实现完整的 Registry 镜像功能。
@@ -12,7 +13,8 @@
## 🚀 快速开始
### 🆕 新建一个 tegg 应用
> 我们以 https://github.com/eggjs/examples/tree/master/hello-tegg 为例
> 我们以 <https://github.com/eggjs/examples/tree/master/hello-tegg> 为例
```shell
.
@@ -37,6 +39,7 @@
```
1. 修改 `ts-config.json` 配置,这是因为 cnpmcore 使用了 [subPath](https://nodejs.org/api/packages.html#subpath-exports)
```json
{
"extends": "@eggjs/tsconfig",
@@ -50,6 +53,7 @@
```
2. 修改 `config/plugin.ts` 文件,开启 cnpmcore 依赖的一些插件
```typescript
// 开启如下插件
{
@@ -76,7 +80,22 @@
}
```
3. 修改 `config.default.ts` 文件,可以直接复制 cnpmcore 中的内容
3. 修改 `config.default.ts` 文件,可以直接覆盖默认配置
```typescript
import { SyncMode } from 'cnpmcore/common/constants';
import { cnpmcoreConfig } from 'cnpmcore/common/config';
export default () => {
const config = {};
config.cnpmcore = {
...cnpmcoreConfig,
enableChangesStream: false,
syncMode: SyncMode.all,
};
return config;
}
```
### 🧑‍🤝‍🧑 集成 cnpmcore
@@ -90,7 +109,7 @@
│   └── package.json
```
* 添加 `package.json` ,声明 infra 作为一个 eggModule 单元
* 添加 `package.json` ,声明 infra 作为一个 eggModule 单元
```JSON
{
@@ -101,7 +120,7 @@
}
```
* 添加 `XXXAdapter.ts` 在对应的 Adapter 中继承 cnpmcore 默认的 Adapter以 AuthAdapter 为例
* 添加 `XXXAdapter.ts` 在对应的 Adapter 中继承 cnpmcore 默认的 Adapter以 AuthAdapter 为例
```typescript
import { AccessLevel, SingletonProto } from '@eggjs/tegg';
@@ -145,12 +164,14 @@
我们以 AuthAdapter 为例,来实现 npm cli 的 SSO 登录的功能。
我们需要实现了 getAuthUrl 和 ensureCurrentUser 这两个方法:
1. getAuthUrl 引导用户访问企业内实际的登录中心。
2. ensureCurrentUser 当用户完成访问后,需要回调到应用进行鉴权流程。
我们约定通过 `POST /-/v1/login/sso/:sessionId` 这个路由来进行登录验证。
当然,你也可以任意修改地址和登录回调,只需保证更新 redis 中的 token 状态即可。
修改 AuthAdapter.ts 文件
```typescript
import { AccessLevel, EggContext, SingletonProto } from '@eggjs/tegg';
import { AuthAdapter } from 'cnpmcore/infra/AuthAdapter';
@@ -185,6 +206,7 @@ export class MyAuthAdapter extends AuthAdapter {
```
修改 HelloController 的实现,实际也可以通过登录中心回调、页面确认等方式实现
```typescript
// 触发回调接口,会自动完成用户创建
await this.httpclient.request(`${ctx.origin}/-/v1/login/sso/${name}`, { method: 'POST' });
@@ -195,22 +217,24 @@ export class MyAuthAdapter extends AuthAdapter {
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...
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/.
```
4. 在命令行输入 `npm whoami --registry=http://127.0.0.1:7001` 验证
```shell
$ npm whoami --registry=http://127.0.0.1:7001
$ hello
npm whoami --registry=http://127.0.0.1:7001
hello
```

View File

@@ -3,14 +3,13 @@
[![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)
[![emoji-log](https://cdn.rawgit.com/ahmadawais/stuff/ca97874/emoji-log/non-flat-round.svg)](https://github.com/ahmadawais/Emoji-Log/)
[![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)
Reimplementation based on [cnpmjs.org](https://github.com/cnpm/cnpmjs.org) with TypeScript.
## Registry HTTP API
See https://github.com/cnpm/cnpmjs.org/blob/master/docs/registry-api.md#npm-registry-api
See [registry-api.md](docs/registry-api.md)
## How to contribute
@@ -28,13 +27,14 @@ See [INTEGRATE.md](INTEGRATE.md)
## Contributors
|[<img src="https://avatars.githubusercontent.com/u/156269?v=4" width="100px;"/><br/><sub><b>fengmk2</b></sub>](https://github.com/fengmk2)<br/>|[<img src="https://avatars.githubusercontent.com/u/6897780?v=4" width="100px;"/><br/><sub><b>killagu</b></sub>](https://github.com/killagu)<br/>|[<img src="https://avatars.githubusercontent.com/u/32174276?v=4" width="100px;"/><br/><sub><b>semantic-release-bot</b></sub>](https://github.com/semantic-release-bot)<br/>|[<img src="https://avatars.githubusercontent.com/u/5574625?v=4" width="100px;"/><br/><sub><b>elrrrrrrr</b></sub>](https://github.com/elrrrrrrr)<br/>|[<img src="https://avatars.githubusercontent.com/u/35598090?v=4" width="100px;"/><br/><sub><b>hezhengxu2018</b></sub>](https://github.com/hezhengxu2018)<br/>|[<img src="https://avatars.githubusercontent.com/u/26033663?v=4" width="100px;"/><br/><sub><b>Zian502</b></sub>](https://github.com/Zian502)<br/>|
|[<img src="https://avatars.githubusercontent.com/u/156269?v=4" width="100px;"/><br/><sub><b>fengmk2</b></sub>](https://github.com/fengmk2)<br/>|[<img src="https://avatars.githubusercontent.com/u/32174276?v=4" width="100px;"/><br/><sub><b>semantic-release-bot</b></sub>](https://github.com/semantic-release-bot)<br/>|[<img src="https://avatars.githubusercontent.com/u/5574625?v=4" width="100px;"/><br/><sub><b>elrrrrrrr</b></sub>](https://github.com/elrrrrrrr)<br/>|[<img src="https://avatars.githubusercontent.com/u/6897780?v=4" width="100px;"/><br/><sub><b>killagu</b></sub>](https://github.com/killagu)<br/>|[<img src="https://avatars.githubusercontent.com/u/35598090?v=4" width="100px;"/><br/><sub><b>hezhengxu2018</b></sub>](https://github.com/hezhengxu2018)<br/>|[<img src="https://avatars.githubusercontent.com/u/13284978?v=4" width="100px;"/><br/><sub><b>Beace</b></sub>](https://github.com/Beace)<br/>|
| :---: | :---: | :---: | :---: | :---: | :---: |
|[<img src="https://avatars.githubusercontent.com/u/4635838?v=4" width="100px;"/><br/><sub><b>gemwuu</b></sub>](https://github.com/gemwuu)<br/>|[<img src="https://avatars.githubusercontent.com/u/17879221?v=4" width="100px;"/><br/><sub><b>laibao101</b></sub>](https://github.com/laibao101)<br/>|[<img src="https://avatars.githubusercontent.com/u/3478550?v=4" width="100px;"/><br/><sub><b>coolyuantao</b></sub>](https://github.com/coolyuantao)<br/>|[<img src="https://avatars.githubusercontent.com/u/13284978?v=4" width="100px;"/><br/><sub><b>Beace</b></sub>](https://github.com/Beace)<br/>|[<img src="https://avatars.githubusercontent.com/u/10163680?v=4" width="100px;"/><br/><sub><b>Wellaiyo</b></sub>](https://github.com/Wellaiyo)<br/>|[<img src="https://avatars.githubusercontent.com/u/227713?v=4" width="100px;"/><br/><sub><b>atian25</b></sub>](https://github.com/atian25)<br/>|
|[<img src="https://avatars.githubusercontent.com/u/8198408?v=4" width="100px;"/><br/><sub><b>BlackHole1</b></sub>](https://github.com/BlackHole1)<br/>|[<img src="https://avatars.githubusercontent.com/u/1814071?v=4" width="100px;"/><br/><sub><b>xiekw2010</b></sub>](https://github.com/xiekw2010)<br/>|[<img src="https://avatars.githubusercontent.com/u/13471233?v=4" width="100px;"/><br/><sub><b>OpportunityLiu</b></sub>](https://github.com/OpportunityLiu)<br/>|[<img src="https://avatars.githubusercontent.com/u/958063?v=4" width="100px;"/><br/><sub><b>thonatos</b></sub>](https://github.com/thonatos)<br/>|[<img src="https://avatars.githubusercontent.com/u/11039003?v=4" width="100px;"/><br/><sub><b>chenpx976</b></sub>](https://github.com/chenpx976)<br/>|[<img src="https://avatars.githubusercontent.com/u/29791463?v=4" width="100px;"/><br/><sub><b>fossabot</b></sub>](https://github.com/fossabot)<br/>|
[<img src="https://avatars.githubusercontent.com/u/1119126?v=4" width="100px;"/><br/><sub><b>looksgood</b></sub>](https://github.com/looksgood)<br/>|[<img src="https://avatars.githubusercontent.com/u/23701019?v=4" width="100px;"/><br/><sub><b>laoboxie</b></sub>](https://github.com/laoboxie)<br/>|[<img src="https://avatars.githubusercontent.com/u/5550931?v=4" width="100px;"/><br/><sub><b>shinima</b></sub>](https://github.com/shinima)<br/>
|[<img src="https://avatars.githubusercontent.com/u/4635838?v=4" width="100px;"/><br/><sub><b>gemwuu</b></sub>](https://github.com/gemwuu)<br/>|[<img src="https://avatars.githubusercontent.com/u/26033663?v=4" width="100px;"/><br/><sub><b>Zian502</b></sub>](https://github.com/Zian502)<br/>|[<img src="https://avatars.githubusercontent.com/u/17879221?v=4" width="100px;"/><br/><sub><b>laibao101</b></sub>](https://github.com/laibao101)<br/>|[<img src="https://avatars.githubusercontent.com/u/3478550?v=4" width="100px;"/><br/><sub><b>coolyuantao</b></sub>](https://github.com/coolyuantao)<br/>|[<img src="https://avatars.githubusercontent.com/u/10163680?v=4" width="100px;"/><br/><sub><b>Wellaiyo</b></sub>](https://github.com/Wellaiyo)<br/>|[<img src="https://avatars.githubusercontent.com/u/227713?v=4" width="100px;"/><br/><sub><b>atian25</b></sub>](https://github.com/atian25)<br/>|
|[<img src="https://avatars.githubusercontent.com/u/33210001?v=4" width="100px;"/><br/><sub><b>hljwkwm</b></sub>](https://github.com/hljwkwm)<br/>|[<img src="https://avatars.githubusercontent.com/u/8198408?v=4" width="100px;"/><br/><sub><b>BlackHole1</b></sub>](https://github.com/BlackHole1)<br/>|[<img src="https://avatars.githubusercontent.com/u/1814071?v=4" width="100px;"/><br/><sub><b>xiekw2010</b></sub>](https://github.com/xiekw2010)<br/>|[<img src="https://avatars.githubusercontent.com/u/7054676?v=4" width="100px;"/><br/><sub><b>Zheaoli</b></sub>](https://github.com/Zheaoli)<br/>|[<img src="https://avatars.githubusercontent.com/u/13471233?v=4" width="100px;"/><br/><sub><b>OpportunityLiu</b></sub>](https://github.com/OpportunityLiu)<br/>|[<img src="https://avatars.githubusercontent.com/u/958063?v=4" width="100px;"/><br/><sub><b>thonatos</b></sub>](https://github.com/thonatos)<br/>|
|[<img src="https://avatars.githubusercontent.com/u/26962197?v=4" width="100px;"/><br/><sub><b>chilingling</b></sub>](https://github.com/chilingling)<br/>|[<img src="https://avatars.githubusercontent.com/u/11039003?v=4" width="100px;"/><br/><sub><b>chenpx976</b></sub>](https://github.com/chenpx976)<br/>|[<img src="https://avatars.githubusercontent.com/u/29791463?v=4" width="100px;"/><br/><sub><b>fossabot</b></sub>](https://github.com/fossabot)<br/>|[<img src="https://avatars.githubusercontent.com/u/1119126?v=4" width="100px;"/><br/><sub><b>looksgood</b></sub>](https://github.com/looksgood)<br/>|[<img src="https://avatars.githubusercontent.com/u/23701019?v=4" width="100px;"/><br/><sub><b>laoboxie</b></sub>](https://github.com/laoboxie)<br/>|[<img src="https://avatars.githubusercontent.com/u/5772358?v=4" width="100px;"/><br/><sub><b>unbyte</b></sub>](https://github.com/unbyte)<br/>|
[<img src="https://avatars.githubusercontent.com/u/5799374?v=4" width="100px;"/><br/><sub><b>wandergis</b></sub>](https://github.com/wandergis)<br/>|[<img src="https://avatars.githubusercontent.com/u/13448833?v=4" width="100px;"/><br/><sub><b>windhc</b></sub>](https://github.com/windhc)<br/>|[<img src="https://avatars.githubusercontent.com/u/2784308?v=4" width="100px;"/><br/><sub><b>yisibl</b></sub>](https://github.com/yisibl)<br/>|[<img src="https://avatars.githubusercontent.com/u/13127586?v=4" width="100px;"/><br/><sub><b>vimplus</b></sub>](https://github.com/vimplus)<br/>|[<img src="https://avatars.githubusercontent.com/u/5550931?v=4" width="100px;"/><br/><sub><b>feichao93</b></sub>](https://github.com/feichao93)<br/>
This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Sat May 06 2023 12:40:20 GMT+0800`.
This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Fri May 17 2024 22:31:22 GMT+0800`.
<!-- GITCONTRIBUTOR_END -->

1
app.ts
View File

@@ -2,6 +2,7 @@ import path from 'path';
import { readFile } from 'fs/promises';
import { Application } from 'egg';
import { ChangesStreamService } from './app/core/service/ChangesStreamService';
declare module 'egg' {
interface Application {
binaryHTML: string;

View File

@@ -1,4 +1,5 @@
import { generateKeyPairSync, publicEncrypt, privateDecrypt, constants } from 'crypto';
import { generateKeyPairSync } from 'crypto';
import NodeRSA from 'node-rsa';
// generate rsa key pair
export function genRSAKeys(): { publicKey: string, privateKey: string } {
@@ -17,17 +18,19 @@ export function genRSAKeys(): { publicKey: string, privateKey: string } {
}
// encrypt rsa private key
export function encryptRSA(publicKey: string, data: string): string {
return publicEncrypt({
key: publicKey,
padding: constants.RSA_PKCS1_PADDING,
}, Buffer.from(data, 'utf8')).toString('base64');
export function encryptRSA(publicKey: string, plainText: string): string {
const key = new NodeRSA(publicKey, 'pkcs1-public-pem', {
encryptionScheme: 'pkcs1',
environment: 'browser',
});
return key.encrypt(plainText, 'base64');
}
// decrypt rsa private key
export function decryptRSA(privateKey: string, data: string) {
return privateDecrypt({
key: privateKey,
padding: constants.RSA_PKCS1_PADDING,
}, Buffer.from(data, 'base64')).toString('utf8');
export function decryptRSA(privateKey: string, encryptedBase64: string): string {
const key = new NodeRSA(privateKey, 'pkcs1-private-pem', {
encryptionScheme: 'pkcs1',
environment: 'browser',
});
return key.decrypt(encryptedBase64, 'utf8');
}

View File

@@ -64,10 +64,13 @@ async function _downloadToTempfile(httpclient: EggContextHttpClient,
try {
// max 10 mins to download
// FIXME: should show download progress
const authorization = optionalConfig?.remoteAuthToken ? `Bearer ${optionalConfig?.remoteAuthToken}` : '';
const requestHeaders: Record<string, string> = {};
if (optionalConfig?.remoteAuthToken) {
requestHeaders.authorization = `Bearer ${optionalConfig.remoteAuthToken}`;
}
const { status, headers, res } = await httpclient.request(url, {
timeout: 60000 * 10,
headers: { authorization },
headers: requestHeaders,
writeStream,
timing: true,
followRedirect: true,
@@ -105,13 +108,13 @@ const WHITE_FILENAME_CONTENT_TYPES = {
'.eslintignore': PLAIN_TEXT,
'.jshintrc': 'application/json',
'.eslintrc': 'application/json',
};
} as const;
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] ||
WHITE_FILENAME_CONTENT_TYPES[filename as keyof typeof WHITE_FILENAME_CONTENT_TYPES] ||
DEFAULT_CONTENT_TYPE;
}

View File

@@ -2,7 +2,9 @@ import { createReadStream } from 'node:fs';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import * as ssri from 'ssri';
import tar from 'tar';
import tar from '@fengmk2/tar';
import { AuthorType, PackageJSONType } from '../repository/PackageRepository';
// /@cnpm%2ffoo
// /@cnpm%2Ffoo
@@ -27,6 +29,10 @@ export function cleanUserPrefix(username: string): string {
return username.replace(/^.*:/, '');
}
export function getPrefixedName(prefix: string, username: string): string {
return prefix ? `${prefix}${username}` : username;
}
export async function calculateIntegrity(contentOrFile: Uint8Array | string) {
let integrityObj;
if (typeof contentOrFile === 'string') {
@@ -98,3 +104,37 @@ export async function hasShrinkWrapInTgz(contentOrFile: Uint8Array | string): Pr
throw Object.assign(new Error('[hasShrinkWrapInTgz] Fail to parse input file'), { cause: e });
}
}
/** 写入 ES 时,格式化 author */
export function formatAuthor(author: string | AuthorType | undefined): AuthorType | undefined {
if (author === undefined) {
return author;
}
if (typeof author === 'string') {
return { name: author };
}
return author;
}
export async function extractPackageJSON(tarballBytes: Buffer): Promise<PackageJSONType> {
return new Promise((resolve, reject) => {
Readable.from(tarballBytes)
.pipe(tar.t({
filter: name => name === 'package/package.json',
onentry: async entry => {
const chunks: Buffer[] = [];
for await (const chunk of entry) {
chunks.push(chunk);
}
try {
const data = Buffer.concat(chunks);
return resolve(JSON.parse(data.toString()));
} catch (err) {
reject(new Error('Error parsing package.json'));
}
},
}));
});
}

View File

@@ -34,7 +34,7 @@ export function integrity(plain: string): string {
}
export function checkIntegrity(plain: string, expectedIntegrity: string): boolean {
return ssri.checkData(plain, expectedIntegrity);
return !!ssri.checkData(plain, expectedIntegrity);
}
export function sha512(plain: string): string {

View File

@@ -64,12 +64,13 @@ export class CacheAdapter {
async usingLock(key: string, seconds: number, func: () => Promise<void>) {
const lockTimestamp = await this.lock(key, seconds);
if (!lockTimestamp) return;
if (!lockTimestamp) return false;
try {
await func();
} finally {
await this.unlock(key, lockTimestamp);
}
return true;
}
private getLockName(key: string) {

View File

@@ -11,6 +11,7 @@ import {
HttpClientRequestOptions,
HttpClientResponse,
} from 'egg';
import { PackageManifestType } from '../../repository/PackageRepository';
type HttpMethod = HttpClientRequestOptions['method'];
@@ -40,7 +41,7 @@ export class NPMRegistry {
this.registryHost = registryHost;
}
public async getFullManifests(fullname: string, optionalConfig?: {retries?:number, remoteAuthToken?:string}): Promise<RegistryResponse> {
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
@@ -97,6 +98,7 @@ export class NPMRegistry {
data: params,
dataType: 'json',
timing: true,
retry: 3,
timeout: this.timeout,
followRedirect: true,
gzip: true,

View File

@@ -17,6 +17,8 @@ export type FetchResult = {
nextParams?: any;
};
const platforms = [ 'darwin', 'linux', 'win32' ] as const;
export const BINARY_ADAPTER_ATTRIBUTE = Symbol('BINARY_ADAPTER_ATTRIBUTE');
export abstract class AbstractBinary {
@@ -26,12 +28,17 @@ export abstract class AbstractBinary {
@Inject()
protected httpclient: EggHttpClient;
abstract init(binaryName: BinaryName): Promise<void>;
abstract initFetch(binaryName: BinaryName): Promise<void>;
abstract fetch(dir: string, binaryName: BinaryName): Promise<FetchResult | undefined>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async finishFetch(_success: boolean, _binaryName: BinaryName): Promise<void> {
// do not thing by default
}
protected async requestXml(url: string) {
const { status, data, headers } = await this.httpclient.request(url, {
timeout: 20000,
timeout: 30000,
followRedirect: true,
gzip: true,
});
@@ -43,12 +50,13 @@ export abstract class AbstractBinary {
return xml;
}
protected async requestJSON(url: string) {
protected async requestJSON(url: string, requestHeaders?: Record<string, string>) {
const { status, data, headers } = await this.httpclient.request(url, {
timeout: 20000,
timeout: 30000,
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);
@@ -74,7 +82,7 @@ export abstract class AbstractBinary {
protected listNodePlatforms() {
// https://nodejs.org/api/os.html#osplatform
return [ 'darwin', 'linux', 'win32' ];
return platforms;
}
protected listNodeArchs(binaryConfig?: BinaryTaskConfig) {
@@ -87,11 +95,11 @@ export abstract class AbstractBinary {
};
}
protected listNodeLibcs() {
protected listNodeLibcs(): Record<typeof platforms[number], string[]> {
// https://github.com/lovell/detect-libc/blob/master/lib/detect-libc.js#L42
return {
linux: [ 'glibc', 'musl' ],
darwin: [ 'unknown' ],
linux: [ 'glibc', 'musl' ],
win32: [ 'unknown' ],
};
}

View File

@@ -9,7 +9,7 @@ export class ApiBinary extends AbstractBinary {
@Inject()
private readonly config: EggAppConfig;
async init() {
async initFetch() {
// do nothing
return;
}

View File

@@ -7,7 +7,7 @@ import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './Abstra
@SingletonProto()
@BinaryAdapter(BinaryType.Bucket)
export class BucketBinary extends AbstractBinary {
async init() {
async initFetch() {
// do nothing
return;
}

View File

@@ -1,3 +1,4 @@
import { basename } from 'path';
import { SingletonProto } from '@eggjs/tegg';
import { BinaryType } from '../../enum/Binary';
import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './AbstractBinary';
@@ -5,65 +6,129 @@ import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './Abstra
@SingletonProto()
@BinaryAdapter(BinaryType.ChromeForTesting)
export class ChromeForTestingBinary extends AbstractBinary {
static lastTimestamp = '';
#timestamp = '';
private dirItems?: {
[key: string]: BinaryItem[];
};
async init() {
async initFetch() {
this.dirItems = undefined;
}
async fetch(dir: string): Promise<FetchResult | undefined> {
if (!this.dirItems) {
this.dirItems = {};
this.dirItems['/'] = [];
let chromeVersion = '';
async finishFetch(success: boolean) {
if (success && this.#timestamp && ChromeForTestingBinary.lastTimestamp !== this.#timestamp) {
ChromeForTestingBinary.lastTimestamp = this.#timestamp;
}
}
// exports.PUPPETEER_REVISIONS = Object.freeze({
// chrome: '113.0.5672.63',
// firefox: 'latest',
// });
const unpkgURL = 'https://unpkg.com/puppeteer-core@latest/lib/cjs/puppeteer/revisions.js';
const text = await this.requestXml(unpkgURL);
const m = /chrome:\s+\'([\d\.]+)\'\,/.exec(text);
if (m) {
chromeVersion = m[1];
}
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,
});
if (status !== 200) {
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);
if (!hasNewData) {
return;
}
const platforms = [ 'linux64', 'mac-arm64', 'mac-x64', 'win32', 'win64' ];
const date = new Date().toISOString();
// "timestamp": "2023-09-16T00:21:21.964Z",
// "versions": [
// {
// "version": "113.0.5672.0",
// "revision": "1121455",
// "downloads": {
// "chrome": [
// {
// "platform": "linux64",
// "url": "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/linux64/chrome-linux64.zip"
// },
// {
// "platform": "mac-arm64",
// "url": "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/mac-arm64/chrome-mac-arm64.zip"
// },
// {
// "platform": "mac-x64",
// "url": "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/mac-x64/chrome-mac-x64.zip"
// },
// {
// "platform": "win32",
// "url": "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/win32/chrome-win32.zip"
// },
// {
// "platform": "win64",
// "url": "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.0/win64/chrome-win64.zip"
// }
// ]
// }
// },
const versions = data.versions as {
version: string;
revision: string;
downloads: {
[key: string]: {
platform: string;
url: string;
}[];
};
}[];
for (const item of versions) {
this.dirItems['/'].push({
name: `${chromeVersion}/`,
date,
name: `${item.version}/`,
date: item.revision,
size: '-',
isDir: true,
url: '',
});
this.dirItems[`/${chromeVersion}/`] = [];
for (const platform of platforms) {
this.dirItems[`/${chromeVersion}/`].push({
name: `${platform}/`,
date,
size: '-',
isDir: true,
url: '',
});
// https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/113.0.5672.63/mac-arm64/chrome-mac-arm64.zip
const name = `chrome-${platform}.zip`;
this.dirItems[`/${chromeVersion}/${platform}/`] = [
{
name,
date,
const versionDir = `/${item.version}/`;
if (!this.dirItems[versionDir]) {
this.dirItems[versionDir] = [];
}
for (const category in item.downloads) {
const downloads = item.downloads[category];
for (const download of downloads) {
const platformDir = `${versionDir}${download.platform}/`;
if (!this.dirItems[platformDir]) {
this.dirItems[platformDir] = [];
this.dirItems[versionDir].push({
name: `${download.platform}/`,
date: item.revision,
size: '-',
isDir: true,
url: '',
});
}
this.dirItems[platformDir].push({
name: basename(download.url),
date: data.timestamp,
size: '-',
isDir: false,
url: `https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${chromeVersion}/${platform}/${name}`,
},
];
url: download.url,
});
}
}
}
}
return { items: this.dirItems[dir], nextParams: null };
async fetch(dir: string): Promise<FetchResult | undefined> {
// use https://github.com/GoogleChromeLabs/chrome-for-testing#json-api-endpoints
if (!this.dirItems) {
await this.#syncDirItems();
}
return { items: this.dirItems![dir], nextParams: null };
}
}

View File

@@ -9,7 +9,7 @@ export class CypressBinary extends AbstractBinary {
[key: string]: BinaryItem[];
} | null;
async init() {
async initFetch() {
this.dirItems = undefined;
}
@@ -42,8 +42,21 @@ export class CypressBinary extends AbstractBinary {
// "https://cdn.cypress.io/desktop/9.2.0/darwin-arm64/cypress.zip"
// "https://cdn.cypress.io/desktop/9.2.0/darwin-x64/cypress.zip"
// "https://cdn.cypress.io/desktop/9.2.0/linux-x64/cypress.zip"
// "https://cdn.cypress.io/desktop/9.2.0/linux-arm64/cypress.zip"
// "https://cdn.cypress.io/desktop/9.2.0/win32-x64/cypress.zip"
const platforms = [ 'darwin-x64', 'darwin-arm64', 'linux-x64', 'win32-x64' ];
// https://github.com/cypress-io/cypress/blob/develop/scripts/binary/index.js#L146
// const systems = [
// { platform: 'linux', arch: 'x64' },
// { platform: 'linux', arch: 'arm64' },
// { platform: 'darwin', arch: 'x64' },
// { platform: 'darwin', arch: 'arm64' },
// { platform: 'win32', arch: 'x64' },
// ]
const platforms = [
'darwin-x64', 'darwin-arm64',
'linux-x64', 'linux-arm64',
'win32-x64',
];
for (const platform of platforms) {
this.dirItems[subDir].push({
name: `${platform}/`,

View File

@@ -0,0 +1,213 @@
import path from 'node:path';
import { SingletonProto } from '@eggjs/tegg';
import {
AbstractBinary, FetchResult, BinaryItem, BinaryAdapter,
} from './AbstractBinary';
import { BinaryType } from '../../enum/Binary';
@SingletonProto()
@BinaryAdapter(BinaryType.Edgedriver)
export class EdgedriverBinary extends AbstractBinary {
private dirItems?: {
[key: string]: BinaryItem[];
};
async initFetch() {
this.dirItems = undefined;
}
async #syncDirItems() {
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,
});
if (status !== 200) {
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);
// [
// {
// "Product": "Stable",
// "Releases": [
// {
// "ReleaseId": 73376,
// "Platform": "iOS",
// "Architecture": "arm64",
// "CVEs": [],
// "ProductVersion": "124.0.2478.89",
// "Artifacts": [],
// "PublishedTime": "2024-05-07T02:57:00",
// "ExpectedExpiryDate": "2025-05-07T02:57:00"
// },
// {
// "ReleaseId": 73629,
// "Platform": "Windows",
// "Architecture": "x86",
// "CVEs": [
// "CVE-2024-4559",
// "CVE-2024-4671"
// ],
// "ProductVersion": "124.0.2478.97",
// "Artifacts": [
// {
// "ArtifactName": "msi",
// "Location": "https://msedge.sf.dl.delivery.mp.microsoft.com/filestreamingservice/files/aa1c9fe3-bb9c-4a80-9ff7-5c109701fbfe/MicrosoftEdgeEnterpriseX86.msi",
// "Hash": "4CEF7B907D3E2371E953C41190E32C3560CEE7D3F16D7550CA156DC976EBCB80",
// "HashAlgorithm": "SHA256",
// "SizeInBytes": 162029568
// }
// ],
// "PublishedTime": "2024-05-11T06:47:00",
// "ExpectedExpiryDate": "2025-05-10T16:59:00"
// },
// {
// "ReleaseId": 73630,
// "Platform": "Linux",
// "Architecture": "x64",
// "CVEs": [
// "CVE-2024-4559"
// ],
// "ProductVersion": "124.0.2478.97",
// "Artifacts": [
// {
// "ArtifactName": "rpm",
// "Location": "https://packages.microsoft.com/yumrepos/edge/microsoft-edge-stable-124.0.2478.97-1.x86_64.rpm",
// "Hash": "32D9C333544DDD9C56FED54844E89EF00F3E5620942C07B9B68D214016687895",
// "HashAlgorithm": "SHA256",
// "SizeInBytes": 169877932
// },
// {
// "ArtifactName": "deb",
// "Location": "https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable/microsoft-edge-stable_124.0.2478.97-1_amd64.deb",
// "Hash": "85D0AD1D63847B3DD54F0F214D18A2B54462BB43291536E773AD1B8B29BBF799",
// "HashAlgorithm": "SHA256",
// "SizeInBytes": 167546042
// }
// ],
// "PublishedTime": "2024-05-10T17:01:00",
// "ExpectedExpiryDate": "2025-05-10T17:01:00"
// },
// {
// "Product": "EdgeUpdate",
// "Releases": [
// {
// "ReleaseId": 73493,
// "Platform": "Windows",
// "Architecture": "x86",
// "CVEs": [],
// "ProductVersion": "1.3.187.37",
// "Artifacts": [
// {
// "ArtifactName": "exe",
// "Location": "https://msedge.sf.dl.delivery.mp.microsoft.com/filestreamingservice/files/a2fa84fe-796b-4f80-b1cd-f4d1f5731aa8/MicrosoftEdgeUpdateSetup_X86_1.3.187.37.exe",
// "Hash": "503088D22461FEE5D7B6B011609D73FFD5869D3ACE1DBB0F00F8F3B9D122C514",
// "HashAlgorithm": "SHA256",
// "SizeInBytes": 1622072
// }
// ],
// "PublishedTime": "2024-05-08T05:44:00",
// "ExpectedExpiryDate": "2025-05-08T05:44:00"
// }
// ]
// }
const products = data as {
Product: string;
Releases: {
ReleaseId: number;
Platform: string;
Architecture: string;
CVEs: string[];
ProductVersion: string;
Artifacts: {
ArtifactName: string;
Location: string;
Hash: string;
HashAlgorithm: string;
SizeInBytes: string;
}[];
PublishedTime: string;
ExpectedExpiryDate: string;
}[];
}[];
const existsVersions = new Set<string>();
for (const product of products) {
if (product.Product === 'EdgeUpdate') continue;
for (const release of product.Releases) {
if (!release.Artifacts || release.Artifacts.length === 0) continue;
if (existsVersions.has(release.ProductVersion)) continue;
this.dirItems['/'].push({
name: `${release.ProductVersion}/`,
date: release.PublishedTime,
size: '-',
isDir: true,
url: '',
});
existsVersions.add(release.ProductVersion);
}
}
}
async fetch(dir: string): Promise<FetchResult | undefined> {
if (!this.dirItems) {
await this.#syncDirItems();
}
// fetch root dir
if (dir === '/') {
return { items: this.dirItems![dir], nextParams: null };
}
// fetch sub dir
// /foo/ => foo/
const subDir = dir.substring(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);
return { items: this.#parseItems(xml), nextParams: null };
}
#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 matchItems = xml.matchAll(fileRe);
for (const m of matchItems) {
const fullname = m[1].trim();
// <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>
// ignore size = 0 dir
const name = path.basename(fullname);
const url = m[2].trim();
const date = m[3].trim();
const size = parseInt(m[4].trim());
items.push({
name,
isDir: false,
url,
size,
date,
});
}
return items;
}
}

View File

@@ -8,7 +8,7 @@ import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './Abstra
export class GithubBinary extends AbstractBinary {
private releases: Record<string, any[]> = {};
async init(binaryName: BinaryName) {
async initFetch(binaryName: BinaryName) {
delete this.releases[binaryName];
}
@@ -21,7 +21,11 @@ export class GithubBinary extends AbstractBinary {
const maxPage = binaryConfig.options?.maxPage || 1;
for (let i = 0; i < maxPage; i++) {
const url = `https://api.github.com/repos/${binaryConfig.repo}/releases?per_page=100&page=${i + 1}`;
const data = await this.requestJSON(url);
const requestHeaders: Record<string, string> = {};
if (process.env.GITHUB_TOKEN) {
requestHeaders.Authorization = `token ${process.env.GITHUB_TOKEN}`;
}
const data = await this.requestJSON(url, requestHeaders);
if (!Array.isArray(data)) {
// {"message":"API rate limit exceeded for 47.57.239.54. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)","documentation_url":"https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting"}
if (typeof data?.message === 'string' && data.message.includes('rate limit')) {
@@ -40,10 +44,13 @@ export class GithubBinary extends AbstractBinary {
protected formatItems(releaseItem: any, binaryConfig: BinaryTaskConfig) {
const items: BinaryItem[] = [];
// 200MB
const maxFileSize = 1024 * 1024 * 200;
// 250MB
const maxFileSize = 1024 * 1024 * 250;
for (const asset of releaseItem.assets) {
if (asset.size > maxFileSize) continue;
if (asset.size > maxFileSize) {
this.logger.info('[GithubBinary.formatItems] asset reach max file size(> 250MB), ignore download it, asset: %j', asset);
continue;
}
items.push({
name: asset.name,
isDir: false,

View File

@@ -6,7 +6,7 @@ import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './Abstra
@SingletonProto()
@BinaryAdapter(BinaryType.Imagemin)
export class ImageminBinary extends AbstractBinary {
async init() {
async initFetch() {
// do nothing
return;
}

View File

@@ -6,7 +6,7 @@ import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './Abstra
@SingletonProto()
@BinaryAdapter(BinaryType.Node)
export class NodeBinary extends AbstractBinary {
async init() {
async initFetch() {
// do nothing
return;
}

View File

@@ -7,7 +7,7 @@ import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './Abstra
@SingletonProto()
@BinaryAdapter(BinaryType.NodePreGyp)
export class NodePreGypBinary extends AbstractBinary {
async init() {
async initFetch() {
// do nothing
return;
}
@@ -15,7 +15,8 @@ export class NodePreGypBinary extends AbstractBinary {
// https://github.com/mapbox/node-pre-gyp
async fetch(dir: string, binaryName: BinaryName): Promise<FetchResult | undefined> {
const binaryConfig = binaries[binaryName];
const pkgUrl = `https://registry.npmjs.com/${binaryName}`;
const npmPackageName = binaryConfig.options?.npmPackageName ?? binaryName;
const pkgUrl = `https://registry.npmjs.com/${npmPackageName}`;
const data = await this.requestJSON(pkgUrl);
const dirItems: {
[key: string]: BinaryItem[];

View File

@@ -13,14 +13,16 @@ const DOWNLOAD_HOST = 'https://playwright.azureedge.net/';
const DOWNLOAD_PATHS = {
'chromium': {
'<unknown>': undefined,
'generic-linux': 'builds/chromium/%s/chromium-linux.zip',
'generic-linux-arm64': 'builds/chromium/%s/chromium-linux-arm64.zip',
'ubuntu18.04': 'builds/chromium/%s/chromium-linux.zip',
'ubuntu20.04': 'builds/chromium/%s/chromium-linux.zip',
'ubuntu22.04': 'builds/chromium/%s/chromium-linux.zip',
'ubuntu18.04-arm64': 'builds/chromium/%s/chromium-linux-arm64.zip',
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': 'builds/chromium/%s/chromium-linux.zip',
'ubuntu22.04-x64': 'builds/chromium/%s/chromium-linux.zip',
'ubuntu18.04-arm64': undefined,
'ubuntu20.04-arm64': 'builds/chromium/%s/chromium-linux-arm64.zip',
'ubuntu22.04-arm64': 'builds/chromium/%s/chromium-linux-arm64.zip',
'debian11-x64': 'builds/chromium/%s/chromium-linux.zip',
'debian11-arm64': 'builds/chromium/%s/chromium-linux-arm64.zip',
'debian12-x64': 'builds/chromium/%s/chromium-linux.zip',
'debian12-arm64': 'builds/chromium/%s/chromium-linux-arm64.zip',
'mac10.13': 'builds/chromium/%s/chromium-mac.zip',
'mac10.14': 'builds/chromium/%s/chromium-mac.zip',
'mac10.15': 'builds/chromium/%s/chromium-mac.zip',
@@ -28,18 +30,24 @@ const DOWNLOAD_PATHS = {
'mac11-arm64': 'builds/chromium/%s/chromium-mac-arm64.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-arm64': 'builds/chromium/%s/chromium-mac-arm64.zip',
'mac14': 'builds/chromium/%s/chromium-mac.zip',
'mac14-arm64': 'builds/chromium/%s/chromium-mac-arm64.zip',
'win64': 'builds/chromium/%s/chromium-win64.zip',
},
'chromium-tip-of-tree': {
'<unknown>': undefined,
'generic-linux': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip',
'generic-linux-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip',
'ubuntu18.04': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip',
'ubuntu20.04': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip',
'ubuntu22.04': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip',
'ubuntu18.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip',
'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',
'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',
'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',
@@ -47,94 +55,99 @@ const DOWNLOAD_PATHS = {
'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',
'win64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-win64.zip',
},
'chromium-with-symbols': {
'<unknown>': undefined,
'generic-linux': 'builds/chromium/%s/chromium-with-symbols-linux.zip',
'generic-linux-arm64': 'builds/chromium/%s/chromium-with-symbols-linux-arm64.zip',
'ubuntu18.04': 'builds/chromium/%s/chromium-with-symbols-linux.zip',
'ubuntu20.04': 'builds/chromium/%s/chromium-with-symbols-linux.zip',
'ubuntu22.04': 'builds/chromium/%s/chromium-with-symbols-linux.zip',
'ubuntu18.04-arm64': 'builds/chromium/%s/chromium-with-symbols-linux-arm64.zip',
'ubuntu20.04-arm64': 'builds/chromium/%s/chromium-with-symbols-linux-arm64.zip',
'ubuntu22.04-arm64': 'builds/chromium/%s/chromium-with-symbols-linux-arm64.zip',
'mac10.13': 'builds/chromium/%s/chromium-with-symbols-mac.zip',
'mac10.14': 'builds/chromium/%s/chromium-with-symbols-mac.zip',
'mac10.15': 'builds/chromium/%s/chromium-with-symbols-mac.zip',
'mac11': 'builds/chromium/%s/chromium-with-symbols-mac.zip',
'mac11-arm64': 'builds/chromium/%s/chromium-with-symbols-mac-arm64.zip',
'mac12': 'builds/chromium/%s/chromium-with-symbols-mac.zip',
'mac12-arm64': 'builds/chromium/%s/chromium-with-symbols-mac-arm64.zip',
'win64': 'builds/chromium/%s/chromium-with-symbols-win64.zip',
},
'firefox': {
'<unknown>': undefined,
'generic-linux': 'builds/firefox/%s/firefox-ubuntu-20.04.zip',
'generic-linux-arm64': 'builds/firefox/%s/firefox-ubuntu-20.04-arm64.zip',
'ubuntu18.04': 'builds/firefox/%s/firefox-ubuntu-18.04.zip',
'ubuntu20.04': 'builds/firefox/%s/firefox-ubuntu-20.04.zip',
'ubuntu22.04': 'builds/firefox/%s/firefox-ubuntu-22.04.zip',
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': 'builds/firefox/%s/firefox-ubuntu-20.04.zip',
'ubuntu22.04-x64': 'builds/firefox/%s/firefox-ubuntu-22.04.zip',
'ubuntu18.04-arm64': undefined,
'ubuntu20.04-arm64': 'builds/firefox/%s/firefox-ubuntu-20.04-arm64.zip',
'ubuntu22.04-arm64': 'builds/firefox/%s/firefox-ubuntu-22.04-arm64.zip',
'mac10.13': 'builds/firefox/%s/firefox-mac-11.zip',
'mac10.14': 'builds/firefox/%s/firefox-mac-11.zip',
'mac10.15': 'builds/firefox/%s/firefox-mac-11.zip',
'mac11': 'builds/firefox/%s/firefox-mac-11.zip',
'mac11-arm64': 'builds/firefox/%s/firefox-mac-11-arm64.zip',
'mac12': 'builds/firefox/%s/firefox-mac-11.zip',
'mac12-arm64': 'builds/firefox/%s/firefox-mac-11-arm64.zip',
'debian11-x64': 'builds/firefox/%s/firefox-debian-11.zip',
'debian11-arm64': 'builds/firefox/%s/firefox-debian-11-arm64.zip',
'debian12-x64': 'builds/firefox/%s/firefox-debian-12.zip',
'debian12-arm64': 'builds/firefox/%s/firefox-debian-12-arm64.zip',
'mac10.13': 'builds/firefox/%s/firefox-mac-13.zip',
'mac10.14': 'builds/firefox/%s/firefox-mac-13.zip',
'mac10.15': 'builds/firefox/%s/firefox-mac-13.zip',
'mac11': 'builds/firefox/%s/firefox-mac-13.zip',
'mac11-arm64': 'builds/firefox/%s/firefox-mac-13-arm64.zip',
'mac12': 'builds/firefox/%s/firefox-mac-13.zip',
'mac12-arm64': 'builds/firefox/%s/firefox-mac-13-arm64.zip',
'mac13': 'builds/firefox/%s/firefox-mac-13.zip',
'mac13-arm64': 'builds/firefox/%s/firefox-mac-13-arm64.zip',
'mac14': 'builds/firefox/%s/firefox-mac-13.zip',
'mac14-arm64': 'builds/firefox/%s/firefox-mac-13-arm64.zip',
'win64': 'builds/firefox/%s/firefox-win64.zip',
},
'firefox-beta': {
'<unknown>': undefined,
'generic-linux': 'builds/firefox-beta/%s/firefox-beta-ubuntu-20.04.zip',
'generic-linux-arm64': undefined,
'ubuntu18.04': 'builds/firefox-beta/%s/firefox-beta-ubuntu-18.04.zip',
'ubuntu20.04': 'builds/firefox-beta/%s/firefox-beta-ubuntu-20.04.zip',
'ubuntu22.04': 'builds/firefox-beta/%s/firefox-beta-ubuntu-22.04.zip',
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-20.04.zip',
'ubuntu22.04-x64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-22.04.zip',
'ubuntu18.04-arm64': undefined,
'ubuntu20.04-arm64': undefined,
'ubuntu22.04-arm64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-22.04-arm64.zip',
'mac10.13': 'builds/firefox-beta/%s/firefox-beta-mac-11.zip',
'mac10.14': 'builds/firefox-beta/%s/firefox-beta-mac-11.zip',
'mac10.15': 'builds/firefox-beta/%s/firefox-beta-mac-11.zip',
'mac11': 'builds/firefox-beta/%s/firefox-beta-mac-11.zip',
'mac11-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-11-arm64.zip',
'mac12': 'builds/firefox-beta/%s/firefox-beta-mac-11.zip',
'mac12-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-11-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',
'debian12-arm64': 'builds/firefox-beta/%s/firefox-beta-debian-12-arm64.zip',
'mac10.13': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip',
'mac10.14': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip',
'mac10.15': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip',
'mac11': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip',
'mac11-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-13-arm64.zip',
'mac12': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip',
'mac12-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-13-arm64.zip',
'mac13': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip',
'mac13-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-13-arm64.zip',
'mac14': 'builds/firefox-beta/%s/firefox-beta-mac-13.zip',
'mac14-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-13-arm64.zip',
'win64': 'builds/firefox-beta/%s/firefox-beta-win64.zip',
},
'webkit': {
'<unknown>': undefined,
'generic-linux': 'builds/webkit/%s/webkit-ubuntu-20.04.zip',
'generic-linux-arm64': 'builds/webkit/%s/webkit-ubuntu-20.04-arm64.zip',
'ubuntu18.04': 'builds/webkit/%s/webkit-ubuntu-18.04.zip',
'ubuntu20.04': 'builds/webkit/%s/webkit-ubuntu-20.04.zip',
'ubuntu22.04': 'builds/webkit/%s/webkit-ubuntu-22.04.zip',
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': 'builds/webkit/%s/webkit-ubuntu-20.04.zip',
'ubuntu22.04-x64': 'builds/webkit/%s/webkit-ubuntu-22.04.zip',
'ubuntu18.04-arm64': undefined,
'ubuntu20.04-arm64': 'builds/webkit/%s/webkit-ubuntu-20.04-arm64.zip',
'ubuntu22.04-arm64': 'builds/webkit/%s/webkit-ubuntu-22.04-arm64.zip',
'debian11-x64': 'builds/webkit/%s/webkit-debian-11.zip',
'debian11-arm64': 'builds/webkit/%s/webkit-debian-11-arm64.zip',
'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/webkit/%s/webkit-mac-10.15.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-arm64': 'builds/webkit/%s/webkit-mac-12-arm64.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-arm64': 'builds/webkit/%s/webkit-mac-14-arm64.zip',
'win64': 'builds/webkit/%s/webkit-win64.zip',
},
'ffmpeg': {
'<unknown>': undefined,
'generic-linux': 'builds/ffmpeg/%s/ffmpeg-linux.zip',
'generic-linux-arm64': 'builds/ffmpeg/%s/ffmpeg-linux-arm64.zip',
'ubuntu18.04': 'builds/ffmpeg/%s/ffmpeg-linux.zip',
'ubuntu20.04': 'builds/ffmpeg/%s/ffmpeg-linux.zip',
'ubuntu22.04': 'builds/ffmpeg/%s/ffmpeg-linux.zip',
'ubuntu18.04-arm64': 'builds/ffmpeg/%s/ffmpeg-linux-arm64.zip',
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': 'builds/ffmpeg/%s/ffmpeg-linux.zip',
'ubuntu22.04-x64': 'builds/ffmpeg/%s/ffmpeg-linux.zip',
'ubuntu18.04-arm64': undefined,
'ubuntu20.04-arm64': 'builds/ffmpeg/%s/ffmpeg-linux-arm64.zip',
'ubuntu22.04-arm64': 'builds/ffmpeg/%s/ffmpeg-linux-arm64.zip',
'debian11-x64': 'builds/ffmpeg/%s/ffmpeg-linux.zip',
'debian11-arm64': 'builds/ffmpeg/%s/ffmpeg-linux-arm64.zip',
'debian12-x64': 'builds/ffmpeg/%s/ffmpeg-linux.zip',
'debian12-arm64': 'builds/ffmpeg/%s/ffmpeg-linux-arm64.zip',
'mac10.13': 'builds/ffmpeg/%s/ffmpeg-mac.zip',
'mac10.14': 'builds/ffmpeg/%s/ffmpeg-mac.zip',
'mac10.15': 'builds/ffmpeg/%s/ffmpeg-mac.zip',
@@ -142,15 +155,44 @@ const DOWNLOAD_PATHS = {
'mac11-arm64': 'builds/ffmpeg/%s/ffmpeg-mac-arm64.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-arm64': 'builds/ffmpeg/%s/ffmpeg-mac-arm64.zip',
'mac14': 'builds/ffmpeg/%s/ffmpeg-mac.zip',
'mac14-arm64': 'builds/ffmpeg/%s/ffmpeg-mac-arm64.zip',
'win64': 'builds/ffmpeg/%s/ffmpeg-win64.zip',
},
};
'android': {
'<unknown>': 'builds/android/%s/android.zip',
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': 'builds/android/%s/android.zip',
'ubuntu22.04-x64': 'builds/android/%s/android.zip',
'ubuntu18.04-arm64': undefined,
'ubuntu20.04-arm64': 'builds/android/%s/android.zip',
'ubuntu22.04-arm64': 'builds/android/%s/android.zip',
'debian11-x64': 'builds/android/%s/android.zip',
'debian11-arm64': 'builds/android/%s/android.zip',
'debian12-x64': 'builds/android/%s/android.zip',
'debian12-arm64': 'builds/android/%s/android.zip',
'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-arm64': '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-arm64': 'builds/android/%s/android.zip',
'mac14': 'builds/android/%s/android.zip',
'mac14-arm64': 'builds/android/%s/android.zip',
'win64': 'builds/android/%s/android.zip',
},
} as const;
@SingletonProto()
@BinaryAdapter(BinaryType.Playwright)
export class PlaywrightBinary extends AbstractBinary {
private dirItems?: Record<string, BinaryItem[]>;
async init() {
async initFetch() {
this.dirItems = undefined;
}
@@ -170,7 +212,7 @@ export class PlaywrightBinary extends AbstractBinary {
.filter(version => version.match(/^(?:\d+\.\d+\.\d+)(?:-beta-\d+)?$/))
// select recently update 20 items
.slice(-20);
const browsers: { name: string; 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(

View File

@@ -0,0 +1,132 @@
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';
@SingletonProto()
@BinaryAdapter(BinaryType.Prisma)
export class PrismaBinary extends AbstractBinary {
private dirItems: {
[key: string]: BinaryItem[];
} = {};
async initFetch() {
// https://github.com/cnpm/cnpmcore/issues/473#issuecomment-1562115738
const pkgUrl = 'https://registry.npmjs.com/@prisma/engines';
const data = await this.requestJSON(pkgUrl);
const modified = data.time.modified;
this.dirItems = {};
this.dirItems['/'] = [
{
name: 'all_commits/',
date: modified,
size: '-',
isDir: true,
url: '',
},
];
this.dirItems['/all_commits/'] = [];
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]);
// 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'] || '';
// "@prisma/engines-version": "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c"
// "@prisma/engines-version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9"
const matched = /\.(\w{30,})$/.exec(enginesVersion);
if (!matched) continue;
const commitId = matched[1];
if (commitIdMap[commitId]) continue;
commitIdMap[commitId] = true;
this.dirItems['/all_commits/'].push({
name: `${commitId}/`,
date,
size: '-',
isDir: true,
url: '',
});
}
}
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 url = `${binaryConfig.distUrl}?delimiter=/&prefix=${encodeURIComponent(subDir)}`;
const result = await this.requestJSON(url);
return { items: this.#parseItems(result), nextParams: null };
}
#parseItems(result: any): BinaryItem[] {
const items: BinaryItem[] = [];
// objects": [
// {
// "uploaded": "2023-05-23T15:43:05.772Z",
// "checksums": {
// "md5": "d41d8cd98f00b204e9800998ecf8427e"
// },
// "httpEtag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
// "etag": "d41d8cd98f00b204e9800998ecf8427e",
// "size": 0,
// "version": "7e77b6b8c1d214f2c6be3c959749b5a6",
// "key": "all_commits/61023c35d2c8762f66f09bc4183d2f630b541d08/darwin-arm64/.finished"
// },
// {
// "uploaded": "2023-05-23T15:41:33.861Z",
// "checksums": {
// "md5": "4822215a13ae372ae82afd12689fce37"
// },
// "httpEtag": "\"4822215a13ae372ae82afd12689fce37\"",
// "etag": "4822215a13ae372ae82afd12689fce37",
// "size": 96,
// "version": "7e77b6ba29d4e776023e4fa62825c13a",
// "key": "all_commits/61023c35d2c8762f66f09bc4183d2f630b541d08/darwin-arm64/libquery_engine.dylib.node.gz.sha256"
// },
// https://list-binaries.prisma-orm.workers.dev/?delimiter=/&prefix=all_commits/61023c35d2c8762f66f09bc4183d2f630b541d08/darwin-arm64/
const objects: {
uploaded: string;
size: number;
key: string;
}[] = result.objects || [];
for (const o of objects) {
const fullname = o.key;
// ignore size = 0
if (o.size === 0) continue;
const name = path.basename(fullname);
items.push({
name,
isDir: false,
// https://binaries.prisma.sh/all_commits/2452cc6313d52b8b9a96999ac0e974d0aedf88db/darwin-arm64/prisma-fmt.gz
url: `https://binaries.prisma.sh/${fullname}`,
size: o.size,
date: o.uploaded,
});
}
// delimitedPrefixes: [ 'all_commits/61023c35d2c8762f66f09bc4183d2f630b541d08/darwin-arm64/' ]
// https://list-binaries.prisma-orm.workers.dev/?delimiter=/&prefix=all_commits/61023c35d2c8762f66f09bc4183d2f630b541d08/
const delimitedPrefixes: string[] = result.delimitedPrefixes || [];
for (const fullname of delimitedPrefixes) {
const name = `${path.basename(fullname)}/`;
items.push({
name,
isDir: true,
url: '',
size: '-',
date: new Date().toISOString(),
});
}
return items;
}
}

View File

@@ -9,7 +9,7 @@ export class PuppeteerBinary extends AbstractBinary {
[key: string]: BinaryItem[];
};
async init() {
async initFetch() {
this.dirItems = undefined;
}

View File

@@ -5,7 +5,7 @@ import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './Abstra
@SingletonProto()
@BinaryAdapter(BinaryType.Sqlcipher)
export class SqlcipherBinary extends AbstractBinary {
async init() {
async initFetch() {
// do nothing
return;
}

View File

@@ -6,6 +6,16 @@ import { AbstractChangeStream, RegistryChangesStream } from './AbstractChangesSt
const MAX_LIMIT = 10000;
type FetchResults = {
results: {
seq: number;
type: string;
id: string;
changes: Record<string, string>[];
gmt_modified: Date,
}[];
};
@SingletonProto()
@RegistryChangesStream(RegistryType.Cnpmjsorg)
export class CnpmjsorgChangesStream extends AbstractChangeStream {
@@ -18,13 +28,13 @@ export class CnpmjsorgChangesStream extends AbstractChangeStream {
return since;
}
private async tryFetch(registry: Registry, since: string, limit = 1000) {
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}`);
}
const db = this.getChangesStreamUrl(registry, since, limit);
// json mode
const res = await this.httpclient.request(db, {
const res = await this.httpclient.request<FetchResults>(db, {
followRedirect: true,
timeout: 30000,
dataType: 'json',

View File

@@ -1,12 +1,17 @@
export const BUG_VERSIONS = 'bug-versions';
export const LATEST_TAG = 'latest';
export const GLOBAL_WORKER = 'GLOBAL_WORKER';
export const NOT_IMPLEMENTED_PATH = [ '/-/npm/v1/security/audits/quick', '/-/npm/v1/security/advisories/bulk' ];
export enum SyncMode {
none = 'none',
admin = 'admin',
exist = 'exist',
all = 'all',
}
export enum ChangesStreamMode {
json = 'json',
streaming = 'streaming',
}
export enum SyncDeleteMode {
ignore = 'ignore',
block = 'block',

View File

@@ -10,6 +10,8 @@ export enum BinaryType {
Nwjs = 'nwjs',
Playwright = 'playwright',
Puppeteer = 'puppeteer',
Prisma = 'prisma',
Sqlcipher = 'sqlcipher',
ChromeForTesting = 'chromeForTesting',
Edgedriver = 'edgedriver',
}

View File

@@ -1,6 +1,8 @@
import { CnpmcoreConfig } from '../port/config';
import { Readable } from 'stream';
import { IncomingHttpHeaders } from 'http';
import { EggContext } from '@eggjs/tegg';
import { estypes } from '@elastic/elasticsearch';
export interface UploadResult {
key: string;
@@ -49,6 +51,12 @@ export interface QueueAdapter {
length(key: string): Promise<number>;
}
export interface SearchAdapter {
search<T>(query: any): Promise<estypes.SearchHitsMetadata<T>>;
upsert<T>(id: string, document: T): Promise<string>;
delete(id: string): Promise<string>;
}
export interface AuthUrlResult {
loginUrl: string;
doneUrl: string;
@@ -62,3 +70,12 @@ export interface AuthClient {
getAuthUrl(ctx: EggContext): 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

@@ -8,7 +8,7 @@ export type BugVersionPackages = Record<string, BugVersionPackage>;
export class BugVersion {
private readonly data: BugVersionPackages;
constructor(data) {
constructor(data: BugVersionPackages) {
this.data = data;
}

View File

@@ -48,7 +48,7 @@ export class Hook extends Entity {
}
// payload 可能会特别大,如果做多次 stringify 浪费太多 cpu
signPayload(payload: object): { digest, payloadStr } {
signPayload(payload: object) {
const payloadStr = JSON.stringify(payload);
const digest = crypto.createHmac('sha256', this.secret)
.update(JSON.stringify(payload))

View File

@@ -1,6 +1,7 @@
import { Dist } from './Dist';
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import { PaddingSemVer } from './PaddingSemVer';
interface PackageVersionData extends EntityData {
packageId: string;
@@ -11,6 +12,8 @@ interface PackageVersionData extends EntityData {
tarDist: Dist;
readmeDist: Dist;
publishTime: Date;
paddingVersion?: string | null;
isPreRelease?: boolean | null;
}
export class PackageVersion extends Entity {
@@ -22,6 +25,8 @@ export class PackageVersion extends Entity {
tarDist: Dist;
readmeDist: Dist;
publishTime: Date;
paddingVersion: string;
isPreRelease: boolean;
constructor(data: PackageVersionData) {
super(data);
@@ -33,6 +38,14 @@ export class PackageVersion extends Entity {
this.tarDist = data.tarDist;
this.readmeDist = data.readmeDist;
this.publishTime = data.publishTime;
if (data.paddingVersion && typeof data.isPreRelease === 'boolean') {
this.paddingVersion = data.paddingVersion;
this.isPreRelease = data.isPreRelease;
} else {
const paddingSemVer = new PaddingSemVer(this.version);
this.paddingVersion = paddingSemVer.paddingVersion;
this.isPreRelease = paddingSemVer.isPreRelease;
}
}
static create(data: EasyData<PackageVersionData, 'packageVersionId'>): PackageVersion {

View File

@@ -0,0 +1,50 @@
import { SemVer, valid } from 'semver';
export class PaddingSemVer {
private readonly semver: SemVer;
// 跳过 semver 中的 buildInfo, buildInfo 不参与版本比较
private _paddingVersion: string;
readonly isPreRelease: boolean;
constructor(semver: string | SemVer) {
// ignore invalid version, e.g.: '1000000000000000000.0.0' on https://registry.npmjs.com/latentflip-test
if (!valid(semver)) {
this.isPreRelease = true;
this._paddingVersion = PaddingSemVer.anyVersion();
return;
}
this.semver = new SemVer(semver);
if ((this.semver as any).includePrerelease) {
this.isPreRelease = true;
} else if (this.semver.prerelease && this.semver.prerelease.length) {
this.isPreRelease = true;
} else {
this.isPreRelease = false;
}
}
get paddingVersion(): string {
if (!this._paddingVersion) {
this._paddingVersion = PaddingSemVer.paddingVersion(this.semver.major)
+ PaddingSemVer.paddingVersion(this.semver.minor)
+ PaddingSemVer.paddingVersion(this.semver.patch);
}
return this._paddingVersion;
}
// 版本信息中为纯数字, JS 中支持的最大整型为 16 位
// 因此填充成 16 位对齐,如果版本号超过 16 位,则抛出异常
static paddingVersion(v: number) {
const t = String(v);
if (t.length <= 16) {
const padding = new Array(16 - t.length).fill(0)
.join('');
return padding + t;
}
throw new Error(`v ${v} too long`);
}
static anyVersion() {
return '000000000000000000000000000000000000000000000000';
}
}

View File

@@ -9,6 +9,7 @@ interface RegistryData extends EntityData {
changeStream: string;
userPrefix: string;
type: RegistryType;
authToken?: string;
}
export type CreateRegistryData = Omit<EasyData<RegistryData, 'registryId'>, 'id'>;
@@ -20,6 +21,7 @@ export class Registry extends Entity {
changeStream: string;
userPrefix: string;
type: RegistryType;
authToken?: string;
constructor(data: RegistryData) {
super(data);
@@ -29,10 +31,11 @@ export class Registry extends Entity {
this.changeStream = data.changeStream;
this.userPrefix = data.userPrefix;
this.type = data.type;
this.authToken = data.authToken;
}
public static create(data: CreateRegistryData): Registry {
const newData = EntityUtil.defaultData(data, 'registryId');
const newData = EntityUtil.defaultData<RegistryData, 'registryId'>(data, 'registryId');
return new Registry(newData);
}
}

View File

@@ -0,0 +1,81 @@
import { Range, Comparator } from 'semver';
import { PaddingSemVer } from './PaddingSemVer';
const OPERATOR_MAP = {
'<': '$lt',
'<=': '$lte',
'>': '$gt',
'>=': '$gte',
'': '$eq',
};
export class SqlRange {
private readonly range: Range;
private _containPreRelease: boolean;
readonly condition: object;
constructor(range: string | Range) {
this.range = new Range(range);
this._containPreRelease = false;
this.condition = this.generateWhere();
}
private comparatorToSql(comparator: Comparator) {
if (comparator.semver === (Comparator as any).ANY) {
return {
$and: [
{
isPreRelease: {
$lte: 0,
},
},
{
paddingVersion: {
$gte: PaddingSemVer.anyVersion(),
},
},
],
};
}
const paddingSemver = new PaddingSemVer(comparator.semver);
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;
return {
$and: [
{
isPreRelease: {
$lte: paddingSemver.isPreRelease ? 1 : 0,
},
},
{
paddingVersion: {
[operator]: paddingSemver.paddingVersion,
},
},
],
};
}
private comparatorSetToSql(comparatorSet: Array<Comparator>) {
const condition: Array<object> = [];
for (const comparator of comparatorSet) {
condition.push(this.comparatorToSql(comparator));
}
return { $and: condition };
}
private generateWhere() {
const conditions: Array<object> = [];
for (const rangeSet of this.range.set) {
conditions.push(this.comparatorSetToSql(rangeSet as Comparator[]));
}
return { $or: conditions };
}
get containPreRelease(): boolean {
return this._containPreRelease;
}
}

View File

@@ -31,13 +31,13 @@ export interface TaskData<T = TaskBaseData> extends EntityData {
export type SyncPackageTaskOptions = {
authorId?: string;
authorIp?: string;
remoteAuthToken?: string;
tips?: string;
skipDependencies?: boolean;
syncDownloadData?: boolean;
// force sync history version
forceSyncHistory?: boolean;
registryId?: string;
specificVersions?: Array<string>;
};
export interface CreateHookTaskData extends TaskBaseData {
@@ -51,11 +51,11 @@ export interface TriggerHookTaskData extends TaskBaseData {
}
export interface CreateSyncPackageTaskData extends TaskBaseData {
remoteAuthToken?: string;
tips?: string;
skipDependencies?: boolean;
syncDownloadData?: boolean;
forceSyncHistory?: boolean;
specificVersions?: Array<string>;
}
export interface ChangesStreamTaskData extends TaskBaseData {
@@ -131,12 +131,12 @@ export class Task<T extends TaskBaseData = TaskBaseData> extends Entity {
data: {
// task execute worker
taskWorker: '',
remoteAuthToken: options?.remoteAuthToken,
tips: options?.tips,
registryId: options?.registryId ?? '',
skipDependencies: options?.skipDependencies,
syncDownloadData: options?.syncDownloadData,
forceSyncHistory: options?.forceSyncHistory,
specificVersions: options?.specificVersions,
},
};
const task = this.create(data);
@@ -231,6 +231,10 @@ export class Task<T extends TaskBaseData = TaskBaseData> extends Entity {
return task;
}
public static needMergeWhenWaiting(type: TaskType) {
return [ TaskType.SyncBinary, TaskType.SyncPackage ].includes(type);
}
start(): TaskUpdateCondition {
const condition = {
taskId: this.taskId,

View File

@@ -13,7 +13,8 @@ interface BaseTokenData extends EntityData {
cidrWhitelist?: string[];
userId: string;
isReadonly?: boolean;
type?: TokenType;
type?: TokenType | string;
lastUsedAt?: Date;
}
interface ClassicTokenData extends BaseTokenData{
@@ -30,7 +31,7 @@ interface GranularTokenData extends BaseTokenData {
type TokenData = ClassicTokenData | GranularTokenData;
export function isGranularToken(data: TokenData): data is GranularTokenData {
export function isGranularToken(data: TokenData | Token): data is GranularTokenData {
return data.type === TokenType.granular;
}
@@ -48,6 +49,7 @@ export class Token extends Entity {
readonly allowedScopes?: string[];
readonly expiredAt?: Date;
readonly expires?: number;
lastUsedAt: Date | null;
allowedPackages?: string[];
token?: string;
@@ -59,7 +61,8 @@ export class Token extends Entity {
this.tokenKey = data.tokenKey;
this.cidrWhitelist = data.cidrWhitelist || [];
this.isReadonly = data.isReadonly || false;
this.type = data.type || TokenType.classic;
this.type = (data.type as TokenType) || TokenType.classic;
this.lastUsedAt = data.lastUsedAt || null;
if (isGranularToken(data)) {
this.name = data.name;
@@ -67,6 +70,7 @@ export class Token extends Entity {
this.allowedScopes = data.allowedScopes;
this.expiredAt = data.expiredAt;
this.allowedPackages = data.allowedPackages;
this.isAutomation = false;
} else {
this.isAutomation = data.isAutomation || false;
}

View File

@@ -2,15 +2,12 @@ import { Event, Inject } from '@eggjs/tegg';
import { EggLogger } from 'egg';
import { PACKAGE_VERSION_ADDED } from './index';
import { BUG_VERSIONS } from '../../common/constants';
import { PackageManagerService } from '../service/PackageManagerService';
import { BugVersionService } from '../service/BugVersionService';
@Event(PACKAGE_VERSION_ADDED)
export class BugVersionFixHandler {
@Inject()
private readonly bugVersionService: BugVersionService;
@Inject()
private readonly packageManagerService: PackageManagerService;
@Inject()
private readonly logger: EggLogger;
@@ -18,7 +15,7 @@ export class BugVersionFixHandler {
async handle(fullname: string) {
if (fullname !== BUG_VERSIONS) return;
try {
const bugVersion = await this.packageManagerService.getBugVersion();
const bugVersion = await this.bugVersionService.getBugVersion();
if (!bugVersion) return;
await this.bugVersionService.cleanBugVersionPackageCaches(bugVersion);
} catch (e) {

View File

@@ -29,7 +29,7 @@ class ChangesStreamEvent {
protected readonly config: EggAppConfig;
protected get hookEnable() {
return this.config.hookEnable;
return this.config.cnpmcore.hookEnable;
}
protected async addChange(type: string, fullname: string, data: object): Promise<Change> {

View File

@@ -0,0 +1,56 @@
// TODO sync event
/* eslint-disable @typescript-eslint/no-unused-vars */
import { EggAppConfig } from 'egg';
import { Event, Inject } from '@eggjs/tegg';
import {
PACKAGE_UNPUBLISHED,
PACKAGE_VERSION_ADDED,
PACKAGE_VERSION_REMOVED,
PACKAGE_TAG_ADDED,
PACKAGE_TAG_CHANGED,
PACKAGE_TAG_REMOVED,
PACKAGE_MAINTAINER_CHANGED,
PACKAGE_MAINTAINER_REMOVED,
PACKAGE_META_CHANGED,
PACKAGE_BLOCKED,
PACKAGE_UNBLOCKED,
} from './index';
import { PackageSearchService } from '../service/PackageSearchService';
class SyncESPackage {
@Inject()
protected readonly packageSearchService: PackageSearchService;
@Inject()
protected readonly config: EggAppConfig;
protected async syncPackage(fullname: string) {
if (!this.config.cnpmcore.enableElasticsearch) return;
await this.packageSearchService.syncPackage(fullname, true);
}
}
@Event(PACKAGE_UNPUBLISHED)
@Event(PACKAGE_BLOCKED)
export class PackageUnpublished extends SyncESPackage {
async handle(fullname: string) {
if (!this.config.cnpmcore.enableElasticsearch) return;
await this.packageSearchService.removePackage(fullname);
}
}
@Event(PACKAGE_VERSION_ADDED)
@Event(PACKAGE_META_CHANGED)
@Event(PACKAGE_VERSION_REMOVED)
@Event(PACKAGE_TAG_ADDED)
@Event(PACKAGE_TAG_CHANGED)
@Event(PACKAGE_TAG_REMOVED)
@Event(PACKAGE_MAINTAINER_CHANGED)
@Event(PACKAGE_MAINTAINER_REMOVED)
@Event(PACKAGE_UNBLOCKED)
export class PackageVersionAdded extends SyncESPackage {
async handle(fullname: string) {
await this.syncPackage(fullname);
}
}

View File

@@ -1,8 +1,9 @@
import { Event, Inject } from '@eggjs/tegg';
import {
EggAppConfig,
EggAppConfig, EggLogger,
} from 'egg';
import { PACKAGE_VERSION_ADDED } from './index';
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';
@@ -11,19 +12,41 @@ class SyncPackageVersionFileEvent {
@Inject()
protected readonly config: EggAppConfig;
@Inject()
protected readonly logger: EggLogger;
@Inject()
private readonly packageManagerService: PackageManagerService;
@Inject()
private readonly packageVersionFileService: PackageVersionFileService;
protected async syncPackageVersionFile(fullname: string, version: string) {
// must set enableUnpkg and enableSyncUnpkgFiles = true both
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 (!packageVersion) return;
await this.packageVersionFileService.syncPackageVersionFiles(packageVersion);
try {
await this.packageVersionFileService.syncPackageVersionFiles(packageVersion);
} catch (err) {
if (err instanceof ForbiddenError) {
this.logger.info('[SyncPackageVersionFileEvent.syncPackageVersionFile] ignore sync files, cause: %s',
err.message,
);
return;
}
throw err;
}
}
protected async syncPackageReadmeToLatestVersion(fullname: string) {
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);
}
}
@@ -33,3 +56,19 @@ export class PackageVersionAdded extends SyncPackageVersionFileEvent {
await this.syncPackageVersionFile(fullname, version);
}
}
@Event(PACKAGE_TAG_ADDED)
export class PackageTagAdded extends SyncPackageVersionFileEvent {
async handle(fullname: string, tag: string) {
if (tag !== 'latest') return;
await this.syncPackageReadmeToLatestVersion(fullname);
}
}
@Event(PACKAGE_TAG_CHANGED)
export class PackageTagChanged extends SyncPackageVersionFileEvent {
async handle(fullname: string, tag: string) {
if (tag !== 'latest') return;
await this.syncPackageReadmeToLatestVersion(fullname);
}
}

View File

@@ -19,8 +19,8 @@ import { Binary } from '../entity/Binary';
import { TaskService } from './TaskService';
import { AbstractBinary, BinaryItem } from '../../common/adapter/binary/AbstractBinary';
import { AbstractService } from '../../common/AbstractService';
import { TaskRepository } from '../../repository/TaskRepository';
import { BinaryType } from '../../common/enum/Binary';
import { sortBy } from 'lodash';
function isoNow() {
return new Date().toISOString();
@@ -35,8 +35,6 @@ export class BinarySyncerService extends AbstractService {
@Inject()
private readonly taskService: TaskService;
@Inject()
private readonly taskRepository: TaskRepository;
@Inject()
private readonly httpclient: EggHttpClient;
@Inject()
private readonly nfsAdapter: NFSAdapter;
@@ -88,13 +86,7 @@ export class BinarySyncerService extends AbstractService {
return await this.nfsAdapter.getDownloadUrlOrStream(binary.storePath);
}
// SyncBinary 由定时任务每台单机定时触发,手动去重
// 添加 bizId 在 db 防止重复,记录 id 错误
public async createTask(binaryName: BinaryName, lastData?: any) {
const existsTask = await this.taskRepository.findTaskByTargetName(binaryName, TaskType.SyncBinary);
if (existsTask) {
return existsTask;
}
try {
return await this.taskService.createTask(Task.createSyncBinary(binaryName, lastData), false);
} catch (e) {
@@ -135,24 +127,35 @@ export class BinarySyncerService extends AbstractService {
this.logger.info('[BinarySyncerService.executeTask:start] taskId: %s, targetName: %s, log: %s',
task.taskId, task.targetName, logUrl);
try {
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'));
this.logger.info('[BinarySyncerService.executeTask:success] taskId: %s, targetName: %s, log: %s',
task.taskId, task.targetName, logUrl);
// 确保没有下载异常才算 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) {
task.error = err.message;
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(err);
if (err.name === 'HttpClientRequestTimeoutError'
|| err.name === 'ConnectionError'
|| err.name === 'ConnectTimeoutError') {
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(err);
}
await binaryAdapter.finishFetch(false, binaryName);
await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n'));
}
}
private async syncDir(binaryAdapter: AbstractBinary, task: Task, dir: string, parentIndex = '') {
private async syncDir(binaryAdapter: AbstractBinary, task: Task, dir: string, parentIndex = '', latestVersionParent = '/') {
const binaryName = task.targetName as BinaryName;
const result = await binaryAdapter.fetch(dir, binaryName);
let hasDownloadError = false;
@@ -160,14 +163,15 @@ export class BinarySyncerService extends AbstractService {
if (result && result.items.length > 0) {
hasItems = true;
let logs: string[] = [];
const newItems = await this.diff(binaryName, dir, result.items);
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()) {
if (item.isDir) {
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}.`);
const [ hasError, hasSubItems ] = await this.syncDir(binaryAdapter, task, `${dir}${item.name}`, `${parentIndex}${index}.`, latestVersionDir);
if (hasError) {
hasDownloadError = true;
} else {
@@ -195,7 +199,9 @@ export class BinarySyncerService extends AbstractService {
const { tmpfile, headers, timing } =
await downloadToTempfile(
this.httpclient, this.config.dataDir, item.sourceUrl!, { ignoreDownloadStatuses: item.ignoreDownloadStatuses });
logs.push(`[${isoNow()}][${dir}] 🟢 [${parentIndex}${index}] HTTP content-length: ${headers['content-length']}, timing: ${JSON.stringify(timing)}, ${item.sourceUrl} => ${tmpfile}`);
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);
localFile = tmpfile;
const binary = await this.saveBinaryItem(item, tmpfile);
logs.push(`[${isoNow()}][${dir}] 🟢 [${parentIndex}${index}] Synced file success, binaryId: ${binary.binaryId}`);
@@ -206,7 +212,11 @@ export class BinarySyncerService extends AbstractService {
this.logger.info('Not found %s, skip it', item.sourceUrl);
logs.push(`[${isoNow()}][${dir}] 🧪️ [${parentIndex}${index}] Download ${item.sourceUrl} not found, skip it`);
} else {
this.logger.error('Download binary %s %s', item.sourceUrl, err);
if (err.name === 'DownloadStatusInvalidError') {
this.logger.warn('Download binary %s %s', item.sourceUrl, err);
} else {
this.logger.error('Download binary %s %s', item.sourceUrl, err);
}
hasDownloadError = true;
logs.push(`[${isoNow()}][${dir}] ❌ [${parentIndex}${index}] Download ${item.sourceUrl} error: ${err}`);
}
@@ -222,20 +232,26 @@ export class BinarySyncerService extends AbstractService {
if (hasDownloadError) {
logs.push(`[${isoNow()}][${dir}] ❌ Synced dir fail`);
} else {
logs.push(`[${isoNow()}][${dir}] 🟢 Synced dir success`);
logs.push(`[${isoNow()}][${dir}] 🟢 Synced dir success, hasItems: ${hasItems}`);
}
await this.taskService.appendTaskLog(task, logs.join('\n'));
}
return [ hasDownloadError, hasItems ];
}
private async diff(binaryName: BinaryName, dir: string, fetchItems: BinaryItem[]) {
// 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);
const existsMap = new Map<string, Binary>();
for (const item of existsItems) {
existsMap.set(item.name, item);
}
const diffItems: { item: Binary; reason: string }[] = [];
let latestItem: BinaryItem | undefined;
for (const item of fetchItems) {
const existsItem = existsMap.get(item.name);
if (!existsItem) {
@@ -260,9 +276,26 @@ export class BinarySyncerService extends AbstractService {
existsItem.sourceUrl = item.url;
existsItem.ignoreDownloadStatuses = item.ignoreDownloadStatuses;
existsItem.date = item.date;
} else if (dir.endsWith(latestVersionParent)) {
if (!latestItem) {
latestItem = sortBy(fetchItems, [ 'date' ]).pop();
}
const isLatestItem = latestItem?.name === item.name;
if (isLatestItem && existsItem.isDir) {
diffItems.push({
item: existsItem,
reason: `revalidate latest version, latest parent dir is ${latestVersionParent}, current dir is ${dir}, current name is ${existsItem.name}`,
});
latestVersionParent = `${latestVersionParent}${existsItem.name}`;
}
}
}
return diffItems;
return {
newItems: diffItems,
latestVersionDir: latestVersionParent,
};
}
private async saveBinaryItem(binary: Binary, tmpfile?: string) {
@@ -287,7 +320,7 @@ export class BinarySyncerService extends AbstractService {
} else {
binaryAdapter = await this.eggObjectFactory.getEggObject(AbstractBinary, binaryConfig.type);
}
await binaryAdapter.init(binaryName);
await binaryAdapter.initFetch(binaryName);
return binaryAdapter;
}
}

View File

@@ -2,10 +2,12 @@ import { AccessLevel, SingletonProto, Inject } from '@eggjs/tegg';
import { EggLogger } from 'egg';
import pMap from 'p-map';
import { BugVersion } from '../entity/BugVersion';
import { PackageRepository } from '../../repository/PackageRepository';
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';
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
@@ -23,6 +25,27 @@ export class BugVersionService {
@Inject()
private readonly cacheService: CacheService;
@Inject()
private readonly bugVersionStore: BugVersionStore;
async getBugVersion(): Promise<BugVersion | undefined> {
// TODO performance problem, cache bugVersion and update with schedule
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);
if (!tag) return;
let bugVersion = this.bugVersionStore.getBugVersion(tag!.version);
if (!bugVersion) {
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);
}
return bugVersion;
}
async cleanBugVersionPackageCaches(bugVersion: BugVersion) {
const fullnames = bugVersion.listAllPackagesHasBugs();
await pMap(fullnames, async fullname => {

View File

@@ -101,8 +101,14 @@ export class ChangesStreamService extends AbstractService {
await setTimeout(this.config.cnpmcore.checkChangesStreamInterval);
}
} catch (err) {
this.logger.error('[ChangesStreamService.executeTask:error] %s, exit now', err);
this.logger.error(err);
this.logger.warn('[ChangesStreamService.executeTask:error] %s, exit now', err.message);
if (err.name === 'HttpClientRequestTimeoutError'
|| err.name === 'ConnectTimeoutError'
|| err.name === 'BodyTimeoutError') {
this.logger.warn(err);
} else {
this.logger.error(err);
}
task.error = `${err}`;
await this.taskRepository.saveTask(task);
await this.suspendSync();

View File

@@ -0,0 +1,33 @@
import { SingletonProto, AccessLevel, Inject } from '@eggjs/tegg';
import { EggLogger } from 'egg';
import pMap from 'p-map';
import { PackageVersionRepository } from '../../repository/PackageVersionRepository';
import { PaddingSemVer } from '../entity/PaddingSemVer';
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
})
export class FixNoPaddingVersionService {
@Inject()
private readonly packageVersionRepository: PackageVersionRepository;
@Inject()
private readonly logger: EggLogger;
async fixPaddingVersion(id?: number): Promise<void> {
// eslint-disable-next-line no-constant-condition
while (true) {
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));
await pMap(packageVersions, async packageVersion => {
const paddingSemver = new PaddingSemVer(packageVersion.version);
await this.packageVersionRepository.fixPaddingVersion(packageVersion.packageVersionId, paddingSemver);
}, { concurrency: 30 });
}
}
}

View File

@@ -0,0 +1,19 @@
import {
AccessLevel,
SingletonProto,
} from '@eggjs/tegg';
import { AbstractService } from '../../common/AbstractService';
import { NOT_IMPLEMENTED_PATH } from '../../common/constants';
import { NotFoundError, NotImplementedError } from 'egg-errors';
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
})
export class HomeService extends AbstractService {
async misc(path: string) {
if (NOT_IMPLEMENTED_PATH.includes(path)) {
throw new NotImplementedError(`${path} not implemented yet`);
}
throw new NotFoundError(`${path} not found`);
}
}

View File

@@ -1,12 +1,13 @@
import { stat } from 'fs/promises';
import { stat, readFile } from 'node:fs/promises';
import {
AccessLevel,
SingletonProto,
EventBus,
Inject,
} from '@eggjs/tegg';
import { ForbiddenError } from 'egg-errors';
import { ForbiddenError, NotFoundError } from 'egg-errors';
import { RequireAtLeastOne } from 'type-fest';
import npa from 'npm-package-arg';
import semver from 'semver';
import {
calculateIntegrity,
@@ -17,8 +18,6 @@ import {
hasShrinkWrapInTgz,
} from '../../common/PackageUtil';
import { AbstractService } from '../../common/AbstractService';
import { BugVersionStore } from '../../common/adapter/BugVersionStore';
import { BUG_VERSIONS, LATEST_TAG } from '../../common/constants';
import { AbbreviatedPackageJSONType, AbbreviatedPackageManifestType, PackageJSONType, PackageManifestType, PackageRepository } from '../../repository/PackageRepository';
import { PackageVersionBlockRepository } from '../../repository/PackageVersionBlockRepository';
import { PackageVersionDownloadRepository } from '../../repository/PackageVersionDownloadRepository';
@@ -46,6 +45,7 @@ import { BugVersionService } from './BugVersionService';
import { BugVersion } from '../entity/BugVersion';
import { RegistryManagerService } from './RegistryManagerService';
import { Registry } from '../entity/Registry';
import { PackageVersionService } from './PackageVersionService';
export interface PublishPackageCmd {
// maintainer: Maintainer;
@@ -53,7 +53,7 @@ export interface PublishPackageCmd {
// name don't include scope
name: string;
version: string;
description: string;
description?: string;
packageJson: PackageJSONType;
registryId?: string;
readme: string;
@@ -64,7 +64,7 @@ export interface PublishPackageCmd {
// sync worker will use localFile field
localFile?: string;
}, 'content' | 'localFile'>;
tag?: string;
tags?: string[];
isPrivate: boolean;
// only use on sync package
publishTime?: Date;
@@ -91,11 +91,11 @@ export class PackageManagerService extends AbstractService {
@Inject()
private readonly bugVersionService: BugVersionService;
@Inject()
private readonly bugVersionStore: BugVersionStore;
@Inject()
private readonly distRepository: DistRepository;
@Inject()
private readonly registryManagerService: RegistryManagerService;
@Inject()
private readonly packageVersionService: PackageVersionService;
private static downloadCounters = {};
@@ -107,14 +107,14 @@ export class PackageManagerService extends AbstractService {
scope: cmd.scope,
name: cmd.name,
isPrivate: cmd.isPrivate,
description: cmd.description,
description: cmd.description || '',
registryId: cmd.registryId,
});
} else {
// update description
// will read database twice to update description by model to entity and entity to model
if (pkg.description !== cmd.description) {
pkg.description = cmd.description;
pkg.description = cmd.description || '';
}
/* c8 ignore next 3 */
@@ -155,17 +155,17 @@ export class PackageManagerService extends AbstractService {
cmd.packageJson._hasShrinkwrap = await hasShrinkWrapInTgz(cmd.dist.content || cmd.dist.localFile!);
}
// set _npmUser field to cmd.packageJson
cmd.packageJson._npmUser = {
// clean user scope prefix
name: publisher.displayName,
email: publisher.email,
};
// add _registry_name field to cmd.packageJson
if (!cmd.packageJson._source_registry_name) {
let registry: Registry | null;
if (cmd.registryId) {
registry = await this.registryManagerService.findByRegistryId(cmd.registryId);
} else {
registry = await this.registryManagerService.ensureDefaultRegistry();
}
if (registry) {
cmd.packageJson._source_registry_name = registry.name;
}
const registry = await this.getSourceRegistry(pkg);
if (registry) {
cmd.packageJson._source_registry_name = registry.name;
}
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
@@ -201,26 +201,30 @@ export class PackageManagerService extends AbstractService {
integrity: tarDistIntegrity.integrity,
};
// https://github.com/npm/registry/blob/main/docs/responses/package-metadata.md#abbreviated-version-object
// Abbreviated version object
const abbreviated = JSON.stringify({
name: cmd.packageJson.name,
version: cmd.packageJson.version,
deprecated: cmd.packageJson.deprecated,
dependencies: cmd.packageJson.dependencies,
acceptDependencies: cmd.packageJson.acceptDependencies,
optionalDependencies: cmd.packageJson.optionalDependencies,
devDependencies: cmd.packageJson.devDependencies,
bundleDependencies: cmd.packageJson.bundleDependencies,
peerDependencies: cmd.packageJson.peerDependencies,
peerDependenciesMeta: cmd.packageJson.peerDependenciesMeta,
bin: cmd.packageJson.bin,
directories: cmd.packageJson.directories,
os: cmd.packageJson.os,
cpu: cmd.packageJson.cpu,
libc: cmd.packageJson.libc,
workspaces: cmd.packageJson.workspaces,
directories: cmd.packageJson.directories,
dist: cmd.packageJson.dist,
engines: cmd.packageJson.engines,
_hasShrinkwrap: cmd.packageJson._hasShrinkwrap,
hasInstallScript,
funding: cmd.packageJson.funding,
// https://github.com/cnpm/npminstall/blob/13efc7eec21a61e509226e3772bfb75cd5605612/lib/install_package.js#L176
// npminstall require publish time to show the recently update versions
publish_time: cmd.packageJson.publish_time,
@@ -270,13 +274,27 @@ export class PackageManagerService extends AbstractService {
if (cmd.skipRefreshPackageManifests !== true) {
await this.refreshPackageChangeVersionsToDists(pkg, [ pkgVersion.version ]);
}
if (cmd.tag) {
await this.savePackageTag(pkg, cmd.tag, cmd.version, true);
if (cmd.tags) {
for (const tag of cmd.tags) {
await this.savePackageTag(pkg, tag, cmd.version, true);
this.eventBus.emit(PACKAGE_VERSION_ADDED, pkg.fullname, pkgVersion.version, tag);
}
} else {
this.eventBus.emit(PACKAGE_VERSION_ADDED, pkg.fullname, pkgVersion.version, undefined);
}
this.eventBus.emit(PACKAGE_VERSION_ADDED, pkg.fullname, pkgVersion.version, cmd.tag);
return pkgVersion;
}
async blockPackageByFullname(name: string, reason: string) {
const [ scope, pkgName ] = getScopeAndName(name);
const pkg = await this.packageRepository.findPackage(scope, pkgName);
if (!pkg) {
throw new NotFoundError(`Package name(${name}) not found`);
}
return await this.blockPackage(pkg, reason);
}
async blockPackage(pkg: Package, reason: string) {
let block = await this.packageVersionBlockRepository.findPackageBlock(pkg.packageId);
if (block) {
@@ -306,6 +324,15 @@ export class PackageManagerService extends AbstractService {
return block;
}
async unblockPackageByFullname(name: string) {
const [ scope, pkgName ] = getScopeAndName(name);
const pkg = await this.packageRepository.findPackage(scope, pkgName);
if (!pkg) {
throw new NotFoundError(`Package name(${name}) not found`);
}
return await this.unblockPackage(pkg);
}
async unblockPackage(pkg: Package) {
const block = await this.packageVersionBlockRepository.findPackageVersionBlock(pkg.packageId, '*');
if (block) {
@@ -327,9 +354,9 @@ export class PackageManagerService extends AbstractService {
}
}
async replacePackageMaintainers(pkg: Package, maintainers: User[]) {
async replacePackageMaintainersAndDist(pkg: Package, maintainers: User[]) {
await this.packageRepository.replacePackageMaintainers(pkg.packageId, maintainers.map(m => m.userId));
await this._refreshPackageManifestRootAttributeOnlyToDists(pkg, 'maintainers');
await this.refreshPackageMaintainersToDists(pkg);
this.eventBus.emit(PACKAGE_MAINTAINER_CHANGED, pkg.fullname, maintainers);
}
@@ -342,14 +369,12 @@ export class PackageManagerService extends AbstractService {
}
}
if (hasNewRecord) {
await this._refreshPackageManifestRootAttributeOnlyToDists(pkg, 'maintainers');
this.eventBus.emit(PACKAGE_MAINTAINER_CHANGED, pkg.fullname, maintainers);
}
}
async removePackageMaintainer(pkg: Package, maintainer: User) {
await this.packageRepository.removePackageMaintainer(pkg.packageId, maintainer.userId);
await this._refreshPackageManifestRootAttributeOnlyToDists(pkg, 'maintainers');
this.eventBus.emit(PACKAGE_MAINTAINER_REMOVED, pkg.fullname, maintainer.name);
}
@@ -369,7 +394,7 @@ export class PackageManagerService extends AbstractService {
return await this._listPackageFullOrAbbreviatedManifests(scope, name, false, isSync);
}
async showPackageVersionByVersionOrTag(scope: string, name: string, versionOrTag: string): Promise<{
async showPackageVersionByVersionOrTag(scope: string, name: string, spec: string): Promise<{
blockReason?: string,
pkg?: Package,
packageVersion?: PackageVersion | null,
@@ -380,40 +405,27 @@ export class PackageManagerService extends AbstractService {
if (block) {
return { blockReason: block.reason, pkg };
}
let version = versionOrTag;
if (!semver.valid(versionOrTag)) {
// invalid version, versionOrTag is a tag
const packageTag = await this.packageRepository.findPackageTag(pkg.packageId, versionOrTag);
if (packageTag) {
version = packageTag.version;
}
const fullname = getFullname(scope, name);
const result = npa(`${fullname}@${spec}`);
const version = await this.packageVersionService.getVersion(result);
if (!version) {
return {};
}
const packageVersion = await this.packageRepository.findPackageVersion(pkg.packageId, version);
return { packageVersion, pkg };
}
async showPackageVersionManifest(scope: string, name: string, versionOrTag: string, isSync = false) {
let manifest;
const { blockReason, packageVersion, pkg } = await this.showPackageVersionByVersionOrTag(scope, name, versionOrTag);
if (blockReason) {
return {
blockReason,
manifest,
pkg,
};
async showPackageVersionManifest(scope: string, name: string, spec: string, isSync = false, isFullManifests = false) {
const pkg = await this.packageRepository.findPackage(scope, name);
if (!pkg) return {};
const block = await this.packageVersionBlockRepository.findPackageBlock(pkg.packageId);
if (block) {
return { blockReason: block.reason, pkg };
}
if (!packageVersion) return { manifest: null, blockReason, pkg };
manifest = await this.distRepository.findPackageVersionManifest(packageVersion.packageId, packageVersion.version);
let bugVersion: BugVersion | undefined;
// sync mode response no bug version fixed
if (!isSync) {
bugVersion = await this.getBugVersion();
}
if (bugVersion) {
const fullname = getFullname(scope, name);
manifest = await this.bugVersionService.fixPackageBugVersion(bugVersion, fullname, manifest);
}
return { manifest, blockReason, pkg };
const fullname = getFullname(scope, name);
const result = npa(`${fullname}@${spec}`);
const manifest = await this.packageVersionService.readManifest(pkg.packageId, result, isFullManifests, !isSync);
return { manifest, blockReason: null, pkg };
}
async downloadPackageVersionTar(packageVersion: PackageVersion) {
@@ -422,7 +434,7 @@ export class PackageManagerService extends AbstractService {
public plusPackageVersionCounter(fullname: string, version: string) {
// set counter + 1, schedule will store them into database
const counters = PackageManagerService.downloadCounters;
const counters: Record<string, Record<string, number>> = PackageManagerService.downloadCounters;
if (!counters[fullname]) counters[fullname] = {};
counters[fullname][version] = (counters[fullname][version] || 0) + 1;
// Total
@@ -441,7 +453,7 @@ export class PackageManagerService extends AbstractService {
// will be call by schedule/SavePackageVersionDownloadCounter.ts
async savePackageVersionCounters() {
// { [fullname]: { [version]: number } }
const counters = PackageManagerService.downloadCounters;
const counters: Record<string, Record<string, number>> = PackageManagerService.downloadCounters;
const fullnames = Object.keys(counters);
if (fullnames.length === 0) return;
@@ -492,6 +504,25 @@ export class PackageManagerService extends AbstractService {
await this._mergeManifestDist(pkgVersion.abbreviatedDist, mergeAbbreviated);
}
/**
* save package version readme
*/
public async savePackageVersionReadme(pkgVersion: PackageVersion, readmeFile: string) {
await this.distRepository.saveDist(pkgVersion.readmeDist, readmeFile);
this.logger.info('[PackageManagerService.savePackageVersionReadme] save packageVersionId:%s readme:%s to dist:%s',
pkgVersion.packageVersionId, readmeFile, pkgVersion.readmeDist.distId);
}
public async savePackageReadme(pkg: Package, readmeFile: string) {
if (!pkg.manifestsDist) return;
const fullManifests = await this.distRepository.readDistBytesToJSON<PackageManifestType>(pkg.manifestsDist);
if (!fullManifests) return;
fullManifests.readme = await readFile(readmeFile, 'utf-8');
await this._updatePackageManifestsToDists(pkg, fullManifests, null);
this.logger.info('[PackageManagerService.savePackageReadme] save packageId:%s readme, size: %s',
pkg.packageId, fullManifests.readme.length);
}
private async _removePackageVersionAndDist(pkgVersion: PackageVersion) {
// remove nfs dists
await Promise.all([
@@ -504,10 +535,10 @@ export class PackageManagerService extends AbstractService {
await this.packageRepository.removePackageVersion(pkgVersion);
}
public async unpublishPackage(pkg: Package, forceRefresh = false) {
public async unpublishPackage(pkg: Package) {
const pkgVersions = await this.packageRepository.listPackageVersions(pkg.packageId);
// already unpublished
if (pkgVersions.length === 0 && !forceRefresh) {
if (pkgVersions.length === 0) {
this.logger.info(`[packageManagerService.unpublishPackage:skip] ${pkg.packageId} already unpublished`);
return;
}
@@ -533,8 +564,14 @@ export class PackageManagerService extends AbstractService {
}
public async removePackageVersion(pkg: Package, pkgVersion: PackageVersion, skipRefreshPackageManifests = false) {
const currentVersions = await this.packageRepository.listPackageVersionNames(pkg.packageId);
// only one version, unpublish the package
if (currentVersions.length === 1 && currentVersions[0] === pkgVersion.version) {
await this.unpublishPackage(pkg);
return;
}
// remove version & update tags
await this._removePackageVersionAndDist(pkgVersion);
// all versions removed
const versions = await this.packageRepository.listPackageVersionNames(pkg.packageId);
if (versions.length > 0) {
let updateTag: string | undefined;
@@ -555,8 +592,6 @@ export class PackageManagerService extends AbstractService {
}
return;
}
// unpublish
await this.unpublishPackage(pkg, true);
}
public async savePackageTag(pkg: Package, tag: string, version: string, skipEvent = false) {
@@ -640,22 +675,14 @@ export class PackageManagerService extends AbstractService {
await this._updatePackageManifestsToDists(pkg, fullManifests, abbreviatedManifests);
}
async getBugVersion(): Promise<BugVersion | undefined> {
// TODO performance problem, cache bugVersion and update with schedule
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);
if (!tag) return;
let bugVersion = this.bugVersionStore.getBugVersion(tag!.version);
if (!bugVersion) {
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);
async getSourceRegistry(pkg: Package): Promise<Registry | null> {
let registry: Registry | null;
if (pkg.registryId) {
registry = await this.registryManagerService.findByRegistryId(pkg.registryId);
} else {
registry = await this.registryManagerService.ensureDefaultRegistry();
}
return bugVersion;
return registry;
}
private async _listPackageDistTags(pkg: Package) {
@@ -706,13 +733,16 @@ export class PackageManagerService extends AbstractService {
const fieldsFromLatestManifest = [
'author', 'bugs', 'contributors', 'description', 'homepage', 'keywords', 'license',
'readmeFilename', 'repository',
];
] as const;
// the latest version metas
for (const field of fieldsFromLatestManifest) {
fullManifests[field] = latestManifest[field];
if (latestManifest[field]) {
(fullManifests as Record<string, unknown>)[field] = latestManifest[field];
}
}
}
private async _setPackageDistTagsAndLatestInfos(pkg: Package, fullManifests: PackageManifestType, abbreviatedManifests: AbbreviatedPackageManifestType) {
const distTags = await this._listPackageDistTags(pkg);
if (distTags.latest) {
@@ -789,6 +819,7 @@ export class PackageManagerService extends AbstractService {
let blockReason = '';
const pkg = await this.packageRepository.findPackage(scope, name);
if (!pkg) return { etag, data: null, blockReason };
const registry = await this.getSourceRegistry(pkg);
const block = await this.packageVersionBlockRepository.findPackageBlock(pkg.packageId);
if (block) {
@@ -798,7 +829,7 @@ export class PackageManagerService extends AbstractService {
let bugVersion: BugVersion | undefined;
// sync mode response no bug version fixed
if (!isSync) {
bugVersion = await this.getBugVersion();
bugVersion = await this.bugVersionService.getBugVersion();
}
const fullname = getFullname(scope, name);
@@ -810,6 +841,11 @@ export class PackageManagerService extends AbstractService {
if (bugVersion) {
await this.bugVersionService.fixPackageBugVersions(bugVersion, fullname, data.versions);
}
// set _source_registry_name in full manifestDist
if (registry) {
data._source_registry_name = registry?.name;
}
const distBytes = Buffer.from(JSON.stringify(data));
const distIntegrity = await calculateIntegrity(distBytes);
etag = `"${distIntegrity.shasum}"`;
@@ -850,8 +886,9 @@ export class PackageManagerService extends AbstractService {
const distTags = await this._listPackageDistTags(pkg);
const maintainers = await this._listPackageMaintainers(pkg);
const registry = await this.getSourceRegistry(pkg);
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#full-metadata-format
const data:PackageManifestType = {
const data: PackageManifestType = {
_id: `${pkg.fullname}`,
_rev: `${pkg.id}-${pkg.packageId}`,
'dist-tags': distTags,
@@ -884,6 +921,7 @@ export class PackageManagerService extends AbstractService {
// as given in package.json, for the latest version
repository: undefined,
// users: an object whose keys are the npm user names of people who have starred this package
_source_registry_name: registry?.name,
};
let latestTagVersion = '';

View File

@@ -0,0 +1,249 @@
import { AccessLevel, Inject, SingletonProto } from '@eggjs/tegg';
import { estypes, errors } from '@elastic/elasticsearch';
import dayjs from 'dayjs';
import { AbstractService } from '../../common/AbstractService';
import { formatAuthor, getScopeAndName } from '../../common/PackageUtil';
import { PackageManagerService } from './PackageManagerService';
import { SearchManifestType, SearchMappingType, SearchRepository } from '../../repository/SearchRepository';
import { PackageVersionDownloadRepository } from '../../repository/PackageVersionDownloadRepository';
import { PackageRepository } from '../../repository/PackageRepository';
import { PackageVersionBlockRepository } from '../../repository/PackageVersionBlockRepository';
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
})
export class PackageSearchService extends AbstractService {
@Inject()
private readonly packageManagerService: PackageManagerService;
@Inject()
private readonly searchRepository: SearchRepository;
@Inject()
private packageVersionDownloadRepository: PackageVersionDownloadRepository;
@Inject()
protected packageRepository: PackageRepository;
@Inject()
protected packageVersionBlockRepository: PackageVersionBlockRepository;
async syncPackage(fullname: string, isSync = true) {
const [ scope, name ] = getScopeAndName(fullname);
const fullManifests = await this.packageManagerService.listPackageFullManifests(scope, name, isSync);
if (!fullManifests.data) {
this.logger.warn('[PackageSearchService.syncPackage] save package:%s not found', fullname);
return;
}
const pkg = await this.packageRepository.findPackage(scope, name);
if (!pkg) {
this.logger.warn('[PackageSearchService.syncPackage] findPackage:%s not found', fullname);
return;
}
const block = await this.packageVersionBlockRepository.findPackageBlock(pkg.packageId);
if (block) {
this.logger.warn('[PackageSearchService.syncPackage] package:%s is blocked, try to remove es', fullname);
await this.removePackage(fullname);
return;
}
// get last year download data
const startDate = dayjs().subtract(1, 'year');
const endDate = dayjs();
const entities = await this.packageVersionDownloadRepository.query(pkg.packageId, startDate.toDate(), endDate.toDate());
let downloadsAll = 0;
for (const entity of entities) {
for (let i = 1; i <= 31; i++) {
const day = String(i).padStart(2, '0');
const field = `d${day}`;
const counter = entity[field];
if (!counter) continue;
downloadsAll += counter;
}
}
const { data: manifest } = fullManifests;
const latestVersion = manifest['dist-tags'].latest;
const latestManifest = manifest.versions[latestVersion];
const packageDoc: SearchMappingType = {
name: manifest.name,
version: latestVersion,
_rev: manifest._rev,
scope: scope ? scope.replace('@', '') : 'unscoped',
keywords: manifest.keywords || [],
versions: Object.keys(manifest.versions),
description: manifest.description,
license: typeof manifest.license === 'object' ? manifest.license?.type : manifest.license,
maintainers: manifest.maintainers,
author: formatAuthor(manifest.author),
'dist-tags': manifest['dist-tags'],
date: manifest.time[latestVersion],
created: manifest.time.created,
modified: manifest.time.modified,
// 归属 registrykeywords 枚举值
_source_registry_name: manifest._source_registry_name,
// 最新版本发布人 _npmUser:
_npmUser: latestManifest?._npmUser,
// 最新版本发布信息
publish_time: latestManifest?.publish_time,
};
// http://npmmirror.com/package/npm/files/lib/utils/format-search-stream.js#L147-L148
// npm cli 使用 username 字段
if (packageDoc.maintainers) {
packageDoc.maintainers = packageDoc.maintainers.map(maintainer => {
return {
username: maintainer.name,
...maintainer,
};
});
}
const document: SearchManifestType = {
package: packageDoc,
downloads: {
all: downloadsAll,
},
};
return await this.searchRepository.upsertPackage(document);
}
async searchPackage(text: string, from: number, size: number): Promise<{ objects: (SearchManifestType | undefined)[], total: number }> {
const matchQueries = this._buildMatchQueries(text);
const scriptScore = this._buildScriptScore({
text,
scoreEffect: 0.25,
});
const res = await this.searchRepository.searchPackage({
body: {
size,
from,
query: {
function_score: {
boost_mode: 'replace',
query: {
bool: {
should: matchQueries,
minimum_should_match: matchQueries.length ? 1 : 0,
},
},
script_score: scriptScore,
},
},
},
});
const { hits, total } = res;
return {
objects: hits?.map(item => {
return item._source;
}),
total: (total as estypes.SearchTotalHits).value,
};
}
async removePackage(fullname: string) {
try {
return await this.searchRepository.removePackage(fullname);
} catch (error) {
// if the package does not exist, returns success
if (error instanceof errors.ResponseError && error?.statusCode === 404) {
this.logger.warn('[PackageSearchService.removePackage] remove package:%s not found', fullname);
return fullname;
}
throw error;
}
}
// https://github.com/npms-io/queries/blob/master/lib/search.js#L8C1-L78C2
private _buildMatchQueries(text: string) {
if (!text) {
return [];
}
return [
// Standard match using cross_fields
{
multi_match: {
query: text,
operator: 'and',
fields: [
'package.name.standard^4',
'package.description.standard',
'package.keywords.standard^2',
],
type: 'cross_fields',
boost: 6,
tie_breaker: 0.5,
},
},
// Partial match using edge-ngram
{
multi_match: {
query: text,
operator: 'and',
fields: [
'package.name.edge_ngram^4',
'package.description.edge_ngram',
'package.keywords.edge_ngram^2',
],
type: 'phrase',
slop: 3,
boost: 3,
tie_breaker: 0.5,
},
},
// Normal term match with an english stemmer
{
multi_match: {
query: text,
operator: 'and',
fields: [
'package.name.english_docs^4',
'package.description.english_docs',
'package.keywords.english_docs^2',
],
type: 'cross_fields',
boost: 3,
tie_breaker: 0.5,
},
},
// Normal term match with a more aggressive english stemmer (not so important)
{
multi_match: {
query: text,
operator: 'and',
fields: [
'package.name.english_aggressive_docs^4',
'package.description.english_aggressive_docs',
'package.keywords.english_aggressive_docs^2',
],
type: 'cross_fields',
tie_breaker: 0.5,
},
},
];
}
private _buildScriptScore(params: { text: string | undefined, scoreEffect: number }) {
// keep search simple, only download(popularity)
const downloads = 'doc["downloads.all"].value';
const source = `doc["package.name.raw"].value.equals(params.text) ? 100000 + ${downloads} : _score * Math.pow(${downloads}, params.scoreEffect)`;
return {
script: {
source,
params: {
text: params.text || '',
scoreEffect: params.scoreEffect,
},
},
};
}
}

View File

@@ -1,15 +1,14 @@
import os from 'os';
import os from 'node:os';
import { setTimeout } from 'node:timers/promises';
import { rm } from 'node:fs/promises';
import {
AccessLevel,
SingletonProto,
Inject,
} from '@eggjs/tegg';
import { Pointcut } from '@eggjs/tegg/aop';
import {
EggContextHttpClient,
} from 'egg';
import { setTimeout } from 'timers/promises';
import { rm } from 'fs/promises';
import { EggHttpClient } from 'egg';
import { isEqual, isEmpty } from 'lodash';
import semver from 'semver';
import { NPMRegistry, RegistryResponse } from '../../common/adapter/NPMRegistry';
import { detectInstallScript, getScopeAndName } from '../../common/PackageUtil';
@@ -17,7 +16,7 @@ import { downloadToTempfile } from '../../common/FileUtil';
import { TaskState, TaskType } from '../../common/enum/Task';
import { AbstractService } from '../../common/AbstractService';
import { TaskRepository } from '../../repository/TaskRepository';
import { PackageRepository } from '../../repository/PackageRepository';
import { PackageJSONType, PackageManifestType, PackageRepository } from '../../repository/PackageRepository';
import { PackageVersionDownloadRepository } from '../../repository/PackageVersionDownloadRepository';
import { UserRepository } from '../../repository/UserRepository';
import { Task, SyncPackageTaskOptions, CreateSyncPackageTask } from '../entity/Task';
@@ -32,7 +31,7 @@ import { Registry } from '../entity/Registry';
import { BadRequestError } from 'egg-errors';
import { ScopeManagerService } from './ScopeManagerService';
import { EventCorkAdvice } from './EventCorkerAdvice';
import { SyncDeleteMode } from '../../common/constants';
import { PresetRegistryName, SyncDeleteMode } from '../../common/constants';
type syncDeletePkgOptions = {
task: Task,
@@ -73,7 +72,7 @@ export class PackageSyncerService extends AbstractService {
@Inject()
private readonly cacheService: CacheService;
@Inject()
private readonly httpclient: EggContextHttpClient;
private readonly httpclient: EggHttpClient;
@Inject()
private readonly registryManagerService: RegistryManagerService;
@Inject()
@@ -115,17 +114,18 @@ export class PackageSyncerService extends AbstractService {
if (!this.allowSyncDownloadData) {
return;
}
const fullname = pkg.fullname;
const start = '2011-01-01';
const end = this.config.cnpmcore.syncDownloadDataMaxDate;
const registry = this.config.cnpmcore.syncDownloadDataSourceRegistry;
const remoteAuthToken = await this.registryManagerService.getAuthTokenByRegistryHost(registry);
const logs: string[] = [];
let downloads: { day: string; downloads: number }[];
logs.push(`[${isoNow()}][DownloadData] 🚧🚧🚧🚧🚧 Syncing "${fullname}" download data "${start}:${end}" on ${registry} 🚧🚧🚧🚧🚧`);
const failEnd = '❌❌❌❌❌ 🚮 give up 🚮 ❌❌❌❌❌';
try {
const { remoteAuthToken } = task.data as SyncPackageTaskOptions;
const { data, status, res } = await this.npmRegistry.getDownloadRanges(registry, fullname, start, end, { remoteAuthToken });
downloads = data.downloads || [];
logs.push(`[${isoNow()}][DownloadData] 🚧 HTTP [${status}] timing: ${JSON.stringify(res.timing)}, downloads: ${downloads.length}`);
@@ -162,7 +162,7 @@ export class PackageSyncerService extends AbstractService {
private async syncUpstream(task: Task) {
const registry = this.npmRegistry.registry;
const fullname = task.targetName;
const { remoteAuthToken } = task.data as SyncPackageTaskOptions;
const remoteAuthToken = await this.registryManagerService.getAuthTokenByRegistryHost(registry);
let logs: string[] = [];
let logId = '';
logs.push(`[${isoNow()}][UP] 🚧🚧🚧🚧🚧 Waiting sync "${fullname}" task on ${registry} 🚧🚧🚧🚧🚧`);
@@ -173,7 +173,9 @@ export class PackageSyncerService extends AbstractService {
logId = data.logId;
} catch (err: any) {
const status = err.status || 'unknow';
logs.push(`[${isoNow()}][UP] ❌ Sync ${fullname} fail, create sync task error: ${err}, status: ${status}`);
// 可能会抛出 AggregateError 异常
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError
logs.push(`[${isoNow()}][UP] ❌ Sync ${fullname} fail, create sync task error: ${err}, status: ${status} ${err instanceof AggregateError ? err.errors : ''}`);
logs.push(`[${isoNow()}][UP] ${failEnd}`);
await this.taskService.appendTaskLog(task, logs.join('\n'));
return;
@@ -202,8 +204,8 @@ export class PackageSyncerService extends AbstractService {
const log = data && data.log || '';
offset += log.length;
if (data && data.syncDone) {
logs.push(`[${isoNow()}][UP] 🟢 Sync ${fullname} success [${useTime}ms], log: ${logUrl}, offset: ${offset}`);
logs.push(`[${isoNow()}][UP] 🟢🟢🟢🟢🟢 ${registry}/${fullname} 🟢🟢🟢🟢🟢`);
logs.push(`[${isoNow()}][UP] 🎉 Sync ${fullname} success [${useTime}ms], log: ${logUrl}, offset: ${offset}`);
logs.push(`[${isoNow()}][UP] 🔗 ${registry}/${fullname}`);
await this.taskService.appendTaskLog(task, logs.join('\n'));
return;
}
@@ -297,8 +299,8 @@ export class PackageSyncerService extends AbstractService {
}
// update log
logs.push(`[${isoNow()}] 🟢 log: ${logUrl}`);
logs.push(`[${isoNow()}] 🟢🟢🟢🟢🟢 ${url} 🟢🟢🟢🟢🟢`);
logs.push(`[${isoNow()}] 📝 Log URL: ${logUrl}`);
logs.push(`[${isoNow()}] 🔗 ${url}`);
await this.taskService.finishTask(task, TaskState.Success, logs.join('\n'));
this.logger.info('[PackageSyncerService.executeTask:remove-package] taskId: %s, targetName: %s',
task.taskId, task.targetName);
@@ -310,7 +312,7 @@ export class PackageSyncerService extends AbstractService {
// 1. 其次从 task.data.registryId (创建单包同步任务时传入)
// 2. 接着根据 scope 进行计算 (作为子包依赖同步时候,无 registryId)
// 3. 最后返回 default registryId (可能 default registry 也不存在)
public async initSpecRegistry(task: Task, pkg: Package | null = null, scope?: string): Promise<Registry | null> {
public async initSpecRegistry(task: Task, pkg: Package | null = null, scope?: string): Promise<Registry> {
const registryId = pkg?.registryId || (task.data as SyncPackageTaskOptions).registryId;
let targetHost: string = this.config.cnpmcore.sourceRegistry;
let registry: Registry | null = null;
@@ -349,10 +351,11 @@ export class PackageSyncerService extends AbstractService {
public async executeTask(task: Task) {
const fullname = task.targetName;
const [ scope, name ] = getScopeAndName(fullname);
const { tips, skipDependencies: originSkipDependencies, syncDownloadData, forceSyncHistory, remoteAuthToken } = task.data as SyncPackageTaskOptions;
const { tips, skipDependencies: originSkipDependencies, syncDownloadData, forceSyncHistory, specificVersions } = task.data as SyncPackageTaskOptions;
let pkg = await this.packageRepository.findPackage(scope, name);
const registry = await this.initSpecRegistry(task, pkg, scope);
const registryHost = this.npmRegistry.registry;
const remoteAuthToken = registry.authToken;
let logs: string[] = [];
if (tips) {
logs.push(`[${isoNow()}] 👉👉👉👉👉 Tips: ${tips} 👈👈👈👈👈`);
@@ -361,13 +364,24 @@ export class PackageSyncerService extends AbstractService {
const taskQueueHighWaterSize = this.config.cnpmcore.taskQueueHighWaterSize;
const taskQueueInHighWaterState = taskQueueLength >= taskQueueHighWaterSize;
const skipDependencies = taskQueueInHighWaterState ? true : !!originSkipDependencies;
const syncUpstream = !!(!taskQueueInHighWaterState && this.config.cnpmcore.sourceRegistryIsCNpm && this.config.cnpmcore.syncUpstreamFirst);
const syncUpstream = !!(!taskQueueInHighWaterState && this.config.cnpmcore.sourceRegistryIsCNpm && this.config.cnpmcore.syncUpstreamFirst && registry.name === PresetRegistryName.default);
const logUrl = `${this.config.cnpmcore.registry}/-/package/${fullname}/syncs/${task.taskId}/log`;
this.logger.info('[PackageSyncerService.executeTask:start] taskId: %s, targetName: %s, attempts: %s, taskQueue: %s/%s, syncUpstream: %s, log: %s',
task.taskId, task.targetName, task.attempts, taskQueueLength, taskQueueHighWaterSize, syncUpstream, logUrl);
logs.push(`[${isoNow()}] 🚧🚧🚧🚧🚧 Syncing from ${registryHost}/${fullname}, skipDependencies: ${skipDependencies}, syncUpstream: ${syncUpstream}, syncDownloadData: ${!!syncDownloadData}, forceSyncHistory: ${!!forceSyncHistory} attempts: ${task.attempts}, worker: "${os.hostname()}/${process.pid}", taskQueue: ${taskQueueLength}/${taskQueueHighWaterSize} 🚧🚧🚧🚧🚧`);
if (specificVersions) {
logs.push(`[${isoNow()}] 👉 syncing specific versions: ${specificVersions.join(' | ')} 👈`);
}
logs.push(`[${isoNow()}] 🚧 log: ${logUrl}`);
if (registry?.name === PresetRegistryName.self) {
logs.push(`[${isoNow()}] ❌❌❌❌❌ ${fullname} has been published to the self registry, skip sync ❌❌❌❌❌`);
await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n'));
this.logger.info('[PackageSyncerService.executeTask:fail] taskId: %s, targetName: %s, invalid registryId',
task.taskId, task.targetName);
return;
}
if (pkg && pkg?.registryId !== registry?.registryId) {
if (pkg.registryId) {
logs.push(`[${isoNow()}] ❌❌❌❌❌ ${fullname} registry is ${pkg.registryId} not belong to ${registry?.registryId}, skip sync ❌❌❌❌❌`);
@@ -472,7 +486,7 @@ export class PackageSyncerService extends AbstractService {
// { name: 'jasonlaster11', email: 'jason.laster.11@gmail.com' }
// ],
let maintainers = data.maintainers;
const maintainersMap = {};
const maintainersMap: Record<string, PackageManifestType['maintainers']> = {};
const users: User[] = [];
let changedUserCount = 0;
if (!Array.isArray(maintainers) || maintainers.length === 0) {
@@ -544,10 +558,40 @@ export class PackageSyncerService extends AbstractService {
const existsVersionCount = Object.keys(existsVersionMap).length;
const abbreviatedVersionMap = abbreviatedManifests?.versions ?? {};
// 2. save versions
const versions = Object.values<any>(versionMap);
logs.push(`[${isoNow()}] 🚧 Syncing versions ${existsVersionCount} => ${versions.length}`);
if (specificVersions && !this.config.cnpmcore.strictSyncSpecivicVersion && !specificVersions.includes(distTags.latest)) {
logs.push(`[${isoNow()}] 📦 Add latest tag version "${fullname}: ${distTags.latest}"`);
specificVersions.push(distTags.latest);
}
const versions = specificVersions ?
Object.values<PackageJSONType>(versionMap).filter(verItem => specificVersions.includes(verItem.version)) :
Object.values<PackageJSONType>(versionMap);
// 全量同步时跳过排序
const sortedAvailableVersions = specificVersions ?
versions.map(item => item.version).sort(semver.rcompare) : [];
// 在strictSyncSpecivicVersion模式下不同步latest且所有传入的version均不可用
if (specificVersions && sortedAvailableVersions.length === 0) {
logs.push(`[${isoNow()}] ❌ `);
task.error = 'There is no available specific versions, stop task.';
logs.push(`[${isoNow()}] ${task.error}, log: ${logUrl}`);
logs.push(`[${isoNow()}] ❌❌❌❌❌ ${fullname} ❌❌❌❌❌`);
await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n'));
this.logger.info('[PackageSyncerService.executeTask:fail-empty-list] taskId: %s, targetName: %s, %s',
task.taskId, task.targetName, task.error);
return;
}
if (specificVersions) {
// specific versions may not in manifest.
const notAvailableVersionList = specificVersions.filter(i => !sortedAvailableVersions.includes(i));
logs.push(`[${isoNow()}] 🚧 Syncing specific versions: ${sortedAvailableVersions.join(' | ')}`);
if (notAvailableVersionList.length > 0) {
logs.push(`🚧 Some specific versions are not available: 👉 ${notAvailableVersionList.join(' | ')} 👈`);
}
} else {
logs.push(`[${isoNow()}] 🚧 Syncing versions ${existsVersionCount} => ${versions.length}`);
}
const updateVersions: string[] = [];
const differentMetas: any[] = [];
const differentMetas: [PackageJSONType, Partial<PackageJSONType>][] = [];
let syncIndex = 0;
for (const item of versions) {
const version: string = item.version;
@@ -581,10 +625,15 @@ export class PackageSyncerService extends AbstractService {
// check metaDataKeys, if different value, override exists one
// https://github.com/cnpm/cnpmjs.org/issues/1667
// need libc field https://github.com/cnpm/cnpmcore/issues/187
// fix _npmUser field since https://github.com/cnpm/cnpmcore/issues/553
const metaDataKeys = [
'peerDependenciesMeta', 'os', 'cpu', 'libc', 'workspaces', 'hasInstallScript', 'deprecated',
'peerDependenciesMeta', 'os', 'cpu', 'libc', 'workspaces', 'hasInstallScript',
'deprecated', '_npmUser', 'funding',
// https://github.com/cnpm/cnpmcore/issues/689
'acceptDependencies',
];
let diffMeta: any;
const ignoreInAbbreviated = [ '_npmUser' ];
const diffMeta: Partial<PackageJSONType> = {};
for (const key of metaDataKeys) {
let remoteItemValue = item[key];
// make sure hasInstallScript exists
@@ -593,34 +642,30 @@ export class PackageSyncerService extends AbstractService {
remoteItemValue = true;
}
}
const remoteItemDiffValue = JSON.stringify(remoteItemValue);
if (remoteItemDiffValue !== JSON.stringify(existsItem[key])) {
if (!diffMeta) diffMeta = {};
if (!isEqual(remoteItemValue, existsItem[key])) {
diffMeta[key] = remoteItemValue;
} else if (existsAbbreviatedItem && remoteItemDiffValue !== JSON.stringify(existsAbbreviatedItem[key])) {
} else if (!ignoreInAbbreviated.includes(key) && existsAbbreviatedItem && !isEqual(remoteItemValue, (existsAbbreviatedItem as Record<string, unknown>)[key])) {
// should diff exists abbreviated item too
if (!diffMeta) diffMeta = {};
diffMeta[key] = remoteItemValue;
}
}
// should delete readme
if (shouldDeleteReadme) {
if (!diffMeta) diffMeta = {};
diffMeta.readme = undefined;
}
if (diffMeta) {
if (!isEmpty(diffMeta)) {
differentMetas.push([ existsItem, diffMeta ]);
}
continue;
}
syncIndex++;
const description: string = item.description;
const description = item.description;
// "dist": {
// "shasum": "943e0ec03df00ebeb6273a5b94b916ba54b47581",
// "tarball": "https://registry.npmjs.org/foo/-/foo-1.0.0.tgz"
// },
const dist = item.dist;
const tarball: string = dist && dist.tarball;
const tarball = dist && dist.tarball;
if (!tarball) {
lastErrorMessage = `missing tarball, dist: ${JSON.stringify(dist)}`;
logs.push(`[${isoNow()}] ❌ [${syncIndex}] Synced version ${version} fail, ${lastErrorMessage}`);
@@ -639,7 +684,11 @@ export class PackageSyncerService extends AbstractService {
localFile = tmpfile;
logs.push(`[${isoNow()}] 🚧 [${syncIndex}] HTTP content-length: ${headers['content-length']}, timing: ${JSON.stringify(timing)} => ${localFile}`);
} catch (err: any) {
this.logger.error('Download tarball %s error: %s', tarball, err);
if (err.name === 'DownloadNotFoundError' || err.name === 'DownloadStatusInvalidError') {
this.logger.warn('Download tarball %s error: %s', tarball, err);
} else {
this.logger.error('Download tarball %s error: %s', tarball, err);
}
lastErrorMessage = `download tarball error: ${err}`;
logs.push(`[${isoNow()}] ❌ [${syncIndex}] Synced version ${version} fail, ${lastErrorMessage}`);
await this.taskService.appendTaskLog(task, logs.join('\n'));
@@ -667,9 +716,10 @@ export class PackageSyncerService extends AbstractService {
};
try {
// 当 version 记录已经存在时,还需要校验一下 pkg.manifests 是否存在
const pkgVersion = await this.packageManagerService.publish(publishCmd, users[0]);
const publisher = users.find(user => user.displayName === item._npmUser?.name) || users[0];
const pkgVersion = await this.packageManagerService.publish(publishCmd, publisher);
updateVersions.push(pkgVersion.version);
logs.push(`[${isoNow()}] 🟢 [${syncIndex}] Synced version ${version} success, packageVersionId: ${pkgVersion.packageVersionId}, db id: ${pkgVersion.id}`);
logs.push(`[${isoNow()}] 🎉 [${syncIndex}] Synced version ${version} success, packageVersionId: ${pkgVersion.packageVersionId}, db id: ${pkgVersion.id}`);
} catch (err: any) {
if (err.name === 'ForbiddenError') {
logs.push(`[${isoNow()}] 🐛 [${syncIndex}] Synced version ${version} already exists, skip publish, try to set in local manifest`);
@@ -787,6 +837,22 @@ export class PackageSyncerService extends AbstractService {
}
}
}
// 3.2 shoud add latest tag
// 在同步 sepcific version 时如果没有同步 latestTag 的版本会出现 latestTag 丢失或指向版本不正确的情况
if (specificVersions && this.config.cnpmcore.strictSyncSpecivicVersion) {
// 不允许自动同步 latest 版本,从已同步版本中选出 latest
let latestStableVersion = semver.maxSatisfying(sortedAvailableVersions, '*');
// 所有版本都不是稳定版本则指向非稳定版本保证 latest 存在
if (!latestStableVersion) {
latestStableVersion = sortedAvailableVersions[0];
}
if (!existsDistTags.latest || semver.rcompare(existsDistTags.latest, latestStableVersion) === 1) {
logs.push(`[${isoNow()}] 🚧 patch latest tag from specific versions 🚧`);
changedTags.push({ action: 'change', tag: 'latest', version: latestStableVersion });
await this.packageManagerService.savePackageTag(pkg, 'latest', latestStableVersion);
}
}
if (changedTags.length > 0) {
logs.push(`[${isoNow()}] 🟢 Synced ${changedTags.length} tags: ${JSON.stringify(changedTags)}`);
}
@@ -814,6 +880,15 @@ export class PackageSyncerService extends AbstractService {
logs.push(`[${isoNow()}] 🟢 Removed ${removedMaintainers.length} maintainers: ${JSON.stringify(removedMaintainers)}`);
}
// 4.2 update package maintainers in dist
// The event is initialized in the repository and distributed after uncork.
// maintainers' information is updated in bulk to ensure consistency.
if (!isEqual(maintainers, existsMaintainers)) {
logs.push(`[${isoNow()}] 🚧 Syncing maintainers to package manifest, from: ${JSON.stringify(maintainers)} to: ${JSON.stringify(existsMaintainers)}`);
await this.packageManagerService.refreshPackageMaintainersToDists(pkg);
logs.push(`[${isoNow()}] 🟢 Syncing maintainers to package manifest done`);
}
// 5. add deps sync task
for (const dependencyName of dependenciesSet) {
const existsTask = await this.taskRepository.findTaskByTargetName(dependencyName, TaskType.SyncPackage, TaskState.Waiting);
@@ -836,9 +911,9 @@ export class PackageSyncerService extends AbstractService {
// clean cache
await this.cacheService.removeCache(fullname);
logs.push(`[${isoNow()}] 🟢 Clean cache`);
logs.push(`[${isoNow()}] 🟢 log: ${logUrl}`);
logs.push(`[${isoNow()}] 🟢🟢🟢🟢🟢 ${url} 🟢🟢🟢🟢🟢`);
logs.push(`[${isoNow()}] 🗑️ Clean cache`);
logs.push(`[${isoNow()}] 📝 Log URL: ${logUrl}`);
logs.push(`[${isoNow()}] 🔗 ${url}`);
task.error = lastErrorMessage;
await this.taskService.finishTask(task, TaskState.Success, logs.join('\n'));
this.logger.info('[PackageSyncerService.executeTask:success] taskId: %s, targetName: %s',

View File

@@ -1,36 +1,58 @@
import fs from 'node:fs/promises';
import { join, dirname, basename } from 'node:path';
import { randomUUID } from 'node:crypto';
import tar from 'tar';
import tar from '@fengmk2/tar';
import {
AccessLevel,
SingletonProto,
Inject,
} from '@eggjs/tegg';
import { ConflictError, ForbiddenError } from 'egg-errors';
import semver from 'semver';
import { AbstractService } from '../../common/AbstractService';
import {
calculateIntegrity,
getFullname,
} from '../../common/PackageUtil';
import { createTempDir, mimeLookup } from '../../common/FileUtil';
import {
PackageRepository,
} from '../../repository/PackageRepository';
import { PackageVersionFileRepository } from '../../repository/PackageVersionFileRepository';
import { PackageVersionRepository } from '../../repository/PackageVersionRepository';
import { DistRepository } from '../../repository/DistRepository';
import { PackageVersionFile } from '../entity/PackageVersionFile';
import { PackageVersion } from '../entity/PackageVersion';
import { Package } from '../entity/Package';
import { PackageManagerService } from './PackageManagerService';
import { CacheAdapter } from '../../common/adapter/CacheAdapter';
const unpkgWhiteListUrl = 'https://github.com/cnpm/unpkg-white-list';
const CHECK_TIMEOUT = process.env.NODE_ENV === 'test' ? 1 : 60000;
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
})
export class PackageVersionFileService extends AbstractService {
@Inject()
private readonly packageVersionRepository: PackageVersionRepository;
@Inject()
private readonly packageRepository: PackageRepository;
@Inject()
private readonly packageVersionFileRepository: PackageVersionFileRepository;
@Inject()
private readonly distRepository: DistRepository;
@Inject()
private readonly packageManagerService: PackageManagerService;
@Inject()
private readonly cacheAdapter: CacheAdapter;
#unpkgWhiteListCheckTime: number = 0;
#unpkgWhiteListCurrentVersion: string = '';
#unpkgWhiteListAllowPackages: Record<string, {
version: string;
}> = {};
#unpkgWhiteListAllowScopes: string[] = [];
async listPackageVersionFiles(pkgVersion: PackageVersion, directory: string) {
await this.#ensurePackageVersionFilesSync(pkgVersion);
@@ -47,18 +69,131 @@ export class PackageVersionFileService extends AbstractService {
async #ensurePackageVersionFilesSync(pkgVersion: PackageVersion) {
const hasFiles = await this.packageVersionFileRepository.hasPackageVersionFiles(pkgVersion.packageVersionId);
if (!hasFiles) {
await this.syncPackageVersionFiles(pkgVersion);
const lockName = `${pkgVersion.packageVersionId}:syncFiles`;
const lockRes = await this.cacheAdapter.usingLock(lockName, 60, async () => {
await this.syncPackageVersionFiles(pkgVersion);
});
// lock fail
if (!lockRes) {
this.logger.warn('[package:version:syncPackageVersionFiles] check lock:%s fail', lockName);
throw new ConflictError('Package version file sync is currently in progress. Please try again later.');
}
}
}
async #updateUnpkgWhiteList() {
if (!this.config.cnpmcore.enableSyncUnpkgFilesWhiteList) return;
if (Date.now() - this.#unpkgWhiteListCheckTime <= CHECK_TIMEOUT) {
// check update every 60s
return;
}
this.#unpkgWhiteListCheckTime = Date.now();
const whiteListScope = '';
const whiteListPackageName = 'unpkg-white-list';
const whiteListPackageVersion = await this.packageVersionRepository.findVersionByTag(
whiteListScope, whiteListPackageName, 'latest');
if (!whiteListPackageVersion) return;
// same version, skip update for performance
if (this.#unpkgWhiteListCurrentVersion === whiteListPackageVersion) return;
// update the new version white list
const { manifest } = await this.packageManagerService.showPackageVersionManifest(
whiteListScope, whiteListPackageName, whiteListPackageVersion, false, true);
if (!manifest) return;
this.#unpkgWhiteListCurrentVersion = manifest.version;
this.#unpkgWhiteListAllowPackages = manifest.allowPackages ?? {} as any;
this.#unpkgWhiteListAllowScopes = manifest.allowScopes ?? [] as any;
this.logger.info('[PackageVersionFileService.updateUnpkgWhiteList] version:%s, total %s packages, %s scopes',
whiteListPackageVersion,
Object.keys(this.#unpkgWhiteListAllowPackages).length,
this.#unpkgWhiteListAllowScopes.length,
);
}
async checkPackageVersionInUnpkgWhiteList(pkgScope: string, pkgName: string, pkgVersion: string) {
if (!this.config.cnpmcore.enableSyncUnpkgFilesWhiteList) return;
await this.#updateUnpkgWhiteList();
// check allow scopes
if (this.#unpkgWhiteListAllowScopes.includes(pkgScope)) return;
// check allow packages
const fullname = getFullname(pkgScope, pkgName);
const pkgConfig = this.#unpkgWhiteListAllowPackages[fullname];
if (!pkgConfig?.version) {
throw new ForbiddenError(`"${fullname}" is not allow to unpkg files, see ${unpkgWhiteListUrl}`);
}
// satisfies 默认不会包含 prerelease 版本
// https://docs.npmjs.com/about-semantic-versioning#using-semantic-versioning-to-specify-update-types-your-package-can-accept
// [x, *] 代表任意版本,这里统一通过 semver 来判断
if (!semver.satisfies(pkgVersion, pkgConfig.version, { includePrerelease: true })) {
throw new ForbiddenError(`"${fullname}@${pkgVersion}" not satisfies "${pkgConfig.version}" to unpkg files, see ${unpkgWhiteListUrl}`);
}
}
// 基于 latest version 同步 package readme
async syncPackageReadme(pkg: Package, latestPkgVersion: PackageVersion) {
const dirname = `unpkg_${pkg.fullname.replace('/', '_')}@${latestPkgVersion.version}_latest_readme_${randomUUID()}`;
const tmpdir = await createTempDir(this.config.dataDir, dirname);
const tarFile = `${tmpdir}.tgz`;
const readmeFilenames: string[] = [];
try {
this.logger.info('[PackageVersionFileService.syncPackageReadme:download-start] dist:%s(path:%s, size:%s) => tarFile:%s',
latestPkgVersion.tarDist.distId, latestPkgVersion.tarDist.path, latestPkgVersion.tarDist.size, tarFile);
await this.distRepository.downloadDistToFile(latestPkgVersion.tarDist, tarFile);
this.logger.info('[PackageVersionFileService.syncPackageReadme:extract-start] tmpdir:%s', tmpdir);
await tar.extract({
file: tarFile,
cwd: tmpdir,
strip: 1,
onentry: entry => {
const filename = this.#formatTarEntryFilename(entry);
if (!filename) return;
if (this.#matchReadmeFilename(filename)) {
readmeFilenames.push(filename);
}
},
});
if (readmeFilenames.length > 0) {
const readmeFilename = this.#preferMarkdownReadme(readmeFilenames);
const readmeFile = join(tmpdir, readmeFilename);
await this.packageManagerService.savePackageReadme(pkg, readmeFile);
}
} catch (err) {
this.logger.warn('[PackageVersionFileService.syncPackageReadme:error] packageVersionId: %s, readmeFilenames: %j, tmpdir: %s, error: %s',
latestPkgVersion.packageVersionId, readmeFilenames, tmpdir, err);
// ignore TAR_BAD_ARCHIVE error
if (err.code === 'TAR_BAD_ARCHIVE') return;
throw err;
} finally {
try {
await fs.rm(tarFile, { force: true });
await fs.rm(tmpdir, { recursive: true, force: true });
} catch (err) {
this.logger.warn('[PackageVersionFileService.syncPackageReadme:warn] remove tmpdir: %s, error: %s',
tmpdir, err);
}
}
}
async syncPackageVersionFiles(pkgVersion: PackageVersion) {
const files: PackageVersionFile[] = [];
// must set enableUnpkg and enableSyncUnpkgFiles = true both
if (!this.config.cnpmcore.enableUnpkg) return files;
if (!this.config.cnpmcore.enableSyncUnpkgFiles) return files;
const pkg = await this.packageRepository.findPackageByPackageId(pkgVersion.packageId);
if (!pkg) return files;
// check unpkg white list
await this.checkPackageVersionInUnpkgWhiteList(pkg.scope, pkg.name, pkgVersion.version);
const dirname = `unpkg_${pkg.fullname.replace('/', '_')}@${pkgVersion.version}_${randomUUID()}`;
const tmpdir = await createTempDir(this.config.dataDir, dirname);
const tarFile = `${tmpdir}.tgz`;
const paths: string[] = [];
const readmeFilenames: string[] = [];
try {
this.logger.info('[PackageVersionFileService.syncPackageVersionFiles:download-start] dist:%s(path:%s, size:%s) => tarFile:%s',
pkgVersion.tarDist.distId, pkgVersion.tarDist.path, pkgVersion.tarDist.size, tarFile);
@@ -69,11 +204,12 @@ export class PackageVersionFileService extends AbstractService {
cwd: tmpdir,
strip: 1,
onentry: entry => {
if (entry.type !== 'File') return;
if (!entry.path.startsWith('package/')) return;
// ignore hidden dir
if (entry.path.includes('/./')) return;
paths.push(entry.path.replace(/^package\//i, '/'));
const filename = this.#formatTarEntryFilename(entry);
if (!filename) return;
paths.push('/' + filename);
if (this.#matchReadmeFilename(filename)) {
readmeFilenames.push(filename);
}
},
});
for (const path of paths) {
@@ -83,6 +219,11 @@ export class PackageVersionFileService extends AbstractService {
}
this.logger.info('[PackageVersionFileService.syncPackageVersionFiles:success] packageVersionId: %s, %d paths, %d files, tmpdir: %s',
pkgVersion.packageVersionId, paths.length, files.length, tmpdir);
if (readmeFilenames.length > 0) {
const readmeFilename = this.#preferMarkdownReadme(readmeFilenames);
const readmeFile = join(tmpdir, readmeFilename);
await this.packageManagerService.savePackageVersionReadme(pkgVersion, readmeFile);
}
return files;
} catch (err) {
this.logger.warn('[PackageVersionFileService.syncPackageVersionFiles:error] packageVersionId: %s, %d paths, tmpdir: %s, error: %s',
@@ -123,7 +264,7 @@ export class PackageVersionFileService extends AbstractService {
name,
dist,
contentType: mimeLookup(path),
mtime: stat.mtime,
mtime: pkgVersion.publishTime,
});
try {
await this.packageVersionFileRepository.createPackageVersionFile(file);
@@ -143,4 +284,37 @@ export class PackageVersionFileService extends AbstractService {
name: basename(path),
};
}
#formatTarEntryFilename(entry: tar.ReadEntry) {
if (entry.type !== 'File') return;
// ignore hidden dir
if (entry.path.includes('/./')) return;
// https://github.com/cnpm/cnpmcore/issues/452#issuecomment-1570077310
// strip first dir, e.g.: 'package/', 'lodash-es/'
const filename = entry.path.split('/').slice(1).join('/');
return filename;
}
#matchReadmeFilename(filename: string) {
// support README,README.*
// https://github.com/npm/read-package-json/blob/main/lib/read-json.js#L280
return (/^README(\.\w{1,20}|$)/i.test(filename));
}
// https://github.com/npm/read-package-json/blob/main/lib/read-json.js#L280
#preferMarkdownReadme(files: string[]) {
let fallback = 0;
const markdownRE = /\.m?a?r?k?d?o?w?n?$/i;
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (markdownRE.test(file)) {
return file;
} else if (file.toLowerCase() === 'README') {
fallback = i;
}
}
// prefer README.md, followed by README; otherwise, return
// the first filename (which could be README)
return files[fallback];
}
}

View File

@@ -0,0 +1,118 @@
import { AccessLevel, SingletonProto, Inject } from '@eggjs/tegg';
import semver, { Range } from 'semver';
import { Result, AliasResult } from 'npm-package-arg';
import { PackageVersionRepository } from '../../repository/PackageVersionRepository';
import { getScopeAndName } from '../../common/PackageUtil';
import { SqlRange } from '../entity/SqlRange';
import { BugVersionService } from './BugVersionService';
import type { PackageJSONType } from '../../repository/PackageRepository';
import { DistRepository } from '../../repository/DistRepository';
import { BugVersionAdvice } from '../entity/BugVersion';
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
})
export class PackageVersionService {
@Inject()
private packageVersionRepository: PackageVersionRepository;
@Inject()
private readonly bugVersionService: BugVersionService;
@Inject()
private readonly distRepository: DistRepository;
async readManifest(pkgId: string, spec: Result, isFullManifests: boolean, withBugVersion = true): Promise<PackageJSONType | undefined> {
const realSpec = this.findRealSpec(spec);
let version = await this.getVersion(realSpec, false);
if (!version) {
return undefined;
}
let bugVersionAdvice: {
advice: BugVersionAdvice,
version: string,
} | undefined;
if (withBugVersion) {
const bugVersion = await this.bugVersionService.getBugVersion();
if (bugVersion) {
const advice = bugVersion.fixVersion(spec.name!, version);
if (advice) {
bugVersionAdvice = {
advice,
version,
};
version = advice.version;
}
}
}
let manifest;
if (isFullManifests) {
manifest = await this.distRepository.findPackageVersionManifest(pkgId, version);
} else {
manifest = await this.distRepository.findPackageAbbreviatedManifest(pkgId, version);
}
if (manifest && bugVersionAdvice) {
manifest.deprecated = `[WARNING] Use ${bugVersionAdvice.advice.version} instead of ${bugVersionAdvice.version}, reason: ${bugVersionAdvice.advice.reason}`;
manifest.version = bugVersionAdvice.version;
}
return manifest;
}
private findRealSpec(spec: Result) {
let realSpec: Result;
switch (spec.type) {
case 'alias':
realSpec = (spec as AliasResult).subSpec;
break;
case 'version':
case 'tag':
case 'range':
realSpec = spec;
break;
default:
throw new Error(`npmcore not support spec: ${spec.raw}`);
}
return realSpec;
}
async getVersion(spec: Result, withBugVersion = true): Promise<string | undefined | null> {
let version: string | undefined | null;
const [ scope, name ] = getScopeAndName(spec.name!);
// 优先通过 tag 来进行判断
if (spec.type === 'tag') {
version = await this.packageVersionRepository.findVersionByTag(scope, name, spec.fetchSpec!);
} else if (spec.type === 'version') {
// 1.0.0
// '=1.0.0' => '1.0.0'
// https://github.com/npm/npm-package-arg/blob/main/lib/npa.js#L392
version = semver.valid(spec.fetchSpec!, true);
} else if (spec.type === 'range') {
// a@1.1 情况下1.1 会解析为 range如果有对应的 distTag 时会失效
// 这里需要进行兼容
// 仅当 spec 不为 version 时才查询,减少请求次数
const versionMatchTag = await this.packageVersionRepository.findVersionByTag(scope, name, spec.fetchSpec!);
if (versionMatchTag) {
version = versionMatchTag;
} else {
const range = new Range(spec.fetchSpec!);
const paddingSemVer = new SqlRange(range);
if (paddingSemVer.containPreRelease) {
const versions = await this.packageVersionRepository.findSatisfyVersionsWithPrerelease(scope, name, paddingSemVer);
version = semver.maxSatisfying(versions, range);
} else {
version = await this.packageVersionRepository.findMaxSatisfyVersion(scope, name, paddingSemVer);
}
}
}
if (version && withBugVersion) {
const bugVersion = await this.bugVersionService.getBugVersion();
if (bugVersion) {
const advice = bugVersion.fixVersion(spec.name!, version);
if (advice) {
version = advice.version;
}
}
}
return version;
}
}

View File

@@ -11,13 +11,13 @@ import { PageOptions, PageResult } from '../util/EntityUtil';
import { ScopeManagerService } from './ScopeManagerService';
import { TaskService } from './TaskService';
import { Task } from '../entity/Task';
import { PresetRegistryName } from '../../common/constants';
import { ChangesStreamMode, PresetRegistryName } from '../../common/constants';
import { RegistryType } from '../../common/enum/Registry';
export interface CreateRegistryCmd extends Pick<Registry, 'changeStream' | 'host' | 'userPrefix' | 'type' | 'name'> {
export interface CreateRegistryCmd extends Pick<Registry, 'changeStream' | 'host' | 'userPrefix' | 'type' | 'name' | 'authToken' > {
operatorId?: string;
}
export interface UpdateRegistryCmd extends Pick<Registry, 'changeStream' | 'host' | 'userPrefix' | 'type' | 'name' | 'registryId'> {
export interface UpdateRegistryCmd extends Pick<Registry, 'changeStream' | 'host' | 'type' | 'name' | 'authToken' > {
operatorId?: string;
}
export interface RemoveRegistryCmd extends Pick<Registry, 'registryId'> {
@@ -61,7 +61,7 @@ export class RegistryManagerService extends AbstractService {
}
async createRegistry(createCmd: CreateRegistryCmd): Promise<Registry> {
const { name, changeStream = '', host, userPrefix = '', type, operatorId = '-' } = createCmd;
const { name, changeStream = '', host, userPrefix = '', type, operatorId = '-', authToken } = createCmd;
this.logger.info('[RegistryManagerService.createRegistry:prepare] operatorId: %s, createCmd: %j', operatorId, createCmd);
const registry = Registry.create({
name,
@@ -69,6 +69,7 @@ export class RegistryManagerService extends AbstractService {
host,
userPrefix,
type,
authToken,
});
await this.registryRepository.saveRegistry(registry);
return registry;
@@ -76,8 +77,8 @@ export class RegistryManagerService extends AbstractService {
// 更新部分 registry 信息
// 不允许 userPrefix 字段变更
async updateRegistry(updateCmd: UpdateRegistryCmd) {
const { name, changeStream, host, type, registryId, operatorId = '-' } = updateCmd;
async updateRegistry(registryId: string, updateCmd: UpdateRegistryCmd) {
const { name, changeStream, host, type, operatorId = '-', authToken } = updateCmd;
this.logger.info('[RegistryManagerService.updateRegistry:prepare] operatorId: %s, updateCmd: %j', operatorId, updateCmd);
const registry = await this.registryRepository.findRegistryByRegistryId(registryId);
if (!registry) {
@@ -88,6 +89,7 @@ export class RegistryManagerService extends AbstractService {
changeStream,
host,
type,
authToken,
});
await this.registryRepository.saveRegistry(registry);
}
@@ -105,6 +107,10 @@ export class RegistryManagerService extends AbstractService {
return await this.registryRepository.findRegistry(registryName);
}
async findByRegistryHost(host?: string): Promise<Registry | null> {
return host ? await this.registryRepository.findRegistryByRegistryHost(host) : null;
}
// 删除 Registry 方法
// 可选传入 operatorId 作为参数,用于记录操作人员
// 同时删除对应的 scope 数据
@@ -143,7 +149,7 @@ export class RegistryManagerService extends AbstractService {
// 从配置文件默认生成
const { changesStreamRegistryMode, changesStreamRegistry: changesStreamHost, sourceRegistry: host } = this.config.cnpmcore;
const type = changesStreamRegistryMode === 'json' ? RegistryType.Cnpmcore : RegistryType.Npm;
const type = changesStreamRegistryMode === ChangesStreamMode.json ? RegistryType.Cnpmcore : RegistryType.Npm;
const registry = await this.createRegistry({
name: PresetRegistryName.default,
type,
@@ -156,4 +162,12 @@ export class RegistryManagerService extends AbstractService {
}
async getAuthTokenByRegistryHost(host: string): Promise<string|undefined> {
const registry = await this.findByRegistryHost(host);
if (!registry) {
return undefined;
}
return registry.authToken;
}
}

View File

@@ -7,7 +7,7 @@ import { NFSAdapter } from '../../common/adapter/NFSAdapter';
import { TaskState, TaskType } from '../../common/enum/Task';
import { AbstractService } from '../../common/AbstractService';
import { TaskRepository } from '../../repository/TaskRepository';
import { Task } from '../entity/Task';
import { Task, CreateSyncPackageTaskData } from '../entity/Task';
import { QueueAdapter } from '../../common/typing';
@SingletonProto({
@@ -27,10 +27,27 @@ export class TaskService extends AbstractService {
public async createTask(task: Task, addTaskQueueOnExists: boolean) {
const existsTask = await this.taskRepository.findTaskByTargetName(task.targetName, task.type);
if (existsTask) {
// 如果任务还未被触发,就不继续重复创建
// 只在包同步场景下做任务合并,其余场景通过 bizId 来进行任务幂等
if (existsTask && Task.needMergeWhenWaiting(task.type)) {
// 在包同步场景,如果任务还未被触发,就不继续重复创建
// 如果任务正在执行,可能任务状态已更新,这种情况需要继续创建
if (existsTask.state === TaskState.Waiting) {
if (task.type === TaskType.SyncPackage) {
// 如果是specificVersions的任务则可能可以和存量任务进行合并
const specificVersions = (task as Task<CreateSyncPackageTaskData>).data?.specificVersions;
const existsTaskSpecificVersions = (existsTask as Task<CreateSyncPackageTaskData>).data?.specificVersions;
if (existsTaskSpecificVersions) {
if (specificVersions) {
// 存量的任务和新增任务都是同步指定版本的任务,合并两者版本至存量任务
await this.taskRepository.updateSpecificVersionsOfWaitingTask(existsTask, specificVersions);
} else {
// 新增任务是全量同步任务,移除存量任务中的指定版本使其成为全量同步任务
await this.taskRepository.updateSpecificVersionsOfWaitingTask(existsTask);
}
}
// 存量任务是全量同步任务,直接提高任务优先级
}
// 提高任务的优先级
if (addTaskQueueOnExists) {
const queueLength = await this.getTaskQueueLength(task.type);

View File

@@ -13,6 +13,8 @@ import { ModelConvertor } from '../../../app/repository/util/ModelConvertor';
import { Package as PackageEntity } from '../entity/Package';
import { ForbiddenError, UnauthorizedError } from 'egg-errors';
import { getScopeAndName } from '../../../app/common/PackageUtil';
import { sha512 } from '../../../app/common/UserUtil';
import { UserRepository } from '../../../app/repository/UserRepository';
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
@@ -22,6 +24,8 @@ export class TokenService extends AbstractService {
private readonly TokenPackage: typeof TokenPackageModel;
@Inject()
private readonly Package: typeof PackageModel;
@Inject()
private readonly userRepository: UserRepository;
public async listTokenPackages(token: Token) {
if (isGranularToken(token)) {
@@ -32,17 +36,17 @@ export class TokenService extends AbstractService {
return null;
}
public async checkGranularTokenAccess(token: Token, fullname: string) {
// skip classic token
if (!isGranularToken(token)) {
return true;
}
public async checkTokenStatus(token: Token) {
// check for expires
if (dayjs(token.expiredAt).isBefore(new Date())) {
if (isGranularToken(token) && dayjs(token.expiredAt).isBefore(new Date())) {
throw new UnauthorizedError('Token expired');
}
token.lastUsedAt = new Date();
this.userRepository.saveToken(token);
}
public async checkGranularTokenAccess(token: Token, fullname: string) {
// check for scope whitelist
const [ scope, name ] = getScopeAndName(fullname);
// check for packages whitelist
@@ -67,4 +71,14 @@ export class TokenService extends AbstractService {
}
async getUserAndToken(authorization: string) {
if (!authorization) return null;
const matchs = /^Bearer ([\w\.]+?)$/.exec(authorization);
if (!matchs) return null;
const tokenValue = matchs[1];
const tokenKey = sha512(tokenValue);
const authorizedUserAndToken = await this.userRepository.findUserAndTokenByTokenKey(tokenKey);
return authorizedUserAndToken;
}
}

View File

@@ -12,6 +12,9 @@ import { WebauthnCredential as WebauthnCredentialEntity } from '../entity/Webaut
import { LoginResultCode } from '../../common/enum/User';
import { integrity, checkIntegrity, randomToken, sha512 } from '../../common/UserUtil';
import { AbstractService } from '../../common/AbstractService';
import { RegistryManagerService } from './RegistryManagerService';
import { getPrefixedName } from '../../common/PackageUtil';
import { Registry } from '../entity/Registry';
type Optional<T, K extends keyof T> = Omit < T, K > & Partial<T> ;
@@ -59,12 +62,36 @@ type CreateWebauthnCredentialOptions = {
export class UserService extends AbstractService {
@Inject()
private readonly userRepository: UserRepository;
@Inject()
private readonly registryManagerService: RegistryManagerService;
checkPassword(user: UserEntity, password: string): boolean {
const plain = `${user.passwordSalt}${password}`;
return checkIntegrity(plain, user.passwordIntegrity);
}
async findUserByNameOrDisplayName(name: string) {
const hasPrefix = name.includes(':');
if (hasPrefix) {
return await this.findUserByName(name);
}
const selfRegistry = await this.registryManagerService.ensureSelfRegistry();
const selfUser = await this.findUserByName(getPrefixedName(selfRegistry.userPrefix, name));
if (selfUser) {
return selfUser;
}
const defaultRegistry = await this.registryManagerService.ensureDefaultRegistry();
const defaultUser = await this.findUserByName(getPrefixedName(defaultRegistry.userPrefix, name));
return defaultUser;
}
async findInRegistry(registry:Registry, name: string): Promise<UserEntity | null> {
return await this.findUserByName(getPrefixedName(registry.userPrefix, name));
}
async findUserByName(name: string): Promise<UserEntity | null> {
return await this.userRepository.findUserByName(name);
}
@@ -79,19 +106,23 @@ export class UserService extends AbstractService {
return { code: LoginResultCode.Success, user, token };
}
async ensureTokenByUser({ name, email, password = crypto.randomUUID(), ip }: Optional<CreateUser, 'password'>) {
async findOrCreateUser({ name, email, ip, password = crypto.randomUUID() }: Optional<CreateUser, 'password'>) {
let user = await this.userRepository.findUserByName(name);
if (!user) {
const createRes = await this.create({
name,
email,
// Authentication via sso
// should use token instead of password
password,
ip,
});
user = createRes.user;
}
return user;
}
async ensureTokenByUser(opts: Optional<CreateUser, 'password'>) {
const user = await this.findOrCreateUser(opts);
const token = await this.createToken(user.userId);
return { user, token };
}
@@ -172,14 +203,14 @@ export class UserService extends AbstractService {
await this.userRepository.removeToken(token.tokenId);
}
async findWebauthnCredential(userId: string, browserType?: string) {
async findWebauthnCredential(userId: string, browserType: string | undefined | null) {
const credential = await this.userRepository.findCredentialByUserIdAndBrowserType(userId, browserType || null);
return credential;
}
async createWebauthnCredential(userId: string, options: CreateWebauthnCredentialOptions) {
async createWebauthnCredential(userId: string | undefined, options: CreateWebauthnCredentialOptions) {
const credentialEntity = WebauthnCredentialEntity.create({
userId,
userId: userId as string,
credentialId: options.credentialId,
publicKey: options.publicKey,
browserType: options.browserType,
@@ -188,10 +219,11 @@ export class UserService extends AbstractService {
return credentialEntity;
}
async removeWebauthnCredential(userId: string, browserType?: string) {
async removeWebauthnCredential(userId?: string, browserType?: string) {
const credential = await this.userRepository.findCredentialByUserIdAndBrowserType(userId, browserType || null);
if (credential) {
await this.userRepository.removeCredential(credential.wancId);
}
}
}

View File

@@ -1,10 +1,8 @@
import {
AccessLevel,
EggObjectLifecycle,
LifecycleInit,
Inject,
SingletonProto,
EggQualifier,
EggType,
} from '@eggjs/tegg';
import { EggAppConfig, EggLogger } from 'egg';
import FSClient from 'fs-cnpm';
@@ -15,9 +13,8 @@ import { Readable } from 'stream';
name: 'nfsClient',
accessLevel: AccessLevel.PUBLIC,
})
export class NFSClientAdapter implements EggObjectLifecycle, NFSClient {
export class NFSClientAdapter implements NFSClient {
@Inject()
@EggQualifier(EggType.APP)
private logger: EggLogger;
@Inject()
@@ -31,7 +28,8 @@ export class NFSClientAdapter implements EggObjectLifecycle, NFSClient {
url?(key: string): string;
async init() {
@LifecycleInit()
protected async init() {
// NFS interface https://github.com/cnpm/cnpmjs.org/wiki/NFS-Guide
if (this.config.nfs.client) {
this._client = this.config.nfs.client;

View File

@@ -0,0 +1,52 @@
import {
AccessLevel,
Inject,
SingletonProto,
} from '@eggjs/tegg';
import { EggAppConfig } from 'egg';
import { Client as ElasticsearchClient, estypes } from '@elastic/elasticsearch';
import { SearchAdapter } from '../common/typing';
/**
* Use elasticsearch to search the huge npm packages.
*/
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
name: 'searchAdapter',
})
export class ESSearchAdapter implements SearchAdapter {
@Inject()
private config: EggAppConfig;
@Inject()
private readonly elasticsearch: ElasticsearchClient; // 由 elasticsearch 插件引入
async search<T>(query: any): Promise<estypes.SearchHitsMetadata<T>> {
const { cnpmcore: { elasticsearchIndex: index } } = this.config;
const result = await this.elasticsearch.search<T>({
index,
...query,
});
return result.hits;
}
async upsert<T>(id: string, document: T): Promise<string> {
const { cnpmcore: { elasticsearchIndex: index } } = this.config;
const res = await this.elasticsearch.index({
id,
index,
document,
});
return res._id;
}
async delete(id: string): Promise<string> {
const { cnpmcore: { elasticsearchIndex: index } } = this.config;
const res = await this.elasticsearch.delete({
index,
id,
});
return res._id;
}
}

View File

@@ -1,17 +1,15 @@
import {
AccessLevel,
ContextProto,
Inject,
EggContext,
ContextProto,
} from '@eggjs/tegg';
import { EggAppConfig, EggLogger } from 'egg';
import { UnauthorizedError, ForbiddenError } from 'egg-errors';
import { UserRepository } from '../repository/UserRepository';
import { PackageRepository } from '../repository/PackageRepository';
import { Package as PackageEntity } from '../core/entity/Package';
import { User as UserEntity } from '../core/entity/User';
import { Token as TokenEntity } from '../core/entity/Token';
import { sha512 } from '../common/UserUtil';
import { getScopeAndName } from '../common/PackageUtil';
import { RegistryManagerService } from '../core/service/RegistryManagerService';
import { TokenService } from '../core/service/TokenService';
@@ -24,8 +22,6 @@ export type TokenRole = 'read' | 'publish' | 'setting';
accessLevel: AccessLevel.PRIVATE,
})
export class UserRoleManager {
@Inject()
private readonly userRepository: UserRepository;
@Inject()
private readonly packageRepository: PackageRepository;
@Inject()
@@ -108,20 +104,19 @@ export class UserRoleManager {
user: this.currentAuthorizedUser,
};
}
this.handleAuthorized = true;
const authorization = ctx.get('authorization');
if (!authorization) return null;
const matchs = /^Bearer ([\w\.]+?)$/.exec(authorization);
if (!matchs) return null;
const tokenValue = matchs[1];
const tokenKey = sha512(tokenValue);
const authorizedUserAndToken = await this.userRepository.findUserAndTokenByTokenKey(tokenKey);
if (authorizedUserAndToken) {
this.currentAuthorizedToken = authorizedUserAndToken.token;
this.currentAuthorizedUser = authorizedUserAndToken.user;
ctx.userId = authorizedUserAndToken.user.userId;
const authorizedUserAndToken = await this.tokenService.getUserAndToken(authorization);
if (!authorizedUserAndToken) {
return null;
}
// check token expired & set lastUsedAt
await this.tokenService.checkTokenStatus(authorizedUserAndToken.token);
this.currentAuthorizedToken = authorizedUserAndToken.token;
this.currentAuthorizedUser = authorizedUserAndToken.user;
ctx.userId = authorizedUserAndToken.user.userId;
return authorizedUserAndToken;
}

173
app/port/config.ts Normal file
View File

@@ -0,0 +1,173 @@
import { SyncDeleteMode, SyncMode, ChangesStreamMode } from '../common/constants';
export { cnpmcoreConfig } from '../../config/config.default';
export type CnpmcoreConfig = {
name: string,
/**
* enable hook or not
*/
hookEnable: boolean,
/**
* mac custom hooks count
*/
hooksLimit: number,
/**
* upstream registry url
*/
sourceRegistry: string,
/**
* upstream registry is base on `cnpmcore` or not
* if your upstream is official npm registry, please turn it off
*/
sourceRegistryIsCNpm: boolean,
/**
* sync upstream first
*/
syncUpstreamFirst: boolean,
/**
* sync upstream timeout, default is 3mins
*/
sourceRegistrySyncTimeout: number,
/**
* sync task high water size, default is 100
*/
taskQueueHighWaterSize: number,
/**
* sync mode
* - none: don't sync npm package
* - admin: don't sync npm package,only admin can create sync task by sync contorller.
* - all: sync all npm packages
* - exist: only sync exist packages, effected when `enableCheckRecentlyUpdated` or `enableChangesStream` is enabled
*/
syncMode: SyncMode,
syncDeleteMode: SyncDeleteMode,
syncPackageWorkerMaxConcurrentTasks: number,
triggerHookWorkerMaxConcurrentTasks: number,
createTriggerHookWorkerMaxConcurrentTasks: number,
/**
* stop syncing these packages in future
*/
syncPackageBlockList: string[],
/**
* check recently from https://www.npmjs.com/browse/updated, if use set changesStreamRegistry to cnpmcore,
* maybe you should disable it
*/
enableCheckRecentlyUpdated: boolean,
/**
* mirror binary, default is false
*/
enableSyncBinary: boolean,
/**
* sync binary source api, default is `${sourceRegistry}/-/binary`
*/
syncBinaryFromAPISource: string,
/**
* enable sync downloads data from source registry https://github.com/cnpm/cnpmcore/issues/108
* all three parameters must be configured at the same time to take effect
*/
enableSyncDownloadData: boolean,
syncDownloadDataSourceRegistry: string,
/**
* should be YYYY-MM-DD format
*/
syncDownloadDataMaxDate: string,
/**
* @see https://github.com/npm/registry-follower-tutorial
*/
enableChangesStream: boolean,
checkChangesStreamInterval: number,
changesStreamRegistry: string,
/**
* handle _changes request mode, default is 'streaming', please set it to 'json' when on cnpmcore registry
*/
changesStreamRegistryMode: ChangesStreamMode,
/**
* registry url
*/
registry: string,
/**
* https://docs.npmjs.com/cli/v6/using-npm/config#always-auth npm <= 6
* if `alwaysAuth=true`, all api request required access token
*/
alwaysAuth: boolean,
/**
* white scope list
*/
allowScopes: string [],
/**
* allow publish non-scope package, disable by default
*/
allowPublishNonScopePackage: boolean,
/**
* Public registration is allowed, otherwise only admins can login
*/
allowPublicRegistration: boolean,
/**
* default system admins
*/
admins: Record<string, string>,
/**
* use webauthn for login, https://webauthn.guide/
* only support platform authenticators, browser support: https://webauthn.me/browser-support
*/
enableWebAuthn: boolean,
/**
* http response cache control header
*/
enableCDN: boolean,
/**
* if you are using CDN, can override it
* it meaning cache 300s on CDN server and client side.
*/
cdnCacheControlHeader: string,
/**
* if you are using CDN, can set it to 'Accept, Accept-Encoding'
*/
cdnVaryHeader: string,
/**
* store full package version manifests data to database table(package_version_manifests), default is false
*/
enableStoreFullPackageVersionManifestsToDatabase: boolean,
/**
* only support npm as client and npm >= 7.0.0 allow publish action
*/
enableNpmClientAndVersionCheck: boolean,
/**
* sync when package not found, only effect when syncMode = all/exist
*/
syncNotFound: boolean,
/**
* redirect to source registry when package not found
*/
redirectNotFound: boolean,
/**
* enable unpkg features, https://github.com/cnpm/cnpmcore/issues/452
*/
enableUnpkg: boolean,
/**
* enable sync unpkg files
*/
enableSyncUnpkgFiles: boolean;
/**
* enable sync unpkg files from the white list, https://github.com/cnpm/unpkg-white-list
*/
enableSyncUnpkgFilesWhiteList: boolean;
/**
* enable this would make sync specific version task not append latest version into this task automatically,it would mark the local latest stable version as latest tag.
* in most cases, you should set to false to keep the same behavior as source registry.
*/
strictSyncSpecivicVersion: boolean,
/**
* enable elasticsearch
*/
enableElasticsearch: boolean,
/**
* elasticsearch index. if enableElasticsearch is true, you must set a index to write es doc.
*/
elasticsearchIndex: string,
/**
* strictly enforces/validates manifest and tgz when publish, https://github.com/cnpm/cnpmcore/issues/542
*/
strictValidateTarballPkg?: boolean,
};

View File

@@ -146,25 +146,25 @@ export abstract class AbstractController extends MiddlewareController {
return new UnavailableForLegalReasonsError(`${message}, reason: ${reason}`);
}
protected async getPackageEntityByFullname(fullname: string): Promise<PackageEntity> {
protected async getPackageEntityByFullname(fullname: string, allowSync?: boolean): Promise<PackageEntity> {
const [ scope, name ] = getScopeAndName(fullname);
return await this.getPackageEntity(scope, name);
return await this.getPackageEntity(scope, name, allowSync);
}
// try to get package entity, throw NotFoundError when package not exists
protected async getPackageEntity(scope: string, name: string): Promise<PackageEntity> {
protected async getPackageEntity(scope: string, name: string, allowSync?:boolean): Promise<PackageEntity> {
const packageEntity = await this.packageRepository.findPackage(scope, name);
if (!packageEntity) {
const fullname = getFullname(scope, name);
throw this.createPackageNotFoundErrorWithRedirect(fullname);
throw this.createPackageNotFoundErrorWithRedirect(fullname, undefined, allowSync);
}
return packageEntity;
}
protected async getPackageVersionEntity(pkg: PackageEntity, version: string): Promise<PackageVersionEntity> {
protected async getPackageVersionEntity(pkg: PackageEntity, version: string, allowSync?: boolean): Promise<PackageVersionEntity> {
const packageVersion = await this.packageRepository.findPackageVersion(pkg.packageId, version);
if (!packageVersion) {
throw new NotFoundError(`${pkg.fullname}@${version} not found`);
throw this.createPackageNotFoundErrorWithRedirect(pkg.fullname, version, allowSync);
}
return packageVersion;
}

View File

@@ -28,15 +28,15 @@ export class DownloadController extends AbstractController {
const pkg = await this.packageRepository.findPackage(scope, name);
if (!pkg) throw new NotFoundError(`${fullname} not found`);
const entities = await this.packageVersionDownloadRepository.query(pkg.packageId, startDate.toDate(), endDate.toDate());
const days = {};
const versions = {};
const days: Record<string, number> = {};
const versions: Record<string, { day: string, downloads: number }[]> = {};
for (const entity of entities) {
const yearMonth = String(entity.yearMonth);
const prefix = yearMonth.substring(0, 4) + '-' + yearMonth.substring(4, 6);
for (let i = 1; i <= 31; i++) {
const day = String(i).padStart(2, '0');
const field = `d${day}`;
const counter = entity[field];
const field = `d${day}` as keyof typeof entity;
const counter = entity[field] as number;
if (!counter) continue;
const date = `${prefix}-${day}`;
days[date] = (days[date] || 0) + counter;
@@ -66,14 +66,14 @@ export class DownloadController extends AbstractController {
async showTotalDownloads(@HTTPParam() scope: string, @HTTPParam() range: string) {
const [ startDate, endDate ] = this.checkAndGetRange(range);
const entities = await this.packageVersionDownloadRepository.query(scope, startDate.toDate(), endDate.toDate());
const days = {};
const days: Record<string, number> = {};
for (const entity of entities) {
const yearMonth = String(entity.yearMonth);
const prefix = yearMonth.substring(0, 4) + '-' + yearMonth.substring(4, 6);
for (let i = 1; i <= 31; i++) {
const day = String(i).padStart(2, '0');
const field = `d${day}`;
const counter = entity[field];
const field = `d${day}` as keyof typeof entity;
const counter = entity[field] as number;
if (!counter) continue;
const date = `${prefix}-${day}`;
days[date] = (days[date] || 0) + counter;
@@ -115,4 +115,3 @@ export class DownloadController extends AbstractController {
return [ startDate, endDate ];
}
}

View File

@@ -9,6 +9,7 @@ import {
} from '@eggjs/tegg';
import { AbstractController } from './AbstractController';
import { CacheService, DownloadInfo, UpstreamRegistryInfo } from '../../core/service/CacheService';
import { HomeService } from '../../core/service/HomeService';
const startTime = new Date();
@@ -27,7 +28,7 @@ type LegacyInfo = {
type SiteEnvInfo = {
sync_model: string;
sync_binary: string;
sync_binary: boolean;
instance_start_time: Date;
node_version: string;
app_version: string;
@@ -51,6 +52,9 @@ export class HomeController extends AbstractController {
@Inject()
private readonly cacheService: CacheService;
@Inject()
private readonly homeService: HomeService;
@HTTPMethod({
// GET /
// https://github.com/cnpm/cnpmjs.org/blob/master/docs/registry-api.md#schema
@@ -97,4 +101,23 @@ export class HomeController extends AbstractController {
use: performance.now() - ctx.performanceStarttime!,
};
}
@HTTPMethod({
path: '/*',
method: HTTPMethodEnum.POST,
priority: -Infinity,
})
async miscPost(@Context() ctx: EggContext) {
await this.homeService.misc(ctx.path);
}
@HTTPMethod({
path: '/*',
method: HTTPMethodEnum.GET,
priority: -Infinity,
})
async miscGet(@Context() ctx: EggContext) {
await this.homeService.misc(ctx.path);
}
}

View File

@@ -67,13 +67,13 @@ export class PackageSyncController extends AbstractController {
const params = {
fullname,
remoteAuthToken: data.remoteAuthToken,
tips,
skipDependencies: !!data.skipDependencies,
syncDownloadData: !!data.syncDownloadData,
force: !!data.force,
// only admin allow to sync history version
forceSyncHistory: !!data.forceSyncHistory && isAdmin,
specificVersions: data.specificVersions,
};
ctx.tValidate(SyncPackageTaskRule, params);
const [ scope, name ] = getScopeAndName(params.fullname);
@@ -96,12 +96,12 @@ export class PackageSyncController extends AbstractController {
const task = await this.packageSyncerService.createTask(params.fullname, {
authorIp: ctx.ip,
authorId: authorized?.user.userId,
remoteAuthToken: params.remoteAuthToken,
tips: params.tips,
skipDependencies: params.skipDependencies,
syncDownloadData: params.syncDownloadData,
forceSyncHistory: params.forceSyncHistory,
registryId: registry?.registryId,
specificVersions: params.specificVersions && JSON.parse(params.specificVersions),
});
ctx.logger.info('[PackageSyncController.createSyncTask:success] taskId: %s, fullname: %s',
task.taskId, fullname);

View File

@@ -29,7 +29,7 @@ export class PackageTagController extends AbstractController {
async showTags(@HTTPParam() fullname: string) {
const packageEntity = await this.getPackageEntityByFullname(fullname);
const tagEntities = await this.packageRepository.listPackageTags(packageEntity.packageId);
const tags = {};
const tags: Record<string, string> = {};
for (const entity of tagEntities) {
tags[entity.tag] = entity.version;
}

View File

@@ -19,6 +19,7 @@ import { PackageManagerService } from '../../core/service/PackageManagerService'
import { PackageVersionFile } from '../../core/entity/PackageVersionFile';
import { PackageVersion } from '../../core/entity/PackageVersion';
import { DistRepository } from '../../repository/DistRepository';
import { Spec } from '../typebox';
type FileItem = {
path: string,
@@ -65,85 +66,99 @@ export class PackageVersionFileController extends AbstractController {
}
@HTTPMethod({
// PUT /:fullname/:versionOrTag/files
path: `/:fullname(${FULLNAME_REG_STRING})/:versionOrTag/files`,
// PUT /:fullname/:versionSpec/files
path: `/:fullname(${FULLNAME_REG_STRING})/:versionSpec/files`,
method: HTTPMethodEnum.PUT,
})
@Middleware(AdminAccess)
async sync(@HTTPParam() fullname: string, @HTTPParam() versionOrTag: string) {
async sync(@Context() ctx: EggContext, @HTTPParam() fullname: string, @HTTPParam() versionSpec: string) {
ctx.tValidate(Spec, `${fullname}@${versionSpec}`);
this.#requireUnpkgEnable();
const [ scope, name ] = getScopeAndName(fullname);
const { packageVersion } = await this.packageManagerService.showPackageVersionByVersionOrTag(
scope, name, versionOrTag);
scope, name, versionSpec);
if (!packageVersion) {
throw new NotFoundError(`${fullname}@${versionOrTag} not found`);
throw new NotFoundError(`${fullname}@${versionSpec} not found`);
}
const files = await this.packageVersionFileService.syncPackageVersionFiles(packageVersion);
return files.map(file => formatFileItem(file));
}
@HTTPMethod({
// GET /:fullname/:versionOrTag/files => /:fullname/:versionOrTag/files/${pkg.main}
// GET /:fullname/:versionOrTag/files?meta
// GET /:fullname/:versionOrTag/files/
path: `/:fullname(${FULLNAME_REG_STRING})/:versionOrTag/files`,
// GET /:fullname/:versionSpec/files => /:fullname/:versionSpec/files/${pkg.main}
// GET /:fullname/:versionSpec/files?meta
// GET /:fullname/:versionSpec/files/
path: `/:fullname(${FULLNAME_REG_STRING})/:versionSpec/files`,
method: HTTPMethodEnum.GET,
})
async listFiles(@Context() ctx: EggContext,
@HTTPParam() fullname: string,
@HTTPParam() versionOrTag: string,
@HTTPParam() versionSpec: string,
@HTTPQuery() meta: string) {
this.#requireUnpkgEnable();
ctx.tValidate(Spec, `${fullname}@${versionSpec}`);
ctx.vary(this.config.cnpmcore.cdnVaryHeader);
const [ scope, name ] = getScopeAndName(fullname);
const packageVersion = await this.#getPackageVersion(ctx, fullname, scope, name, versionOrTag);
const packageVersion = await this.#getPackageVersion(ctx, fullname, scope, name, versionSpec);
ctx.set('cache-control', META_CACHE_CONTROL);
const hasMeta = typeof meta === 'string' || ctx.path.endsWith('/files/');
// meta request
if (hasMeta) {
const files = await this.#listFilesByDirectory(packageVersion, '/');
if (!files) {
throw new NotFoundError(`${fullname}@${versionOrTag}/files not found`);
throw new NotFoundError(`${fullname}@${versionSpec}/files not found`);
}
return files;
}
const { manifest } = await this.packageManagerService.showPackageVersionManifest(scope, name, versionOrTag);
const { manifest } = await this.packageManagerService.showPackageVersionManifest(scope, name, versionSpec, false, true);
// GET /foo/1.0.0/files => /foo/1.0.0/files/{main}
const indexFile = manifest?.main ?? 'index.js';
// ignore empty entry exp: @types/node@20.2.5/
const indexFile = manifest?.main || 'index.js';
ctx.redirect(join(ctx.path, indexFile));
}
@HTTPMethod({
// GET /:fullname/:versionOrTag/files/:path
// GET /:fullname/:versionOrTag/files/:path?meta
path: `/:fullname(${FULLNAME_REG_STRING})/:versionOrTag/files/:path(.+)`,
// GET /:fullname/:versionSpec/files/:path
// GET /:fullname/:versionSpec/files/:path?meta
path: `/:fullname(${FULLNAME_REG_STRING})/:versionSpec/files/:path(.+)`,
method: HTTPMethodEnum.GET,
})
async raw(@Context() ctx: EggContext,
@HTTPParam() fullname: string,
@HTTPParam() versionOrTag: string,
@HTTPParam() versionSpec: string,
@HTTPParam() path: string,
@HTTPQuery() meta: string) {
this.#requireUnpkgEnable();
ctx.tValidate(Spec, `${fullname}@${versionSpec}`);
ctx.vary(this.config.cnpmcore.cdnVaryHeader);
const [ scope, name ] = getScopeAndName(fullname);
path = `/${path}`;
const packageVersion = await this.#getPackageVersion(ctx, fullname, scope, name, versionOrTag);
const packageVersion = await this.#getPackageVersion(ctx, fullname, scope, name, versionSpec);
if (path.endsWith('/')) {
const directory = path.substring(0, path.length - 1);
const files = await this.#listFilesByDirectory(packageVersion, directory);
if (!files) {
throw new NotFoundError(`${fullname}@${versionOrTag}/files${directory} not found`);
throw new NotFoundError(`${fullname}@${versionSpec}/files${directory} not found`);
}
ctx.set('cache-control', META_CACHE_CONTROL);
return files;
}
await this.packageVersionFileService.checkPackageVersionInUnpkgWhiteList(scope, name, packageVersion.version);
const file = await this.packageVersionFileService.showPackageVersionFile(packageVersion, path);
if (!file) {
throw new NotFoundError(`File ${fullname}@${versionOrTag}${path} not found`);
}
const hasMeta = typeof meta === 'string';
if (!file) {
const possibleFile = await this.#searchPossibleEntries(packageVersion, path);
if (possibleFile) {
const route = `/${fullname}/${versionSpec}/files${possibleFile.path}${hasMeta ? '?meta' : ''}`;
ctx.redirect(route);
return;
}
throw new NotFoundError(`File ${fullname}@${versionSpec}${path} not found`);
}
if (hasMeta) {
ctx.set('cache-control', META_CACHE_CONTROL);
return formatFileItem(file);
@@ -156,51 +171,64 @@ export class PackageVersionFileController extends AbstractController {
return await this.distRepository.getDistStream(file.dist);
}
async #getPackageVersion(ctx: EggContext, fullname: string, scope: string, name: string, versionOrTag: string) {
/**
* compatibility with unpkg
* 1. try to match alias entry. e.g. accessing `index.js` or `index.json` using /index
* 2. if given path is directory and has `index.js` file, redirect to it. e.g. using `lib` alias to access `lib/index.js` or `lib/index.json`
* @param {PackageVersion} packageVersion packageVersion
* @param {String} path filepath
* @return {Promise<PackageVersionFile | undefined>} return packageVersionFile or null
*/
async #searchPossibleEntries(packageVersion: PackageVersion, path: string) {
const possiblePath = [ `${path}.js`, `${path}.json`, `${path}/index.js`, `${path}/index.json` ];
for (const pathItem of possiblePath) {
const file = await this.packageVersionFileService.showPackageVersionFile(packageVersion, pathItem);
if (file) {
return file;
}
}
}
async #getPackageVersion(ctx: EggContext, fullname: string, scope: string, name: string, versionSpec: string) {
const { blockReason, packageVersion } = await this.packageManagerService.showPackageVersionByVersionOrTag(
scope, name, versionOrTag);
scope, name, versionSpec);
if (blockReason) {
this.setCDNHeaders(ctx);
throw this.createPackageBlockError(blockReason, fullname, versionOrTag);
throw this.createPackageBlockError(blockReason, fullname, versionSpec);
}
if (!packageVersion) {
throw new NotFoundError(`${fullname}@${versionOrTag} not found`);
throw new NotFoundError(`${fullname}@${versionSpec} not found`);
}
if (packageVersion.version !== versionOrTag) {
if (packageVersion.version !== versionSpec) {
ctx.set('cache-control', META_CACHE_CONTROL);
const location = ctx.url.replace(`/${fullname}/${versionOrTag}/files`, `/${fullname}/${packageVersion.version}/files`);
let location = ctx.url.replace(`/${fullname}/${versionSpec}/files`, `/${fullname}/${packageVersion.version}/files`);
location = location.replace(`/${fullname}/${encodeURIComponent(versionSpec)}/files`, `/${fullname}/${packageVersion.version}/files`);
throw this.createControllerRedirectError(location);
}
return packageVersion;
}
async #listFilesByDirectory(packageVersion: PackageVersion, directory: string) {
const files = await this.packageVersionFileService.listPackageVersionFiles(packageVersion, directory);
if (!files || files.length === 0) return null;
// convert files to directory and file
const directories = new Map<string, DirectoryItem>();
const { files, directories } = await this.packageVersionFileService.listPackageVersionFiles(packageVersion, directory);
if (files.length === 0 && directories.length === 0) return null;
const info: DirectoryItem = {
path: directory,
type: 'directory',
files: [],
};
for (const file of files) {
// make sure parent directories exists
const splits = file.directory.split('/');
for (const [ index, name ] of splits.entries()) {
const parentPath = index === 0 ? '' : `/${splits.slice(1, index).join('/')}`;
const directoryPath = parentPath !== '/' ? `${parentPath}/${name}` : `/${name}`;
let directoryItem = directories.get(directoryPath);
if (!directoryItem) {
directoryItem = {
path: directoryPath,
type: 'directory',
files: [],
};
directories.set(directoryPath, directoryItem);
if (parentPath) {
// only set the first time
directories.get(parentPath!)!.files.push(directoryItem);
}
}
}
directories.get(file.directory)!.files.push(formatFileItem(file));
info.files.push(formatFileItem(file));
}
return directories.get(directory);
for (const name of directories) {
info.files.push({
path: name,
type: 'directory',
files: [],
} as DirectoryItem);
}
return info;
}
}

View File

@@ -13,10 +13,10 @@ import {
import { NotFoundError } from 'egg-errors';
import { AbstractController } from './AbstractController';
import { Static } from 'egg-typebox-validate/typebox';
import { RegistryManagerService } from '../../core/service/RegistryManagerService';
import { RegistryManagerService, UpdateRegistryCmd } from '../../core/service/RegistryManagerService';
import { AdminAccess } from '../middleware/AdminAccess';
import { ScopeManagerService } from '../../core/service/ScopeManagerService';
import { RegistryCreateOptions, QueryPageOptions, RegistryCreateSyncOptions } from '../typebox';
import { RegistryCreateOptions, QueryPageOptions, RegistryCreateSyncOptions, RegistryUpdateOptions } from '../typebox';
@HTTPController()
export class RegistryController extends AbstractController {
@@ -67,7 +67,7 @@ export class RegistryController extends AbstractController {
async createRegistry(@Context() ctx: EggContext, @HTTPBody() registryOptions: Static<typeof RegistryCreateOptions>) {
ctx.tValidate(RegistryCreateOptions, registryOptions);
const authorizedUser = await this.userRoleManager.requiredAuthorizedUser(ctx, 'setting');
const { name, changeStream, host, userPrefix = '', type } = registryOptions;
const { name, changeStream, host, userPrefix = '', type, authToken } = registryOptions;
await this.registryManagerService.createRegistry({
name,
changeStream,
@@ -75,6 +75,7 @@ export class RegistryController extends AbstractController {
userPrefix,
operatorId: authorizedUser.userId,
type,
authToken,
});
return { ok: true };
}
@@ -106,4 +107,29 @@ export class RegistryController extends AbstractController {
await this.registryManagerService.remove({ registryId: id, operatorId: authorizedUser.userId });
return { ok: true };
}
@HTTPMethod({
path: '/-/registry/:id',
method: HTTPMethodEnum.PATCH,
})
@Middleware(AdminAccess)
async updateRegistry(@Context() ctx: EggContext, @HTTPParam() id: string, @HTTPBody() updateRegistryOptions: Partial<UpdateRegistryCmd>) {
ctx.tValidate(RegistryUpdateOptions, updateRegistryOptions);
const registry = await this.registryManagerService.findByRegistryId(id);
if (!registry) {
throw new NotFoundError('registry not found');
} else {
const { name, changeStream, host, type, authToken } = registry;
const _updateRegistryOptions = {
name,
changeStream,
host,
type,
authToken,
...updateRegistryOptions,
};
await this.registryManagerService.updateRegistry(registry.registryId, _updateRegistryOptions);
}
return { ok: true };
}
}

View File

@@ -13,8 +13,6 @@ import {
import { Static, Type } from '@sinclair/typebox';
import { AbstractController } from './AbstractController';
import { TokenType, isGranularToken } from '../../core/entity/Token';
import { TokenService } from '../../../app/core/service/TokenService';
import { getFullname } from '../../../app/common/PackageUtil';
// Creating and viewing access tokens
// https://docs.npmjs.com/creating-and-viewing-access-tokens#viewing-access-tokens
@@ -44,8 +42,6 @@ type GranularTokenOptions = Static<typeof GranularTokenOptionsRule>;
export class TokenController extends AbstractController {
@Inject()
private readonly authAdapter: AuthAdapter;
@Inject()
private readonly tokenService: TokenService;
// https://github.com/npm/npm-profile/blob/main/lib/index.js#L233
@HTTPMethod({
path: '/-/npm/v1/tokens',
@@ -127,6 +123,7 @@ export class TokenController extends AbstractController {
readonly: token.isReadonly,
automation: token.isAutomation,
created: token.createdAt,
lastUsedAt: token.lastUsedAt,
updated: token.updatedAt,
};
});
@@ -134,15 +131,12 @@ export class TokenController extends AbstractController {
return { objects, total: objects.length, urls: {} };
}
private async ensureWebUser() {
private async ensureWebUser(ip = '') {
const userRes = await this.authAdapter.ensureCurrentUser();
if (!userRes?.name || !userRes?.email) {
throw new ForbiddenError('need login first');
}
const user = await this.userService.findUserByName(userRes.name);
if (!user?.userId) {
throw new ForbiddenError('invalid user info');
}
const user = await this.userService.findOrCreateUser({ name: userRes.name, email: userRes.email, ip });
return user;
}
@@ -158,7 +152,7 @@ export class TokenController extends AbstractController {
// 3. Need to implement ensureCurrentUser method in AuthAdapter, or pass in this.user
async createGranularToken(@Context() ctx: EggContext, @HTTPBody() tokenOptions: GranularTokenOptions) {
ctx.tValidate(GranularTokenOptionsRule, tokenOptions);
const user = await this.ensureWebUser();
const user = await this.ensureWebUser(ctx.ip);
// 生成 Token
const { name, description, allowedPackages, allowedScopes, cidr_whitelist, automation, readonly, expires } = tokenOptions;
@@ -197,19 +191,14 @@ export class TokenController extends AbstractController {
const tokens = await this.userRepository.listTokens(user.userId);
const granularTokens = tokens.filter(token => isGranularToken(token));
for (const token of granularTokens) {
const packages = await this.tokenService.listTokenPackages(token);
if (Array.isArray(packages)) {
token.allowedPackages = packages.map(p => getFullname(p.scope, p.name));
}
}
const objects = granularTokens.map(token => {
const { name, description, expiredAt, allowedPackages, allowedScopes } = token;
const { name, description, expiredAt, allowedPackages, allowedScopes, lastUsedAt, type } = token;
return {
name,
description,
allowedPackages,
allowedScopes,
lastUsedAt,
expiredAt,
token: token.tokenMark,
key: token.tokenKey,
@@ -217,6 +206,7 @@ export class TokenController extends AbstractController {
readonly: token.isReadonly,
created: token.createdAt,
updated: token.updatedAt,
type,
};
});
return { objects, total: granularTokens.length, urls: {} };

View File

@@ -17,6 +17,7 @@ import { Static, Type } from '@sinclair/typebox';
import { AbstractController } from './AbstractController';
import { LoginResultCode } from '../../common/enum/User';
import { sha512 } from '../../common/UserUtil';
import { isGranularToken } from '../../core/entity/Token';
// body: {
// _id: 'org.couchdb.user:dddd',
@@ -89,7 +90,7 @@ export class UserController extends AbstractController {
ctx.status = 201;
return {
ok: true,
id: `org.couchdb.user:${result.user?.name}`,
id: `org.couchdb.user:${result.user?.displayName}`,
rev: result.user?.userId,
token: result.token?.token,
};
@@ -112,7 +113,7 @@ export class UserController extends AbstractController {
ctx.status = 201;
return {
ok: true,
id: `org.couchdb.user:${userEntity.name}`,
id: `org.couchdb.user:${userEntity.displayName}`,
rev: userEntity.userId,
token: token.token,
};
@@ -139,14 +140,14 @@ export class UserController extends AbstractController {
method: HTTPMethodEnum.GET,
})
async showUser(@Context() ctx: EggContext, @HTTPParam() username: string) {
const user = await this.userRepository.findUserByName(username);
const user = await this.userService.findUserByNameOrDisplayName(username);
if (!user) {
throw new NotFoundError(`User "${username}" not found`);
}
const authorized = await this.userRoleManager.getAuthorizedUserAndToken(ctx);
return {
_id: `org.couchdb.user:${user.name}`,
name: user.name,
_id: `org.couchdb.user:${user.displayName}`,
name: user.displayName,
email: authorized ? user.email : undefined,
};
}
@@ -157,10 +158,34 @@ export class UserController extends AbstractController {
method: HTTPMethodEnum.GET,
})
async whoami(@Context() ctx: EggContext) {
const authorizedUser = await this.userRoleManager.requiredAuthorizedUser(ctx, 'read');
await this.userRoleManager.requiredAuthorizedUser(ctx, 'read');
const authorizedRes = await this.userRoleManager.getAuthorizedUserAndToken(ctx);
const { token, user } = authorizedRes!;
if (isGranularToken(token)) {
const { name, description, expiredAt, allowedPackages, allowedScopes, lastUsedAt, type } = token;
return {
username: user.displayName,
name,
description,
allowedPackages,
allowedScopes,
lastUsedAt,
expiredAt,
// do not return token value
// token: token.token,
key: token.tokenKey,
cidr_whitelist: token.cidrWhitelist,
readonly: token.isReadonly,
created: token.createdAt,
updated: token.updatedAt,
type,
};
}
return {
username: authorizedUser.displayName,
username: user.displayName,
};
}
// https://github.com/cnpm/cnpmcore/issues/64
@@ -184,7 +209,7 @@ export class UserController extends AbstractController {
// "pending": false,
// "mode": "auth-only"
// },
name: authorizedUser.name,
name: authorizedUser.displayName,
email: authorizedUser.email,
email_verified: false,
created: authorizedUser.createdAt,

View File

@@ -0,0 +1,43 @@
import {
HTTPController,
HTTPMethod,
HTTPMethodEnum,
Inject,
HTTPQuery, Context, EggContext,
} from '@eggjs/tegg';
import { AbstractController } from '../AbstractController';
import { FixNoPaddingVersionService } from '../../../core/service/FixNoPaddingVersionService';
@HTTPController()
export class PaddingVersionController extends AbstractController {
@Inject()
private readonly fixNoPaddingVersionService: FixNoPaddingVersionService;
@HTTPMethod({
method: HTTPMethodEnum.PUT,
path: '/-/admin/npm/fixPaddingVersion',
})
async fixNoPaddingVersion(@Context() ctx: EggContext, @HTTPQuery() id: string) {
const isAdmin = await this.userRoleManager.isAdmin(ctx);
if (!isAdmin) {
return {
ok: false,
error: 'only admin can do this',
};
}
let idNum: number | undefined;
if (id) {
idNum = parseInt(id);
if (Number.isNaN(idNum)) {
return {
ok: false,
error: `id is not a number ${id}`,
};
}
}
await this.fixNoPaddingVersionService.fixPaddingVersion(idNum);
return {
ok: true,
};
}
}

View File

@@ -14,6 +14,7 @@ import { AbstractController } from '../AbstractController';
import { FULLNAME_REG_STRING, getScopeAndName } from '../../../common/PackageUtil';
import { NFSAdapter } from '../../../common/adapter/NFSAdapter';
import { PackageManagerService } from '../../../core/service/PackageManagerService';
import { SyncMode } from '../../../common/constants';
@HTTPController()
export class DownloadPackageVersionTarController extends AbstractController {
@@ -22,26 +23,47 @@ export class DownloadPackageVersionTarController extends AbstractController {
@Inject()
private nfsAdapter: NFSAdapter;
// Support OPTIONS Request on tgz download
@HTTPMethod({
// GET /:fullname/-/:filenameWithVersion.tgz
path: `/:fullname(${FULLNAME_REG_STRING})/-/:filenameWithVersion.tgz`,
method: HTTPMethodEnum.OPTIONS,
})
async downloadForOptions(@Context() ctx: EggContext) {
ctx.set('access-control-allow-origin', '*');
ctx.set('access-control-allow-methods', 'GET,HEAD');
ctx.status = 204;
}
@HTTPMethod({
// GET /:fullname/-/:filenameWithVersion.tgz
path: `/:fullname(${FULLNAME_REG_STRING})/-/:filenameWithVersion.tgz`,
method: HTTPMethodEnum.GET,
})
async download(@Context() ctx: EggContext, @HTTPParam() fullname: string, @HTTPParam() filenameWithVersion: string) {
// try nfs url first, avoid db query
// tgz file storeKey: `/packages/${this.fullname}/${version}/${filename}`
const version = this.getAndCheckVersionFromFilename(ctx, fullname, filenameWithVersion);
const storeKey = `/packages/${fullname}/${version}/${filenameWithVersion}.tgz`;
const downloadUrl = await this.nfsAdapter.getDownloadUrl(storeKey);
if (downloadUrl) {
if (this.config.cnpmcore.syncMode === SyncMode.all && downloadUrl) {
// try nfs url first, avoid db query
this.packageManagerService.plusPackageVersionCounter(fullname, version);
ctx.redirect(downloadUrl);
return;
}
// check package version in database
const allowSync = this.getAllowSync(ctx);
const pkg = await this.getPackageEntityByFullname(fullname, allowSync);
const packageVersion = await this.getPackageVersionEntity(pkg, version, allowSync);
// read by nfs url
if (downloadUrl) {
this.packageManagerService.plusPackageVersionCounter(fullname, version);
ctx.redirect(downloadUrl);
return;
}
// read from database
const pkg = await this.getPackageEntityByFullname(fullname);
const packageVersion = await this.getPackageVersionEntity(pkg, version);
ctx.logger.info('[PackageController:downloadVersionTar] %s@%s, packageVersionId: %s',
pkg.fullname, version, packageVersion.packageVersionId);
const urlOrStream = await this.packageManagerService.downloadPackageVersionTar(packageVersion);

View File

@@ -1,7 +1,9 @@
import { PackageJson, Simplify } from 'type-fest';
import { isEqual } from 'lodash';
import {
UnprocessableEntityError,
ForbiddenError,
ConflictError,
} from 'egg-errors';
import {
HTTPController,
@@ -17,8 +19,9 @@ import * as ssri from 'ssri';
import validateNpmPackageName from 'validate-npm-package-name';
import { Static, Type } from '@sinclair/typebox';
import { AbstractController } from '../AbstractController';
import { getScopeAndName, FULLNAME_REG_STRING } from '../../../common/PackageUtil';
import { getScopeAndName, FULLNAME_REG_STRING, extractPackageJSON } from '../../../common/PackageUtil';
import { PackageManagerService } from '../../../core/service/PackageManagerService';
import { PackageVersion as PackageVersionEntity } from '../../../core/entity/PackageVersion';
import {
VersionRule,
TagWithVersionRule,
@@ -27,6 +30,9 @@ import {
} from '../../typebox';
import { RegistryManagerService } from '../../../core/service/RegistryManagerService';
import { PackageJSONType } from '../../../repository/PackageRepository';
import { CacheAdapter } from '../../../common/adapter/CacheAdapter';
const STRICT_CHECK_TARBALL_FIELDS: (keyof PackageJson)[] = [ 'name', 'version', 'scripts', 'dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies', 'license', 'licenses', 'bin' ];
type PackageVersion = Simplify<PackageJson.PackageJsonStandard & {
name: 'string';
@@ -71,6 +77,9 @@ export class SavePackageVersionController extends AbstractController {
@Inject()
private readonly registryManagerService: RegistryManagerService;
@Inject()
private readonly cacheAdapter: CacheAdapter;
// https://github.com/cnpm/cnpmjs.org/blob/master/docs/registry-api.md#publish-a-new-package
// https://github.com/npm/libnpmpublish/blob/main/publish.js#L43
@HTTPMethod({
@@ -87,11 +96,17 @@ export class SavePackageVersionController extends AbstractController {
if (fullname !== pkg.name) {
throw new UnprocessableEntityError(`fullname(${fullname}) not match package.name(${pkg.name})`);
}
// Using https://github.com/npm/validate-npm-package-name to validate package name
const validateResult = validateNpmPackageName(pkg.name);
if (!validateResult.validForNewPackages) {
const errors = (validateResult.errors || validateResult.warnings).join(', ');
throw new UnprocessableEntityError(`package.name invalid, errors: ${errors}`);
// if pkg already exists, still allow to publish
const [ scope, name ] = getScopeAndName(fullname);
const pkg = await this.packageRepository.findPackage(scope, name);
if (!pkg) {
const errors = (validateResult.errors || validateResult.warnings || []).join(', ');
throw new UnprocessableEntityError(`package.name invalid, errors: ${errors}`);
}
}
const versions = Object.values(pkg.versions);
if (versions.length === 0) {
@@ -123,11 +138,25 @@ export class SavePackageVersionController extends AbstractController {
const attachment = attachments[attachmentFilename];
const distTags = pkg['dist-tags'] ?? {};
const tagName = Object.keys(distTags)[0];
if (!tagName) {
let tagNames = Object.keys(distTags);
if (tagNames.length === 0) {
throw new UnprocessableEntityError('dist-tags is empty');
}
const tagWithVersion = { tag: tagName, version: distTags[tagName] };
const [ scope, name ] = getScopeAndName(fullname);
// see @https://github.com/cnpm/cnpmcore/issues/574
// add default latest tag
if (!pkg['dist-tags']!.latest) {
const existsPkg = await this.packageRepository.findPackage(scope, name);
const existsLatestTag = existsPkg && await this.packageRepository.findPackageTag(existsPkg?.packageId, 'latest');
if (!existsPkg || !existsLatestTag) {
this.logger.warn('[package:version:add] add default latest tag');
pkg['dist-tags']!.latest = pkg['dist-tags']![tagNames[0]];
tagNames = [ ...tagNames, 'latest' ];
}
}
const tagWithVersion = { tag: tagNames[0], version: distTags[tagNames[0]] };
ctx.tValidate(TagWithVersionRule, tagWithVersion);
if (tagWithVersion.version !== packageVersion.version) {
throw new UnprocessableEntityError(`dist-tags version "${tagWithVersion.version}" not match package version "${packageVersion.version}"`);
@@ -165,7 +194,20 @@ export class SavePackageVersionController extends AbstractController {
}
}
const [ scope, name ] = getScopeAndName(fullname);
// https://github.com/cnpm/cnpmcore/issues/542
// check tgz & manifests
if (this.config.cnpmcore.strictValidateTarballPkg) {
const tarballPkg = await extractPackageJSON(tarballBytes);
const versionManifest = pkg.versions[tarballPkg.version];
const diffKeys = STRICT_CHECK_TARBALL_FIELDS.filter(key => {
const targetKey = key as unknown as keyof typeof versionManifest;
return !isEqual(tarballPkg[key], versionManifest[targetKey]);
});
if (diffKeys.length > 0) {
throw new UnprocessableEntityError(`${diffKeys} mismatch between tarball and manifest`);
}
}
// make sure readme is string
const readme = typeof packageVersion.readme === 'string' ? packageVersion.readme : '';
@@ -177,28 +219,39 @@ export class SavePackageVersionController extends AbstractController {
}
const registry = await this.registryManagerService.ensureSelfRegistry();
const packageVersionEntity = await this.packageManagerService.publish({
scope,
name,
version: packageVersion.version,
description: packageVersion.description,
packageJson: packageVersion as PackageJSONType,
readme,
dist: {
content: tarballBytes,
},
tag: tagWithVersion.tag,
registryId: registry.registryId,
isPrivate: true,
}, user);
let packageVersionEntity: PackageVersionEntity | undefined;
const lockName = `${pkg.name}:publish`;
const lockRes = await this.cacheAdapter.usingLock(`${pkg.name}:publish`, 60, async () => {
packageVersionEntity = await this.packageManagerService.publish({
scope,
name,
version: packageVersion.version,
description: packageVersion.description as string,
packageJson: packageVersion as PackageJSONType,
readme,
dist: {
content: tarballBytes,
},
tags: tagNames,
registryId: registry.registryId,
isPrivate: true,
}, user);
});
// lock fail
if (!lockRes) {
this.logger.warn('[package:version:add] check lock:%s fail', lockName);
throw new ConflictError('Unable to create the publication lock, please try again later.');
}
this.logger.info('[package:version:add] %s@%s, packageVersionId: %s, tag: %s, userId: %s',
packageVersion.name, packageVersion.version, packageVersionEntity.packageVersionId,
tagWithVersion.tag, user.userId);
packageVersion.name, packageVersion.version, packageVersionEntity?.packageVersionId,
tagWithVersion.tag, user?.userId);
ctx.status = 201;
return {
ok: true,
rev: `${packageVersionEntity.id}-${packageVersionEntity.packageVersionId}`,
rev: `${packageVersionEntity?.id}-${packageVersionEntity?.packageVersionId}`,
};
}

View File

@@ -0,0 +1,69 @@
import {
HTTPController,
HTTPMethod,
HTTPMethodEnum,
HTTPParam,
HTTPQuery,
Inject,
Middleware,
Context,
EggContext,
} from '@eggjs/tegg';
import { Static } from 'egg-typebox-validate/typebox';
import { E451 } from 'egg-errors';
import { AbstractController } from '../AbstractController';
import { SearchQueryOptions } from '../../typebox';
import { PackageSearchService } from '../../../core/service/PackageSearchService';
import { FULLNAME_REG_STRING } from '../../../common/PackageUtil';
import { AdminAccess } from '../../middleware/AdminAccess';
@HTTPController()
export class SearchPackageController extends AbstractController {
@Inject()
private readonly packageSearchService: PackageSearchService;
@HTTPMethod({
// GET /-/v1/search?text=react&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5
path: '/-/v1/search',
method: HTTPMethodEnum.GET,
})
async search(
@Context() ctx: EggContext,
@HTTPQuery() text: Static<typeof SearchQueryOptions>['text'],
@HTTPQuery() from: Static<typeof SearchQueryOptions>['from'],
@HTTPQuery() size: Static<typeof SearchQueryOptions>['size'],
) {
if (!this.config.cnpmcore.enableElasticsearch) {
throw new E451('search feature not enabled in `config.cnpmcore.enableElasticsearch`');
}
const data = await this.packageSearchService.searchPackage(text, from, size);
this.setCDNHeaders(ctx);
return data;
}
@HTTPMethod({
path: `/-/v1/search/sync/:fullname(${FULLNAME_REG_STRING})`,
method: HTTPMethodEnum.PUT,
})
async sync(@HTTPParam() fullname: string) {
if (!this.config.cnpmcore.enableElasticsearch) {
throw new E451('search feature not enabled in `config.cnpmcore.enableElasticsearch`');
}
const name = await this.packageSearchService.syncPackage(fullname, true);
return { package: name };
}
@HTTPMethod({
path: `/-/v1/search/sync/:fullname(${FULLNAME_REG_STRING})`,
method: HTTPMethodEnum.DELETE,
})
@Middleware(AdminAccess)
async delete(@HTTPParam() fullname: string) {
if (!this.config.cnpmcore.enableElasticsearch) {
throw new E451('search feature not enabled in `config.cnpmcore.enableElasticsearch`');
}
const name = await this.packageSearchService.removePackage(fullname);
return { package: name };
}
}

View File

@@ -12,6 +12,7 @@ import { AbstractController } from '../AbstractController';
import { getScopeAndName, FULLNAME_REG_STRING } from '../../../common/PackageUtil';
import { isSyncWorkerRequest } from '../../../common/SyncUtil';
import { PackageManagerService } from '../../../core/service/PackageManagerService';
import { Spec } from '../../../port/typebox';
@HTTPController()
export class ShowPackageVersionController extends AbstractController {
@@ -19,25 +20,29 @@ export class ShowPackageVersionController extends AbstractController {
private packageManagerService: PackageManagerService;
@HTTPMethod({
// GET /:fullname/:versionOrTag
path: `/:fullname(${FULLNAME_REG_STRING})/:versionOrTag`,
// GET /:fullname/:versionSpec
path: `/:fullname(${FULLNAME_REG_STRING})/:versionSpec`,
method: HTTPMethodEnum.GET,
})
async show(@Context() ctx: EggContext, @HTTPParam() fullname: string, @HTTPParam() versionOrTag: string) {
async show(@Context() ctx: EggContext, @HTTPParam() fullname: string, @HTTPParam() versionSpec: string) {
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#full-metadata-format
ctx.tValidate(Spec, `${fullname}@${versionSpec}`);
const [ scope, name ] = getScopeAndName(fullname);
const isSync = isSyncWorkerRequest(ctx);
const { blockReason, manifest, pkg } = await this.packageManagerService.showPackageVersionManifest(scope, name, versionOrTag, isSync);
const abbreviatedMetaType = 'application/vnd.npm.install-v1+json';
const isFullManifests = ctx.accepts([ 'json', abbreviatedMetaType ]) !== abbreviatedMetaType;
const { blockReason, manifest, pkg } = await this.packageManagerService.showPackageVersionManifest(scope, name, versionSpec, isSync, isFullManifests);
if (!pkg) {
const allowSync = this.getAllowSync(ctx);
throw this.createPackageNotFoundErrorWithRedirect(fullname, undefined, allowSync);
}
if (blockReason) {
this.setCDNHeaders(ctx);
throw this.createPackageBlockError(blockReason, fullname, versionOrTag);
throw this.createPackageBlockError(blockReason, fullname, versionSpec);
}
if (!manifest) {
throw new NotFoundError(`${fullname}@${versionOrTag} not found`);
throw new NotFoundError(`${fullname}@${versionSpec} not found`);
}
this.setCDNHeaders(ctx);
return manifest;

View File

@@ -50,17 +50,21 @@ export class UpdatePackageController extends AbstractController {
ctx.tValidate(MaintainerDataRule, data);
const ensureRes = await this.ensurePublishAccess(ctx, fullname, true);
const pkg = ensureRes.pkg!;
const registry = await this.packageManagerService.getSourceRegistry(pkg);
// make sure all maintainers exists
const users: UserEntity[] = [];
for (const maintainer of data.maintainers) {
// TODO check userPrefix
if (registry?.userPrefix && !maintainer.name.startsWith(registry.userPrefix)) {
maintainer.name = `${registry?.userPrefix}${maintainer.name}`;
}
const user = await this.userRepository.findUserByName(maintainer.name);
if (!user) {
throw new UnprocessableEntityError(`Maintainer "${maintainer.name}" not exists`);
}
users.push(user);
}
await this.packageManagerService.replacePackageMaintainers(pkg, users);
await this.packageManagerService.replacePackageMaintainersAndDist(pkg, users);
return { ok: true };
}

View File

@@ -175,8 +175,8 @@
<button id="submit" disabled="true">Sign in</button>
</div>
</div>
<script src="https://gw.alipayobjects.com/os/lib/jsencrypt/3.3.1/bin/jsencrypt.min.js"></script>
<script src="https://gw.alipayobjects.com/os/lib/jquery/3.6.3/dist/jquery.min.js"></script>
<script src="https://registry.npmmirror.com/jsencrypt/3.3.2/files/bin/jsencrypt.min.js"></script>
<script src="https://registry.npmmirror.com/jquery/3.6.3/files/dist/jquery.min.js"></script>
<script>
$(function() {
const $submitBtn = $('#submit');

View File

@@ -31,8 +31,9 @@ export async function ErrorHandler(ctx: EggContext, next: Next) {
}
// http status, default is DEFAULT_SERVER_ERROR_STATUS
ctx.status = err.status || DEFAULT_SERVER_ERROR_STATUS;
if (ctx.status >= DEFAULT_SERVER_ERROR_STATUS) {
ctx.status = (typeof err.status === 'number' && err.status >= 200) ? err.status : DEFAULT_SERVER_ERROR_STATUS;
// don't log NotImplementedError
if (ctx.status >= DEFAULT_SERVER_ERROR_STATUS && err.name !== 'NotImplementedError') {
ctx.logger.error(err);
}
let message = err.message;

View File

@@ -29,7 +29,20 @@ export class SyncBinaryWorker {
this.logger.info('[SyncBinaryWorker:executeTask:start] taskId: %s, targetName: %s, attempts: %s, params: %j, updatedAt: %s, delay %sms',
task.taskId, task.targetName, task.attempts, task.data, task.updatedAt,
startTime - task.updatedAt.getTime());
await this.binarySyncerService.executeTask(task);
try {
await this.binarySyncerService.executeTask(task);
} catch (err) {
const use = Date.now() - startTime;
this.logger.warn('[SyncBinaryWorker:executeTask:error] taskId: %s, targetName: %s, use %sms, error: %s',
task.taskId, task.targetName, use, err.message);
if (err.name === 'ConnectTimeoutError'
|| err.name === 'HttpClientRequestTimeoutError') {
this.logger.warn(err);
} else {
this.logger.error(err);
}
return;
}
const use = Date.now() - startTime;
this.logger.info('[SyncBinaryWorker:executeTask:success] taskId: %s, targetName: %s, use %sms',
task.taskId, task.targetName, use);

View File

@@ -27,6 +27,7 @@ export class TriggerHookWorker {
private readonly taskService: TaskService;
async subscribe() {
if (!this.config.cnpmcore.hookEnable) return;
if (executingCount >= this.config.cnpmcore.triggerHookWorkerMaxConcurrentTasks) return;
executingCount++;

View File

@@ -79,8 +79,8 @@ export class UpdateTotalData {
for (const row of rows) {
for (let i = 1; i <= 31; i++) {
const day = String(i).padStart(2, '0');
const field = `d${day}`;
const counter = row[field];
const field = `d${day}` as keyof typeof row;
const counter = row[field] as number;
if (!counter) continue;
const dayInt = row.yearMonth * 100 + i;
if (dayInt === todayInt) download.today += counter;

View File

@@ -1,8 +1,10 @@
import { Type, Static } from '@sinclair/typebox';
import { RegistryType } from '../common/enum/Registry';
import semver from 'semver';
import npa from 'npm-package-arg';
import { HookType } from '../common/enum/Hook';
import binaryConfig from '../../config/binaries';
import binaryConfig, { BinaryName } from '../../config/binaries';
import { uniq } from 'lodash';
export const Name = Type.String({
transform: [ 'trim' ],
@@ -53,6 +55,16 @@ export const Version = Type.String({
maxLength: 256,
});
export const VersionStringArray = Type.String({
format: 'unique-semver-version-array',
transform: [ 'trim' ],
});
export const Spec = Type.String({
format: 'semver-spec',
minLength: 1,
});
export const Description = Type.String({ maxLength: 10240, transform: [ 'trim' ] });
export const TagRule = Type.Object({
@@ -68,17 +80,12 @@ export const TagWithVersionRule = Type.Object({
export const SyncPackageTaskRule = Type.Object({
fullname: Name,
remoteAuthToken: Type.Optional(
Type.String({
transform: [ 'trim' ],
maxLength: 200,
}),
),
tips: Type.String({
transform: [ 'trim' ],
maxLength: 1024,
}),
skipDependencies: Type.Boolean(),
specificVersions: Type.Optional(VersionStringArray),
syncDownloadData: Type.Boolean(),
// force sync immediately, only allow by admin
force: Type.Boolean(),
@@ -125,10 +132,39 @@ export function patchAjv(ajv: any) {
return !semver.validRange(tag);
},
});
ajv.addFormat('semver-spec', {
type: 'string',
validate: (spec: string) => {
try {
// do not support alias
// exp: https://unpkg.com/good@npm:cnpmcore@3.17.1/dist/app.js
return [ 'tag', 'version', 'range' ].includes(npa(spec).type);
} catch (e) {
return false;
}
},
});
ajv.addFormat('binary-name', {
type: 'string',
validate: (binaryName: string) => {
return !!binaryConfig[binaryName];
validate: (binaryName: BinaryName) => {
return binaryConfig[binaryName];
},
});
ajv.addFormat('unique-semver-version-array', {
type: 'string',
validate: (versionStringList: string) => {
let versionList;
try {
versionList = JSON.parse(versionStringList);
} catch (error) {
return false;
}
if (Array.isArray(versionList)) {
const isSemver = versionList.every(version => !!semver.valid(version));
const isUnique = uniq(versionList).length === versionList.length;
return isSemver && isUnique;
}
return false;
},
});
}
@@ -171,35 +207,44 @@ export const RegistryCreateOptions = Type.Object({
maxLength: 256,
})),
type: Type.Enum(RegistryType),
authToken: Type.Optional(
Type.String({
transform: [ 'trim' ],
maxLength: 256,
}),
),
});
export const RegistryUpdateOptions = Type.Object({
name: Type.String({
transform: [ 'trim' ],
minLength: 1,
maxLength: 256,
}),
host: Type.String({
transform: [ 'trim' ],
minLength: 1,
maxLength: 4096,
}),
changeStream: Type.String({
transform: [ 'trim' ],
minLength: 1,
maxLength: 4096,
}),
userPrefix: Type.Optional(Type.String({
transform: [ 'trim' ],
minLength: 1,
maxLength: 256,
})),
type: Type.Enum(RegistryType),
registryId: Type.String({
transform: [ 'trim' ],
minLength: 1,
maxLength: 256,
}),
name: Type.Optional(
Type.String({
transform: [ 'trim' ],
minLength: 1,
maxLength: 256,
}),
),
host: Type.Optional(
Type.String({
transform: [ 'trim' ],
minLength: 1,
maxLength: 4096,
}),
),
changeStream: Type.Optional(
Type.String({
transform: [ 'trim' ],
minLength: 1,
maxLength: 4096,
}),
),
type: Type.Optional(Type.Enum(RegistryType)),
authToken: Type.Optional(
Type.String({
transform: [ 'trim' ],
minLength: 1,
maxLength: 256,
}),
),
});
export const ScopeCreateOptions = Type.Object({
@@ -232,3 +277,21 @@ export const ScopeUpdateOptions = Type.Object({
maxLength: 256,
}),
});
export const SearchQueryOptions = Type.Object({
from: Type.Number({
transform: [ 'trim' ],
minimum: 0,
default: 0,
}),
size: Type.Number({
transform: [ 'trim' ],
minimum: 1,
default: 20,
}),
text: Type.String({
transform: [ 'trim' ],
minLength: 1,
maxLength: 256,
}),
});

View File

@@ -22,6 +22,8 @@ import {
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse,
VerifyRegistrationResponseOpts,
VerifyAuthenticationResponseOpts,
} from '@simplewebauthn/server';
import type { PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/typescript-types';
import { LoginResultCode, WanStatusCode } from '../../common/enum/User';
@@ -44,6 +46,17 @@ type LoginPrepareResult = {
wanCredentialAuthOption?: PublicKeyCredentialRequestOptionsJSON;
};
type LoginImplementRequest = {
accData: {
username: string;
password: string;
};
wanCredentialRegiData: unknown;
wanCredentialAuthData: unknown;
needUnbindWan: boolean;
};
const UserRule = Type.Object({
name: Type.String({ minLength: 1, maxLength: 100 }),
password: Type.String({ minLength: 8, maxLength: 100 }),
@@ -102,7 +115,7 @@ export class WebauthController extends MiddlewareController {
path: '/-/v1/login/request/session/:sessionId',
method: HTTPMethodEnum.POST,
})
async loginImplement(@Context() ctx: EggContext, @HTTPParam() sessionId: string, @HTTPBody() loginImplementRequest) {
async loginImplement(@Context() ctx: EggContext, @HTTPParam() sessionId: string, @HTTPBody() loginImplementRequest: LoginImplementRequest) {
ctx.tValidate(SessionRule, { sessionId });
const sessionToken = await this.cacheAdapter.get(sessionId);
if (typeof sessionToken !== 'string') {
@@ -123,7 +136,7 @@ export class WebauthController extends MiddlewareController {
}
}
const browserType = getBrowserTypeForWebauthn(ctx.headers['user-agent']);
const browserType = getBrowserTypeForWebauthn(ctx.headers['user-agent']) || undefined;
const expectedChallenge = (await this.cacheAdapter.get(`${sessionId}_challenge`)) || '';
const expectedOrigin = this.config.cnpmcore.registry;
const expectedRPID = new URL(expectedOrigin).hostname;
@@ -139,7 +152,7 @@ export class WebauthController extends MiddlewareController {
}
try {
const verification = await verifyAuthenticationResponse({
response: wanCredentialAuthData,
response: wanCredentialAuthData as VerifyAuthenticationResponseOpts['response'],
expectedChallenge,
expectedOrigin,
expectedRPID,
@@ -193,7 +206,7 @@ export class WebauthController extends MiddlewareController {
user = result.user;
// need unbind webauthn credential
if (needUnbindWan) {
await this.userService.removeWebauthnCredential(user.userId, browserType);
await this.userService.removeWebauthnCredential(user?.userId, browserType);
}
} else {
// others: LoginResultCode.UserNotFound
@@ -215,7 +228,7 @@ export class WebauthController extends MiddlewareController {
if (enableWebAuthn && isSupportWebAuthn && wanCredentialRegiData) {
try {
const verification = await verifyRegistrationResponse({
response: wanCredentialRegiData,
response: wanCredentialRegiData as VerifyRegistrationResponseOpts['response'],
expectedChallenge,
expectedOrigin,
expectedRPID,
@@ -225,7 +238,7 @@ export class WebauthController extends MiddlewareController {
const { credentialPublicKey, credentialID } = registrationInfo;
const base64CredentialPublicKey = base64url.encode(Buffer.from(new Uint8Array(credentialPublicKey)));
const base64CredentialID = base64url.encode(Buffer.from(new Uint8Array(credentialID)));
this.userService.createWebauthnCredential(user.userId, {
this.userService.createWebauthnCredential(user?.userId, {
credentialId: base64CredentialID,
publicKey: base64CredentialPublicKey,
browserType,

View File

@@ -15,9 +15,9 @@ export class BinaryRepository extends AbstractRepository {
if (binary.id) {
const model = await this.Binary.findOne({ id: binary.id });
if (!model) return;
await ModelConvertor.saveEntityToModel(binary, model);
await ModelConvertor.saveEntityToModel<BinaryModel>(binary as unknown as Record<string, unknown>, model);
} else {
const model = await ModelConvertor.convertEntityToModel(binary, this.Binary);
const model = await ModelConvertor.convertEntityToModel(binary as unknown as Record<string, unknown>, this.Binary);
this.logger.info('[BinaryRepository:saveBinary:new] id: %s, binaryId: %s', model.id, model.binaryId);
}
}

View File

@@ -13,7 +13,7 @@ export class DistRepository {
@Inject()
private readonly nfsAdapter: NFSAdapter;
async findPackageVersionManifest(packageId: string, version: string) {
async findPackageVersionManifest(packageId: string, version: string): Promise<PackageJSONType | undefined> {
const packageVersion = await this.packageRepository.findPackageVersion(packageId, version);
if (packageVersion) {
const [ packageVersionJson, readme ] = await Promise.all([
@@ -27,7 +27,7 @@ export class DistRepository {
}
}
async findPackageAbbreviatedManifest(packageId: string, version: string) {
async findPackageAbbreviatedManifest(packageId: string, version: string): Promise<PackageJSONType | undefined> {
const packageVersion = await this.packageRepository.findPackageVersion(packageId, version);
if (packageVersion) {
return await this.readDistBytesToJSON(packageVersion.abbreviatedDist);

View File

@@ -17,6 +17,7 @@ import type { Maintainer as MaintainerModel } from './model/Maintainer';
import type { User as UserModel } from './model/User';
import { User as UserEntity } from '../core/entity/User';
import { AbstractRepository } from './AbstractRepository';
import { BugVersionPackages } from '../core/entity/BugVersion';
export type PackageManifestType = Pick<PackageJSONType, PackageJSONPickKey> & {
_id: string;
@@ -50,7 +51,7 @@ export type PackageJSONType = CnpmcorePatchInfo & {
url?: string;
email?: string;
};
license?: string;
license?: LicenseType | string;
author?: AuthorType | string;
contributors?: ContributorType[] | string[];
maintainers?: ContributorType[] | string[];
@@ -63,8 +64,11 @@ export type PackageJSONType = CnpmcorePatchInfo & {
directories?: DirectoriesType;
repository?: RepositoryType;
scripts?: Record<string, string>;
config?: Record<string, unknown>;
config?: {
'bug-versions'?: BugVersionPackages;
};
dependencies?: DepInfo;
acceptDependencies?: DepInfo;
devDependencies?: DepInfo;
peerDependencies?: DepInfo;
peerDependenciesMeta?: {
@@ -95,19 +99,27 @@ export type PackageJSONType = CnpmcorePatchInfo & {
hasInstallScript?: boolean;
dist?: DistType;
workspace?: string[];
_npmUser?: {
name: string;
email: string;
};
[key: string]: unknown;
};
type PackageJSONPickKey = 'name' | 'author' | 'bugs' | 'description' | 'homepage' | 'keywords' | 'license' | 'readme' | 'readmeFilename' | 'repository' | 'versions';
type PackageJSONPickKey = 'name' | 'author' | 'bugs' | 'description' | 'homepage' | 'keywords' |
'license' | 'readme' | 'readmeFilename' | 'repository' | 'versions' | 'contributors';
type CnpmcorePatchInfo = {
export type CnpmcorePatchInfo = {
_cnpmcore_publish_time?: Date;
publish_time?: number;
_source_registry_name?: string;
block?: string;
};
type AbbreviatedKey = 'name' | 'version' | 'deprecated' | 'dependencies' | 'optionalDependencies' | 'devDependencies' | 'bundleDependencies' | 'peerDependencies' | 'peerDependenciesMeta' | 'bin' | 'os' | 'cpu' | 'libc' | 'workspaces' | 'directories' | 'dist' | 'engines' | 'hasInstallScript' | 'publish_time' | 'block' | '_hasShrinkwrap';
type AbbreviatedKey = 'name' | 'version' | 'deprecated' | 'dependencies' | 'optionalDependencies' |
'devDependencies' | 'bundleDependencies' | 'peerDependencies' | 'peerDependenciesMeta' | 'bin' |
'os' | 'cpu' | 'libc' | 'workspaces' | 'directories' | 'dist' | 'engines' | 'hasInstallScript' |
'publish_time' | 'block' | '_hasShrinkwrap' | 'acceptDependencies' | 'funding';
type DistType = {
tarball: string,
@@ -117,12 +129,18 @@ type DistType = {
[key: string]: unknown,
};
type AuthorType = {
export type AuthorType = {
name: string;
username?: string;
email?: string;
url?: string;
};
type LicenseType = {
type: string;
url: string;
};
type ContributorType = {
name?: string;
email?: string;

View File

@@ -65,7 +65,7 @@ export class PackageVersionDownloadRepository extends AbstractRepository {
}
for (const [ date, counter ] of counters) {
const field = `d${date}`;
model[field] = counter;
(model as unknown as Record<string, number>)[field] = counter;
}
await model.save();
}

View File

@@ -33,20 +33,39 @@ export class PackageVersionFileRepository extends AbstractRepository {
}
async listPackageVersionFiles(packageVersionId: string, directory: string) {
const where = directory === '/' ? { packageVersionId } :
const isRoot = directory === '/';
const where = isRoot ? { packageVersionId } :
{ packageVersionId, directory: { $or: [{ $eq: directory }, { $like: `${directory}/%` }] } };
// only return current directory's files and directories
// https://github.com/cnpm/cnpmcore/issues/680
const models = await this.PackageVersionFile.find(where);
const distIds = models.map(model => model.distId);
const distIds: string[] = [];
const prefix = isRoot ? directory : `${directory}/`;
const subDirectories = new Set<string>();
const needModels: PackageVersionFileModel[] = [];
for (const item of models) {
if (item.directory === directory) {
// sub file
distIds.push(item.distId);
needModels.push(item);
} else {
// only keep directory = '/' or sub directory like `/dist` but not `/dist/foo`
// sub directory
const subDirectoryName = item.directory.substring(prefix.length).split('/')[0];
subDirectories.add(`${prefix}${subDirectoryName}`);
}
}
const distModels = await this.Dist.find({ distId: distIds });
const distEntitiesMap = new Map<string, DistEntity>();
for (const distModel of distModels) {
const dist = ModelConvertor.convertModelToEntity(distModel, DistEntity);
distEntitiesMap.set(distModel.distId, dist);
}
return models.map(model => {
const files = needModels.map(model => {
const dist = distEntitiesMap.get(model.distId);
return ModelConvertor.convertModelToEntity(model, PackageVersionFileEntity, { dist });
});
return { files, directories: Array.from(subDirectories) };
}
async hasPackageVersionFiles(packageVersionId: string) {

View File

@@ -0,0 +1,83 @@
import { AccessLevel, Inject, SingletonProto } from '@eggjs/tegg';
import { PaddingSemVer } from '../core/entity/PaddingSemVer';
import type { Package as PackageModel } from './model/Package';
import { PackageVersion } from '../core/entity/PackageVersion';
import type { PackageTag } from './model/PackageTag';
import { ModelConvertor } from './util/ModelConvertor';
import type { PackageVersion as PackageVersionModel } from './model/PackageVersion';
import { SqlRange } from '../core/entity/SqlRange';
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
})
export class PackageVersionRepository {
@Inject()
private readonly Package: typeof PackageModel;
@Inject()
private readonly PackageVersion: typeof PackageVersionModel;
@Inject()
private readonly PackageTag: typeof PackageTag;
async findHaveNotPaddingVersion(id?: number): Promise<PackageVersion[]> {
if (!id) {
id = await this.PackageVersion.minimum('id')
.where('paddingVersion is null') as number;
}
if (!id) return [];
const versions = await this.PackageVersion.find({ id: { $gte: id } } as object)
.limit(1000);
const versionModels = versions.map(t => ModelConvertor.convertModelToEntity(t, PackageVersion));
return (versionModels as any).toObject();
}
async fixPaddingVersion(pkgVersionId: string, paddingSemver: PaddingSemVer): Promise<void> {
await this.PackageVersion.update({ packageVersionId: pkgVersionId }, {
paddingVersion: paddingSemver.paddingVersion,
isPreRelease: paddingSemver.isPreRelease,
});
}
async findVersionByTag(scope: string, name: string, tag: string): Promise<string | undefined> {
const tags = await this.PackageTag.select('version')
.join(this.Package as any, 'packageTags.packageId = packages.packageId')
.where({
scope,
name,
tag,
} as object) as { version: string }[];
const tagModel = tags && tags[0];
return tagModel?.version;
}
/**
* if sql version not contains prerelease, find the max version
*/
async findMaxSatisfyVersion(scope: string, name: string, sqlRange: SqlRange): Promise<string | undefined> {
const versions = await this.PackageVersion
.select('packageVersions.version')
.join(this.Package as any, 'packageVersions.packageId = packages.packageId')
.where({
'packages.scope': scope,
'packages.name': name,
...sqlRange.condition,
} as object)
.order('packageVersions.paddingVersion', 'desc') as { version: string }[];
return versions?.[0]?.version;
}
async findSatisfyVersionsWithPrerelease(scope: string, name: string, sqlRange: SqlRange): Promise<Array<string>> {
const versions = await this.PackageVersion
.select('version')
.join(this.Package as any, 'packageVersions.packageId = packages.packageId')
.where({
scope,
name,
...sqlRange.condition,
} as object);
return (versions as any).toObject()
.map((t: { version: string }) => t.version);
}
}

View File

@@ -38,6 +38,14 @@ export class RegistryRepository extends AbstractRepository {
return null;
}
async findRegistryByRegistryHost(host: string): Promise<RegistryEntity | null> {
const model = await this.Registry.findOne({ host });
if (model) {
return ModelConvertor.convertModelToEntity(model, RegistryEntity);
}
return null;
}
async saveRegistry(registry: Registry) {
if (registry.id) {
const model = await this.Registry.findOne({ id: registry.id });

View File

@@ -0,0 +1,48 @@
import { SingletonProto, AccessLevel, Inject } from '@eggjs/tegg';
import { SearchAdapter } from '../common/typing';
import { AuthorType, CnpmcorePatchInfo, PackageManifestType } from './PackageRepository';
export type SearchJSONPickKey = '_rev' | 'name' | 'description' | 'keywords' | 'license' | 'maintainers' | 'dist-tags' | '_source_registry_name';
export type SearchMappingType = Pick<PackageManifestType, SearchJSONPickKey> & CnpmcorePatchInfo & {
scope: string;
version: string;
versions: string[];
date: Date;
created: Date;
modified: Date;
author?: AuthorType | undefined;
_npmUser?: {
name: string;
email: string;
}
};
export type SearchManifestType = {
package: SearchMappingType;
downloads: {
all: number;
};
};
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
})
export class SearchRepository {
@Inject()
private readonly searchAdapter: SearchAdapter;
async searchPackage(query) {
return await this.searchAdapter.search<SearchManifestType>(query);
}
async upsertPackage(document: SearchManifestType) {
return await this.searchAdapter.upsert(document.package.name, document);
}
async removePackage(fullname: string) {
return await this.searchAdapter.delete(fullname);
}
}

View File

@@ -1,4 +1,4 @@
import assert from 'assert';
import { strict as assert } from 'node:assert';
import { AccessLevel, SingletonProto, Inject } from '@eggjs/tegg';
import { ModelConvertor } from './util/ModelConvertor';
import type { Task as TaskModel } from './model/Task';
@@ -6,6 +6,7 @@ import type { HistoryTask as HistoryTaskModel } from './model/HistoryTask';
import { Task as TaskEntity, TaskUpdateCondition } from '../core/entity/Task';
import { AbstractRepository } from './AbstractRepository';
import { TaskType, TaskState } from '../../app/common/enum/Task';
import { uniq } from 'lodash';
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
@@ -67,6 +68,21 @@ export class TaskRepository extends AbstractRepository {
await model.remove();
}
async updateSpecificVersionsOfWaitingTask(task: TaskEntity, specificVersions?: Array<string>): Promise<void> {
const model = await this.Task.findOne({ id: task.id });
if (!model || !model.data.specificVersions) return;
if (specificVersions) {
const data = model.data;
const combinedVersions = uniq<string>(data.specificVersions.concat(specificVersions));
data.specificVersions = combinedVersions;
await model.update({ data });
} else {
const data = model.data;
Reflect.deleteProperty(data, 'specificVersions');
await model.update({ data });
}
}
async findTask(taskId: string) {
const task = await this.Task.findOne({ taskId });
if (task) {

View File

@@ -1,6 +1,7 @@
import { AccessLevel, SingletonProto, Inject } from '@eggjs/tegg';
import { ModelConvertor } from './util/ModelConvertor';
import type { User as UserModel } from './model/User';
import type { Package as PackageModel } from './model/Package';
import type { Token as TokenModel } from './model/Token';
import type { WebauthnCredential as WebauthnCredentialModel } from './model/WebauthnCredential';
import { User as UserEntity } from '../core/entity/User';
@@ -8,7 +9,7 @@ import { Token as TokenEntity, isGranularToken } from '../core/entity/Token';
import { WebauthnCredential as WebauthnCredentialEntity } from '../core/entity/WebauthnCredential';
import { AbstractRepository } from './AbstractRepository';
import { TokenPackage as TokenPackageModel } from './model/TokenPackage';
import { getScopeAndName } from '../common/PackageUtil';
import { getFullname, getScopeAndName } from '../common/PackageUtil';
import { PackageRepository } from './PackageRepository';
@SingletonProto({
@@ -24,6 +25,9 @@ export class UserRepository extends AbstractRepository {
@Inject()
private readonly TokenPackage: typeof TokenPackageModel;
@Inject()
private readonly Package: typeof PackageModel;
@Inject()
private readonly packageRepository: PackageRepository;
@@ -58,6 +62,7 @@ export class UserRepository extends AbstractRepository {
if (!token) return null;
const userModel = await this.User.findOne({ userId: token.userId });
if (!userModel) return null;
return {
token,
user: ModelConvertor.convertModelToEntity(userModel, UserEntity),
@@ -67,7 +72,19 @@ export class UserRepository extends AbstractRepository {
async findTokenByTokenKey(tokenKey: string) {
const model = await this.Token.findOne({ tokenKey });
if (!model) return null;
return ModelConvertor.convertModelToEntity(model, TokenEntity);
const token = ModelConvertor.convertModelToEntity(model, TokenEntity);
await this._injectTokenPackages(token);
return token;
}
private async _injectTokenPackages(token: TokenEntity) {
if (isGranularToken(token)) {
const models = await this.TokenPackage.find({ tokenId: token.tokenId });
const packages = await this.Package.find({ packageId: models.map(m => m.packageId) });
if (Array.isArray(packages)) {
token.allowedPackages = packages.map(p => getFullname(p.scope, p.name));
}
}
}
async saveToken(token: TokenEntity): Promise<void> {
@@ -111,7 +128,11 @@ export class UserRepository extends AbstractRepository {
async listTokens(userId: string): Promise<TokenEntity[]> {
const models = await this.Token.find({ userId });
return models.map(model => ModelConvertor.convertModelToEntity(model, TokenEntity));
const tokens = models.map(model => ModelConvertor.convertModelToEntity(model, TokenEntity));
for (const token of tokens) {
await this._injectTokenPackages(token);
}
return tokens;
}
async saveCredential(credential: WebauthnCredentialEntity): Promise<void> {
@@ -125,7 +146,7 @@ export class UserRepository extends AbstractRepository {
}
}
async findCredentialByUserIdAndBrowserType(userId: string, browserType: string | null) {
async findCredentialByUserIdAndBrowserType(userId: string | undefined, browserType: string | null) {
const model = await this.WebauthnCredential.findOne({
userId,
browserType,

View File

@@ -1,6 +1,7 @@
import { Attribute, Model } from '@eggjs/tegg/orm';
import { DataTypes, Bone } from 'leoric';
import { EntityProperty } from '../util/EntityProperty';
import { PaddingSemVer } from '../../core/entity/PaddingSemVer';
@Model()
export class PackageVersion extends Bone {
@@ -47,4 +48,18 @@ export class PackageVersion extends Bone {
@Attribute(DataTypes.DATE)
publishTime: Date;
@Attribute(DataTypes.STRING)
paddingVersion: string;
@Attribute(DataTypes.BOOLEAN)
isPreRelease: boolean;
static beforeCreate(instance: { version: string; paddingVersion: string; isPreRelease: boolean }) {
if (!instance.paddingVersion) {
const paddingSemVer = new PaddingSemVer(instance.version);
instance.paddingVersion = paddingSemVer.paddingVersion;
instance.isPreRelease = paddingSemVer.isPreRelease;
}
}
}

View File

@@ -36,4 +36,7 @@ export class Registry extends Bone {
@Attribute(DataTypes.STRING(256))
type: RegistryType;
@Attribute(DataTypes.STRING(256), { name: 'auth_token' })
authToken?: string;
}

View File

@@ -54,4 +54,7 @@ export class Token extends Bone {
@Attribute(DataTypes.DATE)
expiredAt: Date;
@Attribute(DataTypes.DATE)
lastUsedAt: Date;
}

Some files were not shown because too many files have changed in this diff Show More