mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-12-24 21:15:51 +01:00
feat: improve url_prefix behavior (#2122)
read pr 2122 for more details
This commit is contained in:
parent
e5ce44c395
commit
15bb350ae4
@ -15,6 +15,7 @@ Dockerfile
|
||||
*.png
|
||||
*.jpg
|
||||
*.sh
|
||||
*.ico
|
||||
test/unit/partials/
|
||||
types/custom.d.ts
|
||||
docker-examples/
|
||||
|
@ -13,8 +13,8 @@ src/
|
||||
.vscode/
|
||||
.circleci/
|
||||
debug/
|
||||
|
||||
|
||||
docker-examples/
|
||||
reports/
|
||||
## assets and website
|
||||
assets/
|
||||
|
||||
|
@ -27,3 +27,6 @@ test/functional/store/*
|
||||
storage_default_storage/*
|
||||
docker-examples/
|
||||
.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.
|
||||
server:
|
||||
keepAliveTimeout: 60
|
||||
# behindProxy: false
|
||||
|
||||
middlewares:
|
||||
audit:
|
||||
|
@ -66,6 +66,14 @@ packages:
|
||||
# if package is not available locally, proxy requests to 'npmjs' registry
|
||||
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:
|
||||
audit:
|
||||
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
|
||||
|
||||
## IMPORTANT
|
||||
##
|
||||
url_prefix: /verdaccio
|
||||
## This setup is required for relative path
|
||||
url_prefix: /verdaccio/
|
||||
server:
|
||||
behindProxy: true
|
||||
|
||||
uplinks:
|
||||
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'
|
||||
depends_on:
|
||||
- verdaccio
|
||||
- verdaccio3
|
||||
- verdaccio-root
|
||||
verdaccio:
|
||||
image: verdaccio/verdaccio:4
|
||||
image: verdaccio/verdaccio:local
|
||||
container_name: 'verdaccio_relative_path_v4'
|
||||
networks:
|
||||
- node-network
|
||||
environment:
|
||||
- VERDACCIO_PORT=4873
|
||||
- DEBUG=verdaccio*
|
||||
ports:
|
||||
- '4873:4873'
|
||||
volumes:
|
||||
- './storage:/verdaccio/storage'
|
||||
- './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:
|
||||
node-network:
|
||||
driver: bridge
|
||||
|
@ -17,7 +17,7 @@ services:
|
||||
- verdaccio
|
||||
- verdaccio-root
|
||||
verdaccio:
|
||||
image: verdaccio/verdaccio:4
|
||||
image: verdaccio/verdaccio:local
|
||||
container_name: 'verdaccio_relative_path_v4'
|
||||
networks:
|
||||
- node-network
|
||||
@ -29,7 +29,7 @@ services:
|
||||
- './storage:/verdaccio/storage'
|
||||
- './conf/v4:/verdaccio/conf'
|
||||
verdaccio-root:
|
||||
image: verdaccio/verdaccio:4
|
||||
image: verdaccio/verdaccio:local
|
||||
container_name: 'verdaccio_relative_path_v4_root'
|
||||
networks:
|
||||
- node-network
|
||||
|
@ -3,31 +3,11 @@ upstream verdaccio_v4 {
|
||||
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 {
|
||||
listen 80 default_server;
|
||||
access_log /var/log/nginx/verdaccio.log;
|
||||
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/(.*)$ {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
@ -36,14 +16,4 @@ server {
|
||||
proxy_pass http://verdaccio_v4/$1;
|
||||
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).
|
||||
|
||||
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/readme": "9.7.5",
|
||||
"@verdaccio/streams": "9.7.2",
|
||||
"@verdaccio/ui-theme": "1.15.1",
|
||||
"@verdaccio/ui-theme": "3.0.0",
|
||||
"JSONStream": "1.3.5",
|
||||
"async": "3.2.0",
|
||||
"body-parser": "1.19.0",
|
||||
@ -32,6 +32,7 @@
|
||||
"cookies": "0.8.0",
|
||||
"cors": "2.8.5",
|
||||
"dayjs": "1.10.4",
|
||||
"debug": "^4.3.1",
|
||||
"envinfo": "7.7.4",
|
||||
"express": "4.17.1",
|
||||
"handlebars": "4.7.7",
|
||||
@ -49,6 +50,7 @@
|
||||
"pkginfo": "0.4.1",
|
||||
"request": "2.88.0",
|
||||
"semver": "7.3.4",
|
||||
"validator": "13.5.2",
|
||||
"verdaccio-audit": "9.7.3",
|
||||
"verdaccio-htpasswd": "9.7.2"
|
||||
},
|
||||
@ -121,7 +123,9 @@
|
||||
"jest-junit": "9.0.0",
|
||||
"lint-staged": "8.2.1",
|
||||
"lockfile-lint": "4.3.7",
|
||||
"lru-cache": "6.0.0",
|
||||
"nock": "12.0.3",
|
||||
"node-mocks-http": "^1.10.1",
|
||||
"prettier": "2.2.1",
|
||||
"puppeteer": "5.5.0",
|
||||
"rimraf": "3.0.2",
|
||||
@ -164,10 +168,10 @@
|
||||
"lint": "yarn run type-check && yarn run lint:ts",
|
||||
"lint:ts": "eslint \"**/*.{js,jsx,ts,tsx}\"",
|
||||
"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: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"
|
||||
},
|
||||
"engines": {
|
||||
@ -186,8 +190,6 @@
|
||||
"linters": {
|
||||
"*": [
|
||||
"eslint .",
|
||||
"prettier --write",
|
||||
"detect-secrets-launcher --baseline .secrets-baseline",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
|
@ -10,23 +10,21 @@ import Auth from '../lib/auth';
|
||||
import { ErrorCode } from '../lib/utils';
|
||||
import { API_ERROR, HTTP_STATUS } from '../lib/constants';
|
||||
import AppConfig from '../lib/config';
|
||||
import {
|
||||
$ResponseExtend,
|
||||
$RequestExtend,
|
||||
$NextFunctionVer,
|
||||
IStorageHandler,
|
||||
IAuth
|
||||
} from '../../types';
|
||||
import { $ResponseExtend, $RequestExtend, $NextFunctionVer, IStorageHandler, IAuth } from '../../types';
|
||||
import { setup, logger } from '../lib/logger';
|
||||
import webAPI from './web/api';
|
||||
import web from './web';
|
||||
import apiEndpoint from './endpoint';
|
||||
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 auth: IAuth = new Auth(config);
|
||||
const app: Application = express();
|
||||
if (config?.server?.behindProxy === true) {
|
||||
// app.use('trust proxy');
|
||||
}
|
||||
|
||||
// run in production mode by default, just in case
|
||||
// it shouldn't make any difference anyway
|
||||
app.set('env', process.env.NODE_ENV || 'production');
|
||||
@ -42,13 +40,7 @@ const defineAPI = function (config: IConfig, storage: IStorageHandler): any {
|
||||
|
||||
app.use(compression());
|
||||
|
||||
app.get(
|
||||
'/favicon.ico',
|
||||
function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
req.url = '/-/static/favicon.png';
|
||||
next();
|
||||
}
|
||||
);
|
||||
app.get('/-/static/favicon.ico', serveFavicon(config));
|
||||
|
||||
// Hook for tests only
|
||||
if (config._debug) {
|
||||
@ -58,17 +50,12 @@ const defineAPI = function (config: IConfig, storage: IStorageHandler): any {
|
||||
// register middleware plugins
|
||||
const plugin_params = {
|
||||
config: config,
|
||||
logger: logger
|
||||
logger: logger,
|
||||
};
|
||||
|
||||
const plugins: IPluginMiddleware<IConfig>[] = loadPlugin(
|
||||
config,
|
||||
config.middlewares,
|
||||
plugin_params,
|
||||
function (plugin: IPluginMiddleware<IConfig>) {
|
||||
return plugin.register_middlewares;
|
||||
}
|
||||
);
|
||||
const plugins: IPluginMiddleware<IConfig>[] = loadPlugin(config, config.middlewares, plugin_params, function (plugin: IPluginMiddleware<IConfig>) {
|
||||
return plugin.register_middlewares;
|
||||
});
|
||||
plugins.forEach((plugin: IPluginMiddleware<IConfig>) => {
|
||||
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));
|
||||
});
|
||||
|
||||
app.use(function (
|
||||
err: HttpError,
|
||||
req: $RequestExtend,
|
||||
res: $ResponseExtend,
|
||||
next: $NextFunctionVer
|
||||
) {
|
||||
app.use(function (err: HttpError, req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {
|
||||
if (_.isError(err)) {
|
||||
if (err.code === 'ECONNABORT' && res.statusCode === HTTP_STATUS.NOT_MODIFIED) {
|
||||
return next();
|
||||
@ -124,14 +106,9 @@ export default (async function (configHash: any): Promise<any> {
|
||||
// register middleware plugins
|
||||
const plugin_params = {
|
||||
config: config,
|
||||
logger: logger
|
||||
logger: logger,
|
||||
};
|
||||
const filters = loadPlugin(
|
||||
config,
|
||||
config.filters || {},
|
||||
plugin_params,
|
||||
(plugin: IPluginStorageFilter<IConfig>) => plugin.filter_metadata
|
||||
);
|
||||
const filters = loadPlugin(config, config.filters || {}, plugin_params, (plugin: IPluginStorageFilter<IConfig>) => plugin.filter_metadata);
|
||||
const storage: IStorageHandler = new Storage(config);
|
||||
// waits until init calls have been initialized
|
||||
await storage.init(config, filters);
|
||||
|
@ -1,33 +1,21 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import _ from 'lodash';
|
||||
import buildDebug from 'debug';
|
||||
import validator from 'validator';
|
||||
|
||||
import { Config, Package, RemoteUser } from '@verdaccio/types';
|
||||
import { VerdaccioError } from '@verdaccio/commons-api';
|
||||
import {
|
||||
validateName as utilValidateName,
|
||||
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 { validateName as utilValidateName, 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 { $ResponseExtend, $RequestExtend, $NextFunctionVer, IAuth } from '../../types';
|
||||
import { logger } from '../lib/logger';
|
||||
|
||||
const debug = buildDebug('verdaccio');
|
||||
|
||||
export function match(regexp: RegExp): any {
|
||||
return function (
|
||||
req: $RequestExtend,
|
||||
res: $ResponseExtend,
|
||||
next: $NextFunctionVer,
|
||||
value: string
|
||||
): void {
|
||||
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer, value: string): void {
|
||||
if (regexp.exec(value)) {
|
||||
next();
|
||||
} else {
|
||||
@ -36,11 +24,52 @@ export function match(regexp: RegExp): any {
|
||||
};
|
||||
}
|
||||
|
||||
export function setSecurityWebHeaders(
|
||||
req: $RequestExtend,
|
||||
res: $ResponseExtend,
|
||||
next: $NextFunctionVer
|
||||
): void {
|
||||
export function serveFavicon(config: Config) {
|
||||
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
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.)
|
||||
res.header(HEADERS.FRAMES_OPTIONS, 'deny');
|
||||
// avoid stablish connections outside of domain
|
||||
@ -54,13 +83,7 @@ export function setSecurityWebHeaders(
|
||||
|
||||
// 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
|
||||
export function validateName(
|
||||
req: $RequestExtend,
|
||||
res: $ResponseExtend,
|
||||
next: $NextFunctionVer,
|
||||
value: string,
|
||||
name: string
|
||||
): void {
|
||||
export function validateName(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer, value: string, name: string): void {
|
||||
if (value === '-') {
|
||||
// special case in couchdb usually
|
||||
next('route');
|
||||
@ -73,13 +96,7 @@ export function validateName(
|
||||
|
||||
// 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
|
||||
export function validatePackage(
|
||||
req: $RequestExtend,
|
||||
res: $ResponseExtend,
|
||||
next: $NextFunctionVer,
|
||||
value: string,
|
||||
name: string
|
||||
): void {
|
||||
export function validatePackage(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer, value: string, name: string): void {
|
||||
if (value === '-') {
|
||||
// special case in couchdb usually
|
||||
next('route');
|
||||
@ -93,26 +110,14 @@ export function validatePackage(
|
||||
export function media(expect: string | null): any {
|
||||
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
if (req.headers[HEADER_TYPE.CONTENT_TYPE] !== expect) {
|
||||
next(
|
||||
ErrorCode.getCode(
|
||||
HTTP_STATUS.UNSUPPORTED_MEDIA,
|
||||
'wrong content-type, expect: ' +
|
||||
expect +
|
||||
', got: ' +
|
||||
req.headers[HEADER_TYPE.CONTENT_TYPE]
|
||||
)
|
||||
);
|
||||
next(ErrorCode.getCode(HTTP_STATUS.UNSUPPORTED_MEDIA, 'wrong content-type, expect: ' + expect + ', got: ' + req.headers[HEADER_TYPE.CONTENT_TYPE]));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function encodeScopePackage(
|
||||
req: $RequestExtend,
|
||||
res: $ResponseExtend,
|
||||
next: $NextFunctionVer
|
||||
): void {
|
||||
export function encodeScopePackage(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
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
|
||||
req.url = req.url.replace(/^(\/@[^\/%]+)\/(?!$)/, '$1%2F');
|
||||
@ -120,11 +125,7 @@ export function encodeScopePackage(
|
||||
next();
|
||||
}
|
||||
|
||||
export function expectJson(
|
||||
req: $RequestExtend,
|
||||
res: $ResponseExtend,
|
||||
next: $NextFunctionVer
|
||||
): void {
|
||||
export function expectJson(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
if (!isObject(req.body)) {
|
||||
return next(ErrorCode.getBadRequest("can't parse incoming json"));
|
||||
}
|
||||
@ -151,34 +152,23 @@ export function allow(auth: IAuth): Function {
|
||||
return function (action: string): Function {
|
||||
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
req.pause();
|
||||
const packageName = req.params.scope
|
||||
? `@${req.params.scope}/${req.params.package}`
|
||||
: req.params.package;
|
||||
const packageVersion = req.params.filename
|
||||
? getVersionFromTarball(req.params.filename)
|
||||
: undefined;
|
||||
const packageName = req.params.scope ? `@${req.params.scope}/${req.params.package}` : req.params.package;
|
||||
const packageVersion = req.params.filename ? getVersionFromTarball(req.params.filename) : undefined;
|
||||
const remote: RemoteUser = req.remote_user;
|
||||
logger.trace(
|
||||
{ action, user: remote.name },
|
||||
`[middleware/allow][@{action}] allow for @{user}`
|
||||
);
|
||||
logger.trace({ action, user: remote.name }, `[middleware/allow][@{action}] allow for @{user}`);
|
||||
|
||||
auth['allow_' + action](
|
||||
{ packageName, packageVersion },
|
||||
remote,
|
||||
function (error, allowed): void {
|
||||
req.resume();
|
||||
if (error) {
|
||||
next(error);
|
||||
} else if (allowed) {
|
||||
next();
|
||||
} else {
|
||||
// last plugin (that's our built-in one) returns either
|
||||
// cb(err) or cb(null, true), so this should never happen
|
||||
throw ErrorCode.getInternalError(API_ERROR.PLUGIN_ERROR);
|
||||
}
|
||||
auth['allow_' + action]({ packageName, packageVersion }, remote, function (error, allowed): void {
|
||||
req.resume();
|
||||
if (error) {
|
||||
next(error);
|
||||
} else if (allowed) {
|
||||
next();
|
||||
} else {
|
||||
// last plugin (that's our built-in one) returns either
|
||||
// cb(err) or cb(null, true), so this should never happen
|
||||
throw ErrorCode.getInternalError(API_ERROR.PLUGIN_ERROR);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -189,12 +179,7 @@ export interface MiddlewareError {
|
||||
|
||||
export type FinalBody = Package | MiddlewareError | string;
|
||||
|
||||
export function final(
|
||||
body: FinalBody,
|
||||
req: $RequestExtend,
|
||||
res: $ResponseExtend,
|
||||
next: $NextFunctionVer
|
||||
): void {
|
||||
export function final(body: FinalBody, req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
if (res.statusCode === HTTP_STATUS.UNAUTHORIZED && !res.getHeader(HEADERS.WWW_AUTH)) {
|
||||
// they say it's required for 401, so...
|
||||
res.header(HEADERS.WWW_AUTH, `${TOKEN_BASIC}, ${TOKEN_BEARER}`);
|
||||
@ -214,10 +199,7 @@ export function final(
|
||||
}
|
||||
|
||||
// don't send etags with errors
|
||||
if (
|
||||
!res.statusCode ||
|
||||
(res.statusCode >= HTTP_STATUS.OK && res.statusCode < HTTP_STATUS.MULTIPLE_CHOICES)
|
||||
) {
|
||||
if (!res.statusCode || (res.statusCode >= HTTP_STATUS.OK && res.statusCode < HTTP_STATUS.MULTIPLE_CHOICES)) {
|
||||
res.header(HEADERS.ETAG, '"' + stringToMD5(body as string) + '"');
|
||||
}
|
||||
} else {
|
||||
@ -239,8 +221,7 @@ export function final(
|
||||
res.send(body);
|
||||
}
|
||||
|
||||
export const LOG_STATUS_MESSAGE =
|
||||
"@{status}, user: @{user}(@{remoteIP}), req: '@{request.method} @{request.url}'";
|
||||
export const LOG_STATUS_MESSAGE = "@{status}, user: @{user}(@{remoteIP}), req: '@{request.method} @{request.url}'";
|
||||
export const LOG_VERDACCIO_ERROR = `${LOG_STATUS_MESSAGE}, error: @{!error}`;
|
||||
export const LOG_VERDACCIO_BYTES = `${LOG_STATUS_MESSAGE}, bytes: @{bytes.in}/@{bytes.out}`;
|
||||
|
||||
@ -316,7 +297,7 @@ export function log(config: Config) {
|
||||
{
|
||||
request: {
|
||||
method: req.method,
|
||||
url: req.url
|
||||
url: req.url,
|
||||
},
|
||||
level: 35, // http
|
||||
user: (req.remote_user && req.remote_user.name) || null,
|
||||
@ -325,8 +306,8 @@ export function log(config: Config) {
|
||||
error: res._verdaccio_error,
|
||||
bytes: {
|
||||
in: bytesin,
|
||||
out: bytesout
|
||||
}
|
||||
out: bytesout,
|
||||
},
|
||||
},
|
||||
message
|
||||
);
|
||||
@ -353,11 +334,7 @@ export function log(config: Config) {
|
||||
}
|
||||
|
||||
// Middleware
|
||||
export function errorReportingMiddleware(
|
||||
req: $RequestExtend,
|
||||
res: $ResponseExtend,
|
||||
next: $NextFunctionVer
|
||||
): void {
|
||||
export function errorReportingMiddleware(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
res.report_error =
|
||||
res.report_error ||
|
||||
function (err: VerdaccioError): void {
|
||||
|
@ -10,20 +10,14 @@ import {
|
||||
formatAuthor,
|
||||
convertDistRemoteToLocalTarballUrls,
|
||||
getLocalRegistryTarballUri,
|
||||
isVersionValid
|
||||
isVersionValid,
|
||||
ErrorCode,
|
||||
} from '../../../lib/utils';
|
||||
import { allow } from '../../middleware';
|
||||
import { DIST_TAGS, HEADER_TYPE, HEADERS, HTTP_STATUS } from '../../../lib/constants';
|
||||
import { generateGravatarUrl } from '../../../utils/user';
|
||||
import { logger } from '../../../lib/logger';
|
||||
import {
|
||||
IAuth,
|
||||
$ResponseExtend,
|
||||
$RequestExtend,
|
||||
$NextFunctionVer,
|
||||
IStorageHandler,
|
||||
$SidebarPackage
|
||||
} from '../../../../types';
|
||||
import { IAuth, $ResponseExtend, $RequestExtend, $NextFunctionVer, IStorageHandler, $SidebarPackage } from '../../../../types';
|
||||
|
||||
const getOrder = (order = 'asc') => {
|
||||
return order === 'asc';
|
||||
@ -31,12 +25,7 @@ const getOrder = (order = 'asc') => {
|
||||
|
||||
export type PackcageExt = Package & { author: any; dist?: { tarball: string } };
|
||||
|
||||
function addPackageWebApi(
|
||||
route: Router,
|
||||
storage: IStorageHandler,
|
||||
auth: IAuth,
|
||||
config: Config
|
||||
): void {
|
||||
function addPackageWebApi(route: Router, storage: IStorageHandler, auth: IAuth, config: Config): void {
|
||||
const can = allow(auth);
|
||||
|
||||
const checkAllow = (name, remoteUser): Promise<boolean> =>
|
||||
@ -54,135 +43,109 @@ function addPackageWebApi(
|
||||
});
|
||||
|
||||
// Get list of all visible package
|
||||
route.get(
|
||||
'/packages',
|
||||
function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
storage.getLocalDatabase(async function (err, packages): Promise<void> {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
route.get('/packages', function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
storage.getLocalDatabase(async function (err, packages): Promise<void> {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
async function processPackages(packages: PackcageExt[] = []): Promise<any> {
|
||||
const permissions: PackcageExt[] = [];
|
||||
const packgesCopy = packages.slice();
|
||||
for (const pkg of packgesCopy) {
|
||||
const pkgCopy = { ...pkg };
|
||||
pkgCopy.author = formatAuthor(pkg.author);
|
||||
try {
|
||||
if (await checkAllow(pkg.name, req.remote_user)) {
|
||||
if (config.web) {
|
||||
pkgCopy.author.avatar = generateGravatarUrl(
|
||||
pkgCopy.author.email,
|
||||
config.web.gravatar
|
||||
);
|
||||
}
|
||||
if (!_.isNil(pkgCopy.dist) && !_.isNull(pkgCopy.dist.tarball)) {
|
||||
pkgCopy.dist.tarball = getLocalRegistryTarballUri(
|
||||
pkgCopy.dist.tarball,
|
||||
pkg.name,
|
||||
req,
|
||||
config.url_prefix
|
||||
);
|
||||
}
|
||||
permissions.push(pkgCopy);
|
||||
async function processPackages(packages: PackcageExt[] = []): Promise<any> {
|
||||
const permissions: PackcageExt[] = [];
|
||||
const packgesCopy = packages.slice();
|
||||
for (const pkg of packgesCopy) {
|
||||
const pkgCopy = { ...pkg };
|
||||
pkgCopy.author = formatAuthor(pkg.author);
|
||||
try {
|
||||
if (await checkAllow(pkg.name, req.remote_user)) {
|
||||
if (config.web) {
|
||||
pkgCopy.author.avatar = generateGravatarUrl(pkgCopy.author.email, config.web.gravatar);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.logger.error(
|
||||
{ name: pkg.name, error: err },
|
||||
'permission process for @{name} has failed: @{error}'
|
||||
);
|
||||
throw err;
|
||||
if (!_.isNil(pkgCopy.dist) && !_.isNull(pkgCopy.dist.tarball)) {
|
||||
pkgCopy.dist.tarball = getLocalRegistryTarballUri(pkgCopy.dist.tarball, pkg.name, req, config.url_prefix);
|
||||
}
|
||||
permissions.push(pkgCopy);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({ name: pkg.name, error: err }, 'permission process for @{name} has failed: @{error}');
|
||||
throw err;
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
const { web } = config;
|
||||
// @ts-ignore
|
||||
const order: boolean = config.web ? getOrder(web.sort_packages) : true;
|
||||
return permissions;
|
||||
}
|
||||
|
||||
const { web } = config;
|
||||
// @ts-ignore
|
||||
const order: boolean = config.web ? getOrder(web.sort_packages) : true;
|
||||
|
||||
try {
|
||||
next(sortByName(await processPackages(packages), order));
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
next(ErrorCode.getInternalError());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Get package readme
|
||||
route.get(
|
||||
'/package/readme/(@:scope/)?:package/:version?',
|
||||
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;
|
||||
route.get('/package/readme/(@:scope/)?:package/:version?', 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({
|
||||
name: packageName,
|
||||
uplinksLook: true,
|
||||
req,
|
||||
callback: function (err, info): void {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.TEXT_PLAIN);
|
||||
next(parseReadme(info.name, info.readme));
|
||||
storage.getPackage({
|
||||
name: packageName,
|
||||
uplinksLook: true,
|
||||
req,
|
||||
callback: function (err, info): void {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
route.get(
|
||||
'/sidebar/(@:scope/)?: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;
|
||||
res.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.TEXT_PLAIN);
|
||||
next(parseReadme(info.name, info.readme));
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
storage.getPackage({
|
||||
name: packageName,
|
||||
uplinksLook: true,
|
||||
keepUpLinkData: true,
|
||||
req,
|
||||
callback: function (err: Error, info: $SidebarPackage): void {
|
||||
if (_.isNil(err)) {
|
||||
const { v } = req.query;
|
||||
let sideBarInfo: any = _.clone(info);
|
||||
sideBarInfo.versions = convertDistRemoteToLocalTarballUrls(
|
||||
info,
|
||||
req,
|
||||
config.url_prefix
|
||||
).versions;
|
||||
if (isVersionValid(info, v)) {
|
||||
// @ts-ignore
|
||||
sideBarInfo.latest = sideBarInfo.versions[v];
|
||||
route.get('/sidebar/(@:scope/)?: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({
|
||||
name: packageName,
|
||||
uplinksLook: true,
|
||||
keepUpLinkData: true,
|
||||
req,
|
||||
callback: function (err: Error, info: $SidebarPackage): void {
|
||||
if (_.isNil(err)) {
|
||||
const { v } = req.query;
|
||||
let sideBarInfo: any = _.clone(info);
|
||||
sideBarInfo.versions = convertDistRemoteToLocalTarballUrls(info, req, config.url_prefix).versions;
|
||||
if (isVersionValid(info, v)) {
|
||||
// @ts-ignore
|
||||
sideBarInfo.latest = sideBarInfo.versions[v];
|
||||
sideBarInfo.latest.author = formatAuthor(sideBarInfo.latest.author);
|
||||
} else {
|
||||
sideBarInfo.latest = sideBarInfo.versions[info[DIST_TAGS].latest];
|
||||
if (sideBarInfo?.latest) {
|
||||
sideBarInfo.latest.author = formatAuthor(sideBarInfo.latest.author);
|
||||
} else {
|
||||
sideBarInfo.latest = sideBarInfo.versions[info[DIST_TAGS].latest];
|
||||
if (sideBarInfo?.latest) {
|
||||
sideBarInfo.latest.author = formatAuthor(sideBarInfo.latest.author);
|
||||
} else {
|
||||
res.status(HTTP_STATUS.NOT_FOUND);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
res.status(HTTP_STATUS.NOT_FOUND);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
sideBarInfo = deleteProperties(['readme', '_attachments', '_rev', 'name'], sideBarInfo);
|
||||
if (config.web) {
|
||||
sideBarInfo = addGravatarSupport(sideBarInfo, config.web.gravatar);
|
||||
} else {
|
||||
sideBarInfo = addGravatarSupport(sideBarInfo);
|
||||
}
|
||||
next(sideBarInfo);
|
||||
} else {
|
||||
res.status(HTTP_STATUS.NOT_FOUND);
|
||||
res.end();
|
||||
}
|
||||
sideBarInfo = deleteProperties(['readme', '_attachments', '_rev', 'name'], sideBarInfo);
|
||||
if (config.web) {
|
||||
sideBarInfo = addGravatarSupport(sideBarInfo, config.web.gravatar);
|
||||
} else {
|
||||
sideBarInfo = addGravatarSupport(sideBarInfo);
|
||||
}
|
||||
next(sideBarInfo);
|
||||
} else {
|
||||
res.status(HTTP_STATUS.NOT_FOUND);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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 express from 'express';
|
||||
import buildDebug from 'debug';
|
||||
|
||||
import { combineBaseUrl, getWebProtocol, isHTTPProtocol } from '../../lib/utils';
|
||||
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 renderHTML from './html/renderHTML';
|
||||
|
||||
const { setSecurityWebHeaders } = require('../middleware');
|
||||
const pkgJSON = require('../../../package.json');
|
||||
|
||||
const DEFAULT_LANGUAGE = 'es-US';
|
||||
const debug = buildDebug('verdaccio');
|
||||
|
||||
export function loadTheme(config) {
|
||||
if (_.isNil(config.theme) === false) {
|
||||
@ -37,7 +30,8 @@ export function loadTheme(config) {
|
||||
export function validatePrimaryColor(primaryColor) {
|
||||
const isHex = /^#+([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/i.test(primaryColor);
|
||||
if (!isHex) {
|
||||
return '';
|
||||
debug('invalid primary color %o', primaryColor);
|
||||
return;
|
||||
}
|
||||
|
||||
return primaryColor;
|
||||
@ -55,81 +49,31 @@ const sendFileCallback = (next) => (err) => {
|
||||
};
|
||||
|
||||
export default function (config, auth, storage) {
|
||||
let { staticPath, manifest, manifestFiles } = loadTheme(config) || require('@verdaccio/ui-theme')();
|
||||
debug('static path %o', staticPath);
|
||||
Search.configureStorage(storage);
|
||||
|
||||
/* eslint new-cap:off */
|
||||
const router = express.Router();
|
||||
|
||||
router.use(auth.webUIJWTmiddleware());
|
||||
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
|
||||
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
|
||||
// static assets
|
||||
router.get('/-/static/*', function (req, res, next) {
|
||||
const filename = req.params[0];
|
||||
const file = `${themePath}/${filename}`;
|
||||
const file = `${staticPath}/${filename}`;
|
||||
debug('render static file %o', file);
|
||||
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) {
|
||||
renderHTML(req, res);
|
||||
renderHTML(config, manifest, manifestFiles, req, res);
|
||||
debug('render html section');
|
||||
});
|
||||
|
||||
router.get('/', function (req, res) {
|
||||
renderHTML(req, res);
|
||||
renderHTML(config, manifest, manifestFiles, req, res);
|
||||
debug('render root');
|
||||
});
|
||||
|
||||
return router;
|
||||
|
134
src/lib/utils.ts
134
src/lib/utils.ts
@ -1,17 +1,16 @@
|
||||
/**
|
||||
* @prettier
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import assert from 'assert';
|
||||
import URL from 'url';
|
||||
import { IncomingHttpHeaders } from 'http2';
|
||||
import DefaultURL, { URL } from 'url';
|
||||
import _ from 'lodash';
|
||||
import buildDebug from 'debug';
|
||||
import semver from 'semver';
|
||||
import YAML from 'js-yaml';
|
||||
import validator from 'validator';
|
||||
import sanitizyReadme from '@verdaccio/readme';
|
||||
|
||||
import { Package, Version, Author } from '@verdaccio/types';
|
||||
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 { generateGravatarUrl, GENERIC_AVATAR } from '../utils/user';
|
||||
import { StringValue, AuthorAvatar } from '../../types';
|
||||
@ -21,11 +20,14 @@ import { normalizeContributors } from './storage-utils';
|
||||
|
||||
import { logger } from './logger';
|
||||
|
||||
const debug = buildDebug('verdaccio');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
require('pkginfo')(module);
|
||||
const pkgVersion = module.exports.version;
|
||||
const pkgName = module.exports.name;
|
||||
const validProtocols = ['https', 'http'];
|
||||
|
||||
export function getUserAgent(): string {
|
||||
assert(_.isString(pkgName));
|
||||
@ -116,32 +118,9 @@ export function validateMetadata(object: Package, name: string): Package {
|
||||
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 {
|
||||
// @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;
|
||||
}
|
||||
const tarballName = extractTarballFromUrl(uri);
|
||||
const headers = req.headers as IncomingHttpHeaders;
|
||||
const protocol = getWebProtocol(req.get(HEADERS.FORWARDED_PROTO), req.protocol);
|
||||
const domainRegistry = combineBaseUrl(protocol, headers.host, urlPrefix);
|
||||
const domainRegistry = getPublicUrl(urlPrefix || '', req);
|
||||
|
||||
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 {
|
||||
if (tag && data[DIST_TAGS][tag] !== version && semver.parse(version, true)) {
|
||||
// valid version - store
|
||||
@ -362,12 +332,19 @@ export function parseInterval(interval: any): number {
|
||||
* Detect running protocol (http or https)
|
||||
*/
|
||||
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 !== '') {
|
||||
debug('header protocol: %o', protocol);
|
||||
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 {
|
||||
@ -623,3 +600,76 @@ export function isRelatedToDeprecation(pkgInfo: Package): boolean {
|
||||
}
|
||||
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:
|
||||
npmjs:
|
||||
url: https://registry.npmjs.org/
|
||||
url: https://registry.verdaccio.org/
|
||||
|
||||
logs:
|
||||
- { 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 { spliceURL } from '../../../../src/utils/string';
|
||||
import {
|
||||
@ -14,7 +16,8 @@ import {
|
||||
getVersionFromTarball,
|
||||
sortByName,
|
||||
formatAuthor,
|
||||
isHTTPProtocol
|
||||
isHTTPProtocol,
|
||||
getPublicUrl,
|
||||
} from '../../../../src/lib/utils';
|
||||
import { DIST_TAGS, DEFAULT_USER } from '../../../../src/lib/constants';
|
||||
import { logger, setup } from '../../../../src/lib/logger';
|
||||
@ -32,15 +35,15 @@ describe('Utilities', () => {
|
||||
versions: {
|
||||
'1.0.0': {
|
||||
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': {
|
||||
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);
|
||||
@ -49,40 +52,40 @@ describe('Utilities', () => {
|
||||
describe('Sort packages', () => {
|
||||
const packages = [
|
||||
{
|
||||
name: 'ghc'
|
||||
name: 'ghc',
|
||||
},
|
||||
{
|
||||
name: 'abc'
|
||||
name: 'abc',
|
||||
},
|
||||
{
|
||||
name: 'zxy'
|
||||
}
|
||||
name: 'zxy',
|
||||
},
|
||||
];
|
||||
test('should order ascending', () => {
|
||||
expect(sortByName(packages)).toEqual([
|
||||
{
|
||||
name: 'abc'
|
||||
name: 'abc',
|
||||
},
|
||||
{
|
||||
name: 'ghc'
|
||||
name: 'ghc',
|
||||
},
|
||||
{
|
||||
name: 'zxy'
|
||||
}
|
||||
name: 'zxy',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should order descending', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
test('should have handle invalid protocol', () => {
|
||||
expect(getWebProtocol('ftp', '')).toBe('http');
|
||||
});
|
||||
|
||||
describe('getWebProtocol and HAProxy variant', () => {
|
||||
// https://github.com/verdaccio/verdaccio/issues/695
|
||||
|
||||
test('should handle http', () => {
|
||||
expect(getWebProtocol('http,http', 'https')).toBe('http');
|
||||
});
|
||||
@ -119,14 +125,15 @@ describe('Utilities', () => {
|
||||
|
||||
describe('convertDistRemoteToLocalTarballUrls', () => {
|
||||
test('should build a URI for dist tarball based on new domain', () => {
|
||||
const convertDist = convertDistRemoteToLocalTarballUrls(cloneMetadata(), {
|
||||
const req = httpMocks.createRequest({
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: fakeHost
|
||||
host: fakeHost,
|
||||
},
|
||||
// @ts-ignore
|
||||
get: () => 'http',
|
||||
protocol: 'http'
|
||||
protocol: 'http',
|
||||
url: '/',
|
||||
});
|
||||
const convertDist = convertDistRemoteToLocalTarballUrls(cloneMetadata(), req);
|
||||
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'));
|
||||
});
|
||||
@ -136,11 +143,9 @@ describe('Utilities', () => {
|
||||
headers: {},
|
||||
// @ts-ignore
|
||||
get: () => 'http',
|
||||
protocol: 'http'
|
||||
protocol: 'http',
|
||||
});
|
||||
expect(convertDist.versions['1.0.0'].dist.tarball).toEqual(
|
||||
convertDist.versions['1.0.0'].dist.tarball
|
||||
);
|
||||
expect(convertDist.versions['1.0.0'].dist.tarball).toEqual(convertDist.versions['1.0.0'].dist.tarball);
|
||||
});
|
||||
});
|
||||
|
||||
@ -148,7 +153,7 @@ describe('Utilities', () => {
|
||||
test('should delete a invalid latest version', () => {
|
||||
const pkg = cloneMetadata();
|
||||
pkg[DIST_TAGS] = {
|
||||
latest: '20000'
|
||||
latest: '20000',
|
||||
};
|
||||
|
||||
normalizeDistTags(pkg);
|
||||
@ -168,7 +173,7 @@ describe('Utilities', () => {
|
||||
test('should define last published version as latest with a custom dist-tag', () => {
|
||||
const pkg = cloneMetadata();
|
||||
pkg[DIST_TAGS] = {
|
||||
beta: '1.0.1'
|
||||
beta: '1.0.1',
|
||||
};
|
||||
|
||||
normalizeDistTags(pkg);
|
||||
@ -179,7 +184,7 @@ describe('Utilities', () => {
|
||||
test('should convert any array of dist-tags to a plain string', () => {
|
||||
const pkg = cloneMetadata();
|
||||
pkg[DIST_TAGS] = {
|
||||
latest: ['1.0.1']
|
||||
latest: ['1.0.1'],
|
||||
};
|
||||
|
||||
normalizeDistTags(pkg);
|
||||
@ -206,17 +211,21 @@ describe('Utilities', () => {
|
||||
|
||||
describe('combineBaseUrl', () => {
|
||||
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', () => {
|
||||
expect(combineBaseUrl('http', 'domain', '')).toEqual('http://domain');
|
||||
expect(combineBaseUrl('http', 'domain', '/')).toEqual('http://domain');
|
||||
expect(combineBaseUrl('http', 'domain', '/prefix/')).toEqual('http://domain/prefix');
|
||||
expect(combineBaseUrl('http', 'domain', '/prefix/deep')).toEqual(
|
||||
'http://domain/prefix/deep'
|
||||
);
|
||||
expect(combineBaseUrl('http', 'domain', 'only-prefix')).toEqual('only-prefix');
|
||||
expect(combineBaseUrl('http', 'domain.com', '')).toEqual('http://domain.com/');
|
||||
expect(combineBaseUrl('http', 'domain.com', '/')).toEqual('http://domain.com/');
|
||||
expect(combineBaseUrl('http', 'domain.com', '/prefix/')).toEqual('http://domain.com/prefix/');
|
||||
expect(combineBaseUrl('http', 'domain.com', '/prefix/deep')).toEqual('http://domain.com/prefix/deep/');
|
||||
expect(combineBaseUrl('http', 'domain.com', 'prefix/')).toEqual('http://domain.com/prefix/');
|
||||
expect(combineBaseUrl('http', 'domain.com', 'prefix')).toEqual('http://domain.com/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', simpleText)).toEqual('<p>simple text</p>');
|
||||
expect(parseReadme('testPackage', randomTextMarkdown)).toEqual(
|
||||
'<p>simple text </p>\n<h1 id="markdown">markdown</h1>'
|
||||
);
|
||||
expect(parseReadme('testPackage', randomTextMarkdown)).toEqual('<p>simple text </p>\n<h1 id="markdown">markdown</h1>');
|
||||
});
|
||||
|
||||
test('should show error for no readme data', () => {
|
||||
const noData = '';
|
||||
const spy = jest.spyOn(logger, 'error');
|
||||
expect(parseReadme('testPackage', noData)).toEqual('<p>ERROR: No README data found!</p>');
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
{ packageName: 'testPackage' },
|
||||
'@{packageName}: No readme found'
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith({ packageName: 'testPackage' }, '@{packageName}: No readme found');
|
||||
});
|
||||
});
|
||||
|
||||
@ -413,7 +417,7 @@ describe('Utilities', () => {
|
||||
|
||||
test('author, contributors and maintainers fields are not present', () => {
|
||||
const packageInfo = {
|
||||
latest: {}
|
||||
latest: {},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
@ -429,16 +433,16 @@ describe('Utilities', () => {
|
||||
|
||||
test('author field is a string type', () => {
|
||||
const packageInfo = {
|
||||
latest: { author: 'user@verdccio.org' }
|
||||
latest: { author: 'user@verdccio.org' },
|
||||
};
|
||||
const result = {
|
||||
latest: {
|
||||
author: {
|
||||
author: 'user@verdccio.org',
|
||||
avatar: GENERIC_AVATAR,
|
||||
email: ''
|
||||
}
|
||||
}
|
||||
email: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
@ -447,16 +451,16 @@ describe('Utilities', () => {
|
||||
|
||||
test('author field is an object type with author information', () => {
|
||||
const packageInfo = {
|
||||
latest: { author: { name: 'verdaccio', email: 'user@verdccio.org' } }
|
||||
latest: { author: { name: 'verdaccio', email: 'user@verdccio.org' } },
|
||||
};
|
||||
const result = {
|
||||
latest: {
|
||||
author: {
|
||||
avatar: 'https://www.gravatar.com/avatar/794d7f6ef93d0689437de3c3e48fadc7',
|
||||
email: 'user@verdccio.org',
|
||||
name: 'verdaccio'
|
||||
}
|
||||
}
|
||||
name: 'verdaccio',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
@ -466,8 +470,8 @@ describe('Utilities', () => {
|
||||
test('contributor field is a blank array', () => {
|
||||
const packageInfo = {
|
||||
latest: {
|
||||
contributors: []
|
||||
}
|
||||
contributors: [],
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
@ -480,9 +484,9 @@ describe('Utilities', () => {
|
||||
latest: {
|
||||
contributors: [
|
||||
{ name: 'user', email: 'user@verdccio.org' },
|
||||
{ name: 'user1', email: 'user1@verdccio.org' }
|
||||
]
|
||||
}
|
||||
{ name: 'user1', email: 'user1@verdccio.org' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = {
|
||||
@ -491,15 +495,15 @@ describe('Utilities', () => {
|
||||
{
|
||||
avatar: 'https://www.gravatar.com/avatar/794d7f6ef93d0689437de3c3e48fadc7',
|
||||
email: 'user@verdccio.org',
|
||||
name: 'user'
|
||||
name: 'user',
|
||||
},
|
||||
{
|
||||
avatar: 'https://www.gravatar.com/avatar/51105a49ce4a9c2bfabf0f6a2cba3762',
|
||||
email: 'user1@verdccio.org',
|
||||
name: 'user1'
|
||||
}
|
||||
]
|
||||
}
|
||||
name: 'user1',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
@ -509,8 +513,8 @@ describe('Utilities', () => {
|
||||
test('contributors field is an object', () => {
|
||||
const packageInfo = {
|
||||
latest: {
|
||||
contributors: { name: 'user', email: 'user@verdccio.org' }
|
||||
}
|
||||
contributors: { name: 'user', email: 'user@verdccio.org' },
|
||||
},
|
||||
};
|
||||
|
||||
const result = {
|
||||
@ -519,10 +523,10 @@ describe('Utilities', () => {
|
||||
{
|
||||
avatar: 'https://www.gravatar.com/avatar/794d7f6ef93d0689437de3c3e48fadc7',
|
||||
email: 'user@verdccio.org',
|
||||
name: 'user'
|
||||
}
|
||||
]
|
||||
}
|
||||
name: 'user',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
@ -533,8 +537,8 @@ describe('Utilities', () => {
|
||||
const contributor = 'Barney Rubble <b@rubble.com> (http://barnyrubble.tumblr.com/)';
|
||||
const packageInfo = {
|
||||
latest: {
|
||||
contributors: contributor
|
||||
}
|
||||
contributors: contributor,
|
||||
},
|
||||
};
|
||||
|
||||
const result = {
|
||||
@ -543,10 +547,10 @@ describe('Utilities', () => {
|
||||
{
|
||||
avatar: GENERIC_AVATAR,
|
||||
email: contributor,
|
||||
name: contributor
|
||||
}
|
||||
]
|
||||
}
|
||||
name: contributor,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
@ -557,8 +561,8 @@ describe('Utilities', () => {
|
||||
test('maintainers field is a blank array', () => {
|
||||
const packageInfo = {
|
||||
latest: {
|
||||
maintainers: []
|
||||
}
|
||||
maintainers: [],
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
@ -570,9 +574,9 @@ describe('Utilities', () => {
|
||||
latest: {
|
||||
maintainers: [
|
||||
{ name: 'user', email: 'user@verdccio.org' },
|
||||
{ name: 'user1', email: 'user1@verdccio.org' }
|
||||
]
|
||||
}
|
||||
{ name: 'user1', email: 'user1@verdccio.org' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = {
|
||||
@ -581,15 +585,15 @@ describe('Utilities', () => {
|
||||
{
|
||||
avatar: 'https://www.gravatar.com/avatar/794d7f6ef93d0689437de3c3e48fadc7',
|
||||
email: 'user@verdccio.org',
|
||||
name: 'user'
|
||||
name: 'user',
|
||||
},
|
||||
{
|
||||
avatar: 'https://www.gravatar.com/avatar/51105a49ce4a9c2bfabf0f6a2cba3762',
|
||||
email: 'user1@verdccio.org',
|
||||
name: 'user1'
|
||||
}
|
||||
]
|
||||
}
|
||||
name: 'user1',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
@ -606,7 +610,7 @@ describe('Utilities', () => {
|
||||
const user = {
|
||||
name: 'Verdaccion NPM',
|
||||
email: 'verdaccio@verdaccio.org',
|
||||
url: 'https://verdaccio.org'
|
||||
url: 'https://verdaccio.org',
|
||||
};
|
||||
expect(formatAuthor(user).url).toEqual(user.url);
|
||||
expect(formatAuthor(user).email).toEqual(user.email);
|
||||
@ -618,4 +622,254 @@ describe('Utilities', () => {
|
||||
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
|
||||
proxy: npmjs
|
||||
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 New Issue
Block a user