Compare commits
38 Commits
@verdaccio
...
@verdaccio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd7947adbc | ||
|
|
5fed1955a9 | ||
|
|
9dbf73e955 | ||
|
|
2c594910d8 | ||
|
|
90818700a3 | ||
|
|
7af1e6cc84 | ||
|
|
757bded72e | ||
|
|
1b9bf35c87 | ||
|
|
eb2afc4d6d | ||
|
|
5be013a059 | ||
|
|
8d6d6097c6 | ||
|
|
531289f59d | ||
|
|
5e784d1188 | ||
|
|
7dde848d0c | ||
|
|
71874de027 | ||
|
|
f13dacef9c | ||
|
|
761f1696f2 | ||
|
|
f412e8f8d6 | ||
|
|
f5fd7bf5bf | ||
|
|
3d26a8190c | ||
|
|
e50410a875 | ||
|
|
60d7b35d88 | ||
|
|
1042f9bf76 | ||
|
|
e10637f0f3 | ||
|
|
459b6fa72b | ||
|
|
10868ed434 | ||
|
|
ada8165f98 | ||
|
|
57755f31ba | ||
|
|
9e29bf8890 | ||
|
|
e2a67bafbe | ||
|
|
7c9561b019 | ||
|
|
ed3677a5b6 | ||
|
|
055544238a | ||
|
|
4937dba06e | ||
|
|
3e65791564 | ||
|
|
cf1b6cdb04 | ||
|
|
ca86082e08 | ||
|
|
ef60e83d6c |
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/olive-candles-speak.md
Normal file
5
.changeset/olive-candles-speak.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@verdaccio/ui-theme': minor
|
||||
---
|
||||
|
||||
feat: integrate rematch for ui state storage
|
||||
@@ -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,6 +66,7 @@
|
||||
"many-vans-care",
|
||||
"modern-spies-tell",
|
||||
"neat-toes-report",
|
||||
"olive-candles-speak",
|
||||
"perfect-emus-clean",
|
||||
"perfect-kangaroos-agree",
|
||||
"plenty-news-remember",
|
||||
@@ -72,6 +75,7 @@
|
||||
"pretty-hounds-tap",
|
||||
"proud-jeans-walk",
|
||||
"red-chefs-float",
|
||||
"shaggy-carrots-unite",
|
||||
"shiny-chefs-heal",
|
||||
"smart-apricots-kneel",
|
||||
"spicy-frogs-press",
|
||||
|
||||
5
.changeset/shaggy-carrots-unite.md
Normal file
5
.changeset/shaggy-carrots-unite.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@verdaccio/logger': patch
|
||||
---
|
||||
|
||||
do not show deprecation warning on default logger config
|
||||
@@ -9,3 +9,5 @@ static/
|
||||
website/
|
||||
wiki/
|
||||
dist/
|
||||
docs/
|
||||
test/functional/store/*
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -7,7 +7,7 @@ contact_links:
|
||||
url: https://github.com/verdaccio/verdaccio/security/policy
|
||||
about: I want to report a security vulnerability
|
||||
- name: Chat 🏘
|
||||
url: http://chat.verdaccio.org
|
||||
url: https://discord.gg/7qWJxBf
|
||||
about: For a quick question you should do it through our community chat
|
||||
- name: User Interface Report 👩🏼🎨👨🏼🎨
|
||||
url: https://github.com/verdaccio/ui/issues/new/choose
|
||||
|
||||
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.23
|
||||
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.23
|
||||
name: Benchmark hyperfine
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
46
.github/workflows/ci.yml
vendored
46
.github/workflows/ci.yml
vendored
@@ -83,6 +83,7 @@ jobs:
|
||||
run: pnpm recursive install --frozen-lockfile --ignore-scripts
|
||||
- name: Lint
|
||||
run: pnpm format:check
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: build
|
||||
@@ -101,6 +102,12 @@ jobs:
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
run: pnpm recursive install --frozen-lockfile --ignore-scripts
|
||||
- name: crowdin download
|
||||
env:
|
||||
CROWDIN_VERDACCIO_PROJECT_ID: ${{ secrets.CROWDIN_VERDACCIO_PROJECT_ID }}
|
||||
CROWDIN_VERDACCIO_API_KEY: ${{ secrets.CROWDIN_VERDACCIO_API_KEY }}
|
||||
CONTEXT: production
|
||||
run: pnpm crowdin:download
|
||||
- name: build
|
||||
run: pnpm build
|
||||
- name: tar packages
|
||||
@@ -116,6 +123,7 @@ jobs:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
## Node 16 breaks UI test, jest issue
|
||||
node_version: [14]
|
||||
name: ${{ matrix.os }} / Node ${{ matrix.node_version }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -165,8 +173,8 @@ jobs:
|
||||
run: pnpm recursive install --frozen-lockfile
|
||||
- name: Test UI
|
||||
run: pnpm test:e2e:ui
|
||||
env:
|
||||
DEBUG: verdaccio:e2e*
|
||||
# env:
|
||||
# DEBUG: verdaccio:e2e*
|
||||
ci-e2e-cli:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
@@ -192,6 +200,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
|
||||
@@ -215,3 +225,35 @@ jobs:
|
||||
run: pnpm build
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
sync-translations:
|
||||
needs: [ci-e2e-cli, ci-e2e-ui, test-windows, test]
|
||||
runs-on: ubuntu-latest
|
||||
name: synchronize translations
|
||||
if: (github.event_name == 'push' && github.ref == 'refs/heads/master') || github.event_name == 'workflow_dispatch'
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: verdaccio-artifact
|
||||
- name: untar packages
|
||||
run: tar -xzvf pkg.tar.gz -C ${{ github.workspace }}/packages
|
||||
- name: Install pnpm
|
||||
run: npm i pnpm@6.10.3 -g
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install
|
||||
## we need scripts, pupetter downloads aditional content
|
||||
run: pnpm recursive install --frozen-lockfile
|
||||
- name: generate website translations
|
||||
run: pnpm write-translations --filter ...@verdaccio/website
|
||||
- name: sync
|
||||
env:
|
||||
CROWDIN_VERDACCIO_PROJECT_ID: ${{ secrets.CROWDIN_VERDACCIO_PROJECT_ID }}
|
||||
CROWDIN_VERDACCIO_API_KEY: ${{ secrets.CROWDIN_VERDACCIO_API_KEY }}
|
||||
CONTEXT: production
|
||||
run: pnpm crowdin:sync
|
||||
|
||||
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:
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -34,6 +34,9 @@ packages/standalone/dist/
|
||||
|
||||
## ui
|
||||
packages/plugins/ui-theme/static
|
||||
/packages/plugins/ui-theme/src/i18n/download_translations/
|
||||
!/packages/plugins/ui-theme/src/i18n/crowdin/ui.json
|
||||
|
||||
|
||||
# CI Pnpm cache
|
||||
.pnpm-store/
|
||||
@@ -43,3 +46,7 @@ api-results.json
|
||||
hyper-results.json
|
||||
hyper-results*.json
|
||||
api-results*.json
|
||||
|
||||
#docs
|
||||
./api
|
||||
packages/core/core/docs
|
||||
|
||||
5
.husky/pre-commit
Executable file
5
.husky/pre-commit
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
#./node_modules/.bin/lint-staged
|
||||
npm run husky:pre-commit
|
||||
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,
|
||||
|
||||
@@ -19,6 +19,7 @@ guidelines for you:
|
||||
- [What's is not considered a bug?](#whats-is-not-considered-a-bug)
|
||||
- [Issue Search](#issue-search)
|
||||
- [Chat](#chat)
|
||||
- [Translations](#translations)
|
||||
- [Request Features](#request-features)
|
||||
- [Contributing Guidelines](#contributing-guidelines)
|
||||
- [Submitting a Pull Request](#submitting-a-pull-request)
|
||||
@@ -190,10 +191,28 @@ affecting multiple people.
|
||||
|
||||
### Chat
|
||||
|
||||
Questions can be asked via [Discord](http://chat.verdaccio.org/)
|
||||
Questions can be asked via [Discord](https://discord.gg/7qWJxBf)
|
||||
|
||||
**Please use the `#help` channel.**
|
||||
|
||||
## Translations
|
||||
|
||||
All translations are provided by the `crowdin` platform:
|
||||
[https://translate.verdaccio.org/](https://translate.verdaccio.org/)
|
||||
|
||||
If you want to contribute by adding translations, create an account (GitHub could be used as fast alternative), in the platform you can contribute to two areas, the website or improve User Interface translations.
|
||||
|
||||
If a language is not listed, ask for it in the [Discord](https://discord.gg/7qWJxBf) channel #contribute channel.
|
||||
|
||||
For adding a new **language** on the UI follow these steps:
|
||||
|
||||
1. Ensure the **language** has been enabled, must be visible in the `crowdin` platform.
|
||||
2. Find in the explorer the file `en.US.json` in the path `packages/plugins/ui-theme/src/i18n/crowdin/ui.json` and complete the translations, **not need to find approval on this**.
|
||||
3. Into the project, add a new field into `packages/plugins/ui-theme/src/i18n/crowdin/ui.json` file, in the section `lng`, the new language, eg: `{ lng: {korean:"Korean"}}`. (This file is English based, once the PR has been merged, this string will be available in crowdin for translate to the targeted language).
|
||||
4. Add the language, [flag icon](https://www.npmjs.com/package/country-flag-icons), and the menu key fort he new language eg: `menuKey: 'lng.korean'` to the file `packages/plugins/ui-theme/src/i18n/enabledLanguages.ts`.
|
||||
5. For local testing, read `packages/plugins/ui-theme/src/i18n/ABOUT_TRANSLATIONS.md`.
|
||||
6. Add a `changeset` file, see more info below.
|
||||
|
||||
## Request Features
|
||||
|
||||
New feature requests are welcome. Analyse whether the idea fits within scope of
|
||||
|
||||
39
README.md
39
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=bRKZbrlQqLY&t=16s&ab_channel=ReactFinland)
|
||||
|
||||
You might want to check out as well our previous talks:
|
||||
|
||||
@@ -186,7 +190,20 @@ Verdaccio aims to support all features of a standard npm client that make sense
|
||||
|
||||
If you want to report a security vulnerability, please follow the steps which we have defined for you in our [security policy](https://github.com/verdaccio/verdaccio/security/policy).
|
||||
|
||||
## Core Team
|
||||
## Special Thanks
|
||||
|
||||
Thanks to the following companies to help us to achieve our goals providing free open source licenses. Every company provides enough resources to move this project forward.
|
||||
|
||||
| Company | Logo | License |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| JetBrains | [](https://www.jetbrains.com/) | JetBrains provides licenses for products for active maintainers, renewable yearly |
|
||||
| Crowdin | [](https://crowdin.com/) | Crowdin provides platform for translations |
|
||||
| BrowserStack | [](https://www.browserstack.com/) | BrowserStack provides plan to run End to End testing for the UI |
|
||||
| Netlify | [](https://www.netlify.com/) | Netlify provides pro plan for website deployment |
|
||||
| Algolia | [](https://algolia.com/) | Algolia provides search services for the website |
|
||||
| Docker | [](https://www.docker.com/community/open-source/application) | Docker offers unlimited pulls and unlimited egress to any and all users |
|
||||
|
||||
## Maintainers
|
||||
|
||||
| [Juan Picado](https://github.com/juanpicado) | [Ayush Sharma](https://github.com/ayusharma) | [Sergio Hg](https://github.com/sergiohgz) |
|
||||
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
|
||||
@@ -240,18 +257,6 @@ Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com
|
||||
|
||||
[](https://opencollective.com/verdaccio#backers)
|
||||
|
||||
## Special Thanks
|
||||
|
||||
Thanks to the following companies to help us to achieve our goals providing free open source licenses.
|
||||
|
||||
[](https://www.jetbrains.com/)
|
||||
[](https://crowdin.com/)
|
||||
[](https://www.browserstack.com/)
|
||||
[](https://www.netlify.com/)
|
||||
[](https://algolia.com/)
|
||||
|
||||
Verdaccio also is part of to the [Docker Open Source Program](https://www.docker.com/blog/expanded-support-for-open-source-software-projects/).
|
||||
|
||||
## Contributors
|
||||
|
||||
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
|
||||
@@ -266,7 +271,7 @@ If you have any issue you can try the following options, do no desist to ask or
|
||||
- [Donations](https://github.com/sponsors/verdaccio)
|
||||
- [Reporting an issue](https://github.com/verdaccio/verdaccio/issues/new/choose)
|
||||
- [Running discussions](https://github.com/verdaccio/verdaccio/issues?q=is%3Aissue+is%3Aopen+label%3Adiscuss)
|
||||
- [Chat](http://chat.verdaccio.org/)
|
||||
- [Chat](https://discord.gg/7qWJxBf)
|
||||
- [Logos](https://verdaccio.org/docs/en/logo)
|
||||
- [Docker Examples](https://github.com/verdaccio/verdaccio/tree/master/docker-examples)
|
||||
- [FAQ](https://github.com/verdaccio/verdaccio/discussions/categories/q-a)
|
||||
|
||||
@@ -5,6 +5,10 @@ preserve_hierarchy: true
|
||||
|
||||
files:
|
||||
[
|
||||
{
|
||||
source: 'packages/plugins/ui-theme/src/i18n/crowdin/**/*',
|
||||
translation: '/packages/plugins/ui-theme/src/i18n/download_translations/%locale%/**/%original_file_name%',
|
||||
},
|
||||
{
|
||||
source: '/website/i18n/en/**/*',
|
||||
translation: '/website/i18n/%locale%/**/%original_file_name%',
|
||||
@@ -13,4 +17,4 @@ files:
|
||||
source: '/website/docs/**/*',
|
||||
translation: '/website/i18n/%locale%/docusaurus-plugin-content-docs/current/**/%original_file_name%',
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
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.
|
||||
|
||||
71
package.json
71
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,37 +31,27 @@
|
||||
"@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",
|
||||
"@types/react": "17.0.19",
|
||||
"@types/react-autosuggest": "10.1.5",
|
||||
"@types/react-dom": "17.0.9",
|
||||
"@types/react-helmet": "6.1.2",
|
||||
"@types/react-router-dom": "5.1.8",
|
||||
"@types/react-virtualized": "9.21.13",
|
||||
"@types/request": "2.48.7",
|
||||
"@types/semver": "7.3.8",
|
||||
"@types/supertest": "2.0.11",
|
||||
@@ -80,32 +70,21 @@
|
||||
"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",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-google": "0.14.0",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"eslint-plugin-babel": "5.3.1",
|
||||
"eslint-plugin-import": "2.24.2",
|
||||
"eslint-plugin-jest": "24.4.0",
|
||||
"eslint-plugin-jsx-a11y": "6.4.1",
|
||||
"eslint-plugin-prettier": "4.0.0",
|
||||
"eslint-plugin-react": "7.25.1",
|
||||
"eslint-plugin-react-hooks": "4.2.0",
|
||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||
"eslint-plugin-verdaccio": "10.0.0",
|
||||
"fs-extra": "10.0.0",
|
||||
"husky": "4.3.5",
|
||||
"husky": "7.0.2",
|
||||
"in-publish": "2.0.1",
|
||||
"jest": "27.1.0",
|
||||
"jest-environment-jsdom": "27.1.0",
|
||||
"jest-environment-jsdom-global": "3.0.0",
|
||||
"jest-environment-node": "27.1.0",
|
||||
"jest-fetch-mock": "3.0.3",
|
||||
"jest-junit": "12.2.0",
|
||||
"kleur": "3.0.3",
|
||||
"lint-staged": "11.1.2",
|
||||
@@ -127,12 +106,13 @@
|
||||
"verdaccio-memory": "workspace:*"
|
||||
},
|
||||
"scripts": {
|
||||
"husky:pre-commit": "lint-staged",
|
||||
"clean": "pnpm recursive run clean",
|
||||
"build": "pnpm recursive run build --filter=!@verdaccio/website",
|
||||
"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",
|
||||
@@ -142,12 +122,12 @@
|
||||
"benchmark:submit": "pnpm ts-node ./scripts/submit-metrics.ts",
|
||||
"start:watch": "concurrently --kill-others \"pnpm _build:watch\" \"pnpm _start:server\" \"pnpm _debug:reload\"",
|
||||
"_build:watch": "pnpm run --parallel watch --filter ./packages",
|
||||
"_start:server": "node packages/verdaccio/debug/bootstrap.js --listen 8000",
|
||||
"_start:server": "node --inspect packages/verdaccio/debug/bootstrap.js --listen 8000",
|
||||
"_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",
|
||||
@@ -157,20 +137,11 @@
|
||||
"ts:ref": "update-ts-references --discardComments",
|
||||
"website": "pnpm build --filter ...@verdaccio/website",
|
||||
"crowdin:upload": "crowdin upload sources --auto-update --config ./crowdin.yaml",
|
||||
"crowdin:download": "crowdin download --config ./crowdin.yaml",
|
||||
"crowdin:sync": "pnpm crowdin:upload && pnpm crowdin:download --verbose"
|
||||
"crowdin:download": "crowdin download --verbose --config ./crowdin.yaml",
|
||||
"crowdin:sync": "pnpm crowdin:upload && pnpm crowdin:download --verbose",
|
||||
"postinstall": "husky install"
|
||||
},
|
||||
"license": "MIT",
|
||||
"commitlint": {
|
||||
"extends": [
|
||||
"@commitlint/config-conventional"
|
||||
]
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,json,yml,yaml,md}": "prettier --write",
|
||||
"*.{js,jsx,ts,tsx}": "eslint --cache --fix"
|
||||
|
||||
@@ -1,5 +1,67 @@
|
||||
# @verdaccio/api
|
||||
|
||||
## 6.0.0-6-next.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2c594910]
|
||||
- @verdaccio/logger@6.0.0-6-next.5
|
||||
- @verdaccio/auth@6.0.0-6-next.12
|
||||
- @verdaccio/hooks@6.0.0-6-next.7
|
||||
- @verdaccio/middleware@6.0.0-6-next.12
|
||||
- @verdaccio/store@6.0.0-6-next.13
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@verdaccio/api",
|
||||
"version": "6.0.0-6-next.13",
|
||||
"version": "6.0.0-6-next.15",
|
||||
"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.10",
|
||||
"@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.5",
|
||||
"@verdaccio/logger": "workspace:6.0.0-6-next.4",
|
||||
"@verdaccio/middleware": "workspace:6.0.0-6-next.10",
|
||||
"@verdaccio/store": "workspace:6.0.0-6-next.11",
|
||||
"@verdaccio/tarball": "workspace:11.0.0-6-next.6",
|
||||
"@verdaccio/utils": "workspace:6.0.0-6-next.5",
|
||||
"@verdaccio/auth": "workspace:6.0.0-6-next.12",
|
||||
"@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.7",
|
||||
"@verdaccio/logger": "workspace:6.0.0-6-next.5",
|
||||
"@verdaccio/middleware": "workspace:6.0.0-6-next.12",
|
||||
"@verdaccio/store": "workspace:6.0.0-6-next.13",
|
||||
"@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,9 @@
|
||||
"semver": "7.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@verdaccio/server": "workspace:6.0.0-6-next.18",
|
||||
"@verdaccio/types": "workspace:11.0.0-6-next.7",
|
||||
"@types/node": "16.9.1",
|
||||
"@verdaccio/server": "workspace:6.0.0-6-next.20",
|
||||
"@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,75 @@
|
||||
import semver from 'semver';
|
||||
import _ from 'lodash';
|
||||
import buildDebug from 'debug';
|
||||
import { logger } from '@verdaccio/logger';
|
||||
import { IAuth } from '@verdaccio/auth';
|
||||
import { searchUtils } from '@verdaccio/core';
|
||||
import { HTTP_STATUS } from '@verdaccio/commons-api';
|
||||
import { Storage } from '@verdaccio/store';
|
||||
import { Package } from '@verdaccio/types';
|
||||
|
||||
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');
|
||||
|
||||
if (typeof person === 'object') {
|
||||
for (const field of Object.values(person)) {
|
||||
if (typeof field === 'string' && field.includes(search)) {
|
||||
return true;
|
||||
/**
|
||||
* 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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
size = parseInt(size) || 20;
|
||||
from = parseInt(from) || 0;
|
||||
|
||||
const isInteresting = compileTextSearch(text);
|
||||
|
||||
const resultStream = storage.search(0, { req: { query: { local: true } } });
|
||||
const resultBuf = [] as any;
|
||||
let completed = false;
|
||||
|
||||
const sendResponse = (): void => {
|
||||
completed = true;
|
||||
resultStream.destroy();
|
||||
|
||||
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 = {
|
||||
});
|
||||
}
|
||||
|
||||
route.get('/-/v1/search', async (req, res, next) => {
|
||||
let [size, from] = ['size', 'from'].map((k) => req.query[k]);
|
||||
let data;
|
||||
|
||||
size = parseInt(size, 10) || 20;
|
||||
from = parseInt(from, 10) || 0;
|
||||
|
||||
try {
|
||||
data = await storage.searchManager?.search({
|
||||
query: req.query,
|
||||
url: req.url,
|
||||
});
|
||||
debug('stream finish');
|
||||
const checkAccessPromises: searchUtils.SearchItemPkg[] = await Promise.all(
|
||||
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: searchUtils.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);
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'search endpoint has failed @{error.message}');
|
||||
next(next);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2c594910]
|
||||
- @verdaccio/logger@6.0.0-6-next.5
|
||||
- @verdaccio/loaders@6.0.0-6-next.5
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@verdaccio/auth",
|
||||
"version": "6.0.0-6-next.10",
|
||||
"version": "6.0.0-6-next.12",
|
||||
"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/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/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.5",
|
||||
"@verdaccio/logger": "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-6-next.7"
|
||||
"verdaccio-htpasswd": "workspace:11.0.0-6-next.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@verdaccio/mock": "workspace:6.0.0-6-next.8",
|
||||
"@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,59 @@
|
||||
# @verdaccio/cli
|
||||
|
||||
## 6.0.0-6-next.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2c594910]
|
||||
- @verdaccio/logger@6.0.0-6-next.5
|
||||
- @verdaccio/fastify-migration@6.0.0-6-next.13
|
||||
- @verdaccio/node-api@6.0.0-6-next.21
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@verdaccio/cli",
|
||||
"version": "6.0.0-6-next.20",
|
||||
"version": "6.0.0-6-next.22",
|
||||
"author": {
|
||||
"name": "Juan Picado",
|
||||
"email": "juanpicado19@gmail.com"
|
||||
@@ -44,10 +44,10 @@
|
||||
"start": "ts-node src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@verdaccio/config": "workspace:6.0.0-6-next.7",
|
||||
"@verdaccio/logger": "workspace:6.0.0-6-next.4",
|
||||
"@verdaccio/node-api": "workspace:6.0.0-6-next.19",
|
||||
"@verdaccio/fastify-migration": "workspace:6.0.0-6-next.11",
|
||||
"@verdaccio/config": "workspace:6.0.0-6-next.8",
|
||||
"@verdaccio/logger": "workspace:6.0.0-6-next.5",
|
||||
"@verdaccio/node-api": "workspace:6.0.0-6-next.21",
|
||||
"@verdaccio/fastify-migration": "workspace:6.0.0-6-next.13",
|
||||
"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,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,8 +39,8 @@
|
||||
"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.1",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -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, {});
|
||||
57
packages/core/core/package.json
Normal file
57
packages/core/core/package.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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[]>;
|
||||
}
|
||||
70
packages/core/core/src/search-utils.ts
Normal file
70
packages/core/core/src/search-utils.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
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;
|
||||
};
|
||||
|
||||
export type SearchResults = {
|
||||
objects: SearchItemPkg[];
|
||||
total: number;
|
||||
time: string;
|
||||
};
|
||||
|
||||
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,13 @@
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "verdaccio-htpasswd",
|
||||
"version": "11.0.0-6-next.7",
|
||||
"version": "11.0.0-6-next.8",
|
||||
"description": "htpasswd auth plugin for Verdaccio",
|
||||
"keywords": [
|
||||
"private",
|
||||
@@ -34,17 +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.16.4",
|
||||
"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,50 @@
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@verdaccio/local-storage",
|
||||
"version": "11.0.0-6-next.7",
|
||||
"version": "11.0.0-6-next.8",
|
||||
"description": "Local storage implementation",
|
||||
"keywords": [
|
||||
"private",
|
||||
@@ -33,24 +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.16.4",
|
||||
"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"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user