Compare commits

...

289 Commits

Author SHA1 Message Date
semantic-release-bot
914aee2560 Release 3.75.1
[skip ci]

## [3.75.1](https://github.com/cnpm/cnpmcore/compare/v3.75.0...v3.75.1) (2025-03-09)

### Bug Fixes

* only get packageId from database ([#768](https://github.com/cnpm/cnpmcore/issues/768)) ([bc068d1](bc068d165c))
2025-03-09 02:10:36 +00:00
fengmk2
bc068d165c fix: only get packageId from database (#768)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Refactor**
- Enhanced the internal process for retrieving package version details,
resulting in more accurate and reliable version information.
- Streamlined the handling of missing package data to reduce unexpected
issues.
- Optimized overall data processing, contributing to improved stability
and a smoother user experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-09 10:09:03 +08:00
semantic-release-bot
9ffb09eaa8 Release 3.75.0
[skip ci]

## [3.75.0](https://github.com/cnpm/cnpmcore/compare/v3.74.2...v3.75.0) (2025-03-09)

### Features

* mirror node-pty-prebuilt-multiarch ([#767](https://github.com/cnpm/cnpmcore/issues/767)) ([cbefb5c](cbefb5c6d0))
2025-03-09 01:56:37 +00:00
时瑾
cbefb5c6d0 feat: mirror node-pty-prebuilt-multiarch (#767)
closes https://github.com/cnpm/cnpmcore/issues/766

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

## Summary by CodeRabbit

- **New Features**
- Expanded our binary support to include prebuilt binaries for the
node-pty package, enhancing integration possibilities and deployment
options for users relying on these binaries.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-09 09:55:02 +08:00
elrrrrrrr
1922bf2f76 chore: disbale public registration by default (#764)
> The 'allowPublicRegistration' is enabled by default, which my cause
unexpected users registering arbitrarily
1. ⚙ Modify the default configuration 'allowPublicRegistration' to
'false'`

-------------
> 目前默认开启了 `allowPublicRegistration` 配置,公网部署可能会导致预期外的用户任意注册

1. ⚙ 修改默认配置 `allowPublicRegistration` 为 `false`


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

- **New Features**
- Public registration can now be enabled through a new configuration
option, allowing for more flexible user onboarding.
  
- **Bug Fixes**
- Updated the configuration to disallow public registration by default,
ensuring only administrators can log in unless changed.

- **Documentation**
- Added an informational note in the developer documentation regarding
public registration settings.

- **Tests**
- Introduced a setup method to enable public registration before each
test case runs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-28 09:34:11 +08:00
semantic-release-bot
47da2f40cf Release 3.74.2
[skip ci]

## [3.74.2](https://github.com/cnpm/cnpmcore/compare/v3.74.1...v3.74.2) (2025-02-27)

### Bug Fixes

* block tgz ([#763](https://github.com/cnpm/cnpmcore/issues/763)) ([3054577](305457777e))
2025-02-27 06:44:47 +00:00
elrrrrrrr
305457777e fix: block tgz (#763)
> The tgz download interface does not check if the package is blocked,
which may pose additional risks for parsing package-lock.json or other
lock files.
[exp](https://registry.npmmirror.com/joker-su/-/joker-su-1.0.0.tgz)
1. 🛡️ Add validation logic for
DownloadPackageVersionTarController#download to check if the package is
allowed to be downloaded.
2. 🧶 Add PackageVersionService#findBlockInfo to check if the
corresponding package is blocked.
3. ♻️ When a single version is blocked, skip check as per the current
manifest logic.

---------

> tgz 下载接口没有判断包是否被 block,对于 package-lock.json
或者其他依赖锁文件解析可能会有额外风险,[exp](https://registry.npmmirror.com/joker-su/-/joker-su-1.0.0.tgz)

1. 🛡️ `DownloadPackageVersionTarController#download` 接口新增校验逻辑,判断是否允许下载
2. 🧶 新增 PackageVersionService#findBlockInfo 判断对应包是否被全局拦截
3. ♻️ 单版本被 block 时,考虑到误封场景,按目前 manifest 逻辑,不在 tgz 下载时进行拦截操作

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

- **New Features**
- Enhanced the package download process with an additional block check.
Now, if a package is flagged, the download will be halted and a clear
error response is returned to inform users of the block.
- Introduced a method to retrieve block information related to package
versions, improving the service's capabilities.

- **Tests**
- Added new test cases to verify the blocking functionality for package
downloads, ensuring the application correctly handles requests for
blocked packages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-27 06:43:28 +00:00
Kevin Cui
ae88145317 docs(elk): add missing version field (#760) 2025-02-26 18:01:05 +08:00
semantic-release-bot
05b3b798b6 Release 3.74.1
[skip ci]

## [3.74.1](https://github.com/cnpm/cnpmcore/compare/v3.74.0...v3.74.1) (2025-02-10)

### Bug Fixes

* should return default value when env is empty string ([#758](https://github.com/cnpm/cnpmcore/issues/758)) ([e72e396](e72e396e3c))
2025-02-10 01:28:21 +00:00
fengmk2
e72e396e3c fix: should return default value when env is empty string (#758)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- Refactor
- Enhanced environment variable handling to trim extra whitespace and
improve default value checks for more robust configuration processing.
  
- Tests
- Expanded test coverage to validate default behavior, type conversions,
and error handling for various environment variable scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-10 09:26:47 +08:00
semantic-release-bot
386974272d Release 3.74.0
[skip ci]

## [3.74.0](https://github.com/cnpm/cnpmcore/compare/v3.73.1...v3.74.0) (2025-02-09)

### Features

* allow to set sourceRegistry by CNPMCORE_CONFIG_SOURCE_REGISTRY ([#753](https://github.com/cnpm/cnpmcore/issues/753)) ([9f4f1f1](9f4f1f1e28))
2025-02-09 03:16:57 +00:00
fengmk2
9f4f1f1e28 feat: allow to set sourceRegistry by CNPMCORE_CONFIG_SOURCE_REGISTRY (#753)
Improve the local development process based on docker-compose

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

## Summary by CodeRabbit

- **New Features**
- Introduced a centralized configuration utility that validates
environment variable types and provides fallback defaults.
- **Refactor**
- Standardized environment variable handling across configuration files,
improving maintainability and consistency in system setup.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-09 11:15:28 +08:00
semantic-release-bot
aba2b36291 Release 3.73.1
[skip ci]

## [3.73.1](https://github.com/cnpm/cnpmcore/compare/v3.73.0...v3.73.1) (2025-02-08)

### Bug Fixes

* webauth no need to validate hostname on cli request ([#752](https://github.com/cnpm/cnpmcore/issues/752)) ([f1fc249](f1fc2492b6))
2025-02-08 09:34:38 +00:00
fengmk2
f1fc2492b6 fix: webauth no need to validate hostname on cli request (#752) 2025-02-08 17:33:12 +08:00
semantic-release-bot
c23a6699f3 Release 3.73.0
[skip ci]

## [3.73.0](https://github.com/cnpm/cnpmcore/compare/v3.72.1...v3.73.0) (2025-02-07)

### Features

* update playwright DOWNLOAD_PATHS ([#751](https://github.com/cnpm/cnpmcore/issues/751)) ([1850c8b](1850c8b2d4))
2025-02-07 16:09:19 +00:00
fengmk2
1850c8b2d4 feat: update playwright DOWNLOAD_PATHS (#751)
closes https://github.com/cnpm/cnpmcore/issues/750

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

## Summary by CodeRabbit

- **New Features**
- Expanded support with additional platform configurations for Ubuntu
24.04 and macOS 15 (including ARM variants).
	- Introduced a new Windows configuration for improved compatibility.
- Updated browser configurations to deliver the latest versions across
Chromium, Firefox, Webkit, FFmpeg, and Android.
- **Tests**
- Refined validation checks and error reporting to ensure consistent and
reliable browser setups.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-08 00:08:03 +08:00
semantic-release-bot
c70fdccc04 Release 3.72.1
[skip ci]

## [3.72.1](https://github.com/cnpm/cnpmcore/compare/v3.72.0...v3.72.1) (2025-02-07)

### Bug Fixes

* chromium-headless-shell should under the chromium dir ([#749](https://github.com/cnpm/cnpmcore/issues/749)) ([a580b05](a580b05004))
2025-02-07 15:03:41 +00:00
fengmk2
a580b05004 fix: chromium-headless-shell should under the chromium dir (#749)
closes https://github.com/cnpm/cnpmcore/issues/742

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

## Summary by CodeRabbit

- New Features
- Improved organization for browser downloads: Secondary browser
components are now integrated under the primary browser category,
resulting in a more intuitive and streamlined binary structure for
users.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-07 23:02:11 +08:00
semantic-release-bot
43636bd80b Release 3.72.0
[skip ci]

## [3.72.0](https://github.com/cnpm/cnpmcore/compare/v3.71.3...v3.72.0) (2025-02-07)

### Features

* **mirror:** add chromium-headless-shell ([#748](https://github.com/cnpm/cnpmcore/issues/748)) ([3a3aa81](3a3aa818a3)), closes [#742](https://github.com/cnpm/cnpmcore/issues/742)
2025-02-07 14:12:55 +00:00
Beace
3a3aa818a3 feat(mirror): add chromium-headless-shell (#748)
在内部也遇到了,感觉直接加就行,可以试试看看 @fengmk2 

close #742 

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

## Summary by CodeRabbit

- **New Features**
- Introduced enhanced support for the chromium-headless-shell binary
with updated download options across multiple operating systems,
including popular Linux distributions, macOS (with arm64 support), and
Windows. This improvement ensures smoother integration and broader
compatibility for users running different platforms.

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

---------

Co-authored-by: fengmk2 <fengmk2@gmail.com>
2025-02-07 22:11:25 +08:00
semantic-release-bot
4012f584ba Release 3.71.3
[skip ci]

## [3.71.3](https://github.com/cnpm/cnpmcore/compare/v3.71.2...v3.71.3) (2024-12-23)

### Bug Fixes

* incorrect request headers in proxy mode and deleted unparsable cached data ([#719](https://github.com/cnpm/cnpmcore/issues/719)) ([2780c53](2780c532e1))
2024-12-23 06:43:23 +00:00
hezhengxu2018
2780c532e1 fix: incorrect request headers in proxy mode and deleted unparsable cached data (#719)
proxy时因为一个低级的拼写错误没有正确的携带请求头,导致代理模式时返回的数据不正确。但是现在用户发起的请求中的user-agent和x-forwarded等头部信息也没有正确的携带。虽然影响不大但还是想和跑批时更新的请求做一下区分。


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

- **Bug Fixes**
- Enhanced error handling and logging for task execution, improving
traceability.
  
- **Improvements**
- Updated HTTP header access method for better alignment with context
structure.
- Clarified logic for manifest retrieval based on file type, ensuring
correct API usage.
- Streamlined cache handling and response generation logic in package
management.
- Improved method visibility and organization within the cache service
and controller.
- Simplified task creation logic and cache removal processes in the
controller.
- Updated expected outcomes for cache-related operations in the test
cases.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-12-23 14:41:56 +08:00
semantic-release-bot
638a3da767 Release 3.71.2
[skip ci]

## [3.71.2](https://github.com/cnpm/cnpmcore/compare/v3.71.1...v3.71.2) (2024-12-18)

### Bug Fixes

* **search:** author is `???` in result ([#741](https://github.com/cnpm/cnpmcore/issues/741)) ([acffb14](acffb14ea0)), closes [/github.com/npm/cli/pull/7407/files#diff-4bc15933c685fc9a9ce8be0c13a2f067f5e2b3334bacd6664bdfa7ddc46aedb6L58](https://github.com/cnpm//github.com/npm/cli/pull/7407/files/issues/diff-4bc15933c685fc9a9ce8be0c13a2f067f5e2b3334bacd6664bdfa7ddc46aedb6L58) [/github.com/npm/cli/pull/7407/files#diff-4bc15933c685fc9a9ce8be0c13a2f067f5e2b3334bacd6664bdfa7ddc46aedb6R162](https://github.com/cnpm//github.com/npm/cli/pull/7407/files/issues/diff-4bc15933c685fc9a9ce8be0c13a2f067f5e2b3334bacd6664bdfa7ddc46aedb6R162)
2024-12-18 12:47:03 +00:00
Kevin Cui
acffb14ea0 fix(search): author is ??? in result (#741)
### 改动原因

在今年 4 月份,npm cli 对 search 做了[一些改动],在 4 月份以前(npm cli < `v10.6.0`),采用的是
`maintainers` 字段,而在 https://github.com/npm/cli/pull/7407 后,改为使用:
`publisher` 字段。

导致当 npm 版本大于等于 `v10.6.0` 后,search 结果中的 `author` 将变成 `???`,如图:

![CleanShot 2024-12-18 at 16 46
58@2x](https://github.com/user-attachments/assets/4b97bf63-78cb-4720-8c00-469eeff3e92f)

预期的结果应该为:

![CleanShot 2024-12-18 at 16 47
33@2x](https://github.com/user-attachments/assets/2d7fca89-4383-42bd-8b83-66257efe65e8)

### 技术细节说明

当前改动没有为 `es` 增加新的索引,原因是处于以下考虑:

1. es 的 `mapping` 一旦创建,就无法修改(虽然使用了 `dynamic: true` 但无法细粒度的进行控制)
2. 源数据中的 `_npmUser` 已经有相关信息了,没有必要为此浪费额外的磁盘空间
3. 如果想对以前的数据进行更新会比较麻烦,性价比较低

npm cli 老版本:
https://github.com/npm/cli/pull/7407/files#diff-4bc15933c685fc9a9ce8be0c13a2f067f5e2b3334bacd6664bdfa7ddc46aedb6L58
npm cli 新版本:
https://github.com/npm/cli/pull/7407/files#diff-4bc15933c685fc9a9ce8be0c13a2f067f5e2b3334bacd6664bdfa7ddc46aedb6R162

### 其他

相关 PR: https://github.com/cnpm/cnpmcore/pull/513

PTAL @Beace @fengmk2 @elrrrrrrr

[一些改动]: https://github.com/npm/cli/pull/7407

Signed-off-by: Kevin Cui <bh@bugs.cc>
2024-12-18 20:45:32 +08:00
semantic-release-bot
feba680795 Release 3.71.1
[skip ci]

## [3.71.1](https://github.com/cnpm/cnpmcore/compare/v3.71.0...v3.71.1) (2024-12-09)

### Bug Fixes

* ignore npm registry 404 status response on sync process ([#740](https://github.com/cnpm/cnpmcore/issues/740)) ([57226c5](57226c57a6))
2024-12-09 11:29:42 +00:00
fengmk2
57226c57a6 fix: ignore npm registry 404 status response on sync process (#740)
closes https://github.com/cnpm/cnpmcore/issues/739

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

## Summary by CodeRabbit

- **New Features**
- Enhanced error handling for package synchronization, including
specific logging for package not found scenarios.
	- Simplified criteria for identifying removed packages.

- **Bug Fixes**
	- Corrected documentation for the `syncMode` property.

- **Chores**
	- Updated dependency versions in `package.json`.

- **Tests**
- Added new test cases and refined existing assertions to improve
logging and error handling verification.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-12-09 19:28:08 +08:00
semantic-release-bot
167e37c241 Release 3.71.0
[skip ci]

## [3.71.0](https://github.com/cnpm/cnpmcore/compare/v3.70.0...v3.71.0) (2024-11-30)

### Features

* sync chrome-for-testing json files ([#737](https://github.com/cnpm/cnpmcore/issues/737)) ([9bb12fd](9bb12fde12))
2024-11-30 14:31:33 +00:00
fengmk2
9bb12fde12 feat: sync chrome-for-testing json files (#737)
closes https://github.com/cnpm/cnpmcore/issues/730

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

## Summary by CodeRabbit

- **New Features**
- Enhanced data fetching for Chrome for Testing with new JSON entries
for version management.
  
- **Bug Fixes**
- Improved data validation in tests to ensure correct structure and
content of fetched data.

- **Tests**
- Added assertions to validate properties of fetched items in the
ChromeForTestingBinary tests.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-11-30 22:29:55 +08:00
semantic-release-bot
1d128e280c Release 3.70.0
[skip ci]

## [3.70.0](https://github.com/cnpm/cnpmcore/compare/v3.69.0...v3.70.0) (2024-11-30)

### Features

* support PostgreSQL ([#733](https://github.com/cnpm/cnpmcore/issues/733)) ([f240799](f240799fa2))
2024-11-30 13:58:14 +00:00
fengmk2
f240799fa2 feat: support PostgreSQL (#733)
closes https://github.com/cnpm/cnpmcore/issues/731

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

## Release Notes

- **New Features**
- Added support for PostgreSQL alongside MySQL, including a new database
setup script and comprehensive documentation for local development.
- Introduced a new CI job for PostgreSQL testing in the GitHub Actions
workflow.
- Enhanced the `README.md` and `DEVELOPER.md` files to provide clearer
instructions for using both database systems.
- Added new environment variable configurations for PostgreSQL in the
Docker deployment documentation.

- **Bug Fixes**
- Improved error handling in tests for duplicate entries to accommodate
both MySQL and PostgreSQL error messages.

- **Documentation**
- Updated setup instructions for PostgreSQL and clarified MySQL setup in
the documentation.
	- Enhanced contributor information in the README.
- Expanded instructions for setting up Elasticsearch and Kibana,
including environment variable configurations.

- **Chores**
- Updated package dependencies to include PostgreSQL client libraries
and modified scripts to support both databases.
	- Changed the base image in the Dockerfile to a newer Node.js version.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-11-30 21:56:45 +08:00
semantic-release-bot
dd15b08fa2 Release 3.69.0
[skip ci]

## [3.69.0](https://github.com/cnpm/cnpmcore/compare/v3.68.0...v3.69.0) (2024-11-30)

### Features

* mirror deno ([#736](https://github.com/cnpm/cnpmcore/issues/736)) ([6de0876](6de0876d35))
2024-11-30 08:36:07 +00:00
fengmk2
6de0876d35 feat: mirror deno (#736)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced support for the Deno runtime, providing users with a modern
runtime for JavaScript and TypeScript.
- **Documentation**
- Updated configuration to include Deno, ensuring users can easily
access and utilize this new option.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-11-30 16:34:36 +08:00
semantic-release-bot
63a8473af7 Release 3.68.0
[skip ci]

## [3.68.0](https://github.com/cnpm/cnpmcore/compare/v3.67.1...v3.68.0) (2024-11-30)

### Features

* enable allowH2 by default and require Node.js >= 18.20.0 ([#734](https://github.com/cnpm/cnpmcore/issues/734)) ([9b01383](9b01383210))
2024-11-30 07:54:28 +00:00
fengmk2
9b01383210 feat: enable allowH2 by default and require Node.js >= 18.20.0 (#734)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

- **New Features**
- Enhanced error handling across various services by introducing a
centralized timeout error checking function.
	- HTTP/2 support enabled in the HTTP client configuration.

- **Bug Fixes**
	- Corrected a typographical error in comments for better clarity.

- **Documentation**
	- Updated Node.js version requirements in the project configuration.

- **Tests**
- Improved test cases for `NpmChangesStream` and `TaskRepository` to
ensure accurate behavior and performance.

- **Chores**
	- Updated Node.js version in CI workflow for more precise testing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-11-30 15:53:00 +08:00
fengmk2
b808ebcd60 chore: security support >= 3.0.0 (#732)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Documentation**
- Updated the "Supported Versions" section in the SECURITY.md file to
reflect that security updates are now provided for versions 3.0.0 and
above.
- Minor formatting adjustments made in the "Disclosure Policy" section
for consistency.
- **Chores**
- Simplified debugger configurations by removing explicit protocol and
port settings from the `.vscode/launch.json` file.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: elrrrrrrr <elrrrrrrr@gmail.com>
2024-11-24 12:59:15 +08:00
semantic-release-bot
71cc3381d7 Release 3.67.1
[skip ci]

## [3.67.1](https://github.com/cnpm/cnpmcore/compare/v3.67.0...v3.67.1) (2024-11-14)

### Bug Fixes

* adapter new html format on nodejs.org/dist ([#728](https://github.com/cnpm/cnpmcore/issues/728)) ([914b59c](914b59c7ef))
2024-11-14 15:14:40 +00:00
fengmk2
914b59c7ef fix: adapter new html format on nodejs.org/dist (#728)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Enhanced file fetching capabilities with updated logic to handle a
broader range of file formats and attributes.
- Introduced a new index HTML page for Node.js distribution version
18.15.0, listing downloadable files with metadata.

- **Bug Fixes**
- Improved regex for matching HTML anchor tags to accurately capture
additional file types and structures.

- **Tests**
- Added new test cases for the `fetch()` method to verify functionality
against the Node.js distribution version 18.15.0.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-11-14 23:13:03 +08:00
semantic-release-bot
ac4709a7be Release 3.67.0
[skip ci]

## [3.67.0](https://github.com/cnpm/cnpmcore/compare/v3.66.0...v3.67.0) (2024-11-09)

### Features

* say goodbye to alinode ([#726](https://github.com/cnpm/cnpmcore/issues/726)) ([99a5ef1](99a5ef1715))
2024-11-09 07:09:12 +00:00
fengmk2
99a5ef1715 feat: say goodbye to alinode (#726)
> getaddrinfo ENOTFOUND alinode.aliyun.com

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

## Summary by CodeRabbit

- **New Features**
- Introduced a new property to disable the `alinode` binary, affecting
its availability in the application.

- **Improvements**
- Updated the `ignoreDownloadStatuses` for the `python` binary to
enhance type safety, ensuring proper adherence to expected data types.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-11-09 15:07:44 +08:00
semantic-release-bot
2edbec6008 Release 3.66.0
[skip ci]

## [3.66.0](https://github.com/cnpm/cnpmcore/compare/v3.65.0...v3.66.0) (2024-11-03)

### Features

* compatible verdaccio path style ([#723](https://github.com/cnpm/cnpmcore/issues/723)) ([7158e66](7158e66c9f))
2024-11-03 12:37:16 +00:00
ZhengJin
7158e66c9f feat: compatible verdaccio path style (#723)
兼容Verdaccio下载地址风格,镜像库从Verdaccio切换至cnpmcore后无需大面积调整lock文件

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

- **New Features**
- Introduced a comprehensive test suite for the download functionality,
ensuring robust behavior for various package retrieval scenarios.
- Enhanced validation for CORS requests and CDN redirection during
package downloads.
- Added new test cases for downloading version tarballs with scoped
package handling.

- **Bug Fixes**
- Improved error handling for non-existent packages and versions,
providing clearer error messages.
- Ensured proper handling of deprecated download paths and
scoped/non-scoped package names.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-11-03 20:35:45 +08:00
semantic-release-bot
4facf90ae0 Release 3.65.0
[skip ci]

## [3.65.0](https://github.com/cnpm/cnpmcore/compare/v3.64.0...v3.65.0) (2024-10-26)

### Features

* strict validate deps ([#720](https://github.com/cnpm/cnpmcore/issues/720)) ([12650ac](12650acf72))
2024-10-26 11:59:49 +00:00
elrrrrrrr
12650acf72 feat: strict validate deps (#720)
> During the sync process, dependency installation errors may occur due
to incorrect timing or incorrect declaration.
1. ⚙️ Added `strictValidatePackageDeps` configuration, disabled by
default.
2. 🔄 In publish process, if the dependencies for the current version do
not exist, it will be interrupted.
3. 📦 In synch process, will automatically enter the queue to wait for
the next synchronization attempt (up to 3 retries).
4. ♻️ Packages that have already been published or synchronized will not
be affected.

-----------

> 在版本同步时,由于同步时机或自身依赖声明错误,导致依赖安装报错。
1. ⚙️ 新增 `strictValidatePackageDeps` 配置,默认关闭
2. 🔄 在包发布时,如果当前版本的 `dependencies` 不存在,则中断发布
3. 📦 在包同步时,如果校验未通过,则中断发布流程,自动进入队列等待下次同步(最多重试3次)
4. ♻️ 已发布、同步的包不受影响

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

## Release Notes

- **New Features**
- Introduced strict validation for package dependencies during
publishing and synchronization.
- Added a new configuration option for enforcing dependency validation.

- **Bug Fixes**
- Enhanced error handling for dependency validation failures, allowing
for task retries.

- **Tests**
- Added new test cases to ensure proper validation of package
dependencies under strict settings.
- Created a new test suite for handling invalid dependencies in package
synchronization.

- **Chores**
- Updated logging for package synchronization processes to improve
clarity and error tracking.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-10-26 19:58:07 +08:00
semantic-release-bot
2b812a161e Release 3.64.0
[skip ci]

## [3.64.0](https://github.com/cnpm/cnpmcore/compare/v3.63.1...v3.64.0) (2024-10-18)

### Features

* mirror protobuf binary ([#717](https://github.com/cnpm/cnpmcore/issues/717)) ([d6f0e1d](d6f0e1d866))
2024-10-18 15:35:28 +00:00
akitaSummer
d6f0e1d866 feat: mirror protobuf binary (#717)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added support for Protocol Buffers with a new binary entry in the
configuration.

- **Tests**
- Introduced a new test case to verify the fetching of release data for
the Protocol Buffers repository from GitHub.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: shixia.ly <shixia.ly@antgroup.com>
2024-10-18 23:33:39 +08:00
semantic-release-bot
efac8a97e2 Release 3.63.1
[skip ci]

## [3.63.1](https://github.com/cnpm/cnpmcore/compare/v3.63.0...v3.63.1) (2024-10-13)

### Bug Fixes

* change skia-canvas to github release ([#715](https://github.com/cnpm/cnpmcore/issues/715)) ([99a8660](99a86600db))
2024-10-13 03:22:57 +00:00
fengmk2
99a86600db fix: change skia-canvas to github release (#715)
closes https://github.com/cnpm/cnpmcore/issues/710

pick from https://github.com/cnpm/cnpmcore/pull/712

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

## Summary by CodeRabbit

- **New Features**
- Updated the `binaries` configuration to include new entries and
modified existing ones, enhancing the variety and sources of available
binaries.
  
- **Bug Fixes**
- Improved the test coverage for the `GithubBinary` class with a new
test case to ensure proper fetching of `skia-canvas` release data from
GitHub.

- **Refactor**
- Removed outdated test cases related to the `skia-canvas` package from
the `NodePreGypBinary` test suite, streamlining the testing process.

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

Co-authored-by: ltxhhz <ltxhhz@qq.com>
2024-10-13 11:21:31 +08:00
fengmk2
b0cd0ba387 chore: change 3.63.0.sql (#714) 2024-10-13 10:41:07 +08:00
semantic-release-bot
d987bf4a55 Release 3.63.0
[skip ci]

## [3.63.0](https://github.com/cnpm/cnpmcore/compare/v3.62.2...v3.63.0) (2024-10-13)

### Features

* proxy mode [sql changed] ([#571](https://github.com/cnpm/cnpmcore/issues/571)) ([91aea0f](91aea0f106))
2024-10-13 02:23:31 +00:00
hezhengxu2018
91aea0f106 feat: proxy mode [sql changed] (#571)
closes https://github.com/cnpm/cnpmcore/issues/366

开启代理模式时如果找不到依赖会直接返回上游仓库的manifest信息并缓存于nfs,当请求的tgz文件不存在时从上游仓库获取并返回,同时创建对应版本的同步任务。每小时检查更新已缓存的manifest文件保证上游仓库发布新版本时不会因为缓存落后而404。

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

- **New Features**
- Introduced proxy cache management for package manifests and versions.
  - Added new HTTP methods for managing proxy caches.
- Implemented scheduled workers for updating and synchronizing proxy
cache.

- **Updates**
  - Expanded `SyncMode` enum to include a new value `proxy`.
- Updated constants with `PROXY_CACHE_DIR_NAME` and
`ABBREVIATED_META_TYPE`.

- **Tests**
- Added comprehensive test cases for `ProxyCacheService`,
`ProxyCacheRepository`, and related controllers.
- Verified functionality of scheduled workers for proxy cache updates
and synchronization.
- Enhanced testing coverage for handling package downloads in proxy
mode.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: fengmk2 <suqian.yf@antgroup.com>
2024-10-13 10:21:57 +08:00
fengmk2
75d3a66b5c chore: brew install mysql v9 (#713)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Documentation**
- Updated setup instructions to reflect the requirement for MySQL 9,
removing references to MySQL 5.7 and 8.
- Removed outdated troubleshooting information related to MySQL 8
authentication issues.

- **Chores**
- Updated `.gitignore` to include `.egg/` and improved formatting for
`.DS_Store`.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-10-12 09:48:46 +08:00
semantic-release-bot
44ca113931 Release 3.62.2
[skip ci]

## [3.62.2](https://github.com/cnpm/cnpmcore/compare/v3.62.1...v3.62.2) (2024-10-10)

### Bug Fixes

* dup clz name ([#711](https://github.com/cnpm/cnpmcore/issues/711)) ([f7c49e5](f7c49e55fa))
2024-10-10 11:18:41 +00:00
elrrrrrrr
f7c49e55fa fix: dup clz name (#711)
> https://github.com/eggjs/tegg/pull/242/files 
* 🤖 Updated the duplicate class name, fix the error when dev.
------
> https://github.com/eggjs/tegg/pull/242/files 
* 🤖 修改同名 class 名称定义,修复本地启动报错 

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

## Summary by CodeRabbit

- **New Features**
- Enhanced clarity in event handling by renaming various package-related
event classes for better understanding.

- **Bug Fixes**
- Updated test cases to reflect the renamed event classes, ensuring
accurate functionality in the testing framework.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-10-10 17:04:59 +08:00
semantic-release-bot
e6ed2215a4 Release 3.62.1
[skip ci]

## [3.62.1](https://github.com/cnpm/cnpmcore/compare/v3.62.0...v3.62.1) (2024-09-25)

### Reverts

* Revert "feat: set allowH2 to true and require Node.js >= 18 (#705)" (#707) ([526b66a](526b66a93c)), closes [#705](https://github.com/cnpm/cnpmcore/issues/705) [#707](https://github.com/cnpm/cnpmcore/issues/707)
2024-09-25 10:37:30 +00:00
fengmk2
526b66a93c Revert "feat: set allowH2 to true and require Node.js >= 18 (#705)" (#707)
This reverts commit 9a7994090b.


![image](https://github.com/user-attachments/assets/eeb9ea95-60ec-4bcf-a695-60be303e2f5f)


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

## Summary by CodeRabbit

- **New Features**
	- Updated HTTP client configuration to enhance compatibility.
	- Adjusted minimum Node.js version requirement for broader support.

- **Bug Fixes**
	- Removed HTTP/2 support from the HTTP client configuration.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-09-25 18:36:04 +08:00
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
293 changed files with 48772 additions and 1999 deletions

6
.dockerignore Normal file
View File

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

9
.env.example Normal file
View File

@@ -0,0 +1,9 @@
# CNPMCORE_DATABASE_TYPE=MySQL
# CNPMCORE_DATABASE_USER=root
# CNPMCORE_DATABASE_PASSWORD=
# CNPMCORE_DATABASE_NAME=cnpmcore
# CNPMCORE_DATABASE_TYPE=PostgreSQL
# CNPMCORE_DATABASE_USER=postgres
# CNPMCORE_DATABASE_PASSWORD=postgres
# CNPMCORE_DATABASE_NAME=cnpmcore

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,9 +3,74 @@
name: Node.js CI
on: [push, pull_request]
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test-postgresql-fs-nfs:
runs-on: ${{ matrix.os }}
services:
# https://docs.github.com/en/actions/use-cases-and-examples/using-containerized-services/creating-postgresql-service-containers
# Label used to access the service container
postgres:
# Docker Hub image
image: postgres
# Provide the password for postgres
env:
POSTGRES_PASSWORD: postgres
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
# Maps tcp port 5432 on service container to the host
- 5432:5432
redis:
# https://docs.github.com/en/actions/using-containerized-services/about-service-containers#example-mapping-redis-ports
image: redis
ports:
# Opens tcp port 6379 on the host and service container
- 6379:6379
strategy:
fail-fast: false
matrix:
node-version: [18.20.0, 18, 20, 22]
os: [ubuntu-latest]
steps:
- name: Checkout Git Source
uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install Dependencies
run: npm i -g npminstall && npminstall
- name: Continuous Integration
run: npm run ci:postgresql
env:
# The hostname used to communicate with the PostgreSQL service container
POSTGRES_HOST: localhost
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
# The default PostgreSQL port
POSTGRES_PORT: 5432
- name: Code Coverage
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
test-mysql57-fs-nfs:
runs-on: ${{ matrix.os }}
@@ -28,15 +93,15 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [16, 18, 20]
node-version: [18.20.0, 18, 20, 22]
os: [ubuntu-latest]
steps:
- name: Checkout Git Source
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
@@ -79,15 +144,15 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [16, 18, 20]
node-version: [18.20.0, 18, 20, 22]
os: [ubuntu-latest]
steps:
- name: Checkout Git Source
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

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

2
.gitignore vendored
View File

@@ -120,3 +120,5 @@ dist
.DS_Store
run
!test/ctx_register.js
.egg/

4
.vscode/launch.json vendored
View File

@@ -17,8 +17,6 @@
],
"console": "integratedTerminal",
"restart": true,
"protocol": "auto",
"port": 9229,
"autoAttachChildProcesses": true
},
{
@@ -32,8 +30,6 @@
"--",
"--inspect-brk"
],
"protocol": "auto",
"port": 9229,
"autoAttachChildProcesses": true
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -2,19 +2,37 @@
## 环境初始化
本项目的外部服务依赖有MySQL 数据服务、Redis 缓存服务。
本项目的外部服务依赖有MySQL 数据库或 PostgreSQL 数据库、Redis 缓存服务。
生成本地开发环境配置文件:
```bash
cp .env.example .env
```
可以通过 Docker 来快速启动本地开发环境:
MySQL 开发环境:
```bash
# 启动本地依赖服务
docker-compose up -d
# 启动本地依赖服务 - MySQL + Redis
docker-compose -f docker-compose.yml up -d
# 关闭本地依赖服务
docker-compose down
docker-compose -f docker-compose.yml down
```
> 手动初始化依赖服务参见[文档](./docs/setup.md)
PostgreSQL 开发环境:
```bash
# 启动本地依赖服务 - PostgreSQL + Redis
docker-compose -f docker-compose-postgres.yml up -d
# 关闭本地依赖服务
docker-compose -f docker-compose-postgres.yml down
```
> 手动初始化依赖服务参见[本地开发环境 - MySQL](./docs/setup.md) 或 [本地开发环境 - PostgreSQL](./docs/setup-with-postgresql.md)
## 本地开发
@@ -24,25 +42,69 @@ docker-compose down
npm install
```
### 开发运行
### 开发运行 - MySQL
```bash
# 初始化数据库
MYSQL_DATABASE=cnpmcore bash ./prepare-database.sh
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-mysql.sh
# 启动 Web 服务
npm run dev
# 访问
curl -v http://127.0.0.1:7001
# cnpmcore_admin 注册管理员
npm login --registry=http://127.0.0.1:7001
```
### 开发运行 - PostgreSQL
```bash
# 初始化数据库
CNPMCORE_DATABASE_NAME=cnpmcore bash ./prepare-database-postgresql.sh
# 启动 Web 服务
npm run dev:postgresql
# 访问
curl -v http://127.0.0.1:7001
```
### 登录和测试发包
> cnpmcore 默认不开放注册,可以通过 `config.default.ts` 中的 `allowPublicRegistration` 配置开启,否则只有管理员可以登录
注册 cnpmcore_admin 管理员
```bash
npm login --registry=http://127.0.0.1:7001
# 验证登录
npm whoami --registry=http://127.0.0.1:7001
```
发包
```bash
npm publish --registry=http://127.0.0.1:7001
```
### 单元测试
MySQL
```bash
npm run test
```
PostgreSQL
```bash
npm run test:postgresql
```
## 项目结构
```txt
@@ -268,9 +330,9 @@ Repository 依赖 Model然后被 Service 和 Controller 依赖
可能需要涉及3个地方的修改
1. sql/*.sql
2. repository/model/*.ts
3. core/entity/*.ts
1. `sql/mysql/*.sql`, `sql/postgresql/*.sql`
2. `repository/model/*.ts`
3. `core/entity/*.ts`
目前还不会做 Model 到 SQL 的自动转换生成,核心原因有:

17
Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM node:20
# 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,24 @@
}
```
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,
allowPublicRegistration: true,
// 放开注册配置
};
return config;
}
```
### 🧑‍🤝‍🧑 集成 cnpmcore
@@ -90,7 +111,7 @@
│   └── package.json
```
* 添加 `package.json` ,声明 infra 作为一个 eggModule 单元
* 添加 `package.json` ,声明 infra 作为一个 eggModule 单元
```JSON
{
@@ -101,7 +122,7 @@
}
```
* 添加 `XXXAdapter.ts` 在对应的 Adapter 中继承 cnpmcore 默认的 Adapter以 AuthAdapter 为例
* 添加 `XXXAdapter.ts` 在对应的 Adapter 中继承 cnpmcore 默认的 Adapter以 AuthAdapter 为例
```typescript
import { AccessLevel, SingletonProto } from '@eggjs/tegg';
@@ -145,12 +166,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 +208,7 @@ export class MyAuthAdapter extends AuthAdapter {
```
修改 HelloController 的实现,实际也可以通过登录中心回调、页面确认等方式实现
```typescript
// 触发回调接口,会自动完成用户创建
await this.httpclient.request(`${ctx.origin}/-/v1/login/sso/${name}`, { method: 'POST' });
@@ -195,22 +219,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.
Reimplement 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
@@ -24,18 +23,10 @@ See [INTEGRATE.md](INTEGRATE.md)
[MIT](LICENSE)
<!-- GITCONTRIBUTOR_START -->
## 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/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/>
[![Contributors](https://contrib.rocks/image?repo=cnpm/cnpmcore)](https://github.com/cnpm/cnpmcore/graphs/contributors)
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`.
<!-- GITCONTRIBUTOR_END -->
Made with [contributors-img](https://contrib.rocks).
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fcnpm%2Fcnpmcore.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fcnpm%2Fcnpmcore?ref=badge_large)

View File

@@ -6,7 +6,7 @@ Currently being supported with security updates.
| Version | Supported |
| -------- | ------------------ |
| >= 1.0.0 | :white_check_mark: |
| >= 3.0.0 | :white_check_mark: |
## Reporting a Vulnerability
@@ -35,7 +35,7 @@ When the security team receives a security bug report, they will assign it
to a primary handler. This person will coordinate the fix and release
process, involving the following steps:
* Confirm the problem and determine the affected versions.
* Audit code to find any potential similar problems.
* Prepare fixes for all releases still under maintenance. These fixes
* Confirm the problem and determine the affected versions.
* Audit code to find any potential similar problems.
* Prepare fixes for all releases still under maintenance. These fixes
will be released as fast as possible to NPM.

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');
}

40
app/common/EnvUtil.ts Normal file
View File

@@ -0,0 +1,40 @@
export type ValueType = 'string' | 'boolean' | 'number';
export function env(key: string, valueType: ValueType, defaultValue: string): string;
export function env(key: string, valueType: ValueType, defaultValue: boolean): boolean;
export function env(key: string, valueType: ValueType, defaultValue: number): number;
export function env(key: string, valueType: ValueType, defaultValue: string | boolean | number): string | boolean | number {
let value = process.env[key];
if (typeof value === 'string') {
value = value.trim();
}
if (!value) {
return defaultValue;
}
if (valueType === 'string') {
return value;
}
if (valueType === 'boolean') {
let booleanValue = false;
if (value === 'true' || value === '1') {
booleanValue = true;
} else if (value === 'false' || value === '0') {
booleanValue = false;
} else {
throw new TypeError(`Invalid boolean value: ${value} on process.env.${key}`);
}
return booleanValue;
}
if (valueType === 'number') {
const numberValue = Number(value);
if (isNaN(numberValue)) {
throw new TypeError(`Invalid number value: ${value} on process.env.${key}`);
}
return numberValue;
}
throw new TypeError(`Invalid value type: ${valueType}`);
}

27
app/common/ErrorUtil.ts Normal file
View File

@@ -0,0 +1,27 @@
const TimeoutErrorNames = [
'HttpClientRequestTimeoutError',
'HttpClientConnectTimeoutError',
'ConnectionError',
'ConnectTimeoutError',
'BodyTimeoutError',
'ResponseTimeoutError',
];
export function isTimeoutError(err: Error) {
if (TimeoutErrorNames.includes(err.name)) {
return true;
}
if (err instanceof AggregateError && err.errors) {
for (const subError of err.errors) {
if (TimeoutErrorNames.includes(subError.name)) {
return true;
}
}
}
if ('cause' in err && err.cause instanceof Error) {
if (TimeoutErrorNames.includes(err.cause.name)) {
return true;
}
}
return false;
}

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,8 @@ import {
HttpClientRequestOptions,
HttpClientResponse,
} from 'egg';
import { PackageManifestType } from '../../repository/PackageRepository';
import { isTimeoutError } from '../ErrorUtil';
type HttpMethod = HttpClientRequestOptions['method'];
@@ -40,7 +42,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
@@ -51,9 +53,14 @@ export class NPMRegistry {
// large package: https://r.cnpmjs.org/%40procore%2Fcore-icons
// https://r.cnpmjs.org/intraactive-sdk-ui 44s
const authorization = this.genAuthorizationHeader(optionalConfig?.remoteAuthToken);
return await this.request('GET', url, undefined, { timeout: 120000, headers: { authorization } });
return await this.request('GET', url, undefined, {
timeout: 120000,
headers: { authorization },
});
} catch (err: any) {
if (err.name === 'ResponseTimeoutError') throw err;
if (isTimeoutError(err)) {
throw err;
}
lastError = err;
}
retries--;
@@ -97,6 +104,7 @@ export class NPMRegistry {
data: params,
dataType: 'json',
timing: true,
retry: 3,
timeout: this.timeout,
followRedirect: true,
gzip: true,
@@ -109,7 +117,7 @@ export class NPMRegistry {
};
}
private genAuthorizationHeader(remoteAuthToken?:string) {
public genAuthorizationHeader(remoteAuthToken?:string) {
return remoteAuthToken ? `Bearer ${remoteAuthToken}` : '';
}
}

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);
@@ -64,7 +72,7 @@ export abstract class AbstractBinary {
for (const version of versions) {
if (!version.modules) continue;
const modulesVersion = parseInt(version.modules);
// node v6.0.0 moduels 48 min
// node v6.0.0 modules 48 min
if (modulesVersion >= 48 && !nodeABIVersions.includes(modulesVersion)) {
nodeABIVersions.push(modulesVersion);
}
@@ -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,151 @@ 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();
this.dirItems['/'].push({
name: 'known-good-versions-with-downloads.json',
date: data.timestamp,
size: '-',
isDir: false,
url: jsonApiEndpoint,
});
this.dirItems['/'].push({
name: 'latest-patch-versions-per-build.json',
date: data.timestamp,
size: '-',
isDir: false,
url: 'https://googlechromelabs.github.io/chrome-for-testing/latest-patch-versions-per-build.json',
});
this.dirItems['/'].push({
name: 'last-known-good-versions.json',
date: data.timestamp,
size: '-',
isDir: false,
url: 'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json',
});
// "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

@@ -1,3 +1,4 @@
import { basename } from 'node:path';
import { SingletonProto } from '@eggjs/tegg';
import { BinaryType } from '../../enum/Binary';
import binaries, { BinaryName } from '../../../../config/binaries';
@@ -6,7 +7,7 @@ import { AbstractBinary, FetchResult, BinaryItem, BinaryAdapter } from './Abstra
@SingletonProto()
@BinaryAdapter(BinaryType.Node)
export class NodeBinary extends AbstractBinary {
async init() {
async initFetch() {
// do nothing
return;
}
@@ -21,26 +22,39 @@ export class NodeBinary extends AbstractBinary {
// <a href="index.tab">index.tab</a> 17-Dec-2021 23:16 136319
// <a href="node-0.0.1.tar.gz">node-0.0.1.tar.gz</a> 26-Aug-2011 16:22 2846972
// <a href="node-v14.0.0-nightly20200119b318926634-linux-armv7l.tar.xz">node-v14.0.0-nightly20200119b318926634-linux-ar..&gt;</a> 19-Jan-2020 06:07 18565976
const re = /<a href="([^\"]+?)"[^>]*?>[^<]+?<\/a>\s+?([\w\-]+? \w{2}\:\d{2})\s+?(\d+|\-)/ig;
// new html format
// <a href="docs/">docs/</a> - -
// <a href="win-x64/">win-x64/</a> - -
// <a href="win-x86/">win-x86/</a> - -
// <a href="/dist/v18.15.0/SHASUMS256.txt.asc">SHASUMS256.txt.asc</a> 04-Nov-2024 17:29 3.7 KB
// <a href="/dist/v18.15.0/SHASUMS256.txt.sig">SHASUMS256.txt.sig</a> 04-Nov-2024 17:29 310 B
// <a href="/dist/v18.15.0/SHASUMS256.txt">SHASUMS256.txt</a> 04-Nov-2024 17:29 3.2 KB
const re = /<a href="([^\"]+?)"[^>]*?>[^<]+?<\/a>\s+?((?:[\w\-]+? \w{2}\:\d{2})|\-)\s+?([\d\.\-\s\w]+)/ig;
const matchs = html.matchAll(re);
const items: BinaryItem[] = [];
for (const m of matchs) {
const name = m[1];
let name = m[1];
const isDir = name.endsWith('/');
if (!isDir) {
// /dist/v18.15.0/SHASUMS256.txt => SHASUMS256.txt
name = basename(name);
}
const fileUrl = isDir ? '' : `${url}${name}`;
const date = m[2];
const size = m[3];
const size = m[3].trim();
if (size === '0') continue;
if (binaryConfig.ignoreFiles?.includes(`${dir}${name}`)) continue;
items.push({
const item = {
name,
isDir,
url: fileUrl,
size,
date,
ignoreDownloadStatuses: binaryConfig.options?.ignoreDownloadStatuses,
});
};
items.push(item);
}
return { items, nextParams: null };
}

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,18 @@ 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',
'ubuntu24.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',
'ubuntu24.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 +32,57 @@ 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',
'mac15': 'builds/chromium/%s/chromium-mac.zip',
'mac15-arm64': 'builds/chromium/%s/chromium-mac-arm64.zip',
'win64': 'builds/chromium/%s/chromium-win64.zip',
},
'chromium-headless-shell': {
'<unknown>': undefined,
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': 'builds/chromium/%s/chromium-headless-shell-linux.zip',
'ubuntu22.04-x64': 'builds/chromium/%s/chromium-headless-shell-linux.zip',
'ubuntu24.04-x64': 'builds/chromium/%s/chromium-headless-shell-linux.zip',
'ubuntu18.04-arm64': undefined,
'ubuntu20.04-arm64': 'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip',
'ubuntu22.04-arm64': 'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip',
'ubuntu24.04-arm64': 'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip',
'debian11-x64': 'builds/chromium/%s/chromium-headless-shell-linux.zip',
'debian11-arm64': 'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip',
'debian12-x64': 'builds/chromium/%s/chromium-headless-shell-linux.zip',
'debian12-arm64': 'builds/chromium/%s/chromium-headless-shell-linux-arm64.zip',
'mac10.13': undefined,
'mac10.14': undefined,
'mac10.15': undefined,
'mac11': 'builds/chromium/%s/chromium-headless-shell-mac.zip',
'mac11-arm64': 'builds/chromium/%s/chromium-headless-shell-mac-arm64.zip',
'mac12': 'builds/chromium/%s/chromium-headless-shell-mac.zip',
'mac12-arm64': 'builds/chromium/%s/chromium-headless-shell-mac-arm64.zip',
'mac13': 'builds/chromium/%s/chromium-headless-shell-mac.zip',
'mac13-arm64': 'builds/chromium/%s/chromium-headless-shell-mac-arm64.zip',
'mac14': 'builds/chromium/%s/chromium-headless-shell-mac.zip',
'mac14-arm64': 'builds/chromium/%s/chromium-headless-shell-mac-arm64.zip',
'mac15': 'builds/chromium/%s/chromium-headless-shell-mac.zip',
'mac15-arm64': 'builds/chromium/%s/chromium-headless-shell-mac-arm64.zip',
'win64': 'builds/chromium/%s/chromium-headless-shell-win64.zip',
},
'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',
'ubuntu24.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip',
'ubuntu18.04-arm64': undefined,
'ubuntu20.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip',
'ubuntu22.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip',
'ubuntu24.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip',
'debian11-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip',
'debian11-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip',
'debian12-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux.zip',
'debian12-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-linux-arm64.zip',
'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 +90,144 @@ 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',
'mac15': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac.zip',
'mac15-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac-arm64.zip',
'win64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-win64.zip',
},
'chromium-with-symbols': {
'chromium-tip-of-tree-headless-shell': {
'<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',
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'ubuntu22.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'ubuntu24.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'ubuntu18.04-arm64': undefined,
'ubuntu20.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'ubuntu22.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'ubuntu24.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'debian11-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'debian11-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'debian12-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'debian12-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'mac10.13': undefined,
'mac10.14': undefined,
'mac10.15': undefined,
'mac11': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac11-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
'mac12': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac12-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
'mac13': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac13-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
'mac14': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac14-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
'mac15': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac15-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
'win64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-win64.zip',
},
'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',
'ubuntu24.04-x64': 'builds/firefox/%s/firefox-ubuntu-24.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',
'ubuntu24.04-arm64': 'builds/firefox/%s/firefox-ubuntu-24.04-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.zip',
'mac10.14': 'builds/firefox/%s/firefox-mac.zip',
'mac10.15': 'builds/firefox/%s/firefox-mac.zip',
'mac11': 'builds/firefox/%s/firefox-mac.zip',
'mac11-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip',
'mac12': 'builds/firefox/%s/firefox-mac.zip',
'mac12-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip',
'mac13': 'builds/firefox/%s/firefox-mac.zip',
'mac13-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip',
'mac14': 'builds/firefox/%s/firefox-mac.zip',
'mac14-arm64': 'builds/firefox/%s/firefox-mac-arm64.zip',
'mac15': 'builds/firefox/%s/firefox-mac.zip',
'mac15-arm64': 'builds/firefox/%s/firefox-mac-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',
'ubuntu24.04-x64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-24.04.zip',
'ubuntu18.04-arm64': undefined,
'ubuntu20.04-arm64': undefined,
'ubuntu22.04-arm64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-22.04-arm64.zip',
'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',
'ubuntu24.04-arm64': 'builds/firefox-beta/%s/firefox-beta-ubuntu-24.04-arm64.zip',
'debian11-x64': 'builds/firefox-beta/%s/firefox-beta-debian-11.zip',
'debian11-arm64': 'builds/firefox-beta/%s/firefox-beta-debian-11-arm64.zip',
'debian12-x64': 'builds/firefox-beta/%s/firefox-beta-debian-12.zip',
'debian12-arm64': 'builds/firefox-beta/%s/firefox-beta-debian-12-arm64.zip',
'mac10.13': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
'mac10.14': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
'mac10.15': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
'mac11': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
'mac11-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip',
'mac12': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
'mac12-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip',
'mac13': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
'mac13-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip',
'mac14': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
'mac14-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-arm64.zip',
'mac15': 'builds/firefox-beta/%s/firefox-beta-mac.zip',
'mac15-arm64': 'builds/firefox-beta/%s/firefox-beta-mac-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',
'ubuntu24.04-x64': 'builds/webkit/%s/webkit-ubuntu-24.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',
'ubuntu24.04-arm64': 'builds/webkit/%s/webkit-ubuntu-24.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',
'mac15': 'builds/webkit/%s/webkit-mac-15.zip',
'mac15-arm64': 'builds/webkit/%s/webkit-mac-15-arm64.zip',
'win64': 'builds/webkit/%s/webkit-win64.zip',
},
'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',
'ubuntu24.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',
'ubuntu24.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 +235,79 @@ 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',
'mac15': 'builds/ffmpeg/%s/ffmpeg-mac.zip',
'mac15-arm64': 'builds/ffmpeg/%s/ffmpeg-mac-arm64.zip',
'win64': 'builds/ffmpeg/%s/ffmpeg-win64.zip',
},
};
'winldd': {
'<unknown>': undefined,
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': undefined,
'ubuntu22.04-x64': undefined,
'ubuntu24.04-x64': undefined,
'ubuntu18.04-arm64': undefined,
'ubuntu20.04-arm64': undefined,
'ubuntu22.04-arm64': undefined,
'ubuntu24.04-arm64': undefined,
'debian11-x64': undefined,
'debian11-arm64': undefined,
'debian12-x64': undefined,
'debian12-arm64': undefined,
'mac10.13': undefined,
'mac10.14': undefined,
'mac10.15': undefined,
'mac11': undefined,
'mac11-arm64': undefined,
'mac12': undefined,
'mac12-arm64': undefined,
'mac13': undefined,
'mac13-arm64': undefined,
'mac14': undefined,
'mac14-arm64': undefined,
'mac15': undefined,
'mac15-arm64': undefined,
'win64': 'builds/winldd/%s/winldd-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',
'ubuntu24.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',
'ubuntu24.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',
'mac15': 'builds/android/%s/android.zip',
'mac15-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;
}
@@ -158,19 +315,30 @@ export class PlaywrightBinary extends AbstractBinary {
if (!this.dirItems) {
const packageData = await this.requestJSON(PACKAGE_URL);
const nowDateISO = new Date().toISOString();
const buildDirs: BinaryItem[] = [];
for (const browserName of Object.keys(DOWNLOAD_PATHS)) {
if (browserName === 'chromium-headless-shell' || browserName === 'chromium-tip-of-tree-headless-shell') {
continue;
}
buildDirs.push({ name: `${browserName}/`, isDir: true, url: '', size: '-', date: nowDateISO });
}
this.dirItems = {
'/': [{ name: 'builds/', isDir: true, url: '', size: '-', date: nowDateISO }],
'/builds/': Object.keys(DOWNLOAD_PATHS).map(
dist => ({ name: `${dist}/`, isDir: true, url: '', size: '-', date: nowDateISO })),
...Object.fromEntries(Object.keys(DOWNLOAD_PATHS).map(dist => [ `/builds/${dist}/`, []])),
'/builds/': buildDirs,
};
for (const browserName of Object.keys(DOWNLOAD_PATHS)) {
if (browserName === 'chromium-headless-shell' || browserName === 'chromium-tip-of-tree-headless-shell') {
continue;
}
this.dirItems[`/builds/${browserName}/`] = [];
}
// Only download beta and release versions of packages to reduce amount of request
const packageVersions = Object.keys(packageData.versions)
.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(
@@ -195,19 +363,55 @@ export class PlaywrightBinary extends AbstractBinary {
}),
),
);
// if chromium-headless-shell not exists on browsers, copy chromium to chromium-headless-shell
if (!browsers.find(browser => browser.name === 'chromium-headless-shell')) {
const chromium = browsers.find(browser => browser.name === 'chromium');
// {
// "name": "chromium",
// "revision": "1155",
// "installByDefault": true,
// "browserVersion": "133.0.6943.16"
// }
if (chromium) {
browsers.push({
...chromium,
name: 'chromium-headless-shell',
});
}
}
// if chromium-tip-of-tree-headless-shell not exists on browsers, copy chromium-tip-of-tree to chromium-tip-of-tree-headless-shell
if (!browsers.find(browser => browser.name === 'chromium-tip-of-tree-headless-shell')) {
const chromiumTipOfTree = browsers.find(browser => browser.name === 'chromium-tip-of-tree');
if (chromiumTipOfTree) {
browsers.push({
...chromiumTipOfTree,
name: 'chromium-tip-of-tree-headless-shell',
});
}
}
for (const browser of browsers) {
const downloadPaths = DOWNLOAD_PATHS[browser.name];
if (!downloadPaths) continue;
let browserDirname = browser.name;
if (browser.name === 'chromium-headless-shell') {
// chromium-headless-shell should be under chromium
// https://playwright.azureedge.net/builds/chromium/1155/chromium-headless-shell-mac-arm64.zip
browserDirname = 'chromium';
} else if (browser.name === 'chromium-tip-of-tree-headless-shell') {
// chromium-tip-of-tree-headless-shell should be under chromium-tip-of-tree
// https://playwright.azureedge.net/builds/chromium-tip-of-tree/1293/chromium-tip-of-tree-headless-shell-mac-arm64.zip
browserDirname = 'chromium-tip-of-tree';
}
for (const [ platform, remotePath ] of Object.entries(downloadPaths)) {
if (typeof remotePath !== 'string') continue;
const revision = browser.revisionOverrides?.[platform] ?? browser.revision;
const itemDate = browser.browserVersion || revision;
const url = DOWNLOAD_HOST + util.format(remotePath, revision);
const name = path.basename(remotePath);
const dir = `/builds/${browser.name}/${revision}/`;
const dir = `/builds/${browserDirname}/${revision}/`;
if (!this.dirItems[dir]) {
this.dirItems[`/builds/${browser.name}/`].push({
this.dirItems[`/builds/${browserDirname}/`].push({
name: `${revision}/`,
isDir: true,
url: '',

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

@@ -28,7 +28,7 @@ export class NpmChangesStream extends AbstractChangeStream {
const db = this.getChangesStreamUrl(registry, since);
const { res } = await this.httpclient.request(db, {
streaming: true,
timeout: 10000,
timeout: 60000,
});
let buf = '';

View File

@@ -1,12 +1,21 @@
export const BUG_VERSIONS = 'bug-versions';
export const LATEST_TAG = 'latest';
export const GLOBAL_WORKER = 'GLOBAL_WORKER';
export const PROXY_CACHE_DIR_NAME = 'proxy-cache-packages';
export const ABBREVIATED_META_TYPE = 'application/vnd.npm.install-v1+json';
export const NOT_IMPLEMENTED_PATH = [ '/-/npm/v1/security/audits/quick', '/-/npm/v1/security/advisories/bulk' ];
export enum SyncMode {
none = 'none',
admin = 'admin',
proxy = 'proxy',
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

@@ -2,6 +2,7 @@ export enum TaskType {
SyncPackage = 'sync_package',
ChangesStream = 'changes_stream',
SyncBinary = 'sync_binary',
UpdateProxyCache = 'update_proxy_cache',
CreateHook = 'create_hook',
TriggerHook = 'trigger_hook',
}

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

@@ -0,0 +1,42 @@
import { Entity, EntityData } from './Entity';
import { EasyData } from '../util/EntityUtil';
import { DIST_NAMES } from './Package';
import { isPkgManifest } from '../service/ProxyCacheService';
import { PROXY_CACHE_DIR_NAME } from '../../common/constants';
interface ProxyCacheData extends EntityData {
fullname: string;
fileType: DIST_NAMES;
version?: string;
}
export type CreateProxyCacheData = Omit<EasyData<ProxyCacheData, 'id'>, 'id'| 'filePath'>;
export class ProxyCache extends Entity {
readonly fullname: string;
readonly fileType: DIST_NAMES;
readonly filePath: string;
readonly version?: string;
constructor(data: ProxyCacheData) {
super(data);
this.fullname = data.fullname;
this.fileType = data.fileType;
this.version = data.version;
if (isPkgManifest(data.fileType)) {
this.filePath = `/${PROXY_CACHE_DIR_NAME}/${data.fullname}/${data.fileType}`;
} else {
this.filePath = `/${PROXY_CACHE_DIR_NAME}/${data.fullname}/${data.version}/${data.fileType}`;
}
}
public static create(data: CreateProxyCacheData): ProxyCache {
const newData = { ...data, createdAt: new Date(), updatedAt: new Date() };
return new ProxyCache(newData);
}
public static update(data: ProxyCache): ProxyCache {
data.updatedAt = new Date();
return data;
}
}

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

@@ -3,8 +3,12 @@ import path from 'path';
import { Entity, EntityData } from './Entity';
import { EasyData, EntityUtil } from '../util/EntityUtil';
import { TaskType, TaskState } from '../../common/enum/Task';
import { PROXY_CACHE_DIR_NAME } from '../../common/constants';
import dayjs from '../../common/dayjs';
import { HookEvent } from './HookEvent';
import { DIST_NAMES } from './Package';
import { isPkgManifest } from '../service/ProxyCacheService';
import { InternalServerError } from 'egg-errors';
export const HOST_NAME = os.hostname();
export const PID = process.pid;
@@ -31,13 +35,19 @@ 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 type UpdateProxyCacheTaskOptions = {
fullname: string,
version?: string,
fileType: DIST_NAMES,
};
export interface CreateHookTaskData extends TaskBaseData {
@@ -51,11 +61,18 @@ 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 CreateUpdateProxyCacheTaskData extends TaskBaseData {
fullname: string,
version?: string,
fileType: DIST_NAMES,
filePath: string
}
export interface ChangesStreamTaskData extends TaskBaseData {
@@ -75,6 +92,7 @@ export type CreateHookTask = Task<CreateHookTaskData>;
export type TriggerHookTask = Task<TriggerHookTaskData>;
export type CreateSyncPackageTask = Task<CreateSyncPackageTaskData>;
export type ChangesStreamTask = Task<ChangesStreamTaskData>;
export type CreateUpdateProxyCacheTask = Task<CreateUpdateProxyCacheTaskData>;
export class Task<T extends TaskBaseData = TaskBaseData> extends Entity {
taskId: string;
@@ -131,12 +149,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 +249,34 @@ export class Task<T extends TaskBaseData = TaskBaseData> extends Entity {
return task;
}
public static needMergeWhenWaiting(type: TaskType) {
return [ TaskType.SyncBinary, TaskType.SyncPackage ].includes(type);
}
public static createUpdateProxyCache(targetName: string, options: UpdateProxyCacheTaskOptions):CreateUpdateProxyCacheTask {
if (!isPkgManifest(options.fileType)) {
throw new InternalServerError('should not update package version manifest.');
}
const filePath = `/${PROXY_CACHE_DIR_NAME}/${options.fullname}/${options.fileType}`;
const data = {
type: TaskType.UpdateProxyCache,
state: TaskState.Waiting,
targetName,
authorId: `pid_${PID}`,
authorIp: HOST_NAME,
data: {
taskWorker: '',
fullname: options.fullname,
version: options?.version,
fileType: options.fileType,
filePath,
},
};
const task = this.create(data);
task.logPath = `/${PROXY_CACHE_DIR_NAME}/${options.fullname}/update-manifest-log/${options.fileType.split('.json')[0]}-${dayjs().format('YYYY/MM/DDHHmm')}-${task.taskId}.log`;
return task;
}
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

@@ -24,77 +24,77 @@ class CacheCleanerEvent {
}
@Event(PACKAGE_UNPUBLISHED)
export class PackageUnpublished extends CacheCleanerEvent {
export class PackageUnpublishedCacheCleanEvent extends CacheCleanerEvent {
async handle(fullname: string) {
await this.removeCache(fullname);
}
}
@Event(PACKAGE_BLOCKED)
export class PackageBlocked extends CacheCleanerEvent {
export class PackageBlockedCacheCleanEvent extends CacheCleanerEvent {
async handle(fullname: string) {
await this.removeCache(fullname);
}
}
@Event(PACKAGE_UNBLOCKED)
export class PackageUnblocked extends CacheCleanerEvent {
export class PackageUnblockedCacheCleanEvent extends CacheCleanerEvent {
async handle(fullname: string) {
await this.removeCache(fullname);
}
}
@Event(PACKAGE_VERSION_ADDED)
export class PackageVersionAdded extends CacheCleanerEvent {
export class PackageVersionAddedCacheCleanEvent extends CacheCleanerEvent {
async handle(fullname: string) {
await this.removeCache(fullname);
}
}
@Event(PACKAGE_VERSION_REMOVED)
export class PackageVersionRemoved extends CacheCleanerEvent {
export class PackageVersionRemovedCacheCleanEvent extends CacheCleanerEvent {
async handle(fullname: string) {
await this.removeCache(fullname);
}
}
@Event(PACKAGE_TAG_ADDED)
export class PackageTagAdded extends CacheCleanerEvent {
export class PackageTagAddedCacheCleanEvent extends CacheCleanerEvent {
async handle(fullname: string) {
await this.removeCache(fullname);
}
}
@Event(PACKAGE_TAG_CHANGED)
export class PackageTagChanged extends CacheCleanerEvent {
export class PackageTagChangedCacheCleanEvent extends CacheCleanerEvent {
async handle(fullname: string) {
await this.removeCache(fullname);
}
}
@Event(PACKAGE_TAG_REMOVED)
export class PackageTagRemoved extends CacheCleanerEvent {
export class PackageTagRemovedCacheCleanEvent extends CacheCleanerEvent {
async handle(fullname: string) {
await this.removeCache(fullname);
}
}
@Event(PACKAGE_MAINTAINER_CHANGED)
export class PackageMaintainerChanged extends CacheCleanerEvent {
export class PackageMaintainerChangedCacheCleanEvent extends CacheCleanerEvent {
async handle(fullname: string) {
await this.removeCache(fullname);
}
}
@Event(PACKAGE_MAINTAINER_REMOVED)
export class PackageMaintainerRemoved extends CacheCleanerEvent {
export class PackageMaintainerRemovedCacheCleanEvent extends CacheCleanerEvent {
async handle(fullname: string) {
await this.removeCache(fullname);
}
}
@Event(PACKAGE_META_CHANGED)
export class PackageMetaChanged extends CacheCleanerEvent {
export class PackageMetaChangedCacheCleanEvent extends CacheCleanerEvent {
async handle(fullname: string) {
await this.removeCache(fullname);
}

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> {
@@ -44,7 +44,7 @@ class ChangesStreamEvent {
}
@Event(PACKAGE_UNPUBLISHED)
export class PackageUnpublished extends ChangesStreamEvent {
export class PackageUnpublishedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string) {
const change = await this.addChange(PACKAGE_UNPUBLISHED, fullname, {});
if (this.hookEnable) {
@@ -55,7 +55,7 @@ export class PackageUnpublished extends ChangesStreamEvent {
}
@Event(PACKAGE_VERSION_ADDED)
export class PackageVersionAdded extends ChangesStreamEvent {
export class PackageVersionAddedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string, version: string, tag?: string) {
const change = await this.addChange(PACKAGE_VERSION_ADDED, fullname, { version });
if (this.hookEnable) {
@@ -66,7 +66,7 @@ export class PackageVersionAdded extends ChangesStreamEvent {
}
@Event(PACKAGE_VERSION_REMOVED)
export class PackageVersionRemoved extends ChangesStreamEvent {
export class PackageVersionRemovedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string, version: string, tag?: string) {
const change = await this.addChange(PACKAGE_VERSION_REMOVED, fullname, { version });
if (this.hookEnable) {
@@ -77,7 +77,7 @@ export class PackageVersionRemoved extends ChangesStreamEvent {
}
@Event(PACKAGE_TAG_ADDED)
export class PackageTagAdded extends ChangesStreamEvent {
export class PackageTagAddedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string, tag: string) {
const change = await this.addChange(PACKAGE_TAG_ADDED, fullname, { tag });
if (this.hookEnable) {
@@ -88,7 +88,7 @@ export class PackageTagAdded extends ChangesStreamEvent {
}
@Event(PACKAGE_TAG_CHANGED)
export class PackageTagChanged extends ChangesStreamEvent {
export class PackageTagChangedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string, tag: string) {
const change = await this.addChange(PACKAGE_TAG_CHANGED, fullname, { tag });
if (this.hookEnable) {
@@ -99,7 +99,7 @@ export class PackageTagChanged extends ChangesStreamEvent {
}
@Event(PACKAGE_TAG_REMOVED)
export class PackageTagRemoved extends ChangesStreamEvent {
export class PackageTagRemovedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string, tag: string) {
const change = await this.addChange(PACKAGE_TAG_REMOVED, fullname, { tag });
if (this.hookEnable) {
@@ -110,7 +110,7 @@ export class PackageTagRemoved extends ChangesStreamEvent {
}
@Event(PACKAGE_MAINTAINER_CHANGED)
export class PackageMaintainerChanged extends ChangesStreamEvent {
export class PackageMaintainerChangedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string, maintainers: User[]) {
const change = await this.addChange(PACKAGE_MAINTAINER_CHANGED, fullname, {});
// TODO 应该比较差值,而不是全量推送
@@ -124,7 +124,7 @@ export class PackageMaintainerChanged extends ChangesStreamEvent {
}
@Event(PACKAGE_MAINTAINER_REMOVED)
export class PackageMaintainerRemoved extends ChangesStreamEvent {
export class PackageMaintainerRemovedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string, maintainer: string) {
const change = await this.addChange(PACKAGE_MAINTAINER_REMOVED, fullname, { maintainer });
if (this.hookEnable) {
@@ -135,7 +135,7 @@ export class PackageMaintainerRemoved extends ChangesStreamEvent {
}
@Event(PACKAGE_META_CHANGED)
export class PackageMetaChanged extends ChangesStreamEvent {
export class PackageMetaChangedChangesStreamEvent extends ChangesStreamEvent {
async handle(fullname: string, meta: PackageMetaChange) {
const change = await this.addChange(PACKAGE_META_CHANGED, fullname, { ...meta });
const { deprecateds } = meta;

View File

@@ -36,7 +36,7 @@ class StoreManifestEvent {
}
@Event(PACKAGE_VERSION_ADDED)
export class PackageVersionAdded extends StoreManifestEvent {
export class PackageVersionAddedStoreManifestEvent extends StoreManifestEvent {
async handle(fullname: string, version: string) {
await this.savePackageVersionManifest(fullname, version);
}

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 PackageUnpublishedSyncESEvent 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 PackageVersionAddedSyncESEvent 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,25 +12,63 @@ 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);
}
}
@Event(PACKAGE_VERSION_ADDED)
export class PackageVersionAdded extends SyncPackageVersionFileEvent {
export class PackageVersionAddedSyncPackageVersionFileEvent extends SyncPackageVersionFileEvent {
async handle(fullname: string, version: string) {
await this.syncPackageVersionFile(fullname, version);
}
}
@Event(PACKAGE_TAG_ADDED)
export class PackageTagAddedSyncPackageVersionFileEvent extends SyncPackageVersionFileEvent {
async handle(fullname: string, tag: string) {
if (tag !== 'latest') return;
await this.syncPackageReadmeToLatestVersion(fullname);
}
}
@Event(PACKAGE_TAG_CHANGED)
export class PackageTagChangedSyncPackageVersionFileEvent extends SyncPackageVersionFileEvent {
async handle(fullname: string, tag: string) {
if (tag !== 'latest') return;
await this.syncPackageReadmeToLatestVersion(fullname);
}
}

View File

@@ -9,18 +9,19 @@ import {
EggHttpClient,
} from 'egg';
import fs from 'fs/promises';
import { sortBy } from 'lodash';
import binaries, { BinaryName, CategoryName } from '../../../config/binaries';
import { NFSAdapter } from '../../common/adapter/NFSAdapter';
import { TaskType, TaskState } from '../../common/enum/Task';
import { downloadToTempfile } from '../../common/FileUtil';
import { BinaryRepository } from '../../repository/BinaryRepository';
import { Task } from '../entity/Task';
import { Binary } from '../entity/Binary';
import { TaskService } from './TaskService';
import { NFSAdapter } from '../../common/adapter/NFSAdapter';
import { downloadToTempfile } from '../../common/FileUtil';
import { isTimeoutError } from '../../common/ErrorUtil';
import { AbstractBinary, BinaryItem } from '../../common/adapter/binary/AbstractBinary';
import { AbstractService } from '../../common/AbstractService';
import { TaskRepository } from '../../repository/TaskRepository';
import { BinaryType } from '../../common/enum/Binary';
import { TaskType, TaskState } from '../../common/enum/Task';
function isoNow() {
return new Date().toISOString();
@@ -35,8 +36,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 +87,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 +128,33 @@ 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;
task.error = `${err.name}: ${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 (isTimeoutError(err)) {
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 +162,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 +198,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 +211,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 +231,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 +275,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 +319,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

@@ -6,20 +6,21 @@ import {
EggObjectFactory,
Inject,
} from '@eggjs/tegg';
import { TaskState, TaskType } from '../../common/enum/Task';
import { AbstractService } from '../../common/AbstractService';
import { TaskRepository } from '../../repository/TaskRepository';
import { HOST_NAME, ChangesStreamTask, Task } from '../entity/Task';
import { E500 } from 'egg-errors';
import { PackageSyncerService, RegistryNotMatchError } from './PackageSyncerService';
import { TaskService } from './TaskService';
import { RegistryManagerService } from './RegistryManagerService';
import { E500 } from 'egg-errors';
import { ScopeManagerService } from './ScopeManagerService';
import { PackageRepository } from '../../repository/PackageRepository';
import { TaskRepository } from '../../repository/TaskRepository';
import { HOST_NAME, ChangesStreamTask, Task } from '../entity/Task';
import { Registry } from '../entity/Registry';
import { AbstractChangeStream } from '../../common/adapter/changesStream/AbstractChangesStream';
import { getScopeAndName } from '../../common/PackageUtil';
import { isTimeoutError } from '../../common/ErrorUtil';
import { GLOBAL_WORKER } from '../../common/constants';
import { ScopeManagerService } from './ScopeManagerService';
import { PackageRepository } from '../../repository/PackageRepository';
import { TaskState, TaskType } from '../../common/enum/Task';
import { AbstractService } from '../../common/AbstractService';
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
@@ -101,8 +102,12 @@ 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 (isTimeoutError(err)) {
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,13 +1,16 @@
import { stat } from 'fs/promises';
import { stat, readFile } from 'node:fs/promises';
import { strict as assert } from 'node:assert';
import {
AccessLevel,
SingletonProto,
EventBus,
Inject,
} from '@eggjs/tegg';
import { ForbiddenError } from 'egg-errors';
import { BadRequestError, ForbiddenError, NotFoundError } from 'egg-errors';
import { RequireAtLeastOne } from 'type-fest';
import npa from 'npm-package-arg';
import semver from 'semver';
import pMap from 'p-map';
import {
calculateIntegrity,
detectInstallScript,
@@ -17,12 +20,11 @@ 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';
import { DistRepository } from '../../repository/DistRepository';
import { isDuplicateKeyError } from '../../repository/util/ErrorUtil';
import { Package } from '../entity/Package';
import { PackageVersion } from '../entity/PackageVersion';
import { PackageVersionBlock } from '../entity/PackageVersionBlock';
@@ -46,6 +48,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 +56,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 +67,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,30 +94,33 @@ 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 = {};
// support user publish private package and sync worker publish public package
async publish(cmd: PublishPackageCmd, publisher: User) {
if (this.config.cnpmcore.strictValidatePackageDeps) {
await this._checkPackageDepsVersion(cmd.packageJson);
}
let pkg = await this.packageRepository.findPackage(cmd.scope, cmd.name);
if (!pkg) {
pkg = Package.create({
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 +161,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 +207,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,
@@ -262,7 +272,7 @@ export class PackageManagerService extends AbstractService {
try {
await this.packageRepository.createPackageVersion(pkgVersion);
} catch (e) {
if (e.code === 'ER_DUP_ENTRY') {
if (isDuplicateKeyError(e)) {
throw new ForbiddenError(`Can't modify pre-existing version: ${pkg.fullname}@${cmd.version}`);
}
throw e;
@@ -270,13 +280,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 +330,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 +360,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 +375,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 +400,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 +411,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 +440,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 +459,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 +510,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 +541,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 +570,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 +598,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 +681,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 +739,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 +825,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 +835,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 +847,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 +892,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 +927,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 = '';
@@ -940,4 +984,25 @@ export class PackageManagerService extends AbstractService {
}
return data;
}
private async _checkPackageDepsVersion(pkgJSON: PackageJSONType) {
// 只校验 dependencies
// devDependencies、optionalDependencies、peerDependencies 不会影响依赖安装 不在这里进行校验
const { dependencies } = pkgJSON;
await pMap(Object.entries(dependencies || {}), async ([ fullname, spec ]) => {
try {
const specResult = npa(`${fullname}@${spec}`);
// 对于 git、alias、file 等类型的依赖,不进行版本校验
if (![ 'range', 'tag', 'version' ].includes(specResult.type)) {
return;
}
const pkgVersion = await this.packageVersionService.getVersion(npa(`${fullname}@${spec}`));
assert(pkgVersion);
} catch (e) {
throw new BadRequestError(`deps ${fullname}@${spec} not found`);
}
}, {
concurrency: 12,
});
}
}

View File

@@ -0,0 +1,259 @@
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 => {
// 从 https://github.com/npm/cli/pull/7407 (npm cli v10.6.0) 开始npm cli 使用 publisher 字段(以前使用 maintainers 字段)
// 从现有数据来看_npmUser 字段和 publisher 字段是等价的
// 为了兼容老版本,不删除 _npmUser 字段
if (!item._source?.package.publisher && item._source?.package._npmUser) {
item._source.package.publisher = {
username: item._source.package._npmUser.name,
email: item._source.package._npmUser.email,
};
}
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;
}
@@ -226,7 +228,7 @@ export class PackageSyncerService extends AbstractService {
const { status, data } = remoteFetchResult;
// deleted or blocked
if (status === 404 || status === 451) {
if (status === 451) {
return true;
}
@@ -243,6 +245,23 @@ export class PackageSyncerService extends AbstractService {
// security holder
// test/fixtures/registry.npmjs.org/security-holding-package.json
// {
// "_id": "xxx",
// "_rev": "9-a740a77bcd978abeec47d2d027bf688c",
// "name": "xxx",
// "time": {
// "modified": "2017-11-28T00:45:24.162Z",
// "created": "2013-09-20T23:25:18.122Z",
// "0.0.0": "2013-09-20T23:25:20.242Z",
// "1.0.0": "2016-06-22T00:07:41.958Z",
// "0.0.1-security": "2016-12-15T01:03:58.663Z",
// "unpublished": {
// "time": "2017-11-28T00:45:24.163Z",
// "versions": []
// }
// },
// "_attachments": {}
// }
let isSecurityHolder = true;
for (const versionInfo of Object.entries<{ _npmUser?: { name: string } }>(data.versions || {})) {
const [ v, info ] = versionInfo;
@@ -297,8 +316,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 +329,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,25 +368,38 @@ 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} 👈👈👈👈👈`);
}
const taskQueueLength = await this.taskService.getTaskQueueLength(task.type);
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 ❌❌❌❌❌`);
@@ -440,6 +472,19 @@ export class PackageSyncerService extends AbstractService {
return;
}
if (status === 404) {
// ignore 404 status
// https://github.com/node-modules/detect-port/issues/57
task.error = `Package not found, status 404, data: ${JSON.stringify(data)}`;
logs.push(`[${isoNow()}] ❌ ${task.error}, log: ${logUrl}`);
logs.push(`[${isoNow()}] ❌ Synced ${fullname} fail, ${task.error}, log: ${logUrl}`);
logs.push(`[${isoNow()}] ❌❌❌❌❌ ${fullname} ❌❌❌❌❌`);
this.logger.info('[PackageSyncerService.executeTask:fail-request-error] taskId: %s, targetName: %s, %s',
task.taskId, task.targetName, task.error);
await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n'));
return;
}
let readme = data.readme || '';
if (typeof readme !== 'string') {
readme = JSON.stringify(readme);
@@ -472,7 +517,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) {
@@ -530,9 +575,9 @@ export class PackageSyncerService extends AbstractService {
task.error = `invalid maintainers: ${JSON.stringify(maintainers)}`;
logs.push(`[${isoNow()}] ❌ ${task.error}, log: ${logUrl}`);
logs.push(`[${isoNow()}] ${failEnd}`);
await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n'));
this.logger.info('[PackageSyncerService.executeTask:fail-invalid-maintainers] taskId: %s, targetName: %s, %s',
task.taskId, task.targetName, task.error);
await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n'));
return;
}
@@ -544,10 +589,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} ❌❌❌❌❌`);
this.logger.info('[PackageSyncerService.executeTask:fail-empty-list] taskId: %s, targetName: %s, %s',
task.taskId, task.targetName, task.error);
await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n'));
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 +656,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 +673,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 +715,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 +747,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`);
@@ -680,6 +761,14 @@ export class PackageSyncerService extends AbstractService {
this.logger.error(err);
lastErrorMessage = `publish error: ${err}`;
logs.push(`[${isoNow()}] ❌ [${syncIndex}] Synced version ${version} error, ${lastErrorMessage}`);
if (err.name === 'BadRequestError') {
// 由于当前版本的依赖不满足,尝试重试
// 默认会在当前队列最后重试
this.logger.info('[PackageSyncerService.executeTask:fail-validate-deps] taskId: %s, targetName: %s, %s',
task.taskId, task.targetName, task.error);
await this.taskService.retryTask(task, logs.join('\n'));
return;
}
}
}
await this.taskService.appendTaskLog(task, logs.join('\n'));
@@ -705,9 +794,9 @@ export class PackageSyncerService extends AbstractService {
logs.push(`[${isoNow()}] ❌ All versions sync fail, package not exists, log: ${logUrl}`);
logs.push(`[${isoNow()}] ${failEnd}`);
task.error = lastErrorMessage;
await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n'));
this.logger.info('[PackageSyncerService.executeTask:fail] taskId: %s, targetName: %s, package not exists',
task.taskId, task.targetName);
await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n'));
return;
}
@@ -787,6 +876,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 +919,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,12 +950,12 @@ 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',
task.taskId, task.targetName);
await this.taskService.finishTask(task, TaskState.Success, logs.join('\n'));
}
}

View File

@@ -1,36 +1,59 @@
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 { isDuplicateKeyError } from '../../repository/util/ErrorUtil';
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 +70,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 +205,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 +220,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 +265,7 @@ export class PackageVersionFileService extends AbstractService {
name,
dist,
contentType: mimeLookup(path),
mtime: stat.mtime,
mtime: pkgVersion.publishTime,
});
try {
await this.packageVersionFileRepository.createPackageVersionFile(file);
@@ -131,7 +273,9 @@ export class PackageVersionFileService extends AbstractService {
file.packageVersionFileId, dist.size, file.path);
} catch (err) {
// ignore Duplicate entry
if (err.code === 'ER_DUP_ENTRY') return file;
if (isDuplicateKeyError(err)) {
return file;
}
throw err;
}
return file;
@@ -143,4 +287,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,134 @@
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, PackageRepository } from '../../repository/PackageRepository';
import { DistRepository } from '../../repository/DistRepository';
import { BugVersionAdvice } from '../entity/BugVersion';
import { PackageVersionBlockRepository } from '../../repository/PackageVersionBlockRepository';
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
})
export class PackageVersionService {
@Inject()
private packageVersionRepository: PackageVersionRepository;
@Inject()
private packageRepository: PackageRepository;
@Inject()
private packageVersionBlockRepository: PackageVersionBlockRepository;
@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;
}
async findBlockInfo(fullname: string) {
const [ scope, name ] = getScopeAndName(fullname);
const packageId = await this.packageRepository.findPackageId(scope, name);
if (!packageId) {
return null;
}
return await this.packageVersionBlockRepository.findPackageBlock(packageId);
}
}

View File

@@ -0,0 +1,274 @@
import { EggHttpClient, HttpClientRequestOptions, HttpClientResponse } from 'egg';
import { ForbiddenError } from 'egg-errors';
import { SingletonProto, AccessLevel, Inject, EggContext } from '@eggjs/tegg';
import { BackgroundTaskHelper } from '@eggjs/tegg-background-task';
import { valid as semverValid } from 'semver';
import { AbstractService } from '../../common/AbstractService';
import { TaskService } from './TaskService';
import { CacheService } from './CacheService';
import { RegistryManagerService } from './RegistryManagerService';
import { NPMRegistry } from '../../common/adapter/NPMRegistry';
import { NFSAdapter } from '../../common/adapter/NFSAdapter';
import { ProxyCache } from '../entity/ProxyCache';
import { Task, UpdateProxyCacheTaskOptions, CreateUpdateProxyCacheTask } from '../entity/Task';
import { ProxyCacheRepository } from '../../repository/ProxyCacheRepository';
import { TaskType, TaskState } from '../../common/enum/Task';
import { calculateIntegrity } from '../../common/PackageUtil';
import { ABBREVIATED_META_TYPE, PROXY_CACHE_DIR_NAME } from '../../common/constants';
import { DIST_NAMES } from '../entity/Package';
import type { AbbreviatedPackageManifestType, AbbreviatedPackageJSONType, PackageManifestType, PackageJSONType } from '../../repository/PackageRepository';
function isoNow() {
return new Date().toISOString();
}
export function isPkgManifest(fileType: DIST_NAMES) {
return fileType === DIST_NAMES.FULL_MANIFESTS || fileType === DIST_NAMES.ABBREVIATED_MANIFESTS;
}
type GetSourceManifestAndCacheReturnType<T> = T extends DIST_NAMES.ABBREVIATED | DIST_NAMES.MANIFEST ? AbbreviatedPackageJSONType | PackageJSONType :
T extends DIST_NAMES.FULL_MANIFESTS | DIST_NAMES.ABBREVIATED_MANIFESTS ? AbbreviatedPackageManifestType|PackageManifestType : never;
@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
})
export class ProxyCacheService extends AbstractService {
@Inject()
private readonly httpclient: EggHttpClient;
@Inject()
private readonly npmRegistry: NPMRegistry;
@Inject()
private readonly nfsAdapter: NFSAdapter;
@Inject()
private readonly proxyCacheRepository: ProxyCacheRepository;
@Inject()
private readonly registryManagerService: RegistryManagerService;
@Inject()
private readonly taskService: TaskService;
@Inject()
private readonly cacheService: CacheService;
@Inject()
private readonly backgroundTaskHelper:BackgroundTaskHelper;
async getPackageVersionTarResponse(fullname: string, ctx: EggContext): Promise<HttpClientResponse> {
if (this.config.cnpmcore.syncPackageBlockList.includes(fullname)) {
throw new ForbiddenError(`stop proxy by block list: ${JSON.stringify(this.config.cnpmcore.syncPackageBlockList)}`);
}
return await this.getProxyResponse(ctx);
}
async getPackageManifest(fullname: string, fileType: DIST_NAMES.FULL_MANIFESTS| DIST_NAMES.ABBREVIATED_MANIFESTS): Promise<AbbreviatedPackageManifestType|PackageManifestType> {
const isFullManifests = fileType === DIST_NAMES.FULL_MANIFESTS;
const cachedStoreKey = (await this.proxyCacheRepository.findProxyCache(fullname, fileType))?.filePath;
if (cachedStoreKey) {
try {
const nfsBytes = await this.nfsAdapter.getBytes(cachedStoreKey);
if (!nfsBytes) throw new Error('not found proxy cache, try again later.');
const nfsBuffer = Buffer.from(nfsBytes);
const { shasum: etag } = await calculateIntegrity(nfsBytes);
await this.cacheService.savePackageEtagAndManifests(fullname, isFullManifests, etag, nfsBuffer);
const nfsString = nfsBuffer.toString();
const nfsPkgManifest = JSON.parse(nfsString);
return nfsPkgManifest as AbbreviatedPackageManifestType|PackageManifestType;
} catch (error) {
/* c8 ignore next 6 */
if (error.message.includes('not found proxy cache') || error.message.includes('Unexpected token : in JSON at')) {
await this.nfsAdapter.remove(cachedStoreKey);
await this.proxyCacheRepository.removeProxyCache(fullname, fileType);
}
throw error;
}
}
const manifest = await this.getRewrittenManifest<typeof fileType>(fullname, fileType);
this.backgroundTaskHelper.run(async () => {
await this.storeRewrittenManifest(manifest, fullname, fileType);
const cachedFiles = ProxyCache.create({ fullname, fileType });
await this.proxyCacheRepository.saveProxyCache(cachedFiles);
});
return manifest;
}
// used by GET /:fullname/:versionOrTag
async getPackageVersionManifest(fullname: string, fileType: DIST_NAMES.ABBREVIATED | DIST_NAMES.MANIFEST, versionOrTag: string): Promise<AbbreviatedPackageJSONType|PackageJSONType> {
let version;
if (semverValid(versionOrTag)) {
version = versionOrTag;
} else {
const pkgManifest = await this.getPackageManifest(fullname, DIST_NAMES.ABBREVIATED_MANIFESTS);
const distTags = pkgManifest['dist-tags'] || {};
version = distTags[versionOrTag] ? distTags[versionOrTag] : versionOrTag;
}
const cachedStoreKey = (await this.proxyCacheRepository.findProxyCache(fullname, fileType, version))?.filePath;
if (cachedStoreKey) {
try {
const nfsBytes = await this.nfsAdapter.getBytes(cachedStoreKey);
if (!nfsBytes) throw new Error('not found proxy cache, try again later.');
const nfsString = Buffer.from(nfsBytes!).toString();
return JSON.parse(nfsString) as PackageJSONType | AbbreviatedPackageJSONType;
} catch (error) {
/* c8 ignore next 6 */
if (error.message.includes('not found proxy cache') || error.message.includes('Unexpected token : in JSON at')) {
await this.nfsAdapter.remove(cachedStoreKey);
await this.proxyCacheRepository.removeProxyCache(fullname, fileType);
}
throw error;
}
}
const manifest = await this.getRewrittenManifest(fullname, fileType, versionOrTag);
this.backgroundTaskHelper.run(async () => {
await this.storeRewrittenManifest(manifest, fullname, fileType);
const cachedFiles = ProxyCache.create({ fullname, fileType, version });
await this.proxyCacheRepository.saveProxyCache(cachedFiles);
});
return manifest;
}
async removeProxyCache(fullname: string, fileType: DIST_NAMES, version?: string) {
const storeKey = isPkgManifest(fileType)
? `/${PROXY_CACHE_DIR_NAME}/${fullname}/${fileType}`
: `/${PROXY_CACHE_DIR_NAME}/${fullname}/${version}/${fileType}`;
await this.nfsAdapter.remove(storeKey);
await this.proxyCacheRepository.removeProxyCache(fullname, fileType, version);
}
replaceTarballUrl<T extends DIST_NAMES>(manifest: GetSourceManifestAndCacheReturnType<T>, fileType: T) {
const { sourceRegistry, registry } = this.config.cnpmcore;
if (isPkgManifest(fileType)) {
// pkg manifest
const versionMap = (manifest as AbbreviatedPackageManifestType|PackageManifestType)?.versions;
for (const key in versionMap) {
const versionItem = versionMap[key];
if (versionItem?.dist?.tarball) {
versionItem.dist.tarball = versionItem.dist.tarball.replace(sourceRegistry, registry);
}
}
} else {
// pkg version manifest
const distItem = (manifest as AbbreviatedPackageJSONType | PackageJSONType).dist;
if (distItem?.tarball) {
distItem.tarball = distItem.tarball.replace(sourceRegistry, registry);
}
}
return manifest;
}
async createTask(targetName: string, options: UpdateProxyCacheTaskOptions): Promise<CreateUpdateProxyCacheTask> {
return await this.taskService.createTask(Task.createUpdateProxyCache(targetName, options), false) as CreateUpdateProxyCacheTask;
}
async findExecuteTask() {
return await this.taskService.findExecuteTask(TaskType.UpdateProxyCache);
}
async executeTask(task: Task) {
const logs: string[] = [];
const fullname = (task as CreateUpdateProxyCacheTask).data.fullname;
const { fileType, version } = (task as CreateUpdateProxyCacheTask).data;
let cachedManifest;
logs.push(`[${isoNow()}] 🚧🚧🚧🚧🚧 Start update "${fullname}-${fileType}" 🚧🚧🚧🚧🚧`);
try {
const cachedFiles = await this.proxyCacheRepository.findProxyCache(fullname, fileType);
if (!cachedFiles) throw new Error('task params error, can not found record in repo.');
cachedManifest = await this.getRewrittenManifest<typeof fileType>(fullname, fileType);
await this.storeRewrittenManifest(cachedManifest, fullname, fileType);
ProxyCache.update(cachedFiles);
await this.proxyCacheRepository.saveProxyCache(cachedFiles);
} catch (error) {
task.error = error;
logs.push(`[${isoNow()}] ❌ ${task.error}`);
logs.push(`[${isoNow()}] ❌❌❌❌❌ ${fullname}-${fileType} ${version ?? ''} ❌❌❌❌❌`);
await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n'));
this.logger.info('[ProxyCacheService.executeTask:fail] taskId: %s, targetName: %s, %s',
task.taskId, task.targetName, task.error);
return;
}
logs.push(`[${isoNow()}] 🟢 Update Success.`);
const isFullManifests = fileType === DIST_NAMES.FULL_MANIFESTS;
const cachedKey = await this.cacheService.getPackageEtag(fullname, isFullManifests);
if (cachedKey) {
const cacheBytes = Buffer.from(JSON.stringify(cachedManifest));
const { shasum: etag } = await calculateIntegrity(cacheBytes);
await this.cacheService.savePackageEtagAndManifests(fullname, isFullManifests, etag, cacheBytes);
logs.push(`[${isoNow()}] 🟢 Update Cache Success.`);
}
await this.taskService.finishTask(task, TaskState.Success, logs.join('\n'));
}
// only used by schedule task
private async getRewrittenManifest<T extends DIST_NAMES>(fullname:string, fileType: T, versionOrTag?:string): Promise<GetSourceManifestAndCacheReturnType<T>> {
let responseResult;
const USER_AGENT = 'npm_service.cnpmjs.org/cnpmcore';
switch (fileType) {
case DIST_NAMES.FULL_MANIFESTS: {
const url = `/${encodeURIComponent(fullname)}?t=${Date.now()}&cache=0`;
responseResult = await this.getProxyResponse({ url, headers: { accept: 'application/json', 'user-agent': USER_AGENT } }, { dataType: 'json' });
break;
}
case DIST_NAMES.ABBREVIATED_MANIFESTS: {
const url = `/${encodeURIComponent(fullname)}?t=${Date.now()}&cache=0`;
responseResult = await this.getProxyResponse({ url, headers: { accept: ABBREVIATED_META_TYPE, 'user-agent': USER_AGENT } }, { dataType: 'json' });
break;
}
case DIST_NAMES.MANIFEST: {
const url = `/${encodeURIComponent(fullname)}/${encodeURIComponent(versionOrTag!)}`;
responseResult = await this.getProxyResponse({ url, headers: { accept: 'application/json', 'user-agent': USER_AGENT } }, { dataType: 'json' });
break;
}
case DIST_NAMES.ABBREVIATED: {
const url = `/${encodeURIComponent(fullname)}/${encodeURIComponent(versionOrTag!)}`;
responseResult = await this.getProxyResponse({ url, headers: { accept: ABBREVIATED_META_TYPE, 'user-agent': USER_AGENT } }, { dataType: 'json' });
break;
}
default:
break;
}
// replace tarball url
const manifest = this.replaceTarballUrl(responseResult.data, fileType);
return manifest;
}
private async storeRewrittenManifest(manifest, fullname: string, fileType: DIST_NAMES) {
let storeKey: string;
if (isPkgManifest(fileType)) {
storeKey = `/${PROXY_CACHE_DIR_NAME}/${fullname}/${fileType}`;
} else {
const version = manifest.version;
storeKey = `/${PROXY_CACHE_DIR_NAME}/${fullname}/${version}/${fileType}`;
}
const nfsBytes = Buffer.from(JSON.stringify(manifest));
await this.nfsAdapter.uploadBytes(storeKey, nfsBytes);
}
async getProxyResponse(ctx: Partial<EggContext>, options?: HttpClientRequestOptions): Promise<HttpClientResponse> {
const registry = this.npmRegistry.registry;
const remoteAuthToken = await this.registryManagerService.getAuthTokenByRegistryHost(registry);
const authorization = this.npmRegistry.genAuthorizationHeader(remoteAuthToken);
const url = `${this.npmRegistry.registry}${ctx.url}`;
const res = await this.httpclient.request(url, {
timing: true,
followRedirect: true,
// once redirection is also count as a retry
retry: 7,
dataType: 'stream',
timeout: 10000,
compressed: true,
...options,
headers: {
accept: ctx.headers?.accept,
'user-agent': ctx.headers?.['user-agent'],
authorization,
'x-forwarded-for': ctx?.ip,
via: `1.1, ${this.config.cnpmcore.registry}`,
},
}) as HttpClientResponse;
this.logger.info('[ProxyCacheService:getProxyStreamResponse] %s, status: %s', url, res.status);
return res;
}
}

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;
}

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

@@ -0,0 +1,186 @@
import { SyncDeleteMode, SyncMode, ChangesStreamMode } from '../common/constants';
import { DATABASE_TYPE } from '../../config/database';
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 controller.
* - 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,
/**
* strictly enforces/validates dependencies version when publish or sync
*/
strictValidatePackageDeps?: boolean,
/**
* database config
*/
database: {
type: DATABASE_TYPE | string,
},
};

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

@@ -0,0 +1,143 @@
import {
HTTPController,
HTTPMethod,
HTTPMethodEnum,
Inject,
HTTPQuery,
HTTPParam,
Context,
EggContext,
} from '@eggjs/tegg';
import { ForbiddenError, NotFoundError, UnauthorizedError, NotImplementedError } from 'egg-errors';
import { AbstractController } from './AbstractController';
import { ProxyCacheRepository } from '../../repository/ProxyCacheRepository';
import { Static } from 'egg-typebox-validate/typebox';
import { QueryPageOptions } from '../typebox';
import { FULLNAME_REG_STRING } from '../../common/PackageUtil';
import {
ProxyCacheService,
isPkgManifest,
} from '../../core/service/ProxyCacheService';
import { SyncMode } from '../../common/constants';
import { CacheService } from '../../core/service/CacheService';
@HTTPController()
export class ProxyCacheController extends AbstractController {
@Inject()
private readonly proxyCacheRepository: ProxyCacheRepository;
@Inject()
private readonly proxyCacheService: ProxyCacheService;
@Inject()
private readonly cacheService: CacheService;
@HTTPMethod({
method: HTTPMethodEnum.GET,
path: '/-/proxy-cache',
})
async listProxyCache(
@HTTPQuery() pageSize: Static<typeof QueryPageOptions>['pageSize'],
@HTTPQuery() pageIndex: Static<typeof QueryPageOptions>['pageIndex'],
) {
if (this.config.cnpmcore.syncMode !== SyncMode.proxy) {
throw new ForbiddenError('proxy mode is not enabled');
}
return await this.proxyCacheRepository.listCachedFiles({
pageSize,
pageIndex,
});
}
@HTTPMethod({
method: HTTPMethodEnum.GET,
path: `/-/proxy-cache/:fullname(${FULLNAME_REG_STRING})`,
})
async showProxyCaches(@HTTPQuery() pageSize: Static<typeof QueryPageOptions>['pageSize'],
@HTTPQuery() pageIndex: Static<typeof QueryPageOptions>['pageIndex'], @HTTPParam() fullname: string) {
if (this.config.cnpmcore.syncMode !== SyncMode.proxy) {
throw new ForbiddenError('proxy mode is not enabled');
}
return await this.proxyCacheRepository.listCachedFiles({
pageSize,
pageIndex,
}, fullname);
}
@HTTPMethod({
method: HTTPMethodEnum.PATCH,
path: `/-/proxy-cache/:fullname(${FULLNAME_REG_STRING})`,
})
async refreshProxyCaches(@HTTPParam() fullname: string) {
if (this.config.cnpmcore.syncMode !== SyncMode.proxy) {
throw new ForbiddenError('proxy mode is not enabled');
}
const refreshList = await this.proxyCacheRepository.findProxyCaches(
fullname,
);
if (refreshList.length === 0) {
throw new NotFoundError();
}
await this.cacheService.removeCache(fullname);
const taskList = refreshList
// only refresh package.json and abbreviated.json
.filter(i => isPkgManifest(i.fileType))
.map(item => {
const task = this.proxyCacheService.createTask(
`${item.fullname}/${item.fileType}`,
{
fullname: item.fullname,
fileType: item.fileType,
},
);
return task;
});
const tasks = await Promise.all(taskList);
return {
ok: true,
tasks,
};
}
@HTTPMethod({
method: HTTPMethodEnum.DELETE,
path: `/-/proxy-cache/:fullname(${FULLNAME_REG_STRING})`,
})
async removeProxyCaches(@HTTPParam() fullname: string) {
if (this.config.cnpmcore.syncMode !== SyncMode.proxy) {
throw new ForbiddenError('proxy mode is not enabled');
}
const proxyCachesList = await this.proxyCacheRepository.findProxyCaches(
fullname,
);
if (proxyCachesList.length === 0) {
throw new NotFoundError();
}
await this.cacheService.removeCache(fullname);
const removingList = proxyCachesList.map(item => {
return this.proxyCacheService.removeProxyCache(item.fullname, item.fileType, item.version);
});
await Promise.all(removingList);
return {
ok: true,
result: proxyCachesList,
};
}
@HTTPMethod({
method: HTTPMethodEnum.DELETE,
path: '/-/proxy-cache',
})
async truncateProxyCaches(@Context() ctx: EggContext) {
const isAdmin = await this.userRoleManager.isAdmin(ctx);
if (!isAdmin) {
throw new UnauthorizedError('only admin can do this');
}
if (this.config.cnpmcore.syncMode !== SyncMode.proxy) {
throw new ForbiddenError('proxy mode is not enabled');
}
throw new NotImplementedError('not implemented yet');
}
}

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

@@ -1,3 +1,4 @@
import { PassThrough } from 'node:stream';
import {
NotFoundError,
} from 'egg-errors';
@@ -12,36 +13,95 @@ import {
} from '@eggjs/tegg';
import { AbstractController } from '../AbstractController';
import { FULLNAME_REG_STRING, getScopeAndName } from '../../../common/PackageUtil';
import { SyncMode } from '../../../common/constants';
import { NFSAdapter } from '../../../common/adapter/NFSAdapter';
import { PackageManagerService } from '../../../core/service/PackageManagerService';
import { ProxyCacheService } from '../../../core/service/ProxyCacheService';
import { PackageSyncerService } from '../../../core/service/PackageSyncerService';
import { RegistryManagerService } from '../../../core/service/RegistryManagerService';
import { PackageVersionService } from '../../../core/service/PackageVersionService';
@HTTPController()
export class DownloadPackageVersionTarController extends AbstractController {
@Inject()
private packageManagerService: PackageManagerService;
@Inject()
registryManagerService: RegistryManagerService;
@Inject()
private proxyCacheService: ProxyCacheService;
@Inject()
private packageSyncerService: PackageSyncerService;
@Inject()
private packageVersionService: PackageVersionService;
@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) {
// block tgz only all versions have been blocked
const blockInfo = await this.packageVersionService.findBlockInfo(fullname);
if (blockInfo?.reason) {
this.setCDNHeaders(ctx);
ctx.logger.info('[PackageController:downloadVersionTar] %s@%s, block for %s',
fullname, version, blockInfo.reason);
throw this.createPackageBlockError(blockInfo.reason, fullname, version);
}
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);
let pkg;
let packageVersion;
try {
pkg = await this.getPackageEntityByFullname(fullname, allowSync);
packageVersion = await this.getPackageVersionEntity(pkg, version, allowSync);
} catch (error) {
if (this.config.cnpmcore.syncMode === SyncMode.proxy) {
// proxy mode package version not found.
const tgzStream = await this.getTgzProxyStream(ctx, fullname, version);
this.packageManagerService.plusPackageVersionCounter(fullname, version);
const passThroughRemoteStream = new PassThrough();
tgzStream.pipe(passThroughRemoteStream);
ctx.attachment(`${filenameWithVersion}.tgz`);
return passThroughRemoteStream;
}
throw error;
}
// 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);
@@ -68,4 +128,42 @@ export class DownloadPackageVersionTarController extends AbstractController {
const filenameWithVersion = getScopeAndName(fullnameWithVersion)[1];
return await this.download(ctx, fullname, filenameWithVersion);
}
private async getTgzProxyStream(ctx: EggContext, fullname: string, version: string) {
const { headers, status, res } = await this.proxyCacheService.getPackageVersionTarResponse(fullname, ctx);
ctx.status = status;
ctx.set(headers as { [key: string]: string | string[] });
ctx.runInBackground(async () => {
const task = await this.packageSyncerService.createTask(fullname, {
authorIp: ctx.ip,
authorId: `pid_${process.pid}`,
tips: `Sync specific version in proxy mode cause by "${ctx.href}"`,
skipDependencies: true,
specificVersions: [ version ],
});
ctx.logger.info('[DownloadPackageVersionTarController.createSyncTask:success] taskId: %s, fullname: %s',
task.taskId, fullname);
});
return res;
}
// Compatible Verdaccio path style
@HTTPMethod({
// GET /:fullname/-/:scope/:filenameWithVersion.tgz
path: `/:fullname(${FULLNAME_REG_STRING})/-/:scope/:filenameWithVersion.tgz`,
method: HTTPMethodEnum.OPTIONS,
})
async downloadVerdaccioPathStyleorOptions(@Context() ctx: EggContext) {
return this.downloadForOptions(ctx);
}
@HTTPMethod({
// GET /:fullname/-/:scope/:filenameWithVersion.tgz
path: `/:fullname(${FULLNAME_REG_STRING})/-/:scope/:filenameWithVersion.tgz`,
method: HTTPMethodEnum.GET,
})
async downloadVerdaccioPathStyle(@Context() ctx: EggContext, @HTTPParam() fullname: string, @HTTPParam() filenameWithVersion: string) {
return this.download(ctx, fullname, filenameWithVersion);
}
}

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,10 @@ import { getScopeAndName, FULLNAME_REG_STRING } from '../../../common/PackageUti
import { isSyncWorkerRequest } from '../../../common/SyncUtil';
import { PackageManagerService } from '../../../core/service/PackageManagerService';
import { CacheService } from '../../../core/service/CacheService';
import { ABBREVIATED_META_TYPE, SyncMode } from '../../../common/constants';
import { ProxyCacheService } from '../../../core/service/ProxyCacheService';
import { calculateIntegrity } from '../../../common/PackageUtil';
import { DIST_NAMES } from '../../../core/entity/Package';
@HTTPController()
export class ShowPackageController extends AbstractController {
@@ -19,6 +23,8 @@ export class ShowPackageController extends AbstractController {
private packageManagerService: PackageManagerService;
@Inject()
private cacheService: CacheService;
@Inject()
private proxyCacheService: ProxyCacheService;
@HTTPMethod({
// GET /:fullname
@@ -29,8 +35,7 @@ export class ShowPackageController extends AbstractController {
async show(@Context() ctx: EggContext, @HTTPParam() fullname: string) {
const [ scope, name ] = getScopeAndName(fullname);
const isSync = isSyncWorkerRequest(ctx);
const abbreviatedMetaType = 'application/vnd.npm.install-v1+json';
const isFullManifests = ctx.accepts([ 'json', abbreviatedMetaType ]) !== abbreviatedMetaType;
const isFullManifests = ctx.accepts([ 'json', ABBREVIATED_META_TYPE ]) !== ABBREVIATED_META_TYPE;
// handle cache
// fallback to db when cache error
@@ -64,10 +69,22 @@ export class ShowPackageController extends AbstractController {
// handle cache miss
let result: { etag: string; data: any, blockReason: string };
if (isFullManifests) {
result = await this.packageManagerService.listPackageFullManifests(scope, name, isSync);
if (this.config.cnpmcore.syncMode === SyncMode.proxy) {
// proxy mode
const fileType = isFullManifests ? DIST_NAMES.FULL_MANIFESTS : DIST_NAMES.ABBREVIATED_MANIFESTS;
const { data: sourceManifest } = await this.proxyCacheService.getProxyResponse(ctx, { dataType: 'json' });
const pkgManifest = this.proxyCacheService.replaceTarballUrl(sourceManifest, fileType);
const nfsBytes = Buffer.from(JSON.stringify(pkgManifest));
const { shasum: etag } = await calculateIntegrity(nfsBytes);
result = { data: pkgManifest, etag, blockReason: '' };
} else {
result = await this.packageManagerService.listPackageAbbreviatedManifests(scope, name, isSync);
// sync mode
if (isFullManifests) {
result = await this.packageManagerService.listPackageFullManifests(scope, name, isSync);
} else {
result = await this.packageManagerService.listPackageAbbreviatedManifests(scope, name, isSync);
}
}
const { etag, data, blockReason } = result;
// 404, no data

View File

@@ -7,38 +7,78 @@ import {
Context,
EggContext,
} from '@eggjs/tegg';
import { NotFoundError } from 'egg-errors';
import { AbstractController } from '../AbstractController';
import { getScopeAndName, FULLNAME_REG_STRING } from '../../../common/PackageUtil';
import {
getScopeAndName,
FULLNAME_REG_STRING,
} from '../../../common/PackageUtil';
import { isSyncWorkerRequest } from '../../../common/SyncUtil';
import { PackageManagerService } from '../../../core/service/PackageManagerService';
import { ProxyCacheService } from '../../../core/service/ProxyCacheService';
import { Spec } from '../../../port/typebox';
import { ABBREVIATED_META_TYPE, SyncMode } from '../../../common/constants';
import { DIST_NAMES } from '../../../core/entity/Package';
import { NotFoundError } from 'egg-errors';
@HTTPController()
export class ShowPackageVersionController extends AbstractController {
@Inject()
private packageManagerService: PackageManagerService;
@Inject()
private proxyCacheService: ProxyCacheService;
@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);
if (!pkg) {
const allowSync = this.getAllowSync(ctx);
throw this.createPackageNotFoundErrorWithRedirect(fullname, undefined, allowSync);
}
const isFullManifests =
ctx.accepts([ 'json', ABBREVIATED_META_TYPE ]) !== ABBREVIATED_META_TYPE;
const { blockReason, manifest, pkg } =
await this.packageManagerService.showPackageVersionManifest(
scope,
name,
versionSpec,
isSync,
isFullManifests,
);
const allowSync = this.getAllowSync(ctx);
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`);
if (!pkg || !manifest) {
if (this.config.cnpmcore.syncMode === SyncMode.proxy) {
const fileType = isFullManifests
? DIST_NAMES.MANIFEST
: DIST_NAMES.ABBREVIATED;
return await this.proxyCacheService.getPackageVersionManifest(
fullname,
fileType,
versionSpec,
);
}
if (!pkg) {
throw this.createPackageNotFoundErrorWithRedirect(fullname, undefined, allowSync);
}
if (!manifest) {
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

@@ -0,0 +1,53 @@
import { EggAppConfig, EggLogger } from 'egg';
import { CronParams, Schedule, ScheduleType } from '@eggjs/tegg/schedule';
import { Inject } from '@eggjs/tegg';
import { ProxyCacheRepository } from '../../repository/ProxyCacheRepository';
import { SyncMode } from '../../common/constants';
import { ProxyCacheService, isPkgManifest } from '../../core/service/ProxyCacheService';
@Schedule<CronParams>({
type: ScheduleType.WORKER,
scheduleData: {
cron: '0 3 * * *', // run every day at 03:00
},
})
export class CheckProxyCacheUpdateWorker {
@Inject()
private readonly config: EggAppConfig;
@Inject()
private readonly logger: EggLogger;
@Inject()
private proxyCacheService: ProxyCacheService;
@Inject()
private readonly proxyCacheRepository:ProxyCacheRepository;
async subscribe() {
if (this.config.cnpmcore.syncMode !== SyncMode.proxy) return;
let pageIndex = 0;
let { data: list } = await this.proxyCacheRepository.listCachedFiles({ pageSize: 5, pageIndex });
while (list.length !== 0) {
for (const item of list) {
try {
if (isPkgManifest(item.fileType)) {
// 仅manifests需要更新指定版本的package.json文件发布后不会改变
const task = await this.proxyCacheService.createTask(`${item.fullname}/${item.fileType}`, {
fullname: item.fullname,
fileType: item.fileType,
});
this.logger.info('[CheckProxyCacheUpdateWorker.subscribe:createTask][%s] taskId: %s, targetName: %s',
pageIndex, task.taskId, task.targetName);
}
} catch (err) {
this.logger.error(err);
}
}
pageIndex++;
list = (await this.proxyCacheRepository.listCachedFiles({ pageSize: 5, pageIndex })).data;
}
}
}

View File

@@ -30,7 +30,7 @@ export class CheckRecentlyUpdatedPackages {
private readonly httpclient: EggHttpClient;
async subscribe() {
const notAllowUpdateModeList = [ SyncMode.none, SyncMode.admin ];
const notAllowUpdateModeList = [ SyncMode.none, SyncMode.admin, SyncMode.proxy ];
if (notAllowUpdateModeList.includes(this.config.cnpmcore.syncMode) || !this.config.cnpmcore.enableCheckRecentlyUpdated) return;
const pageSize = 36;
const pageCount = this.config.env === 'unittest' ? 2 : 5;

View File

@@ -2,6 +2,7 @@ import { EggAppConfig, EggLogger } from 'egg';
import { IntervalParams, Schedule, ScheduleType } from '@eggjs/tegg/schedule';
import { Inject } from '@eggjs/tegg';
import { BinarySyncerService } from '../../core/service/BinarySyncerService';
import { isTimeoutError } from '../../common/ErrorUtil';
@Schedule<IntervalParams>({
type: ScheduleType.ALL,
@@ -29,7 +30,19 @@ 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 (isTimeoutError(err)) {
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);

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