mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-10 09:42:29 +00:00
tgui: Typescript and Jest update (#57081)
Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com>
This commit is contained in:
1
.github/workflows/ci_suite.yml
vendored
1
.github/workflows/ci_suite.yml
vendored
@@ -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
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -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
|
||||
|
||||
@@ -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
1
tgui/.gitignore
vendored
@@ -10,6 +10,7 @@ package-lock.json
|
||||
!/.yarn/plugins
|
||||
!/.yarn/sdks
|
||||
!/.yarn/versions
|
||||
!/.yarn/lock.yml
|
||||
|
||||
## Build artifacts
|
||||
/public/.tmp/**/*
|
||||
|
||||
2
tgui/.yarn/sdks/eslint/bin/eslint.js
vendored
2
tgui/.yarn/sdks/eslint/bin/eslint.js
vendored
@@ -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";
|
||||
|
||||
|
||||
2
tgui/.yarn/sdks/eslint/package.json
vendored
2
tgui/.yarn/sdks/eslint/package.json
vendored
@@ -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
20
tgui/.yarn/sdks/typescript/bin/tsc
vendored
Normal 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
20
tgui/.yarn/sdks/typescript/bin/tsserver
vendored
Normal 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`);
|
||||
6
tgui/.yarn/sdks/typescript/package.json
vendored
Normal file
6
tgui/.yarn/sdks/typescript/package.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "typescript",
|
||||
"version": "4.1.5-pnpify",
|
||||
"main": "./lib/typescript.js",
|
||||
"type": "commonjs"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
23
tgui/docs/writing-tests.md
Normal file
23
tgui/docs/writing-tests.md
Normal 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
28
tgui/global.d.ts
vendored
@@ -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
14
tgui/jest.config.js
Normal 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,
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
20
tgui/packages/common/react.spec.ts
Normal file
20
tgui/packages/common/react.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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] };
|
||||
|
||||
@@ -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:*"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ?? {};
|
||||
@@ -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,
|
||||
@@ -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)} />
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
@@ -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 && (
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
28
tgui/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
4911
tgui/yarn.lock
4911
tgui/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user