0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-12-16 21:56:25 -05:00

feats: components UI for custom user interfaces (#3548)

This commit is contained in:
Juan Picado 2023-01-02 20:13:45 +01:00 committed by GitHub
parent 86d813840d
commit 9997879743
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
409 changed files with 124796 additions and 9391 deletions

View file

@ -0,0 +1,27 @@
---
'@verdaccio/types': major
'@verdaccio/ui-theme': major
'@verdaccio/ui-components': major
---
feat(web): components for custom user interfaces
Provides a package that includes all components from the user interface, instead being embedded at the `@verdaccio/ui-theme` package.
```
npm i -D @verdaccio/ui-components
```
The package contains
- Components
- Providers
- Redux Storage
- Layouts (precomposed layouts ready to use)
- Custom Material Theme
The `@verdaccio/ui-theme` will consume this package and will use only those are need it.
> Prerequisites are using Redux, Material-UI and Translations with `i18next`.
Users could have their own Material UI theme and build custom layouts, adding new features without the need to modify the default project.

View file

@ -14,3 +14,7 @@ test/functional/store/*
docker-examples/**/lib/**/*.js docker-examples/**/lib/**/*.js
test/cli/e2e-yarn4/bin/yarn-4.0.0-rc.14.cjs test/cli/e2e-yarn4/bin/yarn-4.0.0-rc.14.cjs
yarn.js yarn.js
# storybook
packages/ui-components/storybook-static
dist.js
bundle.js

74
.github/workflows/ui-components.yml vendored Normal file
View file

@ -0,0 +1,74 @@
name: UI Components
on:
pull_request:
paths:
- .github/workflows/ui-components.yml
- 'packages/ui-components/**'
- 'package.json'
- 'pnpm-workspace.yaml'
- 'pnpm-lock.yaml'
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
deploy:
permissions:
contents: read # to fetch code (actions/checkout)
deployments: write
pull-requests: write # to comment on pull-requests
runs-on: ubuntu-latest
env:
NODE_OPTIONS: --max_old_space_size=4096
steps:
- uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # tag=v3
- name: Use Node
uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # tag=v3
with:
node-version-file: '.nvmrc'
- name: Cache pnpm modules
uses: actions/cache@c1a5de879eb890d062a85ee0252d6036480b1fe2 # v3
env:
cache-name: cache-pnpm-modules
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}-
- name: Install pnpm
run: |
corepack enable
corepack prepare --activate pnpm@6.32.15
- name: Install
run: pnpm recursive install --frozen-lockfile
- name: Build storybook
run: pnpm ui:storybook:build
- name: Copy public content
# the msw.js worker is need it at the storybook-static folder in production
run: cp -R packages/ui-components/public/* packages/ui-components/storybook-static
- name: 🔥 Deploy Production UI Netlify
if: (github.event_name == 'push' && github.ref == 'refs/heads/master') || github.event_name == 'workflow_dispatch'
uses: verdaccio/action-netlify-deploy@1a53f098745bf78555d11b436f5ee3af87e6b566
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
netlify-auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
netlify-site-id: ${{ secrets.NETLIFY_UI_SITE_ID }}
build-dir: './packages/ui-components/storybook-static'
- name: 🤖 Deploy Preview UI Components Netlify
if: github.repository == 'verdaccio/verdaccio'
uses: verdaccio/action-netlify-deploy@1a53f098745bf78555d11b436f5ee3af87e6b566
id: netlify_preview_ui
with:
draft: true
comment-on-pull-request: true
github-deployment-is-production: false
github-deployment-is-transient: true
github-token: ${{ secrets.GITHUB_TOKEN }}
netlify-auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
netlify-site-id: ${{ secrets.NETLIFY_UI_SITE_ID }}
build-dir: './packages/ui-components/storybook-static'

10
.gitignore vendored
View file

@ -41,13 +41,6 @@ packages/plugins/ui-theme/static
# CI Pnpm cache # CI Pnpm cache
.pnpm-store/ .pnpm-store/
# benchmark
api-results.json
hyper-results.json
hyper-results*.json
api-results*.json
.clinic/
#docs #docs
website/docs/api/**/*.md website/docs/api/**/*.md
website/docs/api/**/*.yml website/docs/api/**/*.yml
@ -57,3 +50,6 @@ packages/**/docs
# cypress # cypress
e2e/ui/cypress/videos/**/* e2e/ui/cypress/videos/**/*
e2e/ui/cypress/screenshots/**/* e2e/ui/cypress/screenshots/**/*
# storybook
packages/ui-components/storybook-static

View file

@ -36,3 +36,4 @@ packages/plugins/ui-theme/static/
test/cli/e2e-yarn4/bin/yarn-4.0.0-rc.14.cjs test/cli/e2e-yarn4/bin/yarn-4.0.0-rc.14.cjs
yarn.js yarn.js
website/docs/api/* website/docs/api/*
packages/ui-components/storybook-static/*

View file

@ -45,12 +45,11 @@
"@crowdin/cli": "3.9.1", "@crowdin/cli": "3.9.1",
"@emotion/react": "11.10.5", "@emotion/react": "11.10.5",
"@emotion/styled": "11.10.5", "@emotion/styled": "11.10.5",
"@mui/material": "5.11.1", "@testing-library/dom": "8.19.1",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.4",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"@mui/icons-material": "5.11.0",
"@mui/styles": "5.11.1",
"@mui/system": "5.11.1",
"@trivago/prettier-plugin-sort-imports": "3.4.0", "@trivago/prettier-plugin-sort-imports": "3.4.0",
"@types/async": "3.2.16", "@types/async": "3.2.16",
"@types/express": "4.17.15", "@types/express": "4.17.15",
@ -150,6 +149,8 @@
"ci:publish": "changeset publish", "ci:publish": "changeset publish",
"ts:ref": "update-ts-references --discardComments", "ts:ref": "update-ts-references --discardComments",
"website": "pnpm build --filter ...@verdaccio/website", "website": "pnpm build --filter ...@verdaccio/website",
"ui:storybook:build": "pnpm build-storybook --filter ...@verdaccio/ui-components",
"ui:storybook": "pnpm storybook --filter ...@verdaccio/ui-components",
"translations": "local-crowdin-api translations", "translations": "local-crowdin-api translations",
"crowdin:upload": "crowdin upload sources --auto-update --config ./crowdin.yaml", "crowdin:upload": "crowdin upload sources --auto-update --config ./crowdin.yaml",
"crowdin:download": "crowdin download --verbose --config ./crowdin.yaml", "crowdin:download": "crowdin download --verbose --config ./crowdin.yaml",

View file

@ -126,7 +126,7 @@ export type TemplateUIOptions = {
showDownloadTarball?: boolean; showDownloadTarball?: boolean;
showRaw?: boolean; showRaw?: boolean;
base: string; base: string;
primaryColor?: string; primaryColor: string;
version?: string; version?: string;
logoURI?: string; logoURI?: string;
flags: FlagsConfig; flags: FlagsConfig;

View file

@ -35,8 +35,6 @@ module.exports = Object.assign({}, config, {
// note: this section has to be on sync with webpack configuration // note: this section has to be on sync with webpack configuration
'verdaccio-ui/components/(.*)': '<rootDir>/src/components/$1', 'verdaccio-ui/components/(.*)': '<rootDir>/src/components/$1',
'verdaccio-ui/utils/(.*)': '<rootDir>/src/utils/$1', 'verdaccio-ui/utils/(.*)': '<rootDir>/src/utils/$1',
'verdaccio-ui/providers/(.*)': '<rootDir>/src/providers/$1',
'verdaccio-ui/design-tokens/(.*)': '<rootDir>/src/design-tokens/$1',
'react-markdown': '<rootDir>/src/__mocks__/react-markdown.tsx', 'react-markdown': '<rootDir>/src/__mocks__/react-markdown.tsx',
'remark-*': '<rootDir>/src/__mocks__/remark-plugin.ts', 'remark-*': '<rootDir>/src/__mocks__/remark-plugin.ts',
}, },

View file

@ -12,16 +12,12 @@
}, },
"homepage": "https://verdaccio.org", "homepage": "https://verdaccio.org",
"main": "index.js", "main": "index.js",
"peerDependencies": { "devDependencies": {
"@emotion/react": "11.10.5", "@emotion/react": "11.10.5",
"@emotion/styled": "11.10.5", "@emotion/styled": "11.10.5",
"@mui/material": "5.11.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"@mui/icons-material": "5.11.0", "@mui/icons-material": "5.11.0",
"@mui/styles": "5.11.1" "@mui/styles": "5.11.1",
}, "@mui/material": "5.11.1",
"devDependencies": {
"@emotion/babel-plugin": "11.10.5", "@emotion/babel-plugin": "11.10.5",
"@emotion/css": "11.10.5", "@emotion/css": "11.10.5",
"@emotion/jest": "11.10.5", "@emotion/jest": "11.10.5",
@ -34,6 +30,7 @@
"@verdaccio/node-api": "workspace:6.0.0-6-next.52", "@verdaccio/node-api": "workspace:6.0.0-6-next.52",
"@verdaccio/ui-components": "workspace:2.0.0-alpha.0", "@verdaccio/ui-components": "workspace:2.0.0-alpha.0",
"@verdaccio/types": "workspace:*", "@verdaccio/types": "workspace:*",
"normalize.css": "8.0.1",
"babel-loader": "8.3.0", "babel-loader": "8.3.0",
"babel-plugin-dynamic-import-node": "2.3.3", "babel-plugin-dynamic-import-node": "2.3.3",
"country-flag-icons": "1.5.5", "country-flag-icons": "1.5.5",
@ -53,7 +50,6 @@
"msw": "0.49.2", "msw": "0.49.2",
"mutationobserver-shim": "0.3.7", "mutationobserver-shim": "0.3.7",
"node-mocks-http": "1.12.1", "node-mocks-http": "1.12.1",
"normalize.css": "8.0.1",
"optimize-css-assets-webpack-plugin": "6.0.1", "optimize-css-assets-webpack-plugin": "6.0.1",
"ora": "5.4.1", "ora": "5.4.1",
"dompurify": "2.4.1", "dompurify": "2.4.1",

View file

@ -1,30 +1,9 @@
import React from 'react'; import React from 'react';
import { renderWithStore, screen } from 'verdaccio-ui/utils/test-react-testing-library'; import { renderWithStore, screen } from 'verdaccio-ui/utils/test-react-testing-library';
import { store } from '../store'; import { store } from '@verdaccio/ui-components';
import App from './App';
jest.mock('verdaccio-ui/utils/storage', () => { import App from './App';
class LocalStorageMock {
private store: Record<string, string>;
public constructor() {
this.store = {};
}
public clear(): void {
this.store = {};
}
public getItem(key: string): unknown {
return this.store[key] || null;
}
public setItem(key: string, value: string): void {
this.store[key] = value.toString();
}
public removeItem(key: string): void {
delete this.store[key];
}
}
return new LocalStorageMock();
});
// force the windows to expand to display items // force the windows to expand to display items
// https://github.com/bvaughn/react-virtualized/issues/493#issuecomment-640084107 // https://github.com/bvaughn/react-virtualized/issues/493#issuecomment-640084107
@ -40,6 +19,7 @@ describe('<App />', () => {
}); });
test('should not display the Header component', () => { test('should not display the Header component', () => {
// @ts-ignore
window.__VERDACCIO_BASENAME_UI_OPTIONS = { window.__VERDACCIO_BASENAME_UI_OPTIONS = {
showFooter: false, showFooter: false,
}; };

View file

@ -1,18 +1,33 @@
/* eslint-disable react/jsx-pascal-case */
/* eslint-disable react/jsx-max-depth */ /* eslint-disable react/jsx-max-depth */
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import FlagsIcon from 'country-flag-icons/react/3x2';
import React, { StrictMode, Suspense, useEffect } from 'react'; import React, { StrictMode, Suspense, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import ReactMarkdown from 'react-markdown';
import { Router } from 'react-router-dom'; import { Router } from 'react-router-dom';
import Loading from 'verdaccio-ui/components/Loading'; import remarkGfm from 'remark-gfm';
import StyleBaseline from 'verdaccio-ui/design-tokens/StyleBaseline';
import loadDayJSLocale from 'verdaccio-ui/design-tokens/load-dayjs-locale';
import { Theme } from 'verdaccio-ui/design-tokens/theme';
import { useConfig } from 'verdaccio-ui/providers/config';
import '../i18n/config'; import {
Footer,
Header,
HeaderInfoDialog,
Loading,
Theme,
TranslatorProvider,
useConfig,
} from '@verdaccio/ui-components';
import Contributors from '../components/Contributors';
import Support from '../components/Support';
import about from '../components/about.md';
import license from '../components/license.md';
import i18n from '../i18n/config';
import { listLanguages } from '../i18n/enabledLanguages';
import loadDayJSLocale from '../i18n/load-dayjs-locale';
import AppRoute, { history } from './AppRoute'; import AppRoute, { history } from './AppRoute';
import Footer from './Footer';
import Header from './Header';
const StyledBox = styled(Box)<{ theme?: Theme }>(({ theme }) => ({ const StyledBox = styled(Box)<{ theme?: Theme }>(({ theme }) => ({
backgroundColor: theme?.palette.background.default, backgroundColor: theme?.palette.background.default,
@ -27,6 +42,45 @@ const StyledBoxContent = styled(Box)<{ theme?: Theme }>(({ theme }) => ({
}, },
})); }));
const Flags = styled('span')<{ theme?: Theme }>(() => ({
width: '25px',
}));
function CustomInfoDialog({ onCloseDialog, title, isOpen }) {
const { t } = useTranslation();
return (
<HeaderInfoDialog
dialogTitle={title}
isOpen={isOpen}
onCloseDialog={onCloseDialog}
tabPanels={[
{
element: (
<>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{about}</ReactMarkdown>
<Contributors />
</>
),
},
{ element: <ReactMarkdown remarkPlugins={[remarkGfm]}>{license}</ReactMarkdown> },
{ element: <Support /> },
]}
tabs={[
{ label: t('about') },
{ label: t('dialog.license') },
{
label: '',
icon: (
<Flags>
<FlagsIcon.UA />
</Flags>
),
},
]}
/>
);
}
const App: React.FC = () => { const App: React.FC = () => {
const { configOptions } = useConfig(); const { configOptions } = useConfig();
@ -35,20 +89,23 @@ const App: React.FC = () => {
}, []); }, []);
return ( return (
<StrictMode> <StrictMode>
<Suspense fallback={<Loading />}> <TranslatorProvider
<StyleBaseline /> i18n={i18n}
<StyledBox display="flex" flexDirection="column" height="100%"> listLanguages={listLanguages}
<> loadDayJSLocale={loadDayJSLocale}
>
<Suspense fallback={<Loading />}>
<StyledBox display="flex" flexDirection="column" height="100%">
<Router history={history}> <Router history={history}>
<Header /> <Header HeaderInfoDialog={CustomInfoDialog} />
<StyledBoxContent flexGrow={1}> <StyledBoxContent flexGrow={1}>
<AppRoute /> <AppRoute />
</StyledBoxContent> </StyledBoxContent>
</Router> </Router>
{configOptions.showFooter && <Footer />} {configOptions.showFooter && <Footer />}
</> </StyledBox>
</StyledBox> </Suspense>
</Suspense> </TranslatorProvider>
</StrictMode> </StrictMode>
); );
}; };

View file

@ -2,26 +2,13 @@ import { createBrowserHistory } from 'history';
import React from 'react'; import React from 'react';
import { Route as ReactRouterDomRoute, Router, Switch } from 'react-router-dom'; import { Route as ReactRouterDomRoute, Router, Switch } from 'react-router-dom';
import loadable from './utils/loadable'; import { NotFound, Route, VersionProvider, loadable } from '@verdaccio/ui-components';
const NotFound = loadable(
() => import(/* webpackChunkName: "NotFound" */ 'verdaccio-ui/components/NotFound')
);
const VersionContextProvider = loadable(
() => import(/* webpackChunkName: "Provider" */ '../pages/Version/VersionContextProvider')
);
const VersionPage = loadable(() => import(/* webpackChunkName: "Version" */ '../pages/Version')); const VersionPage = loadable(() => import(/* webpackChunkName: "Version" */ '../pages/Version'));
const HomePage = loadable(() => import(/* webpackChunkName: "Home" */ '../pages/home')); const Front = loadable(() => import(/* webpackChunkName: "Home" */ '../pages/Front'));
enum Route {
ROOT = '/',
SCOPE_PACKAGE = '/-/web/detail/@:scope/:package',
SCOPE_PACKAGE_VERSION = '/-/web/detail/@:scope/:package/v/:version',
PACKAGE = '/-/web/detail/:package',
PACKAGE_VERSION = '/-/web/detail/:package/v/:version',
}
export const history = createBrowserHistory({ export const history = createBrowserHistory({
// @ts-ignore
basename: window?.__VERDACCIO_BASENAME_UI_OPTIONS?.url_prefix, basename: window?.__VERDACCIO_BASENAME_UI_OPTIONS?.url_prefix,
}); });
@ -30,27 +17,27 @@ const AppRoute: React.FC = () => {
<Router history={history}> <Router history={history}>
<Switch> <Switch>
<ReactRouterDomRoute exact={true} path={Route.ROOT}> <ReactRouterDomRoute exact={true} path={Route.ROOT}>
<HomePage /> <Front />
</ReactRouterDomRoute> </ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.PACKAGE}> <ReactRouterDomRoute exact={true} path={Route.PACKAGE}>
<VersionContextProvider> <VersionProvider>
<VersionPage /> <VersionPage />
</VersionContextProvider> </VersionProvider>
</ReactRouterDomRoute> </ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.PACKAGE_VERSION}> <ReactRouterDomRoute exact={true} path={Route.PACKAGE_VERSION}>
<VersionContextProvider> <VersionProvider>
<VersionPage /> <VersionPage />
</VersionContextProvider> </VersionProvider>
</ReactRouterDomRoute> </ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.SCOPE_PACKAGE_VERSION}> <ReactRouterDomRoute exact={true} path={Route.SCOPE_PACKAGE_VERSION}>
<VersionContextProvider> <VersionProvider>
<VersionPage /> <VersionPage />
</VersionContextProvider> </VersionProvider>
</ReactRouterDomRoute> </ReactRouterDomRoute>
<ReactRouterDomRoute exact={true} path={Route.SCOPE_PACKAGE}> <ReactRouterDomRoute exact={true} path={Route.SCOPE_PACKAGE}>
<VersionContextProvider> <VersionProvider>
<VersionPage /> <VersionPage />
</VersionContextProvider> </VersionProvider>
</ReactRouterDomRoute> </ReactRouterDomRoute>
<ReactRouterDomRoute> <ReactRouterDomRoute>
<NotFound /> <NotFound />

View file

@ -1,96 +0,0 @@
/* eslint-disable react/jsx-pascal-case */
/* eslint-disable verdaccio/jsx-spread */
import styled from '@emotion/styled';
import { Theme } from '@mui/material';
import Box from '@mui/material/Box';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import FlagsIcon from 'country-flag-icons/react/3x2';
import React from 'react';
import { useTranslation } from 'react-i18next';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import Contributors from './Contributors';
import RegistryInfoDialog from './RegistryInfoDialog';
import { Support } from './Support';
import about from './about.md';
import license from './license.md';
interface Props {
isOpen: boolean;
onCloseDialog: () => void;
}
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div
aria-labelledby={`simple-tab-${index}`}
hidden={value !== index}
id={`simple-tabpanel-${index}`}
role="tabpanel"
{...other}
>
{value === index && <Box sx={{ paddingTop: 3 }}>{children}</Box>}
</div>
);
}
const Flags = styled('span')<{ theme?: Theme }>(() => ({
width: '25px',
}));
const HeaderInfoDialog: React.FC<Props> = ({ onCloseDialog, isOpen }) => {
const [value, setValue] = React.useState(0);
const handleChange = (_event, newValue) => {
setValue(newValue);
};
const { t } = useTranslation();
return (
<RegistryInfoDialog
onClose={onCloseDialog}
open={isOpen}
title={t('dialog.registry-info.title')}
>
<Box sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs aria-label="basic tabs example" onChange={handleChange} value={value}>
<Tab label={t('about')} {...a11yProps(0)} />
<Tab label={t('dialog.license')} {...a11yProps(1)} />
<Tab
{...a11yProps(2)}
icon={
<Flags>
<FlagsIcon.UA />
</Flags>
}
/>
</Tabs>
</Box>
<TabPanel index={0} value={value}>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{about}</ReactMarkdown>
<Contributors />
</TabPanel>
<TabPanel index={1} value={value}>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{license}</ReactMarkdown>
</TabPanel>
<TabPanel index={2} value={value}>
<Support />
</TabPanel>
</Box>
</RegistryInfoDialog>
);
};
export default HeaderInfoDialog;

View file

@ -1,3 +0,0 @@
import { Support } from './Support';
export { Support };

View file

@ -1,125 +0,0 @@
import React from 'react';
import {
cleanup,
fireEvent,
renderWithStore,
screen,
} from 'verdaccio-ui/utils/test-react-testing-library';
import { DetailContext, DetailContextProps } from '../../pages/Version';
import { store } from '../../store/store';
import ActionBar, { Props } from './ActionBar';
const detailContextValue: DetailContextProps = {
packageName: 'foo',
readMe: 'test',
enableLoading: () => {},
isLoading: false,
hasNotBeenFound: false,
packageMeta: {
_uplinks: {},
latest: {
name: 'verdaccio-ui/local-storage',
version: '8.0.1-next.1',
dist: {
fileCount: 0,
unpackedSize: 0,
tarball: 'http://localhost:8080/bootstrap/-/bootstrap-4.3.1.tgz',
},
homepage: 'https://verdaccio.org',
bugs: {
url: 'https://github.com/verdaccio/monorepo/issues',
},
},
},
};
const ComponentToBeRendered: React.FC<{ contextValue: DetailContextProps; props?: Props }> = ({
contextValue,
props,
}) => (
<DetailContext.Provider value={contextValue}>
<ActionBar {...props} />
</DetailContext.Provider>
);
describe('<ActionBar /> component', () => {
afterEach(() => {
cleanup();
});
test('should render the component in default state', () => {
const { container } = renderWithStore(
<ComponentToBeRendered contextValue={detailContextValue} />,
store
);
expect(container.firstChild).toMatchSnapshot();
});
test('when there is no action bar data', () => {
const packageMeta = {
...detailContextValue.packageMeta,
latest: {
...detailContextValue.packageMeta.latest,
homepage: undefined,
bugs: undefined,
dist: {
...detailContextValue.packageMeta.latest.dist,
tarball: undefined,
},
},
};
const { container } = renderWithStore(
<ComponentToBeRendered contextValue={{ ...detailContextValue, packageMeta }} />,
store
);
expect(container.firstChild).toMatchSnapshot();
});
test('when there is a button to download a tarball', () => {
renderWithStore(<ComponentToBeRendered contextValue={{ ...detailContextValue }} />, store);
expect(screen.getByLabelText('Download tarball')).toBeTruthy();
});
test('when there is a button to raw manifest', () => {
renderWithStore(
<ComponentToBeRendered contextValue={{ ...detailContextValue }} props={{ showRaw: true }} />,
store
);
expect(screen.getByLabelText('Raw Manifest')).toBeTruthy();
});
test('when click button to raw manifest open a dialog with viewver', () => {
renderWithStore(
<ComponentToBeRendered contextValue={{ ...detailContextValue }} props={{ showRaw: true }} />,
store
);
fireEvent.click(screen.getByLabelText('Raw Manifest'));
expect(screen.getByTestId('raw-viewver-dialog')).toBeInTheDocument();
});
test('should not display download tarball button', () => {
renderWithStore(
<ComponentToBeRendered
contextValue={{ ...detailContextValue }}
props={{ showDownloadTarball: false }}
/>,
store
);
expect(screen.queryByLabelText('Download tarball')).toBeFalsy();
});
test('should not display show raw button', () => {
renderWithStore(
<ComponentToBeRendered contextValue={{ ...detailContextValue }} props={{ showRaw: false }} />,
store
);
expect(screen.queryByLabelText('Raw Manifest')).toBeFalsy();
});
test('when there is a button to open an issue', () => {
renderWithStore(<ComponentToBeRendered contextValue={{ ...detailContextValue }} />, store);
expect(screen.getByLabelText('Open an issue')).toBeTruthy();
});
});

View file

@ -1,3 +0,0 @@
import { default as MaterialUIAppBar } from '@mui/material/AppBar';
export default MaterialUIAppBar;

View file

@ -0,0 +1 @@
export { default } from './Contributors';

View file

@ -1,16 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { CopyClipboard as CopyClipboardOriginal } from '@verdaccio/ui-components';
interface Props {
text: string;
children?: React.ReactNode;
}
function CopyToClipBoard(props: Props) {
const { t } = useTranslation();
return <CopyClipboardOriginal title={t('copy-to-clipboard')} dataTestId="copy-icon" {...props} />;
}
export default CopyToClipBoard;

View file

@ -1,3 +0,0 @@
import { default as MaterialUIFormHelperText } from '@mui/material/FormHelperText';
export default MaterialUIFormHelperText;

View file

@ -1,3 +0,0 @@
import { default as MaterialUIInput } from '@mui/material/Input';
export default MaterialUIInput;

View file

@ -1,40 +0,0 @@
import React, { MouseEvent } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import Text, { TextProps } from './Text';
interface Props extends Pick<TextProps, 'variant'> {
external?: boolean;
className?: string;
to: string;
children?: React.ReactNode;
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
}
type LinkRef = HTMLAnchorElement;
/* eslint-disable verdaccio/jsx-spread */
const Link = React.forwardRef<LinkRef, Props>(function Link(
{ external, to, children, variant, className, ...props },
ref
) {
const LinkTextContent = <Text variant={variant}>{children}</Text>;
return external ? (
<a
className={className}
href={to}
ref={ref}
rel="noopener noreferrer"
target="_blank"
{...props}
>
{LinkTextContent}
</a>
) : (
<RouterLink className={className} innerRef={ref} to={to} {...props}>
{LinkTextContent}
</RouterLink>
);
});
export default Link;

View file

@ -1,39 +0,0 @@
import styled from '@emotion/styled';
import List from '@mui/material/List';
import { PRIMARY_COLOR } from 'verdaccio-ui/utils/colors';
import { default as MuiCard } from '../Card';
import { default as Typography } from '../Heading';
export const Wrapper = styled('div')({
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
justifyContent: 'center',
flex: 1,
padding: '16px',
});
export const Inner = styled('div')({
maxWidth: '650px',
display: 'flex',
flexDirection: 'column',
});
export const EmptyPackage = styled('img')({
width: '150px',
margin: '0 auto',
});
export const Heading = styled(Typography)({
color: PRIMARY_COLOR,
});
export const StyledList = styled(List)({
padding: 0,
color: PRIMARY_COLOR,
});
export const Card = styled(MuiCard)({
marginTop: '24px',
});

View file

@ -1,3 +0,0 @@
import { default as MaterialUIPaper } from '@mui/material/Paper';
export default MaterialUIPaper;

View file

@ -78,4 +78,4 @@ const Support = () => {
); );
}; };
export { Support }; export default Support;

View file

@ -0,0 +1 @@
export { default } from './Support';

View file

@ -1,6 +0,0 @@
import styled from '@emotion/styled';
import Chip from '@mui/material/Chip';
export const Tag = styled(Chip)({
margin: '5px',
});

View file

@ -1 +0,0 @@
export { default } from './Text';

View file

@ -1,3 +0,0 @@
import { default as MaterialUIToolbar } from '@mui/material/Toolbar';
export default MaterialUIToolbar;

View file

@ -1,13 +0,0 @@
import { createContext } from 'react';
import { Language } from 'src/i18n/enabledLanguages';
interface Props {
isDarkMode: boolean;
setIsDarkMode: (isDarkMode: boolean) => void;
language: Language;
setLanguage: (language: Language) => void;
}
const ThemeContext = createContext<undefined | Props>(undefined);
export default ThemeContext;

View file

@ -1,55 +0,0 @@
import { StyledEngineProvider, Theme, ThemeProvider } from '@mui/material/styles';
import i18next from 'i18next';
import React, { useCallback, useEffect } from 'react';
import { useConfig } from 'verdaccio-ui/providers/config';
import useLocalStorage from '../hooks/useLocalStorage';
import ThemeContext from './ThemeContext';
import loadDayJSLocale from './load-dayjs-locale';
import { ThemeMode, getTheme } from './theme';
declare module '@mui/styles/defaultTheme' {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface DefaultTheme extends Theme {}
}
function getDarkModeDefault(darkModeConfig) {
const prefersDarkMode = window.matchMedia?.('(prefers-color-scheme:dark)').matches;
if (typeof darkModeConfig === 'boolean') {
return darkModeConfig;
} else {
return prefersDarkMode;
}
}
const ThemeProviderWrapper: React.FC<{ children: any }> = ({ children }) => {
const currentLanguage = i18next.languages?.[0];
const { configOptions } = useConfig();
const isDarkModeDefault = getDarkModeDefault(configOptions.darkMode);
const isSwitchThemeEnabled = configOptions.showThemeSwitch;
const [isDarkModeStorage, setIsDarkMode] = useLocalStorage('darkMode', isDarkModeDefault);
const [language, setLanguage] = useLocalStorage('language', currentLanguage);
const isDarkMode = isSwitchThemeEnabled === true ? isDarkModeStorage : isDarkModeDefault;
const themeMode: ThemeMode = isDarkMode ? 'dark' : 'light';
const changeLanguage = useCallback(async () => {
await i18next.changeLanguage(language);
}, [language]);
const currentTheme = getTheme(themeMode, configOptions?.primaryColor);
useEffect(() => {
changeLanguage();
loadDayJSLocale();
}, [language, changeLanguage]);
return (
<ThemeContext.Provider value={{ isDarkMode, setIsDarkMode, language, setLanguage }}>
<StyledEngineProvider injectFirst={true}>
<ThemeProvider theme={currentTheme}>{children}</ThemeProvider>
</StyledEngineProvider>
</ThemeContext.Provider>
);
};
export default ThemeProviderWrapper;

View file

@ -1,7 +0,0 @@
import { useTheme as muiUseTheme } from '@mui/styles';
import { Theme } from './theme';
const useTheme = () => muiUseTheme<Theme>();
export default useTheme;

View file

@ -28,6 +28,10 @@ i18n
fallbackLng: DEFAULT_LANGUAGE, fallbackLng: DEFAULT_LANGUAGE,
whitelist: [...listLanguagesAsString], whitelist: [...listLanguagesAsString],
load: 'currentOnly', load: 'currentOnly',
react: {
wait: true,
useSuspense: false,
},
resources: languages, resources: languages,
debug: false, debug: false,
interpolation: { interpolation: {

View file

@ -2,24 +2,27 @@ import React from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { hot } from 'react-hot-loader/root'; import { hot } from 'react-hot-loader/root';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import AppConfigurationContext from 'verdaccio-ui/providers/config';
import {
AppConfigurationProvider,
StyleBaseline,
ThemeProvider,
store,
} from '@verdaccio/ui-components';
import App from './App'; import App from './App';
import StyleBaseline from './design-tokens/StyleBaseline';
import ThemeProvider from './design-tokens/ThemeProvider';
import { store } from './store';
const container = document.getElementById('root'); const container = document.getElementById('root');
const root = createRoot(container as HTMLElement); const root = createRoot(container as HTMLElement);
const AppContainer = () => ( const AppContainer = () => (
<Provider store={store}> <Provider store={store}>
<AppConfigurationContext> <AppConfigurationProvider>
<ThemeProvider> <ThemeProvider>
<StyleBaseline /> <StyleBaseline />
<App /> <App />
</ThemeProvider> </ThemeProvider>
</AppConfigurationContext> </AppConfigurationProvider>
</Provider> </Provider>
); );

View file

@ -0,0 +1,3 @@
import { Home } from '@verdaccio/ui-components';
export default Home;

View file

@ -1,32 +0,0 @@
import React from 'react';
import { cleanup, render, screen } from 'verdaccio-ui/utils/test-react-testing-library';
import { DetailContextProvider } from '../../context';
import Deprecated from './Deprecated';
describe('test Deprecated', () => {
afterEach(() => {
cleanup();
});
const packageMeta = {
latest: {
packageName: 'foo',
version: '1.0.0',
deprecated: 'duuuude, this is deprecated',
maintainers: [],
contributors: [],
},
};
test('should render the deprecated message', () => {
render(
// @ts-ignore
<DetailContextProvider value={{ packageMeta }}>
<Deprecated message={packageMeta.latest.deprecated} />
</DetailContextProvider>
);
expect(screen.getByText('duuuude, this is deprecated')).toBeInTheDocument();
});
});

View file

@ -1,40 +0,0 @@
import React from 'react';
import loadable from '../../../App/utils/loadable';
import DetailContainerContentReadme from './DetailContainerContentReadme';
export enum TabPosition {
README = 'readme',
DEPENDENCIES = 'dependencies',
VERSIONS = 'versions',
UPLINKS = 'uplinks',
}
const Versions = loadable(() => import(/* webpackChunkName: "Versions" */ './Versions'));
const UpLinks = loadable(() => import(/* webpackChunkName: "UpLinks" */ './UpLinks'));
const Dependencies = loadable(
() => import(/* webpackChunkName: "Dependencies" */ './Dependencies')
);
interface Props {
tabPosition: TabPosition;
readDescription?: string;
}
const DetailContainerContent: React.FC<Props> = ({ tabPosition, readDescription }) => {
switch (tabPosition) {
case TabPosition.README:
return <DetailContainerContentReadme description={readDescription} />;
case TabPosition.UPLINKS:
return <UpLinks />;
case TabPosition.VERSIONS:
return <Versions />;
case TabPosition.DEPENDENCIES:
return <Dependencies />;
default:
return null;
}
};
export default DetailContainerContent;

View file

@ -1,58 +0,0 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { cleanup, render } from 'verdaccio-ui/utils/test-react-testing-library';
import translationEN from '../../../../i18n/crowdin/ui.json';
import { DetailContext } from '../../context';
import { DetailContextProps } from '../../version-config';
import Versions from './Versions';
import data from './__partials__/data.json';
const detailContextValue: Partial<DetailContextProps> = {
packageName: 'foo',
packageMeta: data,
};
const ComponentToBeRendered: React.FC<{ contextValue: Partial<DetailContextProps> }> = ({
contextValue,
}) => (
<MemoryRouter>
<DetailContext.Provider value={contextValue}>
<Versions />
</DetailContext.Provider>
</MemoryRouter>
);
describe('<Version /> component', () => {
afterEach(() => {
cleanup();
});
// FIXME: this test is not deterministic (writes `N days ago` in the snapshot, where N is random number)
test.skip('should render the component in default state', () => {
const wrapper = render(<ComponentToBeRendered contextValue={detailContextValue} />);
expect(wrapper).toMatchSnapshot();
});
test('should render versions', () => {
const { getByText } = render(<ComponentToBeRendered contextValue={detailContextValue} />);
expect(getByText(translationEN.versions['version-history'])).toBeTruthy();
expect(getByText(translationEN.versions['current-tags'])).toBeTruthy();
// pick some versions
expect(getByText('2.3.0')).toBeTruthy();
expect(getByText('canary')).toBeTruthy();
});
test('should not render versions', () => {
const { queryByText } = render(
<ComponentToBeRendered contextValue={{ packageName: detailContextValue.packageName }} />
);
expect(queryByText(translationEN.versions['version-history'])).toBeFalsy();
expect(queryByText(translationEN.versions['current-tags'])).toBeFalsy();
});
test.todo('should click on version link');
});

View file

@ -1,39 +0,0 @@
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { DetailContext } from '../../context';
import VersionsHistoryList from './VersionsHistoryList';
import VersionsTagList from './VersionsTagList';
import { StyledText } from './styles';
const Versions: React.FC = () => {
const detailContext = useContext(DetailContext);
const { t } = useTranslation();
const { packageMeta, packageName } = detailContext;
if (!packageMeta) {
return null;
}
const { versions = {}, time = {}, ['dist-tags']: distTags = {} } = packageMeta;
return (
<>
{distTags && Object.keys(distTags).length > 0 && packageName && (
<>
<StyledText variant="subtitle1">{t('versions.current-tags')}</StyledText>
<VersionsTagList packageName={packageName} tags={distTags} time={time} />
</>
)}
{versions && Object.keys(versions).length > 0 && packageName && (
<>
<StyledText variant="subtitle1">{t('versions.version-history')}</StyledText>
<VersionsHistoryList packageName={packageName} time={time} versions={versions} />
</>
)}
</>
);
};
export default Versions;

View file

@ -1 +0,0 @@
export { default } from './DetailContainer';

View file

@ -1,98 +0,0 @@
import _ from 'lodash';
import React from 'react';
import { renderWithStore, screen, waitFor } from 'verdaccio-ui/utils/test-react-testing-library';
import { store } from '../../../store';
import { DetailContext } from '../context';
import { DetailContextProps } from '../version-config';
import DetailSidebar from './DetailSidebar';
const ComponentToBeRendered: React.FC<{ contextValue: DetailContextProps }> = ({
contextValue,
}) => (
<DetailContext.Provider value={contextValue}>
<DetailSidebar />
</DetailContext.Provider>
);
// https://stackoverflow.com/a/54010619/308341
jest.mock('react', () => {
const React = jest.requireActual('react');
React.Suspense = ({ children }) => children;
return React;
});
const detailContextValue: DetailContextProps = {
packageName: 'foo',
readMe: 'test',
enableLoading: () => {},
isLoading: false,
hasNotBeenFound: false,
packageMeta: {
_uplinks: {},
latest: {
name: 'verdaccio-ui/local-storage',
version: '8.0.1-next.1',
dist: {
fileCount: 0,
unpackedSize: 0,
tarball: 'http://localhost:8080/bootstrap/-/bootstrap-4.3.1.tgz',
},
homepage: 'https://verdaccio.org',
bugs: {
url: 'https://github.com/verdaccio/monorepo/issues',
},
},
},
};
describe('DetailSidebar', () => {
test('should render commonjs module icon', async () => {
const { getByAltText } = renderWithStore(
<ComponentToBeRendered
contextValue={_.merge(detailContextValue, {
packageMeta: {
latest: {
type: 'commonjs',
},
},
})}
/>,
store
);
await waitFor(() => expect(getByAltText('commonjs')).toBeInTheDocument());
});
test('should render ts module icon', () => {
renderWithStore(
<ComponentToBeRendered
contextValue={_.merge(detailContextValue, {
packageMeta: {
latest: {
types: './src/index.d.ts',
},
},
})}
/>,
store
);
expect(screen.getByAltText('typescript')).toBeInTheDocument();
});
test('should render es6 module icon', () => {
renderWithStore(
<ComponentToBeRendered
contextValue={_.merge(detailContextValue, {
packageMeta: {
latest: {
type: 'module',
},
},
})}
/>,
store
);
expect(screen.getByAltText('es6 modules')).toBeInTheDocument();
});
});

View file

@ -1,73 +0,0 @@
import styled from '@emotion/styled';
import React, { useContext } from 'react';
import { PackageMetaInterface } from 'types/packageMeta';
import ActionBar from 'verdaccio-ui/components/ActionBar';
import Author from 'verdaccio-ui/components/Author';
import Paper from 'verdaccio-ui/components/Paper';
import { Theme } from 'verdaccio-ui/design-tokens/theme';
import { useConfig } from 'verdaccio-ui/providers/config';
import { DetailContext } from '..';
import loadable from '../../../App/utils/loadable';
import DetailSidebarFundButton from './DetailSidebarFundButton';
import DetailSidebarTitle from './DetailSidebarTitle';
import Developers from './Developers';
import { DeveloperType } from './Developers/DevelopersTitle';
const Engines = loadable(() => import(/* webpackChunkName: "Engines" */ './Engines'));
const Dist = loadable(() => import(/* webpackChunkName: "Dist" */ './Dist'));
const Install = loadable(() => import(/* webpackChunkName: "Install" */ './Install'));
const Repository = loadable(() => import(/* webpackChunkName: "Repository" */ './Repository'));
const getModuleType = (manifest: PackageMetaInterface) => {
if (manifest.latest.main) {
return 'commonjs';
} else if (manifest.latest.type) {
return manifest.latest.type;
}
return;
};
const DetailSidebar: React.FC = () => {
const detailContext = useContext(DetailContext);
const { packageMeta, packageName, packageVersion } = detailContext;
const { configOptions } = useConfig();
const version = packageVersion || packageMeta?.latest.version || '';
const time = packageMeta?.time ? packageMeta.time[version] : '';
if (!packageMeta || !packageName) {
return null;
}
return (
<StyledPaper className={'sidebar-info'}>
<DetailSidebarTitle
description={packageMeta.latest?.description}
hasTypes={typeof packageMeta.latest.types === 'string'}
isLatest={typeof packageVersion === 'undefined'}
moduleType={getModuleType(packageMeta)}
packageName={packageName}
time={time}
version={version}
/>
<ActionBar
showDownloadTarball={configOptions.showDownloadTarball}
showRaw={configOptions.showRaw}
/>
<Install />
<DetailSidebarFundButton />
<Repository />
<Engines />
<Dist />
<Author />
<Developers type={DeveloperType.MAINTAINERS} />
<Developers type={DeveloperType.CONTRIBUTORS} />
</StyledPaper>
);
};
export default DetailSidebar;
const StyledPaper = styled(Paper)<{ theme?: Theme }>(({ theme }) => ({
padding: theme?.spacing(3, 2),
}));

View file

@ -1,109 +0,0 @@
import _ from 'lodash';
import React from 'react';
import { render } from 'verdaccio-ui/utils/test-react-testing-library';
import { DetailContext } from '../context';
import { DetailContextProps } from '../version-config';
import DetailSidebarFundButton from './DetailSidebarFundButton';
const ComponentToBeRendered: React.FC<{ contextValue: DetailContextProps }> = ({
contextValue,
}) => (
<DetailContext.Provider value={contextValue}>
<DetailSidebarFundButton />
</DetailContext.Provider>
);
const detailContextValue: DetailContextProps = {
packageName: 'foo',
readMe: 'test',
enableLoading: () => {},
isLoading: false,
hasNotBeenFound: false,
packageMeta: {
_uplinks: {},
latest: {
name: 'verdaccio-ui/local-storage',
version: '8.0.1-next.1',
dist: {
fileCount: 0,
unpackedSize: 0,
tarball: 'http://localhost:8080/bootstrap/-/bootstrap-4.3.1.tgz',
},
homepage: 'https://verdaccio.org',
bugs: {
url: 'https://github.com/verdaccio/monorepo/issues',
},
},
},
};
describe('test DetailSidebarFundButton', () => {
test('should not display the button if fund is missing', () => {
const wrapper = render(<ComponentToBeRendered contextValue={detailContextValue} />);
expect(wrapper.queryByText('Fund')).toBeNull();
});
test('should not display the button if url is missing', () => {
const value = _.merge(detailContextValue, {
packageMeta: {
latest: {
funding: {},
},
},
});
const wrapper = render(<ComponentToBeRendered contextValue={value} />);
expect(wrapper.queryByText('Fund')).toBeNull();
});
test('should not display the button if url is not a string', () => {
const value = _.merge(detailContextValue, {
packageMeta: {
latest: {
funding: {
url: null,
},
},
},
});
const wrapper = render(<ComponentToBeRendered contextValue={value} />);
expect(wrapper.queryByText('Fund')).toBeNull();
});
test('should not display the button if url is not an url', () => {
const value = _.merge(detailContextValue, {
packageMeta: {
latest: {
funding: {
url: 'somethign different as url',
},
},
},
});
const wrapper = render(<ComponentToBeRendered contextValue={value} />);
expect(wrapper.queryByText('Fund')).toBeNull();
});
test('should display the button if url is a valid url', () => {
const value = _.merge(detailContextValue, {
packageMeta: {
latest: {
funding: {
url: 'https://opencollective.com/verdaccio',
},
},
},
});
const wrapper = render(<ComponentToBeRendered contextValue={value} />);
expect(wrapper.getByText('Fund')).toBeTruthy();
});
});

View file

@ -1 +0,0 @@
export { default } from './Developers';

View file

@ -1,58 +0,0 @@
import Avatar from '@mui/material/Avatar';
import Grid from '@mui/material/Grid';
import List from '@mui/material/List';
import ListItemText from '@mui/material/ListItemText';
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { DetailContext } from '../../context';
import npm from '../Install/img/npm.svg';
import node from './img/node.png';
import { EngineListItem, StyledText } from './styles';
const Engine: React.FC = () => {
const { packageMeta } = useContext(DetailContext);
const { t } = useTranslation();
const engines = packageMeta?.latest?.engines;
if (!engines || (!engines.node && !engines.npm)) {
return null;
}
return (
<Grid container={true}>
{engines.node && (
<Grid item={true} xs={6}>
<List
subheader={
<StyledText variant={'subtitle1'}>{t('sidebar.engines.node-js')}</StyledText>
}
>
<EngineListItem button={true}>
<Avatar src={node} />
<ListItemText primary={engines.node} />
</EngineListItem>
</List>
</Grid>
)}
{engines.npm && (
<Grid item={true} xs={6}>
<List
subheader={
<StyledText variant={'subtitle1'}>{t('sidebar.engines.npm-version')}</StyledText>
}
>
<EngineListItem button={true}>
<Avatar src={npm} />
<ListItemText primary={engines.npm} />
</EngineListItem>
</List>
</Grid>
)}
</Grid>
);
};
export default Engine;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View file

@ -1,62 +0,0 @@
import React from 'react';
import { render, screen } from 'verdaccio-ui/utils/test-react-testing-library';
import { DetailContext } from '../../context';
import { DetailContextProps } from '../../version-config';
import Install from './Install';
import data from './__partials__/data.json';
const detailContextValue: Partial<DetailContextProps> = {
packageName: 'foo',
packageMeta: data,
};
const ComponentToBeRendered: React.FC = () => (
<DetailContext.Provider value={detailContextValue}>
<Install />
</DetailContext.Provider>
);
/* eslint-disable react/jsx-no-bind*/
describe('<Install />', () => {
test('renders correctly', () => {
render(<ComponentToBeRendered />);
expect(screen.getByText('pnpm install foo')).toBeInTheDocument();
expect(screen.getByText('yarn add foo')).toBeInTheDocument();
expect(screen.getByText('npm install foo')).toBeInTheDocument();
});
test('should have 3 children', () => {
window.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers = ['yarn', 'pnpm', 'npm'];
const { getByTestId } = render(<ComponentToBeRendered />);
const installListItems = getByTestId('installList');
// installitems + subHeader = 4
expect(installListItems.children.length).toBe(4);
});
test('should have the element NPM', () => {
window.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers = ['npm'];
const { getByTestId, queryByText } = render(<ComponentToBeRendered />);
expect(getByTestId('installListItem-npm')).toBeTruthy();
expect(queryByText(`npm install ${detailContextValue.packageName}`)).toBeTruthy();
expect(queryByText('Install using npm')).toBeTruthy();
expect(screen.queryByText('pnpm install foo')).not.toBeInTheDocument();
expect(screen.queryByText('yarn add foo')).not.toBeInTheDocument();
});
test('should have the element YARN', () => {
window.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers = ['yarn'];
const { getByTestId, queryByText } = render(<ComponentToBeRendered />);
expect(getByTestId('installListItem-yarn')).toBeTruthy();
expect(queryByText(`yarn add ${detailContextValue.packageName}`)).toBeTruthy();
expect(queryByText('Install using yarn')).toBeTruthy();
});
test('should have the element PNPM', () => {
window.__VERDACCIO_BASENAME_UI_OPTIONS.pkgManagers = ['pnpm'];
const { getByTestId, queryByText } = render(<ComponentToBeRendered />);
expect(getByTestId('installListItem-pnpm')).toBeTruthy();
expect(queryByText(`pnpm install ${detailContextValue.packageName}`)).toBeTruthy();
expect(queryByText('Install using pnpm')).toBeTruthy();
});
});

View file

@ -1,76 +0,0 @@
import React from 'react';
import { render } from 'verdaccio-ui/utils/test-react-testing-library';
import { DetailContext } from '../../context';
import { DetailContextProps } from '../../version-config';
import Repository from './Repository';
import data from './__partials__/data.json';
const detailContextValue: DetailContextProps = {
packageName: 'foo',
readMe: 'readMe',
enableLoading: () => {},
isLoading: false,
hasNotBeenFound: false,
packageMeta: data,
};
const ComponentToBeRendered: React.FC<{ contextValue: DetailContextProps }> = ({
contextValue,
}) => (
<DetailContext.Provider value={contextValue}>
<Repository />
</DetailContext.Provider>
);
describe('<Repository /> component', () => {
test('should load the component in default state', () => {
const { container } = render(<ComponentToBeRendered contextValue={detailContextValue} />);
expect(container.firstChild).toMatchSnapshot();
});
test('should render the component in with no repository data', () => {
const packageMeta = {
...detailContextValue.packageMeta,
latest: {
...detailContextValue.packageMeta?.latest,
repository: undefined,
},
};
const { queryByText } = render(
<ComponentToBeRendered
contextValue={{
...detailContextValue,
packageMeta,
}}
/>
);
expect(queryByText('Repository')).toBeFalsy();
});
test('should render the component in with invalid url', () => {
const packageMeta = {
...detailContextValue.packageMeta,
latest: {
...detailContextValue.packageMeta?.latest,
repository: {
type: 'git',
url: 'git://github.com/verdaccio/ui.git',
},
},
};
const { queryByText } = render(
<ComponentToBeRendered
contextValue={{
...detailContextValue,
packageMeta,
}}
/>
);
expect(queryByText('Repository')).toBeFalsy();
});
});

View file

@ -1 +0,0 @@
export { default } from './DetailSidebar';

View file

@ -1,67 +0,0 @@
import { waitFor } from '@testing-library/dom';
import React from 'react';
import { MemoryRouter } from 'react-router';
import { render } from 'verdaccio-ui/utils/test-react-testing-library';
import translationEN from '../../i18n/crowdin/ui.json';
import Version from './Version';
import data from './__partials__/data.json';
import { DetailContext } from './context';
// :-) we mock this otherways fails on render, some weird issue on material-ui
jest.mock('@mui/material/Avatar');
const detailContextValue = {
packageName: 'foo',
packageMeta: data,
readMe: 'Read me!',
enableLoading: jest.fn(),
isLoading: false,
hasNotBeenFound: false,
version: '1.0.0',
};
describe.skip('test Version page', () => {
test('should render the version page', async () => {
const { getByTestId, getByText } = render(
<MemoryRouter>
<DetailContext.Provider value={detailContextValue}>
<Version />
</DetailContext.Provider>
</MemoryRouter>
);
// we wait fetch response (mocked above)
await waitFor(() => getByTestId('version-layout'));
// check whether readme was loaded
const hasReadme = getByText(detailContextValue.readMe);
expect(hasReadme).toBeTruthy();
});
test('should render 404 page if the resources are not found', async () => {
const { getByText } = render(
<MemoryRouter>
<DetailContext.Provider
value={{
...detailContextValue,
hasNotBeenFound: true,
}}
>
<Version />
</DetailContext.Provider>
</MemoryRouter>
);
// we wait fetch response (mocked above)
const notFoundElement = await waitFor(() =>
getByText(translationEN.error['404']['sorry-we-could-not-find-it'])
);
expect(notFoundElement).toBeTruthy();
});
// Wanna contribute? Here we some scenarios we need to test
test.todo('should test click on tabs');
test.todo('should check what is rendered int he sidebar is correct');
test.todo('should test click back home on 404');
test.todo('should test click on elements in the sidebar');
test.todo('should test other not consider scenarios');
});

View file

@ -1,10 +1,7 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import Loading from 'verdaccio-ui/components/Loading';
import NotFound from 'verdaccio-ui/components/NotFound';
import { RootState } from '../../store/store'; import { Loading, NotFound, RootState, VersionLayout } from '@verdaccio/ui-components';
import VersionLayout from './VersionLayout';
const Version: React.FC = () => { const Version: React.FC = () => {
const manifestStore = useSelector((state: RootState) => state.manifest); const manifestStore = useSelector((state: RootState) => state.manifest);

View file

@ -1,10 +0,0 @@
import { Consumer, Provider, createContext } from 'react';
import { DetailContextProps, VersionPageConsumerProps } from './version-config';
export const DetailContext = createContext<Partial<DetailContextProps>>({});
export const DetailContextProvider: Provider<Partial<VersionPageConsumerProps>> =
DetailContext.Provider;
export const DetailContextConsumer: Consumer<Partial<VersionPageConsumerProps>> =
DetailContext.Consumer;

View file

@ -1,9 +0,0 @@
function getRouterPackageName(packageName: string, scope?: string): string {
if (scope) {
return `@${scope}/${packageName}`;
}
return packageName;
}
export default getRouterPackageName;

View file

@ -1,4 +1 @@
export { DetailContext, DetailContextConsumer, DetailContextProvider } from './context';
export { VersionPageConsumerProps, DetailContextProps } from './version-config';
export { default } from './Version'; export { default } from './Version';

View file

@ -1 +0,0 @@
export { PackageList } from './PackageList';

View file

@ -1 +0,0 @@
export { store } from './store';

View file

@ -1,5 +0,0 @@
export const NODE_MANAGER = {
npm: 'npm',
yarn: 'yarn',
pnpm: 'pnpm',
};

View file

@ -1,13 +0,0 @@
export default function fileSizeSI(
a: number,
b?: typeof Math,
c?: (p: number) => number,
d?: number,
e?: number
): string {
return (
((b = Math), (c = b.log), (d = 1e3), (e = (c(a) / c(d)) | 0), a / b.pow(d, e)).toFixed(2) +
' ' +
(e ? 'kMGTPEZY'[--e] + 'B' : 'Bytes')
);
}

View file

@ -1,25 +0,0 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
// NOTE: not used yet, but should replace the original implementation
// https://react.i18next.com/misc/testing#testing-without-stubbing
i18n.use(initReactI18next).init({
lng: 'en',
fallbackLng: 'en',
// have a common namespace used around the full app
ns: ['translations'],
defaultNS: 'translations',
debug: false,
interpolation: {
escapeValue: false, // not needed for react!!
},
resources: { en: { translations: {} } },
});
export default i18n;

View file

@ -1,70 +0,0 @@
// eslint-disable-next-line jest/no-mocks-import
import {
generateInvalidToken,
generateTokenWithExpirationAsString,
generateTokenWithOutExpiration,
generateTokenWithTimeRange,
} from '../../jest/unit/components/__mocks__/token';
import { isTokenExpire } from './login';
/* eslint-disable no-console */
console.error = jest.fn();
jest.mock('verdaccio-ui/providers/API/api', () => ({
// eslint-disable-next-line jest/no-mocks-import
request: require('../../jest/unit/components/__mocks__/api').default.request,
}));
jest.mock('i18next', () => {
const translationEN = require('../../i18n/translations/ui.json');
return {
t: (key: string) => {
const splittedKey = key.split('.');
let result = translationEN;
for (const element of splittedKey) {
result = result[element];
}
return result;
},
};
});
describe('isTokenExpire', (): void => {
test('isTokenExpire - null is not a valid payload', (): void => {
expect(isTokenExpire(null)).toBeTruthy();
});
test('isTokenExpire - token is not a valid payload', (): void => {
expect(isTokenExpire('not_a_valid_token')).toBeTruthy();
});
test('isTokenExpire - token should not expire in 24 hrs range', (): void => {
const token = generateTokenWithTimeRange(24);
expect(isTokenExpire(token)).toBeFalsy();
});
test('isTokenExpire - token should expire for current time', (): void => {
const token = generateTokenWithTimeRange();
expect(isTokenExpire(token)).toBeTruthy();
});
test('isTokenExpire - token expiration is not available', (): void => {
const token = generateTokenWithOutExpiration();
expect(isTokenExpire(token)).toBeTruthy();
});
test('isTokenExpire - token is not a valid json token', (): void => {
const token = generateInvalidToken();
const errorToken = new SyntaxError('Unexpected token i in JSON at position 0');
const result = ['Invalid token:', errorToken, 'xxxxxx.aW52YWxpZHRva2Vu.xxxxxx'];
expect(isTokenExpire(token)).toBeTruthy();
expect(console.error).toHaveBeenCalledWith(...result);
});
test('isTokenExpire - token expiration is not a number', (): void => {
const token = generateTokenWithExpirationAsString();
expect(isTokenExpire(token)).toBeTruthy();
});
});

View file

@ -1,44 +0,0 @@
import { Base64 } from 'js-base64';
import isNumber from 'lodash/isNumber';
import isString from 'lodash/isString';
export function isTokenExpire(token: string | null): boolean {
if (!isString(token)) {
return true;
}
const [, payload] = token.split('.');
if (!payload) {
return true;
}
let exp: number;
try {
exp = JSON.parse(Base64.decode(payload)).exp;
} catch (error: any) {
// eslint-disable-next-line no-console
console.error('Invalid token:', error, token);
return true;
}
if (!exp || !isNumber(exp)) {
return true;
}
// Report as expire before (real expire time - 30s)
const jsTimestamp = exp * 1000 - 30000;
const expired = Date.now() >= jsTimestamp;
return expired;
}
export interface LoginBody {
username?: string;
token?: string;
error?: LoginError;
}
export interface LoginError {
type: string;
description: string;
}

View file

@ -1,12 +0,0 @@
import memoryStorage from 'localstorage-memory';
let storage: Storage;
try {
localStorage.setItem('__TEST__', '');
localStorage.removeItem('__TEST__');
storage = localStorage;
} catch (err: any) {
storage = memoryStorage;
}
export default storage;

View file

@ -3,8 +3,8 @@ import { render } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { I18nextProvider } from 'react-i18next'; import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import ThemeProvider from 'verdaccio-ui/design-tokens/ThemeProvider';
import AppConfigurationProvider from 'verdaccio-ui/providers/config'; import { AppConfigurationProvider, ThemeProvider } from '@verdaccio/ui-components';
import i18nConfig from '../i18n/config'; import i18nConfig from '../i18n/config';

View file

@ -1,3 +0,0 @@
export function goToVerdaccioWebsite(): void {
window.open('https://verdaccio.org', '_blank');
}

View file

@ -16,7 +16,6 @@ module.exports = {
modules: ['node_modules'], modules: ['node_modules'],
alias: { alias: {
'verdaccio-ui/components': `${env.SRC_ROOT}/components`, 'verdaccio-ui/components': `${env.SRC_ROOT}/components`,
'verdaccio-ui/design-tokens': `${env.SRC_ROOT}/design-tokens`,
'verdaccio-ui/utils': `${env.SRC_ROOT}/utils`, 'verdaccio-ui/utils': `${env.SRC_ROOT}/utils`,
'verdaccio-ui/providers': `${env.SRC_ROOT}/providers`, 'verdaccio-ui/providers': `${env.SRC_ROOT}/providers`,
}, },

View file

@ -15,7 +15,6 @@
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"verdaccio-ui/components/*": ["src/components/*"], "verdaccio-ui/components/*": ["src/components/*"],
"verdaccio-ui/design-tokens/*": ["src/design-tokens/*"],
"verdaccio-ui/utils/*": ["src/utils/*"] "verdaccio-ui/utils/*": ["src/utils/*"]
} }
}, },

View file

@ -12,7 +12,6 @@
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"verdaccio-ui/components/*": ["src/components/*"], "verdaccio-ui/components/*": ["src/components/*"],
"verdaccio-ui/design-tokens/*": ["src/design-tokens/*"],
"verdaccio-ui/utils/*": ["src/utils/*"] "verdaccio-ui/utils/*": ["src/utils/*"]
} }
}, },

View file

@ -1,5 +1,6 @@
{ {
"extends": "../../.babelrc", "extends": "../../.babelrc",
"plugins": ["@emotion"],
"presets": [ "presets": [
[ [
"@babel/preset-env", "@babel/preset-env",
@ -11,5 +12,5 @@
} }
], ],
"@babel/preset-react" "@babel/preset-react"
], ]
} }

View file

@ -0,0 +1,100 @@
{
"settings": {
"react": {
"version": "16"
}
},
"extends": [
"plugin:verdaccio/recommended"
],
"plugins": [
"verdaccio"
],
"rules": {
"react/display-name": 0,
"react/no-deprecated": 1,
"react/jsx-no-target-blank": 1,
"react/destructuring-assignment": ["error", "always"],
"react/forbid-component-props": ["warn", { "forbid": ["style"] }],
"react/no-this-in-sfc": ["warn"],
"react/no-unsafe": ["warn"],
"react/sort-comp": ["warn", {
"order": [
"static-methods",
"lifecycle",
"render",
"everything-else",
"/^on.+$/",
"/^render.+$/"
]
}],
"react/void-dom-elements-no-children": ["warn"],
"react/no-did-mount-set-state": ["error", "disallow-in-func"],
"react/jsx-wrap-multilines": ["error",{
"declaration": "parens",
"assignment": "parens",
"return": "parens",
"arrow": "parens",
"condition": "parens",
"logical": "parens",
"prop": "ignore"
}],
"react/jsx-boolean-value": ["error", "always"],
"react/jsx-closing-tag-location": ["error"],
"react/jsx-curly-spacing": ["error", "never"],
"react/jsx-equals-spacing": ["error", "never"],
"react/jsx-first-prop-new-line": ["error", "multiline-multiprop"],
"react/jsx-handler-names": ["warn"],
"react/jsx-indent": ["error", 2],
"react/jsx-indent-props": ["error", 2],
"react/jsx-key": ["error"],
"react/jsx-max-depth":["error", { "max": 5}],
"react/jsx-max-props-per-line": ["error", {"maximum": 3, "when": "multiline" }],
"react/jsx-no-bind": ["error", {"allowArrowFunctions": true}],
"react/jsx-no-comment-textnodes": ["error"],
"react/jsx-no-duplicate-props": ["error"],
"react/jsx-no-literals": ["error"],
"react/jsx-no-undef": ["error"],
"react/prop-types": 0,
"react/jsx-one-expression-per-line": ["error", {"allow": "single-child"}],
"react/jsx-curly-brace-presence": ["warn", { "props": "ignore", "children": "ignore" }],
"react/jsx-pascal-case": ["error"],
"react/jsx-props-no-multi-spaces": ["error"],
"react/jsx-sort-default-props": ["error"],
"react/jsx-sort-props": ["error"],
"react/no-string-refs": ["error"],
"react/no-danger-with-children": ["error"],
"react/jsx-tag-spacing": ["error", {
"closingSlash": "never",
"beforeSelfClosing": "always",
"afterOpening": "allow-multiline",
"beforeClosing": "allow"
}],
"react/prefer-es6-class": [
2,
"always"
],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"verdaccio/jsx-no-style": ["warn"],
"verdaccio/jsx-spread": ["warn"],
"jest/expect-expect": 0,
"quote-props":["error", "as-needed"],
"prefer-spread": 1,
"linebreak-style": 0,
"max-len": ["error", 160]
},
"globals": {
"VERDACCIO_API_URL": true,
"RequestInit": true,
"TextFieldProps": true,
"HTMLElementTagNameMap": true,
"JSX": true,
"Reflect": true,
"__DEBUG__": true
},
"env": {
"browser": true,
"jest/globals": true
}
}

View file

@ -0,0 +1,13 @@
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: '@storybook/react',
layout: 'centered',
core: {
builder: 'webpack5',
},
};

View file

@ -0,0 +1,10 @@
<script>
window.__VERDACCIO_BASENAME_UI_OPTIONS = {
"title": "Verdaccio Local Dev",
"sort_packages": "asc", "primary_color": null,
"pkgManagers": ["npm", "yarn", "pnpm"],
"version": "1.0.0", "flags": { "searchRemote": true },
"filename": "index.html",
"base": "http://localhost:9000/"
}
</script>

View file

@ -0,0 +1,52 @@
import Flags from 'country-flag-icons/react/3x2';
import { Provider } from 'react-redux';
import config from '../../plugins/ui-theme/src/i18n/config';
import {
AppConfigurationProvider,
StyleBaseline,
ThemeProvider,
TranslatorProvider,
store,
} from '../src/';
const DEFAULT_LANGUAGE = 'en-US';
const listLanguages = [
{ lng: DEFAULT_LANGUAGE, icon: Flags.US, menuKey: 'lng.english' },
{ lng: 'cs-CZ', icon: Flags.CZ, menuKey: 'lng.czech' },
{ lng: 'pt-BR', icon: Flags.BR, menuKey: 'lng.portuguese' },
{ lng: 'es-ES', icon: Flags.ES, menuKey: 'lng.spanish' },
{ lng: 'de-DE', icon: Flags.DE, menuKey: 'lng.german' },
];
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
// preview-head file contains the __VERDACCIO_BASENAME_UI_OPTIONS
// required by AppConfigurationProvider
export const withMuiTheme = (Story) => (
<Provider store={store}>
<TranslatorProvider loadDayJSLocale={() => {}} i18n={config} listLanguages={listLanguages}>
<AppConfigurationProvider>
<ThemeProvider>
<StyleBaseline />
<Story />
</ThemeProvider>
</AppConfigurationProvider>
</TranslatorProvider>
</Provider>
);
if (typeof global.process === 'undefined') {
const { worker } = require('../msw/browser');
worker.start();
}
export const decorators = [withMuiTheme];

View file

@ -0,0 +1,40 @@
{
"name": "test",
"version": "1.0.22",
"description": "test",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "http",
"url": "git+https://github.com/test/test.git"
},
"keywords": [],
"author": {
"name": "",
"email": "",
"url": "",
"avatar": "data:image/svg+xml;utf8,%3Csvg%20height%3D%22100%22%20viewBox%3D%22-27%2024%20100%20100%22%20width%3D%22100%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3Cdefs%3E%3Ccircle%20cx%3D%2223%22%20cy%3D%2274%22%20id%3D%22a%22%20r%3D%2250%22%2F%3E%3C%2Fdefs%3E%3Cuse%20fill%3D%22%23F5EEE5%22%20overflow%3D%22visible%22%20xlink%3Ahref%3D%22%23a%22%2F%3E%3CclipPath%20id%3D%22b%22%3E%3Cuse%20overflow%3D%22visible%22%20xlink%3Ahref%3D%22%23a%22%2F%3E%3C%2FclipPath%3E%3Cg%20clip-path%3D%22url(%23b)%22%3E%3Cdefs%3E%3Cpath%20d%3D%22M36%2095.9c0%204%204.7%205.2%207.1%205.8%207.6%202%2022.8%205.9%2022.8%205.9%203.2%201.1%205.7%203.5%207.1%206.6v9.8H-27v-9.8c1.3-3.1%203.9-5.5%207.1-6.6%200%200%2015.2-3.9%2022.8-5.9%202.4-.6%207.1-1.8%207.1-5.8V85h26v10.9z%22%20id%3D%22c%22%2F%3E%3C%2Fdefs%3E%3Cuse%20fill%3D%22%23E6C19C%22%20overflow%3D%22visible%22%20xlink%3Ahref%3D%22%23c%22%2F%3E%3CclipPath%20id%3D%22d%22%3E%3Cuse%20overflow%3D%22visible%22%20xlink%3Ahref%3D%22%23c%22%2F%3E%3C%2FclipPath%3E%3Cpath%20clip-path%3D%22url(%23d)%22%20d%3D%22M23.2%2035h.2c3.3%200%208.2.2%2011.4%202%203.3%201.9%207.3%205.6%208.5%2012.1%202.4%2013.7-2.1%2035.4-6.3%2042.4-4%206.7-9.8%209.2-13.5%209.4H23h-.1c-3.7-.2-9.5-2.7-13.5-9.4-4.2-7-8.7-28.7-6.3-42.4%201.2-6.5%205.2-10.2%208.5-12.1%203.2-1.8%208.1-2%2011.4-2h.2z%22%20fill%3D%22%23D4B08C%22%2F%3E%3C%2Fg%3E%3Cpath%20d%3D%22M22.6%2040c19.1%200%2020.7%2013.8%2020.8%2015.1%201.1%2011.9-3%2028.1-6.8%2033.7-4%205.9-9.8%208.1-13.5%208.3h-.5c-3.8-.3-9.6-2.5-13.6-8.4-3.8-5.6-7.9-21.8-6.8-33.8C2.3%2053.7%203.5%2040%2022.6%2040z%22%20fill%3D%22%23F2CEA5%22%2F%3E%3C%2Fsvg%3E"
},
"license": "ISC",
"dependencies": {
"lodash": "^4.17.21"
},
"readmeFilename": "README.md",
"bugs": {
"url": "https://github.com/test/test/issues"
},
"homepage": "https://github.com/test/test#readme",
"_id": "test@1.0.22",
"_nodeVersion": "14.17.4",
"_npmVersion": "7.20.5",
"dist": {
"integrity": "sha512-2IDD0lLzGUL7YJ+17Oh9VtbOwdKLqBLS+ZFATDXi5R22TL2hZ9LBFE10bzsDovNc4xtgwZAk1/K+5LHTye4ztg==",
"shasum": "c9152f57636bce762ccb5a83113c42a5831579bc",
"tarball": "http://localhost:4873/test/-/test-1.0.22.tgz"
},
"contributors": [],
"time": "2021-08-14T20:15:19.336Z",
"users": {}
}

View file

@ -0,0 +1,3 @@
module.exports = () =>
// eslint-disable-next-line max-len
'# jQuery\n\n> jQuery is a fast, small, and feature-rich JavaScript library.\n\nFor information on how to get started and how to use jQuery, please see [jQuery\'s documentation](https://api.jquery.com/).\nFor source files and issues, please visit the [jQuery repo](https://github.com/jquery/jquery).\n\nIf upgrading, please see the [blog post for 3.6.3](https://blog.jquery.com/2022/12/20/jquery-3-6-3-released-a-quick-selector-fix/). This includes notable differences from the previous version and a more readable changelog.\n\n## Including jQuery\n\nBelow are some of the most common ways to include jQuery.\n\n### Browser\n\n#### Script tag\n\n```html\n<script src="https://code.jquery.com/jquery-3.6.3.min.js"></script>\n```\n\n#### Webpack / Browserify / Babel\n\nThere are several ways to use [Webpack](https://webpack.js.org/), [Browserify](http://browserify.org/) or [Babel](https://babeljs.io/). For more information on using these tools, please refer to the corresponding project\'s documentation. In the script, including jQuery will usually look like this:\n\n```js\nimport $ from "jquery";\n```\n\nIf you need to use jQuery in a file that\'s not an ECMAScript module, you can use the CommonJS syntax:\n\n```js\nvar $ = require( "jquery" );\n```\n\n#### AMD (Asynchronous Module Definition)\n\nAMD is a module format built for the browser. For more information, we recommend [require.js\' documentation](https://requirejs.org/docs/whyamd.html).\n\n```js\ndefine( [ "jquery" ], function( $ ) {\n\n} );\n```\n\n### Node\n\nTo include jQuery in [Node](https://nodejs.org/), first install with npm.\n\n```sh\nnpm install jquery\n```\n\nFor jQuery to work in Node, a window with a document is required. Since no such window exists natively in Node, one can be mocked by tools such as [jsdom](https://github.com/jsdom/jsdom). This can be useful for testing purposes.\n\n```js\nconst { JSDOM } = require( "jsdom" );\nconst { window } = new JSDOM( "" );\nconst $ = require( "jquery" )( window );';

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
<h1 id="storybook-cli">Storybook CLI</h1>
<p>This is a wrapper for <a href="https://www.npmjs.com/package/@storybook/cli">https://www.npmjs.com/package/@storybook/cli</a></p>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
let Reflect;
let idObj;
function checkIsNodeV6OrAbove() {
if (typeof process === 'undefined') {
return false;
}
return parseInt(process.versions.node.split('.')[0], 10) >= 6;
}
if (!checkIsNodeV6OrAbove()) {
Reflect = require('harmony-reflect');
}
idObj = new Proxy(
{},
{
get: function getter(target, key) {
if (key === '__esModule') {
return false;
}
return key;
},
}
);
module.exports = idObj;

View file

@ -0,0 +1,37 @@
const config = require('../../../jest/config');
module.exports = Object.assign({}, config, {
automock: false,
collectCoverage: false,
testEnvironment: 'jest-environment-jsdom-global',
transform: {
'^.+\\.(js|ts|tsx)$': 'babel-jest',
},
moduleFileExtensions: ['js', 'ts', 'tsx'],
testEnvironmentOptions: {
url: 'http://localhost:9000/',
},
rootDir: '..',
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect', '<rootDir>/jest/setup-env.ts'],
setupFiles: ['<rootDir>/jest/setup.ts'],
transformIgnorePatterns: ['<rootDir>/node_modules/(?!react-syntax-highlighter)'],
modulePathIgnorePatterns: [
'<rootDir>/coverage',
'<rootDir>/scripts',
'<rootDir>/tools',
'<rootDir>/build',
'<rootDir>/.vscode/',
'<rootDir>/test/e2e/',
],
snapshotSerializers: ['@emotion/jest/serializer'],
moduleNameMapper: {
'\\.(s?css)$': '<rootDir>/jest/identity.js',
'\\.(png)$': '<rootDir>/jest/identity.js',
'\\.(svg)$': '<rootDir>/jest/unit/empty.ts',
'\\.(jpg)$': '<rootDir>/jest/unit/empty.ts',
'\\.(md)$': '<rootDir>/jest/unit/empty-string.ts',
'github-markdown-css': '<rootDir>/jest/identity.js',
'react-markdown': '<rootDir>/src/__mocks__/react-markdown.tsx',
'remark-*': '<rootDir>/src/__mocks__/remark-plugin.ts',
},
});

View file

@ -0,0 +1 @@
jest.requireActual('babel/polyfill');

View file

@ -0,0 +1,57 @@
import { rest } from 'msw';
const packagesPayload = require('./api/home-packages.json');
export const handlers = [
rest.get('http://localhost:9000/-/verdaccio/data/packages', (req, res, ctx) => {
return res(ctx.json(Array(400).fill(packagesPayload)));
}),
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/storybook', (req, res, ctx) => {
const { v } = req.params;
if (v) {
return res(ctx.json(require('./api/storybook-v.json')));
} else {
return res(ctx.json(require('./api/storybook-sidebar.json')));
}
}),
rest.get('http://localhost:9000/-/verdaccio/data/search/*', (req, res, ctx) => {
return res(ctx.json(require('./api/search-verdaccio.json')));
}),
rest.get('http://localhost:9000/-/verdaccio/data/package/readme/storybook', (req, res, ctx) => {
return res(
ctx.text(`<h1 id="storybook-cli">Storybook CLI MSW.js</h1>
<p>This is a wrapper for <a href="https://www.npmjs.com/package/@storybook/cli">https://www.npmjs.com/package/@storybook/cli</a></p>
`)
);
}),
rest.get('http://localhost:9000/-/verdaccio/data/sidebar/jquery', (req, res, ctx) => {
return res(ctx.json(require('./api/jquery-sidebar.json')));
}),
rest.get('http://localhost:9000/-/verdaccio/data/package/readme/jquery', (req, res, ctx) => {
return res(ctx.text(require('./api/jquery-readme')()));
}),
rest.post<{ username: string; password: string }, { token: string; username: string }>(
'http://localhost:9000/-/verdaccio/sec/login',
// @ts-ignore
async (req, res, ctx) => {
// @ts-ignore
const body = await req.json();
if (body.username === 'fail') {
return ctx.status(401);
}
return res(
ctx.json({
username: body.username,
token: 'valid token',
})
);
}
),
];

View file

@ -0,0 +1,2 @@
declare const server: import('msw/lib/SetupServerApi-70cc71a7').S;
export { server };

View file

@ -0,0 +1,6 @@
import { setupServer } from 'msw/node';
import { handlers } from './server-handlers';
const server = setupServer(...handlers);
export { server };

View file

@ -0,0 +1,18 @@
import '@testing-library/jest-dom';
import '@testing-library/jest-dom/extend-expect';
import 'whatwg-fetch';
import { server } from './server';
// mock load translations to avoid warnings
// jest.mock('../src/i18n/loadTranslationFile');
beforeAll(() => {
server.listen({
onUnhandledRequest: 'warn',
});
});
afterEach(() => server.resetHandlers());
afterAll(() => {
server.close();
});

View file

@ -0,0 +1,31 @@
/**
* Setup configuration for Jest
* This file includes global settings for the JEST environment.
*/
import 'mutationobserver-shim';
// @ts-ignore : Property '__VERDACCIO_BASENAME_UI_OPTIONS' does not exist on type 'Global'.
global.__VERDACCIO_BASENAME_UI_OPTIONS = {
base: 'http://localhost:9000/',
protocol: 'http',
host: 'localhost',
primaryColor: '#4b5e40',
url_prefix: '',
darkMode: false,
language: 'en-US',
uri: 'http://localhost:9000/',
pkgManagers: ['pnpm', 'yarn', 'npm'],
title: 'Verdaccio Dev UI',
scope: '',
version: 'v1.0.0',
};
// mocking few DOM methods
// @ts-ignore : Property 'document' does not exist on type 'Global'.
if (global.document) {
// @ts-ignore : Type 'Mock<{ selectNodeContents: () => void; }, []>' is not assignable to type '() => Range'.
document.createRange = jest.fn((): void => ({
selectNodeContents: (): void => {},
}));
document.execCommand = jest.fn();
}

View file

@ -0,0 +1,24 @@
import { API_ERROR } from '../../../../lib/constants';
/**
* API mock for login endpoint
* @param {object} config configuration of api call
* @returns {promise}
*/
export default function login(config): Promise<unknown> {
return new Promise(function loginCallbackPromise(resolve, reject): void {
const body = JSON.parse(config.body);
if (body.username === 'sam' && body.password === '1234') {
resolve({
username: 'sam',
token: 'TEST_TOKEN',
});
} else {
// perhaps we should rethink this reject regarding the eslint rule
/* eslint-disable prefer-promise-reject-errors */
reject({
error: API_ERROR.BAD_USERNAME_PASSWORD,
});
}
});
}

View file

@ -0,0 +1,7 @@
/**
* Mock response for logo api
* @returns {promise}
*/
export default function <T>(): Promise<T> {
return Promise.resolve('http://localhost/-/static/logo.png');
}

View file

@ -0,0 +1,174 @@
export const packageInformation = [
{
name: 'jquery',
title: 'jQuery',
description: 'JavaScript library for DOM operations',
version: '3.3.2-pre',
main: 'dist/jquery.js',
homepage: 'https://jquery.com',
author: {
name: 'JS Foundation and other contributors',
url: 'https://github.com/jquery/jquery/blob/master/AUTHORS.txt',
avatar: '',
},
repository: {
type: 'git',
url: 'https://github.com/jquery/jquery.git',
},
keywords: ['jquery', 'javascript', 'browser', 'library'],
bugs: {
url: 'https://github.com/jquery/jquery/issues',
},
license: 'MIT',
dependencies: {},
devDependencies: {
'babel-core': '7.0.0-beta.0',
'babel-plugin-transform-es2015-for-of': '7.0.0-beta.0',
commitplease: '3.2.0',
'core-js': '2.5.7',
'eslint-config-jquery': '1.0.1',
grunt: '1.0.3',
'grunt-babel': '7.0.0',
'grunt-cli': '1.2.0',
'grunt-compare-size': '0.4.2',
'grunt-contrib-uglify': '3.3.0',
'grunt-contrib-watch': '1.1.0',
'grunt-eslint': '20.2.0',
'grunt-git-authors': '3.2.0',
'grunt-jsonlint': '1.1.0',
'grunt-karma': '2.0.0',
'grunt-newer': '1.3.0',
'grunt-npmcopy': '0.1.0',
'gzip-js': '0.3.2',
husky: '0.14.3',
insight: '0.10.1',
jsdom: '5.6.1',
karma: '2.0.3',
'karma-browserstack-launcher': '1.3.0',
'karma-chrome-launcher': '2.2.0',
'karma-firefox-launcher': '1.1.0',
'karma-qunit': '1.2.1',
'load-grunt-tasks': '4.0.0',
'native-promise-only': '0.8.1',
'promises-aplus-tests': '2.1.2',
q: '1.5.1',
'qunit-assert-step': '1.1.1',
qunitjs: '1.23.1',
'raw-body': '2.3.3',
requirejs: '2.3.5',
sinon: '2.3.7',
sizzle: '2.3.3',
'strip-json-comments': '2.0.1',
testswarm: '1.1.0',
'uglify-js': '3.4.0',
},
scripts: {
build: 'npm install && grunt',
start: 'grunt watch',
'test:browserless': 'grunt && grunt test:slow',
'test:browser': 'grunt && grunt karma:main',
test: 'grunt && grunt test:slow && grunt karma:main',
jenkins: 'npm run test:browserless',
precommit: 'grunt lint:newer qunit_fixture',
commitmsg: 'node node_modules/commitplease',
},
commitplease: {
nohook: true,
components: [
'Docs',
'Tests',
'Build',
'Support',
'Release',
'Core',
'Ajax',
'Attributes',
'Callbacks',
'CSS',
'Data',
'Deferred',
'Deprecated',
'Dimensions',
'Effects',
'Event',
'Manipulation',
'Offset',
'Queue',
'Selector',
'Serialize',
'Traversing',
'Wrap',
],
markerPattern: '^((clos|fix|resolv)(e[sd]|ing))|^(refs?)',
ticketPattern: '^((Closes|Fixes) ([a-zA-Z]{2,}-)[0-9]+)|^(Refs? [^#])',
},
},
{
name: 'lodash',
version: '4.17.4',
license: 'MIT',
private: true,
main: 'lodash.js',
author: {
name: 'John david dalton',
url: 'test url',
avatar: 'test avatar',
},
engines: {
node: '>=4.0.0',
},
sideEffects: false,
scripts: {
build: 'npm run build:main && npm run build:fp',
'build:fp': 'node lib/fp/build-dist.js',
'build:fp-modules': 'node lib/fp/build-modules.js',
'build:main': 'node lib/main/build-dist.js',
'build:main-modules': 'node lib/main/build-modules.js',
doc: 'node lib/main/build-doc github && npm run test:doc',
'doc:fp': 'node lib/fp/build-doc',
'doc:site': 'node lib/main/build-doc site',
'doc:sitehtml':
'optional-dev-dependency marky-markdown@^9.0.1 && npm run doc:site && node lib/main/build-site',
pretest: 'npm run build',
style: 'eslint *.js .internal/**/*.js',
test: 'npm run test:main && npm run test:fp',
'test:doc': 'markdown-doctest doc/*.md',
'test:fp': 'node test/test-fp',
'test:main': 'node test/test',
validate: 'npm run style && npm run test',
},
devDependencies: {
async: '^2.1.4',
benchmark: '^2.1.3',
chalk: '^1.1.3',
cheerio: '^0.22.0',
'codecov.io': '~0.1.6',
coveralls: '^2.11.15',
'curl-amd': '~0.8.12',
docdown: '~0.7.2',
dojo: '^1.12.1',
ecstatic: '^2.1.0',
eslint: '^3.15.0',
'eslint-plugin-import': '^2.2.0',
'fs-extra': '~1.0.0',
glob: '^7.1.1',
istanbul: '0.4.5',
jquery: '^3.1.1',
lodash: '4.17.3',
'lodash-doc-globals': '^0.1.1',
'markdown-doctest': '^0.9.1',
'optional-dev-dependency': '^2.0.0',
platform: '^1.3.3',
'qunit-extras': '^3.0.0',
qunitjs: '^2.1.0',
request: '^2.79.0',
requirejs: '^2.3.2',
'sauce-tunnel': '^2.5.0',
'uglify-js': '2.7.5',
webpack: '^1.14.0',
},
greenkeeper: {
ignore: ['lodash'],
},
},
];

View file

@ -0,0 +1,591 @@
export const packageMeta = {
name: 'verdaccio',
'dist-tags': { latest: '2.7.1', beta: '2.4.1-beta' },
time: {
modified: '2017-12-14T15:43:27.317Z',
created: '2016-07-28T12:48:43.536Z',
'1.4.0': '2016-07-28T12:48:43.536Z',
'2.0.0': '2016-08-26T22:36:41.762Z',
'2.0.1': '2016-08-29T13:26:21.754Z',
'2.1.0': '2016-10-12T00:48:03.025Z',
'2.1.1': '2017-02-07T06:43:22.801Z',
'2.2.0-v20170212': '2017-02-12T14:48:27.322Z',
'2.1.2': '2017-03-09T06:25:28.107Z',
'2.1.3': '2017-03-29T20:03:36.850Z',
'2.1.4': '2017-04-13T20:08:41.131Z',
'2.1.5': '2017-04-22T09:07:39.821Z',
'2.1.6': '2017-05-12T07:43:36.616Z',
'2.1.7': '2017-05-14T13:50:14.016Z',
'2.1.10': '2017-06-03T09:53:52.449Z',
'2.2.0': '2017-06-08T19:02:53.618Z',
'2.2.1': '2017-06-17T16:23:14.158Z',
'2.2.2': '2017-07-02T13:13:13.304Z',
'2.2.3': '2017-07-04T20:43:59.442Z',
'2.2.4': '2017-07-05T17:28:07.187Z',
'2.2.5': '2017-07-05T17:34:11.089Z',
'2.2.6': '2017-07-13T05:04:54.418Z',
'2.2.7': '2017-07-15T23:27:24.523Z',
'2.3.0-beta': '2017-07-15T23:31:31.664Z',
'2.2.7-r': '2017-07-18T19:44:48.946Z',
'2.3.0-beta-1': '2017-07-22T16:27:45.025Z',
'2.3.0-beta-2': '2017-07-22T17:12:09.905Z',
'2.3.0-beta-3': '2017-07-22T17:35:05.771Z',
'2.3.0-beta-4': '2017-07-22T18:22:42.563Z',
'2.3.0': '2017-07-22T23:08:37.513Z',
'2.3.1-pre': '2017-07-24T05:50:40.852Z',
'2.3.1': '2017-07-25T05:24:27.651Z',
'2.3.2': '2017-07-28T23:05:36.431Z',
'2.3.3': '2017-07-29T10:05:30.120Z',
'2.3.4': '2017-07-29T10:18:44.061Z',
'2.3.5': '2017-08-14T06:22:57.686Z',
'2.3.6': '2017-08-17T04:30:44.872Z',
'2.4.0': '2017-09-23T08:01:22.780Z',
'2.4.1-beta': '2017-10-01T08:57:14.509Z',
'2.5.0': '2017-10-01T12:31:06.333Z',
'2.5.1': '2017-10-01T13:32:06.584Z',
'2.6.0': '2017-10-18T20:22:32.836Z',
'2.6.1': '2017-10-19T17:26:24.083Z',
'2.6.2': '2017-10-21T08:37:16.527Z',
'2.6.3': '2017-10-21T16:04:05.556Z',
'2.6.4': '2017-10-31T17:47:03.647Z',
'2.6.5': '2017-11-05T09:09:31.332Z',
'2.6.6': '2017-11-08T22:47:16.504Z',
'2.7.0': '2017-12-05T23:25:06.372Z',
'2.7.1': '2017-12-14T15:43:27.317Z',
},
_uplinks: {
abc: { etag: 'ddfdxjn8m8n6gn70-8m', fetched: 1532297472000 },
npmjs: { etag: '"5a272ad2-4f6b1"', fetched: 1513266232741 },
xyz: { etag: '564748hydydygs-s7ehj', fetched: 1532124672000 },
},
_rev: '16-ba1b806df0298246',
_attachments: {},
latest: {
name: 'verdaccio',
version: '2.7.1',
description: 'Private npm repository server',
author: {
name: 'User NPM',
email: 'test@author.local',
avatar: 'https://www.gravatar.com/avatar/a5a236ba477ee98908600c40cda74f4a',
},
repository: {
type: 'git',
url: 'git://github.com/verdaccio/verdaccio.git',
},
main: 'index.js',
bin: { verdaccio: './bin/verdaccio' },
dependencies: {
'@verdaccio/file-locking': '0.0.3',
JSONStream: '^1.1.1',
'apache-md5': '^1.1.2',
async: '^2.0.1',
'body-parser': '^1.15.0',
bunyan: '^1.8.0',
chalk: '^2.0.1',
commander: '^2.11.0',
compression: '1.6.2',
cookies: '^0.7.0',
cors: '^2.8.3',
express: '4.15.3',
global: '^4.3.2',
handlebars: '4.0.5',
'http-errors': '^1.4.0',
'js-string-escape': '1.0.1',
'js-yaml': '^3.6.0',
jsonwebtoken: '^7.4.1',
lockfile: '^1.0.1',
lodash: '4.17.4',
lunr: '^0.7.0',
marked: '0.3.6',
mime: '^1.3.6',
minimatch: '^3.0.2',
mkdirp: '^0.5.1',
pkginfo: '^0.4.0',
request: '^2.72.0',
semver: '^5.1.0',
'unix-crypt-td-js': '^1.0.0',
},
devDependencies: {
axios: '0.16.2',
'babel-cli': '6.24.1',
'babel-core': '6.25.0',
'babel-eslint': '7.2.3',
'babel-loader': '7.1.1',
'babel-plugin-flow-runtime': '0.11.1',
'babel-plugin-transform-decorators-legacy': '1.3.4',
'babel-plugin-transform-runtime': '6.23.0',
'babel-polyfill': '^6.26.0',
'babel-preset-env': '1.5.2',
'babel-preset-flow': '6.23.0',
'babel-preset-react': '6.24.1',
'babel-preset-stage-2': '6.24.1',
'babel-preset-stage-3': '6.24.1',
'babel-runtime': '6.23.0',
'codacy-coverage': '2.0.2',
codecov: '2.2.0',
coveralls: '2.13.1',
'css-loader': '0.28.4',
'element-react': '1.0.16',
'element-theme-default': '1.3.7',
eslint: '4.2.0',
'eslint-config-google': '0.8.0',
'eslint-loader': '1.8.0',
'eslint-plugin-babel': '4.1.1',
'eslint-plugin-flowtype': '2.35.0',
'eslint-plugin-import': '2.6.1',
'eslint-plugin-react': '7.1.0',
'extract-text-webpack-plugin': '3.0.0',
'file-loader': '0.11.2',
'flow-runtime': '0.13.0',
'friendly-errors-webpack-plugin': '1.6.1',
'fs-extra': '4.0.1',
'github-markdown-css': '2.8.0',
'html-webpack-plugin': '2.29.0',
'in-publish': '2.0.0',
'localstorage-memory': '1.0.2',
mocha: '3.4.2',
'mocha-lcov-reporter': '1.3.0',
'node-sass': '4.5.3',
'normalize.css': '7.0.0',
nyc: '11.0.3',
ora: '1.3.0',
'prop-types': '15.5.10',
react: '15.6.1',
'react-dom': '15.6.1',
'react-hot-loader': '3.0.0-beta.7',
'react-router-dom': '4.1.1',
'react-syntax-highlighter': '5.6.2',
rimraf: '2.6.1',
'sass-loader': '6.0.6',
'source-map-loader': '0.2.1',
'standard-version': '4.2.0',
'style-loader': '0.18.2',
stylelint: '7.13.0',
'stylelint-config-standard': '16.0.0',
'stylelint-webpack-plugin': '0.8.0',
'url-loader': '0.5.8',
webpack: '3.2.0',
'webpack-dev-server': '2.5.0',
'webpack-merge': '4.1.0',
},
keywords: [
'private',
'package',
'repository',
'registry',
'enterprise',
'modules',
'proxy',
'server',
],
scripts: {
release: 'standard-version -a -s',
prepublish: 'in-publish && npm run build:webui || not-in-publish',
test: 'mocha ./test/functional ./test/unit --reporter=spec --full-trace',
'pre:ci': 'npm run build:webui',
'test:ci': 'npm run test:coverage',
'test:only': 'mocha ./test/functional ./test/unit',
'test:coverage': 'nyc npm t',
'coverage:html': 'nyc report --reporter=html',
'coverage:publish': 'nyc report --reporter=lcov | codecov',
lint: 'eslint .',
'lint:css': "stylelint 'src/**/*.scss' --syntax scss",
'pre:webpack': 'npm run lint && rimraf static/*',
'dev:webui': 'babel-node tools/dev.server.js',
'build:webui': 'npm run pre:webpack && webpack --config tools/webpack.prod.config.babel.js',
'build:docker': 'docker build -t verdaccio . --no-cache',
'build:docker:rpi': 'docker build -f Dockerfile.rpi -t verdaccio:rpi .',
},
engines: { node: '>=4.6.1', npm: '>=2.15.9' },
preferGlobal: true,
publishConfig: { registry: 'https://registry.verdaccio.org' },
license: 'WTFPL',
contributors: [
{
name: '030',
email: 'test1@test.local',
avatar: 'https://www.gravatar.com/avatar/4ef03c2bf8d8689527903212d96fb45b',
},
{
name: 'User NPM',
email: 'test2@test.local',
avatar: 'https://www.gravatar.com/avatar/a5a236ba477ee98908600c40cda74f4a',
},
{
name: 'User NPM',
email: 'test3@test.comu',
avatar: 'https://www.gravatar.com/avatar/41a61049006855759bd6ec82ef0543a0',
},
{
name: 'Alex Vernacchia',
email: 'tes4@test.local',
avatar: 'https://www.gravatar.com/avatar/06975001f7f2be7052bcf978700c6112',
},
{
name: 'Alexander Makarenko',
email: 'test5@test.local',
avatar: 'https://www.gravatar.com/avatar/d9acfc4ed4e49a436738ff26a722dce4',
},
{
name: 'Alexandre-io',
email: 'test6@test.local',
avatar: 'https://www.gravatar.com/avatar/2e095c7cfd278f72825d0fed6e12e3b1',
},
{
name: 'Aram Drevekenin',
email: 'test7@test.local',
avatar: 'https://www.gravatar.com/avatar/371edff6d79c39bb9e36bde39d41a4b0',
},
{
name: 'Bart Dubois',
email: 'test8@test.local',
avatar: 'https://www.gravatar.com/avatar/4acf72b14fcb459286c988c4523bafc8',
},
{
name: 'Barthélemy Vessemont',
email: 'test9@test.local',
avatar: 'https://www.gravatar.com/avatar/322cd2fad528a55c4351ec76d85ef525',
},
{
name: 'Brandon Nicholls',
email: 'test10@test.local',
avatar: 'https://www.gravatar.com/avatar/2d3b462f08f214ed459967aa7ef206f7',
},
{
name: 'Bren Norris',
email: 'test11@test.local',
avatar: 'https://www.gravatar.com/avatar/465a42204a22efada0f15b46a7cdad3a',
},
{
name: 'Brett Trotter',
email: 'test12@test.local',
avatar: 'https://www.gravatar.com/avatar/27a54519dcbe64c6d705f3cc4854595a',
},
{
name: 'Brian Peacock',
email: 'test13@test.local',
avatar: 'https://www.gravatar.com/avatar/3dd3d627330e7e048c13a7480f19842e',
},
{
name: 'Cedric Darne',
email: 'test14@test.local',
avatar: 'https://www.gravatar.com/avatar/0a617cebc6539940d7956c86e86c72a6',
},
{
name: 'Chad Killingsworth',
email: 'test15@test.local',
avatar: 'https://www.gravatar.com/avatar/a5825b2d69311e559e28a535e5f0d483',
},
{
name: 'Chris Breneman',
email: 'test16@test.local',
avatar: 'https://www.gravatar.com/avatar/3c5c3edef955c93edac672cbad04d7cd',
},
{
name: 'Cody Droz',
email: 'test17@test.local',
avatar: 'https://www.gravatar.com/avatar/b762ce4d14acfece36e783b1592d882b',
},
{
name: 'Daniel Rodríguez Rivero',
email: 'test18@test.local',
avatar: 'https://www.gravatar.com/avatar/ac7f548c31e8a002cfa41bd4c71e222d',
},
{
name: 'Denis Babineau',
email: 'test19@test.local',
avatar: 'https://www.gravatar.com/avatar/ee5a522e067759ba0403824ecebeab4d',
},
{
name: 'Emmanuel Narh',
email: 'test20@test.local',
avatar: 'https://www.gravatar.com/avatar/93a84a6120969fd181785ff9de834f0a',
},
{
name: 'Fabio Poloni',
email: 'test21@test.local',
avatar: 'https://www.gravatar.com/avatar/f9a05677360e5f52fcca6e1af9b0f2ee',
},
{
name: 'Facundo Chambó',
email: 'test22@test.local',
avatar: 'https://www.gravatar.com/avatar/ec9e7c590ba4081c25fcf197f90a4ea0',
},
{
name: 'Guilherme Bernal',
email: 'test23@test.local',
avatar: 'https://www.gravatar.com/avatar/e5d55dcf2495618e8b9f8778f8353ee0',
},
{
name: 'Jakub Jirutka',
email: 'test24@test.local',
avatar: 'https://www.gravatar.com/avatar/061bdb74aa4a543108658b277a257b4b',
},
{
name: 'James Newell',
email: 'test25@test.local',
avatar: 'https://www.gravatar.com/avatar/825190aaae6ec7fd95085e1fb6f261d2',
},
{
name: 'Jan Vansteenkiste',
email: 'test26@test.local',
avatar: 'https://www.gravatar.com/avatar/41835625a324201c796a0a0cffe4796b',
},
{
name: 'Jannis Achstetter',
email: 'test27@test.local',
avatar: 'https://www.gravatar.com/avatar/92d1cce007b032f4a63c6df764f18030',
},
{
name: 'Jeremy Moritz',
email: 'test28@test.local',
avatar: 'https://www.gravatar.com/avatar/008127e8f10293f43e62de3b7b3520e1',
},
{
name: 'John Gozde',
email: 'test29@test.local',
avatar: 'https://www.gravatar.com/avatar/3e8927c60cb043a56fdd6531cfcaddbc',
},
{
name: 'Jon de la Motte',
email: 'test30@test.local',
avatar: 'https://www.gravatar.com/avatar/126c1ea4fdb20bbb85c3ff735b7b0964',
},
{
name: 'Joseph Gentle',
email: 'test31@test.local',
avatar: 'https://www.gravatar.com/avatar/484f0b8ba8b7cc43db0be8f910a91254',
},
{
name: 'José De Paz',
email: 'test32@test.local',
avatar: 'https://www.gravatar.com/avatar/2532122835f5ebf1642b707ae088c895',
},
{
name: 'Juan Carlos Picado',
email: 'test33@test.local',
avatar: 'https://www.gravatar.com/avatar/c676605ff39f9c7a43f5518a8ce54e12',
},
{
name: 'Juan Carlos Picado',
email: 'test34@test.local',
avatar: 'https://www.gravatar.com/avatar/fba48015a688c38cc84e5b55b07858c0',
},
{
name: 'User NPM',
email: 'test35@test.local',
avatar: 'https://www.gravatar.com/avatar/fba48015a688c38cc84e5b55b07858c0',
},
{
name: 'User NPM @nickname',
email: 'test36@test.local',
avatar: 'https://www.gravatar.com/avatar/fba48015a688c38cc84e5b55b07858c0',
},
{
name: 'Kalman Speier',
email: 'test37@test.local',
avatar: 'https://www.gravatar.com/avatar/272806ba17639e2fbf811e51eb8bfb99',
},
{
name: 'Keyvan Fatehi',
email: 'test38@test.local',
avatar: 'https://www.gravatar.com/avatar/22735d1ba5765955914eb2d597dfaab5',
},
{
name: 'Kody J. Peterson',
email: 'test39@test.local',
avatar: 'https://www.gravatar.com/avatar/918a15afc52e9b0a67b2651191b23d04',
},
{
name: 'Madison Grubb',
email: 'test40@test.local',
avatar: 'https://www.gravatar.com/avatar/73b84fdf661c11d48d3370bfa197162b',
},
{
name: 'Manuel de Brito Fontes',
email: 'test41@test.local',
avatar: 'https://www.gravatar.com/avatar/8798ca0a499428e5e8f25d3614ac8b6e',
},
{
name: 'Mark Doeswijk',
email: 'test42@test.local',
avatar: 'https://www.gravatar.com/avatar/0d70ebd6c46dc01502bfab5f8c2d2bc5',
},
{
name: 'Meeeeow',
email: 'test43@test.local',
avatar: 'https://www.gravatar.com/avatar/baa061890d7b352ba121082272419a8a',
},
{
name: 'Meeeeow',
email: 'test44@test.local',
avatar: 'https://www.gravatar.com/avatar/12a36e093451d4c0f75d4240960ce29b',
},
{
name: 'Michael Arnel',
email: 'test45@test.local',
avatar: 'https://www.gravatar.com/avatar/5f9a5ed24c63609d52651258f6dd8c12',
},
{
name: 'Michael Crowe',
email: 'test46@test.local',
avatar: 'https://www.gravatar.com/avatar/eec9ee62019852da28a3bc91c57907f9',
},
{
name: 'Miguel Mejias',
email: 'test47@test.local',
avatar: 'https://www.gravatar.com/avatar/7289a01fedfdb9ddf855ee4dd4d41ae2',
},
{
name: 'Miroslav Bajtoš',
email: 'test48@test.local',
avatar: 'https://www.gravatar.com/avatar/b4d8831300713259f74aea79f842ca57',
},
{
name: 'Nate Ziarek',
email: 'test49@test.local',
avatar: 'https://www.gravatar.com/avatar/6442023756294fd43aa518bbe5cc6dcc',
},
{
name: 'Nick',
email: 'test50@test.local',
avatar: 'https://www.gravatar.com/avatar/8a810f12c9624ea2092852fe7c19f1ee',
},
{
name: 'Piotr Synowiec',
email: 'test51@test.local',
avatar: 'https://www.gravatar.com/avatar/87028f33a3e1e5b4201c371abddf93e2',
},
{
name: 'Rafael Cesar',
email: 'test52@test.local',
avatar: 'https://www.gravatar.com/avatar/204ed93fa5be7e2f9f299ad8bca6431f',
},
{
name: 'Robert Ewald',
email: 'test53@test.local',
avatar: 'https://www.gravatar.com/avatar/ec2166ce419f78fb354f128b01a4a44d',
},
{
name: 'Robert Groh',
email: 'test54@test.local',
avatar: 'https://www.gravatar.com/avatar/565ccb5374a3e0e31a75f11da2eb57aa',
},
{
name: 'Robin Persson',
email: 'test55@test.local',
avatar: 'https://www.gravatar.com/avatar/99da46e4d59664134b176869340f464b',
},
{
name: 'Romain Lai-King',
email: 'test56@test.local',
avatar: 'https://www.gravatar.com/avatar/69d0370c58399d0e0bbd15ccabfe1ec5',
},
{
name: 'Ryan Graham',
email: 'test57@test.local',
avatar: 'https://www.gravatar.com/avatar/8bd1dd86bbf8705a5a702b86a2f3a390',
},
{
name: 'Ryan Graham',
email: 'test58@test.local',
avatar: 'https://www.gravatar.com/avatar/e272ab422c1c629e9be26cba8b6c0166',
},
{
name: 'Sam Day',
email: 'test59@test.local',
avatar: 'https://www.gravatar.com/avatar/1886554b0562a0eeeb78a4d1f27917ea',
},
{
name: 'Tarun Garg',
email: 'test60@test.local',
avatar: 'https://www.gravatar.com/avatar/185e200c3451cfbe341f0e758626303a',
},
{
name: 'Thomas Cort',
email: 'test61@test.local',
avatar: 'https://www.gravatar.com/avatar/120d2921c33c1bd8dedfce67a28dcc63',
},
{
name: 'Tom Vincent',
email: 'test62@test.local',
avatar: 'https://www.gravatar.com/avatar/fb0c7faeda7f5d5632182a3d80381bfa',
},
{
name: 'Trent Earl',
email: 'test63@test.local',
avatar: 'https://www.gravatar.com/avatar/1e30abe66d21824b89c28d05e5b57d84',
},
{
name: 'Yannick Croissant',
email: 'test64@test.local',
avatar: 'https://www.gravatar.com/avatar/1e619ddb2a180222dd3d9f0348e65b9b',
},
{
name: 'Yannick Galatol',
email: 'test65@test.local',
avatar: 'https://www.gravatar.com/avatar/2f624f92326fef845bb2c07b392b7e48',
},
{
name: 'cklein',
email: 'test66@test.local',
avatar: 'https://www.gravatar.com/avatar/f8288370380881cf3afc5a92a63d652d',
},
{
name: 'danielo515',
email: 'test67@test.local',
avatar: 'https://www.gravatar.com/avatar/ac7f548c31e8a002cfa41bd4c71e222d',
},
{
name: 'jmwilkinson',
email: 'test68@test.local',
avatar: 'https://www.gravatar.com/avatar/3b99683f0a4c26a8906ecbe7968a4ade',
},
{
name: 'nickname',
email: 'test69@test.local',
avatar: 'https://www.gravatar.com/avatar/fba48015a688c38cc84e5b55b07858c0',
},
{
name: 'nickname',
email: 'test70@test.local',
avatar: 'https://www.gravatar.com/avatar/047ba1e853d20459e531619af5493c56',
},
{
name: 'maxlaverse',
email: 'test71@test.local',
avatar: 'https://www.gravatar.com/avatar/74324a2900906c45949a8c5cee6d0730',
},
{
name: 'saheba',
email: 'test72@test.local',
avatar: 'https://www.gravatar.com/avatar/77644c51856cab149e0f550c5f0c6ed8',
},
{
name: 'steve-p-com',
email: 'test73@test.local',
avatar: 'https://www.gravatar.com/avatar/bef1821d3036b8b9242c4999826c1c3c',
},
{
name: 'trent.earl',
email: 'test74@test.local',
avatar: 'https://www.gravatar.com/avatar/f84b8ae496f7c988dce5a71d773e75bb',
},
],
readmeFilename: 'README.md',
gitHead: '567dbe327819ed30afb96906f8d43f19740e2e3d',
bugs: { url: 'https://github.com/verdaccio/verdaccio/issues' },
homepage: 'https://github.com/verdaccio/verdaccio#readme',
_id: 'verdaccio@2.7.1',
_shasum: '958c919180e7f2ed6775f48d4ec64bd8de2a14df',
_from: '.',
_npmVersion: '3.10.10',
_nodeVersion: '6.9.5',
_npmUser: {},
dist: {
shasum: '958c919180e7f2ed6775f48d4ec64bd8de2a14df',
tarball: 'https://registry.verdaccio.org/verdaccio/-/verdaccio-2.7.1.tgz',
},
},
};

Some files were not shown because too many files have changed in this diff Show more