[Proposal] Make able to write React in Typescript (#16210)

Co-authored-by: berlysia <berlysia@gmail.com>
Co-authored-by: fusagiko / takayamaki <takayamaki@users.noreply.github.com>
shrike
fusagiko / takayamaki 2023-04-03 10:31:39 +09:00 committed by GitHub
parent 2f7c3cb628
commit 4520e6473a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1099 additions and 211 deletions

View File

@ -20,13 +20,14 @@ module.exports = {
ATTACHMENT_HOST: false, ATTACHMENT_HOST: false,
}, },
parser: '@babel/eslint-parser', parser: '@typescript-eslint/parser',
plugins: [ plugins: [
'react', 'react',
'jsx-a11y', 'jsx-a11y',
'import', 'import',
'promise', 'promise',
'@typescript-eslint',
], ],
parserOptions: { parserOptions: {
@ -41,14 +42,13 @@ module.exports = {
presets: ['@babel/react', '@babel/env'], presets: ['@babel/react', '@babel/env'],
}, },
}, },
extends: [
'plugin:import/typescript',
],
settings: { settings: {
react: { react: {
version: 'detect', version: 'detect',
}, },
'import/extensions': [
'.js', '.jsx',
],
'import/ignore': [ 'import/ignore': [
'node_modules', 'node_modules',
'\\.(css|scss|json)$', '\\.(css|scss|json)$',
@ -56,7 +56,7 @@ module.exports = {
'import/resolver': { 'import/resolver': {
node: { node: {
paths: ['app/javascript'], paths: ['app/javascript'],
extensions: ['.js', '.jsx'], extensions: ['.js', '.jsx', '.ts', '.tsx'],
}, },
}, },
}, },
@ -97,7 +97,8 @@ module.exports = {
'no-self-assign': 'off', 'no-self-assign': 'off',
'no-trailing-spaces': 'warn', 'no-trailing-spaces': 'warn',
'no-unused-expressions': 'error', 'no-unused-expressions': 'error',
'no-unused-vars': [ 'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error', 'error',
{ {
vars: 'all', vars: 'all',
@ -116,7 +117,7 @@ module.exports = {
semi: 'error', semi: 'error',
'valid-typeof': 'error', 'valid-typeof': 'error',
'react/jsx-filename-extension': ['error', { 'allow': 'as-needed' }], 'react/jsx-filename-extension': ['error', { extensions: ['.jsx', 'tsx'] }],
'react/jsx-boolean-value': 'error', 'react/jsx-boolean-value': 'error',
'react/jsx-closing-bracket-location': ['error', 'line-aligned'], 'react/jsx-closing-bracket-location': ['error', 'line-aligned'],
'react/jsx-curly-spacing': 'error', 'react/jsx-curly-spacing': 'error',
@ -192,6 +193,8 @@ module.exports = {
{ {
js: 'never', js: 'never',
jsx: 'never', jsx: 'never',
ts: 'never',
tsx: 'never',
}, },
], ],
'import/newline-after-import': 'error', 'import/newline-after-import': 'error',

View File

@ -0,0 +1,17 @@
import { useCallback, useState } from 'react';
export const useHovering = (animate?: boolean) => {
const [hovering, setHovering] = useState<boolean>(animate ?? false);
const handleMouseEnter = useCallback(() => {
if (animate) return;
setHovering(true);
}, [animate]);
const handleMouseLeave = useCallback(() => {
if (animate) return;
setHovering(false);
}, [animate]);
return { hovering, handleMouseEnter, handleMouseLeave };
};

View File

@ -23,6 +23,7 @@ export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';
* @return {object} * @return {object}
*/ */
export const deployPictureInPicture = (statusId, accountId, playerType, props) => { export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
// @ts-expect-error
return (dispatch, getState) => { return (dispatch, getState) => {
// Do not open a player for a toot that does not exist // Do not open a player for a toot that does not exist
if (getState().hasIn(['statuses', statusId])) { if (getState().hasIn(['statuses', statusId])) {

View File

@ -46,6 +46,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
connectStream(channelName, params, (dispatch, getState) => { connectStream(channelName, params, (dispatch, getState) => {
const locale = getState().getIn(['meta', 'locale']); const locale = getState().getIn(['meta', 'locale']);
// @ts-expect-error
let pollingId; let pollingId;
/** /**
@ -61,9 +62,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
onConnect() { onConnect() {
dispatch(connectTimeline(timelineId)); dispatch(connectTimeline(timelineId));
// @ts-expect-error
if (pollingId) { if (pollingId) {
clearTimeout(pollingId); // @ts-ignore
pollingId = null; clearTimeout(pollingId); pollingId = null;
} }
if (options.fillGaps) { if (options.fillGaps) {
@ -75,31 +77,38 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
dispatch(disconnectTimeline(timelineId)); dispatch(disconnectTimeline(timelineId));
if (options.fallback) { if (options.fallback) {
// @ts-expect-error
pollingId = setTimeout(() => useFallback(options.fallback), randomUpTo(40000)); pollingId = setTimeout(() => useFallback(options.fallback), randomUpTo(40000));
} }
}, },
onReceive (data) { onReceive(data) {
switch(data.event) { switch (data.event) {
case 'update': case 'update':
// @ts-expect-error
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept)); dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
break; break;
case 'status.update': case 'status.update':
// @ts-expect-error
dispatch(updateStatus(JSON.parse(data.payload))); dispatch(updateStatus(JSON.parse(data.payload)));
break; break;
case 'delete': case 'delete':
dispatch(deleteFromTimelines(data.payload)); dispatch(deleteFromTimelines(data.payload));
break; break;
case 'notification': case 'notification':
// @ts-expect-error
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale)); dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
break; break;
case 'conversation': case 'conversation':
// @ts-expect-error
dispatch(updateConversations(JSON.parse(data.payload))); dispatch(updateConversations(JSON.parse(data.payload)));
break; break;
case 'announcement': case 'announcement':
// @ts-expect-error
dispatch(updateAnnouncements(JSON.parse(data.payload))); dispatch(updateAnnouncements(JSON.parse(data.payload)));
break; break;
case 'announcement.reaction': case 'announcement.reaction':
// @ts-expect-error
dispatch(updateAnnouncementsReaction(JSON.parse(data.payload))); dispatch(updateAnnouncementsReaction(JSON.parse(data.payload)));
break; break;
case 'announcement.delete': case 'announcement.delete':
@ -115,7 +124,9 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
* @param {function(): void} done * @param {function(): void} done
*/ */
const refreshHomeTimelineAndNotification = (dispatch, done) => { const refreshHomeTimelineAndNotification = (dispatch, done) => {
// @ts-expect-error
dispatch(expandHomeTimeline({}, () => dispatch(expandHomeTimeline({}, () =>
// @ts-expect-error
dispatch(expandNotifications({}, () => dispatch(expandNotifications({}, () =>
dispatch(fetchAnnouncements(done)))))); dispatch(fetchAnnouncements(done))))));
}; };
@ -124,6 +135,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
* @return {function(): void} * @return {function(): void}
*/ */
export const connectUserStream = () => export const connectUserStream = () =>
// @ts-expect-error
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps }); connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
/** /**

View File

@ -36,7 +36,7 @@ const setCSRFHeader = () => {
ready(setCSRFHeader); ready(setCSRFHeader);
/** /**
* @param {() => import('immutable').Map} getState * @param {() => import('immutable').Map<string,any>} getState
* @returns {import('axios').RawAxiosRequestHeaders} * @returns {import('axios').RawAxiosRequestHeaders}
*/ */
const authorizationHeaderFromState = getState => { const authorizationHeaderFromState = getState => {
@ -52,7 +52,7 @@ const authorizationHeaderFromState = getState => {
}; };
/** /**
* @param {() => import('immutable').Map} getState * @param {() => import('immutable').Map<string,any>} getState
* @returns {import('axios').AxiosInstance} * @returns {import('axios').AxiosInstance}
*/ */
export default function api(getState) { export default function api(getState) {

View File

@ -1,62 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from '../initial_state';
import classNames from 'classnames';
export default class Avatar extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
size: PropTypes.number.isRequired,
style: PropTypes.object,
inline: PropTypes.bool,
animate: PropTypes.bool,
};
static defaultProps = {
animate: autoPlayGif,
size: 20,
inline: false,
};
state = {
hovering: false,
};
handleMouseEnter = () => {
if (this.props.animate) return;
this.setState({ hovering: true });
};
handleMouseLeave = () => {
if (this.props.animate) return;
this.setState({ hovering: false });
};
render () {
const { account, size, animate, inline } = this.props;
const { hovering } = this.state;
const style = {
...this.props.style,
width: `${size}px`,
height: `${size}px`,
};
let src;
if (hovering || animate) {
src = account?.get('avatar');
} else {
src = account?.get('avatar_static');
}
return (
<div className={classNames('account__avatar', { 'account__avatar-inline': inline })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={style}>
{src && <img src={src} alt={account?.get('acct')} />}
</div>
);
}
}

View File

@ -0,0 +1,40 @@
import * as React from 'react';
import classNames from 'classnames';
import { autoPlayGif } from '../initial_state';
import { useHovering } from '../../hooks/useHovering';
import type { Account } from '../../types/resources';
type Props = {
account: Account;
size: number;
style?: React.CSSProperties;
inline?: boolean;
animate?: boolean;
}
export const Avatar: React.FC<Props> = ({
account,
animate = autoPlayGif,
size = 20,
inline = false,
style: styleFromParent,
}) => {
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
const style = {
...styleFromParent,
width: `${size}px`,
height: `${size}px`,
};
const src = (hovering || animate) ? account?.get('avatar') : account?.get('avatar_static');
return (
<div className={classNames('account__avatar', { 'account__avatar-inline': inline })} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} style={style}>
{src && <img src={src} alt={account?.get('acct')} />}
</div>
);
};
export default Avatar;

View File

@ -44,6 +44,7 @@ function Blurhash({
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
const imageData = new ImageData(pixels, width, height); const imageData = new ImageData(pixels, width, height);
// @ts-expect-error
ctx.putImageData(imageData, 0, 0); ctx.putImageData(imageData, 0, 0);
} catch (err) { } catch (err) {
console.error('Blurhash decoding failure', { err, hash }); console.error('Blurhash decoding failure', { err, hash });

View File

@ -1,5 +1,6 @@
// @ts-check // @ts-check
import React from 'react'; import React from 'react';
// @ts-expect-error
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
/** /**

View File

@ -1,11 +1,14 @@
// @ts-check // @ts-check
import React from 'react'; import React from 'react';
import { Sparklines, SparklinesCurve } from 'react-sparklines'; import { Sparklines, SparklinesCurve } from 'react-sparklines';
// @ts-expect-error
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
// @ts-expect-error
import ShortNumber from 'mastodon/components/short_number'; import ShortNumber from 'mastodon/components/short_number';
// @ts-expect-error
import Skeleton from 'mastodon/components/skeleton'; import Skeleton from 'mastodon/components/skeleton';
import classNames from 'classnames'; import classNames from 'classnames';
@ -19,11 +22,11 @@ class SilentErrorBoundary extends React.Component {
error: false, error: false,
}; };
componentDidCatch () { componentDidCatch() {
this.setState({ error: true }); this.setState({ error: true });
} }
render () { render() {
if (this.state.error) { if (this.state.error) {
return null; return null;
} }
@ -50,11 +53,13 @@ export const accountsCountRenderer = (displayNumber, pluralReady) => (
/> />
); );
// @ts-expect-error
export const ImmutableHashtag = ({ hashtag }) => ( export const ImmutableHashtag = ({ hashtag }) => (
<Hashtag <Hashtag
name={hashtag.get('name')} name={hashtag.get('name')}
to={`/tags/${hashtag.get('name')}`} to={`/tags/${hashtag.get('name')}`}
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1} people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
// @ts-expect-error
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()} history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
/> />
); );
@ -63,6 +68,7 @@ ImmutableHashtag.propTypes = {
hashtag: ImmutablePropTypes.map.isRequired, hashtag: ImmutablePropTypes.map.isRequired,
}; };
// @ts-expect-error
const Hashtag = ({ name, to, people, uses, history, className, description, withGraph }) => ( const Hashtag = ({ name, to, people, uses, history, className, description, withGraph }) => (
<div className={classNames('trends__item', className)}> <div className={classNames('trends__item', className)}>
<div className='trends__item__name'> <div className='trends__item__name'>
@ -86,7 +92,9 @@ const Hashtag = ({ name, to, people, uses, history, className, description, with
{withGraph && ( {withGraph && (
<div className='trends__item__sparkline'> <div className='trends__item__sparkline'>
<SilentErrorBoundary> <SilentErrorBoundary>
{/* @ts-expect-error */}
<Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}> <Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
{/* @ts-expect-error */}
<SparklinesCurve style={{ fill: 'none' }} /> <SparklinesCurve style={{ fill: 'none' }} />
</Sparklines> </Sparklines>
</SilentErrorBoundary> </SilentErrorBoundary>

View File

@ -9,7 +9,7 @@ const emojis = {};
// decompress // decompress
Object.keys(shortCodesToEmojiData).forEach((shortCode) => { Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
let [ let [
filenameData, // eslint-disable-line no-unused-vars filenameData, // eslint-disable-line @typescript-eslint/no-unused-vars
searchData, searchData,
] = shortCodesToEmojiData[shortCode]; ] = shortCodesToEmojiData[shortCode];
let [ let [

View File

@ -4,9 +4,9 @@
const [ const [
shortCodesToEmojiData, shortCodesToEmojiData,
skins, // eslint-disable-line no-unused-vars skins, // eslint-disable-line @typescript-eslint/no-unused-vars
categories, // eslint-disable-line no-unused-vars categories, // eslint-disable-line @typescript-eslint/no-unused-vars
short_names, // eslint-disable-line no-unused-vars short_names, // eslint-disable-line @typescript-eslint/no-unused-vars
emojisWithoutShortCodes, emojisWithoutShortCodes,
] = require('./emoji_compressed'); ] = require('./emoji_compressed');
const { unicodeToFilename } = require('./unicode_to_filename'); const { unicodeToFilename } = require('./unicode_to_filename');

View File

@ -132,6 +132,7 @@ export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items'); export const usePendingItems = getMeta('use_pending_items');
export const version = getMeta('version'); export const version = getMeta('version');
export const languages = initialState?.languages; export const languages = initialState?.languages;
// @ts-expect-error
export const statusPageUrl = getMeta('status_page_url'); export const statusPageUrl = getMeta('status_page_url');
export default initialState; export default initialState;

View File

@ -1,6 +1,7 @@
// @ts-check // @ts-check
import { supportsPassiveEvents } from 'detect-passive-events'; import { supportsPassiveEvents } from 'detect-passive-events';
// @ts-expect-error
import { forceSingleColumn } from 'mastodon/initial_state'; import { forceSingleColumn } from 'mastodon/initial_state';
const LAYOUT_BREAKPOINT = 630; const LAYOUT_BREAKPOINT = 630;
@ -24,6 +25,7 @@ export const layoutFromWindow = () => {
} }
}; };
// @ts-expect-error
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
const listenerOptions = supportsPassiveEvents ? { passive: true } : false; const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
@ -33,7 +35,7 @@ let userTouching = false;
const touchListener = () => { const touchListener = () => {
userTouching = true; userTouching = true;
window.removeEventListener('touchstart', touchListener, listenerOptions); window.removeEventListener('touchstart', touchListener);
}; };
window.addEventListener('touchstart', touchListener, listenerOptions); window.addEventListener('touchstart', touchListener, listenerOptions);

View File

@ -59,6 +59,7 @@ const subscribe = ({ channelName, params, onConnect }) => {
subscriptionCounters[key] = subscriptionCounters[key] || 0; subscriptionCounters[key] = subscriptionCounters[key] || 0;
if (subscriptionCounters[key] === 0) { if (subscriptionCounters[key] === 0) {
// @ts-expect-error
sharedConnection.send(JSON.stringify({ type: 'subscribe', stream: channelName, ...params })); sharedConnection.send(JSON.stringify({ type: 'subscribe', stream: channelName, ...params }));
} }
@ -74,7 +75,9 @@ const unsubscribe = ({ channelName, params, onDisconnect }) => {
subscriptionCounters[key] = subscriptionCounters[key] || 1; subscriptionCounters[key] = subscriptionCounters[key] || 1;
// @ts-expect-error
if (subscriptionCounters[key] === 1 && sharedConnection.readyState === WebSocketClient.OPEN) { if (subscriptionCounters[key] === 1 && sharedConnection.readyState === WebSocketClient.OPEN) {
// @ts-expect-error
sharedConnection.send(JSON.stringify({ type: 'unsubscribe', stream: channelName, ...params })); sharedConnection.send(JSON.stringify({ type: 'unsubscribe', stream: channelName, ...params }));
} }
@ -83,11 +86,12 @@ const unsubscribe = ({ channelName, params, onDisconnect }) => {
}; };
const sharedCallbacks = { const sharedCallbacks = {
connected () { connected() {
subscriptions.forEach(subscription => subscribe(subscription)); subscriptions.forEach(subscription => subscribe(subscription));
}, },
received (data) { // @ts-expect-error
received(data) {
const { stream } = data; const { stream } = data;
subscriptions.filter(({ channelName, params }) => { subscriptions.filter(({ channelName, params }) => {
@ -111,11 +115,11 @@ const sharedCallbacks = {
}); });
}, },
disconnected () { disconnected() {
subscriptions.forEach(subscription => unsubscribe(subscription)); subscriptions.forEach(subscription => unsubscribe(subscription));
}, },
reconnected () { reconnected() {
}, },
}; };
@ -138,6 +142,7 @@ const channelNameWithInlineParams = (channelName, params) => {
* @param {function(Function, Function): { onConnect: (function(): void), onReceive: (function(StreamEvent): void), onDisconnect: (function(): void) }} callbacks * @param {function(Function, Function): { onConnect: (function(): void), onReceive: (function(StreamEvent): void), onDisconnect: (function(): void) }} callbacks
* @return {function(): void} * @return {function(): void}
*/ */
// @ts-expect-error
export const connectStream = (channelName, params, callbacks) => (dispatch, getState) => { export const connectStream = (channelName, params, callbacks) => (dispatch, getState) => {
const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']); const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
const accessToken = getState().getIn(['meta', 'access_token']); const accessToken = getState().getIn(['meta', 'access_token']);
@ -147,19 +152,19 @@ export const connectStream = (channelName, params, callbacks) => (dispatch, getS
// to using individual connections for each channel // to using individual connections for each channel
if (!streamingAPIBaseURL.startsWith('ws')) { if (!streamingAPIBaseURL.startsWith('ws')) {
const connection = createConnection(streamingAPIBaseURL, accessToken, channelNameWithInlineParams(channelName, params), { const connection = createConnection(streamingAPIBaseURL, accessToken, channelNameWithInlineParams(channelName, params), {
connected () { connected() {
onConnect(); onConnect();
}, },
received (data) { received(data) {
onReceive(data); onReceive(data);
}, },
disconnected () { disconnected() {
onDisconnect(); onDisconnect();
}, },
reconnected () { reconnected() {
onConnect(); onConnect();
}, },
}); });
@ -227,14 +232,19 @@ const handleEventSourceMessage = (e, received) => {
const createConnection = (streamingAPIBaseURL, accessToken, channelName, { connected, received, disconnected, reconnected }) => { const createConnection = (streamingAPIBaseURL, accessToken, channelName, { connected, received, disconnected, reconnected }) => {
const params = channelName.split('&'); const params = channelName.split('&');
// @ts-expect-error
channelName = params.shift(); channelName = params.shift();
if (streamingAPIBaseURL.startsWith('ws')) { if (streamingAPIBaseURL.startsWith('ws')) {
// @ts-expect-error
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken); const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken);
ws.onopen = connected; // @ts-expect-error
ws.onmessage = e => received(JSON.parse(e.data)); ws.onopen = connected;
ws.onclose = disconnected; ws.onmessage = e => received(JSON.parse(e.data));
// @ts-expect-error
ws.onclose = disconnected;
// @ts-expect-error
ws.onreconnect = reconnected; ws.onreconnect = reconnected;
return ws; return ws;
@ -256,7 +266,7 @@ const createConnection = (streamingAPIBaseURL, accessToken, channelName, { conne
}; };
KNOWN_EVENT_TYPES.forEach(type => { KNOWN_EVENT_TYPES.forEach(type => {
es.addEventListener(type, e => handleEventSourceMessage(/** @type {MessageEvent} */ (e), received)); es.addEventListener(type, e => handleEventSourceMessage(/** @type {MessageEvent} */(e), received));
}); });
es.onerror = /** @type {function(): void} */ (disconnected); es.onerror = /** @type {function(): void} */ (disconnected);

View File

@ -3,7 +3,7 @@
const checkNotificationPromise = () => { const checkNotificationPromise = () => {
try { try {
// eslint-disable-next-line promise/catch-or-return, promise/valid-params // eslint-disable-next-line promise/catch-or-return
Notification.requestPermission().then(); Notification.requestPermission().then();
} catch(e) { } catch(e) {
return false; return false;

View File

@ -1,3 +0,0 @@
export default function uuid(a) {
return a ? (a^Math.random() * 16 >> a / 4).toString(16) : ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, uuid);
}

View File

@ -0,0 +1,3 @@
export default function uuid(a?: string): string {
return a ? ((a as any as number) ^ Math.random() * 16 >> (a as any as number) / 4).toString(16) : ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid);
}

View File

@ -17,5 +17,4 @@ function formatPublicPath(host = '', path = '') {
const cdnHost = document.querySelector('meta[name=cdn-host]'); const cdnHost = document.querySelector('meta[name=cdn-host]');
// eslint-disable-next-line no-undef
__webpack_public_path__ = formatPublicPath(cdnHost ? cdnHost.content : '', process.env.PUBLIC_OUTPUT_PATH); __webpack_public_path__ = formatPublicPath(cdnHost ? cdnHost.content : '', process.env.PUBLIC_OUTPUT_PATH);

View File

@ -0,0 +1,13 @@
interface MastodonMap<T> {
get<K extends keyof T>(key: K): T[K];
has<K extends keyof T>(key: K): boolean;
set<K extends keyof T>(key: K, value: T[K]): this;
}
type AccountValues = {
id: number;
avatar: string;
avatar_static: string;
[key: string]: any;
}
export type Account = MastodonMap<AccountValues>

View File

@ -13,6 +13,7 @@ module.exports = (api) => {
const config = { const config = {
presets: [ presets: [
'@babel/preset-typescript',
['@babel/react', reactOptions], ['@babel/react', reactOptions],
['@babel/env', envOptions], ['@babel/env', envOptions],
], ],

View File

@ -2,7 +2,7 @@ const { join, resolve } = require('path');
const { env, settings } = require('../configuration'); const { env, settings } = require('../configuration');
module.exports = { module.exports = {
test: /\.(js|jsx|mjs)$/, test: /\.(js|jsx|mjs|ts|tsx)$/,
include: [ include: [
settings.source_path, settings.source_path,
...settings.resolved_paths, ...settings.resolved_paths,

View File

@ -36,6 +36,8 @@ default: &default
- .mjs - .mjs
- .js - .js
- .jsx - .jsx
- .ts
- .tsx
- .sass - .sass
- .scss - .scss
- .css - .css

View File

@ -10,10 +10,11 @@
"build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/webpack", "build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/webpack",
"manage:translations": "node ./config/webpack/translationRunner.js", "manage:translations": "node ./config/webpack/translationRunner.js",
"start": "node ./streaming/index.js", "start": "node ./streaming/index.js",
"test": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:jest", "test": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:typecheck && ${npm_execpath} run test:jest",
"test:lint": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:lint:sass", "test:lint": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:lint:sass",
"test:lint:js": "eslint --ext=.js,.jsx . --cache --report-unused-disable-directives", "test:lint:js": "eslint --ext=.js,.jsx,.ts,.tsx . --cache --report-unused-disable-directives",
"test:lint:sass": "stylelint \"**/*.{css,scss}\" && prettier --check \"**/*.{css,scss}\"", "test:lint:sass": "stylelint \"**/*.{css,scss}\" && prettier --check \"**/*.{css,scss}\"",
"test:typecheck": "tsc --noEmit",
"test:jest": "cross-env NODE_ENV=test jest", "test:jest": "cross-env NODE_ENV=test jest",
"format": "prettier --write .", "format": "prettier --write .",
"format-check": "prettier --check .", "format-check": "prettier --check .",
@ -139,9 +140,45 @@
"ws": "^8.12.1" "ws": "^8.12.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.21.3", "@babel/preset-typescript": "^7.21.0",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5", "@testing-library/react": "^12.1.5",
"@types/babel__core": "^7.20.0",
"@types/emoji-mart": "^3.0.9",
"@types/escape-html": "^1.0.2",
"@types/eslint": "^8.21.2",
"@types/express": "^4.17.17",
"@types/glob": "^8.1.0",
"@types/http-link-header": "^1.0.3",
"@types/intl": "^1.2.0",
"@types/jest": "^29.4.2",
"@types/js-yaml": "^4.0.5",
"@types/lodash": "^4.14.191",
"@types/npmlog": "^4.1.4",
"@types/object-assign": "^4.0.30",
"@types/pg": "^8.6.6",
"@types/prop-types": "^15.7.5",
"@types/punycode": "^2.1.0",
"@types/raf": "^3.4.0",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/react-intl": "2.3.18",
"@types/react-motion": "^0.0.33",
"@types/react-redux": "^7.1.25",
"@types/react-router-dom": "^5.3.3",
"@types/react-sparklines": "^1.7.2",
"@types/react-swipeable-views": "^0.13.1",
"@types/react-test-renderer": "^18.0.0",
"@types/react-toggle": "^4.0.3",
"@types/redux-immutable": "^4.0.3",
"@types/requestidlecallback": "^0.3.5",
"@types/throng": "^5.0.4",
"@types/uuid": "^9.0.1",
"@types/webpack": "^5.28.0",
"@types/webpack-bundle-analyzer": "^4.6.0",
"@types/yargs": "^17.0.22",
"@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0",
"babel-jest": "^29.5.0", "babel-jest": "^29.5.0",
"eslint": "^8.36.0", "eslint": "^8.36.0",
"eslint-plugin-import": "~2.27.5", "eslint-plugin-import": "~2.27.5",
@ -160,6 +197,7 @@
"react-test-renderer": "^16.14.0", "react-test-renderer": "^16.14.0",
"stylelint": "^15.3.0", "stylelint": "^15.3.0",
"stylelint-config-standard-scss": "^7.0.1", "stylelint-config-standard-scss": "^7.0.1",
"typescript": "^4.9.5",
"webpack-dev-server": "^3.11.3", "webpack-dev-server": "^3.11.3",
"yargs": "^17.7.1" "yargs": "^17.7.1"
}, },

13
tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"jsx": "react",
"target": "esnext",
"moduleResolution": "node",
"allowJs": true,
"noEmit": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["app/javascript/mastodon", "app/javascript/packs"]
}

1002
yarn.lock

File diff suppressed because it is too large Load Diff