Merge pull request #524 from Hornwitser/typescript-plugin-web

Port plugin web code to TypeScript
This commit is contained in:
Hornwitser authored and GitHub committed 2023-11-29 20:16:24 +01:00
commit faadf873fb
80 files changed
+312 -193

No files matched your search

+11 -2
View File
@@ -22,6 +22,7 @@ module.exports = {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-useless-constructor": "error",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/triple-slash-reference": "off",
"prefer-const": "off",
},
},
@@ -39,7 +40,11 @@ module.exports = {
},
},
{
"files": ["packages/web_ui/src/**/*.ts?(x)", "packages/lib/browser.js", "plugins/*/web/**/*.ts?(x)"],
"files": [
"packages/web_ui/src/**/*.ts?(x)",
"packages/lib/browser.ts",
"{packages,plugins}/*/web/**/*.ts?(x)",
],
"env": {
"browser": true,
},
@@ -315,7 +320,11 @@ module.exports = {
"space-in-parens": ["error", "never"],
"space-infix-ops": "off",
"space-unary-ops": "error",
"spaced-comment": ["error", "always"],
"spaced-comment": ["error", "always", {
"line": {
"markers": ["/"],
},
}],
"strict": "error",
"switch-colon-spacing": "error",
"symbol-description": "error",
+1
View File
@@ -1 +1,2 @@
webpack.config.js
dist/web/build
+1 -13
View File
@@ -93,20 +93,8 @@ async function getMetrics(req: Request, res: Response, next: any) {
}
export type PluginWebApi = {
name: string;
version: string;
enabled: boolean;
loaded: boolean;
web: {
main: string;
error?: string;
};
requirePath: string;
}
function getPlugins(req: Request, res: Response) {
let plugins: PluginWebApi[] = [];
let plugins: lib.PluginWebApi[] = [];
for (let pluginInfo of req.app.locals.controller.pluginInfos) {
let name = pluginInfo.name;
let loaded = req.app.locals.controller.plugins.has(name);
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.web.json",
"references": [
{ "path": "../lib" },
{ "path": "../web_ui/tsconfig.web.json" },
],
"include": [ "web/**/*.tsx", "web/**/*.ts", "package.json" ],
"compilerOptions": {
"rootDir": "..",
"outDir": "dist/web/build",
}
}
+20 -9
View File
@@ -17,22 +17,33 @@
margin: auto;
margin-top: 1em;
width: fit-content;
white-space: pre;
}
</style>
<script>
window.webRoot = new URL("__WEB_ROOT__", document.location).pathname;
window.staticRoot = new URL("__STATIC_ROOT__", document.location).pathname;
function scriptError() {
let bootstrapMsg = document.getElementById("bootstrap-msg");
bootstrapMsg.textContent = "Error loading main bundle, see web console for details.";
}
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<div id="bootstrap-msg">Loading web interface...</div>
</div>
<script src="__STATIC_ROOT____MAIN_BUNDLE__" defer onerror="scriptError()"></script>
<script id="script-bundle" src="__STATIC_ROOT____MAIN_BUNDLE__" defer></script>
<script>
webRoot = new URL("__WEB_ROOT__", document.location).pathname;
staticRoot = new URL("__STATIC_ROOT__", document.location).pathname;
function scriptError(err) {
console.error(err);
let bootstrapMsg = document.getElementById("bootstrap-msg");
if (bootstrapMsg) {
let errorMsg = err;
if (err?.message) {
errorMsg = `${err?.constructor?.name ?? "Error"}: ${err.message}`;
}
bootstrapMsg.textContent = `${errorMsg}\nSee web console for details.`;
}
}
const bundleScript = document.getElementById("script-bundle");
bundleScript.addEventListener("error", () => scriptError("Main bundle failed to load."));
window.addEventListener("error", event => scriptError(event.error));
</script>
</body>
</html>
-3
View File
@@ -1,3 +0,0 @@
import("@clusterio/web_ui").then(webUi => {
return webUi.bootstrap();
}).catch(console.error);
+5
View File
@@ -0,0 +1,5 @@
declare function scriptError(err: any): void;
import("@clusterio/web_ui")
.then(webUi => webUi.bootstrap())
.catch(scriptError)
;
+1 -1
View File
@@ -7,7 +7,7 @@ const common = require("@clusterio/web_ui/webpack.common");
module.exports = (env = {}) => merge(common(env), {
context: __dirname,
entry: "./web/index.jsx",
entry: "./web/index.tsx",
devServer: {
contentBase: "./dist/web",
},
+1
View File
@@ -0,0 +1 @@
/src/data/message_validate.js
File renamed without changes.
-16
View File
@@ -1,16 +0,0 @@
export * from "./dist/src/config";
export * from "./dist/src/data";
export * from "./dist/src/errors";
export * from "./dist/src/factorio/exchange_string";
export * from "./dist/src/helpers";
export * from "./dist/src/ini";
export * from "./dist/src/link";
export * from "./dist/src/logging";
export * from "./dist/src/plugin";
export * from "./dist/src/schema";
export * from "./dist/src/users";
export * from "./dist/src/subscriptions";
export { default as ExponentialBackoff } from "./dist/src/ExponentialBackoff";
export { default as PlayerStats } from "./dist/src/PlayerStats";
export { default as RateLimiter } from "./dist/src/RateLimiter";
+17
View File
@@ -0,0 +1,17 @@
export * from "./src/api";
export * from "./src/config";
export * from "./src/data";
export * from "./src/errors";
export * from "./src/factorio/exchange_string";
export * from "./src/helpers";
export * from "./src/ini";
export * from "./src/link";
export * from "./src/logging";
export * from "./src/plugin";
export * from "./src/schema";
export * from "./src/users";
export * from "./src/subscriptions";
export { default as ExponentialBackoff } from "./src/ExponentialBackoff";
export { default as PlayerStats } from "./src/PlayerStats";
export { default as RateLimiter } from "./src/RateLimiter";
+1
View File
@@ -3,6 +3,7 @@
* @module lib
*/
export * from "./build_mod";
export * from "./src/api";
export * from "./src/command";
export * from "./src/config";
export * from "./src/data";
+1 -1
View File
@@ -9,7 +9,7 @@
"compile_validator": "node scripts/compile_validator.js"
},
"main": "dist/index.js",
"browser": "browser.js",
"browser": "browser.ts",
"keywords": [
"factorio"
],
+8 -4
View File
@@ -22,7 +22,11 @@ const ajv = new Ajv({
});
const validate = ajv.compile(Message.jsonSchema);
let moduleCode = standaloneCode(ajv, validate);
fs.outputFileSync(
path.join(__dirname, "../dist/src/data/message_validate.js"),
`// generated by scripts/compile_validator.js\n// @ts-nocheck\n/* eslint-disable */\n${moduleCode}`
);
function write(output) {
fs.outputFileSync(
path.join(__dirname, output),
`// generated by scripts/compile_validator.js\n// @ts-nocheck\n/* eslint-disable */\n${moduleCode}`
);
}
write("../dist/src/data/message_validate.js");
write("../src/data/message_validate.js");
+11
View File
@@ -0,0 +1,11 @@
export type PluginWebApi = {
name: string;
version: string;
enabled: boolean;
loaded: boolean;
web: {
main: string;
error?: string;
};
requirePath: string;
}
+3 -5
View File
@@ -1,12 +1,10 @@
declare module "*.png" {
const value: import("react-native").ImageSourcePropType;
const value: string;
export default value;
}
declare interface Window {
webRoot: string;
staticRoot: string;
}
declare const webRoot: string;
declare const staticRoot: string;
declare const __webpack_init_sharing__: (shareScope: string) => Promise<void>;
declare const __webpack_share_scopes__: { default: any };
+1
View File
@@ -5,6 +5,7 @@
"license": "MIT",
"version": "2.0.0-alpha.13",
"browser": "src/index.ts",
"main": "dist/src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
+9 -7
View File
@@ -1,5 +1,6 @@
import type React from "react";
import type { AccountRole, Link, Logger, PluginWebpackEnvInfo } from "@clusterio/lib";
import type { AccountRole, Logger, PluginWebpackEnvInfo } from "@clusterio/lib";
import type { Control } from "./util/websocket";
/**
* Plugin supplied login form
@@ -19,7 +20,7 @@ export interface PluginLoginForm {
* supplied the setToken function via its props which should be called
* when an authentication token is aquired via this form.
*/
Component: typeof React.Component;
Component: React.ComponentType<{ setToken(token: string): void }>;
};
export type UserAccount = {
@@ -77,11 +78,6 @@ export default class BaseWebPlugin {
* Contents of the plugin's package.json file
*/
package: any;
/**
* Control link to the controller, not available until the
* connect event in onControllerConnectionEvent is signaled.
*/
control?: Link;
/**
* Logger for this plugin
*
@@ -152,6 +148,12 @@ export default class BaseWebPlugin {
* The plugin's own info module
*/
public info: PluginWebpackEnvInfo,
/**
* Control link to the controller
*
* Not connected at the time init is invoked.
*/
public control: Control,
logger: Logger,
) {
this.package = packageData;
+10 -11
View File
@@ -1,10 +1,9 @@
import "./index.css";
import "./style.css";
import React from "react";
import { createRoot } from "react-dom/client";
import * as lib from "@clusterio/lib";
import type { PluginWebApi } from "@clusterio/controller/src/routes";
import App from "./components/App";
import BaseWebPlugin from "./BaseWebPlugin";
@@ -28,8 +27,8 @@ async function loadScript(url: string) {
}
async function loadPluginInfos(): Promise<lib.PluginWebpackEnvInfo[]> {
let response = await fetch(`${window.webRoot}api/plugins`);
let pluginList: PluginWebApi[];
let response = await fetch(`${webRoot}api/plugins`);
let pluginList: lib.PluginWebApi[];
if (response.ok) {
pluginList = await response.json();
@@ -46,7 +45,7 @@ async function loadPluginInfos(): Promise<lib.PluginWebpackEnvInfo[]> {
continue;
}
try {
await loadScript(`${window.webRoot}${meta.web.main}`);
await loadScript(`${webRoot}${meta.web.main}`);
let container: any = (window as { [key: string]: any })[`plugin_${meta.name}`];
if (!container) {
throw new Error(`Plugin did not expose its container via plugin_${meta.name}`);
@@ -68,8 +67,8 @@ async function loadPluginInfos(): Promise<lib.PluginWebpackEnvInfo[]> {
return pluginInfos;
}
async function loadPlugins(pluginInfos: lib.PluginWebpackEnvInfo[]) {
let plugins = new Map();
async function loadPlugins(pluginInfos: lib.PluginWebpackEnvInfo[], control: Control) {
let plugins = new Map<string, BaseWebPlugin>();
for (let pluginInfo of pluginInfos) {
if (!pluginInfo.enabled) {
continue;
@@ -84,7 +83,7 @@ async function loadPlugins(pluginInfos: lib.PluginWebpackEnvInfo[]) {
WebPluginClass = webModule.WebPlugin;
}
let plugin = new WebPluginClass(pluginInfo.container, pluginInfo.package, pluginInfo, logger);
let plugin = new WebPluginClass(pluginInfo.container, pluginInfo.package, pluginInfo, control, logger);
await plugin.init();
plugins.set(pluginInfo.name, plugin);
@@ -107,11 +106,11 @@ export default async function bootstrap() {
lib.registerPluginMessages(pluginInfos);
lib.registerPluginConfigGroups(pluginInfos);
lib.finalizeConfigs();
let plugins = await loadPlugins(pluginInfos);
let wsUrl = new URL(window.webRoot, document.location.href);
let wsUrl = new URL(webRoot, document.location.href);
let controlConnector = new ControlConnector(wsUrl.href, 120, undefined);
let control = new Control(controlConnector, plugins);
let control = new Control(controlConnector);
control.plugins = await loadPlugins(pluginInfos, control);
const root = createRoot(document.getElementById("root") as HTMLDivElement);
root.render(<App control={control}/>);
+1 -1
View File
@@ -109,7 +109,7 @@ export default function App(props: AppProps) {
theme={{ algorithm: theme.darkAlgorithm }}
>
<ControlContext.Provider value={props.control}>
<BrowserRouter basename={window.webRoot}>
<BrowserRouter basename={webRoot}>
{page}
</BrowserRouter>
</ControlContext.Provider>
+2 -3
View File
@@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom";
import { Button, Form, InputNumber, Modal, Table, Tag, Typography } from "antd";
import CopyOutlined from "@ant-design/icons/lib/icons/CopyOutlined";
import type { PluginWebApi } from "@clusterio/controller/src/routes";
import * as lib from "@clusterio/lib";
import { useAccount } from "../model/account";
@@ -23,10 +22,10 @@ function GenerateHostTokenButton() {
let [token, setToken] = useState<string|null>(null);
let [hostId, setHostId] = useState<number|null>(null);
let [form] = Form.useForm();
let [pluginList, setPluginList] = useState<PluginWebApi[]>([]);
let [pluginList, setPluginList] = useState<lib.PluginWebApi[]>([]);
useEffect(() => {
(async () => {
let response = await fetch(`${window.webRoot}api/plugins`);
let response = await fetch(`${webRoot}api/plugins`);
if (response.ok) {
const plugins = await response.json();
setPluginList(plugins);
@@ -623,7 +623,7 @@ function useExportedAsset(modPack: lib.ModPack, asset: "settings"|"locale"): Map
return;
}
let response = await fetch(`${window.staticRoot}static/${assetFilename}`);
let response = await fetch(`${staticRoot}static/${assetFilename}`);
if (response.ok) {
let data = await response.json();
setAssetData(asset === "locale" ? new Map(data) : data);
+2 -2
View File
@@ -140,7 +140,7 @@ export default function ModsPage() {
control.send(
new lib.ModDownloadRequest(mod.name, mod.version)
).then((streamId: string) => {
let url = new URL(window.webRoot, document.location.origin);
let url = new URL(webRoot, document.location.origin);
url.pathname += `api/stream/${streamId}`;
document.location = url.href;
}).catch(
@@ -175,7 +175,7 @@ export default function ModsPage() {
headers={{
"X-Access-Token": control.connector.token||"",
}}
action={`${window.webRoot}api/upload-mod`}
action={`${webRoot}api/upload-mod`}
>
<Button icon={<UploadOutlined/>}>Upload</Button>
</Upload>;
@@ -2,7 +2,7 @@ import React, { useEffect, useContext, useState } from "react";
import { useParams } from "react-router-dom";
import { Alert, Descriptions, Spin } from "antd";
import type { PluginWebApi } from "@clusterio/controller/src/routes";
import type { PluginWebApi } from "@clusterio/lib";
import ControlContext from "./ControlContext";
import PageLayout from "./PageLayout";
@@ -16,7 +16,7 @@ export default function PluginViewPage() {
useEffect(() => {
(async () => {
let response = await fetch(`${window.webRoot}api/plugins`);
let response = await fetch(`${webRoot}api/plugins`);
if (response.ok) {
setPluginList(await response.json());
} else {
@@ -2,7 +2,7 @@ import React, { useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Table } from "antd";
import type { PluginWebApi } from "@clusterio/controller/src/routes";
import type { PluginWebApi } from "@clusterio/lib";
import notify from "../util/notify";
import ControlContext from "./ControlContext";
@@ -18,7 +18,7 @@ export default function PluginsPage() {
useEffect(() => {
(async () => {
let response = await fetch(`${window.webRoot}api/plugins`);
let response = await fetch(`${webRoot}api/plugins`);
if (response.ok) {
setPluginList(await response.json());
} else {
+2 -2
View File
@@ -268,7 +268,7 @@ export default function SavesList(props: { instance: InstanceState }) {
control.send(
new lib.InstanceDownloadSaveRequest(props.instance.id!, save.name)
).then(streamId => {
let url = new URL(window.webRoot, document.location.href);
let url = new URL(webRoot, document.location.href);
url.pathname += `api/stream/${streamId}`;
document.location.assign(url);
}).catch(
@@ -339,7 +339,7 @@ export default function SavesList(props: { instance: InstanceState }) {
instance_id: props.instance.id,
},
showUploadList: false,
action: `${window.webRoot}api/upload-save`,
action: `${webRoot}api/upload-save`,
onChange,
};
+2
View File
@@ -1,6 +1,8 @@
/// <reference path="../global.d.ts" />
export { default as bootstrap } from "./bootstrap";
export { default as notify, notifyErrorHandler } from "./util/notify";
export { default as BaseWebPlugin } from "./BaseWebPlugin";
export * from "./util/websocket";
export * from "./model/instance";
export * from "./model/host";
+3 -2
View File
@@ -7,6 +7,7 @@ type Metadata = {
x: number;
y: number;
size: number;
localised_name?: any[];
};
let itemMetadataCache: Map<string, Metadata> | null = null;
export function useItemMetadata() {
@@ -22,7 +23,7 @@ export function useItemMetadata() {
) {
return;
}
let response = await fetch(`${window.staticRoot}static/${exportManifest.assets["item-metadata"]}`);
let response = await fetch(`${staticRoot}static/${exportManifest.assets["item-metadata"]}`);
if (response.ok) {
let data = await response.json();
itemMetadataCache = new Map(data);
@@ -31,7 +32,7 @@ export function useItemMetadata() {
for (let [name, meta] of itemMetadataCache) {
style.sheet!.insertRule(`\
.item-${CSS.escape(name)} {
background-image: url("${window.staticRoot}static/${exportManifest.assets["item-spritesheet"]}");
background-image: url("${staticRoot}static/${exportManifest.assets["item-spritesheet"]}");
background-repeat: no-repeat;
background-position: -${meta.x}px -${meta.y}px;
height: ${meta.size}px;
+1 -1
View File
@@ -12,7 +12,7 @@ export function useLocale(): Map<string, string> {
if (!exportManifest?.assets?.["locale"]) {
return;
}
let response = await fetch(`${window.staticRoot}static/${exportManifest.assets["locale"]}`);
let response = await fetch(`${staticRoot}static/${exportManifest.assets["locale"]}`);
if (response.ok) {
let data = await response.json();
localeCache = new Map(data);
File renamed without changes.
+3 -5
View File
@@ -52,6 +52,9 @@ export class Control extends lib.Link {
/** Roles of the account this control link is connected as. */
accountRoles: lib.AccountRole[] | null = null;
/** Plugins loaded in the web interface */
public plugins = new Map<string, BaseWebPlugin>();
/** Updates not handled by the subscription service */
accountUpdateHandlers: accountHandler[] = [];
logHandlers: Map<lib.LogFilter, logHandler[]> = new Map();
@@ -85,14 +88,9 @@ export class Control extends lib.Link {
constructor(
connector: ControlConnector,
public plugins: Map<string, BaseWebPlugin>,
) {
super(connector);
for (let plugin of plugins.values()) {
plugin.control = this;
}
this.connector.on("connect", data => {
this.accountName = data.account.name;
this.accountRoles = data.account.roles;
-20
View File
@@ -1,20 +0,0 @@
{
"include": [ "src/**/*.tsx", "src/**/*.ts", "package.json", "global.d.ts" ],
"compilerOptions": {
"jsx": "react",
"lib": ["dom", "es2023"],
"module": "commonjs",
"target": "es2022",
"rootDir": "./",
"outDir": "dist",
"composite": true,
"sourceMap": true,
"strict": true,
"alwaysStrict": true,
"noImplicitAny": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"noEmitOnError": true
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.web.json",
"references": [
{ "path": "../lib" },
],
"include": [ "src/**/*.tsx", "src/**/*.ts", "package.json", "global.d.ts" ],
"compilerOptions": {
"outDir": "dist",
}
}
+4
View File
@@ -78,6 +78,10 @@ module.exports = (env = {}) => ({
exclude: /node_modules/,
use: {
loader: require.resolve("ts-loader"),
options: {
configFile: "tsconfig.web.json",
projectReferences: true,
},
},
},
{
+1
View File
@@ -1 +1,2 @@
webpack.config.js
dist/web/build
+2 -4
View File
@@ -2,7 +2,7 @@ import type * as lib from "@clusterio/lib";
import { ChatEvent } from "./messages";
const info: lib.PluginDeclaration = {
export default {
name: "global_chat",
title: "Global Chat",
description: "Forwards chat between instances.",
@@ -12,6 +12,4 @@ const info: lib.PluginDeclaration = {
messages: [
ChatEvent,
],
};
export default info;
} satisfies lib.PluginDeclaration;
+1
View File
@@ -24,6 +24,7 @@
"@clusterio/lib": "^2.0.0-alpha.13",
"@clusterio/web_ui": "^2.0.0-alpha.13",
"@types/node": "^20.4.5",
"@types/react": "^18.2.21",
"typescript": "^5.1.6",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.web.json",
"references": [
{ "path": "../../packages/lib" },
{ "path": "../../packages/web_ui/tsconfig.web.json" },
],
"include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ],
"compilerOptions": {
"outDir": "dist/web/build",
}
}
File renamed without changes.
+2 -2
View File
@@ -7,7 +7,7 @@ const common = require("@clusterio/web_ui/webpack.common");
module.exports = (env = {}) => merge(common(env), {
context: __dirname,
entry: "./web/index.jsx",
entry: "./web/index.tsx",
output: {
path: path.resolve(__dirname, "dist", "web"),
},
@@ -16,7 +16,7 @@ module.exports = (env = {}) => merge(common(env), {
name: "global_chat",
library: { type: "var", name: "plugin_global_chat" },
exposes: {
"./info": "./dist/plugin/info.js",
"./info": "./info.ts",
"./package.json": "./package.json",
},
shared: {
+1
View File
@@ -1 +1,2 @@
webpack.config.js
dist/web/build
+2 -4
View File
@@ -40,7 +40,7 @@ lib.definePermission({
grantByDefault: true,
});
const info: lib.PluginDeclaration = {
export default {
name: "inventory_sync",
title: "Inventory sync",
description: "Synchronizes players inventories between instances",
@@ -60,6 +60,4 @@ const info: lib.PluginDeclaration = {
],
webEntrypoint: "./web",
routes: ["/inventory"],
};
export default info;
} satisfies lib.PluginDeclaration;
+1 -1
View File
@@ -139,7 +139,7 @@ export class DownloadRequest {
}
}
class DatabaseStatsResponse {
export class DatabaseStatsResponse {
constructor(
public databaseSize: number,
public databaseEntries: number,
+1
View File
@@ -29,6 +29,7 @@
"@clusterio/web_ui": "^2.0.0-alpha.13",
"@types/fs-extra": "^11.0.1",
"@types/node": "^20.4.5",
"@types/react": "^18.2.21",
"antd": "^5.7.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.web.json",
"references": [
{ "path": "../../packages/lib" },
{ "path": "../../packages/web_ui/tsconfig.web.json" },
],
"include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "instance.ts", "package.json" ],
"compilerOptions": {
"outDir": "dist/web/build",
}
}
@@ -1,14 +1,14 @@
import React, { useContext, useEffect, useState } from "react";
import { BaseWebPlugin, PageLayout, ControlContext } from "@clusterio/web_ui";
import { DatabaseStatsRequest } from "../dist/plugin/messages";
import { DatabaseStatsRequest, DatabaseStatsResponse } from "../messages";
import "./index.css";
import "./style.css";
function InventoryPage() {
let control = useContext(ControlContext);
let [statsData, updateStatsData] = useState();
let [statsData, updateStatsData] = useState<DatabaseStatsResponse>();
useEffect(() => {
(async () => {
@@ -38,10 +38,4 @@ export class WebPlugin extends BaseWebPlugin {
},
];
}
onControllerConnectionEvent(event) {
if (event === "connect") {
}
}
}
File renamed without changes.
+3 -3
View File
@@ -7,7 +7,7 @@ const common = require("@clusterio/web_ui/webpack.common");
module.exports = (env = {}) => merge(common(env), {
context: __dirname,
entry: "./web/index.jsx",
entry: "./web/index.tsx",
output: {
path: path.resolve(__dirname, "dist", "web"),
},
@@ -16,9 +16,9 @@ module.exports = (env = {}) => merge(common(env), {
name: "inventory_sync",
library: { type: "var", name: "plugin_inventory_sync" },
exposes: {
"./info": "./dist/plugin/info.js",
"./info": "./info.ts",
"./package.json": "./package.json",
"./web": "./web/index.jsx",
"./web": "./web/index.tsx",
},
shared: {
"@clusterio/lib": { import: false },
+1
View File
@@ -1 +1,2 @@
webpack.config.js
dist/web/build
+2 -2
View File
@@ -46,10 +46,10 @@ export class ControllerPlugin extends BaseControllerPlugin {
}, 60e3).unref();
this.controller.app.get("/api/player_auth/servers", (req: Request, res: Response) => {
let servers = [];
let servers: string[] = [];
for (let instance of this.controller.instances!.values()) {
if (instance.status === "running" && instance.config.get("player_auth.load_plugin")) {
servers.push(instance.config.get("factorio.settings")["name"] || "unnamed server");
servers.push(instance.config.get("factorio.settings")["name"] as string || "unnamed server");
}
}
res.send(servers);
+2 -4
View File
@@ -20,7 +20,7 @@ ControllerConfigGroup.define({
});
ControllerConfigGroup.finalize();
const info: lib.PluginDeclaration = {
export default {
name: "player_auth",
title: "Player Auth",
description: "Provides authentication to the cluster via logging into a Factorio server.",
@@ -33,6 +33,4 @@ const info: lib.PluginDeclaration = {
messages.FetchPlayerCodeRequest,
messages.SetVerifyCodeRequest,
],
};
export default info;
} satisfies lib.PluginDeclaration;
+1
View File
@@ -31,6 +31,7 @@
"@types/express": "^4.17.17",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^20.4.5",
"@types/react": "^18.2.21",
"antd": "^5.7.3",
"phin": "^3.7.0",
"react": "^18.2.0",
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.web.json",
"references": [
{ "path": "../../packages/lib" },
{ "path": "../../packages/web_ui/tsconfig.web.json" },
],
"include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ],
"compilerOptions": {
"outDir": "dist/web/build",
}
}
@@ -3,17 +3,20 @@ import React, { useEffect, useState } from "react";
import { BaseWebPlugin, notifyErrorHandler } from "@clusterio/web_ui";
import { Button, Form, Input, Spin, Typography } from "antd";
import "./index.css";
import "./style.css";
const { Paragraph, Text } = Typography;
interface LoginFormProps {
setToken(token: string): void,
}
function LoginForm(props) {
let [servers, setServers] = useState(null);
let [playerCode, setPlayerCode] = useState(null);
let [playerCodeError, setPlayerCodeError] = useState(null);
let [verifyCode, setVerifyCode] = useState(null);
let [verifyToken, setVerifyToken] = useState(null);
function LoginForm(props: LoginFormProps) {
let [servers, setServers] = useState<string[] | null>(null);
let [playerCode, setPlayerCode] = useState<string | null>(null);
let [playerCodeError, setPlayerCodeError] = useState<string | null>(null);
let [verifyCode, setVerifyCode] = useState<string | undefined>();
let [verifyToken, setVerifyToken] = useState<string | null>(null);
useEffect(() => {
(async () => {
@@ -137,7 +140,7 @@ function LoginForm(props) {
<li>
<Paragraph>Enter this code into the in-game dialog:</Paragraph>
<Paragraph>
<Input disabled={verifyCode === null} style={{ width: "10em" }} value={verifyCode} />
<Input disabled={verifyCode === undefined} style={{ width: "10em" }} value={verifyCode} />
</Paragraph>
</li>
</ol>
File renamed without changes.
+3 -3
View File
@@ -7,7 +7,7 @@ const common = require("@clusterio/web_ui/webpack.common");
module.exports = (env = {}) => merge(common(env), {
context: __dirname,
entry: "./web/index.jsx",
entry: "./web/index.tsx",
output: {
path: path.resolve(__dirname, "dist", "web"),
},
@@ -16,9 +16,9 @@ module.exports = (env = {}) => merge(common(env), {
name: "player_auth",
library: { type: "var", name: "plugin_player_auth" },
exposes: {
"./info": "./dist/plugin/info.js",
"./info": "./info.ts",
"./package.json": "./package.json",
"./web": "./web/index.jsx",
"./web": "./web/index.tsx",
},
shared: {
"@clusterio/lib": { import: false },
+1
View File
@@ -1 +1,2 @@
webpack.config.js
dist/web/build
+2 -4
View File
@@ -2,7 +2,7 @@ import type * as lib from "@clusterio/lib";
import * as messages from "./messages";
const info: lib.PluginDeclaration = {
export default {
name: "research_sync",
title: "Research Sync",
description: "Synchronises technology research progress between instances.",
@@ -15,6 +15,4 @@ const info: lib.PluginDeclaration = {
messages.FinishedEvent,
messages.SyncTechnologiesRequest,
],
};
export default info;
} satisfies lib.PluginDeclaration;
+2 -1
View File
@@ -27,8 +27,9 @@
"devDependencies": {
"@clusterio/lib": "^2.0.0-alpha.13",
"@clusterio/web_ui": "^2.0.0-alpha.13",
"@types/node": "^20.4.5",
"@types/fs-extra": "^11.0.1",
"@types/node": "^20.4.5",
"@types/react": "^18.2.21",
"typescript": "^5.1.6",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.web.json",
"references": [
{ "path": "../../packages/lib" },
{ "path": "../../packages/web_ui/tsconfig.web.json" },
],
"include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ],
"compilerOptions": {
"outDir": "dist/web/build",
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ const common = require("@clusterio/web_ui/webpack.common");
module.exports = (env = {}) => merge(common(env), {
context: __dirname,
entry: "./web/index.jsx",
entry: "./web/index.tsx",
output: {
path: path.resolve(__dirname, "dist", "web"),
},
@@ -16,7 +16,7 @@ module.exports = (env = {}) => merge(common(env), {
name: "research_sync",
library: { type: "var", name: "plugin_research_sync" },
exposes: {
"./info": "./dist/plugin/info.js",
"./info": "./info.ts",
"./package.json": "./package.json",
},
shared: {
+1
View File
@@ -1 +1,2 @@
webpack.config.js
dist/web/build
+2 -4
View File
@@ -12,7 +12,7 @@ InstanceConfigGroup.define({
});
InstanceConfigGroup.finalize();
const info: lib.PluginDeclaration = {
export default {
name: "statistics_exporter",
title: "Statistics Exporter",
description:
@@ -20,6 +20,4 @@ const info: lib.PluginDeclaration = {
"statistics to the cluster's Prometheus endpoint.",
instanceEntrypoint: "dist/plugin/instance",
InstanceConfigGroup,
};
export default info;
} satisfies lib.PluginDeclaration;
+1
View File
@@ -24,6 +24,7 @@
"@clusterio/lib": "^2.0.0-alpha.13",
"@clusterio/web_ui": "^2.0.0-alpha.13",
"@types/node": "^20.4.5",
"@types/react": "^18.2.21",
"typescript": "^5.1.6",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.web.json",
"references": [
{ "path": "../../packages/lib" },
{ "path": "../../packages/web_ui/tsconfig.web.json" },
],
"include": [ "web/**/*.tsx", "web/**/*.ts", "package.json" ],
"compilerOptions": {
"outDir": "dist/web/build",
}
}
Whitespace-only changes.
@@ -7,7 +7,7 @@ const common = require("@clusterio/web_ui/webpack.common");
module.exports = (env = {}) => merge(common(env), {
context: __dirname,
entry: "./web/index.jsx",
entry: "./web/index.tsx",
output: {
path: path.resolve(__dirname, "dist", "web"),
},
@@ -16,7 +16,7 @@ module.exports = (env = {}) => merge(common(env), {
name: "statistics_exporter",
library: { type: "var", name: "plugin_statistics_exporter" },
exposes: {
"./info": "./dist/plugin/info.js",
"./info": "./info.ts",
"./package.json": "./package.json",
},
shared: {
+1
View File
@@ -1 +1,2 @@
webpack.config.js
dist/web/build
+2 -2
View File
@@ -47,7 +47,7 @@ lib.definePermission({
grantByDefault: true,
});
module.exports.default = {
export default {
name: "subspace_storage",
title: "Subspace Storage",
description: "Provides shared storage across instances for the Subspace Storage mod",
@@ -66,4 +66,4 @@ module.exports.default = {
],
webEntrypoint: "./web",
routes: ["/storage"],
};
} satisfies lib.PluginDeclaration;
+1
View File
@@ -30,6 +30,7 @@
"@types/express": "^4.17.17",
"@types/fs-extra": "^11.0.1",
"@types/node": "^20.4.5",
"@types/react": "^18.2.21",
"antd": "^5.7.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.web.json",
"references": [
{ "path": "../../packages/lib" },
{ "path": "../../packages/web_ui/tsconfig.web.json" },
],
"include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ],
"compilerOptions": {
"outDir": "dist/web/build",
}
}
@@ -3,18 +3,18 @@ import { Input, Table, Typography } from "antd";
import * as lib from "@clusterio/lib";
import {
BaseWebPlugin, PageLayout, ControlContext,
BaseWebPlugin, PageLayout, Control, ControlContext,
notifyErrorHandler, useItemMetadata, useLocale,
} from "@clusterio/web_ui";
import { GetStorageRequest, SetStorageSubscriptionRequest } from "../dist/plugin/messages";
import { GetStorageRequest, Item, SetStorageSubscriptionRequest, UpdateStorageEvent } from "../messages";
import "./index.css";
import "./style.css";
const { Paragraph } = Typography;
function useStorage(control) {
let plugin = control.plugins.get("subspace_storage");
function useStorage(control: Control) {
let plugin = control.plugins.get("subspace_storage") as WebPlugin;
let [storage, setStorage] = useState([...plugin.storage]);
useEffect(() => {
@@ -35,14 +35,15 @@ function StoragePage() {
let locale = useLocale();
let itemMetadata = useItemMetadata();
let storage = useStorage(control);
let [filter, setFilter] = useState(null);
type ItemFilter = ([name, count]: [string, number]) => boolean;
let [filter, setFilter] = useState<null | ItemFilter>(null);
function getLocaleName(itemName) {
function getLocaleName(itemName: string) {
let localeName = itemName;
let meta = itemMetadata.get(itemName);
if (meta && meta.localized_name) {
if (meta && meta.localised_name) {
// TODO: implement the locale to name conversion.
localeName = locale.get(meta.localized_name[0]);
localeName = locale.get(meta.localised_name[0])!;
} else {
for (let section of ["item-name", "entity-name", "fluid-name", "equipment-name"]) {
let name = locale.get(`${section}.${itemName}`);
@@ -72,7 +73,7 @@ function StoragePage() {
search = search.replace(/(^| )(\w)/g, "$1\\b$2");
search = search.replace(/ +/g, ".*");
let filterExpr = new RegExp(search, "i");
setFilter(() => (item => {
setFilter(() => ((item: [string, number]) => {
let name = getLocaleName(item[0]);
return filterExpr.test(name) || filterExpr.test(item[0]);
}));
@@ -118,6 +119,9 @@ function StoragePage() {
}
export class WebPlugin extends BaseWebPlugin {
storage = new Map<string, number>();
callbacks: (() => void)[] = [];
async init() {
this.pages = [
{
@@ -127,29 +131,27 @@ export class WebPlugin extends BaseWebPlugin {
content: <StoragePage/>,
},
];
this.storage = new Map();
this.callbacks = [];
this.control.handle(UpdateStorageEvent, this.handleUpdateStorageEvent.bind(this));
}
onControllerConnectionEvent(event) {
onControllerConnectionEvent(event: "connect" | "drop" | "resume" | "close") {
if (event === "connect") {
this.updateSubscription();
}
}
async updateStorageEventHandler(event) {
async handleUpdateStorageEvent(event: UpdateStorageEvent) {
this.updateStorage(event.items);
}
onUpdate(callback) {
onUpdate(callback: () => void) {
this.callbacks.push(callback);
if (this.callbacks.length) {
this.updateSubscription();
}
}
offUpdate(callback) {
offUpdate(callback: () => void) {
let index = this.callbacks.lastIndexOf(callback);
if (index === -1) {
throw new Error("callback is not registered");
@@ -161,7 +163,7 @@ export class WebPlugin extends BaseWebPlugin {
}
}
updateStorage(items) {
updateStorage(items: Item[]) {
for (let item of items) {
this.storage.set(item.name, item.count);
}
@@ -180,7 +182,7 @@ export class WebPlugin extends BaseWebPlugin {
).catch(notifyErrorHandler("Error subscribing to storage"));
if (this.callbacks.length) {
this.control.send(new GetStorageRequest()).then(
this.control!.send(new GetStorageRequest()).then(
items => {
this.updateStorage(items);
}
+3 -3
View File
@@ -7,7 +7,7 @@ const common = require("@clusterio/web_ui/webpack.common");
module.exports = (env = {}) => merge(common(env), {
context: __dirname,
entry: "./web/index.jsx",
entry: "./web/index.tsx",
output: {
path: path.resolve(__dirname, "dist", "web"),
},
@@ -16,9 +16,9 @@ module.exports = (env = {}) => merge(common(env), {
name: "subspace_storage",
library: { type: "var", name: "plugin_subspace_storage" },
exposes: {
"./info": "./dist/plugin/info.js",
"./info": "./info.ts",
"./package.json": "./package.json",
"./web": "./web/index.jsx",
"./web": "./web/index.tsx",
},
shared: {
"@clusterio/lib": { import: false },
+1
View File
@@ -6,6 +6,7 @@
"incremental": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
},
+8 -1
View File
@@ -3,14 +3,21 @@
"references": [
{ "path": "./packages/lib" },
{ "path": "./packages/controller" },
{ "path": "./packages/controller/tsconfig.web.json" },
{ "path": "./packages/host" },
{ "path": "./packages/ctl" },
{ "path": "./packages/web_ui" },
{ "path": "./packages/web_ui/tsconfig.web.json" },
{ "path": "./plugins/global_chat" },
{ "path": "./plugins/global_chat/tsconfig.web.json" },
{ "path": "./plugins/inventory_sync" },
{ "path": "./plugins/inventory_sync/tsconfig.web.json" },
{ "path": "./plugins/player_auth" },
{ "path": "./plugins/player_auth/tsconfig.web.json" },
{ "path": "./plugins/research_sync" },
{ "path": "./plugins/research_sync/tsconfig.web.json" },
{ "path": "./plugins/statistics_exporter" },
{ "path": "./plugins/statistics_exporter/tsconfig.web.json" },
{ "path": "./plugins/subspace_storage" },
{ "path": "./plugins/subspace_storage/tsconfig.web.json" },
],
}
-2
View File
@@ -5,8 +5,6 @@
"module": "node16",
"moduleResolution": "node16",
"target": "es2022",
"esModuleInterop": true,
"alwaysStrict": true,
"forceConsistentCasingInFileNames": true,
},
}
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"lib": ["dom", "es2023"],
"module": "es2022",
"moduleResolution": "bundler",
"target": "es2022",
"jsx": "react",
"noEmitOnError": true,
},
}