mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-12-30 22:34:10 -05:00
feat: improve url_prefix behavior (#2122)
read pr 2122 for more details
This commit is contained in:
parent
e5ce44c395
commit
15bb350ae4
34 changed files with 1089 additions and 637 deletions
|
@ -15,6 +15,7 @@ Dockerfile
|
||||||
*.png
|
*.png
|
||||||
*.jpg
|
*.jpg
|
||||||
*.sh
|
*.sh
|
||||||
|
*.ico
|
||||||
test/unit/partials/
|
test/unit/partials/
|
||||||
types/custom.d.ts
|
types/custom.d.ts
|
||||||
docker-examples/
|
docker-examples/
|
||||||
|
|
|
@ -13,8 +13,8 @@ src/
|
||||||
.vscode/
|
.vscode/
|
||||||
.circleci/
|
.circleci/
|
||||||
debug/
|
debug/
|
||||||
|
docker-examples/
|
||||||
|
reports/
|
||||||
## assets and website
|
## assets and website
|
||||||
assets/
|
assets/
|
||||||
|
|
||||||
|
|
|
@ -27,3 +27,6 @@ test/functional/store/*
|
||||||
storage_default_storage/*
|
storage_default_storage/*
|
||||||
docker-examples/
|
docker-examples/
|
||||||
.prettierignore
|
.prettierignore
|
||||||
|
.npmignore
|
||||||
|
.gitignore
|
||||||
|
*.ico
|
||||||
|
|
|
@ -66,6 +66,7 @@ packages:
|
||||||
# WORKAROUND: Through given configuration you can workaround following issue https://github.com/verdaccio/verdaccio/issues/301. Set to 0 in case 60 is not enough.
|
# WORKAROUND: Through given configuration you can workaround following issue https://github.com/verdaccio/verdaccio/issues/301. Set to 0 in case 60 is not enough.
|
||||||
server:
|
server:
|
||||||
keepAliveTimeout: 60
|
keepAliveTimeout: 60
|
||||||
|
# behindProxy: false
|
||||||
|
|
||||||
middlewares:
|
middlewares:
|
||||||
audit:
|
audit:
|
||||||
|
|
|
@ -66,6 +66,14 @@ packages:
|
||||||
# if package is not available locally, proxy requests to 'npmjs' registry
|
# if package is not available locally, proxy requests to 'npmjs' registry
|
||||||
proxy: npmjs
|
proxy: npmjs
|
||||||
|
|
||||||
|
# You can specify HTTP/1.1 server keep alive timeout in seconds for incoming connections.
|
||||||
|
# A value of 0 makes the http server behave similarly to Node.js versions prior to 8.0.0, which did not have a keep-alive timeout.
|
||||||
|
# WORKAROUND: Through given configuration you can workaround following issue https://github.com/verdaccio/verdaccio/issues/301. Set to 0 in case 60 is not enough.
|
||||||
|
server:
|
||||||
|
keepAliveTimeout: 60
|
||||||
|
# enable this if you run behind a proxy
|
||||||
|
# behindProxy: false
|
||||||
|
|
||||||
middlewares:
|
middlewares:
|
||||||
audit:
|
audit:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
storage: /verdaccio/storage
|
|
||||||
|
|
||||||
web:
|
|
||||||
enable: true
|
|
||||||
title: VerdaccioV3 Relative Path
|
|
||||||
|
|
||||||
auth:
|
|
||||||
htpasswd:
|
|
||||||
file: /verdaccio/conf/htpasswd
|
|
||||||
security:
|
|
||||||
api:
|
|
||||||
jwt:
|
|
||||||
sign:
|
|
||||||
expiresIn: 60d
|
|
||||||
notBefore: 1
|
|
||||||
web:
|
|
||||||
sign:
|
|
||||||
expiresIn: 7d
|
|
||||||
|
|
||||||
## IMPORTANT
|
|
||||||
##
|
|
||||||
url_prefix: /verdacciov3/
|
|
||||||
|
|
||||||
uplinks:
|
|
||||||
npmjs:
|
|
||||||
url: https://registry.npmjs.org/
|
|
||||||
|
|
||||||
packages:
|
|
||||||
'@jota/*':
|
|
||||||
access: $all
|
|
||||||
publish: $all
|
|
||||||
|
|
||||||
'@*/*':
|
|
||||||
# scoped packages
|
|
||||||
access: $all
|
|
||||||
publish: $all
|
|
||||||
proxy: npmjs
|
|
||||||
|
|
||||||
'**':
|
|
||||||
access: $all
|
|
||||||
publish: $all
|
|
||||||
proxy: npmjs
|
|
||||||
|
|
||||||
middlewares:
|
|
||||||
audit:
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
logs:
|
|
||||||
- { type: stdout, format: pretty, level: trace }
|
|
|
@ -1 +0,0 @@
|
||||||
test:$6FrCaT/v0dwE:autocreated 2019-05-01T09:29:55.707Z
|
|
|
@ -19,8 +19,10 @@ security:
|
||||||
expiresIn: 7d
|
expiresIn: 7d
|
||||||
|
|
||||||
## IMPORTANT
|
## IMPORTANT
|
||||||
##
|
## This setup is required for relative path
|
||||||
url_prefix: /verdaccio
|
url_prefix: /verdaccio/
|
||||||
|
server:
|
||||||
|
behindProxy: true
|
||||||
|
|
||||||
uplinks:
|
uplinks:
|
||||||
npmjs:
|
npmjs:
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
storage: /verdaccio/storage
|
|
||||||
|
|
||||||
web:
|
|
||||||
enable: true
|
|
||||||
title: VerdaccioV4 Relative Path
|
|
||||||
primary_color: red
|
|
||||||
|
|
||||||
auth:
|
|
||||||
htpasswd:
|
|
||||||
file: /verdaccio/conf/htpasswd
|
|
||||||
security:
|
|
||||||
api:
|
|
||||||
jwt:
|
|
||||||
sign:
|
|
||||||
expiresIn: 60d
|
|
||||||
notBefore: 1
|
|
||||||
web:
|
|
||||||
sign:
|
|
||||||
expiresIn: 7d
|
|
||||||
|
|
||||||
uplinks:
|
|
||||||
npmjs:
|
|
||||||
url: https://registry.npmjs.org/
|
|
||||||
|
|
||||||
packages:
|
|
||||||
'@jota/*':
|
|
||||||
access: $all
|
|
||||||
publish: $all
|
|
||||||
|
|
||||||
'@*/*':
|
|
||||||
# scoped packages
|
|
||||||
access: $all
|
|
||||||
publish: $all
|
|
||||||
proxy: npmjs
|
|
||||||
|
|
||||||
'**':
|
|
||||||
access: $all
|
|
||||||
publish: $all
|
|
||||||
proxy: npmjs
|
|
||||||
|
|
||||||
middlewares:
|
|
||||||
audit:
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
logs:
|
|
||||||
- { type: stdout, format: pretty, level: trace }
|
|
|
@ -1 +0,0 @@
|
||||||
jpicado:$6vkdNgRX2npc:autocreated 2017-07-11T18:48:38.003Z
|
|
|
@ -12,45 +12,19 @@ services:
|
||||||
container_name: 'nginx'
|
container_name: 'nginx'
|
||||||
depends_on:
|
depends_on:
|
||||||
- verdaccio
|
- verdaccio
|
||||||
- verdaccio3
|
|
||||||
- verdaccio-root
|
|
||||||
verdaccio:
|
verdaccio:
|
||||||
image: verdaccio/verdaccio:4
|
image: verdaccio/verdaccio:local
|
||||||
container_name: 'verdaccio_relative_path_v4'
|
container_name: 'verdaccio_relative_path_v4'
|
||||||
networks:
|
networks:
|
||||||
- node-network
|
- node-network
|
||||||
environment:
|
environment:
|
||||||
- VERDACCIO_PORT=4873
|
- VERDACCIO_PORT=4873
|
||||||
|
- DEBUG=verdaccio*
|
||||||
ports:
|
ports:
|
||||||
- '4873:4873'
|
- '4873:4873'
|
||||||
volumes:
|
volumes:
|
||||||
- './storage:/verdaccio/storage'
|
- './storage:/verdaccio/storage'
|
||||||
- './conf/v4:/verdaccio/conf'
|
- './conf/v4:/verdaccio/conf'
|
||||||
verdaccio-root:
|
|
||||||
image: verdaccio/verdaccio:4
|
|
||||||
container_name: 'verdaccio_relative_path_v4_root'
|
|
||||||
networks:
|
|
||||||
- node-network
|
|
||||||
environment:
|
|
||||||
- VERDACCIO_PORT=8000
|
|
||||||
ports:
|
|
||||||
- '8000:8000'
|
|
||||||
volumes:
|
|
||||||
- './storage:/verdaccio/storage'
|
|
||||||
- './conf/v4_root:/verdaccio/conf'
|
|
||||||
verdaccio3:
|
|
||||||
image: verdaccio/verdaccio:3
|
|
||||||
container_name: 'verdaccio_relative_path_latest_v3'
|
|
||||||
networks:
|
|
||||||
- node-network
|
|
||||||
ports:
|
|
||||||
- '7771:7771'
|
|
||||||
environment:
|
|
||||||
- PORT=7771
|
|
||||||
volumes:
|
|
||||||
- './storage:/verdaccio/storage'
|
|
||||||
- './conf/v3:/verdaccio/conf'
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
node-network:
|
node-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
|
@ -17,7 +17,7 @@ services:
|
||||||
- verdaccio
|
- verdaccio
|
||||||
- verdaccio-root
|
- verdaccio-root
|
||||||
verdaccio:
|
verdaccio:
|
||||||
image: verdaccio/verdaccio:4
|
image: verdaccio/verdaccio:local
|
||||||
container_name: 'verdaccio_relative_path_v4'
|
container_name: 'verdaccio_relative_path_v4'
|
||||||
networks:
|
networks:
|
||||||
- node-network
|
- node-network
|
||||||
|
@ -29,7 +29,7 @@ services:
|
||||||
- './storage:/verdaccio/storage'
|
- './storage:/verdaccio/storage'
|
||||||
- './conf/v4:/verdaccio/conf'
|
- './conf/v4:/verdaccio/conf'
|
||||||
verdaccio-root:
|
verdaccio-root:
|
||||||
image: verdaccio/verdaccio:4
|
image: verdaccio/verdaccio:local
|
||||||
container_name: 'verdaccio_relative_path_v4_root'
|
container_name: 'verdaccio_relative_path_v4_root'
|
||||||
networks:
|
networks:
|
||||||
- node-network
|
- node-network
|
||||||
|
|
|
@ -3,31 +3,11 @@ upstream verdaccio_v4 {
|
||||||
keepalive 8;
|
keepalive 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
upstream verdaccio_v4_root {
|
|
||||||
server verdaccio_relative_path_v4_root:8000;
|
|
||||||
keepalive 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
upstream verdaccio_v3 {
|
|
||||||
server verdaccio_relative_path_latest_v3:7771;
|
|
||||||
keepalive 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80 default_server;
|
listen 80 default_server;
|
||||||
access_log /var/log/nginx/verdaccio.log;
|
access_log /var/log/nginx/verdaccio.log;
|
||||||
charset utf-8;
|
charset utf-8;
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-NginX-Proxy true;
|
|
||||||
proxy_pass http://verdaccio_v4_root;
|
|
||||||
proxy_redirect off;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/verdaccio/(.*)$ {
|
location ~ ^/verdaccio/(.*)$ {
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
@ -36,14 +16,4 @@ server {
|
||||||
proxy_pass http://verdaccio_v4/$1;
|
proxy_pass http://verdaccio_v4/$1;
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ ^/verdacciov3/(.*)$ {
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-NginX-Proxy true;
|
|
||||||
|
|
||||||
proxy_pass http://verdaccio_v3/$1;
|
|
||||||
proxy_redirect off;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
0
docker-examples/v4/reverse_proxy/nginx/relative_path/storage/@verdaccio/streams/package.json
Normal file → Executable file
0
docker-examples/v4/reverse_proxy/nginx/relative_path/storage/@verdaccio/streams/package.json
Normal file → Executable file
0
docker-examples/v4/reverse_proxy/nginx/relative_path/storage/jquery/package.json
Normal file → Executable file
0
docker-examples/v4/reverse_proxy/nginx/relative_path/storage/jquery/package.json
Normal file → Executable file
0
docker-examples/v4/reverse_proxy/nginx/relative_path/storage/verdaccio/package.json
Normal file → Executable file
0
docker-examples/v4/reverse_proxy/nginx/relative_path/storage/verdaccio/package.json
Normal file → Executable file
|
@ -8,3 +8,28 @@ internal features.
|
||||||
Enables gracefully shutdown, more info [here](https://github.com/verdaccio/verdaccio/pull/2121).
|
Enables gracefully shutdown, more info [here](https://github.com/verdaccio/verdaccio/pull/2121).
|
||||||
|
|
||||||
This will be enable by default on Verdaccio 5.
|
This will be enable by default on Verdaccio 5.
|
||||||
|
|
||||||
|
#### VERDACCIO_PUBLIC_URL
|
||||||
|
|
||||||
|
Define a specific public url for your server, it overrules the `Host` and `X-Forwarded-Proto` header if a reverse proxy is being used, it takes in account the `url_prefix` if is defined.
|
||||||
|
|
||||||
|
This is handy in such situations where a dynamic url is required.
|
||||||
|
|
||||||
|
eg:
|
||||||
|
|
||||||
|
```
|
||||||
|
VERDACCIO_PUBLIC_URL='https://somedomain.org';
|
||||||
|
url_prefix: '/my_prefix'
|
||||||
|
|
||||||
|
// url -> https://somedomain.org/my_prefix/
|
||||||
|
|
||||||
|
VERDACCIO_PUBLIC_URL='https://somedomain.org';
|
||||||
|
url_prefix: '/'
|
||||||
|
|
||||||
|
// url -> https://somedomain.org/
|
||||||
|
|
||||||
|
VERDACCIO_PUBLIC_URL='https://somedomain.org/first_prefix';
|
||||||
|
url_prefix: '/second_prefix'
|
||||||
|
|
||||||
|
// url -> https://somedomain.org/second_prefix/'
|
||||||
|
```
|
||||||
|
|
12
package.json
12
package.json
|
@ -22,7 +22,7 @@
|
||||||
"@verdaccio/local-storage": "9.7.5",
|
"@verdaccio/local-storage": "9.7.5",
|
||||||
"@verdaccio/readme": "9.7.5",
|
"@verdaccio/readme": "9.7.5",
|
||||||
"@verdaccio/streams": "9.7.2",
|
"@verdaccio/streams": "9.7.2",
|
||||||
"@verdaccio/ui-theme": "1.15.1",
|
"@verdaccio/ui-theme": "3.0.0",
|
||||||
"JSONStream": "1.3.5",
|
"JSONStream": "1.3.5",
|
||||||
"async": "3.2.0",
|
"async": "3.2.0",
|
||||||
"body-parser": "1.19.0",
|
"body-parser": "1.19.0",
|
||||||
|
@ -32,6 +32,7 @@
|
||||||
"cookies": "0.8.0",
|
"cookies": "0.8.0",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"dayjs": "1.10.4",
|
"dayjs": "1.10.4",
|
||||||
|
"debug": "^4.3.1",
|
||||||
"envinfo": "7.7.4",
|
"envinfo": "7.7.4",
|
||||||
"express": "4.17.1",
|
"express": "4.17.1",
|
||||||
"handlebars": "4.7.7",
|
"handlebars": "4.7.7",
|
||||||
|
@ -49,6 +50,7 @@
|
||||||
"pkginfo": "0.4.1",
|
"pkginfo": "0.4.1",
|
||||||
"request": "2.88.0",
|
"request": "2.88.0",
|
||||||
"semver": "7.3.4",
|
"semver": "7.3.4",
|
||||||
|
"validator": "13.5.2",
|
||||||
"verdaccio-audit": "9.7.3",
|
"verdaccio-audit": "9.7.3",
|
||||||
"verdaccio-htpasswd": "9.7.2"
|
"verdaccio-htpasswd": "9.7.2"
|
||||||
},
|
},
|
||||||
|
@ -121,7 +123,9 @@
|
||||||
"jest-junit": "9.0.0",
|
"jest-junit": "9.0.0",
|
||||||
"lint-staged": "8.2.1",
|
"lint-staged": "8.2.1",
|
||||||
"lockfile-lint": "4.3.7",
|
"lockfile-lint": "4.3.7",
|
||||||
|
"lru-cache": "6.0.0",
|
||||||
"nock": "12.0.3",
|
"nock": "12.0.3",
|
||||||
|
"node-mocks-http": "^1.10.1",
|
||||||
"prettier": "2.2.1",
|
"prettier": "2.2.1",
|
||||||
"puppeteer": "5.5.0",
|
"puppeteer": "5.5.0",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
|
@ -164,10 +168,10 @@
|
||||||
"lint": "yarn run type-check && yarn run lint:ts",
|
"lint": "yarn run type-check && yarn run lint:ts",
|
||||||
"lint:ts": "eslint \"**/*.{js,jsx,ts,tsx}\"",
|
"lint:ts": "eslint \"**/*.{js,jsx,ts,tsx}\"",
|
||||||
"lint:lockfile": "lockfile-lint --path yarn.lock --type yarn --validate-https --allowed-hosts verdaccio npm yarn",
|
"lint:lockfile": "lockfile-lint --path yarn.lock --type yarn --validate-https --allowed-hosts verdaccio npm yarn",
|
||||||
"dev:start": "yarn babel-node --extensions \".ts,.tsx\" src/lib/cli",
|
"start": "yarn babel-node --extensions \".ts,.tsx\" src/lib/cli",
|
||||||
"code:build": "yarn babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps inline",
|
"code:build": "yarn babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps inline",
|
||||||
"code:docker-build": "yarn babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\"",
|
"code:docker-build": "yarn babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\"",
|
||||||
"docker": "docker build -t verdaccio/verdaccio:local . --no-cache",
|
"docker": "docker build -t verdaccio/verdaccio:pr-2122 . --no-cache",
|
||||||
"docker:run": "docker run -it --rm -p 4873:4873 verdaccio/verdaccio:local"
|
"docker:run": "docker run -it --rm -p 4873:4873 verdaccio/verdaccio:local"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -186,8 +190,6 @@
|
||||||
"linters": {
|
"linters": {
|
||||||
"*": [
|
"*": [
|
||||||
"eslint .",
|
"eslint .",
|
||||||
"prettier --write",
|
|
||||||
"detect-secrets-launcher --baseline .secrets-baseline",
|
|
||||||
"git add"
|
"git add"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,23 +10,21 @@ import Auth from '../lib/auth';
|
||||||
import { ErrorCode } from '../lib/utils';
|
import { ErrorCode } from '../lib/utils';
|
||||||
import { API_ERROR, HTTP_STATUS } from '../lib/constants';
|
import { API_ERROR, HTTP_STATUS } from '../lib/constants';
|
||||||
import AppConfig from '../lib/config';
|
import AppConfig from '../lib/config';
|
||||||
import {
|
import { $ResponseExtend, $RequestExtend, $NextFunctionVer, IStorageHandler, IAuth } from '../../types';
|
||||||
$ResponseExtend,
|
|
||||||
$RequestExtend,
|
|
||||||
$NextFunctionVer,
|
|
||||||
IStorageHandler,
|
|
||||||
IAuth
|
|
||||||
} from '../../types';
|
|
||||||
import { setup, logger } from '../lib/logger';
|
import { setup, logger } from '../lib/logger';
|
||||||
import webAPI from './web/api';
|
import webAPI from './web/api';
|
||||||
import web from './web';
|
import web from './web';
|
||||||
import apiEndpoint from './endpoint';
|
import apiEndpoint from './endpoint';
|
||||||
import hookDebug from './debug';
|
import hookDebug from './debug';
|
||||||
import { log, final, errorReportingMiddleware } from './middleware';
|
import { log, final, errorReportingMiddleware, serveFavicon } from './middleware';
|
||||||
|
|
||||||
const defineAPI = function (config: IConfig, storage: IStorageHandler): any {
|
const defineAPI = function (config: IConfig, storage: IStorageHandler): any {
|
||||||
const auth: IAuth = new Auth(config);
|
const auth: IAuth = new Auth(config);
|
||||||
const app: Application = express();
|
const app: Application = express();
|
||||||
|
if (config?.server?.behindProxy === true) {
|
||||||
|
// app.use('trust proxy');
|
||||||
|
}
|
||||||
|
|
||||||
// run in production mode by default, just in case
|
// run in production mode by default, just in case
|
||||||
// it shouldn't make any difference anyway
|
// it shouldn't make any difference anyway
|
||||||
app.set('env', process.env.NODE_ENV || 'production');
|
app.set('env', process.env.NODE_ENV || 'production');
|
||||||
|
@ -42,13 +40,7 @@ const defineAPI = function (config: IConfig, storage: IStorageHandler): any {
|
||||||
|
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
|
|
||||||
app.get(
|
app.get('/-/static/favicon.ico', serveFavicon(config));
|
||||||
'/favicon.ico',
|
|
||||||
function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
|
||||||
req.url = '/-/static/favicon.png';
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Hook for tests only
|
// Hook for tests only
|
||||||
if (config._debug) {
|
if (config._debug) {
|
||||||
|
@ -58,17 +50,12 @@ const defineAPI = function (config: IConfig, storage: IStorageHandler): any {
|
||||||
// register middleware plugins
|
// register middleware plugins
|
||||||
const plugin_params = {
|
const plugin_params = {
|
||||||
config: config,
|
config: config,
|
||||||
logger: logger
|
logger: logger,
|
||||||
};
|
};
|
||||||
|
|
||||||
const plugins: IPluginMiddleware<IConfig>[] = loadPlugin(
|
const plugins: IPluginMiddleware<IConfig>[] = loadPlugin(config, config.middlewares, plugin_params, function (plugin: IPluginMiddleware<IConfig>) {
|
||||||
config,
|
|
||||||
config.middlewares,
|
|
||||||
plugin_params,
|
|
||||||
function (plugin: IPluginMiddleware<IConfig>) {
|
|
||||||
return plugin.register_middlewares;
|
return plugin.register_middlewares;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
plugins.forEach((plugin: IPluginMiddleware<IConfig>) => {
|
plugins.forEach((plugin: IPluginMiddleware<IConfig>) => {
|
||||||
plugin.register_middlewares(app, auth, storage);
|
plugin.register_middlewares(app, auth, storage);
|
||||||
});
|
});
|
||||||
|
@ -91,12 +78,7 @@ const defineAPI = function (config: IConfig, storage: IStorageHandler): any {
|
||||||
next(ErrorCode.getNotFound(API_ERROR.FILE_NOT_FOUND));
|
next(ErrorCode.getNotFound(API_ERROR.FILE_NOT_FOUND));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(function (
|
app.use(function (err: HttpError, req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {
|
||||||
err: HttpError,
|
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer
|
|
||||||
) {
|
|
||||||
if (_.isError(err)) {
|
if (_.isError(err)) {
|
||||||
if (err.code === 'ECONNABORT' && res.statusCode === HTTP_STATUS.NOT_MODIFIED) {
|
if (err.code === 'ECONNABORT' && res.statusCode === HTTP_STATUS.NOT_MODIFIED) {
|
||||||
return next();
|
return next();
|
||||||
|
@ -124,14 +106,9 @@ export default (async function (configHash: any): Promise<any> {
|
||||||
// register middleware plugins
|
// register middleware plugins
|
||||||
const plugin_params = {
|
const plugin_params = {
|
||||||
config: config,
|
config: config,
|
||||||
logger: logger
|
logger: logger,
|
||||||
};
|
};
|
||||||
const filters = loadPlugin(
|
const filters = loadPlugin(config, config.filters || {}, plugin_params, (plugin: IPluginStorageFilter<IConfig>) => plugin.filter_metadata);
|
||||||
config,
|
|
||||||
config.filters || {},
|
|
||||||
plugin_params,
|
|
||||||
(plugin: IPluginStorageFilter<IConfig>) => plugin.filter_metadata
|
|
||||||
);
|
|
||||||
const storage: IStorageHandler = new Storage(config);
|
const storage: IStorageHandler = new Storage(config);
|
||||||
// waits until init calls have been initialized
|
// waits until init calls have been initialized
|
||||||
await storage.init(config, filters);
|
await storage.init(config, filters);
|
||||||
|
|
|
@ -1,33 +1,21 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import buildDebug from 'debug';
|
||||||
|
import validator from 'validator';
|
||||||
|
|
||||||
import { Config, Package, RemoteUser } from '@verdaccio/types';
|
import { Config, Package, RemoteUser } from '@verdaccio/types';
|
||||||
import { VerdaccioError } from '@verdaccio/commons-api';
|
import { VerdaccioError } from '@verdaccio/commons-api';
|
||||||
import {
|
import { validateName as utilValidateName, validatePackage as utilValidatePackage, getVersionFromTarball, isObject, ErrorCode } from '../lib/utils';
|
||||||
validateName as utilValidateName,
|
import { API_ERROR, HEADER_TYPE, HEADERS, HTTP_STATUS, TOKEN_BASIC, TOKEN_BEARER } from '../lib/constants';
|
||||||
validatePackage as utilValidatePackage,
|
|
||||||
getVersionFromTarball,
|
|
||||||
isObject,
|
|
||||||
ErrorCode
|
|
||||||
} from '../lib/utils';
|
|
||||||
import {
|
|
||||||
API_ERROR,
|
|
||||||
HEADER_TYPE,
|
|
||||||
HEADERS,
|
|
||||||
HTTP_STATUS,
|
|
||||||
TOKEN_BASIC,
|
|
||||||
TOKEN_BEARER
|
|
||||||
} from '../lib/constants';
|
|
||||||
import { stringToMD5 } from '../lib/crypto-utils';
|
import { stringToMD5 } from '../lib/crypto-utils';
|
||||||
import { $ResponseExtend, $RequestExtend, $NextFunctionVer, IAuth } from '../../types';
|
import { $ResponseExtend, $RequestExtend, $NextFunctionVer, IAuth } from '../../types';
|
||||||
import { logger } from '../lib/logger';
|
import { logger } from '../lib/logger';
|
||||||
|
|
||||||
|
const debug = buildDebug('verdaccio');
|
||||||
|
|
||||||
export function match(regexp: RegExp): any {
|
export function match(regexp: RegExp): any {
|
||||||
return function (
|
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer, value: string): void {
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer,
|
|
||||||
value: string
|
|
||||||
): void {
|
|
||||||
if (regexp.exec(value)) {
|
if (regexp.exec(value)) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
|
@ -36,11 +24,52 @@ export function match(regexp: RegExp): any {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setSecurityWebHeaders(
|
export function serveFavicon(config: Config) {
|
||||||
req: $RequestExtend,
|
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {
|
||||||
res: $ResponseExtend,
|
try {
|
||||||
next: $NextFunctionVer
|
// @ts-ignore
|
||||||
): void {
|
const logoConf: string = config?.web?.logo as string;
|
||||||
|
if (logoConf === '') {
|
||||||
|
debug('favicon disabled');
|
||||||
|
res.status(404);
|
||||||
|
} else if (!_.isEmpty(logoConf)) {
|
||||||
|
debug('custom favicon');
|
||||||
|
if (
|
||||||
|
validator.isURL(logoConf, {
|
||||||
|
require_host: true,
|
||||||
|
require_valid_protocol: true,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
debug('redirect to %o', logoConf);
|
||||||
|
res.redirect(logoConf);
|
||||||
|
} else {
|
||||||
|
const faviconPath = path.normalize(logoConf);
|
||||||
|
debug('serving favicon from %o', faviconPath);
|
||||||
|
fs.access(faviconPath, fs.constants.R_OK, (err) => {
|
||||||
|
if (err) {
|
||||||
|
debug('no read permissions to read: %o, reason:', logoConf, err?.message);
|
||||||
|
return res.status(HTTP_STATUS.NOT_FOUND).end();
|
||||||
|
} else {
|
||||||
|
res.setHeader('Content-Type', 'image/x-icon');
|
||||||
|
fs.createReadStream(faviconPath).pipe(res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
} else {
|
||||||
|
res.setHeader('Content-Type', 'image/x-icon');
|
||||||
|
fs.createReadStream(path.join(__dirname, './web/html/favicon.ico')).pipe(res);
|
||||||
|
debug('rendered ico');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
debug('error triggered, favicon not found');
|
||||||
|
res.status(HTTP_STATUS.NOT_FOUND).end();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSecurityWebHeaders(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||||
// disable loading in frames (clickjacking, etc.)
|
// disable loading in frames (clickjacking, etc.)
|
||||||
res.header(HEADERS.FRAMES_OPTIONS, 'deny');
|
res.header(HEADERS.FRAMES_OPTIONS, 'deny');
|
||||||
// avoid stablish connections outside of domain
|
// avoid stablish connections outside of domain
|
||||||
|
@ -54,13 +83,7 @@ export function setSecurityWebHeaders(
|
||||||
|
|
||||||
// flow: express does not match properly
|
// flow: express does not match properly
|
||||||
// flow info https://github.com/flowtype/flow-typed/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+express
|
// flow info https://github.com/flowtype/flow-typed/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+express
|
||||||
export function validateName(
|
export function validateName(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer, value: string, name: string): void {
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer,
|
|
||||||
value: string,
|
|
||||||
name: string
|
|
||||||
): void {
|
|
||||||
if (value === '-') {
|
if (value === '-') {
|
||||||
// special case in couchdb usually
|
// special case in couchdb usually
|
||||||
next('route');
|
next('route');
|
||||||
|
@ -73,13 +96,7 @@ export function validateName(
|
||||||
|
|
||||||
// flow: express does not match properly
|
// flow: express does not match properly
|
||||||
// flow info https://github.com/flowtype/flow-typed/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+express
|
// flow info https://github.com/flowtype/flow-typed/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+express
|
||||||
export function validatePackage(
|
export function validatePackage(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer, value: string, name: string): void {
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer,
|
|
||||||
value: string,
|
|
||||||
name: string
|
|
||||||
): void {
|
|
||||||
if (value === '-') {
|
if (value === '-') {
|
||||||
// special case in couchdb usually
|
// special case in couchdb usually
|
||||||
next('route');
|
next('route');
|
||||||
|
@ -93,26 +110,14 @@ export function validatePackage(
|
||||||
export function media(expect: string | null): any {
|
export function media(expect: string | null): any {
|
||||||
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||||
if (req.headers[HEADER_TYPE.CONTENT_TYPE] !== expect) {
|
if (req.headers[HEADER_TYPE.CONTENT_TYPE] !== expect) {
|
||||||
next(
|
next(ErrorCode.getCode(HTTP_STATUS.UNSUPPORTED_MEDIA, 'wrong content-type, expect: ' + expect + ', got: ' + req.headers[HEADER_TYPE.CONTENT_TYPE]));
|
||||||
ErrorCode.getCode(
|
|
||||||
HTTP_STATUS.UNSUPPORTED_MEDIA,
|
|
||||||
'wrong content-type, expect: ' +
|
|
||||||
expect +
|
|
||||||
', got: ' +
|
|
||||||
req.headers[HEADER_TYPE.CONTENT_TYPE]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encodeScopePackage(
|
export function encodeScopePackage(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer
|
|
||||||
): void {
|
|
||||||
if (req.url.indexOf('@') !== -1) {
|
if (req.url.indexOf('@') !== -1) {
|
||||||
// e.g.: /@org/pkg/1.2.3 -> /@org%2Fpkg/1.2.3, /@org%2Fpkg/1.2.3 -> /@org%2Fpkg/1.2.3
|
// e.g.: /@org/pkg/1.2.3 -> /@org%2Fpkg/1.2.3, /@org%2Fpkg/1.2.3 -> /@org%2Fpkg/1.2.3
|
||||||
req.url = req.url.replace(/^(\/@[^\/%]+)\/(?!$)/, '$1%2F');
|
req.url = req.url.replace(/^(\/@[^\/%]+)\/(?!$)/, '$1%2F');
|
||||||
|
@ -120,11 +125,7 @@ export function encodeScopePackage(
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function expectJson(
|
export function expectJson(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer
|
|
||||||
): void {
|
|
||||||
if (!isObject(req.body)) {
|
if (!isObject(req.body)) {
|
||||||
return next(ErrorCode.getBadRequest("can't parse incoming json"));
|
return next(ErrorCode.getBadRequest("can't parse incoming json"));
|
||||||
}
|
}
|
||||||
|
@ -151,22 +152,12 @@ export function allow(auth: IAuth): Function {
|
||||||
return function (action: string): Function {
|
return function (action: string): Function {
|
||||||
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||||
req.pause();
|
req.pause();
|
||||||
const packageName = req.params.scope
|
const packageName = req.params.scope ? `@${req.params.scope}/${req.params.package}` : req.params.package;
|
||||||
? `@${req.params.scope}/${req.params.package}`
|
const packageVersion = req.params.filename ? getVersionFromTarball(req.params.filename) : undefined;
|
||||||
: req.params.package;
|
|
||||||
const packageVersion = req.params.filename
|
|
||||||
? getVersionFromTarball(req.params.filename)
|
|
||||||
: undefined;
|
|
||||||
const remote: RemoteUser = req.remote_user;
|
const remote: RemoteUser = req.remote_user;
|
||||||
logger.trace(
|
logger.trace({ action, user: remote.name }, `[middleware/allow][@{action}] allow for @{user}`);
|
||||||
{ action, user: remote.name },
|
|
||||||
`[middleware/allow][@{action}] allow for @{user}`
|
|
||||||
);
|
|
||||||
|
|
||||||
auth['allow_' + action](
|
auth['allow_' + action]({ packageName, packageVersion }, remote, function (error, allowed): void {
|
||||||
{ packageName, packageVersion },
|
|
||||||
remote,
|
|
||||||
function (error, allowed): void {
|
|
||||||
req.resume();
|
req.resume();
|
||||||
if (error) {
|
if (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
@ -177,8 +168,7 @@ export function allow(auth: IAuth): Function {
|
||||||
// cb(err) or cb(null, true), so this should never happen
|
// cb(err) or cb(null, true), so this should never happen
|
||||||
throw ErrorCode.getInternalError(API_ERROR.PLUGIN_ERROR);
|
throw ErrorCode.getInternalError(API_ERROR.PLUGIN_ERROR);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -189,12 +179,7 @@ export interface MiddlewareError {
|
||||||
|
|
||||||
export type FinalBody = Package | MiddlewareError | string;
|
export type FinalBody = Package | MiddlewareError | string;
|
||||||
|
|
||||||
export function final(
|
export function final(body: FinalBody, req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||||
body: FinalBody,
|
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer
|
|
||||||
): void {
|
|
||||||
if (res.statusCode === HTTP_STATUS.UNAUTHORIZED && !res.getHeader(HEADERS.WWW_AUTH)) {
|
if (res.statusCode === HTTP_STATUS.UNAUTHORIZED && !res.getHeader(HEADERS.WWW_AUTH)) {
|
||||||
// they say it's required for 401, so...
|
// they say it's required for 401, so...
|
||||||
res.header(HEADERS.WWW_AUTH, `${TOKEN_BASIC}, ${TOKEN_BEARER}`);
|
res.header(HEADERS.WWW_AUTH, `${TOKEN_BASIC}, ${TOKEN_BEARER}`);
|
||||||
|
@ -214,10 +199,7 @@ export function final(
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't send etags with errors
|
// don't send etags with errors
|
||||||
if (
|
if (!res.statusCode || (res.statusCode >= HTTP_STATUS.OK && res.statusCode < HTTP_STATUS.MULTIPLE_CHOICES)) {
|
||||||
!res.statusCode ||
|
|
||||||
(res.statusCode >= HTTP_STATUS.OK && res.statusCode < HTTP_STATUS.MULTIPLE_CHOICES)
|
|
||||||
) {
|
|
||||||
res.header(HEADERS.ETAG, '"' + stringToMD5(body as string) + '"');
|
res.header(HEADERS.ETAG, '"' + stringToMD5(body as string) + '"');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -239,8 +221,7 @@ export function final(
|
||||||
res.send(body);
|
res.send(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LOG_STATUS_MESSAGE =
|
export const LOG_STATUS_MESSAGE = "@{status}, user: @{user}(@{remoteIP}), req: '@{request.method} @{request.url}'";
|
||||||
"@{status}, user: @{user}(@{remoteIP}), req: '@{request.method} @{request.url}'";
|
|
||||||
export const LOG_VERDACCIO_ERROR = `${LOG_STATUS_MESSAGE}, error: @{!error}`;
|
export const LOG_VERDACCIO_ERROR = `${LOG_STATUS_MESSAGE}, error: @{!error}`;
|
||||||
export const LOG_VERDACCIO_BYTES = `${LOG_STATUS_MESSAGE}, bytes: @{bytes.in}/@{bytes.out}`;
|
export const LOG_VERDACCIO_BYTES = `${LOG_STATUS_MESSAGE}, bytes: @{bytes.in}/@{bytes.out}`;
|
||||||
|
|
||||||
|
@ -316,7 +297,7 @@ export function log(config: Config) {
|
||||||
{
|
{
|
||||||
request: {
|
request: {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
url: req.url
|
url: req.url,
|
||||||
},
|
},
|
||||||
level: 35, // http
|
level: 35, // http
|
||||||
user: (req.remote_user && req.remote_user.name) || null,
|
user: (req.remote_user && req.remote_user.name) || null,
|
||||||
|
@ -325,8 +306,8 @@ export function log(config: Config) {
|
||||||
error: res._verdaccio_error,
|
error: res._verdaccio_error,
|
||||||
bytes: {
|
bytes: {
|
||||||
in: bytesin,
|
in: bytesin,
|
||||||
out: bytesout
|
out: bytesout,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
message
|
message
|
||||||
);
|
);
|
||||||
|
@ -353,11 +334,7 @@ export function log(config: Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
export function errorReportingMiddleware(
|
export function errorReportingMiddleware(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer
|
|
||||||
): void {
|
|
||||||
res.report_error =
|
res.report_error =
|
||||||
res.report_error ||
|
res.report_error ||
|
||||||
function (err: VerdaccioError): void {
|
function (err: VerdaccioError): void {
|
||||||
|
|
|
@ -10,20 +10,14 @@ import {
|
||||||
formatAuthor,
|
formatAuthor,
|
||||||
convertDistRemoteToLocalTarballUrls,
|
convertDistRemoteToLocalTarballUrls,
|
||||||
getLocalRegistryTarballUri,
|
getLocalRegistryTarballUri,
|
||||||
isVersionValid
|
isVersionValid,
|
||||||
|
ErrorCode,
|
||||||
} from '../../../lib/utils';
|
} from '../../../lib/utils';
|
||||||
import { allow } from '../../middleware';
|
import { allow } from '../../middleware';
|
||||||
import { DIST_TAGS, HEADER_TYPE, HEADERS, HTTP_STATUS } from '../../../lib/constants';
|
import { DIST_TAGS, HEADER_TYPE, HEADERS, HTTP_STATUS } from '../../../lib/constants';
|
||||||
import { generateGravatarUrl } from '../../../utils/user';
|
import { generateGravatarUrl } from '../../../utils/user';
|
||||||
import { logger } from '../../../lib/logger';
|
import { logger } from '../../../lib/logger';
|
||||||
import {
|
import { IAuth, $ResponseExtend, $RequestExtend, $NextFunctionVer, IStorageHandler, $SidebarPackage } from '../../../../types';
|
||||||
IAuth,
|
|
||||||
$ResponseExtend,
|
|
||||||
$RequestExtend,
|
|
||||||
$NextFunctionVer,
|
|
||||||
IStorageHandler,
|
|
||||||
$SidebarPackage
|
|
||||||
} from '../../../../types';
|
|
||||||
|
|
||||||
const getOrder = (order = 'asc') => {
|
const getOrder = (order = 'asc') => {
|
||||||
return order === 'asc';
|
return order === 'asc';
|
||||||
|
@ -31,12 +25,7 @@ const getOrder = (order = 'asc') => {
|
||||||
|
|
||||||
export type PackcageExt = Package & { author: any; dist?: { tarball: string } };
|
export type PackcageExt = Package & { author: any; dist?: { tarball: string } };
|
||||||
|
|
||||||
function addPackageWebApi(
|
function addPackageWebApi(route: Router, storage: IStorageHandler, auth: IAuth, config: Config): void {
|
||||||
route: Router,
|
|
||||||
storage: IStorageHandler,
|
|
||||||
auth: IAuth,
|
|
||||||
config: Config
|
|
||||||
): void {
|
|
||||||
const can = allow(auth);
|
const can = allow(auth);
|
||||||
|
|
||||||
const checkAllow = (name, remoteUser): Promise<boolean> =>
|
const checkAllow = (name, remoteUser): Promise<boolean> =>
|
||||||
|
@ -54,9 +43,7 @@ function addPackageWebApi(
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get list of all visible package
|
// Get list of all visible package
|
||||||
route.get(
|
route.get('/packages', function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||||
'/packages',
|
|
||||||
function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
|
||||||
storage.getLocalDatabase(async function (err, packages): Promise<void> {
|
storage.getLocalDatabase(async function (err, packages): Promise<void> {
|
||||||
if (err) {
|
if (err) {
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -71,26 +58,15 @@ function addPackageWebApi(
|
||||||
try {
|
try {
|
||||||
if (await checkAllow(pkg.name, req.remote_user)) {
|
if (await checkAllow(pkg.name, req.remote_user)) {
|
||||||
if (config.web) {
|
if (config.web) {
|
||||||
pkgCopy.author.avatar = generateGravatarUrl(
|
pkgCopy.author.avatar = generateGravatarUrl(pkgCopy.author.email, config.web.gravatar);
|
||||||
pkgCopy.author.email,
|
|
||||||
config.web.gravatar
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (!_.isNil(pkgCopy.dist) && !_.isNull(pkgCopy.dist.tarball)) {
|
if (!_.isNil(pkgCopy.dist) && !_.isNull(pkgCopy.dist.tarball)) {
|
||||||
pkgCopy.dist.tarball = getLocalRegistryTarballUri(
|
pkgCopy.dist.tarball = getLocalRegistryTarballUri(pkgCopy.dist.tarball, pkg.name, req, config.url_prefix);
|
||||||
pkgCopy.dist.tarball,
|
|
||||||
pkg.name,
|
|
||||||
req,
|
|
||||||
config.url_prefix
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
permissions.push(pkgCopy);
|
permissions.push(pkgCopy);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.logger.error(
|
logger.error({ name: pkg.name, error: err }, 'permission process for @{name} has failed: @{error}');
|
||||||
{ name: pkg.name, error: err },
|
|
||||||
'permission process for @{name} has failed: @{error}'
|
|
||||||
);
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,19 +78,17 @@ function addPackageWebApi(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const order: boolean = config.web ? getOrder(web.sort_packages) : true;
|
const order: boolean = config.web ? getOrder(web.sort_packages) : true;
|
||||||
|
|
||||||
|
try {
|
||||||
next(sortByName(await processPackages(packages), order));
|
next(sortByName(await processPackages(packages), order));
|
||||||
});
|
} catch (error) {
|
||||||
|
next(ErrorCode.getInternalError());
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Get package readme
|
// Get package readme
|
||||||
route.get(
|
route.get('/package/readme/(@:scope/)?:package/:version?', can('access'), function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||||
'/package/readme/(@:scope/)?:package/:version?',
|
const packageName = req.params.scope ? addScope(req.params.scope, req.params.package) : req.params.package;
|
||||||
can('access'),
|
|
||||||
function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
|
||||||
const packageName = req.params.scope
|
|
||||||
? addScope(req.params.scope, req.params.package)
|
|
||||||
: req.params.package;
|
|
||||||
|
|
||||||
storage.getPackage({
|
storage.getPackage({
|
||||||
name: packageName,
|
name: packageName,
|
||||||
|
@ -127,18 +101,12 @@ function addPackageWebApi(
|
||||||
|
|
||||||
res.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.TEXT_PLAIN);
|
res.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.TEXT_PLAIN);
|
||||||
next(parseReadme(info.name, info.readme));
|
next(parseReadme(info.name, info.readme));
|
||||||
}
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
route.get(
|
route.get('/sidebar/(@:scope/)?:package', can('access'), function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||||
'/sidebar/(@:scope/)?:package',
|
const packageName: string = req.params.scope ? addScope(req.params.scope, req.params.package) : req.params.package;
|
||||||
can('access'),
|
|
||||||
function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
|
||||||
const packageName: string = req.params.scope
|
|
||||||
? addScope(req.params.scope, req.params.package)
|
|
||||||
: req.params.package;
|
|
||||||
|
|
||||||
storage.getPackage({
|
storage.getPackage({
|
||||||
name: packageName,
|
name: packageName,
|
||||||
|
@ -149,11 +117,7 @@ function addPackageWebApi(
|
||||||
if (_.isNil(err)) {
|
if (_.isNil(err)) {
|
||||||
const { v } = req.query;
|
const { v } = req.query;
|
||||||
let sideBarInfo: any = _.clone(info);
|
let sideBarInfo: any = _.clone(info);
|
||||||
sideBarInfo.versions = convertDistRemoteToLocalTarballUrls(
|
sideBarInfo.versions = convertDistRemoteToLocalTarballUrls(info, req, config.url_prefix).versions;
|
||||||
info,
|
|
||||||
req,
|
|
||||||
config.url_prefix
|
|
||||||
).versions;
|
|
||||||
if (isVersionValid(info, v)) {
|
if (isVersionValid(info, v)) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
sideBarInfo.latest = sideBarInfo.versions[v];
|
sideBarInfo.latest = sideBarInfo.versions[v];
|
||||||
|
@ -179,10 +143,9 @@ function addPackageWebApi(
|
||||||
res.status(HTTP_STATUS.NOT_FOUND);
|
res.status(HTTP_STATUS.NOT_FOUND);
|
||||||
res.end();
|
res.end();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default addPackageWebApi;
|
export default addPackageWebApi;
|
||||||
|
|
BIN
src/api/web/html/favicon.ico
Normal file
BIN
src/api/web/html/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
19
src/api/web/html/manifest.ts
Normal file
19
src/api/web/html/manifest.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import buildDebug from 'debug';
|
||||||
|
|
||||||
|
export type Manifest = {
|
||||||
|
// goes on first place at the header
|
||||||
|
ico: string;
|
||||||
|
css: string[];
|
||||||
|
js: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const debug = buildDebug('verdaccio');
|
||||||
|
|
||||||
|
export function getManifestValue(manifestItems: string[], manifest, basePath: string = ''): string[] {
|
||||||
|
return manifestItems?.map((item) => {
|
||||||
|
debug('resolve item %o', item);
|
||||||
|
const resolvedItem = `${basePath}${manifest[item]}`;
|
||||||
|
debug('resolved item %o', resolvedItem);
|
||||||
|
return resolvedItem;
|
||||||
|
});
|
||||||
|
}
|
95
src/api/web/html/renderHTML.ts
Normal file
95
src/api/web/html/renderHTML.ts
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import { URL } from 'url';
|
||||||
|
import buildDebug from 'debug';
|
||||||
|
import LRU from 'lru-cache';
|
||||||
|
import { HEADERS } from '@verdaccio/commons-api';
|
||||||
|
|
||||||
|
import { getPublicUrl } from '../../../lib/utils';
|
||||||
|
import { WEB_TITLE } from '../../../lib/constants';
|
||||||
|
import renderTemplate from './template';
|
||||||
|
|
||||||
|
const pkgJSON = require('../../../../package.json');
|
||||||
|
const DEFAULT_LANGUAGE = 'es-US';
|
||||||
|
const cache = new LRU({ max: 500, maxAge: 1000 * 60 * 60 });
|
||||||
|
|
||||||
|
const debug = buildDebug('verdaccio');
|
||||||
|
|
||||||
|
const defaultManifestFiles = {
|
||||||
|
js: ['runtime.js', 'vendors.js', 'main.js'],
|
||||||
|
ico: 'favicon.ico',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function validatePrimaryColor(primaryColor) {
|
||||||
|
const isHex = /^#+([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/i.test(primaryColor);
|
||||||
|
if (!isHex) {
|
||||||
|
debug('invalid primary color %o', primaryColor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return primaryColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function renderHTML(config, manifest, manifestFiles, req, res) {
|
||||||
|
const { url_prefix } = config;
|
||||||
|
const base = getPublicUrl(config?.url_prefix, req);
|
||||||
|
const basename = new URL(base).pathname;
|
||||||
|
const language = config?.i18n?.web ?? DEFAULT_LANGUAGE;
|
||||||
|
const darkMode = config?.web?.darkMode ?? false;
|
||||||
|
const title = config?.web?.title ?? WEB_TITLE;
|
||||||
|
const scope = config?.web?.scope ?? '';
|
||||||
|
// FIXME: logo URI is incomplete
|
||||||
|
let logoURI = config?.web?.logo ?? '';
|
||||||
|
const version = pkgJSON.version;
|
||||||
|
const primaryColor = validatePrimaryColor(config?.web?.primary_color) ?? '#4b5e40';
|
||||||
|
const { scriptsBodyAfter, metaScripts, scriptsbodyBefore } = Object.assign(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
scriptsBodyAfter: [],
|
||||||
|
bodyBefore: [],
|
||||||
|
metaScripts: [],
|
||||||
|
},
|
||||||
|
config?.web
|
||||||
|
);
|
||||||
|
const options = {
|
||||||
|
darkMode,
|
||||||
|
url_prefix,
|
||||||
|
basename,
|
||||||
|
base,
|
||||||
|
primaryColor,
|
||||||
|
version,
|
||||||
|
logoURI,
|
||||||
|
title,
|
||||||
|
scope,
|
||||||
|
language,
|
||||||
|
};
|
||||||
|
|
||||||
|
let webPage;
|
||||||
|
|
||||||
|
try {
|
||||||
|
webPage = cache.get('template');
|
||||||
|
|
||||||
|
if (!webPage) {
|
||||||
|
debug('web options %o', options);
|
||||||
|
debug('web manifestFiles %o', manifestFiles);
|
||||||
|
webPage = renderTemplate(
|
||||||
|
{
|
||||||
|
manifest: manifestFiles ?? defaultManifestFiles,
|
||||||
|
options,
|
||||||
|
scriptsBodyAfter,
|
||||||
|
metaScripts,
|
||||||
|
scriptsbodyBefore,
|
||||||
|
},
|
||||||
|
manifest
|
||||||
|
);
|
||||||
|
debug('template :: %o', webPage);
|
||||||
|
cache.set('template', webPage);
|
||||||
|
debug('set template cache');
|
||||||
|
} else {
|
||||||
|
debug('reuse template cache');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`theme could not be load, stack ${error.stack}`);
|
||||||
|
}
|
||||||
|
res.setHeader('Content-Type', HEADERS.TEXT_HTML);
|
||||||
|
res.send(webPage);
|
||||||
|
debug('render web');
|
||||||
|
}
|
60
src/api/web/html/template.ts
Normal file
60
src/api/web/html/template.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import buildDebug from 'debug';
|
||||||
|
import { getManifestValue, Manifest } from './manifest';
|
||||||
|
|
||||||
|
const debug = buildDebug('verdaccio');
|
||||||
|
|
||||||
|
export type TemplateUIOptions = {
|
||||||
|
title?: string;
|
||||||
|
uri?: string;
|
||||||
|
darkMode?: boolean;
|
||||||
|
protocol?: string;
|
||||||
|
host?: string;
|
||||||
|
url_prefix?: string;
|
||||||
|
base: string;
|
||||||
|
primaryColor?: string;
|
||||||
|
version?: string;
|
||||||
|
logoURI?: string;
|
||||||
|
scope?: string;
|
||||||
|
language?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Template = {
|
||||||
|
manifest: Manifest;
|
||||||
|
options: TemplateUIOptions;
|
||||||
|
metaScripts?: string[];
|
||||||
|
scriptsBodyAfter?: string[];
|
||||||
|
scriptsbodyBefore?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// the outcome of the Webpack Manifest Plugin
|
||||||
|
export interface WebpackManifest {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function renderTemplate(template: Template, manifest: WebpackManifest) {
|
||||||
|
debug('template %o', template);
|
||||||
|
debug('manifest %o', manifest);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en-us">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<base href="${template?.options.base}">
|
||||||
|
<title>${template?.options?.title ?? ''}</title>
|
||||||
|
<link rel="icon" href="${template?.options.base}-/static/favicon.ico"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<script>
|
||||||
|
window.__VERDACCIO_BASENAME_UI_OPTIONS=${JSON.stringify(template.options)}
|
||||||
|
</script>
|
||||||
|
${template?.metaScripts ? template.metaScripts.join('') : ''}
|
||||||
|
</head>
|
||||||
|
<body class="body">
|
||||||
|
${template?.scriptsbodyBefore ? template.scriptsbodyBefore.join('') : ''}
|
||||||
|
<div id="root"></div>
|
||||||
|
${getManifestValue(template.manifest.js, manifest, template?.options.base).map((item) => `<script defer="defer" src="${item}"></script>`).join('')}
|
||||||
|
${template?.scriptsBodyAfter ? template.scriptsBodyAfter.join('') : ''}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
|
@ -1,22 +1,15 @@
|
||||||
/**
|
|
||||||
* @prettier
|
|
||||||
*/
|
|
||||||
|
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
import buildDebug from 'debug';
|
||||||
|
|
||||||
import { combineBaseUrl, getWebProtocol, isHTTPProtocol } from '../../lib/utils';
|
|
||||||
import Search from '../../lib/search';
|
import Search from '../../lib/search';
|
||||||
import { HEADERS, HTTP_STATUS, WEB_TITLE } from '../../lib/constants';
|
import { HTTP_STATUS } from '../../lib/constants';
|
||||||
import loadPlugin from '../../lib/plugin-loader';
|
import loadPlugin from '../../lib/plugin-loader';
|
||||||
|
import renderHTML from './html/renderHTML';
|
||||||
|
|
||||||
const { setSecurityWebHeaders } = require('../middleware');
|
const { setSecurityWebHeaders } = require('../middleware');
|
||||||
const pkgJSON = require('../../../package.json');
|
|
||||||
|
|
||||||
const DEFAULT_LANGUAGE = 'es-US';
|
const debug = buildDebug('verdaccio');
|
||||||
|
|
||||||
export function loadTheme(config) {
|
export function loadTheme(config) {
|
||||||
if (_.isNil(config.theme) === false) {
|
if (_.isNil(config.theme) === false) {
|
||||||
|
@ -37,7 +30,8 @@ export function loadTheme(config) {
|
||||||
export function validatePrimaryColor(primaryColor) {
|
export function validatePrimaryColor(primaryColor) {
|
||||||
const isHex = /^#+([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/i.test(primaryColor);
|
const isHex = /^#+([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/i.test(primaryColor);
|
||||||
if (!isHex) {
|
if (!isHex) {
|
||||||
return '';
|
debug('invalid primary color %o', primaryColor);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return primaryColor;
|
return primaryColor;
|
||||||
|
@ -55,81 +49,31 @@ const sendFileCallback = (next) => (err) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function (config, auth, storage) {
|
export default function (config, auth, storage) {
|
||||||
|
let { staticPath, manifest, manifestFiles } = loadTheme(config) || require('@verdaccio/ui-theme')();
|
||||||
|
debug('static path %o', staticPath);
|
||||||
Search.configureStorage(storage);
|
Search.configureStorage(storage);
|
||||||
|
|
||||||
/* eslint new-cap:off */
|
/* eslint new-cap:off */
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.use(auth.webUIJWTmiddleware());
|
router.use(auth.webUIJWTmiddleware());
|
||||||
router.use(setSecurityWebHeaders);
|
router.use(setSecurityWebHeaders);
|
||||||
const themePath = loadTheme(config) || require('@verdaccio/ui-theme')();
|
|
||||||
const indexTemplate = path.join(themePath, 'index.html');
|
|
||||||
const template = fs.readFileSync(indexTemplate).toString();
|
|
||||||
|
|
||||||
// Logo
|
// static assets
|
||||||
let logoURI = _.get(config, 'web.logo') ? config.web.logo : '';
|
|
||||||
if (logoURI && !isHTTPProtocol(logoURI)) {
|
|
||||||
// URI related to a local file
|
|
||||||
|
|
||||||
// Note: `path.join` will break on Windows, because it transforms `/` to `\`
|
|
||||||
// Use POSIX version `path.posix.join` instead.
|
|
||||||
logoURI = path.posix.join('/-/static/', path.basename(logoURI));
|
|
||||||
router.get(logoURI, function (req, res, next) {
|
|
||||||
res.sendFile(path.resolve(config.web.logo), sendFileCallback(next));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static
|
|
||||||
router.get('/-/static/*', function (req, res, next) {
|
router.get('/-/static/*', function (req, res, next) {
|
||||||
const filename = req.params[0];
|
const filename = req.params[0];
|
||||||
const file = `${themePath}/${filename}`;
|
const file = `${staticPath}/${filename}`;
|
||||||
|
debug('render static file %o', file);
|
||||||
res.sendFile(file, sendFileCallback(next));
|
res.sendFile(file, sendFileCallback(next));
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderHTML(req, res) {
|
|
||||||
const protocol = getWebProtocol(req.get(HEADERS.FORWARDED_PROTO), req.protocol);
|
|
||||||
const host = req.get('host');
|
|
||||||
const { url_prefix } = config;
|
|
||||||
const uri = `${protocol}://${host}`;
|
|
||||||
const base = combineBaseUrl(protocol, host, url_prefix);
|
|
||||||
const language = config?.i18n?.web ?? DEFAULT_LANGUAGE;
|
|
||||||
const darkMode = config?.web?.darkMode ?? false;
|
|
||||||
const primaryColor = validatePrimaryColor(config?.web?.primary_color);
|
|
||||||
const title = _.get(config, 'web.title') ? config.web.title : WEB_TITLE;
|
|
||||||
const scope = _.get(config, 'web.scope') ? config.web.scope : '';
|
|
||||||
const options = {
|
|
||||||
uri,
|
|
||||||
darkMode,
|
|
||||||
protocol,
|
|
||||||
host,
|
|
||||||
url_prefix,
|
|
||||||
base,
|
|
||||||
primaryColor,
|
|
||||||
title,
|
|
||||||
scope,
|
|
||||||
language
|
|
||||||
};
|
|
||||||
|
|
||||||
const webPage = template
|
|
||||||
.replace(/ToReplaceByVerdaccioUI/g, JSON.stringify(options))
|
|
||||||
.replace(/ToReplaceByVerdaccio/g, base)
|
|
||||||
.replace(/ToReplaceByPrefix/g, url_prefix)
|
|
||||||
.replace(/ToReplaceByVersion/g, pkgJSON.version)
|
|
||||||
.replace(/ToReplaceByTitle/g, title)
|
|
||||||
.replace(/ToReplaceByLogo/g, logoURI)
|
|
||||||
.replace(/ToReplaceByPrimaryColor/g, primaryColor)
|
|
||||||
.replace(/ToReplaceByScope/g, scope);
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', HEADERS.TEXT_HTML);
|
|
||||||
|
|
||||||
res.send(webPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
router.get('/-/web/:section/*', function (req, res) {
|
router.get('/-/web/:section/*', function (req, res) {
|
||||||
renderHTML(req, res);
|
renderHTML(config, manifest, manifestFiles, req, res);
|
||||||
|
debug('render html section');
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/', function (req, res) {
|
router.get('/', function (req, res) {
|
||||||
renderHTML(req, res);
|
renderHTML(config, manifest, manifestFiles, req, res);
|
||||||
|
debug('render root');
|
||||||
});
|
});
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|
134
src/lib/utils.ts
134
src/lib/utils.ts
|
@ -1,17 +1,16 @@
|
||||||
/**
|
|
||||||
* @prettier
|
|
||||||
*/
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import URL from 'url';
|
import DefaultURL, { URL } from 'url';
|
||||||
import { IncomingHttpHeaders } from 'http2';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import buildDebug from 'debug';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
import YAML from 'js-yaml';
|
import YAML from 'js-yaml';
|
||||||
|
import validator from 'validator';
|
||||||
import sanitizyReadme from '@verdaccio/readme';
|
import sanitizyReadme from '@verdaccio/readme';
|
||||||
|
|
||||||
import { Package, Version, Author } from '@verdaccio/types';
|
import { Package, Version, Author } from '@verdaccio/types';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
import { getConflict, getBadData, getBadRequest, getInternalError, getUnauthorized, getForbidden, getServiceUnavailable, getNotFound, getCode } from '@verdaccio/commons-api';
|
import { getConflict, getBadData, getBadRequest, getInternalError, getUnauthorized, getForbidden, getServiceUnavailable, getNotFound, getCode } from '@verdaccio/commons-api';
|
||||||
import { generateGravatarUrl, GENERIC_AVATAR } from '../utils/user';
|
import { generateGravatarUrl, GENERIC_AVATAR } from '../utils/user';
|
||||||
import { StringValue, AuthorAvatar } from '../../types';
|
import { StringValue, AuthorAvatar } from '../../types';
|
||||||
|
@ -21,11 +20,14 @@ import { normalizeContributors } from './storage-utils';
|
||||||
|
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
|
|
||||||
|
const debug = buildDebug('verdaccio');
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
require('pkginfo')(module);
|
require('pkginfo')(module);
|
||||||
const pkgVersion = module.exports.version;
|
const pkgVersion = module.exports.version;
|
||||||
const pkgName = module.exports.name;
|
const pkgName = module.exports.name;
|
||||||
|
const validProtocols = ['https', 'http'];
|
||||||
|
|
||||||
export function getUserAgent(): string {
|
export function getUserAgent(): string {
|
||||||
assert(_.isString(pkgName));
|
assert(_.isString(pkgName));
|
||||||
|
@ -116,32 +118,9 @@ export function validateMetadata(object: Package, name: string): Package {
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create base url for registry.
|
|
||||||
* @return {String} base registry url
|
|
||||||
*/
|
|
||||||
export function combineBaseUrl(protocol: string, host: string | void, prefix?: string | void): string {
|
|
||||||
const result = `${protocol}://${host}`;
|
|
||||||
|
|
||||||
const prefixOnlySlash = prefix === '/';
|
|
||||||
if (prefix && !prefixOnlySlash) {
|
|
||||||
if (prefix.endsWith('/')) {
|
|
||||||
prefix = prefix.slice(0, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prefix.startsWith('/')) {
|
|
||||||
return `${result}${prefix}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return prefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extractTarballFromUrl(url: string): string {
|
export function extractTarballFromUrl(url: string): string {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return URL.parse(url).pathname.replace(/^.*\//, '');
|
return DefaultURL.parse(url).pathname.replace(/^.*\//, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -176,20 +155,11 @@ export function getLocalRegistryTarballUri(uri: string, pkgName: string, req: Re
|
||||||
return uri;
|
return uri;
|
||||||
}
|
}
|
||||||
const tarballName = extractTarballFromUrl(uri);
|
const tarballName = extractTarballFromUrl(uri);
|
||||||
const headers = req.headers as IncomingHttpHeaders;
|
const domainRegistry = getPublicUrl(urlPrefix || '', req);
|
||||||
const protocol = getWebProtocol(req.get(HEADERS.FORWARDED_PROTO), req.protocol);
|
|
||||||
const domainRegistry = combineBaseUrl(protocol, headers.host, urlPrefix);
|
|
||||||
|
|
||||||
return `${domainRegistry}/${encodeScopedUri(pkgName)}/-/${tarballName}`;
|
return `${domainRegistry}${encodeScopedUri(pkgName)}/-/${tarballName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a tag for a package
|
|
||||||
* @param {*} data
|
|
||||||
* @param {*} version
|
|
||||||
* @param {*} tag
|
|
||||||
* @return {Boolean} whether a package has been tagged
|
|
||||||
*/
|
|
||||||
export function tagVersion(data: Package, version: string, tag: StringValue): boolean {
|
export function tagVersion(data: Package, version: string, tag: StringValue): boolean {
|
||||||
if (tag && data[DIST_TAGS][tag] !== version && semver.parse(version, true)) {
|
if (tag && data[DIST_TAGS][tag] !== version && semver.parse(version, true)) {
|
||||||
// valid version - store
|
// valid version - store
|
||||||
|
@ -362,12 +332,19 @@ export function parseInterval(interval: any): number {
|
||||||
* Detect running protocol (http or https)
|
* Detect running protocol (http or https)
|
||||||
*/
|
*/
|
||||||
export function getWebProtocol(headerProtocol: string | void, protocol: string): string {
|
export function getWebProtocol(headerProtocol: string | void, protocol: string): string {
|
||||||
|
let returnProtocol;
|
||||||
|
const [, defaultProtocol] = validProtocols;
|
||||||
|
// HAProxy variant might return http,http with X-Forwarded-Proto
|
||||||
if (typeof headerProtocol === 'string' && headerProtocol !== '') {
|
if (typeof headerProtocol === 'string' && headerProtocol !== '') {
|
||||||
|
debug('header protocol: %o', protocol);
|
||||||
const commaIndex = headerProtocol.indexOf(',');
|
const commaIndex = headerProtocol.indexOf(',');
|
||||||
return commaIndex > 0 ? headerProtocol.substr(0, commaIndex) : headerProtocol;
|
returnProtocol = commaIndex > 0 ? headerProtocol.substr(0, commaIndex) : headerProtocol;
|
||||||
|
} else {
|
||||||
|
debug('req protocol: %o', headerProtocol);
|
||||||
|
returnProtocol = protocol;
|
||||||
}
|
}
|
||||||
|
|
||||||
return protocol;
|
return validProtocols.includes(returnProtocol) ? returnProtocol : defaultProtocol;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLatestVersion(pkgInfo: Package): string {
|
export function getLatestVersion(pkgInfo: Package): string {
|
||||||
|
@ -623,3 +600,76 @@ export function isRelatedToDeprecation(pkgInfo: Package): boolean {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validateURL(publicUrl: string | void) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(publicUrl as string);
|
||||||
|
if (!validProtocols.includes(parsed.protocol.replace(':', ''))) {
|
||||||
|
throw Error('invalid protocol');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
// TODO: add error logger here
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isHost(url: string = '', options = {}): boolean {
|
||||||
|
return validator.isURL(url, {
|
||||||
|
require_host: true,
|
||||||
|
allow_trailing_dot: false,
|
||||||
|
require_valid_protocol: false,
|
||||||
|
// @ts-ignore
|
||||||
|
require_port: false,
|
||||||
|
require_tld: false,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPublicUrl(url_prefix: string = '', req): string {
|
||||||
|
if (validateURL(process.env.VERDACCIO_PUBLIC_URL as string)) {
|
||||||
|
const envURL = new URL(wrapPrefix(url_prefix), process.env.VERDACCIO_PUBLIC_URL as string).href;
|
||||||
|
debug('public url by env %o', envURL);
|
||||||
|
return envURL;
|
||||||
|
} else if (req.get('host')) {
|
||||||
|
const host = req.get('host');
|
||||||
|
if (!isHost(host)) {
|
||||||
|
throw new Error('invalid host');
|
||||||
|
}
|
||||||
|
const protocol = getWebProtocol(req.get(HEADERS.FORWARDED_PROTO), req.protocol);
|
||||||
|
const combinedUrl = combineBaseUrl(protocol, host, url_prefix);
|
||||||
|
debug('public url by request %o', combinedUrl);
|
||||||
|
return combinedUrl;
|
||||||
|
} else {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create base url for registry.
|
||||||
|
* @return {String} base registry url
|
||||||
|
*/
|
||||||
|
export function combineBaseUrl(protocol: string, host: string, prefix: string = ''): string {
|
||||||
|
debug('combined protocol %o', protocol);
|
||||||
|
debug('combined host %o', host);
|
||||||
|
const newPrefix = wrapPrefix(prefix);
|
||||||
|
debug('combined prefix %o', newPrefix);
|
||||||
|
const groupedURI = new URL(wrapPrefix(prefix), `${protocol}://${host}`);
|
||||||
|
const result = groupedURI.href;
|
||||||
|
debug('combined url %o', result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wrapPrefix(prefix: string | void): string {
|
||||||
|
if (prefix === '' || typeof prefix === 'undefined' || prefix === null) {
|
||||||
|
return '';
|
||||||
|
} else if (!prefix.startsWith('/') && prefix.endsWith('/')) {
|
||||||
|
return `/${prefix}`;
|
||||||
|
} else if (!prefix.startsWith('/') && !prefix.endsWith('/')) {
|
||||||
|
return `/${prefix}/`;
|
||||||
|
} else if (prefix.startsWith('/') && !prefix.endsWith('/')) {
|
||||||
|
return `${prefix}/`;
|
||||||
|
} else {
|
||||||
|
return prefix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ web:
|
||||||
|
|
||||||
uplinks:
|
uplinks:
|
||||||
npmjs:
|
npmjs:
|
||||||
url: https://registry.npmjs.org/
|
url: https://registry.verdaccio.org/
|
||||||
|
|
||||||
logs:
|
logs:
|
||||||
- { type: stdout, format: pretty, level: warn }
|
- { type: stdout, format: pretty, level: warn }
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import * as httpMocks from 'node-mocks-http';
|
||||||
|
import { HEADERS } from '@verdaccio/commons-api';
|
||||||
import { generateGravatarUrl, GENERIC_AVATAR } from '../../../../src/utils/user';
|
import { generateGravatarUrl, GENERIC_AVATAR } from '../../../../src/utils/user';
|
||||||
import { spliceURL } from '../../../../src/utils/string';
|
import { spliceURL } from '../../../../src/utils/string';
|
||||||
import {
|
import {
|
||||||
|
@ -14,7 +16,8 @@ import {
|
||||||
getVersionFromTarball,
|
getVersionFromTarball,
|
||||||
sortByName,
|
sortByName,
|
||||||
formatAuthor,
|
formatAuthor,
|
||||||
isHTTPProtocol
|
isHTTPProtocol,
|
||||||
|
getPublicUrl,
|
||||||
} from '../../../../src/lib/utils';
|
} from '../../../../src/lib/utils';
|
||||||
import { DIST_TAGS, DEFAULT_USER } from '../../../../src/lib/constants';
|
import { DIST_TAGS, DEFAULT_USER } from '../../../../src/lib/constants';
|
||||||
import { logger, setup } from '../../../../src/lib/logger';
|
import { logger, setup } from '../../../../src/lib/logger';
|
||||||
|
@ -32,15 +35,15 @@ describe('Utilities', () => {
|
||||||
versions: {
|
versions: {
|
||||||
'1.0.0': {
|
'1.0.0': {
|
||||||
dist: {
|
dist: {
|
||||||
tarball: 'http://registry.org/npm_test/-/npm_test-1.0.0.tgz'
|
tarball: 'http://registry.org/npm_test/-/npm_test-1.0.0.tgz',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
'1.0.1': {
|
'1.0.1': {
|
||||||
dist: {
|
dist: {
|
||||||
tarball: 'http://registry.org/npm_test/-/npm_test-1.0.1.tgz'
|
tarball: 'http://registry.org/npm_test/-/npm_test-1.0.1.tgz',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const cloneMetadata = (pkg = metadata) => Object.assign({}, pkg);
|
const cloneMetadata = (pkg = metadata) => Object.assign({}, pkg);
|
||||||
|
@ -49,40 +52,40 @@ describe('Utilities', () => {
|
||||||
describe('Sort packages', () => {
|
describe('Sort packages', () => {
|
||||||
const packages = [
|
const packages = [
|
||||||
{
|
{
|
||||||
name: 'ghc'
|
name: 'ghc',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'abc'
|
name: 'abc',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'zxy'
|
name: 'zxy',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
test('should order ascending', () => {
|
test('should order ascending', () => {
|
||||||
expect(sortByName(packages)).toEqual([
|
expect(sortByName(packages)).toEqual([
|
||||||
{
|
{
|
||||||
name: 'abc'
|
name: 'abc',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ghc'
|
name: 'ghc',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'zxy'
|
name: 'zxy',
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should order descending', () => {
|
test('should order descending', () => {
|
||||||
expect(sortByName(packages, false)).toEqual([
|
expect(sortByName(packages, false)).toEqual([
|
||||||
{
|
{
|
||||||
name: 'zxy'
|
name: 'zxy',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ghc'
|
name: 'ghc',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'abc'
|
name: 'abc',
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -104,9 +107,12 @@ describe('Utilities', () => {
|
||||||
expect(getWebProtocol('https', '')).toBe('https');
|
expect(getWebProtocol('https', '')).toBe('https');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should have handle invalid protocol', () => {
|
||||||
|
expect(getWebProtocol('ftp', '')).toBe('http');
|
||||||
|
});
|
||||||
|
|
||||||
describe('getWebProtocol and HAProxy variant', () => {
|
describe('getWebProtocol and HAProxy variant', () => {
|
||||||
// https://github.com/verdaccio/verdaccio/issues/695
|
// https://github.com/verdaccio/verdaccio/issues/695
|
||||||
|
|
||||||
test('should handle http', () => {
|
test('should handle http', () => {
|
||||||
expect(getWebProtocol('http,http', 'https')).toBe('http');
|
expect(getWebProtocol('http,http', 'https')).toBe('http');
|
||||||
});
|
});
|
||||||
|
@ -119,14 +125,15 @@ describe('Utilities', () => {
|
||||||
|
|
||||||
describe('convertDistRemoteToLocalTarballUrls', () => {
|
describe('convertDistRemoteToLocalTarballUrls', () => {
|
||||||
test('should build a URI for dist tarball based on new domain', () => {
|
test('should build a URI for dist tarball based on new domain', () => {
|
||||||
const convertDist = convertDistRemoteToLocalTarballUrls(cloneMetadata(), {
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
host: fakeHost
|
host: fakeHost,
|
||||||
},
|
},
|
||||||
// @ts-ignore
|
protocol: 'http',
|
||||||
get: () => 'http',
|
url: '/',
|
||||||
protocol: 'http'
|
|
||||||
});
|
});
|
||||||
|
const convertDist = convertDistRemoteToLocalTarballUrls(cloneMetadata(), req);
|
||||||
expect(convertDist.versions['1.0.0'].dist.tarball).toEqual(buildURI(fakeHost, '1.0.0'));
|
expect(convertDist.versions['1.0.0'].dist.tarball).toEqual(buildURI(fakeHost, '1.0.0'));
|
||||||
expect(convertDist.versions['1.0.1'].dist.tarball).toEqual(buildURI(fakeHost, '1.0.1'));
|
expect(convertDist.versions['1.0.1'].dist.tarball).toEqual(buildURI(fakeHost, '1.0.1'));
|
||||||
});
|
});
|
||||||
|
@ -136,11 +143,9 @@ describe('Utilities', () => {
|
||||||
headers: {},
|
headers: {},
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
get: () => 'http',
|
get: () => 'http',
|
||||||
protocol: 'http'
|
protocol: 'http',
|
||||||
});
|
});
|
||||||
expect(convertDist.versions['1.0.0'].dist.tarball).toEqual(
|
expect(convertDist.versions['1.0.0'].dist.tarball).toEqual(convertDist.versions['1.0.0'].dist.tarball);
|
||||||
convertDist.versions['1.0.0'].dist.tarball
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -148,7 +153,7 @@ describe('Utilities', () => {
|
||||||
test('should delete a invalid latest version', () => {
|
test('should delete a invalid latest version', () => {
|
||||||
const pkg = cloneMetadata();
|
const pkg = cloneMetadata();
|
||||||
pkg[DIST_TAGS] = {
|
pkg[DIST_TAGS] = {
|
||||||
latest: '20000'
|
latest: '20000',
|
||||||
};
|
};
|
||||||
|
|
||||||
normalizeDistTags(pkg);
|
normalizeDistTags(pkg);
|
||||||
|
@ -168,7 +173,7 @@ describe('Utilities', () => {
|
||||||
test('should define last published version as latest with a custom dist-tag', () => {
|
test('should define last published version as latest with a custom dist-tag', () => {
|
||||||
const pkg = cloneMetadata();
|
const pkg = cloneMetadata();
|
||||||
pkg[DIST_TAGS] = {
|
pkg[DIST_TAGS] = {
|
||||||
beta: '1.0.1'
|
beta: '1.0.1',
|
||||||
};
|
};
|
||||||
|
|
||||||
normalizeDistTags(pkg);
|
normalizeDistTags(pkg);
|
||||||
|
@ -179,7 +184,7 @@ describe('Utilities', () => {
|
||||||
test('should convert any array of dist-tags to a plain string', () => {
|
test('should convert any array of dist-tags to a plain string', () => {
|
||||||
const pkg = cloneMetadata();
|
const pkg = cloneMetadata();
|
||||||
pkg[DIST_TAGS] = {
|
pkg[DIST_TAGS] = {
|
||||||
latest: ['1.0.1']
|
latest: ['1.0.1'],
|
||||||
};
|
};
|
||||||
|
|
||||||
normalizeDistTags(pkg);
|
normalizeDistTags(pkg);
|
||||||
|
@ -206,17 +211,21 @@ describe('Utilities', () => {
|
||||||
|
|
||||||
describe('combineBaseUrl', () => {
|
describe('combineBaseUrl', () => {
|
||||||
test('should create a URI', () => {
|
test('should create a URI', () => {
|
||||||
expect(combineBaseUrl('http', 'domain')).toEqual('http://domain');
|
expect(combineBaseUrl('http', 'domain')).toEqual('http://domain/');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create a base url for registry', () => {
|
test('should create a base url for registry', () => {
|
||||||
expect(combineBaseUrl('http', 'domain', '')).toEqual('http://domain');
|
expect(combineBaseUrl('http', 'domain.com', '')).toEqual('http://domain.com/');
|
||||||
expect(combineBaseUrl('http', 'domain', '/')).toEqual('http://domain');
|
expect(combineBaseUrl('http', 'domain.com', '/')).toEqual('http://domain.com/');
|
||||||
expect(combineBaseUrl('http', 'domain', '/prefix/')).toEqual('http://domain/prefix');
|
expect(combineBaseUrl('http', 'domain.com', '/prefix/')).toEqual('http://domain.com/prefix/');
|
||||||
expect(combineBaseUrl('http', 'domain', '/prefix/deep')).toEqual(
|
expect(combineBaseUrl('http', 'domain.com', '/prefix/deep')).toEqual('http://domain.com/prefix/deep/');
|
||||||
'http://domain/prefix/deep'
|
expect(combineBaseUrl('http', 'domain.com', 'prefix/')).toEqual('http://domain.com/prefix/');
|
||||||
);
|
expect(combineBaseUrl('http', 'domain.com', 'prefix')).toEqual('http://domain.com/prefix/');
|
||||||
expect(combineBaseUrl('http', 'domain', 'only-prefix')).toEqual('only-prefix');
|
});
|
||||||
|
|
||||||
|
test('invalid url prefix', () => {
|
||||||
|
expect(combineBaseUrl('http', 'domain.com', 'only-prefix')).toEqual('http://domain.com/only-prefix/');
|
||||||
|
expect(combineBaseUrl('https', 'domain.com', 'only-prefix')).toEqual('https://domain.com/only-prefix/');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -389,19 +398,14 @@ describe('Utilities', () => {
|
||||||
|
|
||||||
expect(parseReadme('testPackage', randomText)).toEqual('<p>%%%%%**##==</p>');
|
expect(parseReadme('testPackage', randomText)).toEqual('<p>%%%%%**##==</p>');
|
||||||
expect(parseReadme('testPackage', simpleText)).toEqual('<p>simple text</p>');
|
expect(parseReadme('testPackage', simpleText)).toEqual('<p>simple text</p>');
|
||||||
expect(parseReadme('testPackage', randomTextMarkdown)).toEqual(
|
expect(parseReadme('testPackage', randomTextMarkdown)).toEqual('<p>simple text </p>\n<h1 id="markdown">markdown</h1>');
|
||||||
'<p>simple text </p>\n<h1 id="markdown">markdown</h1>'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show error for no readme data', () => {
|
test('should show error for no readme data', () => {
|
||||||
const noData = '';
|
const noData = '';
|
||||||
const spy = jest.spyOn(logger, 'error');
|
const spy = jest.spyOn(logger, 'error');
|
||||||
expect(parseReadme('testPackage', noData)).toEqual('<p>ERROR: No README data found!</p>');
|
expect(parseReadme('testPackage', noData)).toEqual('<p>ERROR: No README data found!</p>');
|
||||||
expect(spy).toHaveBeenCalledWith(
|
expect(spy).toHaveBeenCalledWith({ packageName: 'testPackage' }, '@{packageName}: No readme found');
|
||||||
{ packageName: 'testPackage' },
|
|
||||||
'@{packageName}: No readme found'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -413,7 +417,7 @@ describe('Utilities', () => {
|
||||||
|
|
||||||
test('author, contributors and maintainers fields are not present', () => {
|
test('author, contributors and maintainers fields are not present', () => {
|
||||||
const packageInfo = {
|
const packageInfo = {
|
||||||
latest: {}
|
latest: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -429,16 +433,16 @@ describe('Utilities', () => {
|
||||||
|
|
||||||
test('author field is a string type', () => {
|
test('author field is a string type', () => {
|
||||||
const packageInfo = {
|
const packageInfo = {
|
||||||
latest: { author: 'user@verdccio.org' }
|
latest: { author: 'user@verdccio.org' },
|
||||||
};
|
};
|
||||||
const result = {
|
const result = {
|
||||||
latest: {
|
latest: {
|
||||||
author: {
|
author: {
|
||||||
author: 'user@verdccio.org',
|
author: 'user@verdccio.org',
|
||||||
avatar: GENERIC_AVATAR,
|
avatar: GENERIC_AVATAR,
|
||||||
email: ''
|
email: '',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -447,16 +451,16 @@ describe('Utilities', () => {
|
||||||
|
|
||||||
test('author field is an object type with author information', () => {
|
test('author field is an object type with author information', () => {
|
||||||
const packageInfo = {
|
const packageInfo = {
|
||||||
latest: { author: { name: 'verdaccio', email: 'user@verdccio.org' } }
|
latest: { author: { name: 'verdaccio', email: 'user@verdccio.org' } },
|
||||||
};
|
};
|
||||||
const result = {
|
const result = {
|
||||||
latest: {
|
latest: {
|
||||||
author: {
|
author: {
|
||||||
avatar: 'https://www.gravatar.com/avatar/794d7f6ef93d0689437de3c3e48fadc7',
|
avatar: 'https://www.gravatar.com/avatar/794d7f6ef93d0689437de3c3e48fadc7',
|
||||||
email: 'user@verdccio.org',
|
email: 'user@verdccio.org',
|
||||||
name: 'verdaccio'
|
name: 'verdaccio',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -466,8 +470,8 @@ describe('Utilities', () => {
|
||||||
test('contributor field is a blank array', () => {
|
test('contributor field is a blank array', () => {
|
||||||
const packageInfo = {
|
const packageInfo = {
|
||||||
latest: {
|
latest: {
|
||||||
contributors: []
|
contributors: [],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -480,9 +484,9 @@ describe('Utilities', () => {
|
||||||
latest: {
|
latest: {
|
||||||
contributors: [
|
contributors: [
|
||||||
{ name: 'user', email: 'user@verdccio.org' },
|
{ name: 'user', email: 'user@verdccio.org' },
|
||||||
{ name: 'user1', email: 'user1@verdccio.org' }
|
{ name: 'user1', email: 'user1@verdccio.org' },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
|
@ -491,15 +495,15 @@ describe('Utilities', () => {
|
||||||
{
|
{
|
||||||
avatar: 'https://www.gravatar.com/avatar/794d7f6ef93d0689437de3c3e48fadc7',
|
avatar: 'https://www.gravatar.com/avatar/794d7f6ef93d0689437de3c3e48fadc7',
|
||||||
email: 'user@verdccio.org',
|
email: 'user@verdccio.org',
|
||||||
name: 'user'
|
name: 'user',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
avatar: 'https://www.gravatar.com/avatar/51105a49ce4a9c2bfabf0f6a2cba3762',
|
avatar: 'https://www.gravatar.com/avatar/51105a49ce4a9c2bfabf0f6a2cba3762',
|
||||||
email: 'user1@verdccio.org',
|
email: 'user1@verdccio.org',
|
||||||
name: 'user1'
|
name: 'user1',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -509,8 +513,8 @@ describe('Utilities', () => {
|
||||||
test('contributors field is an object', () => {
|
test('contributors field is an object', () => {
|
||||||
const packageInfo = {
|
const packageInfo = {
|
||||||
latest: {
|
latest: {
|
||||||
contributors: { name: 'user', email: 'user@verdccio.org' }
|
contributors: { name: 'user', email: 'user@verdccio.org' },
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
|
@ -519,10 +523,10 @@ describe('Utilities', () => {
|
||||||
{
|
{
|
||||||
avatar: 'https://www.gravatar.com/avatar/794d7f6ef93d0689437de3c3e48fadc7',
|
avatar: 'https://www.gravatar.com/avatar/794d7f6ef93d0689437de3c3e48fadc7',
|
||||||
email: 'user@verdccio.org',
|
email: 'user@verdccio.org',
|
||||||
name: 'user'
|
name: 'user',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -533,8 +537,8 @@ describe('Utilities', () => {
|
||||||
const contributor = 'Barney Rubble <b@rubble.com> (http://barnyrubble.tumblr.com/)';
|
const contributor = 'Barney Rubble <b@rubble.com> (http://barnyrubble.tumblr.com/)';
|
||||||
const packageInfo = {
|
const packageInfo = {
|
||||||
latest: {
|
latest: {
|
||||||
contributors: contributor
|
contributors: contributor,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
|
@ -543,10 +547,10 @@ describe('Utilities', () => {
|
||||||
{
|
{
|
||||||
avatar: GENERIC_AVATAR,
|
avatar: GENERIC_AVATAR,
|
||||||
email: contributor,
|
email: contributor,
|
||||||
name: contributor
|
name: contributor,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -557,8 +561,8 @@ describe('Utilities', () => {
|
||||||
test('maintainers field is a blank array', () => {
|
test('maintainers field is a blank array', () => {
|
||||||
const packageInfo = {
|
const packageInfo = {
|
||||||
latest: {
|
latest: {
|
||||||
maintainers: []
|
maintainers: [],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -570,9 +574,9 @@ describe('Utilities', () => {
|
||||||
latest: {
|
latest: {
|
||||||
maintainers: [
|
maintainers: [
|
||||||
{ name: 'user', email: 'user@verdccio.org' },
|
{ name: 'user', email: 'user@verdccio.org' },
|
||||||
{ name: 'user1', email: 'user1@verdccio.org' }
|
{ name: 'user1', email: 'user1@verdccio.org' },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
|
@ -581,15 +585,15 @@ describe('Utilities', () => {
|
||||||
{
|
{
|
||||||
avatar: 'https://www.gravatar.com/avatar/794d7f6ef93d0689437de3c3e48fadc7',
|
avatar: 'https://www.gravatar.com/avatar/794d7f6ef93d0689437de3c3e48fadc7',
|
||||||
email: 'user@verdccio.org',
|
email: 'user@verdccio.org',
|
||||||
name: 'user'
|
name: 'user',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
avatar: 'https://www.gravatar.com/avatar/51105a49ce4a9c2bfabf0f6a2cba3762',
|
avatar: 'https://www.gravatar.com/avatar/51105a49ce4a9c2bfabf0f6a2cba3762',
|
||||||
email: 'user1@verdccio.org',
|
email: 'user1@verdccio.org',
|
||||||
name: 'user1'
|
name: 'user1',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -606,7 +610,7 @@ describe('Utilities', () => {
|
||||||
const user = {
|
const user = {
|
||||||
name: 'Verdaccion NPM',
|
name: 'Verdaccion NPM',
|
||||||
email: 'verdaccio@verdaccio.org',
|
email: 'verdaccio@verdaccio.org',
|
||||||
url: 'https://verdaccio.org'
|
url: 'https://verdaccio.org',
|
||||||
};
|
};
|
||||||
expect(formatAuthor(user).url).toEqual(user.url);
|
expect(formatAuthor(user).url).toEqual(user.url);
|
||||||
expect(formatAuthor(user).email).toEqual(user.email);
|
expect(formatAuthor(user).email).toEqual(user.email);
|
||||||
|
@ -618,4 +622,254 @@ describe('Utilities', () => {
|
||||||
expect(formatAuthor([]).name).toEqual(DEFAULT_USER);
|
expect(formatAuthor([]).name).toEqual(DEFAULT_USER);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('host', () => {
|
||||||
|
// this scenario is usual when reverse proxy is setup
|
||||||
|
// without the host header
|
||||||
|
test('get empty string with missing host header', () => {
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
expect(getPublicUrl(undefined, req)).toEqual('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get a valid host', () => {
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: 'some.com',
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
expect(getPublicUrl(undefined, req)).toEqual('http://some.com/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('check a valid host header injection', () => {
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: `some.com"><svg onload="alert(1)">`,
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
expect(function () {
|
||||||
|
// @ts-expect-error
|
||||||
|
getPublicUrl({}, req);
|
||||||
|
}).toThrow('invalid host');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get a valid host with prefix', () => {
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: 'some.com',
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getPublicUrl('/prefix/', req)).toEqual('http://some.com/prefix/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get a valid host with prefix no trailing', () => {
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: 'some.com',
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getPublicUrl('/prefix-no-trailing', req)).toEqual('http://some.com/prefix-no-trailing/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get a valid host with null prefix', () => {
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: 'some.com',
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
expect(getPublicUrl(null, req)).toEqual('http://some.com/');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('X-Forwarded-Proto', () => {
|
||||||
|
test('with a valid X-Forwarded-Proto https', () => {
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: 'some.com',
|
||||||
|
[HEADERS.FORWARDED_PROTO]: 'https',
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getPublicUrl(undefined, req)).toEqual('https://some.com/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with a invalid X-Forwarded-Proto https', () => {
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: 'some.com',
|
||||||
|
[HEADERS.FORWARDED_PROTO]: 'invalidProto',
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getPublicUrl(undefined, req)).toEqual('http://some.com/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with a HAProxy X-Forwarded-Proto https', () => {
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: 'some.com',
|
||||||
|
[HEADERS.FORWARDED_PROTO]: 'https,https',
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getPublicUrl(undefined, req)).toEqual('https://some.com/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with a HAProxy X-Forwarded-Proto different protocol', () => {
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: 'some.com',
|
||||||
|
[HEADERS.FORWARDED_PROTO]: 'http,https',
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getPublicUrl(undefined, req)).toEqual('http://some.com/');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('env variable', () => {
|
||||||
|
test('with a valid X-Forwarded-Proto https and env variable', () => {
|
||||||
|
process.env.VERDACCIO_PUBLIC_URL = 'https://env.domain.com/';
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: 'some.com',
|
||||||
|
[HEADERS.FORWARDED_PROTO]: 'https',
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getPublicUrl(undefined, req)).toEqual('https://env.domain.com/');
|
||||||
|
delete process.env.VERDACCIO_PUBLIC_URL;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with a valid X-Forwarded-Proto https and env variable with prefix', () => {
|
||||||
|
process.env.VERDACCIO_PUBLIC_URL = 'https://env.domain.com/urlPrefix/';
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: 'some.com',
|
||||||
|
[HEADERS.FORWARDED_PROTO]: 'http',
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getPublicUrl(undefined, req)).toEqual('https://env.domain.com/urlPrefix/');
|
||||||
|
delete process.env.VERDACCIO_PUBLIC_URL;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with a valid X-Forwarded-Proto https and env variable with prefix as url prefix', () => {
|
||||||
|
process.env.VERDACCIO_PUBLIC_URL = 'https://env.domain.com/urlPrefix/';
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: 'some.com',
|
||||||
|
[HEADERS.FORWARDED_PROTO]: 'https',
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getPublicUrl('conf_url_prefix', req)).toEqual('https://env.domain.com/conf_url_prefix/');
|
||||||
|
delete process.env.VERDACCIO_PUBLIC_URL;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with a valid X-Forwarded-Proto https and env variable with prefix as root url prefix', () => {
|
||||||
|
process.env.VERDACCIO_PUBLIC_URL = 'https://env.domain.com/urlPrefix/';
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: 'some.com',
|
||||||
|
[HEADERS.FORWARDED_PROTO]: 'https',
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getPublicUrl('/', req)).toEqual('https://env.domain.com/');
|
||||||
|
delete process.env.VERDACCIO_PUBLIC_URL;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with a invalid X-Forwarded-Proto https and env variable', () => {
|
||||||
|
process.env.VERDACCIO_PUBLIC_URL = 'https://env.domain.com';
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: 'some.com',
|
||||||
|
[HEADERS.FORWARDED_PROTO]: 'invalidProtocol',
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getPublicUrl(undefined, req)).toEqual('https://env.domain.com/');
|
||||||
|
delete process.env.VERDACCIO_PUBLIC_URL;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with a invalid X-Forwarded-Proto https and invalid url with env variable', () => {
|
||||||
|
process.env.VERDACCIO_PUBLIC_URL = 'ftp://env.domain.com';
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: 'some.com',
|
||||||
|
[HEADERS.FORWARDED_PROTO]: 'invalidProtocol',
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getPublicUrl(undefined, req)).toEqual('http://some.com/');
|
||||||
|
delete process.env.VERDACCIO_PUBLIC_URL;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with a invalid X-Forwarded-Proto https and host injection with host', () => {
|
||||||
|
process.env.VERDACCIO_PUBLIC_URL = 'http://injection.test.com"><svg onload="alert(1)">';
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: 'some.com',
|
||||||
|
[HEADERS.FORWARDED_PROTO]: 'invalidProtocol',
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getPublicUrl(undefined, req)).toEqual('http://some.com/');
|
||||||
|
delete process.env.VERDACCIO_PUBLIC_URL;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with a invalid X-Forwarded-Proto https and host injection with invalid host', () => {
|
||||||
|
process.env.VERDACCIO_PUBLIC_URL = 'http://injection.test.com"><svg onload="alert(1)">';
|
||||||
|
const req = httpMocks.createRequest({
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
host: 'some',
|
||||||
|
[HEADERS.FORWARDED_PROTO]: 'invalidProtocol',
|
||||||
|
},
|
||||||
|
url: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getPublicUrl(undefined, req)).toEqual('http://some/');
|
||||||
|
delete process.env.VERDACCIO_PUBLIC_URL;
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
151
test/unit/modules/web/__snapshots__/template.spec.ts.snap
Normal file
151
test/unit/modules/web/__snapshots__/template.spec.ts.snap
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`template custom body after 1`] = `
|
||||||
|
"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang=\\"en-us\\">
|
||||||
|
<head>
|
||||||
|
<meta charset=\\"utf-8\\">
|
||||||
|
<base href=\\"http://domain.com\\">
|
||||||
|
<title></title>
|
||||||
|
<link rel=\\"icon\\" href=\\"http://domain.com-/static/favicon.ico\\"/>
|
||||||
|
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1\\" />
|
||||||
|
<script>
|
||||||
|
window.__VERDACCIO_BASENAME_UI_OPTIONS={\\"base\\":\\"http://domain.com\\"}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body class=\\"body\\">
|
||||||
|
|
||||||
|
<div id=\\"root\\"></div>
|
||||||
|
<script defer=\\"defer\\" src=\\"http://domain.com-/static/runtime.9be80fd172e81558124c.js\\"></script><script defer=\\"defer\\" src=\\"http://domain.com-/static/main.9be80fd172e81558124c.js\\"></script>
|
||||||
|
<script src=\\"foo\\"/>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`template custom body before 1`] = `
|
||||||
|
"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang=\\"en-us\\">
|
||||||
|
<head>
|
||||||
|
<meta charset=\\"utf-8\\">
|
||||||
|
<base href=\\"http://domain.com\\">
|
||||||
|
<title></title>
|
||||||
|
<link rel=\\"icon\\" href=\\"http://domain.com-/static/favicon.ico\\"/>
|
||||||
|
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1\\" />
|
||||||
|
<script>
|
||||||
|
window.__VERDACCIO_BASENAME_UI_OPTIONS={\\"base\\":\\"http://domain.com\\"}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body class=\\"body\\">
|
||||||
|
<script src=\\"fooBefore\\"/><script src=\\"barBefore\\"/>
|
||||||
|
<div id=\\"root\\"></div>
|
||||||
|
<script defer=\\"defer\\" src=\\"http://domain.com-/static/runtime.9be80fd172e81558124c.js\\"></script><script defer=\\"defer\\" src=\\"http://domain.com-/static/main.9be80fd172e81558124c.js\\"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`template custom render 1`] = `
|
||||||
|
"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang=\\"en-us\\">
|
||||||
|
<head>
|
||||||
|
<meta charset=\\"utf-8\\">
|
||||||
|
<base href=\\"http://domain.com\\">
|
||||||
|
<title></title>
|
||||||
|
<link rel=\\"icon\\" href=\\"http://domain.com-/static/favicon.ico\\"/>
|
||||||
|
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1\\" />
|
||||||
|
<script>
|
||||||
|
window.__VERDACCIO_BASENAME_UI_OPTIONS={\\"base\\":\\"http://domain.com\\"}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body class=\\"body\\">
|
||||||
|
|
||||||
|
<div id=\\"root\\"></div>
|
||||||
|
<script defer=\\"defer\\" src=\\"http://domain.com-/static/runtime.9be80fd172e81558124c.js\\"></script><script defer=\\"defer\\" src=\\"http://domain.com-/static/main.9be80fd172e81558124c.js\\"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`template custom title 1`] = `
|
||||||
|
"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang=\\"en-us\\">
|
||||||
|
<head>
|
||||||
|
<meta charset=\\"utf-8\\">
|
||||||
|
<base href=\\"http://domain.com\\">
|
||||||
|
<title>foo title</title>
|
||||||
|
<link rel=\\"icon\\" href=\\"http://domain.com-/static/favicon.ico\\"/>
|
||||||
|
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1\\" />
|
||||||
|
<script>
|
||||||
|
window.__VERDACCIO_BASENAME_UI_OPTIONS={\\"base\\":\\"http://domain.com\\",\\"title\\":\\"foo title\\"}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body class=\\"body\\">
|
||||||
|
|
||||||
|
<div id=\\"root\\"></div>
|
||||||
|
<script defer=\\"defer\\" src=\\"http://domain.com-/static/runtime.9be80fd172e81558124c.js\\"></script><script defer=\\"defer\\" src=\\"http://domain.com-/static/main.9be80fd172e81558124c.js\\"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`template custom title 2`] = `
|
||||||
|
"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang=\\"en-us\\">
|
||||||
|
<head>
|
||||||
|
<meta charset=\\"utf-8\\">
|
||||||
|
<base href=\\"http://domain.com\\">
|
||||||
|
<title>foo title</title>
|
||||||
|
<link rel=\\"icon\\" href=\\"http://domain.com-/static/favicon.ico\\"/>
|
||||||
|
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1\\" />
|
||||||
|
<script>
|
||||||
|
window.__VERDACCIO_BASENAME_UI_OPTIONS={\\"base\\":\\"http://domain.com\\",\\"title\\":\\"foo title\\"}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body class=\\"body\\">
|
||||||
|
|
||||||
|
<div id=\\"root\\"></div>
|
||||||
|
<script defer=\\"defer\\" src=\\"http://domain.com-/static/runtime.9be80fd172e81558124c.js\\"></script><script defer=\\"defer\\" src=\\"http://domain.com-/static/main.9be80fd172e81558124c.js\\"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`template meta scripts 1`] = `
|
||||||
|
"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang=\\"en-us\\">
|
||||||
|
<head>
|
||||||
|
<meta charset=\\"utf-8\\">
|
||||||
|
<base href=\\"http://domain.com\\">
|
||||||
|
<title></title>
|
||||||
|
<link rel=\\"icon\\" href=\\"http://domain.com-/static/favicon.ico\\"/>
|
||||||
|
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1\\" />
|
||||||
|
<script>
|
||||||
|
window.__VERDACCIO_BASENAME_UI_OPTIONS={\\"base\\":\\"http://domain.com\\"}
|
||||||
|
</script>
|
||||||
|
<style>.someclass{font-size:10px;}</style>
|
||||||
|
</head>
|
||||||
|
<body class=\\"body\\">
|
||||||
|
|
||||||
|
<div id=\\"root\\"></div>
|
||||||
|
<script defer=\\"defer\\" src=\\"http://domain.com-/static/runtime.9be80fd172e81558124c.js\\"></script><script defer=\\"defer\\" src=\\"http://domain.com-/static/main.9be80fd172e81558124c.js\\"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"
|
||||||
|
`;
|
21
test/unit/modules/web/partials/manifest/manifest.json
Normal file
21
test/unit/modules/web/partials/manifest/manifest.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"main.js": "-/static/main.9be80fd172e81558124c.js",
|
||||||
|
"runtime.js": "-/static/runtime.9be80fd172e81558124c.js",
|
||||||
|
"NotFound.js": "-/static/NotFound.9be80fd172e81558124c.js",
|
||||||
|
"Provider.js": "-/static/Provider.9be80fd172e81558124c.js",
|
||||||
|
"Version.js": "-/static/Version.9be80fd172e81558124c.js",
|
||||||
|
"Home.js": "-/static/Home.9be80fd172e81558124c.js",
|
||||||
|
"Versions.js": "-/static/Versions.9be80fd172e81558124c.js",
|
||||||
|
"UpLinks.js": "-/static/UpLinks.9be80fd172e81558124c.js",
|
||||||
|
"Dependencies.js": "-/static/Dependencies.9be80fd172e81558124c.js",
|
||||||
|
"Engines.js": "-/static/Engines.9be80fd172e81558124c.js",
|
||||||
|
"Dist.js": "-/static/Dist.9be80fd172e81558124c.js",
|
||||||
|
"Install.js": "-/static/Install.9be80fd172e81558124c.js",
|
||||||
|
"Repository.js": "-/static/Repository.9be80fd172e81558124c.js",
|
||||||
|
"vendors.js": "-/static/vendors.9be80fd172e81558124c.js",
|
||||||
|
"vendors-node_modules_pnpm_material-ui_core_4_11_2_react-dom_16_13_1_react_16_13_1_node_module-2c2376.9be80fd172e81558124c.js": "-/static/vendors-node_modules_pnpm_material-ui_core_4_11_2_react-dom_16_13_1_react_16_13_1_node_module-2c2376.9be80fd172e81558124c.js",
|
||||||
|
"vendors-node_modules_pnpm_material-ui_core_4_11_2_react-dom_16_13_1_react_16_13_1_node_module-a68215.9be80fd172e81558124c.js": "-/static/vendors-node_modules_pnpm_material-ui_core_4_11_2_react-dom_16_13_1_react_16_13_1_node_module-a68215.9be80fd172e81558124c.js",
|
||||||
|
"vendors-node_modules_pnpm_material-ui_core_4_11_2_react-dom_16_13_1_react_16_13_1_node_module-3c0585.9be80fd172e81558124c.js": "-/static/vendors-node_modules_pnpm_material-ui_core_4_11_2_react-dom_16_13_1_react_16_13_1_node_module-3c0585.9be80fd172e81558124c.js",
|
||||||
|
"favicon.ico": "-/static/favicon.ico",
|
||||||
|
"index.html": "-/static/index.html"
|
||||||
|
}
|
52
test/unit/modules/web/template.spec.ts
Normal file
52
test/unit/modules/web/template.spec.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import renderTemplate from "../../../../src/api/web/html/template";
|
||||||
|
|
||||||
|
const manifest = require('./partials/manifest/manifest.json');
|
||||||
|
|
||||||
|
const exampleManifest = {
|
||||||
|
css: ['main.css'],
|
||||||
|
js: ['runtime.js', 'main.js'],
|
||||||
|
ico: '/static/foo.ico',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('template', () => {
|
||||||
|
test('custom render', () => {
|
||||||
|
expect(renderTemplate({ options: {base: 'http://domain.com'}, manifest: exampleManifest }, manifest)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('custom title', () => {
|
||||||
|
expect(
|
||||||
|
renderTemplate({ options: {base: 'http://domain.com', title: 'foo title' }, manifest: exampleManifest }, manifest)
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('custom title', () => {
|
||||||
|
expect(
|
||||||
|
renderTemplate({ options: {base: 'http://domain.com', title: 'foo title' }, manifest: exampleManifest }, manifest)
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('meta scripts', () => {
|
||||||
|
expect(
|
||||||
|
renderTemplate({ options: {base: 'http://domain.com'}, metaScripts: [`<style>.someclass{font-size:10px;}</style>`], manifest: exampleManifest }, manifest)
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('custom body after', () => {
|
||||||
|
expect(
|
||||||
|
renderTemplate({ options: {base: 'http://domain.com'}, scriptsBodyAfter: [`<script src="foo"/>`], manifest: exampleManifest }, manifest)
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('custom body before', () => {
|
||||||
|
expect(
|
||||||
|
renderTemplate(
|
||||||
|
{
|
||||||
|
options: {base: 'http://domain.com'},
|
||||||
|
scriptsbodyBefore: [`<script src="fooBefore"/>`, `<script src="barBefore"/>`],
|
||||||
|
manifest: exampleManifest,
|
||||||
|
},
|
||||||
|
manifest
|
||||||
|
)
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -86,4 +86,4 @@ packages:
|
||||||
unpublish: xxx
|
unpublish: xxx
|
||||||
proxy: npmjs
|
proxy: npmjs
|
||||||
logs:
|
logs:
|
||||||
- { type: stdout, format: pretty, level: warn }
|
- { type: stdout, format: pretty, level: trace }
|
||||||
|
|
BIN
yarn.lock
BIN
yarn.lock
Binary file not shown.
Loading…
Reference in a new issue