1
0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-11-08 23:25:51 +01:00

feat: improve url_prefix behavior (#2122)

read pr 2122 for more details
This commit is contained in:
Juan Picado 2021-03-29 12:32:37 +02:00 committed by GitHub
parent e5ce44c395
commit 15bb350ae4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1089 additions and 637 deletions

@ -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;
}
}

@ -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/'
```

@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@ -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;
});
}

@ -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');
}

@ -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;

@ -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;
});
});
});

@ -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>
"
`;

@ -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"
}

@ -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

Binary file not shown.