1
0
mirror of https://github.com/SomboChea/ui synced 2026-01-17 08:35:47 +07:00

feat: migrating flow to typescript (#47)

This PR convert the code base to Typescript, the changes are the following:

- migrate code base to Typescript (3.4.x)
- enable `eslint` and `@typescript-eslint/eslint-plugin` (warnings still need to be addressed in future pull request
- update relevant dependencies for this PR (linting, etc)
- enable `bundlezise` (it was disabled for some reason)

* refactor: refactoring to typescript

* refactor: migrating to typescript

* refactor: applied feedbacks

* fix: fixed conflicts

* refactored: changed registry

* refactor: updated registry & removed unnecessary lib

* fix: fixed registry ur

* fix: fixed page load

* refactor: refactored footer wip

* refactor: converting to ts..wip

* refactor: converting to ts. wip

* refactor: converting to ts. wip

* refactor: converting to ts

* refactor: converting to ts

* fix: fixed load errors

* refactor: converted files to ts

* refactor: removed flow from tests

* fix: removed transpiled files

* refactor: added ts-ignore

* fix: fixed errors

* fix: fixed types

* fix: fixing jest import -.-

* fix: fixing lint errors

* fix: fixing lint errors

* fix: fixed lint errors

* refactor: removed unnecessary tsconfig's config

* fix: fixing errors

* fix: fixed warning

* fix: fixed test

* refactor: wip

* refactor: wip

* refactor: wip

* fix: fixing tests: wip

* wip

* wip

* fix: fixed search test

* wip

* fix: fixing lint errors

* fix: re-added stylelint

* refactor: updated stylelint script

* fix: fixed: 'styles.js'  were found.

* fix: fixed Search tests

* chore: enable eslint

eslint needs expecitely to know which file has to lint, by default is JS, in this case we need also ts,tsx files eslint . --ext .js,.ts

* chore: vcode eslint settings

* chore: restore eslint previous conf

* chore: clean jest config

* chore: fix eslint warnings

* chore: eslint errors cleared

chore: clean warnings

chore: remove github actions test phases

chore: remove dupe rule

* chore: update handler name

* chore: restore logo from img to url css prop

- loading images with css is more performant than using img html tags, switching this might be a breaking change
- restore no-empty-source seems the linting do not accept false
- update snapshots
- remove @material-ui/styles

* chore: update stylelint linting

* chore: update stylelint linting

* chore: fix a mistake on move tabs to a function

* chore: eanble bundlezie

* chore: use default_executor in circleci

* chore: update readme
This commit is contained in:
Priscila Oliveira
2019-06-20 14:37:28 +02:00
committed by Juan Picado @jotadeveloper
parent 7d1764458b
commit 6b5d0b7e2e
358 changed files with 4730 additions and 58431 deletions

5
src/utils/.eslintrc Normal file
View File

@@ -0,0 +1,5 @@
{
"rules": {
"no-invalid-this": 0
}
}

View File

@@ -0,0 +1,5 @@
// @ts-ignore
if (!__DEBUG__) {
// @ts-ignore
__webpack_public_path__ = window.VERDACCIO_API_URL.replace(/\/verdaccio\/$/, '/static/') // eslint-disable-line
}

67
src/utils/api.ts Normal file
View File

@@ -0,0 +1,67 @@
import storage from './storage';
import '../../types';
/**
* Handles response according to content type
* @param {object} response
* @returns {promise}
*/
function handleResponseType(response): Promise<any> {
if (response.headers) {
const contentType = response.headers.get('Content-Type');
if (contentType.includes('application/pdf')) {
return Promise.all([response.ok, response.blob()]);
}
if (contentType.includes('application/json')) {
return Promise.all([response.ok, response.json()]);
}
// it includes all text types
if (contentType.includes('text/')) {
return Promise.all([response.ok, response.text()]);
}
}
return Promise.resolve();
}
class API {
public request(url: string, method = 'GET', options: any = {}): Promise<any> {
if (!window.VERDACCIO_API_URL) {
throw new Error('VERDACCIO_API_URL is not defined!');
}
const token = storage.getItem('token');
if (token) {
if (!options.headers) options.headers = {};
options.headers.authorization = `Bearer ${token}`;
}
if (!['http://', 'https://', '//'].some(prefix => url.startsWith(prefix))) {
// @ts-ignore
url = window.VERDACCIO_API_URL + url;
}
return new Promise<any>((resolve, reject) => {
fetch(url, {
method,
credentials: 'same-origin',
...options,
})
// @ts-ignore
.then(handleResponseType)
.then(([responseOk, body]) => {
if (responseOk) {
resolve(body);
} else {
reject(body);
}
})
.catch(error => {
reject(error);
});
});
}
}
export default new API();

View File

@@ -0,0 +1,33 @@
import React from 'react';
export function asyncComponent(getComponent) {
return class AsyncComponent extends React.Component {
static Component = null;
state = { Component: AsyncComponent.Component };
componentDidMount() {
const { Component } = this.state;
if (!Component) {
getComponent()
.then(({ default: Component }) => {
AsyncComponent.Component = Component;
/* eslint react/no-did-mount-set-state:0 */
this.setState({ Component });
})
.catch(err => {
console.error(err);
});
}
}
render() {
const { Component } = this.state;
if (Component) {
// eslint-disable-next-line verdaccio/jsx-spread
// @ts-ignore
return <Component {...this.props} />;
}
return null;
}
};
}

13
src/utils/calls.ts Normal file
View File

@@ -0,0 +1,13 @@
import API from './api';
export interface DetailPage {
readMe: any;
packageMeta: any;
}
export async function callDetailPage(packageName): Promise<DetailPage> {
const readMe = await API.request(`package/readme/${packageName}`, 'GET');
const packageMeta = await API.request(`sidebar/${packageName}`, 'GET');
return { readMe, packageMeta };
}

31
src/utils/cli-utils.ts Normal file
View File

@@ -0,0 +1,31 @@
import { SyntheticEvent } from 'react';
export const copyToClipBoardUtility = (str: string): any => (event: SyntheticEvent<HTMLElement>): void => {
event.preventDefault();
const node = document.createElement('div');
node.innerText = str;
if (document.body) {
document.body.appendChild(node);
const range = document.createRange();
const selection = window.getSelection() as Selection;
range.selectNodeContents(node);
selection.removeAllRanges();
selection.addRange(range);
document.execCommand('copy');
document.body.removeChild(node);
}
};
export function getCLISetConfigRegistry(command: string, scope: string, registryUrl: string): string {
return `${command} ${scope}registry ${registryUrl}`;
}
export function getCLISetRegistry(command: string, registryUrl: string): string {
return `${command} --registry ${registryUrl}`;
}
export function getCLIChangePassword(command: string, registryUrl: string): string {
return `${command} profile set password --registry ${registryUrl}`;
}

9
src/utils/constants.ts Normal file
View File

@@ -0,0 +1,9 @@
export const TEXT = {
CLIPBOARD_COPY: 'Copy to Clipboard',
};
export const NODE_MANAGER = {
npm: 'npm',
yarn: 'yarn',
pnpm: 'pnpm',
};

3
src/utils/file-size.ts Normal file
View File

@@ -0,0 +1,3 @@
export default function fileSizeSI(a?: any, b?: any, c?: any, d?: any, e?: any) {
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');
}

90
src/utils/login.test.ts Normal file
View File

@@ -0,0 +1,90 @@
import { isTokenExpire, makeLogin } from './login';
import { generateTokenWithTimeRange, generateTokenWithExpirationAsString, generateTokenWithOutExpiration } from '../../jest/unit/components/__mocks__/token';
/* eslint-disable no-console */
console.error = jest.fn();
jest.mock('./api', () => ({
request: require('../../jest/unit/components/__mocks__/api').default.request,
}));
describe('isTokenExpire', (): void => {
test('isTokenExpire - token is not present', () => {
expect(isTokenExpire()).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 = generateTokenWithExpirationAsString();
const result = ['Invalid token:', new SyntaxError('Unexpected token o in JSON at position 1'), 'xxxxxx.W29iamVjdCBPYmplY3Rd.xxxxxx'];
expect(isTokenExpire(token)).toBeTruthy();
expect(console.error).toHaveBeenCalledWith(...result);
});
});
describe('makeLogin', (): void => {
test('makeLogin - should give error for blank username and password', async (): Promise<void> => {
const result = {
error: {
description: "Username or password can't be empty!",
title: 'Unable to login',
type: 'error',
},
};
const login = await makeLogin();
expect(login).toEqual(result);
});
test('makeLogin - should login successfully', async (): Promise<void> => {
const { username, password } = { username: 'sam', password: '1234' };
const result = { token: 'TEST_TOKEN', username: 'sam' };
const login = await makeLogin(username, password);
expect(login).toEqual(result);
});
test('makeLogin - login should failed with 401', async () => {
const result = {
error: {
description: 'bad username/password, access denied',
title: 'Unable to login',
type: 'error',
},
};
const { username, password } = { username: 'sam', password: '123456' };
const login = await makeLogin(username, password);
expect(login).toEqual(result);
});
test('makeLogin - login should failed with when no data is sent', async () => {
const result = {
error: {
title: 'Unable to login',
type: 'error',
description: "Username or password can't be empty!",
},
};
const { username, password } = { username: '', password: '' };
const login = await makeLogin(username, password);
expect(login).toEqual(result);
});
});

81
src/utils/login.ts Normal file
View File

@@ -0,0 +1,81 @@
import isString from 'lodash/isString';
import isNumber from 'lodash/isNumber';
import isEmpty from 'lodash/isEmpty';
import { Base64 } from 'js-base64';
import API from './api';
import { HEADERS } from '../../lib/constants';
export function isTokenExpire(token?: any) {
if (!isString(token)) {
return true;
}
let [, payload]: any = token.split('.');
if (!payload) {
return true;
}
try {
payload = JSON.parse(Base64.decode(payload));
} catch (error) {
// eslint-disable-next-line
console.error('Invalid token:', error, token);
return true;
}
if (!payload.exp || !isNumber(payload.exp)) {
return true;
}
// Report as expire before (real expire time - 30s)
const jsTimestamp = payload.exp * 1000 - 30000;
const expired = Date.now() >= jsTimestamp;
return expired;
}
export interface LoginBody {
username?: string;
token?: string;
error?: LoginError;
}
export interface LoginError {
title: string;
type: string;
description: string;
}
export async function makeLogin(username?: string, password?: string): Promise<LoginBody> {
// checks isEmpty
if (isEmpty(username) || isEmpty(password)) {
const error = {
title: 'Unable to login',
type: 'error',
description: "Username or password can't be empty!",
};
return { error };
}
try {
const response: LoginBody = await API.request('login', 'POST', {
body: JSON.stringify({ username, password }),
headers: {
Accept: HEADERS.JSON,
'Content-Type': HEADERS.JSON,
},
});
const result: LoginBody = {
username: response.username,
token: response.token,
};
return result;
} catch (e) {
const error = {
title: 'Unable to login',
type: 'error',
description: e.error,
};
return { error };
}
}

94
src/utils/package.test.ts Normal file
View File

@@ -0,0 +1,94 @@
import { formatLicense, formatRepository, formatDate, formatDateDistance, getLastUpdatedPackageTime, getRecentReleases } from './package';
import { packageMeta } from '../../jest/unit/components/store/packageMeta';
describe('formatLicense', (): void => {
test('should check license field different values', (): void => {
expect(formatLicense('MIT')).toEqual('MIT');
});
test('should check license field for object value', (): void => {
const license = { type: 'ISC', url: 'https://opensource.org/licenses/ISC' };
expect(formatLicense(license)).toEqual('ISC');
});
test('should check license field for other value', (): void => {
expect(formatLicense(null)).toBeNull();
expect(formatLicense({})).toBeNull();
expect(formatLicense([])).toBeNull();
});
});
describe('formatRepository', (): void => {
test('should check repository field different values', (): void => {
const repository = 'https://github.com/verdaccio/verdaccio';
expect(formatRepository(repository)).toEqual(repository);
});
test('should check repository field for object value', (): void => {
const license = {
type: 'git',
url: 'https://github.com/verdaccio/verdaccio',
};
expect(formatRepository(license)).toEqual(license.url);
});
test('should check repository field for other value', (): void => {
expect(formatRepository(null)).toBeNull();
expect(formatRepository({})).toBeNull();
expect(formatRepository([])).toBeNull();
});
});
describe('formatDate', (): void => {
test('should format the date', (): void => {
const date = 1532211072138;
expect(formatDate(date)).toEqual('21.07.2018, 22:11:12');
});
});
describe('formatDateDistance', (): void => {
test('should calculate the distance', (): void => {
// const dateAboutTwoMonthsAgo = () => {
// const date = new Date();
// date.setMonth(date.getMonth() - 1);
// date.setDate(date.getDay() - 20);
// return date;
// };
const dateTwoMonthsAgo = (): Date => {
const date = new Date();
date.setMonth(date.getMonth() - 2);
return date;
};
// const date1 = dateAboutTwoMonthsAgo();
const date2 = dateTwoMonthsAgo();
// FIXME: we need to review this expect, fails every x time.
// expect(formatDateDistance(date1)).toEqual('about 2 months');
expect(formatDateDistance(date2)).toEqual('2 months');
});
});
describe('getLastUpdatedPackageTime', (): void => {
test('should get the last update time', (): void => {
const lastUpdated = packageMeta._uplinks;
expect(getLastUpdatedPackageTime(lastUpdated)).toEqual('22.07.2018, 22:11:12');
});
test('should get the last update time for blank uplink', (): void => {
const lastUpdated = {};
expect(getLastUpdatedPackageTime(lastUpdated)).toEqual('');
});
});
describe('getRecentReleases', (): void => {
test('should get the recent releases', (): void => {
const { time } = packageMeta;
const result = [
{ time: '14.12.2017, 15:43:27', version: '2.7.1' },
{ time: '05.12.2017, 23:25:06', version: '2.7.0' },
{ time: '08.11.2017, 22:47:16', version: '2.6.6' },
];
expect(getRecentReleases(time)).toEqual(result);
expect(getRecentReleases()).toEqual([]);
});
});

102
src/utils/package.ts Normal file
View File

@@ -0,0 +1,102 @@
import { UpLinks } from '@verdaccio/types';
import isString from 'lodash/isString';
import format from 'date-fns/format';
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
import { isObject } from 'util';
export const TIMEFORMAT = 'DD.MM.YYYY, HH:mm:ss';
export interface License {
type: string;
url: string;
}
/**
* Formats license field for webui.
* @see https://docs.npmjs.com/files/package.json#license
*/
// License should use type License defined above, but conflicts with the unit test that provide array or empty object
/* eslint-disable @typescript-eslint/no-explicit-any */
export function formatLicense(license: any): string | null {
if (isString(license)) {
return license;
}
if (license && isObject(license) && license.type) {
return license.type;
}
return null;
}
export interface Repository {
type: string;
url: string;
}
/**
* Formats repository field for webui.
* @see https://docs.npmjs.com/files/package.json#repository
*/
// Repository should use type Repository defined above, but conflicts with the unit test that provide array or empty object
/* eslint-disable @typescript-eslint/no-explicit-any */
export function formatRepository(repository: any): string | null {
if (isString(repository)) {
return repository;
}
if (repository && isObject(repository) && repository.url) {
return repository.url;
}
return null;
}
export function formatDate(lastUpdate): string {
return format(new Date(lastUpdate), TIMEFORMAT);
}
export function formatDateDistance(lastUpdate): string {
return distanceInWordsToNow(new Date(lastUpdate));
}
export function getRouterPackageName(match): string {
const packageName = match.params.package;
const scope = match.params.scope;
if (scope) {
return `@${scope}/${packageName}`;
}
return packageName;
}
/**
* For <LastSync /> component
* @param {array} uplinks
*/
export function getLastUpdatedPackageTime(uplinks: UpLinks = {}): string {
let lastUpdate = 0;
Object.keys(uplinks).forEach(function computeUplink(upLinkName): void {
const status = uplinks[upLinkName];
if (status.fetched > lastUpdate) {
lastUpdate = status.fetched;
}
});
return lastUpdate ? formatDate(lastUpdate) : '';
}
/**
* For <LastSync /> component
* @param {Object} time
* @returns {Array} last 3 releases
*/
export function getRecentReleases(time = {}): unknown {
const recent = Object.keys(time).map((version): unknown => ({
version,
time: formatDate(time[version]),
}));
return recent.slice(recent.length - 3, recent.length).reverse();
}

7
src/utils/sec-utils.ts Normal file
View File

@@ -0,0 +1,7 @@
import parseXSS from 'xss';
export function preventXSS(text: string) {
const encodedText = parseXSS.filterXSS(text);
return encodedText;
}

12
src/utils/storage.ts Normal file
View File

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

View File

@@ -0,0 +1,38 @@
// Verdaccio
// -------------------------
// Main colors
// -------------------------
const colors = {
black: '#000',
white: '#fff',
red: '#d32f2f',
grey: '#808080',
greySuperLight: '#f5f5f5',
greyLight: '#d3d3d3',
greyLight2: '#908ba1',
greyLight3: '#f3f4f240',
greyDark: '#a9a9a9',
greyDark2: '#586069',
greyChateau: '#95989a',
greyGainsboro: '#e3e3e3',
greyAthens: '#d3dddd',
eclipse: '#3c3c3c',
paleNavy: '#e4e8f1',
saltpan: '#f7f8f6',
snow: '#f9f9f9',
love: '#e25555',
nobel01: '#999999',
nobel02: '#9f9f9f',
// Main colors
// -------------------------
// @ts-ignore
primary: window.VERDACCIO_PRIMARY_COLOR || '#4b5e40',
secondary: '#20232a',
};
export default colors;

View File

@@ -0,0 +1,23 @@
import { injectGlobal } from 'emotion';
import { fontSize, fontWeight } from './sizes';
export default injectGlobal`
html,
body {
height: 100%;
}
body {
font-size: ${fontSize.base};
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
strong {
font-weight: ${fontWeight.semiBold};
}
`;

22
src/utils/styles/media.ts Normal file
View File

@@ -0,0 +1,22 @@
import { css } from 'emotion';
export const breakpoints = {
small: 576,
medium: 768,
large: 1024,
xlarge: 1275,
};
const mq = Object.keys(breakpoints).reduce((accumulator, label) => {
const prefix = typeof breakpoints[label] === 'string' ? '' : 'min-width:';
const suffix = typeof breakpoints[label] === 'string' ? '' : 'px';
accumulator[label] = cls =>
css`
@media (${prefix + breakpoints[label] + suffix}) {
${cls};
}
`;
return accumulator;
}, {});
export default mq;

View File

@@ -0,0 +1,40 @@
/**
* CSS to represent truncated text with an ellipsis.
*/
export function ellipsis(width: string | number) {
return {
display: 'inline-block',
maxWidth: width,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
wordWrap: 'normal',
};
}
/**
* Shorthand that accepts up to four values, including null to skip a value, and maps them to their respective directions.
*/
interface SpacingShortHand<type> {
top?: type;
right?: type;
bottom?: type;
left?: type;
}
const positionMap = ['Top', 'Right', 'Bottom', 'Left'];
export function spacing(property: 'padding' | 'margin', ...values: SpacingShortHand<number | string>[]) {
const [firstValue = 0, secondValue = 0, thirdValue = 0, fourthValue = 0] = values;
const valuesWithDefaults = [firstValue, secondValue, thirdValue, fourthValue];
let styles = {};
for (let i = 0; i < valuesWithDefaults.length; i += 1) {
if (valuesWithDefaults[i] || valuesWithDefaults[i] === 0) {
styles = {
...styles,
[`${property}${positionMap[i]}`]: valuesWithDefaults[i],
};
}
}
return styles;
}

22
src/utils/styles/sizes.ts Normal file
View File

@@ -0,0 +1,22 @@
export const fontSize = {
xxl: '26px',
xl: '24px',
lg: '21px',
md: '18px',
base: '16px',
sm: '14px',
};
export const lineHeight = {
xl: '30px',
sm: '18px',
xs: '2',
xxs: '1.5',
};
export const fontWeight = {
light: 300,
regular: 400,
semiBold: 500,
bold: 700,
};

View File

@@ -0,0 +1,6 @@
// Spacings
// -------------------------
export const spacings = {
lg: '30px',
};

27
src/utils/url.ts Normal file
View File

@@ -0,0 +1,27 @@
import isURLValidator from 'validator/lib/isURL';
import isEmailValidator from 'validator/lib/isEmail';
import '../../types';
export function isURL(url): boolean {
return isURLValidator(url || '', {
protocols: ['http', 'https', 'git+https'],
require_protocol: true,
});
}
export function isEmail(email): boolean {
return isEmailValidator(email || '');
}
export function getRegistryURL(): string {
// Don't add slash if it's not a sub directory
return `${location.origin}${location.pathname === '/' ? '' : location.pathname}`;
}
export function getBaseNamePath(): string {
return window.__VERDACCIO_BASENAME_UI_OPTIONS && window.__VERDACCIO_BASENAME_UI_OPTIONS.url_prefix;
}
export function getRootPath(): string {
return window.__VERDACCIO_BASENAME_UI_OPTIONS && window.__VERDACCIO_BASENAME_UI_OPTIONS.base;
}

3
src/utils/windows.ts Normal file
View File

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