tgui: Typescript and Jest update (#57081)

Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com>
This commit is contained in:
Aleksej Komarov
2021-02-22 11:18:35 +02:00
committed by GitHub
parent 3b0ec441a8
commit 1fb5d68b53
34 changed files with 4155 additions and 1587 deletions

View File

@@ -31,6 +31,7 @@ jobs:
find . -name "*.php" -print0 | xargs -0 -n1 php -l
find . -name "*.json" -not -path "*/node_modules/*" -print0 | xargs -0 python3 ./tools/json_verifier.py
tgui/bin/tgui --lint
tgui/bin/tgui --test
bash tools/ci/check_grep.sh
tools/bootstrap/python -m dmi.test
tools/bootstrap/python -m mapmerge2.dmm_test

View File

@@ -1,8 +1,10 @@
{
"eslint.nodePath": "tgui/.yarn/sdks",
"eslint.nodePath": "./tgui/.yarn/sdks",
"eslint.workingDirectories": [
"./tgui"
],
"typescript.tsdk": "./tgui/.yarn/sdks/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"search.exclude": {
"tgui/.yarn": true,
"tgui/.pnp.*": true

View File

@@ -1,6 +1,7 @@
parser: '@babel/eslint-parser'
root: true
parser: '@typescript-eslint/parser'
parserOptions:
ecmaVersion: 2019
ecmaVersion: 2020
sourceType: module
ecmaFeatures:
jsx: true
@@ -279,7 +280,8 @@ rules:
no-shadow-restricted-names: error
## Disallow the use of undeclared variables unless mentioned
## in /*global*/ comments
no-undef: error
## NOTE: Pointless when TypeScript can check for this
# no-undef: error
## Disallow initializing variables to undefined
no-undef-init: error
## Disallow the use of undefined as an identifier

1
tgui/.gitignore vendored
View File

@@ -10,6 +10,7 @@ package-lock.json
!/.yarn/plugins
!/.yarn/sdks
!/.yarn/versions
!/.yarn/lock.yml
## Build artifacts
/public/.tmp/**/*

View File

@@ -2,7 +2,7 @@
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve, dirname} = require(`path`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";

View File

@@ -1,6 +1,6 @@
{
"name": "eslint",
"version": "7.4.0-pnpify",
"version": "7.19.0-pnpify",
"main": "./lib/api.js",
"type": "commonjs"
}

20
tgui/.yarn/sdks/typescript/bin/tsc vendored Normal file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/bin/tsc
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/bin/tsc your application uses
module.exports = absRequire(`typescript/bin/tsc`);

20
tgui/.yarn/sdks/typescript/bin/tsserver vendored Normal file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/bin/tsserver
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/bin/tsserver your application uses
module.exports = absRequire(`typescript/bin/tsserver`);

View File

@@ -0,0 +1,6 @@
{
"name": "typescript",
"version": "4.1.5-pnpify",
"main": "./lib/typescript.js",
"type": "commonjs"
}

View File

@@ -87,6 +87,7 @@ Run `.\bin\tgui.bat` with any of the options listed below.
doing development on IE8).
- `bin/tgui --lint` - Show problems with the code.
- `bin/tgui --fix` - Auto-fix problems with the code.
- `bin/tgui --test` - Run tests.
- `bin/tgui --analyze` - Run a bundle analyzer.
- `bin/tgui --clean` - Clean up project repo.
- `bin/tgui [webpack options]` - Build the project with custom webpack

View File

@@ -8,6 +8,9 @@ const createBabelConfig = options => {
const { mode, presets = [], plugins = [] } = options;
return {
presets: [
['@babel/preset-typescript', {
allowDeclareFields: true,
}],
['@babel/preset-env', {
modules: 'commonjs',
useBuiltIns: 'entry',
@@ -19,6 +22,9 @@ const createBabelConfig = options => {
...presets,
],
plugins: [
['@babel/plugin-proposal-class-properties', {
loose: true,
}],
'@babel/plugin-transform-jscript',
'babel-plugin-inferno',
'babel-plugin-transform-remove-console',
@@ -28,7 +34,7 @@ const createBabelConfig = options => {
};
};
module.exports = (api) => {
module.exports = api => {
api.cache(true);
const mode = process.env.NODE_ENV;
return createBabelConfig({ mode });

View File

@@ -63,12 +63,19 @@ task-dev-server() {
}
## Run a linter through all packages
task-eslint() {
task-lint() {
cd "${base_dir}"
yarn run eslint packages "${@}"
yarn run tsc
echo "tgui: type check passed"
yarn run eslint packages --ext .js,.jsx,.ts,.tsx,.cjs,.mjs "${@}"
echo "tgui: eslint check passed"
}
task-test() {
cd "${base_dir}"
yarn run jest
}
## Mr. Proper
task-clean() {
cd "${base_dir}"
@@ -147,21 +154,28 @@ fi
if [[ ${1} == '--lint' ]]; then
shift 1
task-install
task-eslint "${@}"
task-lint "${@}"
exit 0
fi
if [[ ${1} == '--lint-harder' ]]; then
shift 1
task-install
task-eslint -c .eslintrc-harder.yml "${@}"
task-lint -c .eslintrc-harder.yml "${@}"
exit 0
fi
if [[ ${1} == '--fix' ]]; then
shift 1
task-install
task-eslint --fix "${@}"
task-lint --fix "${@}"
exit 0
fi
if [[ ${1} == '--test' ]]; then
shift 1
task-install
task-test "${@}"
exit 0
fi
@@ -182,7 +196,7 @@ fi
## Make a production webpack build + Run eslint
if [[ -z ${1} ]]; then
task-install
task-eslint --fix
task-lint --fix
task-webpack --mode=production
exit 0
fi

View File

@@ -51,11 +51,17 @@ function task-dev-server {
}
## Run a linter through all packages
function task-eslint {
yarn run eslint packages @Args
function task-lint {
yarn run tsc
Write-Output "tgui: type check passed"
yarn run eslint packages --ext .js,.jsx,.ts,.tsx,.cjs,.mjs @Args
Write-Output "tgui: eslint check passed"
}
function task-test {
yarn run jest
}
## Mr. Proper
function task-clean {
## Build artifacts
@@ -94,21 +100,28 @@ if ($Args.Length -gt 0) {
if ($Args[0] -eq "--lint") {
$Rest = $Args | Select-Object -Skip 1
task-install
task-eslint @Rest
task-lint @Rest
exit 0
}
if ($Args[0] -eq "--lint-harder") {
$Rest = $Args | Select-Object -Skip 1
task-install
task-eslint -c ".eslintrc-harder.yml" @Rest
task-lint -c ".eslintrc-harder.yml" @Rest
exit 0
}
if ($Args[0] -eq "--fix") {
$Rest = $Args | Select-Object -Skip 1
task-install
task-eslint --fix @Rest
task-lint --fix @Rest
exit 0
}
if ($Args[0] -eq "--test") {
$Rest = $Args | Select-Object -Skip 1
task-install
task-test @Rest
exit 0
}
@@ -123,7 +136,7 @@ if ($Args.Length -gt 0) {
## Make a production webpack build
if ($Args.Length -eq 0) {
task-install
task-eslint
task-lint
task-webpack --mode=production
exit 0
}

View File

@@ -0,0 +1,23 @@
## Jest
You can now write and run unit tests in tgui.
It's quite simple: create a file ending in `.test.ts` or `.spec.ts` (usually with the same filename as the file you're testing), and create a test case:
```js
test('something', () => {
expect('a').toBe('a');
});
```
To run the tests, type the following into the terminal:
```
bin/tgui --test
```
There is an example test in `packages/common/react.spec.ts`.
You can read more about Jest here: https://jestjs.io/docs/en/getting-started
Note, that there is still no real solution to test UIs for now, even though a lot of the support is here (jest + jsdom). That will come later.

28
tgui/global.d.ts vendored
View File

@@ -1,4 +1,11 @@
interface ByondType {
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
declare global {
type ByondType = {
/**
* True if javascript is running in BYOND.
*/
@@ -112,6 +119,23 @@ interface ByondType {
* Loads a script into the document.
*/
loadJs(url: string): void;
};
/**
* Object that provides access to Byond Skin API and is available in
* any tgui application.
*/
const Byond: ByondType;
interface Window {
/**
* ID of the Byond window this script is running on.
* Should be used as a parameter to winget/winset.
*/
__windowId__: string;
Byond: ByondType;
}
declare const Byond: ByondType;
}
export {};

14
tgui/jest.config.js Normal file
View File

@@ -0,0 +1,14 @@
module.exports = {
roots: ['<rootDir>/packages'],
testMatch: [
'<rootDir>/packages/**/__tests__/*.{js,jsx,ts,tsx}',
'<rootDir>/packages/**/*.{spec,test}.{js,jsx,ts,tsx}',
],
testEnvironment: 'jsdom',
testRunner: require.resolve('jest-circus/runner'),
transform: {
'^.+\\.(js|jsx|ts|tsx|cjs|mjs)$': require.resolve('babel-jest'),
},
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],
resetMocks: true,
};

View File

@@ -6,27 +6,37 @@
"packages/*"
],
"dependencies": {
"@babel/core": "^7.12.13",
"@babel/eslint-parser": "^7.12.13",
"@babel/core": "^7.12.17",
"@babel/eslint-parser": "^7.12.17",
"@babel/plugin-proposal-class-properties": "^7.12.13",
"@babel/plugin-transform-jscript": "^7.12.13",
"@babel/preset-env": "^7.12.13",
"@babel/preset-env": "^7.12.17",
"@babel/preset-typescript": "^7.12.17",
"@types/jest": "^26.0.20",
"@types/jsdom": "^16.2.6",
"@types/node": "^14.14.31",
"@typescript-eslint/parser": "^4.15.1",
"babel-jest": "^26.6.3",
"babel-loader": "^8.2.2",
"babel-plugin-inferno": "^6.1.1",
"babel-plugin-transform-remove-console": "^6.9.4",
"common": "workspace:*",
"css-loader": "^5.0.2",
"cssnano": "^4.1.10",
"eslint": "^7.19.0",
"eslint": "^7.20.0",
"eslint-plugin-react": "^7.22.0",
"file-loader": "^6.2.0",
"inferno": "^7.4.7",
"mini-css-extract-plugin": "^1.3.6",
"sass": "^1.32.6",
"inferno": "^7.4.8",
"jest": "^26.6.3",
"jest-circus": "^26.6.3",
"jsdom": "^16.4.0",
"mini-css-extract-plugin": "^1.3.8",
"sass": "^1.32.8",
"sass-loader": "^11.0.1",
"style-loader": "^2.0.0",
"terser-webpack-plugin": "^5.1.1",
"typescript": "^4.1.5",
"url-loader": "^4.1.1",
"webpack": "^5.21.2",
"webpack": "^5.23.0",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.5.0"
}

View File

@@ -0,0 +1,20 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { classes } from './react';
describe('classes', () => {
test('empty', () => {
expect(classes([])).toBe('');
});
test('result contains inputs', () => {
const output = classes(['foo', 'bar', false, true, 0, 1, 'baz']);
expect(output).toContain('foo');
expect(output).toContain('bar');
expect(output).toContain('baz');
});
});

View File

@@ -6,11 +6,8 @@
/**
* Helper for conditionally adding/removing classes in React
*
* @param {any[]} classNames
* @return {string}
*/
export const classes = classNames => {
export const classes = (classNames: (string | BooleanLike)[]) => {
let className = '';
for (let i = 0; i < classNames.length; i++) {
const part = classNames[i];
@@ -25,9 +22,9 @@ export const classes = classNames => {
* Normalizes children prop, so that it is always an array of VDom
* elements.
*/
export const normalizeChildren = children => {
export const normalizeChildren = <T>(children: T | T[]) => {
if (Array.isArray(children)) {
return children.flat().filter(value => value);
return children.flat().filter(value => value) as T[];
}
if (typeof children === 'object') {
return [children];
@@ -39,7 +36,7 @@ export const normalizeChildren = children => {
* Shallowly checks if two objects are different.
* Credit: https://github.com/developit/preact-compat
*/
export const shallowDiffers = (a, b) => {
export const shallowDiffers = (a: object, b: object) => {
let i;
for (i in a) {
if (!(i in b)) {
@@ -66,8 +63,14 @@ export const pureComponentHooks = {
/**
* A helper to determine whether the object is renderable by React.
*/
export const canRender = value => {
export const canRender = (value: unknown) => {
return value !== undefined
&& value !== null
&& typeof value !== 'boolean';
};
/**
* A common case in tgui, when you pass a value conditionally, these are
* the types that can fall through the condition.
*/
export type BooleanLike = number | boolean | null | undefined;

View File

@@ -108,15 +108,15 @@ export const combineReducers = reducersObj => {
* returns the action type, allowing it to be used in reducer logic that
* is looking for that action type.
*
* @param type The action type to use for created actions.
* @param prepare (optional) a method that takes any number of arguments
* @param {string} type The action type to use for created actions.
* @param {any} prepare (optional) a method that takes any number of arguments
* and returns { payload } or { payload, meta }. If this is given, the
* resulting action creator will pass it's arguments to this method to
* calculate payload & meta.
*
* @public
*/
export const createAction = (type, prepare) => {
export const createAction = (type, prepare = null) => {
const actionCreator = (...args) => {
if (!prepare) {
return { type, payload: args[0] };

View File

@@ -5,7 +5,7 @@
"dependencies": {
"common": "workspace:*",
"dompurify": "^2.2.6",
"inferno": "^7.4.7",
"inferno": "^7.4.8",
"tgui": "workspace:*",
"tgui-dev-server": "workspace:*",
"tgui-polyfill": "workspace:*"

View File

@@ -3,8 +3,8 @@
"name": "tgui-polyfill",
"version": "4.3.0",
"dependencies": {
"core-js": "^3.8.3",
"core-js": "^3.9.0",
"regenerator-runtime": "^0.13.7",
"whatwg-fetch": "^3.5.0"
"whatwg-fetch": "^3.6.1"
}
}

View File

@@ -12,6 +12,7 @@
*/
import { perf } from 'common/perf';
import { createAction } from 'common/redux';
import { setupDrag } from './drag';
import { focusMap } from './focus';
import { createLogger } from './logging';
@@ -19,19 +20,9 @@ import { resumeRenderer, suspendRenderer } from './renderer';
const logger = createLogger('backend');
export const backendUpdate = state => ({
type: 'backend/update',
payload: state,
});
export const backendSetSharedState = (key, nextState) => ({
type: 'backend/setSharedState',
payload: { key, nextState },
});
export const backendSuspendStart = () => ({
type: 'backend/suspendStart',
});
export const backendUpdate = createAction('backend/update');
export const backendSetSharedState = createAction('backend/setSharedState');
export const backendSuspendStart = createAction('backend/suspendStart');
export const backendSuspendSuccess = () => ({
type: 'backend/suspendSuccess',
@@ -222,9 +213,9 @@ export const backendMiddleware = store => {
/**
* Sends a message to /datum/tgui_window.
*/
export const sendMessage = (message = {}) => {
export const sendMessage = (message: any = {}) => {
const { payload, ...rest } = message;
const data = {
const data: any = {
// Message identifying header
tgui: 1,
window_id: window.__windowId__,
@@ -242,7 +233,7 @@ export const sendMessage = (message = {}) => {
* Sends an action to `ui_act` on `src_object` that this tgui window
* is associated with.
*/
export const sendAct = (action, payload = {}) => {
export const sendAct = (action: string, payload: object = {}) => {
// Validate that payload is an object
const isObject = typeof payload === 'object'
&& payload !== null
@@ -257,42 +248,39 @@ export const sendAct = (action, payload = {}) => {
});
};
/**
* @typedef BackendState
* @type {{
* config: {
* title: string,
* status: number,
* interface: string,
* window: {
* key: string,
* size: [number, number],
* fancy: boolean,
* locked: boolean,
* },
* client: {
* ckey: string,
* address: string,
* computer_id: string,
* },
* user: {
* name: string,
* observer: number,
* },
* },
* data: any,
* shared: any,
* suspending: boolean,
* suspended: boolean,
* }}
*/
type BackendState<TData> = {
config: {
title: string,
status: number,
interface: string,
window: {
key: string,
size: [number, number],
fancy: boolean,
locked: boolean,
},
client: {
ckey: string,
address: string,
computer_id: string,
},
user: {
name: string,
observer: number,
},
},
data: TData,
shared: Record<string, any>,
suspending: boolean,
suspended: boolean,
}
/**
* Selects a backend-related slice of Redux state
*
* @return {BackendState}
*/
export const selectBackend = state => state.backend || {};
export const selectBackend = <TData>(state: any): BackendState<TData> => (
state.backend || {}
);
/**
* A React hook (sort of) for getting tgui state and related functions.
@@ -300,19 +288,22 @@ export const selectBackend = state => state.backend || {};
* This is supposed to be replaced with a real React Hook, which can only
* be used in functional components.
*
* @return {BackendState & {
* act: sendAct,
* }}
* You can make
*/
export const useBackend = context => {
export const useBackend = <TData>(context: any) => {
const { store } = context;
const state = selectBackend(store.getState());
const state = selectBackend<TData>(store.getState());
return {
...state,
act: sendAct,
};
};
/**
* A tuple that contains the state and a setter function for it.
*/
type StateWithSetter<T> = [T, (nextState: T) => void];
/**
* Allocates state on Redux store without sharing it with other clients.
*
@@ -322,11 +313,15 @@ export const useBackend = context => {
*
* It is a lot more performant than `setSharedState`.
*
* @param {any} context React context.
* @param {string} key Key which uniquely identifies this state in Redux store.
* @param {any} initialState Initializes your global variable with this value.
* @param context React context.
* @param key Key which uniquely identifies this state in Redux store.
* @param initialState Initializes your global variable with this value.
*/
export const useLocalState = (context, key, initialState) => {
export const useLocalState = <T>(
context: any,
key: string,
initialState: T,
): StateWithSetter<T> => {
const { store } = context;
const state = selectBackend(store.getState());
const sharedStates = state.shared ?? {};
@@ -336,11 +331,14 @@ export const useLocalState = (context, key, initialState) => {
return [
sharedState,
nextState => {
store.dispatch(backendSetSharedState(key, (
store.dispatch(backendSetSharedState({
key,
nextState: (
typeof nextState === 'function'
? nextState(sharedState)
: nextState
)));
),
}));
},
];
};
@@ -355,11 +353,15 @@ export const useLocalState = (context, key, initialState) => {
*
* This makes creation of observable s
*
* @param {any} context React context.
* @param {string} key Key which uniquely identifies this state in Redux store.
* @param {any} initialState Initializes your global variable with this value.
* @param context React context.
* @param key Key which uniquely identifies this state in Redux store.
* @param initialState Initializes your global variable with this value.
*/
export const useSharedState = (context, key, initialState) => {
export const useSharedState = <T>(
context: any,
key: string,
initialState: T,
): StateWithSetter<T> => {
const { store } = context;
const state = selectBackend(store.getState());
const sharedStates = state.shared ?? {};

View File

@@ -4,15 +4,64 @@
* @license MIT
*/
import { classes, pureComponentHooks } from 'common/react';
import { createVNode } from 'inferno';
import { BooleanLike, classes, pureComponentHooks } from 'common/react';
import { createVNode, InfernoNode } from 'inferno';
import { ChildFlags, VNodeFlags } from 'inferno-vnode-flags';
import { CSS_COLORS } from '../constants';
export interface BoxProps {
[key: string]: any;
as?: string;
className?: string | BooleanLike;
children?: InfernoNode;
position?: string | BooleanLike;
overflow?: string | BooleanLike;
overflowX?: string | BooleanLike;
overflowY?: string | BooleanLike;
top?: string | BooleanLike;
bottom?: string | BooleanLike;
left?: string | BooleanLike;
right?: string | BooleanLike;
width?: string | BooleanLike;
minWidth?: string | BooleanLike;
maxWidth?: string | BooleanLike;
height?: string | BooleanLike;
minHeight?: string | BooleanLike;
maxHeight?: string | BooleanLike;
fontSize?: string | BooleanLike;
fontFamily?: string;
lineHeight?: string | BooleanLike;
opacity?: number;
textAlign?: string | BooleanLike;
verticalAlign?: string | BooleanLike;
inline?: BooleanLike;
bold?: BooleanLike;
italic?: BooleanLike;
nowrap?: BooleanLike;
m?: string | BooleanLike;
mx?: string | BooleanLike;
my?: string | BooleanLike;
mt?: string | BooleanLike;
mb?: string | BooleanLike;
ml?: string | BooleanLike;
mr?: string | BooleanLike;
p?: string | BooleanLike;
px?: string | BooleanLike;
py?: string | BooleanLike;
pt?: string | BooleanLike;
pb?: string | BooleanLike;
pl?: string | BooleanLike;
pr?: string | BooleanLike;
color?: string | BooleanLike;
textColor?: string | BooleanLike;
backgroundColor?: string | BooleanLike;
fillPositionedParent?: boolean;
}
/**
* Coverts our rem-like spacing unit into a CSS unit.
*/
export const unit = value => {
export const unit = (value: unknown): string | undefined => {
if (typeof value === 'string') {
// Transparently convert pixels into rem units
if (value.endsWith('px') && !Byond.IS_LTE_IE8) {
@@ -31,7 +80,7 @@ export const unit = value => {
/**
* Same as `unit`, but half the size for integers numbers.
*/
export const halfUnit = value => {
export const halfUnit = (value: unknown): string | undefined => {
if (typeof value === 'string') {
return unit(value);
}
@@ -40,10 +89,13 @@ export const halfUnit = value => {
}
};
const isColorCode = str => !isColorClass(str);
const isColorCode = (str: unknown) => !isColorClass(str);
const isColorClass = str => typeof str === 'string'
&& CSS_COLORS.includes(str);
const isColorClass = (str: unknown): boolean => {
if (typeof str === 'string') {
return CSS_COLORS.includes(str);
}
};
const mapRawPropTo = attrName => (style, value) => {
if (typeof value === 'number' || typeof value === 'string') {
@@ -155,8 +207,8 @@ const styleMapperByPropName = {
},
};
export const computeBoxProps = props => {
const computedProps = {};
export const computeBoxProps = (props: BoxProps) => {
const computedProps: HTMLAttributes<any> = {};
const computedStyles = {};
// Compute props
for (let propName of Object.keys(props)) {
@@ -195,7 +247,7 @@ export const computeBoxProps = props => {
return computedProps;
};
export const computeBoxClassName = props => {
export const computeBoxClassName = (props: BoxProps) => {
const color = props.textColor || props.color;
const backgroundColor = props.backgroundColor;
return classes([
@@ -204,7 +256,7 @@ export const computeBoxClassName = props => {
]);
};
export const Box = props => {
export const Box = (props: BoxProps) => {
const {
as = 'div',
className,

View File

@@ -4,10 +4,18 @@
* @license MIT
*/
import { classes, pureComponentHooks } from 'common/react';
import { Box, unit } from './Box';
import { BooleanLike, classes, pureComponentHooks } from 'common/react';
import { Box, BoxProps, unit } from './Box';
export const computeFlexProps = props => {
export interface FlexProps extends BoxProps {
direction: string | BooleanLike;
wrap: string | BooleanLike;
align: string | BooleanLike;
justify: string | BooleanLike;
inline: BooleanLike;
}
export const computeFlexProps = (props: FlexProps) => {
const {
className,
direction,
@@ -45,7 +53,15 @@ export const Flex = props => (
Flex.defaultHooks = pureComponentHooks;
export const computeFlexItemProps = props => {
export interface FlexItemProps extends BoxProps {
grow?: number;
order?: number;
shrink?: number;
basis?: string | BooleanLike;
align?: string | BooleanLike;
}
export const computeFlexItemProps = (props: FlexItemProps) => {
const {
className,
style,
@@ -77,7 +93,7 @@ export const computeFlexItemProps = props => {
};
};
export const FlexItem = props => (
const FlexItem = props => (
<Box {...computeFlexItemProps(props)} />
);

View File

@@ -7,6 +7,7 @@
import { Table } from './Table';
import { pureComponentHooks } from 'common/react';
/** @deprecated */
export const Grid = props => {
const { children, ...rest } = props;
return (
@@ -20,6 +21,7 @@ export const Grid = props => {
Grid.defaultHooks = pureComponentHooks;
/** @deprecated */
export const GridColumn = props => {
const { size = 1, style, ...rest } = props;
return (

View File

@@ -4,11 +4,16 @@
* @license MIT
*/
import { classes, pureComponentHooks } from 'common/react';
import { BooleanLike, classes, pureComponentHooks } from 'common/react';
import { InfernoNode } from 'inferno';
import { Box, unit } from './Box';
import { Divider } from './Divider';
export const LabeledList = props => {
type LabeledListProps = {
children: InfernoNode;
};
export const LabeledList = (props: LabeledListProps) => {
const { children } = props;
return (
<table className="LabeledList">
@@ -19,7 +24,19 @@ export const LabeledList = props => {
LabeledList.defaultHooks = pureComponentHooks;
export const LabeledListItem = props => {
type LabeledListItemProps = {
className?: string | BooleanLike;
label?: string | BooleanLike;
labelColor?: string | BooleanLike;
color?: string | BooleanLike;
textAlign?: string | BooleanLike;
buttons?: InfernoNode,
/** @deprecated */
content?: any,
children?: InfernoNode;
};
const LabeledListItem = (props: LabeledListItemProps) => {
const {
className,
label,
@@ -68,7 +85,11 @@ export const LabeledListItem = props => {
LabeledListItem.defaultHooks = pureComponentHooks;
export const LabeledListDivider = props => {
type LabeledListDividerProps = {
size?: number;
};
const LabeledListDivider = (props: LabeledListDividerProps) => {
const padding = props.size
? unit(Math.max(0, props.size - 1))
: 0;

View File

@@ -5,11 +5,27 @@
*/
import { canRender, classes } from 'common/react';
import { Component, createRef } from 'inferno';
import { Component, createRef, InfernoNode, RefObject } from 'inferno';
import { addScrollableNode, removeScrollableNode } from '../events';
import { computeBoxClassName, computeBoxProps } from './Box';
import { BoxProps, computeBoxClassName, computeBoxProps } from './Box';
interface SectionProps extends BoxProps {
className?: string;
title?: string;
buttons?: InfernoNode;
fill?: boolean;
fitted?: boolean;
scrollable?: boolean;
/** @deprecated This property no longer works, please remove it. */
level?: boolean;
/** @deprecated Please use `scrollable` property */
overflowY?: any;
}
export class Section extends Component<SectionProps> {
scrollableRef: RefObject<HTMLDivElement>;
scrollable: boolean;
export class Section extends Component {
constructor(props) {
super(props);
this.scrollableRef = createRef();
@@ -49,7 +65,7 @@ export class Section extends Component {
fitted && 'Section--fitted',
scrollable && 'Section--scrollable',
className,
...computeBoxClassName(rest),
computeBoxClassName(rest),
])}
{...computeBoxProps(rest)}>
{hasTitle && (

View File

@@ -5,9 +5,14 @@
*/
import { classes } from 'common/react';
import { Flex } from './Flex';
import { Flex, FlexItemProps, FlexProps } from './Flex';
export const Stack = props => {
interface StackProps extends FlexProps {
vertical?: boolean;
fill?: boolean;
}
export const Stack = (props: StackProps) => {
const { className, vertical, fill, ...rest } = props;
return (
<Flex
@@ -24,7 +29,7 @@ export const Stack = props => {
);
};
const StackItem = props => {
const StackItem = (props: FlexProps) => {
const { className, ...rest } = props;
return (
<Flex.Item
@@ -38,7 +43,11 @@ const StackItem = props => {
Stack.Item = StackItem;
const StackDivider = props => {
interface StackDividerProps extends FlexItemProps {
hidden?: boolean;
}
const StackDivider = (props: StackDividerProps) => {
const { className, hidden, ...rest } = props;
return (
<Flex.Item

View File

@@ -1,6 +1,5 @@
import { useBackend } from '../backend';
import { Box, Button, LabeledList, ProgressBar, Section } from '../components';
import { LabeledListItem } from '../components/LabeledList';
import { Window } from '../layouts';
export const SatelliteControl = (props, context) => {
@@ -14,7 +13,7 @@ export const SatelliteControl = (props, context) => {
{data.meteor_shield && (
<Section>
<LabeledList>
<LabeledListItem label="Coverage">
<LabeledList.Item label="Coverage">
<ProgressBar
value={data.meteor_shield_coverage
/ data.meteor_shield_coverage_max}
@@ -25,7 +24,7 @@ export const SatelliteControl = (props, context) => {
average: [0.30, 1],
bad: [-Infinity, 0.30],
}} />
</LabeledListItem>
</LabeledList.Item>
</LabeledList>
</Section>
)}

View File

@@ -5,8 +5,8 @@
"dependencies": {
"common": "workspace:*",
"dompurify": "^2.2.6",
"inferno": "^7.4.7",
"inferno-vnode-flags": "^7.4.7",
"inferno": "^7.4.8",
"inferno-vnode-flags": "^7.4.8",
"marked": "^2.0.0",
"tgui-dev-server": "workspace:*",
"tgui-polyfill": "workspace:*"

28
tgui/tsconfig.json Normal file
View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es3",
"noEmit": true,
"strict": false,
"allowJs": true,
"checkJs": false,
"jsx": "preserve",
"lib": [
"dom",
"dom.iterable",
"esnext",
"ScriptHost"
],
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": false
},
"include": [
"./global.d.ts",
"./packages"
]
}

View File

@@ -49,13 +49,13 @@ module.exports = (env = {}, argv) => {
chunkLoadTimeout: 15000,
},
resolve: {
extensions: ['.js', '.jsx'],
extensions: ['.tsx', '.ts', '.jsx', '.js'],
alias: {},
},
module: {
rules: [
{
test: /\.m?jsx?$/,
test: /\.(js|jsx|cjs|mjs|ts|tsx)$/,
use: [
{
loader: 'babel-loader',

File diff suppressed because it is too large Load Diff