refactor: use tegg and egg v4 beta (#836)

🦀 for https://github.com/eggjs/egg/issues/3644
This commit is contained in:
MK (fengmk2)
2025-10-06 14:48:45 +08:00
committed by GitHub
parent c16ec7bad0
commit c7df471c0e
24 changed files with 91 additions and 44 deletions

View File

@@ -13,7 +13,10 @@
"no-console": "warn", "no-console": "warn",
"import/no-anonymous-default-export": "error", "import/no-anonymous-default-export": "error",
"no-unassigned-import": "allow", "no-unassigned-import": "allow",
"new-cap": "allow" "new-cap": "allow",
"class-methods-use-this": "allow",
"import/no-named-export": "allow",
"unicorn/no-array-sort": "allow"
}, },
"ignorePatterns": ["index.d.ts"] "ignorePatterns": ["index.d.ts"]
} }

View File

@@ -2,5 +2,6 @@
"singleQuote": true, "singleQuote": true,
"trailingComma": "es5", "trailingComma": "es5",
"tabWidth": 2, "tabWidth": 2,
"printWidth": 120,
"arrowParens": "avoid" "arrowParens": "avoid"
} }

View File

@@ -236,7 +236,7 @@ private async getPackageEntity(scope: string, name: string) {
#### 1、请求参数校验 #### 1、请求参数校验
使用 [egg-typebox-validate](https://github.com/xiekw2010/egg-typebox-validate) 来做请求参数校验,只需要定义一次参数类型和规则,就能同时拥有参数校验和类型定义。 使用 [@eggjs/typebox-validate](https://github.com/eggjs/egg/tree/next/plugins/typebox-validate) 来做请求参数校验,只需要定义一次参数类型和规则,就能同时拥有参数校验和类型定义。
详细使用方式可以参考 [PR#12](https://github.com/cnpm/cnpmcore/pull/12)。 详细使用方式可以参考 [PR#12](https://github.com/cnpm/cnpmcore/pull/12)。
使用方式请直接参考 `app/port/typebox.ts` 代码。 使用方式请直接参考 `app/port/typebox.ts` 代码。

View File

@@ -73,7 +73,7 @@
}, },
typeboxValidate: { typeboxValidate: {
enable: true, enable: true,
package: 'egg-typebox-validate', package: '@eggjs/typebox-validate',
}, },
} }
``` ```

1
app.ts
View File

@@ -1,6 +1,7 @@
import path from 'node:path'; import path from 'node:path';
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import type { Application, ILifecycleBoot } from 'egg'; import type { Application, ILifecycleBoot } from 'egg';
import { ChangesStreamService } from './app/core/service/ChangesStreamService.js'; import { ChangesStreamService } from './app/core/service/ChangesStreamService.js';
declare module 'egg' { declare module 'egg' {

View File

@@ -77,8 +77,6 @@ export interface AuthClient {
} }
declare module 'egg' { declare module 'egg' {
// oxlint-disable-next-line prefer-ts-expect-error ban-ts-comment
// @ts-ignore
interface EggAppConfig { interface EggAppConfig {
cnpmcore: CnpmcoreConfig; cnpmcore: CnpmcoreConfig;
} }

View File

@@ -140,16 +140,20 @@ export class UserRoleManager {
`Read-only Token "${token.tokenMark}" can't publish` `Read-only Token "${token.tokenMark}" can't publish`
); );
} }
const userAgent: string = ctx.get('user-agent');
// only support npm >= 7.0.0 allow publish action // only support npm >= 7.0.0 allow publish action
// user-agent: "npm/6.14.12 node/v10.24.1 darwin x64" // user-agent: "npm/6.14.12 node/v10.24.1 darwin x64"
const m = /\bnpm\/(\d{1,5})\./.exec(ctx.get('user-agent')); // pnpm: "pnpm/10.17.0 npm/? node/v20.19.5 darwin arm64"
if (!m) { const isPnpm = userAgent.startsWith('pnpm/');
const m = /\bnpm\/(\d{1,5})\./.exec(userAgent);
if (m) {
const major = Number.parseInt(m[1]);
if (major < 7) {
throw new ForbiddenError('Only allow npm@>=7.0.0 client to access');
}
} else if (!isPnpm) {
throw new ForbiddenError('Only allow npm client to access'); throw new ForbiddenError('Only allow npm client to access');
} }
const major = Number.parseInt(m[1]);
if (major < 7) {
throw new ForbiddenError('Only allow npm@>=7.0.0 client to access');
}
} }
if (role === 'setting') { if (role === 'setting') {
if (token.isReadonly) { if (token.isReadonly) {

View File

@@ -7,7 +7,7 @@ import {
HTTPQuery, HTTPQuery,
Inject, Inject,
} from '@eggjs/tegg'; } from '@eggjs/tegg';
import { Type } from 'egg-typebox-validate/typebox'; import { Type } from '@eggjs/typebox-validate/typebox';
import { AbstractController } from './AbstractController.js'; import { AbstractController } from './AbstractController.js';
import type { ChangeRepository } from '../../repository/ChangeRepository.js'; import type { ChangeRepository } from '../../repository/ChangeRepository.js';

View File

@@ -17,7 +17,7 @@ import {
import { AbstractController } from './AbstractController.js'; import { AbstractController } from './AbstractController.js';
import type { ProxyCacheRepository } from '../../repository/ProxyCacheRepository.js'; import type { ProxyCacheRepository } from '../../repository/ProxyCacheRepository.js';
import type { Static } from 'egg-typebox-validate/typebox'; import type { Static } from '@eggjs/typebox-validate/typebox';
import type { QueryPageOptions } from '../typebox.js'; import type { QueryPageOptions } from '../typebox.js';
import { FULLNAME_REG_STRING } from '../../common/PackageUtil.js'; import { FULLNAME_REG_STRING } from '../../common/PackageUtil.js';
import type { ProxyCacheService } from '../../core/service/ProxyCacheService.js'; import type { ProxyCacheService } from '../../core/service/ProxyCacheService.js';

View File

@@ -11,7 +11,7 @@ import {
Middleware, Middleware,
} from '@eggjs/tegg'; } from '@eggjs/tegg';
import { NotFoundError } from 'egg-errors'; import { NotFoundError } from 'egg-errors';
import type { Static } from 'egg-typebox-validate/typebox'; import type { Static } from '@eggjs/typebox-validate/typebox';
import { AbstractController } from './AbstractController.js'; import { AbstractController } from './AbstractController.js';
import type { import type {

View File

@@ -10,7 +10,7 @@ import {
Middleware, Middleware,
} from '@eggjs/tegg'; } from '@eggjs/tegg';
import { E400 } from 'egg-errors'; import { E400 } from 'egg-errors';
import type { Static } from 'egg-typebox-validate/typebox'; import type { Static } from '@eggjs/typebox-validate/typebox';
import { AbstractController } from './AbstractController.js'; import { AbstractController } from './AbstractController.js';
import { AdminAccess } from '../middleware/AdminAccess.js'; import { AdminAccess } from '../middleware/AdminAccess.js';

View File

@@ -9,7 +9,7 @@ import {
HTTPParam, HTTPParam,
Inject, Inject,
} from '@eggjs/tegg'; } from '@eggjs/tegg';
import { Type, type Static } from 'egg-typebox-validate/typebox'; import { Type, type Static } from '@eggjs/typebox-validate/typebox';
import type { AuthAdapter } from '../../infra/AuthAdapter.js'; import type { AuthAdapter } from '../../infra/AuthAdapter.js';
import { AbstractController } from './AbstractController.js'; import { AbstractController } from './AbstractController.js';

View File

@@ -13,7 +13,7 @@ import {
UnauthorizedError, UnauthorizedError,
UnprocessableEntityError, UnprocessableEntityError,
} from 'egg-errors'; } from 'egg-errors';
import { Type, type Static } from 'egg-typebox-validate/typebox'; import { Type, type Static } from '@eggjs/typebox-validate/typebox';
import { AbstractController } from './AbstractController.js'; import { AbstractController } from './AbstractController.js';
import { LoginResultCode } from '../../common/enum/User.js'; import { LoginResultCode } from '../../common/enum/User.js';

View File

@@ -17,7 +17,7 @@ import {
} from '@eggjs/tegg'; } from '@eggjs/tegg';
import { checkData, fromData } from 'ssri'; import { checkData, fromData } from 'ssri';
import validateNpmPackageName from 'validate-npm-package-name'; import validateNpmPackageName from 'validate-npm-package-name';
import { Type, type Static } from 'egg-typebox-validate/typebox'; import { Type, type Static } from '@eggjs/typebox-validate/typebox';
import { AbstractController } from '../AbstractController.js'; import { AbstractController } from '../AbstractController.js';
import { import {

View File

@@ -9,7 +9,7 @@ import {
Inject, Inject,
Middleware, Middleware,
} from '@eggjs/tegg'; } from '@eggjs/tegg';
import type { Static } from 'egg-typebox-validate/typebox'; import type { Static } from '@eggjs/typebox-validate/typebox';
import { E451 } from 'egg-errors'; import { E451 } from 'egg-errors';
import { AbstractController } from '../AbstractController.js'; import { AbstractController } from '../AbstractController.js';

View File

@@ -9,7 +9,7 @@ import {
HTTPParam, HTTPParam,
Inject, Inject,
} from '@eggjs/tegg'; } from '@eggjs/tegg';
import { Type, type Static } from 'egg-typebox-validate/typebox'; import { Type, type Static } from '@eggjs/typebox-validate/typebox';
import { AbstractController } from '../AbstractController.js'; import { AbstractController } from '../AbstractController.js';
import { FULLNAME_REG_STRING } from '../../../common/PackageUtil.js'; import { FULLNAME_REG_STRING } from '../../../common/PackageUtil.js';

View File

@@ -1,8 +1,8 @@
import { Type, type Static } from 'egg-typebox-validate/typebox'; import { Type, type Static } from '@eggjs/typebox-validate/typebox';
import semver from 'semver'; import semver from 'semver';
import npa from 'npm-package-arg'; import npa from 'npm-package-arg';
import { uniq } from 'lodash-es'; import { uniq } from 'lodash-es';
import type { Ajv } from 'egg-typebox-validate'; import type { Ajv } from '@eggjs/typebox-validate';
import { RegistryType } from '../common/enum/Registry.js'; import { RegistryType } from '../common/enum/Registry.js';
import { HookType } from '../common/enum/Hook.js'; import { HookType } from '../common/enum/Hook.js';

View File

@@ -10,7 +10,8 @@ import {
Inject, Inject,
} from '@eggjs/tegg'; } from '@eggjs/tegg';
import type { EggAppConfig, EggLogger } from 'egg'; import type { EggAppConfig, EggLogger } from 'egg';
import { Type, type Static } from 'egg-typebox-validate/typebox'; import '@eggjs/typebox-validate';
import { Type, type Static } from '@eggjs/typebox-validate/typebox';
import { ForbiddenError, NotFoundError } from 'egg-errors'; import { ForbiddenError, NotFoundError } from 'egg-errors';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import base64url from 'base64url'; import base64url from 'base64url';

View File

@@ -1,7 +1,8 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import { join } from 'node:path'; import { join } from 'node:path';
import type { Context, EggAppConfig, PowerPartial } from 'egg';
import type { Context, EggAppConfig, PartialEggConfig } from 'egg';
import OSSClient from 'oss-cnpm'; import OSSClient from 'oss-cnpm';
import S3Client from 's3-cnpmcore'; import S3Client from 's3-cnpmcore';
import { env } from 'read-env-value'; import { env } from 'read-env-value';
@@ -84,9 +85,9 @@ export interface NFSConfig {
removeBeforeUpload: boolean; removeBeforeUpload: boolean;
} }
export type Config = PowerPartial<EggAppConfig> & { nfs: NFSConfig }; export type Config = PartialEggConfig & { nfs: NFSConfig };
export default function startConfig(appInfo: EggAppConfig) { export default function startConfig(appInfo: EggAppConfig): Config {
const config = {} as Config; const config = {} as Config;
config.keys = env('CNPMCORE_EGG_KEYS', 'string', randomUUID()); config.keys = env('CNPMCORE_EGG_KEYS', 'string', randomUUID());

View File

@@ -7,7 +7,7 @@ import { database } from './database.js';
// @ts-expect-error has no construct signatures // @ts-expect-error has no construct signatures
export const mockES = new Mock(); export const mockES = new Mock();
export default function startConfig(appInfo: EggAppConfig) { export default function startConfig(appInfo: EggAppConfig): PowerPartial<EggAppConfig> {
const config = {} as PowerPartial<EggAppConfig>; const config = {} as PowerPartial<EggAppConfig>;
config.dataDir = join(appInfo.root, '.cnpmcore_unittest'); config.dataDir = join(appInfo.root, '.cnpmcore_unittest');

View File

@@ -42,7 +42,7 @@ const plugin: EggPlugin = {
}, },
typeboxValidate: { typeboxValidate: {
enable: true, enable: true,
package: 'egg-typebox-validate', package: '@eggjs/typebox-validate',
}, },
redis: { redis: {
enable: true, enable: true,

View File

@@ -41,24 +41,25 @@
"dev:postgresql": "CNPMCORE_DATABASE_TYPE=PostgreSQL egg-bin dev", "dev:postgresql": "CNPMCORE_DATABASE_TYPE=PostgreSQL egg-bin dev",
"lint": "oxlint", "lint": "oxlint",
"lint:fix": "npm run lint -- --fix", "lint:fix": "npm run lint -- --fix",
"test:postgresql": "npm run lint:fix && npm run test:local:postgresql", "typecheck": "tsc --noEmit",
"test:postgresql": "npm run lint && npm run test:local:postgresql",
"pretest:local:postgresql": "bash prepare-database-postgresql.sh", "pretest:local:postgresql": "bash prepare-database-postgresql.sh",
"test:local:postgresql": "CNPMCORE_DATABASE_TYPE=PostgreSQL egg-bin test", "test:local:postgresql": "CNPMCORE_DATABASE_TYPE=PostgreSQL egg-bin test",
"pretest": "npm run clean", "pretest": "npm run clean",
"test": "npm run lint:fix && npm run test:local", "test": "npm run lint && npm run typecheck && npm run test:local",
"pretest:local": "bash prepare-database-mysql.sh", "pretest:local": "bash prepare-database-mysql.sh",
"test:local": "egg-bin test", "test:local": "egg-bin test",
"pret": "bash prepare-database-mysql.sh", "pret": "bash prepare-database-mysql.sh",
"t": "npm run lint:fix && egg-bin test --changed", "t": "npm run lint && egg-bin test --changed",
"precov": "bash prepare-database-mysql.sh", "precov": "bash prepare-database-mysql.sh",
"cov": "egg-bin cov", "cov": "egg-bin cov",
"precov:postgresql": "bash prepare-database-postgresql.sh", "precov:postgresql": "bash prepare-database-postgresql.sh",
"cov:postgresql": "CNPMCORE_DATABASE_TYPE=PostgreSQL egg-bin cov", "cov:postgresql": "CNPMCORE_DATABASE_TYPE=PostgreSQL egg-bin cov",
"preci": "npm run clean && npm run lint", "preci": "npm run clean && npm run lint && npm run typecheck",
"ci": "npm run cov ", "ci": "npm run cov ",
"postci": "npm run tsc:prod && npm run clean", "postci": "npm run tsc:prod && npm run clean",
"ci:postgresql": "npm run lint && npm run cov:postgresql && npm run tsc:prod && npm run clean", "ci:postgresql": "npm run lint && npm run cov:postgresql && npm run tsc:prod && npm run clean",
"clean": "tsc -b --clean && rm -rf dist", "clean": "tsc -b --clean && rm -rf dist *.tsbuildinfo",
"tsc": "npm run clean && tsc -p ./tsconfig.json", "tsc": "npm run clean && tsc -p ./tsconfig.json",
"tsc:prod": "npm run clean && tsc -p ./tsconfig.prod.json", "tsc:prod": "npm run clean && tsc -p ./tsconfig.prod.json",
"prepublishOnly": "npm run tsc:prod", "prepublishOnly": "npm run tsc:prod",
@@ -88,7 +89,7 @@
"mocha": "11.6.0" "mocha": "11.6.0"
}, },
"dependencies": { "dependencies": {
"@eggjs/redis": "^3.0.0", "@eggjs/redis": "beta",
"@eggjs/scripts": "^4.0.0", "@eggjs/scripts": "^4.0.0",
"@eggjs/tegg": "beta", "@eggjs/tegg": "beta",
"@eggjs/tegg-aop-plugin": "beta", "@eggjs/tegg-aop-plugin": "beta",
@@ -99,7 +100,8 @@
"@eggjs/tegg-orm-plugin": "beta", "@eggjs/tegg-orm-plugin": "beta",
"@eggjs/tegg-plugin": "beta", "@eggjs/tegg-plugin": "beta",
"@eggjs/tegg-schedule-plugin": "beta", "@eggjs/tegg-schedule-plugin": "beta",
"@eggjs/tracer": "^3.0.0", "@eggjs/tracer": "beta",
"@eggjs/typebox-validate": "beta",
"@elastic/elasticsearch": "^8.8.1", "@elastic/elasticsearch": "^8.8.1",
"@fengmk2/tar": "^6.2.0", "@fengmk2/tar": "^6.2.0",
"@node-rs/crc32": "^1.2.2", "@node-rs/crc32": "^1.2.2",
@@ -108,11 +110,10 @@
"base64url": "^3.0.1", "base64url": "^3.0.1",
"bson-objectid": "^2.0.4", "bson-objectid": "^2.0.4",
"dayjs": "^1.10.7", "dayjs": "^1.10.7",
"egg": "^4.0.8", "egg": "beta",
"egg-cors": "^3.0.0", "egg-cors": "^3.0.0",
"egg-errors": "^2.3.0", "egg-errors": "^2.3.0",
"egg-status": "^1.0.0", "egg-status": "^1.0.0",
"egg-typebox-validate": "^3.0.0",
"egg-view-nunjucks": "^2.3.0", "egg-view-nunjucks": "^2.3.0",
"eggjs-elasticsearch": "^0.0.6", "eggjs-elasticsearch": "^0.0.6",
"fast-xml-parser": "^5.0.9", "fast-xml-parser": "^5.0.9",
@@ -138,10 +139,10 @@
"s3-cnpmcore": "^1.1.2" "s3-cnpmcore": "^1.1.2"
}, },
"devDependencies": { "devDependencies": {
"@eggjs/bin": "^7.1.0", "@eggjs/bin": "beta",
"@eggjs/mock": "^6.0.7", "@eggjs/mock": "beta",
"@eggjs/oxlint-config": "^1.0.0", "@eggjs/oxlint-config": "^1.0.0",
"@eggjs/tsconfig": "^2.0.0", "@eggjs/tsconfig": "beta",
"@elastic/elasticsearch-mock": "^2.0.0", "@elastic/elasticsearch-mock": "^2.0.0",
"@simplewebauthn/typescript-types": "^7.0.0", "@simplewebauthn/typescript-types": "^7.0.0",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
@@ -159,6 +160,7 @@
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^15.5.0", "lint-staged": "^15.5.0",
"oxlint": "^1.11.0", "oxlint": "^1.11.0",
"oxlint-tsgolint": "^0.2.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "5" "typescript": "5"
}, },
@@ -169,7 +171,7 @@
}, },
"homepage": "https://github.com/cnpm/cnpmcore#readme", "homepage": "https://github.com/cnpm/cnpmcore#readme",
"engines": { "engines": {
"node": ">= 20.18.0" "node": "^20.18.0 || >=22.18.0"
}, },
"lint-staged": { "lint-staged": {
"*": "prettier --write --ignore-unknown --cache", "*": "prettier --write --ignore-unknown --cache",

View File

@@ -263,6 +263,24 @@ describe('test/port/controller/package/UpdatePackageController.test.ts', () => {
assert.deepEqual(res.body, { ok: true }); assert.deepEqual(res.body, { ok: true });
}); });
it('should support pnpm client', async () => {
const user = await TestUtil.createUser();
mock(app.config.cnpmcore, 'admins', { [user.name]: user.email });
const res = await app
.httpRequest()
.put(`/${scopedName}/-rev/${rev}`)
.set('authorization', user.authorization)
.set('user-agent', 'pnpm/7.3.1 npm/?')
.set('npm-command', 'owner')
.send({
_id: rev,
_rev: rev,
maintainers: [{ name: user.name, email: user.email }],
});
assert.deepEqual(res.body, { ok: true });
assert.equal(res.statusCode, 200);
});
it('should 403 when npm client invalid', async () => { it('should 403 when npm client invalid', async () => {
const user = await TestUtil.createUser(); const user = await TestUtil.createUser();
let res = await app let res = await app
@@ -474,6 +492,24 @@ describe('test/port/controller/package/UpdatePackageController.test.ts', () => {
}) })
.expect(200); .expect(200);
assert.equal(res.body.ok, true); assert.equal(res.body.ok, true);
// should valid with pnpm10 and npm=?
res = await app
.httpRequest()
.put(`/${scopedName}/-rev/${rev}`)
.set('authorization', publisher.authorization)
.set('user-agent', 'pnpm/10.0.0 npm/?')
.set('npm-command', 'owner')
.send({
_id: rev,
_rev: rev,
maintainers: [
{ name: user.name, email: user.email },
{ name: publisher.name, email: publisher.email },
],
})
.expect(200);
assert.equal(res.body.ok, true);
}); });
}); });
}); });

View File

@@ -4,7 +4,7 @@
"target": "ES2021", "target": "ES2021",
"resolveJsonModule": true, "resolveJsonModule": true,
"useUnknownInCatchVariables": false, "useUnknownInCatchVariables": false,
"declaration": false "declaration": false,
}, "erasableSyntaxOnly": false
"exclude": ["test"] }
} }