Compare commits
25 Commits
@verdaccio
...
@verdaccio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e10637f0f3 | ||
|
|
459b6fa72b | ||
|
|
10868ed434 | ||
|
|
ada8165f98 | ||
|
|
57755f31ba | ||
|
|
9e29bf8890 | ||
|
|
e2a67bafbe | ||
|
|
7c9561b019 | ||
|
|
ed3677a5b6 | ||
|
|
055544238a | ||
|
|
4937dba06e | ||
|
|
3e65791564 | ||
|
|
cf1b6cdb04 | ||
|
|
ca86082e08 | ||
|
|
ef60e83d6c | ||
|
|
ff1822c961 | ||
|
|
df0da3d699 | ||
|
|
af035e4f87 | ||
|
|
99dad7759e | ||
|
|
2e3b9552cc | ||
|
|
b6e8554764 | ||
|
|
b51892cb9a | ||
|
|
7e4d5615a4 | ||
|
|
d21279b35c | ||
|
|
5941edcf38 |
59
.changeset/dry-planes-tap.md
Normal file
59
.changeset/dry-planes-tap.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
'@verdaccio/api': major
|
||||
'@verdaccio/auth': major
|
||||
'@verdaccio/cli': major
|
||||
'@verdaccio/config': major
|
||||
'@verdaccio/commons-api': major
|
||||
'@verdaccio/core': major
|
||||
'@verdaccio/local-storage': major
|
||||
'@verdaccio/fastify-migration': major
|
||||
'@verdaccio/streams': major
|
||||
'@verdaccio/types': major
|
||||
'@verdaccio/hooks': major
|
||||
'verdaccio-audit': major
|
||||
'verdaccio-aws-s3-storage': major
|
||||
'verdaccio-google-cloud': major
|
||||
'verdaccio-memory': major
|
||||
'@verdaccio/ui-theme': major
|
||||
'@verdaccio/proxy': major
|
||||
'@verdaccio/server': major
|
||||
'@verdaccio/store': major
|
||||
'@verdaccio/eslint-config': major
|
||||
'@verdaccio/dev-types': major
|
||||
'@verdaccio/utils': major
|
||||
'verdaccio': major
|
||||
'@verdaccio/web': major
|
||||
---
|
||||
|
||||
refactor: search v1 endpoint and local-database
|
||||
|
||||
- refactor search `api v1` endpoint, improve performance
|
||||
- remove usage of `async` dependency https://github.com/verdaccio/verdaccio/issues/1225
|
||||
- refactor method storage class
|
||||
- create new module `core` to reduce the ammount of modules with utilities
|
||||
- use `undici` instead `node-fetch`
|
||||
- use `fastify` instead `express` for functional test
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- plugin storage API changes
|
||||
- remove old search endpoint (return 404)
|
||||
- filter local private packages at plugin level
|
||||
|
||||
The storage api changes for methods `get`, `add`, `remove` as promise base. The `search` methods also changes and recieves a `query` object that contains all query params from the client.
|
||||
|
||||
```ts
|
||||
export interface IPluginStorage<T> extends IPlugin {
|
||||
add(name: string): Promise<void>;
|
||||
remove(name: string): Promise<void>;
|
||||
get(): Promise<any>;
|
||||
init(): Promise<void>;
|
||||
getSecret(): Promise<string>;
|
||||
setSecret(secret: string): Promise<any>;
|
||||
getPackageStorage(packageInfo: string): IPackageStorage;
|
||||
search(query: searchUtils.SearchQuery): Promise<searchUtils.SearchItem[]>;
|
||||
saveToken(token: Token): Promise<any>;
|
||||
deleteToken(user: string, tokenKey: string): Promise<any>;
|
||||
readTokens(filter: TokenFilter): Promise<Token[]>;
|
||||
}
|
||||
```
|
||||
5
.changeset/perfect-emus-clean.md
Normal file
5
.changeset/perfect-emus-clean.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@verdaccio/cli': major
|
||||
---
|
||||
|
||||
feat: node 14 as minimum for running cli
|
||||
@@ -42,12 +42,14 @@
|
||||
"@verdaccio/fastify-migration": "6.0.0-6-next.9",
|
||||
"@verdaccio/eslint-config": "1.0.0",
|
||||
"@verdaccio/benchmark": "1.0.0",
|
||||
"@verdaccio/website": "5.1.3"
|
||||
"@verdaccio/website": "5.1.3",
|
||||
"@verdaccio/core": "6.0.0-next.0"
|
||||
},
|
||||
"changesets": [
|
||||
"afraid-mice-obey",
|
||||
"big-lobsters-sin",
|
||||
"calm-pants-impress",
|
||||
"dry-planes-tap",
|
||||
"few-cooks-destroy",
|
||||
"few-mangos-grow",
|
||||
"fifty-jars-rest",
|
||||
@@ -64,11 +66,13 @@
|
||||
"many-vans-care",
|
||||
"modern-spies-tell",
|
||||
"neat-toes-report",
|
||||
"perfect-emus-clean",
|
||||
"perfect-kangaroos-agree",
|
||||
"plenty-news-remember",
|
||||
"plenty-spiders-melt",
|
||||
"plenty-tables-refuse",
|
||||
"pretty-hounds-tap",
|
||||
"proud-jeans-walk",
|
||||
"red-chefs-float",
|
||||
"shiny-chefs-heal",
|
||||
"smart-apricots-kneel",
|
||||
|
||||
10
.changeset/proud-jeans-walk.md
Normal file
10
.changeset/proud-jeans-walk.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
'verdaccio-htpasswd': patch
|
||||
'@verdaccio/local-storage': patch
|
||||
'@verdaccio/fastify-migration': patch
|
||||
'@verdaccio/hooks': patch
|
||||
'@verdaccio/mock': patch
|
||||
'@verdaccio/node-api': patch
|
||||
---
|
||||
|
||||
Added core-js missing from dependencies though referenced in .js sources
|
||||
@@ -9,3 +9,5 @@ static/
|
||||
website/
|
||||
wiki/
|
||||
dist/
|
||||
docs/
|
||||
test/functional/store/*
|
||||
|
||||
7
.github/workflows/benckmark.yml
vendored
7
.github/workflows/benckmark.yml
vendored
@@ -2,6 +2,7 @@
|
||||
name: ci - benchmark
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# 3 times day
|
||||
# collecting enough data to draw some graphics
|
||||
@@ -56,7 +57,8 @@ jobs:
|
||||
# - local
|
||||
- 3.13.1
|
||||
- 4.12.2
|
||||
- 5.1.2
|
||||
- 5.1.3
|
||||
- 6.0.0-6-next.22
|
||||
name: Benchmark autocannon
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -116,7 +118,8 @@ jobs:
|
||||
# old versions to compare same test along previous releases
|
||||
- 3.13.1
|
||||
- 4.12.2
|
||||
- 5.1.2
|
||||
- 5.1.3
|
||||
- 6.0.0-6-next.22
|
||||
name: Benchmark hyperfine
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
15
.github/workflows/changesets.yml
vendored
15
.github/workflows/changesets.yml
vendored
@@ -28,24 +28,24 @@ jobs:
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
registry-url: 'https://registry.verdaccio.org'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.VERDACCIO_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.REGISTRY_AUTH_TOKEN }}
|
||||
|
||||
- name: install pnpm
|
||||
run: npm i pnpm@6.10.3 -g
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.VERDACCIO_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.REGISTRY_AUTH_TOKEN }}
|
||||
|
||||
- name: setup pnpm config
|
||||
run: pnpm config set store-dir $PNPM_CACHE_FOLDER
|
||||
- name: setup pnpm config registry
|
||||
run: pnpm config set registry https://registry.verdaccio.org
|
||||
run: pnpm config set registry https://registry.npmjs.org
|
||||
|
||||
- name: install dependencies
|
||||
run: pnpm install
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.VERDACCIO_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.REGISTRY_AUTH_TOKEN }}
|
||||
|
||||
- name: build
|
||||
run: pnpm build
|
||||
@@ -59,5 +59,6 @@ jobs:
|
||||
publish: pnpm ci:publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.VERDACCIO_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.VERDACCIO_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.REGISTRY_AUTH_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.REGISTRY_AUTH_TOKEN }}
|
||||
NPM_CONFIG_REGISTRY: https://registry.npmjs.org
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -192,6 +192,8 @@ jobs:
|
||||
run: pnpm recursive install --frozen-lockfile
|
||||
- name: Test CLI
|
||||
run: pnpm test:e2e:cli
|
||||
env:
|
||||
DEBUG: verdaccio*
|
||||
test-windows:
|
||||
needs: [format, lint]
|
||||
runs-on: windows-latest
|
||||
|
||||
10
.github/workflows/website.yml
vendored
10
.github/workflows/website.yml
vendored
@@ -82,13 +82,13 @@ jobs:
|
||||
|
||||
# Will deploy to Preview URL, only when a pull request is open with changes on the website
|
||||
- name: Build Deployment Preview
|
||||
if: github.event_name == 'pull_request' && github.ref != 'refs/heads/master'
|
||||
if: github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && github.event.label.name == 'trigger-preview'
|
||||
env:
|
||||
CONTEXT: deploy-preview
|
||||
run: pnpm netlify:build:deployPreview --filter ...@verdaccio/website
|
||||
|
||||
- name: 🤖 Deploy Preview Netlify
|
||||
if: github.event_name == 'pull_request' && github.ref != 'refs/heads/master'
|
||||
if: github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && github.event.label.name == 'trigger-preview'
|
||||
uses: semoal/action-netlify-deploy@master
|
||||
id: netlify_preview
|
||||
with:
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
build-dir: './website/build'
|
||||
|
||||
- name: Audit preview URL with Lighthouse
|
||||
if: github.event_name == 'pull_request' && github.ref != 'refs/heads/master'
|
||||
if: github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && github.event.label.name == 'trigger-preview'
|
||||
id: lighthouse_audit
|
||||
uses: treosh/lighthouse-ci-action@v3
|
||||
with:
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
temporaryPublicStorage: true
|
||||
|
||||
- name: Format lighthouse score
|
||||
if: github.event_name == 'pull_request' && github.ref != 'refs/heads/master'
|
||||
if: github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && github.event.label.name == 'trigger-preview'
|
||||
id: format_lighthouse_score
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
core.setOutput("comment", comment);
|
||||
|
||||
- name: Add comment to PR
|
||||
if: github.event_name == 'pull_request' && github.ref != 'refs/heads/master'
|
||||
if: github.event_name == 'pull_request' && github.ref != 'refs/heads/master' && github.event.label.name == 'trigger-preview'
|
||||
id: comment_to_pr
|
||||
uses: marocchino/sticky-pull-request-comment@v1
|
||||
with:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -43,3 +43,7 @@ api-results.json
|
||||
hyper-results.json
|
||||
hyper-results*.json
|
||||
api-results*.json
|
||||
|
||||
#docs
|
||||
api/
|
||||
packages/core/core/docs
|
||||
|
||||
2
.npmrc
2
.npmrc
@@ -1,5 +1,5 @@
|
||||
always-auth = true
|
||||
recursive-install = true
|
||||
registry = https://registry.verdaccio.org
|
||||
loglevel=warn
|
||||
loglevel=info
|
||||
fetch-retries="10"
|
||||
|
||||
@@ -17,6 +17,7 @@ node_modules/
|
||||
**/static/*.js
|
||||
**/build/*.js
|
||||
packages/core/local-storage/_storage/**
|
||||
packages/partials/storage_default_storage/
|
||||
packages/standalone/dist/bundle.js
|
||||
docker-examples/v5/reverse_proxy/nginx/relative_path/storage/*
|
||||
docker-examples/
|
||||
@@ -24,3 +25,9 @@ build/
|
||||
.vscode/
|
||||
.github/
|
||||
.netlify/
|
||||
packages/**/docs/**
|
||||
packages/mock/mock-store/**
|
||||
api/**
|
||||
packages/core/local-storage/tests/__fixtures__/test-storage/
|
||||
packages/plugins/ui-theme/static/
|
||||
.verdaccio-db.json
|
||||
|
||||
10
.vscode/launch.json
vendored
10
.vscode/launch.json
vendored
@@ -4,6 +4,16 @@
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"name": "Attach",
|
||||
"port": 9229,
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "pwa-node"
|
||||
},
|
||||
{
|
||||
"name": "Verdaccio Debug",
|
||||
"port": 9229,
|
||||
|
||||
10
README.md
10
README.md
@@ -32,10 +32,14 @@ Google Cloud Storage** or create your own plugin.
|
||||
Install with npm:
|
||||
|
||||
```bash
|
||||
npm install --global verdaccio@6-next --registry https://registry.verdaccio.org/
|
||||
npm install --global verdaccio@6-next
|
||||
```
|
||||
|
||||
> Published on a temporary registry while setup is ready to publish on npmjs
|
||||
or
|
||||
|
||||
```bash
|
||||
docker pull verdaccio/verdaccio:nightly-master
|
||||
```
|
||||
|
||||
## Donations
|
||||
|
||||
@@ -71,7 +75,7 @@ booted in a couple of seconds, fast enough for any CI. Many open source projects
|
||||
|
||||
### **Testing the integrity of your React components by publishing in a private registry - React Finland 2021**.
|
||||
|
||||
[](https://react-finland.fi/schedule/#testing-the-integrity-of-your-react-components-by-publishing-in-a-private-registry)
|
||||
[](https://www.youtube.com/watch?v=5olfi5wbgF4)
|
||||
|
||||
You might want to check out as well our previous talks:
|
||||
|
||||
|
||||
2
docker-examples/v4/reverse_proxy/nginx/relative_path/nginx_ssl/Dockerfile
Executable file → Normal file
2
docker-examples/v4/reverse_proxy/nginx/relative_path/nginx_ssl/Dockerfile
Executable file → Normal file
@@ -1,4 +1,4 @@
|
||||
FROM nginx
|
||||
FROM nginx:1
|
||||
|
||||
COPY cert.crt /etc/nginx/cert.crt
|
||||
COPY cert.key /etc/nginx/cert.key
|
||||
|
||||
@@ -55919,7 +55919,6 @@
|
||||
"@babel/plugin-transform-async-to-generator": "7.2.0",
|
||||
"@babel/plugin-transform-classes": "7.2.2",
|
||||
"@babel/plugin-transform-runtime": "7.2.0",
|
||||
"@babel/polyfill": "7.2.3",
|
||||
"@babel/preset-env": "7.2.3",
|
||||
"@babel/preset-flow": "7.0.0",
|
||||
"@babel/preset-react": "7.0.0",
|
||||
|
||||
2
docker-examples/v5/reverse_proxy/nginx_relative/nginx_ssl/Dockerfile
Executable file → Normal file
2
docker-examples/v5/reverse_proxy/nginx_relative/nginx_ssl/Dockerfile
Executable file → Normal file
@@ -1,4 +1,4 @@
|
||||
FROM nginx
|
||||
FROM nginx:1
|
||||
|
||||
COPY cert.crt /etc/nginx/cert.crt
|
||||
COPY cert.key /etc/nginx/cert.key
|
||||
|
||||
@@ -48,3 +48,7 @@ $ VERDACCIO_FORWARDED_PROTO=CloudFront-Forwarded-Proto verdaccio --listen 5000
|
||||
#### VERDACCIO_STORAGE_PATH
|
||||
|
||||
By default, the storage is taken from config file, but using this variable allows to set it from environment variable.
|
||||
|
||||
#### VERDACCIO_STORAGE_NAME
|
||||
|
||||
The database name for `@verdaccio/local-storge` is by default `.verdaccio-db.json`, but this can be update by using this variable.
|
||||
|
||||
44
package.json
44
package.json
@@ -15,11 +15,11 @@
|
||||
"url": "https://opencollective.com/verdaccio"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.14.8",
|
||||
"@babel/core": "7.15.0",
|
||||
"@babel/node": "7.14.9",
|
||||
"@babel/cli": "7.15.4",
|
||||
"@babel/core": "7.15.5",
|
||||
"@babel/node": "7.15.4",
|
||||
"@babel/plugin-proposal-class-properties": "7.14.5",
|
||||
"@babel/plugin-proposal-decorators": "7.14.5",
|
||||
"@babel/plugin-proposal-decorators": "7.15.4",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.14.5",
|
||||
"@babel/plugin-proposal-function-sent": "7.14.5",
|
||||
"@babel/plugin-proposal-json-strings": "7.14.5",
|
||||
@@ -31,28 +31,24 @@
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/plugin-syntax-import-meta": "7.10.4",
|
||||
"@babel/plugin-transform-async-to-generator": "7.14.5",
|
||||
"@babel/plugin-transform-classes": "7.14.9",
|
||||
"@babel/plugin-transform-classes": "7.15.4",
|
||||
"@babel/plugin-transform-runtime": "7.15.0",
|
||||
"@babel/polyfill": "7.12.1",
|
||||
"@babel/preset-env": "7.15.0",
|
||||
"@babel/preset-env": "7.15.4",
|
||||
"@babel/preset-react": "7.14.5",
|
||||
"@babel/preset-typescript": "7.15.0",
|
||||
"@babel/register": "7.15.3",
|
||||
"@babel/runtime": "7.15.3",
|
||||
"@changesets/changelog-github": "^0.2.8",
|
||||
"@babel/runtime": "7.15.4",
|
||||
"@changesets/changelog-github": "0.2.8",
|
||||
"@changesets/cli": "2.15.0",
|
||||
"@changesets/get-dependents-graph": "^1.2.0",
|
||||
"@commitlint/cli": "8.3.5",
|
||||
"@commitlint/config-conventional": "8.2.0",
|
||||
"@changesets/get-dependents-graph": "1.2.2",
|
||||
"@crowdin/cli": "3.6.5",
|
||||
"@types/async": "3.2.7",
|
||||
"@types/autocannon": "4.1.1",
|
||||
"@types/autosuggest-highlight": "3.1.1",
|
||||
"@types/express": "4.17.8",
|
||||
"@types/express": "4.17.6",
|
||||
"@types/http-errors": "1.8.1",
|
||||
"@types/jest": "27.0.1",
|
||||
"@types/lodash": "4.14.172",
|
||||
"@types/lowdb": "1.0.11",
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/minimatch": "3.0.5",
|
||||
"@types/node": "14.6.0",
|
||||
@@ -67,7 +63,7 @@
|
||||
"@types/supertest": "2.0.11",
|
||||
"@types/testing-library__jest-dom": "5.14.1",
|
||||
"@types/validator": "13.6.3",
|
||||
"@types/webpack": "4.41.26",
|
||||
"@types/webpack": "5.28.0",
|
||||
"@types/webpack-env": "1.16.2",
|
||||
"@typescript-eslint/eslint-plugin": "4.30.0",
|
||||
"@typescript-eslint/parser": "4.30.0",
|
||||
@@ -80,9 +76,10 @@
|
||||
"babel-eslint": "10.1.0",
|
||||
"babel-jest": "27.1.0",
|
||||
"babel-plugin-dynamic-import-node": "2.3.3",
|
||||
"babel-plugin-emotion": "10.0.33",
|
||||
"babel-plugin-emotion": "10.2.2",
|
||||
"codecov": "3.8.3",
|
||||
"concurrently": "6.2.1",
|
||||
"core-js": "3.17.2",
|
||||
"cross-env": "7.0.3",
|
||||
"debug": "4.3.2",
|
||||
"detect-secrets": "1.0.6",
|
||||
@@ -99,7 +96,7 @@
|
||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||
"eslint-plugin-verdaccio": "10.0.0",
|
||||
"fs-extra": "10.0.0",
|
||||
"husky": "2.7.0",
|
||||
"husky": "4.3.5",
|
||||
"in-publish": "2.0.1",
|
||||
"jest": "27.1.0",
|
||||
"jest-environment-jsdom": "27.1.0",
|
||||
@@ -108,7 +105,7 @@
|
||||
"jest-fetch-mock": "3.0.3",
|
||||
"jest-junit": "12.2.0",
|
||||
"kleur": "3.0.3",
|
||||
"lint-staged": "9.5.0",
|
||||
"lint-staged": "11.1.2",
|
||||
"nock": "12.0.3",
|
||||
"node-fetch": "3.0.0-beta.6-exportfix",
|
||||
"nodemon": "2.0.12",
|
||||
@@ -132,7 +129,7 @@
|
||||
"docker": "docker build -t verdaccio/verdaccio:local . --no-cache",
|
||||
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,yml,yaml,md}\"",
|
||||
"format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,yml,yaml,md}\"",
|
||||
"lint": "eslint --max-warnings 49 \"**/*.{js,jsx,ts,tsx}\"",
|
||||
"lint": "eslint --max-warnings 60 \"**/*.{js,jsx,ts,tsx}\"",
|
||||
"test": "pnpm recursive test --filter ./packages",
|
||||
"test:e2e:cli": "pnpm test --filter ...@verdaccio/e2e-cli",
|
||||
"test:e2e:ui": "pnpm test --filter ...@verdaccio/e2e-ui",
|
||||
@@ -146,8 +143,8 @@
|
||||
"_start:web": "pnpm start --filter ...@verdaccio/ui-theme",
|
||||
"_debug:reload": "nodemon -d 3 packages/verdaccio/debug/bootstrap.js",
|
||||
"start:ts": "ts-node packages/verdaccio/src/start.ts -- --listen 8000",
|
||||
"debug": "node --inspect packages/verdaccio/debug/bootstrap.js",
|
||||
"debug:break": "node --inspect-brk packages/verdaccio/debug/bootstrap.js",
|
||||
"debug": "node --trace-warnings --trace-uncaught --inspect packages/verdaccio/debug/bootstrap.js",
|
||||
"debug:break": "node --trace-warnings --trace-uncaught --inspect-brk packages/verdaccio/debug/bootstrap.js",
|
||||
"changeset": "changeset",
|
||||
"changeset:check": "changeset status --since-master",
|
||||
"ci:version": "run-s ci:version:changeset ci:version:install",
|
||||
@@ -161,11 +158,6 @@
|
||||
"crowdin:sync": "pnpm crowdin:upload && pnpm crowdin:download --verbose"
|
||||
},
|
||||
"license": "MIT",
|
||||
"commitlint": {
|
||||
"extends": [
|
||||
"@commitlint/config-conventional"
|
||||
]
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
|
||||
@@ -1,5 +1,66 @@
|
||||
# @verdaccio/api
|
||||
|
||||
## 6.0.0-6-next.14
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 459b6fa7: refactor: search v1 endpoint and local-database
|
||||
|
||||
- refactor search `api v1` endpoint, improve performance
|
||||
- remove usage of `async` dependency https://github.com/verdaccio/verdaccio/issues/1225
|
||||
- refactor method storage class
|
||||
- create new module `core` to reduce the ammount of modules with utilities
|
||||
- use `undici` instead `node-fetch`
|
||||
- use `fastify` instead `express` for functional test
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- plugin storage API changes
|
||||
- remove old search endpoint (return 404)
|
||||
- filter local private packages at plugin level
|
||||
|
||||
The storage api changes for methods `get`, `add`, `remove` as promise base. The `search` methods also changes and recieves a `query` object that contains all query params from the client.
|
||||
|
||||
```ts
|
||||
export interface IPluginStorage<T> extends IPlugin {
|
||||
add(name: string): Promise<void>;
|
||||
remove(name: string): Promise<void>;
|
||||
get(): Promise<any>;
|
||||
init(): Promise<void>;
|
||||
getSecret(): Promise<string>;
|
||||
setSecret(secret: string): Promise<any>;
|
||||
getPackageStorage(packageInfo: string): IPackageStorage;
|
||||
search(query: searchUtils.SearchQuery): Promise<searchUtils.SearchItem[]>;
|
||||
saveToken(token: Token): Promise<any>;
|
||||
deleteToken(user: string, tokenKey: string): Promise<any>;
|
||||
readTokens(filter: TokenFilter): Promise<Token[]>;
|
||||
}
|
||||
```
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [459b6fa7]
|
||||
- @verdaccio/auth@6.0.0-6-next.11
|
||||
- @verdaccio/config@6.0.0-6-next.8
|
||||
- @verdaccio/commons-api@11.0.0-6-next.4
|
||||
- @verdaccio/core@6.0.0-6-next.1
|
||||
- @verdaccio/hooks@6.0.0-6-next.6
|
||||
- @verdaccio/store@6.0.0-6-next.12
|
||||
- @verdaccio/utils@6.0.0-6-next.6
|
||||
- @verdaccio/middleware@6.0.0-6-next.11
|
||||
- @verdaccio/tarball@11.0.0-6-next.7
|
||||
- @verdaccio/logger@6.0.0-6-next.4
|
||||
|
||||
## 6.0.0-6-next.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [df0da3d6]
|
||||
- @verdaccio/hooks@6.0.0-6-next.5
|
||||
- @verdaccio/auth@6.0.0-6-next.10
|
||||
- @verdaccio/store@6.0.0-6-next.11
|
||||
- @verdaccio/middleware@6.0.0-6-next.10
|
||||
|
||||
## 6.0.0-6-next.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@verdaccio/api",
|
||||
"version": "6.0.0-6-next.12",
|
||||
"version": "6.0.0-6-next.14",
|
||||
"description": "loaders logic",
|
||||
"main": "./build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
@@ -39,15 +39,16 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@verdaccio/auth": "workspace:6.0.0-6-next.9",
|
||||
"@verdaccio/commons-api": "workspace:11.0.0-alpha.3",
|
||||
"@verdaccio/config": "workspace:6.0.0-6-next.7",
|
||||
"@verdaccio/hooks": "workspace:6.0.0-6-next.4",
|
||||
"@verdaccio/auth": "workspace:6.0.0-6-next.11",
|
||||
"@verdaccio/commons-api": "workspace:11.0.0-6-next.4",
|
||||
"@verdaccio/config": "workspace:6.0.0-6-next.8",
|
||||
"@verdaccio/core": "workspace:6.0.0-6-next.1",
|
||||
"@verdaccio/hooks": "workspace:6.0.0-6-next.6",
|
||||
"@verdaccio/logger": "workspace:6.0.0-6-next.4",
|
||||
"@verdaccio/middleware": "workspace:6.0.0-6-next.9",
|
||||
"@verdaccio/store": "workspace:6.0.0-6-next.10",
|
||||
"@verdaccio/tarball": "workspace:11.0.0-6-next.6",
|
||||
"@verdaccio/utils": "workspace:6.0.0-6-next.5",
|
||||
"@verdaccio/middleware": "workspace:6.0.0-6-next.11",
|
||||
"@verdaccio/store": "workspace:6.0.0-6-next.12",
|
||||
"@verdaccio/tarball": "workspace:11.0.0-6-next.7",
|
||||
"@verdaccio/utils": "workspace:6.0.0-6-next.6",
|
||||
"cookies": "0.8.0",
|
||||
"debug": "4.3.2",
|
||||
"express": "4.17.1",
|
||||
@@ -56,8 +57,8 @@
|
||||
"semver": "7.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@verdaccio/server": "workspace:6.0.0-6-next.17",
|
||||
"@verdaccio/types": "workspace:11.0.0-6-next.7",
|
||||
"@verdaccio/server": "workspace:6.0.0-6-next.19",
|
||||
"@verdaccio/types": "workspace:11.0.0-6-next.8",
|
||||
"body-parser": "1.19.0",
|
||||
"lodash": "4.17.21",
|
||||
"supertest": "6.1.6"
|
||||
|
||||
@@ -5,11 +5,11 @@ import { Router } from 'express';
|
||||
import { media, allow } from '@verdaccio/middleware';
|
||||
import { API_MESSAGE, HTTP_STATUS, DIST_TAGS, VerdaccioError } from '@verdaccio/commons-api';
|
||||
import { Package } from '@verdaccio/types';
|
||||
import { IStorageHandler } from '@verdaccio/store';
|
||||
import { Storage } from '@verdaccio/store';
|
||||
import { IAuth } from '@verdaccio/auth';
|
||||
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';
|
||||
|
||||
export default function (route: Router, auth: IAuth, storage: IStorageHandler): void {
|
||||
export default function (route: Router, auth: IAuth, storage: Storage): void {
|
||||
const can = allow(auth);
|
||||
const tag_package_version = function (
|
||||
req: $RequestExtend,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
antiLoop,
|
||||
} from '@verdaccio/middleware';
|
||||
import { IAuth } from '@verdaccio/auth';
|
||||
import { IStorageHandler } from '@verdaccio/store';
|
||||
import { Storage } from '@verdaccio/store';
|
||||
import { Config } from '@verdaccio/types';
|
||||
import bodyParser from 'body-parser';
|
||||
|
||||
@@ -23,7 +23,7 @@ import profile from './v1/profile';
|
||||
import token from './v1/token';
|
||||
import v1Search from './v1/search';
|
||||
|
||||
export default function (config: Config, auth: IAuth, storage: IStorageHandler): Router {
|
||||
export default function (config: Config, auth: IAuth, storage: Storage): Router {
|
||||
/* eslint new-cap:off */
|
||||
const app = express.Router();
|
||||
/* eslint new-cap:off */
|
||||
@@ -52,19 +52,15 @@ export default function (config: Config, auth: IAuth, storage: IStorageHandler):
|
||||
whoami(app);
|
||||
pkg(app, auth, storage, config);
|
||||
profile(app, auth);
|
||||
search(app, auth, storage);
|
||||
// @deprecated endpoint, 404 by default
|
||||
search(app);
|
||||
user(app, auth, config);
|
||||
distTags(app, auth, storage);
|
||||
publish(app, auth, storage, config);
|
||||
ping(app);
|
||||
stars(app, storage);
|
||||
|
||||
if (config?.flags?.search === true) {
|
||||
v1Search(app, auth, storage);
|
||||
}
|
||||
|
||||
if (config?.flags?.token === true) {
|
||||
token(app, auth, storage, config);
|
||||
}
|
||||
// @ts-ignore
|
||||
v1Search(app, auth, storage);
|
||||
token(app, auth, storage, config);
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getVersion, ErrorCode } from '@verdaccio/utils';
|
||||
import { HEADERS, DIST_TAGS, API_ERROR } from '@verdaccio/commons-api';
|
||||
import { Config, Package } from '@verdaccio/types';
|
||||
import { IAuth } from '@verdaccio/auth';
|
||||
import { IStorageHandler } from '@verdaccio/store';
|
||||
import { Storage } from '@verdaccio/store';
|
||||
import { convertDistRemoteToLocalTarballUrls } from '@verdaccio/tarball';
|
||||
import { $RequestExtend, $ResponseExtend, $NextFunctionVer } from '../types/custom';
|
||||
|
||||
@@ -34,12 +34,7 @@ const downloadStream = (
|
||||
stream.pipe(res);
|
||||
};
|
||||
|
||||
export default function (
|
||||
route: Router,
|
||||
auth: IAuth,
|
||||
storage: IStorageHandler,
|
||||
config: Config
|
||||
): void {
|
||||
export default function (route: Router, auth: IAuth, storage: Storage, config: Config): void {
|
||||
const can = allow(auth);
|
||||
// TODO: anonymous user?
|
||||
route.get(
|
||||
|
||||
@@ -8,10 +8,10 @@ import { API_MESSAGE, HEADERS, DIST_TAGS, API_ERROR, HTTP_STATUS } from '@verdac
|
||||
import { validateMetadata, isObject, ErrorCode, hasDiffOneKey } from '@verdaccio/utils';
|
||||
import { media, expectJson, allow } from '@verdaccio/middleware';
|
||||
import { notify } from '@verdaccio/hooks';
|
||||
import { Config, Callback, MergeTags, Version, Package } from '@verdaccio/types';
|
||||
import { Config, Callback, MergeTags, Version, Package, CallbackAction } from '@verdaccio/types';
|
||||
import { logger } from '@verdaccio/logger';
|
||||
import { IAuth } from '@verdaccio/auth';
|
||||
import { IStorageHandler } from '@verdaccio/store';
|
||||
import { Storage } from '@verdaccio/store';
|
||||
import { $RequestExtend, $ResponseExtend, $NextFunctionVer } from '../types/custom';
|
||||
|
||||
import star from './star';
|
||||
@@ -22,7 +22,7 @@ const debug = buildDebug('verdaccio:api:publish');
|
||||
export default function publish(
|
||||
router: Router,
|
||||
auth: IAuth,
|
||||
storage: IStorageHandler,
|
||||
storage: Storage,
|
||||
config: Config
|
||||
): void {
|
||||
const can = allow(auth);
|
||||
@@ -138,7 +138,7 @@ export default function publish(
|
||||
/**
|
||||
* Publish a package
|
||||
*/
|
||||
export function publishPackage(storage: IStorageHandler, config: Config, auth: IAuth): any {
|
||||
export function publishPackage(storage: Storage, config: Config, auth: IAuth): any {
|
||||
const starApi = star(storage);
|
||||
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
const packageName = req.params.package;
|
||||
@@ -172,7 +172,7 @@ export function publishPackage(storage: IStorageHandler, config: Config, auth: I
|
||||
/**
|
||||
* Add new package version in storage
|
||||
*/
|
||||
const createVersion = function (version: string, metadata: Version, cb: Callback): void {
|
||||
const createVersion = function (version: string, metadata: Version, cb: CallbackAction): void {
|
||||
debug('add a new package version %o to storage %o', version, metadata);
|
||||
storage.addVersion(packageName, version, metadata, null, cb);
|
||||
};
|
||||
@@ -180,7 +180,7 @@ export function publishPackage(storage: IStorageHandler, config: Config, auth: I
|
||||
/**
|
||||
* Add new tags in storage
|
||||
*/
|
||||
const addTags = function (tags: MergeTags, cb: Callback): void {
|
||||
const addTags = function (tags: MergeTags, cb: CallbackAction): void {
|
||||
debug('add new tag %o to storage', packageName);
|
||||
storage.mergeTags(packageName, tags, cb);
|
||||
};
|
||||
@@ -330,25 +330,27 @@ export function publishPackage(storage: IStorageHandler, config: Config, auth: I
|
||||
/**
|
||||
* un-publish a package
|
||||
*/
|
||||
export function unPublishPackage(storage: IStorageHandler) {
|
||||
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
export function unPublishPackage(storage: Storage) {
|
||||
return async function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {
|
||||
const packageName = req.params.package;
|
||||
|
||||
logger.debug({ packageName }, `unpublishing @{packageName}`);
|
||||
storage.removePackage(packageName, function (err) {
|
||||
try {
|
||||
await storage.removePackage(packageName);
|
||||
} catch (err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.status(HTTP_STATUS.CREATED);
|
||||
return next({ ok: API_MESSAGE.PKG_REMOVED });
|
||||
});
|
||||
}
|
||||
res.status(HTTP_STATUS.CREATED);
|
||||
return next({ ok: API_MESSAGE.PKG_REMOVED });
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete tarball
|
||||
*/
|
||||
export function removeTarball(storage: IStorageHandler) {
|
||||
export function removeTarball(storage: Storage) {
|
||||
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
const packageName = req.params.package;
|
||||
const { filename, revision } = req.params;
|
||||
@@ -374,7 +376,7 @@ export function removeTarball(storage: IStorageHandler) {
|
||||
/**
|
||||
* Adds a new version
|
||||
*/
|
||||
export function addVersion(storage: IStorageHandler) {
|
||||
export function addVersion(storage: Storage) {
|
||||
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
const { version, tag } = req.params;
|
||||
const packageName = req.params.package;
|
||||
@@ -398,7 +400,7 @@ export function addVersion(storage: IStorageHandler) {
|
||||
/**
|
||||
* uploadPackageTarball
|
||||
*/
|
||||
export function uploadPackageTarball(storage: IStorageHandler) {
|
||||
export function uploadPackageTarball(storage: Storage) {
|
||||
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
const packageName = req.params.package;
|
||||
const stream = storage.addTarball(packageName, req.params.filename);
|
||||
|
||||
@@ -1,102 +1,11 @@
|
||||
import { HEADERS } from '@verdaccio/commons-api';
|
||||
import { HTTP_STATUS } from '@verdaccio/commons-api';
|
||||
import { logger } from '@verdaccio/logger';
|
||||
|
||||
export default function (route, auth, storage): void {
|
||||
// searching packages
|
||||
route.get('/-/all(/since)?', function (req, res) {
|
||||
let received_end = false;
|
||||
let response_finished = false;
|
||||
let processing_pkgs = 0;
|
||||
let firstPackage = true;
|
||||
|
||||
res.status(200);
|
||||
res.set(HEADERS.CONTENT_TYPE, HEADERS.JSON_CHARSET);
|
||||
|
||||
/*
|
||||
* Offical NPM registry (registry.npmjs.org) no longer return whole database,
|
||||
* They only return packages matched with keyword in `referer: search pkg-name`,
|
||||
* And NPM client will request server in every search.
|
||||
*
|
||||
* The magic number 99999 was sent by NPM registry. Modify it may caused strange
|
||||
* behaviour in the future.
|
||||
*
|
||||
* BTW: NPM will not return result if user-agent does not contain string 'npm',
|
||||
* See: method 'request' in up-storage.js
|
||||
*
|
||||
* If there is no cache in local, NPM will request /-/all, then get response with
|
||||
* _updated: 99999, 'Date' in response header was Mon, 10 Oct 1983 00:12:48 GMT,
|
||||
* this will make NPM always query from server
|
||||
*
|
||||
* Data structure also different, whel request /-/all, response is an object, but
|
||||
* when request /-/all/since, response is an array
|
||||
*/
|
||||
const respShouldBeArray = req.path.endsWith('/since');
|
||||
if (!respShouldBeArray) {
|
||||
res.set('Date', 'Mon, 10 Oct 1983 00:12:48 GMT');
|
||||
}
|
||||
const check_finish = function (): void {
|
||||
if (!received_end) {
|
||||
return;
|
||||
}
|
||||
if (processing_pkgs) {
|
||||
return;
|
||||
}
|
||||
if (response_finished) {
|
||||
return;
|
||||
}
|
||||
response_finished = true;
|
||||
if (respShouldBeArray) {
|
||||
res.end(']\n');
|
||||
} else {
|
||||
res.end('}\n');
|
||||
}
|
||||
};
|
||||
|
||||
if (respShouldBeArray) {
|
||||
res.write('[');
|
||||
} else {
|
||||
res.write('{"_updated":' + 99999);
|
||||
}
|
||||
|
||||
const stream = storage.search(req.query.startkey || 0, { req: req });
|
||||
|
||||
stream.on('data', function each(pkg) {
|
||||
processing_pkgs++;
|
||||
|
||||
auth.allow_access({ packageName: pkg.name }, req.remote_user, function (err, allowed) {
|
||||
processing_pkgs--;
|
||||
|
||||
if (err) {
|
||||
if (err.status && String(err.status).match(/^4\d\d$/)) {
|
||||
// auth plugin returns 4xx user error,
|
||||
// that's equivalent of !allowed basically
|
||||
allowed = false;
|
||||
} else {
|
||||
stream.abort(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (allowed) {
|
||||
if (respShouldBeArray) {
|
||||
res.write(`${firstPackage ? '' : ','}${JSON.stringify(pkg)}\n`);
|
||||
if (firstPackage) {
|
||||
firstPackage = false;
|
||||
}
|
||||
} else {
|
||||
res.write(',\n' + JSON.stringify(pkg.name) + ':' + JSON.stringify(pkg));
|
||||
}
|
||||
}
|
||||
|
||||
check_finish();
|
||||
});
|
||||
});
|
||||
|
||||
stream.on('error', function () {
|
||||
res.socket.destroy();
|
||||
});
|
||||
|
||||
stream.on('end', function () {
|
||||
received_end = true;
|
||||
check_finish();
|
||||
});
|
||||
export default function (route): void {
|
||||
// TODO: next major version, remove this
|
||||
route.get('/-/all(/since)?', function (_req, res) {
|
||||
logger.warn('search endpoint has been removed, please use search v1');
|
||||
res.status(HTTP_STATUS.NOT_FOUND);
|
||||
res.json({ error: 'not found, endpoint was removed' });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ import { Response } from 'express';
|
||||
import _ from 'lodash';
|
||||
import buildDebug from 'debug';
|
||||
|
||||
import { IStorageHandler } from '@verdaccio/store';
|
||||
import { Storage } from '@verdaccio/store';
|
||||
import { $RequestExtend, $NextFunctionVer } from '../types/custom';
|
||||
|
||||
const debug = buildDebug('verdaccio:api:publish:star');
|
||||
|
||||
export default function (
|
||||
storage: IStorageHandler
|
||||
storage: Storage
|
||||
): (req: $RequestExtend, res: Response, next: $NextFunctionVer) => void {
|
||||
const validateInputs = (newUsers, localUsers, username, isStar): boolean => {
|
||||
const isExistlocalUsers = _.isNil(localUsers[username]) === false;
|
||||
@@ -40,6 +40,7 @@ export default function (
|
||||
};
|
||||
|
||||
debug('get package info package for %o', name);
|
||||
// @ts-ignore
|
||||
storage.getPackage({
|
||||
name,
|
||||
req,
|
||||
|
||||
@@ -4,12 +4,12 @@ import { Response, Router } from 'express';
|
||||
import { USERS, HTTP_STATUS } from '@verdaccio/commons-api';
|
||||
import { Package } from '@verdaccio/types';
|
||||
|
||||
import { IStorageHandler } from '@verdaccio/store';
|
||||
import { Storage } from '@verdaccio/store';
|
||||
import { $RequestExtend, $NextFunctionVer } from '../types/custom';
|
||||
|
||||
type Packages = Package[];
|
||||
|
||||
export default function (route: Router, storage: IStorageHandler): void {
|
||||
export default function (route: Router, storage: Storage): void {
|
||||
route.get(
|
||||
'/-/_view/starredByUser',
|
||||
(req: $RequestExtend, res: Response, next: $NextFunctionVer): void => {
|
||||
|
||||
@@ -1,106 +1,178 @@
|
||||
import semver from 'semver';
|
||||
import { Transform, pipeline, PassThrough } from 'stream';
|
||||
import _ from 'lodash';
|
||||
import buildDebug from 'debug';
|
||||
import { Package } from '@verdaccio/types';
|
||||
import { logger } from '@verdaccio/logger';
|
||||
import { IAuth } from '@verdaccio/auth';
|
||||
import { searchUtils } from '@verdaccio/core';
|
||||
import { HTTP_STATUS, getInternalError } from '@verdaccio/commons-api';
|
||||
import { Storage } from '@verdaccio/store';
|
||||
|
||||
function compileTextSearch(textSearch: string): (pkg: Package) => boolean {
|
||||
const personMatch = (person, search) => {
|
||||
if (typeof person === 'string') {
|
||||
return person.includes(search);
|
||||
const debug = buildDebug('verdaccio:api:search');
|
||||
|
||||
type SearchResults = {
|
||||
objects: searchUtils.SearchItemPkg[];
|
||||
total: number;
|
||||
time: string;
|
||||
};
|
||||
|
||||
// const personMatch = (person, search) => {
|
||||
// if (typeof person === 'string') {
|
||||
// return person.includes(search);
|
||||
// }
|
||||
|
||||
// if (typeof person === 'object') {
|
||||
// for (const field of Object.values(person)) {
|
||||
// if (typeof field === 'string' && field.includes(search)) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return false;
|
||||
// };
|
||||
|
||||
// const matcher = function (query) {
|
||||
// const match = query.match(/author:(.*)/);
|
||||
// if (match !== null) {
|
||||
// return function (pkg) {
|
||||
// return personMatch(pkg.author, match[1]);
|
||||
// };
|
||||
// }
|
||||
|
||||
// // TODO: maintainer, keywords, boost-exact
|
||||
// // TODO implement some scoring system for freetext
|
||||
// return (pkg) => {
|
||||
// return ['name', 'displayName', 'description']
|
||||
// .map((k) => {
|
||||
// return pkg[k];
|
||||
// })
|
||||
// .filter((x) => {
|
||||
// return x !== undefined;
|
||||
// })
|
||||
// .some((txt) => {
|
||||
// return txt.includes(query);
|
||||
// });
|
||||
// };
|
||||
// };
|
||||
|
||||
function removeDuplicates(results) {
|
||||
const pkgNames: any[] = [];
|
||||
return results.filter((pkg) => {
|
||||
if (pkgNames.includes(pkg?.package?.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof person === 'object') {
|
||||
for (const field of Object.values(person)) {
|
||||
if (typeof field === 'string' && field.includes(search)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
const matcher = function (q) {
|
||||
const match = q.match(/author:(.*)/);
|
||||
if (match !== null) {
|
||||
return (pkg) => personMatch(pkg.author, match[1]);
|
||||
}
|
||||
|
||||
// TODO: maintainer, keywords, not/is unstable insecure, boost-exact
|
||||
// TODO implement some scoring system for freetext
|
||||
return (pkg) => {
|
||||
return ['name', 'displayName', 'description']
|
||||
.map((k) => pkg[k])
|
||||
.filter((x) => x !== undefined)
|
||||
.some((txt) => txt.includes(q));
|
||||
};
|
||||
};
|
||||
|
||||
const textMatchers = (textSearch || '').split(' ').map(matcher);
|
||||
return (pkg) => textMatchers.every((m) => m(pkg));
|
||||
pkgNames.push(pkg?.package?.name);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export default function (route, auth, storage): void {
|
||||
route.get('/-/v1/search', (req, res) => {
|
||||
// TODO: implement proper result scoring weighted by quality, popularity and
|
||||
// maintenance query parameters
|
||||
let [text, size, from] = [
|
||||
'text',
|
||||
'size',
|
||||
'from' /* , 'quality', 'popularity', 'maintenance' */,
|
||||
].map((k) => req.query[k]);
|
||||
function checkAccess(pkg: any, auth: any, remoteUser): Promise<Package | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
auth.allow_access({ packageName: pkg?.package?.name }, remoteUser, function (err, allowed) {
|
||||
if (err) {
|
||||
if (err.status && String(err.status).match(/^4\d\d$/)) {
|
||||
// auth plugin returns 4xx user error,
|
||||
// that's equivalent of !allowed basically
|
||||
allowed = false;
|
||||
return resolve(null);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
} else {
|
||||
return resolve(allowed ? pkg : null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
size = parseInt(size) || 20;
|
||||
from = parseInt(from) || 0;
|
||||
class TransFormResults extends Transform {
|
||||
public constructor(options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
const isInteresting = compileTextSearch(text);
|
||||
/**
|
||||
* Transform either array of packages or a single package into a stream of packages.
|
||||
* From uplinks the chunks are array but from local packages are objects.
|
||||
* @param {string} chunk
|
||||
* @param {string} encoding
|
||||
* @param {function} done
|
||||
* @returns {void}
|
||||
* @override
|
||||
*/
|
||||
public _transform(chunk, _encoding, callback) {
|
||||
if (_.isArray(chunk)) {
|
||||
(chunk as searchUtils.SearchItem[])
|
||||
.filter((pkgItem) => {
|
||||
debug(`streaming remote pkg name ${pkgItem?.package?.name}`);
|
||||
return true;
|
||||
})
|
||||
.forEach((pkgItem) => {
|
||||
this.push(pkgItem);
|
||||
});
|
||||
return callback();
|
||||
} else {
|
||||
debug(`streaming local pkg name ${chunk?.package?.name}`);
|
||||
this.push(chunk);
|
||||
return callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resultStream = storage.search(0, { req: { query: { local: true } } });
|
||||
const resultBuf = [] as any;
|
||||
let completed = false;
|
||||
/**
|
||||
* Endpoint for npm search v1
|
||||
* Empty value
|
||||
* - {"objects":[],"total":0,"time":"Sun Jul 25 2021 14:09:11 GMT+0000 (Coordinated Universal Time)"}
|
||||
* req: 'GET /-/v1/search?text=react&size=20&frpom=0&quality=0.65&popularity=0.98&maintenance=0.5'
|
||||
*/
|
||||
export default function (route, auth: IAuth, storage: Storage): void {
|
||||
route.get('/-/v1/search', async (req, res, next) => {
|
||||
let [size, from] = ['size', 'from'].map((k) => req.query[k]);
|
||||
|
||||
const sendResponse = (): void => {
|
||||
completed = true;
|
||||
resultStream.destroy();
|
||||
size = parseInt(size, 10) || 20;
|
||||
from = parseInt(from, 10) || 0;
|
||||
const data: any[] = [];
|
||||
const transformResults = new TransFormResults({ objectMode: true });
|
||||
|
||||
const final = resultBuf.slice(from, size).map((pkg) => {
|
||||
return {
|
||||
package: pkg,
|
||||
flags: {
|
||||
unstable: Object.keys(pkg.versions).some((v) => semver.satisfies(v, '^1.0.0'))
|
||||
? undefined
|
||||
: true,
|
||||
},
|
||||
score: {
|
||||
final: 1,
|
||||
detail: {
|
||||
quality: 1,
|
||||
popularity: 1,
|
||||
maintenance: 0,
|
||||
},
|
||||
},
|
||||
searchScore: 100000,
|
||||
};
|
||||
});
|
||||
const response = {
|
||||
const streamPassThrough = new PassThrough({ objectMode: true });
|
||||
storage.searchManager?.search(streamPassThrough, {
|
||||
query: req.query,
|
||||
url: req.url,
|
||||
});
|
||||
|
||||
const outPutStream = new PassThrough({ objectMode: true });
|
||||
pipeline(streamPassThrough, transformResults, outPutStream, (err) => {
|
||||
if (err) {
|
||||
next(getInternalError(err ? err.message : 'unknown error'));
|
||||
} else {
|
||||
debug('Pipeline succeeded.');
|
||||
}
|
||||
});
|
||||
|
||||
outPutStream.on('data', (chunk) => {
|
||||
data.push(chunk);
|
||||
});
|
||||
|
||||
outPutStream.on('finish', async () => {
|
||||
debug('stream finish');
|
||||
const checkAccessPromises: searchUtils.SearchItemPkg[] = await Promise.all(
|
||||
removeDuplicates(data).map((pkgItem) => {
|
||||
return checkAccess(pkgItem, auth, req.remote_user);
|
||||
})
|
||||
);
|
||||
|
||||
const final: searchUtils.SearchItemPkg[] = checkAccessPromises
|
||||
.filter((i) => !_.isNull(i))
|
||||
.slice(from, size);
|
||||
logger.debug(`search results ${final?.length}`);
|
||||
|
||||
const response: SearchResults = {
|
||||
objects: final,
|
||||
total: final.length,
|
||||
time: new Date().toUTCString(),
|
||||
};
|
||||
|
||||
res.status(200).json(response);
|
||||
};
|
||||
|
||||
resultStream.on('data', (pkg) => {
|
||||
if (!isInteresting(pkg)) {
|
||||
return;
|
||||
}
|
||||
resultBuf.push(pkg);
|
||||
if (!completed && resultBuf.length >= size + from) {
|
||||
sendResponse();
|
||||
}
|
||||
});
|
||||
resultStream.on('end', () => {
|
||||
if (!completed) {
|
||||
sendResponse();
|
||||
}
|
||||
res.status(HTTP_STATUS.OK).json(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Response, Router } from 'express';
|
||||
|
||||
import { Config, RemoteUser, Token } from '@verdaccio/types';
|
||||
import { IAuth } from '@verdaccio/auth';
|
||||
import { IStorageHandler } from '@verdaccio/store';
|
||||
import { Storage } from '@verdaccio/store';
|
||||
import { $RequestExtend, $NextFunctionVer } from '../../types/custom';
|
||||
|
||||
export type NormalizeToken = Token & {
|
||||
@@ -22,12 +22,7 @@ function normalizeToken(token: Token): NormalizeToken {
|
||||
}
|
||||
|
||||
// https://github.com/npm/npm-profile/blob/latest/lib/index.js
|
||||
export default function (
|
||||
route: Router,
|
||||
auth: IAuth,
|
||||
storage: IStorageHandler,
|
||||
config: Config
|
||||
): void {
|
||||
export default function (route: Router, auth: IAuth, storage: Storage, config: Config): void {
|
||||
route.get(
|
||||
'/-/npm/v1/tokens',
|
||||
async function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function initializeServer(configName): Promise<Application> {
|
||||
return app;
|
||||
}
|
||||
|
||||
export function publishVersion(app, configFile, pkgName, version): supertest.Test {
|
||||
export function publishVersion(app, _configFile, pkgName, version): supertest.Test {
|
||||
const pkgMetadata = generatePackageMetadata(pkgName, version);
|
||||
|
||||
return supertest(app)
|
||||
|
||||
@@ -64,7 +64,7 @@ describe('package', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: investigate the 404
|
||||
// FIXME: investigate the 404
|
||||
test.skip('should return a package by dist-tag', async (done) => {
|
||||
// await publishVersion(app, 'package.yaml', 'foo3', '1.0.0');
|
||||
await publishVersion(app, 'package.yaml', 'foo-tagged', '1.0.0');
|
||||
@@ -80,7 +80,7 @@ describe('package', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.skip('should return 404', async () => {
|
||||
test('should return 404', async () => {
|
||||
return supertest(app)
|
||||
.get('/404-not-found')
|
||||
.set('Accept', HEADERS.JSON)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { HTTP_STATUS, API_ERROR } from '@verdaccio/commons-api';
|
||||
import { ErrorCode } from '@verdaccio/utils';
|
||||
import {
|
||||
addVersion,
|
||||
uploadPackageTarball,
|
||||
@@ -183,35 +184,31 @@ describe('Publish endpoints - un-publish package', () => {
|
||||
next = jest.fn();
|
||||
});
|
||||
|
||||
test('should un-publish package successfully', (done) => {
|
||||
test('should un-publish package successfully', async () => {
|
||||
const storage = {
|
||||
removePackage(packageName, cb) {
|
||||
removePackage(packageName) {
|
||||
expect(packageName).toEqual(req.params.package);
|
||||
cb();
|
||||
done();
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
unPublishPackage(storage)(req, res, next);
|
||||
await unPublishPackage(storage)(req, res, next);
|
||||
expect(res.status).toHaveBeenCalledWith(HTTP_STATUS.CREATED);
|
||||
expect(next).toHaveBeenCalledWith({ ok: 'package removed' });
|
||||
});
|
||||
|
||||
test('un-publish failed', (done) => {
|
||||
const error = {
|
||||
message: 'un-publish failed',
|
||||
};
|
||||
test('un-publish failed', async () => {
|
||||
const storage = {
|
||||
removePackage(packageName, cb) {
|
||||
cb(error);
|
||||
done();
|
||||
removePackage(packageName) {
|
||||
expect(packageName).toEqual(req.params.package);
|
||||
return Promise.reject(ErrorCode.getInternalError());
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
unPublishPackage(storage)(req, res, next);
|
||||
expect(next).toHaveBeenCalledWith(error);
|
||||
await unPublishPackage(storage)(req, res, next);
|
||||
expect(next).toHaveBeenCalledWith(ErrorCode.getInternalError());
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
{
|
||||
"path": "../core/commons-api"
|
||||
},
|
||||
{
|
||||
"path": "../core/core"
|
||||
},
|
||||
{
|
||||
"path": "../core/tarball"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,60 @@
|
||||
# @verdaccio/auth
|
||||
|
||||
## 6.0.0-6-next.11
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 459b6fa7: refactor: search v1 endpoint and local-database
|
||||
|
||||
- refactor search `api v1` endpoint, improve performance
|
||||
- remove usage of `async` dependency https://github.com/verdaccio/verdaccio/issues/1225
|
||||
- refactor method storage class
|
||||
- create new module `core` to reduce the ammount of modules with utilities
|
||||
- use `undici` instead `node-fetch`
|
||||
- use `fastify` instead `express` for functional test
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- plugin storage API changes
|
||||
- remove old search endpoint (return 404)
|
||||
- filter local private packages at plugin level
|
||||
|
||||
The storage api changes for methods `get`, `add`, `remove` as promise base. The `search` methods also changes and recieves a `query` object that contains all query params from the client.
|
||||
|
||||
```ts
|
||||
export interface IPluginStorage<T> extends IPlugin {
|
||||
add(name: string): Promise<void>;
|
||||
remove(name: string): Promise<void>;
|
||||
get(): Promise<any>;
|
||||
init(): Promise<void>;
|
||||
getSecret(): Promise<string>;
|
||||
setSecret(secret: string): Promise<any>;
|
||||
getPackageStorage(packageInfo: string): IPackageStorage;
|
||||
search(query: searchUtils.SearchQuery): Promise<searchUtils.SearchItem[]>;
|
||||
saveToken(token: Token): Promise<any>;
|
||||
deleteToken(user: string, tokenKey: string): Promise<any>;
|
||||
readTokens(filter: TokenFilter): Promise<Token[]>;
|
||||
}
|
||||
```
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [459b6fa7]
|
||||
- @verdaccio/config@6.0.0-6-next.8
|
||||
- @verdaccio/commons-api@11.0.0-6-next.4
|
||||
- @verdaccio/utils@6.0.0-6-next.6
|
||||
- @verdaccio/loaders@6.0.0-6-next.4
|
||||
- verdaccio-htpasswd@11.0.0-6-next.8
|
||||
- @verdaccio/logger@6.0.0-6-next.4
|
||||
|
||||
## 6.0.0-6-next.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [df0da3d6]
|
||||
- verdaccio-htpasswd@11.0.0-6-next.7
|
||||
- @verdaccio/loaders@6.0.0-6-next.4
|
||||
|
||||
## 6.0.0-6-next.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@verdaccio/auth",
|
||||
"version": "6.0.0-6-next.9",
|
||||
"version": "6.0.0-6-next.11",
|
||||
"description": "logger",
|
||||
"main": "./build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
@@ -39,20 +39,20 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@verdaccio/commons-api": "workspace:11.0.0-alpha.3",
|
||||
"@verdaccio/config": "workspace:6.0.0-6-next.7",
|
||||
"@verdaccio/commons-api": "workspace:11.0.0-6-next.4",
|
||||
"@verdaccio/config": "workspace:6.0.0-6-next.8",
|
||||
"@verdaccio/loaders": "workspace:6.0.0-6-next.4",
|
||||
"@verdaccio/logger": "workspace:6.0.0-6-next.4",
|
||||
"@verdaccio/utils": "workspace:6.0.0-6-next.5",
|
||||
"@verdaccio/utils": "workspace:6.0.0-6-next.6",
|
||||
"debug": "4.3.2",
|
||||
"express": "4.17.1",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"lodash": "4.17.21",
|
||||
"verdaccio-htpasswd": "workspace:11.0.0-alpha.6"
|
||||
"verdaccio-htpasswd": "workspace:11.0.0-6-next.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@verdaccio/mock": "workspace:6.0.0-6-next.7",
|
||||
"@verdaccio/types": "workspace:11.0.0-6-next.7"
|
||||
"@verdaccio/mock": "workspace:6.0.0-6-next.9",
|
||||
"@verdaccio/types": "workspace:11.0.0-6-next.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
|
||||
@@ -29,12 +29,8 @@ import {
|
||||
PluginOptions,
|
||||
} from '@verdaccio/types';
|
||||
|
||||
import { isNil, isFunction } from '@verdaccio/utils';
|
||||
import {
|
||||
getMatchedPackagesSpec,
|
||||
createAnonymousRemoteUser,
|
||||
createRemoteUser,
|
||||
} from '@verdaccio/config';
|
||||
import { getMatchedPackagesSpec, isNil, isFunction } from '@verdaccio/utils';
|
||||
import { createAnonymousRemoteUser, createRemoteUser } from '@verdaccio/config';
|
||||
|
||||
import {
|
||||
getMiddlewareCredentials,
|
||||
@@ -414,7 +410,7 @@ class Auth implements IAuth {
|
||||
};
|
||||
|
||||
if (this._isRemoteUserValid(req.remote_user)) {
|
||||
debug('jwt has remote user');
|
||||
debug('jwt has a valid authentication header');
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -423,12 +419,12 @@ class Auth implements IAuth {
|
||||
|
||||
const { authorization } = req.headers;
|
||||
if (_.isNil(authorization)) {
|
||||
debug('jwt invalid auth header');
|
||||
debug('jwt, authentication header is missing');
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!isAuthHeaderValid(authorization)) {
|
||||
debug('api middleware auth heather is not valid');
|
||||
debug('api middleware authentication heather is invalid');
|
||||
return next(getBadRequest(API_ERROR.BAD_AUTH_HEADER));
|
||||
}
|
||||
const { secret, security } = this.config;
|
||||
|
||||
@@ -1,5 +1,64 @@
|
||||
# @verdaccio/cli
|
||||
|
||||
## 6.0.0-6-next.21
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 459b6fa7: refactor: search v1 endpoint and local-database
|
||||
|
||||
- refactor search `api v1` endpoint, improve performance
|
||||
- remove usage of `async` dependency https://github.com/verdaccio/verdaccio/issues/1225
|
||||
- refactor method storage class
|
||||
- create new module `core` to reduce the ammount of modules with utilities
|
||||
- use `undici` instead `node-fetch`
|
||||
- use `fastify` instead `express` for functional test
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- plugin storage API changes
|
||||
- remove old search endpoint (return 404)
|
||||
- filter local private packages at plugin level
|
||||
|
||||
The storage api changes for methods `get`, `add`, `remove` as promise base. The `search` methods also changes and recieves a `query` object that contains all query params from the client.
|
||||
|
||||
```ts
|
||||
export interface IPluginStorage<T> extends IPlugin {
|
||||
add(name: string): Promise<void>;
|
||||
remove(name: string): Promise<void>;
|
||||
get(): Promise<any>;
|
||||
init(): Promise<void>;
|
||||
getSecret(): Promise<string>;
|
||||
setSecret(secret: string): Promise<any>;
|
||||
getPackageStorage(packageInfo: string): IPackageStorage;
|
||||
search(query: searchUtils.SearchQuery): Promise<searchUtils.SearchItem[]>;
|
||||
saveToken(token: Token): Promise<any>;
|
||||
deleteToken(user: string, tokenKey: string): Promise<any>;
|
||||
readTokens(filter: TokenFilter): Promise<Token[]>;
|
||||
}
|
||||
```
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [459b6fa7]
|
||||
- @verdaccio/config@6.0.0-6-next.8
|
||||
- @verdaccio/fastify-migration@6.0.0-6-next.12
|
||||
- @verdaccio/node-api@6.0.0-6-next.20
|
||||
- @verdaccio/logger@6.0.0-6-next.4
|
||||
|
||||
## 6.0.0-6-next.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [df0da3d6]
|
||||
- @verdaccio/fastify-migration@6.0.0-6-next.11
|
||||
- @verdaccio/node-api@6.0.0-6-next.19
|
||||
|
||||
## 6.0.0-6-next.19
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 2e3b9552: feat: node 14 as minimum for running cli
|
||||
|
||||
## 6.0.0-6-next.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@verdaccio/cli",
|
||||
"version": "6.0.0-6-next.18",
|
||||
"version": "6.0.0-6-next.21",
|
||||
"author": {
|
||||
"name": "Juan Picado",
|
||||
"email": "juanpicado19@gmail.com"
|
||||
@@ -44,11 +44,10 @@
|
||||
"start": "ts-node src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@verdaccio/config": "workspace:6.0.0-6-next.7",
|
||||
"@verdaccio/config": "workspace:6.0.0-6-next.8",
|
||||
"@verdaccio/logger": "workspace:6.0.0-6-next.4",
|
||||
"@verdaccio/node-api": "workspace:6.0.0-6-next.18",
|
||||
"@verdaccio/fastify-migration": "workspace:6.0.0-6-next.10",
|
||||
"commander": "6.2.0",
|
||||
"@verdaccio/node-api": "workspace:6.0.0-6-next.20",
|
||||
"@verdaccio/fastify-migration": "workspace:6.0.0-6-next.12",
|
||||
"clipanion": "3.0.1",
|
||||
"envinfo": "7.8.1",
|
||||
"kleur": "3.0.3",
|
||||
|
||||
@@ -46,7 +46,9 @@ export class InitCommand extends Command {
|
||||
private initLogger(logConfig: ConfigRuntime) {
|
||||
try {
|
||||
if (logConfig.logs) {
|
||||
process.emitWarning('config.logs is deprecated, rename configuration to "config.log"');
|
||||
process.emitWarning(
|
||||
'config.logs is deprecated, rename configuration to "config.log" in singular'
|
||||
);
|
||||
}
|
||||
// FUTURE: remove fallback when is ready
|
||||
setup(logConfig.log || logConfig.logs);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import semver from 'semver';
|
||||
|
||||
export const MIN_NODE_VERSION = '12';
|
||||
export const MIN_NODE_VERSION = '14';
|
||||
|
||||
export function isVersionValid(version) {
|
||||
return semver.satisfies(version, `>=${MIN_NODE_VERSION}`);
|
||||
|
||||
@@ -5,9 +5,5 @@ test('valid version node.js', () => {
|
||||
});
|
||||
|
||||
test('is invalid version node.js', () => {
|
||||
expect(isVersionValid('11.0.0')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('Node 12 should valid version node.js', () => {
|
||||
expect(isVersionValid('12.0.0')).toBeTruthy();
|
||||
expect(isVersionValid('13.0.0')).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,48 @@
|
||||
# @verdaccio/config
|
||||
|
||||
## 6.0.0-6-next.8
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 459b6fa7: refactor: search v1 endpoint and local-database
|
||||
|
||||
- refactor search `api v1` endpoint, improve performance
|
||||
- remove usage of `async` dependency https://github.com/verdaccio/verdaccio/issues/1225
|
||||
- refactor method storage class
|
||||
- create new module `core` to reduce the ammount of modules with utilities
|
||||
- use `undici` instead `node-fetch`
|
||||
- use `fastify` instead `express` for functional test
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- plugin storage API changes
|
||||
- remove old search endpoint (return 404)
|
||||
- filter local private packages at plugin level
|
||||
|
||||
The storage api changes for methods `get`, `add`, `remove` as promise base. The `search` methods also changes and recieves a `query` object that contains all query params from the client.
|
||||
|
||||
```ts
|
||||
export interface IPluginStorage<T> extends IPlugin {
|
||||
add(name: string): Promise<void>;
|
||||
remove(name: string): Promise<void>;
|
||||
get(): Promise<any>;
|
||||
init(): Promise<void>;
|
||||
getSecret(): Promise<string>;
|
||||
setSecret(secret: string): Promise<any>;
|
||||
getPackageStorage(packageInfo: string): IPackageStorage;
|
||||
search(query: searchUtils.SearchQuery): Promise<searchUtils.SearchItem[]>;
|
||||
saveToken(token: Token): Promise<any>;
|
||||
deleteToken(user: string, tokenKey: string): Promise<any>;
|
||||
readTokens(filter: TokenFilter): Promise<Token[]>;
|
||||
}
|
||||
```
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [459b6fa7]
|
||||
- @verdaccio/commons-api@11.0.0-6-next.4
|
||||
- @verdaccio/utils@6.0.0-6-next.6
|
||||
|
||||
## 6.0.0-6-next.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@verdaccio/config",
|
||||
"version": "6.0.0-6-next.7",
|
||||
"version": "6.0.0-6-next.8",
|
||||
"description": "logger",
|
||||
"main": "./build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
@@ -39,10 +39,10 @@
|
||||
"build": "pnpm run build:js && pnpm run build:types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@verdaccio/commons-api": "workspace:11.0.0-alpha.3",
|
||||
"@verdaccio/utils": "workspace:6.0.0-6-next.5",
|
||||
"@verdaccio/commons-api": "workspace:11.0.0-6-next.4",
|
||||
"@verdaccio/utils": "workspace:6.0.0-6-next.6",
|
||||
"debug": "4.3.2",
|
||||
"js-yaml": "3.14.0",
|
||||
"js-yaml": "3.14.1",
|
||||
"lodash": "4.17.21",
|
||||
"minimatch": "3.0.4",
|
||||
"yup": "0.32.9"
|
||||
|
||||
@@ -100,11 +100,6 @@ logs:
|
||||
# { type: file, path: verdaccio.log, level: http}
|
||||
# FIXME: this should be documented
|
||||
# More info about log rotation https://github.com/pinojs/pino/blob/master/docs/help.md#log-rotation
|
||||
flags:
|
||||
# support for npm token command
|
||||
token: false
|
||||
# support for the new v1 search endpoint, functional by incomplete read more on ticket 1732
|
||||
search: false
|
||||
|
||||
# This affect the web and api (not developed yet)
|
||||
i18n:
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from 'assert';
|
||||
import _ from 'lodash';
|
||||
import buildDebug from 'debug';
|
||||
|
||||
import { generateRandomHexString, isObject } from '@verdaccio/utils';
|
||||
import { getMatchedPackagesSpec, generateRandomHexString, isObject } from '@verdaccio/utils';
|
||||
import { APP_ERROR } from '@verdaccio/commons-api';
|
||||
import {
|
||||
PackageList,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '@verdaccio/types';
|
||||
|
||||
import { generateRandomSecretKey } from './token';
|
||||
import { getMatchedPackagesSpec, normalisePackageAccess } from './package-access';
|
||||
import { normalisePackageAccess } from './package-access';
|
||||
import { sanityCheckUplinksProps, uplinkSanityCheck } from './uplinks';
|
||||
import { defaultSecurity } from './security';
|
||||
import { getUserAgent } from './agent';
|
||||
@@ -23,9 +23,6 @@ import serverSettings from './serverSettings';
|
||||
|
||||
const strategicConfigProps = ['uplinks', 'packages'];
|
||||
const allowedEnvConfig = ['http_proxy', 'https_proxy', 'no_proxy'];
|
||||
|
||||
export type MatchedPackage = PackageAccess | void;
|
||||
|
||||
const debug = buildDebug('verdaccio:config');
|
||||
|
||||
export const WEB_TITLE = 'Verdaccio';
|
||||
@@ -99,7 +96,8 @@ class Config implements AppConfig {
|
||||
/**
|
||||
* Check for package spec
|
||||
*/
|
||||
public getMatchedPackagesSpec(pkgName: string): MatchedPackage {
|
||||
public getMatchedPackagesSpec(pkgName: string): PackageAccess | void {
|
||||
// TODO: remove this method and replace by library utils
|
||||
return getMatchedPackagesSpec(pkgName, this.packages);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import assert from 'assert';
|
||||
import _ from 'lodash';
|
||||
import minimatch from 'minimatch';
|
||||
|
||||
import { PackageList, PackageAccess } from '@verdaccio/types';
|
||||
import { PackageAccess } from '@verdaccio/types';
|
||||
import { ErrorCode } from '@verdaccio/utils';
|
||||
import { MatchedPackage } from './config';
|
||||
|
||||
export interface LegacyPackageList {
|
||||
[key: string]: PackageAccess;
|
||||
}
|
||||
@@ -47,15 +43,6 @@ export function normalizeUserList(groupsList: any): any {
|
||||
return _.flatten(result);
|
||||
}
|
||||
|
||||
export function getMatchedPackagesSpec(pkgName: string, packages: PackageList): MatchedPackage {
|
||||
for (const i in packages) {
|
||||
if (minimatch.makeRe(i).exec(pkgName)) {
|
||||
return packages[i];
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
export function normalisePackageAccess(packages: LegacyPackageList): LegacyPackageList {
|
||||
const normalizedPkgs: LegacyPackageList = { ...packages };
|
||||
if (_.isNil(normalizedPkgs['**'])) {
|
||||
|
||||
@@ -3,17 +3,25 @@ import YAML from 'js-yaml';
|
||||
import { APP_ERROR } from '@verdaccio/commons-api';
|
||||
import { ConfigRuntime, ConfigYaml } from '@verdaccio/types';
|
||||
|
||||
/**
|
||||
* Parse a config file from yaml to JSON.
|
||||
* @param configPath the absolute path of the configuration file
|
||||
*/
|
||||
export function parseConfigFile(configPath: string): ConfigRuntime {
|
||||
try {
|
||||
if (/\.ya?ml$/i.test(configPath)) {
|
||||
const yamlConfig = YAML.safeLoad(fs.readFileSync(configPath, 'utf8')) as ConfigYaml;
|
||||
return Object.assign({}, yamlConfig, {
|
||||
configPath,
|
||||
// @deprecated use configPath instead
|
||||
config_path: configPath,
|
||||
});
|
||||
}
|
||||
|
||||
const jsonConfig = require(configPath) as ConfigYaml;
|
||||
return Object.assign({}, jsonConfig, {
|
||||
configPath,
|
||||
// @deprecated use configPath instead
|
||||
config_path: configPath,
|
||||
});
|
||||
} catch (e: any) {
|
||||
@@ -21,6 +29,6 @@ export function parseConfigFile(configPath: string): ConfigRuntime {
|
||||
e.message = APP_ERROR.CONFIG_NOT_VALID;
|
||||
}
|
||||
|
||||
throw new Error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import assert from 'assert';
|
||||
import { getMatchedPackagesSpec } from '@verdaccio/utils';
|
||||
import { PackageList, UpLinksConfList } from '@verdaccio/types';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { getMatchedPackagesSpec } from './package-access';
|
||||
import { MatchedPackage } from './config';
|
||||
|
||||
export const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
||||
export const DEFAULT_UPLINK = 'npmjs';
|
||||
|
||||
@@ -49,11 +47,8 @@ export function sanityCheckUplinksProps(configUpLinks: UpLinksConfList): UpLinks
|
||||
return uplinks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an uplink can proxy
|
||||
*/
|
||||
export function hasProxyTo(pkg: string, upLink: string, packages: PackageList): boolean {
|
||||
const matchedPkg: MatchedPackage = getMatchedPackagesSpec(pkg, packages);
|
||||
const matchedPkg = getMatchedPackagesSpec(pkg, packages);
|
||||
const proxyList = typeof matchedPkg !== 'undefined' ? matchedPkg.proxy : [];
|
||||
if (proxyList) {
|
||||
return proxyList.some((curr) => upLink === curr);
|
||||
|
||||
@@ -12,13 +12,13 @@ describe('Package access utilities', () => {
|
||||
test('parse invalid.json', () => {
|
||||
expect(function () {
|
||||
parseConfigFile(parseConfigurationFile('invalid.json'));
|
||||
}).toThrow(/Error/);
|
||||
}).toThrow(/CONFIG: it does not look like a valid config file/);
|
||||
});
|
||||
|
||||
test('parse not-exists.json', () => {
|
||||
expect(function () {
|
||||
parseConfigFile(parseConfigurationFile('not-exists.json'));
|
||||
}).toThrow(/Error/);
|
||||
}).toThrow(/Cannot find module/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,13 +32,13 @@ describe('Package access utilities', () => {
|
||||
test('parse invalid.js', () => {
|
||||
expect(function () {
|
||||
parseConfigFile(parseConfigurationFile('invalid.js'));
|
||||
}).toThrow(/Error/);
|
||||
}).toThrow(/CONFIG: it does not look like a valid config file/);
|
||||
});
|
||||
|
||||
test('parse not-exists.js', () => {
|
||||
expect(function () {
|
||||
parseConfigFile(parseConfigurationFile('not-exists.js'));
|
||||
}).toThrow(/Error/);
|
||||
}).toThrow(/Cannot find module/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,9 +96,9 @@ describe('config-path', () => {
|
||||
delete process.env.XDG_CONFIG_HOME;
|
||||
delete process.env.HOME;
|
||||
process.env.APPDATA = '/app/data/';
|
||||
expect(findConfigFile()).toEqual('D:\\app\\data\\verdaccio\\config.yaml');
|
||||
expect(mockwriteFile).toHaveBeenCalledWith('D:\\app\\data\\verdaccio\\config.yaml');
|
||||
expect(mockmkDir).toHaveBeenCalledWith('D:\\app\\data\\verdaccio');
|
||||
expect(findConfigFile()).toMatch('\\app\\data\\verdaccio\\config.yaml');
|
||||
expect(mockwriteFile).toHaveBeenCalled();
|
||||
expect(mockmkDir).toHaveBeenCalled();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import {
|
||||
getMatchedPackagesSpec,
|
||||
normalisePackageAccess,
|
||||
PACKAGE_ACCESS,
|
||||
} from '../src/package-access';
|
||||
import { normalisePackageAccess, PACKAGE_ACCESS } from '../src/package-access';
|
||||
import { parseConfigFile } from '../src';
|
||||
import { parseConfigurationFile } from './utils';
|
||||
|
||||
@@ -127,30 +123,4 @@ describe('Package access utilities', () => {
|
||||
expect(_.isArray(all.publish)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMatchedPackagesSpec', () => {
|
||||
test('should test basic config', () => {
|
||||
const { packages } = parseConfigFile(parseConfigurationFile('pkgs-custom'));
|
||||
// @ts-expect-error
|
||||
expect(getMatchedPackagesSpec('react', packages).proxy).toMatch('facebook');
|
||||
// @ts-expect-error
|
||||
expect(getMatchedPackagesSpec('angular', packages).proxy).toMatch('google');
|
||||
// @ts-expect-error
|
||||
expect(getMatchedPackagesSpec('vue', packages).proxy).toMatch('npmjs');
|
||||
// @ts-expect-error
|
||||
expect(getMatchedPackagesSpec('@scope/vue', packages).proxy).toMatch('npmjs');
|
||||
});
|
||||
|
||||
test('should test no ** wildcard on config', () => {
|
||||
const { packages } = parseConfigFile(parseConfigurationFile('pkgs-nosuper-wildcard-custom'));
|
||||
// @ts-expect-error
|
||||
expect(getMatchedPackagesSpec('react', packages).proxy).toMatch('facebook');
|
||||
// @ts-expect-error
|
||||
expect(getMatchedPackagesSpec('angular', packages).proxy).toMatch('google');
|
||||
// @ts-expect-error
|
||||
expect(getMatchedPackagesSpec('@fake/angular', packages).proxy).toMatch('npmjs');
|
||||
expect(getMatchedPackagesSpec('vue', packages)).toBeUndefined();
|
||||
expect(getMatchedPackagesSpec('@scope/vue', packages)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
packages:
|
||||
'react':
|
||||
access: admin
|
||||
publish: admin
|
||||
proxy: facebook
|
||||
'angular':
|
||||
access: admin
|
||||
publish: admin
|
||||
proxy: google
|
||||
'@fake/*':
|
||||
access: $all
|
||||
publish: $authenticated
|
||||
proxy: npmjs
|
||||
@@ -1,5 +1,42 @@
|
||||
# Change Log
|
||||
|
||||
## 11.0.0-6-next.4
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 459b6fa7: refactor: search v1 endpoint and local-database
|
||||
|
||||
- refactor search `api v1` endpoint, improve performance
|
||||
- remove usage of `async` dependency https://github.com/verdaccio/verdaccio/issues/1225
|
||||
- refactor method storage class
|
||||
- create new module `core` to reduce the ammount of modules with utilities
|
||||
- use `undici` instead `node-fetch`
|
||||
- use `fastify` instead `express` for functional test
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- plugin storage API changes
|
||||
- remove old search endpoint (return 404)
|
||||
- filter local private packages at plugin level
|
||||
|
||||
The storage api changes for methods `get`, `add`, `remove` as promise base. The `search` methods also changes and recieves a `query` object that contains all query params from the client.
|
||||
|
||||
```ts
|
||||
export interface IPluginStorage<T> extends IPlugin {
|
||||
add(name: string): Promise<void>;
|
||||
remove(name: string): Promise<void>;
|
||||
get(): Promise<any>;
|
||||
init(): Promise<void>;
|
||||
getSecret(): Promise<string>;
|
||||
setSecret(secret: string): Promise<any>;
|
||||
getPackageStorage(packageInfo: string): IPackageStorage;
|
||||
search(query: searchUtils.SearchQuery): Promise<searchUtils.SearchItem[]>;
|
||||
saveToken(token: Token): Promise<any>;
|
||||
deleteToken(user: string, tokenKey: string): Promise<any>;
|
||||
readTokens(filter: TokenFilter): Promise<Token[]>;
|
||||
}
|
||||
```
|
||||
|
||||
## 10.0.0-alpha.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@verdaccio/commons-api",
|
||||
"version": "11.0.0-alpha.3",
|
||||
"version": "11.0.0-6-next.4",
|
||||
"description": "Commons API utilities for Verdaccio",
|
||||
"keywords": [
|
||||
"private",
|
||||
|
||||
@@ -80,6 +80,7 @@ export const API_MESSAGE = {
|
||||
LOGGED_OUT: 'Logged out',
|
||||
};
|
||||
|
||||
// @deprecated
|
||||
export const SUPPORT_ERRORS = {
|
||||
PLUGIN_MISSING_INTERFACE: 'the plugin does not provide implementation of the requested feature',
|
||||
TFA_DISABLED: 'the two-factor authentication is not yet supported',
|
||||
@@ -87,6 +88,7 @@ export const SUPPORT_ERRORS = {
|
||||
PARAMETERS_NOT_VALID: 'the parameters are not valid',
|
||||
};
|
||||
|
||||
// @deprecated
|
||||
export const API_ERROR = {
|
||||
PASSWORD_SHORT: (passLength = DEFAULT_MIN_LIMIT_PASSWORD): string =>
|
||||
`The provided password is too short. Please pick a password longer than ` +
|
||||
@@ -125,12 +127,14 @@ export const API_ERROR = {
|
||||
USERNAME_ALREADY_REGISTERED: 'username is already registered',
|
||||
};
|
||||
|
||||
// @deprecated
|
||||
export const APP_ERROR = {
|
||||
CONFIG_NOT_VALID: 'CONFIG: it does not look like a valid config file',
|
||||
PROFILE_ERROR: 'profile unexpected error',
|
||||
PASSWORD_VALIDATION: 'not valid password',
|
||||
};
|
||||
|
||||
// @deprecated
|
||||
export type VerdaccioError = HttpError & { code: number };
|
||||
|
||||
function getError(code: number, message: string): VerdaccioError {
|
||||
|
||||
3
packages/core/core/.babelrc
Normal file
3
packages/core/core/.babelrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../../.babelrc"
|
||||
}
|
||||
6
packages/core/core/.eslintignore
Normal file
6
packages/core/core/.eslintignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
coverage/
|
||||
lib/
|
||||
.nyc_output
|
||||
tests-report/
|
||||
build/
|
||||
5
packages/core/core/.eslintrc.json
Normal file
5
packages/core/core/.eslintrc.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"@typescript-eslint/no-use-before-define": "off"
|
||||
}
|
||||
}
|
||||
1
packages/core/core/.gitignore
vendored
Normal file
1
packages/core/core/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
lib/
|
||||
37
packages/core/core/CHANGELOG.md
Normal file
37
packages/core/core/CHANGELOG.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# @verdaccio/core
|
||||
|
||||
## 6.0.0-6-next.1
|
||||
### Major Changes
|
||||
|
||||
- 459b6fa7: refactor: search v1 endpoint and local-database
|
||||
|
||||
- refactor search `api v1` endpoint, improve performance
|
||||
- remove usage of `async` dependency https://github.com/verdaccio/verdaccio/issues/1225
|
||||
- refactor method storage class
|
||||
- create new module `core` to reduce the ammount of modules with utilities
|
||||
- use `undici` instead `node-fetch`
|
||||
- use `fastify` instead `express` for functional test
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- plugin storage API changes
|
||||
- remove old search endpoint (return 404)
|
||||
- filter local private packages at plugin level
|
||||
|
||||
The storage api changes for methods `get`, `add`, `remove` as promise base. The `search` methods also changes and recieves a `query` object that contains all query params from the client.
|
||||
|
||||
```ts
|
||||
export interface IPluginStorage<T> extends IPlugin {
|
||||
add(name: string): Promise<void>;
|
||||
remove(name: string): Promise<void>;
|
||||
get(): Promise<any>;
|
||||
init(): Promise<void>;
|
||||
getSecret(): Promise<string>;
|
||||
setSecret(secret: string): Promise<any>;
|
||||
getPackageStorage(packageInfo: string): IPackageStorage;
|
||||
search(query: searchUtils.SearchQuery): Promise<searchUtils.SearchItem[]>;
|
||||
saveToken(token: Token): Promise<any>;
|
||||
deleteToken(user: string, tokenKey: string): Promise<any>;
|
||||
readTokens(filter: TokenFilter): Promise<Token[]>;
|
||||
}
|
||||
```
|
||||
21
packages/core/core/LICENSE
Normal file
21
packages/core/core/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Verdaccio
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
19
packages/core/core/README.md
Normal file
19
packages/core/core/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Core
|
||||
|
||||
[](https://circleci.com/gh/ayusharma/@verdaccio/streams)
|
||||
[](https://codecov.io/gh/verdaccio/streams)
|
||||
[](https://www.npmjs.com/package/@verdaccio/streams)
|
||||
[](https://opencollective.com/verdaccio)
|
||||
[](http://chat.verdaccio.org/)
|
||||

|
||||
[](https://www.npmjs.com/package/@verdaccio/streams)
|
||||
|
||||
This project provides an extension of `PassThrough` stream.
|
||||
|
||||
## Detail
|
||||
|
||||
It provides 2 additional methods `abort()` and `done()`. Those implementations are widely use in the verdaccio core for handle `tarballs`.
|
||||
|
||||
## License
|
||||
|
||||
MIT (http://www.opensource.org/licenses/mit-license.php)
|
||||
3
packages/core/core/jest.config.js
Normal file
3
packages/core/core/jest.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
const config = require('../../../jest/config');
|
||||
|
||||
module.exports = Object.assign({}, config, {});
|
||||
59
packages/core/core/package.json
Normal file
59
packages/core/core/package.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "@verdaccio/core",
|
||||
"version": "6.0.0-6-next.1",
|
||||
"description": "core utilities",
|
||||
"keywords": [
|
||||
"private",
|
||||
"package",
|
||||
"repository",
|
||||
"registry",
|
||||
"enterprise",
|
||||
"modules",
|
||||
"proxy",
|
||||
"server",
|
||||
"verdaccio"
|
||||
],
|
||||
"main": "./build/index.js",
|
||||
"types": "./build/index.d.ts",
|
||||
"author": "Juan Picado <juanpicado19@gmail.com>",
|
||||
"license": "MIT",
|
||||
"homepage": "https://verdaccio.org",
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
},
|
||||
"repository": {
|
||||
"type": "https",
|
||||
"url": "https://github.com/verdaccio/verdaccio",
|
||||
"directory": "packages/core/core"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/verdaccio/verdaccio/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"http-errors": "1.8.0",
|
||||
"http-status-codes": "2.1.4",
|
||||
"semver": "7.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@verdaccio/types": "workspace:11.0.0-6-next.8",
|
||||
"typedoc": "next"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf ./build",
|
||||
"test": "cross-env NODE_ENV=test BABEL_ENV=test jest",
|
||||
"type-check": "tsc --noEmit -p tsconfig.build.json",
|
||||
"build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
|
||||
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",
|
||||
"watch": "pnpm build:js -- --watch",
|
||||
"build": "pnpm run build:js && pnpm run build:types",
|
||||
"doc": "typedoc src/index.ts --tsconfig tsconfig.build.json"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/verdaccio"
|
||||
}
|
||||
}
|
||||
49
packages/core/core/src/constants.ts
Normal file
49
packages/core/core/src/constants.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
export const DEFAULT_MIN_LIMIT_PASSWORD = 3;
|
||||
export const TIME_EXPIRATION_24H = '24h';
|
||||
export const TIME_EXPIRATION_7D = '7d';
|
||||
export const DIST_TAGS = 'dist-tags';
|
||||
export const LATEST = 'latest';
|
||||
export const USERS = 'users';
|
||||
export const DEFAULT_USER = 'Anonymous';
|
||||
|
||||
export const HEADER_TYPE = {
|
||||
CONTENT_ENCODING: 'content-encoding',
|
||||
CONTENT_TYPE: 'content-type',
|
||||
CONTENT_LENGTH: 'content-length',
|
||||
ACCEPT_ENCODING: 'accept-encoding',
|
||||
};
|
||||
|
||||
export const CHARACTER_ENCODING = {
|
||||
UTF8: 'utf8',
|
||||
};
|
||||
|
||||
export const TOKEN_BASIC = 'Basic';
|
||||
export const TOKEN_BEARER = 'Bearer';
|
||||
|
||||
export const HEADERS = {
|
||||
ACCEPT: 'Accept',
|
||||
ACCEPT_ENCODING: 'Accept-Encoding',
|
||||
USER_AGENT: 'User-Agent',
|
||||
JSON: 'application/json',
|
||||
CONTENT_TYPE: 'Content-type',
|
||||
CONTENT_LENGTH: 'content-length',
|
||||
TEXT_PLAIN: 'text/plain',
|
||||
TEXT_PLAIN_UTF8: 'text/plain; charset=utf-8',
|
||||
TEXT_HTML_UTF8: 'text/html; charset=utf-8',
|
||||
TEXT_HTML: 'text/html',
|
||||
AUTHORIZATION: 'authorization',
|
||||
// only set with proxy that setup HTTPS
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
|
||||
FORWARDED_PROTO: 'X-Forwarded-Proto',
|
||||
FORWARDED_FOR: 'X-Forwarded-For',
|
||||
FRAMES_OPTIONS: 'X-Frame-Options',
|
||||
CSP: 'Content-Security-Policy',
|
||||
CTO: 'X-Content-Type-Options',
|
||||
XSS: 'X-XSS-Protection',
|
||||
ETAG: 'ETag',
|
||||
JSON_CHARSET: 'application/json; charset=utf-8',
|
||||
OCTET_STREAM: 'application/octet-stream; charset=utf-8',
|
||||
TEXT_CHARSET: 'text/plain; charset=utf-8',
|
||||
WWW_AUTH: 'WWW-Authenticate',
|
||||
GZIP: 'gzip',
|
||||
};
|
||||
144
packages/core/core/src/error-utils.ts
Normal file
144
packages/core/core/src/error-utils.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import createError, { HttpError } from 'http-errors';
|
||||
import httpCodes from 'http-status-codes';
|
||||
import { DEFAULT_MIN_LIMIT_PASSWORD } from './constants';
|
||||
|
||||
export const HTTP_STATUS = {
|
||||
OK: httpCodes.OK,
|
||||
CREATED: httpCodes.CREATED,
|
||||
MULTIPLE_CHOICES: httpCodes.MULTIPLE_CHOICES,
|
||||
NOT_MODIFIED: httpCodes.NOT_MODIFIED,
|
||||
BAD_REQUEST: httpCodes.BAD_REQUEST,
|
||||
UNAUTHORIZED: httpCodes.UNAUTHORIZED,
|
||||
FORBIDDEN: httpCodes.FORBIDDEN,
|
||||
NOT_FOUND: httpCodes.NOT_FOUND,
|
||||
CONFLICT: httpCodes.CONFLICT,
|
||||
NOT_IMPLEMENTED: httpCodes.NOT_IMPLEMENTED,
|
||||
UNSUPPORTED_MEDIA: httpCodes.UNSUPPORTED_MEDIA_TYPE,
|
||||
BAD_DATA: httpCodes.UNPROCESSABLE_ENTITY,
|
||||
INTERNAL_ERROR: httpCodes.INTERNAL_SERVER_ERROR,
|
||||
SERVICE_UNAVAILABLE: httpCodes.SERVICE_UNAVAILABLE,
|
||||
LOOP_DETECTED: 508,
|
||||
};
|
||||
|
||||
export const ERROR_CODE = {
|
||||
token_required: 'token is required',
|
||||
};
|
||||
|
||||
export const API_MESSAGE = {
|
||||
PKG_CREATED: 'created new package',
|
||||
PKG_CHANGED: 'package changed',
|
||||
PKG_REMOVED: 'package removed',
|
||||
PKG_PUBLISHED: 'package published',
|
||||
TARBALL_UPLOADED: 'tarball uploaded successfully',
|
||||
TARBALL_REMOVED: 'tarball removed',
|
||||
TAG_UPDATED: 'tags updated',
|
||||
TAG_REMOVED: 'tag removed',
|
||||
TAG_ADDED: 'package tagged',
|
||||
LOGGED_OUT: 'Logged out',
|
||||
};
|
||||
|
||||
export const SUPPORT_ERRORS = {
|
||||
PLUGIN_MISSING_INTERFACE: 'the plugin does not provide implementation of the requested feature',
|
||||
TFA_DISABLED: 'the two-factor authentication is not yet supported',
|
||||
STORAGE_NOT_IMPLEMENT: 'the storage does not support token saving',
|
||||
PARAMETERS_NOT_VALID: 'the parameters are not valid',
|
||||
};
|
||||
|
||||
export const API_ERROR = {
|
||||
PASSWORD_SHORT: (passLength = DEFAULT_MIN_LIMIT_PASSWORD): string =>
|
||||
`The provided password is too short. Please pick a password longer than ` +
|
||||
`${passLength} characters.`,
|
||||
MUST_BE_LOGGED: 'You must be logged in to publish packages.',
|
||||
PLUGIN_ERROR: 'bug in the auth plugin system',
|
||||
CONFIG_BAD_FORMAT: 'config file must be an object',
|
||||
BAD_USERNAME_PASSWORD: 'bad username/password, access denied',
|
||||
NO_PACKAGE: 'no such package available',
|
||||
PACKAGE_CANNOT_BE_ADDED: 'this package cannot be added',
|
||||
BAD_DATA: 'bad data',
|
||||
NOT_ALLOWED: 'not allowed to access package',
|
||||
NOT_ALLOWED_PUBLISH: 'not allowed to publish package',
|
||||
INTERNAL_SERVER_ERROR: 'internal server error',
|
||||
UNKNOWN_ERROR: 'unknown error',
|
||||
NOT_PACKAGE_UPLINK: 'package does not exist on uplink',
|
||||
UPLINK_OFFLINE_PUBLISH: 'one of the uplinks is down, refuse to publish',
|
||||
UPLINK_OFFLINE: 'uplink is offline',
|
||||
CONTENT_MISMATCH: 'content length mismatch',
|
||||
NOT_FILE_UPLINK: "file doesn't exist on uplink",
|
||||
MAX_USERS_REACHED: 'maximum amount of users reached',
|
||||
VERSION_NOT_EXIST: "this version doesn't exist",
|
||||
UNSUPORTED_REGISTRY_CALL: 'unsupported registry call',
|
||||
FILE_NOT_FOUND: 'File not found',
|
||||
REGISTRATION_DISABLED: 'user registration disabled',
|
||||
UNAUTHORIZED_ACCESS: 'unauthorized access',
|
||||
BAD_STATUS_CODE: 'bad status code',
|
||||
PACKAGE_EXIST: 'this package is already present',
|
||||
BAD_AUTH_HEADER: 'bad authorization header',
|
||||
WEB_DISABLED: 'Web interface is disabled in the config file',
|
||||
DEPRECATED_BASIC_HEADER: 'basic authentication is deprecated, please use JWT instead',
|
||||
BAD_FORMAT_USER_GROUP: 'user groups is different than an array',
|
||||
RESOURCE_UNAVAILABLE: 'resource unavailable',
|
||||
BAD_PACKAGE_DATA: 'bad incoming package data',
|
||||
USERNAME_PASSWORD_REQUIRED: 'username and password is required',
|
||||
USERNAME_ALREADY_REGISTERED: 'username is already registered',
|
||||
};
|
||||
|
||||
export const APP_ERROR = {
|
||||
CONFIG_NOT_VALID: 'CONFIG: it does not look like a valid config file',
|
||||
PROFILE_ERROR: 'profile unexpected error',
|
||||
PASSWORD_VALIDATION: 'not valid password',
|
||||
};
|
||||
|
||||
export type VerdaccioError = HttpError & { code: number };
|
||||
|
||||
function getError(code: number, message: string): VerdaccioError {
|
||||
const httpError = createError(code, message);
|
||||
|
||||
httpError.code = code;
|
||||
|
||||
return httpError as VerdaccioError;
|
||||
}
|
||||
|
||||
export function getConflict(message: string = API_ERROR.PACKAGE_EXIST): VerdaccioError {
|
||||
return getError(HTTP_STATUS.CONFLICT, message);
|
||||
}
|
||||
|
||||
export function getBadData(customMessage?: string): VerdaccioError {
|
||||
return getError(HTTP_STATUS.BAD_DATA, customMessage || API_ERROR.BAD_DATA);
|
||||
}
|
||||
|
||||
export function getBadRequest(customMessage: string): VerdaccioError {
|
||||
return getError(HTTP_STATUS.BAD_REQUEST, customMessage);
|
||||
}
|
||||
|
||||
export function getInternalError(customMessage?: string): VerdaccioError {
|
||||
return customMessage
|
||||
? getError(HTTP_STATUS.INTERNAL_ERROR, customMessage)
|
||||
: getError(HTTP_STATUS.INTERNAL_ERROR, API_ERROR.UNKNOWN_ERROR);
|
||||
}
|
||||
|
||||
export function getUnauthorized(message = 'no credentials provided'): VerdaccioError {
|
||||
return getError(HTTP_STATUS.UNAUTHORIZED, message);
|
||||
}
|
||||
|
||||
export function getForbidden(message = "can't use this filename"): VerdaccioError {
|
||||
return getError(HTTP_STATUS.FORBIDDEN, message);
|
||||
}
|
||||
|
||||
export function getServiceUnavailable(
|
||||
message: string = API_ERROR.RESOURCE_UNAVAILABLE
|
||||
): VerdaccioError {
|
||||
return getError(HTTP_STATUS.SERVICE_UNAVAILABLE, message);
|
||||
}
|
||||
|
||||
export function getNotFound(customMessage?: string): VerdaccioError {
|
||||
return getError(HTTP_STATUS.NOT_FOUND, customMessage || API_ERROR.NO_PACKAGE);
|
||||
}
|
||||
|
||||
export function getCode(statusCode: number, customMessage: string): VerdaccioError {
|
||||
return getError(statusCode, customMessage);
|
||||
}
|
||||
|
||||
export const LOG_STATUS_MESSAGE =
|
||||
"@{status}, user: @{user}(@{remoteIP}), req: '@{request.method} @{request.url}'";
|
||||
export const LOG_VERDACCIO_ERROR = `${LOG_STATUS_MESSAGE}, error: @{!error}`;
|
||||
export const LOG_VERDACCIO_BYTES = `${LOG_STATUS_MESSAGE}, bytes: @{bytes.in}/@{bytes.out}`;
|
||||
3
packages/core/core/src/file-utils.ts
Normal file
3
packages/core/core/src/file-utils.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const Files = {
|
||||
DatabaseName: '.verdaccio-db.json',
|
||||
};
|
||||
19
packages/core/core/src/index.ts
Normal file
19
packages/core/core/src/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as searchUtils from './search-utils';
|
||||
import * as streamUtils from './stream-utils';
|
||||
import * as errorUtils from './error-utils';
|
||||
import * as validatioUtils from './validation-utils';
|
||||
import * as constants from './constants';
|
||||
import * as pluginUtils from './plugin-utils';
|
||||
import * as fileUtils from './file-utils';
|
||||
import * as pkgUtils from './pkg-utils';
|
||||
|
||||
export {
|
||||
fileUtils,
|
||||
pkgUtils,
|
||||
searchUtils,
|
||||
streamUtils,
|
||||
errorUtils,
|
||||
validatioUtils,
|
||||
constants,
|
||||
pluginUtils,
|
||||
};
|
||||
0
packages/core/core/src/path-utils.ts
Normal file
0
packages/core/core/src/path-utils.ts
Normal file
64
packages/core/core/src/pkg-utils.ts
Normal file
64
packages/core/core/src/pkg-utils.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Package } from '@verdaccio/types';
|
||||
import semver from 'semver';
|
||||
import { DIST_TAGS } from './constants';
|
||||
|
||||
/**
|
||||
* Function filters out bad semver versions and sorts the array.
|
||||
* @return {Array} sorted Array
|
||||
*/
|
||||
export function semverSort(listVersions: string[]): string[] {
|
||||
return listVersions
|
||||
.filter(function (x): boolean {
|
||||
if (!semver.parse(x, true)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sort(semver.compareLoose)
|
||||
.map(String);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest publihsed version of a package.
|
||||
* @param package metadata
|
||||
**/
|
||||
export function getLatest(pkg: Package): string {
|
||||
const listVersions: string[] = Object.keys(pkg.versions);
|
||||
if (listVersions.length < 1) {
|
||||
throw Error('cannot get lastest version of none');
|
||||
}
|
||||
|
||||
const versions: string[] = semverSort(listVersions);
|
||||
const latest: string | undefined = pkg[DIST_TAGS]?.latest ? pkg[DIST_TAGS].latest : versions[0];
|
||||
|
||||
return latest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function gets a local info and an info from uplinks and tries to merge it
|
||||
exported for unit tests only.
|
||||
* @param {*} local
|
||||
* @param {*} upstream
|
||||
* @param {*} config sds
|
||||
*/
|
||||
export function mergeVersions(local: Package, upstream: Package) {
|
||||
// copy new versions to a cache
|
||||
// NOTE: if a certain version was updated, we can't refresh it reliably
|
||||
for (const i in upstream.versions) {
|
||||
if (typeof local.versions[i] === 'undefined') {
|
||||
local.versions[i] = upstream.versions[i];
|
||||
}
|
||||
}
|
||||
|
||||
for (const i in upstream[DIST_TAGS]) {
|
||||
if (local[DIST_TAGS][i] !== upstream[DIST_TAGS][i]) {
|
||||
if (!local[DIST_TAGS][i] || semver.lte(local[DIST_TAGS][i], upstream[DIST_TAGS][i])) {
|
||||
local[DIST_TAGS][i] = upstream[DIST_TAGS][i];
|
||||
}
|
||||
if (i === 'latest' && local[DIST_TAGS][i] === upstream[DIST_TAGS][i]) {
|
||||
// if remote has more fresh package, we should borrow its readme
|
||||
local.readme = upstream.readme;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
packages/core/core/src/plugin-utils.ts
Normal file
23
packages/core/core/src/plugin-utils.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Config, IPackageStorage, Token, TokenFilter } from '@verdaccio/types';
|
||||
import { searchUtils } from '.';
|
||||
|
||||
interface IPlugin {
|
||||
version?: string;
|
||||
// In case a plugin needs to be cleaned up/removed
|
||||
close?(): void;
|
||||
}
|
||||
|
||||
export interface IPluginStorage<T> extends IPlugin {
|
||||
config: T & Config;
|
||||
add(name: string): Promise<void>;
|
||||
remove(name: string): Promise<void>;
|
||||
get(): Promise<any>;
|
||||
init(): Promise<void>;
|
||||
getSecret(): Promise<string>;
|
||||
setSecret(secret: string): Promise<any>;
|
||||
getPackageStorage(packageInfo: string): IPackageStorage;
|
||||
search(query: searchUtils.SearchQuery): Promise<searchUtils.SearchItem[]>;
|
||||
saveToken(token: Token): Promise<any>;
|
||||
deleteToken(user: string, tokenKey: string): Promise<any>;
|
||||
readTokens(filter: TokenFilter): Promise<Token[]>;
|
||||
}
|
||||
64
packages/core/core/src/search-utils.ts
Normal file
64
packages/core/core/src/search-utils.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
export type SearchMetrics = {
|
||||
quality: number;
|
||||
popularity: number;
|
||||
maintenance: number;
|
||||
};
|
||||
export type UnStable = {
|
||||
flags?: {
|
||||
// if is false is not be included in search results (majority are stable)
|
||||
unstable?: boolean;
|
||||
};
|
||||
};
|
||||
export type SearchItemPkg = {
|
||||
name: string;
|
||||
scoped?: string;
|
||||
path?: string;
|
||||
time?: number | Date;
|
||||
};
|
||||
|
||||
export type SearchItem = {
|
||||
package: SearchItemPkg;
|
||||
score: Score;
|
||||
} & UnStable;
|
||||
|
||||
export type Score = {
|
||||
final: number;
|
||||
detail: SearchMetrics;
|
||||
};
|
||||
|
||||
type PublisherMaintainer = {
|
||||
username: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type SearchPackageBody = {
|
||||
name: string;
|
||||
scope: string;
|
||||
description: string;
|
||||
author: string | PublisherMaintainer;
|
||||
version: string;
|
||||
keywords: string | string[] | undefined;
|
||||
date: string;
|
||||
links?: {
|
||||
npm: string; // only include placeholder for URL eg: {url}/{packageName}
|
||||
homepage?: string;
|
||||
repository?: string;
|
||||
bugs?: string;
|
||||
};
|
||||
publisher?: any;
|
||||
maintainers?: PublisherMaintainer[];
|
||||
};
|
||||
|
||||
export type SearchPackageItem = {
|
||||
package: SearchPackageBody;
|
||||
score: Score;
|
||||
searchScore?: number;
|
||||
} & UnStable;
|
||||
|
||||
export const UNSCOPED = 'unscoped';
|
||||
|
||||
export type SearchQuery = {
|
||||
text: string;
|
||||
size?: number;
|
||||
from?: number;
|
||||
} & SearchMetrics;
|
||||
109
packages/core/core/src/stream-utils.ts
Normal file
109
packages/core/core/src/stream-utils.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { PassThrough, TransformOptions, Transform } from 'stream';
|
||||
|
||||
export interface IReadTarball {
|
||||
abort?: () => void;
|
||||
}
|
||||
|
||||
export interface IUploadTarball {
|
||||
done?: () => void;
|
||||
abort?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This stream is used to read tarballs from repository.
|
||||
* @param {*} options
|
||||
* @return {Stream}
|
||||
*/
|
||||
class ReadTarball extends PassThrough implements IReadTarball {
|
||||
/**
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
public constructor(options: TransformOptions) {
|
||||
super(options);
|
||||
// called when data is not needed anymore
|
||||
addAbstractMethods(this, 'abort');
|
||||
}
|
||||
|
||||
public abort(): void {}
|
||||
}
|
||||
|
||||
/**
|
||||
* This stream is used to upload tarballs to a repository.
|
||||
* @param {*} options
|
||||
* @return {Stream}
|
||||
*/
|
||||
class UploadTarball extends PassThrough implements IUploadTarball {
|
||||
/**
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
public constructor(options: any) {
|
||||
super(options);
|
||||
// called when user closes connection before upload finishes
|
||||
addAbstractMethods(this, 'abort');
|
||||
|
||||
// called when upload finishes successfully
|
||||
addAbstractMethods(this, 'done');
|
||||
}
|
||||
|
||||
public abort(): void {}
|
||||
public done(): void {}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function intercepts abstract calls and replays them allowing.
|
||||
* us to attach those functions after we are ready to do so
|
||||
* @param {*} self
|
||||
* @param {*} name
|
||||
*/
|
||||
// Perhaps someone knows a better way to write this
|
||||
function addAbstractMethods(self: any, name: any): void {
|
||||
self._called_methods = self._called_methods || {};
|
||||
|
||||
self.__defineGetter__(name, function () {
|
||||
return function (): void {
|
||||
self._called_methods[name] = true;
|
||||
};
|
||||
});
|
||||
|
||||
self.__defineSetter__(name, function (fn: any) {
|
||||
delete self[name];
|
||||
|
||||
self[name] = fn;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
|
||||
if (self._called_methods && self._called_methods[name]) {
|
||||
delete self._called_methods[name];
|
||||
|
||||
self[name]();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a buffer stream to a string.
|
||||
*/
|
||||
const readableToString = async (stream) => {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (let chunk of stream) {
|
||||
chunks.push(Buffer.from(chunk));
|
||||
}
|
||||
const buffer = Buffer.concat(chunks);
|
||||
const str = buffer.toString('utf-8');
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform stream object mode to string
|
||||
**/
|
||||
const transformObjectToString = () => {
|
||||
return new Transform({
|
||||
objectMode: true,
|
||||
transform: (chunk, encoding, callback) => {
|
||||
callback(null, JSON.stringify(chunk));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export { ReadTarball, UploadTarball, readableToString, transformObjectToString };
|
||||
100
packages/core/core/src/validation-utils.ts
Normal file
100
packages/core/core/src/validation-utils.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import assert from 'assert';
|
||||
import { Package } from '@verdaccio/types';
|
||||
import { DIST_TAGS } from './constants';
|
||||
|
||||
export function isPackageNameScoped(name: string): boolean {
|
||||
return name.startsWith('@');
|
||||
}
|
||||
|
||||
/**
|
||||
* From normalize-package-data/lib/fixer.js
|
||||
* @param {*} name the package name
|
||||
* @return {Boolean} whether is valid or not
|
||||
*/
|
||||
export function validateName(name: string): boolean {
|
||||
if (typeof name !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let normalizedName: string = name.toLowerCase();
|
||||
|
||||
const isScoped: boolean = isPackageNameScoped(name);
|
||||
const scopedName = name.split('/', 2)[1];
|
||||
|
||||
if (isScoped && typeof scopedName !== 'undefined') {
|
||||
normalizedName = scopedName.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Some context about the first regex
|
||||
* - npm used to have a different tarball naming system.
|
||||
* eg: http://registry.npmjs.com/thirty-two
|
||||
* https://registry.npmjs.org/thirty-two/-/thirty-two@0.0.1.tgz
|
||||
* The file name thirty-two@0.0.1.tgz, the version and the pkg name was separated by an at (@)
|
||||
* while nowadays the naming system is based in dashes
|
||||
* https://registry.npmjs.org/verdaccio/-/verdaccio-1.4.0.tgz
|
||||
*
|
||||
* more info here: https://github.com/rlidwka/sinopia/issues/75
|
||||
*/
|
||||
return !(
|
||||
!normalizedName.match(/^[-a-zA-Z0-9_.!~*'()@]+$/) ||
|
||||
normalizedName.startsWith('.') || // ".bin", etc.
|
||||
['node_modules', '__proto__', 'favicon.ico'].includes(normalizedName)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a package.
|
||||
* @return {Boolean} whether the package is valid or not
|
||||
*/
|
||||
export function validatePackage(name: string): boolean {
|
||||
const nameList = name.split('/', 2);
|
||||
if (nameList.length === 1) {
|
||||
// normal package
|
||||
return validateName(nameList[0]);
|
||||
}
|
||||
// scoped package
|
||||
return nameList[0][0] === '@' && validateName(nameList[0].slice(1)) && validateName(nameList[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the package metadata, add additional properties whether are missing within
|
||||
* the metadata properties.
|
||||
* @param {*} object
|
||||
* @param {*} name
|
||||
* @return {Object} the object with additional properties as dist-tags ad versions
|
||||
*/
|
||||
export function validateMetadata(object: Package, name: string): Package {
|
||||
assert(isObject(object), 'not a json object');
|
||||
assert.strictEqual(object.name, name);
|
||||
|
||||
if (!isObject(object[DIST_TAGS])) {
|
||||
object[DIST_TAGS] = {};
|
||||
}
|
||||
|
||||
if (!isObject(object['versions'])) {
|
||||
object['versions'] = {};
|
||||
}
|
||||
|
||||
if (!isObject(object['time'])) {
|
||||
object['time'] = {};
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an element is an Object
|
||||
* @param {*} obj the element
|
||||
* @return {Boolean}
|
||||
*/
|
||||
export function isObject(obj: any): boolean {
|
||||
if (obj === null || typeof obj === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
(typeof obj === 'object' || typeof obj.prototype === 'undefined') &&
|
||||
Array.isArray(obj) === false
|
||||
);
|
||||
}
|
||||
108
packages/core/core/test/errors.spec.ts
Normal file
108
packages/core/core/test/errors.spec.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import {
|
||||
getNotFound,
|
||||
VerdaccioError,
|
||||
HTTP_STATUS,
|
||||
getConflict,
|
||||
getBadData,
|
||||
getInternalError,
|
||||
API_ERROR,
|
||||
getUnauthorized,
|
||||
getForbidden,
|
||||
getServiceUnavailable,
|
||||
getCode,
|
||||
} from '../src/error-utils';
|
||||
|
||||
describe('testing errors', () => {
|
||||
test('should qualify as an native error', () => {
|
||||
expect(_.isError(getNotFound())).toBeTruthy();
|
||||
expect(_.isError(getConflict())).toBeTruthy();
|
||||
expect(_.isError(getBadData())).toBeTruthy();
|
||||
expect(_.isError(getInternalError())).toBeTruthy();
|
||||
expect(_.isError(getUnauthorized())).toBeTruthy();
|
||||
expect(_.isError(getForbidden())).toBeTruthy();
|
||||
expect(_.isError(getServiceUnavailable())).toBeTruthy();
|
||||
expect(_.isError(getCode(400, 'fooError'))).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should test not found', () => {
|
||||
const err: VerdaccioError = getNotFound('foo');
|
||||
|
||||
expect(err.code).toBeDefined();
|
||||
expect(err.code).toEqual(HTTP_STATUS.NOT_FOUND);
|
||||
expect(err.statusCode).toEqual(HTTP_STATUS.NOT_FOUND);
|
||||
expect(err.message).toEqual('foo');
|
||||
});
|
||||
|
||||
test('should test conflict', () => {
|
||||
const err: VerdaccioError = getConflict('foo');
|
||||
|
||||
expect(err.code).toBeDefined();
|
||||
expect(err.code).toEqual(HTTP_STATUS.CONFLICT);
|
||||
expect(err.message).toEqual('foo');
|
||||
});
|
||||
|
||||
test('should test bad data', () => {
|
||||
const err: VerdaccioError = getBadData('foo');
|
||||
|
||||
expect(err.code).toBeDefined();
|
||||
expect(err.code).toEqual(HTTP_STATUS.BAD_DATA);
|
||||
expect(err.message).toEqual('foo');
|
||||
});
|
||||
|
||||
test('should test internal error custom message', () => {
|
||||
const err: VerdaccioError = getInternalError('foo');
|
||||
|
||||
expect(err.code).toBeDefined();
|
||||
expect(err.code).toEqual(HTTP_STATUS.INTERNAL_ERROR);
|
||||
expect(err.message).toEqual('foo');
|
||||
});
|
||||
|
||||
test('should test internal error', () => {
|
||||
const err: VerdaccioError = getInternalError();
|
||||
|
||||
expect(err.code).toBeDefined();
|
||||
expect(err.code).toEqual(HTTP_STATUS.INTERNAL_ERROR);
|
||||
expect(err.message).toEqual(API_ERROR.UNKNOWN_ERROR);
|
||||
});
|
||||
|
||||
test('should test Unauthorized message', () => {
|
||||
const err: VerdaccioError = getUnauthorized('foo');
|
||||
|
||||
expect(err.code).toBeDefined();
|
||||
expect(err.code).toEqual(HTTP_STATUS.UNAUTHORIZED);
|
||||
expect(err.message).toEqual('foo');
|
||||
});
|
||||
|
||||
test('should test forbidden message', () => {
|
||||
const err: VerdaccioError = getForbidden('foo');
|
||||
|
||||
expect(err.code).toBeDefined();
|
||||
expect(err.code).toEqual(HTTP_STATUS.FORBIDDEN);
|
||||
expect(err.message).toEqual('foo');
|
||||
});
|
||||
|
||||
test('should test service unavailable message', () => {
|
||||
const err: VerdaccioError = getServiceUnavailable('foo');
|
||||
|
||||
expect(err.code).toBeDefined();
|
||||
expect(err.code).toEqual(HTTP_STATUS.SERVICE_UNAVAILABLE);
|
||||
expect(err.message).toEqual('foo');
|
||||
});
|
||||
|
||||
test('should test custom code error message', () => {
|
||||
const err: VerdaccioError = getCode(HTTP_STATUS.NOT_FOUND, 'foo');
|
||||
|
||||
expect(err.code).toBeDefined();
|
||||
expect(err.code).toEqual(HTTP_STATUS.NOT_FOUND);
|
||||
expect(err.message).toEqual('foo');
|
||||
});
|
||||
|
||||
test('should test custom code ok message', () => {
|
||||
const err: VerdaccioError = getCode(HTTP_STATUS.OK, 'foo');
|
||||
|
||||
expect(err.code).toBeDefined();
|
||||
expect(err.code).toEqual(HTTP_STATUS.OK);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,4 @@
|
||||
import { semverSort } from '@verdaccio/utils';
|
||||
import { setup } from '@verdaccio/logger';
|
||||
|
||||
import { mergeVersions } from '../src/metadata-utils';
|
||||
|
||||
setup([]);
|
||||
import { semverSort, mergeVersions } from '../src/pkg-utils';
|
||||
|
||||
describe('Storage._merge_versions versions', () => {
|
||||
test('simple', () => {
|
||||
42
packages/core/core/test/mystreams.spec.ts
Normal file
42
packages/core/core/test/mystreams.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Stream } from 'stream';
|
||||
import { readableToString, ReadTarball, UploadTarball } from '../src/stream-utils';
|
||||
|
||||
describe('mystreams', () => {
|
||||
test('should delay events on ReadTarball abort', (cb) => {
|
||||
const readTballStream = new ReadTarball({});
|
||||
readTballStream.abort();
|
||||
setTimeout(function () {
|
||||
readTballStream.abort = function (): void {
|
||||
cb();
|
||||
};
|
||||
readTballStream.abort = function (): never {
|
||||
throw Error('fail');
|
||||
};
|
||||
}, 10);
|
||||
});
|
||||
|
||||
test('should delay events on UploadTarball abort', (cb) => {
|
||||
const uploadTballStream = new UploadTarball({});
|
||||
uploadTballStream.abort();
|
||||
setTimeout(function () {
|
||||
uploadTballStream.abort = function (): void {
|
||||
cb();
|
||||
};
|
||||
uploadTballStream.abort = function (): never {
|
||||
throw Error('fail');
|
||||
};
|
||||
}, 10);
|
||||
});
|
||||
|
||||
test('readableToString single string', async () => {
|
||||
expect(await readableToString(Stream.Readable.from('foo'))).toEqual('foo');
|
||||
});
|
||||
|
||||
test('readableToString single object', async () => {
|
||||
expect(
|
||||
JSON.parse(await readableToString(Stream.Readable.from(JSON.stringify({ foo: 1 }))))
|
||||
).toEqual({
|
||||
foo: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
74
packages/core/core/test/validation-utilts.spec.ts
Normal file
74
packages/core/core/test/validation-utilts.spec.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { validateName, validatePackage, isObject } from '../src/validation-utils';
|
||||
|
||||
describe('validatePackage', () => {
|
||||
test('should validate package names', () => {
|
||||
expect(validatePackage('package-name')).toBeTruthy();
|
||||
expect(validatePackage('@scope/package-name')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should fails on validate package names', () => {
|
||||
expect(validatePackage('package-name/test/fake')).toBeFalsy();
|
||||
expect(validatePackage('@/package-name')).toBeFalsy();
|
||||
expect(validatePackage('$%$%#$%$#%#$%$#')).toBeFalsy();
|
||||
expect(validatePackage('node_modules')).toBeFalsy();
|
||||
expect(validatePackage('__proto__')).toBeFalsy();
|
||||
expect(validatePackage('favicon.ico')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isObject', () => {
|
||||
test('isObject metadata', () => {
|
||||
expect(isObject({ foo: 'bar' })).toBeTruthy();
|
||||
expect(isObject('foo')).toBeTruthy();
|
||||
expect(isObject(['foo'])).toBeFalsy();
|
||||
expect(isObject(null)).toBeFalsy();
|
||||
expect(isObject(undefined)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateName', () => {
|
||||
test('should fails with no string', () => {
|
||||
// intended to fail with Typescript, do not remove
|
||||
// @ts-ignore
|
||||
expect(validateName(null)).toBeFalsy();
|
||||
// @ts-ignore
|
||||
expect(validateName(undefined)).toBeFalsy();
|
||||
});
|
||||
|
||||
test('good ones', () => {
|
||||
expect(validateName('verdaccio')).toBeTruthy();
|
||||
expect(validateName('some.weird.package-zzz')).toBeTruthy();
|
||||
expect(validateName('old-package@0.1.2.tgz')).toBeTruthy();
|
||||
// fix https://github.com/verdaccio/verdaccio/issues/1400
|
||||
expect(validateName('-build-infra')).toBeTruthy();
|
||||
expect(validateName('@pkg-scoped/without-extension')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should be valid using uppercase', () => {
|
||||
expect(validateName('ETE')).toBeTruthy();
|
||||
expect(validateName('JSONStream')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should fails with path seps', () => {
|
||||
expect(validateName('some/thing')).toBeFalsy();
|
||||
expect(validateName('some\\thing')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should fail with no hidden files', () => {
|
||||
expect(validateName('.bin')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should fails with reserved words', () => {
|
||||
expect(validateName('favicon.ico')).toBeFalsy();
|
||||
expect(validateName('node_modules')).toBeFalsy();
|
||||
expect(validateName('__proto__')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should fails with other options', () => {
|
||||
expect(validateName('pk g')).toBeFalsy();
|
||||
expect(validateName('pk\tg')).toBeFalsy();
|
||||
expect(validateName('pk%20g')).toBeFalsy();
|
||||
expect(validateName('pk+g')).toBeFalsy();
|
||||
expect(validateName('pk:g')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
9
packages/core/core/tsconfig.build.json
Normal file
9
packages/core/core/tsconfig.build.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./build"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
||||
11
packages/core/core/tsconfig.json
Normal file
11
packages/core/core/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.reference.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./build",
|
||||
"composite": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
||||
@@ -40,7 +40,7 @@
|
||||
"lockfile": "1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@verdaccio/types": "workspace:11.0.0-6-next.7"
|
||||
"@verdaccio/types": "workspace:11.0.0-6-next.8"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf ./build",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# Change Log
|
||||
|
||||
## 11.0.0-6-next.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [459b6fa7]
|
||||
- @verdaccio/commons-api@11.0.0-6-next.4
|
||||
- @verdaccio/file-locking@11.0.0-alpha.3
|
||||
|
||||
## 11.0.0-6-next.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- df0da3d6: Added core-js missing from dependencies though referenced in .js sources
|
||||
|
||||
## 10.0.0-alpha.6
|
||||
|
||||
### Major Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "verdaccio-htpasswd",
|
||||
"version": "11.0.0-alpha.6",
|
||||
"version": "11.0.0-6-next.8",
|
||||
"description": "htpasswd auth plugin for Verdaccio",
|
||||
"keywords": [
|
||||
"private",
|
||||
@@ -34,16 +34,17 @@
|
||||
"npm": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@verdaccio/commons-api": "workspace:11.0.0-alpha.3",
|
||||
"@verdaccio/commons-api": "workspace:11.0.0-6-next.4",
|
||||
"@verdaccio/file-locking": "workspace:11.0.0-alpha.3",
|
||||
"apache-md5": "1.1.7",
|
||||
"bcryptjs": "2.4.3",
|
||||
"core-js": "3.17.2",
|
||||
"http-errors": "1.8.0",
|
||||
"unix-crypt-td-js": "1.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@verdaccio/types": "workspace:11.0.0-6-next.7",
|
||||
"@verdaccio/types": "workspace:11.0.0-6-next.8",
|
||||
"mockdate": "3.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,56 @@
|
||||
# Change Log
|
||||
|
||||
## 11.0.0-6-next.8
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 459b6fa7: refactor: search v1 endpoint and local-database
|
||||
|
||||
- refactor search `api v1` endpoint, improve performance
|
||||
- remove usage of `async` dependency https://github.com/verdaccio/verdaccio/issues/1225
|
||||
- refactor method storage class
|
||||
- create new module `core` to reduce the ammount of modules with utilities
|
||||
- use `undici` instead `node-fetch`
|
||||
- use `fastify` instead `express` for functional test
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- plugin storage API changes
|
||||
- remove old search endpoint (return 404)
|
||||
- filter local private packages at plugin level
|
||||
|
||||
The storage api changes for methods `get`, `add`, `remove` as promise base. The `search` methods also changes and recieves a `query` object that contains all query params from the client.
|
||||
|
||||
```ts
|
||||
export interface IPluginStorage<T> extends IPlugin {
|
||||
add(name: string): Promise<void>;
|
||||
remove(name: string): Promise<void>;
|
||||
get(): Promise<any>;
|
||||
init(): Promise<void>;
|
||||
getSecret(): Promise<string>;
|
||||
setSecret(secret: string): Promise<any>;
|
||||
getPackageStorage(packageInfo: string): IPackageStorage;
|
||||
search(query: searchUtils.SearchQuery): Promise<searchUtils.SearchItem[]>;
|
||||
saveToken(token: Token): Promise<any>;
|
||||
deleteToken(user: string, tokenKey: string): Promise<any>;
|
||||
readTokens(filter: TokenFilter): Promise<Token[]>;
|
||||
}
|
||||
```
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [459b6fa7]
|
||||
- @verdaccio/commons-api@11.0.0-6-next.4
|
||||
- @verdaccio/core@6.0.0-6-next.1
|
||||
- @verdaccio/streams@11.0.0-6-next.4
|
||||
- @verdaccio/file-locking@11.0.0-alpha.3
|
||||
|
||||
## 11.0.0-6-next.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- df0da3d6: Added core-js missing from dependencies though referenced in .js sources
|
||||
|
||||
## 11.0.0-6-next.6
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@verdaccio/local-storage",
|
||||
"version": "11.0.0-6-next.6",
|
||||
"version": "11.0.0-6-next.8",
|
||||
"description": "Local storage implementation",
|
||||
"keywords": [
|
||||
"private",
|
||||
@@ -33,23 +33,30 @@
|
||||
"build/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"npm": ">=6"
|
||||
"node": ">=14",
|
||||
"npm": ">=7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@verdaccio/commons-api": "workspace:11.0.0-alpha.3",
|
||||
"@verdaccio/commons-api": "workspace:11.0.0-6-next.4",
|
||||
"@verdaccio/core": "workspace:6.0.0-6-next.1",
|
||||
"@verdaccio/file-locking": "workspace:11.0.0-alpha.3",
|
||||
"@verdaccio/streams": "workspace:11.0.0-alpha.3",
|
||||
"@verdaccio/streams": "workspace:11.0.0-6-next.4",
|
||||
"async": "3.2.1",
|
||||
"core-js": "3.17.2",
|
||||
"debug": "4.3.2",
|
||||
"globby": "11.0.1",
|
||||
"lockfile": "1.0.4",
|
||||
"lodash": "4.17.21",
|
||||
"lowdb": "1.0.0"
|
||||
"lowdb": "1.0.0",
|
||||
"lru-cache": "6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/minimatch": "3.0.5",
|
||||
"@verdaccio/types": "workspace:11.0.0-6-next.7",
|
||||
"@verdaccio/types": "workspace:11.0.0-6-next.8",
|
||||
"@verdaccio/config": "workspace:6.0.0-6-next.8",
|
||||
"@verdaccio/utils": "workspace:6.0.0-6-next.6",
|
||||
"minimatch": "3.0.4",
|
||||
"rmdir-sync": "1.0.1"
|
||||
"tmp-promise": "3.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf ./build",
|
||||
|
||||
115
packages/core/local-storage/src/dir-utils.ts
Normal file
115
packages/core/local-storage/src/dir-utils.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { join } from 'path';
|
||||
import globby from 'globby';
|
||||
import buildDebug from 'debug';
|
||||
import { searchUtils, validatioUtils } from '@verdaccio/core';
|
||||
|
||||
const debug = buildDebug('verdaccio:plugin:local-storage:utils');
|
||||
|
||||
/**
|
||||
* Retrieve a list of absolute paths to all folders in the given storage path
|
||||
* @param storagePath the base path of the storage
|
||||
* @return a promise that resolves to an array of absolute paths
|
||||
*/
|
||||
export async function getFolders(storagePath: string, pattern = '*'): Promise<string[]> {
|
||||
// @ts-ignore - check why this fails, types are correct
|
||||
const files = await globby(pattern, {
|
||||
// @ts-ignore
|
||||
cwd: storagePath,
|
||||
expandDirectories: true,
|
||||
onlyDirectories: true,
|
||||
onlyFiles: false,
|
||||
// should not go deeper than the storage path (10 is reseaon for the storage))
|
||||
deep: 10,
|
||||
dot: false,
|
||||
followSymbolicLinks: true,
|
||||
caseSensitiveMatch: true,
|
||||
unique: true,
|
||||
// FIXME: add here list of forbiden patterns
|
||||
// don't include scoped folders.
|
||||
// ignore: [`@*`],
|
||||
});
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search packages on the the storage. The storage could be
|
||||
* - storage
|
||||
* - pkg1
|
||||
* - @company
|
||||
* - pkg2 -> @scompany/pkg2
|
||||
* - storage1
|
||||
* - pkg2
|
||||
* - pkg3
|
||||
* - storage2
|
||||
* - @scope
|
||||
* - pkg4 > @scope/pkg4
|
||||
* The search return a data structure like:
|
||||
* [
|
||||
* {
|
||||
* name: 'pkg1', // package name could be @scope/pkg1
|
||||
* path: absolute/path/package/name
|
||||
* }
|
||||
* ]
|
||||
* @param {string} storagePath is the base path of the storage folder,
|
||||
* inside could be packages, storages and @scope packages.
|
||||
* @param {Set<string>} storages storages are defined peer package access pattern via `storage` property
|
||||
* @param query is the search query from the user via npm search command.
|
||||
* and are intended to organize packages in a tree structure.
|
||||
* @returns {Promise<searchUtils.SearchItemPkg[]>}
|
||||
*/
|
||||
export async function searchOnStorage(
|
||||
storagePath: string,
|
||||
storages: Map<string, string>
|
||||
): Promise<searchUtils.SearchItemPkg[]> {
|
||||
const matchedStorages = Array.from(storages);
|
||||
const storageFolders = Array.from(storages.keys());
|
||||
// const getScopedFolders = async (pkgName) => {
|
||||
// const scopedPackages = await getFolders(join(storagePath, pkgName), '*');
|
||||
// const listScoped = scopedPackages.map((scoped) => ({
|
||||
// name: `${pkgName}/${scoped}`,
|
||||
// }));
|
||||
// };
|
||||
debug('search on %o', storagePath);
|
||||
debug('storage folders %o', matchedStorages.length);
|
||||
let results: searchUtils.SearchItemPkg[] = [];
|
||||
// watch base path and ignore storage folders
|
||||
const basePathFolders = (await getFolders(storagePath, '*')).filter(
|
||||
(storageFolder) => !storageFolders.includes(storageFolder)
|
||||
);
|
||||
|
||||
for (let store of basePathFolders) {
|
||||
if (validatioUtils.isPackageNameScoped(store)) {
|
||||
const scopedPackages = await getFolders(join(storagePath, store), '*');
|
||||
const listScoped = scopedPackages.map((scoped) => ({
|
||||
name: `${store}/${scoped}`,
|
||||
scoped: store,
|
||||
}));
|
||||
results.push(...listScoped);
|
||||
} else {
|
||||
results.push({
|
||||
name: store,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// iterate each storage folder
|
||||
for (const store of storageFolders) {
|
||||
const foldersOnStorage = await getFolders(join(storagePath, store), '*');
|
||||
for (let pkgName of foldersOnStorage) {
|
||||
if (validatioUtils.isPackageNameScoped(pkgName)) {
|
||||
const scopedPackages = await getFolders(join(storagePath, store, pkgName), '*');
|
||||
const listScoped = scopedPackages.map((scoped) => ({
|
||||
name: `${pkgName}/${scoped}`,
|
||||
scoped: pkgName,
|
||||
}));
|
||||
results.push(...listScoped);
|
||||
} else {
|
||||
results.push({
|
||||
name: pkgName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
17
packages/core/local-storage/src/fs.ts
Normal file
17
packages/core/local-storage/src/fs.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { promisify } from 'util';
|
||||
import fs from 'fs';
|
||||
|
||||
// FUTURE: when v15 is min replace by fs/promises
|
||||
const readFile = promisify(fs.readFile);
|
||||
const mkdirPromise = promisify(fs.mkdir);
|
||||
const writeFilePromise = promisify(fs.writeFile);
|
||||
const readdirPromise = promisify(fs.readdir);
|
||||
const statPromise = promisify(fs.stat);
|
||||
const unlinkPromise = promisify(fs.unlink);
|
||||
const rmdirPromise = promisify(fs.rmdir);
|
||||
|
||||
export const readFilePromise = async (path) => {
|
||||
return await readFile(path, 'utf8');
|
||||
};
|
||||
|
||||
export { mkdirPromise, writeFilePromise, readdirPromise, statPromise, unlinkPromise, rmdirPromise };
|
||||
@@ -1,35 +1,33 @@
|
||||
import fs from 'fs';
|
||||
import Path from 'path';
|
||||
import path from 'path';
|
||||
// import LRU from 'lru-cache';
|
||||
import buildDebug from 'debug';
|
||||
|
||||
import _ from 'lodash';
|
||||
import async from 'async';
|
||||
import {
|
||||
Callback,
|
||||
Config,
|
||||
IPackageStorage,
|
||||
IPluginStorage,
|
||||
LocalStorage,
|
||||
Logger,
|
||||
StorageList,
|
||||
} from '@verdaccio/types';
|
||||
import { getInternalError } from '@verdaccio/commons-api';
|
||||
import { Config, IPackageStorage, LocalStorage, Logger } from '@verdaccio/types';
|
||||
import { errorUtils, searchUtils, pluginUtils, fileUtils } from '@verdaccio/core';
|
||||
import { getMatchedPackagesSpec } from '@verdaccio/utils';
|
||||
|
||||
import LocalDriver, { noSuchFile } from './local-fs';
|
||||
import { loadPrivatePackages } from './pkg-utils';
|
||||
import TokenActions from './token';
|
||||
import { mkdirPromise, writeFilePromise } from './fs';
|
||||
import { searchOnStorage } from './dir-utils';
|
||||
import { _dbGenPath } from './utils';
|
||||
|
||||
const DB_NAME = '.verdaccio-db.json';
|
||||
const DB_NAME = process.env.VERDACCIO_STORAGE_NAME ?? fileUtils.Files.DatabaseName;
|
||||
|
||||
const debug = buildDebug('verdaccio:plugin:local-storage');
|
||||
const debug = buildDebug('verdaccio:plugin:local-storage:experimental');
|
||||
|
||||
class LocalDatabase extends TokenActions implements IPluginStorage<{}> {
|
||||
public path: string;
|
||||
public logger: Logger;
|
||||
// @ts-ignore
|
||||
public data: LocalStorage;
|
||||
public config: Config;
|
||||
export const ERROR_DB_LOCKED =
|
||||
'Database is locked, please check error message printed during startup to prevent data loss';
|
||||
|
||||
type IPluginStorage = pluginUtils.IPluginStorage<{}>;
|
||||
|
||||
class LocalDatabase extends TokenActions implements IPluginStorage {
|
||||
private readonly path: string;
|
||||
private readonly logger: Logger;
|
||||
public readonly config: Config;
|
||||
public readonly storages: Map<string, string>;
|
||||
public data: LocalStorage | void;
|
||||
public locked: boolean;
|
||||
|
||||
public constructor(config: Config, logger: Logger) {
|
||||
@@ -37,185 +35,162 @@ class LocalDatabase extends TokenActions implements IPluginStorage<{}> {
|
||||
this.config = config;
|
||||
this.logger = logger;
|
||||
this.locked = false;
|
||||
this.data = undefined;
|
||||
this.path = _dbGenPath(DB_NAME, config);
|
||||
this.storages = this._getCustomPackageLocalStorages();
|
||||
debug('plugin storage path %o', this.path);
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
debug('plugin init');
|
||||
this.data = await this._fetchLocalPackages();
|
||||
this._sync();
|
||||
debug('local packages loaded');
|
||||
await this._sync();
|
||||
}
|
||||
|
||||
public getSecret(): Promise<string> {
|
||||
public async getSecret(): Promise<string> {
|
||||
if (typeof this.data === 'undefined') {
|
||||
throw Error('no data secret available');
|
||||
}
|
||||
|
||||
return Promise.resolve(this.data.secret);
|
||||
}
|
||||
|
||||
public setSecret(secret: string): Promise<Error | null> {
|
||||
return new Promise((resolve): void => {
|
||||
public async setSecret(secret: string): Promise<void> {
|
||||
if (typeof this.data === 'undefined') {
|
||||
throw Error('no data secret available');
|
||||
} else {
|
||||
this.data.secret = secret;
|
||||
}
|
||||
|
||||
resolve(this._sync());
|
||||
});
|
||||
await this._sync();
|
||||
}
|
||||
|
||||
public add(name: string, cb: Callback): void {
|
||||
public async add(name: string): Promise<void> {
|
||||
if (typeof this.data === 'undefined') {
|
||||
throw Error('no data secret available');
|
||||
}
|
||||
|
||||
if (this.data.list.indexOf(name) === -1) {
|
||||
this.data.list.push(name);
|
||||
|
||||
debug('the private package %o has been added', name);
|
||||
cb(this._sync());
|
||||
debug('the private package %s has been added', name);
|
||||
await this._sync();
|
||||
} else {
|
||||
debug('the private package %o was not added', name);
|
||||
cb(null);
|
||||
debug('the private package %s already exist on database', name);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
public search(
|
||||
onPackage: Callback,
|
||||
onEnd: Callback,
|
||||
validateName: (name: string) => boolean
|
||||
): void {
|
||||
const storages = this._getCustomPackageLocalStorages();
|
||||
debug(`search custom local packages: %o`, JSON.stringify(storages));
|
||||
const base = Path.dirname(this.config.config_path);
|
||||
const self = this;
|
||||
const storageKeys = Object.keys(storages);
|
||||
debug(`search base: %o keys: %o`, base, storageKeys);
|
||||
/**
|
||||
* The field storage could be absolute or relative.
|
||||
* If relative, it will be resolved against the config path.
|
||||
* If absolute, it will be returned as is.
|
||||
**/
|
||||
private getStoragePath() {
|
||||
const { storage } = this.config;
|
||||
if (typeof storage !== 'string') {
|
||||
throw new TypeError('storage field is mandatory');
|
||||
}
|
||||
|
||||
async.eachSeries(
|
||||
storageKeys,
|
||||
function (storage, cb) {
|
||||
const position = storageKeys.indexOf(storage);
|
||||
const base2 = Path.join(position !== 0 ? storageKeys[0] : '');
|
||||
const storagePath: string = Path.resolve(base, base2, storage);
|
||||
debug('search path: %o : %o', storagePath, storage);
|
||||
fs.readdir(storagePath, (err, files) => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
async.eachSeries(
|
||||
files,
|
||||
function (file, cb) {
|
||||
debug('local-storage: [search] search file path: %o', file);
|
||||
if (storageKeys.includes(file)) {
|
||||
return cb();
|
||||
}
|
||||
|
||||
if (file.match(/^@/)) {
|
||||
// scoped
|
||||
const fileLocation = Path.resolve(base, storage, file);
|
||||
debug('search scoped file location: %o', fileLocation);
|
||||
fs.readdir(fileLocation, function (err, files) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
async.eachSeries(
|
||||
files,
|
||||
(file2, cb) => {
|
||||
if (validateName(file2)) {
|
||||
const packagePath = Path.resolve(base, storage, file, file2);
|
||||
|
||||
fs.stat(packagePath, (err, stats) => {
|
||||
if (_.isNil(err) === false) {
|
||||
return cb(err);
|
||||
}
|
||||
const item = {
|
||||
name: `${file}/${file2}`,
|
||||
path: packagePath,
|
||||
time: stats.mtime.getTime(),
|
||||
};
|
||||
onPackage(item, cb);
|
||||
});
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
},
|
||||
cb
|
||||
);
|
||||
});
|
||||
} else if (validateName(file)) {
|
||||
const base2 = Path.join(position !== 0 ? storageKeys[0] : '');
|
||||
const packagePath = Path.resolve(base, base2, storage, file);
|
||||
debug('search file location: %o', packagePath);
|
||||
fs.stat(packagePath, (err, stats) => {
|
||||
if (_.isNil(err) === false) {
|
||||
return cb(err);
|
||||
}
|
||||
onPackage(
|
||||
{
|
||||
name: file,
|
||||
path: packagePath,
|
||||
time: self.getTime(stats.mtime.getTime(), stats.mtime),
|
||||
},
|
||||
cb
|
||||
);
|
||||
});
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
},
|
||||
cb
|
||||
);
|
||||
});
|
||||
},
|
||||
// @ts-ignore
|
||||
onEnd
|
||||
);
|
||||
const storagePath = path.isAbsolute(storage)
|
||||
? storage
|
||||
: path.normalize(path.join(this.getBaseConfigPath(), storage));
|
||||
debug('storage path %o', storagePath);
|
||||
return storagePath;
|
||||
}
|
||||
|
||||
public remove(name: string, cb: Callback): void {
|
||||
this.get((err, data) => {
|
||||
if (err) {
|
||||
cb(getInternalError('error remove private package'));
|
||||
this.logger.error(
|
||||
{ err },
|
||||
'[local-storage/remove]: remove the private package has failed @{err}'
|
||||
);
|
||||
debug('error on remove package %o', name);
|
||||
private getBaseConfigPath(): string {
|
||||
return path.dirname(this.config.config_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by query.
|
||||
**/
|
||||
public async filterByQuery(results: searchUtils.SearchItemPkg[], query: searchUtils.SearchQuery) {
|
||||
// FUTURE: apply new filters, keyword, version, ...
|
||||
return results.filter((item: searchUtils.SearchItemPkg) => {
|
||||
return item?.name?.match(query.text) !== null;
|
||||
}) as searchUtils.SearchItemPkg[];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async getScore(_pkg: searchUtils.SearchItemPkg): Promise<searchUtils.Score> {
|
||||
return Promise.resolve({
|
||||
final: 1,
|
||||
detail: {
|
||||
maintenance: 0,
|
||||
popularity: 1,
|
||||
quality: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async search(query: searchUtils.SearchQuery): Promise<searchUtils.SearchItem[]> {
|
||||
const results: searchUtils.SearchItem[] = [];
|
||||
const storagePath = this.getStoragePath();
|
||||
const packagesOnStorage = await this.filterByQuery(
|
||||
await searchOnStorage(storagePath, this.storages),
|
||||
query
|
||||
);
|
||||
debug('packages found %o', packagesOnStorage.length);
|
||||
for (let storage of packagesOnStorage) {
|
||||
const score = await this.getScore(storage);
|
||||
results.push({
|
||||
package: storage,
|
||||
// there is no particular reason to predefined scores
|
||||
// could be improved by using
|
||||
score,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public async remove(name: string): Promise<void> {
|
||||
try {
|
||||
if (typeof this.data === 'undefined') {
|
||||
throw Error('no data secret available');
|
||||
}
|
||||
|
||||
const data = await this.get();
|
||||
|
||||
const pkgName = data.indexOf(name);
|
||||
if (pkgName !== -1) {
|
||||
this.data.list.splice(pkgName, 1);
|
||||
|
||||
debug('remove package %o has been removed', name);
|
||||
}
|
||||
|
||||
cb(this._sync());
|
||||
});
|
||||
await this._sync();
|
||||
} catch (err) {
|
||||
this.logger.error({ err }, 'remove the private package has failed @{err}');
|
||||
throw errorUtils.getInternalError('error remove private package');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all database elements.
|
||||
* @return {Array}
|
||||
*/
|
||||
public get(cb: Callback): void {
|
||||
const list = this.data.list;
|
||||
const totalItems = this.data.list.length;
|
||||
|
||||
cb(null, list);
|
||||
public async get(): Promise<any> {
|
||||
if (typeof this.data === 'undefined') {
|
||||
throw Error('no data secret available');
|
||||
}
|
||||
|
||||
const { list } = this.data;
|
||||
const totalItems = list?.length;
|
||||
debug('get full list of packages (%o) has been fetched', totalItems);
|
||||
return Promise.resolve(list);
|
||||
}
|
||||
|
||||
public getPackageStorage(packageName: string): IPackageStorage {
|
||||
const packageAccess = this.config.getMatchedPackagesSpec(packageName);
|
||||
const packageAccess = getMatchedPackagesSpec(packageName, this.config.packages);
|
||||
|
||||
const packagePath: string = this._getLocalStoragePath(
|
||||
packageAccess ? packageAccess.storage : undefined
|
||||
);
|
||||
debug('storage path selected: ', packagePath);
|
||||
|
||||
if (_.isString(packagePath) === false) {
|
||||
debug('the package %o has no storage defined ', packageName);
|
||||
return;
|
||||
}
|
||||
|
||||
const packageStoragePath: string = Path.join(
|
||||
Path.resolve(Path.dirname(this.config.config_path || ''), packagePath),
|
||||
const packageStoragePath: string = path.join(
|
||||
// FIXME: use getBaseStoragePath instead
|
||||
path.resolve(path.dirname(this.config.config_path || ''), packagePath),
|
||||
packageName
|
||||
);
|
||||
|
||||
@@ -224,31 +199,21 @@ class LocalDatabase extends TokenActions implements IPluginStorage<{}> {
|
||||
return new LocalDriver(packageStoragePath, this.logger);
|
||||
}
|
||||
|
||||
public clean(): void {
|
||||
this._sync();
|
||||
public async clean(): Promise<void> {
|
||||
await this._sync();
|
||||
}
|
||||
|
||||
private getTime(time: number, mtime: Date): number | Date {
|
||||
return time ? time : mtime;
|
||||
}
|
||||
|
||||
private _getCustomPackageLocalStorages(): object {
|
||||
const storages = {};
|
||||
|
||||
// add custom storage if exist
|
||||
if (this.config.storage) {
|
||||
storages[this.config.storage] = true;
|
||||
}
|
||||
|
||||
private _getCustomPackageLocalStorages(): Map<string, string> {
|
||||
const storages = new Map<string, string>();
|
||||
const { packages } = this.config;
|
||||
|
||||
if (packages) {
|
||||
const listPackagesConf = Object.keys(packages || {});
|
||||
|
||||
listPackagesConf.map((pkg) => {
|
||||
const storage = packages[pkg].storage;
|
||||
if (storage) {
|
||||
storages[storage] = false;
|
||||
Object.keys(packages || {}).map((pkg) => {
|
||||
const { storage } = packages[pkg];
|
||||
if (typeof storage === 'string') {
|
||||
const storagePath = path.join(this.getStoragePath(), storage);
|
||||
debug('add custom storage for %s on %s', storage, storagePath);
|
||||
storages.set(storage, storagePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -256,80 +221,59 @@ class LocalDatabase extends TokenActions implements IPluginStorage<{}> {
|
||||
return storages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncronize {create} database whether does not exist.
|
||||
* @return {Error|*}
|
||||
*/
|
||||
private _sync(): Error | null {
|
||||
private async _sync(): Promise<null> {
|
||||
debug('sync database started');
|
||||
|
||||
if (this.locked) {
|
||||
this.logger.error(
|
||||
'Database is locked, please check error message printed during startup to ' +
|
||||
'prevent data loss.'
|
||||
);
|
||||
return new Error(
|
||||
'Verdaccio database is locked, please contact your administrator to checkout ' +
|
||||
'logs during verdaccio startup.'
|
||||
);
|
||||
this.logger.error(ERROR_DB_LOCKED);
|
||||
throw new Error(ERROR_DB_LOCKED);
|
||||
}
|
||||
// Uses sync to prevent ugly race condition
|
||||
try {
|
||||
const folderName = Path.dirname(this.path);
|
||||
const folderName = path.dirname(this.path);
|
||||
debug('creating folder %o', folderName);
|
||||
fs.mkdirSync(folderName, { recursive: true });
|
||||
debug('sync folder %o created succeed', folderName);
|
||||
} catch (err: any) {
|
||||
debug('sync create folder has failed with error: %o', err);
|
||||
return null;
|
||||
await mkdirPromise(folderName, { recursive: true });
|
||||
debug('creating folder %o created succeed', folderName);
|
||||
} catch (err) {
|
||||
this.logger.error({ err }, 'sync create folder has failed with error: @{err}');
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
fs.writeFileSync(this.path, JSON.stringify(this.data));
|
||||
await writeFilePromise(this.path, JSON.stringify(this.data));
|
||||
debug('sync write succeed');
|
||||
|
||||
return null;
|
||||
} catch (err: any) {
|
||||
debug('sync failed %o', err);
|
||||
|
||||
return err;
|
||||
this.logger.error({ err }, 'sync database file failed: @{err}');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the right local storage location.
|
||||
* @param {String} path
|
||||
* @return {String}
|
||||
* @private
|
||||
*/
|
||||
private _getLocalStoragePath(storage: string | void): string {
|
||||
const globalConfigStorage = this.config ? this.config.storage : undefined;
|
||||
const globalConfigStorage = this.getStoragePath();
|
||||
if (_.isNil(globalConfigStorage)) {
|
||||
throw new Error('global storage is required for this plugin');
|
||||
this.logger.error('property storage in config.yaml is required for using this plugin');
|
||||
throw new Error('property storage in config.yaml is required for using this plugin');
|
||||
} else {
|
||||
if (_.isNil(storage) === false && _.isString(storage)) {
|
||||
return Path.join(globalConfigStorage as string, storage as string);
|
||||
if (typeof storage === 'string') {
|
||||
return path.join(globalConfigStorage as string, storage as string);
|
||||
}
|
||||
|
||||
return globalConfigStorage as string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch local packages.
|
||||
* @private
|
||||
* @return {Object}
|
||||
*/
|
||||
private async _fetchLocalPackages(): Promise<LocalStorage> {
|
||||
const list: StorageList = [];
|
||||
const emptyDatabase = { list, secret: '' };
|
||||
|
||||
try {
|
||||
return await loadPrivatePackages(this.path, this.logger);
|
||||
} catch (err: any) {
|
||||
// readFileSync is platform specific, macOS, Linux and Windows thrown an error
|
||||
// Only recreate if file not found to prevent data loss
|
||||
debug('error on fetch local packages %o', err);
|
||||
this.logger.warn(
|
||||
{ path: this.path },
|
||||
'no private database found, recreating new one on @{path}'
|
||||
);
|
||||
if (err.code !== noSuchFile) {
|
||||
this.locked = true;
|
||||
this.logger.error(
|
||||
@@ -338,7 +282,7 @@ class LocalDatabase extends TokenActions implements IPluginStorage<{}> {
|
||||
);
|
||||
}
|
||||
|
||||
return emptyDatabase;
|
||||
return { list: [], secret: '' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,14 @@ import { UploadTarball, ReadTarball } from '@verdaccio/streams';
|
||||
import { unlockFile, readFile } from '@verdaccio/file-locking';
|
||||
import { Callback, Logger, Package, ILocalPackageManager, IUploadTarball } from '@verdaccio/types';
|
||||
import { getCode, getInternalError, getNotFound, VerdaccioError } from '@verdaccio/commons-api';
|
||||
import { unlinkPromise, rmdirPromise, readFilePromise } from './fs';
|
||||
|
||||
export const fileExist = 'EEXISTS';
|
||||
export const noSuchFile = 'ENOENT';
|
||||
export const resourceNotAvailable = 'EAGAIN';
|
||||
export const pkgFileName = 'package.json';
|
||||
export const packageJSONFileName = 'package.json';
|
||||
|
||||
const debug = buildDebug('verdaccio:plugin:local-storage:fs');
|
||||
const debug = buildDebug('verdaccio:plugin:local-storage:local-fs');
|
||||
|
||||
export const fSError = function (message: string, code = 409): VerdaccioError {
|
||||
const err: VerdaccioError = getCode(code, message);
|
||||
@@ -86,7 +87,7 @@ export default class LocalFS implements ILocalFSPackageManager {
|
||||
transformPackage: Function,
|
||||
onEnd: Callback
|
||||
): void {
|
||||
this._lockAndReadJSON(pkgFileName, (err, json) => {
|
||||
this._lockAndReadJSON(packageJSONFileName, (err, json) => {
|
||||
let locked = false;
|
||||
const self = this;
|
||||
// callback that cleans up lock first
|
||||
@@ -95,7 +96,8 @@ export default class LocalFS implements ILocalFSPackageManager {
|
||||
const _args = arguments;
|
||||
|
||||
if (locked) {
|
||||
self._unlockJSON(pkgFileName, () => {
|
||||
debug('unlock %s', packageJSONFileName);
|
||||
self._unlockJSON(packageJSONFileName, () => {
|
||||
// ignore any error from the unlock
|
||||
if (lockError !== null) {
|
||||
debug('lock file: %o has failed with error %o', name, lockError);
|
||||
@@ -134,54 +136,50 @@ export default class LocalFS implements ILocalFSPackageManager {
|
||||
});
|
||||
}
|
||||
|
||||
public deletePackage(
|
||||
packageName: string,
|
||||
callback: (err: NodeJS.ErrnoException | null) => void
|
||||
): void {
|
||||
debug('delete a package %o', packageName);
|
||||
public async deletePackage(packageName: string): Promise<void> {
|
||||
debug('delete a file/package %o', packageName);
|
||||
|
||||
return fs.unlink(this._getStorage(packageName), callback);
|
||||
return await unlinkPromise(this._getStorage(packageName));
|
||||
}
|
||||
|
||||
public removePackage(callback: (err: NodeJS.ErrnoException | null) => void): void {
|
||||
debug('remove a package %o', this.path);
|
||||
public async removePackage(): Promise<void> {
|
||||
debug('remove a package folder %o', this.path);
|
||||
|
||||
fs.rmdir(this._getStorage('.'), callback);
|
||||
await rmdirPromise(this._getStorage('.'));
|
||||
}
|
||||
|
||||
public createPackage(name: string, value: Package, cb: Callback): void {
|
||||
debug('create a package %o', name);
|
||||
|
||||
this._createFile(this._getStorage(pkgFileName), this._convertToString(value), cb);
|
||||
this._createFile(this._getStorage(packageJSONFileName), this._convertToString(value), cb);
|
||||
}
|
||||
|
||||
public savePackage(name: string, value: Package, cb: Callback): void {
|
||||
debug('save a package %o', name);
|
||||
|
||||
this._writeFile(this._getStorage(pkgFileName), this._convertToString(value), cb);
|
||||
this._writeFile(this._getStorage(packageJSONFileName), this._convertToString(value), cb);
|
||||
}
|
||||
|
||||
public readPackage(name: string, cb: Callback): void {
|
||||
debug('read a package %o', name);
|
||||
|
||||
this._readStorageFile(this._getStorage(pkgFileName)).then(
|
||||
(res) => {
|
||||
this._readStorageFile(this._getStorage(packageJSONFileName))
|
||||
.then((res) => {
|
||||
try {
|
||||
const data: any = JSON.parse(res.toString('utf8'));
|
||||
|
||||
debug('read storage file %o has succeed', name);
|
||||
cb(null, data);
|
||||
} catch (err: any) {
|
||||
debug('parse storage file %o has failed with error %o', name, err);
|
||||
cb(err);
|
||||
debug('parse error');
|
||||
this.logger.error({ err, name }, 'error @{err.message} on parse @{name}');
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
debug('read storage file %o has failed with error %o', name, err);
|
||||
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.error({ err }, 'error on read storage file @{err.message}');
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public writeTarball(name: string): IUploadTarball {
|
||||
@@ -308,21 +306,15 @@ export default class LocalFS implements ILocalFSPackageManager {
|
||||
});
|
||||
}
|
||||
|
||||
private _readStorageFile(name: string): Promise<any> {
|
||||
return new Promise((resolve, reject): void => {
|
||||
private async _readStorageFile(name: string): Promise<any> {
|
||||
debug('reading the file: %o', name);
|
||||
try {
|
||||
debug('reading the file: %o', name);
|
||||
|
||||
fs.readFile(name, (err, data) => {
|
||||
if (err) {
|
||||
debug('error reading the file: %o with error %o', name, err);
|
||||
reject(err);
|
||||
} else {
|
||||
debug('read file %o succeed', name);
|
||||
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
return await readFilePromise(name);
|
||||
} catch (err: any) {
|
||||
debug('error reading the file: %o with error %o', name, err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private _convertToString(value: Package): string {
|
||||
@@ -366,7 +358,7 @@ export default class LocalFS implements ILocalFSPackageManager {
|
||||
|
||||
private _lockAndReadJSON(name: string, cb: Function): void {
|
||||
const fileName: string = this._getStorage(name);
|
||||
|
||||
debug('lock and read a file %o', fileName);
|
||||
readFile(
|
||||
fileName,
|
||||
{
|
||||
@@ -375,6 +367,7 @@ export default class LocalFS implements ILocalFSPackageManager {
|
||||
},
|
||||
(err, res) => {
|
||||
if (err) {
|
||||
this.logger.error({ err }, 'error on lock file @{err.message}');
|
||||
debug('error on lock and read json for file: %o', name);
|
||||
|
||||
return cb(err);
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import _ from 'lodash';
|
||||
import { LocalStorage, StorageList, Logger } from '@verdaccio/types';
|
||||
import { readFilePromise } from './read-file';
|
||||
import { readFilePromise } from './fs';
|
||||
|
||||
export async function loadPrivatePackages(path: string, logger: Logger): Promise<LocalStorage> {
|
||||
const list: StorageList = [];
|
||||
const emptyDatabase = { list, secret: '' };
|
||||
const data = await readFilePromise(path);
|
||||
|
||||
if (_.isNil(data)) {
|
||||
// readFileSync is platform specific, FreeBSD might return null
|
||||
// readFilePromise is platform specific, FreeBSD might return null
|
||||
return emptyDatabase;
|
||||
}
|
||||
|
||||
@@ -17,9 +16,11 @@ export async function loadPrivatePackages(path: string, logger: Logger): Promise
|
||||
db = JSON.parse(data);
|
||||
} catch (err: any) {
|
||||
logger.error(
|
||||
`Package database file corrupted (invalid JSON), please check the error` +
|
||||
` printed below.\nFile Path: ${path}`,
|
||||
err
|
||||
{
|
||||
err,
|
||||
path,
|
||||
},
|
||||
`Package database file corrupted (invalid JSON) @{err.message}, please check the error printed below.File Path: @{path}`
|
||||
);
|
||||
throw Error('Package database file corrupted (invalid JSON)');
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { promisify } from 'util';
|
||||
import fs from 'fs';
|
||||
|
||||
const readFile = promisify(fs.readFile);
|
||||
|
||||
export const readFilePromise = async (path) => {
|
||||
return await readFile(path, 'utf8');
|
||||
};
|
||||
@@ -84,4 +84,3 @@ export function _dbGenPath(
|
||||
path.resolve(path.dirname(config.config_path || ''), config.storage as string, dbName)
|
||||
);
|
||||
}
|
||||
/* eslint-enable no-async-promise-executor */
|
||||
|
||||
67
packages/core/local-storage/tests/dir-utils.test.ts
Normal file
67
packages/core/local-storage/tests/dir-utils.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { join } from 'path';
|
||||
import { getFolders, searchOnStorage } from '../src/dir-utils';
|
||||
|
||||
const mockFolder = join(__dirname, 'mockStorage');
|
||||
|
||||
const pathStorage1 = join(mockFolder, 'storage1');
|
||||
const pathStorage2 = join(mockFolder, 'storage2');
|
||||
const storages = new Map<string, string>();
|
||||
storages.set('storage1', pathStorage1);
|
||||
storages.set('storage2', pathStorage2);
|
||||
|
||||
test('getFolders storage 1', async () => {
|
||||
const files = await getFolders(join(pathStorage1, '@bar'));
|
||||
expect(files).toHaveLength(2);
|
||||
expect(files).toEqual(['pkg1', 'pkg2']);
|
||||
});
|
||||
|
||||
test('getFolders storage 2', async () => {
|
||||
const files = await getFolders(pathStorage2);
|
||||
expect(files).toHaveLength(1);
|
||||
expect(files).toEqual(['pkg4']);
|
||||
});
|
||||
|
||||
test('getFolders storage 2 with pattern', async () => {
|
||||
const files = await getFolders(pathStorage1, '*bar*');
|
||||
expect(files).toHaveLength(1);
|
||||
expect(files).toEqual(['@bar']);
|
||||
});
|
||||
|
||||
describe('searchOnFolders', () => {
|
||||
test('should find results', async () => {
|
||||
const packages = await searchOnStorage(mockFolder, storages);
|
||||
expect(packages).toHaveLength(9);
|
||||
expect(packages).toEqual([
|
||||
{
|
||||
name: '@foo/pkg1',
|
||||
scoped: '@foo',
|
||||
},
|
||||
{
|
||||
name: '@foo/pkg2',
|
||||
scoped: '@foo',
|
||||
},
|
||||
{ name: 'dont-include' },
|
||||
{
|
||||
name: 'pkg1',
|
||||
},
|
||||
|
||||
{
|
||||
name: 'pkg2',
|
||||
},
|
||||
{
|
||||
name: 'pkg3',
|
||||
},
|
||||
{
|
||||
name: '@bar/pkg1',
|
||||
scoped: '@bar',
|
||||
},
|
||||
{
|
||||
name: '@bar/pkg2',
|
||||
scoped: '@bar',
|
||||
},
|
||||
{
|
||||
name: 'pkg4',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,40 +1,54 @@
|
||||
/* eslint-disable jest/no-mocks-import */
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { dirSync } from 'tmp-promise';
|
||||
|
||||
import { assign } from 'lodash';
|
||||
import { IPluginStorage, PluginOptions } from '@verdaccio/types';
|
||||
|
||||
import LocalDatabase from '../src/local-database';
|
||||
import { ILocalFSPackageManager } from '../src/local-fs';
|
||||
import * as pkgUtils from '../src/pkg-utils';
|
||||
import LocalDatabase, { ERROR_DB_LOCKED } from '../src/local-database';
|
||||
|
||||
const mockWrite = jest.fn(() => Promise.resolve());
|
||||
const mockmkdir = jest.fn(() => Promise.resolve());
|
||||
const mockRead = jest.fn(() => Promise.resolve());
|
||||
|
||||
jest.mock('../src/fs', () => ({
|
||||
mkdirPromise: () => mockRead(),
|
||||
readFilePromise: () => mockmkdir(),
|
||||
writeFilePromise: () => mockWrite(),
|
||||
}));
|
||||
|
||||
// FIXME: remove this mocks imports
|
||||
import Config from './__mocks__/Config';
|
||||
import logger from './__mocks__/Logger';
|
||||
|
||||
// @ts-expect-error
|
||||
const optionsPlugin: PluginOptions<{}> = {
|
||||
logger,
|
||||
config: new Config(),
|
||||
};
|
||||
|
||||
let locaDatabase: IPluginStorage<{}>;
|
||||
let loadPrivatePackages;
|
||||
|
||||
describe('Local Database', () => {
|
||||
let tmpFolder;
|
||||
beforeEach(async () => {
|
||||
const writeMock = jest.spyOn(fs, 'writeFileSync').mockImplementation();
|
||||
loadPrivatePackages = jest
|
||||
.spyOn(pkgUtils, 'loadPrivatePackages')
|
||||
.mockResolvedValue({ list: [], secret: '' });
|
||||
locaDatabase = new LocalDatabase(optionsPlugin.config, optionsPlugin.logger);
|
||||
await (locaDatabase as LocalDatabase).init();
|
||||
(locaDatabase as LocalDatabase).clean();
|
||||
writeMock.mockClear();
|
||||
tmpFolder = dirSync({ unsafeCleanup: true });
|
||||
const tempFolder = path.join(tmpFolder.name, 'verdaccio-test.yaml');
|
||||
// @ts-expect-error
|
||||
locaDatabase = new LocalDatabase(
|
||||
// @ts-expect-error
|
||||
{
|
||||
storage: 'storage',
|
||||
config_path: tempFolder,
|
||||
checkSecretKey: () => 'fooX',
|
||||
},
|
||||
optionsPlugin.logger
|
||||
);
|
||||
await (locaDatabase as any).init();
|
||||
(locaDatabase as any).clean();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
// tmpFolder.removeCallback();
|
||||
});
|
||||
|
||||
test('should create an instance', () => {
|
||||
@@ -43,183 +57,188 @@ describe('Local Database', () => {
|
||||
});
|
||||
|
||||
test('should display log error if fails on load database', async () => {
|
||||
loadPrivatePackages.mockImplementation(() => {
|
||||
mockmkdir.mockImplementation(() => {
|
||||
throw Error();
|
||||
});
|
||||
const tmpFolder = dirSync({ unsafeCleanup: true });
|
||||
const tempFolder = path.join(tmpFolder.name, 'verdaccio-test.yaml');
|
||||
const instance = new LocalDatabase(
|
||||
// @ts-expect-error
|
||||
{
|
||||
storage: 'storage',
|
||||
config_path: tempFolder,
|
||||
},
|
||||
optionsPlugin.logger
|
||||
);
|
||||
|
||||
const instance = new LocalDatabase(optionsPlugin.config, optionsPlugin.logger);
|
||||
await instance.init();
|
||||
|
||||
await expect(instance.init()).rejects.toEqual(new Error(ERROR_DB_LOCKED));
|
||||
expect(optionsPlugin.logger.error).toHaveBeenCalled();
|
||||
expect(optionsPlugin.logger.error).toHaveBeenCalledTimes(2);
|
||||
tmpFolder.removeCallback();
|
||||
});
|
||||
|
||||
describe('should create set secret', () => {
|
||||
describe('should handle secret', () => {
|
||||
test('should create get secret', async () => {
|
||||
const secretKey = await locaDatabase.getSecret();
|
||||
|
||||
expect(secretKey).toBeDefined();
|
||||
expect(typeof secretKey === 'string').toBeTruthy();
|
||||
});
|
||||
|
||||
test('should create set secret', async () => {
|
||||
await locaDatabase.setSecret(optionsPlugin.config.checkSecretKey(''));
|
||||
|
||||
expect(optionsPlugin.config.secret).toBeDefined();
|
||||
expect(typeof optionsPlugin.config.secret === 'string').toBeTruthy();
|
||||
|
||||
await locaDatabase.setSecret('foooo');
|
||||
const fetchedSecretKey = await locaDatabase.getSecret();
|
||||
expect(optionsPlugin.config.secret).toBe(fetchedSecretKey);
|
||||
expect('foooo').toBe(fetchedSecretKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPackageStorage', () => {
|
||||
test('should get default storage', () => {
|
||||
const pkgName = 'someRandomePackage';
|
||||
const storage = locaDatabase.getPackageStorage(pkgName);
|
||||
expect(storage).toBeDefined();
|
||||
test.todo('write tarball');
|
||||
test.todo('read tarball');
|
||||
|
||||
if (storage) {
|
||||
const storagePath = path.normalize((storage as ILocalFSPackageManager).path).toLowerCase();
|
||||
expect(storagePath).toBe(
|
||||
path
|
||||
.normalize(
|
||||
path.join(__dirname, '__fixtures__', optionsPlugin.config.storage || '', pkgName)
|
||||
)
|
||||
.toLowerCase()
|
||||
);
|
||||
}
|
||||
});
|
||||
// describe('getPackageStorage', () => {
|
||||
// test('should get default storage', () => {
|
||||
// const pkgName = 'someRandomePackage';
|
||||
// const storage = locaDatabase.getPackageStorage(pkgName);
|
||||
// expect(storage).toBeDefined();
|
||||
|
||||
test('should use custom storage', () => {
|
||||
const pkgName = 'local-private-custom-storage';
|
||||
const storage = locaDatabase.getPackageStorage(pkgName);
|
||||
// if (storage) {
|
||||
// const storagePath = path.normalize((storage as ILocalFSPackageManager).path).toLowerCase();
|
||||
// expect(storagePath).toBe(
|
||||
// path
|
||||
// .normalize(
|
||||
// path.join(__dirname, '__fixtures__', optionsPlugin.config.storage || '', pkgName)
|
||||
// )
|
||||
// .toLowerCase()
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
|
||||
expect(storage).toBeDefined();
|
||||
// test('should use custom storage', () => {
|
||||
// const pkgName = 'local-private-custom-storage';
|
||||
// const storage = locaDatabase.getPackageStorage(pkgName);
|
||||
|
||||
if (storage) {
|
||||
const storagePath = path.normalize((storage as ILocalFSPackageManager).path).toLowerCase();
|
||||
expect(storagePath).toBe(
|
||||
path
|
||||
.normalize(
|
||||
path.join(
|
||||
__dirname,
|
||||
'__fixtures__',
|
||||
optionsPlugin.config.storage || '',
|
||||
'private_folder',
|
||||
pkgName
|
||||
)
|
||||
)
|
||||
.toLowerCase()
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
// expect(storage).toBeDefined();
|
||||
|
||||
describe('Database CRUD', () => {
|
||||
test('should add an item to database', (done) => {
|
||||
const pgkName = 'jquery';
|
||||
locaDatabase.get((err, data) => {
|
||||
expect(err).toBeNull();
|
||||
expect(data).toHaveLength(0);
|
||||
// if (storage) {
|
||||
// const storagePath = path.normalize((storage as ILocalFSPackageManager).path).toLowerCase();
|
||||
// expect(storagePath).toBe(
|
||||
// path
|
||||
// .normalize(
|
||||
// path.join(
|
||||
// __dirname,
|
||||
// '__fixtures__',
|
||||
// optionsPlugin.config.storage || '',
|
||||
// 'private_folder',
|
||||
// pkgName
|
||||
// )
|
||||
// )
|
||||
// .toLowerCase()
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
locaDatabase.add(pgkName, (err) => {
|
||||
expect(err).toBeNull();
|
||||
locaDatabase.get((err, data) => {
|
||||
expect(err).toBeNull();
|
||||
expect(data).toHaveLength(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
// describe('Database CRUD', () => {
|
||||
// test('should add an item to database', (done) => {
|
||||
// const pgkName = 'jquery';
|
||||
// locaDatabase.get((err, data) => {
|
||||
// expect(err).toBeNull();
|
||||
// expect(data).toHaveLength(0);
|
||||
|
||||
test('should remove an item to database', (done) => {
|
||||
const pgkName = 'jquery';
|
||||
locaDatabase.get((err, data) => {
|
||||
expect(err).toBeNull();
|
||||
expect(data).toHaveLength(0);
|
||||
locaDatabase.add(pgkName, (err) => {
|
||||
expect(err).toBeNull();
|
||||
locaDatabase.remove(pgkName, (err) => {
|
||||
expect(err).toBeNull();
|
||||
locaDatabase.get((err, data) => {
|
||||
expect(err).toBeNull();
|
||||
expect(data).toHaveLength(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
// locaDatabase.add(pgkName, (err) => {
|
||||
// expect(err).toBeNull();
|
||||
// locaDatabase.get((err, data) => {
|
||||
// expect(err).toBeNull();
|
||||
// expect(data).toHaveLength(1);
|
||||
// done();
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('search', () => {
|
||||
const onPackageMock = jest.fn((item, cb) => cb());
|
||||
const validatorMock = jest.fn(() => true);
|
||||
const callSearch = (db, numberTimesCalled, cb): void => {
|
||||
db.search(
|
||||
onPackageMock,
|
||||
function onEnd() {
|
||||
expect(onPackageMock).toHaveBeenCalledTimes(numberTimesCalled);
|
||||
expect(validatorMock).toHaveBeenCalledTimes(numberTimesCalled);
|
||||
cb();
|
||||
},
|
||||
validatorMock
|
||||
);
|
||||
};
|
||||
// test('should remove an item to database', (done) => {
|
||||
// const pgkName = 'jquery';
|
||||
// locaDatabase.get((err, data) => {
|
||||
// expect(err).toBeNull();
|
||||
// expect(data).toHaveLength(0);
|
||||
// locaDatabase.add(pgkName, (err) => {
|
||||
// expect(err).toBeNull();
|
||||
// locaDatabase.remove(pgkName, (err) => {
|
||||
// expect(err).toBeNull();
|
||||
// locaDatabase.get((err, data) => {
|
||||
// expect(err).toBeNull();
|
||||
// expect(data).toHaveLength(0);
|
||||
// done();
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
test('should find scoped packages', (done) => {
|
||||
const scopedPackages = ['@pkg1/test'];
|
||||
const stats = { mtime: new Date() };
|
||||
jest.spyOn(fs, 'stat').mockImplementation((_, cb) => cb(null, stats as fs.Stats));
|
||||
jest
|
||||
.spyOn(fs, 'readdir')
|
||||
.mockImplementation((storePath, cb) =>
|
||||
cb(null, storePath.match('test-storage') ? scopedPackages : [])
|
||||
);
|
||||
// describe('search', () => {
|
||||
// const onPackageMock = jest.fn((item, cb) => cb());
|
||||
// const validatorMock = jest.fn(() => true);
|
||||
// const callSearch = (db, numberTimesCalled, cb): void => {
|
||||
// db.search(
|
||||
// onPackageMock,
|
||||
// function onEnd() {
|
||||
// expect(onPackageMock).toHaveBeenCalledTimes(numberTimesCalled);
|
||||
// cb();
|
||||
// },
|
||||
// validatorMock
|
||||
// );
|
||||
// };
|
||||
|
||||
callSearch(locaDatabase, 1, done);
|
||||
});
|
||||
// test('should find scoped packages', (done) => {
|
||||
// const scopedPackages = ['@pkg1/test'];
|
||||
// const stats = { mtime: new Date() };
|
||||
// jest.spyOn(fs, 'stat').mockImplementation((_, cb) => cb(null, stats as fs.Stats));
|
||||
// jest
|
||||
// .spyOn(fs, 'readdir')
|
||||
// .mockImplementation((storePath, cb) =>
|
||||
// cb(null, storePath.match('test-storage') ? scopedPackages : [])
|
||||
// );
|
||||
|
||||
test('should find non scoped packages', (done) => {
|
||||
const nonScopedPackages = ['pkg1', 'pkg2'];
|
||||
const stats = { mtime: new Date() };
|
||||
jest.spyOn(fs, 'stat').mockImplementation((_, cb) => cb(null, stats as fs.Stats));
|
||||
jest
|
||||
.spyOn(fs, 'readdir')
|
||||
.mockImplementation((storePath, cb) =>
|
||||
cb(null, storePath.match('test-storage') ? nonScopedPackages : [])
|
||||
);
|
||||
// callSearch(locaDatabase, 1, done);
|
||||
// });
|
||||
|
||||
const db = new LocalDatabase(
|
||||
assign({}, optionsPlugin.config, {
|
||||
// clean up this, it creates noise
|
||||
packages: {},
|
||||
}),
|
||||
optionsPlugin.logger
|
||||
);
|
||||
// test('should find non scoped packages', (done) => {
|
||||
// const nonScopedPackages = ['pkg1', 'pkg2'];
|
||||
// const stats = { mtime: new Date() };
|
||||
// jest.spyOn(fs, 'stat').mockImplementation((_, cb) => cb(null, stats as fs.Stats));
|
||||
// jest
|
||||
// .spyOn(fs, 'readdir')
|
||||
// .mockImplementation((storePath, cb) =>
|
||||
// cb(null, storePath.match('test-storage') ? nonScopedPackages : [])
|
||||
// );
|
||||
|
||||
callSearch(db, 2, done);
|
||||
});
|
||||
// const db = new LocalDatabase(
|
||||
// assign({}, optionsPlugin.config, {
|
||||
// // clean up this, it creates noise
|
||||
// packages: {},
|
||||
// }),
|
||||
// optionsPlugin.logger
|
||||
// );
|
||||
|
||||
test('should fails on read the storage', (done) => {
|
||||
const spyInstance = jest
|
||||
.spyOn(fs, 'readdir')
|
||||
.mockImplementation((_, cb) => cb(Error('fails'), null));
|
||||
// callSearch(db, 2, done);
|
||||
// });
|
||||
|
||||
const db = new LocalDatabase(
|
||||
assign({}, optionsPlugin.config, {
|
||||
// clean up this, it creates noise
|
||||
packages: {},
|
||||
}),
|
||||
optionsPlugin.logger
|
||||
);
|
||||
// test('should fails on read the storage', (done) => {
|
||||
// const spyInstance = jest
|
||||
// .spyOn(fs, 'readdir')
|
||||
// .mockImplementation((_, cb) => cb(Error('fails'), null));
|
||||
|
||||
callSearch(db, 0, done);
|
||||
spyInstance.mockRestore();
|
||||
});
|
||||
});
|
||||
// const db = new LocalDatabase(
|
||||
// assign({}, optionsPlugin.config, {
|
||||
// // clean up this, it creates noise
|
||||
// packages: {},
|
||||
// }),
|
||||
// optionsPlugin.logger
|
||||
// );
|
||||
|
||||
// callSearch(db, 0, done);
|
||||
// spyInstance.mockRestore();
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
||||
// NOTE: Crear test para verificar que se crea el storage file
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { dirSync } from 'tmp-promise';
|
||||
|
||||
import rm from 'rmdir-sync';
|
||||
import { Logger, ILocalPackageManager, Package } from '@verdaccio/types';
|
||||
import { ILocalPackageManager, Package } from '@verdaccio/types';
|
||||
|
||||
import LocalDriver, { fileExist, fSError, noSuchFile, resourceNotAvailable } from '../src/local-fs';
|
||||
|
||||
// FIXME: remove this mocks imports
|
||||
// eslint-disable-next-line jest/no-mocks-import
|
||||
import logger from './__mocks__/Logger';
|
||||
import pkg from './__fixtures__/pkg';
|
||||
|
||||
let localTempStorage: string;
|
||||
const pkgFileName = 'package.json';
|
||||
|
||||
const logger: Logger = {
|
||||
error: () => jest.fn(),
|
||||
info: () => jest.fn(),
|
||||
debug: () => jest.fn(),
|
||||
warn: () => jest.fn(),
|
||||
child: () => jest.fn(),
|
||||
http: () => jest.fn(),
|
||||
trace: () => jest.fn(),
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
localTempStorage = path.join('./_storage');
|
||||
rm(localTempStorage);
|
||||
});
|
||||
|
||||
describe('Local FS test', () => {
|
||||
let tmpFolder;
|
||||
beforeAll(() => {
|
||||
tmpFolder = dirSync({ unsafeCleanup: true });
|
||||
localTempStorage = path.join(tmpFolder.name, './_storage');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// tmpFolder.removeCallback();
|
||||
});
|
||||
|
||||
describe('savePackage() group', () => {
|
||||
test('savePackage()', (done) => {
|
||||
const data = {};
|
||||
@@ -98,14 +96,11 @@ describe('Local FS test', () => {
|
||||
});
|
||||
|
||||
describe('deletePackage() group', () => {
|
||||
test('deletePackage()', (done) => {
|
||||
test('deletePackage()', async () => {
|
||||
const localFs = new LocalDriver(path.join(localTempStorage, 'createPackage'), logger);
|
||||
|
||||
// verdaccio removes the package.json instead the package name
|
||||
localFs.deletePackage('package.json', (err) => {
|
||||
expect(err).toBeNull();
|
||||
done();
|
||||
});
|
||||
await localFs.deletePackage('package.json');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -115,32 +110,26 @@ describe('Local FS test', () => {
|
||||
fs.mkdirSync(path.join(localTempStorage, '_toDelete'), { recursive: true });
|
||||
});
|
||||
|
||||
test('removePackage() success', (done) => {
|
||||
test('should successfully remove the package', async () => {
|
||||
const localFs: ILocalPackageManager = new LocalDriver(
|
||||
path.join(localTempStorage, '_toDelete'),
|
||||
logger
|
||||
);
|
||||
localFs.removePackage((error) => {
|
||||
expect(error).toBeNull();
|
||||
done();
|
||||
});
|
||||
|
||||
await expect(localFs.removePackage()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test('removePackage() fails', (done) => {
|
||||
test('removePackage() fails', async () => {
|
||||
const localFs: ILocalPackageManager = new LocalDriver(
|
||||
path.join(localTempStorage, '_toDelete_fake'),
|
||||
logger
|
||||
);
|
||||
localFs.removePackage((error) => {
|
||||
expect(error).toBeTruthy();
|
||||
expect(error.code).toBe('ENOENT');
|
||||
done();
|
||||
});
|
||||
await expect(localFs.removePackage()).rejects.toThrow(/ENOENT/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readTarball() group', () => {
|
||||
test('readTarball() success', (done) => {
|
||||
describe('readTarball', () => {
|
||||
test('should read tarball successfully', (done) => {
|
||||
const localFs: ILocalPackageManager = new LocalDriver(
|
||||
path.join(__dirname, '__fixtures__/readme-test'),
|
||||
logger
|
||||
@@ -178,83 +167,6 @@ describe('Local FS test', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeTarball() group', () => {
|
||||
beforeEach(() => {
|
||||
const writeTarballFolder: string = path.join(localTempStorage, '_writeTarball');
|
||||
rm(writeTarballFolder);
|
||||
fs.mkdirSync(writeTarballFolder, { recursive: true });
|
||||
});
|
||||
|
||||
test('writeTarball() success', (done) => {
|
||||
const newFileName = 'new-readme-0.0.0.tgz';
|
||||
const readmeStorage: ILocalPackageManager = new LocalDriver(
|
||||
path.join(__dirname, '__fixtures__/readme-test'),
|
||||
logger
|
||||
);
|
||||
const writeStorage: ILocalPackageManager = new LocalDriver(
|
||||
path.join(__dirname, '../_storage'),
|
||||
logger
|
||||
);
|
||||
const readTarballStream = readmeStorage.readTarball('test-readme-0.0.0.tgz');
|
||||
const writeTarballStream = writeStorage.writeTarball(newFileName);
|
||||
|
||||
writeTarballStream.on('error', function (err) {
|
||||
expect(err).toBeNull();
|
||||
done();
|
||||
});
|
||||
|
||||
writeTarballStream.on('success', function () {
|
||||
const fileLocation: string = path.join(__dirname, '../_storage', newFileName);
|
||||
|
||||
expect(fs.existsSync(fileLocation)).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
readTarballStream.on('end', function () {
|
||||
writeTarballStream.done();
|
||||
});
|
||||
|
||||
writeTarballStream.on('end', function () {
|
||||
done();
|
||||
});
|
||||
|
||||
writeTarballStream.on('data', function (data) {
|
||||
expect(data).toBeDefined();
|
||||
});
|
||||
|
||||
readTarballStream.on('error', function (err) {
|
||||
expect(err).toBeNull();
|
||||
done();
|
||||
});
|
||||
|
||||
readTarballStream.pipe(writeTarballStream);
|
||||
});
|
||||
|
||||
test('writeTarball() abort', (done) => {
|
||||
const newFileLocationFolder: string = path.join(localTempStorage, '_writeTarball');
|
||||
const newFileName = 'new-readme-abort-0.0.0.tgz';
|
||||
const readmeStorage: ILocalPackageManager = new LocalDriver(
|
||||
path.join(__dirname, '__fixtures__/readme-test'),
|
||||
logger
|
||||
);
|
||||
const writeStorage: ILocalPackageManager = new LocalDriver(newFileLocationFolder, logger);
|
||||
const readTarballStream = readmeStorage.readTarball('test-readme-0.0.0.tgz');
|
||||
const writeTarballStream = writeStorage.writeTarball(newFileName);
|
||||
|
||||
writeTarballStream.on('error', function (err) {
|
||||
expect(err).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
|
||||
writeTarballStream.on('data', function (data) {
|
||||
expect(data).toBeDefined();
|
||||
writeTarballStream.abort();
|
||||
});
|
||||
|
||||
readTarballStream.pipe(writeTarballStream);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePackage() group', () => {
|
||||
const updateHandler = jest.fn((name, cb) => {
|
||||
cb();
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "@foo/pkg1"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "@foo/pkg2"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "pkg3"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "pkg1"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user